diff --git a/appveyor.yml b/appveyor.yml index 3db28fd..e842829 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,8 +14,8 @@ image: environment: GH_TOKEN: secure: u7qaOQsrkLqq44yS24C0eM2vRCzp1A8gZTWNmlA58TIDJGmrDXguHL9H/vww7Fg/ - donetsdk: 3.1.406 - donetsdk5: 5.0.102 + donetsdk3: 3.1.406 + donetsdk: 5.0.200 JAVA_HOME: C:\Program Files\Java\jdk14 init: - cmd: git config --global core.autocrlf true @@ -26,12 +26,11 @@ install: - sh: sudo apt-get -y install apt-transport-https - sh: sudo apt-get update - sh: sudo chmod +x ./dotnet-install.sh + - sh: sudo ./dotnet-install.sh -Channel Current -Version $donetsdk3 -InstallDir ./dotnetsdk -NoPath - sh: sudo ./dotnet-install.sh -Channel Current -Version $donetsdk -InstallDir ./dotnetsdk -NoPath - - sh: sudo ./dotnet-install.sh -Channel Current -Version $donetsdk5 -InstallDir ./dotnetsdk -NoPath - sh: export PATH=/home/appveyor/projects/identity-ravendb/dotnetsdk:$PATH - sh: sudo apt -y install nuget - ps: if ($isWindows) { .\dotnet-install.ps1 -Version $env:donetsdk } - - ps: if ($isWindows) { .\dotnet-install.ps1 -Version $env:donetsdk5 } - ps: dotnet tool install --global GitVersion.Tool - ps: dotnet gitversion /l console /output buildserver - ps: dotnet tool install --global dotnet-sonarscanner diff --git a/samples/IdentitySample/IdentitySample.csproj b/samples/IdentitySample/IdentitySample.csproj index 72f4be8..ca201b5 100644 --- a/samples/IdentitySample/IdentitySample.csproj +++ b/samples/IdentitySample/IdentitySample.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Aguacongas.Identity.RavenDb/Aguacongas.Identity.RavenDb.csproj b/src/Aguacongas.Identity.RavenDb/Aguacongas.Identity.RavenDb.csproj index 19e2653..715050c 100644 --- a/src/Aguacongas.Identity.RavenDb/Aguacongas.Identity.RavenDb.csproj +++ b/src/Aguacongas.Identity.RavenDb/Aguacongas.Identity.RavenDb.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net462 @@ -25,7 +25,7 @@ - - + + diff --git a/src/Aguacongas.Identity.RavenDb/DocumentStoreExtension.cs b/src/Aguacongas.Identity.RavenDb/DocumentStoreExtension.cs new file mode 100644 index 0000000..3a4011d --- /dev/null +++ b/src/Aguacongas.Identity.RavenDb/DocumentStoreExtension.cs @@ -0,0 +1,93 @@ +using Aguacongas.Identity.RavenDb; +using Microsoft.AspNetCore.Identity; +using System; +using System.Reflection; + +namespace Raven.Client.Documents +{ + public static class DocumentStoreExtension + { + public static IDocumentStore SetFindIdentityPropertyForIdentityModel(this IDocumentStore store) + { + var findId = store.Conventions.FindIdentityProperty; + store.Conventions.FindIdentityProperty = memberInfo => SetConventions(memberInfo, findId); + return store; + } + + private static bool SetConventions(MemberInfo memberInfo, Func findId) + { + if (memberInfo.DeclaringType == typeof(UserData)) + { + return false; + } + if (memberInfo.DeclaringType == typeof(RoleData)) + { + return false; + } + if (IsSubclassOf(memberInfo.DeclaringType, typeof(IdentityUser<>))) + { + return false; + } + if (IsSubclassOf(memberInfo.DeclaringType, typeof(IdentityUserClaim<>))) + { + return false; + } + if (IsSubclassOf(memberInfo.DeclaringType, typeof(IdentityUserRole<>))) + { + return false; + } + if (IsSubclassOf(memberInfo.DeclaringType, typeof(IdentityUserLogin<>))) + { + return false; + } + if (IsSubclassOf(memberInfo.DeclaringType, typeof(IdentityUserToken<>))) + { + return false; + } + if (IsSubclassOf(memberInfo.DeclaringType, typeof(IdentityRole<>))) + { + return false; + } + if (IsSubclassOf(memberInfo.DeclaringType, typeof(IdentityRoleClaim<>))) + { + return false; + } + + return findId(memberInfo); + } + + private static bool IsSubclassOf(Type type, Type baseType) + { + if (type == null || baseType == null || type == baseType) + { + return false; + } + + if (!baseType.IsGenericType) + { + if (!type.IsGenericType) + { + return type.IsSubclassOf(baseType); + } + } + else + { + baseType = baseType.GetGenericTypeDefinition(); + } + + var objectType = typeof(object); + while (type != objectType && type != null) + { + var curentType = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + if (curentType == baseType) + { + return true; + } + + type = type.BaseType; + } + + return false; + } + } +} diff --git a/src/Aguacongas.Identity.RavenDb/IdentityBuilderExtensions.cs b/src/Aguacongas.Identity.RavenDb/IdentityBuilderExtensions.cs index c2f418d..da397c1 100644 --- a/src/Aguacongas.Identity.RavenDb/IdentityBuilderExtensions.cs +++ b/src/Aguacongas.Identity.RavenDb/IdentityBuilderExtensions.cs @@ -3,8 +3,6 @@ using Aguacongas.Identity.RavenDb; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Raven.Client.Documents; using Raven.Client.Documents.Session; using System; diff --git a/src/Aguacongas.Identity.RavenDb/Models/RoleData.cs b/src/Aguacongas.Identity.RavenDb/Models/RoleData.cs index 03dd56e..e1d1d54 100644 --- a/src/Aguacongas.Identity.RavenDb/Models/RoleData.cs +++ b/src/Aguacongas.Identity.RavenDb/Models/RoleData.cs @@ -1,22 +1,15 @@ // Project: Aguafrommars/Identity.RavenDb // Copyright (c) 2021 Olivier Lefebvre -using Microsoft.AspNetCore.Identity; -using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Aguacongas.Identity.RavenDb { - [SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", Justification = "All are needed")] - public class RoleData - where TKey: IEquatable - where TRole : IdentityRole - where TRoleClaims: IdentityRoleClaim + public class RoleData { public string Id { get; set; } - public virtual TRole Role { get; set; } + public string RoleId { get; set; } - public virtual List Claims { get; private set; } = new List(); + public List ClaimIds { get; private set; } = new List(); } } diff --git a/src/Aguacongas.Identity.RavenDb/Models/UserData.cs b/src/Aguacongas.Identity.RavenDb/Models/UserData.cs index fd773fd..a10383e 100644 --- a/src/Aguacongas.Identity.RavenDb/Models/UserData.cs +++ b/src/Aguacongas.Identity.RavenDb/Models/UserData.cs @@ -1,25 +1,17 @@ // Project: Aguafrommars/Identity.RavenDb // Copyright (c) 2021 Olivier Lefebvre -using Microsoft.AspNetCore.Identity; -using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; namespace Aguacongas.Identity.RavenDb { - [SuppressMessage("Major Code Smell", "S2436:Types and methods should not have too many generic parameters", Justification = "All are needed")] - public class UserData - where TKey: IEquatable - where TUser: IdentityUser - where TUserClaim : IdentityUserClaim - where TUserLogin : IdentityUserLogin + public class UserData { public string Id { get; set; } - public virtual TUser User { get; set; } + public string UserId { get; set; } - public virtual List Claims { get; private set; } = new List(); + public List ClaimIds { get; private set; } = new List(); - public virtual List Logins { get; private set; } = new List(); + public List LoginIds { get; private set; } = new List(); } } diff --git a/src/Aguacongas.Identity.RavenDb/Models/UserLoginIndex.cs b/src/Aguacongas.Identity.RavenDb/Models/UserLoginIndex.cs deleted file mode 100644 index c752d20..0000000 --- a/src/Aguacongas.Identity.RavenDb/Models/UserLoginIndex.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Aguacongas.Identity.RavenDb -{ - public class UserLoginIndex - { - public string Id { get; set; } - - public string UserId { get; set; } - - public string LoginProvider { get; set; } - - public string ProviderKey { get; set; } - } -} diff --git a/src/Aguacongas.Identity.RavenDb/RoleStore.cs b/src/Aguacongas.Identity.RavenDb/RoleStore.cs index d3dbc5d..eedbd91 100644 --- a/src/Aguacongas.Identity.RavenDb/RoleStore.cs +++ b/src/Aguacongas.Identity.RavenDb/RoleStore.cs @@ -71,8 +71,7 @@ public class RoleStore : /// /// A navigation property for the roles the store contains. /// - public IQueryable Roles => _session.Query>() - .Select(d => d.Role) + public IQueryable Roles => _session.Query() .ToListAsync().ConfigureAwait(false).GetAwaiter().GetResult().AsQueryable(); /// @@ -104,12 +103,14 @@ public async virtual Task CreateAsync(TRole role, CancellationTo AssertNotNull(role, nameof(role)); var roleId = ConvertIdToString(role.Id); - var data = new RoleData + var data = new RoleData { - Id = $"role/{roleId}", - Role = role + Id = $"roledata/{roleId}", + RoleId = $"role/{roleId}" }; - await _session.StoreAsync(data, cancellationToken).ConfigureAwait(false); + await _session.StoreAsync(role, data.RoleId, cancellationToken).ConfigureAwait(false); + await _session.StoreAsync(data, data.Id, cancellationToken).ConfigureAwait(false); + await _session.SaveChangesAsync(cancellationToken).ConfigureAwait(false); return IdentityResult.Success; @@ -128,8 +129,8 @@ public async virtual Task UpdateAsync(TRole role, CancellationTo AssertNotNull(role, nameof(role)); var roleId = ConvertIdToString(role.Id); - var data = await _session.LoadAsync>($"role/{roleId}").ConfigureAwait(false); - data.Role = role; + var existing = await _session.LoadAsync($"role/{roleId}").ConfigureAwait(false); + CloneEntity(existing, typeof(TRole), role); try { @@ -156,7 +157,13 @@ public async virtual Task DeleteAsync(TRole role, CancellationTo AssertNotNull(role, nameof(role)); var roleId = ConvertIdToString(role.Id); - _session.Delete($"role/{roleId}"); + + var data = await _session.LoadAsync($"roledata/{roleId}", cancellationToken).ConfigureAwait(false); + _session.Delete(data.RoleId); + foreach(var claimId in data.ClaimIds) + { + _session.Delete(claimId); + } _session.Delete($"rolename/{role.NormalizedName}"); try @@ -224,14 +231,12 @@ public virtual Task SetRoleNameAsync(TRole role, string roleName, CancellationTo /// The role ID to look for. /// The used to propagate notifications that the operation should be canceled. /// A that result of the look up. - public virtual async Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default) + public virtual Task FindByIdAsync(string roleId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - var data = await _session.LoadAsync>($"role/{roleId}", cancellationToken).ConfigureAwait(false); - - return data?.Role; + return _session.LoadAsync($"role/{roleId}", cancellationToken); } /// @@ -255,9 +260,7 @@ public virtual async Task FindByNameAsync(string normalizedRoleName, Canc return null; } - var data = await _session.LoadAsync>(index.RoleId, cancellationToken).ConfigureAwait(false); - - return data.Role; + return await _session.LoadAsync(index.RoleId, cancellationToken).ConfigureAwait(false); } /// @@ -335,7 +338,7 @@ public async virtual Task> GetClaimsAsync(TRole role, CancellationT ThrowIfDisposed(); AssertNotNull(role, nameof(role)); - var claimList = await GetRoleClaimsAsync(role).ConfigureAwait(false); + var claimList = await GetRoleClaimsAsync(role, cancellationToken).ConfigureAwait(false); return claimList .Select(c => c.ToClaim()) .ToList(); @@ -354,8 +357,13 @@ public virtual async Task AddClaimAsync(TRole role, Claim claim, CancellationTok AssertNotNull(role, nameof(role)); AssertNotNull(claim, nameof(claim)); - var roleClaims = await GetRoleClaimsAsync(role).ConfigureAwait(false); - roleClaims.Add(CreateRoleClaim(role, claim)); + var roleId = ConvertIdToString(role.Id); + var data = await _session.LoadAsync($"roledata/{roleId}", cancellationToken).ConfigureAwait(false); + var roleClaim = CreateRoleClaim(role, claim); + roleClaim.Id = data.ClaimIds.Count; + var claimId = $"roleclaim/{roleId}@{roleClaim.Id}"; + data.ClaimIds.Add(claimId); + await _session.StoreAsync(roleClaim, claimId, cancellationToken).ConfigureAwait(false); } /// @@ -371,8 +379,15 @@ public async virtual Task RemoveClaimAsync(TRole role, Claim claim, Cancellation AssertNotNull(role, nameof(role)); AssertNotNull(claim, nameof(claim)); - var roleClaims = await GetRoleClaimsAsync(role).ConfigureAwait(false); - roleClaims.RemoveAll(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value); + var roleId = ConvertIdToString(role.Id); + var claimList = await GetRoleClaimsAsync(role, cancellationToken).ConfigureAwait(false); + var data = await _session.LoadAsync($"roledata/{roleId}", cancellationToken).ConfigureAwait(false); + foreach(var roleClain in claimList.Where(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value)) + { + var claimId = $"roleclaim/{roleId}@{roleClain.Id}"; + _session.Delete(claimId); + data.ClaimIds.Remove(claimId); + } } /// @@ -412,11 +427,16 @@ public virtual string ConvertIdToString(TKey id) return id.ToString(); } - protected virtual async Task> GetRoleClaimsAsync(TRole role) + protected virtual async Task> GetRoleClaimsAsync(TRole role, CancellationToken cancellationToken = default) { var roleId = ConvertIdToString(role.Id); - var data = await _session.LoadAsync>($"role/{roleId}").ConfigureAwait(false); - return data.Claims; + var data = await _session.LoadAsync($"roledata/{roleId}", builder => builder.IncludeDocuments(d => d.ClaimIds), cancellationToken).ConfigureAwait(false); + var list = new List(data.ClaimIds.Count); + foreach(var id in data.ClaimIds) + { + list.Add(await _session.LoadAsync(id).ConfigureAwait(false)); + } + return list; } private static void AssertNotNull(object p, string pName) @@ -426,5 +446,13 @@ private static void AssertNotNull(object p, string pName) throw new ArgumentNullException(pName); } } + + private static void CloneEntity(object entity, Type type, object loaded) + { + foreach (var property in type.GetProperties()) + { + property.SetValue(entity, property.GetValue(loaded)); + } + } } } diff --git a/src/Aguacongas.Identity.RavenDb/Stores/UserStoreBase.cs b/src/Aguacongas.Identity.RavenDb/Stores/UserStoreBase.cs index 9814f16..d9b57d6 100644 --- a/src/Aguacongas.Identity.RavenDb/Stores/UserStoreBase.cs +++ b/src/Aguacongas.Identity.RavenDb/Stores/UserStoreBase.cs @@ -802,24 +802,7 @@ public void Dispose() /// The user if it exists. protected abstract Task FindUserAsync(TKey userId, CancellationToken cancellationToken); - /// - /// Return a user login with the matching userId, provider, providerKey if it exists. - /// - /// The user's id. - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected abstract Task FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken); - - /// - /// Return a user login with provider, providerKey if it exists. - /// - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected abstract Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken); + /// /// Called to create a new instance of a . diff --git a/src/Aguacongas.Identity.RavenDb/UserOnlyStore.cs b/src/Aguacongas.Identity.RavenDb/UserOnlyStore.cs index 74ab8ab..bfebe2c 100644 --- a/src/Aguacongas.Identity.RavenDb/UserOnlyStore.cs +++ b/src/Aguacongas.Identity.RavenDb/UserOnlyStore.cs @@ -90,8 +90,7 @@ public class UserOnlyStore : /// A navigation property for the users the store contains. /// public override IQueryable Users - => _session.Query>() - .Select(d => d.User) + => _session.Query() .ToListAsync().ConfigureAwait(false).GetAwaiter().GetResult().AsQueryable(); /// @@ -118,12 +117,13 @@ public async override Task CreateAsync(TUser user, CancellationT var userId = ConvertIdToString(user.Id); - var data = new UserData + var data = new UserData { - Id = $"user/{userId}", - User = user + Id = $"userdata/{userId}", + UserId = $"user/{userId}" }; - await _session.StoreAsync(data, cancellationToken).ConfigureAwait(false); + await _session.StoreAsync(user, data.UserId, cancellationToken).ConfigureAwait(false); + await _session.StoreAsync(data, data.Id, cancellationToken).ConfigureAwait(false); await _session.SaveChangesAsync(cancellationToken).ConfigureAwait(false); return IdentityResult.Success; @@ -142,9 +142,8 @@ public async override Task UpdateAsync(TUser user, CancellationT AssertNotNull(user, nameof(user)); var userId = ConvertIdToString(user.Id); - var data = await _session.LoadAsync>($"user/{userId}", cancellationToken).ConfigureAwait(false); - - data.User = user; + var existing = await _session.LoadAsync($"user/{userId}", cancellationToken).ConfigureAwait(false); + CloneEntity(existing, typeof(TUser), user); try { @@ -171,7 +170,16 @@ public async override Task DeleteAsync(TUser user, CancellationT AssertNotNull(user, nameof(user)); var userId = ConvertIdToString(user.Id); - _session.Delete($"user/{userId}"); + var data = await _session.LoadAsync($"userdata/{userId}", cancellationToken).ConfigureAwait(false); + _session.Delete(data.UserId); + foreach(var id in data.ClaimIds) + { + _session.Delete(id); + } + foreach (var id in data.LoginIds) + { + _session.Delete(id); + } _session.Delete($"username/{user.NormalizedUserName}"); try @@ -265,13 +273,12 @@ public override async Task SetNormalizedEmailAsync(TUser user, string normalized /// /// The that represents the asynchronous operation, containing the user matching the specified if it exists. /// - public override async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) + public override Task FindByIdAsync(string userId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - var data = await _session.LoadAsync>($"user/{userId}", cancellationToken).ConfigureAwait(false); - return data?.User; + return _session.LoadAsync($"user/{userId}", cancellationToken); } /// @@ -295,8 +302,7 @@ public override async Task FindByNameAsync(string normalizedUserName, Can return null; } - var data = await _session.LoadAsync>(index.UserId, cancellationToken).ConfigureAwait(false); - return data.User; + return await _session.LoadAsync(index.UserId, cancellationToken).ConfigureAwait(false); } /// @@ -311,7 +317,19 @@ public async override Task> GetClaimsAsync(TUser user, Cancellation ThrowIfDisposed(); AssertNotNull(user, nameof(user)); - var claimList = await GetUserClaimsAsync(user, cancellationToken).ConfigureAwait(false); + var userId = ConvertIdToString(user.Id); + var data = await _session.LoadAsync($"userdata/{userId}", builder => builder.IncludeDocuments(d => d.ClaimIds), cancellationToken).ConfigureAwait(false); + if (data == null) + { + return null; + } + + var claimList = new List(data.ClaimIds.Count); + foreach(var id in data.ClaimIds) + { + claimList.Add(await _session.LoadAsync(id, cancellationToken).ConfigureAwait(false)); + } + return claimList .Select(c => c.ToClaim()) .ToList(); @@ -331,8 +349,15 @@ public override async Task AddClaimsAsync(TUser user, IEnumerable claims, AssertNotNull(user, nameof(user)); AssertNotNull(claims, nameof(claims)); - var claimList = await GetUserClaimsAsync(user, cancellationToken).ConfigureAwait(false); - claimList.AddRange(claims.Select(c => CreateUserClaim(user, c))); + var userId = ConvertIdToString(user.Id); + var data = await _session.LoadAsync($"userdata/{userId}", cancellationToken).ConfigureAwait(false); + var index = data.ClaimIds.Count; + foreach(var claim in claims) + { + var claimId = $"userclaim/{userId}@{index++}"; + data.ClaimIds.Add(claimId); + await _session.StoreAsync(CreateUserClaim(user, claim), claimId).ConfigureAwait(false); + } } /// @@ -351,13 +376,17 @@ public async override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newC AssertNotNull(claim, nameof(claim)); AssertNotNull(newClaim, nameof(newClaim)); - var claimList = await GetUserClaimsAsync(user, cancellationToken).ConfigureAwait(false); - foreach (var uc in claimList) + var userId = ConvertIdToString(user.Id); + var data = await _session.LoadAsync($"userdata/{userId}", builder => builder.IncludeDocuments(d => d.ClaimIds), cancellationToken).ConfigureAwait(false); + + foreach (var claimId in data.ClaimIds) { - if (uc.ClaimType == claim.Type && uc.ClaimValue == claim.Value) + var userClaim = await _session.LoadAsync(claimId, cancellationToken).ConfigureAwait(false); + if (userClaim.ClaimType == claim.Type && userClaim.ClaimValue == claim.Value) { - uc.ClaimType = newClaim.Type; - uc.ClaimValue = newClaim.Value; + _session.Delete(claimId); + await _session.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await _session.StoreAsync(CreateUserClaim(user, newClaim), claimId, cancellationToken).ConfigureAwait(false); } } } @@ -375,10 +404,23 @@ public async override Task RemoveClaimsAsync(TUser user, IEnumerable clai AssertNotNull(user, nameof(user)); AssertNotNull(claims, nameof(claims)); - var claimList = await GetUserClaimsAsync(user, cancellationToken).ConfigureAwait(false); - foreach (var claim in claims) + var userId = ConvertIdToString(user.Id); + var data = await _session.LoadAsync($"userdata/{userId}", builder => builder.IncludeDocuments(d => d.ClaimIds), cancellationToken).ConfigureAwait(false); + + var toDeleteList = new List(); + foreach (var claimId in data.ClaimIds) { - claimList.RemoveAll(uc => uc.ClaimType == claim.Type && uc.ClaimValue == claim.Value); + var userClaim = await _session.LoadAsync(claimId, cancellationToken).ConfigureAwait(false); + if (claims.Any(c => userClaim.ClaimType == c.Type && userClaim.ClaimValue == c.Value)) + { + toDeleteList.Add(claimId); + } + } + + foreach(var claimId in toDeleteList) + { + _session.Delete(claimId); + data.ClaimIds.Remove(claimId); } } @@ -398,16 +440,11 @@ public override async Task AddLoginAsync(TUser user, UserLoginInfo login, AssertNotNull(login, nameof(login)); var userId = ConvertIdToString(user.Id); - await _session.StoreAsync(new UserLoginIndex - { - Id = $"userlogin/{login.LoginProvider}-{login.ProviderKey}", - UserId = $"user/{userId}", - LoginProvider = login.LoginProvider, - ProviderKey = login.ProviderKey - }).ConfigureAwait(false); + var data = await _session.LoadAsync($"userdata/{userId}", cancellationToken).ConfigureAwait(false); + var userLoginId = $"userlogin/{login.LoginProvider}@{login.ProviderKey}"; - var logins = await GetUserLoginsAsync(userId, cancellationToken).ConfigureAwait(false); - logins.Add(CreateUserLogin(user, login)); + await _session.StoreAsync(CreateUserLogin(user, login), userLoginId, cancellationToken).ConfigureAwait(false); + data.LoginIds.Add(userLoginId); } /// @@ -425,12 +462,12 @@ public override async Task RemoveLoginAsync(TUser user, string loginProvider, st ThrowIfDisposed(); AssertNotNull(user, nameof(user)); - _session.Delete($"userlogin/{loginProvider}-{providerKey}"); - var userId = ConvertIdToString(user.Id); + var data = await _session.LoadAsync($"userdata/{userId}", cancellationToken).ConfigureAwait(false); - var logins = await GetUserLoginsAsync(userId, cancellationToken).ConfigureAwait(false); - logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + var userLoginId = $"userlogin/{loginProvider}@{providerKey}"; + data.LoginIds.Remove(userLoginId); + _session.Delete(userLoginId); } /// @@ -449,8 +486,13 @@ public async override Task> GetLoginsAsync(TUser user, Canc var userId = ConvertIdToString(user.Id); - var logins = await GetUserLoginsAsync(userId, cancellationToken).ConfigureAwait(false); - return logins + var data = await _session.LoadAsync($"userdata/{userId}", builder => builder.IncludeDocuments(d => d.LoginIds), cancellationToken).ConfigureAwait(false); + var list = new List(data.LoginIds.Count); + foreach(var id in data.LoginIds) + { + list.Add(await _session.LoadAsync(id, cancellationToken).ConfigureAwait(false)); + } + return list .Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)) .ToList(); } @@ -470,14 +512,14 @@ public override async Task FindByLoginAsync(string loginProvider, string cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - var index = await _session.LoadAsync($"userlogin/{loginProvider}-{providerKey}", cancellationToken).ConfigureAwait(false); - if (index == null) + var login = await _session.LoadAsync($"userlogin/{loginProvider}@{providerKey}", builder => builder.IncludeDocuments(l => $"user/{l.UserId}"), cancellationToken).ConfigureAwait(false); + if (login == null) { return null; } - var data = await _session.LoadAsync>(index.UserId, cancellationToken).ConfigureAwait(false); - return data.User; + var userId = ConvertIdToString(login.UserId); + return await _session.LoadAsync($"user/{userId}", cancellationToken).ConfigureAwait(false); } /// @@ -499,8 +541,7 @@ public override async Task FindByEmailAsync(string normalizedEmail, Cance return null; } - var data = await _session.LoadAsync>(index.UserId, cancellationToken).ConfigureAwait(false); - return data?.User; + return await _session.LoadAsync(index.UserId, cancellationToken).ConfigureAwait(false); } /// @@ -517,11 +558,15 @@ public async override Task> GetUsersForClaimAsync(Claim claim, Canc ThrowIfDisposed(); AssertNotNull(claim, nameof(claim)); - return await _session.Query>() - .Where(d => d.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value)) - .Select(d => d.User) - .ToListAsync(cancellationToken) + var userClaimsList = await _session.Query() + .Include(c => $"user/{c.UserId}") + .Where(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value) + .ToListAsync() .ConfigureAwait(false); + + var userList = await _session.LoadAsync(userClaimsList.Select(c => $"user/{c.UserId}") + , cancellationToken).ConfigureAwait(false); + return userList.Where(u => u.Value != null).Select(u => u.Value).ToList(); } /// @@ -542,7 +587,7 @@ public override async Task SetTokenAsync(TUser user, string loginProvider, strin ThrowIfDisposed(); var userId = ConvertIdToString(user.Id); - var token = await _session.LoadAsync($"usertoken/{userId}-{loginProvider}-{name}", cancellationToken).ConfigureAwait(false); + var token = await _session.LoadAsync($"usertoken/{userId}@{loginProvider}@{name}", cancellationToken).ConfigureAwait(false); if (token == null) { token = new TUserToken @@ -552,7 +597,7 @@ public override async Task SetTokenAsync(TUser user, string loginProvider, strin UserId = user.Id, Value = value }; - await _session.StoreAsync(token, $"usertoken/{userId}-{loginProvider}-{name}", cancellationToken).ConfigureAwait(false); + await _session.StoreAsync(token, $"usertoken/{userId}@{loginProvider}@{name}", cancellationToken).ConfigureAwait(false); } token.Value = value; await _session.SaveChangesAsync(cancellationToken).ConfigureAwait(false); @@ -575,7 +620,7 @@ public override async Task RemoveTokenAsync(TUser user, string loginProvider, st ThrowIfDisposed(); var userId = ConvertIdToString(user.Id); - _session.Delete($"usertoken/{userId}-{loginProvider}-{name}"); + _session.Delete($"usertoken/{userId}@{loginProvider}@{name}"); await _session.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } @@ -597,35 +642,12 @@ public override async Task GetTokenAsync(TUser user, string loginProvide var userId = ConvertIdToString(user.Id); - var token = await _session.LoadAsync($"usertoken/{userId}-{loginProvider}-{name}", cancellationToken).ConfigureAwait(false); + var token = await _session.LoadAsync($"usertoken/{userId}@{loginProvider}@{name}", cancellationToken).ConfigureAwait(false); return token?.Value; } - /// - /// Return a user login with the matching userId, provider, providerKey if it exists. - /// - /// The user's id. - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - internal Task FindUserLoginInternalAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken) - { - return FindUserLoginAsync(userId, loginProvider, providerKey, cancellationToken); - } - - /// - /// Return a user login with provider, providerKey if it exists. - /// - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - internal Task FindUserLoginInternalAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - { - return FindUserLoginAsync(loginProvider, providerKey, cancellationToken); - } - + + /// /// Return a user with the matching userId if it exists. @@ -638,51 +660,12 @@ protected override Task FindUserAsync(TKey userId, CancellationToken canc return FindByIdAsync(userId.ToString(), cancellationToken); } - /// - /// Return a user login with the matching userId, provider, providerKey if it exists. - /// - /// The user's id. - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected override async Task FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken) + private static void CloneEntity(object entity, Type type, object loaded) { - var data = await GetUserLoginsAsync(userId, cancellationToken).ConfigureAwait(false); - if (data != null) + foreach (var property in type.GetProperties()) { - return data.FirstOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + property.SetValue(entity, property.GetValue(loaded)); } - return null; - } - - /// - /// Return a user login with provider, providerKey if it exists. - /// - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected override Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - { - return _session.Query>() - .Where(d => d.Logins.Any(l=> l.LoginProvider == loginProvider && l.ProviderKey == providerKey)) - .Select(d => d.Logins.First(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey)) - .FirstOrDefaultAsync(); - } - - protected virtual async Task> GetUserClaimsAsync(TUser user, CancellationToken cancellationToken) - { - var userId = ConvertIdToString(user.Id); - var data = await _session.LoadAsync>($"user/{userId}", cancellationToken).ConfigureAwait(false); - - return data.Claims; - } - - protected virtual async Task> GetUserLoginsAsync(string userId, CancellationToken cancellationToken) - { - var data = await _session.LoadAsync>($"user/{userId}", cancellationToken).ConfigureAwait(false); - return data.Logins; } } } diff --git a/src/Aguacongas.Identity.RavenDb/UserStore.cs b/src/Aguacongas.Identity.RavenDb/UserStore.cs index 3e54690..2da8387 100644 --- a/src/Aguacongas.Identity.RavenDb/UserStore.cs +++ b/src/Aguacongas.Identity.RavenDb/UserStore.cs @@ -199,7 +199,7 @@ public async override Task AddToRoleAsync(TUser user, string roleName, Cancellat UserId = user.Id }; - await _session.StoreAsync(userRole, $"userrole/{roleName}-{userId}", cancellationToken).ConfigureAwait(false); + await _session.StoreAsync(userRole, $"userrole/{roleName}@{userId}", cancellationToken).ConfigureAwait(false); await _session.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } @@ -226,7 +226,7 @@ public async override Task RemoveFromRoleAsync(TUser user, string roleName, Canc var userId = ConvertIdToString(user.Id); - var userRole = await _session.LoadAsync($"userrole/{roleName}-{userId}", cancellationToken).ConfigureAwait(false); + var userRole = await _session.LoadAsync($"userrole/{roleName}@{userId}", cancellationToken).ConfigureAwait(false); _session.Delete(userRole); await _session.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } @@ -246,10 +246,9 @@ public override async Task> GetRolesAsync(TUser user, Cancellation var userId = ConvertIdToString(user.Id); - var userRoleList = await _session.Advanced.LoadStartingWithAsync(idPrefix: "userrole/", matches: $"*-{userId}", token: cancellationToken).ConfigureAwait(false); - var roles = await _session.LoadAsync>(userRoleList.Select(r => $"role/{ConvertIdToString(r.RoleId)}"), cancellationToken).ConfigureAwait(false); - - return roles.Select(r => r.Value.Role.Name).ToList(); + var userRoleList = await _session.Advanced.LoadStartingWithAsync(idPrefix: "userrole/", matches: $"*@{userId}", token: cancellationToken).ConfigureAwait(false); + var roles = await _session.LoadAsync(userRoleList.Select(r => $"role/{ConvertIdToString(r.RoleId)}"), cancellationToken).ConfigureAwait(false); + return roles.Where(r => r.Value != null).Select(r => r.Value.Name).ToList(); } /// @@ -268,7 +267,7 @@ public override async Task IsInRoleAsync(TUser user, string roleName, Canc AssertNotNullOrEmpty(roleName, nameof(roleName)); var userId = ConvertIdToString(user.Id); - var userRole = await _session.LoadAsync($"userrole/{roleName}-{userId}", cancellationToken).ConfigureAwait(false); + var userRole = await _session.LoadAsync($"userrole/{roleName}@{userId}", cancellationToken).ConfigureAwait(false); return userRole != null; } @@ -395,10 +394,10 @@ public async override Task> GetUsersInRoleAsync(string roleName, Ca ThrowIfDisposed(); AssertNotNullOrEmpty(roleName, nameof(roleName)); - var userRoleList = await _session.Advanced.LoadStartingWithAsync($"userrole/{roleName}-", token: cancellationToken).ConfigureAwait(false); - var userList = await _session.LoadAsync>(userRoleList.Select(ur => $"user/{ConvertIdToString(ur.UserId)}"), cancellationToken).ConfigureAwait(false); + var userRoleList = await _session.Advanced.LoadStartingWithAsync($"userrole/{roleName}@", token: cancellationToken).ConfigureAwait(false); + var userList = await _session.LoadAsync(userRoleList.Select(ur => $"user/{ConvertIdToString(ur.UserId)}"), cancellationToken).ConfigureAwait(false); - return userList.Select(d => d.Value.User).ToList(); + return userList.Where(u => u.Value != null).Select(u => u.Value).ToList(); } /// @@ -481,9 +480,7 @@ protected override async Task FindRoleAsync(string normalizedRoleName, Ca return null; } - var data = await _session.LoadAsync>(index.RoleId, cancellationToken).ConfigureAwait(false); - - return data.Role; + return await _session.LoadAsync(index.RoleId, cancellationToken).ConfigureAwait(false); } /// @@ -495,26 +492,7 @@ protected override async Task FindRoleAsync(string normalizedRoleName, Ca protected override Task FindUserAsync(TKey userId, CancellationToken cancellationToken) => FindByIdAsync(userId.ToString(), cancellationToken); - /// - /// Return a user login with the matching userId, provider, providerKey if it exists. - /// - /// The user's id. - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected override Task FindUserLoginAsync(string userId, string loginProvider, string providerKey, CancellationToken cancellationToken) - => _userOnlyStore.FindUserLoginInternalAsync(userId, loginProvider, providerKey, cancellationToken); - - /// - /// Return a user login with provider, providerKey if it exists. - /// - /// The login provider name. - /// The key provided by the to identify a user. - /// The used to propagate notifications that the operation should be canceled. - /// The user login if it exists. - protected override Task FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) - => _userOnlyStore.FindUserLoginInternalAsync(loginProvider, providerKey, cancellationToken); + protected override void Dispose(bool disposed) { @@ -527,8 +505,7 @@ protected virtual async Task FindRoleByIdAsync(string id, CancellationTok cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - var data = await _session.LoadAsync>($"role/{id}", cancellationToken).ConfigureAwait(false); - return data.Role; + return await _session.LoadAsync($"role/{id}", cancellationToken).ConfigureAwait(false); } private static void AssertNotNullOrEmpty(string p, string pName) diff --git a/test/Aguacongas.Identity.RavenDb.IntegrationTest/Aguacongas.Identity.RavenDb.IntegrationTest.csproj b/test/Aguacongas.Identity.RavenDb.IntegrationTest/Aguacongas.Identity.RavenDb.IntegrationTest.csproj index 14cb8d6..97964f7 100644 --- a/test/Aguacongas.Identity.RavenDb.IntegrationTest/Aguacongas.Identity.RavenDb.IntegrationTest.csproj +++ b/test/Aguacongas.Identity.RavenDb.IntegrationTest/Aguacongas.Identity.RavenDb.IntegrationTest.csproj @@ -7,16 +7,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all diff --git a/test/Aguacongas.Identity.RavenDb.IntegrationTest/RavenDbTestFixture.cs b/test/Aguacongas.Identity.RavenDb.IntegrationTest/RavenDbTestFixture.cs index df9b78d..801a485 100644 --- a/test/Aguacongas.Identity.RavenDb.IntegrationTest/RavenDbTestFixture.cs +++ b/test/Aguacongas.Identity.RavenDb.IntegrationTest/RavenDbTestFixture.cs @@ -1,7 +1,6 @@ // Project: Aguafrommars/Identity.RavenDb // Copyright (c) 2021 Olivier Lefebvre using Raven.Client.Documents; -using Raven.Client.Documents.Session; using Raven.TestDriver; using System.Runtime.CompilerServices; @@ -22,6 +21,12 @@ class RavenDbTestDriverWrapper : RavenTestDriver { public new IDocumentStore GetDocumentStore(GetDocumentStoreOptions options = null, [CallerMemberName] string database = null) => base.GetDocumentStore(options, database); + + protected override void PreInitialize(IDocumentStore documentStore) + { + documentStore.SetFindIdentityPropertyForIdentityModel(); + base.PreInitialize(documentStore); + } } } } diff --git a/test/Aguacongas.Identity.RavenDb.IntegrationTest/TestRole.cs b/test/Aguacongas.Identity.RavenDb.IntegrationTest/TestRole.cs index 8a58972..0d2ba16 100644 --- a/test/Aguacongas.Identity.RavenDb.IntegrationTest/TestRole.cs +++ b/test/Aguacongas.Identity.RavenDb.IntegrationTest/TestRole.cs @@ -1,9 +1,6 @@ // Project: Aguafrommars/Identity.RavenDb // Copyright (c) 2021 Olivier Lefebvre using Microsoft.AspNetCore.Identity; -using System; -using System.Collections.Generic; -using System.Text; namespace Aguacongas.Identity.RavenDb.IntegrationTest { diff --git a/test/Aguacongas.Identity.RavenDb.IntegrationTest/TestUser.cs b/test/Aguacongas.Identity.RavenDb.IntegrationTest/TestUser.cs index c0fe57f..8126895 100644 --- a/test/Aguacongas.Identity.RavenDb.IntegrationTest/TestUser.cs +++ b/test/Aguacongas.Identity.RavenDb.IntegrationTest/TestUser.cs @@ -1,9 +1,6 @@ // Project: Aguafrommars/Identity.RavenDb // Copyright (c) 2021 Olivier Lefebvre using Microsoft.AspNetCore.Identity; -using System; -using System.Collections.Generic; -using System.Text; namespace Aguacongas.Identity.RavenDb.IntegrationTest {