Download the code for this article.
I recently started a consulting project as an architect on an
ASP.NET MVC application and quickly found myself immersed in the world of
N* open source tools.
MVC(which stands for Model-View-Controller) lends itself to an
Agile development methodology where
TDD and
BDD (Test-Driven and Behavior-Driven Development) are important components. Writing applications that are testable requires that you separate business logic from presentation logic so that they can be independently tested. This is a concept known as
separation of concerns (
SoC), which, in addition to testability, provides other benefits, such as greater application longevity and maintainability. The life of an application is extended because loose coupling makes it easer to upgrade or replace components without affecting other parts of the system.
This is where the “Onion Architecture” comes in. The term was first coined by Jeffery Palermo back in 2008 in a series of
blog posts. It is intended to provide some insurance against the evolution of technology that can make products obsolete not long after they are developed (the technical term is “deprecated”). A classic example is Microsoft’s data access stack, which tends to change every few years (remember the
demise of LINQ to SQL?). What Jeffery proposed (although he’s not the first) is for the application to reference interfaces so that the concrete implementation can be supplied at runtime. If, for example, the data access layer is represented by a number of
repository interfaces, you can swap out LINQ to SQL with
Entity Framework or
NHibernate (or your favorite ORM) without breaking other parts of the application. This same approach is used to decouple things like configuration and logging so they become replaceable components.
In this depiction, the “core” of the onion is the object model, which represents your domain. This layer would contain your
POCO entities. Surrounding your domain entities are repository interfaces, which are in turn surrounded by service interfaces. Representing your repositories and services as interfaces decouples consumers from concrete implementations, enabling you to swap one out for another without affecting consumers, such as client UI’s or tests. The data access layer is represented in the outer layer as a set of repository classes which implement the repository interfaces. Similarly the logging component implements a logging interface in the service interfaces layer.
Here is the project structure for a Visual Studio solution I created to demonstrate the Onion Architecture. I inserted solution folders and aligned project and folder names for ease of use. Infrastructure.Data uses
NHibernate to implement repositories for ICategoryRepository and IProductRepository in Domain.Interfaces. Infrastructure.Logging uses
NLog to implement ILogging in Infrastructure.Interfaces. The Web.Ui project has a ProductService class that implements IProductService in Services.Interfaces. (In a future post I will incorporate WCF into the project structure, but the Service implementation would go in a Service.Core project, with a Web.Services project for the service host.)
You may be asking, “How are concrete implementations of repositories and services created?” If components in the outer layer were to create instances directly, they would be tightly coupled to those implementations, defeating the whole purpose of the Onion Architecture and jeopardizing the application’s long-term viability. The answer is
Dependency Injection (also known as Inversion of Control, or IoC). Components on the
outer rim of the diagram have constructors that accept service or repository interfaces, and it’s the job of the DI framework to serve up a concrete instance, based on some initial configuration or setup. For example, the ProductController class in the ASP.NET MVC application has a constructor that accepts an IProductService, which has methods to get categories and products. The controller doesn’t care about how the interface is implemented and what API the data access component uses.
public class ProductController : Controller
{
// Services will be injected
private IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
//
// GET: /Product/
public ActionResult Index()
{
// Get products
var products = _productService.GetProducts(selectedCategoryId);
// Rest of method follows ...
}
}
There are a number of
DI / IoC containers out there. One of my favorites is
Ninject, which can be added using the
NuGet package manager and has an extension for ASP.NET MVC applications. Installing the
Ninject.MVC3 package places a bootstrapper class in the App_Start folder. There is a private RegisterServices method where you can bind local service implementations and load Ninject modules.
private static void RegisterServices(IKernel kernel)
{
// Bind local services
kernel.Bind<IProductService>().To<ProductService>();
// Add data and infrastructure modules
var modules = new List<INinjectModule>
{
new RepositoryModule()
};
kernel.Load(modules);
}
The RepositoryModule class resides in a separate DependencyResolution assembly, which references the Infrastructure.Data assembly and binds IProductRepository to ProductResository. The assembly containing the Ninject modules references the Data assembly, so that web client doesn’t have to, keeping the web client ignorant of the actual repository implementation. Once the bindings are set up, Ninject serves up the appropriate instance wherever the interface is used.
public class RepositoryModule : NinjectModule
{
public override void Load()
{
// Bind and get config service
Bind<IConfigService>().To<ConfigService>();
var configService = Kernel.Get<IConfigService>();
// Bind repositories
Bind<IProductRepository>().To<ProductRepository>() .WithConstructorArgument("connectionString",configService.NorthwindConnection);
}
}
As you can see, dependency injection is the glue that holds everything together. An integration test, for example, would also use the DI container to get instances of interface implementations, without having to reference assemblies containing those classes.
[TestFixture]
public class RepositoryTests
{
// Ninject kernel
private IKernel _ninjectKernel;
public RepositoryTests()
{
// Init Ninject kernel
_ninjectKernel = new StandardKernel(new RepositoryModule(), new LoggingModule());
}
[Test]
public void Should_Get_All_Categories()
{
// Arrange
var categoriesRep = _ninjectKernel.Get<ICategoryRepository>();
// Act
var categories = categoriesRep.GetCategories();
// Assert
Assert.That(categories != null);
Assert.That(categories.Count() > 0);
}
}
Unit tests would likely combine DI with a mocking tool, such as
Moq, as shown here.
[TestFixture]
public class RepositoryTests
{
// Ninject kernel
private IKernel _ninjectKernel;
public RepositoryTests()
{
// Init Ninject kernel
_ninjectKernel = new StandardKernel();
}
[TestFixtureSetUp]
public void FixtureSetup()
{
// Init categories
var categories = new List<Category>
{
new Category { CategoryId = 1, CategoryName = "Beverages" },
new Category { CategoryId = 2, CategoryName = "Condiments" },
new Category { CategoryId = 1, CategoryName = "Confections" }
};
// Set up mock categories repository
var mockCategoriesRep = new Mock<ICategoryRepository>(); mockCategoriesRep.Setup(m => m.GetCategories()).Returns(categories); _ninjectKernel.Bind<ICategoryRepository>().ToConstant(mockCategories.Object);
}
[Test]
public void Should_Get_All_Categories()
{
// Arrange var categoriesRep = _ninjectKernel.Get<ICategoryRepository>();
// Act var categories = categoriesRep.GetCategories();
// Assert
Assert.That(categories != null);
Assert.That(categories.Count() == 3);
}
}
Jeffrey summarizes the key tenets of the Onion Architecture as follows:
-
The application is built around an independent object model.
-
Inner layers define interfaces; outer layers implement interfaces.
-
Direction of coupling is toward the center.
-
All application core code can be compiled and run separate from infrastructure.
The sample application (which you can download
here) provides a reference architecture based on these principles. To use it you’ll need to download our good old friend, the
Northwind sample database. While the Onion Architecture does not propose anything new in terms of object-oriented and domain-driven design patterns, it provides guidance on how to decouple infrastructure from business and presentation logic, without introducing too much complexity or requiring redundant code. Enjoy.