Tyler Brown
Tyler Brown

Reputation: 427

Mapping an IQueryable to domain model for OData in .NET

I recently implemented OData in my ASP .NET Core web API. I have found success as long as I am returning the database models directly. I run into trouble, however, as soon as I attempt to return domain models instead.

The underlying issue involves mapping a data class to a domain class while maintaining the IQueryable return type. While I have found partial success using AutoMapper's MapTo extension method, I find that I am unsuccessful when using the $extend method to expand a collection of entities that are also domain objects.

I have created a sample project to illustrate this issue. You may view or download the full project on github here. See the description below.

Given the following two database classes:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    public ICollection<Order> Orders { get; set; }

    public Product() {
        Orders = new Collection<Order>();
    }
}

public class Order
{
    public int Id { get; set; }
    public Double Price { get; set; }  
    public DateTime OrderDate { get; set; }

    [Required]
    public int ProductId { get; set; }
    public Product Product { get; set; }    
}

And the following domain models...

public class ProductEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    public ICollection<OrderEntity> Orders { get; set; }
}

public class OrderEntity
{
    public int Id { get; set; }
    public Double Price { get; set; }
    public DateTime OrderDate { get; set; }

    [Required]
    public int ProductId { get; set; }
    public Product Product { get; set; }
}

And the Products Controller

public class ProductsController
{
    private readonly SalesContext context;

    public ProductsController(SalesContext context) {
        this.context = context;
    }   

    [EnableQuery]
    public IQueryable<ProductEntity> Get() {
        return context.Products
            .ProjectTo<ProductEntity>()
            .AsQueryable();
    }
}

All the following OData queries Pass:

The following query, however, does not pass:

An HTTP response is never returned. The only failure message I get comes from the console:

System.InvalidOperationException: Sequence contains no matching element at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source, Func`2 predicate)

Finally, here is a reference to the mapping profile:

    public static class MappingProfile
{
    public static void RegisterMappings() {
        Mapper.Initialize(cfg =>
        {
           cfg.CreateMap<Order, OrderEntity>();
           cfg.CreateMap<Product, ProductEntity>();
        });
    }
}

I can solve the issue by simply returning a List instead of an IEnumerable in the controller, but this of course would trigger a large query against the database that would be performance intensive.

As stated above, you can find a link to the full project on Github here. Let me know if you find any answers!

Upvotes: 5

Views: 988

Answers (1)

Parrish Husband
Parrish Husband

Reputation: 3178

I was able to get this working with a few small revisions.

Updating the domain models:

public class ProductEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    public ICollection<Order> Orders { get; set; }
}

public class OrderEntity
{
    public int Id { get; set; }
    public double Price { get; set; }
    public DateTime OrderDate { get; set; }

    [Required]
    public int ProductId { get; set; }
    public Product Product { get; set; }
}

Manually enabling expansion on the route builder:

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, SalesModelBuilder modelBuilder)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseMvc(routeBuilder =>
        {
            routeBuilder.Expand().Select();
            routeBuilder.MapODataServiceRoute("ODataRoutes", "odata",
                    modelBuilder.GetEdmModel(app.ApplicationServices));
        });
}

Using the following Queries:

Upvotes: 2

Related Questions