如何在DbContext中将“创建者”设置为实体

3

针对Stack Overflow问题如何在DB first方法中为实体设置创建日期和修改日期,该问题由用户LP13提出并由用户Ogglas回答。

我正在编写一个测试项目来学习新的开发方法,但遇到了困难。我正在尝试实现Ogglas提供的答案,但我不确定如何在AutoFac中注册“Wrapper”?

Ogglas和我的代码示例

public interface IEntity
{
    DateTime CreatedDate { get; set; }

    string CreatedBy { get; set; }

    DateTime UpdatedDate { get; set; }

    string UpdatedBy { get; set; }
}

public interface IAuditableEntity
{
    DateTime CreatedDate { get; set; }
    string CreatedBy { get; set; }
    DateTime UpdatedDate { get; set; }
    string UpdatedBy { get; set; }
}

public interface ICurrentUser
{
    string GetUsername();
}

public interface ICurrentUser
{
    string Name();
    string GetUserId();
    bool IsUserAuthenticated();
    bool IsUserAdmin();
    bool IsUserManager();
}

public class ApplicationDbContextUserWrapper
{
    public ApplicationDbContext Context;

    public ApplicationDbContextUserWrapper(ApplicationDbContext context, ICurrentUser currentUser)
    {
        context.CurrentUser = currentUser;
        this.Context = context;
    }
}

public class MyDbContextWrapper
{
    public IMyDbContext Context;

    public MyDbContextWrapper(IMyDbContext context, ICurrentUser currentUser)
    {
        context.CurrentUser = currentUser;
        Context = context;
    }
}

public class ApplicationDbContext : DbContext
{

    public ICurrentUser CurrentUser;

    public override int SaveChanges()
    {
        var now = DateTime.Now;

        foreach (var changedEntity in ChangeTracker.Entries())
        {
            if (changedEntity.Entity is IEntity entity)
            {
                switch (changedEntity.State)
                {
                    case EntityState.Added:
                        entity.CreatedDate = now;
                        entity.UpdatedDate = now;
                        entity.CreatedBy = CurrentUser.GetUsername();
                        entity.UpdatedBy = CurrentUser.GetUsername();
                        break;
                    case EntityState.Modified:
                        Entry(entity).Property(x => x.CreatedBy).IsModified = false;
                        Entry(entity).Property(x => x.CreatedDate).IsModified = false;
                        entity.UpdatedDate = now;
                        entity.UpdatedBy = CurrentUser.GetUsername();
                        break;
                }
            }
        }

        return base.SaveChanges();
    }
}

public class MyDbContext : DbContext, IMyDbContext
{
    public ICurrentUser CurrentUser { get; set; }

    public DbSet<Staff> Staff { get; set; }
    public DbSet<AddressStaff> StaffAddresses { get; set; }

    public MyDbContext() : base("Name=MyWebPortalConnection")
    {
        Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyDbContext, MyWebPortalContextMigrationConfiguration>());
    }

    public override int SaveChanges()
    {
        var modifiedEntries = ChangeTracker.Entries().Where(x => x.Entity is IAuditableEntity
                && (x.State == EntityState.Added
                || x.State == EntityState.Modified));

        foreach (var entry in modifiedEntries)
        {
            if (entry.Entity is IAuditableEntity entity)
            {
                var dateTimeZone = DateTimeZoneProviders.Tzdb["Europe/London"];
                var zonedClock = SystemClock.Instance.InZone(dateTimeZone);
                var localDateTime = zonedClock.GetCurrentLocalDateTime();
                var dateTime = new DateTime(localDateTime.Year,
                                            localDateTime.Month,
                                            localDateTime.Day,
                                            localDateTime.Hour,
                                            localDateTime.Minute,
                                            localDateTime.Second);

                if (entry.State == EntityState.Added)
                {
                    entity.CreatedBy = CurrentUser.Name();
                    entity.CreatedDate = dateTime;
                }
                else if (entry.State == EntityState.Modified)
                {
                    entity.UpdatedBy = CurrentUser.Name();
                    entity.UpdatedDate = dateTime;
                }
                else
                {
                    Entry(entity).Property(x => x.CreatedBy).IsModified = false;
                    Entry(entity).Property(x => x.CreatedDate).IsModified = false;
                }
            }
        }

        return base.SaveChanges();
    }

