Add support for store versioning + IUserActivityStore

This commit is contained in:
Hao Kung 2017-05-26 15:08:57 -07:00
parent f0a6eee30e
commit be8232d3f9
42 changed files with 4617 additions and 2794 deletions

View File

@ -2,7 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
@ -131,13 +135,35 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
/// </summary>
public DbSet<TRoleClaim> RoleClaims { get; set; }
private string _version;
/// <summary>
/// Configures the schema needed for the identity framework.
/// Gets or sets the version to use during <see cref="OnModelCreating(ModelBuilder)"/>.
/// If not set, defaults to the value in the <see cref="IdentityStoreOptions.Version"/>
/// </summary>
public string Version {
get => _version ??
this.GetService<IDbContextOptions>()
.Extensions.OfType<CoreOptionsExtension>()
.FirstOrDefault()?.ApplicationServiceProvider
?.GetService<IOptions<IdentityStoreOptions>>()
?.Value?.Version;
set => _version = value;
}
/// <summary>
/// Creates the latest identity model.
/// </summary>
/// <param name="builder"></param>
protected void OnModelCreatingLatest(ModelBuilder builder)
=> OnModelCreatingV2(builder);
/// <summary>
/// Configures the schema needed for the identity framework version <see cref="IdentityStoreOptions.Version2_0"/>.
/// </summary>
/// <param name="builder">
/// The builder being used to construct the model for this context.
/// </param>
protected override void OnModelCreating(ModelBuilder builder)
protected void OnModelCreatingV2(ModelBuilder builder)
{
builder.Entity<TUser>(b =>
{
@ -152,7 +178,6 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
b.Property(u => u.Email).HasMaxLength(256);
b.Property(u => u.NormalizedEmail).HasMaxLength(256);
// Replace with b.HasMany<IdentityUserClaim>().
b.HasMany<TUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
b.HasMany<TUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
@ -173,19 +198,19 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
b.HasMany<TRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
});
builder.Entity<TUserClaim>(b =>
builder.Entity<TUserClaim>(b =>
{
b.HasKey(uc => uc.Id);
b.ToTable("AspNetUserClaims");
});
builder.Entity<TRoleClaim>(b =>
builder.Entity<TRoleClaim>(b =>
{
b.HasKey(rc => rc.Id);
b.ToTable("AspNetRoleClaims");
});
builder.Entity<TUserRole>(b =>
builder.Entity<TUserRole>(b =>
{
b.HasKey(r => new { r.UserId, r.RoleId });
b.ToTable("AspNetUserRoles");
@ -197,11 +222,53 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
b.ToTable("AspNetUserLogins");
});
builder.Entity<TUserToken>(b =>
builder.Entity<TUserToken>(b =>
{
b.HasKey(l => new { l.UserId, l.LoginProvider, l.Name });
b.ToTable("AspNetUserTokens");
});
}
/// <summary>
/// Configures the schema needed for the identity framework version <see cref="IdentityStoreOptions.Version1_0"/>.
/// </summary>
/// <param name="builder">
/// The builder being used to construct the model for this context.
/// </param>
protected void OnModelCreatingV1(ModelBuilder builder)
{
OnModelCreatingV2(builder);
// Ignore the additional 2.0 properties that were added
builder.Entity<TUser>(b =>
{
b.Ignore(u => u.LastPasswordChangeDate);
b.Ignore(u => u.LastSignInDate);
b.Ignore(u => u.CreateDate);
});
}
/// <summary>
/// Configures the schema needed for the identity framework.
/// </summary>
/// <param name="builder">
/// The builder being used to construct the model for this context.
/// </param>
protected override void OnModelCreating(ModelBuilder builder)
{
if (Version == IdentityStoreOptions.Version_Latest)
{
OnModelCreatingLatest(builder);
}
else if (Version == IdentityStoreOptions.Version2_0)
{
OnModelCreatingV2(builder);
}
else // Default is always the v1 schema
{
OnModelCreatingV1(builder);
}
}
}
}

View File

@ -15,6 +15,52 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
public static class IdentityEntityFrameworkBuilderExtensions
{
/// <summary>
/// Adds an Entity Framework implementation of identity information stores with the schema/features in
/// <see cref="IdentityStoreOptions.Version1_0"/>.
/// </summary>
/// <typeparam name="TContext">The Entity Framework database context to use.</typeparam>
/// <param name="builder">The <see cref="IdentityBuilder"/> instance this method extends.</param>
/// <returns>The <see cref="IdentityBuilder"/> instance this method extends.</returns>
public static IdentityBuilder AddEntityFrameworkStoresV1<TContext>(this IdentityBuilder builder)
where TContext : DbContext
{
builder.Services.Configure<IdentityStoreOptions>(o => o.Version = IdentityStoreOptions.Version1_0);
AddStores(builder.Services, typeof(UserStoreV1<,,,,,,,,>), typeof(RoleStoreV1<,,,,>), builder.UserType, builder.RoleType, typeof(TContext));
return builder;
}
/// <summary>
/// Adds an Entity Framework implementation of identity information stores with the schema/features in
/// <see cref="IdentityStoreOptions.Version2_0"/>.
/// </summary>
/// <typeparam name="TContext">The Entity Framework database context to use.</typeparam>
/// <param name="builder">The <see cref="IdentityBuilder"/> instance this method extends.</param>
/// <returns>The <see cref="IdentityBuilder"/> instance this method extends.</returns>
public static IdentityBuilder AddEntityFrameworkStoresV2<TContext>(this IdentityBuilder builder)
where TContext : DbContext
{
builder.Services.Configure<IdentityStoreOptions>(o => o.Version = IdentityStoreOptions.Version2_0);
// Note: RoleStore was not changed for V2.
AddStores(builder.Services, typeof(UserStoreV2<,,,,,,,,>), typeof(RoleStoreV1<,,,,>), builder.UserType, builder.RoleType, typeof(TContext));
return builder;
}
/// <summary>
/// Adds an Entity Framework implementation of identity information stores with the latest schema/features in
/// <see cref="IdentityStoreOptions.Version"/>.
/// </summary>
/// <typeparam name="TContext">The Entity Framework database context to use.</typeparam>
/// <param name="builder">The <see cref="IdentityBuilder"/> instance this method extends.</param>
/// <returns>The <see cref="IdentityBuilder"/> instance this method extends.</returns>
public static IdentityBuilder AddEntityFrameworkStoresLatest<TContext>(this IdentityBuilder builder)
where TContext : DbContext
{
builder.Services.Configure<IdentityStoreOptions>(o => o.Version = IdentityStoreOptions.Version_Latest);
AddStores(builder.Services, typeof(UserStore<,,,,,,,,>), typeof(RoleStore<,,,,>), builder.UserType, builder.RoleType, typeof(TContext));
return builder;
}
/// <summary>
/// Adds an Entity Framework implementation of identity information stores.
/// </summary>
@ -23,12 +69,9 @@ namespace Microsoft.Extensions.DependencyInjection
/// <returns>The <see cref="IdentityBuilder"/> instance this method extends.</returns>
public static IdentityBuilder AddEntityFrameworkStores<TContext>(this IdentityBuilder builder)
where TContext : DbContext
{
AddStores(builder.Services, builder.UserType, builder.RoleType, typeof(TContext));
return builder;
}
=> builder.AddEntityFrameworkStoresV1<TContext>();
private static void AddStores(IServiceCollection services, Type userType, Type roleType, Type contextType)
private static void AddStores(IServiceCollection services, Type userStore, Type roleStore, Type userType, Type roleType, Type contextType)
{
var identityUserType = FindGenericBaseType(userType, typeof(IdentityUser<,,,,>));
if (identityUserType == null)
@ -43,7 +86,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddScoped(
typeof(IUserStore<>).MakeGenericType(userType),
typeof(UserStore<,,,,,,,,>).MakeGenericType(userType, roleType, contextType,
userStore.MakeGenericType(userType, roleType, contextType,
identityUserType.GenericTypeArguments[0],
identityUserType.GenericTypeArguments[1],
identityUserType.GenericTypeArguments[2],
@ -52,7 +95,7 @@ namespace Microsoft.Extensions.DependencyInjection
identityRoleType.GenericTypeArguments[2]));
services.TryAddScoped(
typeof(IRoleStore<>).MakeGenericType(roleType),
typeof(RoleStore<,,,,>).MakeGenericType(roleType, contextType,
roleStore.MakeGenericType(roleType, contextType,
identityRoleType.GenericTypeArguments[0],
identityRoleType.GenericTypeArguments[1],
identityRoleType.GenericTypeArguments[2]));

View File

@ -2,70 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
/// <summary>
/// Creates a new instance of a persistence store for roles.
/// </summary>
/// <typeparam name="TRole">The type of the class representing a role</typeparam>
public class RoleStore<TRole> : RoleStore<TRole, DbContext, string>
where TRole : IdentityRole<string>
{
/// <summary>
/// Constructs a new instance of <see cref="RoleStore{TRole}"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public RoleStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Creates a new instance of a persistence store for roles.
/// </summary>
/// <typeparam name="TRole">The type of the class representing a role.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
public class RoleStore<TRole, TContext> : RoleStore<TRole, TContext, string>
where TRole : IdentityRole<string>
where TContext : DbContext
{
/// <summary>
/// Constructs a new instance of <see cref="RoleStore{TRole, TContext}"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public RoleStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Creates a new instance of a persistence store for roles.
/// </summary>
/// <typeparam name="TRole">The type of the class representing a role.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
public class RoleStore<TRole, TContext, TKey> : RoleStore<TRole, TContext, TKey, IdentityUserRole<TKey>, IdentityRoleClaim<TKey>>,
IQueryableRoleStore<TRole>,
IRoleClaimStore<TRole>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
where TContext : DbContext
{
/// <summary>
/// Constructs a new instance of <see cref="RoleStore{TRole, TContext, TKey}"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public RoleStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Creates a new instance of a persistence store for roles.
/// </summary>
@ -75,8 +15,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
/// <typeparam name="TUserRole">The type of the class representing a user role.</typeparam>
/// <typeparam name="TRoleClaim">The type of the class representing a role claim.</typeparam>
public class RoleStore<TRole, TContext, TKey, TUserRole, TRoleClaim> :
IQueryableRoleStore<TRole>,
IRoleClaimStore<TRole>
RoleStoreV1<TRole, TContext, TKey, TUserRole, TRoleClaim>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TKey : IEquatable<TKey>
where TContext : DbContext
@ -88,368 +27,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public RoleStore(TContext context, IdentityErrorDescriber describer = null)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
Context = context;
ErrorDescriber = describer ?? new IdentityErrorDescriber();
}
private bool _disposed;
/// <summary>
/// Gets the database context for this store.
/// </summary>
public TContext Context { get; private set; }
/// <summary>
/// Gets or sets the <see cref="IdentityErrorDescriber"/> for any error that occurred with the current operation.
/// </summary>
public IdentityErrorDescriber ErrorDescriber { get; set; }
/// <summary>
/// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
/// </summary>
/// <value>
/// True if changes should be automatically persisted, otherwise false.
/// </value>
public bool AutoSaveChanges { get; set; } = true;
/// <summary>Saves the current store.</summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
private async Task SaveChanges(CancellationToken cancellationToken)
{
if (AutoSaveChanges)
{
await Context.SaveChangesAsync(cancellationToken);
}
}
/// <summary>
/// Creates a new role in a store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to create in the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public async virtual Task<IdentityResult> CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
Context.Add(role);
await SaveChanges(cancellationToken);
return IdentityResult.Success;
}
/// <summary>
/// Updates a role in a store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to update in the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public async virtual Task<IdentityResult> UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
Context.Attach(role);
role.ConcurrencyStamp = Guid.NewGuid().ToString();
Context.Update(role);
try
{
await SaveChanges(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Deletes a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to delete from the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public async virtual Task<IdentityResult> DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
Context.Remove(role);
try
{
await SaveChanges(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Gets the ID for a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose ID should be returned.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the ID of the role.</returns>
public virtual Task<string> GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(ConvertIdToString(role.Id));
}
/// <summary>
/// Gets the name of a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose name should be returned.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the name of the role.</returns>
public virtual Task<string> GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(role.Name);
}
/// <summary>
/// Sets the name of a role in the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose name should be set.</param>
/// <param name="roleName">The name of the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual 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 TaskCache.CompletedTask;
}
/// <summary>
/// Converts the provided <paramref name="id"/> to a strongly typed key object.
/// </summary>
/// <param name="id">The id to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided <paramref name="id"/>.</returns>
public virtual TKey ConvertIdFromString(string id)
{
if (id == null)
{
return default(TKey);
}
return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id);
}
/// <summary>
/// Converts the provided <paramref name="id"/> to its string representation.
/// </summary>
/// <param name="id">The id to convert.</param>
/// <returns>An <see cref="string"/> representation of the provided <paramref name="id"/>.</returns>
public virtual string ConvertIdToString(TKey id)
{
if (id.Equals(default(TKey)))
{
return null;
}
return id.ToString();
}
/// <summary>
/// Finds the role who has the specified ID as an asynchronous operation.
/// </summary>
/// <param name="id">The role ID to look for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that result of the look up.</returns>
public virtual Task<TRole> FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var roleId = ConvertIdFromString(id);
return Roles.FirstOrDefaultAsync(u => u.Id.Equals(roleId), cancellationToken);
}
/// <summary>
/// Finds the role who has the specified normalized name as an asynchronous operation.
/// </summary>
/// <param name="normalizedName">The normalized role name to look for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that result of the look up.</returns>
public virtual Task<TRole> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return Roles.FirstOrDefaultAsync(r => r.NormalizedName == normalizedName, cancellationToken);
}
/// <summary>
/// Get a role's normalized name as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose normalized name should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the name of the role.</returns>
public virtual Task<string> GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(role.NormalizedName);
}
/// <summary>
/// Set a role's normalized name as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose normalized name should be set.</param>
/// <param name="normalizedName">The normalized name to set</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
role.NormalizedName = normalizedName;
return TaskCache.CompletedTask;
}
/// <summary>
/// Throws if this class has been disposed.
/// </summary>
protected void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
}
/// <summary>
/// Dispose the stores
/// </summary>
public void Dispose()
{
_disposed = true;
}
/// <summary>
/// Get the claims associated with the specified <paramref name="role"/> as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a role.</returns>
public async virtual Task<IList<Claim>> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(cancellationToken);
}
/// <summary>
/// Adds the <paramref name="claim"/> given to the specified <paramref name="role"/>.
/// </summary>
/// <param name="role">The role to add the claim to.</param>
/// <param name="claim">The claim to add to the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual 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));
}
RoleClaims.Add(CreateRoleClaim(role, claim));
return Task.FromResult(false);
}
/// <summary>
/// Removes the <paramref name="claim"/> given from the specified <paramref name="role"/>.
/// </summary>
/// <param name="role">The role to remove the claim from.</param>
/// <param name="claim">The claim to remove from the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async virtual 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));
}
var claims = await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id) && rc.ClaimValue == claim.Value && rc.ClaimType == claim.Type).ToListAsync(cancellationToken);
foreach (var c in claims)
{
RoleClaims.Remove(c);
}
}
/// <summary>
/// A navigation property for the roles the store contains.
/// </summary>
public virtual IQueryable<TRole> Roles
{
get { return Context.Set<TRole>(); }
}
private DbSet<TRoleClaim> RoleClaims { get { return Context.Set<TRoleClaim>(); } }
/// <summary>
/// Creates a entity representing a role claim.
/// </summary>
/// <param name="role">The associated role.</param>
/// <param name="claim">The associated claim.</param>
/// <returns>The role claim entity.</returns>
protected virtual TRoleClaim CreateRoleClaim(TRole role, Claim claim)
{
return new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value };
}
public RoleStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer)
{ }
}
}

View File

@ -0,0 +1,254 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
/// <summary>
/// Creates a new instance of a persistence store for roles.
/// </summary>
/// <typeparam name="TRole">The type of the class representing a role.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
/// <typeparam name="TUserRole">The type of the class representing a user role.</typeparam>
/// <typeparam name="TRoleClaim">The type of the class representing a role claim.</typeparam>
public class RoleStoreV1<TRole, TContext, TKey, TUserRole, TRoleClaim> :
RoleStoreBaseV1<TRole, TKey, TUserRole, TRoleClaim>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TKey : IEquatable<TKey>
where TContext : DbContext
where TUserRole : IdentityUserRole<TKey>, new()
where TRoleClaim : IdentityRoleClaim<TKey>, new()
{
/// <summary>
/// Constructs a new instance of <see cref="RoleStore{TRole, TContext, TKey, TUserRole, TRoleClaim}"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public RoleStoreV1(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber())
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
Context = context;
}
/// <summary>
/// Gets the database context for this store.
/// </summary>
public TContext Context { get; private set; }
/// <summary>
/// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
/// </summary>
/// <value>
/// True if changes should be automatically persisted, otherwise false.
/// </value>
public bool AutoSaveChanges { get; set; } = true;
/// <summary>Saves the current store.</summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
private async Task SaveChanges(CancellationToken cancellationToken)
{
if (AutoSaveChanges)
{
await Context.SaveChangesAsync(cancellationToken);
}
}
/// <summary>
/// Creates a new role in a store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to create in the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public override async Task<IdentityResult> CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
Context.Add(role);
await SaveChanges(cancellationToken);
return IdentityResult.Success;
}
/// <summary>
/// Updates a role in a store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to update in the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public async override Task<IdentityResult> UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
Context.Attach(role);
role.ConcurrencyStamp = Guid.NewGuid().ToString();
Context.Update(role);
try
{
await SaveChanges(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Deletes a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to delete from the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public async override Task<IdentityResult> DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
Context.Remove(role);
try
{
await SaveChanges(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Finds the role who has the specified ID as an asynchronous operation.
/// </summary>
/// <param name="id">The role ID to look for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that result of the look up.</returns>
public override Task<TRole> FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var roleId = ConvertIdFromString(id);
return Roles.FirstOrDefaultAsync(u => u.Id.Equals(roleId), cancellationToken);
}
/// <summary>
/// Finds the role who has the specified normalized name as an asynchronous operation.
/// </summary>
/// <param name="normalizedName">The normalized role name to look for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that result of the look up.</returns>
public override Task<TRole> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return Roles.FirstOrDefaultAsync(r => r.NormalizedName == normalizedName, cancellationToken);
}
/// <summary>
/// Get the claims associated with the specified <paramref name="role"/> as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a role.</returns>
public async override Task<IList<Claim>> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id)).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToListAsync(cancellationToken);
}
/// <summary>
/// Adds the <paramref name="claim"/> given to the specified <paramref name="role"/>.
/// </summary>
/// <param name="role">The role to add the claim to.</param>
/// <param name="claim">The claim to add to the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override 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));
}
RoleClaims.Add(CreateRoleClaim(role, claim));
return Task.FromResult(false);
}
/// <summary>
/// Removes the <paramref name="claim"/> given from the specified <paramref name="role"/>.
/// </summary>
/// <param name="role">The role to remove the claim from.</param>
/// <param name="claim">The claim to remove from the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override 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));
}
var claims = await RoleClaims.Where(rc => rc.RoleId.Equals(role.Id) && rc.ClaimValue == claim.Value && rc.ClaimType == claim.Type).ToListAsync(cancellationToken);
foreach (var c in claims)
{
RoleClaims.Remove(c);
}
}
/// <summary>
/// A navigation property for the roles the store contains.
/// </summary>
public override IQueryable<TRole> Roles
{
get { return Context.Set<TRole>(); }
}
private DbSet<TRoleClaim> RoleClaims { get { return Context.Set<TRoleClaim>(); } }
/// <summary>
/// Creates a entity representing a role claim.
/// </summary>
/// <param name="role">The associated role.</param>
/// <param name="claim">The associated claim.</param>
/// <returns>The role claim entity.</returns>
public override TRoleClaim CreateRoleClaim(TRole role, Claim claim)
{
return new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value };
}
}
}

