Identity — EFCore Store — ReplaceClaimsAsync — Digitteck
Identity — EFCore Store — ReplaceClaimsAsync
dotnet·4 June 2020·3 min read

Identity — EFCore Store — ReplaceClaimsAsync

While diving into the ASP.NET Identity implementation details, one particular piece of the EFCore UserStore and UserOnlyStore caught my attention. These stores implement all the functionality required by UserManager — including IUserClaimStore:

csharp
public interface IUserClaimStore<TUser> : IUserStore<TUser>
{
    Task<IList<Claim>> GetClaimsAsync(TUser user, CancellationToken cancellationToken);
    Task AddClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken);
    Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken);
    Task RemoveClaimsAsync(TUser user, IEnumerable<Claim> claims, CancellationToken cancellationToken);
    Task<IList<TUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken);
}

The interface works with the framework Claim type directly — not with derivatives of IdentityUserClaim. That is a good design decision: it leaves the choice of storage model to the implementer, and the EFCore implementation converts between the two. IdentityUserClaim is that EFCore model:

csharp
// IdentityUserClaim — note: no Issuer property
public class IdentityUserClaim<TKey>
{
    public virtual int    Id         { get; set; }
    public virtual TKey   UserId     { get; set; }
    public virtual string ClaimType  { get; set; }
    public virtual string ClaimValue { get; set; }

    public virtual Claim ToClaim() => new Claim(ClaimType, ClaimValue);

    public virtual void InitializeFromClaim(Claim claim)
    {
        ClaimType  = claim.Type;
        ClaimValue = claim.Value;
    }
}

Notice that Issuer is absent from the database model. Without an issuer there is little reason for a user to have multiple values for any given claim type — unless you are using claims as a dictionary with types as keys.

The Peculiar ReplaceClaimAsync

The EFCore implementation of ReplaceClaimAsync matches claims by both type and value:

csharp
// EFCore UserStore — original implementation
public async virtual Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim,
    CancellationToken cancellationToken = default)
{
    var matchedClaims = await UserClaims
        .Where(uc => uc.UserId.Equals(user.Id)
                  && uc.ClaimValue == claim.Value   // checks value
                  && uc.ClaimType  == claim.Type)   // checks type
        .ToListAsync(cancellationToken);

    foreach (var matchedClaim in matchedClaims)
    {
        matchedClaim.ClaimValue = newClaim.Value;
        matchedClaim.ClaimType  = newClaim.Type;
    }
}

This raises two questions:

  1. Issuer awareness IdentityUserClaim is extensible. If you add an Issuer column you can justify multiple values per type (Facebook GivenName, Google GivenName, etc.). The current match logic cannot account for that, and the responsibility arguably belongs in the model, not in the query predicate.
  2. Why check the value at all? — We are replacing a claim with a new claim, not replacing a specific type-value pair. If two claims share the same type but different values, both get replaced.

A Cleaner Alternative

Moving the match logic into IdentityUserClaim makes the intent explicit and opens the door to Issuer-aware matching in subclasses:

csharp
// Author's proposal — move match logic into IdentityUserClaim
// IdentityUserClaim would gain a Matches(Claim) method that can consider Issuer
public async virtual Task ReplaceClaimAsync(TUser user, Claim claim, Claim newClaim,
    CancellationToken cancellationToken = default)
{
    var matchedClaims = await UserClaims
        .Where(uc => uc.UserId.Equals(user.Id) && uc.Matches(claim))
        .ToListAsync(cancellationToken);

    foreach (var matchedClaim in matchedClaims)
        matchedClaim.InitializeFromClaim(newClaim);
}

// The Matches method (added to IdentityUserClaim)
public virtual bool Matches(Claim claim)
    => ClaimType == claim.Type && ClaimValue == claim.Value;
    // With Issuer support: && (Issuer == null || Issuer == claim.Issuer)

Tags

.NETASP.NET CoreIdentityEF Core
digitteck

© 2026 Digitteck