用于 CRUD (Create/Retrieve/Update/Delete) 视图的 EF Core 通用 Context.Find

EF Core Generic Context.Find for CRUD (Create/Retrieve/Update/Delete) Views

考虑以下控制器是 generic where T : DbContext where U : class, new()

注意:这适用于您有很多 table 的情况,比方说在您正在使用的某些现有数据库中有 25+ 甚至高达 100+脚手架,并且您希望 listing/adding/updating/deleting 的后端有一个管理 CRUD 视图,每个 table 的记录都相同,这不需要更新,因为 DbContext 模型中的数据库架构在 运行 像 Scaffold-DbContext 这样的工具。

    [Route("Get/{id}")]
    public async Task<IActionResult> Get(string id)
    {
        var entityType = Context.Model.GetEntityTypes().Where(x => x.ClrType.Name == typeof(U).Name).FirstOrDefault();
        var keyProperty = entityType.FindPrimaryKey().Properties.ToArray()[0];
        var key = Convert.ChangeType(id, keyProperty.PropertyInfo.PropertyType);
        var result = Context.Find(typeof(U), new object[] { key });
        return new JsonResult(result);
    }

如果需要,您如何使它普遍支持主键中的多个字段?有没有更简洁的方法来执行上述操作?

Update,将 ChangeType 更改为 TypeDescriptor,向 Find 添加了多个键:

    [Route("Get/{id}")]
    public async Task<IActionResult> Get(string id)
    {
        string[] keysStr = id.Split(',');
        var entityType = Context.Model.GetEntityTypes().Where(x => x.ClrType.Name == typeof(U).Name).FirstOrDefault();
        var keyProperties = entityType.FindPrimaryKey().Properties.ToArray();
        List<dynamic> keys = new List<object>();
        if (keysStr.Length != keyProperties.Length)
        {
            throw new ArgumentException("Keys must be comma-separated and must be of the same number as the number of keys in the table.");
        }
        for(int i=0;i<keyProperties.Length;i++)
        {
            //var key = Convert.ChangeType(id, keyProperty.PropertyInfo.PropertyType);
            TypeConverter converter = TypeDescriptor.GetConverter(keyProperties[i].PropertyInfo.PropertyType);
            var key = converter.ConvertFromString(id);
            keys.Add(key);
        }
        var result = Context.Find(typeof(U), keys.ToArray());
        return new JsonResult(result);
    }

使用string.Split(',') 分隔路由中的多个关键字段。更好的方法?

虽然这看起来可行,但我建议不要采用这种方法。 尤其有问题的是 CRUD 的“C”和“U”:你打算如何检查 class 无效? 例如。用户可以在以后提交 Birthday 或者在之后提交 StartDate EndDate,你将无法阻止它,或者至少它会很棘手, 特别是对于复杂的先决条件,例如“状态 S 中的 parent 实体不得 超过 N children".

如果无论如何都要这样做,请考虑让客户端指定实体类型。

像这样:

[ApiController]
[Route("any")]
public class JackOfAllTradesCOntroller : ControllerBase
{
    private readonly MyContext _ctx;

    public JackOfAllTradesCOntroller(MyContext ctx)
    {
        _ctx = ctx;
        // Add a random entity.
        // Customer configured with:
        // modelBuilder.Entity<Customer>().HasKey(new[] { "Id", "FirstName" });
        _ctx.Customers.Add(
            new Customer() {
                Id = 1,
                FirstName = Guid.NewGuid().ToString().Substring(1,6),
                LastName = Guid.NewGuid().ToString(),
                Address = "Addr"
            });
        _ctx.SaveChanges();
    }

    private IEntityType GetType(string typeName)
    {
        return _ctx.Model.GetEntityTypes()
            .Where(x => x.ClrType.Name == typeName).First();
    }

    [Route("GetAll/{entityType}")]
    public IActionResult GetAll(string entityType)
    {
        IEntityType eType = GetType(entityType);
        MethodInfo set = _ctx.GetType().GetMethod("Set")
            .MakeGenericMethod(eType.ClrType);
        object result = set.Invoke(_ctx, new object[0]);
        return new JsonResult(result);
    }

