Entity Framework Code First Common Database Audit Fields - c#

I'm new to Entity Framework, in the past I've used Enterprise Library or ADO.NET directly to map models to database tables. One pattern that I've used is to put my common audit fields that appear in every table in a Base Class and then inherit that Base Class for every object.
I take two steps to protect two of the fields (Created, CreatedBy):
Have a parameterless constructor private on the Base Enitity and create a second one that requires Created and CreatedBy are passed on creation.
Make the setters Private so that the values cannot be changed after the object is created.
Base Class:
using System;
namespace App.Model
{
[Serializable()]
public abstract class BaseEntity
{
public bool IsActive { get; private set; }
public DateTimeOffset Created { get; private set; }
public string CreatedBy { get; private set; }
public DateTimeOffset LastUpdated { get; protected set; }
public string LastUpdatedBy { get; protected set; }
private BaseEntity() { }
protected BaseEntity(DateTimeOffset created, string createdBy)
{
IsActive = true;
Created = created;
CreatedBy = createdBy;
LastUpdated = created;
LastUpdatedBy = createdBy;
}
}
}
Inherited Class:
using System;
namespace App.Model
{
[Serializable()]
public class Person : BaseEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public Person(DateTimeOffset created, string createdBy) :
base(created, createdBy) { }
}
}
I've run into issues with both. EF requires a parameterless constructor to create objects. EF will not create the database columns that have a private setter.
My question is if there is a better approach to accomplish my goals with EF:
Require that the values for Created and CreatedBy are populated at instantiation.
The values of Created and CreatedBy cannot be changed.

You could instantiate a context with a constructor that accepts a string createdBy. Then in an override of SaveChanges():
public override int SaveChanges()
{
foreach( var entity in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added)
.Select (e => e.Entity)
.OfType<BaseEntity>())
{
entity.SetAuditValues(DateTimeOffset.Now, this.CreatedBy);
}
return base.SaveChanges();
}
With SetAuditValues() as
internal void SetAuditValues(DateTimeOffset created, string createdBy)
{
if (this.Created == DateTimeOffset.MinValue) this.Created = created;
if (string.IsNullOrEmpty(this.CreatedBy)) this.CreatedBy = createdBy;
}
After the entities have been materialized from the database the values won't be overwritten when someone calls SetAuditValues.

You shouldn't be trying to control access rights directly on your entities/data layer instead your should do this in your application layer. This way you have a finer level of control over what users can do what.
Also rather than have the audit fields repeated on every table you might want to store your Audit records in another table. This is easy to do with code first:
public class AuitRecord
{
public bool IsActive { get; set; }
public DateTimeOffset Created { get; set; }
public string CreatedBy { get; set; }
public DateTimeOffset LastUpdated { get; set; }
public string LastUpdatedBy { get; set; }
}
You would then link the base class with the audit record to it:
public abstract class BaseEntity
{
public AuditRecord Audit { get; set; }
}
And finally your actually entities
public class Person : BaseEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
You can no access the audit data by going:
Person.Audit.IsActive

Related

Entity Framework database update returning "cannot find object error"

I have a class User in a class library as follows:
Superclass called Base:
namespace Shared_class_library.Models;
public abstract class Base
{
public Base()
{
LastUpdated = DateTime.Now;
CreatedDate = CreatedDate ?? LastUpdated;
}
public Guid Id { get; set; }
public DateTime? LastUpdated { get; set; }
public DateTime? CreatedDate { get; set; }
}
Then a class called User which inherits from Base:
namespace Shared_class_library.Models;
public class User : Base
{
private User()
{
}
public User(string name, string email)
{
Name = name;
}
public string Name { get; set; }
public List<DietaryRequirement>? DietaryRequirements { get; set; }
public Squad? Squad { get; set; }
public List<ReferenceChampion>? ChampionRoles { get; set; }
}
I have a class called ReferenceChampions:
namespace Shared_class_library.Models
{
public class ReferenceChampion : Base
{
public ReferenceChampion()
{
}
public ReferenceChampion(string name)
{
Name = name;
}
public string Name { get; set; }
public List<User> users { get; set; }
}
}
I want there to be a many to many relationship between User and Champion, i.e a user can be many different type of champion, and each type of champion can have many different users.
I define this in my DbContext like this:
modelBuilder.Entity<User>()
.HasMany(u => u.ChampionRoles)
.WithMany(u => u.users);
The problem I am having is when I run the migration command
dotnet ef migrations add firstmigration
and then
dotnet ef database update
I get this error:
Cannot find the object "DietaryRequirements" because it does not exist or you do not have permissions.
The class in question DietaryRequirements is:
namespace Shared_class_library.Models;
public class DietaryRequirement : Base
{
private DietaryRequirement()
{
}
public DietaryRequirement(string requirement)
{
Requirement = requirement;
}
public string Requirement { get; set; }
}
I'm not sure why this error is coming up as there isn't any relationship here between ReferenceChampion and DietaryRequirement.
Any ideas why?

