C# EF核心通用上下文。查找CRUD(创建/检索/更新/删除)视图
考虑以下情况,其中控制器是泛型的,其中T:DbContext,其中U:class,new() 注意:这适用于有许多表的情况,比如说在正在搭建的一些现有数据库中有25+甚至高达100+,并且您希望在后端有一个管理CRUD视图,用于列出/添加/更新/删除每个表的相同记录,当运行诸如Scaffold DbContext之类的工具时,DbContext模型中的数据库模式发生变化,因此不需要更新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
[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 })); });
}
}