none
Audit fields in WebAPI RRS feed

  • Question

  • Hi,

    I have a slightly tricky situation at hand and need some help here. I am developing a WebAPI application in C# using Domain Driven Design architecture and using Code First using Entity Framework. Its almost complete but I want some refactoring. 

    So Its WebAPI calls into a Application Layer which gets Repository objects using constructor Injection in Application and connects to the database using EF. A seperate Domain object has all the entities. Application layer gets the domain entities, converts them into ViewModels(DTOs) and sends to API. 

    While things are working, some of my entities require Audit Info i.e. CreatedOn, ModifiedOn, CreatedByUserId, ModifiedByUserId. 

    So currently, I am manually adding these values in Application layer to populate DTO, then populate domain/entity object using automapper, and makes a call to repository to Insert or Update. 

    I want to refactor this code, as the number of entities are increasing, this code is being repeated e.g 

    SomeDTO.CreatedOn = DateTime.Now(),
    SomeDTO.CreatedBy = userId etc... 

    My current entity design is something like this: 

    public class SomeEntity : Entity 
    and Entity is defined as Public class Entity{public Guid Id;}

    Now, I am thinking along the lines to have AuditInfo in this parent Entity 

    So change it to be:
    public class SomeEntity : AuditEntity 
    and Entity is defined as Public class AuditEntity{public Guid Id; public DateTime CreatedOn; public Guid CreatedByUserId etc.. }

    But I am just not able to understand how and when to populate this AuditEntity and how to attach it to SomeEntity. 

    I hope I have explained my challenge well. Thanks alot for any hints.

    Thanks



    vishal

    Friday, March 20, 2020 11:58 AM

Answers

  • Hello,

    Perhaps performing the audit logic in the DbContext. In the following example done with VB.NET with Entity Framework 6 converted to C# BaseEntity class (could also be an Interface) has the properties for the audit.

    Note this is a quick example taken from a code sample I wrote recently for TechNet so there may be things such as mentioning a specific class e.g. Contact1 type which is not needed as the inherit from check supersides this.

    public class BaseEntity
    {
       public DateTime? CreatedAt {get; set;}
       public string CreatedBy {get; set;}
       public DateTime? LastUpdated {get; set;}
       public string LastUser {get; set;}
       public bool? IsDeleted {get; set;}
    }

    In BeforeSave check if the DbEntityEntry inherits BaseEntity and if so set the audit properties. Note I forgot to do the delete state below, it would use the same logic as add and modify.

    public partial class Context : DbContext
    {
    	public Context() : base("name=ContextKaren") 
    	{
    	}
    	public Context(string ConnectionString) : base(ConnectionString)
    	{
    	}
    
    	public virtual DbSet<Contact1> Contact1 {get; set;}
    
    	protected override void OnModelCreating(DbModelBuilder modelBuilder)
    	{
    	}
    
    	/// <summary>
    	/// Responsible for setting created, last updated
    	/// and soft delete property before SaveChanges or SaveChangesAsync
    	/// </summary>
    	private void BeforeSave()
    	{
    		ChangeTracker.DetectChanges();
    		foreach (DbEntityEntry currentEntry in ChangeTracker.Entries())
    		{
    			if (currentEntry.State == EntityState.Added || currentEntry.State == EntityState.Modified)
    			{
    				Type entityType = ObjectContext.GetObjectType(currentEntry.Entity.GetType());
    				if (entityType.IsSubclassOf(typeof(BaseEntity)))
    				{
    					currentEntry.Property("LastUpdated").CurrentValue = DateTime.Now;
    					currentEntry.Property("LastUser").CurrentValue = Environment.UserName;
    
    					if (currentEntry.Entity is Contact1 && currentEntry.State == EntityState.Added)
    					{
    						currentEntry.Property("CreatedAt").CurrentValue = DateTime.Now;
    						currentEntry.Property("CreatedBy").CurrentValue = Environment.UserName;
    					}
    				}
    			}
    			else if (currentEntry.State == EntityState.Deleted)
    			{
    				currentEntry.Property("LastUpdated").CurrentValue = DateTime.Now;
    				currentEntry.Property("LastUser").CurrentValue = Environment.UserName;
    
    				// Change state to modified and set delete flag
    				currentEntry.State = EntityState.Modified;
    				((BaseEntity)currentEntry.Entity).IsDeleted = true;
    			}
    		}
    	}
    	public override Task<int> SaveChangesAsync()
    	{
    		BeforeSave();
    		return base.SaveChangesAsync();
    	}
    
    	public override int SaveChanges()
    	{
    		BeforeSave();
    		return base.SaveChanges();
    	}
    }


    Please remember to mark the replies as answers if they help and unmarked them if they provide no help, this will help others who are looking for solutions to the same or similar problem. Contact via my Twitter (Karen Payne) or Facebook (Karen Payne) via my MSDN profile but will not answer coding question on either.

    NuGet BaseConnectionLibrary for database connections.

    StackOverFlow
    profile for Karen Payne on Stack Exchange



    Friday, March 20, 2020 1:12 PM
    Moderator