Automapper, Entity Framework Core and multiple nested collections

My database has two tables - RuleGroups and Rules. My Entity Framework classes are the following:
public class RuleGroup
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<Rule> Rules { get; set; }
}
public class Rule
{
[Key]
public Guid Id { get; set; }
public Guid RuleGroupId { get; set; }
public string Name { get; set; }
public ICollection<Condition> Conditions { get; set; }
[ForeignKey("RuleGroupId")]
public virtual RuleGroup RuleGroup { get; set; }
}
[NotMapped]
public class Condition
{
public Guid Id { get; set; }
public string Name { get; set; }
}
Class Condition is not mapped because it is being serialized and stored as JSON in Rule Table (using this example)
My DTOS are the following:
public class UpdateRuleGroupDto
{
public string Name { get; set; }
public ICollection<UpdateRuleDto> Rules { get; set; }
}
public class UpdateRuleDto
{
public string Name { get; set; }
public ICollection<UpdateConditionDto> Conditions { get; set; }
}
public class UpdateConditionDto
{
public string Name { get; set; }
}
In my Startup.cs I initialize Automapper :
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.CreateMap<UpdateRuleGroupDto, RuleGroup>();
cfg.CreateMap<UpdateRuleDto, Rule>();
cfg.CreateMap<UpdateConditionDto, Condition>();
}
I have an API controller endpoint that accepts JSON PATCH document to make changes to data stored in database.
public IActionResult Patch(Guid ruleGroupId, [FromBody]JsonPatchDocument<UpdateRuleGroupDto> body)
{
RuleGroup ruleGroupFromRepo = _deviceRules.GetRuleGroup(ruleGroupId);
UpdateRuleGroupDto ruleGroupToPatch = Mapper.Map<UpdateRuleGroupDto>(ruleGroupFromRepo);
// Patching logic here
Mapper.Map(ruleGroupToPatch, ruleGroupFromRepo);
context.SaveChanges();
return NoContent();
}
The problem:
When changes are made/saved, Rules in Rule table change their/get new GUID.
Example, say we have this data in 2 Tables.
RuleGroup Table
[Id][Name]
[ddad5cac-e5a1-4db7-8167-66a6de3b8a0c][Test]
Rule Table
[Id][RuleGroupId][Name][Condition]
[17c38ee8-4158-4ecc-b893-97786fa76e13][ddad5cac-e5a1-4db7-8167-66a6de3b8a0c][Test][[{"Name":"Test"}]]
If I change field [Name] to a new value, Rules Table will look like this.
Rule Table
[Id][RuleGroupId][Name][Condition]
[ba106de8-bcbc-4170-ba56-80fe619cd757][ddad5cac-e5a1-4db7-8167-66a6de3b8a0c][Test2][[{"Name":"Test"}]]
Note that [Id] field has now a new GUID.
EDIT
#Gert Arnold made me realize that I'm not attaching entities.
I ran the following code:
foreach (var item in ruleGroupFromRepo.rules)
{
var x = _context.Entry(item).State;
}
and all the states were Added and not modified. Now I just have to figure out how to do it properly.

Empty Migrations are generated in code first ASP.NET MVC

I have been trying to generate migration but some reason, generated migrations is empty except the basic definition of function up and down but empty inside.
Model class
public class StoryDB
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid id { get; set; }
[Required]
public string StoryContent { get; set; }
private int heartsCount { get; set; }
private int commentsCount { get; set; }
private int shareCount { get; set; }
public DateTime dateCreated { get; set; }
public DateTime dateModified { get; set; }
}
Database Context class
public class StoreDB : DbContext
{
public StoreDB() : base("DefaultConnection")
{
}
public virtual DbSet <StoryDB> Stories { get; set; }
}
Note that I am using the same connection DefaultConnection which is used to by Identity Classes
When I generate the initial seeding class that generates perfectly (i.e. all user tables like roles and users)
However when i try to generate the first migration after seed class, then nothing appears in the class
You have to tell the DbContext which entities you want to map to the DB. In your case StoryDB:
public class StoreDB : DbContext
{
public StoreDB() : base("DefaultConnection")
{
}
public virtual DbSet<StoryDB> Stories { get; set; }
}
Note that DbSet has a generic parameter, which tells EF the actual type to use.

Entity object property with a timestamp/rowversion field?