我的 AutoFac EF 模块已更新

public class EFModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        //builder.RegisterType<MyDbContextWrapper>().As<IMtDbContext>();
        //builder.RegisterDecorator<MyDbContextWrapper, IMyDbContext>();
        //builder.RegisterDecorator<MyDbContextWrapper, IMyDbContext>();
        builder.RegisterType<MyDbContextWrapper>().AsSelf().InstancePerLifetimeScope();
        builder.RegisterType(typeof(MyDbContext)).As(typeof(IMyDbContext)).As(typeof(DbContext)).InstancePerLifetimeScope();
        builder.RegisterType(typeof(UnitOfWork)).As(typeof(IUnitOfWork)).InstancePerRequest();
        builder.Register(_ => new HttpClient()).As<HttpClient>().InstancePerLifetimeScope();
    }
}

我已经使用以下教程作为设置我的项目的指南Tutorial Guide Project。非常感谢您提供的任何帮助。谢谢。

通用存储库已更新

public abstract class GenericRepository<T> : IGenericRepository<T> where T : BaseEntity
{
    protected MyDbContextWrapper DbContextWrapper;
    protected DbContext GenericDbContext;
    protected readonly IDbSet<T> GenericDbset;

    protected GenericRepository(MyDbContextWrapper dbContextWrapper)
    {
        DbContextWrapper = dbContextWrapper;
        GenericDbContext = (DbContext)DbContextWrapper.Context;
        GenericDbset = GenericDbContext.Set<T>();
    }

IMyDbContext已更新

public interface IMyDbContext
{
    ICurrentUser CurrentUser { get; set; }
    DbSet<Staff> Staff { get; set; }
    DbSet<AddressStaff> StaffAddresses { get; set; }

    int SaveChanges();
}

我的 CurrentUser AutoFac 模块

 public class CurrentUserModule : Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterAssemblyTypes(Assembly.Load("MyWebPortal.Model"))
               .Where(t => t.Name.EndsWith("User"))
               .AsImplementedInterfaces()
               .InstancePerLifetimeScope();
    }
}

@Nkosi,感谢您对我的问题进行格式化并使其更易于阅读。 - Peter Wightman
@qujck 这是一个接口,我在其中蓝图化了我的 DbContext。请参见上文。 - Peter Wightman
@qujck 添加了AutoFac模块。 - Peter Wightman
@qujck 谢谢,但我有一个独立的 AutoFac 模块用于 CurrentUser。您如何更改 Wrapper 的注册以成为“装饰者”? - Peter Wightman
请点击我之前评论中的装饰器链接... - qujck
显示剩余4条评论
2个回答

1

这是一个最小化的工作解决方案。

接口:

public interface IMyDbContext
{
    DbSet<Staff> Staff { get; }
    DbChangeTracker ChangeTracker { get; }
    int SaveChanges();
}

public interface IAuditableEntity
{
    string CreatedBy { get; set; }
}

public interface ICurrentUser
{
    string Name();
}

实体:

public class Staff : IAuditableEntity
{
    [Key]
    public int Id { get; set; }
    public string CreatedBy { get; set; }
}

模拟:

public class MockCurrentUser : ICurrentUser
{
    public string Name() => "Mock";
}

public class MockDbContext : DbContext, IMyDbContext
{
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        Database.SetInitializer<MockDbContext>(null);
        base.OnModelCreating(modelBuilder);
    }

    public DbSet<Staff> Staff { get; set; }

    public override int SaveChanges() => 1;
}

装饰器:

public class ApplicationDbContextAuditDecorator : IMyDbContext
{
    private readonly IMyDbContext context;
    private readonly ICurrentUser currentUser;

    public ApplicationDbContextAuditDecorator(IMyDbContext context, ICurrentUser currentUser)
    {
        this.context = context;
        this.currentUser = currentUser;
    }
    public DbSet<Staff> Staff { get => this.context.Staff; }

