How to expand sub levels of childs with AutoMapper and OData?

asp.net automapper c# entity-framework-6 odata

Question

I'm currently working on a web service project in asp.net. And I try to include child elements in my function that retrieves the data.

It works with the url :

http://localhost:8080/myEntity/?$expand=myChildElement

But I can't seem to include the child elements of a level below like this :

http://localhost:8080/myEntity/?$expand=myChildElement/mySubChildElement

Here is my function :

public virtual async Task<IHttpActionResult> GetElements(ODataQueryOptions<TEntity> queryOptions)
{
    IQueryable<TPoco> query = context.Set<TPoco>();

    string[] includes = null;
    string includeText = queryOptions.SelectExpand != null ? queryOptions.SelectExpand.RawExpand : null;
    if(! string.IsNullOrEmpty(includeText))
    {
        includes = queryOptions.SelectExpand.RawExpand.Split(',');
        return Ok(await query.ProjectTo<TEntity>(null, includes).ToListAsync());
    }

    return Ok(await query.ProjectTo<TEntity>().ToListAsync());
}

And here is an example of my TPoco model (myChildElement can be the entity "Project" and mySubChildElement the child entity "ProjectType") :

public class ContractEntity : BaseEntity
{
    public ContractEntity()
    {
        this.Addresses = new HashSet<AddressEntity>();
    }

    override public Guid Id { get; set; }
    override public string DisplayName { get { return No.ToString(); } set { } }
    override public string ClassType { get { return "Contract"; } set { } }
    override public string MarkerColor { get { return Colors.Green.ToString(); } set { } }
    public int? No { get; set; }
    public DateTime? OfficialDate { get; set; }
    public DateTime? ValidDate { get; set; }
    public DateTime? SignatureDate { get; set; }
    public Guid? ProjectId { get; set; }
    [...]

    public virtual ICollection<AccountingItemEntity> AccountingItems { get; set; }
    public virtual ICollection<TagEntity> Tags { get; set; }
    public virtual ProjectEntity Project { get; set; }
    [...]
}

I hope you can help me.

1
2
2/22/2017 3:47:54 PM

Accepted Answer

The way I do this, I use the fact that .ProjectTo returns an IQueryable so I let the OData framework handle querying by not calling ToListAsync() myself, but returning the IQueryable.

// In some class that is ': ODataController'
[EnableQuery] //Attribute makes sure the OData framework handles querying
public IQueryable<TDto> Get(ODataQueryOptions<TDto> options) {
    //The names of the properties requested by the client in the '$expand=' query option
    string[] requestedExpands = Helpers.Expand.GetMembersToExpandNames(options); // I made a helper for this, you can probably just use your code, or see my impementation below.

    var set = Db.Set<TEntity>().AsNoTracking(); //You might want to remove AsNoTracking if it doesn't work
    var converted = set.ProjectTo<TDto>(MapConfig, null, requestedExpands);
    return converted;
}

In this example TDto is the type I want to sent to the client TEntity is the Entity Framework POCO class.

My MapConfig I setup on application startup, here I set expandable properties to 'Explicit expand' mode:

MapConfig = new MapperConfiguration(cfg => {
    cfg.CreateMap<EFType, DTOType>(MemberList.Destination)
        .ForMember(c => c.ExpandableProperty, options => options.ExplicitExpansion());
});

As I said in comments, I request the data by nesting $expand= in the URL:

api/myEntity?$expand=myChildElement($expan‌​d=mySubChildElement)

This works for me, hope you can replicate that success.

EDIT: Looked at it again, if you want to expand myEntity.myChildElement.mySubChildElement the string[] 'requestedExpands' you pass to AutoMapper must contain 1 entry: myChildElement.mySubChildElement. And I have a mapping defined for all 3 entities, again with the ExplicitExpansion option.


Update, as requested by @Tim Pohlmann, my implementation of GetMembersToExpandNames:

public static class Expand {

    public static string[] GetMembersToExpandNames(ODataQueryOptions options) {
        return GetExpandPropertyPaths(GetExpandItems(options?.SelectExpand?.SelectExpandClause)).ToArray();
    }

    private static IEnumerable<string> GetExpandPropertyPaths(IEnumerable<ExpandedNavigationSelectItem> items, string prefix = "") {
        foreach(var item in items) {
            foreach(var res in GetExpandPropertyPaths(item, prefix)) {
                yield return res;
            }
        }
    }

    private static IEnumerable<string> GetExpandPropertyPaths(ExpandedNavigationSelectItem item, string prefix = "") {
        var curPropName = item.NavigationSource.Name;
        var nestedExpand = GetExpandItems(item.SelectAndExpand).ToArray();
        if(nestedExpand.Count() > 0) {
            foreach(var res in GetExpandPropertyPaths(nestedExpand, $"{prefix}{curPropName}.")) {
                yield return res;
            }
        } else {
            yield return $"{prefix}{curPropName}";
        }
    }

    private static IEnumerable<ExpandedNavigationSelectItem> GetExpandItems(SelectExpandClause sec) {
        if(sec != null) {
            var res = sec?.SelectedItems?.OfType<ExpandedNavigationSelectItem>();
            if(res != null) {
                return res;
            }
        }
        return new ExpandedNavigationSelectItem[0];
    }
}
3
9/12/2018 9:40:32 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow