Introduction
Dependency Injection is a nice design pattern that makes your code a bit more nimble and allows for better separation between your tiers. This article goes into detail on how we used Dependency Injection when building Recarded.com, and how programming towards an interface, instead of a concrete class, not only makes Test Driven Development (TDD) easier, but also allowed us to be more flexible when we had to change our implementation due to forces outside of our control.
Background
Back in 2006, a work colleague and myself started a side venture together. We set out to create a gift card marketplace for users to sell their unwanted gift cards and to buy gift cards at a discount. This isn't your typical business venture because aside from trying to create a profitable business, we also set out with the following goals:
- Do everything ourselves in our free time.
- Do things 'Right'. Between Jess and myself, we have almost 30 years of web development / architecture experience, and we wanted this site to be our pet project for using the latest technologies and sticking to solid design patterns and best practices.
- Give back to the development community. We love technical blogs and communities such as CodeProject that save you hours of work and learning when you can Google an issue or an idea and you stumble upon another developer that has gone through the headache for you. By posting some interesting learning or experiences we came across while getting Recarded.com off the ground, it allows us to give back and to better justify the countless hours we dumped into this site (even if it never turns a profit).
Project Layout
Below is an overview of our project structure, but don't feel this is exactly how you should setup your ASP.MVC projects. Every developer tends to have their own flavor of solution structure, balancing flexibility with complexity. Our structure has evolved over the past two years quite a bit, and there are still some improvements we would like to make (e.g., create a project just for interfaces and data models).
Recarded.Tests
We list this one first because it is the most important. My partner Jess is crazy Pro-TDD, and although prior to this project my TDD was really more like developing and creating Unit Tests at the same time, I wanted to do true TDD, and followed his lead here, and promised to always write the tests first. People can argue and persuade you all they want about the benefits of TDD, and prior to this project, I probably have read and heard them all, but to be honest, when I am in the real world, I would start down the TDD path and get so time crunched by the client, I would slip back to the old way of doing things. I did keep my promise for this project, however, and I have to say, I will never go back to my old ways. I won't bother selling you on the benefits, for if you are like me, it won't sell you until you try it yourself.
Some could argue a structure where you have a test project for each project; for example, we could have Recarded.Services.Tests and Recarded.Data.Tests instead of having all our tests in one project. We will probably evolve to that soon, but this is the current state of our application.
Recarded.Web
Recarded.Web is our ASP.MVC application, the entry point into the site. /Content contains our images, JavaScript, and CSS used within the site. /App contains the meat of the application, such as the Controllers, Views, and ViewModels. The natural tendency is to have the Domain Models be passed into the Views, but we soon started to need specific properties that were really view specific and not part of the domain model. Having the Views take in ViewModels instead of Domain Models makes it easier to adjust the presentation layer without having to bubble up through to the Data tier for something that really is presentation specific.
Recarded.Services
Here is our broker layer. The Recarded.Web layer will not call the Data Access Layer directly, it will instead use our Service Layer to get what it needs. This layer of abstraction is usually used to get the raw data from the DAL and further process it, for example, apply filters based on security, paging, etc.
Recarded.Pipelines
We brought the Pipelines project into the solution to handle activities that are better suited for a workflow engine. If you are familiar with Windows Workflow Foundation, that is the type of functionality you would find in this project. We were actually leveraging Windows Workflow Foundation for our order processing until we ran into the issue described in more detail in the 'Why Dependency Injection' section.
Recarded.Data
This is our Data Access Layer. Here, you will find our Data Access, Pipes and Filters functionality, our Domain Models, and some utility classes. At some point, we might actually bring our Domain Models into a separate project, but for now, they are located here. We use a service / repository type pattern, and are leveraging LINQ to SQL for our Data Access. If there is interest, I can go into more detail around this, but for now, you can just understand we have an Interface for each Repository (e.g., ICatalogRepository
) and than a concrete implementation of that Interface (e.g., SqlCatalogRepository
).
Why Dependency Injection
So, the big driver of why I wanted to write this article is because as a developer, sometimes, you just code or architect something in a particular manner because that is what your co-workers were doing, or that is the way a respectable blogger implemented it, etc. I think I was this way around Dependency Injection and programming towards an interface. The first time I used this pattern was with a past client of mine when I architected snow.com for Vail Resorts. The site uses Sitecore as their CMS, and Vail Resorts actually gave me a requirement that they wanted to make it so they are not dependent on Sitecore for their CMS. So, if they want to change to some other CMS (Ektron, home grown, etc.) in the future, they can easily do this without a whole bunch of rewrite. Now, the odds of them swapping out their CMS anytime soon is probably very low, but it was a requirement, and I ended up architecting a solution that uses Dependency Injection to resolve which repository to use, and the Presentation Layer (ASP.NET) doesn't even have a reference to sitecore.dll.
A lot of people will say coding towards an interface and using Dependency Injection is great because it allows for easier TDD, and you can swap out classes at will as long as they implement the same interface. Well, I was already sold because of the TDD aspect, but to be honest, I really never saw us having to swap out classes being a big selling point. If you take Vail Resorts for example, if they never change their CMS until their next big redesign, that additional work we did to make the application CMS agnostic is essentially overhead and a waste of resources.
Well, I became a full fledged believer with Recarded.com because we actually had to change out two implementations; two implementations I thought would probably be part of our code for quite sometime. Because we were using Dependency Injection and programming towards an interface, however, we could just build out the implementation of the interface and change Bootstrapper.cs, and we were good to go. The two instances I am referring to is we initially coded our eCommerce site to use PayPal as our Merchant and Gateway for payment processing, and we were leveraging Microsoft's Windows Workflow Foundation to process orders (submit, cancel, refund, charge, etc.). PayPal's service and API was not quite up for what our requirements dictated, so we actually had to abandon them as our Payment Service provider. We ended up going with Authorize.Net, and with Dependency Injection, we just needed to create an AuthorizeNetPaymentService
that implemented our IPaymentService
interface and then tell our Bootstraper
to use Authorize.Net instead of PayPal.
The other implementation swap was we were using Windows Workflow Foundation for our order processing. Workflow Foundation is pretty sweet, and we had a pretty great implementation for our order processing if I do say so myself, but when we changed from the standard RackSpace hosting to Rackspace Cloud, we were met with trust issues (more info: our blog). With Dependency Injection, we just had to code a new pipeline that implemented our IPipelineEngine
, swap out our WindowsWorkflowPipeline
class, and then update the Bootstrapper.cs file.
The Implementation
The same pattern is basically used throughout the entire application, so we should be able to just walk through hitting one page of the site and bubbling up through the tiers and see what is going on, and you should get a pretty good feel for how Dependency Injection can be used nicely within an ASP.MVC application (or anywhere for that matter). If you are to hit http://www.recarded.com/GiftCards, our routing table will take you to the CatalogController
Index
method.
CatalogController.cs
ICatalogService _catalogService;
IPersonalizationService _personalizationService;
IOrderService _orderService;
public CatalogController(ICatalogService catalogService,
IPersonalizationService personalizationService,
IOrderService orderService) {
_catalogService = catalogService;
_personalizationService = personalizationService;
_orderService = orderService;
}
Here is the constructor of our CatalogController
. Dependency Injection essentially is the act of the application handling what concrete services should be passed into this CatalogController
constructor method - for this class is dependent on these parameters or it can't be instantiated - hence the term Dependency Injection.
Global.asax.cs
protected void Application_Start() {
RegisterRoutes(RouteTable.Routes);
Bootstrapper.ConfigureUnityContainer();
ControllerBuilder.Current.SetControllerFactory(
new Recarded.MVC.Web.Controllers.UnityControllerFactory()
);
ViewEngines.Engines.Add(new Recarded.MVC.Web.CommerceViewEngine());
GetLogger().Info("App is starting");
}
When our web application starts up, this block of code will get fired. The Bootstrapper.ConfigureUnityContainer()
method specifies which concrete classes we should use for when a dependency is resolved.
Bootstrapper.cs
public static void ConfigureUnityContainer()
{
DIFactory.Instance.RegisterInstance(new DB());
DIFactory.Instance.RegisterInstance<IEconomyRepository>(
new SqlEconomyRepository(DIFactory.Resolve<DB>()));
DIFactory.Instance.RegisterInstance<IEconomyEngine>(
new EconomyEngine(DIFactory.Resolve<IEconomyRepository>()));
DIFactory.Instance.RegisterInstance<ICatalogRepository>(
new SqlCatalogRepository(DIFactory.Resolve<DB>(),
DIFactory.Resolve<IEconomyEngine>()));
...
...
DIFactory.Instance.RegisterInstance<IPaymentService>(new AuthorizeNetPaymentService());
DIFactory.Instance.RegisterInstance<IEconomyService>(
new EconomyService(DIFactory.Resolve<IEconomyEngine>()));
DIFactory.Instance.RegisterInstance<ILogger>(new EntLibLogLogger());
...
DIFactory.Instance.RegisterInstance<IOrderService>(new OrderService(
DIFactory.Resolve<IOrderRepository>(),
DIFactory.Resolve<ICatalogRepository>(),
DIFactory.Resolve<IShippingRepository>(),
DIFactory.Resolve<IShippingService>()));
DIFactory.Instance.RegisterInstance<ICatalogService>(new CatalogService(
DIFactory.Resolve<ICatalogRepository>(),
DIFactory.Resolve<IOrderService>(),
DIFactory.Resolve<IEconomyService>()));
...
Here, we are registering instances for each type of interface within our application. So, the last piece of code in the above snippet is telling Unity, if the application is looking to create a class that implements the ICatalogService
, use the CatalogService
, and resolve the three parameters that the CatalogService
is dependant upon. You can also see how in the beginning we were using a FakePaymentService
for testing; we then got the PayPalPaymentService
in place, and it was working great using their Dev Sandbox. We then moved to AuthorizeNetPaymentService
because they fit our model better. All of these Payment Services implemented the IPaymentService
so they could easily be swapped out with each other just by commenting and uncommenting out a line of code.
Unity (and most other Dependency Injection frameworks) is also fully configurable via the web.config, but within our solution, we are doing it within code. There are pros and cons to both; doing it within code is a bit faster, more easily testable, and less breakable by someone fudging with the web.config. This locks the application down more tightly, and you can test this requirement since it is in code and it is kind of difficult to test the web.config. You will not be able to compile if you are trying to register a concrete class that doesn't exist or does not implement the appropriate interface.
Of course, all of the Dependency Injection doesn't have to be within a constructor; you can use DI within a method as well, for example:
ISomeService svc = DIFactory.Resolve<ISomeService>();
svc.DoSomething();
DIFactory.cs
namespace Recarded.Common{
public static class DIFactory
{
private static object _Lock = new object();
private static IUnityContainer _UnityContainer = null;
public static IUnityContainer Instance
{
get
{
if (_UnityContainer == null)
{
lock (_Lock)
{
if (_UnityContainer == null)
{
_UnityContainer = new UnityContainer();
}
UnityConfigurationSection section =
ConfigurationManager.GetSection("unity")
as UnityConfigurationSection;
if (section != null)
{
section.Containers.Default.Configure(_UnityContainer);
}
}
}
return _UnityContainer;
}
}
Here, we are just using a Singleton pattern to ensure we are only using one Unity instance. If the Unity Container doesn't exist yet, then we will create it and configure it based on the config file.
Points of Interest
We are planning on turning this into a series to discuss various patterns, headaches, and successes we ran across while getting our site off the ground. Some future article themes under consideration are given below. Feel free to leave a comment on what you would like our next articles to center around.
- jQuery Validation plug-in
- Securing ASP.MVC pages and using the
[ValidateAntiForgeryToken]
attribute
- Google Analytics and eCommerce
- TDD and Mocking
- Service, Repository
- LINQ to SQL and Pipes/Filters
- Your suggestions
History
Will be updated as necessarily per community comments / feedback.