diff --git a/samples/Daarto.WebUI/Daarto.WebUI.csproj b/samples/Daarto.WebUI/Daarto.WebUI.csproj index ea183f9..e3f5d17 100644 --- a/samples/Daarto.WebUI/Daarto.WebUI.csproj +++ b/samples/Daarto.WebUI/Daarto.WebUI.csproj @@ -4,7 +4,7 @@ aspnet-Daarto.WebUI-0BC4D06A-1285-4595-85EA-07A68DD02CF3 - + diff --git a/samples/Daarto.WebUI/Data/Tables/ExtendedRolesTable.cs b/samples/Daarto.WebUI/Data/Tables/ExtendedRolesTable.cs index f9d1f1d..7530527 100644 --- a/samples/Daarto.WebUI/Data/Tables/ExtendedRolesTable.cs +++ b/samples/Daarto.WebUI/Data/Tables/ExtendedRolesTable.cs @@ -9,9 +9,9 @@ namespace Daarto.WebUI.Data.Tables { public class ExtendedRolesTable : RolesTable> { - public ExtendedRolesTable(IDbConnectionFactory dbConnectionFactory) : base(dbConnectionFactory) { } + public ExtendedRolesTable(IDbConnectionStore dbConnectionFactory) : base(dbConnectionFactory) { } - public override async Task CreateAsync(ExtendedIdentityRole role) { + public override async Task CreateAsync(ExtendedIdentityRole role) { const string sql = "INSERT INTO [dbo].[AspNetRoles] " + "VALUES (@Id, @Name, @NormalizedName, @ConcurrencyStamp, @Description);"; var rowsInserted = await DbConnection.ExecuteAsync(sql, new { @@ -21,10 +21,15 @@ public override async Task CreateAsync(ExtendedIdentityRole role) { role.ConcurrencyStamp, role.Description }); - return rowsInserted == 1; + return rowsInserted == 1 + ? IdentityResult.Success + : IdentityResult.Failed(new IdentityError { + Code = string.Empty, + Description = $"Role '{role.Name}' could not be created." + }); } - public override async Task UpdateAsync(ExtendedIdentityRole role, IList> claims = null) { + public override async Task UpdateAsync(ExtendedIdentityRole role, IList> claims = null) { const string updateRoleSql = "UPDATE [dbo].[AspNetRoles] " + "SET [Name] = @Name, [NormalizedName] = @NormalizedName, [ConcurrencyStamp] = @ConcurrencyStamp, [Description] = @Description " + "WHERE [Id] = @Id;"; @@ -55,10 +60,13 @@ public override async Task UpdateAsync(ExtendedIdentityRole role, IList /// Creates a new role in the store. /// /// The role to create in the store. - Task CreateAsync(TRole role); + Task CreateAsync(TRole role); /// /// Deletes a role from the store. /// /// The id of the role to delete from the store. - Task DeleteAsync(TKey roleId); + Task DeleteAsync(TKey roleId); /// /// Finds the role who has the specified id. /// @@ -41,6 +41,6 @@ public interface IRolesTable /// /// The role to update in the store. /// The claims of the role. - Task UpdateAsync(TRole role, IList claims = null); + Task UpdateAsync(TRole role, IList claims = null); } } diff --git a/src/AspNetCore.Identity.Dapper/Abstract/IUsersTable.cs b/src/AspNetCore.Identity.Dapper/Abstract/IUsersTable.cs index 699cee2..10232d9 100644 --- a/src/AspNetCore.Identity.Dapper/Abstract/IUsersTable.cs +++ b/src/AspNetCore.Identity.Dapper/Abstract/IUsersTable.cs @@ -25,12 +25,12 @@ public interface IUsersOnlyTable /// The user to create in the store. - Task CreateAsync(TUser user); + Task CreateAsync(TUser user); /// /// Deletes a user from the store. /// /// The id of the user to delete from the store. - Task DeleteAsync(TKey userId); + Task DeleteAsync(TKey userId); /// /// Finds the user who has the specified id. /// @@ -53,7 +53,7 @@ public interface IUsersOnlyTableThe claims of the user. /// The logins of the user. /// The tokens of the user. - Task UpdateAsync(TUser user, IList claims, IList logins, IList tokens); + Task UpdateAsync(TUser user, IList claims, IList logins, IList tokens); /// /// Gets the users that own the specified claim. /// @@ -86,7 +86,7 @@ public interface IUsersTableThe roles of the user. /// The logins of the user. /// The tokens of the user. - Task UpdateAsync(TUser user, IList claims, IList roles, IList logins, IList tokens); + Task UpdateAsync(TUser user, IList claims, IList roles, IList logins, IList tokens); /// /// Gets the users that belong to the specified role. /// diff --git a/src/AspNetCore.Identity.Dapper/AspNetCore.Identity.Dapper.csproj b/src/AspNetCore.Identity.Dapper/AspNetCore.Identity.Dapper.csproj index 53d2060..79277b1 100644 --- a/src/AspNetCore.Identity.Dapper/AspNetCore.Identity.Dapper.csproj +++ b/src/AspNetCore.Identity.Dapper/AspNetCore.Identity.Dapper.csproj @@ -24,9 +24,9 @@ ea4ca2df-aa2d-4174-8f13-b1e594019d86 - + - - + + diff --git a/src/AspNetCore.Identity.Dapper/DapperStoreOptions.cs b/src/AspNetCore.Identity.Dapper/DapperStoreOptions.cs index 1395276..8b24986 100644 --- a/src/AspNetCore.Identity.Dapper/DapperStoreOptions.cs +++ b/src/AspNetCore.Identity.Dapper/DapperStoreOptions.cs @@ -1,4 +1,5 @@ -using System.Data; +using System; +using System.Data; using Microsoft.Extensions.DependencyInjection; namespace AspNetCore.Identity.Dapper @@ -14,8 +15,8 @@ public class DapperStoreOptions /// public string ConnectionString { get; set; } /// - /// A factory for creating instances of . + /// The type of the DbConnectionFactory (A factory for creating instances of ) /// - public IDbConnectionFactory DbConnectionFactory { get; set; } + public Type DbConnectionStoreType { get; set; } } } diff --git a/src/AspNetCore.Identity.Dapper/IDbConnectionFactory.cs b/src/AspNetCore.Identity.Dapper/IDbConnectionFactory.cs index bba84f8..6523f80 100644 --- a/src/AspNetCore.Identity.Dapper/IDbConnectionFactory.cs +++ b/src/AspNetCore.Identity.Dapper/IDbConnectionFactory.cs @@ -1,11 +1,12 @@ -using System.Data; +using System; +using System.Data; namespace AspNetCore.Identity.Dapper { /// /// A factory for creating instances of . /// - public interface IDbConnectionFactory + public interface IDbConnectionStore : IDisposable { /// /// The connection string to use for connecting to Microsoft SQL Server. @@ -14,6 +15,6 @@ public interface IDbConnectionFactory /// /// Creates a new instance of the underlying . /// - IDbConnection Create(); + IDbConnection GetOrCreateConnection(); } } diff --git a/src/AspNetCore.Identity.Dapper/IdentityBuilderExtensions.cs b/src/AspNetCore.Identity.Dapper/IdentityBuilderExtensions.cs index 1ddb799..e50e23b 100644 --- a/src/AspNetCore.Identity.Dapper/IdentityBuilderExtensions.cs +++ b/src/AspNetCore.Identity.Dapper/IdentityBuilderExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Reflection; using AspNetCore.Identity.Dapper; using Microsoft.AspNetCore.Identity; @@ -32,14 +33,14 @@ private static void AddStores(IServiceCollection services, Type userType, Type r var configuration = serviceProvider.GetRequiredService(); var dbConnectionContextOptions = new DapperStoreOptions { ConnectionString = configuration.GetConnectionString("DefaultConnection"), - DbConnectionFactory = new SqlServerDbConnectionFactory(), + DbConnectionStoreType = typeof(SqlServerDbConnectionStore), Services = services }; configureAction?.Invoke(dbConnectionContextOptions); dbConnectionContextOptions.Services = null; var keyType = identityUserType.GenericTypeArguments[0]; - services.TryAddScoped(typeof(IDbConnectionFactory), x => { - var dbConnectionFactoryInstance = (IDbConnectionFactory)Activator.CreateInstance(dbConnectionContextOptions.DbConnectionFactory.GetType()); + services.TryAddScoped(typeof(IDbConnectionStore), x => { + var dbConnectionFactoryInstance = (IDbConnectionStore)Activator.CreateInstance(dbConnectionContextOptions.DbConnectionStoreType); dbConnectionFactoryInstance.ConnectionString = dbConnectionContextOptions.ConnectionString; return dbConnectionFactoryInstance; }); @@ -49,13 +50,14 @@ private static void AddStores(IServiceCollection services, Type userType, Type r var userLoginType = typeof(IdentityUserLogin<>).MakeGenericType(keyType); var roleClaimType = typeof(IdentityRoleClaim<>).MakeGenericType(keyType); var userTokenType = typeof(IdentityUserToken<>).MakeGenericType(keyType); + var usersTableServiceType = typeof(IUsersTable<,,,,,>).MakeGenericType(userType, keyType, userClaimType, userRoleType, userLoginType, userTokenType); if (roleType != null) { var identityRoleType = FindGenericBaseType(roleType, typeof(IdentityRole<>)); if (identityRoleType == null) { throw new InvalidOperationException($"Method {nameof(AddDapperStores)} can only be called with a role that derives from IdentityRole."); } services.TryAddScoped( - typeof(IUsersTable<,,,,,>).MakeGenericType(userType, keyType, userClaimType, userRoleType, userLoginType, userTokenType), + usersTableServiceType, typeof(UsersTable<,,,,,>).MakeGenericType(userType, keyType, userClaimType, userRoleType, userLoginType, userTokenType) ); services.TryAddScoped(typeof(IRolesTable<,,>).MakeGenericType(roleType, keyType, roleClaimType), typeof(RolesTable<,,>).MakeGenericType(roleType, keyType, roleClaimType)); @@ -64,10 +66,14 @@ private static void AddStores(IServiceCollection services, Type userType, Type r services.TryAddScoped(typeof(IRoleStore<>).MakeGenericType(roleType), typeof(RoleStore<,,,>).MakeGenericType(roleType, keyType, userRoleType, roleClaimType)); userStoreType = typeof(UserStore<,,,,,,,>).MakeGenericType(userType, roleType, keyType, userClaimType, userRoleType, userLoginType, userTokenType, roleClaimType); } else { - services.TryAddScoped( - typeof(IUsersOnlyTable<,,,,>).MakeGenericType(userType, keyType, userClaimType, userLoginType, userTokenType), - typeof(UsersTable<,,,,,>).MakeGenericType(userType, keyType, userClaimType, roleType, userLoginType, userTokenType) - ); + var matchingServiceType = services.FirstOrDefault(s => s.ServiceType == usersTableServiceType); + usersTableServiceType = typeof(IUsersOnlyTable<,,,,>).MakeGenericType(userType, keyType, userClaimType, userLoginType, userTokenType); + if (matchingServiceType == null) { + services.TryAddScoped(usersTableServiceType, typeof(UsersTable<,,,,,>).MakeGenericType(userType, keyType, userClaimType, userRoleType, userLoginType, userTokenType)); + } else { + services.Remove(matchingServiceType); + services.TryAddScoped(usersTableServiceType, matchingServiceType.ImplementationType); + } userStoreType = typeof(UserOnlyStore<,,,,>).MakeGenericType(userType, keyType, userClaimType, userLoginType, userTokenType); } services.TryAddScoped(typeof(IUserClaimsTable<,>).MakeGenericType(keyType, userClaimType), typeof(UserClaimsTable<,>).MakeGenericType(keyType, userClaimType)); diff --git a/src/AspNetCore.Identity.Dapper/SqlServerDbConnectionFactory.cs b/src/AspNetCore.Identity.Dapper/SqlServerDbConnectionFactory.cs index 96ae414..7a3db6c 100644 --- a/src/AspNetCore.Identity.Dapper/SqlServerDbConnectionFactory.cs +++ b/src/AspNetCore.Identity.Dapper/SqlServerDbConnectionFactory.cs @@ -6,18 +6,36 @@ namespace AspNetCore.Identity.Dapper /// /// Creates a new instance for connecting to Microsoft SQL Server. /// - public class SqlServerDbConnectionFactory : IDbConnectionFactory + public class SqlServerDbConnectionStore : IDbConnectionStore { /// /// The connection string to use for connecting to Microsoft SQL Server. /// public string ConnectionString { get; set; } + private IDbConnection _sqlConnection; /// - public IDbConnection Create() { - var sqlConnection = new SqlConnection(ConnectionString); - sqlConnection.Open(); - return sqlConnection; + public IDbConnection GetOrCreateConnection() { + // we could open the connection here and it will remain open until explicitly closed, but if we return a closed connection (as below), + // dapper will open and close with each query or transaction + return _sqlConnection ??= new SqlConnection(ConnectionString); + } + + private bool _disposed = false; + /// + public void Dispose() { + Dispose(true); + } + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// Indicates whether the method call comes from a Dispose method (its value is true) or from a finalizer (its value is false). + protected virtual void Dispose(bool disposing) { + if (!_disposed && disposing) { + // Free any other managed objects here. + _sqlConnection?.Dispose(); + _disposed = true; + } } } } diff --git a/src/AspNetCore.Identity.Dapper/Stores/RoleStore.cs b/src/AspNetCore.Identity.Dapper/Stores/RoleStore.cs index 56d42f6..feeeb8f 100644 --- a/src/AspNetCore.Identity.Dapper/Stores/RoleStore.cs +++ b/src/AspNetCore.Identity.Dapper/Stores/RoleStore.cs @@ -99,27 +99,19 @@ public override async Task AddClaimAsync(TRole role, Claim claim, CancellationTo } /// - public override async Task CreateAsync(TRole role, CancellationToken cancellationToken) { + public override Task CreateAsync(TRole role, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); role.ThrowIfNull(nameof(role)); - var created = await RolesTable.CreateAsync(role); - return created ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"Role '{role.Name}' could not be created." - }); + return RolesTable.CreateAsync(role); } /// - public override async Task DeleteAsync(TRole role, CancellationToken cancellationToken) { + public override Task DeleteAsync(TRole role, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); role.ThrowIfNull(nameof(role)); - var deleted = await RolesTable.DeleteAsync(role.Id); - return deleted ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"Role '{role.Name}' could not be deleted." - }); + return RolesTable.DeleteAsync(role.Id); } /// @@ -204,16 +196,12 @@ public override Task SetRoleNameAsync(TRole role, string roleName, CancellationT } /// - public override async Task UpdateAsync(TRole role, CancellationToken cancellationToken) { + public override Task UpdateAsync(TRole role, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); role.ThrowIfNull(nameof(role)); role.ConcurrencyStamp = Guid.NewGuid().ToString(); - var updated = await RolesTable.UpdateAsync(role, RoleClaims); - return updated ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"Role '{role.Name}' could not be updated." - }); + return RolesTable.UpdateAsync(role, RoleClaims); } } } diff --git a/src/AspNetCore.Identity.Dapper/Stores/UserOnlyStore.cs b/src/AspNetCore.Identity.Dapper/Stores/UserOnlyStore.cs index eed3c2e..b25dd2d 100644 --- a/src/AspNetCore.Identity.Dapper/Stores/UserOnlyStore.cs +++ b/src/AspNetCore.Identity.Dapper/Stores/UserOnlyStore.cs @@ -95,27 +95,19 @@ public override async Task AddLoginAsync(TUser user, UserLoginInfo login, Cancel } /// - public override async Task CreateAsync(TUser user, CancellationToken cancellationToken) { + public override Task CreateAsync(TUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); user.ThrowIfNull(nameof(user)); - var created = await UsersTable.CreateAsync(user); - return created ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"User '{user.UserName}' could not be created." - }); + return UsersTable.CreateAsync(user); } /// - public override async Task DeleteAsync(TUser user, CancellationToken cancellationToken) { + public override Task DeleteAsync(TUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); user.ThrowIfNull(nameof(user)); - var deleted = await UsersTable.DeleteAsync(user.Id); - return deleted ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"User '{user.UserName}' could not be deleted." - }); + return UsersTable.DeleteAsync(user.Id); } /// @@ -212,16 +204,12 @@ public override async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newC } /// - public override async Task UpdateAsync(TUser user, CancellationToken cancellationToken) { + public override Task UpdateAsync(TUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); user.ThrowIfNull(nameof(user)); user.ConcurrencyStamp = Guid.NewGuid().ToString(); - var updated = await UsersTable.UpdateAsync(user, UserClaims, UserLogins, UserTokens); - return updated ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"User '{user.UserName}' could not be deleted." - }); + return UsersTable.UpdateAsync(user, UserClaims, UserLogins, UserTokens); } /// diff --git a/src/AspNetCore.Identity.Dapper/Stores/UserStore.cs b/src/AspNetCore.Identity.Dapper/Stores/UserStore.cs index 7429260..86188ed 100644 --- a/src/AspNetCore.Identity.Dapper/Stores/UserStore.cs +++ b/src/AspNetCore.Identity.Dapper/Stores/UserStore.cs @@ -137,27 +137,19 @@ public override async Task AddToRoleAsync(TUser user, string normalizedRoleName, } /// - public override async Task CreateAsync(TUser user, CancellationToken cancellationToken = default) { + public override Task CreateAsync(TUser user, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); user.ThrowIfNull(nameof(user)); - var created = await UsersTable.CreateAsync(user); - return created ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"User '{user.UserName}' could not be created." - }); + return UsersTable.CreateAsync(user); } /// - public override async Task DeleteAsync(TUser user, CancellationToken cancellationToken) { + public override Task DeleteAsync(TUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); user.ThrowIfNull(nameof(user)); - var deleted = await UsersTable.DeleteAsync(user.Id); - return deleted ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"User '{user.UserName}' could not be deleted." - }); + return UsersTable.DeleteAsync(user.Id); } /// @@ -333,16 +325,12 @@ public override async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newC } /// - public override async Task UpdateAsync(TUser user, CancellationToken cancellationToken) { + public override Task UpdateAsync(TUser user, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); user.ThrowIfNull(nameof(user)); user.ConcurrencyStamp = Guid.NewGuid().ToString(); - var updated = await UsersTable.UpdateAsync(user, UserClaims, UserRoles, UserLogins, UserTokens); - return updated ? IdentityResult.Success : IdentityResult.Failed(new IdentityError { - Code = string.Empty, - Description = $"User '{user.UserName}' could not be deleted." - }); + return UsersTable.UpdateAsync(user, UserClaims, UserRoles, UserLogins, UserTokens); } /// diff --git a/src/AspNetCore.Identity.Dapper/Tables/IdentityTable.cs b/src/AspNetCore.Identity.Dapper/Tables/IdentityTable.cs index c56f271..b5b57cc 100644 --- a/src/AspNetCore.Identity.Dapper/Tables/IdentityTable.cs +++ b/src/AspNetCore.Identity.Dapper/Tables/IdentityTable.cs @@ -1,30 +1,30 @@ -using System.Data; +using System; +using System.Data; namespace AspNetCore.Identity.Dapper { /// /// A base class for all identity tables. /// - public class IdentityTable : TableBase + public abstract class IdentityTable { /// /// Creates a new instance of . /// - /// - public IdentityTable(IDbConnectionFactory dbConnectionFactory) { - DbConnection = dbConnectionFactory.Create(); + /// + protected IdentityTable(IDbConnectionStore dbConnectionStore) { + _dbConnectionStore = dbConnectionStore; } + private readonly IDbConnectionStore _dbConnectionStore; + private IDbConnection _dbConnection; + /// /// The type of the database connection class used to access the store. /// - protected IDbConnection DbConnection { get; } - - /// - protected override void OnDispose() { - if (DbConnection != null) { - DbConnection.Dispose(); - } + protected IDbConnection DbConnection + { + get => _dbConnection ??= _dbConnectionStore.GetOrCreateConnection(); } } } diff --git a/src/AspNetCore.Identity.Dapper/Tables/RoleClaimsTable.cs b/src/AspNetCore.Identity.Dapper/Tables/RoleClaimsTable.cs index 93e95df..908f238 100644 --- a/src/AspNetCore.Identity.Dapper/Tables/RoleClaimsTable.cs +++ b/src/AspNetCore.Identity.Dapper/Tables/RoleClaimsTable.cs @@ -22,7 +22,7 @@ public class RoleClaimsTable : /// Creates a new instance of . /// /// A factory for creating instances of . - public RoleClaimsTable(IDbConnectionFactory dbConnectionFactory) : base(dbConnectionFactory) { } + public RoleClaimsTable(IDbConnectionStore dbConnectionFactory) : base(dbConnectionFactory) { } /// public virtual async Task> GetClaimsAsync(TKey roleId) { diff --git a/src/AspNetCore.Identity.Dapper/Tables/RolesTable.cs b/src/AspNetCore.Identity.Dapper/Tables/RolesTable.cs index b0cca8b..7f39f09 100644 --- a/src/AspNetCore.Identity.Dapper/Tables/RolesTable.cs +++ b/src/AspNetCore.Identity.Dapper/Tables/RolesTable.cs @@ -25,10 +25,10 @@ public class RolesTable : /// Creates a new instance of . /// /// A factory for creating instances of . - public RolesTable(IDbConnectionFactory dbConnectionFactory) : base(dbConnectionFactory) { } + public RolesTable(IDbConnectionStore dbConnectionFactory) : base(dbConnectionFactory) { } /// - public virtual async Task CreateAsync(TRole role) { + public virtual async Task CreateAsync(TRole role) { const string sql = "INSERT INTO [dbo].[AspNetRoles] " + "VALUES (@Id, @Name, @NormalizedName, @ConcurrencyStamp);"; var rowsInserted = await DbConnection.ExecuteAsync(sql, new { @@ -37,16 +37,26 @@ public virtual async Task CreateAsync(TRole role) { role.NormalizedName, role.ConcurrencyStamp }); - return rowsInserted == 1; + return rowsInserted == 1 + ? IdentityResult.Success + : IdentityResult.Failed(new IdentityError { + Code = string.Empty, + Description = $"Role '{role.Name}' could not be created." + }); } /// - public virtual async Task DeleteAsync(TKey roleId) { + public virtual async Task DeleteAsync(TKey roleId) { const string sql = "DELETE " + "FROM [dbo].[AspNetRoles] " + "WHERE [Id] = @Id;"; var rowsDeleted = await DbConnection.ExecuteAsync(sql, new { Id = roleId }); - return rowsDeleted == 1; + return rowsDeleted == 1 + ? IdentityResult.Success + : IdentityResult.Failed(new IdentityError { + Code = string.Empty, + Description = $"Role id '{roleId}' could not be deleted." + }); } /// @@ -68,7 +78,7 @@ public virtual async Task FindByNameAsync(string normalizedName) { } /// - public virtual async Task UpdateAsync(TRole role, IList claims = null) { + public virtual async Task UpdateAsync(TRole role, IList claims = null) { const string updateRoleSql = "UPDATE [dbo].[AspNetRoles] " + "SET [Name] = @Name, [NormalizedName] = @NormalizedName, [ConcurrencyStamp] = @ConcurrencyStamp " + "WHERE [Id] = @Id;"; @@ -98,10 +108,13 @@ public virtual async Task UpdateAsync(TRole role, IList claims transaction.Commit(); } catch { transaction.Rollback(); - return false; + return IdentityResult.Failed(new IdentityError { + Code = string.Empty, + Description = $"Role '{role.Name}' could not be updated." + }); } } - return true; + return IdentityResult.Success; } } } diff --git a/src/AspNetCore.Identity.Dapper/Tables/TableBase.cs b/src/AspNetCore.Identity.Dapper/Tables/TableBase.cs deleted file mode 100644 index 466be4e..0000000 --- a/src/AspNetCore.Identity.Dapper/Tables/TableBase.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace AspNetCore.Identity.Dapper -{ - /// - /// A base implementation for all tables. - /// - public abstract class TableBase : IDisposable - { - private bool _disposed = false; - - /// - public void Dispose() { - // Dispose of unmanaged resources. - Dispose(true); - // Suppress finalization. - GC.SuppressFinalize(this); - } - - /// - /// A method to free any other managed objects on classes that onherit from . - /// - protected abstract void OnDispose(); - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// Indicates whether the method call comes from a Dispose method (its value is true) or from a finalizer (its value is false). - protected virtual void Dispose(bool disposing) { - if (_disposed) { - return; - } - if (disposing) { - // Free any other managed objects here. - OnDispose(); - } - _disposed = true; - } - } -} diff --git a/src/AspNetCore.Identity.Dapper/Tables/UserClaimsTable.cs b/src/AspNetCore.Identity.Dapper/Tables/UserClaimsTable.cs index 46b0b6b..21839fc 100644 --- a/src/AspNetCore.Identity.Dapper/Tables/UserClaimsTable.cs +++ b/src/AspNetCore.Identity.Dapper/Tables/UserClaimsTable.cs @@ -22,7 +22,7 @@ public class UserClaimsTable : /// Creates a new instance of . /// /// A factory for creating instances of . - public UserClaimsTable(IDbConnectionFactory dbConnectionFactory) : base(dbConnectionFactory) { } + public UserClaimsTable(IDbConnectionStore dbConnectionFactory) : base(dbConnectionFactory) { } /// public virtual async Task> GetClaimsAsync(TKey userId) { diff --git a/src/AspNetCore.Identity.Dapper/Tables/UserLoginsTable.cs b/src/AspNetCore.Identity.Dapper/Tables/UserLoginsTable.cs index 10f76fa..bf0a923 100644 --- a/src/AspNetCore.Identity.Dapper/Tables/UserLoginsTable.cs +++ b/src/AspNetCore.Identity.Dapper/Tables/UserLoginsTable.cs @@ -24,7 +24,7 @@ public class UserLoginsTable : /// Creates a new instance of . /// /// A factory for creating instances of . - public UserLoginsTable(IDbConnectionFactory dbConnectionFactory) : base(dbConnectionFactory) { } + public UserLoginsTable(IDbConnectionStore dbConnectionFactory) : base(dbConnectionFactory) { } /// public virtual async Task> GetLoginsAsync(TKey userId) { diff --git a/src/AspNetCore.Identity.Dapper/Tables/UserRolesTable.cs b/src/AspNetCore.Identity.Dapper/Tables/UserRolesTable.cs index 756390a..754139b 100644 --- a/src/AspNetCore.Identity.Dapper/Tables/UserRolesTable.cs +++ b/src/AspNetCore.Identity.Dapper/Tables/UserRolesTable.cs @@ -24,7 +24,7 @@ public class UserRolesTable : /// Creates a new instance of . /// /// A factory for creating instances of . - public UserRolesTable(IDbConnectionFactory dbConnectionFactory) : base(dbConnectionFactory) { } + public UserRolesTable(IDbConnectionStore dbConnectionFactory) : base(dbConnectionFactory) { } /// public virtual async Task> GetRolesAsync(TKey userId) { diff --git a/src/AspNetCore.Identity.Dapper/Tables/UserTokensTable.cs b/src/AspNetCore.Identity.Dapper/Tables/UserTokensTable.cs index 8df8616..8a96788 100644 --- a/src/AspNetCore.Identity.Dapper/Tables/UserTokensTable.cs +++ b/src/AspNetCore.Identity.Dapper/Tables/UserTokensTable.cs @@ -22,7 +22,7 @@ public class UserTokensTable : /// Creates a new instance of . /// /// A factory for creating instances of . - public UserTokensTable(IDbConnectionFactory dbConnectionFactory) : base(dbConnectionFactory) { } + public UserTokensTable(IDbConnectionStore dbConnectionFactory) : base(dbConnectionFactory) { } /// public virtual async Task> GetTokensAsync(TKey userId) { diff --git a/src/AspNetCore.Identity.Dapper/Tables/UsersTable.cs b/src/AspNetCore.Identity.Dapper/Tables/UsersTable.cs index 690af4b..215a740 100644 --- a/src/AspNetCore.Identity.Dapper/Tables/UsersTable.cs +++ b/src/AspNetCore.Identity.Dapper/Tables/UsersTable.cs @@ -32,10 +32,10 @@ public class UsersTable. /// /// A factory for creating instances of . - public UsersTable(IDbConnectionFactory dbConnectionFactory) : base(dbConnectionFactory) { } + public UsersTable(IDbConnectionStore dbConnectionFactory) : base(dbConnectionFactory) { } /// - public virtual async Task CreateAsync(TUser user) { + public virtual async Task CreateAsync(TUser user) { const string sql = "INSERT INTO [dbo].[AspNetUsers] " + "VALUES (@Id, @UserName, @NormalizedUserName, @Email, @NormalizedEmail, @EmailConfirmed, @PasswordHash, @SecurityStamp, @ConcurrencyStamp, " + "@PhoneNumber, @PhoneNumberConfirmed, @TwoFactorEnabled, @LockoutEnd, @LockoutEnabled, @AccessFailedCount);"; @@ -56,16 +56,26 @@ public virtual async Task CreateAsync(TUser user) { user.LockoutEnabled, user.AccessFailedCount }); - return rowsInserted == 1; + return rowsInserted == 1 + ? IdentityResult.Success + : IdentityResult.Failed(new IdentityError { + Code = string.Empty, + Description = $"User '{user.UserName}' could not be created." + }); } /// - public virtual async Task DeleteAsync(TKey userId) { + public virtual async Task DeleteAsync(TKey userId) { const string sql = "DELETE " + "FROM [dbo].[AspNetUsers] " + "WHERE [Id] = @Id;"; var rowsDeleted = await DbConnection.ExecuteAsync(sql, new { Id = userId }); - return rowsDeleted == 1; + return rowsDeleted == 1 + ? IdentityResult.Success + : IdentityResult.Failed(new IdentityError { + Code = string.Empty, + Description = $"User Id '{userId}' could not be deleted." + }); } /// @@ -96,12 +106,12 @@ public virtual async Task FindByEmailAsync(string normalizedEmail) { } /// - public virtual Task UpdateAsync(TUser user, IList claims, IList logins, IList tokens) { + public virtual Task UpdateAsync(TUser user, IList claims, IList logins, IList tokens) { return UpdateAsync(user, claims, null, logins, tokens); } /// - public virtual async Task UpdateAsync(TUser user, IList claims, IList roles, IList logins, IList tokens) { + public virtual async Task UpdateAsync(TUser user, IList claims, IList roles, IList logins, IList tokens) { const string updateUserSql = "UPDATE [dbo].[AspNetUsers] " + "SET [UserName] = @UserName, [NormalizedUserName] = @NormalizedUserName, [Email] = @Email, [NormalizedEmail] = @NormalizedEmail, [EmailConfirmed] = @EmailConfirmed, " + @@ -128,66 +138,28 @@ public virtual async Task UpdateAsync(TUser user, IList claims user.Id }, transaction); if (claims?.Count() > 0) { - const string deleteClaimsSql = "DELETE " + - "FROM [dbo].[AspNetUserClaims] " + - "WHERE [UserId] = @UserId;"; - await DbConnection.ExecuteAsync(deleteClaimsSql, new { UserId = user.Id }, transaction); - const string insertClaimsSql = "INSERT INTO [dbo].[AspNetUserClaims] (UserId, ClaimType, ClaimValue) " + - "VALUES (@UserId, @ClaimType, @ClaimValue);"; - await DbConnection.ExecuteAsync(insertClaimsSql, claims.Select(x => new { - UserId = user.Id, - x.ClaimType, - x.ClaimValue - }), transaction); + await ReplaceClaims(claims, user.Id, transaction); } if (roles?.Count() > 0) { - const string deleteRolesSql = "DELETE " + - "FROM [dbo].[AspNetUserRoles] " + - "WHERE [UserId] = @UserId;"; - await DbConnection.ExecuteAsync(deleteRolesSql, new { UserId = user.Id }, transaction); - const string insertRolesSql = "INSERT INTO [dbo].[AspNetUserRoles] (UserId, RoleId) " + - "VALUES (@UserId, @RoleId);"; - await DbConnection.ExecuteAsync(insertRolesSql, roles.Select(x => new { - UserId = user.Id, - x.RoleId - }), transaction); + await ReplaceRoles(roles, user.Id, transaction); } if (logins?.Count() > 0) { - const string deleteLoginsSql = "DELETE " + - "FROM [dbo].[AspNetUserLogins] " + - "WHERE [UserId] = @UserId;"; - await DbConnection.ExecuteAsync(deleteLoginsSql, new { UserId = user.Id }, transaction); - const string insertLoginsSql = "INSERT INTO [dbo].[AspNetUserLogins] (LoginProvider, ProviderKey, ProviderDisplayName, UserId) " + - "VALUES (@LoginProvider, @ProviderKey, @ProviderDisplayName, @UserId);"; - await DbConnection.ExecuteAsync(insertLoginsSql, logins.Select(x => new { - x.LoginProvider, - x.ProviderKey, - x.ProviderDisplayName, - UserId = user.Id - }), transaction); + await ReplaceLogins(logins, user.Id, transaction); } if (tokens?.Count() > 0) { - const string deleteTokensSql = "DELETE " + - "FROM [dbo].[AspNetUserTokens] " + - "WHERE [UserId] = @UserId;"; - await DbConnection.ExecuteAsync(deleteTokensSql, new { UserId = user.Id }, transaction); - const string insertTokensSql = "INSERT INTO [dbo].[AspNetUserTokens] (UserId, LoginProvider, Name, Value) " + - "VALUES (@UserId, @LoginProvider, @Name, @Value);"; - await DbConnection.ExecuteAsync(insertTokensSql, tokens.Select(x => new { - x.UserId, - x.LoginProvider, - x.Name, - x.Value - }), transaction); + await ReplaceTokens(tokens, user.Id, transaction); } try { transaction.Commit(); } catch { transaction.Rollback(); - return false; + return IdentityResult.Failed(new IdentityError { + Code = string.Empty, + Description = $"User '{user.UserName}' could not be updated." + }); } } - return true; + return IdentityResult.Success; } /// @@ -213,5 +185,62 @@ public virtual async Task> GetUsersForClaimAsync(Claim claim) }); return users; } + /// + protected virtual async Task ReplaceClaims(IList claims, TKey userId, IDbTransaction transaction) { + const string deleteClaimsSql = "DELETE " + + "FROM [dbo].[AspNetUserClaims] " + + "WHERE [UserId] = @UserId;"; + await DbConnection.ExecuteAsync(deleteClaimsSql, new { UserId = userId }, transaction); + const string insertClaimsSql = "INSERT INTO [dbo].[AspNetUserClaims] (UserId, ClaimType, ClaimValue) " + + "VALUES (@UserId, @ClaimType, @ClaimValue);"; + return await DbConnection.ExecuteAsync(insertClaimsSql, claims.Select(x => new { + UserId = userId, + x.ClaimType, + x.ClaimValue + }), transaction); + } + /// + protected virtual async Task ReplaceRoles(IList roles, TKey userId, IDbTransaction transaction) { + const string deleteRolesSql = "DELETE " + + "FROM [dbo].[AspNetUserRoles] " + + "WHERE [UserId] = @UserId;"; + await DbConnection.ExecuteAsync(deleteRolesSql, new { UserId = userId }, transaction); + const string insertRolesSql = "INSERT INTO [dbo].[AspNetUserRoles] (UserId, RoleId) " + + "VALUES (@UserId, @RoleId);"; + return await DbConnection.ExecuteAsync(insertRolesSql, roles.Select(x => new { + UserId = userId, + x.RoleId + }), transaction); + } + /// + protected virtual async Task ReplaceLogins(IList logins, TKey userId, IDbTransaction transaction) { + const string deleteLoginsSql = "DELETE " + + "FROM [dbo].[AspNetUserLogins] " + + "WHERE [UserId] = @UserId;"; + await DbConnection.ExecuteAsync(deleteLoginsSql, new { UserId = userId }, transaction); + const string insertLoginsSql = "INSERT INTO [dbo].[AspNetUserLogins] (LoginProvider, ProviderKey, ProviderDisplayName, UserId) " + + "VALUES (@LoginProvider, @ProviderKey, @ProviderDisplayName, @UserId);"; + return await DbConnection.ExecuteAsync(insertLoginsSql, logins.Select(x => new { + x.LoginProvider, + x.ProviderKey, + x.ProviderDisplayName, + UserId = userId + }), transaction); + } + /// + protected virtual async Task ReplaceTokens(IList tokens, TKey userId, IDbTransaction transaction) { + const string deleteTokensSql = "DELETE " + + "FROM [dbo].[AspNetUserTokens] " + + "WHERE [UserId] = @UserId;"; + await DbConnection.ExecuteAsync(deleteTokensSql, new { UserId = userId }, transaction); + const string insertTokensSql = "INSERT INTO [dbo].[AspNetUserTokens] (UserId, LoginProvider, Name, Value) " + + "VALUES (@UserId, @LoginProvider, @Name, @Value);"; + return await DbConnection.ExecuteAsync(insertTokensSql, tokens.Select(x => new { + x.UserId, + x.LoginProvider, + x.Name, + x.Value + }), transaction); + } } }