    public DbChangeTracker ChangeTracker => this.context.ChangeTracker;

    public int SaveChanges()
    {
        foreach (var changedEntity in ChangeTracker.Entries())
        {
            if (changedEntity.Entity is IAuditableEntity entity)
            {
                switch (changedEntity.State)
                {
                    case EntityState.Added:
                        entity.CreatedBy = this.currentUser.Name();
                        break;
                }
            }
        }

        return this.context.SaveChanges();
    }
}

并进行测试:

[TestMethod]
public void TestMethod1()
{
    var builder = new ContainerBuilder();
    builder.RegisterType<MockDbContext>().As<IMyDbContext>().InstancePerLifetimeScope();
    builder.RegisterDecorator<ApplicationDbContextAuditDecorator, IMyDbContext>();
    builder.RegisterType<MockCurrentUser>().As<ICurrentUser>();

    var container = builder.Build();
    var context = container.Resolve<IMyDbContext>();
    context.Staff.Add(new Staff());
    context.SaveChanges();

    Assert.AreEqual("Mock", context.Staff.Local.Single().CreatedBy);
}

非常感谢您的帮助,我非常感激。我已经基于 .Net Core 3.1.1 创建了一个新版本,希望我可以更轻松地完成所需的工作。但是,我已经保存了旧的 Web 应用程序项目,并将在周末尝试实施您的解决方案。 - Peter Wightman
好的,祝你好运!请注意,.net core DI实现很糟糕,例如它不支持本地装饰器。 - qujck
我将在旧的Web应用程序版本上实施您的解决方案。我将尝试在.NET Core 3应用程序中采用不同的方法。感谢您的所有建议,我非常感激。 - Peter Wightman

0
builder.RegisterType<ApplicationDbContextUserWrapper>().AsSelf().

请看这里: https://autofaccn.readthedocs.io/en/latest/register/registration.html

这个链接讲解得非常好。基本上,你在告诉 Autofac “保存”一个特定的类型,并以该类型本身命名,你也可以指定接口和参数。完成后,你需要在构造函数中添加该类型,以确保它被注入。还要注册所有依赖项,以便它们也能被解析。

一般来说,将包装器用作装饰器也是一个不错的主意:

public class ApplicationDbContextUserWrapper : IDbContext
{
    public ApplicationDbContext Context;

    public ApplicationDbContextUserWrapper(ApplicationDbContext context, 
           ICurrentUser currentUser)
    {
        context.CurrentUser = currentUser;
        this.Context = context;
     }

 int IDbContext.SaveChanges() => context.SaveChanges();
...

通过这种方式,您可以明确地与接口进行交互,与 dbcontext 相同的接口,但包装您想要调用的方法。因此,在不触及基类的情况下仍然可以更改行为。


我看不到你的GenericRepository,确定已经添加了吗?基本上,如果你想使用它,只需在你的类中添加一个构造函数,在该构造函数中添加(ApplicationDbContextUserWrapper wrapper)。请注意,只有在依赖类也被注入时,这种注入才会自动发生。否则,你需要从IOC容器中获取它。 - Glenn van Acker
好的,现在我明白了。将您的DbContext替换为ApplicationDbContextUserWrapper。还要注册您的存储库及其接口,并在需要时注入它,它将自动注入您的包装器。 - Glenn van Acker
我在上面的主要问题底部添加了通用存储库。 - Peter Wightman
在类型'MyWebPortal.Dal.Context.MyDbContextWrapper'中,没有使用'Autofac.Core.Activators.Reflection.DefaultConstructorFinder'找到的任何构造函数可以使用可用的服务和参数调用: 无法解析构造函数'Void .ctor(MyWebPortal.Dal.Context.IMyDbContext, MyWebPortal.Model.ICurrentUser)'的参数'MyWebPortal.Model.ICurrentUser currentUser'。 - Peter Wightman
有道理,你应该拥有一个可以获取currentUser的服务,而不是将其注入到DbContext的构造函数中。我知道你想做什么,但这并不是常见的做法。 - Glenn van Acker
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接