diff --git a/Volo.Abp.sln b/Volo.Abp.sln index 4cb5402a6e..884d660572 100644 --- a/Volo.Abp.sln +++ b/Volo.Abp.sln @@ -82,6 +82,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Abp.Identity", "Abp.Identit EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Volo.Abp.Identity.EntityFrameworkCore", "src\Volo.Abp.Identity.EntityFrameworkCore\Volo.Abp.Identity.EntityFrameworkCore.xproj", "{439DFC0F-1BA2-464F-900E-EA7E18C08975}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Volo.Abp.Identity.Tests", "test\Volo.Abp.Identity.Tests\Volo.Abp.Identity.Tests.xproj", "{4AB91077-82DC-4335-9274-BCE017BD9C8B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -196,6 +198,10 @@ Global {439DFC0F-1BA2-464F-900E-EA7E18C08975}.Debug|Any CPU.Build.0 = Debug|Any CPU {439DFC0F-1BA2-464F-900E-EA7E18C08975}.Release|Any CPU.ActiveCfg = Release|Any CPU {439DFC0F-1BA2-464F-900E-EA7E18C08975}.Release|Any CPU.Build.0 = Release|Any CPU + {4AB91077-82DC-4335-9274-BCE017BD9C8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4AB91077-82DC-4335-9274-BCE017BD9C8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4AB91077-82DC-4335-9274-BCE017BD9C8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4AB91077-82DC-4335-9274-BCE017BD9C8B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -236,5 +242,6 @@ Global {17DBB40A-243E-41F7-A672-FA316ECB1E33} = {1895A5C9-50D4-4568-9A3A-14657E615A5E} {146F561E-C7B8-4166-9383-47E1BC1A2E62} = {447C8A77-E5F0-4538-8687-7383196D04EA} {439DFC0F-1BA2-464F-900E-EA7E18C08975} = {1895A5C9-50D4-4568-9A3A-14657E615A5E} + {4AB91077-82DC-4335-9274-BCE017BD9C8B} = {146F561E-C7B8-4166-9383-47E1BC1A2E62} EndGlobalSection EndGlobal diff --git a/src/AbpDesk/AbpDesk.EntityFrameworkCore/AbpDesk/EntityFrameworkCore/AbpDeskEntityFrameworkCoreModule.cs b/src/AbpDesk/AbpDesk.EntityFrameworkCore/AbpDesk/EntityFrameworkCore/AbpDeskEntityFrameworkCoreModule.cs index bd7fabdd18..6dedcafaef 100644 --- a/src/AbpDesk/AbpDesk.EntityFrameworkCore/AbpDesk/EntityFrameworkCore/AbpDeskEntityFrameworkCoreModule.cs +++ b/src/AbpDesk/AbpDesk.EntityFrameworkCore/AbpDesk/EntityFrameworkCore/AbpDeskEntityFrameworkCoreModule.cs @@ -10,7 +10,7 @@ namespace AbpDesk.EntityFrameworkCore public override void ConfigureServices(IServiceCollection services) { services.AddAbpDbContext(); - services.AddDefaultEfCoreRepositories(); //TODO: Move this into AddAbpDbContext as an option + services.AddDefaultEfCoreRepositories(); services.AddAssemblyOf(); } diff --git a/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreServiceCollectionExtensions.cs b/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreServiceCollectionExtensions.cs index 86d87634fd..b58ae91065 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreServiceCollectionExtensions.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreServiceCollectionExtensions.cs @@ -6,6 +6,10 @@ using Volo.Abp.EntityFrameworkCore; namespace Microsoft.Extensions.DependencyInjection { + //TODO: By default, only create repositories for Aggregate Roots. + //TODO: Move AddDefaultEfCoreRepositories into AddAbpDbContext as optional which will have it's own options + //TODO: Add options to use a provided type as default repository. + public static class AbpEfCoreServiceCollectionExtensions { public static IServiceCollection AddAbpDbContext( @@ -25,8 +29,6 @@ namespace Microsoft.Extensions.DependencyInjection public static IServiceCollection AddDefaultEfCoreRepositories(this IServiceCollection services) where TDbContext : AbpDbContext { - //TODO: Add options to use a provided type as default repository. - var dbContextType = typeof(TDbContext); foreach (var entityType in DbContextHelper.GetEntityTypes(dbContextType)) diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 0ddd648fe5..f37e3d51e6 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -7,6 +8,16 @@ using Volo.Abp.EntityFrameworkCore; namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore { + public class EfCoreRepository : EfCoreRepository, IEfCoreRepository + where TDbContext : AbpDbContext + where TEntity : class, IEntity + { + public EfCoreRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + } + public class EfCoreRepository : QueryableRepositoryBase, IEfCoreRepository where TDbContext : AbpDbContext where TEntity : class, IEntity @@ -72,13 +83,25 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore public override async Task InsertAndGetIdAsync(TEntity entity) { var insertedEntity = await InsertAsync(entity); - await DbContext.SaveChangesAsync(); + await DbContext.SaveChangesAsync(true); return insertedEntity.Id; } public override TEntity Update(TEntity entity) { - return DbSet.Update(entity).Entity; + //TODO: This code is got from UserStore.UpdateAsync and revised Update method based on that, but we should be sure that it's valid + //Context.Attach(user); + //user.ConcurrencyStamp = Guid.NewGuid().ToString(); + //Context.Update(user); + + DbContext.Attach(entity); //TODO: What is different for DbSet.Attach(entity)? + + if (entity is IHasConcurrencyStamp) + { + (entity as IHasConcurrencyStamp).ConcurrencyStamp = Guid.NewGuid().ToString(); //TODO: Use IGuidGenerator! + } + + return DbContext.Update(entity).Entity; //TODO: or DbSet.Update(entity) ? } public override void Delete(TEntity entity) diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/IEfCoreRepository.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/IEfCoreRepository.cs index f7099e57f7..38d083dac7 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/IEfCoreRepository.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/IEfCoreRepository.cs @@ -3,6 +3,12 @@ using Volo.Abp.Domain.Entities; namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore { + public interface IEfCoreRepository : IEfCoreRepository, IQueryableRepository + where TEntity : class, IEntity + { + + } + public interface IEfCoreRepository : IQueryableRepository where TEntity : class, IEntity { diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index c1235e85a7..12a1d1018c 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore { @@ -10,5 +13,31 @@ namespace Volo.Abp.EntityFrameworkCore { } + + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + try + { + return base.SaveChanges(acceptAllChangesOnSuccess); + } + catch (DbUpdateConcurrencyException ex) + { + //TODO: Better exception message using DbUpdateConcurrencyException + throw new AbpDbConcurrencyException(ex.Message, ex); + } + } + + public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) + { + try + { + return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + } + catch (DbUpdateConcurrencyException ex) + { + //TODO: Better exception message using DbUpdateConcurrencyException + throw new AbpDbConcurrencyException(ex.Message, ex); + } + } } } \ No newline at end of file diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextDatabaseApi.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextDatabaseApi.cs index cf08eab64f..7123b40ed6 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextDatabaseApi.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/DbContextDatabaseApi.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; using Volo.Abp.EntityFrameworkCore; @@ -18,6 +19,11 @@ namespace Volo.Abp.Uow.EntityFrameworkCore return DbContext.SaveChangesAsync(); } + public Task SaveChangesAsync(CancellationToken cancellationToken) + { + return DbContext.SaveChangesAsync(cancellationToken); + } + public Task CommitAsync() { return DbContext.SaveChangesAsync(); diff --git a/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EfCoreIdentityRoleRepository.cs b/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EfCoreIdentityRoleRepository.cs new file mode 100644 index 0000000000..a127887687 --- /dev/null +++ b/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EfCoreIdentityRoleRepository.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Identity.EntityFrameworkCore; + +namespace Volo.Abp.Identity +{ + //TODO: Register for all repositories! + public class EfCoreIdentityRoleRepository : EfCoreRepository, IIdentityRoleRepository + { + public EfCoreIdentityRoleRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public Task FindByNormalizedNameAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + return DbSet.FirstOrDefaultAsync(r => r.NormalizedName == normalizedRoleName, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EfCoreIdentityUserRepository.cs b/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EfCoreIdentityUserRepository.cs new file mode 100644 index 0000000000..2a9975a197 --- /dev/null +++ b/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EfCoreIdentityUserRepository.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Threading; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Identity.EntityFrameworkCore; + +namespace Volo.Abp.Identity +{ + //TODO: Register for; + //- IIdentityUserRepository + //- IRepository + //- IRepository + //- IQueryableRepository + //- IQueryableRepository + public class EfCoreIdentityUserRepository : EfCoreRepository, IIdentityUserRepository + { + public EfCoreIdentityUserRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public Task FindByNormalizedUserNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + return DbSet.FirstOrDefaultAsync(u => u.NormalizedUserName == normalizedUserName, cancellationToken); + } + + public Task> GetRoleNamesAsync(string userId) + { + var query = from userRole in DbContext.UserRoles + join role in DbContext.Roles on userRole.RoleId equals role.Id + where userRole.UserId.Equals(userId) + select role.Name; + + return query.ToListAsync(); + } + + public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + var userLogin = await DbContext.UserLogins.FindAsync(new object[] { loginProvider, providerKey }, cancellationToken); + if (userLogin == null) + { + return null; + } + + return await DbSet.FindAsync(new object[] { userLogin.UserId }, cancellationToken); + } + + public Task FindByNormalizedEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + { + return DbSet.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken); + } + + public async Task> GetListByClaimAsync(Claim claim, CancellationToken cancellationToken) + { + var query = from userclaims in DbContext.UserClaims + join user in DbContext.Users on userclaims.UserId equals user.Id + where userclaims.ClaimValue == claim.Value && userclaims.ClaimType == claim.Type + select user; + + return await query.ToListAsync(cancellationToken); + } + + public async Task> GetListByNormalizedRoleNameAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + var role = await DbContext.Roles.Where(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken); + + if (role == null) + { + return new List(); + } + + var query = from userrole in DbContext.UserRoles + join user in DbContext.Users on userrole.UserId equals user.Id + where userrole.RoleId.Equals(role.Id) + select user; + + return await query.ToListAsync(cancellationToken); + } + } +} diff --git a/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreModule.cs b/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreModule.cs index 20fc9f3fda..0617ecef3c 100644 --- a/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreModule.cs +++ b/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/AbpIdentityEntityFrameworkCoreModule.cs @@ -10,7 +10,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore public override void ConfigureServices(IServiceCollection services) { services.AddAbpDbContext(); - services.AddDefaultEfCoreRepositories(); //TODO: Move this into AddAbpDbContext as optional which will have it's own options + services.AddDefaultEfCoreRepositories(); services.AddAssemblyOf(); } } diff --git a/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs b/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs index 6e38eeffcf..a5265a9ce1 100644 --- a/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs +++ b/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContext.cs @@ -17,7 +17,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { } - + /// /// Gets or sets the of Users. /// diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IIdentityRoleRepository.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IIdentityRoleRepository.cs new file mode 100644 index 0000000000..666eecc3a5 --- /dev/null +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IIdentityRoleRepository.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Identity +{ + public interface IIdentityRoleRepository : IRepository + { + Task FindByNormalizedNameAsync(string normalizedRoleName, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IIdentityUserRepository.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IIdentityUserRepository.cs new file mode 100644 index 0000000000..347b02c202 --- /dev/null +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IIdentityUserRepository.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.Identity +{ + public interface IIdentityUserRepository : IRepository + { + Task FindByNormalizedUserNameAsync([NotNull] string normalizedUserName, CancellationToken cancellationToken); + + Task> GetRoleNamesAsync([NotNull] string userId); + + Task FindByLoginAsync([NotNull] string loginProvider, [NotNull] string providerKey, CancellationToken cancellationToken); + + Task FindByNormalizedEmailAsync([NotNull] string normalizedEmail, CancellationToken cancellationToken); + + Task> GetListByClaimAsync(Claim claim, CancellationToken cancellationToken); + + Task> GetListByNormalizedRoleNameAsync(string normalizedRoleName, CancellationToken cancellationToken); + } +} diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUser.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUser.cs index e8901e5047..4f66d60150 100644 --- a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUser.cs +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUser.cs @@ -1,15 +1,21 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; +using System.Security.Claims; +using System.Threading; using JetBrains.Annotations; +using Microsoft.AspNetCore.Identity; using Volo.Abp.Domain.Entities; +using Volo.ExtensionMethods.Collections.Generic; namespace Volo.Abp.Identity { //TODO: Should set Id to a GUID (where? on repository?) //TODO: Properties should not be public! + //TODO: Add Name/Surname/FullName? - public class IdentityUser : AggregateRoot + public class IdentityUser : AggregateRoot, IHasConcurrencyStamp { /// /// Gets or sets the user name for this user. @@ -88,6 +94,8 @@ namespace Volo.Abp.Identity /// public virtual int AccessFailedCount { get; set; } + //TODO: Can we make collections readonly collection, which will provide encapsulation but can work for all ORMs? + /// /// Navigation property for the roles this user belongs to. /// @@ -120,6 +128,109 @@ namespace Volo.Abp.Identity UserName = userName; } + public void AddRole([NotNull] string roleId) + { + Check.NotNull(roleId, nameof(roleId)); + + if (Roles.Any(r => r.RoleId == roleId)) + { + return; + } + + Roles.Add(new IdentityUserRole(Id, roleId)); + } + + public void RemoveRole([NotNull] string roleId) + { + Check.NotNull(roleId, nameof(roleId)); + + if (Roles.All(r => r.RoleId != roleId)) + { + return; + } + + Roles.Add(new IdentityUserRole(Id, roleId)); + } + + public bool IsInRole([NotNull] string roleId) + { + Check.NotNull(roleId, nameof(roleId)); + + return Roles.Any(r => r.RoleId == roleId); + } + + public void AddClaims([NotNull] IEnumerable claims) + { + Check.NotNull(claims, nameof(claims)); + + foreach (var claim in claims) + { + Claims.Add(new IdentityUserClaim(Id, claim)); + } + } + + public void ReplaceClaim([NotNull] Claim claim, [NotNull] Claim newClaim) + { + Check.NotNull(claim, nameof(claim)); + Check.NotNull(newClaim, nameof(newClaim)); + + var userClaims = Claims.Where(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type); + foreach (var userClaim in userClaims) + { + userClaim.SetClaim(newClaim); + } + } + + public void RemoveClaims([NotNull] IEnumerable claims) + { + Check.NotNull(claims, nameof(claims)); + + foreach (var claim in claims) + { + Claims.RemoveAll(uc => uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type); + } + } + + public void AddLogin([NotNull] UserLoginInfo login) + { + Check.NotNull(login, nameof(login)); + + Logins.Add(new IdentityUserLogin(Id, login)); + } + + public void RemoveLogin([NotNull] string loginProvider, [NotNull] string providerKey) + { + Check.NotNull(loginProvider, nameof(loginProvider)); + Check.NotNull(providerKey, nameof(providerKey)); + + Logins.RemoveAll(userLogin => userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey); + } + + [CanBeNull] + public IdentityUserToken FindToken(string loginProvider, string name) + { + return Tokens.FirstOrDefault(t => t.LoginProvider == loginProvider && t.Name == name); + } + + public void SetToken(string loginProvider, string name, string value) + { + var token = FindToken(loginProvider, name); + if (token == null) + { + + Tokens.Add(new IdentityUserToken(Id, loginProvider, name, value)); + } + else + { + token.Value = value; + } + } + + public void RemoveToken(string loginProvider, string name) + { + Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name); + } + /// /// Returns the username for this user. /// diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserClaim.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserClaim.cs index b7ff073cf5..9eb97a4eeb 100644 --- a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserClaim.cs +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserClaim.cs @@ -57,5 +57,13 @@ namespace Volo.Abp.Identity { return new Claim(ClaimType, ClaimValue); } + + public void SetClaim([NotNull] Claim claim) + { + Check.NotNull(claim, nameof(claim)); + + ClaimType = claim.Type; + ClaimValue = claim.Value; + } } } \ No newline at end of file diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserLogin.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserLogin.cs index 836fa99f9d..defec8c4cc 100644 --- a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserLogin.cs +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserLogin.cs @@ -1,8 +1,11 @@ using JetBrains.Annotations; +using Microsoft.AspNetCore.Identity; using Volo.Abp.Domain.Entities; namespace Volo.Abp.Identity { + //TODO: Make constructors internal to ensure that it's called by only from IdentityUser? + /// /// Represents a login and its associated provider for a user. /// @@ -44,5 +47,21 @@ namespace Volo.Abp.Identity ProviderKey = providerKey; ProviderDisplayName = providerDisplayName; } + + public IdentityUserLogin(string userId, UserLoginInfo login) + { + Check.NotNull(userId, nameof(userId)); + Check.NotNull(login, nameof(login)); + + UserId = userId; + LoginProvider = login.LoginProvider; + ProviderKey = login.ProviderKey; + ProviderDisplayName = login.ProviderDisplayName; + } + + public UserLoginInfo ToUserLoginInfo() + { + return new UserLoginInfo(LoginProvider, ProviderKey, ProviderDisplayName); + } } } \ No newline at end of file diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserManager.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserManager.cs index c3bb1f2a97..3b1b66c799 100644 --- a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserManager.cs +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserManager.cs @@ -11,7 +11,7 @@ namespace Volo.Abp.Identity public class IdentityUserManager : UserManager, IDomainService { public IdentityUserManager( - IUserStore store, + UserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/UserStore.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/UserStore.cs new file mode 100644 index 0000000000..0b5478818a --- /dev/null +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/UserStore.cs @@ -0,0 +1,1106 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Identity; +using Volo.Abp.Uow; +using Volo.DependencyInjection; +using Volo.ExtensionMethods; + +namespace Volo.Abp.Identity +{ + /// + /// Represents a new instance of a persistence store for the specified user and role types. + /// + public class UserStore : + IUserLoginStore, + IUserRoleStore, + IUserClaimStore, + IUserPasswordStore, + IUserSecurityStampStore, + IUserEmailStore, + IUserLockoutStore, + IUserPhoneNumberStore, + IUserTwoFactorStore, + IUserAuthenticationTokenStore, + ITransientDependency + { + /// + /// Gets or sets the for any error that occurred with the current operation. + /// + public IdentityErrorDescriber ErrorDescriber { get; set; } + + /// + /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called. + /// + /// + /// True if changes should be automatically persisted, otherwise false. + /// + public bool AutoSaveChanges { get; set; } = true; + + private readonly IIdentityRoleRepository _roleRepository; + private readonly IIdentityUserRepository _userRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + private bool _disposed; + + public UserStore( + IUnitOfWorkManager unitOfWorkManager, + IIdentityUserRepository userRepository, + IIdentityRoleRepository roleRepository, + IdentityErrorDescriber describer = null) //TODO: describer? TODO: Test if DI supports optional injection + { + _unitOfWorkManager = unitOfWorkManager; + _userRepository = userRepository; + _roleRepository = roleRepository; + + ErrorDescriber = describer ?? new IdentityErrorDescriber(); + } + + /// Saves the current store. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + protected Task SaveChanges(CancellationToken cancellationToken) + { + if (!AutoSaveChanges || _unitOfWorkManager.Current == null) + { + return Task.CompletedTask; + } + + return _unitOfWorkManager.Current.SaveChangesAsync(cancellationToken); + } + + /// + /// Gets the user identifier for the specified . + /// + /// The user whose identifier should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the identifier for the specified . + public virtual Task GetUserIdAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.Id); + } + + /// + /// Gets the user name for the specified . + /// + /// The user whose name should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the name for the specified . + public virtual Task GetUserNameAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.UserName); + } + + /// + /// Sets the given for the specified . + /// + /// The user whose name should be set. + /// The user name to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetUserNameAsync([NotNull] IdentityUser user, string userName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.UserName = userName; + + return Task.CompletedTask; + } + + /// + /// Gets the normalized user name for the specified . + /// + /// The user whose normalized name should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the normalized user name for the specified . + public virtual Task GetNormalizedUserNameAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.NormalizedUserName); + } + + /// + /// Sets the given normalized name for the specified . + /// + /// The user whose name should be set. + /// The normalized name to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetNormalizedUserNameAsync([NotNull] IdentityUser user, string normalizedName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.NormalizedUserName = normalizedName; + + return Task.CompletedTask; + } + + /// + /// Creates the specified in the user store. + /// + /// The user to create. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the creation operation. + public virtual async Task CreateAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + await _userRepository.InsertAsync(user); + await SaveChanges(cancellationToken); + + return IdentityResult.Success; + } + + /// + /// Updates the specified in the user store. + /// + /// The user to update. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public virtual async Task UpdateAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + try + { + await _userRepository.UpdateAsync(user); + await SaveChanges(cancellationToken); + } + catch (AbpDbConcurrencyException ex) //(DbUpdateConcurrencyException) + { + //TODO: Log... + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + + return IdentityResult.Success; + } + + /// + /// Deletes the specified from the user store. + /// + /// The user to delete. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the of the update operation. + public virtual async Task DeleteAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + try + { + await _userRepository.DeleteAsync(user); + await SaveChanges(cancellationToken); + } + catch (AbpDbConcurrencyException ex) //(DbUpdateConcurrencyException) + { + //TODO: Log... + return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure()); + } + + return IdentityResult.Success; + } + + /// + /// Finds and returns a user, if any, who has the specified . + /// + /// The user ID to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public virtual Task FindByIdAsync([NotNull] string userId, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(userId, nameof(userId)); + + //TODO: Add cancellationToken to Repository.FindAsync method as overload + return _userRepository.FindAsync(userId); + } + + /// + /// Finds and returns a user, if any, who has the specified normalized user name. + /// + /// The normalized user name to search for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing the user matching the specified if it exists. + /// + public virtual Task FindByNameAsync([NotNull] string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(normalizedUserName, nameof(normalizedUserName)); + + return _userRepository.FindByNormalizedUserNameAsync(normalizedUserName, cancellationToken); + } + + /// + /// Sets the password hash for a user. + /// + /// The user to set the password hash for. + /// The password hash to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetPasswordHashAsync([NotNull] IdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.PasswordHash = passwordHash; + + return Task.CompletedTask; + } + + /// + /// Gets the password hash for a user. + /// + /// The user to retrieve the password hash for. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the password hash for the user. + public virtual Task GetPasswordHashAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.PasswordHash); + } + + /// + /// Returns a flag indicating if the specified user has a password. + /// + /// The user to retrieve the password hash for. + /// The used to propagate notifications that the operation should be canceled. + /// A containing a flag indicating if the specified user has a password. If the + /// user has a password the returned value with be true, otherwise it will be false. + public virtual Task HasPasswordAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + //ThrowIfDisposed(); TODO: This was not exists in MS implementation, why? + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.PasswordHash != null); + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the role to. + /// The role to add. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual async Task AddToRoleAsync([NotNull] IdentityUser user, [NotNull] string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + if (normalizedRoleName.IsNullOrWhiteSpace()) //TODO: Create a Check.NotNullOrWhiteSpace()? + { + throw new ArgumentException(nameof(normalizedRoleName) + " can not be null or whitespace"); + } + + var role = await _roleRepository.FindByNormalizedNameAsync(normalizedRoleName, cancellationToken); + + if (role == null) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Role {0} does not exist.", normalizedRoleName)); //TODO: Localize + } + + user.AddRole(role.Id); + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the role from. + /// The role to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual async Task RemoveFromRoleAsync([NotNull] IdentityUser user, [NotNull] string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException(nameof(normalizedRoleName) + " can not be null or whitespace"); + } + + var role = await _roleRepository.FindByNormalizedNameAsync(normalizedRoleName, cancellationToken); + if (role == null) + { + return; + } + + user.RemoveRole(role.Id); + } + + /// + /// Retrieves the roles the specified is a member of. + /// + /// The user whose roles should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the roles the user is a member of. + public virtual async Task> GetRolesAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return await _userRepository.GetRoleNamesAsync(user.Id); + } + + /// + /// Returns a flag indicating if the specified user is a member of the give . + /// + /// The user whose role membership should be checked. + /// The role to check membership of + /// The used to propagate notifications that the operation should be canceled. + /// A containing a flag indicating if the specified user is a member of the given group. If the + /// user is a member of the group the returned value with be true, otherwise it will be false. + public virtual async Task IsInRoleAsync([NotNull] IdentityUser user, [NotNull] string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + if (string.IsNullOrWhiteSpace(normalizedRoleName)) + { + throw new ArgumentException(nameof(normalizedRoleName) + " can not be null or whitespace"); + } + + var role = await _roleRepository.FindByNormalizedNameAsync(normalizedRoleName, cancellationToken); + if (role == null) + { + return false; + } + + return user.IsInRole(role.Id); + } + + /// + /// Throws if this class has been disposed. + /// + protected void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + /// Dispose the store + /// + public void Dispose() + { + _disposed = true; //TODO: Remove dispose code? + } + + /// + /// Get the claims associated with the specified as an asynchronous operation. + /// + /// The user whose claims should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// A that contains the claims granted to a user. + public virtual Task> GetClaimsAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult>(user.Claims.Select(c => c.ToClaim()).ToList()); + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the claim to. + /// The claim to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task AddClaimsAsync([NotNull] IdentityUser user, [NotNull] IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + Check.NotNull(claims, nameof(claims)); + + user.AddClaims(claims); + + return Task.CompletedTask; + } + + /// + /// Replaces the on the specified , with the . + /// + /// The user to replace the claim on. + /// The claim replace. + /// The new claim replacing the . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task ReplaceClaimAsync([NotNull] IdentityUser user, [NotNull] Claim claim, [NotNull] Claim newClaim, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + Check.NotNull(claim, nameof(claim)); + Check.NotNull(newClaim, nameof(newClaim)); + + user.ReplaceClaim(claim, newClaim); + + return Task.CompletedTask; + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the claims from. + /// The claim to remove. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task RemoveClaimsAsync([NotNull] IdentityUser user, [NotNull] IEnumerable claims, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + Check.NotNull(claims, nameof(claims)); + + user.RemoveClaims(claims); + + return Task.CompletedTask; + } + + /// + /// Adds the given to the specified . + /// + /// The user to add the login to. + /// The login to add to the user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task AddLoginAsync([NotNull] IdentityUser user, [NotNull] UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + Check.NotNull(login, nameof(login)); + + user.AddLogin(login); + + return Task.CompletedTask; + } + + /// + /// Removes the given from the specified . + /// + /// The user to remove the login from. + /// The login to remove from the user. + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task RemoveLoginAsync([NotNull] IdentityUser user, [NotNull] string loginProvider, [NotNull] string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + Check.NotNull(loginProvider, nameof(loginProvider)); + Check.NotNull(providerKey, nameof(providerKey)); + + user.RemoveLogin(loginProvider, providerKey); + + return Task.CompletedTask; + } + + /// + /// Retrieves the associated logins for the specified . + /// + /// The user whose associated logins to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing a list of for the specified , if any. + /// + public virtual Task> GetLoginsAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult>(user.Logins.Select(l => l.ToUserLoginInfo()).ToList()); + } + + /// + /// Retrieves the user associated with the specified login provider and login provider key.. + /// + /// The login provider who provided the . + /// The key provided by the to identify a user. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The for the asynchronous operation, containing the user, if any which matched the specified login provider and key. + /// + public virtual Task FindByLoginAsync([NotNull] string loginProvider, [NotNull] string providerKey, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(loginProvider, nameof(loginProvider)); + Check.NotNull(providerKey, nameof(providerKey)); + + return _userRepository.FindByLoginAsync(loginProvider, providerKey, cancellationToken); + } + + /// + /// Gets a flag indicating whether the email address for the specified has been verified, true if the email address is verified otherwise + /// false. + /// + /// The user whose email confirmation status should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous operation, a flag indicating whether the email address for the specified + /// has been confirmed or not. + /// + public virtual Task GetEmailConfirmedAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.EmailConfirmed); + } + + /// + /// Sets the flag indicating whether the specified 's email address has been confirmed or not. + /// + /// The user whose email confirmation status should be set. + /// A flag indicating if the email address has been confirmed, true if the address is confirmed otherwise false. + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public virtual Task SetEmailConfirmedAsync([NotNull] IdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.EmailConfirmed = confirmed; + + return Task.CompletedTask; + } + + /// + /// Sets the address for a . + /// + /// The user whose email should be set. + /// The email to set. + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public virtual Task SetEmailAsync([NotNull] IdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.Email = email; + + return Task.CompletedTask; + } + + /// + /// Gets the email address for the specified . + /// + /// The user whose email should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// The task object containing the results of the asynchronous operation, the email address for the specified . + public virtual Task GetEmailAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.Email); + } + + /// + /// Returns the normalized email for the specified . + /// + /// The user whose email address to retrieve. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the normalized email address if any associated with the specified user. + /// + public virtual Task GetNormalizedEmailAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.NormalizedEmail); + } + + /// + /// Sets the normalized email for the specified . + /// + /// The user whose email address to set. + /// The normalized email to set for the specified . + /// The used to propagate notifications that the operation should be canceled. + /// The task object representing the asynchronous operation. + public virtual Task SetNormalizedEmailAsync([NotNull] IdentityUser user, string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.NormalizedEmail = normalizedEmail; + + return Task.CompletedTask; + } + + /// + /// Gets the user, if any, associated with the specified, normalized email address. + /// + /// The normalized email address to return the user for. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address. + /// + public virtual Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + return _userRepository.FindByNormalizedEmailAsync(normalizedEmail, cancellationToken); + } + + /// + /// Gets the last a user's last lockout expired, if any. + /// Any time in the past should be indicates a user is not locked out. + /// + /// The user whose lockout date should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// A that represents the result of the asynchronous query, a containing the last time + /// a user's lockout expired, if any. + /// + public virtual Task GetLockoutEndDateAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.LockoutEnd); + } + + /// + /// Locks out a user until the specified end date has passed. Setting a end date in the past immediately unlocks a user. + /// + /// The user whose lockout date should be set. + /// The after which the 's lockout should end. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetLockoutEndDateAsync([NotNull] IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.LockoutEnd = lockoutEnd; + + return Task.CompletedTask; + } + + /// + /// Records that a failed access has occurred, incrementing the failed access count. + /// + /// The user whose cancellation count should be incremented. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the incremented failed access count. + public virtual Task IncrementAccessFailedCountAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.AccessFailedCount++; + + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Resets a user's failed access count. + /// + /// The user whose failed access count should be reset. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + /// This is typically called after the account is successfully accessed. + public virtual Task ResetAccessFailedCountAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.AccessFailedCount = 0; + + return Task.CompletedTask; + } + + /// + /// Retrieves the current failed access count for the specified .. + /// + /// The user whose failed access count should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the failed access count. + public virtual Task GetAccessFailedCountAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Retrieves a flag indicating whether user lockout can enabled for the specified user. + /// + /// The user whose ability to be locked out should be returned. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, true if a user can be locked out, otherwise false. + /// + public virtual Task GetLockoutEnabledAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.LockoutEnabled); + } + + /// + /// Set the flag indicating if the specified can be locked out.. + /// + /// The user whose ability to be locked out should be set. + /// A flag indicating if lock out can be enabled for the specified . + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetLockoutEnabledAsync([NotNull] IdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.LockoutEnabled = enabled; + + return Task.CompletedTask; + } + + /// + /// Sets the telephone number for the specified . + /// + /// The user whose telephone number should be set. + /// The telephone number to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetPhoneNumberAsync([NotNull] IdentityUser user, string phoneNumber, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.PhoneNumber = phoneNumber; + + return Task.CompletedTask; + } + + /// + /// Gets the telephone number, if any, for the specified . + /// + /// The user whose telephone number should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the user's telephone number, if any. + public virtual Task GetPhoneNumberAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.PhoneNumber); + } + + /// + /// Gets a flag indicating whether the specified 's telephone number has been confirmed. + /// + /// The user to return a flag for, indicating whether their telephone number is confirmed. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, returning true if the specified has a confirmed + /// telephone number otherwise false. + /// + public virtual Task GetPhoneNumberConfirmedAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.PhoneNumberConfirmed); + } + + /// + /// Sets a flag indicating if the specified 's phone number has been confirmed.. + /// + /// The user whose telephone number confirmation status should be set. + /// A flag indicating whether the user's telephone number has been confirmed. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetPhoneNumberConfirmedAsync([NotNull] IdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.PhoneNumberConfirmed = confirmed; + + return Task.CompletedTask; + } + + /// + /// Sets the provided security for the specified . + /// + /// The user whose security stamp should be set. + /// The security stamp to set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetSecurityStampAsync([NotNull] IdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.SecurityStamp = stamp; + + return Task.CompletedTask; + } + + /// + /// Get the security stamp for the specified . + /// + /// The user whose security stamp should be set. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation, containing the security stamp for the specified . + public virtual Task GetSecurityStampAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.SecurityStamp); + } + + /// + /// Sets a flag indicating whether the specified has two factor authentication enabled or not, + /// as an asynchronous operation. + /// + /// The user whose two factor authentication enabled status should be set. + /// A flag indicating whether the specified has two factor authentication enabled. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetTwoFactorEnabledAsync([NotNull] IdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.TwoFactorEnabled = enabled; + + return Task.CompletedTask; + } + + /// + /// Returns a flag indicating whether the specified has two factor authentication enabled or not, + /// as an asynchronous operation. + /// + /// The user whose two factor authentication enabled status should be set. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The that represents the asynchronous operation, containing a flag indicating whether the specified + /// has two factor authentication enabled or not. + /// + public virtual Task GetTwoFactorEnabledAsync([NotNull] IdentityUser user, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.TwoFactorEnabled); + } + + /// + /// Retrieves all users with the specified claim. + /// + /// The claim whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that contain the specified claim. + /// + public virtual Task> GetUsersForClaimAsync([NotNull] Claim claim, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(claim, nameof(claim)); + + return _userRepository.GetListByClaimAsync(claim, cancellationToken); + } + + /// + /// Retrieves all users in the specified role. + /// + /// The role whose users should be retrieved. + /// The used to propagate notifications that the operation should be canceled. + /// + /// The contains a list of users, if any, that are in the specified role. + /// + public virtual Task> GetUsersInRoleAsync([NotNull] string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (string.IsNullOrEmpty(normalizedRoleName)) + { + throw new ArgumentNullException(nameof(normalizedRoleName)); + } + + return _userRepository.GetListByNormalizedRoleNameAsync(normalizedRoleName, cancellationToken); + } + + /// + /// Sets the token value for a particular user. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The value of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public virtual Task SetTokenAsync([NotNull] IdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.SetToken(loginProvider, name, value); + + return Task.CompletedTask; + } + + /// + /// Deletes a token for a user. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + user.RemoveToken(loginProvider, name); + + return Task.CompletedTask; + } + + /// + /// Returns the token value. + /// + /// The user. + /// The authentication provider for the token. + /// The name of the token. + /// The used to propagate notifications that the operation should be canceled. + /// The that represents the asynchronous operation. + public Task GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + Check.NotNull(user, nameof(user)); + + return Task.FromResult(user.FindToken(loginProvider, name)?.Value); + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/Domain/Entities/IHasConcurrencyStamp.cs b/src/Volo.Abp/Volo/Abp/Domain/Entities/IHasConcurrencyStamp.cs new file mode 100644 index 0000000000..94c41bc687 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Domain/Entities/IHasConcurrencyStamp.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Domain.Entities +{ + //TODO: Think a better naming? + public interface IHasConcurrencyStamp + { + string ConcurrencyStamp { get; set; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Domain/Repositories/IQueryableRepository.cs b/src/Volo.Abp/Volo/Abp/Domain/Repositories/IQueryableRepository.cs index d3450fd690..b54c8eda3c 100644 --- a/src/Volo.Abp/Volo/Abp/Domain/Repositories/IQueryableRepository.cs +++ b/src/Volo.Abp/Volo/Abp/Domain/Repositories/IQueryableRepository.cs @@ -7,6 +7,12 @@ using Volo.Abp.Domain.Entities; namespace Volo.Abp.Domain.Repositories { + public interface IQueryableRepository : IQueryableRepository + where TEntity : class, IEntity + { + + } + public interface IQueryableRepository : IRepository, IQueryable where TEntity : class, IEntity { diff --git a/src/Volo.Abp/Volo/Abp/Domain/Repositories/IRepository.cs b/src/Volo.Abp/Volo/Abp/Domain/Repositories/IRepository.cs index 5555355822..e9219c78dc 100644 --- a/src/Volo.Abp/Volo/Abp/Domain/Repositories/IRepository.cs +++ b/src/Volo.Abp/Volo/Abp/Domain/Repositories/IRepository.cs @@ -11,6 +11,15 @@ namespace Volo.Abp.Domain.Repositories } + public interface IRepository : IRepository + where TEntity : class, IEntity + { + + } + + //TODO: Add overloads with cancellationToken to appropriate methods? + //TODO: Add overloads for Find/Get/Delete to get multiple PK? + public interface IRepository : IRepository where TEntity : class, IEntity { @@ -30,6 +39,7 @@ namespace Volo.Abp.Domain.Repositories /// /// Gets an entity with given primary key. + /// Throws if can not find an entity with given id. /// /// Primary key of the entity to get /// Entity @@ -38,6 +48,7 @@ namespace Volo.Abp.Domain.Repositories /// /// Gets an entity with given primary key. + /// Throws if can not find an entity with given id. /// /// Primary key of the entity to get /// Entity diff --git a/src/Volo.Abp/Volo/Abp/Domain/Repositories/QueryableRepositoryBase.cs b/src/Volo.Abp/Volo/Abp/Domain/Repositories/QueryableRepositoryBase.cs index 74d77c7d05..dd2e1f6416 100644 --- a/src/Volo.Abp/Volo/Abp/Domain/Repositories/QueryableRepositoryBase.cs +++ b/src/Volo.Abp/Volo/Abp/Domain/Repositories/QueryableRepositoryBase.cs @@ -8,6 +8,12 @@ using Volo.Abp.Domain.Entities; namespace Volo.Abp.Domain.Repositories { + public abstract class QueryableRepositoryBase : QueryableRepositoryBase, IQueryableRepository + where TEntity : class, IEntity + { + + } + public abstract class QueryableRepositoryBase : RepositoryBase, IQueryableRepository where TEntity : class, IEntity { diff --git a/src/Volo.Abp/Volo/Abp/Domain/Repositories/RepositoryBase.cs b/src/Volo.Abp/Volo/Abp/Domain/Repositories/RepositoryBase.cs index a35a7ce179..08ce710839 100644 --- a/src/Volo.Abp/Volo/Abp/Domain/Repositories/RepositoryBase.cs +++ b/src/Volo.Abp/Volo/Abp/Domain/Repositories/RepositoryBase.cs @@ -6,6 +6,12 @@ using Volo.Abp.Domain.Entities; namespace Volo.Abp.Domain.Repositories { + public abstract class RepositoryBase : RepositoryBase, IRepository + where TEntity : class, IEntity + { + + } + public abstract class RepositoryBase : IRepository where TEntity : class, IEntity { diff --git a/src/Volo.Abp/Volo/Abp/Linq/DefaultAsyncQueryableExecuter.cs b/src/Volo.Abp/Volo/Abp/Linq/DefaultAsyncQueryableExecuter.cs new file mode 100644 index 0000000000..7346c07c7a --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Linq/DefaultAsyncQueryableExecuter.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.DependencyInjection; + +namespace Volo.Abp.Linq +{ + //TODO: DefaultAsyncQueryableExecuter should be able to work with multiple Executer, each will try to execute it! + //TODO: Implement with EF Core as first executer implementation! + public class DefaultAsyncQueryableExecuter : IAsyncQueryableExecuter, ITransientDependency + { + public Task CountAsync(IQueryable queryable) + { + return Task.FromResult(queryable.Count()); + } + + public Task> ToListAsync(IQueryable queryable) + { + return Task.FromResult(queryable.ToList()); + } + + public Task FirstOrDefaultAsync(IQueryable queryable) + { + return Task.FromResult(queryable.FirstOrDefault()); + } + + public Task FirstOrDefaultAsync(IQueryable queryable, CancellationToken cancellationToken) + { + return Task.FromResult(queryable.FirstOrDefault()); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Linq/IAsyncQueryableExecuter.cs b/src/Volo.Abp/Volo/Abp/Linq/IAsyncQueryableExecuter.cs new file mode 100644 index 0000000000..f2c34fd2ad --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Linq/IAsyncQueryableExecuter.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.Linq +{ + /// + /// This interface is intended to be used by ABP. + /// + public interface IAsyncQueryableExecuter + { + Task CountAsync(IQueryable queryable); + + Task> ToListAsync(IQueryable queryable); + + Task FirstOrDefaultAsync(IQueryable queryable); + + Task FirstOrDefaultAsync(IQueryable queryable, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Modularity/AbpModuleDescriptor.cs b/src/Volo.Abp/Volo/Abp/Modularity/AbpModuleDescriptor.cs index 0bf04605a0..635ee359d1 100644 --- a/src/Volo.Abp/Volo/Abp/Modularity/AbpModuleDescriptor.cs +++ b/src/Volo.Abp/Volo/Abp/Modularity/AbpModuleDescriptor.cs @@ -28,5 +28,10 @@ namespace Volo.Abp.Modularity Dependencies = new List(); } + + public override string ToString() + { + return $"[AbpModuleDescriptor {Type.FullName}]"; + } } } diff --git a/src/Volo.Abp/Volo/Abp/Uow/AbpDbConcurrencyException.cs b/src/Volo.Abp/Volo/Abp/Uow/AbpDbConcurrencyException.cs new file mode 100644 index 0000000000..0c026c4361 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/AbpDbConcurrencyException.cs @@ -0,0 +1,36 @@ +using System; + +namespace Volo.Abp.Uow +{ + public class AbpDbConcurrencyException : AbpException + { + /// + /// Creates a new object. + /// + public AbpDbConcurrencyException() + { + + } + + /// + /// Creates a new object. + /// + /// Exception message + public AbpDbConcurrencyException(string message) + : base(message) + { + + } + + /// + /// Creates a new object. + /// + /// Exception message + /// Inner exception + public AbpDbConcurrencyException(string message, Exception innerException) + : base(message, innerException) + { + + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs index c1158f5abe..6ad8706710 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -22,6 +23,11 @@ namespace Volo.Abp.Uow return _parent.SaveChangesAsync(); } + public Task SaveChangesAsync(CancellationToken cancellationToken) + { + return _parent.SaveChangesAsync(cancellationToken); + } + public Task CompleteAsync() { return Task.CompletedTask; diff --git a/src/Volo.Abp/Volo/Abp/Uow/IDatabaseApi.cs b/src/Volo.Abp/Volo/Abp/Uow/IDatabaseApi.cs index c30b8d3195..1b65185832 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IDatabaseApi.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IDatabaseApi.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; namespace Volo.Abp.Uow { @@ -6,6 +7,8 @@ namespace Volo.Abp.Uow { Task SaveChangesAsync(); - Task CommitAsync(); + Task SaveChangesAsync(CancellationToken cancellationToken); + + Task CommitAsync(); //TODO: Add CancellationToken to CommitAsync? } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs index c59bbe858c..9b446fc3c0 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Volo.Abp.DependencyInjection; @@ -18,6 +19,8 @@ namespace Volo.Abp.Uow Task SaveChangesAsync(); + Task SaveChangesAsync(CancellationToken cancellationToken); + Task CompleteAsync(); } } diff --git a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkManager.cs b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkManager.cs index 6ec2d6911f..a49c960021 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkManager.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkManager.cs @@ -1,9 +1,13 @@ -namespace Volo.Abp.Uow +using JetBrains.Annotations; + +namespace Volo.Abp.Uow { public interface IUnitOfWorkManager { + [CanBeNull] IUnitOfWork Current { get; } + [NotNull] IUnitOfWork Begin(); } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs index 51dce746f3..a9169eab12 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Volo.ExtensionMethods.Collections.Generic; @@ -31,6 +32,14 @@ namespace Volo.Abp.Uow } } + public async Task SaveChangesAsync(CancellationToken cancellationToken) + { + foreach (var databaseApi in _databaseApis.Values) + { + await databaseApi.SaveChangesAsync(cancellationToken); + } + } + public async Task CompleteAsync() { foreach (var databaseApi in _databaseApis.Values) diff --git a/src/Volo.ExtensionMethods/Volo/ExtensionMethods/Collections/Generic/CollectionExtensions.cs b/src/Volo.ExtensionMethods/Volo/ExtensionMethods/Collections/Generic/CollectionExtensions.cs index 4da8884fd8..db560d1b06 100644 --- a/src/Volo.ExtensionMethods/Volo/ExtensionMethods/Collections/Generic/CollectionExtensions.cs +++ b/src/Volo.ExtensionMethods/Volo/ExtensionMethods/Collections/Generic/CollectionExtensions.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; namespace Volo.ExtensionMethods.Collections.Generic @@ -35,5 +37,17 @@ namespace Volo.ExtensionMethods.Collections.Generic source.Add(item); return true; } + + public static IList RemoveAll([NotNull] this ICollection source, Func predicate) + { + var items = source.Where(predicate).ToList(); + + foreach (var item in items) + { + source.Remove(item); + } + + return items; + } } } \ No newline at end of file diff --git a/test/AbpDesk/AbpDesk.Application.Tests/project.json b/test/AbpDesk/AbpDesk.Application.Tests/project.json index 4ff1d373c2..746825c0c5 100644 --- a/test/AbpDesk/AbpDesk.Application.Tests/project.json +++ b/test/AbpDesk/AbpDesk.Application.Tests/project.json @@ -7,8 +7,7 @@ "AbpDesk.Application": "1.0.0-*", "AbpDesk.EntityFrameworkCore": "1.0.0-*", "AbpTestBase": "1.0.0-*", - "Microsoft.EntityFrameworkCore.InMemory": "1.1.0", - "Volo.Abp.TestBase": "1.0.0-*" + "Microsoft.EntityFrameworkCore.InMemory": "1.1.0" }, "frameworks": { diff --git a/test/Volo.Abp.Identity.Tests/Properties/AssemblyInfo.cs b/test/Volo.Abp.Identity.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ecfa3fa16d --- /dev/null +++ b/test/Volo.Abp.Identity.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Volo.Abp.Identity.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4ab91077-82dc-4335-9274-bce017bd9c8b")] diff --git a/test/Volo.Abp.Identity.Tests/Volo.Abp.Identity.Tests.xproj b/test/Volo.Abp.Identity.Tests/Volo.Abp.Identity.Tests.xproj new file mode 100644 index 0000000000..22b8879caa --- /dev/null +++ b/test/Volo.Abp.Identity.Tests/Volo.Abp.Identity.Tests.xproj @@ -0,0 +1,23 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 4ab91077-82dc-4335-9274-bce017bd9c8b + + + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Volo.Abp.Identity.Tests/Volo/Abp/Identity/AbpIdentityTestModule.cs b/test/Volo.Abp.Identity.Tests/Volo/Abp/Identity/AbpIdentityTestModule.cs new file mode 100644 index 0000000000..ecf749f279 --- /dev/null +++ b/test/Volo.Abp.Identity.Tests/Volo/Abp/Identity/AbpIdentityTestModule.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Identity +{ + [DependsOn(typeof(AbpIdentityModule))] + public class AbpIdentityTestModule : AbpModule + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddEntityFrameworkInMemoryDatabase(); + + services.Configure(options => + { + options.ConnectionStrings.Default = Guid.NewGuid().ToString(); + }); + + services.Configure(options => + { + options.Configure(context => + { + context.DbContextOptions.UseInMemoryDatabase(context.ConnectionString); + }); + }); + } + } +} diff --git a/test/Volo.Abp.Identity.Tests/Volo/Abp/Identity/Initialize_Tests.cs b/test/Volo.Abp.Identity.Tests/Volo/Abp/Identity/Initialize_Tests.cs new file mode 100644 index 0000000000..30289e669c --- /dev/null +++ b/test/Volo.Abp.Identity.Tests/Volo/Abp/Identity/Initialize_Tests.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.TestBase; +using Xunit; + +namespace Volo.Abp.Identity +{ + public class Initialize_Tests : AbpIntegratedTest + { + [Fact] + public void Should_Initialize_Identity_Module() + { + var userManager = ServiceProvider.GetRequiredService(); + } + } +} diff --git a/test/Volo.Abp.Identity.Tests/project.json b/test/Volo.Abp.Identity.Tests/project.json new file mode 100644 index 0000000000..564aa89a19 --- /dev/null +++ b/test/Volo.Abp.Identity.Tests/project.json @@ -0,0 +1,23 @@ +{ + "version": "1.0.0-*", + + "testRunner": "xunit", + + "dependencies": { + "AbpTestBase": "1.0.0-*", + "Microsoft.EntityFrameworkCore.InMemory": "1.1.0", + "Volo.Abp.Identity": "1.0.0-*", + "Volo.Abp.Identity.EntityFrameworkCore": "1.0.0-*" + }, + + "frameworks": { + "netcoreapp1.1": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.1.0" + } + } + } + } +}