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:
*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");
}
public List<KeyValuePair<object,string>> GetTable(Type type)
{
var ttype = GetType();
var props = ttype.GetProperties().ToList();
var prop = props.Where(i => i.PropertyType.GenericTypeArguments.Any()&&i.PropertyType.GenericTypeArguments.First() == type).FirstOrDefault();
var pvalue = prop?.GetValue(this);
var l = new Dictionary<object, string>();
var pv = (IEnumerable<object>)pvalue;
var keyprop = type.GetProperties().First(i => i.CustomAttributes.Any(j => j.AttributeType == typeof(KeyAttribute)));
foreach (Base item in pv)
{
l.Add(keyprop.GetValue(item), item.ToString());
}
return l.ToList();
}
public IEnumerable<object> GetTable(Type type,bool cast = true)
{
var ttype = GetType();
var props = ttype.GetProperties().ToList();
var prop = props.Where(i => i.PropertyType.GenericTypeArguments.Any() && i.PropertyType.GenericTypeArguments.First() == type).FirstOrDefault();
var pvalue = prop?.GetValue(this);
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:
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();
}
private List<HtmlPropertyControl> AddCreationChainProperties(List<HtmlPropertyControl> props,Base model = null)
{
var mtype = typeof(T);
var etypes = new Dictionary<Type, string>();
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;
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))
{
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() });
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);
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.