Object mappings

DTOs versus ViewModels

Business layer communicates with presentation layer through interface objects called DTOs to hide the DB classes and to keep separation between layers. All ICRUDRepository methods accept DTOs and automatically map them to DB classes according to name conventions and specification expressed as LinQ expressions as described here.
Each ICRUDRepository implementation has its own way to declare the LinQ expressions that specifies how to map DTOs to/from db objects. See DefaultCRUDRepository and DocumentDBCRUDRepository.

May we use DTOs in Views? Sometimes we can! We need just to insert them into some properties of the overall View ViewModel. However, if our classes need presentation layer specific attributes like for instance, DisplayAttribute, ColumnConnectionAttribute, ColumnLayoutAttribute, etc., then we can't place them on DTOs since DTOs can't contain any presentation layer specific stuff. Thus, we are forced to copy DTOs into ViewModels containing all needed attributes. Typically, ViewModels contain exactly the same properties of their corresponding DTOs but marked with the appropriate attributes, plus possible further properties needed for their rendering (for instance a value/display list needed by a select).

How to map DTOs to/from ViewModels

The way DTOs and ViewModels, are mapped into each others are specified by LinQ expressions that follow the general rules described here. If no LinQ specification is provided for a pair then the objects are mapped according to the default name conventions described here.

Mapping specifications are associated to MappingContext instances. Whenever no MappingContext is specified in a mapping operations the default MappingContext contained in the static property MappingContext.Default is used.

All class mapping types are contained in the MvcControlsToolkit.Core.Business.Transformations namespace.

Defining mappings in a MappingContext

The LinQ expression that specifies how to map an object of a class S into an object of a class D is declared with the Add MappingContextmethod:

public MappingContext Add<S, D>(
    Expression<Func<S, D>> expression =null)
    where D: class, new()

If the expression parameter is omitted or null, just the standard naming conventions are used. When a mapping is performed between objects whose class mappings have not been declared an Add with a null expression is automatically performed.

The Add returns the MappingContext instance it was called on, so several method calls may be chained. Below a declaration is added to the default MappingContext

MappingContext.Default
    .Add<ReferenceModel, ReferenceTypeWithChildren>(
    m => new ReferenceTypeWithChildren
    {
        AMonth = Month.FromDateTime(m.AMonth),
        ANMonth = Month.FromDateTime(m.ANMonth.Value),
        AWeek = Week.FromDateTime(m.AWeek),
        ANWeek = Week.FromDateTime(m.ANWeek.Value),
        Children = m.Children.Select(
            l => new NestedReferenceType { })
    });

Declarations may be added to the default MappingContext in any static method. Custom MappingContext, instead are better defined together with their declarations in subclasses of MappingContext that are then added to the Asp.net Core Dependency Injection engine as singletons.

Important: for each DTO/ViewModel pair two declarations are needed, namely how to map the DTO into the ViewModel, and how to map the ViewModel into the DTO. Needless to say, mappings are not limited to DTO/ViewModel pairs but may be defined for all class pairs.

Mapping objects

Once the MvcControlsToolkit.Core.Business.Transformations namespace is added the Map extension method becomes available to all objects, while the MapIEnumerable method becomes available to all IEnumerable<T>.

Thus, an object may be mapped into another class by simply writing something like:

myDTO.Map(context).To<ReferenceTypeWithChildren>();

Where context is the MappingContext to be used for the transformation. If context is omitted or null the default MappingContext is used.

Analogously all objects of an IEnumerable<T> may be mapped with something like:

allModels.MapIEnumerable(context).To<ReferenceTypeWithChildren>();

ICRUDRepository implementations that operates on ViewModels instead of DTOs

Any ICRUDRepository implementation may be wrapped with an ICRUDRepository implementation that operates directly on ViewModels and maps them to the right DTOs before invoking the wrapped ICRUDRepository implementation.

The first step to define the wrapper is the declaration of wicht DTO to associate to each ViewModel. This may be done with the TransformationRepositoryFarm class or with the ODataTransformationRepositoryFarm class if we want to add also IWebQueryable capabilities to the wrapped repository.

odataFarm = new ODataTransformationRepositoryFarm()
                .Add<PersonVM, PersonDTO>()
                .Add<ReferenceVM, ReferenceType, ReferenceTypeExtended>();

The overload with 3 generics is used when the ViewModel must be used in ODATA queries that allow grouping. In this case the third generic is the subclass of the secondo generic (DTO) to be used when the query contains a grouping operation (it may contain further fields for the aggregated properties). In case grouping operations don't need further fields for aggregations the third generic argument may be equal to the second. For more information on ODATA queries see the ODATA query documentation.

In ViewModels used for ODATA queries all properties that might be involved in queries must have exactly the same names of their corresponding DTO properties, since each ODATA query use the ViewModel properties (that is their names), but is "interpreted" on the DTO.

For a similar reason the GetPage and GetPageExtended methods are not pre-processed by the wrapper that passes the ViewModel as it is to the wrapped repository without any DTO coversion. In fact, all query related parameters of these methods (filters, sortings, and grouping) are expressed in terms of the ViewModel and can't be applied to an IQueryable that is based on a different class (the DTO). For this reason ODATA queries may be processed by just the ODataTransformationRepositoryFarm class that may create an ICRUDRepository implementation that is also an IWebQueryable implementation, that is an implementation that contains the ExecuteQuery methods that accepts directly an ODATA query, instead of strongly typed LinQ stuffs.

TransformationRepositoryFarm and ODataTransformationRepositoryFarm may be configured in a static method since they need to be configured just once. A Good place to configure them is the static constructor of each controller that uses the wrapped repository, so that they are available in a static private property of the controller and may be used by any controller instance that needs to wrap a repository instance.

Once configured TransformationRepositoryFarm and ODataTransformationRepositoryFarm may be used to create the wrapper repository by invoking their Create method whose parameters are the ICRUDRepository to wrap, and the MappingContext to use for all mappings. If the MappingContext is omitted or null the default MappingContext is used.

Since the repository to wrap is usually injected through Dependency Injection either in the Controller constructor or in a specific action method, the creation operation should be performed there.


Fork me on GitHub