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
MappingContext
method:
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
.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:
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:
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.
.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.