3 votes

Comment obtenir l'ID de l'utilisateur actuellement connecté dans ApplicationDbContext en utilisant Identity?

J'ai créé une application .net core 2.1 MVC en utilisant le modèle dans Visual Studio avec le préréglage Identity (comptes utilisateur stockés dans l'application) et j'essaie d'automatiser certains champs d'audit.

Essentiellement, ce que j'essaie de faire est de remplacer la méthode SaveChangesAsync() afin que chaque fois que des modifications sont apportées à une entité, l'ID de l'utilisateur actuellement connecté soit défini sur la propriété d'audit de CreatedBy ou ModifiedBy, des propriétés créées en tant que propriétés fantômes sur l'entité.

J'ai examiné ce qui semble être de nombreuses réponses et étonnamment aucune ne fonctionne pour moi. J'ai essayé d'injecter IHttpContext, HttpContext, UserManager, et je ne semble pas pouvoir accéder à une méthode qui renvoie l'ID de l'utilisateur ou j'obtiens une erreur de dépendance circulaire dont je ne comprends pas vraiment la raison.

Je suis vraiment désespéré avec celui-ci. Je pense qu'une tâche comme celle-ci devrait être vraiment simple à faire, mais j'ai vraiment du mal à comprendre comment le faire. Il semble y avoir des solutions bien documentées pour les contrôleurs d'api web ou pour les contrôleurs MVC mais pas pour une utilisation à l'intérieur de ApplicationDbContext.

Si quelqu'un peut m'aider ou au moins me diriger dans la bonne direction, je vous en serais vraiment reconnaissant, merci.

8voto

Alex Herman Points 662

Appelons cela DbContextWithUserAuditing

public class DBContextWithUserAuditing : IdentityDbContext
{
    public string UserId { get; set; }
    public int? TenantId { get; set; }

    public DBContextWithUserAuditing(DbContextOptions options) : base(options) { }

    // ici nous déclarons nos ensembles de base de données

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.NamesToSnakeCase(); // PostgreSQL
        modelBuilder.EnableSoftDelete();
    }

    public override int SaveChanges()
    {
        ChangeTracker.DetectChanges();
        ChangeTracker.ProcessModification(UserId);
        ChangeTracker.ProcessDeletion(UserId);
        ChangeTracker.ProcessCreation(UserId, TenantId);

        return base.SaveChanges();
    }

    public override async Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        ChangeTracker.DetectChanges();
        ChangeTracker.ProcessModification(UserId);
        ChangeTracker.ProcessDeletion(UserId);
        ChangeTracker.ProcessCreation(UserId, TenantId);

        return (await base.SaveChangesAsync(true, cancellationToken));
    }
}

Ensuite, vous avez le pipeline de requête et ce dont vous avez besoin est un hook de filtre où vous définissez votre ID utilisateur

public class AppInitializerFilter : IAsyncActionFilter
{
    private DBContextWithUserAuditing _dbContext;

    public AppInitializerFilter(
        DBContextWithUserAuditing dbContext
        )
    {
        _dbContext = dbContext;
    }

    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next
        )
    {
        string userId = null;
        int? tenantId = null;

        var claimsIdentity = (ClaimsIdentity)context.HttpContext.User.Identity;

        var userIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
        if (userIdClaim != null)
        {
            userId = userIdClaim.Value;
        }

        var tenantIdClaim = claimsIdentity.Claims.SingleOrDefault(c => c.Type == CustomClaims.TenantId);
        if (tenantIdClaim != null)
        {
            tenantId = !string.IsNullOrEmpty(tenantIdClaim.Value) ? int.Parse(tenantIdClaim.Value) : (int?)null;
        }

        _dbContext.UserId = userId;
        _dbContext.TenantId = tenantId;

        var resultContext = await next();
    }
}

Vous activez ce filtre de la manière suivante (fichier Startup.cs)

services
    .AddMvc(options =>
    {
        options.Filters.Add(typeof(OnRequestInit));
    })

Votre application est alors capable de définir automatiquement l'ID utilisateur et l'ID du locataire pour les enregistrements nouvellement créés

public static class ChangeTrackerExtensions
{
    public static void ProcessCreation(this ChangeTracker changeTracker, string userId, int? tenantId)
    {
        foreach (var item in changeTracker.Entries().Where(e => e.State == EntityState.Added))
        {
            item.Entity.CreationTime = DateTime.Now;
        }

        foreach (var item in changeTracker.Entries().Where(e => e.State == EntityState.Added))
        {
            item.Entity.CreatorUserId = userId;
        }

        foreach (var item in changeTracker.Entries().Where(e => e.State == EntityState.Added))
        {
            if (tenantId.HasValue)
            {
                item.Entity.TenantId = tenantId.Value;
            }
        }
    }

Je ne recommande pas d'injecter HttpContext, UserManager ou tout autre chose dans votre classe DbContext car de cette manière vous violez le Principe de Responsabilité Unique.

3voto

João Paiva Points 580

Merci pour toutes les réponses. En fin de compte, j'ai décidé de créer un UserResolveService qui reçoit grâce à DI le HttpContextAccessor et qui peut ensuite obtenir le nom de l'utilisateur actuel. Avec le nom, je peux alors interroger la base de données pour obtenir toutes les informations dont j'ai besoin. Ensuite, j'injecte ce service sur ApplicationDbContext.

IUserResolveService.cs

public interface IUserResolveService
{
    Task GetCurrentSessionUserId(IdentityDbContext dbContext);
}

UserResolveService.cs

public class UserResolveService : IUserResolveService
{
    private readonly IHttpContextAccessor httpContextAccessor;

    public UserResolveService(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    public async Task GetCurrentSessionUserId(IdentityDbContext dbContext)
    {
        var currentSessionUserEmail = httpContextAccessor.HttpContext.User.Identity.Name;

        var user = await dbContext.Users                
            .SingleAsync(u => u.Email.Equals(currentSessionUserEmail));
        return user.Id;
    }
}

Vous devez enregistrer le service au démarrage et l'injecter sur ApplicationDbContext et vous pouvez l'utiliser ainsi :

ApplicationDbContext.cs

var dbContext = this;

var currentSessionUserId = await userResolveService.GetCurrentSessionUserId(dbContext);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X