All replies

  • Hello,

    Perhaps performing the audit logic in the DbContext. In the following example done with VB.NET with Entity Framework 6 converted to C# BaseEntity class (could also be an Interface) has the properties for the audit.

    Note this is a quick example taken from a code sample I wrote recently for TechNet so there may be things such as mentioning a specific class e.g. Contact1 type which is not needed as the inherit from check supersides this.

    public class BaseEntity
    {
       public DateTime? CreatedAt {get; set;}
       public string CreatedBy {get; set;}
       public DateTime? LastUpdated {get; set;}
       public string LastUser {get; set;}
       public bool? IsDeleted {get; set;}
    }

    In BeforeSave check if the DbEntityEntry inherits BaseEntity and if so set the audit properties. Note I forgot to do the delete state below, it would use the same logic as add and modify.

    public partial class Context : DbContext
    {
    	public Context() : base("name=ContextKaren") 
    	{
    	}
    	public Context(string ConnectionString) : base(ConnectionString)
    	{
    	}
    
    	public virtual DbSet<Contact1> Contact1 {get; set;}
    
    	protected override void OnModelCreating(DbModelBuilder modelBuilder)
    	{
    	}
    
    	/// <summary>
    	/// Responsible for setting created, last updated
    	/// and soft delete property before SaveChanges or SaveChangesAsync
    	/// </summary>
    	private void BeforeSave()
    	{
    		ChangeTracker.DetectChanges();
    		foreach (DbEntityEntry currentEntry in ChangeTracker.Entries())
    		{
    			if (currentEntry.State == EntityState.Added || currentEntry.State == EntityState.Modified)
    			{
    				Type entityType = ObjectContext.GetObjectType(currentEntry.Entity.GetType());
    				if (entityType.IsSubclassOf(typeof(BaseEntity)))
    				{
    					currentEntry.Property("LastUpdated").CurrentValue = DateTime.Now;
    					currentEntry.Property("LastUser").CurrentValue = Environment.UserName;
    
    					if (currentEntry.Entity is Contact1 && currentEntry.State == EntityState.Added)
    					{
    						currentEntry.Property("CreatedAt").CurrentValue = DateTime.Now;
    						currentEntry.Property("CreatedBy").CurrentValue = Environment.UserName;
    					}
    				}
    			}
    			else if (currentEntry.State == EntityState.Deleted)
    			{
    				currentEntry.Property("LastUpdated").CurrentValue = DateTime.Now;
    				currentEntry.Property("LastUser").CurrentValue = Environment.UserName;
    
    				// Change state to modified and set delete flag
    				currentEntry.State = EntityState.Modified;
    				((BaseEntity)currentEntry.Entity).IsDeleted = true;
    			}
    		}
    	}
    	public override Task<int> SaveChangesAsync()
    	{
    		BeforeSave();
    		return base.SaveChangesAsync();
    	}
    
    	public override int SaveChanges()
    	{
    		BeforeSave();
    		return base.SaveChanges();
    	}
    }


    Please remember to mark the replies as answers if they help and unmarked them if they provide no help, this will help others who are looking for solutions to the same or similar problem. Contact via my Twitter (Karen Payne) or Facebook (Karen Payne) via my MSDN profile but will not answer coding question on either.

    NuGet BaseConnectionLibrary for database connections.

    StackOverFlow
    profile for Karen Payne on Stack Exchange



    Friday, March 20, 2020 1:12 PM
    Moderator
  • https://programmingwithmosh.com/net/common-mistakes-with-the-repository-pattern/

    Using the repository  pattern may not be the optimal choice, since EF already uses the repository  pattern.

    Friday, March 20, 2020 6:25 PM
  • Thank you. This is very neat solution. 

    vishal

    Saturday, March 21, 2020 3:17 AM
  • great article but I could not relate this with the problem I was facing. and I have read your some articles which were great.

    vishal

    Saturday, March 21, 2020 3:18 AM
  • great article but I could not relate this with the problem I was facing. and I have read your some articles which were great.

    vishal

    I am just giving you a friendly tip, if you have the repository doing data persistence. That's not its job.

    You have a misusage of the repository pattern over EF that is already using the repository pattern, if you persisting data with the  generic repository pattern.


    Saturday, March 21, 2020 5:22 AM