You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何自动审计Entity Framework生成的ASP.NET Web API所有CRUD操作

实现ASP.NET Web API控制器CRUD操作的自动审计方案

要实现新增控制器自动具备审计能力的需求,最优雅的方式是结合ASP.NET Web API动作过滤器(Action Filter)和泛型基类控制器,这样既能统一拦截CRUD操作,又能让新增控制器只需继承基类就能自动拥有审计功能。以下是完整的落地方案:


整体思路

  1. 用泛型基类控制器封装通用CRUD逻辑,所有需要审计的业务控制器都继承这个基类。
  2. 编写审计动作过滤器,拦截基类控制器的所有请求,自动收集操作信息(用户ID、操作类型、表名、操作描述等)并写入Audit表。
  3. 通过依赖注入确保过滤器能访问你的DbContext,完成审计记录的持久化。

步骤1:实现审计动作过滤器

这个过滤器会在控制器动作执行完成后,自动收集审计信息并保存到数据库。

using System;
using System.Linq;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations.Schema;

public class AuditActionFilter : ActionFilterAttribute
{
    private readonly YourDbContext _dbContext;

    // 通过依赖注入获取DbContext
    public AuditActionFilter(YourDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public override void OnActionExecuted(HttpActionExecutedContext actionContext)
    {
        // 跳过未处理的异常请求(可根据需求改为记录失败操作)
        if (actionContext.Exception != null && !actionContext.ExceptionHandled)
        {
            return;
        }

        // 1. 获取当前操作的用户ID(根据你的身份验证方式调整,这里假设从Claims中获取)
        short userId = 0;
        var userIdClaim = actionContext.ActionContext.RequestContext.Principal?
            .Claims.FirstOrDefault(c => c.Type == "UserId");
        if (userIdClaim != null)
        {
            short.TryParse(userIdClaim.Value, out userId);
        }

        // 2. 获取当前控制器对应的实体类型和表名
        var controller = actionContext.ActionContext.ControllerContext.Controller;
        var baseControllerType = controller.GetType().BaseType;
        if (!baseControllerType.IsGenericType || 
            baseControllerType.GetGenericTypeDefinition() != typeof(BaseAuditableApiController<>))
        {
            return; // 非审计控制器,跳过
        }

        var entityType = baseControllerType.GetGenericArguments()[0];
        var tableAttr = entityType.GetCustomAttribute<TableAttribute>();
        var tableName = tableAttr?.Name ?? entityType.Name;

        // 3. 根据HTTP方法和动作生成审计信息
        var httpMethod = actionContext.ActionContext.Request.Method.Method;
        var actionName = actionContext.ActionContext.ActionDescriptor.ActionName;
        
        string auditAction, shortDesc, fullDesc;
        switch (httpMethod)
        {
            case "GET":
                auditAction = "GET";
                if (actionName.Equals("Get", StringComparison.OrdinalIgnoreCase) && 
                    actionContext.ActionContext.ActionDescriptor.GetParameters().Any(p => p.ParameterType == typeof(int)))
                {
                    var recordId = actionContext.ActionContext.ActionArguments["id"];
                    shortDesc = $"读取{tableName}单条记录";
                    fullDesc = $"用户[{userId}]读取了{tableName}表中ID为{recordId}的记录";
                }
                else
                {
                    shortDesc = $"读取{tableName}列表";
                    fullDesc = $"用户[{userId}]读取了{tableName}表的全部/分页记录";
                }
                break;
            
            case "POST":
                auditAction = "POST";
                shortDesc = $"创建{tableName}记录";
                fullDesc = $"用户[{userId}]在{tableName}表中新增了一条记录";
                // 可选:从ActionArguments中获取新增实体,记录更多字段信息
                break;
            
            case "PUT":
                auditAction = "PUT";
                var updateId = actionContext.ActionContext.ActionArguments["id"];
                shortDesc = $"更新{tableName}记录";
                fullDesc = $"用户[{userId}]更新了{tableName}表中ID为{updateId}的记录";
                break;
            
            case "DELETE":
                auditAction = "DELETE";
                var deleteId = actionContext.ActionContext.ActionArguments["id"];
                shortDesc = $"删除{tableName}记录";
                fullDesc = $"用户[{userId}]删除了{tableName}表中ID为{deleteId}的记录";
                break;
            
            default:
                return; // 忽略非CRUD的HTTP方法
        }

        // 4. 保存审计记录到数据库
        var auditRecord = new Audit
        {
            AuditDate = DateTime.Now,
            AuditTableName = tableName,
            AuditDescription = fullDesc,
            AuditShortDescription = shortDesc,
            AuditAction = auditAction,
            UserId = userId
        };

        _dbContext.Audit.Add(auditRecord);
        _dbContext.SaveChanges();

        base.OnActionExecuted(actionContext);
    }
}

步骤2:创建泛型基类控制器

所有需要审计的业务控制器只需继承这个基类,就能自动获得CRUD逻辑和审计功能:

using System.Web.Http;
using System.Data.Entity;
using System.Linq;

[AuditActionFilter] // 给基类添加审计过滤器,所有子类自动继承
public class BaseAuditableApiController<T> : ApiController where T : class
{
    protected readonly YourDbContext _dbContext;

