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 0d3d63f92b..2b41e3874b 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 @@ -20,7 +20,10 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore } } - public class EfCoreRepository : QueryableRepositoryBase, IEfCoreRepository + public class EfCoreRepository : QueryableRepositoryBase, + IEfCoreRepository, + ISupportsExplicitLoading + where TDbContext : AbpDbContext where TEntity : class, IEntity { @@ -117,5 +120,23 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore { return GetQueryable().LongCountAsync(cancellationToken); } + + public virtual Task EnsureCollectionLoadedAsync( + TEntity entity, + Expression>> propertyExpression, + CancellationToken cancellationToken) + where TProperty : class + { + return DbContext.Entry(entity).Collection(propertyExpression).LoadAsync(cancellationToken); + } + + public virtual Task EnsurePropertyLoadedAsync( + TEntity entity, + Expression> propertyExpression, + CancellationToken cancellationToken) + where TProperty : class + { + return DbContext.Entry(entity).Reference(propertyExpression).LoadAsync(cancellationToken); + } } } diff --git a/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserAppService.cs b/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserAppService.cs index 1af6e86581..ea61572342 100644 --- a/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserAppService.cs +++ b/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityUserAppService.cs @@ -8,5 +8,7 @@ namespace Volo.Abp.Identity public interface IIdentityUserAppService : IAsyncCrudAppService { Task> GetRolesAsync(Guid id); + + Task UpdateRolesAsync(Guid id, UpdateIdentityUserRolesDto input); } } diff --git a/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDto.cs b/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs similarity index 89% rename from src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDto.cs rename to src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs index ba61f3ec8f..1e316bf838 100644 --- a/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDto.cs +++ b/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IdentityUserCreateOrUpdateDtoBase.cs @@ -15,6 +15,6 @@ namespace Volo.Abp.Identity public bool LockoutEnabled { get; set; } //TODO: Optional? [CanBeNull] - public string[] Roles { get; set; } + public string[] RoleNames { get; set; } } } \ No newline at end of file diff --git a/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/UpdateIdentityUserRolesDto.cs b/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/UpdateIdentityUserRolesDto.cs new file mode 100644 index 0000000000..772d6f44e4 --- /dev/null +++ b/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/UpdateIdentityUserRolesDto.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Volo.Abp.Identity +{ + public class UpdateIdentityUserRolesDto + { + [Required] + public string[] RoleNames { get; set; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs b/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs index 548f47bf24..6035678f20 100644 --- a/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs +++ b/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs @@ -75,6 +75,12 @@ namespace Volo.Abp.Identity ); } + public async Task UpdateRolesAsync(Guid id, UpdateIdentityUserRolesDto input) + { + var user = await _userManager.GetByIdAsync(id); + await _userManager.SetRolesAsync(user, input.RoleNames); + } + private async Task UpdateUserByInput(IdentityUser user, IdentityUserCreateOrUpdateDtoBase input) { await _userManager.SetEmailAsync(user, input.Email); @@ -82,9 +88,9 @@ namespace Volo.Abp.Identity await _userManager.SetTwoFactorEnabledAsync(user, input.TwoFactorEnabled); await _userManager.SetLockoutEnabledAsync(user, input.LockoutEnabled); - if (input.Roles != null) + if (input.RoleNames != null) { - await _userManager.SetRolesAsync(user, input.Roles); + await _userManager.SetRolesAsync(user, input.RoleNames); } } } diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUser.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUser.cs index 2c50e5283d..f749eb06bb 100644 --- a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUser.cs +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUser.cs @@ -131,6 +131,7 @@ namespace Volo.Abp.Identity UserName = userName; NormalizedUserName = userName.ToUpperInvariant(); ConcurrencyStamp = Guid.NewGuid().ToString(); + SecurityStamp = Guid.NewGuid().ToString(); Roles = new Collection(); Claims = new Collection(); diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserManager.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserManager.cs index d94b104a79..92cee54e1b 100644 --- a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserManager.cs +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserManager.cs @@ -47,15 +47,15 @@ namespace Volo.Abp.Identity return user; } - public async Task SetRolesAsync([NotNull] IdentityUser user, [NotNull] string[] roleNames) + public async Task SetRolesAsync([NotNull] IdentityUser user, [NotNull] IEnumerable roleNames) { Check.NotNull(user, nameof(user)); Check.NotNull(roleNames, nameof(roleNames)); - + var currentRoleNames = await GetRolesAsync(user); - await RemoveFromRolesAsync(user, currentRoleNames.Except(roleNames)); - await AddToRolesAsync(user, roleNames.Except(currentRoleNames)); + await RemoveFromRolesAsync(user, currentRoleNames.Except(roleNames).Distinct()); + await AddToRolesAsync(user, roleNames.Except(currentRoleNames).Distinct()); } } } diff --git a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserStore.cs b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserStore.cs index 63c3f4370a..2cc48a9c3f 100644 --- a/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserStore.cs +++ b/src/Volo.Abp.Identity/Volo/Abp/Identity/IdentityUserStore.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; using Volo.Abp.Guids; using Volo.Abp.Uow; @@ -332,6 +333,8 @@ namespace Volo.Abp.Identity throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Role {0} does not exist!", normalizedRoleName)); } + await _userRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); + user.AddRole(_guidGenerator, role.Id); } @@ -359,6 +362,8 @@ namespace Volo.Abp.Identity return; } + await _userRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); + user.RemoveRole(role.Id); } @@ -402,15 +407,9 @@ namespace Volo.Abp.Identity return false; } - return user.IsInRole(role.Id); - } + await _userRepository.EnsureCollectionLoadedAsync(user, u => u.Roles, cancellationToken); - /// - /// Dispose the store - /// - public void Dispose() - { - + return user.IsInRole(role.Id); } /// @@ -1037,5 +1036,10 @@ namespace Volo.Abp.Identity return Task.FromResult(user.FindToken(loginProvider, name)?.Value); } + + public void Dispose() + { + + } } } diff --git a/src/Volo.Abp/Volo/Abp/Domain/Repositories/ISupportsExplicitLoading.cs b/src/Volo.Abp/Volo/Abp/Domain/Repositories/ISupportsExplicitLoading.cs new file mode 100644 index 0000000000..9b4a98863c --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Domain/Repositories/ISupportsExplicitLoading.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.Domain.Repositories +{ + public interface ISupportsExplicitLoading + where TEntity : class, IEntity + { + Task EnsureCollectionLoadedAsync( + TEntity entity, + Expression>> propertyExpression, + CancellationToken cancellationToken) + where TProperty : class; + + Task EnsurePropertyLoadedAsync( + TEntity entity, + Expression> propertyExpression, + CancellationToken cancellationToken) + where TProperty : class; + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs b/src/Volo.Abp/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs new file mode 100644 index 0000000000..80a8acb0a0 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Domain.Entities; +using Volo.Abp.DynamicProxy; +using Volo.Abp.Threading; + +namespace Volo.Abp.Domain.Repositories +{ + public static class RepositoryExtensions + { + public static async Task EnsureCollectionLoadedAsync( + this IRepository repository, + TEntity entity, + Expression>> propertyExpression, + CancellationToken cancellationToken = default(CancellationToken) + ) + where TEntity : class, IEntity + where TProperty : class + { + var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading; + if (repo != null) + { + await repo.EnsureCollectionLoadedAsync(entity, propertyExpression, cancellationToken); + } + } + + public static void EnsureCollectionLoaded( + this IRepository repository, + TEntity entity, + Expression>> propertyExpression + ) + where TEntity : class, IEntity + where TProperty : class + { + AsyncHelper.RunSync(() => repository.EnsureCollectionLoadedAsync(entity, propertyExpression)); + } + + public static async Task EnsurePropertyLoadedAsync( + this IRepository repository, + TEntity entity, + Expression> propertyExpression, + CancellationToken cancellationToken = default(CancellationToken) + ) + where TEntity : class, IEntity + where TProperty : class + { + var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading; + if (repo != null) + { + await repo.EnsurePropertyLoadedAsync(entity, propertyExpression, cancellationToken); + } + } + + public static void EnsurePropertyLoaded( + this IRepository repository, + TEntity entity, + Expression> propertyExpression + ) + where TEntity : class, IEntity + where TProperty : class + { + AsyncHelper.RunSync(() => repository.EnsurePropertyLoadedAsync(entity, propertyExpression)); + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/DynamicProxy/ProxyHelper.cs b/src/Volo.Abp/Volo/Abp/DynamicProxy/ProxyHelper.cs new file mode 100644 index 0000000000..06a2ae264b --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/DynamicProxy/ProxyHelper.cs @@ -0,0 +1,32 @@ +using System.Linq; +using System.Reflection; + +namespace Volo.Abp.DynamicProxy +{ + public static class ProxyHelper + { + /// + /// Returns dynamic proxy target object if this is a proxied object, otherwise returns the given object. + /// + public static object UnProxy(object obj) + { + //TODO: This code depends on Castle, so we should find a better way. + + if (obj.GetType().Namespace != "Castle.Proxies") + { + return obj; + } + + var targetField = obj.GetType().GetTypeInfo() + .GetFields() + .FirstOrDefault(f => f.Name == "__target"); + + if (targetField == null) + { + return obj; + } + + return targetField.GetValue(obj); + } + } +} diff --git a/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs b/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs index 0ef2ad4524..931fdf3f87 100644 --- a/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs +++ b/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/AbpIdentityTestDataBuilder.cs @@ -44,6 +44,7 @@ namespace Volo.Abp.Identity { var john = new IdentityUser(_guidGenerator.Create(), "john.nash"); john.Roles.Add(new IdentityUserRole(_guidGenerator.Create(), john.Id, _moderator.Id)); + john.Roles.Add(new IdentityUserRole(_guidGenerator.Create(), john.Id, _supporterRole.Id)); _userRepository.Insert(john); } } diff --git a/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs b/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs index 9c891bf859..f69028e5f4 100644 --- a/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs +++ b/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityUserAppService_Tests.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.Application.Dtos; -using Volo.Abp.Domain.Repositories; using Xunit; namespace Volo.Abp.Identity @@ -12,12 +11,12 @@ namespace Volo.Abp.Identity public class IdentityUserAppService_Tests : AbpIdentityApplicationTestBase { private readonly IIdentityUserAppService _identityUserAppService; - private readonly IRepository _userRepository; + private readonly IIdentityUserRepository _userRepository; public IdentityUserAppService_Tests() { _identityUserAppService = ServiceProvider.GetRequiredService(); - _userRepository = ServiceProvider.GetRequiredService>(); + _userRepository = ServiceProvider.GetRequiredService(); } [Fact] @@ -65,7 +64,7 @@ namespace Volo.Abp.Identity LockoutEnabled = true, PhoneNumber = CreateRandomPhoneNumber(), Password = "123qwe", - Roles = new[] {"moderator"} + RoleNames = new[] { "moderator" } }; //Act @@ -153,8 +152,34 @@ namespace Volo.Abp.Identity //Assert - result.Items.Count.ShouldBe(1); - result.Items[0].Name.ShouldBe("moderator"); + result.Items.Count.ShouldBe(2); + result.Items.ShouldContain(r => r.Name == "moderator"); + result.Items.ShouldContain(r => r.Name == "supporter"); + } + + [Fact] + public async Task UpdateRolesAsync() + { + //Arrange + + var johnNash = await GetUserAsync("john.nash"); + + //Act + + await _identityUserAppService.UpdateRolesAsync( + johnNash.Id, + new UpdateIdentityUserRolesDto + { + RoleNames = new[] {"moderator", "admin"} + } + ); + + //Assert + + var roleNames = await _userRepository.GetRoleNamesAsync(johnNash.Id); + roleNames.Count.ShouldBe(2); + roleNames.ShouldContain("admin"); + roleNames.ShouldContain("moderator"); } private async Task GetUserAsync(string userName)