Mapping between Database Types and Client Types in the .NET Backend using AutoMapper

Most of the samples showing how to use the .NET Backend have a single type that represents both the data stored in the database and the data that is exposed to the service callers. However, sometimes you cannot expose your database data as-is, or you want to provide and accept extra data to callers that should not be in the database. The .NET backend is strongly-typed, so in those situations, you must map your data between the database type and the client type. For example:

  • You have extra information in your database table that you do not want to expose publicly, such as a Salary field.
  • You have an existing database table that you want to expose through Mobile Services, but the table's shape in "wrong" in some way (the Id property is not a string, missing some of the members needed by ITableData, etc.)
  • You want the expose the information from a database columns in a different way, such as expose a start date as a string (like "May 2013") instead of as a DateTimeOffset object.

The .NET Backend was built with this mapping in mind, and has first-class support for AutoMapper through the MappedEntityDomainManager type. The basic steps for getting started are:

  1. For each database table type you want to map, create a new class that will represent the data exposed to client.
  2. Initialize AutoMapper in WebApiConfig with your description of the mappings between your types.
  3. Create a simple mapped domain manager derived from MappedEntityDomainManager (which uses AutoMapper) specific for your mapped types.
  4. Update your table controllers to use the type that is exposed to the client, and set the DomainManager property to your mapped domain manager.

In this post, I'll use the Mobile Service starter Todo project as a starting point. You can download from the QuickStart tab in the Azure Mobile Service portal. In the default project, the TodoItem type defines what is stored in the database and what is exposed to users of the service.

 public class TodoItem : EntityData
{
    public string Text { get; set; }
    public bool Complete { get; set; }
}

Going forward, this will be the type that describes only the shape of the data in the database.

  1. Create a new class, TodoItemDto, that will represent the data exposed to client, which has the same shape as TodoItem,

     public class TodoItemDto : EntityData
    {
        public string Text { get; set; }
        public bool Complete { get; set; }
    }
    
  2. Need to define a mapping in App_Start\WebApiConfig.cs. In the line following the one where config is defined, add this:

     AutoMapper.Mapper.Initialize(cfg =>
    {
        // Define a map from the database type TodoItem to 
        // client type TodoItemDto. Used when getting data.
        cfg.CreateMap<TodoItem, TodoItemDto>();
        // Define a map from the client type to the database
        // type. Used when inserting and updating data.
        cfg.CreateMap<TodoItemDto, TodoItem>();
    });
    

    By default, the AutoMapper's CreateMap function includes mappings between properties with the same name and type.

    Note: If you get an error The name 'AutoMapper' does not exist in the current context, then add references to AutoMapper.dll and AutoMapper.Net4.dll from \packages\AutoMapper.3.1.1\lib\net40. This was an issue with older quickstarts, but it should be fixed soon.

  3. Create a non-abstract domain manager that maps between the TodoItem and TodoItemDto. (The MappedEntityDomainManager knows how to use the mapping, but it is abstract and does not know what the key is for the database type.)

     public class SimpleMappedEntityDomainManager
    : MappedEntityDomainManager<TodoItemDto, TodoItem>
    {
        public SimpleMappedEntityDomainManager(DbContext context,
            HttpRequestMessage request, ApiServices services)
            : base(context, request, services)
        { }
        public override SingleResult<TodoItemDto> Lookup(string id)
        {
            return this.LookupEntity(p => p.Id == id);
        }
        public override Task<TodoItemDto> UpdateAsync(string id, Delta<TodoItemDto> patch)
        {
            return this.UpdateEntityAsync(patch, id);
        }
        public override Task<bool> DeleteAsync(string id)
        {
            return this.DeleteItemAsync(id);
        }
    }
    
  4. In the TodoItemController replace all instances of TodoItem type with TodoItemDto. The assignment of DomainManager needs to be updated, because TodoItemDto is not part of the model. Replace with the SimpleMappedEntityDomainManager:

     public class TodoItemController : TableController<TodoItemDto>
    {
        protected override void Initialize(HttpControllerContext controllerContext)
        {
            base.Initialize(controllerContext);
            yourContext context = new yourContext();
            DomainManager = new SimpleMappedEntityDomainManager(context, Request, Services);
        }
        public IQueryable<TodoItemDto> GetAllTodoItems()
        {
            return Query();
        }
        public SingleResult<TodoItemDto> GetTodoItem(string id)
        {
            return Lookup(id);
        }
        public Task<TodoItemDto> PatchTodoItem(string id, Delta<TodoItemDto> patch)
        {
            return UpdateAsync(id, patch);
        }
        public async Task<IHttpActionResult> PostTodoItem(TodoItemDto item)
        {
            TodoItemDto current = await InsertAsync(item);
            return CreatedAtRoute("Tables", new { id = current.Id }, current);
        }
        public Task DeleteTodoItem(string id)
        {
            return DeleteAsync(id);
        }
    }    
    