View File

@ -2,86 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
/// <summary>
/// Represents a new instance of a persistence store for users, using the default implementation
/// of <see cref="IdentityUser{TKey}"/> with a string as a primary key.
/// </summary>
public class UserStore : UserStore<IdentityUser<string>>
{
/// <summary>
/// Constructs a new instance of <see cref="UserStore"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public UserStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Creates a new instance of a persistence store for the specified user type.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
public class UserStore<TUser> : UserStore<TUser, IdentityRole, DbContext, string>
where TUser : IdentityUser<string>, new()
{
/// <summary>
/// Constructs a new instance of <see cref="UserStore{TUser}"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public UserStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TRole">The type representing a role.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
public class UserStore<TUser, TRole, TContext> : UserStore<TUser, TRole, TContext, string>
where TUser : IdentityUser<string>
where TRole : IdentityRole<string>
where TContext : DbContext
{
/// <summary>
/// Constructs a new instance of <see cref="UserStore{TUser, TRole, TContext}"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public UserStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TRole">The type representing a role.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
public class UserStore<TUser, TRole, TContext, TKey> : UserStore<TUser, TRole, TContext, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>, IdentityUserLogin<TKey>, IdentityUserToken<TKey>, IdentityRoleClaim<TKey>>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TContext : DbContext
where TKey : IEquatable<TKey>
{
/// <summary>
/// Constructs a new instance of <see cref="UserStore{TUser, TRole, TContext, TKey}"/>.
/// </summary>
/// <param name="context">The <see cref="DbContext"/>.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public UserStore(TContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types.
/// </summary>
@ -95,20 +19,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
/// <typeparam name="TUserToken">The type representing a user token.</typeparam>
/// <typeparam name="TRoleClaim">The type representing a role claim.</typeparam>
public class UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>,
IUserLoginStore<TUser>,
IUserRoleStore<TUser>,
IUserClaimStore<TUser>,
IUserPasswordStore<TUser>,
IUserSecurityStampStore<TUser>,
IUserEmailStore<TUser>,
IUserLockoutStore<TUser>,
IUserPhoneNumberStore<TUser>,
IQueryableUserStore<TUser>,
IUserTwoFactorStore<TUser>,
IUserAuthenticationTokenStore<TUser>,
IUserAuthenticatorKeyStore<TUser>,
IUserTwoFactorRecoveryCodeStore<TUser>
UserStoreV2<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>
where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TContext : DbContext
@ -124,622 +35,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
/// </summary>
/// <param name="context">The context used to access the store.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/> used to describe store errors.</param>
public UserStore(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber())
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
Context = context;
}
/// <summary>
/// Gets the database context for this store.
/// </summary>
public TContext Context { get; private set; }
private DbSet<TUser> UsersSet { get { return Context.Set<TUser>(); } }
private DbSet<TRole> Roles { get { return Context.Set<TRole>(); } }
private DbSet<TUserClaim> UserClaims { get { return Context.Set<TUserClaim>(); } }
private DbSet<TUserRole> UserRoles { get { return Context.Set<TUserRole>(); } }
private DbSet<TUserLogin> UserLogins { get { return Context.Set<TUserLogin>(); } }
private DbSet<TUserToken> UserTokens { get { return Context.Set<TUserToken>(); } }
/// <summary>
/// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
/// </summary>
/// <value>
/// True if changes should be automatically persisted, otherwise false.
/// </value>
public bool AutoSaveChanges { get; set; } = true;
/// <summary>Saves the current store.</summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
protected Task SaveChanges(CancellationToken cancellationToken)
{
return AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : TaskCache.CompletedTask;
}
/// <summary>
/// Creates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the creation operation.</returns>
public async override Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Context.Add(user);
await SaveChanges(cancellationToken);
return IdentityResult.Success;
}
/// <summary>
/// Updates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public async override Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Context.Attach(user);
user.ConcurrencyStamp = Guid.NewGuid().ToString();
Context.Update(user);
try
{
await SaveChanges(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Deletes the specified <paramref name="user"/> from the user store.
/// </summary>
/// <param name="user">The user to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public async override Task<IdentityResult> DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Context.Remove(user);
try
{
await SaveChanges(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Finds and returns a user, if any, who has the specified <paramref name="userId"/>.
/// </summary>
/// <param name="userId">The user ID to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="userId"/> if it exists.
/// </returns>
public override Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var id = ConvertIdFromString(userId);
return UsersSet.FindAsync(new object[] { id }, cancellationToken);
}
/// <summary>
/// Finds and returns a user, if any, who has the specified normalized user name.
/// </summary>
/// <param name="normalizedUserName">The normalized user name to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="normalizedUserName"/> if it exists.
/// </returns>
public override Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return Users.FirstOrDefaultAsync(u => u.NormalizedUserName == normalizedUserName, cancellationToken);
}
/// <summary>
/// A navigation property for the users the store contains.
/// </summary>
public override IQueryable<TUser> Users
{
get { return UsersSet; }
}
/// <summary>
/// Return a role with the normalized name if it exists.
/// </summary>
/// <param name="normalizedRoleName">The normalized role name.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The role if it exists.</returns>
protected override Task<TRole> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
return Roles.SingleOrDefaultAsync(r => r.NormalizedName == normalizedRoleName, cancellationToken);
}
/// <summary>
/// Return a user role for the userId and roleId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="roleId">The role's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user role if it exists.</returns>
protected override Task<TUserRole> FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken)
{
return UserRoles.FindAsync(new object[] { userId, roleId }, cancellationToken);
}
/// <summary>
/// Return a user with the matching userId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user if it exists.</returns>
protected override Task<TUser> FindUserAsync(TKey userId, CancellationToken cancellationToken)
{
return Users.SingleOrDefaultAsync(u => u.Id.Equals(userId), cancellationToken);
}
/// <summary>
/// Return a user login with the matching userId, provider, providerKey if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
protected override Task<TUserLogin> FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
{
return UserLogins.SingleOrDefaultAsync(userLogin => userLogin.UserId.Equals(userId) && userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey, cancellationToken);
}
/// <summary>
/// Return a user login with provider, providerKey if it exists.
/// </summary>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
protected override Task<TUserLogin> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{
return UserLogins.SingleOrDefaultAsync(userLogin => userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey, cancellationToken);
}
/// <summary>
/// Adds the given <paramref name="normalizedRoleName"/> to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the role to.</param>
/// <param name="normalizedRoleName">The role to add.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName));
}
var roleEntity = await FindRoleAsync(normalizedRoleName, cancellationToken);
if (roleEntity == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.RoleNotFound, normalizedRoleName));
}
UserRoles.Add(CreateUserRole(user, roleEntity));
}
/// <summary>
/// Removes the given <paramref name="normalizedRoleName"/> from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the role from.</param>
/// <param name="normalizedRoleName">The role to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName));
}
var roleEntity = await FindRoleAsync(normalizedRoleName, cancellationToken);
if (roleEntity != null)
{
var userRole = await FindUserRoleAsync(user.Id, roleEntity.Id, cancellationToken);
if (userRole != null)
{
UserRoles.Remove(userRole);
}
}
}
/// <summary>
/// Retrieves the roles the specified <paramref name="user"/> is a member of.
/// </summary>
/// <param name="user">The user whose roles should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the roles the user is a member of.</returns>
public override async Task<IList<string>> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = user.Id;
var query = from userRole in UserRoles
join role in Roles on userRole.RoleId equals role.Id
where userRole.UserId.Equals(userId)
select role.Name;
return await query.ToListAsync();
}
/// <summary>
/// Returns a flag indicating if the specified user is a member of the give <paramref name="normalizedRoleName"/>.
/// </summary>
/// <param name="user">The user whose role membership should be checked.</param>
/// <param name="normalizedRoleName">The role to check membership of</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> containing a flag indicating if the specified user is a member of the given group. If the
/// user is a member of the group the returned value with be true, otherwise it will be false.</returns>
public override async Task<bool> IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName));
}
var role = await FindRoleAsync(normalizedRoleName, cancellationToken);
if (role != null)
{
var userRole = await FindUserRoleAsync(user.Id, role.Id, cancellationToken);
return userRole != null;
}
return false;
}
/// <summary>
/// Get the claims associated with the specified <paramref name="user"/> as an asynchronous operation.
/// </summary>
/// <param name="user">The user whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a user.</returns>
public async override Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return await UserClaims.Where(uc => uc.UserId.Equals(user.Id)).Select(c => c.ToClaim()).ToListAsync(cancellationToken);
}
/// <summary>
/// Adds the <paramref name="claims"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the claim to.</param>
/// <param name="claims">The claim to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claims == null)
{
throw new ArgumentNullException(nameof(claims));
}
foreach (var claim in claims)
{
UserClaims.Add(CreateUserClaim(user, claim));
}
return Task.FromResult(false);
}
/// <summary>
/// Replaces the <paramref name="claim"/> on the specified <paramref name="user"/>, with the <paramref name="newClaim"/>.
/// </summary>
/// <param name="user">The user to replace the claim on.</param>
/// <param name="claim">The claim replace.</param>
/// <param name="newClaim">The new claim replacing the <paramref name="claim"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claim == null)
{
throw new ArgumentNullException(nameof(claim));
}
if (newClaim == null)
{
throw new ArgumentNullException(nameof(newClaim));
}
var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken);
foreach (var matchedClaim in matchedClaims)
{
matchedClaim.ClaimValue = newClaim.Value;
matchedClaim.ClaimType = newClaim.Type;
}
}
/// <summary>
/// Removes the <paramref name="claims"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the claims from.</param>
/// <param name="claims">The claim to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claims == null)
{
throw new ArgumentNullException(nameof(claims));
}
foreach (var claim in claims)
{
var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken);
foreach (var c in matchedClaims)
{
UserClaims.Remove(c);
}
}
}
/// <summary>
/// Adds the <paramref name="login"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the login to.</param>
/// <param name="login">The login to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddLoginAsync(TUser user, UserLoginInfo login,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (login == null)
{
throw new ArgumentNullException(nameof(login));
}
UserLogins.Add(CreateUserLogin(user, login));
return Task.FromResult(false);
}
/// <summary>
/// Removes the <paramref name="loginProvider"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the login from.</param>
/// <param name="loginProvider">The login to remove from the user.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var entry = await FindUserLoginAsync(user.Id, loginProvider, providerKey, cancellationToken);
if (entry != null)
{
UserLogins.Remove(entry);
}
}
/// <summary>
/// Retrieves the associated logins for the specified <param ref="user"/>.
/// </summary>
/// <param name="user">The user whose associated logins to retrieve.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing a list of <see cref="UserLoginInfo"/> for the specified <paramref name="user"/>, if any.
/// </returns>
public async override Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = user.Id;
return await UserLogins.Where(l => l.UserId.Equals(userId))
.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToListAsync(cancellationToken);
}
/// <summary>
/// Retrieves the user associated with the specified login provider and login provider key.
/// </summary>
/// <param name="loginProvider">The login provider who provided the <paramref name="providerKey"/>.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing the user, if any which matched the specified login provider and key.
/// </returns>
public async override Task<TUser> FindByLoginAsync(string loginProvider, string providerKey,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken);
if (userLogin != null)
{
return await FindUserAsync(userLogin.UserId, cancellationToken);
}
return null;
}
/// <summary>
/// Gets the user, if any, associated with the specified, normalized email address.
/// </summary>
/// <param name="normalizedEmail">The normalized email address to return the user for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address.
/// </returns>
public override Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken);
}
/// <summary>
/// Retrieves all users with the specified claim.
/// </summary>
/// <param name="claim">The claim whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that contain the specified claim.
/// </returns>
public async override Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (claim == null)
{
throw new ArgumentNullException(nameof(claim));
}
var query = from userclaims in UserClaims
join user in Users on userclaims.UserId equals user.Id
where userclaims.ClaimValue == claim.Value
&& userclaims.ClaimType == claim.Type
select user;
return await query.ToListAsync(cancellationToken);
}
/// <summary>
/// Retrieves all users in the specified role.
/// </summary>
/// <param name="normalizedRoleName">The role whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that are in the specified role.
/// </returns>
public async override Task<IList<TUser>> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (string.IsNullOrEmpty(normalizedRoleName))
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
var role = await FindRoleAsync(normalizedRoleName, cancellationToken);
if (role != null)
{
var query = from userrole in UserRoles
join user in Users on userrole.UserId equals user.Id
where userrole.RoleId.Equals(role.Id)
select user;
return await query.ToListAsync(cancellationToken);
}
return new List<TUser>();
}
/// <summary>
/// Find a user token if it exists.
/// </summary>
/// <param name="user">The token owner.</param>
/// <param name="loginProvider">The login provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user token if it exists.</returns>
protected override Task<TUserToken> FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
=> UserTokens.FindAsync(new object[] { user.Id, loginProvider, name }, cancellationToken);
/// <summary>
/// Add a new user token.
/// </summary>
/// <param name="token">The token to be added.</param>
/// <returns></returns>
protected override Task AddUserTokenAsync(TUserToken token)
{
UserTokens.Add(token);
return TaskCache.CompletedTask;
}
/// <summary>
/// Remove a new user token.
/// </summary>
/// <param name="token">The token to be removed.</param>
/// <returns></returns>
protected override Task RemoveUserTokenAsync(TUserToken token)
{
UserTokens.Remove(token);
return TaskCache.CompletedTask;
}
public UserStore(TContext context, IdentityErrorDescriber describer) : base(context, describer)
{ }
}
}

View File

