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?
1 Answer 1
DDD How often have you heard from your domain expert manually managing room booking in his/her Excel all those terms like IQueryHandler
, Result
, ConvertionProcessor
? I bet that never. This stuff does not belong to the domain, does not solve any business problems, and probably should not exist. Having names like that clearly indicates the problem - those words are not in the Ubiquitous Language. Domain logic is way too corrupted with technicalities. Sure, no database related code should be allowed in domain. P.S. It depends on the task, but I could probably load all the data in memory and run Linq to Objects
here easily.
Design What is the Software Design? All the things helping you to keep your project maintainable and that you cannot change latter easily. What helps keep project maintainable? Placing dependencies in a way where volatile components depend on a minimum amount of stable abstractions using Dependency Inversion where necessary. There are too many concrete tech elements in your code to follow "minimum amount of stable abstractions" criteria to keep things maintainable, so design misses its goal here. P.S. It is not a responsibility of the Controller to decide on room/combined room booking priorities – it belongs to the business logic – to the place where you actually implement the logic – repository in your case, as you do not need DDD here at all.
I would have the following models in the Core project without dependencies on anything else:
class Slot { ... }
class SlotQuery { ... }
interface IScheduler
{
Task<Slot[]> FindSlotAsync(SlotQuery query);
}
And in the DB adaptor referencing Core:
class Scheduler : IScheduler { ... }
Where Web project will reference all above and will have SlotController action:
IScheduler Scheduler { get; }
[HttpGet]
public async Task<Slot[]> FindAync([FromQuery] SlotQuery query) =>
await Scheduler.FindSlotAsync(query);
We need to project EF Entities to models (Slot
), but it should be really cheap to do manually in the Scheduler
repository, as it probably has a very narrow interface.
It would help to publish your input and output (query/result) structures. Sorry, everything else could just turn out to be a redundant overhead in this case :)
-
\$\begingroup\$ Hi Dmitry. First I want to thank you for the quality of your answer. I really appreciate that. My first approach was indeed not DDD oriented. I end up changing it completely to something similar to what you proposed! Thx for your time \$\endgroup\$Seb– Seb2017年11月05日 12:55:28 +00:00Commented Nov 5, 2017 at 12:55
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\$