3

Further to the Stack Overflow question How to set created date and Modified Date to enitites in DB first approach asked by user LP13 and answered by user Ogglas.

I am writing a test project to learn new development approaches and have hit a wall. I am trying to implement the answer provided by Ogglas, however I am unsure how to register the "Wrapper" in AutoFac?

Ogglas's and My Code Example

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();
    }

My AutoFac EF Module Updated

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();
    }
}

I have used the following tutorial as a guide to setting up my project Tutorial Guide Project I would very much appreciate any assistance given. Thank you.

Generic Repository Updated

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 Updated

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

    int SaveChanges();
}

My CurrentUser AutoFac Module

 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 Thank you for formatting my question and making it easier to read. – Peter Wightman Dec 03 '19 at 12:11
  • what is `IMyDbContext`? – qujck Dec 03 '19 at 14:15
  • @qujck it is an interface were I blueprunt my DbContext. See above. – Peter Wightman Dec 03 '19 at 14:59
  • ok, there are conflicting references for injecting this dependency - ideally you would zero in on one `IMyDbContext` for injection (into `GenericRepository` and `ApplicationDbContextUserWrapper`) and don't register `DbContext`. Register `ApplicationDbContext` as `IMyDbContext` and `ApplicationDbContextUserWrapper` as a [decorator](https://alexmg.com/posts/upcoming-decorator-enhancements-in-autofac-4-9) of `IMyDbContext`. – qujck Dec 03 '19 at 15:16
  • @qujck Added AutoFac Module – Peter Wightman Dec 03 '19 at 15:34
  • `MyDbContextWrapper` needs to be registered as a decorator and you're missing a registration for `ICurrentUser`. – qujck Dec 03 '19 at 16:31
  • @qujck Thank you but I have a seperate AutoFac Module for CurrentUser. How would you alter the registration for Wrapper to be a "Decorator"? – Peter Wightman Dec 03 '19 at 17:01
  • Follow the [decorator](https://alexmg.com/posts/upcoming-decorator-enhancements-in-autofac-4-9) link in my earlier comment ... – qujck Dec 03 '19 at 17:03
  • @qujck I have updated my code above. I have tried the commented out code in the EF Module but either does not work or won't compile. – Peter Wightman Dec 03 '19 at 18:21

2 Answers2

1

Here's a minimal working solution.

Interfaces:

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

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

public interface ICurrentUser
{
    string Name();
}

Entity:

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

Mocks:

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;
}

Decorator:

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();
    }
}

And test:

[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);
}
qujck
  • 14,388
  • 4
  • 45
  • 74
  • Thank you for this I very much appreciate it. I have created a new version based on .Net Core 3.1.1 were I am hoping I can do what I need easier. However I have saved a copy of the old web app project and will try and implement your solution over the weekend. – Peter Wightman Dec 05 '19 at 14:01
  • Ok, good luck! Be aware that the .net core DI implementation is lame, for example it does not natively support decorators. – qujck Dec 05 '19 at 14:20
  • I will be implementing your solution on the old web application version. I am going to try a different approach in the .NET Core 3 app. Thanks for all you advice I really appreciated it. – Peter Wightman Dec 06 '19 at 15:13
0
builder.RegisterType<ApplicationDbContextUserWrapper>().AsSelf().

look here: https://autofaccn.readthedocs.io/en/latest/register/registration.html

This explains it very well. basically, you're telling autofac, to "save" a certain type, as itself. you could also specify interfaces and parameters. After you've done this. you need to add the type in your constructor, to make sure it is injected. make sure to also register all dependencies, so they can too be resolved.

generally, it may also be a good idea to use the wrapper as a decorator:

public class ApplicationDbContextUserWrapper : IDbContext
{
    public ApplicationDbContext Context;

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

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

this way, you are explicitly talking to an interface, the same interface as the dbcontext, but wrapping the methods you want to call. so you can still change the behavior without touching the base class.

Glenn van Acker
  • 317
  • 1
  • 14
  • Thank you for your swift response. I cannot figure this out. How does the wrapper get used in place of the DbContect? I have added the GenericRepository to my question above. – Peter Wightman Dec 03 '19 at 12:35
  • i can't see your GenericRepository, sure you added it? basically, if you want to use it, you just add a constructor to your class, and in that constructor you add (ApplicationDbContextUserWrapper wrapper). be aware, that this injection only happens automatically, if the dependent class is also injected. otherwise, you need to fetch it from the ioc container – Glenn van Acker Dec 03 '19 at 12:38
  • Ok, now i see it. replace your DbContext with ApplicationDbContextUserWrapper. Also register your repository with it's interface, and inject that where you need it, it will automatically have your wrapper injected as well. – Glenn van Acker Dec 03 '19 at 12:39
  • I added the Generic Repository to the bottom of the main question above. – Peter Wightman Dec 03 '19 at 12:41
  • I am getting an error with the DbContext inside the wrapper. – Peter Wightman Dec 03 '19 at 15:03
  • None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'MyWebPortal.Dal.Context.MyDbContextWrapper' can be invoked with the available services and parameters: Cannot resolve parameter 'MyWebPortal.Model.ICurrentUser currentUser' of constructor 'Void .ctor(MyWebPortal.Dal.Context.IMyDbContext, MyWebPortal.Model.ICurrentUser)'. – Peter Wightman Dec 03 '19 at 15:18
  • That makes sense, you should have a service that can get the currentUser, rather than injecting it in the constructor of the DbContext. I see what you're trying to do, but it's not really common practice. – Glenn van Acker Dec 04 '19 at 07:29