@ -0,0 +1,650 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
internal class UserStoreCore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>
where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TContext : DbContext
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserRole : IdentityUserRole<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
where TRoleClaim : IdentityRoleClaim<TKey>, new()
{
/// <summary>
/// Creates a new instance of the store.
/// </summary>
/// <param name="context">The context used to access the store.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/> used to describe store errors.</param>
public UserStoreCore(TContext context, IdentityErrorDescriber describer) : base(describer)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
Context = context;
}
/// <summary>
/// Gets the database context for this store.
/// </summary>
public TContext Context { get; private set; }
public DbSet<TUser> UsersSet { get { return Context.Set<TUser>(); } }
public DbSet<TRole> Roles { get { return Context.Set<TRole>(); } }
public DbSet<TUserClaim> UserClaims { get { return Context.Set<TUserClaim>(); } }
public DbSet<TUserRole> UserRoles { get { return Context.Set<TUserRole>(); } }
public DbSet<TUserLogin> UserLogins { get { return Context.Set<TUserLogin>(); } }
public DbSet<TUserToken> UserTokens { get { return Context.Set<TUserToken>(); } }
/// <summary>
/// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
/// </summary>
/// <value>
/// True if changes should be automatically persisted, otherwise false.
/// </value>
public bool AutoSaveChanges { get; set; } = true;
/// <summary>Saves the current store.</summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public Task SaveChanges(CancellationToken cancellationToken)
{
return AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : TaskCache.CompletedTask;
}
/// <summary>
/// Creates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the creation operation.</returns>
public async override Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Context.Add(user);
await SaveChanges(cancellationToken);
return IdentityResult.Success;
}
/// <summary>
/// Updates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public async override Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Context.Attach(user);
user.ConcurrencyStamp = Guid.NewGuid().ToString();
Context.Update(user);
try
{
await SaveChanges(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Deletes the specified <paramref name="user"/> from the user store.
/// </summary>
/// <param name="user">The user to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public async override Task<IdentityResult> DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Context.Remove(user);
try
{
await SaveChanges(cancellationToken);
}
catch (DbUpdateConcurrencyException)
{
return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
}
return IdentityResult.Success;
}
/// <summary>
/// Finds and returns a user, if any, who has the specified <paramref name="userId"/>.
/// </summary>
/// <param name="userId">The user ID to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="userId"/> if it exists.
/// </returns>
public override Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var id = ConvertIdFromString(userId);
return UsersSet.FindAsync(new object[] { id }, cancellationToken);
}
/// <summary>
/// Finds and returns a user, if any, who has the specified normalized user name.
/// </summary>
/// <param name="normalizedUserName">The normalized user name to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="normalizedUserName"/> if it exists.
/// </returns>
public override Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return Users.FirstOrDefaultAsync(u => u.NormalizedUserName == normalizedUserName, cancellationToken);
}
/// <summary>
/// A navigation property for the users the store contains.
/// </summary>
public override IQueryable<TUser> Users
{
get { return UsersSet; }
}
/// <summary>
/// Return a role with the normalized name if it exists.
/// </summary>
/// <param name="normalizedRoleName">The normalized role name.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The role if it exists.</returns>
public override Task<TRole> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken)
{
return Roles.SingleOrDefaultAsync(r => r.NormalizedName == normalizedRoleName, cancellationToken);
}
/// <summary>
/// Return a user role for the userId and roleId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="roleId">The role's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user role if it exists.</returns>
public override Task<TUserRole> FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken)
{
return UserRoles.FindAsync(new object[] { userId, roleId }, cancellationToken);
}
/// <summary>
/// Return a user with the matching userId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user if it exists.</returns>
public override Task<TUser> FindUserAsync(TKey userId, CancellationToken cancellationToken)
{
return Users.SingleOrDefaultAsync(u => u.Id.Equals(userId), cancellationToken);
}
/// <summary>
/// Return a user login with the matching userId, provider, providerKey if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
public override Task<TUserLogin> FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
{
return UserLogins.SingleOrDefaultAsync(userLogin => userLogin.UserId.Equals(userId) && userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey, cancellationToken);
}
/// <summary>
/// Return a user login with provider, providerKey if it exists.
/// </summary>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
public override Task<TUserLogin> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{
return UserLogins.SingleOrDefaultAsync(userLogin => userLogin.LoginProvider == loginProvider && userLogin.ProviderKey == providerKey, cancellationToken);
}
/// <summary>
/// Adds the given <paramref name="normalizedRoleName"/> to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the role to.</param>
/// <param name="normalizedRoleName">The role to add.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName));
}
var roleEntity = await FindRoleAsync(normalizedRoleName, cancellationToken);
if (roleEntity == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.RoleNotFound, normalizedRoleName));
}
UserRoles.Add(CreateUserRole(user, roleEntity));
}
/// <summary>
/// Removes the given <paramref name="normalizedRoleName"/> from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the role from.</param>
/// <param name="normalizedRoleName">The role to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName));
}
var roleEntity = await FindRoleAsync(normalizedRoleName, cancellationToken);
if (roleEntity != null)
{
var userRole = await FindUserRoleAsync(user.Id, roleEntity.Id, cancellationToken);
if (userRole != null)
{
UserRoles.Remove(userRole);
}
}
}
/// <summary>
/// Retrieves the roles the specified <paramref name="user"/> is a member of.
/// </summary>
/// <param name="user">The user whose roles should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the roles the user is a member of.</returns>
public override async Task<IList<string>> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = user.Id;
var query = from userRole in UserRoles
join role in Roles on userRole.RoleId equals role.Id
where userRole.UserId.Equals(userId)
select role.Name;
return await query.ToListAsync();
}
/// <summary>
/// Returns a flag indicating if the specified user is a member of the give <paramref name="normalizedRoleName"/>.
/// </summary>
/// <param name="user">The user whose role membership should be checked.</param>
/// <param name="normalizedRoleName">The role to check membership of</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> containing a flag indicating if the specified user is a member of the given group. If the
/// user is a member of the group the returned value with be true, otherwise it will be false.</returns>
public override async Task<bool> IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (string.IsNullOrWhiteSpace(normalizedRoleName))
{
throw new ArgumentException(Resources.ValueCannotBeNullOrEmpty, nameof(normalizedRoleName));
}
var role = await FindRoleAsync(normalizedRoleName, cancellationToken);
if (role != null)
{
var userRole = await FindUserRoleAsync(user.Id, role.Id, cancellationToken);
return userRole != null;
}
return false;
}
/// <summary>
/// Get the claims associated with the specified <paramref name="user"/> as an asynchronous operation.
/// </summary>
/// <param name="user">The user whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a user.</returns>
public async override Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return await UserClaims.Where(uc => uc.UserId.Equals(user.Id)).Select(c => c.ToClaim()).ToListAsync(cancellationToken);
}
/// <summary>
/// Adds the <paramref name="claims"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the claim to.</param>
/// <param name="claims">The claim to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claims == null)
{
throw new ArgumentNullException(nameof(claims));
}
foreach (var claim in claims)
{
UserClaims.Add(CreateUserClaim(user, claim));
}
return Task.FromResult(false);
}
/// <summary>
/// Replaces the <paramref name="claim"/> on the specified <paramref name="user"/>, with the <paramref name="newClaim"/>.
/// </summary>
/// <param name="user">The user to replace the claim on.</param>
/// <param name="claim">The claim replace.</param>
/// <param name="newClaim">The new claim replacing the <paramref name="claim"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claim == null)
{
throw new ArgumentNullException(nameof(claim));
}
if (newClaim == null)
{
throw new ArgumentNullException(nameof(newClaim));
}
var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken);
foreach (var matchedClaim in matchedClaims)
{
matchedClaim.ClaimValue = newClaim.Value;
matchedClaim.ClaimType = newClaim.Type;
}
}
/// <summary>
/// Removes the <paramref name="claims"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the claims from.</param>
/// <param name="claims">The claim to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public async override Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
{
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (claims == null)
{
throw new ArgumentNullException(nameof(claims));
}
foreach (var claim in claims)
{
var matchedClaims = await UserClaims.Where(uc => uc.UserId.Equals(user.Id) && uc.ClaimValue == claim.Value && uc.ClaimType == claim.Type).ToListAsync(cancellationToken);
foreach (var c in matchedClaims)
{
UserClaims.Remove(c);
}
}
}
/// <summary>
/// Adds the <paramref name="login"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the login to.</param>
/// <param name="login">The login to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddLoginAsync(TUser user, UserLoginInfo login,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (login == null)
{
throw new ArgumentNullException(nameof(login));
}
UserLogins.Add(CreateUserLogin(user, login));
return Task.FromResult(false);
}
/// <summary>
/// Removes the <paramref name="loginProvider"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the login from.</param>
/// <param name="loginProvider">The login to remove from the user.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override async Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var entry = await FindUserLoginAsync(user.Id, loginProvider, providerKey, cancellationToken);
if (entry != null)
{
UserLogins.Remove(entry);
}
}
/// <summary>
/// Retrieves the associated logins for the specified <param ref="user"/>.
/// </summary>
/// <param name="user">The user whose associated logins to retrieve.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing a list of <see cref="UserLoginInfo"/> for the specified <paramref name="user"/>, if any.
/// </returns>
public async override Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = user.Id;
return await UserLogins.Where(l => l.UserId.Equals(userId))
.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.ProviderDisplayName)).ToListAsync(cancellationToken);
}
/// <summary>
/// Retrieves the user associated with the specified login provider and login provider key.
/// </summary>
/// <param name="loginProvider">The login provider who provided the <paramref name="providerKey"/>.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing the user, if any which matched the specified login provider and key.
/// </returns>
public async override Task<TUser> FindByLoginAsync(string loginProvider, string providerKey,
CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
var userLogin = await FindUserLoginAsync(loginProvider, providerKey, cancellationToken);
if (userLogin != null)
{
return await FindUserAsync(userLogin.UserId, cancellationToken);
}
return null;
}
/// <summary>
/// Gets the user, if any, associated with the specified, normalized email address.
/// </summary>
/// <param name="normalizedEmail">The normalized email address to return the user for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address.
/// </returns>
public override Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
return Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken);
}
/// <summary>
/// Retrieves all users with the specified claim.
/// </summary>
/// <param name="claim">The claim whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that contain the specified claim.
/// </returns>
public async override Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (claim == null)
{
throw new ArgumentNullException(nameof(claim));
}
var query = from userclaims in UserClaims
join user in Users on userclaims.UserId equals user.Id
where userclaims.ClaimValue == claim.Value
&& userclaims.ClaimType == claim.Type
select user;
return await query.ToListAsync(cancellationToken);
}
/// <summary>
/// Retrieves all users in the specified role.
/// </summary>
/// <param name="normalizedRoleName">The role whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that are in the specified role.
/// </returns>
public async override Task<IList<TUser>> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (string.IsNullOrEmpty(normalizedRoleName))
{
throw new ArgumentNullException(nameof(normalizedRoleName));
}
var role = await FindRoleAsync(normalizedRoleName, cancellationToken);
if (role != null)
{
var query = from userrole in UserRoles
join user in Users on userrole.UserId equals user.Id
where userrole.RoleId.Equals(role.Id)
select user;
return await query.ToListAsync(cancellationToken);
}
return new List<TUser>();
}
/// <summary>
/// Find a user token if it exists.
/// </summary>
/// <param name="user">The token owner.</param>
/// <param name="loginProvider">The login provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user token if it exists.</returns>
public override Task<TUserToken> FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
=> UserTokens.FindAsync(new object[] { user.Id, loginProvider, name }, cancellationToken);
/// <summary>
/// Add a new user token.
/// </summary>
/// <param name="token">The token to be added.</param>
/// <returns></returns>
public override Task AddUserTokenAsync(TUserToken token)
{
UserTokens.Add(token);
return TaskCache.CompletedTask;
}
/// <summary>
/// Remove a new user token.
/// </summary>
/// <param name="token">The token to be removed.</param>
/// <returns></returns>
public override Task RemoveUserTokenAsync(TUserToken token)
{
UserTokens.Remove(token);
return TaskCache.CompletedTask;
}
}
}

View File

@ -0,0 +1,373 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types that supports
/// <see cref="IdentityStoreOptions.Version1_0"/>.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TRole">The type representing a role.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
/// <typeparam name="TUserClaim">The type representing a claim.</typeparam>
/// <typeparam name="TUserRole">The type representing a user role.</typeparam>
/// <typeparam name="TUserLogin">The type representing a user external login.</typeparam>
/// <typeparam name="TUserToken">The type representing a user token.</typeparam>
/// <typeparam name="TRoleClaim">The type representing a role claim.</typeparam>
public class UserStoreV1<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
UserStoreBaseV1<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>
where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TContext : DbContext
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserRole : IdentityUserRole<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
where TRoleClaim : IdentityRoleClaim<TKey>, new()
{
/// <summary>
/// Creates a new instance of the store.
/// </summary>
/// <param name="context">The context used to access the store.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/> used to describe store errors.</param>
public UserStoreV1(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber())
{
_core = new UserStoreCore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>(context, describer);
}
private readonly UserStoreCore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> _core;
/// <summary>
/// Gets the database context for this store.
/// </summary>
public TContext Context => _core.Context;
/// <summary>
/// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
/// </summary>
/// <value>
/// True if changes should be automatically persisted, otherwise false.
/// </value>
public bool AutoSaveChanges
{
get => _core.AutoSaveChanges;
set => _core.AutoSaveChanges = value;
}
/// <summary>Saves the current store.</summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
protected Task SaveChanges(CancellationToken cancellationToken) => _core.SaveChanges(cancellationToken);
/// <summary>
/// Creates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the creation operation.</returns>
public override Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.CreateAsync(user, cancellationToken);
/// <summary>
/// Updates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public override Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.UpdateAsync(user, cancellationToken);
/// <summary>
/// Deletes the specified <paramref name="user"/> from the user store.
/// </summary>
/// <param name="user">The user to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public override Task<IdentityResult> DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.DeleteAsync(user, cancellationToken);
/// <summary>
/// Finds and returns a user, if any, who has the specified <paramref name="userId"/>.
/// </summary>
/// <param name="userId">The user ID to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="userId"/> if it exists.
/// </returns>
public override Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
=> _core.FindByIdAsync(userId, cancellationToken);
/// <summary>
/// Finds and returns a user, if any, who has the specified normalized user name.
/// </summary>
/// <param name="normalizedUserName">The normalized user name to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="normalizedUserName"/> if it exists.
/// </returns>
public override Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.FindByNameAsync(normalizedUserName, cancellationToken);
/// <summary>
/// A navigation property for the users the store contains.
/// </summary>
public override IQueryable<TUser> Users => _core.UsersSet;
/// <summary>
/// Return a role with the normalized name if it exists.
/// </summary>
/// <param name="normalizedRoleName">The normalized role name.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The role if it exists.</returns>
public override Task<TRole> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken)
=> _core.FindRoleAsync(normalizedRoleName, cancellationToken);
/// <summary>
/// Return a user role for the userId and roleId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="roleId">The role's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user role if it exists.</returns>
public override Task<TUserRole> FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken)
=> _core.FindUserRoleAsync(userId, roleId, cancellationToken);
/// <summary>
/// Return a user with the matching userId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user if it exists.</returns>
public override Task<TUser> FindUserAsync(TKey userId, CancellationToken cancellationToken)
=> _core.FindUserAsync(userId, cancellationToken);
/// <summary>
/// Return a user login with the matching userId, provider, providerKey if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
public override Task<TUserLogin> FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
=> _core.FindUserLoginAsync(userId, loginProvider, providerKey, cancellationToken);
/// <summary>
/// Return a user login with provider, providerKey if it exists.
/// </summary>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
public override Task<TUserLogin> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
=> _core.FindUserLoginAsync(loginProvider, providerKey, cancellationToken);
/// <summary>
/// Adds the given <paramref name="normalizedRoleName"/> to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the role to.</param>
/// <param name="normalizedRoleName">The role to add.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.AddToRoleAsync(user, normalizedRoleName, cancellationToken);
/// <summary>
/// Removes the given <paramref name="normalizedRoleName"/> from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the role from.</param>
/// <param name="normalizedRoleName">The role to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.RemoveFromRoleAsync(user, normalizedRoleName, cancellationToken);
/// <summary>
/// Retrieves the roles the specified <paramref name="user"/> is a member of.
/// </summary>
/// <param name="user">The user whose roles should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the roles the user is a member of.</returns>
public override Task<IList<string>> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetRolesAsync(user, cancellationToken);
/// <summary>
/// Returns a flag indicating if the specified user is a member of the give <paramref name="normalizedRoleName"/>.
/// </summary>
/// <param name="user">The user whose role membership should be checked.</param>
/// <param name="normalizedRoleName">The role to check membership of</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> containing a flag indicating if the specified user is a member of the given group. If the
/// user is a member of the group the returned value with be true, otherwise it will be false.</returns>
public override Task<bool> IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.IsInRoleAsync(user, normalizedRoleName, cancellationToken);
/// <summary>
/// Get the claims associated with the specified <paramref name="user"/> as an asynchronous operation.
/// </summary>
/// <param name="user">The user whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a user.</returns>
public override Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetClaimsAsync(user, cancellationToken);
/// <summary>
/// Adds the <paramref name="claims"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the claim to.</param>
/// <param name="claims">The claim to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
=> _core.AddClaimsAsync(user, claims, cancellationToken);
/// <summary>
/// Replaces the <paramref name="claim"/> on the specified <paramref name="user"/>, with the <paramref name="newClaim"/>.
/// </summary>
/// <param name="user">The user to replace the claim on.</param>
/// <param name="claim">The claim replace.</param>
/// <param name="newClaim">The new claim replacing the <paramref name="claim"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken))
=> _core.ReplaceClaimAsync(user, claim, newClaim, cancellationToken);
/// <summary>
/// Removes the <paramref name="claims"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the claims from.</param>
/// <param name="claims">The claim to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
=> _core.RemoveClaimsAsync(user, claims, cancellationToken);
/// <summary>
/// Adds the <paramref name="login"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the login to.</param>
/// <param name="login">The login to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken))
=> _core.AddLoginAsync(user, login, cancellationToken);
/// <summary>
/// Removes the <paramref name="loginProvider"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the login from.</param>
/// <param name="loginProvider">The login to remove from the user.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
=> _core.RemoveLoginAsync(user, loginProvider, providerKey, cancellationToken);
/// <summary>
/// Retrieves the associated logins for the specified <param ref="user"/>.
/// </summary>
/// <param name="user">The user whose associated logins to retrieve.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing a list of <see cref="UserLoginInfo"/> for the specified <paramref name="user"/>, if any.
/// </returns>
public override Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetLoginsAsync(user, cancellationToken);
/// <summary>
/// Retrieves the user associated with the specified login provider and login provider key.
/// </summary>
/// <param name="loginProvider">The login provider who provided the <paramref name="providerKey"/>.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing the user, if any which matched the specified login provider and key.
/// </returns>
public override Task<TUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
=> _core.FindByLoginAsync(loginProvider, providerKey, cancellationToken);
/// <summary>
/// Gets the user, if any, associated with the specified, normalized email address.
/// </summary>
/// <param name="normalizedEmail">The normalized email address to return the user for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address.
/// </returns>
public override Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
=> _core.FindByEmailAsync(normalizedEmail, cancellationToken);
/// <summary>
/// Retrieves all users with the specified claim.
/// </summary>
/// <param name="claim">The claim whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that contain the specified claim.
/// </returns>
public override Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetUsersForClaimAsync(claim, cancellationToken);
/// <summary>
/// Retrieves all users in the specified role.
/// </summary>
/// <param name="normalizedRoleName">The role whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that are in the specified role.
/// </returns>
public override Task<IList<TUser>> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetUsersInRoleAsync(normalizedRoleName, cancellationToken);
/// <summary>
/// Find a user token if it exists.
/// </summary>
/// <param name="user">The token owner.</param>
/// <param name="loginProvider">The login provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user token if it exists.</returns>
public override Task<TUserToken> FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
=> _core.FindTokenAsync(user, loginProvider, name, cancellationToken);
/// <summary>
/// Add a new user token.
/// </summary>
/// <param name="token">The token to be added.</param>
/// <returns></returns>
public override Task AddUserTokenAsync(TUserToken token)
=> _core.AddUserTokenAsync(token);
/// <summary>
/// Remove a new user token.
/// </summary>
/// <param name="token">The token to be removed.</param>
/// <returns></returns>
public override Task RemoveUserTokenAsync(TUserToken token)
=> _core.RemoveUserTokenAsync(token);
/// <summary>
/// Dispose the store
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_core.Dispose();
}
}
}
}

View File

