Quickly create a CRUD application framework on Entity Framework / ASP.Net MVC

    Most of the applications that have to be developed in practice are reduced to a primitive template: there is a certain subject area in which objects and the relationships between them are highlighted. All this is easily represented in the form of tables in the database, and the basic functionality of the application is to perform four basic actions on these tables : create, modify, view and delete objects. Further, usually, additional business logic, a reporting module and the rest of the necessary functionality are screwed onto this basis.
    The natural reaction of the developer’s body to the presence of a certain pattern is the desire to automate its use, for example, using code generation. Joke. Code generation is the same copy-paste method, only a specially written tool does it for the programmer. Sometimes this is justified, but before deciding to generate code, it is better to think carefully, but is it possible to get by with OOP tools, for example?

    Background


    Recently, I had to help my friends write such an application for, say, a course project of a student team. In short, the task was to write a web-application that allowed to generate reports on the database from about twenty tables. The difficulty was that there was no base yet, it had to be designed and entered a decent amount of data manually, and it took about a week to create the application and populate the base, after which it was necessary to show the working prototype to the “customer”. After the demonstration, it was planned to do decorating the interface, expanding the business logic, so the application architecture should have been as flexible as possible. The situation was complicated by the fact that only I had real programming experience, the rest of the project participants in the first stage only help with filling the database or, at most with design and layout. Before rushing to the embrasure, I sat down and thought for a couple of hours how to write a CRUD framework for the application in the shortest possible time, which others can already work with. In this article I will try to voice some thoughts that have helped me a lot in solving the problem and may be useful for relatively inexperienced developers. Experienced, most likely, have repeatedly applied such patterns in practice. will be useful for relatively inexperienced developers. Experienced, most likely, have repeatedly applied such patterns in practice. will be useful for relatively inexperienced developers. Experienced, most likely, have repeatedly applied such patterns in practice.
    Due to historical circumstances (two years of experience as a .Net developer), the application was based on a bunch of MS SQL Server 2008 / ADO.Net Entity Framework / ASP.Net MVC. I deliberately miss some points in the article, such as “how to create a table in a database” or “how to add an ADO.NET Entity Data Model to a project”, you can read about it in other articles. This is about how, by correctly applying the necessary tools, you can quickly create a CRUD framework for an application, which can then be easily worked with.

    Database


    So, as I said above, for all objects in the database you need to provide for the implementation of four basic operations: create, read, update, delete. Accordingly, this must be taken into account from the very beginning when the database is being designed. In the database, this was expressed in the fact that each table contained the primary key "Id":
    Id int not null identity(1,1)

    * This source code was highlighted with Source Code Highlighter.

    Such a general approach will greatly simplify our life in the future, you will soon see why. Next, we sit down and carefully and carefully create tables in the database. Hereinafter, I will show everything using two of them as an example:

    14.61 KB
    The rest of the tables do not differ in anything significant.

    ORM


    So, tables and relationships between them are created. We add the ADO.NET Entity Data Model to the project, having previously specified our database for it, and EF generates the ORM code. Now remember that all of our objects have an Id field that identifies the object. Add the following interface to the code:
    public interface IDataObject
    {
      int Id { get; }
      string DisplayName { get; }
    }

    * This source code was highlighted with Source Code Highlighter.

    All model objects in EF are marked as partial by default, so we can add all the necessary functionality to them (for example, inherit them from the desired interface) without affecting the generated code:
    public partial class HomeWorkUnitType : IDataObject
    {
      public string DisplayName
      {
        get { return Name; }
      }
    }

    public partial class HomeWorkUnit : IDataObject
    {
      public string DisplayName
      {
        get { return string.Format("[{0}] {1}", Id, Theme); }
      }
    }

    * This source code was highlighted with Source Code Highlighter.

    The DisplayName property comes in handy when it comes to the UI.

    Controllers


    So, the next step - for each object of the model you need to create a controller that will support five operations - issuing a list of objects, viewing information about one object, creating, editing and deleting an object. All our objects are inherited from the IDataObject interface and the internal class EF System.Data.Objects.DataClasses.EntityObject. Let's try to get as much logic as possible into the base class of the controller, from which we will inherit the controllers for each object of the model:
    public abstract class DataObjectController : Controller
        where T : EntityObject, IDataObject
    {
    }

    * This source code was highlighted with Source Code Highlighter.

    Each controller will use a data context. We implement it as a protected property using lazy initialization (in some actions it is not used, why create it again?):
    private DataModelEntities m_DataContext;
    protected DataModelEntities DataContext
    {
      get
      {
        if(m_DataContext == null)
        {
          m_DataContext = new DataModelEntities();
        }
        return m_DataContext;
      }
    }

    * This source code was highlighted with Source Code Highlighter.

    To work with a specific object of type T, we add two abstract protected properties:
    protected abstract IQueryable Table { get; }
    protected abstract Action AddObject { get; }

    * This source code was highlighted with Source Code Highlighter.

    Unfortunately, I did not find a normal way how to define these two properties at the base class level, so all controllers returned them depending on the type of object with which they work.
    Next, you need to implement the basic CRUD operations. Here is the final version of the base controller code, which allows you to perform basic operations:
    public abstract class DataObjectController : Controller
        where T : EntityObject, IDataObject
    {
      private DataModelEntities m_DataContext;
      protected DataModelEntities DataContext
      {
        get
        {
          if(m_DataContext == null)
          {
            m_DataContext = new DataModelEntities();
          }
          return m_DataContext;
        }
      }

      protected override void OnActionExecuted(ActionExecutedContext filterContext)
      {
        base.OnActionExecuted(filterContext);
        if (ViewData["Error"] == null)
        {
          foreach (var entry in DataContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified | EntityState.Deleted | EntityState.Added))
          {
            throw new InvalidOperationException("Unsaved entries at data context!");
          }
        }
      }

      protected abstract IQueryable Table { get; }
      protected abstract Action AddObject { get; }

      protected virtual IEnumerable GetAll()
      {
        foreach (var t in Table.AsEnumerable())
        {
          LoadAllDependedObjects(t);
          yield return t;
        }
      }

      protected virtual T GetById(int id)
      {
        var t = Table.First(obj => obj.Id == id);
        LoadAllDependedObjects(t);
        return t;
      }

      protected virtual void CreateItem(T data)
      {
        AddObject(data);
      }

      protected virtual T EditItem(T data)
      {
        T existing = GetById(data.Id);
        DataContext.ApplyPropertyChanges(existing.EntityKey.EntitySetName, data);
        return existing;
      }

      protected virtual void DeleteItem(int id)
      {
        DataContext.DeleteObject(GetById(id));
      }

      public virtual ActionResult Index()
      {
        return View(GetAll());
      }

      public virtual ActionResult Details(int id)
      {
        return View(GetById(id));
      }

      public virtual ActionResult Create()
      {
        LoadAllDependedCollections();
        return View();
      }

      [AcceptVerbs(HttpVerbs.Post)]
      public virtual ActionResult Create(T data)
      {
        try
        {
          ValidateIdOnCreate();
          ValidateModel();
          CreateItem(data);
          DataContext.SaveChanges();
          return RedirectToAction("Index");
        }
        catch (Exception ex)
        {
          LoadAllDependedCollections();
          ViewData["Error"] = ex.JoinMessages();
          return View(data);
        }
      }

      public virtual ActionResult Edit(int id)
      {
        LoadAllDependedCollections();
        return View(GetById(id));
      }

      [AcceptVerbs(HttpVerbs.Post)]
      public virtual ActionResult Edit(T data)
      {
        try
        {
          ValidateModel();
          data = EditItem(data);
          DataContext.SaveChanges();
          return RedirectToAction("Index");
        }
        catch (Exception ex)
        {
          LoadAllDependedCollections();
          ViewData["Error"] = ex.JoinMessages();
          return View(data);
        }
      }

      public virtual ActionResult Delete(int id)
      {
        try
        {
          ValidateModel();
          DeleteItem(id);
          DataContext.SaveChanges();
        }
        catch (Exception ex)
        {
          ViewData["Error"] = ex.JoinMessages();
        }
        return RedirectToAction("Index");
      }

      protected void ValidateModel()
      {
        if(!ModelState.IsValid)
        {
          throw new Exception("Model contains errors.");
        }
      }

      protected virtual void LoadAllDependedCollections()
      {
      }

      protected virtual void LoadAllDependedObjects(T obj)
      {
      }

      protected virtual void ValidateIdOnCreate()
      {
        ModelState["Id"].Errors.Clear();
      }
    }

    * This source code was highlighted with Source Code Highlighter.


    Simple Object Controllers

    Most of the methods are marked as virtual, so that at the controller implementation level for a particular type of object, it would not be difficult to modify their behavior. Here is an example of a child controller for a simple object that is not connected to others in the database (the table does not contain foreign keys):
    public class HomeWorkUnitTypeController : DataObjectController
    {
      protected override IQueryable Table
      {
        get { return DataContext.HomeWorkUnitType; }
      }

      protected override Action AddObject
      {
        get { return DataContext.AddToHomeWorkUnitType; }
      }
    }

    * This source code was highlighted with Source Code Highlighter.


    Complex Object Controllers

    With object controllers whose tables contain foreign keys, things are a little more complicated. When displaying them on the UI, instead of identifiers of related objects, you need to show their DisplayName, which involves loading the associated object from the database, and when creating and editing, let the user select the associated object from the list of existing ones. In order to deal with the problem of related objects, virtual methods were created:
    protected virtual void LoadAllDependedCollections()
    {
    }

    protected virtual void LoadAllDependedObjects(T obj)
    {
    }

    * This source code was highlighted with Source Code Highlighter.

    The first loads all the objects from the linked tables to display the list on the interface, and the second launches lazy initialization for the related objects of a particular object.
    Another problem is that the basic implementation of the Edit and Create methods does not know how to bind objects, so for complex objects you will have to do these methods manually. The controller implementation for an object with foreign keys is as follows:
    public class HomeWorkUnitController : DataObjectController
    {
      protected override IQueryable Table
      {
        get { return DataContext.HomeWorkUnit; }
      }

      protected override Action AddObject
      {
        get { return DataContext.AddToHomeWorkUnit; }
      }

      protected override void LoadAllDependedCollections()
      {
        ViewData["DisciplinePlan"] = DataContext.DisciplinePlan.AsEnumerable();
        ViewData["HomeWorkUnitType"] = DataContext.HomeWorkUnitType.AsEnumerable();
        base.LoadAllDependedCollections();
      }

      protected override void LoadAllDependedObjects(HomeWorkUnit obj)
      {
        obj.DisciplinePlanReference.Load();
        obj.HomeWorkUnitTypeReference.Load();
        base.LoadAllDependedObjects(obj);
      }

      [AcceptVerbs(HttpVerbs.Post)]
      public ActionResult CreateHomeWorkUnit(HomeWorkUnit data, int disciplinePlanId, int workTypeId)
      {
        try
        {
          ValidateIdOnCreate();
          ValidateModel();
          CreateItem(data);
          data.DisciplinePlan = DataContext.DisciplinePlan.First(d => d.Id == disciplinePlanId);
          data.HomeWorkUnitType = DataContext.HomeWorkUnitType.First(c => c.Id == workTypeId);
          DataContext.SaveChanges();
          return RedirectToAction("Index");
        }
        catch (Exception ex)
        {
          LoadAllDependedCollections();
          ViewData["Error"] = ex.JoinMessages();
          return View("Create", data);
        }
      }

      [AcceptVerbs(HttpVerbs.Post)]
      public ActionResult EditHomeWorkUnit(HomeWorkUnit data, int disciplinePlanId, int workTypeId)
      {
        try
        {
          ValidateModel();
          data = EditItem(data);
          data.DisciplinePlan = DataContext.DisciplinePlan.First(d => d.Id == disciplinePlanId);
          data.HomeWorkUnitType = DataContext.HomeWorkUnitType.First(c => c.Id == workTypeId);
          DataContext.SaveChanges();
          return RedirectToAction("Index");
        }
        catch (Exception ex)
        {
          LoadAllDependedCollections();
          ViewData["Error"] = ex.JoinMessages();
          return View("Edit", data);
        }
      }
    }

    * This source code was highlighted with Source Code Highlighter.

    Now the only thing left is to generate Views for each of the created controllers. I’ll talk about how to make this process faster and more enjoyable in the next article.
    Thanks for attention.
    PS. The article deliberately overlooked issues of security, speed, etc., undoubtedly, important things that are not directly related to the subject of the conversation.
    Progg it

    Also popular now: