In developing any software application, it's important to keep things as simple as possible and add complexity only when needed. (I spoke about this in a previous post, Planning for vs. Reacting to Change.) On the flip-side, a certain amount of architecture may be assumed at the start of a project depending on the type of application being developed. Selecting an appropriate, default architecture provides your application with a solid foundation and offers guidance for other developers, and yourself, for further development. This entry describes a suggested, default architecture for data-driven, ASP.NET applications for both individual and team development. Disclaimer: it is important to note that there are many appropriate architectural foundations and this is just one of them.
In providing an architectural approach, some assumptions are made concerning the project goals. These assumptions should be used as a litmus test to determine if the described architecture may be an appropriate fit for your ASP.NET application.
- Testability is of utmost importance. If test-driven development (TDD) will not be practiced, then most of what follows becomes a moot point.
- Separation of concerns is strongly enforced. This policy is enforced using separate, physical assemblies which communicate with each-other via dependency-inversion with interfaces.
- Presentation and business logic layers are "data-layer agnostic" as much as reasonably possible. Like the previous point, this is more for testability than it is for the expectation that the data-layer will be actually switched out at a later time. The phrase "reasonably possible" is mentioned because there is often an unavoidable amount of implicit, data-layer assumptions built into both the presentation and business logic layers. For example, if the data-layer uses proxies to watch for an "is-dirty" state, as NHibernate does, then the business layer need not concern itself with these details; but switching to a less-autonomous data-layer may force the business layer to take on this responsibility. Certainly, with enough effort, the business layer could become completely data-layer agnostic; but in most cases, the extra effort isn't worth the added cost of premature generalization.
- NHibernate is used as the data-layer ORM. A data-access object (DAO) abstract factory pattern is employed to easily switch between production and "mock" DAOs for unit-testing.
- Model-View-Presenter (MVP) is optionally employed to keep code-behind pages as part of the view, pure and simple. MVP is the simplest solution I have found for making ASP.NET code-behind logic more maintainable and testable. But MVP comes with a cost - it adds another layer of indirection and complexity to the application; therefore, the "Presenters" layer (the "P" in MVP) may be considered as an optional piece to the suggested architecture. Furthermore, the benefits of MVP may be employed later in the project life-cycle, when warranted, without having to modify the existing presentation layer. (Martin Fowler has suggested that MVP be split into Supervising Controller and Passive View. What's described below is consistent with Supervising Controller.)
In a Nutshell
Below is a graphical summary of the architecture.
In the above diagram, each raised box represents a distinct specialization of the application. Each gray box then represents a separate, physical assembly; e.g. MyProject.Web.dll, MyProject.Presenters.dll, MyProject.Core.dll, etc. The arrows represent assembly dependencies. For example, the .Web assembly depends on the .Presenters and .Core assemblies.
The assemblies avoid bi-directional dependency using the techniques "Dependency Inversion" and "Dependency Injection." ("DI" in the diagram should be read as "dependency injection.") To illustrate, note that the .Core assembly contains, along with domain objects, the DAO interfaces. The .Data assembly contains classes which inherit from these interfaces to define the DAO implementations. The .Core assembly is then given its DAO dependencies from another layer, such as from .Presenters or .Tests. Furthermore, another "service layer" could be employed to centralize the DAO-creation services. (Martin Fowler discusses the service-layer approach in Patterns of Enterprise Application Architecture.) One approach that I've had success with is leveraging the Castle Windsor project to inject DAO dependencies. Note that this third party tool adds another layer of complexity to the application and should be carefully considered before use (aka "happy fun ball").
In all seriousness, an entire book could be written to describe the preceding diagram in full detail. What follows are a number of articles which explain the architecture in greater depth. (Warning: utterly shameless plugs for articles I've written!)
Dependency Injection for Loose Coupling: http://www.codeproject.com/cs/design/DependencyInjection.asp. As mentioned previously, the architecture uses dependency injection (DI) throughout the application to keep the application layers loosely coupled. This article offers a simplified overview of the technique - it assumes DI is being performed from one layer to another layer. For a more complete discussion of Dependency Injection, or more cryptically called "Inversion of Control," be sure to read Martin Fowler's article on the subject. Fowler's article also goes into greater depth of using DI "containers."
NHibernate Best Practices with ASP.NET: http://www.codeproject.com/aspnet/NHibernateBestPractices.asp. This audaciously named article gives a good overview of the abstract data-access-object factory used within the architecture and how it's used with test-driven development. This article includes many details of the suggested architecture, sans the inclusion of MVP and Castle Windsor.
Using NHibernate with Multiple Databases: http://www.codeproject.com/useritems/NHibernateMultipleDBs.asp. If your project requires communications with multiple databases, then this article provides an extension to the previous NHibernate article. There is very little, published content concerning this subject and I am open to hearing alternative approaches.
Model-View-Presenter with ASP.NET: http://www.codeproject.com/aspnet/ModelViewPresenter.asp. There are many resources available for learning about MVP; the emphasis here is with ASP.NET and a variant I call "User-Control as View." The Model-View-Presenter pattern may be employed in the architecture to allow code to be unit-tested that would usually have been found in the code-behind pages. The downloadable sample “MVP Enterprise Solution,” found within the article, includes a working example of using the Castle Windsor project within ASP.NET. As noted previously, both MVP and the Castle Windsor project add additional layers of complexity to the application and should only be employed if their benefits of maintainability and testability are warranted in your environment.
There is no single best-practice for architecting ASP.NET, web applications; but, after taking described assumptions into account, what I've described may serve as a solid, default architecture for future project work. At the very least, it provides a pool of ideas for consideration. As always, I'm open to thoughts, rebuttals, criticisms and suggestions and welcome your comments.
10-05-2006 10:56 AM