Recommended framework: https://nidoframework.codeplex.com/[^]
Introduction
In this whitepaper, I will walk you through developing a simple MVC 3 application architecture using Entity Framework’s
Code First technology. Later hopefully you can evolve this to be an architecture that you can use for small to mid-size web application development.
Try to read this from start to end and that way you will be able to gradually build the architecture together with me.
Pre-Requisites
- Visual Studio 2010
- SQL Server 2005/ 2008
- Entity Framework
- MVC 3
- Create the database
Create the database
In a real world scenario, the Code-First technique does not encourage you to create a database at all. When you code the models, the system creates the respective database for you. However it does have its own issues, and out of the issue list, the most difficult to live with is, it re-creates the whole database every time the DB schema changes. This makes you lose the existing data in the database. I don't want to deal with that hassle, hence as a work-around we will be taking a different approach. We use the
Code-First technique, but with an existing database. As the first step of this, unlike with regular
Code First technique, you have to create the database. What you see below is a simple database I have created for me to do this architecture demonstration.
In general when you create a database, the following few best practices are good to follow:
- Let the composite table have its own primary key.
- Use field
IsActive
to avoid physical deletion of records from the database. If not you will end up losing vital information when records are deleted.
- As a habit use the field Name to describe the table record. This can be used as the record descriptor when displaying records to the user.
Create MVC3 Projects
In this demo I will be using ASP.NET MVC3 Web Application, so let’s create a project and name it "MVC3Demo". Let’s also make it a Razor Internet Application.
I decided to have a separate project for the business logic code. So let's create
a Class Library type project with the name MVC3Demo.Bll.
Examine the Web.Config of the MVC Web Project
The Web.Config of our Web project has a connection string already defined and if you examine that closely, you will notice that the connection string with the name ‘ApplicationServices’ is by default using the SQL-Express database, which is installed with the
Visual Studio 2010 itself. This configuration setting will create a database inside the
App_Data folder for you. This is where the system stores application services, such as membership service (if you create a new user in the MVC sample project, then it uses the membership service, to be precise ‘AspNetSqlMembershipProvider’, to create those users in
the auto-created SQL Express database) related records. However we don’t want to use that database, so let us modify it and also add another connection string for our database that stores our system's data objects.
The modified web configuration file will look like this..
Now, with this change, we add another entry with the name DemoDbContext
and you need to use this very same name to name the database context class that we created to communicate with the database via
Entity-Framework. This context class has to be created by inheriting the .NET Framework system class
DbContext
. This way the system auto-looks for a configuration tag with the name of the class that inherits
DbContext
to find out the connection string details. Additionally in our case we decide to keep the application service setting untouched, so that we have the option of even using a separate database for our role and profile management.
Create Membership Database
You need to use the command named aspnet_regsql to newly create the membership database in our database server. In order to do that, let’s go to Star >> ‘All Programs’ >> ‘Microsoft Visual Studio 2010’ >> ‘Visual Studio Tools’ >> ‘Visual Studio Command Prompt (2010)’ and type 'aspnet_regsql'. This will drive you through a wizard and let you create the ‘AccessControlDemo’ database in for your database.
Run the Project and verify that new users can be registered with the system and you can login to the system with those newly created users.
Install ‘EntityFramework’ Package for your BLL Project
Since our business logic layer is a Class Library type project, you do not have
the required reference for it to be the Entity Framework back end. You can use the Library Package Manager, if you have it (it installs automatically with MVC 3.0), to install
the required 'Entity-Framework DLL' for this project. In order to do that, from within your project in Visual Studio 2010, go to Tools > Library Package Manager > Package Manager Console. In the console, after the "PM>" prompt, type "install-package entityframework",
and this will install the package and add the ‘EntityFramework’ reference to
your WEB project. Use the Installed location and reference EntityFramework.Dll to your BLL project.
Create the DemoDbContext Class
As stated a few times before, this has to be created by inheriting the DbContext
class.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
namespace MVC3Demo.Bll
{
public class DemoDbContext : DbContext
{
}
}
Once this is done, you have almost completed the design framework for your project.
The remainder is all about building your system on top of this established framework.
Create Model Classes
These model classes are pretty much like the model classes that you have in the MVC web project itself. However in this case, we must create them to tally with our database tables. There has to have separate model classes for each table and there has to have properties of
the same type as fields of the tables. This separation will allow me to share my Models and model access logic across many other projects too.
When you develop by this technique, you need to be extra careful when naming models and their respective properties. One misspell record will break the whole system, and to make it worse, the system generated errors around this region
are not a help at all.
So having that in mind, let me creates a class with the name Student
. Use 'Copy' and 'Paste' as much as possible to avoid typos. I used to copy the table records like this and..
paste them on to my code like this..
and then modify it to be like this…
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
namespace MVC3Demo.Bll.Models
{
[Table("Student", Schema = "dbo")]
public class Student
{
[ScaffoldColumn(false)]
public int StudentId { get; set; }
[Required]
[StringLength(50, ErrorMessage="{0} cannot exceed {1} characters")]
public string Name { get; set; }
[Required]
public int Age { get; set; }
[Required]
[StringLength(150)]
public string Address { get; set; }
[Required]
[DisplayName("Registered Date")]
public DateTime RegisteredDate { get; set; }
[NotMapped]
public bool SeniorStudent
{
get
{
DateTime today = DateTime.Now;
TimeSpan tSpan = today.Subtract(RegisteredDate);
return (tSpan.TotalDays > 365);
}
}
[Required]
[DisplayName("Is Active")]
public bool IsActive { get; set; }
public string Description { get; set; }
}
}
Here is an important point, I have used attributes to include validation logic. Not only that, you can have
the respective error messages defined and associate them with the property. In addition to that, you have the option of having new properties that are not in the database defined
inside the class with a special attribute (see the NotMapped
attribute) as well. The property named
SeniorStudent
is one such record. In that case you need to mark that with
the NotMapped
attribute. To find out more about property attributes, please look at
this link: MSDN.
Just by following the same pattern you can create model classes for the other two tables as well, that is for the tables ‘Course’ and ‘StudentCourse’.
Educate the Code to Relate Related Classes
Now, you may wonder how the relationship in-between tables have been defined here. That is pretty easy, you can educate the model to maintain its relations like this..
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MVC3Demo.Bll.Models
{
public class StudentCourse
{
public int StudentCourseId { get; set; }
public int CourseId { get; set; }
public Course Course { get; set; }
public int StudentId { get; set; }
public Student Student { get; set; }
}
}
- Since
StudentCourse
has a many-to-one relationship with both
the Student
and Course
classes, you need to define
Student
and Course
objects inside the StudentCourse
class. Tip: Look for Id type properties and as a general practice, add a property with the same name right below it. - Go to the
Student
and Course
classes and add a property of type
ICollection<entity>
to indicate the many sides of the one-to-many relationship.
See how the Course
class looks now..
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MVC3Demo.Bll.Models
{
public class Course
{
public int CourseId { get; set; }
public string Name { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Period { get; set; }
public bool IsWeekEnd { get; set; }
public bool IsActive { get; set; }
public ICollection<StudentCourse> StudentCourses { get; set; }
}
}
Note: We have used this technique to develop a few systems in some of the places I worked for and some of these systems are now run in production environments. In development stage, when naming the properties, we always recommended copy
and paste of the name as a small typo often leads us to errors that are very difficult to track. However this technique is proven to be good for architecting small to mid-size systems.
Create Controllers
Just compile Mvc3Demo.Bll and add its reference to the main web project. Now compile the web-project, as when not compiled, the
Add Controller wizard does not show the modification done in the referenced projects. (I believe Microsoft is aggressively working on some of these minor issues that we see in this version of the release). Then, right click on the controller and fill the respective fields to create the ‘Controller’ and the associated ‘Views’ for our models with names
Student
, Course
, and StudentCourse
.
Compile it and see if everything works. There is one more final adjustment needed. If you run this project as is and try to create a new
Course
, then you will get an "Invalid object name 'dbo.Courses'" error message. This is because the names of the tables in our database
are singular but the context has plural names for the respective lists that directly match with
the tables. To avoid this you need to modify the DemoDbContext
class as given below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using MVC3Demo.Bll.Models;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace MVC3Demo.Bll
{
public class DemoDbContext : DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<course /> Courses { get; set; }
public DbSet<studentcourse /> StudentCourses { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<pluralizingtablenameconvention />();
}
}
}
Go to the Global.asax file and modify the register route method to point the system to
the 'Student' Index page.
Then run the project and see if everything is all right..
Let’s now look at the ‘StudentCourse’ create view and see how the required dropdown box
is being automatically created..
Now with this, we have completed developing a basic system. However if you look at the three controller classes, you will see that there
is duplicate code being written. To improve the overall quality of the code, I decided to add an abstract controller class. That way I can avoid duplicate code, and have a better architecture in place for the system. This way I can allow newly joining developers to easily adjust to the code and write high-quality code with very little learning curve.
Create an Abstract Controller Class
In the process of creating this class, I thought of doing a few more additional refactoring as noted below..
- Add a separate folder called Common, and add a few classes that you may want to commonly use across any project you do to this folder. This includes exception logging, handling of cookies, handling of
web.config's application settings data, and a class to have all commonly used strings.
- In addition to this, I decided to add two extensions to the same Common folder. I hope that may come handy at
a later part of this code. One extension is for the
Query
class and another is added for
the View
class. Note: As you develop multiple projects, the classes you find in this folder can easily be grown to a separate project itself. Such projects can be grouped under your company name. As an example, if your company name is XYZ, then
the common project can be named as ‘Xyz.Common’ and let all the projects your company develop reference this project. In this way you can set
a companywide standard for areas in which such standards are applicable.
- Add a separate folder called Libraries. This will keep all third party DLLs that are being referenced by this project. This way you can avoid some of the common DLL reference related errors that you get after deploying
a project into the production environment.
- Add another class called
ConfirmationViewModel
to Web projects
Model folder. This is just to demonstrate how to call a shared view for
the deletion of records. You will see this being used later..
With these refactoring, I managed to remove almost all the duplicated code from my controller class and implement them in my generic abstract class. Finally, the fully re-furbished
CourseController
class looks like this..
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MVC3Demo.Bll.Models;
using MVC3Demo.Bll;
namespace Mvc3Demo.Controllers
{
public class CourseController : BaseController<Course>
{
public CourseController()
: this(new DemoDbContext())
{ }
public CourseController(DemoDbContext db)
: base(db.Courses, db)
{
}
protected override void LoadEditViewBags(Course entity)
{
}
protected override void LoadAddViewBags()
{
}
protected override Type LogPrefix
{
get { return this.GetType(); }
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
The important base controller is given below. A part of this code is taken from one of our internal company projects. Hence
it has a few additional commonly used methods that are being implemented. I have no issues even if you all decide to use this code in your projects.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MVC3Demo.Bll;
using System.Data.Entity;
using System.Data;
using MvcContrib.UI.Grid;
using log4net;
using System.Reflection;
using System.Data.Entity.Infrastructure;
using Mvc3Demo.Common;
using Mvc3Demo.Models;
namespace Mvc3Demo.Controllers
{
public abstract class BaseController<E> : Controller
where E : class
{
protected DbSet<e /> dbEntitySet;
protected DemoDbContext db;
protected readonly ILog logManager;
public BaseController()
{ }
public BaseController(DbSet<e /> dbSet, DemoDbContext dbContext)
{
dbEntitySet = dbSet;
db = dbContext;
logManager = LogManager.GetLogger(LogPrefix);
}
public BaseController(DemoDbContext dbContext)
{
dbEntitySet = null;
db = dbContext;
logManager = LogManager.GetLogger(LogPrefix);
}
public virtual ViewResult Index()
{
return View(dbEntitySet);
}
public virtual PartialViewResult Details(int id)
{
return PartialView(GetDetailsData(id));
}
public virtual ActionResult Create()
{
GetCreateData();
return View();
}
[HttpPost]
public virtual ActionResult Create(E entity, bool? DoNotRedirect)
{
try
{
int i = GetCreatePostBackData(entity, DoNotRedirect);
if (i < 0)
{
LoadEditViewBags(entity);
return View(entity);
}
else
return Redirect(PortalCookie.Get(ConstString.URL_DATA, ConstString.URL_REDIRECT_TO));
}
catch (Exception e)
{
this.AddToMessage(e.Message, MessageTypes.Error);
logManager.Error("Error in Controller", e);
throw e;
}
}
public virtual ActionResult Delete(int id)
{
try
{
PortalCookie.Set(ConstString.URL_DATA, ConstString.URL_REDIRECT_TO, Request.UrlReferrer.ToString());
ConfirmationViewModel confirm = new ConfirmationViewModel();
confirm.Id = id;
confirm.Action = "Delete";
confirm.Controller = typeof(E).Name;
confirm.OperationName = "Delete";
if (Request.IsAjaxRequest())
return PartialView("Confirmation", confirm);
else
return View("Confirmation", confirm);
}
catch (Exception e)
{
this.AddToMessage(e.Message, MessageTypes.Error);
logManager.Error("Error in Controller", e);
throw e;
}
}
[HttpPost, ActionName("Delete")]
public virtual ActionResult DeleteConfirmed(int id)
{
try
{
E entity = dbEntitySet.Find(id);
dbEntitySet.Remove(entity);
int i = db.SaveChanges();
this.AddToMessagePlus(i + " record(s) Deleted Successfully!"
, MessageTypes.Success);
return Redirect(PortalCookie.Get(ConstString.URL_DATA, ConstString.URL_REDIRECT_TO));
}
catch (Exception e)
{
this.AddToMessage(e.Message, MessageTypes.Error);
logManager.Error("Error in Controller", e);
throw e;
}
}
public virtual ActionResult Edit(int id)
{
try
{
E entity = GetEditData(id);
return View(entity);
}
catch (Exception e)
{
this.AddToMessage(e.Message, MessageTypes.Error);
logManager.Error("Error in Controller", e);
throw e;
}
}
[HttpPost]
public virtual ActionResult Edit(E entity, bool? DoNotRedirect)
{
try
{
int i = GetEditPostBackData(entity, DoNotRedirect);
if (i < 0)
{
LoadEditViewBags(entity);
return View(entity);
}
else
return Redirect(PortalCookie.Get(ConstString.URL_DATA, ConstString.URL_REDIRECT_TO));
}
catch (Exception e)
{
this.AddToMessage(e.Message, MessageTypes.Error);
logManager.Error("Error in Controller", e);
throw e;
}
}
protected int EditEntity(E entity)
{
try
{
if (ModelState.IsValid)
{
UpdateTrackingData(entity, false);
db.Entry(entity).State = EntityState.Modified;
int i = db.SaveChanges();
return i;
}
return -2;
}
catch (Exception e)
{
logManager.Error("Error in Controller", e);
return -1;
}
}
protected int CreateEntity(E entity)
{
try
{
if (ModelState.IsValid)
{
UpdateTrackingData(entity, true);
dbEntitySet.Add(entity);
int i = db.SaveChanges();
return i;
}
return -2;
}
catch (Exception e)
{
this.logManager.Error("Db Related Error Occured", e);
return -1;
}
}
protected IEnumerable<e> GetIndexData()
{
try
{
return UpdateIncludes(dbEntitySet).AsEnumerable();
}
catch (Exception e)
{
this.AddToMessage(e.Message, MessageTypes.Error);
logManager.Error("Error in Controller", e);
throw e;
}
}
protected E GetDetailsData(int id)
{
try
{
return dbEntitySet.Find(id);
}
catch (Exception e)
{
this.AddToMessage(e.Message, MessageTypes.Error);
logManager.Error("Error in Controller", e);
throw e;
}
}
protected void GetCreateData()
{
PortalCookie.Set(ConstString.URL_DATA, ConstString.URL_REDIRECT_TO, (Request.UrlReferrer == null)
? Request.RawUrl.ToString() : Request.UrlReferrer.ToString());
LoadAddViewBags();
}
protected E GetEditData(int id)
{
PortalCookie.Set(ConstString.URL_DATA, ConstString.URL_REDIRECT_TO, Request.UrlReferrer.ToString());
E entity = dbEntitySet.Find(id);
LoadEditViewBags(entity);
return entity;
}
protected int GetCreatePostBackData(E entity, bool? DoNotRedirect)
{
int i = CreateEntity(entity);
return AddCreateSatusMessage(DoNotRedirect, i);
}
protected int AddCreateSatusMessage(bool? DoNotRedirect, int i)
{
if (i == ConstString.ERROR_INT)
this.AddToMessage("Error: No record(s) Created!", MessageTypes.Error);
else if (i == ConstString.ERROR_INVALID_OBJECT_INT)
this.AddToMessage("Warning: Some record(s) yet to be filled!", MessageTypes.Warning);
else if ((DoNotRedirect.HasValue) && (DoNotRedirect.Value))
this.AddToMessage(i + " record(s) Created Successfully!", MessageTypes.Success);
else
this.AddToMessagePlus(i + " record(s) Created Successfully!", MessageTypes.Success);
return i;
}
protected int GetEditPostBackData(E entity, bool? DoNotRedirect)
{
int i = EditEntity(entity);
return AddEditStatusMessage(DoNotRedirect, i);
}
protected int AddEditStatusMessage(bool? DoNotRedirect, int i)
{
if (i == ConstString.ERROR_INT)
this.AddToMessage("Error: No record(s) Updated!", MessageTypes.Error);
else if (i == ConstString.ERROR_INVALID_OBJECT_INT)
this.AddToMessage("Warning: Some record(s) yet to be filled!", MessageTypes.Warning);
else if ((DoNotRedirect.HasValue) && (DoNotRedirect.Value))
this.AddToMessage(i + " record(s) Updated Successfully!", MessageTypes.Success);
else
this.AddToMessagePlus(i + " record(s) Updated Successfully!", MessageTypes.Success);
return i;
}
private PagedViewModel<e> createPageViewModel(GridSortOptions gridSortOptions
, int? page, string sortColumn)
{
ShowCurrentMessage();
return new PagedViewModel<e>
{
ViewData = ViewData,
Query = dbEntitySet,
GridSortOptions = gridSortOptions,
DefaultSortColumn = sortColumn,
Page = page,
PageSize = Common.ConstString.PAGE_SIZE,
};
}
private PagedViewModel<e> createPageViewModel(GridSortOptions gridSortOptions
, int? page, string sortColumn, DbQuery<e> dbQuery)
{
ShowCurrentMessage();
return new PagedViewModel<e>
{
ViewData = ViewData,
Query = dbQuery,
GridSortOptions = gridSortOptions,
DefaultSortColumn = sortColumn,
Page = page,
PageSize = Common.ConstString.PAGE_SIZE,
};
}
private void UpdateTrackingData(E entity, bool isNew)
{
this.UpdateTrackingData<e>(entity, isNew);
}
internal BaseController<e> AddToViewBag(string name, SelectList value)
{
ViewData[name] = value;
return this;
}
protected abstract void LoadAddViewBags();
protected abstract void LoadEditViewBags(E entity);
protected virtual IQueryable<e> UpdateIncludes(DbSet<e> dbEntitySet)
{
return dbEntitySet.AsQueryable<e>();
}
protected void UpdateTrackingData<t>(T entity, bool isNew)
{
PropertyInfo pInfoMBy = entity.GetType().GetProperty(ConstString.PROPERTY_MODIFY_BY);
if (pInfoMBy != null)
pInfoMBy.SetValue(entity, Convert.ChangeType(User.Identity.Name, pInfoMBy.PropertyType), null);
PropertyInfo pInfoMDate = entity.GetType().GetProperty(ConstString.PROPERTY_MODIFY_DATE);
if (pInfoMDate != null)
pInfoMDate.SetValue(entity, Convert.ChangeType(DateTime.Now, pInfoMDate.PropertyType), null);
PropertyInfo pInfoABy = entity.GetType().GetProperty(ConstString.PROPERTY_ADDED_DATE);
if (pInfoABy != null)
pInfoABy.SetValue(entity, Convert.ChangeType(DateTime.Now, pInfoABy.PropertyType), null);
}
protected abstract Type LogPrefix
{
get;
}
public class CommonMessage
{
public CommonMessage()
{
this.Message = "";
this.MessageType = MessageTypes.None;
}
public CommonMessage(string message, MessageTypes mType)
{
this.Message = message;
this.MessageType = mType;
}
public string Message { get; set; }
public MessageTypes MessageType { get; set; }
}
public CommonMessage GetFromMessagePlus()
{
string[] tempArray = TempData.GetMessageSummary();
if (tempArray == null)
return null;
if (tempArray.Length > 0)
{
string[] mesgs = tempArray[0].Split('|');
int t;
if (int.TryParse(mesgs[1], out t))
return new CommonMessage(mesgs[0], (MessageTypes)t);
}
return null;
}
public void AddToMessagePlus(string message, MessageTypes mtype)
{
TempData.AddStatusMessage(message + "|" + (int)mtype);
}
public void ShowCurrentMessage()
{
CommonMessage temp = GetFromMessagePlus();
if (temp != null)
AddToMessage(temp.Message, temp.MessageType);
}
public void AddToMessage(string message, MessageTypes mtype)
{
switch (mtype)
{
case MessageTypes.Success:
{
ViewBag.successbox = message;
} break;
case MessageTypes.Warning:
{
ViewBag.warningbox = message;
} break;
case MessageTypes.Error:
{
ViewBag.errormsgbox = message;
} break;
}
}
}
public enum MessageTypes
{
Error,
Warning,
Success,
None
}
}
You need to study this class and see how this can be further improved. Unlike in some of my other articles, I am not going to explain each line of this class, but if any of you have any specific questions, please do post them as a question type comment to this article and I will get back to you with my answer ASAP.
Make the Grid Sortable
There are many fancy grid views around that can be used with MVC2 Razor views. Yes, as you all know, that area needs lot more improvements too (over to MSFT). In my view point, out of the lot, the jQuery grid is the best but I thought of going with a more basic simpler ‘MvcContrib.Gird’ for this..
You can find out more about how to integrate the ‘MVCContrib’ grid here: http://mvccontrib.codeplex.com/wikipage?title=Grid.
In the process of integrating the grid, I had to remove the Index
method (Action) implementation from my abstract class. That created ambiguity between that and the new method that I wanted in the
CourseController
class. So if I am going with the MVCContrib grid, I have two possible design options:
- Remove the
Index
method implementation from the abstract base controller and have that implemented in the specific controller classes. - Have a new View defined to do the action implementation for it in the specific controller while preserving the
Index
method of the abstract controller class.
I decided to go with the first option as that makes my design more simpler and provides relatively less options to choose . There is a famous theory in software design, ‘it is not about adding but removing all unwanted’. Let's practice that..
This is how the newly modified Course's Index ‘View’ looks like..
The final ‘Course’ Index view looks like this..
Next Recommended Read