@ -0,0 +1,373 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
{
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types that supports
/// <see cref="IdentityStoreOptions.Version2_0"/>.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TRole">The type representing a role.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
/// <typeparam name="TUserClaim">The type representing a claim.</typeparam>
/// <typeparam name="TUserRole">The type representing a user role.</typeparam>
/// <typeparam name="TUserLogin">The type representing a user external login.</typeparam>
/// <typeparam name="TUserToken">The type representing a user token.</typeparam>
/// <typeparam name="TRoleClaim">The type representing a role claim.</typeparam>
public class UserStoreV2<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
UserStoreBaseV2<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>
where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TContext : DbContext
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserRole : IdentityUserRole<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
where TRoleClaim : IdentityRoleClaim<TKey>, new()
{
/// <summary>
/// Creates a new instance of the store.
/// </summary>
/// <param name="context">The context used to access the store.</param>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/> used to describe store errors.</param>
public UserStoreV2(TContext context, IdentityErrorDescriber describer = null) : base(describer ?? new IdentityErrorDescriber())
{
_core = new UserStoreCore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>(context, describer);
}
private readonly UserStoreCore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> _core;
/// <summary>
/// Gets the database context for this store.
/// </summary>
public TContext Context => _core.Context;
/// <summary>
/// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
/// </summary>
/// <value>
/// True if changes should be automatically persisted, otherwise false.
/// </value>
public bool AutoSaveChanges
{
get => _core.AutoSaveChanges;
set => _core.AutoSaveChanges = value;
}
/// <summary>Saves the current store.</summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
protected Task SaveChanges(CancellationToken cancellationToken) => _core.SaveChanges(cancellationToken);
/// <summary>
/// Creates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the creation operation.</returns>
public override Task<IdentityResult> CreateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.CreateAsync(user, cancellationToken);
/// <summary>
/// Updates the specified <paramref name="user"/> in the user store.
/// </summary>
/// <param name="user">The user to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public override Task<IdentityResult> UpdateAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.UpdateAsync(user, cancellationToken);
/// <summary>
/// Deletes the specified <paramref name="user"/> from the user store.
/// </summary>
/// <param name="user">The user to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the update operation.</returns>
public override Task<IdentityResult> DeleteAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.DeleteAsync(user, cancellationToken);
/// <summary>
/// Finds and returns a user, if any, who has the specified <paramref name="userId"/>.
/// </summary>
/// <param name="userId">The user ID to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="userId"/> if it exists.
/// </returns>
public override Task<TUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
=> _core.FindByIdAsync(userId, cancellationToken);
/// <summary>
/// Finds and returns a user, if any, who has the specified normalized user name.
/// </summary>
/// <param name="normalizedUserName">The normalized user name to search for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="normalizedUserName"/> if it exists.
/// </returns>
public override Task<TUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.FindByNameAsync(normalizedUserName, cancellationToken);
/// <summary>
/// A navigation property for the users the store contains.
/// </summary>
public override IQueryable<TUser> Users => _core.UsersSet;
/// <summary>
/// Return a role with the normalized name if it exists.
/// </summary>
/// <param name="normalizedRoleName">The normalized role name.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The role if it exists.</returns>
public override Task<TRole> FindRoleAsync(string normalizedRoleName, CancellationToken cancellationToken)
=> _core.FindRoleAsync(normalizedRoleName, cancellationToken);
/// <summary>
/// Return a user role for the userId and roleId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="roleId">The role's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user role if it exists.</returns>
public override Task<TUserRole> FindUserRoleAsync(TKey userId, TKey roleId, CancellationToken cancellationToken)
=> _core.FindUserRoleAsync(userId, roleId, cancellationToken);
/// <summary>
/// Return a user with the matching userId if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user if it exists.</returns>
public override Task<TUser> FindUserAsync(TKey userId, CancellationToken cancellationToken)
=> _core.FindUserAsync(userId, cancellationToken);
/// <summary>
/// Return a user login with the matching userId, provider, providerKey if it exists.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
public override Task<TUserLogin> FindUserLoginAsync(TKey userId, string loginProvider, string providerKey, CancellationToken cancellationToken)
=> _core.FindUserLoginAsync(userId, loginProvider, providerKey, cancellationToken);
/// <summary>
/// Return a user login with provider, providerKey if it exists.
/// </summary>
/// <param name="loginProvider">The login provider name.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user login if it exists.</returns>
public override Task<TUserLogin> FindUserLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
=> _core.FindUserLoginAsync(loginProvider, providerKey, cancellationToken);
/// <summary>
/// Adds the given <paramref name="normalizedRoleName"/> to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the role to.</param>
/// <param name="normalizedRoleName">The role to add.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddToRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.AddToRoleAsync(user, normalizedRoleName, cancellationToken);
/// <summary>
/// Removes the given <paramref name="normalizedRoleName"/> from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the role from.</param>
/// <param name="normalizedRoleName">The role to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task RemoveFromRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.RemoveFromRoleAsync(user, normalizedRoleName, cancellationToken);
/// <summary>
/// Retrieves the roles the specified <paramref name="user"/> is a member of.
/// </summary>
/// <param name="user">The user whose roles should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the roles the user is a member of.</returns>
public override Task<IList<string>> GetRolesAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetRolesAsync(user, cancellationToken);
/// <summary>
/// Returns a flag indicating if the specified user is a member of the give <paramref name="normalizedRoleName"/>.
/// </summary>
/// <param name="user">The user whose role membership should be checked.</param>
/// <param name="normalizedRoleName">The role to check membership of</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> containing a flag indicating if the specified user is a member of the given group. If the
/// user is a member of the group the returned value with be true, otherwise it will be false.</returns>
public override Task<bool> IsInRoleAsync(TUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.IsInRoleAsync(user, normalizedRoleName, cancellationToken);
/// <summary>
/// Get the claims associated with the specified <paramref name="user"/> as an asynchronous operation.
/// </summary>
/// <param name="user">The user whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a user.</returns>
public override Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetClaimsAsync(user, cancellationToken);
/// <summary>
/// Adds the <paramref name="claims"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the claim to.</param>
/// <param name="claims">The claim to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
=> _core.AddClaimsAsync(user, claims, cancellationToken);
/// <summary>
/// Replaces the <paramref name="claim"/> on the specified <paramref name="user"/>, with the <paramref name="newClaim"/>.
/// </summary>
/// <param name="user">The user to replace the claim on.</param>
/// <param name="claim">The claim replace.</param>
/// <param name="newClaim">The new claim replacing the <paramref name="claim"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken = default(CancellationToken))
=> _core.ReplaceClaimAsync(user, claim, newClaim, cancellationToken);
/// <summary>
/// Removes the <paramref name="claims"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the claims from.</param>
/// <param name="claims">The claim to remove.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken = default(CancellationToken))
=> _core.RemoveClaimsAsync(user, claims, cancellationToken);
/// <summary>
/// Adds the <paramref name="login"/> given to the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to add the login to.</param>
/// <param name="login">The login to add to the user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken))
=> _core.AddLoginAsync(user, login, cancellationToken);
/// <summary>
/// Removes the <paramref name="loginProvider"/> given from the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user to remove the login from.</param>
/// <param name="loginProvider">The login to remove from the user.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public override Task RemoveLoginAsync(TUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
=> _core.RemoveLoginAsync(user, loginProvider, providerKey, cancellationToken);
/// <summary>
/// Retrieves the associated logins for the specified <param ref="user"/>.
/// </summary>
/// <param name="user">The user whose associated logins to retrieve.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing a list of <see cref="UserLoginInfo"/> for the specified <paramref name="user"/>, if any.
/// </returns>
public override Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetLoginsAsync(user, cancellationToken);
/// <summary>
/// Retrieves the user associated with the specified login provider and login provider key.
/// </summary>
/// <param name="loginProvider">The login provider who provided the <paramref name="providerKey"/>.</param>
/// <param name="providerKey">The key provided by the <paramref name="loginProvider"/> to identify a user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> for the asynchronous operation, containing the user, if any which matched the specified login provider and key.
/// </returns>
public override Task<TUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
=> _core.FindByLoginAsync(loginProvider, providerKey, cancellationToken);
/// <summary>
/// Gets the user, if any, associated with the specified, normalized email address.
/// </summary>
/// <param name="normalizedEmail">The normalized email address to return the user for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The task object containing the results of the asynchronous lookup operation, the user if any associated with the specified normalized email address.
/// </returns>
public override Task<TUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default(CancellationToken))
=> _core.FindByEmailAsync(normalizedEmail, cancellationToken);
/// <summary>
/// Retrieves all users with the specified claim.
/// </summary>
/// <param name="claim">The claim whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that contain the specified claim.
/// </returns>
public override Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetUsersForClaimAsync(claim, cancellationToken);
/// <summary>
/// Retrieves all users in the specified role.
/// </summary>
/// <param name="normalizedRoleName">The role whose users should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>
/// The <see cref="Task"/> contains a list of users, if any, that are in the specified role.
/// </returns>
public override Task<IList<TUser>> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
=> _core.GetUsersInRoleAsync(normalizedRoleName, cancellationToken);
/// <summary>
/// Find a user token if it exists.
/// </summary>
/// <param name="user">The token owner.</param>
/// <param name="loginProvider">The login provider for the token.</param>
/// <param name="name">The name of the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The user token if it exists.</returns>
public override Task<TUserToken> FindTokenAsync(TUser user, string loginProvider, string name, CancellationToken cancellationToken)
=> _core.FindTokenAsync(user, loginProvider, name, cancellationToken);
/// <summary>
/// Add a new user token.
/// </summary>
/// <param name="token">The token to be added.</param>
/// <returns></returns>
public override Task AddUserTokenAsync(TUserToken token)
=> _core.AddUserTokenAsync(token);
/// <summary>
/// Remove a new user token.
/// </summary>
/// <param name="token">The token to be removed.</param>
/// <returns></returns>
public override Task RemoveUserTokenAsync(TUserToken token)
=> _core.RemoveUserTokenAsync(token);
/// <summary>
/// Dispose the store
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_core.Dispose();
}
}
}
}

View File

@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Identity.Test
/// </summary>
/// <param name="services"></param>
/// <param name="context"></param>
protected virtual void SetupIdentityServices(IServiceCollection services, object context = null)
protected virtual void SetupIdentityServices(IServiceCollection services, object context)
{
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
@ -192,6 +192,31 @@ namespace Microsoft.AspNetCore.Identity.Test
/// <returns>The query to use.</returns>
protected abstract Expression<Func<TRole, bool>> RoleNameStartsWithPredicate(string roleName);
/// <summary>
/// Test.
/// </summary>
/// <returns>Task</returns>
[Fact]
public async Task CreateUserWillSetCreateDateOnlyIfSupported()
{
if (ShouldSkipDbTests())
{
return;
}
var manager = CreateManager();
var user = CreateTestUser();
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
var userId = await manager.GetUserIdAsync(user);
if (manager.SupportsUserActivity)
{
Assert.NotNull(await manager.GetCreateDateAsync(user));
}
else
{
await Assert.ThrowsAsync<NotSupportedException>(() => manager.GetCreateDateAsync(user));
}
}
/// <summary>
/// Test.
/// </summary>
@ -490,9 +515,17 @@ namespace Microsoft.AspNetCore.Identity.Test
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
manager.PasswordValidators.Clear();
manager.PasswordValidators.Add(new AlwaysBadValidator());
if (manager.SupportsUserActivity)
{
Assert.Null(await manager.GetLastPasswordChangeDateAsync(user));
}
IdentityResultAssert.IsFailure(await manager.AddPasswordAsync(user, "password"),
AlwaysBadValidator.ErrorMessage);
IdentityResultAssert.VerifyLogMessage(manager.Logger, $"User {await manager.GetUserIdAsync(user)} password validation failed: {AlwaysBadValidator.ErrorMessage.Code}.");
if (manager.SupportsUserActivity)
{
Assert.Null(await manager.GetLastPasswordChangeDateAsync(user));
}
}
/// <summary>
@ -578,6 +611,12 @@ namespace Microsoft.AspNetCore.Identity.Test
var logins = await manager.GetLoginsAsync(user);
Assert.NotNull(logins);
Assert.Equal(0, logins.Count());
if (manager.SupportsUserActivity)
{
Assert.NotNull(await manager.GetCreateDateAsync(user));
Assert.Null(await manager.GetLastPasswordChangeDateAsync(user));
Assert.Null(await manager.GetLastSignInDateAsync(user));
}
}
/// <summary>
@ -604,6 +643,12 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.Equal(provider, logins.First().LoginProvider);
Assert.Equal(providerKey, logins.First().ProviderKey);
Assert.Equal(display, logins.First().ProviderDisplayName);
if (manager.SupportsUserActivity)
{
Assert.NotNull(await manager.GetCreateDateAsync(user));
Assert.Null(await manager.GetLastPasswordChangeDateAsync(user));
Assert.Null(await manager.GetLastSignInDateAsync(user));
}
}
/// <summary>
@ -624,13 +669,27 @@ namespace Microsoft.AspNetCore.Identity.Test
var login = new UserLoginInfo("Provider", userId, "display");
IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, login));
Assert.False(await manager.HasPasswordAsync(user));
if (manager.SupportsUserActivity)
{
Assert.Null(await manager.GetLastPasswordChangeDateAsync(user));
}
IdentityResultAssert.IsSuccess(await manager.AddPasswordAsync(user, "password"));
if (manager.SupportsUserActivity)
{
Assert.NotNull(await manager.GetLastPasswordChangeDateAsync(user));
}
Assert.True(await manager.HasPasswordAsync(user));
var logins = await manager.GetLoginsAsync(user);
Assert.NotNull(logins);
Assert.Equal(1, logins.Count());
Assert.Equal(user, await manager.FindByLoginAsync(login.LoginProvider, login.ProviderKey));
Assert.True(await manager.CheckPasswordAsync(user, "password"));
if (manager.SupportsUserActivity)
{
Assert.NotNull(await manager.GetCreateDateAsync(user));
Assert.Null(await manager.GetLastSignInDateAsync(user));
}
}
/// <summary>
@ -728,12 +787,57 @@ namespace Microsoft.AspNetCore.Identity.Test
const string password = "password";
const string newPassword = "newpassword";
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, password));
DateTimeOffset? createChange = null;
if (manager.SupportsUserActivity)
{
createChange = await manager.GetLastPasswordChangeDateAsync(user);
Assert.NotNull(createChange);
Assert.Null(await manager.GetLastSignInDateAsync(user));
}
else
{
await Assert.ThrowsAsync<NotSupportedException>(() => manager.GetLastPasswordChangeDateAsync(user));
await Assert.ThrowsAsync<NotSupportedException>(() => manager.GetLastSignInDateAsync(user));
}
var stamp = await manager.GetSecurityStampAsync(user);
Assert.NotNull(stamp);
IdentityResultAssert.IsSuccess(await manager.ChangePasswordAsync(user, password, newPassword));
Assert.False(await manager.CheckPasswordAsync(user, password));
Assert.True(await manager.CheckPasswordAsync(user, newPassword));
Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user));
if (manager.SupportsUserActivity)
{
var changeDate = await manager.GetLastPasswordChangeDateAsync(user);
Assert.NotNull(changeDate);
Assert.True(createChange < changeDate);
}
}
/// <summary>
/// Test.
/// </summary>
/// <returns>Task</returns>
[Fact]
public async Task CanUpdateLastSignInTime()
{
if (ShouldSkipDbTests())
{
return;
}
var manager = CreateManager();
var user = CreateTestUser();
if (manager.SupportsUserActivity)
{
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
Assert.Null(await manager.GetLastSignInDateAsync(user));
IdentityResultAssert.IsSuccess(await manager.UpdateLastSignInDateAsync(user));
Assert.NotNull(await manager.GetLastSignInDateAsync(user));
}
else
{
await Assert.ThrowsAsync<NotSupportedException>(() => manager.UpdateLastSignInDateAsync(user));
}
}
/// <summary>
@ -884,9 +988,20 @@ namespace Microsoft.AspNetCore.Identity.Test
var manager = CreateManager();
var user = CreateTestUser();
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user, "password"));
DateTimeOffset? createChange = null;
if (manager.SupportsUserActivity)
{
createChange = await manager.GetLastPasswordChangeDateAsync(user);
Assert.NotNull(createChange);
}
var result = await manager.ChangePasswordAsync(user, "bogus", "newpassword");
IdentityResultAssert.IsFailure(result, "Incorrect password.");
IdentityResultAssert.VerifyLogMessage(manager.Logger, $"Change password failed for user {await manager.GetUserIdAsync(user)}.");
if (manager.SupportsUserActivity)
{
Assert.Equal(createChange, await manager.GetLastPasswordChangeDateAsync(user));
}
}
/// <summary>

View File

@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.Identity
{
var auth = await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme);
var authenticationMethod = auth?.Principal?.FindFirstValue(ClaimTypes.AuthenticationMethod);
await SignInAsync(user, auth?.Properties, authenticationMethod);
await SignInAsync(user, auth?.Properties, authenticationMethod, updateLastSignIn: false);
}
/// <summary>
@ -172,8 +172,9 @@ namespace Microsoft.AspNetCore.Identity
/// <param name="user">The user to sign-in.</param>
/// <param name="isPersistent">Flag indicating whether the sign-in cookie should persist after the browser is closed.</param>
/// <param name="authenticationMethod">Name of the method used to authenticate the user.</param>
/// <param name="updateLastSignIn">If true, will update the last sign for the user if supported.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public virtual Task SignInAsync(TUser user, bool isPersistent, string authenticationMethod = null)
public virtual Task SignInAsync(TUser user, bool isPersistent, string authenticationMethod = null, bool updateLastSignIn = true)
{
return SignInAsync(user, new AuthenticationProperties { IsPersistent = isPersistent }, authenticationMethod);
}
@ -184,8 +185,9 @@ namespace Microsoft.AspNetCore.Identity
/// <param name="user">The user to sign-in.</param>
/// <param name="authenticationProperties">Properties applied to the login and authentication cookie.</param>
/// <param name="authenticationMethod">Name of the method used to authenticate the user.</param>
/// <param name="updateLastSignIn">If true, will update the last sign for the user if supported.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public virtual async Task SignInAsync(TUser user, AuthenticationProperties authenticationProperties, string authenticationMethod = null)
public virtual async Task SignInAsync(TUser user, AuthenticationProperties authenticationProperties, string authenticationMethod = null, bool updateLastSignIn = true)
{
var userPrincipal = await CreateUserPrincipalAsync(user);
// Review: should we guard against CreateUserPrincipal returning null?
@ -196,6 +198,10 @@ namespace Microsoft.AspNetCore.Identity
await Context.SignInAsync(IdentityConstants.ApplicationScheme,
userPrincipal,
authenticationProperties ?? new AuthenticationProperties());
if (updateLastSignIn && UserManager.SupportsUserActivity)
{
await UserManager.UpdateLastSignInDateAsync(user);
}
}
/// <summary>