    [Route("Get/{entityType}")]
    public IActionResult Get(
        string entityType,
        [FromQuery(Name = "id")] string[] id)
    {
        IEntityType eType = GetType(entityType);
        IProperty[] keyProperties = eType.FindPrimaryKey().Properties.ToArray();
        // ... validate id here ...
        object[] keys = id.Zip(
            keyProperties,
            (i, p) => Convert.ChangeType(i, p.PropertyInfo.PropertyType)
            ).ToArray();
        object result = _ctx.Find(eType.ClrType, keys);
        return new JsonResult(result);
    }
}

演示:

Although it seems doable, I would advise against this approach. Especially problematic are the "C" and "U" of CRUD: how are you going to check class invaliants? E.g. the user may submit a Birthday in the future or a StartDate after EndDate, and you won't be able to prevent that, or at least it will be tricky, especially with complex preconditions like "a parent entity in state S must not have more than N children". If you want to do it anyway, consider letting the client specify the entity type.

这是一个关于基于 C# 的验证检查 RDBMS 通常不会检查的无效条目的好问题,例如未来的生日,以及其他。

由于 Scaffold-DbContext 为模型和 DbContext 创建部分 classes,一种方法是在单独的位置为上下文创建部分,以免被工具更改覆盖,或者您可以 subclass 并实现一个可用于 pre-validation.

的接口

这是一个简单的:

public interface IPrevalidateModel
{
    ValidationResult Validate(Type t, object value);
}

public class ValidationResult
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public object Data { get; set; }
}

public class ToDoContext : Models.ToDoApp.ToDoAppDbContext, IPrevalidateModel
{
    public ToDoContext() : base()
    {

    }

    public ToDoContext(DbContextOptions<ToDoAppDbContext> options)
        : base(options)
    {
    }

    public ValidationResult Validate(Type t, object value)
    {
        if (t == typeof(ToDo))
        {
            ToDo validate = (ToDo)value;
            if (validate.Done > validate.Started)
            {
                return new ValidationResult() { Success = false, Message = "Error: Done time is greater than Start time.", Data = value };
            }
        }
        return new ValidationResult() { Success = true, Message = "OK" };
    }
}

那个子class是一个简单的 ToDo 应用程序的上下文,并为 pre-validation 提供了一个界面。我一直在想另一种用途可能是 per-table 角色授权检查 context/connection.

中不同的 CRUD 操作

现在让我们回到控制器逻辑,我为逻辑做了一个代理模型class作为控制器和模型的go-between,所以它不受scaffolding-tooling的影响以及减轻控制器中的逻辑阻塞。

这需要一些 clean-up,但为您提供了您想要的大部分主要好东西:

public class ContextCRUD<T> where T : DbContext, IPrevalidateModel
{
    public class DbUpdateResult
    {
        public bool Success { get; set; }
        public string Message { get; set; }
        public int SaveChangesResult { get; set; }
        public object Entity { get; set; }
    }

    public class TableInfo
    {
        public string Name { get; set; }
        public Type DbSetType { get; set; }
        public Type TableType { get; set; }
        public IEntityType EntityType { get; set; }
    }

    T Context { get; set; }
    IServiceProvider Provider { get; set; }
    public List<TableInfo> TableNames { get; set; }

    public ContextCRUD(T context, IServiceProvider provider)
    {
        Context = context;
        Provider = provider;
        TableNames = ListAllTables();
    }

    List<TableInfo> ListAllTables()
    {
        var entityType = Context.Model.GetEntityTypes().ToArray();
        return entityType.Select(x => new TableInfo() { Name = x.ClrType.Name, TableType = x.ClrType, EntityType = x }).ToList();
    }

    TableInfo GetTable(string table)
    {
        var tableInfo = TableNames.Where(x => x.Name == table).FirstOrDefault();
        if (tableInfo == null)
            throw new Exception($"Table not found '{table}'.");
        return tableInfo;
    }

