Updated 2010.03.09 to reflect small modifications that were decided through subsequent discussions on S#arp forum and other DDD posts.
Obviously, S#arp Architecture is the bee's knees when it comes to developing ASP.NET MVC applications. ;) But as a project evolves and gets larger, "out of the box" S#arp Architecture 1.0 guidance runs into a few pain points. Particularly, there's poor use of the application services layer, the separation between controllers and application services is not very clear, entity listing pages become performance bottlenecks as the domain model gets sizable, there is no command/query separation (CQS) "out of the box", and unit tests require re-occurring maintenance to deal with changes in the number of constructor parameters to controllers and application services. While the amicable and adroit Alec Whittington (who is taking over the lead role from me on S#arp Architecture) is hard at work upgrading S#arp Architecture to accommodate recent dependency upgrades and accommodating ASP.NET MVC 2, I wanted to take a stab at resolving some of the architectural issues that I've run into, on S#arp projects over the past year.
I've developed and included a sample project, built on S#arp Architecture 1.0 Q3 2009, for the following key reasons:
- To resolve some "pain points" that develop as S#arp projects grow to large sizes,
- To demonstrate better use of the application services layer,
- To demonstrate better command/query separation of the entity listing pages for dramatically
better performance as the domain model grows,
- To create an architectural spike for a new project I'm working on, and
- To collect feedback from the S#arp community to determine if this is an
appropriate architectural direction for the next release of S#arp Architecture.
Please post any feedback and/or suggestions you may have in the comments below or, more preferably, to the S#arp Architecture forums at
http://groups.google.com/group/sharp-architecture.
Setup instructions
- Unzip the sample project to a BetterAppServices folder
- Create a new database called BetterAppServices
- Using SQL Enterprise Manager, run:
- /BetterAppServices/db/Schema/CreateBetterAppServicesDb_ChangesWillBeLost.sql
- /BetterAppServices/db/StoredProcedures/CreateGetProductCategorySummaries.sql
- /BetterAppServices/db/StoredProcedures/CreateGetProductSummaries.sql
- Open /BetterAppServices/BetterAppServices.sln with VS 2008
- In VS 2008, open BetterAppServices.Web/NHibernate.config and change the connection string to point to your BetterAppServices database
- Right click the BetterAppServices.Web project and "Set as StartUp Project"
- Run (F5) the project to see everything in action.
Changes from "out of the box" S#arp Architecture 1.0 Projects
This project is more of an architectural spike more than anything else at the moment.
Accordingly, the CRUD scaffolding generator has been removed and non-essential unit tests
have been removed to focus on the architecture itself. Many of the changes will be
incorporated into the S#arp CRUD scaffolding generator; this will either be available in the next
release, or will be provided as an add-on, as this new approach does add complexity and
introduces a major breaking change to existing 1.0 projects.
Major changes include:
-
/db Folder Changes
- Added /db/Schema/CreateBetterAppServicesDb_ChangesWillBeLost.sql.
This gets auto-regenerated when unit tests are run. The motivation was to have the DB
schema automatically maintained while developing.
- Added /db/StoredProcedures/CreateGetEntityNamePluralSummaries.sql. These SPs provide
command/query separation for the entity listing (Index.aspx) pages, which frequently became a
performance bottleneck.
-
Cross-Project Changes
- Moved /BetterAppServices.Core/DataInterfaces/* to /BetterAppServices.ApplicationServices/DataInterfaces/.
This is the only major, possible breaking change for existing applications. This was done to further
remove the potential of domain objects using data repositories directly, and to allow the repositories
to return objects from a new Dtos project for command/query separation, among other benefits (dicussed below). Decided not to do this to support domain services which may require the use of repositories and to make upgrading from previous version much simpler in some cases. Having the interfaces in .Core doesn't necessitate that domain objects use them; in fact, my rule of thumb is for all domain objects to avoid the use of repositories unless an exceptive case exists.
- Replaced all uses of "using BetterAppServices.Core.DataInterfaces;" to
"using BetterAppServices.ApplicationServices.DataInterfaces;" to support the above mentioned change.
- Replaced all uses of IRepositoryEntityName with IEntityNameRepository. Be default, all
entities now have an explicit repository. This avoids the need to manually change from the generic to
the explicit, when the need arose, in unit tests and in constructors; which was frequently occurring.
-
/BetterAppServices.Core/QueryDtos (/BetterAppServices.Dtos)
- Added a Dtos class library to provide an appropriate location for View Models and DTOs. While this project
currently has a dependency on BetterAppServices.Core, so that the View Models can contain references to
domain objects, this dependency could be severed for better separation between the view and the domain.
The caveat is that much more work would be required maintaining a more complete DTO layer and transferring
data via DTOs in all of the CRUD pages. Decided that a separate assembly was overkill. Consequently, added this namespace to contain query DTOs for transferring results of "report" queries into objects. With this example project, only the listing page uses "pure" DTOs
for much better performance. The other CRUD pages still communicate directly with domain objects to keep
things much simpler. This is something that can be argued either way and it should depend on the
project needs to determine if a more separated approach is warranted.
- Added EntityNameDto.cs to act as a summary object to be bound to results from stored procedures (e.g., the entity listing pages), or other DTO needs.
- /BetterAppServices.ApplicationServices/ViewModels
- Added this namespace and EntityNameFormViewModel.cs to hold data related to adding and updating the EntityName.
This object gets populated and passed to the entity form pages, accordingly.
-
/BetterAppServices.ApplicationServices
- Added reference to BetterAppServices.Dtos so that app services can return DTOs.
- Added EntityNameManagementService.cs. This app service class removes the CRUD logic
from the controllers, makes the logic more reusable, and creates an appropriate class to add
additional application service logic to.
- Added IEntityNameManagementService.cs. This app service interface makes unit testing more
maintainable because you can mock the service interface and not worry about when the concrete class'
constructor arguments change.
- Added /DataInterfaces/IEntityNameRepository.cs. As described above, every entity now has
an explicit repository interface to avoid changes down the road when custom interfaces would inevitably
be introduced.
-
/BetterAppServices.Data
- Added /BetterAppServices.Data/EntityNameRepository.cs to implement the associated interface.
An important thing to note is that method provided invokes a stored procedure to act as a reporting means in
line with command/query separation. This is useful for entity listing pages which would frequently, previously,
end up loading a huge portion of the domain model to show summary information.
- Added /BetterAppServices.Data/NamedQuery/GetEntityNameSummaries.hbm.xml and set its compile action to
"Embedded Resource." This provides the "short cut" for invoking the stored procedure. It could easily be modified
to accept paging parameters.
- Added reference to BetterAppServices.Dtos so that repositories can return DTOs for better command/query
separation.
-
/BetterAppServices.Web
- Added reference to BetterAppServices.Dtos to be able show DTOs on web pages.
- Changed ComponentRegistrar.AddCustomRepositoriesTo "BetterAppServices.Core" to
"BetterAppServices.ApplicationServices" to reflect the new location of the data repository interfaces.
-
/BetterAppServices.Web.Controllers
- Added reference to BetterAppServices.Dtos.
-
/BetterAppServices.Tests
- Modified MappingIntegrationTests.CanGenerateDatabaseSchema to save DB schema to
/db/Schema/CreateBetterAppServicesDb_ChangesWillBeLost.sql every time the unit test is run.
To reiterate, many of the changes above can be incorporated into a CRUD scaffolding generator; the focus with this example
project is on providing an archtectural spike of the proposed architectural revisions.
Even if you don't use S#arp Archtiecture, this sample project should serve as a good example of using application services and basic use of command/query separation (CQS). Although the CQS in the sample project could be taken much further, I felt that the sample provides a good balance between practical maintainability and a more austere separation of concerns.
Enjoy!
Billy McCafferty
Posted
03-05-2010 11:47 AM
by
Billy McCafferty