View File

@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Provides an abstraction for a store containing users' password expiration data.
/// </summary>
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public interface IUserActivityStore<TUser> : IUserStore<TUser> where TUser : class
{
/// <summary>
/// Sets the last password change date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="changeDate">The last password change date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task SetLastPasswordChangeDateAsync(TUser user, DateTimeOffset? changeDate, CancellationToken cancellationToken);
/// <summary>
/// Gets the last password change date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, returning the password hash for the specified <paramref name="user"/>.</returns>
Task<DateTimeOffset?> GetLastPasswordChangeDateAsync(TUser user, CancellationToken cancellationToken);
/// <summary>
/// Sets the creation date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="creationDate">The date the user was created.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task SetCreateDateAsync(TUser user, DateTimeOffset? creationDate, CancellationToken cancellationToken);
/// <summary>
/// Gets the creation date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, returning the password hash for the specified <paramref name="user"/>.</returns>
Task<DateTimeOffset?> GetCreateDateAsync(TUser user, CancellationToken cancellationToken);
/// <summary>
/// Sets the last signin date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="lastSignIn">The date the user last signed in.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task SetLastSignInDateAsync(TUser user, DateTimeOffset? lastSignIn, CancellationToken cancellationToken);
/// <summary>
/// Gets the last signin date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, returning the password hash for the specified <paramref name="user"/>.</returns>
Task<DateTimeOffset?> GetLastSignInDateAsync(TUser user, CancellationToken cancellationToken);
}
}

View File

@ -762,6 +762,22 @@ namespace Microsoft.AspNetCore.Identity
return string.Format(CultureInfo.CurrentCulture, GetString("PasswordRequiresUniqueChars"), p0);
}
/// <summary>
/// Store does not implement IUserActivityStore&lt;User&gt;.
/// </summary>
internal static string StoreNotIUserActivityStore
{
get { return GetString("StoreNotIUserActivityStore"); }
}
/// <summary>
/// Store does not implement IUserActivityStore&lt;User&gt;.
/// </summary>
internal static string FormatStoreNotIUserActivityStore()
{
return GetString("StoreNotIUserActivityStore");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -305,4 +305,8 @@
<value>Passwords must use at least {0} different characters.</value>
<comment>Error message for passwords that are based on similar characters</comment>
</data>
<data name="StoreNotIUserActivityStore" xml:space="preserve">
<value>Store does not implement IUserActivityStore&lt;User&gt;.</value>
<comment>Error when the store does not implement this interface</comment>
</data>
</root>

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>

View File

@ -38,6 +38,11 @@ namespace Microsoft.AspNetCore.Identity
/// <value>True if the user attempting to sign-in requires two factor authentication, otherwise false.</value>
public bool RequiresTwoFactor { get; protected set; }
/// <summary>
/// Returns a flag indication whether the user attempting to sign-in requires a password change.
/// </summary>
public bool RequiresPasswordChange { get; protected set; }
/// <summary>
/// Returns a <see cref="SignInResult"/> that represents a successful sign-in.
/// </summary>

View File

@ -339,6 +339,21 @@ namespace Microsoft.AspNetCore.Identity
}
}
/// <summary>
/// Gets a flag indicating whether the backing user store supports tracking user activity like creation date.
/// </summary>
/// <value>
/// true if the backing user store supports user activity tracking, otherwise false.
/// </value>
public virtual bool SupportsUserActivity
{
get
{
ThrowIfDisposed();
return Store is IUserActivityStore<TUser>;
}
}
/// <summary>
/// Gets a flag indicating whether the backing user store supports returning
/// <see cref="IQueryable"/> collections of information.
@ -466,6 +481,11 @@ namespace Microsoft.AspNetCore.Identity
await UpdateNormalizedUserNameAsync(user);
await UpdateNormalizedEmailAsync(user);
if (SupportsUserActivity)
{
await GetUserActivityStore().SetCreateDateAsync(user, DateTimeOffset.UtcNow, CancellationToken);
}
return await Store.CreateAsync(user, CancellationToken);
}
@ -2225,6 +2245,79 @@ namespace Microsoft.AspNetCore.Identity
return IdentityResult.Failed(ErrorDescriber.RecoveryCodeRedemptionFailed());
}
/// <summary>
/// Gets a date representing when a user was created.
/// </summary>
/// <param name="user">The user.</param>
/// <returns>
/// A <see cref="Task{TResult}"/> that represents the lookup.
/// </returns>
public virtual Task<DateTimeOffset?> GetCreateDateAsync(TUser user)
{
ThrowIfDisposed();
var store = GetUserActivityStore();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return store.GetCreateDateAsync(user, CancellationToken);
}
/// <summary>
/// Gets a date representing when the user last changed their password.
/// </summary>
/// <param name="user">The user.</param>
/// <returns>
/// A <see cref="Task{TResult}"/> that represents the lookup.
/// </returns>
public virtual Task<DateTimeOffset?> GetLastPasswordChangeDateAsync(TUser user)
{
ThrowIfDisposed();
var store = GetUserActivityStore();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return store.GetLastPasswordChangeDateAsync(user, CancellationToken);
}
/// <summary>
/// Gets a date representing when the user last signed in.
/// </summary>
/// <param name="user">The user.</param>
/// <returns>
/// A <see cref="Task{TResult}"/> that represents the lookup.
/// </returns>
public virtual Task<DateTimeOffset?> GetLastSignInDateAsync(TUser user)
{
ThrowIfDisposed();
var store = GetUserActivityStore();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return store.GetLastSignInDateAsync(user, CancellationToken);
}
/// <summary>
/// Update the last sign in date to DateTimeOffest.UtcNow.
/// </summary>
/// <param name="user">The user.</param>
/// <returns>
/// A <see cref="Task{TResult}"/> that represents the lookup.
/// </returns>
public virtual async Task<IdentityResult> UpdateLastSignInDateAsync(TUser user)
{
ThrowIfDisposed();
var store = GetUserActivityStore();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
await store.SetLastSignInDateAsync(user, DateTimeOffset.UtcNow, CancellationToken);
return await UpdateAsync(user);
}
/// <summary>
/// Releases the unmanaged resources used by the role manager and optionally releases the managed resources.
/// </summary>
@ -2248,6 +2341,17 @@ namespace Microsoft.AspNetCore.Identity
return cast;
}
internal IUserActivityStore<TUser> GetUserActivityStore()
{
var cast = Store as IUserActivityStore<TUser>;
if (cast == null)
{
throw new NotSupportedException(Resources.StoreNotIUserActivityStore);
}
return cast;
}
internal IUserLockoutStore<TUser> GetUserLockoutStore()
{
var cast = Store as IUserLockoutStore<TUser>;
@ -2310,6 +2414,10 @@ namespace Microsoft.AspNetCore.Identity
}
var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null;
await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken);
if (SupportsUserActivity)
{
await GetUserActivityStore().SetLastPasswordChangeDateAsync(user, DateTimeOffset.UtcNow, CancellationToken);
}
await UpdateSecurityStampInternal(user);
return IdentityResult.Success;
}

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Used for store schema versioning.
/// </summary>
public class IdentityStoreOptions
{
/// <summary>
/// Matches version 1.x.x
/// </summary>
public const string Version1_0 = "v1.0";
/// <summary>
/// Matches version 2.0.0
/// </summary>
public const string Version2_0 = "v2.0";
/// <summary>
/// Used to represent the most current version.
/// </summary>
public const string Version_Latest = "latest";
/// <summary>
/// Used to determine what features/schema are supported in the store.
/// </summary>
public string Version { get; set; }
}
}

View File

@ -149,6 +149,21 @@ namespace Microsoft.AspNetCore.Identity
/// </summary>
public virtual int AccessFailedCount { get; set; }
/// <summary>
/// Gets or sets the date and time, in UTC, when the user was created.
/// </summary>
public virtual DateTimeOffset? CreateDate { get; set; }
/// <summary>
/// Gets or sets the date and time, in UTC, when the user last signed in.
/// </summary>
public virtual DateTimeOffset? LastSignInDate { get; set; }
/// <summary>
/// Gets or sets the date and time, in UTC, when the user last changed his password.
/// </summary>
public virtual DateTimeOffset? LastPasswordChangeDate { get; set; }
/// <summary>
/// Returns the username for this user.
/// </summary>

View File

@ -2,26 +2,18 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Creates a new instance of a persistence store for roles.
/// Creates a new instance of a persistence store for roles that always will match the latest /// Creates a new instance of a persistence store for roles that matches <see cref="IdentityStoreOptions.Version"/>.
/// </summary>
/// <typeparam name="TRole">The type of the class representing a role.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
/// <typeparam name="TUserRole">The type of the class representing a user role.</typeparam>
/// <typeparam name="TRoleClaim">The type of the class representing a role claim.</typeparam>
public abstract class RoleStoreBase<TRole, TKey, TUserRole, TRoleClaim> :
IQueryableRoleStore<TRole>,
IRoleClaimStore<TRole>
RoleStoreBaseV1<TRole, TKey, TUserRole, TRoleClaim>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TKey : IEquatable<TKey>
where TUserRole : IdentityUserRole<TKey>, new()
@ -31,242 +23,6 @@ namespace Microsoft.AspNetCore.Identity
/// Constructs a new instance of <see cref="RoleStoreBase{TRole, TKey, TUserRole, TRoleClaim}"/>.
/// </summary>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public RoleStoreBase(IdentityErrorDescriber describer)
{
if (describer == null)
{
throw new ArgumentNullException(nameof(describer));
}
ErrorDescriber = describer;
}
private bool _disposed;
/// <summary>
/// Gets or sets the <see cref="IdentityErrorDescriber"/> for any error that occurred with the current operation.
/// </summary>
public IdentityErrorDescriber ErrorDescriber { get; set; }
/// <summary>
/// Creates a new role in a store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to create in the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public abstract Task<IdentityResult> CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Updates a role in a store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to update in the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public abstract Task<IdentityResult> UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Deletes a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to delete from the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public abstract Task<IdentityResult> DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Gets the ID for a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose ID should be returned.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the ID of the role.</returns>
public virtual Task<string> GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(ConvertIdToString(role.Id));
}
/// <summary>
/// Gets the name of a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose name should be returned.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the name of the role.</returns>
public virtual Task<string> GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(role.Name);
}
/// <summary>
/// Sets the name of a role in the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose name should be set.</param>
/// <param name="roleName">The name of the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual 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 TaskCache.CompletedTask;
}
/// <summary>
/// Converts the provided <paramref name="id"/> to a strongly typed key object.
/// </summary>
/// <param name="id">The id to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided <paramref name="id"/>.</returns>
public virtual TKey ConvertIdFromString(string id)
{
if (id == null)
{
return default(TKey);
}
return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id);
}
/// <summary>
/// Converts the provided <paramref name="id"/> to its string representation.
/// </summary>
/// <param name="id">The id to convert.</param>
/// <returns>An <see cref="string"/> representation of the provided <paramref name="id"/>.</returns>
public virtual string ConvertIdToString(TKey id)
{
if (id.Equals(default(TKey)))
{
return null;
}
return id.ToString();
}
/// <summary>
/// Finds the role who has the specified ID as an asynchronous operation.
/// </summary>
/// <param name="id">The role ID to look for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that result of the look up.</returns>
public abstract Task<TRole> FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Finds the role who has the specified normalized name as an asynchronous operation.
/// </summary>
/// <param name="normalizedName">The normalized role name to look for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that result of the look up.</returns>
public abstract Task<TRole> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Get a role's normalized name as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose normalized name should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the name of the role.</returns>
public virtual Task<string> GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(role.NormalizedName);
}
/// <summary>
/// Set a role's normalized name as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose normalized name should be set.</param>
/// <param name="normalizedName">The normalized name to set</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
role.NormalizedName = normalizedName;
return TaskCache.CompletedTask;
}
/// <summary>
/// Throws if this class has been disposed.
/// </summary>
protected void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
}
/// <summary>
/// Dispose the stores
/// </summary>
public void Dispose()
{
_disposed = true;
}
/// <summary>
/// Get the claims associated with the specified <paramref name="role"/> as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a role.</returns>
public abstract Task<IList<Claim>> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Adds the <paramref name="claim"/> given to the specified <paramref name="role"/>.
/// </summary>
/// <param name="role">The role to add the claim to.</param>
/// <param name="claim">The claim to add to the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public abstract Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Removes the <paramref name="claim"/> given from the specified <paramref name="role"/>.
/// </summary>
/// <param name="role">The role to remove the claim from.</param>
/// <param name="claim">The claim to remove from the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public abstract Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// A navigation property for the roles the store contains.
/// </summary>
public abstract IQueryable<TRole> Roles
{
get;
}
/// <summary>
/// Creates a entity representing a role claim.
/// </summary>
/// <param name="role">The associated role.</param>
/// <param name="claim">The associated claim.</param>
/// <returns>The role claim entity.</returns>
protected virtual TRoleClaim CreateRoleClaim(TRole role, Claim claim)
{
return new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value };
}
public RoleStoreBase(IdentityErrorDescriber describer) : base(describer) { }
}
}

View File

@ -0,0 +1,272 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Creates a new instance of a persistence store for roles that matches <see cref="IdentityStoreOptions.Version1_0"/>.
/// </summary>
/// <typeparam name="TRole">The type of the class representing a role.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
/// <typeparam name="TUserRole">The type of the class representing a user role.</typeparam>
/// <typeparam name="TRoleClaim">The type of the class representing a role claim.</typeparam>
public abstract class RoleStoreBaseV1<TRole, TKey, TUserRole, TRoleClaim> :
IQueryableRoleStore<TRole>,
IRoleClaimStore<TRole>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TKey : IEquatable<TKey>
where TUserRole : IdentityUserRole<TKey>, new()
where TRoleClaim : IdentityRoleClaim<TKey>, new()
{
/// <summary>
/// Constructs a new instance of <see cref="RoleStoreBase{TRole, TKey, TUserRole, TRoleClaim}"/>.
/// </summary>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
public RoleStoreBaseV1(IdentityErrorDescriber describer)
{
if (describer == null)
{
throw new ArgumentNullException(nameof(describer));
}
ErrorDescriber = describer;
}
private bool _disposed;
/// <summary>
/// Gets or sets the <see cref="IdentityErrorDescriber"/> for any error that occurred with the current operation.
/// </summary>
public IdentityErrorDescriber ErrorDescriber { get; set; }
/// <summary>
/// Creates a new role in a store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to create in the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public abstract Task<IdentityResult> CreateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Updates a role in a store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to update in the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public abstract Task<IdentityResult> UpdateAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Deletes a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role to delete from the store.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that represents the <see cref="IdentityResult"/> of the asynchronous query.</returns>
public abstract Task<IdentityResult> DeleteAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Gets the ID for a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose ID should be returned.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the ID of the role.</returns>
public virtual Task<string> GetRoleIdAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(ConvertIdToString(role.Id));
}
/// <summary>
/// Gets the name of a role from the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose name should be returned.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the name of the role.</returns>
public virtual Task<string> GetRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(role.Name);
}
/// <summary>
/// Sets the name of a role in the store as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose name should be set.</param>
/// <param name="roleName">The name of the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual 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 TaskCache.CompletedTask;
}
/// <summary>
/// Converts the provided <paramref name="id"/> to a strongly typed key object.
/// </summary>
/// <param name="id">The id to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided <paramref name="id"/>.</returns>
public virtual TKey ConvertIdFromString(string id)
{
if (id == null)
{
return default(TKey);
}
return (TKey)TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(id);
}
/// <summary>
/// Converts the provided <paramref name="id"/> to its string representation.
/// </summary>
/// <param name="id">The id to convert.</param>
/// <returns>An <see cref="string"/> representation of the provided <paramref name="id"/>.</returns>
public virtual string ConvertIdToString(TKey id)
{
if (id.Equals(default(TKey)))
{
return null;
}
return id.ToString();
}
/// <summary>
/// Finds the role who has the specified ID as an asynchronous operation.
/// </summary>
/// <param name="id">The role ID to look for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that result of the look up.</returns>
public abstract Task<TRole> FindByIdAsync(string id, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Finds the role who has the specified normalized name as an asynchronous operation.
/// </summary>
/// <param name="normalizedName">The normalized role name to look for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that result of the look up.</returns>
public abstract Task<TRole> FindByNameAsync(string normalizedName, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Get a role's normalized name as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose normalized name should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the name of the role.</returns>
public virtual Task<string> GetNormalizedRoleNameAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
return Task.FromResult(role.NormalizedName);
}
/// <summary>
/// Set a role's normalized name as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose normalized name should be set.</param>
/// <param name="normalizedName">The normalized name to set</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual Task SetNormalizedRoleNameAsync(TRole role, string normalizedName, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (role == null)
{
throw new ArgumentNullException(nameof(role));
}
role.NormalizedName = normalizedName;
return TaskCache.CompletedTask;
}
/// <summary>
/// Throws if this class has been disposed.
/// </summary>
protected void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
}
/// <summary>
/// Dispose the stores
/// </summary>
public void Dispose()
{
_disposed = true;
}
/// <summary>
/// Get the claims associated with the specified <paramref name="role"/> as an asynchronous operation.
/// </summary>
/// <param name="role">The role whose claims should be retrieved.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>A <see cref="Task{TResult}"/> that contains the claims granted to a role.</returns>
public abstract Task<IList<Claim>> GetClaimsAsync(TRole role, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Adds the <paramref name="claim"/> given to the specified <paramref name="role"/>.
/// </summary>
/// <param name="role">The role to add the claim to.</param>
/// <param name="claim">The claim to add to the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public abstract Task AddClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Removes the <paramref name="claim"/> given from the specified <paramref name="role"/>.
/// </summary>
/// <param name="role">The role to remove the claim from.</param>
/// <param name="claim">The claim to remove from the role.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public abstract Task RemoveClaimAsync(TRole role, Claim claim, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// A navigation property for the roles the store contains.
/// </summary>
public abstract IQueryable<TRole> Roles
{
get;
}
/// <summary>
/// Creates a entity representing a role claim.
/// </summary>
/// <param name="role">The associated role.</param>
/// <param name="claim">The associated claim.</param>
/// <returns>The role claim entity.</returns>
public virtual TRoleClaim CreateRoleClaim(TRole role, Claim claim)
{
return new TRoleClaim { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value };
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,152 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Identity
{
/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TRole">The type representing a role.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
/// <typeparam name="TUserClaim">The type representing a claim.</typeparam>
/// <typeparam name="TUserRole">The type representing a user role.</typeparam>
/// <typeparam name="TUserLogin">The type representing a user external login.</typeparam>
/// <typeparam name="TUserToken">The type representing a user token.</typeparam>
/// <typeparam name="TRoleClaim">The type representing a role claim.</typeparam>
public abstract class UserStoreBaseV2<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
UserStoreBaseV1<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>,
IUserActivityStore<TUser>
where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>, new()
where TUserRole : IdentityUserRole<TKey>, new()
where TUserLogin : IdentityUserLogin<TKey>, new()
where TUserToken : IdentityUserToken<TKey>, new()
where TRoleClaim : IdentityRoleClaim<TKey>, new()
{
/// <summary>
/// Creates a new instance.
/// </summary>
/// <param name="describer">The <see cref="IdentityErrorDescriber"/> used to describe store errors.</param>
public UserStoreBaseV2(IdentityErrorDescriber describer) : base(describer) { }
/// <summary>
/// Sets the last password change date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="changeDate">The last password change date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual Task SetLastPasswordChangeDateAsync(TUser user, DateTimeOffset? changeDate, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
user.LastPasswordChangeDate = changeDate;
return TaskCache.CompletedTask;
}
/// <summary>
/// Gets the last password change date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, returning the password hash for the specified <paramref name="user"/>.</returns>
public virtual Task<DateTimeOffset?> GetLastPasswordChangeDateAsync(TUser user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.LastPasswordChangeDate);
}
/// <summary>
/// Sets the creation date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="creationDate">The date the user was created.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual Task SetCreateDateAsync(TUser user, DateTimeOffset? creationDate, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
user.CreateDate = creationDate;
return TaskCache.CompletedTask;
}
/// <summary>
/// Gets the creation date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, returning the password hash for the specified <paramref name="user"/>.</returns>
public virtual Task<DateTimeOffset?> GetCreateDateAsync(TUser user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.CreateDate);
}
/// <summary>
/// Sets the last signin date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="lastSignIn">The date the user last signed in.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
public virtual Task SetLastSignInDateAsync(TUser user, DateTimeOffset? lastSignIn, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
user.LastSignInDate = lastSignIn;
return TaskCache.CompletedTask;
}
/// <summary>
/// Gets the last signin date for the specified <paramref name="user"/>.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, returning the password hash for the specified <paramref name="user"/>.</returns>
public virtual Task<DateTimeOffset?> GetLastSignInDateAsync(TUser user, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return Task.FromResult(user.LastSignInDate);
}
}
}

