Reputation: 409
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.
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.
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
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