How do I map an OData query against a DTO to another entity?

I found a solution without using AutoMapper.

The ProductsController now looks like this:

public class ProductsController : ApiController
{
    public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)
    {
        IQueryable<Product> products = this._products.AsQueryable();

        IEdmModel model = GetModel();
        IEdmType type = model.FindDeclaredType("TestAPI.Models.Product");
        IEdmNavigationSource source = model.FindDeclaredEntitySet("Products");
        ODataQueryOptionParser parser = new ODataQueryOptionParser(model, type, source, new Dictionary<string, string> { { "$filter", q.Filter.RawValue } });
        ODataQueryContext context = new ODataQueryContext(model, typeof(Product), q.Context.Path);
        FilterQueryOption filter = new FilterQueryOption(q.Filter.RawValue, context, parser);

        if (filter != null) products = filter.ApplyTo(products, new ODataQuerySettings()) as IQueryable<Product>;
        return products.Select(p => new ProductDTO(p));
    }
}

The WebApiConfig:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        …
        IEdmModel model = GetModel();
        config.MapODataServiceRoute("*", "*", model);
    }

    private static IEdmModel GetModel()
    {
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        EntitySetConfiguration<Product> product = builder.EntitySet<Product>("Products");
        product.EntityType.Name = "Product";
        product.EntityType.Namespace = "TestAPI.Models";
        product.EntityType.Property(p => p.Name).Name = "DisplayName";
        product.EntityType.Property(p => p.Level).Name = "DisplayLevel";
        return builder.GetEdmModel();
    }
}

If you decided you want to use DTOs (which is definitely a good idea in my opinion), then use it...
The $metadata should reflect the DTO's properties names and not of the EF entity, since this is what clients get and this is what the clients should send.
It means you should change the Get endpoint to something like this:

public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)

To avoid the coupling between ProductDTO and Product you can use AutoMapper to map between the classes for you. Also, if you use AutoMapper's Project method, you can cleanup you methods to somthing like:

public IQueryable<ProductDTO> Get(ProductDTO dto)

You can check Asp.net official demo for versioning, it heavily uses DTOs and AutoMapper, it will give you a good direction, just ignore the versioning if it doesn't interest you now.


Try using AutoMapper, you will need to add these references to your controller

using AutoMapper;
using AutoMapper.QueryableExtensions;

Your method

[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IQueryable<ObjectDTO> Get()
{
    return dbContext.Entities.ProjectTo<ObjectDTO>();
}

In your global

protected void Application_Start()
{
        //Usually in a diff class Mapping.ConfigureDataTransferObjects();
        Mapper.CreateMap<MyEntity, ObjectDTO>();
        Mapper.CreateMap<ObjectDTO, MyEntity>();
}