Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Expose only a subset of .NET OData APIs for a route (return 404 for excluded APIs)

Background/Context:

We have two routes, with different route prefixes:

  1. Route 1 prefix: /api
  2. Route 2 prefix: /api/partial

Currently, we use the same EdmModel for both route prefixes. (See the first code snippit, named "What we currently do").

What we want:

We need to only allow a subset of API functionality for Route 2: /api/partial. We want to return 404 when someone tries to access an API that is not available to the "partial" EdmModel

Example:

  1. We want to return 404 for /api/parial/products, where products is not defined in this "partial" API route.
  2. We want to still route /api/products to the controller method

What we've tried:

Using a second EdmModel, that contains only a subset of the entities available in the full EdmModel. (See the second code snippit, named "What we want to do:".)

Problem:

We get an error on service startup: The path template 'products' on the action 'Export' in controller 'Products' is not a valid OData path template. Resource not found for the segment 'products'.)

My best guess at what is happening is that the .NET OData library scans all of the OData controllers, functions, actions and expect each of them to be explicitly defined in the EdmModel for each route. If this is true, then this solution (initializing a new EdmModel) will likely not work...

Is this not supported? If not, what other options are there to accomplish this? Must we explicitly return 404 in the controller API function? This would require analyzing the path for "api/subset" in the API function, which seems to me like a hack.

What we currently do:

private static IEdmModel GetFullEdmModel()
{
    var builder = new ODataConventionModelBuilder();

    var orders = builder.EntitySet<Order>("orders");
    orders.EntityType.HasKey(o => o.Id);
    orders.EntityType.Property(o => o.Id).Name = "id";

    var products = builder.EntitySet<Product>("products");
    products.EntityType.HasKey(p => p.Id);
    products.EntityType.Property(p => p.Id).Name = "id";
    products.EntityType.Action("Export").Returns<ExportResponse>();

    return builder.GetEdmModel();
}

protected override void Register(HttpConfiguration config)
{
    base.Register(config);

    var model = GetFullEdmModel();
    var conventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(config, model);

    // Map route 1 to {model}
    config.MapODataServiceRoute(
        routeName: "route1",
        routePrefix: "/api",
        model: model, 
        pathHandler: new CustomBIODataPathHandler(), 
        routingConventions: conventions);

    // Map route 2 to {model}
    config.MapODataServiceRoute(
        routeName: "route2",
        routePrefix: "/api/partial", // different route prefix
        model: model, // but it uses the same model
        pathHandler: new CustomBIODataPathHandler(), 
        routingConventions: conventions);
}

What we want to do:

private static IEdmModel GetPartialEdmModel()
{
    var builder = new ODataConventionModelBuilder();

    // Include only one entity
    var orders = builder.EntitySet<Order>("orders");
    orders.EntityType.HasKey(o => o.Id);
    orders.EntityType.Property(o => o.Id).Name = "id";

    return builder.GetEdmModel();
}

protected override void Register(HttpConfiguration config)
{
    base.Register(config);

    // Map route 1 to {model}
    var model = GetFullEdmModel();
    var modelConventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(config, model);
    config.MapODataServiceRoute(
        routeName: "route1",
        routePrefix: "/api",
        model: model, // use standard full model
        pathHandler: new CustomBIODataPathHandler(), 
        routingConventions: conventions);

    // Map route 2 to a new partial model: {partialModel}    
    var partialModel = GetPartialEdmModel();
    var partialModelConventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(config, model);
    config.MapODataServiceRoute(
        routeName: "route2",
        routePrefix: "/api/partial", // different route prefix
        model: partialModel, // use a sparate, partial edm model ( a subset of the full edm model )
        pathHandler: new CustomBIODataPathHandler(), 
        routingConventions: conventions);
}
like image 519
James Wierzba Avatar asked Jul 12 '18 20:07

James Wierzba


1 Answers

You need two different models or a more intelligent model with conditions as entitiy products is not available in all paths but only in /api/products.
In general the Error-message is explaining it already quite well but perhaps you just need it in other words.

I think the cleaner way is to provide an own model for each route, then it's quite easy to add or remove whatever you need inside.
If you mix everything in one model it will always be under construction if a new route is added or changed.

// Map route 1 to {model}
config.MapODataServiceRoute(
    routeName: "route1",
    routePrefix: "/api",
    model: GetApiModel(), 
    pathHandler: new CustomBIODataPathHandler(), 
    routingConventions: conventions);


// Map route 2 to {model}
config.MapODataServiceRoute(
    routeName: "route2",
    routePrefix: "/api/partial", // different route prefix
    model: GetApiPartialModel(),
    pathHandler: new CustomBIODataPathHandler(), 
    routingConventions: conventions);

It's a concept and I'm not so firm with the notation in that code so you might have to adjust it a bit.

Alternatively, if you really want to use only one model just try it like this:

// Map route 1 to {model}
config.MapODataServiceRoute(
    routeName: "route1",
    routePrefix: "/api",
    model: GetFullEdmModel("/api"), 
    pathHandler: new CustomBIODataPathHandler(), 
    routingConventions: conventions);

// Map route 2 to {model}
config.MapODataServiceRoute(
    routeName: "route2",
    routePrefix: "/api/partial", // different route prefix
    model: GetFullEdmModel("/api/partial"), // but it uses the same model
    pathHandler: new CustomBIODataPathHandler(), 
    routingConventions: conventions);

Then you can use the parameter to implement any conditions or a switch.

Beside that you've probably faults in your desired code in the bottom:

// Map route 1 to {model}
var model = GetFullEdmModel();
var modelConventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(config, model);
config.MapODataServiceRoute(
    routeName: "route1",
    routePrefix: "/api",
    model: model, // use standard full model
    pathHandler: new CustomBIODataPathHandler(), 
    routingConventions: modelConventions);

See the last line, that has to be adjusted for both blocks then. I never took care before about the two lines above each config-block, so primary the problem is probably that not all variables are fitting together and you've to check it in all details.

like image 139
David Avatar answered Oct 31 '22 01:10

David