    public async Task<object> GetAll(string table)
    {
        var tableInfo = GetTable(table);
        var contextSetMethod = Context.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "Set" && x.ContainsGenericParameters).FirstOrDefault();
        var cMethod = contextSetMethod.MakeGenericMethod(new Type[] { tableInfo.TableType });
        var results = cMethod.Invoke(Context, null);
        return await Task.FromResult(results);
    }

    public async Task<object> Get(string table, string[] id)
    {
        var tableInfo = GetTable(table);
        var entityType = Context.Model.GetEntityTypes().Where(x => x.ClrType.Name == table).FirstOrDefault();
        var keyProperties = entityType.FindPrimaryKey().Properties.ToArray();
        if (id.Length != keyProperties.Length)
        {
            throw new ArgumentException("Keys must be comma-separated and must be of the same number and the number of keys in the table.");
        }
        object[] keys = id.Zip(keyProperties, (i, p) => { return TypeDescriptor.GetConverter(p.PropertyInfo.PropertyType).ConvertFromString(i); }).ToArray();
        var result = Context.Find(entityType.ClrType, keys.ToArray());
        return await Task.FromResult(result);
    }

    public async Task<List<dynamic>> GetFields(string table)
    {
        var tableInfo = TableNames.Where(x => x.Name == table).FirstOrDefault();
        if (tableInfo == null)
        {
            throw new Exception("Invalid table.");
        }

        var tableType = tableInfo.TableType;

        var props = tableInfo.EntityType.GetProperties();
        List<dynamic> results = new List<dynamic>();
        foreach(var p in props)
        {
            var isGeneric = p.PropertyInfo.PropertyType.GenericTypeArguments.Length > 0;
            string name = p.PropertyInfo.PropertyType.Name;
            if (isGeneric)
            {
                name += "[" + p.PropertyInfo.PropertyType.GenericTypeArguments[0] + "]";
            }
            results.Add(new { Name = p.Name, Type = name });
        }
        return await Task.FromResult(results);
    }

    void UpdateRecordFields(object record, FieldValue[] fields, TableInfo tableInfo)
    {
        var recordProperties = tableInfo.EntityType.GetProperties();
        foreach (var f in fields)
        {
            var recordProperty = recordProperties.Where(x => x.Name == f.Name).FirstOrDefault();
            object value = TypeDescriptor.GetConverter(recordProperty.PropertyInfo.PropertyType).ConvertFromString(f.Value);
            recordProperty.PropertyInfo.SetValue(record, value);
        }
    }

    public async Task<DbUpdateResult> Update(string table, string[] id, FieldValue[] fields)
    {
        var tableInfo = GetTable(table);
        var record = await Get(table, id);
        UpdateRecordFields(record, fields, tableInfo);
        var validateResult = Context.Validate(tableInfo.TableType, record);
        if (!validateResult.Success) { 
            return new DbUpdateResult() { Success = false, Message = validateResult.Message, Entity = record, SaveChangesResult = -1 }; 
        };
        Context.Update(record);
        int result = await Context.SaveChangesAsync();
        return await Task.FromResult(new DbUpdateResult() { Success = (result > 0) ? true : false, Message = "OK", Entity = record, SaveChangesResult = result });
    }

    public async Task<DbUpdateResult> Update(string table, FieldValue[] fields)
    {
        var tableInfo = GetTable(table);
        var record = Activator.CreateInstance(tableInfo.TableType);
        UpdateRecordFields(record, fields, tableInfo);
        var validateResult = Context.Validate(tableInfo.TableType, record);
        if (!validateResult.Success)
        {
            return new DbUpdateResult() { Success = false, Message = validateResult.Message, Entity = record, SaveChangesResult = -1 };
        };
        Context.Update(record);
        int result = await Context.SaveChangesAsync();
        return await Task.FromResult(new DbUpdateResult() { Success = (result > 0) ? true : false, Message = "OK", Entity = record, SaveChangesResult = result });
    }

    public async Task<DbUpdateResult> Delete(string table, string[] id)
    {
        var tableInfo = GetTable(table);
        var record = await Get(table, id);
        Context.Remove(record);
        int result = await Context.SaveChangesAsync();
        return await Task.FromResult(new DbUpdateResult() { Success = (result > 0) ? true : false, Message = "OK", Entity = record, SaveChangesResult = result } );
    }
}

