maaax
maaax

Reputation: 31

What is the correct way to handle models and entities in clean architecture?

So I'm trying to set up all the basics for a new .NET application. It's a Blazor app, to be created with clean architecture and code-first EF Core.

In my domain layer I defined some entities which will be used be EF Core to generate the database. And in the presentation/WebUI layer I created a folder for models that I can give to the razor files. The original idea was to map directly from the entities to the models when I'm retrieving the data.

When creating the interfaces for my services in the application layer I realized that the application layer is not supposed to have access to the presentation layer. Therefore I cannot have an interface like Task<SomeModel> GetSomeModelByIdAsync(int id);. I would have to return the entity but I don't want that sent all the way out to the outer layer right?

What would be the correct way of handling this. Do I move my models to the application layer, or do I create DTOs that the presentation layer can map to the models? It's only a hobby project so I'm fine by taking my time and getting things better than they have to be. Yes I'm probably overthinking it, what fun would it be otherwise?

Upvotes: 0

Views: 471

Answers (1)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30310

If you want to apply Clean Design principles you should design your core domain entities based on your application requirements.

Considering the Weather forecast as a simple example, you could define it like this.

Note:

  1. I've created objects to represent temperature and the Id. Using a Guid and a decimal is classic Primitive Obsession where you're letting your datastore dictate your application design.

  2. I use a lot of read only objects to prevent mutation. Inadvertent mutation is the cause of many bugs.

public sealed record DmoWeatherForecast
{
    public WeatherForecastId WeatherForecastId { get; init; } = WeatherForecastId.NewEntity;
    public DateOnly Date { get; init; }
    public Temperature Temperature { get; set; } = new(0);
    public string? Summary { get; set; }
}

Where WeatherForecastId is:

public readonly record struct WeatherForecastId
{
    public Guid Value { get; init; }
    public object KeyValue => this.Value;

    public WeatherForecastId(Guid value)
        => this.Value = value;

    public static WeatherForecastId NewEntity
        => new(Guid.Empty);
}

And Temperature is:

public record Temperature
{
    public decimal TemperatureC { get; init; }
    public decimal TemperatureF => 32 + (this.TemperatureC / 0.5556m);

    public Temperature() { }

    public Temperature(decimal temperature)
    {
        this.TemperatureC = temperature;
    }
}

In the infrastructure layer we need to fall back on primitives to fit with the data store:

public sealed record DboWeatherForecast
{
    [Key] public Guid WeatherForecastID { get; init; } = Guid.Empty;
    public DateTime Date { get; init; }
    public decimal Temperature { get; set; }
    public string? Summary { get; set; }
}

You can use various options to do the mapping. I've created my own mapper:

public sealed class DboWeatherForecastMap : IDboEntityMap<DboWeatherForecast, DmoWeatherForecast>
{
    public DmoWeatherForecast MapTo(DboWeatherForecast item)
        => Map(item);

    public DboWeatherForecast MapTo(DmoWeatherForecast item)
        => Map(item);

    public static DmoWeatherForecast Map(DboWeatherForecast item)
        => new()
        {
            WeatherForecastId = new(item.WeatherForecastID),
            Date = DateOnly.FromDateTime(item.Date),
            Temperature = new(item.Temperature),
            Summary = item.Summary
        };

    public static DboWeatherForecast Map(DmoWeatherForecast item)
        => new()
        {
            WeatherForecastID = item.WeatherForecastId.Value,
            Date = item.Date.ToDateTime(TimeOnly.MinValue),
            Temperature = item.Temperature.TemperatureC,
            Summary = item.Summary
        };
}

You can see a full implementation here: https://github.com/ShaunCurtis/Blazr.Demo

There's also an Invoice implementation to demonstrate how to deal with complex entities.

This approach is more complex that using a simple class WeatherForecast object defined in the core domain, whose design is driven by the datastore. There's nothing wrong in using this approach in a simple application.

Note: I use the term Core Domain to represent what many people call the Domain and Application layers. The Domain Domain doesn't work!

Upvotes: 1

Related Questions