DTO and Projections in WebAPI

2019-05-23 01:20发布

问题:

WebAPI supports OData, so API consumer can specify fields that he requires, and it works fine. But is there a way to use DTO objects and projections in WebAPI?

For example I have

public class WebSite
{
    public string Url {get;set;}
    public string Author {get;set;}
    public string Technology {get;set;}
    public DateTime CreatedAt {get;set;}
    // 20 more different properties
}

And i also have DTO object:

public class WebSiteDTO
{
    public string Url {get;set;}
    public string Author {get;set;}

    public bool IsDotNet {get;set;} // it should be set during mapping as webSite.Technology == ".Net";
    public bool IsTrendThing {get;set;} // should be set as  webSite.Technology == ".Net" and webSite.CreatedAt > new DateTime(2014,0,0);
}

And some typical WebAPI endpoint that supports OData:

[HttpGet]
[Route("Test")]
public IQueryable Test(ODataQueryOptions<WebSiteDTO> options)
{
    var efDbContext = new MyDBContext();
    var query = efDbContext.WebSites;
    var odataQuery = options.ApplyTo(query, settings);
    return odataQuery;
}

In that case WebSite object will be returned. But how to return WebSiteDTO object and still have OData support? Is it possible to do mapping by property and not by class itself? Like if Url was requested via OData, then we will load just Url from DB and map it to Url property in DTO object? I may have complicated cases when properties in DTO must be set by some custom logic like in IsDotNet sample, or it may depend on more than one property.

I suppose i can write some custom middleware that will execute original query with fields specified by OData, and then save it in Dictionary and then do something like that:

MyMapper.Map<WebSite, WebSiteDTO>().
    MapProperty(o, dict => o.Url = (string)dict["Url"]).
    MapProperty(o, dict => o.IsDotNet = (string)dict["Technology"] == ".Net").
    MapProperty(o, dict => o.IsTrendThing = (string)dict["Technology"] == ".Net" && (DateTime)dict["CreatedAt"] > new DateTime(2014,0,0));

But it looks ugly and in that case somehow i need to specify that if IsTrendThing property was requested in OData request, then I also need to load Technology and CreatedAt fields from WebSite object, that makes things complicated.

Is there something that can be usefull in my case? Maybe someone can help me with correct direction?

Automapper has projections mapping but it doesn't work in my scenario, as it requires specific type, and in my case original type can be different because of fields specified by OData.

There is another similar question but it was about pure DTO, and in my case i want to support 'select\expand' operators from OData as my objects can have a lot of properties. And i want to not load them from DB if they weren't requested.

回答1:

You should do the projection before applying the ODataQueryOptions. The ODataQueryOptions can be applied to properties which exist in WebSiteDto.

var date = new DateTime(2014,0,0);
// this will not load your data
var query = efDbContext.WebSites.Select(w => new WebSiteDto() 
{
    /* projection code */
    IsDotNet = w.Technology == ".Net",
    IsTrendThing = w.Technology == ".Net" && w.CreatedAt > date,
    Url = w.Url,
    Author = w.Author
});

// this will still not load your data but will be applied on your projected object
var odataQuery = options.ApplyTo(query, settings);

The full query (with options applied) will only be executed when the controller action returns. You can do any projection code which works with LinqToEntities inside the select method (Avoid calling methods like Equals and use == instead or calling any constructor). You don't even load Technology or CreatedAt, the query is executed on SQL server and you get back only boolean values.

If the client will perform additional $select it will be placed on top of this query, so the unnecessary data will never reach your application server from SQL Server (just like applying another .Select() on your IQueryable)

I am not sure how AutoMapper deals with IQueryable (if it forces evaluation of the query or not), but the Select method will not evaluate your query, will just build on it so that it can be sent to the SQL Server.