View File

@ -9,16 +9,18 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
public class InMemoryContext :
InMemoryContext<IdentityUser, IdentityRole, string>
{
public InMemoryContext(DbContextOptions options) : base(options)
{ }
public InMemoryContext(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseInMemoryDatabase("Scratch");
}
public class InMemoryContext<TUser> :
InMemoryContext<TUser, IdentityRole, string>
where TUser : IdentityUser
{
public InMemoryContext(DbContextOptions options) : base(options)
{ }
public InMemoryContext(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseInMemoryDatabase("Scratch");
}
public class InMemoryContext<TUser, TRole, TKey> : IdentityDbContext<TUser, TRole, TKey>
@ -26,13 +28,10 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
{
public InMemoryContext(DbContextOptions options) : base(options)
{ }
public InMemoryContext(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase("Scratch");
}
=> optionsBuilder.UseInMemoryDatabase("Scratch");
}
public abstract class InMemoryContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken> : IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
@ -45,12 +44,57 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
where TRoleClaim : IdentityRoleClaim<TKey>
where TUserToken : IdentityUserToken<TKey>
{
public InMemoryContext(DbContextOptions options) : base(options)
{ }
public InMemoryContext(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase("Scratch");
}
=> optionsBuilder.UseInMemoryDatabase("Scratch");
}
public class InMemoryContextV1 :
InMemoryContextV1<IdentityUser, IdentityRole, string>
{
public InMemoryContextV1(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseInMemoryDatabase("ScratchV1");
}
public class InMemoryContextV1<TUser> :
InMemoryContext<TUser, IdentityRole, string>
where TUser : IdentityUser
{
public InMemoryContextV1(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseInMemoryDatabase("ScratchV1");
}
public class InMemoryContextV1<TUser, TRole, TKey> : IdentityDbContext<TUser, TRole, TKey>
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where TKey : IEquatable<TKey>
{
public InMemoryContextV1(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseInMemoryDatabase("ScratchV1");
}
public abstract class InMemoryContextV1<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken> : IdentityDbContext<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
where TUser : IdentityUser<TKey, TUserClaim, TUserRole, TUserLogin, TUserToken>
where TRole : IdentityRole<TKey, TUserRole, TRoleClaim>
where TKey : IEquatable<TKey>
where TUserClaim : IdentityUserClaim<TKey>
where TUserRole : IdentityUserRole<TKey>
where TUserLogin : IdentityUserLogin<TKey>
where TRoleClaim : IdentityRoleClaim<TKey>
where TUserToken : IdentityUserToken<TKey>
{
public InMemoryContextV1(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseInMemoryDatabase("ScratchV1");
// TODO: remove once we fix the base
protected override void OnModelCreating(ModelBuilder builder)
=> base.OnModelCreatingV1(builder);
}
}

View File

@ -3,27 +3,25 @@
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.Test;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
{
public class InMemoryEFUserStoreTest : IdentitySpecificationTestBase<IdentityUser, IdentityRole, string>
{
protected override object CreateTestContext()
{
return new InMemoryContext(new DbContextOptionsBuilder().Options);
}
=> new InMemoryContext(new DbContextOptionsBuilder().Options);
protected override void AddUserStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IUserStore<IdentityUser>>(new UserStore<IdentityUser>((InMemoryContext)context));
}
=> services.AddSingleton<IUserStore<IdentityUser>>(new UserStore<IdentityUser, IdentityRole, DbContext, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>((InMemoryContext)context, new IdentityErrorDescriber()));
protected override void AddRoleStore(IServiceCollection services, object context = null)
{
var store = new RoleStore<IdentityRole, InMemoryContext>((InMemoryContext)context);
var store = new RoleStore<IdentityRole, InMemoryContext, string, IdentityUserRole<string>, IdentityRoleClaim<string>>((InMemoryContext)context);
services.AddSingleton<IRoleStore<IdentityRole>>(store);
}

View File

@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.Test;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
{
public class InMemoryEFUserStoreV1Test : IdentitySpecificationTestBase<IdentityUser, IdentityRole, string>
{
protected override object CreateTestContext()
=> new InMemoryContextV1(new DbContextOptionsBuilder().Options);
protected override void AddUserStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IUserStore<IdentityUser>>(new UserStoreV1<IdentityUser, IdentityRole, DbContext, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>((InMemoryContextV1)context, new IdentityErrorDescriber()));
}
protected override void AddRoleStore(IServiceCollection services, object context = null)
{
var store = new RoleStoreV1<IdentityRole, InMemoryContextV1, string, IdentityUserRole<string>, IdentityRoleClaim<string>>((InMemoryContextV1)context);
services.AddSingleton<IRoleStore<IdentityRole>>(store);
}
protected override IdentityUser CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "",
bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false)
{
return new IdentityUser
{
UserName = useNamePrefixAsUserName ? namePrefix : string.Format("{0}{1}", namePrefix, Guid.NewGuid()),
Email = email,
PhoneNumber = phoneNumber,
LockoutEnabled = lockoutEnabled,
LockoutEnd = lockoutEnd
};
}
protected override IdentityRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false)
{
var roleName = useRoleNamePrefixAsRoleName ? roleNamePrefix : string.Format("{0}{1}", roleNamePrefix, Guid.NewGuid());
return new IdentityRole(roleName);
}
protected override void SetUserPasswordHash(IdentityUser user, string hashedPassword)
{
user.PasswordHash = hashedPassword;
}
protected override Expression<Func<IdentityUser, bool>> UserNameEqualsPredicate(string userName) => u => u.UserName == userName;
protected override Expression<Func<IdentityRole, bool>> RoleNameEqualsPredicate(string roleName) => r => r.Name == roleName;
protected override Expression<Func<IdentityUser, bool>> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName);
protected override Expression<Func<IdentityRole, bool>> RoleNameStartsWithPredicate(string roleName) => r => r.Name.StartsWith(roleName);
}
}

View File

@ -200,12 +200,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
{
public string LoginContext { get; set; }
public UserStoreWithGenerics(InMemoryContextWithGenerics context, string loginContext) : base(context)
public UserStoreWithGenerics(InMemoryContextWithGenerics context, string loginContext) : base(context, new IdentityErrorDescriber())
{
LoginContext = loginContext;
}
protected override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role)
public override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role)
{
return new IdentityUserRoleWithDate()
{
@ -215,12 +215,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
};
}
protected override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim)
public override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim)
{
return new IdentityUserClaimWithIssuer { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer };
}
protected override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login)
public override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login)
{
return new IdentityUserLoginWithContext
{
@ -232,7 +232,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
};
}
protected override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value)
public override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value)
{
return new IdentityUserTokenWithStuff
{

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
var services = TestIdentityFactory.CreateTestServices();
services.AddEntityFrameworkInMemoryDatabase();
services.AddSingleton(new InMemoryContext(new DbContextOptionsBuilder().Options));
services.AddTransient<IRoleStore<IdentityRole>, RoleStore<IdentityRole, InMemoryContext>>();
services.AddTransient<IRoleStore<IdentityRole>, RoleStore<IdentityRole, InMemoryContext, string, IdentityUserRole<string>, IdentityRoleClaim<string>>>();
services.AddSingleton<RoleManager<IdentityRole>>();
var provider = services.BuildServiceProvider();
var manager = provider.GetRequiredService<RoleManager<IdentityRole>>();
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
[Fact]
public async Task RoleStoreMethodsThrowWhenDisposedTest()
{
var store = new RoleStore<IdentityRole>(new InMemoryContext(new DbContextOptionsBuilder().Options));
var store = new RoleStore<IdentityRole, InMemoryContext, string, IdentityUserRole<string>, IdentityRoleClaim<string>>(new InMemoryContext(new DbContextOptionsBuilder().Options));
store.Dispose();
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.FindByIdAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.FindByNameAsync(null));
@ -53,8 +53,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
[Fact]
public async Task RoleStorePublicNullCheckTest()
{
Assert.Throws<ArgumentNullException>("context", () => new RoleStore<IdentityRole>(null));
var store = new RoleStore<IdentityRole>(new InMemoryContext(new DbContextOptionsBuilder().Options));
Assert.Throws<ArgumentNullException>("context", () => new RoleStore<IdentityRole, InMemoryContext, string, IdentityUserRole<string>, IdentityRoleClaim<string>>(null));
var store = new RoleStore<IdentityRole, InMemoryContext, string, IdentityUserRole<string>, IdentityRoleClaim<string>>(new InMemoryContext(new DbContextOptionsBuilder().Options));
await Assert.ThrowsAsync<ArgumentNullException>("role", async () => await store.GetRoleIdAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("role", async () => await store.GetRoleNameAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("role", async () => await store.SetRoleNameAsync(null, null));

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test
public static RoleManager<IdentityRole> CreateRoleManager(InMemoryContext context)
{
var services = CreateTestServices();
services.AddSingleton<IRoleStore<IdentityRole>>(new RoleStore<IdentityRole>(context));
services.AddSingleton<IRoleStore<IdentityRole>>(new RoleStore<IdentityRole, DbContext, string, IdentityUserRole<string>, IdentityRoleClaim<string>>(context));
return services.BuildServiceProvider().GetRequiredService<RoleManager<IdentityRole>>();
}

View File

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
where TRole : IdentityRole<TKey>, new()
where TKey : IEquatable<TKey>
{
private readonly ScratchDatabaseFixture _fixture;
protected readonly ScratchDatabaseFixture _fixture;
protected SqlStoreTestBase(ScratchDatabaseFixture fixture)
{
@ -64,7 +65,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
protected override Expression<Func<TUser, bool>> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName);
public TestDbContext CreateContext()
public virtual TestDbContext CreateContext()
{
var db = DbUtil.Create<TestDbContext>(_fixture.ConnectionString);
db.Database.EnsureCreated();
@ -78,12 +79,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
protected override void AddUserStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IUserStore<TUser>>(new UserStore<TUser, TRole, TestDbContext, TKey>((TestDbContext)context));
services.AddSingleton<IUserStore<TUser>>(new UserStore<TUser, TRole, TestDbContext, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>, IdentityUserLogin<TKey>, IdentityUserToken<TKey>, IdentityRoleClaim<TKey>>((TestDbContext)context, new IdentityErrorDescriber()));
}
protected override void AddRoleStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IRoleStore<TRole>>(new RoleStore<TRole, TestDbContext, TKey>((TestDbContext)context));
services.AddSingleton<IRoleStore<TRole>>(new RoleStore<TRole, TestDbContext, TKey, IdentityUserRole<TKey>, IdentityRoleClaim<TKey>>((TestDbContext)context));
}
protected override void SetUserPasswordHash(TUser user, string hashedPassword)
@ -95,26 +96,36 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public void EnsureDefaultSchema()
public virtual void EnsureDefaultSchema()
{
VerifyDefaultSchema(CreateContext());
var db = DbUtil.Create<TestDbContext>(_fixture.ConnectionString);
var services = new ServiceCollection().AddSingleton(db);
services.AddIdentity<TUser, TRole>().AddEntityFrameworkStoresLatest<TestDbContext>();
var sp = services.BuildServiceProvider();
var version = sp.GetRequiredService<IOptions<IdentityStoreOptions>>().Value.Version;
Assert.Equal(IdentityStoreOptions.Version_Latest, version);
db.Version = version;
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
VerifyDefaultSchema(db);
}
internal static void VerifyDefaultSchema(TestDbContext dbContext)
protected virtual void VerifyDefaultSchema(TestDbContext dbContext)
{
var sqlConn = dbContext.Database.GetDbConnection();
using (var db = new SqlConnection(sqlConn.ConnectionString))
{
db.Open();
Assert.True(VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp",
VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp",
"EmailConfirmed", "PhoneNumber", "PhoneNumberConfirmed", "TwoFactorEnabled", "LockoutEnabled",
"LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail"));
Assert.True(VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp"));
Assert.True(VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId"));
Assert.True(VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue"));
Assert.True(VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName"));
Assert.True(VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value"));
"LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail",
"CreateDate", "LastSignInDate", "LastPasswordChangeDate");
VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp");
VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId");
VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue");
VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName");
VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value");
VerifyIndex(db, "AspNetRoles", "RoleNameIndex", isUnique: true);
VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true);
@ -123,7 +134,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
}
}
internal static bool VerifyColumns(SqlConnection conn, string table, params string[] columns)
internal static void VerifyColumns(SqlConnection conn, string table, params string[] columns)
{
var count = 0;
using (
@ -136,12 +147,10 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
while (reader.Read())
{
count++;
if (!columns.Contains(reader.GetString(0)))
{
return false;
}
Assert.True(columns.Contains(reader.GetString(0)),
"Unexpected column: " + reader.GetString(0));
}
return count == columns.Length;
Assert.Equal(count, columns.Length);
}
}
}
@ -363,6 +372,67 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
Assert.Equal(1, (await manager.GetLoginsAsync(userByEmail)).Count);
Assert.Equal(2, (await manager.GetRolesAsync(userByEmail)).Count);
}
}
public abstract class SqlStoreTestBaseV1<TUser, TRole, TKey> : SqlStoreTestBase<TUser, TRole, TKey>, IClassFixture<ScratchDatabaseFixture>
where TUser : IdentityUser<TKey>, new()
where TRole : IdentityRole<TKey>, new()
where TKey : IEquatable<TKey>
{
protected SqlStoreTestBaseV1(ScratchDatabaseFixture fixture) : base(fixture) { }
public override TestDbContext CreateContext()
{
var db = base.CreateContext();
db.Version = IdentityStoreOptions.Version1_0;
return db;
}
protected override void AddUserStore(IServiceCollection services, object context = null)
=> services.AddSingleton<IUserStore<TUser>>(new UserStoreV1<TUser, TRole, TestDbContext, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>, IdentityUserLogin<TKey>, IdentityUserToken<TKey>, IdentityRoleClaim<TKey>>((TestDbContext)context, new IdentityErrorDescriber()));
protected override void AddRoleStore(IServiceCollection services, object context = null)
=> services.AddSingleton<IRoleStore<TRole>>(new RoleStoreV1<TRole, TestDbContext, TKey, IdentityUserRole<TKey>, IdentityRoleClaim<TKey>>((TestDbContext)context));
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public override void EnsureDefaultSchema()
{
var db = DbUtil.Create<TestDbContext>(_fixture.ConnectionString);
var services = new ServiceCollection().AddSingleton(db);
services.AddIdentity<TUser, TRole>().AddEntityFrameworkStoresV1<TestDbContext>();
var sp = services.BuildServiceProvider();
var version = sp.GetRequiredService<IOptions<IdentityStoreOptions>>().Value.Version;
Assert.Equal(IdentityStoreOptions.Version1_0, version);
db.Version = version;
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
VerifyDefaultSchema(db);
}
protected override void VerifyDefaultSchema(TestDbContext dbContext)
{
var sqlConn = dbContext.Database.GetDbConnection();
using (var db = new SqlConnection(sqlConn.ConnectionString))
{
db.Open();
VerifyColumns(db, "AspNetUsers", "Id", "UserName", "Email", "PasswordHash", "SecurityStamp",
"EmailConfirmed", "PhoneNumber", "PhoneNumberConfirmed", "TwoFactorEnabled", "LockoutEnabled",
"LockoutEnd", "AccessFailedCount", "ConcurrencyStamp", "NormalizedUserName", "NormalizedEmail");
VerifyColumns(db, "AspNetRoles", "Id", "Name", "NormalizedName", "ConcurrencyStamp");
VerifyColumns(db, "AspNetUserRoles", "UserId", "RoleId");
VerifyColumns(db, "AspNetUserClaims", "Id", "UserId", "ClaimType", "ClaimValue");
VerifyColumns(db, "AspNetUserLogins", "UserId", "ProviderKey", "LoginProvider", "ProviderDisplayName");
VerifyColumns(db, "AspNetUserTokens", "UserId", "LoginProvider", "Name", "Value");
VerifyIndex(db, "AspNetRoles", "RoleNameIndex", isUnique: true);
VerifyIndex(db, "AspNetUsers", "UserNameIndex", isUnique: true);
VerifyIndex(db, "AspNetUsers", "EmailIndex");
db.Close();
}
}
}
}

View File

@ -25,21 +25,20 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
}
}
public class UserStoreGuidTest : SqlStoreTestBase<GuidUser, GuidRole, Guid>
public class UserStoreGuidV1Test : SqlStoreTestBaseV1<GuidUser, GuidRole, Guid>
{
public UserStoreGuidTest(ScratchDatabaseFixture fixture)
public UserStoreGuidV1Test(ScratchDatabaseFixture fixture)
: base(fixture)
{ }
public class ApplicationUserStore : UserStoreV1<GuidUser, GuidRole, TestDbContext, Guid, IdentityUserClaim<Guid>, IdentityUserRole<Guid>, IdentityUserLogin<Guid>, IdentityUserToken<Guid>, IdentityRoleClaim<Guid>>
{
public ApplicationUserStore(TestDbContext context) : base(context, new IdentityErrorDescriber()) { }
}
public class ApplicationUserStore : UserStore<GuidUser, GuidRole, TestDbContext, Guid>
public class ApplicationRoleStore : RoleStoreV1<GuidRole, TestDbContext, Guid, IdentityUserRole<Guid>, IdentityRoleClaim<Guid>>
{
public ApplicationUserStore(TestDbContext context) : base(context) { }
}
public class ApplicationRoleStore : RoleStore<GuidRole, TestDbContext, Guid>
{
public ApplicationRoleStore(TestDbContext context) : base(context) { }
public ApplicationRoleStore(TestDbContext context) : base(context, new IdentityErrorDescriber()) { }
}
protected override void AddUserStore(IServiceCollection services, object context = null)
@ -60,4 +59,40 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
var builder = services.AddIdentity<GuidUser, GuidRole>().AddEntityFrameworkStores<TestDbContext>();
}
}
public class UserStoreGuidTest : SqlStoreTestBase<GuidUser, GuidRole, Guid>
{
public UserStoreGuidTest(ScratchDatabaseFixture fixture)
: base(fixture)
{ }
public class ApplicationUserStore : UserStore<GuidUser, GuidRole, TestDbContext, Guid, IdentityUserClaim<Guid>, IdentityUserRole<Guid>, IdentityUserLogin<Guid>, IdentityUserToken<Guid>, IdentityRoleClaim<Guid>>
{
public ApplicationUserStore(TestDbContext context) : base(context, new IdentityErrorDescriber()) { }
}
public class ApplicationRoleStore : RoleStore<GuidRole, TestDbContext, Guid, IdentityUserRole<Guid>, IdentityRoleClaim<Guid>>
{
public ApplicationRoleStore(TestDbContext context) : base(context, new IdentityErrorDescriber()) { }
}
protected override void AddUserStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IUserStore<GuidUser>>(new ApplicationUserStore((TestDbContext)context));
}
protected override void AddRoleStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IRoleStore<GuidRole>>(new ApplicationRoleStore((TestDbContext)context));
}
[Fact]
public void AddEntityFrameworkStoresCanInferKey()
{
var services = new ServiceCollection();
// This used to throw
var builder = services.AddIdentity<GuidUser, GuidRole>().AddEntityFrameworkStores<TestDbContext>();
}
}
}

