The normal path in WEB API is to have your "Entity" and the "DTO".
For example:
public class Product // DB Entity
{
public int Id { get; set; }
public string Name { get; set; }
public string SerialNumber { get; set; }
public string ReferenceNumber { get; set; }
public DateTime RecordTimestamp { get; set; }
public string InternalNumber { get; set; }
}
public class ProductDto // Client-facing DTO
{
public string Name { get; set; }
public string SerialNumber { get; set; }
public string ReferenceNumber { get; set; }
}
In this case, not just posting but also viewing is the issue. We want consumer to see part of the data and hide system data. But at the same time, this same Context is used for POSTing (by permitted accounts), hence entity models must have the properties that can be used for CRUD.
The normal OData API is set as following - Controller is deriving from ODataController and GET actions return IQueryable<T> where this conquerable is routed to DbSet<T>
OData controller does all the magic when OData query is passed. But how in this case we could hide the properties we don't want to display in GET, if the DTO model is same as Entity model.
Is there a way to do this via attributes (model binding?) or this case requires different entities for POST vs GET? But even if I do create separate entity for GET I still need to have Key fields because ODAta uses these attributes to build its relations.
Regarding DTOs, the OData Model IS a DTO mapping for ALL of the types that you expose. Unless your DTOs significantly change the structure of the data then it is not necessary to use other ORM tools like AutoMapper or to manually map the OData queries and Model artifacts to their EF counterparts, or to expose your own DTOs for business entities. You are expected to do this in the OData Model definition itself.
DTOs are useful for complex queries where you want to present an entirely different schema to the end client that might be better suited to a specific process or user experience, but not required for CRUD data entry management.
How to prevent Over-Posting in OData
If certain fields are not required by the client at all, then we can remove these fields from the OData Schema altogether, effectively creating a DTO definition that omits these properties.
The first principals approach is to deliberately exclude the specific fields using the OData Fluent Configuration API after you have registered the types and bound them to
EntitySets:Unfortunately, in .Net FX these
Ignoremethods are not chainable... We also cannot useNotMappedAttributeas this would specifically remove this field from the EF context and therefor the database.As detailed in this post you can use
IgnoreDataMemberAttributeon the properties to explicitly exclude them from the OData Model if you are using theODataConventionModelBuilder. This is how that would look:How to prevent Under-Posting
As with MVC implementations, the
RequiredAttributecan be used to require that specific fields are included in a POST/PATCH against a specific entity. But it will only be automatically applied if theODataConventionModelBuilderis used, otherwise you will need to set this explicitly:If you are using the
ODataConventionModelBuilderyou can annotate the model directly:Conditional Logic
You can also explicitly validate Under or Over post constraints in the PATCH handler in your controller implementation. This can be useful for implementing discretionary based validation logic, perhaps dependent on security rules or roles assigned to the user. It is also useful if you want the internal values to be utilised in the client for some operations, perhaps to be readonly, or you want to only allow certain fields to be updated through a specific endpoint that has other criteria or security constraints of its own.
This example is more verbose than many, but it is important to showcase the standard implementations, rather than only part of it, as is common in documentation. I hope this drives follow up questions ;)
More on DTOs
We sometimes see examples on-line of OData implementations that use tools like AutoMapper instead of properly configuring the OData Model. Sometimes this is simply due to the configuration logic being sometimes cumbersome in comparison to AutoMapper. Other instances are examples of API (or developers) that have transitioned from a previous business domain repository that already implemented AutoMapper. Sometimes it is simply that the functionality is not well understood.
The ODataConventionModelBuilder was not designed to allow developers the opportunity to pick and choose the specific conventions that are implemented, nor does it allow you to add your own custom conventions OOTB. That has made it harder for the community to adapt to configuration in this space and due to under-representation in production workloads and therefore support queries, there is minimal reference to this in the documentation provided.
If you do manually map your DTOs to the EF context, then the OData Model DTOs will represent a 1:1 mapping of your internal DTOs to the external callers. There is a negligible performance impact to this, but it is an additional layer of configuration and therefore management that in many cases is not required for an OData API.
If Over/Under Post is not a concern
If the main driver for a DTO is performance then OData solution is to implement a
$selectquery parameter to only return the fields that you need, we do not need to implement a specific DTO for this.When the calling client knows the specific fields that it needs to display, then it is appropriate to allow the client to manage the
$select, in this case$select=Name,SerialNumber,ReferenceNumberYou can also manage the default value of
$selectto apply to all queries for each type where the client does NOT specify a$select. However, if you do this, your client will need to explicitly call$select=*to ensure that all fields are returned, or specifically include the key fields, in this caseIdif you do not already have that information.