审计数据在EntityFramework中的解决方案

审计数据在EntityFramework中的解决方案,第1张

审计数据在EntityFramework中的解决方案 概要

我们在项目开发中,通常会有数据审计的项目需求。即业务数据中要包含创建日期,修改日期,修改人等信息等。有些业务数据需要物理删除,有些数据需要逻辑删除。

通常审计数据并不大量参与业务运算,只是为审计提供技术支持。如果我们在项目开发中,花费大量时间在这些审计数据的处理上,显然得不偿失。

本文提出了一个简单的审计数据处理模型,通过Entityframework加以实现。

审计数据的需求总结
  1. 核心所有业务数据都要包括数据的创建人,创建时间,修改时间和修改人。
  2. 大部分核心数据要支持逻辑删除,所以要包括删除人,删除时间和删除标记位。
  3. 部分非核心业务数据需要支持物理删除。
审计数据模型的实现

在现实开发中,为了实现审计数据功能,我们需要为核心的业务数据类增加相应的审计数据栏位。但是没有必要每个类逐一定义这些栏位,我们可以使用OOP的继承,多态等特性,简化开发。

本文以一个银行分行的管理系统为例,来说明我们的审计数据实现模型。银行分行的业务员数据包括分行(Branch),ATM机和系统的用户User。

审计数据模型的类图

  1. 上图分为两部分,一部分是审计模型,另一部分是该模型在业务数据上的应用。
  2. IEntity是一个接口,包括创建人,创建时间,修改时间和修改人。
  3. Entity是一个抽象类,实现IEnitty接口,因此也包含创建人,创建时间,修改时间和修改人四个栏位。
  4. IDeletable是一个接口,继承IEntity接口,新增了删除人,删除时间和删除标记位。以支持逻辑删除。
  5. DeletableEntity是一个抽象类,继承Entity类并实现IDeletable接口。
审计数据模型的应用 业务数据类继承Entity继承DeletableEntity实现IDeletableEntity接口ATM是Branch是User是
  1. ATM机器升级换代很快,因此需求是直接物理删除数据就可以,每次维护的数据可以记录到数据中。
  2. 分行数据需要逻辑删除,数据本身需要被追溯,可以直接继承DeletableEntity 类
  3. 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();
	 }
 }
  1. 使用DBContext中的ChangeTracker对象,找到我们存取数据的对象。
  2. 如果用户要删除该对象,但是该类实现了IDeletableEntity 接口,我们将对象状态改为Modified,填充删除人,删除时间和删除标记位。
    A. 删除人即为当前用户,不同系统,获取方式不同,因此本文将其略去,默认写成一个Id为2的用户。
    B. 这样用户上层代码就不用再区分物理删除和逻辑删除。所有删除代码都按照物理删除的方式写就行。
  3. 如果当前存取对象实现了IEntity 接口:
    A. 如果是新建数据,则填充创建人和创建时间的栏位
    B. 如果是修改数据,则填充修改人和修改时间的栏位
  4. 复写SaveChanges方法,在数据存取前调用我们定义的方法AddAuditData
  5. 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();
}  

物理删除被自动转化为逻辑删除,删除的审计数据栏位被自动填充

创建ATM
using (var context = new BankContext()){
    var atm = new ATM(){
         Name = "取款机-1",
        
     };
     context.ATMs.Add(atm);
     context.SaveChanges();
 }  

CreatedBy和CreatedOn的审计数据被拦截器自动填充

修改ATM
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的审计数据被拦截器自动填充

删除ATM
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;
                    }
                }
            });
    }

}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5677884.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存