    public BaseAuditableApiController(YourDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    // 通用Get单条记录
    public virtual IHttpActionResult Get(int id)
    {
        var entity = _dbContext.Set<T>().Find(id);
        return entity == null ? NotFound() : Ok(entity);
    }

    // 通用Get列表
    public virtual IHttpActionResult Get()
    {
        var entities = _dbContext.Set<T>().ToList();
        return Ok(entities);
    }

    // 通用Post新增
    public virtual IHttpActionResult Post(T entity)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);
        
        _dbContext.Set<T>().Add(entity);
        _dbContext.SaveChanges();
        
        // 自动获取实体主键(兼容不同命名的主键字段)
        var keyProperty = _dbContext.Entry(entity).EntityType.FindPrimaryKey().Properties.First();
        var entityId = keyProperty.GetValue(entity);
        
        return CreatedAtRoute("DefaultApi", new { id = entityId }, entity);
    }

    // 通用Put更新
    public virtual IHttpActionResult Put(int id, T entity)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);
        
        var existingEntity = _dbContext.Set<T>().Find(id);
        if (existingEntity == null) return NotFound();
        
        _dbContext.Entry(existingEntity).CurrentValues.SetValues(entity);
        _dbContext.SaveChanges();
        
        return Ok(existingEntity);
    }

    // 通用Delete删除
    public virtual IHttpActionResult Delete(int id)
    {
        var entity = _dbContext.Set<T>().Find(id);
        if (entity == null) return NotFound();
        
        _dbContext.Set<T>().Remove(entity);
        _dbContext.SaveChanges();
        
        return Ok();
    }
}

步骤3:注册依赖注入

Web API需要配置依赖注入容器,确保过滤器能正确获取YourDbContext实例。这里以Unity容器为例:

using System.Web.Http;
using Unity;
using Unity.AspNet.WebApi;

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // 1. 配置Unity容器
        var container = new UnityContainer();
        container.RegisterType<YourDbContext>(new HierarchicalLifetimeManager());
        container.RegisterType<AuditActionFilter>();
        
        // 2. 设置Web API的依赖解析器
        config.DependencyResolver = new UnityDependencyResolver(container);

        // 3. 路由配置(保持你的原有配置)
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

步骤4:创建业务控制器

新增控制器只需继承基类即可,无需额外编写审计逻辑:

public class ExampleController : BaseAuditableApiController<Example>
{
    public ExampleController(YourDbContext dbContext) : base(dbContext)
    {
    }

    // 可选:添加自定义业务方法,过滤器依然会自动审计
    public IHttpActionResult GetActiveExamples()
    {
        var activeExamples = _dbContext.Set<Example>().Where(e => e.IsActive).ToList();
        return Ok(activeExamples);
    }
}

可选增强:记录字段级变更

如果需要记录Update操作的具体字段变更,可以在基类的Put方法中对比实体前后值,并将信息传递给过滤器:

public virtual IHttpActionResult Put(int id, T entity)
{
    if (!ModelState.IsValid) return BadRequest(ModelState);
    
    var existingEntity = _dbContext.Set<T>().Find(id);
    if (existingEntity == null) return NotFound();
    
    // 保存原始值
    var originalValues = _dbContext.Entry(existingEntity).CurrentValues.Clone();
    // 更新实体
    _dbContext.Entry(existingEntity).CurrentValues.SetValues(entity);
    _dbContext.SaveChanges();
    
    // 将变更信息存入请求上下文,供过滤器读取
    actionContext.ActionContext.Request.Properties["OriginalValues"] = originalValues;
    actionContext.ActionContext.Request.Properties["UpdatedValues"] = _dbContext.Entry(existingEntity).CurrentValues;
    
    return Ok(existingEntity);
}

然后在过滤器的PUT分支中,读取这些值生成更详细的审计描述:

case "PUT":
    var updateId = actionContext.ActionContext.ActionArguments["id"];
    shortDesc = $"更新{tableName}记录";
    
    // 读取变更信息
    if (actionContext.ActionContext.Request.Properties.TryGetValue("OriginalValues", out object origValues) &&
        actionContext.ActionContext.Request.Properties.TryGetValue("UpdatedValues", out object newValues))
    {
        var origDict = ((DbPropertyValues)origValues).ToObject();
        var newDict = ((DbPropertyValues)newValues).ToObject();
        fullDesc = $"用户[{userId}]更新了{tableName}表中ID为{updateId}的记录:{Newtonsoft.Json.JsonConvert.SerializeObject(origDict)} -> {Newtonsoft.Json.JsonConvert.SerializeObject(newDict)}";
    }
    else
    {
        fullDesc = $"用户[{userId}]更新了{tableName}表中ID为{updateId}的记录";
    }
    break;

内容的提问来源于stack exchange,提问作者Pablito0951

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:25:44