In our last post Separating your ODATA Models from the Persistence Layer with AutoMapper I showed how we can separate our public API Model from the internal persistence layer. In this post I will show how we can re-model the public API model without changing the internal data model.
In the example from the previous post I showed our model Customer
that contained a few properties regarding the creation and modification of the entity:
public class Customer { [Key] public long Id { get; set; } [Required] public string Name { get; set; } public string Description { get; set; } [Required] public string ContractId { get; set; } [Required] public long CreatedById { get; set; } [ForeignKey(nameof(ModifiedById))] public User ModifiedBy { get; set; } [Required] public long ModifiedById { get; set; } [ForeignKey(nameof(ModifiedById))] public User ModifiedBy { get; set; } [Required] public DateTimeOffset Created { get; set; } [Required] public DateTimeOffset Modified { get; set; } [Timestamp] public byte[] RowVersion { get; set; } }
However it seems that most of the information is not really necessary or useful for the caller. Therefore I would like to hide these properties in a normal response and only reveal them upon sepcific request via an ODATA $expand
option.
This is possible with the help of AutoMapper. We just have to refactor our model to this:
public class EntityDetails { [Required] public long CreatedById { get; set; } [Required] public long ModifiedById { get; set; } [Required] public DateTimeOffset Created { get; set; } [Required] public DateTimeOffset Modified { get; set; } [Timestamp] public byte[] RowVersion { get; set; } } public class Customer { [Key] public long Id { get; set; } [Required] public string Name { get; set; } public string Description { get; set; } [Required] public string ContractId { get; set; } public EntityDetails Details { get; set; } }
Now our model contains a subclass with the required information. In order to map the information from the underlying data access model we have to extend our mapping profile (from the previous post) to this:
public class EntityAutoMapperProfile : Profile { public EntityAutoMapperProfile() { CreateMap<CustomerDataAccess, Customer>() .ForMember(dest => dest.Details, opt => opt.MapFrom(src => new EntityDetails { CreatedById = src.CreatedById, ModifiedById = src.ModifiedById, Created = src.Created, Modified = src.Modified, RowVersion = src.RowVersion, })) .ReverseMap(); } }
With this mapping we transform the separate properties from the data access entity to the EntityDetails
class (we just have to make sure that we use a Lambda Expression
).
The only thing that is missing is to tell ODATA that EntityDetails
is a known entity, otherwise we will get an error similar to this:
message=No NavigationLink factory was found for the navigation property 'Details' from entity type 'Net.Appclusive.Public.Domain.EntityDetails' on entity set 'Customers'. Try calling HasNavigationPropertyLink on the EntitySetConfiguration. Parameter name: navigationProperty
For more information on this error have a look at ODataController throws ‘No NavigationLink factory was found’ when returning a different type in IHttpActionResult.
void Initialise(System.Web.Http.OData.Builder.ODataConventionModelBuilder builder) { builder.ComplexType<EntityDetails>(); }
Note: if you use builder.EntitySet("EntityDetails")
the retrieval/return of these objects works as well, but we cannot use these objects to post/send them from the client to the server.
After adding the line above to our model builder we can query our entity set via Invoke-RestMethod 'http://appclusive/api/Core/Customers(1)?$expand=Details'
and get the following output:
{ "odata.metadata": "http://appclusive/api/Core/$metadata#Customers/@Element", "Id": "1", "Name": "Edgar Schnittenfittich", "Description": "Our first customer" "ContractId": "arbitrary-contract-id", "Details": { "CreatedById": "1", "ModifiedById": "1", "Created": "2016-08-15T05:42:08.01Z", "Modified": "2017-01-01T08:05:42.01Z", "RowVersion": "AAAAAAABvVY=" } }
And when querying the entity without any options it just returns:
{ "odata.metadata": "http://appclusive/api/Core/$metadata#Customers/@Element", "Id": "1", "Name": "Edgar Schnittenfittich", "Description": "Our first customer" "ContractId": "arbitrary-contract-id" }
With this approach we can now freely modify the layout of our public models without changing the internal database models and schema.
How would you do this with a self-referencing property?
I am not quite sure if I understand exactly what you mean. We have models where we use self referencing (to build a tree). I can post youi the code for this if you are interested.
I am not quite sure if I understand exactly what you mean. We have models where we use self referencing (to build a tree). I can post youi the code for this if you are interested.