When working with business entities, the first big error that you will make is suppose that this entity is the truth and will be like that forever. You are making a big mistake here.
Let's suppose you are working with the account entity.
- Day 1, the entity might look like X.
- Day 2, you are acquiring a new business using a CRM and your entity might look like X+Y.
- Day 3, .... your entity might look like GODZILLA with the perfermance and maintenance issues related to it...
First thing to do, is abstracting the business entity tighted to technologies (ie.: CRM for an Account for exemple) and generalize it to be able to have a simple, portable and flexible DTO! But you are asking, what is a DTO? DTO is a design pattern/principle. It stands for Data Transfer Object. It is used in development process to create a new object that will be used in data transfer to abstract the business objects. And that methodology:
- will survive new software migration that may modify your entity
- will survive the aggregation of many systems to create your entity
- will obfuscate systems
- ...
I'm not a master in technical explanation of DTO, but it is really working. And my library is supporting it natively, so ... USE IT :) You will need to import this nuget package to get DTO feature working: LSG.GenericCrud.Dto
Here is a link to a DTO mapping source code sample: Link
Before getting into further details, I think this is important for you to understand what is happening behind the scenes.
First of all, you have an entity (further called Business Object @ BO) and a DTO related to it. A BO is a class representing the business data. A DTO is representing the data that will be transfered to the consumer.
To be able to map a BO and a DTO, you will need a Mapper. You can do this all by hand in a service layer for example. My library is doing it for you with simple configurations with the big help of AutoMapper. This library come in handy to facilitate the mapping process.
When you have:
- A BO class
- A DTO class
- A mapping that is mapping BO & DTO
You are done ! :)
To enable the DTO feature, you have few steps to do to make the thing work:
- Create the entity class
- Create the dto class
- Configure the mapping between the two
- Adjust your controller to tell it to manage dto mapping
Create a class named Account.cs:
public class Account : IEntity<Guid>
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int AnnualRevenue { get;set; }
}
Create a class named AccountDto.cs:
public class AccountDto : IEntity<Guid>
{
public Guid Id { get; set; }
public string FullName { get; set; }
}
Adjust your Startup.cs to add data mapping in ConfigureServices(...) method:
var automapperConfiguration = new AutoMapper.MapperConfiguration(_ =>
{
_.CreateMap<AccountDto, Account>()
.ForMember(dest => dest.FirstName, opts => opts.MapFrom(src => src.FullName.Split(',', StringSplitOptions.None)[0]))
.ForMember(dest => dest.LastName, opts => opts.MapFrom(src => src.FullName.Split(',', StringSplitOptions.None)[1]));
_.CreateMap<Account, AccountDto>()
.ForMember(dest => dest.FullName, opts => opts.MapFrom(src => $"{src.FirstName},{src.LastName}"));
});
services.AddSingleton(automapperConfiguration.CreateMapper());
Note: You did two things with your DTO:
- Data masking: You hid the AnnualRevenue property from the business definition
- Data aggregation: You aggregate FirstName and LastName to create a new field called FullName
Here is a CrudController<,> (CrudController<,>)
[Route("api/[controller]")]
public class AccountsController : CrudController<Guid, Account>
{
public AccountsController(ICrudService<Guid, Account> service) : base(service) {}
}
Here is a controller ready for DTO mapping (CrudController<,,>)
[Route("api/[controller]")]
public class AccountsController : CrudController<Guid, AccountDto>
{
public AccountsController(ICrudService<Guid, Account> service) : base(service) {}
}
The differences are:
- The class inherits from a different type: a CrudController able to manage TDto, TEntity mapping
- A second parameter is needed in the default constructor, this paramter is used to pass an injected auto mapper definition to the execution flow.