然后是真正的控制器:

public class ToDoController : GenericController<ToDoContext>
{
    public ToDoController(ToDoContext context, IServiceProvider provider, ILogger<CustomLogger> logger) : base(context, provider, logger)
    {
    }
}

[Route("api/[controller]")]
public class GenericController<T> : Controller where T: DbContext, IPrevalidateModel
{
    T Context { get; set; }
    ILogger<CustomLogger> Logger { get; set; }
    ContextCRUD<T> CRUD { get; set; }

    public GenericController(T context, IServiceProvider provider, ILogger<CustomLogger> logger)
    {
        Context = context;
        Logger = logger;
        CRUD = new ContextCRUD<T>(context, provider);
    }

    void LogAction(string action = "")
    {            
        // ... Logger code here ...
    }

    void LogValues(string json)
    {
        ConsoleEx console = new ConsoleEx(); // Note this is a 24-bit ANSI Color Console class I created and am planning to release.
        // ... Logger code here ...
    }

    async Task<IActionResult> RunAction(string actionName, Func<Task<object>> func)
    {
        LogAction(actionName);
        var values = await func();
        LogValues(JsonConvert.SerializeObject(values, Formatting.Indented));
        return new JsonResult(values);
    }

    [Route("{table}/")]
    public Task<IActionResult> GetAll(string table)
    {
        return RunAction("GetAll", async () => { return await CRUD.GetAll(table); });
    }

    [Route("{table}/Get/{id}/")]
    public Task<IActionResult> Get(string table, [ModelBinder(BinderType = typeof(CRUDModelCommaBinder))] string [] id)
    {
        return RunAction("Get", async () => { return await CRUD.Get(table, id); });
    }

    [Route("{table}/GetFields/")]
    public Task<IActionResult> GetFields(string table)
    {
        return RunAction("GetFields", async () => { return await CRUD.GetFields(table); });
    }

    [Route("{table}/Add/{fields}/")]
    public Task<IActionResult> Add(string table, [ModelBinder(BinderType = typeof(CRUDModelFieldValueBinder))] FieldValue[] fields)
    {
        return RunAction("GetFields", async () => { return await CRUD.Update(table, fields); });
    }

    [Route("{table}/Update/{id}/{fields}/")]
    public Task<IActionResult> Update(string table, [ModelBinder(BinderType = typeof(CRUDModelCommaBinder))] string [] id, [ModelBinder(BinderType = typeof(CRUDModelFieldValueBinder))] FieldValue[] fields)
    {
        return RunAction("Update", async () => { return await CRUD.Update(table, id, fields); });
    }

    [Route("{table}/Delete/{id}/")]
    public Task<IActionResult> Delete(string table, [ModelBinder(BinderType = typeof(CRUDModelCommaBinder))] string [] id)
    {
        return RunAction("Delete", async () => { return await CRUD.Delete(table, id); });
    }

    public Task<IActionResult> Index()
    {
        return RunAction("Index (Show Table Names)", async () => { return await Task.FromResult(CRUD.TableNames.Select(x => new { Name = x.Name })); });
    }
}

If you want to do it anyway, consider letting the client specify the entity type.

另请注意,这个是 DbContext 的通用 T,而不是 ,我之前写过一个通用 T 变体,但在 ContextCRUD 中有很多反射逻辑,现在它被简化了,更干净了,尽管您可以摆脱 ContextCRUD 中的 IServiceProvider,但如果您想要将 CreateScope 用于某些其他范围内的服务,例如访问其他 DbContext 或范围内的存储库,则可以使用它。

我会查看您的外部 http 程序并为您提供带有 unicode Pacman 表情符号的自定义 ILogger。不过,更严肃地说,我目前想不出任何其他陷阱,除非你能想到任何东西。然后是制作视图以使用 JSON api.