65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2015年1月2日

CPOL

4分钟阅读

viewsIcon

55214

downloadIcon

971

该项目是一个 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 表单,其中包含主实体属性以及所有定义类型的核心属性。创建视图的示例如下:

    Create View
    *请注意,不同的背景颜色显示了 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。

此源项目将根据所有用户的建议进行更新。

© . All rights reserved.