diff --git a/RavenDB.Identity.sln b/RavenDB.Identity.sln
index d1c5dd8..d9bcd6e 100644
--- a/RavenDB.Identity.sln
+++ b/RavenDB.Identity.sln
@@ -18,9 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Readme.md = Readme.md
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RazorPages", "Samples\RazorPages\Sample.RazorPages.csproj", "{0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Mvc", "Samples\Mvc\Sample.Mvc.csproj", "{8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Mvc", "Samples\Mvc\Sample.Mvc.csproj", "{8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -32,10 +30,6 @@ Global
{C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C7824FBF-9E9B-40B5-9CF5-4884F8378BA6}.Release|Any CPU.Build.0 = Release|Any CPU
- {0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0C8F67DC-B6B3-4D8C-9E33-BBB47F858BE3}.Release|Any CPU.Build.0 = Release|Any CPU
{8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AA88A38-C74A-4F0D-9853-EFA0D78F0E63}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/RavenDB.Identity/RavenDB.Identity.csproj b/RavenDB.Identity/RavenDB.Identity.csproj
index c62c194..4344cae 100644
--- a/RavenDB.Identity/RavenDB.Identity.csproj
+++ b/RavenDB.Identity/RavenDB.Identity.csproj
@@ -30,7 +30,6 @@
-
diff --git a/RavenDB.Identity/RoleStore.cs b/RavenDB.Identity/RoleStore.cs
index bb81c22..b21e398 100644
--- a/RavenDB.Identity/RoleStore.cs
+++ b/RavenDB.Identity/RoleStore.cs
@@ -1,422 +1,422 @@
-using Microsoft.AspNetCore.Identity;
-using Raven.Client;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Logging;
+using Raven.Client.Documents;
+using Raven.Client.Documents.Session;
+using Raven.Client.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
-using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using Raven.Client.Documents.Session;
-using Raven.Client.Exceptions;
-using Raven.Client.Documents;
namespace Raven.Identity
{
+ ///
+ ///
+ ///
+ ///
+ public class RoleMan : RoleManager
+ where TRole : class
+ {
///
///
///
- ///
- public class RoleMan : RoleManager
- where TRole : class
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public RoleMan(IRoleStore store, IEnumerable> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger> logger, IHttpContextAccessor contextAccessor) :
+ base(store, roleValidators, keyNormalizer, errors, logger)
{
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public RoleMan(IRoleStore store, IEnumerable> roleValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, ILogger> logger, IHttpContextAccessor contextAccessor) :
- base(store, roleValidators, keyNormalizer, errors, logger)
- {
- }
+ }
+ }
+
+ ///
+ /// Creates a new instance of a persistence store for roles.
+ ///
+ /// The type of the class representing a role.
+ public class RoleStore : RoleStore
+ where TRole : IdentityRole
+ {
+ ///
+ /// Constructs a new instance of .
+ ///
+ /// The .
+ /// The .
+ public RoleStore(IDocumentStore store, IdentityErrorDescriber describer = null) : base(store, describer) { }
+
+ ///
+ /// Creates a entity representing a role claim.
+ ///
+ /// The associated role.
+ /// The associated claim.
+ /// The role claim entity.
+ protected override IdentityRoleClaim CreateRoleClaim(TRole role, Claim claim)
+ {
+ return new IdentityRoleClaim { ClaimType = claim.Type, ClaimValue = claim.Value };
+ }
+ }
+
+ ///
+ /// Creates a new instance of a persistence store for roles.
+ ///
+ /// The type of the class representing a role.
+ /// The type of the class representing a role claim.
+ public abstract class RoleStore :
+ IRoleClaimStore
+ where TRole : IdentityRole
+ where TRoleClaim : IdentityRoleClaim
+ {
+ ///
+ /// Constructs a new instance of .
+ ///
+ /// The .
+ /// The .
+ public RoleStore(IDocumentStore store, IdentityErrorDescriber describer = null)
+ {
+ Store = store ?? throw new ArgumentNullException(nameof(store));
+ ErrorDescriber = describer ?? new IdentityErrorDescriber();
+ }
+
+ private bool _disposed;
+
+
+ ///
+ /// Gets the database context for this store.
+ ///
+ public IDocumentStore Store { get; private set; }
+
+ ///
+ /// Gets or sets the for any error that occurred with the current operation.
+ ///
+ public IdentityErrorDescriber ErrorDescriber { get; set; }
+
+ ///
+ /// Creates a new role in a store as an asynchronous operation.
+ ///
+ /// The role to create in the store.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// A that represents the of the asynchronous query.
+ public async virtual Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+ if (string.IsNullOrWhiteSpace(role.Name))
+ {
+ throw new ArgumentNullException(nameof(role.Name));
+ }
+
+ using (IAsyncDocumentSession session = Store.OpenAsyncSession())
+ {
+ var roleId = GetRavenIdFromRoleName(role.Name, Store);
+ await session.StoreAsync(role, roleId);
+ await session.SaveChangesAsync(cancellationToken);
+ }
+ return IdentityResult.Success;
}
///
- /// Creates a new instance of a persistence store for roles.
+ /// Updates a role in a store as an asynchronous operation.
///
- /// The type of the class representing a role.
- public class RoleStore : RoleStore
- where TRole : IdentityRole
+ /// The role to update in the store.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// A that represents the of the asynchronous query.
+ public async virtual Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
- ///
- /// Constructs a new instance of .
- ///
- /// The .
- /// The .
- public RoleStore(IAsyncDocumentSession context, IdentityErrorDescriber describer = null) : base(context, describer) { }
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
- ///
- /// Creates a entity representing a role claim.
- ///
- /// The associated role.
- /// The associated claim.
- /// The role claim entity.
- protected override IdentityRoleClaim CreateRoleClaim(TRole role, Claim claim)
+ // TODO: Assumption is made that TRole entity is being tracked in the current session
+ // If not, then we'll have to Load and overwrite all properties except for Id in the loaded entity
+ //role.ConcurrencyStamp = Guid.NewGuid().ToString();
+ try
+ {
+ using (IAsyncDocumentSession session = Store.OpenAsyncSession())
{
- return new IdentityRoleClaim { ClaimType = claim.Type, ClaimValue = claim.Value };
+ await session.SaveChangesAsync(cancellationToken);
}
+ }
+ catch (ConcurrencyException)
+ {
+ return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
+ }
+ return IdentityResult.Success;
}
///
- /// Creates a new instance of a persistence store for roles.
+ /// Deletes a role from the store as an asynchronous operation.
///
- /// The type of the class representing a role.
- /// The type of the class representing a role claim.
- public abstract class RoleStore :
- IRoleClaimStore
- where TRole : IdentityRole
- where TRoleClaim : IdentityRoleClaim
+ /// The role to delete from the store.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// A that represents the of the asynchronous query.
+ public async virtual Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
- ///
- /// Constructs a new instance of .
- ///
- /// The .
- /// The .
- public RoleStore(IAsyncDocumentSession context, IdentityErrorDescriber describer = null)
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+
+ using (IAsyncDocumentSession session = Store.OpenAsyncSession())
+ {
+ session.Delete(role.Id);
+ }
+
+ try
+ {
+ using (IAsyncDocumentSession session = Store.OpenAsyncSession())
{
- AsyncSession = context ?? throw new ArgumentNullException(nameof(context));
- ErrorDescriber = describer ?? new IdentityErrorDescriber();
- }
-
- private bool _disposed;
-
-
- ///
- /// Gets the database context for this store.
- ///
- public IAsyncDocumentSession AsyncSession { get; private set; }
-
- ///
- /// 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;
-
- /// Saves the current store.
- /// The used to propagate notifications that the operation should be canceled.
- /// The that represents the asynchronous operation.
- private async Task SaveChanges(CancellationToken cancellationToken)
- {
- if (AutoSaveChanges)
- {
- await AsyncSession.SaveChangesAsync(cancellationToken);
- }
- }
-
- ///
- /// Creates a new role in a store as an asynchronous operation.
- ///
- /// The role to create in the store.
- /// The used to propagate notifications that the operation should be canceled.
- /// A that represents the of the asynchronous query.
- public async virtual Task CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
- if (string.IsNullOrWhiteSpace(role.Name))
- {
- throw new ArgumentNullException(nameof(role.Name));
- }
-
- var roleId = GetRavenIdFromRoleName(role.Name, AsyncSession.Advanced.DocumentStore);
- await AsyncSession.StoreAsync(role, roleId);
- await SaveChanges(cancellationToken);
- return IdentityResult.Success;
- }
-
- ///
- /// Updates a role in a store as an asynchronous operation.
- ///
- /// The role to update in the store.
- /// The used to propagate notifications that the operation should be canceled.
- /// A that represents the of the asynchronous query.
- public async virtual Task UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
-
- // TODO: Assumption is made that TRole entity is being tracked in the current session
- // If not, then we'll have to Load and overwrite all properties except for Id in the loaded entity
- //role.ConcurrencyStamp = Guid.NewGuid().ToString();
- try
- {
- await SaveChanges(cancellationToken);
- }
- catch (ConcurrencyException)
- {
- return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
- }
- return IdentityResult.Success;
- }
-
- ///
- /// Deletes a role from the store as an asynchronous operation.
- ///
- /// The role to delete from the store.
- /// The used to propagate notifications that the operation should be canceled.
- /// A that represents the of the asynchronous query.
- public async virtual Task DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
- AsyncSession.Delete(role.Id);
- try
- {
- await SaveChanges(cancellationToken);
- }
- catch (ConcurrencyException)
- {
- return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
- }
- return IdentityResult.Success;
- }
-
- ///
- /// Gets the ID for a role from the store as an asynchronous operation.
- ///
- /// The role whose ID should be returned.
- /// The used to propagate notifications that the operation should be canceled.
- /// A that contains the ID of the role.
- public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
- return Task.FromResult(role.Id);
- }
-
- ///
- /// Gets the name of a role from the store as an asynchronous operation.
- ///
- /// The role whose name should be returned.
- /// The used to propagate notifications that the operation should be canceled.
- /// A that contains the name of the role.
- public Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
- return Task.FromResult(role.Name);
- }
-
- ///
- /// Sets the name of a role in the store as an asynchronous operation.
- ///
- /// The role whose name should be set.
- /// The name of the role.
- /// The used to propagate notifications that the operation should be canceled.
- /// The that represents the asynchronous operation.
- public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
- role.Name = roleName;
- return Task.CompletedTask;
- }
-
- ///
- /// Finds the role who has the specified ID as an asynchronous operation.
- ///
- /// 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 Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- return AsyncSession.LoadAsync(id, cancellationToken);
- }
-
- ///
- /// Finds the role who has the specified normalized name as an asynchronous operation.
- ///
- /// The normalized role name to look for.
- /// The used to propagate notifications that the operation should be canceled.
- /// A that result of the look up.
- public virtual Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
-
- var roleId = GetRavenIdFromRoleName(normalizedName, AsyncSession.Advanced.DocumentStore);
- return AsyncSession.LoadAsync(roleId);
- }
-
- ///
- /// Get a role's normalized name as an asynchronous operation.
- ///
- /// The role whose normalized name should be retrieved.
- /// The used to propagate notifications that the operation should be canceled.
- /// A that contains the name of the role.
- public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default)
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
-
- return Task.FromResult(role.Name.ToLowerInvariant());
- }
-
- ///
- /// Set a role's normalized name as an asynchronous operation.
- ///
- /// The role whose normalized 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 SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
- //role.Name = normalizedName;
- return Task.FromResult(0);
- }
-
- ///
- /// Throws if this class has been disposed.
- ///
- protected void ThrowIfDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
- }
-
- ///
- /// Dispose the stores
- ///
- public void Dispose()
- {
- _disposed = true;
- }
-
- ///
- /// Get the claims associated with the specified as an asynchronous operation.
- ///
- /// The role whose claims should be retrieved.
- /// The used to propagate notifications that the operation should be canceled.
- /// A that contains the claims granted to a role.
- public async Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
- {
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
-
- await Task.FromResult(0);
-
- return role.Claims.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList();
- }
-
- ///
- /// Adds the given to the specified .
- ///
- /// The role to add the claim to.
- /// The claim to add to the role.
- /// The used to propagate notifications that the operation should be canceled.
- /// The that represents the asynchronous operation.
- public Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken))
- {
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
- if (claim == null)
- {
- throw new ArgumentNullException(nameof(claim));
- }
- role.Claims.Add(CreateRoleClaim(role, claim));
-
- return Task.FromResult(false);
- }
-
- ///
- /// Removes the given from the specified .
- ///
- /// The role to remove the claim from.
- /// The claim to remove from the role.
- /// The used to propagate notifications that the operation should be canceled.
- /// The that represents the asynchronous operation.
- public async Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken))
- {
- ThrowIfDisposed();
- if (role == null)
- {
- throw new ArgumentNullException(nameof(role));
- }
- if (claim == null)
- {
- throw new ArgumentNullException(nameof(claim));
- }
-
- await Task.FromResult(0);
-
- var claims = role.Claims.Where(c => c.ClaimValue == claim.Value).ToList();
- foreach (var c in claims)
- {
- role.Claims.Remove(c);
- }
- }
-
- ///
- /// Creates a entity representing a role claim.
- ///
- /// The associated role.
- /// The associated claim.
- /// The role claim entity.
- protected abstract TRoleClaim CreateRoleClaim(TRole role, Claim claim);
-
- internal static string GetRavenIdFromRoleName(string role, IDocumentStore docStore)
- {
- var roleCollection = docStore.Conventions.GetCollectionName(typeof(TRole));
- var prefix = docStore.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(roleCollection);
- var partSeparator = docStore.Conventions.IdentityPartsSeparator;
- return prefix + partSeparator + role;
+ await session.SaveChangesAsync(cancellationToken);
}
+ }
+ catch (ConcurrencyException)
+ {
+ return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
+ }
+ return IdentityResult.Success;
}
+
+ ///
+ /// Gets the ID for a role from the store as an asynchronous operation.
+ ///
+ /// The role whose ID should be returned.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// A that contains the ID of the role.
+ public Task GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+ return Task.FromResult(role.Id);
+ }
+
+ ///
+ /// Gets the name of a role from the store as an asynchronous operation.
+ ///
+ /// The role whose name should be returned.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// A that contains the name of the role.
+ public Task GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+ return Task.FromResult(role.Name);
+ }
+
+ ///
+ /// Sets the name of a role in the store as an asynchronous operation.
+ ///
+ /// The role whose name should be set.
+ /// The name of the role.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// The that represents the asynchronous operation.
+ public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+ role.Name = roleName;
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Finds the role who has the specified ID as an asynchronous operation.
+ ///
+ /// 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 Task FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+
+ using (IAsyncDocumentSession session = Store.OpenAsyncSession())
+ {
+ return session.LoadAsync(id, cancellationToken);
+ }
+ }
+
+ ///
+ /// Finds the role who has the specified normalized name as an asynchronous operation.
+ ///
+ /// The normalized role name to look for.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// A that result of the look up.
+ public virtual Task FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+
+ using (IAsyncDocumentSession session = Store.OpenAsyncSession())
+ {
+ var roleId = GetRavenIdFromRoleName(normalizedName, Store);
+ return session.LoadAsync(roleId);
+ }
+ }
+
+ ///
+ /// Get a role's normalized name as an asynchronous operation.
+ ///
+ /// The role whose normalized name should be retrieved.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// A that contains the name of the role.
+ public virtual Task GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+
+ return Task.FromResult(role.Name.ToLowerInvariant());
+ }
+
+ ///
+ /// Set a role's normalized name as an asynchronous operation.
+ ///
+ /// The role whose normalized 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 SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+ //role.Name = normalizedName;
+ return Task.FromResult(0);
+ }
+
+ ///
+ /// Throws if this class has been disposed.
+ ///
+ protected void ThrowIfDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(GetType().Name);
+ }
+ }
+
+ ///
+ /// Dispose the stores
+ ///
+ public void Dispose()
+ {
+ _disposed = true;
+ }
+
+ ///
+ /// Get the claims associated with the specified as an asynchronous operation.
+ ///
+ /// The role whose claims should be retrieved.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// A that contains the claims granted to a role.
+ public async Task> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+
+ await Task.FromResult(0);
+
+ return role.Claims.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList();
+ }
+
+ ///
+ /// Adds the given to the specified .
+ ///
+ /// The role to add the claim to.
+ /// The claim to add to the role.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// The that represents the asynchronous operation.
+ public Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+ if (claim == null)
+ {
+ throw new ArgumentNullException(nameof(claim));
+ }
+ role.Claims.Add(CreateRoleClaim(role, claim));
+
+ return Task.FromResult(false);
+ }
+
+ ///
+ /// Removes the given from the specified .
+ ///
+ /// The role to remove the claim from.
+ /// The claim to remove from the role.
+ /// The used to propagate notifications that the operation should be canceled.
+ /// The that represents the asynchronous operation.
+ public async Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ ThrowIfDisposed();
+ if (role == null)
+ {
+ throw new ArgumentNullException(nameof(role));
+ }
+ if (claim == null)
+ {
+ throw new ArgumentNullException(nameof(claim));
+ }
+
+ await Task.FromResult(0);
+
+ var claims = role.Claims.Where(c => c.ClaimValue == claim.Value).ToList();
+ foreach (var c in claims)
+ {
+ role.Claims.Remove(c);
+ }
+ }
+
+ ///
+ /// Creates a entity representing a role claim.
+ ///
+ /// The associated role.
+ /// The associated claim.
+ /// The role claim entity.
+ protected abstract TRoleClaim CreateRoleClaim(TRole role, Claim claim);
+
+ internal static string GetRavenIdFromRoleName(string role, IDocumentStore docStore)
+ {
+ var roleCollection = docStore.Conventions.GetCollectionName(typeof(TRole));
+ var prefix = docStore.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(roleCollection);
+ var partSeparator = docStore.Conventions.IdentityPartsSeparator;
+ return prefix + partSeparator + role;
+ }
+ }
}
diff --git a/RavenDB.Identity/ServiceCollectionExtensions.cs b/RavenDB.Identity/ServiceCollectionExtensions.cs
deleted file mode 100644
index fe6f763..0000000
--- a/RavenDB.Identity/ServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using Microsoft.AspNetCore.Identity;
-using Microsoft.Extensions.DependencyInjection;
-using Raven.Client;
-using Raven.Client.Documents;
-using Raven.Client.Documents.Session;
-using System;
-
-namespace Raven.Identity
-{
- ///
- /// Extends the so that RavenDB services can be registered through it.
- ///
- public static class ServiceCollectionExtensions
- {
- ///
- /// Registers a RavenDB as the user store.
- ///
- /// The type of user. This should be a class you created derived from .
- ///
- /// Identity options configuration.
- /// The identity builder.
- public static IdentityBuilder AddRavenDbIdentity(this IServiceCollection services, Action setupAction = null)
- where TUser : IdentityUser
- {
- return AddRavenDbIdentity(services, setupAction);
- }
-
- ///
- /// Registers a RavenDB as the user store.
- ///
- /// The type of user. This should be a class you created derived from .
- /// The type of role. This should be a class you created derived from .
- ///
- /// Identity options configuration.
- /// The identity builder.
- public static IdentityBuilder AddRavenDbIdentity(this IServiceCollection services, Action setupAction = null)
- where TUser : IdentityUser
- where TRole : IdentityRole, new()
- {
- // Add the AspNet identity system to work with our RavenDB identity objects.
- IdentityBuilder identityBuilder;
- if (setupAction != null)
- {
- identityBuilder = services.AddIdentity(setupAction)
- .AddDefaultTokenProviders();
- }
- else
- {
- identityBuilder = services.AddIdentity()
- .AddDefaultTokenProviders();
- }
-
- services.AddScoped, UserStore>();
- services.AddScoped, RoleStore>();
-
- return identityBuilder;
- }
- }
-}
diff --git a/RavenDB.Identity/UserStore.cs b/RavenDB.Identity/UserStore.cs
index 26afb11..3fdb61b 100644
--- a/RavenDB.Identity/UserStore.cs
+++ b/RavenDB.Identity/UserStore.cs
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Identity;
using Raven.Client.Documents;
-using Raven.Client.Documents.Operations;
using Raven.Client.Documents.Operations.CompareExchange;
using Raven.Client.Documents.Session;
using System;
@@ -12,899 +11,887 @@ using System.Threading.Tasks;
namespace Raven.Identity
{
+ ///
+ /// UserStore for entities in a RavenDB database.
+ ///
+ ///
+ ///
+ public class UserStore :
+ IUserStore,
+ IUserLoginStore,
+ IUserClaimStore,
+ IUserRoleStore,
+ IUserPasswordStore,
+ IUserSecurityStampStore,
+ IUserEmailStore,
+ IUserLockoutStore,
+ IUserTwoFactorStore,
+ IUserPhoneNumberStore,
+ IUserAuthenticatorKeyStore,
+ IUserAuthenticationTokenStore,
+ IUserTwoFactorRecoveryCodeStore,
+ IQueryableUserStore
+ where TUser : IdentityUser
+ where TRole : IdentityRole, new()
+ {
+ private bool _disposed;
+ private IDocumentStore _store;
+
+ private const string emailReservationKeyPrefix = "emails/";
+
///
- /// UserStore for entities in a RavenDB database.
+ /// Creates a new user store that uses the specified Raven document store.
///
- ///
- ///
- public class UserStore :
- IUserStore,
- IUserLoginStore,
- IUserClaimStore,
- IUserRoleStore,
- IUserPasswordStore,
- IUserSecurityStampStore,
- IUserEmailStore,
- IUserLockoutStore,
- IUserTwoFactorStore,
- IUserPhoneNumberStore,
- IUserAuthenticatorKeyStore,
- IUserAuthenticationTokenStore,
- IUserTwoFactorRecoveryCodeStore,
- IQueryableUserStore
- where TUser : IdentityUser
- where TRole : IdentityRole, new()
+ ///
+ public UserStore(IDocumentStore store)
{
- private bool _disposed;
- private readonly Func getSessionFunc;
- private IAsyncDocumentSession _session;
+ this._store = store;
+ }
- private const string emailReservationKeyPrefix = "emails/";
+ #region IDispoable implementation
- ///
- /// Creates a new user store that uses the Raven document session returned from the specified session fetcher.
- ///
- /// The function that gets the Raven document session.
- public UserStore(Func getSession)
+ ///
+ /// Disposes the user store.
+ ///
+ public void Dispose()
+ {
+ _disposed = true;
+ }
+
+ #endregion
+
+ #region IUserStore implementation
+
+ ///
+ public Task GetUserIdAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.Id);
+ }
+
+ ///
+ public Task GetUserNameAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.UserName);
+ }
+
+ ///
+ public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.UserName = userName;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ // Raven string comparison queries are case-insensitive. We can just return the user name.
+ return Task.FromResult(user.UserName);
+ }
+
+ ///
+ public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.UserName = normalizedName.ToLowerInvariant();
+ return Task.CompletedTask;
+ }
+
+ ///
+ public async Task CreateAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ // Make sure we have a valid email address.
+ if (string.IsNullOrWhiteSpace(user.Email))
+ {
+ throw new ArgumentException("The user's email address can't be null or empty.", nameof(user));
+ }
+
+ if (string.IsNullOrEmpty(user.Id))
+ {
+ var conventions = _store.Conventions;
+ var entityName = conventions.GetCollectionName(typeof(TUser));
+ var prefix = conventions.TransformTypeCollectionNameToDocumentIdPrefix(entityName);
+ var separator = conventions.IdentityPartsSeparator;
+ var id = $"{prefix}{separator}{user.Email}";
+ user.Id = id;
+ }
+
+ if (string.IsNullOrEmpty(user.UserName))
+ {
+ user.UserName = user.Email;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // See if the email address is already taken.
+ // We do this using Raven's compare/exchange functionality, which works cluster-wide.
+ // https://ravendb.net/docs/article-page/4.1/csharp/client-api/operations/compare-exchange/overview#creating-a-key
+ //
+ // Try to reserve a new user email
+ // Note: This operation takes place outside of the session transaction it is a cluster-wide reservation.
+ var compareExchangeKey = GetCompareExchangeKeyFromEmail(user.Email);
+ var reserveEmailOperation = new PutCompareExchangeValueOperation(compareExchangeKey, user.Id, 0);
+ var reserveEmailResult = await _store.Operations.SendAsync(reserveEmailOperation);
+ if (!reserveEmailResult.Successful)
+ {
+ return IdentityResult.Failed(new[]
{
- this.getSessionFunc = getSession;
- }
-
- ///
- /// Creates a new user store that uses the specified Raven document session.
- ///
- ///
- public UserStore(IAsyncDocumentSession session)
- {
- this._session = session;
- }
-
- #region IDispoable implementation
-
- ///
- /// Disposes the user store.
- ///
- public void Dispose()
- {
- _disposed = true;
- }
-
- #endregion
-
- #region IUserStore implementation
-
- ///
- public Task GetUserIdAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.Id);
- }
-
- ///
- public Task GetUserNameAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.UserName);
- }
-
- ///
- public Task SetUserNameAsync(TUser user, string userName, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.UserName = userName;
- return Task.CompletedTask;
- }
-
- ///
- public Task GetNormalizedUserNameAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- // Raven string comparison queries are case-insensitive. We can just return the user name.
- return Task.FromResult(user.UserName);
- }
-
- ///
- public Task SetNormalizedUserNameAsync(TUser user, string normalizedName, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.UserName = normalizedName.ToLowerInvariant();
- return Task.CompletedTask;
- }
-
- ///
- public async Task CreateAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- // Make sure we have a valid email address.
- if (string.IsNullOrWhiteSpace(user.Email))
- {
- throw new ArgumentException("The user's email address can't be null or empty.", nameof(user));
- }
-
- if (string.IsNullOrEmpty(user.Id))
- {
- var conventions = DbSession.Advanced.DocumentStore.Conventions;
- var entityName = conventions.GetCollectionName(typeof(TUser));
- var prefix = conventions.TransformTypeCollectionNameToDocumentIdPrefix(entityName);
- var separator = conventions.IdentityPartsSeparator;
- var id = $"{prefix}{separator}{user.Email}";
- user.Id = id;
- }
-
- if (string.IsNullOrEmpty(user.UserName))
- {
- user.UserName = user.Email;
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- // See if the email address is already taken.
- // We do this using Raven's compare/exchange functionality, which works cluster-wide.
- // https://ravendb.net/docs/article-page/4.1/csharp/client-api/operations/compare-exchange/overview#creating-a-key
- //
- // Try to reserve a new user email
- // Note: This operation takes place outside of the session transaction it is a cluster-wide reservation.
- var compareExchangeKey = GetCompareExchangeKeyFromEmail(user.Email);
- var reserveEmailOperation = new PutCompareExchangeValueOperation(compareExchangeKey, user.Id, 0);
- var reserveEmailResult = await DbSession.Advanced.DocumentStore.Operations.SendAsync(reserveEmailOperation);
- if (!reserveEmailResult.Successful)
- {
- return IdentityResult.Failed(new[]
- {
new IdentityError
{
Code = "DuplicateEmail",
Description = $"The email address {user.Email} is already taken."
}
});
- }
+ }
- // This model allows us to lookup a user by name in order to get the id
- await DbSession.StoreAsync(user, cancellationToken);
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ // This model allows us to lookup a user by name in order to get the id
+ await session.StoreAsync(user, cancellationToken);
- // Because this a a cluster-wide operation due to compare/exchange tokens,
- // we need to save changes here; if we can't store the user,
- // we need to roll back the email reservation.
- try
- {
- await DbSession.SaveChangesAsync();
- }
- catch (Exception)
- {
- // The compare/exchange email reservation is cluster-wide, outside of the session scope.
- // We need to manually roll it back.
- await this.DeleteUserEmailReservation(user.Email);
- throw;
- }
-
- return IdentityResult.Success;
- }
-
- ///
- public Task UpdateAsync(TUser user, CancellationToken cancellationToken)
+ // Because this a a cluster-wide operation due to compare/exchange tokens,
+ // we need to save changes here; if we can't store the user,
+ // we need to roll back the email reservation.
+ try
{
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(IdentityResult.Success);
+ await session.SaveChangesAsync();
}
-
- ///
- public async Task DeleteAsync(TUser user, CancellationToken cancellationToken)
+ catch (Exception)
{
- ThrowIfNullDisposedCancelled(user, cancellationToken);
- cancellationToken.ThrowIfCancellationRequested();
+ // The compare/exchange email reservation is cluster-wide, outside of the session scope.
+ // We need to manually roll it back.
+ await this.DeleteUserEmailReservation(user.Email);
+ throw;
+ }
+ }
- // Remove the cluster-wide compare/exchange key.
- var deletionResult = await DeleteUserEmailReservation(user.Email);
- if (!deletionResult.Successful)
- {
- return IdentityResult.Failed(new[]
- {
+ return IdentityResult.Success;
+ }
+
+ ///
+ public Task UpdateAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(IdentityResult.Success);
+ }
+
+ ///
+ public async Task DeleteAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Remove the cluster-wide compare/exchange key.
+ var deletionResult = await DeleteUserEmailReservation(user.Email);
+ if (!deletionResult.Successful)
+ {
+ return IdentityResult.Failed(new[]
+ {
new IdentityError
{
Code = "ConcurrencyFailure",
Description = "Unable to delete user email compare/exchange value"
}
});
- }
+ }
- // Delete the user and save it. We must save it because deleting is a cluster-wide operation.
- // Only if the deletion succeeds will we remove the cluseter-wide compare/exchange key.
- this.DbSession.Delete(user);
- await this.DbSession.SaveChangesAsync();
- return IdentityResult.Success;
- }
+ // Delete the user and save it. We must save it because deleting is a cluster-wide operation.
+ // Only if the deletion succeeds will we remove the cluseter-wide compare/exchange key.
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ session.Delete(user);
+ await session.SaveChangesAsync();
+ }
- ///
- public Task FindByIdAsync(string userId, CancellationToken cancellationToken)
- {
- ThrowIfDisposedOrCancelled(cancellationToken);
-
- return DbSession.LoadAsync(userId);
- }
-
- ///
- public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
- {
- ThrowIfDisposedOrCancelled(cancellationToken);
- if (string.IsNullOrEmpty(normalizedUserName))
- {
- throw new ArgumentNullException(nameof(normalizedUserName));
- }
-
- var compareExchangeKey = GetCompareExchangeKeyFromEmail(normalizedUserName);
- var getEmailReservationOperation = new GetCompareExchangeValueOperation(compareExchangeKey);
- var emailReservationResultOrNull = await DbSession.Advanced.DocumentStore.Operations.SendAsync(getEmailReservationOperation);
- var userId = emailReservationResultOrNull?.Value;
- if (string.IsNullOrEmpty(userId))
- {
- return null;
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- return await FindByIdAsync(userId, cancellationToken);
- }
-
- #endregion
-
- #region IUserLoginStore implementation
-
- ///
- public async Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
- if (login == null)
- {
- throw new ArgumentNullException(nameof(login));
- }
-
- if (!user.Logins.Any(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey))
- {
- user.Logins.Add(login);
-
- var userLogin = new IdentityUserLogin
- {
- Id = Util.GetLoginId(login),
- UserId = user.Id,
- Provider = login.LoginProvider,
- ProviderKey = login.ProviderKey
- };
- await DbSession.StoreAsync(userLogin);
- }
- }
-
- ///
- public async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- var login = new UserLoginInfo(loginProvider, providerKey, string.Empty);
- string loginId = Util.GetLoginId(login);
- var loginDoc = await DbSession.LoadAsync(loginId);
- if (loginDoc != null)
- {
- DbSession.Delete(loginDoc);
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- user.Logins.RemoveAll(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey);
- }
-
- ///
- public Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.Logins.ToIList());
- }
-
- ///
- public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
- {
- var login = new UserLoginInfo(loginProvider, providerKey, string.Empty);
- string loginId = Util.GetLoginId(login);
- var loginDoc = await DbSession.LoadAsync(loginId);
- if (loginDoc != null)
- {
- return await DbSession.LoadAsync(loginDoc.UserId);
- }
-
- return null;
- }
-
- #endregion
-
- #region IUserClaimStore implementation
-
- ///
- public Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- IList result = user.Claims
- .Select(c => new Claim(c.ClaimType, c.ClaimValue))
- .ToList();
- return Task.FromResult(result);
- }
-
- ///
- public Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.Claims.AddRange(claims.Select(c => new IdentityUserClaim { ClaimType = c.Type, ClaimValue = c.Value }));
- return Task.CompletedTask;
- }
-
- ///
- public async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- var indexOfClaim = user.Claims.FindIndex(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value);
- if (indexOfClaim != -1)
- {
- user.Claims.RemoveAt(indexOfClaim);
- await this.AddClaimsAsync(user, new[] { newClaim }, cancellationToken);
- }
- }
-
- ///
- public Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.Claims.RemoveAll(identityClaim => claims.Any(c => c.Type == identityClaim.ClaimType && c.Value == identityClaim.ClaimValue));
- return Task.CompletedTask;
- }
-
- ///
- public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken)
- {
- ThrowIfDisposedOrCancelled(cancellationToken);
- if (claim == null)
- {
- throw new ArgumentNullException(nameof(claim));
- }
-
- var list = await DbSession.Query()
- .Where(u => u.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value))
- .ToListAsync();
- return list;
- }
-
- #endregion
-
- #region IUserRoleStore implementation
-
- ///
- public async Task AddToRoleAsync(TUser user, string roleName, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- // See if we have an IdentityRole with that name.
- var identityUserCollection = DbSession.Advanced.DocumentStore.Conventions.GetCollectionName(typeof(TRole));
- var prefix = DbSession.Advanced.DocumentStore.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(identityUserCollection);
- var identityPartSeperator = DbSession.Advanced.DocumentStore.Conventions.IdentityPartsSeparator;
- var roleNameLowered = roleName.ToLowerInvariant();
- var roleId = prefix + identityPartSeperator + roleNameLowered;
- var existingRoleOrNull = await this.DbSession.LoadAsync(roleId, cancellationToken);
- if (existingRoleOrNull == null)
- {
- ThrowIfDisposedOrCancelled(cancellationToken);
- existingRoleOrNull = new TRole();
- existingRoleOrNull.Name = roleNameLowered;
- await this.DbSession.StoreAsync(existingRoleOrNull, roleId, cancellationToken);
- }
-
- // Use the real name (not normalized/uppered/lowered) of the role, as specified by the user.
- var roleRealName = existingRoleOrNull.Name;
- if (!user.Roles.Contains(roleRealName, StringComparer.InvariantCultureIgnoreCase))
- {
- user.GetRolesList().Add(roleRealName);
- }
-
- if (!existingRoleOrNull.Users.Contains(user.Id, StringComparer.InvariantCultureIgnoreCase))
- {
- existingRoleOrNull.Users.Add(user.Id);
- }
- }
-
- ///
- public async Task RemoveFromRoleAsync(TUser user, string roleName, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.GetRolesList().RemoveAll(r => string.Equals(r, roleName, StringComparison.InvariantCultureIgnoreCase));
-
- var roleId = RoleStore.GetRavenIdFromRoleName(roleName, DbSession.Advanced.DocumentStore);
- var roleOrNull = await DbSession.LoadAsync(roleId, cancellationToken);
- if (roleOrNull != null)
- {
- roleOrNull.Users.Remove(user.Id);
- }
- }
-
- ///
- public Task> GetRolesAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult>(new List(user.Roles));
- }
-
- ///
- public Task IsInRoleAsync(TUser user, string roleName, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(roleName))
- {
- throw new ArgumentNullException(nameof(roleName));
- }
-
- return Task.FromResult(user.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase));
- }
-
- ///
- public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
- {
- ThrowIfDisposedOrCancelled(cancellationToken);
- if (string.IsNullOrEmpty(roleName))
- {
- throw new ArgumentNullException(nameof(roleName));
- }
-
- var users = await DbSession.Query()
- .Where(u => u.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase))
- .Take(1024)
- .ToListAsync();
- return users;
- }
-
- #endregion
-
- #region IUserPasswordStore implementation
-
- ///
- public Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.PasswordHash = passwordHash;
- return Task.CompletedTask;
- }
-
- ///
- public Task GetPasswordHashAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.PasswordHash);
- }
-
- ///
- public Task HasPasswordAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.PasswordHash != null);
- }
-
- #endregion
-
- #region IUserSecurityStampStore implementation
-
- ///
- public Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.SecurityStamp = stamp;
- return Task.CompletedTask;
- }
-
- ///
- public Task GetSecurityStampAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.SecurityStamp);
- }
-
- #endregion
-
- #region IUserEmailStore implementation
-
- ///
- public Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken)
- {
- ThrowIfDisposedOrCancelled(cancellationToken);
- user.Email = email ?? throw new ArgumentNullException(nameof(email));
- return Task.CompletedTask;
- }
-
- ///
- public Task GetEmailAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.Email);
- }
-
- ///
- public Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.EmailConfirmed);
- }
-
- ///
- public Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.EmailConfirmed = confirmed;
- return Task.CompletedTask;
- }
-
- ///
- public Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
- {
- ThrowIfDisposedOrCancelled(cancellationToken);
-
- return DbSession.Query()
- .FirstOrDefaultAsync(u => u.Email == normalizedEmail, cancellationToken);
- }
-
- ///
- public Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- // Raven string comparison queries are case-insensitive. We can just return the user name.
- return Task.FromResult(user.Email);
- }
-
- ///
- public Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
- if (string.IsNullOrEmpty(normalizedEmail))
- {
- throw new ArgumentNullException(nameof(normalizedEmail));
- }
-
- user.Email = normalizedEmail.ToLowerInvariant(); // I don't like the ALL CAPS default. We're going all lower.
- return Task.CompletedTask;
- }
-
- #endregion
-
- #region IUserLockoutStore implementation
-
- ///
- public Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.LockoutEnd);
- }
-
- ///
- public Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.LockoutEnd = lockoutEnd;
- return Task.CompletedTask;
- }
-
- ///
- public Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.AccessFailedCount++;
- return Task.FromResult(user.AccessFailedCount);
- }
-
- ///
- public Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.AccessFailedCount = 0;
- return Task.CompletedTask;
- }
-
- ///
- public Task GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.AccessFailedCount);
- }
-
- ///
- public Task GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
- return Task.FromResult(user.LockoutEnabled);
- }
-
- ///
- public Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.LockoutEnabled = enabled;
- return Task.CompletedTask;
- }
-
- #endregion
-
- #region IUserTwoFactorStore implementation
-
- ///
- public Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.TwoFactorEnabled = enabled;
- return Task.CompletedTask;
- }
-
- ///
- public Task GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.TwoFactorEnabled);
- }
-
- #endregion
-
- #region IUserPhoneNumberStore implementation
-
- ///
- public Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.PhoneNumber = phoneNumber;
- return Task.CompletedTask;
- }
-
- ///
- public Task GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.PhoneNumber);
- }
-
- ///
- public Task GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- return Task.FromResult(user.PhoneNumberConfirmed);
- }
-
- ///
- public Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken)
- {
- ThrowIfNullDisposedCancelled(user, cancellationToken);
-
- user.PhoneNumberConfirmed = confirmed;
- return Task.CompletedTask;
- }
-
- #endregion
-
- #region IUserAuthenticatorKeyStore implementation
-
- ///
- public Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken)
- {
- user.TwoFactorAuthenticatorKey = key;
- return Task.CompletedTask;
- }
-
- ///
- public Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken)
- {
- return Task.FromResult(user.TwoFactorAuthenticatorKey);
- }
-
- #endregion
-
- #region IUserAuthenticationTokenStore
-
- ///
- public async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
- {
- var id = IdentityUserAuthToken.GetWellKnownId(DbSession.Advanced.DocumentStore, user.Id, loginProvider, name);
- ThrowIfDisposedOrCancelled(cancellationToken);
-
- var existingOrNull = await DbSession.LoadAsync(id);
- if (existingOrNull == null)
- {
- existingOrNull = new IdentityUserAuthToken
- {
- Id = id,
- LoginProvider = loginProvider,
- Name = name,
- UserId = user.Id,
- Value = value
- };
- await DbSession.StoreAsync(existingOrNull);
- }
-
- existingOrNull.Value = value;
- }
-
- ///
- public Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
- {
- var id = IdentityUserAuthToken.GetWellKnownId(DbSession.Advanced.DocumentStore, user.Id, loginProvider, name);
- DbSession.Delete(id);
-
- return Task.CompletedTask;
- }
-
- ///
- public async Task GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
- {
- var id = IdentityUserAuthToken.GetWellKnownId(DbSession.Advanced.DocumentStore, user.Id, loginProvider, name);
- var tokenOrNull = await DbSession.LoadAsync(id);
- if (tokenOrNull == null)
- {
- return null;
- }
-
- return tokenOrNull.Value;
- }
-
- ///
- public Task ReplaceCodesAsync(TUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken)
- {
- user.TwoFactorRecoveryCodes = new List(recoveryCodes);
- return Task.CompletedTask;
- }
-
- ///
- public Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken)
- {
- return Task.FromResult(user.TwoFactorRecoveryCodes.Remove(code));
- }
-
- ///
- public Task CountCodesAsync(TUser user, CancellationToken cancellationToken)
- {
- return Task.FromResult(user.TwoFactorRecoveryCodes.Count);
- }
-
- #endregion
-
- #region IQueryableUserStore
-
- ///
- /// Gets the users as an IQueryable.
- ///
- public IQueryable Users => this.DbSession.Query();
-
- #endregion
-
- ///
- /// Migrates the data model from a previous version of the RavenDB.Identity framework to the v6 model.
- /// This is necessary if you stored users using an RavenDB.Identity version 5 or ealier.
- ///
- ///
- public static void MigrateToV6(IDocumentStore docStore)
- {
-#pragma warning disable CS0618 // Type or member is obsolete
- var collectionName = docStore.Conventions.FindCollectionName(typeof(IdentityUserByUserName));
-
- // Step 1: find all the old IdentityByUserName objects.
- var emails = new List<(string userId, string email)>(1000);
- using (var dbSession = docStore.OpenSession())
- {
- var stream = dbSession.Advanced.Stream($"{collectionName}/");
-#pragma warning restore CS0618 // Type or member is obsolete
- while (stream.MoveNext())
- {
- var doc = stream.Current.Document;
- emails.Add((userId: doc.UserId, email: doc.UserName));
- }
- }
-
- // Step 2: store each email as a cluster-wide compare/exchange value.
- foreach (var (userId, email) in emails)
- {
- var compareExchangeKey = GetCompareExchangeKeyFromEmail(email);
- var storeOperation = new PutCompareExchangeValueOperation(compareExchangeKey, userId, 0);
- var storeResult = docStore.Operations.Send(storeOperation);
- if (!storeResult.Successful)
- {
- var exception = new Exception($"Unable to migrate to RavenDB.Identity V6. An error occurred while storing the compare/exchange value. Before running this {nameof(MigrateToV6)} again, please delete all compare/exchange values in Raven that begin with {emailReservationKeyPrefix}.");
- exception.Data.Add("compareExchangeKey", compareExchangeKey);
- exception.Data.Add("compareExchangeValue", userId);
- throw exception;
- }
- }
-
- // Step 3: remove all IdentityUserByUserName objects.
- var operation = docStore
- .Operations
- .Send(new DeleteByQueryOperation(new Client.Documents.Queries.IndexQuery
- {
- Query = $"from {collectionName}"
- }));
- operation.WaitForCompletion();
- }
-
- private IAsyncDocumentSession DbSession
- {
- get
- {
- if (_session == null)
- {
- _session = getSessionFunc();
- }
- return _session;
- }
- }
-
- private void ThrowIfNullDisposedCancelled(TUser user, CancellationToken token)
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(this.GetType().Name);
- }
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
- token.ThrowIfCancellationRequested();
- }
-
- private void ThrowIfDisposedOrCancelled(CancellationToken token)
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(this.GetType().Name);
- }
- token.ThrowIfCancellationRequested();
- }
-
- private Task> DeleteUserEmailReservation(string email)
- {
- var key = GetCompareExchangeKeyFromEmail(email);
- var store = DbSession.Advanced.DocumentStore;
-
- var readResult = store.Operations.Send(new GetCompareExchangeValueOperation(key));
- if (readResult == null)
- {
- return Task.FromResult(new CompareExchangeResult() { Successful = false });
- }
-
- var deleteEmailOperation = new DeleteCompareExchangeValueOperation(key, readResult.Index);
- return DbSession.Advanced.DocumentStore.Operations.SendAsync(deleteEmailOperation);
- }
-
- private static string GetCompareExchangeKeyFromEmail(string email)
- {
- return emailReservationKeyPrefix + email.ToLowerInvariant();
- }
+ return IdentityResult.Success;
}
+
+ ///
+ public Task FindByIdAsync(string userId, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposedOrCancelled(cancellationToken);
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ return session.LoadAsync(userId);
+ }
+ }
+
+ ///
+ public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposedOrCancelled(cancellationToken);
+ if (string.IsNullOrEmpty(normalizedUserName))
+ {
+ throw new ArgumentNullException(nameof(normalizedUserName));
+ }
+
+ var compareExchangeKey = GetCompareExchangeKeyFromEmail(normalizedUserName);
+ var getEmailReservationOperation = new GetCompareExchangeValueOperation(compareExchangeKey);
+
+ var emailReservationResultOrNull = await _store.Operations.SendAsync(getEmailReservationOperation);
+ var userId = emailReservationResultOrNull?.Value;
+ if (string.IsNullOrEmpty(userId))
+ {
+ return null;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return await FindByIdAsync(userId, cancellationToken);
+ }
+
+ #endregion
+
+ #region IUserLoginStore implementation
+
+ ///
+ public async Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+ if (login == null)
+ {
+ throw new ArgumentNullException(nameof(login));
+ }
+
+ if (!user.Logins.Any(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey))
+ {
+ user.Logins.Add(login);
+
+ var userLogin = new IdentityUserLogin
+ {
+ Id = Util.GetLoginId(login),
+ UserId = user.Id,
+ Provider = login.LoginProvider,
+ ProviderKey = login.ProviderKey
+ };
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ await session.StoreAsync(userLogin);
+ }
+ }
+ }
+
+ ///
+ public async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ var login = new UserLoginInfo(loginProvider, providerKey, string.Empty);
+ string loginId = Util.GetLoginId(login);
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ var loginDoc = await session.LoadAsync(loginId);
+ if (loginDoc != null)
+ {
+ session.Delete(loginDoc);
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ user.Logins.RemoveAll(x => x.LoginProvider == login.LoginProvider && x.ProviderKey == login.ProviderKey);
+ }
+
+ ///
+ public Task> GetLoginsAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.Logins.ToIList());
+ }
+
+ ///
+ public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
+ {
+ var login = new UserLoginInfo(loginProvider, providerKey, string.Empty);
+ string loginId = Util.GetLoginId(login);
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ var loginDoc = await session.LoadAsync(loginId);
+ if (loginDoc != null)
+ {
+ return await session.LoadAsync(loginDoc.UserId);
+ }
+ }
+
+ return null;
+ }
+
+ #endregion
+
+ #region IUserClaimStore implementation
+
+ ///
+ public Task> GetClaimsAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ IList result = user.Claims
+ .Select(c => new Claim(c.ClaimType, c.ClaimValue))
+ .ToList();
+ return Task.FromResult(result);
+ }
+
+ ///
+ public Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.Claims.AddRange(claims.Select(c => new IdentityUserClaim { ClaimType = c.Type, ClaimValue = c.Value }));
+ return Task.CompletedTask;
+ }
+
+ ///
+ public async Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ var indexOfClaim = user.Claims.FindIndex(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value);
+ if (indexOfClaim != -1)
+ {
+ user.Claims.RemoveAt(indexOfClaim);
+ await this.AddClaimsAsync(user, new[] { newClaim }, cancellationToken);
+ }
+ }
+
+ ///
+ public Task RemoveClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.Claims.RemoveAll(identityClaim => claims.Any(c => c.Type == identityClaim.ClaimType && c.Value == identityClaim.ClaimValue));
+ return Task.CompletedTask;
+ }
+
+ ///
+ public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposedOrCancelled(cancellationToken);
+ if (claim == null)
+ {
+ throw new ArgumentNullException(nameof(claim));
+ }
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ return await session.Query()
+ .Where(u => u.Claims.Any(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value))
+ .ToListAsync();
+ }
+ }
+
+ #endregion
+
+ #region IUserRoleStore implementation
+
+ ///
+ public async Task AddToRoleAsync(TUser user, string roleName, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ // See if we have an IdentityRole with that name.
+ var identityUserCollection = _store.Conventions.GetCollectionName(typeof(TRole));
+ var prefix = _store.Conventions.TransformTypeCollectionNameToDocumentIdPrefix(identityUserCollection);
+ var identityPartSeperator = _store.Conventions.IdentityPartsSeparator;
+ var roleNameLowered = roleName.ToLowerInvariant();
+ var roleId = prefix + identityPartSeperator + roleNameLowered;
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ var existingRoleOrNull = await session.LoadAsync(roleId, cancellationToken);
+ if (existingRoleOrNull == null)
+ {
+ ThrowIfDisposedOrCancelled(cancellationToken);
+ existingRoleOrNull = new TRole();
+ existingRoleOrNull.Name = roleNameLowered;
+ await session.StoreAsync(existingRoleOrNull, roleId, cancellationToken);
+ }
+
+ // Use the real name (not normalized/uppered/lowered) of the role, as specified by the user.
+ var roleRealName = existingRoleOrNull.Name;
+ if (!user.Roles.Contains(roleRealName, StringComparer.InvariantCultureIgnoreCase))
+ {
+ user.GetRolesList().Add(roleRealName);
+ }
+
+ if (!existingRoleOrNull.Users.Contains(user.Id, StringComparer.InvariantCultureIgnoreCase))
+ {
+ existingRoleOrNull.Users.Add(user.Id);
+ }
+ }
+ }
+
+ ///
+ public async Task RemoveFromRoleAsync(TUser user, string roleName, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.GetRolesList().RemoveAll(r => string.Equals(r, roleName, StringComparison.InvariantCultureIgnoreCase));
+
+ var roleId = RoleStore.GetRavenIdFromRoleName(roleName, _store);
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ var roleOrNull = await session.LoadAsync(roleId, cancellationToken);
+ if (roleOrNull != null)
+ {
+ roleOrNull.Users.Remove(user.Id);
+ }
+ }
+ }
+
+ ///
+ public Task> GetRolesAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult>(new List(user.Roles));
+ }
+
+ ///
+ public Task IsInRoleAsync(TUser user, string roleName, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(roleName))
+ {
+ throw new ArgumentNullException(nameof(roleName));
+ }
+
+ return Task.FromResult(user.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase));
+ }
+
+ ///
+ public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposedOrCancelled(cancellationToken);
+ if (string.IsNullOrEmpty(roleName))
+ {
+ throw new ArgumentNullException(nameof(roleName));
+ }
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ return await session.Query()
+ .Where(u => u.Roles.Contains(roleName, StringComparer.InvariantCultureIgnoreCase))
+ .Take(1024)
+ .ToListAsync();
+ }
+ }
+
+ #endregion
+
+ #region IUserPasswordStore implementation
+
+ ///
+ public Task SetPasswordHashAsync(TUser user, string passwordHash, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.PasswordHash = passwordHash;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetPasswordHashAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.PasswordHash);
+ }
+
+ ///
+ public Task HasPasswordAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.PasswordHash != null);
+ }
+
+ #endregion
+
+ #region IUserSecurityStampStore implementation
+
+ ///
+ public Task SetSecurityStampAsync(TUser user, string stamp, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.SecurityStamp = stamp;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetSecurityStampAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.SecurityStamp);
+ }
+
+ #endregion
+
+ #region IUserEmailStore implementation
+
+ ///
+ public Task SetEmailAsync(TUser user, string email, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposedOrCancelled(cancellationToken);
+ user.Email = email ?? throw new ArgumentNullException(nameof(email));
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetEmailAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.Email);
+ }
+
+ ///
+ public Task GetEmailConfirmedAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.EmailConfirmed);
+ }
+
+ ///
+ public Task SetEmailConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.EmailConfirmed = confirmed;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
+ {
+ ThrowIfDisposedOrCancelled(cancellationToken);
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ return session.Query()
+ .FirstOrDefaultAsync(u => u.Email == normalizedEmail, cancellationToken);
+ }
+ }
+
+ ///
+ public Task GetNormalizedEmailAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ // Raven string comparison queries are case-insensitive. We can just return the user name.
+ return Task.FromResult(user.Email);
+ }
+
+ ///
+ public Task SetNormalizedEmailAsync(TUser user, string normalizedEmail, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+ if (string.IsNullOrEmpty(normalizedEmail))
+ {
+ throw new ArgumentNullException(nameof(normalizedEmail));
+ }
+
+ user.Email = normalizedEmail.ToLowerInvariant(); // I don't like the ALL CAPS default. We're going all lower.
+ return Task.CompletedTask;
+ }
+
+ #endregion
+
+ #region IUserLockoutStore implementation
+
+ ///
+ public Task GetLockoutEndDateAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.LockoutEnd);
+ }
+
+ ///
+ public Task SetLockoutEndDateAsync(TUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.LockoutEnd = lockoutEnd;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task IncrementAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.AccessFailedCount++;
+ return Task.FromResult(user.AccessFailedCount);
+ }
+
+ ///
+ public Task ResetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.AccessFailedCount = 0;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetAccessFailedCountAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.AccessFailedCount);
+ }
+
+ ///
+ public Task GetLockoutEnabledAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+ return Task.FromResult(user.LockoutEnabled);
+ }
+
+ ///
+ public Task SetLockoutEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.LockoutEnabled = enabled;
+ return Task.CompletedTask;
+ }
+
+ #endregion
+
+ #region IUserTwoFactorStore implementation
+
+ ///
+ public Task SetTwoFactorEnabledAsync(TUser user, bool enabled, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.TwoFactorEnabled = enabled;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetTwoFactorEnabledAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.TwoFactorEnabled);
+ }
+
+ #endregion
+
+ #region IUserPhoneNumberStore implementation
+
+ ///
+ public Task SetPhoneNumberAsync(TUser user, string phoneNumber, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.PhoneNumber = phoneNumber;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetPhoneNumberAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.PhoneNumber);
+ }
+
+ ///
+ public Task GetPhoneNumberConfirmedAsync(TUser user, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ return Task.FromResult(user.PhoneNumberConfirmed);
+ }
+
+ ///
+ public Task SetPhoneNumberConfirmedAsync(TUser user, bool confirmed, CancellationToken cancellationToken)
+ {
+ ThrowIfNullDisposedCancelled(user, cancellationToken);
+
+ user.PhoneNumberConfirmed = confirmed;
+ return Task.CompletedTask;
+ }
+
+ #endregion
+
+ #region IUserAuthenticatorKeyStore implementation
+
+ ///
+ public Task SetAuthenticatorKeyAsync(TUser user, string key, CancellationToken cancellationToken)
+ {
+ user.TwoFactorAuthenticatorKey = key;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetAuthenticatorKeyAsync(TUser user, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(user.TwoFactorAuthenticatorKey);
+ }
+
+ #endregion
+
+ #region IUserAuthenticationTokenStore
+
+ ///
+ public async Task SetTokenAsync(TUser user, string loginProvider, string name, string value, CancellationToken cancellationToken)
+ {
+ var id = IdentityUserAuthToken.GetWellKnownId(_store, user.Id, loginProvider, name);
+ ThrowIfDisposedOrCancelled(cancellationToken);
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ var existingOrNull = await session.LoadAsync(id);
+ if (existingOrNull == null)
+ {
+ existingOrNull = new IdentityUserAuthToken
+ {
+ Id = id,
+ LoginProvider = loginProvider,
+ Name = name,
+ UserId = user.Id,
+ Value = value
+ };
+ await session.StoreAsync(existingOrNull);
+ }
+
+ existingOrNull.Value = value;
+ }
+ }
+
+ ///
+ public Task RemoveTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
+ {
+ var id = IdentityUserAuthToken.GetWellKnownId(_store, user.Id, loginProvider, name);
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ session.Delete(id);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public async Task GetTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
+ {
+ var id = IdentityUserAuthToken.GetWellKnownId(_store, user.Id, loginProvider, name);
+
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ var tokenOrNull = await session.LoadAsync(id);
+ if (tokenOrNull == null)
+ {
+ return null;
+ }
+
+ return tokenOrNull.Value;
+ }
+ }
+
+ ///
+ public Task ReplaceCodesAsync(TUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken)
+ {
+ user.TwoFactorRecoveryCodes = new List(recoveryCodes);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task RedeemCodeAsync(TUser user, string code, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(user.TwoFactorRecoveryCodes.Remove(code));
+ }
+
+ ///
+ public Task CountCodesAsync(TUser user, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(user.TwoFactorRecoveryCodes.Count);
+ }
+
+ #endregion
+
+ #region IQueryableUserStore
+
+ ///
+ /// Gets the users as an IQueryable.
+ ///
+ public IQueryable Users
+ {
+ get
+ {
+ using (IAsyncDocumentSession session = _store.OpenAsyncSession())
+ {
+ return session.Query();
+ }
+ }
+ }
+
+ #endregion
+
+ private void ThrowIfNullDisposedCancelled(TUser user, CancellationToken token)
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(this.GetType().Name);
+ }
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
+ token.ThrowIfCancellationRequested();
+ }
+
+ private void ThrowIfDisposedOrCancelled(CancellationToken token)
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(this.GetType().Name);
+ }
+ token.ThrowIfCancellationRequested();
+ }
+
+ private Task> DeleteUserEmailReservation(string email)
+ {
+ var key = GetCompareExchangeKeyFromEmail(email);
+ var store = _store;
+
+ var readResult = store.Operations.Send(new GetCompareExchangeValueOperation(key));
+ if (readResult == null)
+ {
+ return Task.FromResult(new CompareExchangeResult() { Successful = false });
+ }
+
+ var deleteEmailOperation = new DeleteCompareExchangeValueOperation(key, readResult.Index);
+ return _store.Operations.SendAsync(deleteEmailOperation);
+ }
+
+ private static string GetCompareExchangeKeyFromEmail(string email)
+ {
+ return emailReservationKeyPrefix + email.ToLowerInvariant();
+ }
+ }
}
diff --git a/Samples/Mvc/Sample.Mvc.csproj b/Samples/Mvc/Sample.Mvc.csproj
index 6d9dfd2..f9b216a 100644
--- a/Samples/Mvc/Sample.Mvc.csproj
+++ b/Samples/Mvc/Sample.Mvc.csproj
@@ -11,6 +11,7 @@
+
diff --git a/Samples/RazorPages/Sample.RazorPages.csproj b/Samples/RazorPages/Sample.RazorPages.csproj
index 406eb1a..187fc3a 100644
--- a/Samples/RazorPages/Sample.RazorPages.csproj
+++ b/Samples/RazorPages/Sample.RazorPages.csproj
@@ -1,4 +1,4 @@
-
+
netcoreapp2.2
@@ -11,6 +11,7 @@
+