Pavel Foltyn
Pavel Foltyn

Reputation: 409

How to map DTO properties to those in a domain object when there is a class hierarchy in C#

Introduction

To put it simple, assume we have 3 classes in the domain (I call them business objects, BOs). I leave out constructors and other stuff, only the properties are relevant, more or less.

public class VehicleBo
{
    public string Manufacturer { get; set; }
}

public class CarBo : VehicleBo
{
    public int PassengerCount { get; set; }
}

public class TruckBo : VehicleBo
{
    public double MaximumLoad { get; set; }
    public bool HasTrailer { get; set; }
}

Then, we have just one DTO object that "aggregates" all the properties from the hierarchy of BOs.

public class VehicleDto
{
    public string Manufacturer { get; set; }
    public int PassengerCount { get; set; }
    public double MaximumLoad { get; set; }
    public bool HasTrailer { get; set; }

    // Corresponds to a discriminator defined on VehicleBo when mapping BOs to the database with EF Core.
    public string VehicleType { get; set; }
}

We use Entity Framework Core to map our BOs to tables in a relational database. There is just one table (Vehicles) for all the BOs, ie VehicleBo, CarBo, TruckBo. The Vehicles table maps to the VehicleBo entity and the other BOs are stored in Vehicles using the "Table per hierarchy" (TPH) strategy.

Behind the scenes

Now, I look for a nice and consistent solution regarding this:

When the user is viewing the contents of the Vehicles table, they see data stored in the DTO object (VehicleDto). For filtering and sorting purposes, there is a tiny "framework" that handles it. It tries to "map" a DTO property to a corresponding BO property, but in a very simple way -- the names must match.

For example, for a filter, our FE sends a request (in symbolic code):

{
    Type: VehicleDto,
    Predicate: v => v.Manufacturer == "Ford"
}

The framework translates this to something like:

{
    Type: VehicleBo,
    Predicate: v => v.Manufacturer == "Ford"
}

(Notice VehicleBo instead of VehicleDto) And it sends it to the BE.

Here, this is simple since the Manufacturer property is found both in the DTO and in the BO.

However, this is not the case for PassengerCount, for example.

FE sends:

{
    Type: VehicleDto,
    Predicate: v => v.PassengerCount > 2
}

The framework cannot translate that to:

{
    Type: VehicleBo,
    Predicate: v => v.PassengerCount > 2
}

Because the PassengerCount property does not exist in VehicleBo.

Therefore, what is then sent to our BE is this:

{
    Type: VehicleBo,
    Predicate: v => ((CarBo) v).PassengerCount > 2
}

This is OK, EF is able to translate this to a SQL query just fine.

Challenge

But I search for an elegant way of how to give a hint to our framework regarding the "DTO-to-BO" mapping.

The first idea is using attributes.

public class PropertyMappingAttribute<TEntity, TProperty> : Attribute
{
    public Expression<Func<TEntity, TProperty>> PropertySelector { get; }

    public PropertyMappingAttribute(Expression<Func<TEntity, TProperty>> propertySelector)
    {
        PropertySelector = propertySelector;
    }
}

// ...

public class VehicleDto
{
    public string Manufacturer { get; set; }
    [PropertyMapping<VehicleBo, int>(v => ((CarBo) v).PassengerCount)]
    public int PassengerCount { get; set; }
    public double MaximumLoad { get; set; }
    public bool HasTrailer { get; set; }

    // Corresponds to a discriminator defined on VehicleBo when mapping BOs to the database with EF Core.
    public string VehicleType { get; set; }
}

Unfortunately, this is currently not possible in C#. For attribute parameters, we must use primitive types such as string:

public class PropertyMappingAttribute : Attribute
{
    public string PropertySelector { get; }

    public PropertyMappingAttribute(string propertySelector)
    {
        PropertySelector = propertySelector;
    }
}

// ...

public class VehicleDto
{
    public string Manufacturer { get; set; }
    [PropertyMapping("((CarBo) v).PassengerCount")]
    public int PassengerCount { get; set; }
    public double MaximumLoad { get; set; }
    public bool HasTrailer { get; set; }

    // Corresponds to a discriminator defined on VehicleBo when mapping BOs to the database with EF Core.
    public string VehicleType { get; set; }
}

But the latter approach is awkward and error-prone.

Do you have any suggestions how to solve this in an elegant way while keeping it simple and type safe?

Upvotes: 0

Views: 136

Answers (1)

Pavel Foltyn
Pavel Foltyn

Reputation: 409

Eventually, I decided to split the difference and define the mapping "partially type-safe".

Here is the definition of PropertyMappingAttribute:

public class PropertyMappingAttribute : Attribute
{
    public Type BoSubtype { get; }
    public string PropertyName { get; }

    public PropertyMappingAttribute(Type boSubtype, string propertyName)
    {
        BoSubtype = boSubtype;
        PropertyName = propertyName;
    }
}

Thus, the DTO object should look like this:

public class VehicleDto
{
    public string Manufacturer { get; set; }
    [PropertyMapping(typeof(CarBo), "PassengerCount")]
    public int PassengerCount { get; set; }
    public double MaximumLoad { get; set; }
    public bool HasTrailer { get; set; }

    // Corresponds to a discriminator defined on VehicleBo when mapping BOs to the database with EF Core.
    public string VehicleType { get; set; }
}

For the name of the BO derived type property, you can use the nameof operator, so for typical cases where you need just a type cast and a member (property) selector, this is almost equivalent to my original idea with a lambda expression which is not supported by EF Core so far:

[PropertyMapping(typeof(CarBo), nameof(CarBo.PassengerCount))]

For the sake of completeness, I'm finally showing the full definition of the DTO object where each property not contained in VehicleBo is decorated with a PropertyMapping attribute accordingly:

public class VehicleDto
{
    // Property common to all VehicleBo derived objects.
    public string Manufacturer { get; set; }

    [PropertyMapping(typeof(CarBo), nameof(CarBo.PassengerCount)]
    public int PassengerCount { get; set; }

    [PropertyMapping(typeof(TruckBo), nameof(TruckBo.MaximumLoad)]
    public double MaximumLoad { get; set; }

    [PropertyMapping(typeof(TruckBo), nameof(TruckBo.HasTrailer)]
    public bool HasTrailer { get; set; }

    // Corresponds to a discriminator defined on VehicleBo when mapping BOs to the database with EF Core.
    public string VehicleType { get; set; }
}

Upvotes: 0

Related Questions