One of the questions that frequently arise on the Domain Driven Design mail list is how to build the query side of CQS? The most popular approaches are as follows:
- Go the 2-tier way and query the store directly from the client
- Implement some sort of thin DTO layer on top of the query store
If the Query side in question is not some sort of BI support store I personally prefer the second approach because of security. Putting security checks on the client is something I can’t agree with, because the server side should never trust the clients (unless, other requirements explicitly states so).
The question is how to enable security rules such as “Account manager can only see the payment history of his own customers”? In this post I will go through the steps of implementing the Query side with security support for such rules.
The implementation below uses LINQ to SQL and WCF and a solution to inject security rules into the queries. While LINQ to SQL with WCF do quite a good job here, the same approach can be used with any ORM that supports query objects (say NHibernate and Criteria API).
Note: because Command side and Query side are completely separated they can be implemented with a different set of technologies (say, Q on top of WCF + L2S, while C on top of NHibernate + NserviceBus).
Implementing the Query Side
The first step is to generate LINQ to SQL objects from the view-specific tables/views in the query store (seems like the first time the designer makes a good job). Note that because most of the queries are just flat data (like grids) the objects generated will have no connections.
The next part is implementing WCF service which returns the results needed to be displayed by the application. The operations in the service will return LINQ to SQL objects which are used as DTOs here.
Injecting Security Rules
Protecting data from being viewed is mostly about filtering it, so security rules for the Q side are easily described as data filters. The following defines how security filters look with LINQ:
public interface ISecurityFilter<T>
{
IQueryable<T> Apply(IQueryable<T> query);
}
The following class can express the rule above (“Account manager can only see the payment history of his own customers”) in code:
public class AccountManagerPaymentHistoryAccessFilter : ISecurityFilter<AccountManagerPaymentHistory>
{
public IQueryable<AccountManagerPaymentHistory> Apply(IQueryable<AccountManagerPaymentHistory> query)
{
var userId = ...; //current user id
return from p in query
where p.AccountManagerId == userId
select p;
}
}
Rules are injected using the following wrapper over the DataContext object generated by LINQ to SQL:
public class QueryContext
{
private QueryDataContext _context = new QueryDataContext();
public IQueryable<T> Get<T>()
{
var filters = IoCContainer.ResolveAll<ISecurityFilter<T>>();
var query = _context.GetTable<T>();
foreach (var filter in filters)
query = filter.Apply(query);
return query;
}
}
The wrapper uses IoC container to get all security filters applicable and apply them one by one. As a result any code in the service layer that queries against QueryContext.Get<T>() operates on prefiltered data.
Versioning
When using DTOs with strict schema versioning can became a hassle. The rule of thumb here is to avoid schema breaking changes such as renaming or removing of fields/properties and rather go with a soft process of deprecating fields/properties in favor of new ones.
DataContractSerializer has built-in support using ExtensionDataObject. All new fields/properties will go to ExtensionDataObject property that is part of WCF autogenerated classes.
Another possible approach here is to return some generic DTO such as DataSet from the service, so that the view will be completely driven by service replies.
Conclusion
With the help of IoC container and security filters we can easily implement security requirements on the query side. Also, because the Query side is free of heavy logic, the implementation can be done on top of any ORM technology that supports query objects.
P.S.: this is one of the examples where DnDDD (Drag’n’Drop Driven Development) and code gen do the job :)