Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET MVC (NetCore 2.0) Generic Controller and Views for Handling Entity Frameworks DbContexts and Objects

0.00/5 (No votes)
22 Mar 2018 1  
This project is an ASPNET Core MVC site with a generic controller for CRUD and searchs.

Introduction

This article and source code has been updated to NetCore 2.0 ASP.NET MVC project.

When we start the development of an ASP.NET MVC website in Microsoft Visual Studio, we find it very easy to create controllers and views for each "table" in our databases by using an Entity Framework DbContext and Visual Studio Tools for self-generate code. However, when your database(s) contains many tables, this operation becomes tedious even to generate one controller by one.

Background

The Basic Rules

For using the code of this article without modifications, you should follow these rules:

  • Use Code First
  • Make sure that in your database design, all entities derive from a Base class.
  • All entities must be flagged with a DisplayTableName attribute.
  • The properties not to be shown at Index views must be flagged as [NotListed]
  • All entities must override ToString() method.
  • The ForeignKey attribute must be set to the ForeignKey property, mapping the naviagation property

.NET Reflection

This technology allow us to fully inspect assemblies and types at runtime getting properties, fields, methods, constructors and many information of a type.

These capabilities are remarkable for reaching our goal: to take an object by its type’s name and create a View for adding/modifying this object and a Controller that handles how to list, add, modify and delete these objects from the database.

Using the Code

First thing to do is modify the Startup.cs file to modify the Default route for processing generic request as follows:

app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}/{id1?}/{id2?}/{id3?}");
            });

Now, we can create the "GenericController<T,T1>" that will handle all ~/<Generic>/Action/some_entity_id requests in the application.

To start coding, we should create a GenericController that will contain methods for CRUD and search.

public partial class GenericController<T,T1> : BaseController<T1> where T : Base where T1 : DbContext, ICustomDbContext
{
}

The following image show us the structure of the database used in this article as example:

The Base Model and other model classes look like this:

    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}";
        }
    }

Several custom attributes are defined on the code:

  • The DisplayTableName: used to display generic views to refer the entities used.
  • The DeleteBehavior: used to define the behavior when deleting and entity, if set to Hide, then when removing an entity, it will be hidden and all entities with foreing keys pointing to the object wiil be hidden to. The same for Delete behavior (CASCADE DELETING)
  • The ChainOfCreation: Composed by a list of types to generate when creating the entity type flagged with this attribute. The create view will compose a HTML5 form with the main entity properties and the main properties of all types defined. An example of a create View look like this:

    Create View
    *Note that diferent background colors shows properties of entity types listed on the ChainOfCreationAttribute.

The DbContext object used by the GenericController should be implemented like this:

    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;
        }
    }

The Controller Actions and Views

The View Code Explains Itself

The Index View

The controller code looks like this very simple code:

/// <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);
}

The View code is available on the source code of this tip.

The GenericController Class

This class contains very usefull methods and views, beside the CRUD methods:

  • Import CSV files
  • Import OpenDocument Excel files (with assignation of columns to properties)
  • Search in table by properties

The Create/Edit Actions

We must generate Edit Create views for these two functions that takes a T Model, its type and its properties and generates all required fields in a form.

At controller's code, we must generate via default constructor the required object and fill all its properties with values coming in the form.

The filling code looks like this:

        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();
            }
        }

In this new version its include a selector for HTML5 controls and input type needed to represent each property type.

The Delete Action

This is very simple. When removing an object from database, the code search for the DeleteBehaivor. If its not defined the Delete is assumed.

        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");
        }

The Controller's Code for Each Model Class

Is possible to generate a controller of any data type based on Models.Base class. In hte definition of the controller class are used to generic parameters, the entity type and the DbContext type. Controllers on this project looks like this:

    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)
        {
        }
    }

History

This new version allows to specify which DbContext is used in the Generic Controller, allowing to have two or more DbContext on your project

This source project will be updated with suggestions from all users.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here