View File

@ -1,41 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
{
public class IntUser : IdentityUser<int>
{
public IntUser()
{
UserName = Guid.NewGuid().ToString();
}
}
public class IntRole : IdentityRole<int>
{
public IntRole()
{
Name = Guid.NewGuid().ToString();
}
}
public class UserStoreIntTest : SqlStoreTestBase<IntUser, IntRole, int>
{
public UserStoreIntTest(ScratchDatabaseFixture fixture)
: base(fixture)
{
}
[Fact]
public void AddEntityFrameworkStoresCanInferKey()
{
var services = new ServiceCollection();
// This used to throw
var builder = services.AddIdentity<IntUser, IntRole>().AddEntityFrameworkStores<TestDbContext>();
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
{
public class IntUser : IdentityUser<int>
{
public IntUser() => UserName = Guid.NewGuid().ToString();
}
public class IntRole : IdentityRole<int>
{
public IntRole() => Name = Guid.NewGuid().ToString();
}
public class UserStoreIntTest : SqlStoreTestBase<IntUser, IntRole, int>
{
public UserStoreIntTest(ScratchDatabaseFixture fixture) : base(fixture) { }
[Fact]
public void AddEntityFrameworkStoresCanInferKey()
{
// This used to throw
var builder = new ServiceCollection().AddIdentity<IntUser, IntRole>().AddEntityFrameworkStores<TestDbContext>();
}
}
public class UserStoreIntV1Test : SqlStoreTestBaseV1<IntUser, IntRole, int>
{
public UserStoreIntV1Test(ScratchDatabaseFixture fixture) : base(fixture) { }
[Fact]
public void AddEntityFrameworkStoresCanInferKey()
{
// This used to throw
var builder = new ServiceCollection().AddIdentity<IntUser, IntRole>().AddEntityFrameworkStores<TestDbContext>();
}
}
}

View File

@ -14,6 +14,11 @@ using Xunit;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
{
internal class UserStore : UserStore<IdentityUser, IdentityRole, IdentityDbContext, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
{
public UserStore(IdentityDbContext context, IdentityErrorDescriber describer = null) : base(context, describer ?? new IdentityErrorDescriber()) { }
}
public class UserStoreTest : IdentitySpecificationTestBase<IdentityUser, IdentityRole>, IClassFixture<ScratchDatabaseFixture>
{
private readonly ScratchDatabaseFixture _fixture;
@ -26,12 +31,6 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
protected override bool ShouldSkipDbTests()
=> TestPlatformHelper.IsMono || !TestPlatformHelper.IsWindows;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{ }
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
@ -64,21 +63,14 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
return CreateContext();
}
public ApplicationDbContext CreateAppContext()
{
var db = DbUtil.Create<ApplicationDbContext>(_fixture.ConnectionString);
db.Database.EnsureCreated();
return db;
}
protected override void AddUserStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IUserStore<IdentityUser>>(new UserStore<IdentityUser, IdentityRole, IdentityDbContext>((IdentityDbContext)context));
services.AddSingleton<IUserStore<IdentityUser>>(new UserStore((IdentityDbContext)context));
}
protected override void AddRoleStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IRoleStore<IdentityRole>>(new RoleStore<IdentityRole, IdentityDbContext>((IdentityDbContext)context));
services.AddSingleton<IRoleStore<IdentityRole>>(new RoleStore<IdentityRole, IdentityDbContext, string, IdentityUserRole<string>, IdentityRoleClaim<string>>((IdentityDbContext)context));
}
[Fact]
@ -193,6 +185,20 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user));
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task CreateUserSetsCreateDate()
{
var manager = CreateManager();
var guid = Guid.NewGuid().ToString();
var user = new IdentityUser { UserName = "New" + guid };
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
Assert.NotNull(await manager.GetCreateDateAsync(user));
IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user));
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
@ -422,6 +428,4 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
protected override Expression<Func<IdentityUser, bool>> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName);
}
public class ApplicationUser : IdentityUser { }
}

View File

