-
Notifications
You must be signed in to change notification settings - Fork 95
GenericServices and DTOs
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.
There are three parts to a DTO for it to work with GenericServices.
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.
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)).
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.
- The
IncludeThen
parameters are string and I recommend usingnameof(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 anInclude
and any following parameters areThenInclude
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
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.
- PerDtoConfig.Generic.cs - Alter AutoMapper mappings (Read and Write)
- PerDtoConfig.cs - Define DDD-styled create/update methods and validation.
NOTE: The PerDtoConfig
MUST be in the same assembly/project as the DTO it configures.
Sometimes you might want a Dto within a Dto - this is known as nested DTOs. There are two sorts:
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.
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.