共用方式為


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.

Comments

  • Anonymous
    May 19, 2014
    Thank you, that's I was looking for last days. Now I can understand how to I use DTOs.
  • Anonymous
    May 20, 2014
    Jason...this is great info!  Question.  I want to develop cross platform apps (Xamarin) so should I make my DTOs not inherit form EntityData..will that still work.  I essentially wanto to post DTOs (some may be complex) and return complex DTOs..which I think may require me to use a mix of standard API controllers and those that inherit from TableController.   Does that seem correct to you?
  • Anonymous
    May 20, 2014
    @Bob Lautenbach, currently the DTO types exposed by the table controllers must implement ITableData (which is implemented by EntityData), and ITableData is defined in a full-.NET DLL. So an assembly containing your DTOs cannot (currently) be used in a Xamarin project.We are thinking about how to could relax this restriction, but no concrete plans at this time.In the meantime, an approach your could use is to share your DTO source-code files between your projects.
  • Anonymous
    May 20, 2014
    Jason..Thanks.  I think that is the plan.  Appreciate the feedback.
  • Anonymous
    August 11, 2014
    I am attempting to map to tables with Id's that are Guid's.  I set up everything just like it is here but when it try's to map between the Id column of my table (guid) and the Id of the ITableData (string) and do any queries it says there is no mapping for string/Guid so I tried to add it to automapper which caused other errors by changing the maps for string.Is there any special steps you need to do when you want to use Guid as your primary key?
  • Anonymous
    August 12, 2014
    @BryanOriginal, the best place to start is to look at blogs.msdn.com/.../tables-with-integer-keys-and-the-net-backend.aspx (which I updated today to address some issues with it).You would use DB and DTO types like this:   public class Customer {       public Guid CustomerId { get; set; }       public string Name { get; set; }   }   public class CustomerDto : EntityData {       public string Name { get; set; }   }Then have a mapping like this:cfg.CreateMap<CustomerDto, Customer>().ForMember(dst => dst.CustomerId, map => map.MapFrom(src => Guid.Parse(src.Id)));cfg.CreateMap<Customer, CustomerDto>().ForMember(dst => dst.Id, map => map.MapFrom(src => src.CustomerId.ToString()));And in your controller, just add this to your initializer method:DomainManager = new SimpleMappedEntityDomainManager<CustomerDto, Customer>(context, this.Request, this.Services, p => p.CustomerId);And copy in the code for the SimpleMappedEntityDomainManager in the linked post.
  • Anonymous
    December 08, 2014
    When I try this and have a property on the Service Entity that doesn't exist on the DB Entity I get an exception as it appears to be looking for that property when doing the LINQ query. Presumably the SELECT aspect of the OData Query is being applied to the EF entities. I found a workaround that involves writing my own code in the Query method of my domain manager, but it seems from this article that it shouldn't be necessary. Did I miss some configuration step? Do I need to do something specific in the AutoMapper Configuration?Thanks!