我们在项目开发中,通常会有数据审计的项目需求。即业务数据中要包含创建日期,修改日期,修改人等信息等。有些业务数据需要物理删除,有些数据需要逻辑删除。
通常审计数据并不大量参与业务运算,只是为审计提供技术支持。如果我们在项目开发中,花费大量时间在这些审计数据的处理上,显然得不偿失。
本文提出了一个简单的审计数据处理模型,通过Entityframework加以实现。
审计数据的需求总结- 核心所有业务数据都要包括数据的创建人,创建时间,修改时间和修改人。
- 大部分核心数据要支持逻辑删除,所以要包括删除人,删除时间和删除标记位。
- 部分非核心业务数据需要支持物理删除。
在现实开发中,为了实现审计数据功能,我们需要为核心的业务数据类增加相应的审计数据栏位。但是没有必要每个类逐一定义这些栏位,我们可以使用OOP的继承,多态等特性,简化开发。
本文以一个银行分行的管理系统为例,来说明我们的审计数据实现模型。银行分行的业务员数据包括分行(Branch),ATM机和系统的用户User。
审计数据模型的类图- 上图分为两部分,一部分是审计模型,另一部分是该模型在业务数据上的应用。
- IEntity是一个接口,包括创建人,创建时间,修改时间和修改人。
- Entity是一个抽象类,实现IEnitty接口,因此也包含创建人,创建时间,修改时间和修改人四个栏位。
- IDeletable是一个接口,继承IEntity接口,新增了删除人,删除时间和删除标记位。以支持逻辑删除。
- DeletableEntity是一个抽象类,继承Entity类并实现IDeletable接口。
| 业务数据类 | 继承Entity | 继承DeletableEntity | 实现IDeletableEntity接口 |
|---|---|---|---|
| ATM | 是 | ||
| Branch | 是 | ||
| User | 是 |
- ATM机器升级换代很快,因此需求是直接物理删除数据就可以,每次维护的数据可以记录到数据中。
- 分行数据需要逻辑删除,数据本身需要被追溯,可以直接继承DeletableEntity 类
- User数据需求和分行数据一样,但是User类已经继承了IdentityUser类,因此只能实现IDeletableEntity接口。
通过继承关系和接口的使用,我们避免了为每个数据类都人工定义审计相关的栏位。
但是审计数据的存取依然是一个问题,我们如果在每个类的存取代码中都增加审计数据的处理,显然做就过于繁琐了。
本文的解决方法是在Entityframework中,定义一个拦截器去实现。而我们模型中定义的类和接口,在拦截器中成了我们重要的路标。
核心代码如下:
public class BankContext : DbContext
{
private void AddAuditData() =>
this.ChangeTracker
.Entries()
.ToList()
.ForEach(entry =>
{
if (entry.State == EntityState.Deleted)
{
if (entry.Entity is IDeletableEntity deletableEnitty)
{
deletableEnitty.DeletedBy = 2;
deletableEnitty.DeletedOn = DateTime.Now;
deletableEnitty.IsDeleted = true;
entry.State = EntityState.Modified;
return;
}
}
if (entry.Entity is IEntity entity)
{
if (entry.State == EntityState.Added)
{
entity.CreatedBy = 2;
entity.CreatedOn = DateTime.Now;
}
else if (entry.State == EntityState.Modified)
{
entity.ModifiedBy = 2;
entity.ModifiedOn = DateTime.Now;
}
}
});
public override int SaveChanges()
{
AddAuditData();
return base.SaveChanges();
}
}
- 使用DBContext中的ChangeTracker对象,找到我们存取数据的对象。
- 如果用户要删除该对象,但是该类实现了IDeletableEntity 接口,我们将对象状态改为Modified,填充删除人,删除时间和删除标记位。
A. 删除人即为当前用户,不同系统,获取方式不同,因此本文将其略去,默认写成一个Id为2的用户。
B. 这样用户上层代码就不用再区分物理删除和逻辑删除。所有删除代码都按照物理删除的方式写就行。 - 如果当前存取对象实现了IEntity 接口:
A. 如果是新建数据,则填充创建人和创建时间的栏位
B. 如果是修改数据,则填充修改人和修改时间的栏位 - 复写SaveChanges方法,在数据存取前调用我们定义的方法AddAuditData
- SaveChanges还有其他重载方法,因为修改方式类似,所以不再赘述。
执行EF的migration指令,生成数据Branch,ATM和User数据表。完整类型定义请参考附录。
执行Migration指令:
dotnet ef migrations add InitialCreate dotnet ef database update
执行结果:
从执行结果中我们可以看出,生成的SQL语句中已经包含的审计相关的栏位,我们不希望每个类都定义审计栏位的需求已经实现。
using (var context = new BankContext()){
var u = new User(){
Name = "Tom",
};
context.Useres.Add(u);
context.SaveChanges();
}
CreatedBy和CreatedOn的审计数据被拦截器自动填充
using (var context = new BankContext(connectionString)){
var u = context.Useres.Where( b=>b.Id == 1).FirstOrDefault();
u.Name = "abc";
context.Update(u);
context.SaveChanges();
}
ModifiedBy和ModifiedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){
var u = context.Useres.Where( b=>b.Id == 1).FirstOrDefault();
context.Useres.Remove(u);
context.SaveChanges();
}
物理删除被自动转化为逻辑删除,删除的审计数据栏位被自动填充
using (var context = new BankContext()){
var branch = new Branch(){
Name = "天津新天地支行",
Address = "天津市滨海新区南海路12号新天地华庭A座1门103号",
};
context.Branches.Add(branch);
context.SaveChanges();
}
CreatedBy和CreatedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){
var branch = context.Branches.Where( b=>b.Id == 1).FirstOrDefault();
context.Update(branch);
context.SaveChanges();
}
ModifiedBy和ModifiedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){
var branch = context.Branches.Where( b=>b.Id == 1).FirstOrDefault();
context.Branches.Remove(branch);
context.SaveChanges();
}
物理删除被自动转化为逻辑删除,删除的审计数据栏位被自动填充
using (var context = new BankContext()){
var atm = new ATM(){
Name = "取款机-1",
};
context.ATMs.Add(atm);
context.SaveChanges();
}
CreatedBy和CreatedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){
var atm = context.ATMs.Where( b=>b.Id == 1).FirstOrDefault();
atm.Name = "abc";
context.Update(atm);
context.SaveChanges();
}
ModifiedBy和ModifiedOn的审计数据被拦截器自动填充
using (var context = new BankContext()){
var atm = context.ATMs.Where( b=>b.Id == 1).FirstOrDefault();
context.ATMs.Remove(atm);
context.SaveChanges();
}
根据需求,ATM数据需要物理删除,因此ATM类没有继承DeletableEntity或实现IDeletableEntity接口,因此直接删除
附录ATM类
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace IQueryableIEnumerable
{
[Table("t_atm")]
public class ATM : Entity{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), Column(Order = 0)]
public int Id { get; set; }
[Required, Column(Order = 1)]
public string Name { get; set; }
[Timestamp, Column(Order = 20)]
public byte[] RowVersion { get; set; }
}
}
Branch类
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace IQueryableIEnumerable
{
[Table("t_branch")]
public class Branch : DeletableEnitty{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), Column(Order = 0)]
public int Id { get; set; }
[Required, Column(Order = 1)]
public string Name { get; set; }
[Required, Column("Addr", Order = 2)]
public string Address { get; set; }
[Timestamp, Column(Order = 20)]
public byte[] RowVersion { get; set; }
}
}
User类
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System;
namespace IQueryableIEnumerable
{
[Table("t_user")]
public class User : IDeletableEntity
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), Column(Order = 0)]
public int Id { get; set; }
[Required, Column(Order = 1)]
public string Name { get; set; }
public DateTime? DeletedOn { get; set; }
public int? DeletedBy { get; set; }
public bool IsDeleted { get; set; } = false;
public int CreatedBy { get; set; }
public DateTime CreatedOn { get; set; }
public int? ModifiedBy { get; set; }
public DateTime? ModifiedOn { get; set; }
[Timestamp, Column(Order = 20)]
public byte[] RowVersion { get; set; }
}
}
BankContext类:
using System;
using System.Linq;
using Microsoft.EntityframeworkCore;
using Microsoft.Extensions.Logging;
namespace IQueryableIEnumerable
{
public class BankContext : DbContext
{
private readonly ILoggerFactory loggerFactory
= LoggerFactory.Create(ConventionForeignKeyExtensions => ConventionForeignKeyExtensions.AddConsole());
private readonly string ConnectionString;
public DbSet Branches { get; set; }
public DbSet Useres { get; set; }
public DbSet ATMs { get; set; }
public BankContext() : base()
{
ConnectionString = @"your connection string";
}
public BankContext(DbContextOptions options) : base(options)
{
ConnectionString = @"your connection string";
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(loggerFactory);
optionsBuilder.UseSqlServer(ConnectionString);
}
public override int SaveChanges()
{
AddAuditData();
return base.SaveChanges();
}
private void AddAuditData() =>
this.ChangeTracker
.Entries()
.ToList()
.ForEach(entry =>
{
if (entry.State == EntityState.Deleted)
{
if (entry.Entity is IDeletableEntity deletableEnitty)
{
deletableEnitty.DeletedBy = 2;
deletableEnitty.DeletedOn = DateTime.Now;
deletableEnitty.IsDeleted = true;
entry.State = EntityState.Modified;
return;
}
}
if (entry.Entity is IEntity entity)
{
if (entry.State == EntityState.Added)
{
entity.CreatedBy = 2;
entity.CreatedOn = DateTime.Now;
}
else if (entry.State == EntityState.Modified)
{
entity.ModifiedBy = 2;
entity.ModifiedOn = DateTime.Now;
}
}
});
}
}



