Introduction
In the first
part of the article I explained how to quickly build an MVC plugin architecture. Since then I had a lot of questions from people about how to proceed
with database calls, or embedded resources management, or other things necessary to know when you plan to design this kind of
an application. In part 2, I will describe mostly a proposition about how to
handle database operations or any kind of business specific logic inside plugin assemblies.
Background
Usually every small, medium, or large application is structured, as a big picture, in
three tiers: Data Access, Business Logic, and User Interface.
Every tier should be decoupled as much as possible for maintainability and unit testing reasons. Most of us use some patterns
and architecture principles like ORM, Repositories,
Business Entities, Business Services, Inversion of control, etc. Mostly these are old and proven principles that every architect knows
even in his dreams. Some of us sometimes
also try to do more like DDD, or maybe CQRS, when time, resources, and complexity allows it.
In the case of this MVC plugin approach I tried to keep the plugins very light in reference to other assemblies, this is why I preferred to keep the business logic
behind web-services and opted for a SOA architecture. This was also a business decision, because it is much more comfortable to deliver fewer assemblies to clients.
Here is where my preferred Dependency Injection framework,
Castle Windsor,
helped a lot with the
Windsor WcfFacility, which basically
allowed to register the service interfaces to the DI container as proxies to the actual web-service; this was extremely helpful because the service references were always
up to date and the code is not aware that the implementation is a service proxy.
For the code samples, I chose to extend the Agenda plugin from part 1
with Get+CRUD functionality, almost as a normal agenda application. All the objects are kept in memory, there is no database to keep the application simple, the repository implementation
does all the persistence in memory. Let's see some code samples.
Different folder structure
Firstly the example from
part 1 was renamed
to Calendar and split in the following layers:
- Infrastructure - Very generic interfaces and application wide infrastructure components.
- DataAccess - Persistence layer, in this case MemoryCache is used, usually database access is done here.
- BusinessLogic - Domain Model and Assemblies for service contracts and implementation.
- Web - web-service hosting, plugins, and web application.
Here is a quick view:
Changes required to the plug-in system
IModule interface and plug-in Module entry
First of all it is required to do a modification to the IModule
interface and a new method Install()
is added.
Every module will have here DI registration and other initialization code.
public interface IModule
{
string Title { get; }
string Name { get; }
Version Version { get; }
string EntryControllerName { get; }
void Install();
}
The AgendaModule
install method implementation looks like this:
public class AgendaModule : IModule
{
....
public void Install()
{
var container = ServiceLocator.Current.GetInstance<IWindsorContainer>();
container.Kernel.AddFacility<WcfFacility>();
container.Register(Component.For<IAgendaService>()
.ActAs(new DefaultClientModel
{
Endpoint = WcfEndpoint.BoundTo(new BasicHttpBinding())
.At("http://localhost:32610/AgendaService.svc")
}));
container.Register(Component.For<AgendaPrefill>());
container.Resolve<AgendaPrefill>().Prefill();
}
}
As you can see in the example above, the IAgendaService
interface is registered as a proxy to the correct WCF end-point.
I would like to insist that this is an implementation detail and a proposition, here you can
also register repositories and other business specific services if you wish,
the approach you can take is highly based on requirements.
Plugins BootStrapper
In the code PluginBootstrapper
is located under the Calendar.Infrastructure.PluginManager
namespace
and it takes care that all plugins are installed correctly on ApplicationStart
. As explained in the first article,
in BeforeApplicationStart
,
all the plugins are stored in a static collection, which PluginBootstrapper
will enumerate and register. Here
is the code:
public static class PluginBootstrapper
{
public static void Initialize()
{
foreach (var module in PluginManager.Current.Modules.Keys)
{
var assembly = PluginManager.Current.Modules[module];
ApplicationPartRegistry.Register(PluginManager.Current.Modules[module]);
module.Install();
var container = Microsoft.Practices.ServiceLocation.ServiceLocator.Current
.GetInstance<Castle.Windsor.IWindsorContainer>();
container.Register(Castle.MicroKernel.Registration.AllTypes
.Of<System.Web.Mvc.IController>()
.FromAssembly(assembly).LifestyleTransient());
}
}
}
As seen above, that is a standard DI registration and a call to the Install()
method for each registered plugin.
The PluginBootstrapper.Initialize()
method is then called on ApplicationStart
and at this point we have the application set up and ready to run.
protected void Application_Start()
{
Bootstrapper.Install();
PluginBootstrapper.Initialize();
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
AgendaPlugin
also contains more code, basically the AgendaController
, Models, and Views required. The Controller, basically the most important,
now has a dependency on IAgendaService
and does mostly dummy web-service calls. The simplified code:
public class AgendaController : Controller
{
IAgendaService _service;
public AgendaController(IAgendaService service)
{
_service = service;
}
[HttpGet]
public ActionResult Index(DateTime? startDate, int dayCount = 1)
{
var agendas = _service.GetAgendaItems(...)
...
}
[HttpDelete]
public ActionResult Delete(Guid id)
{
_service.DeleteAgenda(...)
}
[HttpPost]
public ActionResult Update(UpdateAgendaDto agenda)
{
_service.UpdateAgenda(...)
}
[HttpPut]
public ActionResult Insert(CreateAgendaDto agenda)
{
_service.CreateAgenda(...)
}
}
Basically this is the most of it and what you have read until now should give everybody an understanding about how things work. If you didn't get bored until now, the next
chapters will show how the web-service is structured.
10,000 ft overview: Plugins initialization diagram
Web-service hosting and Business Layer
As I said before this is just one way of implementing it, it is totally up to the development lead. In some cases this might prove a little over-complicated
for some smaller projects.
Service hosting
The service hosting project will contain only the *.svc files. The contracts
and the implementation are defined in the Business Logic layer. The hosting
project is Calendar.WebServices.Hosting and this is the structure:
The AgendaService.svc is configured using Windsor Castle like this:
<%@ ServiceHost Language="C#" Debug="true"
Factory="Castle.Facilities.WcfIntegration.DefaultServiceHostFactory, Castle.Facilities.WcfIntegration"
Service="AgendaService" %>
AgendaService is also configured in the web-service Bootstrapper via WcfFacility
.
Service interface and implementation
The service interface contains all the business operations that are necessary for the agenda:
namespace Calendar.BusinessLogic.Agenda
{
[ServiceContract(Namespace = "http://www.calendarservieces.com/agendaservice/")]
public interface IAgendaService
{
[OperationContract]
GetAgendaResponse GetAgenda(GetAgendaRequest request);
[OperationContract]
GetAgendaItemsResponse GetAgendaItems(GetAgendaItemsRequest request);
[OperationContract]
void CompleteAgendaItem(CompleteAgendaItemRequest request);
[OperationContract]
void UncompleteAgendaItem(UncompleteAgendaItemRequest request);
[OperationContract]
CreateAgendaResponse CreateAgenda(CreateAgendaRequest request);
[OperationContract]
void UpdateAgenda(UpdateAgendaRequest request);
[OperationContract]
void DeleteAgenda(DeleteAgendaRequest request);
}
}
Each operation is then handled by a specific Handler, in the service implementation. Each operation has a handler and it is routed by an IHandlerExecuter
to the required handler. This way the implementation of the service is kept simple, decoupled, and easy to test:
public class AgendaService : IAgendaService
{
private readonly IHandlerExecuter _executer;
public AgendaService(IHandlerExecuter executer)
{
_executer = executer;
}
public GetAgendaItemsResponse GetAgendaItems(GetAgendaItemsRequest request)
{
return _executer.Execute<GetAgendaItemsRequest, GetAgendaItemsResponse>(request);
}
....
public void UpdateAgenda(UpdateAgendaRequest request)
{
_executer.Execute(request);
}
}
A handler is a component that handles a business request. For example, UpdateAgendaRequest
is handled by UpdateAgendaHandler
and makes
the required business changes, it can handle one or more repositories or one or more business service, it all depends on the business operation that is required. This is the code:
namespace Calendar.BusinessLogic.Agenda.Impl.Handlers
{
public class UpdateAgendaHandler: IHandler<UpdateAgendaRequest>
{
private readonly IAgendaRepository _agendaRepository;
public UpdateAgendaHandler(IAgendaRepository agendaRepository)
{
_agendaRepository = agendaRepository;
}
public void Execute(UpdateAgendaRequest request)
{
Domain.Entities.Agenda agenda = _agendaRepository.GetById(request.Id);
agenda.Title = request.Title;
....
_agendaRepository.SaveOrUpdate(agenda);
}
}
}
Domain and Data Layer
The Domain is the core of the system. Here all the business is translated into code. I am not going deeper into this topic because there is a lot of material already
available about
Domain Driven Design. As I said in the previous chapter, a Handler makes business changes, these changes usually go through the Domain, where all business rules are implemented.
The following example shows how the small Calendar domain and Data Layer is structured:
The Domain Entities are persisted through the repository. The repository implementation is done in the Data Access Layer and in this example,
it is a MemoryCache implementation. In a real application the implementation of the repository will use, most probably, a database.
Using the code
The code should be quite easy to use. First make sure you already have Visual Studio 2012 and install the RazorSingleFileGenerator Visual Studio 2012 extension.
You can find it in the archive under $/ThirdPartyLibraries/MVC/RazorSingleFileGenerator.vsix. The next step is to open the solution and rebuild it, the solution
is under $/main/VisualStudioSolutions. All references should be already resolved, if not, third party references should be under
$/ThirdPartyLibraries/.
All the plugin assemblies are set to compile directly to $/WebHosting/Web/Calendar.Web/plugins, so if you want to test pluginable functionality, you should set all the plugin
assemblies to compile to a different folder and then remove some assemblies from the plug-ins folder.
Useful links