I am using entity framework 6 and i have a base entity called EntityBase, very simple as so
public abstract class EntityBase : IEntityBase
{
public virtual long Id { get; set; }
}
Each of my entities inherit from this. Now some entities need audit information, so i have first created an interface called IAudit
public interface IAudit
{
Audit Audit { get; set; }
}
And the Audit object like so
public class Audit
{
public bool IsDeleted { get; set; }
public DateTime? DeletedDate { get; set; }
public long? DeletedByUserId { get; set; }
public DateTime CreatedDate { get; set; }
public long CreatedByUserId { get; set; }
public DateTime UpdatedDate { get; set; }
public long UpdatedByUserId { get; set; }
public byte[] RowVersion { get; set; }
}
And if an entity needs audit information, i apply this interface. Here is an example
public class Attachment : EntityBase, IAudit
{
#region IAudit
public Audit Audit { get; set; }
#endregion
public string Name { get; set; }
}
I am using code first, so now in my DbContext, i have this in my OnModelCreating method
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Configurations.Add(new AttachmentConfig());
}
Here is my AttachmentConfig file, which inherits from EntityBaseConfig, both included below
public abstract class EntityBaseConfig<TEntity> : EntityTypeConfiguration<TEntity>
where TEntity : EntityBase
{
public EntityBaseConfig()
{
this.HasKey(e => e.Id);
}
}
class AttachmentConfig : EntityBaseConfig<Attachment>
{
public AttachmentConfig()
: base()
{
this.Property(e => e.Name)
.HasMaxLength(255)
.IsRequired();
}
}
Now when my table is created, the Attachment table has the columns i would expect, but Audit_RowVersion is varbinary(max) instead of timestamp.
I have tried to put this line in each config file but get this error.
this.Property(e => e.Audit.RowVersion).IsRowVersion();
Schema specified is not valid. Errors: (36,14) : error 2039: The
conceptual side property 'RowVersion' has already been mapped to a
storage property with type 'rowversion'. If the conceptual side
property is mapped to multiple properties in the storage model, make
sure that all the properties in the storage model have the same type.
But i do not want to write this line in each file anyway. How can i get the Audit.RowVersion column to generate as a timestamp, and ideally write it once, so that all objects that implement IAudit also get the configured fields?
EDIT:
I have now added a config file for the Audit object, which looks like this
public class AuditConfig : EntityTypeConfiguration<Audit>
{
public AuditConfig() : base()
{
this.Property(e => e.RowVersion)
.IsRowVersion();
}
}
And i call this in the OnModelCreating method, like so, before the other Configuration calls
modelBuilder.Configurations.Add(new AuditConfig());
Now when i run my project, i get the following error
A table can only have one timestamp column. Because table 'Attachment'
already has one, the column 'Audit_RowVersion' cannot be added.
If i look at the database and Attachment table created so far, it has the fields in the Audit object, but they do not have the Audit_ prefix? Maybe this is a clue to someone?
You can use DataAnnotations.
In your model class
public class Audit
{
public bool IsDeleted { get; set; }
public DateTime? DeletedDate { get; set; }
public long? DeletedByUserId { get; set; }
public DateTime CreatedDate { get; set; }
public long CreatedByUserId { get; set; }
public DateTime UpdatedDate { get; set; }
public long UpdatedByUserId { get; set; }
[TimeStamp]
public byte[] RowVersion { get; set; }
}
You can add [TimeStamp] Attribute for your variable.

BaseEntity Property class .net

I am trying to create base entity class for Update and Delete for all classes.
I created class and interfaces below (Do not know correct or not)
Entity Class (This is my base entity class)
public class Entity
{
public int Id { get; set; }
public int? CreatorId { get; set; }
public DateTime? CreatedDate { get; set; }
public bool IsDeleted { get; set; }
public List<Delete> DeletedUser { get; set; }
public List<Update> UpdatedUser { get; set; }
}
Delete Interface
public interface Delete
{
int? DeletedId { get; set; }
DateTime? DeletedTime { get; set; }
}
Update Interface
public class Update
{
public int? UpdatedId { get; set; }
public DateTime? UpdatedTime { get; set; }
}
If i set object below and use UpdatedUser and DeletedUser can not reach DeletedId,DeletedTime or UpdatedId,UpdatedTime
ActionResult
BaseEntity.Entity baseEntity = new BaseEntity.Entity();
baseEntity.UpdatedUser. (UpdatedId) can not reach here
Updated user and Deleted User can be multiple however i can not reach from baseEntity.UpdatedUser. or baseEntity.DeletedUser.
How can i create baseentity class in order to use OOP in .net?
They are not single objects, they are lists. When you have a list and you try to access a property of single object on that list, it doesn't make any sense. If you wanna access properties of a user, then get the user you want first then you can access it's properties.

Categories

Resources