Skip to content

GenericServices and DTOs

Jon P Smith edited this page Jan 27, 2020 · 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, which improves the likelyhood of a good match (NOTE: in DDD-styled create/update any primary key properties are also set as ReadOnly(true)).

IncludeThen attribute

Version 3.1.0 of EfCore.GenericServices added an attribute to make writing DDD-styled updates of relationships easier. This attribute allows you to ask the UpdateAndSave/Async method to add Include and ThenInclude methods to the load on the entity.
Here is an example:

[IncludeThen(nameof(Book.Reviews))]
public class AddReviewWithIncludeDto : ILinkToEntity<Book>
{
    public int BookId { get; set; }
    public string VoterName { get; set; }
    public int NumStars { get; set; }
    public string Comment { get; set; }
}

Which internally be turned into

var book = context.DbSet<Book>()
      .Include(x => x.Reviews)
      .SingleOrDefault(x => x.BookId == dto.BookId) ;

This makes the the DDD access methods that update relationships simpler to write. See the AddReviewWithInclude in the Book class.

More information on the IncludeThen attribute

  • The IncludeThen parameters are string and I recommend using nameof(EntityType.RelationalProperty) as its safer, but you can also just give the relational property name as a string, e.g. "Reviews".
  • The IncludeThen attribute takes multiple parameters. The first is an Include and any following parameters are ThenInclude calls. So:
[ThenInclude( "Child", "Grandchild", "GreatGrandchild")
public class ExampleClass : ILinkToEntity<Parent>
{
   public int Id {get; set;}
   //... properties left out

Would turn into

context.DbSet<Parent>()
   .Include(x => x.Child)
   .ThenInclude(x => x.Grandchild)
   .ThenInclude(x => x.GreatGrandchild)
   .SingleOrDefault(x => x.Id== dto.Id) ;
  • You can have multiple ThenInclude attributes applied to the same class, e.g.
[IncludeThen(nameof(Book.Reviews))]
[IncludeThen(nameof(Book.AuthorsLink), nameof(BookAuthor.Author))]
private class AnotherDto : ILinkToEntity<Book>
{
   \\... rest of code left out

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.

NOTE: The PerDtoConfig MUST be in the same assembly/project as the DTO it configures.

Nested Dtos

Sometimes you might want a Dto within a Dto - this is known as nested DTOs. There are two sorts:

1. Nested Read DTOs - used in ReadManyNoTracked and GetSingle

Nesting DTOs in reads is fairly standard and works without any extra configration. The example below shows a DTO with a nested DTO called AuthorNestedV1Dto inside it.

public class BookListNestedV1Dto : ILinkToEntity<Book>
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public ICollection<AuthorNestedV1Dto> AuthorsLink { get; set; }
}

And the AuthorNestedV1Dto looks like this

public class AuthorNestedV1Dto : ILinkToEntity<BookAuthor>
{
    public byte Order { get; set; }
    public string AuthorName { get; set; }
}

This works, and allows you to capture the Author's name and the Order number needed to show the author's names in the correct order.

NOTE: see unit test TestNestedDtos for examples of this, and a second example where I add a PerDtoConfig to set up a different mapping in the nested DTO.

2. Nested Save Dtos - used in CreateAndSave and UpdateAndSave

Write nested DTOs aren't that common, and require a PerDtoConfig configuration class. The unit test TestNestedInDtosIssue13 contains an example. The InContactAddressConfig is the configuration class that allows the nested DTO to be mapped to the relationship.