Search code examples
.netasp.net-web-apiasp.net-coreodata

Dynamic EDM model creation in dotnet core


I've seen the example at https://github.com/OData/ODataSamples/tree/master/WebApi/v4/DynamicEdmModelCreation for dynamic Odata route and model generation.

I need the same for dot net core but since Asp.net and Asp.net core vary in terms of the classes/routing etc. I'm unable to translate this code to dot net core.

Can anybody provide some guidance ?

My Startup.cs Configure method has this -

        IEdmModel model = this.GetEdmModel(app.ApplicationServices);

        IList<IODataRoutingConvention> routingConventions = new List<IODataRoutingConvention>();
        routingConventions.Insert(0, new MatchAllRoutingConvention());
        app.UseMvc(routeBuilder => routeBuilder.MapODataServiceRoute("odata", "odata", model, new CustomPathHandler(), routingConventions));

CustomPathHandler.cs -

public class CustomPathHandler : DefaultODataPathHandler
{
    public override ODataPath Parse(string serviceRoot, string odataPath, IServiceProvider requestContainer)
    {
        var path =  base.Parse(serviceRoot, odataPath, requestContainer);
        return path;
    }

    public override string Link(ODataPath path)
    {
        return base.Link(path);
    }
}

ODataEndpointController.cs -

public class ODataEndpointController : ODataController
{
    [EnableQuery]
    [ODataRoute]
    public EdmEntityObjectCollection Get()
    {
        // Get entity set's EDM type: A collection type.            
        ODataPath path = Request.ODataFeature().Path;
        IEdmCollectionType collectionType = (IEdmCollectionType)path.EdmType;
        IEdmEntityTypeReference entityType = collectionType.ElementType.AsEntity();

        // Create an untyped collection with the EDM collection type.
        EdmEntityObjectCollection collection =
            new EdmEntityObjectCollection(new EdmCollectionTypeReference(collectionType));

        // Add untyped objects to collection.
        DataSourceProvider.Get(entityType, collection);

        return collection;
    }

MatchAllRoutingConvention.cs

public class MatchAllRoutingConvention : IODataRoutingConvention
{
    public IEnumerable<ControllerActionDescriptor> SelectAction(RouteContext routeContext)
    {
        ControllerActionDescriptor test = new ControllerActionDescriptor();
        test.ControllerName = "ODataEndpoint";
        return new List<ControllerActionDescriptor> { test }.AsEnumerable();

    }

}

Is there anything I'm doing wrong here ? When I try to hit http://localhost:port/odata/Products I get some source is null error

Edit (15-Jan):

Modifying the MatchAllRoutingConvention as below redirected the routing as required but so do any $metadata requests (which throw an exception). And $filter queries don't work either. So any pointers/tips would be helpful -

public class MatchAllRoutingConvention : IODataRoutingConvention
{
    public IEnumerable<ControllerActionDescriptor> SelectAction(RouteContext routeContext)
    {
        ControllerActionDescriptor odataControllerDescriptor = new ControllerActionDescriptor
            {
                ControllerName = "ODataEndpoint",
                ActionName = "Get",
                Parameters = new List<ParameterDescriptor>(),
                FilterDescriptors = new List<FilterDescriptor>(),
                BoundProperties = new List<ParameterDescriptor>(),
                MethodInfo = typeof(ODataEndpointController).GetMethod("Get"),
                ControllerTypeInfo = typeof(ODataEndpointController).GetTypeInfo()
            };

            return new List<ControllerActionDescriptor> { odataControllerDescriptor };

    }

}

Solution

  • Posting this as an answer since I haven't seen any responses and hopefully this would help someone. I modified the MatchAllRoutingConvention class as below -

    public class MatchAllRoutingConvention : IODataRoutingConvention
    {
        public IEnumerable<ControllerActionDescriptor> SelectAction(RouteContext routeContext)
        {
            if (routeContext.RouteData.Values["odataPath"] == null || 
                routeContext.RouteData.Values["odataPath"].ToString() == "$metadata")
                return new MetadataRoutingConvention().SelectAction(routeContext);
    
            IActionDescriptorCollectionProvider actionCollectionProvider =
                routeContext.HttpContext.RequestServices.GetRequiredService<IActionDescriptorCollectionProvider>();
    
            IEnumerable<ControllerActionDescriptor> actionDescriptors = actionCollectionProvider
                .ActionDescriptors.Items.OfType<ControllerActionDescriptor>()
                .Where(c => c.ControllerName == "ODataEndpoint");
    
            return actionDescriptors;
        }
    
    }
    

    This serves metadata requests correctly as well as simple entity requests. This doesn't however respond to $filter, $select etc. From what I understand the ODataQueryOptions class is used to apply OData filtering etc but they won't work on non-CLR classes which this is. So not sure if there is a solution for this problem.

    [Edit] So I've managed a sort of hack to get around this filtering issue. There is this library LinqToQueryString which you apply on an IQueryable, not EdmEntityObjectCollection obviously (it's not that easy).

    I first load my data into a Dictionary, there's an example on their site to do that. Apply the OData query options string on that dictionary first, Then I Load the filtered result into my entities and then into the EdmEntityObjectCollection

        var list = new List<IEdmEntityObject>();
        var dataList = new List<Dictionary<string, object>>();
    
        // Load data in Dictionary
        foreach (var item in data)
        {
             var dataItem = new Dictionary<string, object>();
    
             // Load data
    
             dataList.Add(dataItem);
        }
    
        var filteredDataList = dataList.AsQueryable().LinqToQuerystring(oDataQuery);
    
        // Now add filtered/sorted etc data in EdmEntityObject
        foreach (var item in filteredDataList)
        {
             var entity = new EdmEntityObject(entityType);
             foreach (var key in item.Keys)
                entity.TrySetPropertyValue(key, item[key].ToString());
             list.Add(entity);
        }