If you haven't already, I would recommend you read through my original post on the Projection-View Pattern first even though this post is technically a prequel to it. While that pattern is still fine, and I still continue to use it and I will not be changing that, I found myself wanting to use it in non-view workloads.
Specifically, in my project at work, I have quite a lot of code that needs to project data out of the database and transform it, but it never returns a view to the screen. It might be sending off an email, or generating a file, outputting JSON, or something else entirely.
I used to just adapt to what I needed at the time and inline my projections, but it felt highly disconnected considering I had been using the Projection-View Pattern with great results and success for my views. Then it kind of hit me that the view was nothing more than a result of the projection, and the result could be anything, not just a view.
So, I decided to step back a moment and after some more thought and tinkering, I settled on a base pattern that I've dubbed the Projection-Result Pattern. You're amazed at my naming skills, I know...
Also, since the original post for the Projection-View Pattern, I've changed up how I use MediatR, and I now implement IRequestHandler<TRequest, TResponse>
instead of HandlerBase<TRequest, TResponse>
. Here's the entire stack of base classes as I am currently using them:
AsyncHandlerBase<TRequest>
is a simple handler that doesn't return a result.AsyncHandlerBase<TRequest, TResponse>
is a simple handler that does return a result.AsyncProjectionHandlerBase<TRequest, TProjection, TResult>
is a slightly more complex handler that does data projection and returns a result.QueryHandlerBase<TQuery, TProjection, TView>
is an optional handler, but it has its place when I'm returning views. As I'm writing this I'm starting to think that maybe it should be called something different like ViewHandlerBase<TQuery, TProjection, TView>
to express it's intent more clearly, but I'll have to sleep on it for a bit. I slept on it and I've accepted it, so it is now called ViewHandlerBase
.
Now let's look at the code for each of them. For the purpose of the example, we'll pretend there's a DbContext
class called MyDbContext
that's being injected. Of course, you may not need to inject anything or you just want to use the built-in handler base classes, and that's perfectly fine, I'm just showcasing how I've decided to use MediatR.
AsyncHandlerBase<TRequest>
public abstract class AsyncHandlerBase<TRequest> :
IRequestHandler<TRequest>
where TRequest : IRequest {
protected MyDbContext Context { get; }
protected IMapper Mapper { get; }
protected AsyncHandlerBase(
MyDbContext context,
IMapper mapper) {
Context = context;
Mapper = mapper;
}
public abstract Task<Unit> Handle(
TRequest request,
CancellationToken cancellationToken = default);
}
AsyncHandlerBase<TRequest, TResponse>
The AsyncHandlerBase<TRequest, TResponse>
class has an additional property called MapperConfig
which I pass to the .ProjectTo<T>()
AutoMapper extension methods when I need to. It's slightly less typing than the full Mapper.ConfigurationProvider
everywhere. You can remove it if you don't like it or want it.
While I've debated on putting the MapperConfig
property in the AsyncProjectionHandlerBase<TRequest, TProjection, TResult>
class only, I decided against it because you may end up using the projection extension methods in a class inheriting from this one as well.
public abstract class AsyncHandlerBase<TRequest, TResponse> :
IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse> {
protected MyDbContext Context { get; }
protected IMapper Mapper { get; }
protected IConfigurationProvider MapperConfig => Mapper.ConfigurationProvider;
protected AsyncHandlerBase(
MyDbContext context,
IMapper mapper) {
Context = context;
Mapper = mapper;
}
public abstract Task<TResponse> Handle(
TRequest request,
CancellationToken cancellationToken = default);
}
AsyncProjectionHandlerBase<TRequest, TProjection, TResult>
public abstract class AsyncProjectionHandlerBase<TRequest, TProjection, TResult> :
AsyncHandlerBase<TRequest, TResult>
where TRequest : IRequest<TResult>
where TProjection : class, new()
where TResult : class {
protected AsyncProjectionHandlerBase(
MyDbContext context,
IMapper mapper) :
base(context, mapper) {
}
public override Task<TResult> Handle(
TRequest request,
CancellationToken cancellationToken = default) {
var result = GetResult(request);
return Task.FromResult(result);
}
protected virtual TProjection GetProjection(
TRequest request) => new TProjection();
protected virtual TResult GetResult(
TRequest request) {
var projection = GetProjection(request);
var result = Mapper.Map<TResult>(projection);
NormalizeResult(request, projection, result);
return result;
}
protected virtual void NormalizeResult(
TRequest request,
TProjection projection,
TResult result) {
}
}
QueryHandlerBase<TQuery, TProjection, TView>
Lastly, we have the QueryHandlerBase<TQuery, TProjection, TView>
class, which is optional, but I've found it useful since I only use it to return views and I usually need to have access to the user identity. In this example, I'm also injecting the IdentityProvider
from my Arex388.AspNetCore NuGet package. I also constrain the TProjection
and TView
parameters to inherit from the ProjectionBase
and ViewBase
base classes.
public abstract class QueryHandlerBase<TQuery, TProjection, TView> :
AsyncProjectionHandlerBase<TQuery, TProjection, TView>
where TQuery : IRequest<TView>
where TProjection : ProjectionBase, new()
where TView : ViewBase {
protected IdentityProvider Identity { get; }
protected QueryHandlerBase(
MyDbContext context,
IMapper mapper,
IdentityProvider identity) => Identity = identity;
}
Summary
With all of that code, I'm wrapping up this post. Really, the Projection-Result Pattern should be thought of as a generic version of the Projection-View Pattern, but it doesn't replace it. Both patterns have their places and use. The Projection-View Pattern simply specializes the Projection-Result Pattern when dealing with returning views to the user.
I hope either pattern will help someone in their coding, they've surely helped me in improving my data access as well as standardizing on a common coding pattern. Between the two patterns, I probably have about 250 or more classes that inherit from them in my work's project.