Reputation: 4675
I am writing a silly program to try to fully understand all the various concepts involved in design patterns in a practical way. For example, I fully understand DI / IOC, (I think), but I don't fully understand how to apply it in a practical ASP.Net MVC 4/5 environment.
I am writing a store program with invoices and products as my only 2 tables. So far I have been successful at fully applying DI/IOC and have finished up with the following structure:
Store.Models <== Entity Framework classes. (Data access layer).
Store.Interfaces <== Interfaces.
Store.Repositories <== Contains the code that actually goes and gets or sets the data.
Store.Web <== My MVC application.
All the dependencies are setup and working fine. Now here is my question and problem.
I want to add a business layer as follows:
Store.Business
For the purposes of the exercise I have decided to simply calculate the number of years since a given date. Of course, under normal circumstances I would store this in the database as a calculated field and retrieve it. But I am doing it for the academic exercise because at some point, I will come across a situation where I will have to actually do some complex calculations on a dataset. I am of the opinion that this should not really be stored with the model, repository, or done in the controller. That there should be a separate "business" layer. Now here is my problem:
Entity framework defined a class called Invoice based on my model. Its a fine class, it worked until now.
I defined an interface, and repository, setup Ninject, got it all working with MVC. Everything is perfect. Couldn't be happier.
I then added a date field to the invoice table. Updated my model in EF, updated the other stuff I needed to update and everything went well.
Next I added a Store.Business class project. I setup a new Invoice class which inherited the Invoice class from the model and added a new property, constructor and method.
namespace Store.Business
{
//NOTE: Because of limitations in EF you cant declare a subclass of the same name.
public class InvoiceBL : Store.Models.Invoice
{
[NotMapped]
public int Age { get; set; }
public InvoiceBL()
{
Age = CalcAge(Date);
}
private int CalcAge(DateTime? Date)
{
Age = 25;
//TODO: Come back and enter proper logic to work out age
return Age;
}
}
}
I then modified my interfaces, repositories, controllers, views etc to use this new InvoiceBL class instead of the one generated by EF.
I started using a partial class. But I have trouble with this apparently because it is in a different project. I even tried using the same namespace but nope. It is vital to me that I keep it separated by project. I want the layers to be clearly defined. So as this did not work, I opted for inheritance. I prefer this approach anyway because I am of the assumption that partial classes are a Microsoft thing and I want my philosophy to be easily transferable to any OOP language that may not have partial classes. Note also I put it back in its own namespace so it is no longer in the namespace Store.Models, but in Store.Business.
Now when I run the program, and type invoices in the url as before I get the following errors:
Invalid column name 'Discriminator'.
Invalid column name 'Age'.
When I add the [NotMapped] attribute I only get this error:
Invalid column name 'Discriminator'.
Below is all the related code starting with the EF Auto Generated model:
Store.Models:
namespace Store.Models
{
using System;
using System.Collections.Generic;
public partial class Invoice
{
public Invoice()
{
this.Products = new HashSet<Product>();
}
public int Id { get; set; }
public string Details { get; set; }
public Nullable<decimal> Total { get; set; }
public Nullable<System.DateTime> Date { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
}
Next we have interface:
namespace Store.Interfaces
{
public interface IInvoice
{
void CreateInvoice(InvoiceBL invoice);
DbSet<InvoiceBL> Invoices { get; }
void UpdateInvoice(InvoiceBL invoice);
InvoiceBL DeleteInvoice(int invoiceId);
}
}
Next we have the repository:
namespace Store.Repositories
{
public class InvoiceRepository : BaseRepository, IInvoice
{
public void CreateInvoice(InvoiceBL invoice)
{
ctx.Invoices.Add(invoice);
ctx.SaveChanges();
}
public DbSet<InvoiceBL> Invoices
{
get { return ctx.Invoices; }
}
public void UpdateInvoice(InvoiceBL invoice)
{
ctx.Entry(invoice).State = EntityState.Modified;
ctx.SaveChanges();
}
public InvoiceBL DeleteInvoice(int invoiceId)
{
InvoiceBL invoice = ctx.Invoices.Find(invoiceId);
if (invoice != null)
{
ctx.Invoices.Remove(invoice);
ctx.SaveChanges();
}
return invoice;
}
}
}
I've shown you the business layer which both Interfaces, and Repositories layers need. So I will move on to the controller:
namespace Store.Web.Controllers
{
public class InvoiceController : Controller
{
//---------------------Initialize---------------------------
private IInvoice _invoiceRepository;
private IProduct _productRepository;
public InvoiceController(IInvoice invoiceRepository, IProduct productRepository)
{
_invoiceRepository = invoiceRepository;
_productRepository = productRepository;
}
//-----------------------Create-----------------------------
public ActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Store.Business.InvoiceBL invoice)
{
if (ModelState.IsValid)
{
_invoiceRepository.CreateInvoice(invoice);
return RedirectToAction("Index");
}
return View(invoice);
}
//-------------------------Read-----------------------------
[ActionName("Index")]
public ActionResult List()
{
return View(_invoiceRepository.Invoices);
}
public ViewResult Details(int id)
{
//How is this DI - If your model changes you have to alter the fields
//addressed here.
return View(_invoiceRepository.Invoices.FirstOrDefault(i => i.Id == id));
}
//-----------------------Update-----------------------------
[ActionName("Edit")]
public ActionResult Update(int id)
{
//How is this DI - If your model changes you have to alter the fields
//addressed here.
var invoice = _invoiceRepository.Invoices.FirstOrDefault(i => i.Id == id);
if (invoice == null) return HttpNotFound();
return View(invoice);
}
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult Update(Store.Business.InvoiceBL invoice)
{
if (ModelState.IsValid)
{
_invoiceRepository.UpdateInvoice(invoice);
return RedirectToAction("Index");
}
else
{
return View(invoice);
}
}
//-----------------------Delete-----------------------------
public ActionResult Delete(int id = 0)
{
//Do you really want to always delete only the first one found?? Not cool?
//Even though in this case, because Id is unique, it will always get the right one.
//But what if you wanted to delete or update based on name which may not be unique.
//The other method (Find(invoice) would be better. See products for more.
//How is this DI - If your model changes you have to alter the fields
//addressed here.
var invoice = _invoiceRepository.Invoices.FirstOrDefault(i => i.Id == id);
if (invoice == null) return HttpNotFound();
return View(invoice);
}
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
if(_invoiceRepository.DeleteInvoice(id)!=null)
{
//Some code
}
return RedirectToAction("Index");
}
//-----------------------Master / Detail--------------------
}
}
Finally the view:
@model IEnumerable<Store.Business.InvoiceBL>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
@Html.DisplayNameFor(model => model.Age)
</th>
<th>
@Html.DisplayNameFor(model => model.Details)
</th>
<th>
@Html.DisplayNameFor(model => model.Total)
</th>
<th>
@Html.DisplayNameFor(model => model.Date)
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Age)
</td>
<td>
@Html.DisplayFor(modelItem => item.Details)
</td>
<td>
@Html.DisplayFor(modelItem => item.Total)
</td>
<td>
@Html.DisplayFor(modelItem => item.Date)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
@Html.ActionLink("Details", "Details", new { id=item.Id }) |
@Html.ActionLink("Delete", "Delete", new { id=item.Id })
</td>
</tr>
}
</table>
Please ignore any comments in the code as they are for my own reference and guidance.
Once again, this question is about specifically why I am getting the error mentioned and what I need to change to resolve it. I taught adding the [NotMapped] attribute would do this but it did not.
However, I am still learning about design patterns related to MVC, so if anyone has suggestions on how to structure the project better or other advice that might help, I would also welcome this.
EDIT: I forgot the NinjectControllerFactory:
namespace Store.Web.Ninject
{
public class NinjectControllerFactory : DefaultControllerFactory
{
private IKernel ninjectKernel;
public NinjectControllerFactory()
{
ninjectKernel = new StandardKernel();
AddBinding();
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
//return base.GetControllerInstance(requestContext, controllerType);
return controllerType == null
? null
: (IController)ninjectKernel.Get(controllerType);
}
private void AddBinding()
{
//TODO FR: Step 4 - Add your interface and repository to the bindings
ninjectKernel.Bind<IProduct>().To<ProductRepository>(); ;
ninjectKernel.Bind<IInvoice>().To<InvoiceRepository>(); ;
}
}
}
Upvotes: 1
Views: 744
Reputation: 2063
You didn't mention whether you had EF auto-regenerate your Invoice
entity after you added the column. Assuming you are using code-first and aren't generating your entities via a T4 template (.TT file), you maintain your entities yourself. The generation was a one-time thing to help you get started, so you don't have to write all the entities from scratch.
In that case, you could add the Age
field right to your Invoice
entity, and have your business service either take an Invoice
entity instance in a CalcAge
function, or just pass a DateTime to that function and get an age back. Normally you'd want to use a view model rather than use an EF entity for that purpose, and you'd probably store the birth date on the DB and have the Age field calculated, either on the DB or in the entity logic in the property getter (it would be [NotMapped] like you already have).
You wouldn't want to couple a class in the business layer to an actual EF entity, but rather perform operations on entities, either on newly-created ones or ones retrieved from the DB via the repository layer, pretty much as you have now.
Since you want to use the business layer, you could do something like this:
namespace Store.Models
{
using System;
using System.Collections.Generic;
public partial class Invoice
{
public Invoice()
{
this.Products = new HashSet<Product>();
}
public int Id { get; set; }
public string Details { get; set; }
public Nullable<decimal> Total { get; set; }
[NotMapped]
public int Age {get; set;
// ...
The business service:
using Store.Models;
namespace Store.Business
{
public class InvoiceBL
{
public int CalcAge(DateTime? date)
{
Age = 25;
//TODO: Come back and enter proper logic to work out age
// Something like:
// return date != null ? <DateTime.Now.Year - date.Year etc.> : null
return Age;
}
In the controller, you'd have to calculate the age field and set it on the Invoice
as your data model each time you return a View.
It's not optimal but it does use the business layer.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Store.Model invoice)
{
if (ModelState.IsValid)
{
_invoiceRepository.CreateInvoice(invoice);
// _service is your business service, injected as a dependency via the constructor, same as the _invoiceRepository is now
invoice.Age = __service.CalcAge(invoice.BirthDate); // or some such thing
return RedirectToAction("Index");
}
return View(invoice);
}
You'd also have to do this for the Update action, etc.; any action that returns an Invoice
as a view model.
The view's model will bind to the Invoice entity:
@model IEnumerable<Store.Models.Invoice>
@{
ViewBag.Title = "Index";
}
// ... and so on
Your Ninject container would bind the service and it would be a dependency for the controller. I would personally have the repository as a dependency injected into the service, and the service injected into the controller, rather than have the service and the repository separated inside the controller, but I'm going with what you have.
namespace Store.Web.Ninject
{
public class NinjectControllerFactory : DefaultControllerFactory
{
private IKernel ninjectKernel;
public NinjectControllerFactory()
{
ninjectKernel = new StandardKernel();
AddBinding();
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
//return base.GetControllerInstance(requestContext, controllerType);
return controllerType == null
? null
: (IController)ninjectKernel.Get(controllerType);
}
private void AddBinding()
{
//TODO FR: Step 4 - Add your interface and repository to the bindings
ninjectKernel.Bind<IProduct>().To<ProductRepository>();
ninjectKernel.Bind<IInvoice>().To<InvoiceRepository>();
// Add this, assuming there isn't an interface for your service
ninjectKernel.Bind<InvoiceBL>().ToSelf();
}
}
}
I don't see any code about the Discriminator
column, but if it's in the entity and it's mapped, it needs to be in the DB table. The mapping is either via a class used in the context (or done in the context directly), or set using DataAnnotation attributes, like the [NotMapped] is.
Upvotes: 1