I have a simple Web API endpoint which can accept incoming OData queries:
public IActionResult GetProducts(ODataQueryOptions<ProductDTO> options)
{
var results = DomainLayer.GetProducts(options);
return Ok(results);
}
I specifically want to be able to query against ProductDTO
objects and to be able to filter or sort against the properties of the DTO representation.
My design issue is that I want to take advantage of the filter parsing/applying logic of the OData library but I don't want to expose my database-bound ProductEntity
objects to my Web API AND I do not want to return an IQueryable
from my DataAccessLayer
, only IEnumerable
s.
What I am trying to do then is to extract the Expression
from the FilterQueryOption
property of the incoming ODataQueryOptions
so I can use AutoMapper's Expression Mapping feature to map the expression from a Expression<Func<ProductDTO, bool>>
to a Expression<Func<Product, bool>>
then finally to a Expression<Func<ProductEntity, bool>>
where I will then pass it into a .Where()
call on my Table<ProductEntity>
where (hopefully) the filter is applied in my SQL database (via Linq-2-SQL) and then I just convert it all the way back to a DTO object after.
The big showstopper I came across is that queryable.Expression
is returning a MethodCallExpression
rather than a Expression<Func<ProductDTO, bool>>
like I expected, which means I can't map the expression with AutoMapper like I had planned...
How can I get around this?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.AspNet.OData.Query;
using AutoMapper.Extensions.ExpressionMapping;
using AutoMapper.QueryableExtensions;
namespace ProductApp
{
public class DomainLayer
{
public IEnumerable<ProductDTO> GetProductsByEntityOptions(ODataQueryOptions<ProductDTO> options)
{
var mapper = MyMapper.GetMapper();
// This is the trick to get the expression out of the FilterQueryOption...
IQueryable queryable = Enumerable.Empty<ProductDTO>().AsQueryable();
queryable = options.Filter.ApplyTo(queryable, new ODataQuerySettings());
var exp = (MethodCallExpression) queryable.Expression; // <-- This comes back as a MethodCallExpression...
// Map the expression to my intermediate Product object type
var mappedExp = mapper.Map<Expression<Func<Product, bool>>>(exp); // <-- But I want it as a Expression<Func<ProductDTO, bool>> so I can map it...
IEnumerable<Product> results = _dataAccessLayer.GetProducts(mappedExp);
return mapper.Map<IEnumerable<ProductDTO>>(results);
}
}
public class DataAccessLayer
{
public IEnumerable<Product> GetProducts(Expression<Func<Product, bool>> exp)
{
var mapper = MyMapper.GetMapper();
var mappedExp = mapper.Map<Expression<Func<ProductEntity, bool>>>(exp);
IEnumerable<ProductEntity> result = _dataContext.GetTable<ProductEntity>().Where(mappedExpression).ToList();
return mapper.Map<IEnumerable<Product>>(result);
}
}
}
References:
- Where I found the trick to get the Expression out of the filter: https://stackoverflow.com/a/16447514/1504964
- A related GitHub Issue: https://github.com/OData/WebApi/issues/33