I\'ve got a simple WebApi method like this decorated with the OData queryable attribute.
[Queryable]
public virtual IQueryable Get()
Use the AutoMapper's Queryable Extensions.
First, define the mapping.
// Old AutoMapper API
// Mapper.CreateMap<Person, PersonDto>();
// Current AutoMapper API
Mapper.Initialize(cfg =>
cfg.CreateMap<Person, PersonDto>()
);
Then you can use something like this:
[EnableQuery]
public IQueryable<PersonDto> Get() {
// Old AutoMapper API
// return this.dbContext.Persons.Project().To<PersonDto>();
// New AutoMapper API
return this.dbContext.Persons.ProjectTo<PersonDto>();
}
Edit 04/2019: Updated to reflect current AutoMapper API.
IMHO the accepted solution is not correct. Generally speaking, if your service is using DTOs, you don't want to expose the underlying Entities (Person) to the service. Why would you query against the Person
model and return PersonDTO
objects?
Since you're already using it, Automapper has Queryable Extensions which allows you to expose only your DTOs and have the filtering applied to the underlying type at the data source. For example:
public IQueryable<PersonDto> Get(ODataQueryOptions<PersonDto> options) {
Mapper.CreateMap<Person, PersonDto>();
var persons = _personRepository.GetPersonsAsQueryable();
var personsDTOs = persons.Project().To<PersonDto>(); // magic happens here...
return options.ApplyTo(personsDTOs);
}
Regarding eagerly loading navigation properties...
@philreed: I couldn't put a decent response in the comment so I added it here. There was a post on how to do this here but I'm getting 403s today. Hopefully that's temporary.
Basically, you examine the Select and Expand clauses for your navigation property. If it is present, then you tell EF to eagerly load via IQueryable<T> Include
extension method.
Controller
public IQueryable<MyDto> GetMyDtos(ODataQueryOptions<MyDto> options)
{
var eagerlyLoad = options.IsNavigationPropertyExpected(t => t.MyNavProperty);
var queryable = _myDtoService.GetMyDtos(eagerlyLoad);
// _myDtoService will eagerly load to prevent select N+1 problems
// return (eagerlyLoad) ? efResults.Include(t => t.MyNavProperty) : efResults;
return queryable;
}
Extension method
public static class ODataQueryOptionsExtensions
{
public static bool IsNavigationPropertyExpected<TSource, TKey>(this ODataQueryOptions<TSource> source, Expression<Func<TSource, TKey>> keySelector)
{
if (source == null) { throw new ArgumentNullException("source"); }
if (keySelector == null) { throw new ArgumentNullException("keySelector"); }
var returnValue = false;
var propertyName = (keySelector.Body as MemberExpression ?? ((UnaryExpression)keySelector.Body).Operand as MemberExpression).Member.Name;
var expandProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawExpand) ? new List<string>().ToArray() : source.SelectExpand.RawExpand.Split(',');
var selectProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawSelect) ? new List<string>().ToArray() : source.SelectExpand.RawSelect.Split(',');
returnValue = returnValue ^ expandProperties.Contains<string>(propertyName);
returnValue = returnValue ^ selectProperties.Contains<string>(propertyName);
return returnValue;
}
}
I have couple of observations/comments on the proposed solution 1. If Person is used in the query context and PersonDTO is returned then whole concept of MVVM goes away. I guess it becomes tightly coupled. I would rather just use Person all the way or if you are not looking to return child records then in my opinion Automapper has an Extension "Project", try using that. 2. With this approach the $inlinepagecount query option will not function as intended because it will get the count of PersonDTO instead of Person. (Well one way to get around that problem would be to use PageResult and set the properties yourself.) Regards.
There is a better solution. Try this:
public virtual IQueryable<PersonDto> Get(ODataQueryOptions<Person> query)
{
var people = query.ApplyTo(uow.Person().GetAll());
return ConvertToDtos(people);
}
This will make sure the query runs on Person instead of PersonDTO. If you want the conversion to happen through an attribute instead of in code, you'll still want to implement an action filter similar to what you put up.