I'm writing a service (asp.net core) in charge of determining availabilities of resources (rooms, desks...). I'm also trying to stick to the DDD principles to improve the quality of my code.
I have been trying to implement this service for the past few days but I’m still not satisfied with the result.
Determining the resource availability is fairly simple:
- A resource (room, desk, empty office) is available during a specific time frame only if it is not booked (= no booking covering this time frame).
- A combined resource (aka two rooms merged into a bigger one) is available during a specific time frame only if the master room and the sub-rooms are available.
This service needs to offer two features:
- Determine if a given resource is available
- Calculate all availabilities of resources located in a given city/building/centre (one building can have several centres)
This service is using CQRS concepts I came up with this system while reading JBogard blog + GitHub.
My pipeline is fairly simple:
My first behavior logs everything
Then handlers are triggered
Finally a post processor converts my entities into DTOs using AutoMapper
Here is the code. For the sake of brevity I have skipped the EF core configuration.
Handler (Domain)
public class AvailabilitiesQueryHandler :
IQueryHandler<GetAvailabilitiesQuery>
, IQueryHandler<GetCombinedRoomAvailabilitiesQuery>
{
private readonly MyDbContext _ctx;
public AvailabilitiesQueryHandler(MyDbContext ctx) => _ctx = ctx;
public async Task<Result> Handle(GetAvailabilitiesQuery query) => new Result(await _ctx [... mega optimized query...]);
public async Task<Result> Handle(GetCombinedRoomAvailabilitiesQuery query) => new Result(await _ctx [... mega optimized query...]);
}
IQueryHandler
is an interface defined below
public interface IQueryHandler<in TQuery> : IAsyncRequestHandler<TQuery, Result>
where TQuery : IQuery
{ }
Here I inject an EF Core DbContext
into my handler. I found out that EF core is really good at generating optimized SQL command based on query. If I use separate repositories I loose on flexibility and performance!
Result
is a simple container. Initially I wanted to make it immutable but Mediatr doesn't allow me to return new object within the pipeline. In other words, I can only mutate the existing response and nothing else.
public class Result
{
protected object _inner { get; private set; }
public bool IsSuccess => !(_inner is Exception);
public Result(object output) => _inner = output;
public void Set(object output) => _inner = output;
public object Get() => _inner;
public T Get<T>() where T : class
=> _inner as T;
}
Once data is retrieved from the DB, my post processor converts entities into DTO objects:
public class ConvertionProcessor<TQuery, TResponse> : IRequestPostProcessor<TQuery, TResponse>
where TQuery : IConvertibleQuery
where TResponse : Result
{
private readonly IMapper _mapper;
public ConvertionProcessor(IMapper mapper) => _mapper = mapper;
public async Task Process(TQuery request, TResponse response)
{
var outputType = request.OutputType;
var output = await Task.FromResult(response.Get<object>());
if (!response.IsSuccess || output == null)
return;
if (output is IPage<object> page)
output = _mapper.Map(page, page.GetType(), typeof(Page<>).MakeGenericType(outputType));
else if (output is IEnumerable<object> col)
output = _mapper.Map(col, col.GetType(), typeof(IEnumerable<>).MakeGenericType(outputType));
else
output = _mapper.Map(output, output.GetType(), outputType);
response.Set(output);
}
}
Controller (Presentation)
public class AvailabilitiesController : Controller
{
private readonly IRequester _req;
public AvailabilitiesController(IRequester requester) => _req = requester;
// v1/availabilities
[HttpGet]
public async Task<IActionResult> GetAvailabilities(GetAvailabilitiesInput input)
{
// Get rooms, hotdesk, empty offices availabilities
var r1 = await _req.Query(new GetAvailabilitiesQuery(input, 1, 3, 4));
if (!r1.IsSuccess) return r1.ToActionResult();
// Combined Room availabilities
var r2 = await _req.Query(new GetCombinedRoomAvailabilitiesQuery(input));
if (!r2.IsSuccess) return r2.ToActionResult();
return Json(r1.Get<IEnumerable<object>>().Concat(r2.Get<IEnumerable<object>>()));
}
IRequester
is a very simple abstraction of IMediatr
which is actually pretty useless.
public class Requester : IRequester
{
private readonly IMediator _mediator;
public Requester(IMediator mediator)
{
_mediator = mediator;
}
public async Task<Result> Query(IQuery query) => await _mediator.Send(query);
}
The method ToActionResult
simply parses the result and return the associated status code
public static class ResultExtensions
{
public static IActionResult ToActionResult(this Result result)
{
if (result?.Get() == null)
return new ObjectResult("The current endpoint did not return a valid result") {StatusCode = 500};
if (result.IsSuccess)
return new JsonResult(result.Get());
if (result.Get() is TEC.Common.Core.Model.ItemNotFoundException notFound)
return new NotFoundObjectResult(notFound.Message);
if (result.Get() is ArgumentException badArgs)
return new BadRequestObjectResult(badArgs.Message);
return new ObjectResult(result.Get<Exception>().Message) { StatusCode = 500 };
}
}
There are a couple of things I don't really like about this design:
"Verbosity": I need to create separate handlers, queries, validators (if validation is needed), inputs, outputs class. In the end, I'm adding dozen of folders and classes.
Reusability: reusing the logic implemented in the handler in not easy. I would need to rely on existing queries. In the end, I need to build more pipelines, more queries and my entire eco-system becomes a gigantic set of handlers, inputs, outputs...
Doesn't smell totally DDD: my model is anemic (simple POCO classes) and the logic is exclusively contained in handlers. I could introduce another abstraction layer like AvServices and send queried from this service layer. This would solve the reusability.
So what do you think about this design?
IRequester
? \$\endgroup\$Result
type which I find is unnecessary. TheTask
can already do that and much more. Then maybe let me ask it this way: could you add the part of your code where you create and return aResult
? \$\endgroup\$