Skip to content

GenericServices and DTOs

Jon P Smith edited this page Apr 10, 2018 · 17 revisions

GenericServices makes heavy use of DTOs (Data Transfer Objects, also known as ViewModels). This page gives an overview of how to set up DTOs and how GenericServices uses them.

The format of a GenericServices' DTO

There are three parts to a DTO for it to work with GenericServices.

ILineToEntity<TEntity> - tells GenericServices what entity the DTO links to

GenericServices needs to know what EF Core database entity class the DTO is linked to. You provide that information by adding the ILineToEntity<TEntity> interface to your DTO (the interface is empty, i.e. you don't have to implement anything extra - its just the TEntity info that GenericServices needs). There MUST be a ILineToEntity<TEntity> on every DTO. Here is a simplified example:

 public class SimpleDto : ILinkToEntity<Book>
 {
     public int BookId { get; set; }
     public string Title { get; set; }            
     public DateTime PublishedOn { get; set; }
 }

The ILinkToEntity<Book> says that the SimpleDto is linked to the Book entity. This means on read it will use AutoMapper to build a Select query on the DbSet<Book> property to extract the BookId, Title and PublishedOn values and put them in the DTO. Similarly, on a create/update it will use the BookId to set the row to update and will use the value of the properties to update the Book entity.

[ReadOnly(true)] - tells GenericServices which properties are only for read, not write

When using a DTO for create/update you sometimes need read, but not write a property. For instance, in the example application I want to show the title of the book that the user is going to update so that they can confirm its the right one. but I don't want the title updated. GenericServices looks for the [ReadOnly(true)] attribute on on a property, and if present (and true) it will not use that property in a create/update. Here is an example for updating the publication date of a book

public class ChangePubDateDto : ILinkToEntity<Book>
{
    [ReadOnly(true)]
    public int BookId { get; set; }

    [ReadOnly(true)]
    public string Title { get; set; }
              
    public DateTime PublishedOn { get; set; }
}

The DTO above would only update the PublishedOn property in the Book entity.
Advanced notes: If working with standard-styled entities then the AutoMapper save mapping has a rule to exclude properties that are null or have the ReadOnly(true) attribute. If working with DDD-styled entities then the ReadOnly(true) attribute removes the property from matching to a parameter in a access method (NOTE: in DDD-styled create/update any primary key properties are automatically set as ReadOnly(true)).

PerDtoConfig<TDto, TEntity> - optional configuration for a specific DTO

You can create a configuration file for a specific DTO by creating a class that inherits from the abstract class, PerDtoConfig<TDto, TEntity>. This allows you to change various things, like the AutoMapper mappings (Read and Write), plus some other features. Here is an example which alters the AutoMapper Read mapping (used in example application).

class BookListDtoConfig : PerDtoConfig<BookListDto, Book>
{
    public override Action<IMappingExpression<Book, BookListDto>> AlterReadMapping
    {
        get
        {
            return cfg => cfg
                .ForMember(x => x.ReviewsCount, x => x.MapFrom(book => book.Reviews.Count()))
                .ForMember(x => x.AuthorsOrdered, y => y.MapFrom(p => string.Join(", ",
                    p.AuthorsLink.OrderBy(q => q.Order).Select(q => q.Author.Name).ToList())))
                .ForMember(x => x.ReviewsAverageVotes,
                    x => x.MapFrom(p => p.Reviews.Select(y => (double?)y.NumStars).Average()));
        }
    }
}

See the comments in the code on these two classes.