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:

  1. We either have a (breaking) change on the ODATA REST API
  2. 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.

3 Comments »

  1. 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.

Leave a Reply to Marc Rufer Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.