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。
此源项目将根据所有用户的建议进行更新。