@ -0,0 +1,418 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.Test;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.AspNetCore.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
{
internal class UserStoreV1 : UserStoreV1<IdentityUser, IdentityRole, IdentityDbContext, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
{
public UserStoreV1(IdentityDbContext context, IdentityErrorDescriber describer = null) : base(context, describer ?? new IdentityErrorDescriber()) { }
}
public class UserStoreV1Test : IdentitySpecificationTestBase<IdentityUser, IdentityRole>, IClassFixture<ScratchDatabaseFixture>
{
private readonly ScratchDatabaseFixture _fixture;
public UserStoreV1Test(ScratchDatabaseFixture fixture)
{
_fixture = fixture;
}
protected override bool ShouldSkipDbTests()
=> TestPlatformHelper.IsMono || !TestPlatformHelper.IsWindows;
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public void CanCreateUserUsingEF()
{
using (var db = CreateContext())
{
var guid = Guid.NewGuid().ToString();
db.Users.Add(new IdentityUser { Id = guid, UserName = guid });
db.SaveChanges();
Assert.True(db.Users.Any(u => u.UserName == guid));
Assert.NotNull(db.Users.FirstOrDefault(u => u.UserName == guid));
}
}
public IdentityDbContext CreateContext(bool delete = false)
{
var db = DbUtil.Create<IdentityDbContext>(_fixture.ConnectionString);
db.Version = IdentityStoreOptions.Version1_0;
if (delete)
{
db.Database.EnsureDeleted();
}
db.Database.EnsureCreated();
return db;
}
protected override object CreateTestContext()
{
return CreateContext();
}
protected override void AddUserStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IUserStore<IdentityUser>>(new UserStoreV1((IdentityDbContext)context));
}
protected override void AddRoleStore(IServiceCollection services, object context = null)
{
services.AddSingleton<IRoleStore<IdentityRole>>(new RoleStoreV1<IdentityRole, IdentityDbContext, string, IdentityUserRole<string>, IdentityRoleClaim<string>>((IdentityDbContext)context));
}
[Fact]
public async Task SqlUserStoreMethodsThrowWhenDisposedTest()
{
var store = new UserStore(new IdentityDbContext(new DbContextOptionsBuilder<IdentityDbContext>().Options));
store.Dispose();
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.AddClaimsAsync(null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.AddLoginAsync(null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.AddToRoleAsync(null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.GetClaimsAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.GetLoginsAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.GetRolesAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.IsInRoleAsync(null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.RemoveClaimsAsync(null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.RemoveLoginAsync(null, null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(
async () => await store.RemoveFromRoleAsync(null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.RemoveClaimsAsync(null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.ReplaceClaimAsync(null, null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.FindByLoginAsync(null, null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.FindByIdAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.FindByNameAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.CreateAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.UpdateAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.DeleteAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(
async () => await store.SetEmailConfirmedAsync(null, true));
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await store.GetEmailConfirmedAsync(null));
await Assert.ThrowsAsync<ObjectDisposedException>(
async () => await store.SetPhoneNumberConfirmedAsync(null, true));
await Assert.ThrowsAsync<ObjectDisposedException>(
async () => await store.GetPhoneNumberConfirmedAsync(null));
}
[Fact]
public async Task UserStorePublicNullCheckTest()
{
Assert.Throws<ArgumentNullException>("context", () => new UserStore(null));
var store = new UserStore(new IdentityDbContext(new DbContextOptionsBuilder<IdentityDbContext>().Options));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetUserIdAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetUserNameAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.SetUserNameAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.CreateAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.UpdateAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.DeleteAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.AddClaimsAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.ReplaceClaimAsync(null, null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.RemoveClaimsAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetClaimsAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetLoginsAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetRolesAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.AddLoginAsync(null, null));
await
Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.RemoveLoginAsync(null, null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.AddToRoleAsync(null, null));
await
Assert.ThrowsAsync<ArgumentNullException>("user",
async () => await store.RemoveFromRoleAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.IsInRoleAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetPasswordHashAsync(null));
await
Assert.ThrowsAsync<ArgumentNullException>("user",
async () => await store.SetPasswordHashAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetSecurityStampAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user",
async () => await store.SetSecurityStampAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("login", async () => await store.AddLoginAsync(new IdentityUser("fake"), null));
await Assert.ThrowsAsync<ArgumentNullException>("claims",
async () => await store.AddClaimsAsync(new IdentityUser("fake"), null));
await Assert.ThrowsAsync<ArgumentNullException>("claims",
async () => await store.RemoveClaimsAsync(new IdentityUser("fake"), null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetEmailConfirmedAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user",
async () => await store.SetEmailConfirmedAsync(null, true));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetEmailAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.SetEmailAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetPhoneNumberAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.SetPhoneNumberAsync(null, null));
await Assert.ThrowsAsync<ArgumentNullException>("user",
async () => await store.GetPhoneNumberConfirmedAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user",
async () => await store.SetPhoneNumberConfirmedAsync(null, true));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetTwoFactorEnabledAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user",
async () => await store.SetTwoFactorEnabledAsync(null, true));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetAccessFailedCountAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetLockoutEnabledAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.SetLockoutEnabledAsync(null, false));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.GetLockoutEndDateAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.SetLockoutEndDateAsync(null, new DateTimeOffset()));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.ResetAccessFailedCountAsync(null));
await Assert.ThrowsAsync<ArgumentNullException>("user", async () => await store.IncrementAccessFailedCountAsync(null));
await Assert.ThrowsAsync<ArgumentException>("normalizedRoleName", async () => await store.AddToRoleAsync(new IdentityUser("fake"), null));
await Assert.ThrowsAsync<ArgumentException>("normalizedRoleName", async () => await store.RemoveFromRoleAsync(new IdentityUser("fake"), null));
await Assert.ThrowsAsync<ArgumentException>("normalizedRoleName", async () => await store.IsInRoleAsync(new IdentityUser("fake"), null));
await Assert.ThrowsAsync<ArgumentException>("normalizedRoleName", async () => await store.AddToRoleAsync(new IdentityUser("fake"), ""));
await Assert.ThrowsAsync<ArgumentException>("normalizedRoleName", async () => await store.RemoveFromRoleAsync(new IdentityUser("fake"), ""));
await Assert.ThrowsAsync<ArgumentException>("normalizedRoleName", async () => await store.IsInRoleAsync(new IdentityUser("fake"), ""));
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task CanCreateUsingManager()
{
var manager = CreateManager();
var guid = Guid.NewGuid().ToString();
var user = new IdentityUser { UserName = "New" + guid };
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
IdentityResultAssert.IsSuccess(await manager.DeleteAsync(user));
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task TwoUsersSamePasswordDifferentHash()
{
var manager = CreateManager();
var userA = new IdentityUser(Guid.NewGuid().ToString());
IdentityResultAssert.IsSuccess(await manager.CreateAsync(userA, "password"));
var userB = new IdentityUser(Guid.NewGuid().ToString());
IdentityResultAssert.IsSuccess(await manager.CreateAsync(userB, "password"));
Assert.NotEqual(userA.PasswordHash, userB.PasswordHash);
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task AddUserToUnknownRoleFails()
{
var manager = CreateManager();
var u = CreateTestUser();
IdentityResultAssert.IsSuccess(await manager.CreateAsync(u));
await Assert.ThrowsAsync<InvalidOperationException>(
async () => await manager.AddToRoleAsync(u, "bogus"));
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task ConcurrentUpdatesWillFail()
{
var user = CreateTestUser();
using (var db = CreateContext())
{
var manager = CreateManager(db);
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
}
using (var db = CreateContext())
using (var db2 = CreateContext())
{
var manager1 = CreateManager(db);
var manager2 = CreateManager(db2);
var user1 = await manager1.FindByIdAsync(user.Id);
var user2 = await manager2.FindByIdAsync(user.Id);
Assert.NotNull(user1);
Assert.NotNull(user2);
Assert.NotSame(user1, user2);
user1.UserName = Guid.NewGuid().ToString();
user2.UserName = Guid.NewGuid().ToString();
IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user1));
IdentityResultAssert.IsFailure(await manager2.UpdateAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure());
}
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task ConcurrentUpdatesWillFailWithDetachedUser()
{
var user = CreateTestUser();
using (var db = CreateContext())
{
var manager = CreateManager(db);
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
}
using (var db = CreateContext())
using (var db2 = CreateContext())
{
var manager1 = CreateManager(db);
var manager2 = CreateManager(db2);
var user2 = await manager2.FindByIdAsync(user.Id);
Assert.NotNull(user2);
Assert.NotSame(user, user2);
user.UserName = Guid.NewGuid().ToString();
user2.UserName = Guid.NewGuid().ToString();
IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user));
IdentityResultAssert.IsFailure(await manager2.UpdateAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure());
}
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task DeleteAModifiedUserWillFail()
{
var user = CreateTestUser();
using (var db = CreateContext())
{
var manager = CreateManager(db);
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
}
using (var db = CreateContext())
using (var db2 = CreateContext())
{
var manager1 = CreateManager(db);
var manager2 = CreateManager(db2);
var user1 = await manager1.FindByIdAsync(user.Id);
var user2 = await manager2.FindByIdAsync(user.Id);
Assert.NotNull(user1);
Assert.NotNull(user2);
Assert.NotSame(user1, user2);
user1.UserName = Guid.NewGuid().ToString();
IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(user1));
IdentityResultAssert.IsFailure(await manager2.DeleteAsync(user2), new IdentityErrorDescriber().ConcurrencyFailure());
}
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task ConcurrentRoleUpdatesWillFail()
{
var role = new IdentityRole(Guid.NewGuid().ToString());
using (var db = CreateContext())
{
var manager = CreateRoleManager(db);
IdentityResultAssert.IsSuccess(await manager.CreateAsync(role));
}
using (var db = CreateContext())
using (var db2 = CreateContext())
{
var manager1 = CreateRoleManager(db);
var manager2 = CreateRoleManager(db2);
var role1 = await manager1.FindByIdAsync(role.Id);
var role2 = await manager2.FindByIdAsync(role.Id);
Assert.NotNull(role1);
Assert.NotNull(role2);
Assert.NotSame(role1, role2);
role1.Name = Guid.NewGuid().ToString();
role2.Name = Guid.NewGuid().ToString();
IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role1));
IdentityResultAssert.IsFailure(await manager2.UpdateAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure());
}
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task ConcurrentRoleUpdatesWillFailWithDetachedRole()
{
var role = new IdentityRole(Guid.NewGuid().ToString());
using (var db = CreateContext())
{
var manager = CreateRoleManager(db);
IdentityResultAssert.IsSuccess(await manager.CreateAsync(role));
}
using (var db = CreateContext())
using (var db2 = CreateContext())
{
var manager1 = CreateRoleManager(db);
var manager2 = CreateRoleManager(db2);
var role2 = await manager2.FindByIdAsync(role.Id);
Assert.NotNull(role);
Assert.NotNull(role2);
Assert.NotSame(role, role2);
role.Name = Guid.NewGuid().ToString();
role2.Name = Guid.NewGuid().ToString();
IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role));
IdentityResultAssert.IsFailure(await manager2.UpdateAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure());
}
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task DeleteAModifiedRoleWillFail()
{
var role = new IdentityRole(Guid.NewGuid().ToString());
using (var db = CreateContext())
{
var manager = CreateRoleManager(db);
IdentityResultAssert.IsSuccess(await manager.CreateAsync(role));
}
using (var db = CreateContext())
using (var db2 = CreateContext())
{
var manager1 = CreateRoleManager(db);
var manager2 = CreateRoleManager(db2);
var role1 = await manager1.FindByIdAsync(role.Id);
var role2 = await manager2.FindByIdAsync(role.Id);
Assert.NotNull(role1);
Assert.NotNull(role2);
Assert.NotSame(role1, role2);
role1.Name = Guid.NewGuid().ToString();
IdentityResultAssert.IsSuccess(await manager1.UpdateAsync(role1));
IdentityResultAssert.IsFailure(await manager2.DeleteAsync(role2), new IdentityErrorDescriber().ConcurrencyFailure());
}
}
protected override IdentityUser CreateTestUser(string namePrefix = "", string email = "", string phoneNumber = "",
bool lockoutEnabled = false, DateTimeOffset? lockoutEnd = default(DateTimeOffset?), bool useNamePrefixAsUserName = false)
{
return new IdentityUser
{
UserName = useNamePrefixAsUserName ? namePrefix : string.Format("{0}{1}", namePrefix, Guid.NewGuid()),
Email = email,
PhoneNumber = phoneNumber,
LockoutEnabled = lockoutEnabled,
LockoutEnd = lockoutEnd
};
}
protected override IdentityRole CreateTestRole(string roleNamePrefix = "", bool useRoleNamePrefixAsRoleName = false)
{
var roleName = useRoleNamePrefixAsRoleName ? roleNamePrefix : string.Format("{0}{1}", roleNamePrefix, Guid.NewGuid());
return new IdentityRole(roleName);
}
protected override void SetUserPasswordHash(IdentityUser user, string hashedPassword)
{
user.PasswordHash = hashedPassword;
}
protected override Expression<Func<IdentityUser, bool>> UserNameEqualsPredicate(string userName) => u => u.UserName == userName;
protected override Expression<Func<IdentityRole, bool>> RoleNameEqualsPredicate(string roleName) => r => r.Name == roleName;
protected override Expression<Func<IdentityRole, bool>> RoleNameStartsWithPredicate(string roleName) => r => r.Name.StartsWith(roleName);
protected override Expression<Func<IdentityUser, bool>> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName);
}
}

View File

@ -230,12 +230,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
{
public string LoginContext { get; set; }
public UserStoreWithGenerics(ContextWithGenerics context, string loginContext) : base(context)
public UserStoreWithGenerics(ContextWithGenerics context, string loginContext) : base(context, new IdentityErrorDescriber())
{
LoginContext = loginContext;
}
protected override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role)
public override IdentityUserRoleWithDate CreateUserRole(IdentityUserWithGenerics user, MyIdentityRole role)
{
return new IdentityUserRoleWithDate()
{
@ -245,12 +245,12 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
};
}
protected override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim)
public override IdentityUserClaimWithIssuer CreateUserClaim(IdentityUserWithGenerics user, Claim claim)
{
return new IdentityUserClaimWithIssuer { UserId = user.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer };
}
protected override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login)
public override IdentityUserLoginWithContext CreateUserLogin(IdentityUserWithGenerics user, UserLoginInfo login)
{
return new IdentityUserLoginWithContext
{
@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
};
}
protected override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value)
public override IdentityUserTokenWithStuff CreateUserToken(IdentityUserWithGenerics user, string loginProvider, string name, string value)
{
return new IdentityUserTokenWithStuff
{
@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
_loginContext = loginContext;
}
protected override IdentityRoleClaimWithIssuer CreateRoleClaim(MyIdentityRole role, Claim claim)
public override IdentityRoleClaimWithIssuer CreateRoleClaim(MyIdentityRole role, Claim claim)
{
return new IdentityRoleClaimWithIssuer { RoleId = role.Id, ClaimType = claim.Type, ClaimValue = claim.Value, Issuer = claim.Issuer };
}

View File

@ -26,7 +26,8 @@ namespace Microsoft.AspNetCore.Identity.InMemory
IRoleClaimStore<TRole>,
IUserAuthenticationTokenStore<TUser>,
IUserAuthenticatorKeyStore<TUser>,
IUserTwoFactorRecoveryCodeStore<TUser>
IUserTwoFactorRecoveryCodeStore<TUser>,
IUserActivityStore<TUser>
where TRole : TestRole
where TUser : TestUser
{
@ -583,6 +584,39 @@ namespace Microsoft.AspNetCore.Identity.InMemory
return false;
}
public Task SetLastPasswordChangeDateAsync(TUser user, DateTimeOffset? changeDate, CancellationToken cancellationToken)
{
user.LastPasswordChangeDate = changeDate;
return Task.FromResult(0);
}
public Task<DateTimeOffset?> GetLastPasswordChangeDateAsync(TUser user, CancellationToken cancellationToken)
{
return Task.FromResult(user.LastPasswordChangeDate);
}
public Task SetCreateDateAsync(TUser user, DateTimeOffset? creationDate, CancellationToken cancellationToken)
{
user.CreateDate = creationDate;
return Task.FromResult(0);
}
public Task<DateTimeOffset?> GetCreateDateAsync(TUser user, CancellationToken cancellationToken)
{
return Task.FromResult(user.CreateDate);
}
public Task SetLastSignInDateAsync(TUser user, DateTimeOffset? lastSignIn, CancellationToken cancellationToken)
{
user.LastSignInDate = lastSignIn;
return Task.FromResult(0);
}
public Task<DateTimeOffset?> GetLastSignInDateAsync(TUser user, CancellationToken cancellationToken)
{
return Task.FromResult(user.LastSignInDate);
}
public IQueryable<TRole> Roles
{
get { return _roles.Values.AsQueryable(); }

View File

@ -27,4 +27,7 @@
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
</ItemGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
</Project>

View File

@ -197,7 +197,7 @@ namespace Microsoft.AspNetCore.Identity.Test
var signInManager = new Mock<SignInManager<TestUser>>(userManager.Object,
contextAccessor.Object, claimsManager.Object, identityOptions.Object, null, new Mock<IAuthenticationSchemeProvider>().Object);
signInManager.Setup(s => s.ValidateSecurityStampAsync(It.IsAny<ClaimsPrincipal>())).Throws(new Exception("Shouldn't be called"));
signInManager.Setup(s => s.SignInAsync(user, false, null)).Throws(new Exception("Shouldn't be called"));
signInManager.Setup(s => s.SignInAsync(user, false, null, It.IsAny<bool>())).Throws(new Exception("Shouldn't be called"));
var services = new ServiceCollection();
services.AddSingleton(options.Object);
services.AddSingleton(signInManager.Object);

View File

@ -202,6 +202,8 @@ namespace Microsoft.AspNetCore.Identity.Test
manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable();
manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable();
manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
var context = new DefaultHttpContext();
var auth = MockAuth(context);
@ -226,6 +228,8 @@ namespace Microsoft.AspNetCore.Identity.Test
manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable();
manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable();
manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
var context = new DefaultHttpContext();
var auth = MockAuth(context);
@ -252,6 +256,8 @@ namespace Microsoft.AspNetCore.Identity.Test
manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
manager.Setup(m => m.CheckPasswordAsync(user, "password")).ReturnsAsync(true).Verifiable();
manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable();
manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
var context = new DefaultHttpContext();
var auth = MockAuth(context);
@ -375,6 +381,8 @@ namespace Microsoft.AspNetCore.Identity.Test
manager.Setup(m => m.SupportsUserLockout).Returns(true).Verifiable();
manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, providerName ?? TokenOptions.DefaultAuthenticatorProvider, code)).ReturnsAsync(true).Verifiable();
manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable();
manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
var context = new DefaultHttpContext();
var auth = MockAuth(context);
@ -419,6 +427,8 @@ namespace Microsoft.AspNetCore.Identity.Test
var manager = SetupUserManager(user);
manager.Setup(m => m.SupportsUserLockout).Returns(supportsLockout).Verifiable();
manager.Setup(m => m.RedeemTwoFactorRecoveryCodeAsync(user, bypassCode)).ReturnsAsync(IdentityResult.Success).Verifiable();
manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable();
manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
if (supportsLockout)
{
manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
@ -456,11 +466,15 @@ namespace Microsoft.AspNetCore.Identity.Test
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public async Task CanExternalSignIn(bool isPersistent, bool supportsLockout)
[InlineData(true, true, true)]
[InlineData(true, false, true)]
[InlineData(false, true, true)]
[InlineData(false, false, true)]
[InlineData(true, true, false)]
[InlineData(true, false, false)]
[InlineData(false, true, false)]
[InlineData(false, false, false)]
public async Task CanExternalSignIn(bool isPersistent, bool supportsLockout, bool supportsActivity)
{
// Setup
var user = new TestUser { UserName = "Foo" };
@ -473,6 +487,11 @@ namespace Microsoft.AspNetCore.Identity.Test
manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
}
manager.Setup(m => m.FindByLoginAsync(loginProvider, providerKey)).ReturnsAsync(user).Verifiable();
if (supportsActivity)
{
manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable();
manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
}
var context = new DefaultHttpContext();
var auth = MockAuth(context);
@ -518,8 +537,7 @@ namespace Microsoft.AspNetCore.Identity.Test
{ CallBase = true };
//signInManager.Setup(s => s.SignInAsync(user, It.Is<AuthenticationProperties>(p => p.IsPersistent == isPersistent),
//externalLogin? loginProvider : null)).Returns(Task.FromResult(0)).Verifiable();
signInManager.Setup(s => s.SignInAsync(user, It.IsAny<AuthenticationProperties>(), null)).Returns(Task.FromResult(0)).Verifiable();
signInManager.Object.Context = context;
signInManager.Setup(s => s.SignInAsync(user, It.IsAny<AuthenticationProperties>(), /*authmethod*/null, /*updateLastSignIn*/false)).Returns(Task.FromResult(0)).Verifiable();
// Act
await signInManager.Object.RefreshSignInAsync(user);
@ -530,23 +548,39 @@ namespace Microsoft.AspNetCore.Identity.Test
}
[Theory]
[InlineData(true, true, true, true)]
[InlineData(true, true, false, true)]
[InlineData(true, false, true, true)]
[InlineData(true, false, false, true)]
[InlineData(false, true, true, true)]
[InlineData(false, true, false, true)]
[InlineData(false, false, true, true)]
[InlineData(false, false, false, true)]
[InlineData(true, true, true, false)]
[InlineData(true, true, false, false)]
[InlineData(true, false, true, false)]
[InlineData(true, false, false, false)]
[InlineData(false, true, true, false)]
[InlineData(false, true, false, false)]
[InlineData(false, false, true, false)]
[InlineData(false, false, false, false)]
public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout, bool externalLogin, bool rememberClient)
[InlineData(true, true, true, true, true)]
[InlineData(true, true, false, true, true)]
[InlineData(true, false, true, true, true)]
[InlineData(true, false, false, true, true)]
[InlineData(false, true, true, true, true)]
[InlineData(false, true, false, true, true)]
[InlineData(false, false, true, true, true)]
[InlineData(false, false, false, true, true)]
[InlineData(true, true, true, false, true)]
[InlineData(true, true, false, false, true)]
[InlineData(true, false, true, false, true)]
[InlineData(true, false, false, false, true)]
[InlineData(false, true, true, false, true)]
[InlineData(false, true, false, false, true)]
[InlineData(false, false, true, false, true)]
[InlineData(false, false, false, false, true)]
[InlineData(true, true, true, true, false)]
[InlineData(true, true, false, true, false)]
[InlineData(true, false, true, true, false)]
[InlineData(true, false, false, true, false)]
[InlineData(false, true, true, true, false)]
[InlineData(false, true, false, true, false)]
[InlineData(false, false, true, true, false)]
[InlineData(false, false, false, true, false)]
[InlineData(true, true, true, false, false)]
[InlineData(true, true, false, false, false)]
[InlineData(true, false, true, false, false)]
[InlineData(true, false, false, false, false)]
[InlineData(false, true, true, false, false)]
[InlineData(false, true, false, false, false)]
[InlineData(false, false, true, false, false)]
[InlineData(false, false, false, false, false)]
public async Task CanTwoFactorSignIn(bool isPersistent, bool supportsLockout, bool externalLogin, bool rememberClient, bool supportsUserActivity)
{
// Setup
var user = new TestUser { UserName = "Foo" };
@ -559,6 +593,11 @@ namespace Microsoft.AspNetCore.Identity.Test
manager.Setup(m => m.IsLockedOutAsync(user)).ReturnsAsync(false).Verifiable();
manager.Setup(m => m.ResetAccessFailedCountAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
}
if (supportsUserActivity)
{
manager.Setup(m => m.SupportsUserActivity).Returns(true).Verifiable();
manager.Setup(m => m.UpdateLastSignInDateAsync(user)).ReturnsAsync(IdentityResult.Success).Verifiable();
}
manager.Setup(m => m.VerifyTwoFactorTokenAsync(user, provider, code)).ReturnsAsync(true).Verifiable();
var context = new DefaultHttpContext();
var auth = MockAuth(context);

View File

@ -580,6 +580,17 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.Throws<NotSupportedException>(() => manager.Users.Count());
}
[Fact]
public async Task UsersActivityMethodsFailWhenStoreNotImplemented()
{
var manager = MockHelpers.TestUserManager(new NoopUserStore());
Assert.False(manager.SupportsUserActivity);
await Assert.ThrowsAsync<NotSupportedException>(() => manager.UpdateLastSignInDateAsync(null));
await Assert.ThrowsAsync<NotSupportedException>(() => manager.GetLastSignInDateAsync(null));
await Assert.ThrowsAsync<NotSupportedException>(() => manager.GetLastPasswordChangeDateAsync(null));
await Assert.ThrowsAsync<NotSupportedException>(() => manager.GetCreateDateAsync(null));
}
[Fact]
public async Task UsersEmailMethodsFailWhenStoreNotImplemented()
{
@ -698,146 +709,6 @@ namespace Microsoft.AspNetCore.Identity.Test
Assert.ThrowsAsync<NotImplementedException>(() => manager.GenerateUserTokenAsync(new TestUser(), "A", "purpose"));
}
[Fact]
public void TOTPTest()
{
//var verify = new TotpAuthenticatorVerification();
//var secret = "abcdefghij";
//var secret = Base32.FromBase32(authKey);
// Assert.Equal(bytes, secret);
//var code = verify.VerifyCode(secret, -1);
//Assert.Equal(code, 287004);
//var bytes = new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'!', (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF };
//var base32 = Base32.ToBase32(bytes);
// var code = Rfc6238AuthenticationService.GenerateCode(bytes);
// Assert.Equal(Rfc6238AuthenticationService.GenerateCode(bytes), Rfc6238AuthenticationService.CalculateOneTimePassword(new HMACSHA1(bytes)));
//Assert.True(Rfc6238AuthenticationService.ValidateCode(bytes, code));
}
public static byte[] ToBytes(string input)
{
if (string.IsNullOrEmpty(input))
{
throw new ArgumentNullException("input");
}
input = input.TrimEnd('='); //remove padding characters
int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
byte[] returnArray = new byte[byteCount];
byte curByte = 0, bitsRemaining = 8;
int mask = 0, arrayIndex = 0;
foreach (char c in input)
{
int cValue = CharToValue(c);
if (bitsRemaining > 5)
{
mask = cValue << (bitsRemaining - 5);
curByte = (byte)(curByte | mask);
bitsRemaining -= 5;
}
else
{
mask = cValue >> (5 - bitsRemaining);
curByte = (byte)(curByte | mask);
returnArray[arrayIndex++] = curByte;
curByte = (byte)(cValue << (3 + bitsRemaining));
bitsRemaining += 3;
}
}
//if we didn't end with a full byte
if (arrayIndex != byteCount)
{
returnArray[arrayIndex] = curByte;
}
return returnArray;
}
public static string ToString(byte[] input)
{
if (input == null || input.Length == 0)
{
throw new ArgumentNullException("input");
}
int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
char[] returnArray = new char[charCount];
byte nextChar = 0, bitsRemaining = 5;
int arrayIndex = 0;
foreach (byte b in input)
{
nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
returnArray[arrayIndex++] = ValueToChar(nextChar);
if (bitsRemaining < 4)
{
nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
returnArray[arrayIndex++] = ValueToChar(nextChar);
bitsRemaining += 5;
}
bitsRemaining -= 3;
nextChar = (byte)((b << bitsRemaining) & 31);
}
//if we didn't end with a full char
if (arrayIndex != charCount)
{
returnArray[arrayIndex++] = ValueToChar(nextChar);
while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
}
return new string(returnArray);
}
private static int CharToValue(char c)
{
var value = (int)c;
//65-90 == uppercase letters
if (value < 91 && value > 64)
{
return value - 65;
}
//50-55 == numbers 2-7
if (value < 56 && value > 49)
{
return value - 24;
}
//97-122 == lowercase letters
if (value < 123 && value > 96)
{
return value - 97;
}
throw new ArgumentException("Character is not a Base32 character.", "c");
}
private static char ValueToChar(byte b)
{
if (b < 26)
{
return (char)(b + 65);
}
if (b < 32)
{
return (char)(b + 24);
}
throw new ArgumentException("Byte is not a value Base32 value.", "b");
}
[Fact]
public void UserManagerWillUseTokenProviderInstanceOverDefaults()
{

View File

@ -124,6 +124,10 @@ namespace Microsoft.AspNetCore.Identity.Test
/// </summary>
public virtual int AccessFailedCount { get; set; }
public virtual DateTimeOffset? CreateDate { get; set; }
public virtual DateTimeOffset? LastSignInDate { get; set; }
public virtual DateTimeOffset? LastPasswordChangeDate { get; set; }
/// <summary>
/// Navigation property
/// </summary>