2
\$\begingroup\$

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:

  1. My first behavior logs everything

  2. Then handlers are triggered

  3. 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?

\$\endgroup\$
4
  • \$\begingroup\$ Could you add the implementation of IRequester? \$\endgroup\$
    – t3chb0t
    Oct 5, 2017 at 20:38
  • \$\begingroup\$ Done. As I said this implementation is very simple and does not bring much value. \$\endgroup\$
    – Seb
    Oct 6, 2017 at 0:47
  • \$\begingroup\$ True, this does not help much but actually I had hoped that it would reveal how and why you are using the Result type which I find is unnecessary. The Task 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 a Result? \$\endgroup\$
    – t3chb0t
    Oct 6, 2017 at 6:17
  • \$\begingroup\$ Good one. I use Result a container. If one of my handler fails to run, I will capture the exception and pass it back to the controller using this object. In addition, a post processor would convert the result set to a set of DTO object. I have added some details about this my post. \$\endgroup\$
    – Seb
    Oct 6, 2017 at 7:14

1 Answer 1

2
\$\begingroup\$

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 :)

\$\endgroup\$
1
  • \$\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
    Nov 5, 2017 at 12:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.