Separating your ODATA Models from the Persistence Layer with AutoMapper

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.

Trackbacks

  1. […] a way that not all ODATA navigation properties map one to one to the data model (as described in Separating your ODATA Models from the Persistence Layer with AutoMapper and More Fun with your ODATA Models and AutoMapper). In addition, we want to implement and enforce […]

Leave a 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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: