C# EF核心通用上下文。查找CRUD(创建/检索/更新/删除)视图

C# EF核心通用上下文。查找CRUD(创建/检索/更新/删除)视图,c#,asp.net-core,entity-framework-core,crud,C#,Asp.net Core,Entity Framework Core,Crud,考虑以下情况,其中控制器是泛型的,其中T:DbContext,其中U:class,new() 注意:这适用于有许多表的情况,比如说在正在搭建的一些现有数据库中有25+甚至高达100+,并且您希望在后端有一个管理CRUD视图,用于列出/添加/更新/删除每个表的相同记录,当运行诸如Scaffold DbContext之类的工具时,DbContext模型中的数据库模式发生变化,因此不需要更新 [Route("Get/{id}")] public async Task

考虑以下情况,其中控制器是泛型的,其中T:DbContext,其中U:class,new()

注意:这适用于有许多表的情况,比如说在正在搭建的一些现有数据库中有25+甚至高达100+,并且您希望在后端有一个管理CRUD视图,用于列出/添加/更新/删除每个表的相同记录,当运行诸如Scaffold DbContext之类的工具时,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);
    }
[Route(“Get/{id}”)]
公共异步任务获取(字符串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),新对象[]{key});
返回新的JsonResult(result);
}
如果需要,您如何使其在主键中一般支持多个字段?有没有更干净的方法来实现上述目标

更新,将ChangeType更改为TypeDescriptor,添加了多个键以查找:

    [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);
    }
[Route(“Get/{id}”)]
公共异步任务获取(字符串id)
{
字符串[]keysStr=id.Split(',');
var entityType=Context.Model.GetEntityTypes().Where(x=>x.ClrType.Name==typeof(U.Name).FirstOrDefault();
var keyProperties=entityType.FindPrimaryKey().Properties.ToArray();
列表键=新列表();
if(keystr.Length!=keyProperties.Length)
{
抛出新ArgumentException(“键必须用逗号分隔,并且必须与表中的键数相同。”);
}

对于(inti=0;i,虽然这似乎是可行的,但我建议不要使用这种方法。 特别有问题的是CRUD的“C”和“U”:你将如何检查类无效? 例如,用户可以在将来提交
生日
,或者在以后提交
开始日期
EndDate
,您将无法阻止这种情况,或者至少会很棘手, 特别是在复杂的前提条件下,如“S国的父实体不得 有超过N个孩子”

如果您想这样做,请考虑让客户端指定实体类型。

大概是这样的:

[ApiController]
[路线(“任何”)]
公共类JackofAlltrades控制器:ControllerBase
{
私有只读MyContext\u ctx;
公共JackOfAllTradesCOntroller(MyContext ctx)
{
_ctx=ctx;
//添加一个随机实体。
//客户配置了:
//modelBuilder.Entity().HasKey(新的[]{“Id”,“FirstName”});
_ctx.Customers.Add(
新客户(){
Id=1,
FirstName=Guid.NewGuid().ToString().Substring(1,6),
LastName=Guid.NewGuid().ToString(),
Address=“Addr”
});
_ctx.SaveChanges();
}
私有IEntityType GetType(字符串类型名)
{
return _ctx.Model.GetEntityTypes()
.Where(x=>x.ClrType.Name==typeName).First();
}
[路由(“GetAll/{entityType}”)]
公共IActionResult GetAll(字符串entityType)
{
entityType eType=GetType(entityType);
MethodInfo集合=_ctx.GetType().GetMethod(“集合”)
.MakeGenericMethod(eType.ClrType);
object result=set.Invoke(_ctx,新对象[0]);
返回新的JsonResult(result);
}
[路由(“Get/{entityType}”)]
公共IActionResult Get(
字符串entityType,
[FromQuery(Name=“id”)]string[]id)
{
entityType eType=GetType(entityType);
IProperty[]keyProperties=eType.FindPrimaryKey().Properties.ToArray();
//…在此验证id。。。
object[]keys=id.Zip(
关键属性,
(i,p)=>Convert.ChangeType(i,p.PropertyInfo.PropertyType)
).ToArray();
对象结果=_ctx.Find(eType.ClrType,keys);
返回新的JsonResult(result);
}
}
演示:


虽然这似乎是可行的,但我建议不要采用这种方法。 问题尤其突出的是CRUD的“C”和“U”:你进展如何 检查类别无效?例如,用户可以在 在EndDate之后的将来或开始日期,您将无法阻止 这一点,或者至少会很棘手,特别是在复杂的情况下 前提条件,如“状态S中的父实体不得超过 N儿童”。 如果您想这样做,请考虑让客户端指定实体类型。

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

由于Scaffold DbContext为模型和DbContext创建分部类,因此一种方法是在单独的位置为该上下文创建分部类,以免被工具更改覆盖,或者您可以子类化并实现一个可用于预验证的接口

这里有一个简单的例子:

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" };
    }
}
公共接口IPrevalidateModel
{
验证结果验证(类型t,对象值);
}
公共类验证结果
{
公共bool成功{get;set;}
公共字符串消息{get;set;}
公共对象数据{get;set;}
}
公共类ToDoContext:Models.ToDoApp.ToDoAppDbContext,IPrevalidateModel
{
public ToDoContext():base()
{
}
公共ToDoContext(DbContextOptions选项)
:基本(选项)
{
}
公共验证结果验证(类型t,对象值)
{
如果(t==typeof(ToDo))
{
ToDo validate=(ToDo)值;
如果(validate.Done>validate.Started)
{
返回新的ValidationResult(){Success=false,Message=“Error:完成时间为gr”
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 })); });
    }
}