Start your service project locally. Click the try it out link. Click GET tables/TodoItem, then click the green try this out button, then click the send button. You will see the same data that you started with.

Untitled2

Now that we have the basics set up, let's add an extra property to be exposed to clients of the service!

  1. Add a new Special property to TodoItemDto.

     public class TodoItemDto : EntityData
    {
        public string Text { get; set; }
        public bool Complete { get; set; }
        public bool Special { get; set; }
    }
    
  2. We need to update the mapping in App_Start\WebApiConfig.cs. We use the ForMember function to say that when mapping to TodoItemDto.Special, use the value true:

     AutoMapper.Mapper.Initialize(cfg =>
    {
        // Define a map from the database type TodoItem to 
        // client type TodoItemDto. Used when getting data.
        cfg.CreateMap<TodoItem, TodoItemDto>()
             .ForMember(todoItemDto => todoItemDto.Special,
                        map => map.UseValue(true));
        // Define a map from the client type to the database
        // type. Used when inserting and updating data.
        cfg.CreateMap<TodoItemDto, TodoItem>();
    });
    
  3. Start your service project locally. Click the try it out link. Click GET tables/TodoItem, then click the green try this out button, then click thesend button. You will see the same data that you started with, along with the new special property set to true.

We can also prevent data from the database from getting to the customer, or change the names of the properties. For example, we might decide that the Text is too sensitive for end-users to access, and that Completed really should be IsDone. Those are easy changes to make in the mapping layer, without altering what is in the database.

  1. Delete the Text property and rename Complete to IsDone.

     public class TodoItemDto : EntityData
    {
        // public string Text { get; set; }
        // public bool Complete { get; set; }
        public bool IsDone { get; set; }
        public bool Special { get; set; }
    }
    
  2. We need to update the mapping in App_Start\WebApiConfig.cs. We use the ForMember function to declare that TodoItem.Complete should map to TodoItemDto.IsDone, and vice versa. We don't need to say what to do with TodoItem.Text because there is noTodoItemDto.Text property, and AutoMapper's convention is to do nothing in that case.

     AutoMapper.Mapper.Initialize(cfg =>
    {
        // Define a map from the database type TodoItem to 
        // client type TodoItemDto. Used when getting data.
        cfg.CreateMap<TodoItem, TodoItemDto>()
            .ForMember(todoItemDto => todoItemDto.Special,
                       map => map.UseValue(true))
            .ForMember(todoItemDto => todoItemDto.IsDone,
                       map => map.MapFrom(todoItem => todoItem.Complete));
        // Define a map from the client type to the database
        // type. Used when inserting and updating data.
        cfg.CreateMap<TodoItemDto, TodoItem>()
            .ForMember(todoItem => todoItem.Complete,
                        map => map.MapFrom(todoItemDto => todoItemDto.IsDone));
    });
    
  3. Start your service project locally. Click the try it out link. Click GET tables/TodoItem, then click the green try this out button, then click thesend button. You will see that text does not appear, and isDone shows up in place of complete.

This post just scratches the surface of how AutoMapper can be used. For more information on AutoMapper, see https://github.com/AutoMapper/AutoMapper/wiki/Getting-started.