Working code can be downloaded from https://github.com/MichaelBuen/AspNetCoreExample
Important codes:
AspNetCoreExample.Infrastructure/_DDL.txt
AspNetCoreExample.Ddd/IdentityDomain/User.cs
AspNetCoreExample.Ddd/IdentityDomain/Role.cs
AspNetCoreExample.Identity/Data/UserStore.cs
AspNetCoreExample.Identity/Data/RoleStore.cs
AspNetCoreExample.Identity/Startup.cs
Database:
create schema identity; create extension citext; create table identity.user ( id int generated by default as identity primary key, user_name citext not null, normalized_user_name citext not null, email citext, normalized_email citext, email_confirmed boolean not null, password_hash text, phone_number text, phone_number_confirmed boolean not null, two_factor_enabled boolean not null, security_stamp text, concurrency_stamp text, lockout_end timestamp with time zone, lockout_enabled boolean not null default false, access_failed_count int not null default 0 ); create table identity.external_login ( user_fk int not null references identity.user(id), id int generated by default as identity primary key, login_provider text not null, provider_key text not null, display_name text not null ); create unique index ix_identity_user__normalized_user_name ON identity.user (normalized_user_name); create unique index ix_identity_user__normalized_email ON identity.user (normalized_email); create table identity.role ( id int generated by default as identity primary key, name citext not null, normalized_name citext not null, concurrency_stamp text ); create table ix_identity_role__normalized_name ON identity.role (normalized_name); CREATE TABLE identity.user_role ( user_fk int not null references identity.user(id), role_fk int not null references identity.role(id), primary key (user_fk, role_fk) );
DDD Models (user and role)
Identity User model:
namespace AspNetCoreExample.Ddd.IdentityDomain { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; public class User : IdentityUser<int> { /// <summary> /// One-to-many to external logins /// </summary> /// <value>The external logins.</value> public IEnumerable<ExternalLogin> ExternalLogins { get; protected set; } = new Collection<ExternalLogin>(); /// <summary> /// Many-to-many between Users and Roles /// </summary> /// <value>The roles.</value> public IEnumerable<Role> Roles { get; protected set; } = new Collection<Role>(); public User(string userName) : base(userName) { } public User(string userName, string email) { this.UserName = userName; this.Email = email; } public void AddExternalLogin(string loginProvider, string providerKey, string providerDisplayName) { var el = new ExternalLogin(this) { LoginProvider = loginProvider, ProviderKey = providerKey, DisplayName = providerDisplayName }; this.ExternalLogins.AsCollection().Add(el); } public async Task RemoveExternalLoginAsync(string loginProvider, string providerKey) { var externalLogin = await this.ExternalLogins.AsQueryable() .SingleOrDefaultAsyncOk(el => el.LoginProvider == loginProvider && el.ProviderKey == providerKey); if (externalLogin != null) { this.ExternalLogins.AsCollection().Remove(externalLogin); } } public async Task<IList<string>> GetRoleNamesAsync() => await this.Roles.AsQueryable().Select(r => r.Name).ToListAsyncOk(); public async Task AddRole(Role roleToAdd) { var isExisting = await this.Roles.AsQueryable().AnyAsyncOk(role => role == roleToAdd); if (!isExisting) { this.Roles.AsCollection().Add(roleToAdd); } } public async Task RemoveRoleAsync(string roleName) { string normalizedRoleName = roleName.ToUpper(); var role = await this.Roles.AsQueryable() .Where(el => el.NormalizedName == normalizedRoleName) .SingleOrDefaultAsyncOk(); if (role != null) { this.Roles.AsCollection().Remove(role); } } public async Task<bool> IsInRole(string roleName) => await this.Roles.AsQueryable() .AnyAsyncOk(role => role.NormalizedName == roleName.ToUpper()); public void SetTwoFactorEnabled(bool enabled) => this.TwoFactorEnabled = enabled; public void SetNormalizedEmail(string normalizedEmail) => this.NormalizedEmail = normalizedEmail; public void SetEmailConfirmed(Boolean confirmed) => this.EmailConfirmed = confirmed; public void SetPhoneNumber(string phoneNumber) => this.PhoneNumber = phoneNumber; public void SetPhoneNumberConfirmed(Boolean confirmed) => this.PhoneNumberConfirmed = confirmed; public void SetPasswordHash(string passwordHash) => this.PasswordHash = passwordHash; public void SetEmail(string email) => this.Email = email; public void SetNormalizedUserName(string normalizedUserName) => this.NormalizedUserName = normalizedUserName; public void SetUserName(string userName) => this.UserName = userName; public void UpdateFromDetached(User user) { this.UserName = user.UserName; this.NormalizedUserName = user.NormalizedUserName; this.Email = user.Email; this.NormalizedEmail = user.NormalizedEmail; this.EmailConfirmed = user.EmailConfirmed; this.PasswordHash = user.PasswordHash; this.PhoneNumber = user.PhoneNumber; this.PhoneNumberConfirmed = user.PhoneNumberConfirmed; this.TwoFactorEnabled = user.TwoFactorEnabled; } public async static Task<User> FindByLoginAsync( IQueryable<User> users, string loginProvider, string providerKey ) => await users.SingleOrDefaultAsyncOk(au => au.ExternalLogins.Any(el => el.LoginProvider == loginProvider && el.ProviderKey == providerKey) ); public async Task<IList<UserLoginInfo>> GetUserLoginInfoListAsync() => await this.ExternalLogins.AsQueryable() .Select(el => new UserLoginInfo( el.LoginProvider, el.ProviderKey, el.DisplayName ) ) // The cache of a user's external logins gets trashed when another user updates his/her external logins. // Explore how to make collection caching more robust. Disable for the meantime. // .CacheableOk() .ToListAsyncOk(); } public class ExternalLogin { ////// Many-to-one to a user /// ///The user. protected User User { get; set; } internal ExternalLogin(User applicationUser) => this.User = applicationUser; // Was: // public int Id { get; protected set; } // Below is better as we don't need to expose primary key of child entities // But the above could be useful if we want to directly update, delete // based on Id, for performance concern. protected int Id { get; set; } public string LoginProvider { get; internal protected set; } // provider: facebook, google, etc public string ProviderKey { get; internal protected set; } // user's id from facebook, google, etc public string DisplayName { get; internal protected set; } // seems same as provider } }
Role model:
namespace AspNetCoreExample.Ddd.IdentityDomain { using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; public class Role : IdentityRole<int> { /// <summary> /// Many-to-many between Roles and Users /// </summary> /// <value>The users.</value> public IEnumerable<User> Users { get; protected set; } = new Collection<User>(); public Role(string roleName) : base(roleName) { } public void UpdateFromDetached(Role role) { this.Name = role.Name; this.NormalizedName = role.NormalizedName; } public void SetRoleName(string roleName) => this.Name = roleName; public void SetNormalizedName(string normalizedName) => this.NormalizedName = normalizedName; public static async Task<IList<User>> GetUsersByRoleNameAsync(IQueryable<User> users, string normalizedRoleName) { var criteria = from user in users where user.Roles.AsQueryable().Any(role => role.NormalizedName == normalizedRoleName) select user; return await criteria.ToListAsyncOk(); } } }
Data stores (user store and role store)
User store:
namespace AspNetCoreExample.Identity.Data { using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using AspNetCoreExample.Ddd.Connection; using AspNetCoreExample.Ddd.IdentityDomain; public class UserStore : IUserStore<User>, IUserEmailStore<User>, IUserPhoneNumberStore<User>, IUserTwoFactorStore<User>, IUserPasswordStore<User>, IUserRoleStore<User>, IUserLoginStore<User> { IDatabaseFactory DbFactory { get; } public UserStore(IDatabaseFactory dbFactory) => this.DbFactory = dbFactory; async Task<IdentityResult> IUserStore<User>.CreateAsync(User user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDddForUpdate()) { await ddd.PersistAsync(user); await ddd.CommitAsync(); } return IdentityResult.Success; } async Task<IdentityResult> IUserStore<User>.DeleteAsync(User user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDddForUpdate()) { await ddd.DeleteAggregateAsync(user); await ddd.CommitAsync(); } return IdentityResult.Success; } async Task<User> IUserStore<User>.FindByIdAsync(string userId, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDddForUpdate()) { var user = await ddd.GetAsync<User>(int.Parse(userId)); return user; } } async Task<User> IUserStore<User>.FindByNameAsync( string normalizedUserName, CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDdd()) { var au = await ddd.Query<User>() .SingleOrDefaultAsyncOk(u => u.NormalizedUserName == normalizedUserName); return au; } } Task<string> IUserStore<User>.GetNormalizedUserNameAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.NormalizedUserName); Task<string> IUserStore<User>.GetUserIdAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.Id.ToString()); Task<string> IUserStore<User>.GetUserNameAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.UserName); Task IUserStore<User>.SetNormalizedUserNameAsync( User user, string normalizedName, CancellationToken cancellationToken ) { user.SetNormalizedUserName(normalizedName); return Task.FromResult(0); } Task IUserStore<User>.SetUserNameAsync(User user, string userName, CancellationToken cancellationToken) { user.SetUserName(userName); return Task.FromResult(0); } async Task<IdentityResult> IUserStore<User>.UpdateAsync(User user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDddForUpdate()) { var au = await ddd.GetAsync<User>(user.Id); au.UpdateFromDetached(user); await ddd.CommitAsync(); } return IdentityResult.Success; } Task IUserEmailStore<User>.SetEmailAsync(User user, string email, CancellationToken cancellationToken) { user.SetEmail(email); return Task.FromResult(0); } Task<string> IUserEmailStore<User>.GetEmailAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.Email); Task<bool> IUserEmailStore<User>.GetEmailConfirmedAsync( User user, CancellationToken cancellationToken ) => Task.FromResult(user.EmailConfirmed); Task IUserEmailStore<User>.SetEmailConfirmedAsync( User user, bool confirmed, CancellationToken cancellationToken ) { user.SetEmailConfirmed(confirmed); return Task.FromResult(0); } async Task<User> IUserEmailStore<User>.FindByEmailAsync( string normalizedEmail, CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDdd()) { var au = await ddd.Query<User>() .SingleOrDefaultAsyncOk(u => u.NormalizedEmail == normalizedEmail); return au; } } Task<string> IUserEmailStore<User>.GetNormalizedEmailAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.NormalizedEmail); Task IUserEmailStore<User>.SetNormalizedEmailAsync( User user, string normalizedEmail, CancellationToken cancellationToken ) { user.SetNormalizedEmail(normalizedEmail); return Task.FromResult(0); } Task IUserPhoneNumberStore<User>.SetPhoneNumberAsync( User user, string phoneNumber, CancellationToken cancellationToken ) { user.SetPhoneNumber(phoneNumber); return Task.FromResult(0); } Task<string> IUserPhoneNumberStore<User>.GetPhoneNumberAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.PhoneNumber); Task<bool> IUserPhoneNumberStore<User>.GetPhoneNumberConfirmedAsync( User user, CancellationToken cancellationToken ) => Task.FromResult(user.PhoneNumberConfirmed); Task IUserPhoneNumberStore<User>.SetPhoneNumberConfirmedAsync( User user, bool confirmed, CancellationToken cancellationToken ) { user.SetPhoneNumberConfirmed(confirmed); return Task.FromResult(0); } Task IUserTwoFactorStore<User>.SetTwoFactorEnabledAsync( User user, bool enabled, CancellationToken cancellationToken ) { user.SetTwoFactorEnabled(enabled); return Task.FromResult(0); } Task<bool> IUserTwoFactorStore<User>.GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.TwoFactorEnabled); Task IUserPasswordStore<User>.SetPasswordHashAsync( User user, string passwordHash, CancellationToken cancellationToken ) { user.SetPasswordHash(passwordHash); return Task.FromResult(0); } Task<string> IUserPasswordStore<User>.GetPasswordHashAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.PasswordHash); Task<bool> IUserPasswordStore<User>.HasPasswordAsync(User user, CancellationToken cancellationToken) => Task.FromResult(user.PasswordHash != null); async Task IUserRoleStore<User>.AddToRoleAsync(User user, string roleName, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDddForUpdate()) { var roleByName = await ddd.Query<Role>() .SingleOrDefaultAsyncOk(role => role.Name == roleName); if (roleByName == null) { roleByName = new Role(roleName); ddd.Persist(roleByName); } var userGot = await ddd.GetAsync<User>(user.Id); await userGot.AddRole(roleByName); await ddd.CommitAsync(); } } async Task IUserRoleStore<User>.RemoveFromRoleAsync( User user, string roleName, CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDddForUpdate()) { var userLoaded = await ddd.GetAsync<User>(user.Id); await userLoaded.RemoveRoleAsync(roleName); await ddd.CommitAsync(); } } async Task<IList<string>> IUserRoleStore<User>.GetRolesAsync(User user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDdd()) { var userGot = await ddd.GetAsync<User>(user.Id); return await userGot.GetRoleNamesAsync(); } } async Task<bool> IUserRoleStore<User>.IsInRoleAsync( User user, string roleName, CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDdd()) { var userGot = await ddd.GetAsync<User>(user.Id); return await userGot.IsInRole(roleName); } } async Task<IList<User>> IUserRoleStore<User>.GetUsersInRoleAsync( string roleName, CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDdd()) { string normalizedRoleName = roleName.ToUpper(); var usersList = await Role.GetUsersByRoleNameAsync(ddd.Query<User>(), normalizedRoleName); return usersList; } } async Task IUserLoginStore<User>.AddLoginAsync( User user, UserLoginInfo login, CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); if (user == null) throw new ArgumentNullException(nameof(user)); if (login == null) throw new ArgumentNullException(nameof(login)); using (var ddd = this.DbFactory.OpenDddForUpdate()) { var au = await ddd.GetAsync<User>(user.Id); au.AddExternalLogin(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName); await ddd.CommitAsync(); } } async Task<User> IUserLoginStore<User>.FindByLoginAsync( string loginProvider, string providerKey, CancellationToken cancellationToken ) { using (var ddd = this.DbFactory.OpenDdd()) { var user = await User.FindByLoginAsync(ddd.Query<User>(), loginProvider, providerKey); return user; } } async Task<IList<UserLoginInfo>> IUserLoginStore<User>.GetLoginsAsync( User user, CancellationToken cancellationToken ) { using (var ddd = this.DbFactory.OpenDdd()) { var au = await ddd.GetAsync<User>(user.Id); var list = await au.GetUserLoginInfoListAsync(); return list; } } async Task IUserLoginStore<User>.RemoveLoginAsync( User user, string loginProvider, string providerKey, CancellationToken cancellationToken ) { using (var ddd = this.DbFactory.OpenDddForUpdate()) { var au = await ddd.GetAsync<User>(user.Id); await au.RemoveExternalLoginAsync(loginProvider, providerKey); await ddd.CommitAsync(); } } public void Dispose() { // Nothing to dispose. } } }
Role store:
namespace AspNetCoreExample.Identity.Data { using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using AspNetCoreExample.Ddd.Connection; using AspNetCoreExample.Ddd.IdentityDomain; public class RoleStore : IRoleStore<Role> { IDatabaseFactory DbFactory { get; } public RoleStore(IConfiguration configuration, IDatabaseFactory dbFactory) => this.DbFactory = dbFactory; async Task<IdentityResult> IRoleStore<Role>.CreateAsync(Role role, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var db = this.DbFactory.OpenDddForUpdate()) { await db.PersistAsync(role); await db.CommitAsync(); } return IdentityResult.Success; } async Task<IdentityResult> IRoleStore<Role>.UpdateAsync(Role role, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDddForUpdate()) { var roleGot = await ddd.GetAsync<Role>(role.Id); roleGot.UpdateFromDetached(role); await ddd.CommitAsync(); } return IdentityResult.Success; } async Task<IdentityResult> IRoleStore<Role>.DeleteAsync(Role role, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var db = this.DbFactory.OpenDddForUpdate()) { await db.DeleteAggregateAsync(role); await db.CommitAsync(); } return IdentityResult.Success; } Task<string> IRoleStore<Role>.GetRoleIdAsync(Role role, CancellationToken cancellationToken) => Task.FromResult(role.Id.ToString()); Task<string> IRoleStore<Role>.GetRoleNameAsync(Role role, CancellationToken cancellationToken) => Task.FromResult(role.Name); Task IRoleStore<Role>.SetRoleNameAsync(Role role, string roleName, CancellationToken cancellationToken) { role.SetRoleName(roleName); return Task.FromResult(0); } Task<string> IRoleStore<Role>.GetNormalizedRoleNameAsync(Role role, CancellationToken cancellationToken) => Task.FromResult(role.NormalizedName); Task IRoleStore<Role>.SetNormalizedRoleNameAsync( Role role, string normalizedName, CancellationToken cancellationToken ) { role.SetNormalizedName(normalizedName); return Task.FromResult(0); } async Task<Role> IRoleStore<Role>.FindByIdAsync(string roleId, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDdd()) { var role = await ddd.GetAsync<Role>(roleId); return role; } } async Task<Role> IRoleStore<Role>.FindByNameAsync( string normalizedRoleName, CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); using (var ddd = this.DbFactory.OpenDdd()) { var role = await ddd.Query<Role>() .SingleOrDefaultAsyncOk(r => r.NormalizedName == normalizedRoleName); return role; } } public void Dispose() { // Nothing to dispose. } } }
Wireup:
namespace AspNetCoreExample.Identity { using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using AspNetCoreExample.Ddd.Connection; using AspNetCoreExample.Identity.Data; using AspNetCoreExample.Identity.Services; public class Startup { IConfiguration Configuration { get; } public Startup(IConfiguration configuration) => this.Configuration = configuration; string ConnectionString => this.Configuration.GetConnectionString("DefaultConnection"); (string appId, string appSecret) FacebookOptions => (this.Configuration["Authentication:Facebook:AppId"], this.Configuration["Authentication:Facebook:AppSecret"]); (string clientId, string clientSecret) GoogleOptions => (this.Configuration["Authentication:Google:ClientId"], this.Configuration["Authentication:Google:ClientSecret"]); (string consumerKey, string consumerSecret) TwitterOptions => (this.Configuration["Authentication:Twitter:ConsumerKey"], this.Configuration["Authentication:Twitter:ConsumerSecret"]); // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddSingleton<NHibernate.ISessionFactory>(serviceProvider => AspNetCoreExample.Ddd.Mapper.TheMapper.BuildSessionFactory(this.ConnectionString) ); services.AddSingleton<IDatabaseFactory, DatabaseFactory>(); services.AddTransient<Microsoft.AspNetCore.Identity.IUserStore<Ddd.IdentityDomain.User>, UserStore>(); services.AddTransient<Microsoft.AspNetCore.Identity.IRoleStore<Ddd.IdentityDomain.Role>, RoleStore>(); services.AddIdentity<Ddd.IdentityDomain.User, Ddd.IdentityDomain.Role>().AddDefaultTokenProviders(); services.AddAuthentication() .AddFacebook(options => (options.AppId, options.AppSecret) = this.FacebookOptions) .AddGoogle(options => (options.ClientId, options.ClientSecret) = this.GoogleOptions) .AddTwitter(options => (options.ConsumerKey, options.ConsumerSecret) = this.TwitterOptions) ; services.ConfigureApplicationCookie(config => { config.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents { OnRedirectToLogin = ctx => { if (ctx.Request.Path.StartsWithSegments("/api")) { ctx.Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized; } else { ctx.Response.Redirect(ctx.RedirectUri); } return Task.FromResult(0); } }; }); // Add application services. services.AddTransient<IEmailSender, EmailSender>(); services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); app.UseDatabaseErrorPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } }
Happy Coding!
No comments:
Post a Comment