ASP.NET MVC (NetCore 2.0) 通用控制器和视图,用于处理 Entity Frameworks DbContexts 和对象






4.96/5 (13投票s)
该项目是一个 ASPNET Core MVC 站点,包含一个通用的 CRUD 和搜索控制器。
引言
本文及源代码已更新至 NetCore 2.0 ASP.NET MVC 项目。
当我们开始在 Microsoft Visual Studio 中开发 ASP.NET MVC 网站时,通过使用 Entity Framework DbContext
和 Visual Studio 工具自动生成代码,为数据库中的每个“表”创建控制器和视图会变得非常容易。然而,当你的数据库包含许多表时,即使只生成一个控制器也会变得很繁琐。
背景
基本规则
要使用本文的代码而不进行修改,您应遵循以下规则:
- 使用 Code First
- 请确保在数据库设计中,所有实体都继承自一个
Base
类。 - 所有实体都必须用
DisplayTableName
属性进行标记。 - 不应在 Index 视图中显示的属性必须标记为
[NotListed]
。 - 所有实体都必须重写
ToString()
方法。 - 必须将
ForeignKey
属性设置到ForeignKey
属性上,映射导航属性。
.NET 反射
这项技术允许我们在运行时完全检查程序集和类型,获取属性、字段、方法、构造函数以及类型的许多信息。
这些功能对于实现我们的目标非常显著:通过类型的名称获取一个对象,并创建一个视图来添加/修改该对象,以及一个控制器来处理如何从数据库中列出、添加、修改和删除这些对象。
Using the Code
首先要做的就是修改 Startup.cs 文件,修改默认路由以处理通用请求,如下所示:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}/{id1?}/{id2?}/{id3?}");
});
现在,我们可以创建“GenericController<T,T1>
”,它将处理应用程序中所有 ~/<Generic>/Action/some_entity_id
请求。
要开始编码,我们应该创建一个 GenericController
,其中包含 CRUD 和搜索方法。
public partial class GenericController<T,T1> : BaseController<T1> where T : Base where T1 : DbContext, ICustomDbContext
{
}
下图展示了本文示例中使用的数据库结构。
Base
模型和其他模型类的样子如下:
public class Base
{
[Key()]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[NotListed]
public int Id { get; set; }
[NotListed]
public bool Visible { get; set; } = true;
}
[DisplayTableName(Name = "Author")]
[DeleteBehavior(DeleteBehaviorAttr.Hide)]
public class Author : Base
{
[Display(Name = "Name")]
[MaxLength(150)]
[Required]
public string Name { get; set; }
[Display(Name = "Alias")]
[MaxLength(150)]
[Required]
public string Alias { get; set; }
public override string ToString()
{
return $"{Name}";
}
[Display(Name = "Date of birth")]
[DataType(DataType.Date)]
public DateTime BirthDate { get; set; }
[Display(Name = "Date of death")]
[DataType(DataType.Date)]
public DateTime? Death { get; set; }
[Display(Name = "Web site")]
[DataType(DataType.Url)]
public string WebSite { get; set; }
[Display(Name = "Books")]
public string Books { get { return AuthorBook!=null && AuthorBook.Any()?AuthorBook.Select(i => i.Book.Title).Aggregate((item, next) => item + ", " + next):""; } }
public ICollection<AuthorBook> AuthorBook { get; set; }
}
[DisplayTableName(Name = "Books")]
[ChainOfCreation(typeof(BookPrice),typeof(BookInGenre),typeof(AuthorBook))]
[DeleteBehavior(DeleteBehaviorAttr.Hide)]
public class Book : Base
{
[MaxLength(150)]
[Required]
public string Title { get; set; }
public ICollection<AuthorBook> AuthorBook { get; set; }
public ICollection<BookInGenre> BookInGenre { get; set; }
public ICollection<BookPrice> BookPrice { get; set; }
[MaxLength(150)]
public string Editorial { get; set; }
[Range(30,3000)]
public int Pages { get; set; }
public override string ToString()
{
return Title;
}
[Display(Name = "Current price")]
public decimal CurrentPrice { get { return BookPrice!=null&&BookPrice.Any()?BookPrice.OrderByDescending(i => i.Date).Select(i=>i.Price).FirstOrDefault():0; } }
[Display(Name = "Authors")]
public string Authors { get { return AuthorBook!=null&&AuthorBook.Any()?AuthorBook.Select(i=>i.Author.Name).Aggregate((item, next) => item + "," + next):""; } }
[Display(Name = "Genres")]
public string Genres { get { return BookInGenre!=null&&BookInGenre.Any()? BookInGenre.Select(i => i.Genre.Name).Aggregate((item, next) => item + "," + next):""; } }
}
[DisplayTableName(Name = "Prices")]
[DeleteBehavior(DeleteBehaviorAttr.Delete)]
public class BookPrice : Base
{
public DateTime Date { get; set; }
[ForeignKey("Book")]
public int BookID { get; set; }
public Book Book { get; set; }
[DataType(DataType.Currency)]
public decimal Price { get; set; }
}
[DisplayTableName(Name = "Books in genre")]
public class BookInGenre: Base
{
[ForeignKey("Book")]
[Display(Name = "Book")]
public int BookID { get; set; }
public Book Book { get; set; }
[ForeignKey("Genre")]
[Display(Name = "Genre")]
public int GenreID { get; set; }
public Genre Genre { get; set; }
public override string ToString()
{
return Genre.ToString();
}
}
[DisplayTableName(Name = "Genre")]
public class Genre : Base
{
[MaxLength(150)]
[Required]
public string Name { get; set; }
public ICollection<BookInGenre> BookInGenre { get; set; }
[Display(Name = "Books")]
public string Books { get { return BookInGenre.Select(i => i.Book.Title).Aggregate((item, next) => item + ", " + next); } }
public override string ToString()
{
return Name;
}
}
[DisplayTableName(Name = "Book's authors")]
public class AuthorBook: Base
{
[ForeignKey("Author")]
[Display(Name = "Author")]
public int AuthorID { get; set; }
public Author Author { get; set; }
[ForeignKey("Book")]
[Display(Name = "Book")]
public int BookID { get; set; }
public Book Book { get; set; }
public override string ToString()
{
return $"{Author}";
}
}
代码中定义了几个自定义属性。
- DisplayTableName:用于通用视图显示,以引用所使用的实体。
- DeleteBehavior:用于定义删除实体时的行为,如果设置为 Hide,则在删除实体时,该实体将被隐藏,并且所有具有指向该对象的外键的实体也将被隐藏。删除行为(级联删除)也是如此。
- ChainOfCreation:由一个类型列表组成,用于在创建带有此属性的实体类型时生成。创建视图将组合一个 HTML5 表单,其中包含主实体属性以及所有定义类型的核心属性。创建视图的示例如下:
*请注意,不同的背景颜色显示了 ChainOfCreationAttribute 中列出的实体类型的属性。
GenericController
使用的 DbContext
对象应如下实现:
public class LibraryContext: DbContext,ICustomDbContext
{
private ModelBuilder mb;
private Dictionary<string,object> list = new Dictionary<string, object>();
public DbSet<Author> Author { get; set; }
public DbSet<Book> Book { get; set; }
public DbSet<BookPrice> BookPrice { get; set; }
public DbSet<BookInGenre> BookInGenre { get; set; }
public DbSet<Genre> Genre { get; set; }
public DbSet<AuthorBook> AuthorBook { get; set; }
public LibraryContext(DbContextOptions options): base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
this.mb = modelBuilder;
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Author>().ToTable("Author");
modelBuilder.Entity<Book>().ToTable("Book");
modelBuilder.Entity<BookPrice>().ToTable("BookPrice");
modelBuilder.Entity<BookInGenre>().ToTable("BookInGenre");
modelBuilder.Entity<Genre>().ToTable("Genre");
modelBuilder.Entity<AuthorBook>().ToTable("AuthorBook");
}
/// <summary>
/// Get a dictionary of a table values with the database Key property and Value as the representation string of the class
/// </summary>
/// <param name="type">Type of the requested Table</param>
/// <returns></returns>
public List<KeyValuePair<object,string>> GetTable(Type type)
{
//Get the DbContext Type
var ttype = GetType();
//The DbContext properties
var props = ttype.GetProperties().ToList();
// The DbSet property with base type @type
var prop = props.Where(i => i.PropertyType.GenericTypeArguments.Any()&&i.PropertyType.GenericTypeArguments.First() == type).FirstOrDefault();
//The DbSet instance
var pvalue = prop?.GetValue(this);
// Dictionary to return
var l = new Dictionary<object, string>();
var pv = (IEnumerable<object>)pvalue;
//The entity Key property
var keyprop = type.GetProperties().First(i => i.CustomAttributes.Any(j => j.AttributeType == typeof(KeyAttribute)));
//Fills the dictionary
foreach (Base item in pv)
{
//with the key and the ToString() entity result
l.Add(keyprop.GetValue(item), item.ToString());
}
return l.ToList();
}
/// <summary>
/// Get a table casted to Objects
/// </summary>
/// <param name="type">Type of the requested Table</param>
/// <param name="cast">Only to generate a different method signature</param>
/// <returns></returns>
public IEnumerable<object> GetTable(Type type,bool cast = true)
{
//Get the DbContext Type
var ttype = GetType();
//The DbContext properties
var props = ttype.GetProperties().ToList();
// The DbSet property with base type @type
var prop = props.Where(i => i.PropertyType.GenericTypeArguments.Any() && i.PropertyType.GenericTypeArguments.First() == type).FirstOrDefault();
//The DbSet instance
var pvalue = prop?.GetValue(this);
// Dictionary to return
var l = new Dictionary<object, string>();
var pv = (IEnumerable<object>)pvalue;
return pv;
}
}
控制器操作和视图
视图代码不言自明。
索引视图
控制器代码看起来像这样非常简单的代码:
/// <summary>
///
/// </summary>
/// <param name="id">Page index</param>
/// <param name="id1">Items per page</param>
/// <returns></returns>
public IActionResult Index(int? id, int? id1)
{
var data = _context.Set<T>().OrderBy(i => i.Id).ToList();
var type = typeof(T);
var attr = type.CustomAttributes.Where(i => i.AttributeType ==
typeof(DisplayTableNameAttribute)).FirstOrDefault();
ViewBag.TypeName = attr.NamedArguments.First().TypedValue.Value;
var props = type.GetProperties().ToList().Where
(i => !i.PropertyType.Name.StartsWith("ICollection")).ToList();
var props1 = props.Where(i => i.CustomAttributes.Any
(k => k.AttributeType == typeof(ForeignKeyAttribute))).ToList();
props = props.Except(props1).ToList();
ViewBag.props = props;
return View(data);
}
View
代码可在本技巧的源代码中找到。
GenericController 类
除了 CRUD 方法外,此类还包含非常有用的方法和视图。
- 导入 CSV 文件
- 导入 OpenDocument Excel 文件(可将列分配给属性)
- 按属性搜索表
创建/编辑操作
我们必须为这两个函数生成 Edit Create
视图,它们接收一个 T Model
、其类型及其属性,并生成表单中所有必需的字段。
在控制器代码中,我们必须通过默认构造函数生成所需的对象,并用表单中传入的值填充所有属性。
填充代码看起来像这样:
public IActionResult Create()
{
var props = PrepareEditorView<T>(null);
props = AddCreationChainProperties(props);
ViewBag.props = props;
return View();
}
/// <summary>
/// Adds the types included on the ChainOfCreation custom attribute to a list of HtmlPropertyControl
/// </summary>
/// <param name="props">Current properties</param>
/// <param name="model">Model</param>
/// <returns></returns>
private List<HtmlPropertyControl> AddCreationChainProperties(List<HtmlPropertyControl> props,Base model = null)
{
var mtype = typeof(T);
var etypes = new Dictionary<Type, string>();
//Quitar los ID y por cada propiedad agregada en lo siguiente, eliminar la propiedad asociada
if (mtype.CustomAttributes.Any(i => i.AttributeType == typeof(ChainOfCreationAttribute)))
{
var types = (IEnumerable<CustomAttributeTypedArgument>)mtype.CustomAttributes.First(i => i.AttributeType == typeof(ChainOfCreationAttribute)).ConstructorArguments.First().Value;
foreach (var type in types)
{
var color = "#" + r.Next(128, 255).ToString("X") + r.Next(128, 255).ToString("X") + r.Next(128, 255).ToString("X");
props.AddRange(PrepareEditorView(null, true, (Type)type.Value, new[] { typeof(T) }, ((Type)type.Value).Name + "_",color));
etypes.Add((Type)type.Value, ((Type)type.Value).Name + "_");
}
}
ViewBag.ETypes = etypes;
// Removing PrimaryKey properties, and NotListed attributes
var p1 = props.Where(i => !i.Attributes.Any(ia => ia.AttributeType == typeof(KeyAttribute) || ia.AttributeType == typeof(NotListedAttribute))).ToList();
foreach (var item in p1)
{
if (item.Attributes.Any(i=>i.AttributeType==typeof(ForeignKeyAttribute)))
{
if (!ViewData.ContainsKey(item.propertyInfo.Name))
item.SelectorName = ViewData.Keys.First(i => i.EndsWith(item.propertyInfo.Name));
}
}
return p1;
}
[HttpPost]
public IActionResult Create(T model, IFormCollection fm)
{
try
{
_context.Add<T>(model);
_context.SaveChanges();
var l1 = AddCreationChainProperties(new List<HtmlPropertyControl>());
foreach (KeyValuePair<Type,string> item in ViewBag.ETypes)
{
var obj = item.Key.GetConstructors()[0].Invoke(null);
var props = item.Key.GetProperties().Where(i=>!i.CustomAttributes.Any(j=>j.AttributeType==typeof(KeyAttribute)||j.AttributeType==typeof(NotListedAttribute))|| !i.PropertyType.Name.StartsWith("ICollection")).ToList();
foreach (var prop in props)
{
var prop1 = prop;
if (prop1.CustomAttributes.Any(i=>i.AttributeType==typeof(ForeignKeyAttribute)))
{
var prop2 = props.First(i => i.Name == (prop1.CustomAttributes.First(j => j.AttributeType == typeof(ForeignKeyAttribute)).ConstructorArguments.First().Value.ToString()));
if (prop2.PropertyType==typeof(T)) // If property type is model type then
{
prop1.SetValue(obj, model.Id);
}
else
{
var pc = l1.FirstOrDefault(i => i.SelectorName == item.Value + prop.Name);
if (pc!=null)
{
var value = int.Parse(fm[pc.SelectorName]);
prop1.SetValue(obj, value);
}
}
}
else
{
var pc = l1.FirstOrDefault(i => i.SelectorName == item.Value + prop.Name);
if (pc != null || fm.ContainsKey(prop1.Name))
{
object value = fm[pc!=null?pc.SelectorName:prop1.Name];
if (new[]{ typeof(DateTime), typeof(Int32) , typeof(Int64) , typeof(Int16) , typeof(Double) , typeof(Decimal) }.Contains( prop.PropertyType))
{
var tp = prop.PropertyType.GetMethods().First(i => i.Name == "Parse");
object v1 = tp.Invoke(null, new[] { value.ToString() }); //ver por que no sale del método TryParse
prop1.SetValue(obj, v1);
}
else
{
prop1.SetValue(obj, value);
}
}
}
}
_context.Add(obj);
_context.SaveChanges();
}
return RedirectToAction("Index");
}
catch (Exception ee)
{
ViewBag.Exception = ee;
var props = PrepareEditorView<T>(model).Where(i => !i.Attributes.Any(j => j.AttributeType == typeof(KeyAttribute))).ToList();
props = AddCreationChainProperties(props);
ViewBag.props = props;
return View();
}
}
在新版本中,它包含了一个 HTML5 控件选择器,以及表示每个属性类型所需的输入类型。
删除操作
这非常简单。当从数据库中删除一个对象时,代码会搜索 DeleteBehaivor。如果没有定义,则假定为删除。
public IActionResult Delete(int id)
{
var element = _context.Find<T>(id);
ViewBag.props = PrepareEditorView<T>(element);
return View(element);
}
[HttpPost]
public IActionResult Delete(IFormCollection fm)
{
var id_element = _context.Find<T>(int.Parse(fm["id"]));
var type = typeof(T);
///Get the user defined property to determinate if remove permanent the database row and it dependencies or hide it all
var dtype = type.CustomAttributes.Any(i => i.AttributeType == typeof(DeleteBehaviorAttribute)) ? (DeleteBehaviorAttr)type.CustomAttributes.First(i => i.AttributeType == typeof(DeleteBehaviorAttribute)).ConstructorArguments.First().Value : DeleteBehaviorAttr.Delete;
List<Base> elements = new List<Base>();
RecursiveCascadesCollection(id_element,type, ref elements);
switch (dtype)
{
case DeleteBehaviorAttr.Delete:
{
_context.RemoveRange(elements.ToArray());
_context.SaveChanges();
_context.Remove<T>(id_element);
_context.SaveChanges();
break;
}
case DeleteBehaviorAttr.Hide:
{
elements.ForEach(k => k.Visible = false);
_context.Find<T>(int.Parse(fm["id"])).Visible = false;
_context.SaveChanges();
break;
}
}
return RedirectToAction("Index");
}
每个模型类的控制器代码
可以基于 Models.Base 类生成任何数据类型的控制器。在控制器类的定义中,使用了两个泛型参数:实体类型和 DbContext 类型。项目中的控制器看起来像这样:
public class AuthorController : GenericController<Author, LibraryContext>
{
public AuthorController(LibraryContext context, IConfiguration config) : base(context,config)
{
}
}
public class BookController : GenericController<Book,LibraryContext>
{
public BookController(LibraryContext context, IConfiguration config) : base(context, config)
{
}
}
public class AuthorBooksController : GenericController<AuthorBook, LibraryContext>
{
public AuthorBooksController(LibraryContext context, IConfiguration config) : base(context, config)
{
}
}
历史
这个新版本允许指定通用控制器中使用的 DbContext,从而可以在项目中拥有两个或多个 DbContext。
此源项目将根据所有用户的建议进行更新。