Normally when creating ODATA Controllers the external Model (such as Customer
) pretty much maps to the internal database backed model when you are using an ORM such as EntityFramework.
This can become problematik when you want to change the model / data format:
- We either have a (breaking) change on the ODATA REST API
- Or we have a database schema change
A typical Get
method for retrieving single entities could look similar to this:
public abstract class OdataControllerBase<TEntity> : OdataControllerBase where TEntity : PublicEntity { private dbContext = new DbContext(); [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Expand | AllowedQueryOptions.OrderBy)] public virtual IHttpActionResult GetEntity([FromODataUri] long key, ODataQueryOptions<TEntity> queryOptions) { queryOptions.Validate(_validationSettings); var entity = dbContext.Set<TEntity>().FirstOrDefault(e => e.Id == id); return Ok(entity); } }
And a simple model for our customer could then look like this:
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 what if we needed the properties like RowVersion
or CreatedById
only internally and did not want to expose them via the API? We would need a model that we could use on the persistence layer:
public class CustomerDataAccess { [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; } } 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; } }
For this to work we could adjust our ODATA controller like this:
public abstract class OdataControllerBase<TEntity, DataAccessEntity> : OdataControllerBase where TEntity : PublicEntity where TEntity : DataAccessEntity { private dbContext = new DbContext(); [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Expand | AllowedQueryOptions.OrderBy)] public virtual IHttpActionResult GetEntity([FromODataUri] long key, ODataQueryOptions<TEntity> queryOptions) { queryOptions.Validate(_validationSettings); var entity = dbContext.Set<DataAccessEntity>().FirstOrDefault(e => e.Id == id); Mapper.Map<TEntity(entity) return Ok(entity); } } public class EntityAutoMapperProfile : Profile { public EntityAutoMapperProfile() { CreateMap<CustomerDataAccess, Customer>() .ReverseMap(); } }
This is all it takes to have more flexible ODATA controllers that are not so tightly coupled to the persistence layer.
Hey Ronald, wouldn’t something like:
var query = dbContext.DataAccessEntity.ProjectTo()
return Ok(query);
(using AutoMapper’s queryable extensions) be better? The .FirstOrDefault() will immediately enumerate the results against the original EF model, which is then mapped to TEntity in-memory. If you return the OData model’s expression tree directly (as an IQueryable), it should query only exactly the data the client needs, after all the OData filters have been applied.
Hi @aschuell,
you could also use queryable extensions of AutoMapper instead of Mapper.Map. In case of querying an entity by Id as done so in the example it doesn’t really make a difference as not more than one entry will be loaded when queried by Id (primary key).
Usage of queryable extensions in this case would look as follows:
dbContext.Set().ProjectTo().FirstOrDefault(e => e.Id == id);
In the ODataControllers Get method that returns a list of entries (i.e. GetBooks) the usage of AutoMappers queryable extensions would be definitely the better approach over using Mapper.Map. It would avoid enumeration of all entries followed by a in memory mapping of them before applying OData filters.
However be aware of the fact, that AutoMapper queries/expands all related entities (navigation properties) by default when performing ProjectTo() even if they are not returned via OData endpoint. According to documentation there is a parameter in ProjectTo that lets us handle the expansion but unfortunately it seemed that it does not work properly when we tried it out.