dvlpr
dvlpr

Reputation: 31

ASP.NET WebApi2 OData handling of queries with slash /

I have made a "standard" Web Api 2 OData project with convention model routing. Following OData queries are working:

/odata/Users

/odata/Users(123)

/odata/$metadata

/odata/Users?$select=Username

So everything seemed to be fine until I tried this, which I think is also a legal OData query:

/odata/Users(123)/Username

Slash / in query breaks everything and it does not hit the controller class and OData authentication flow at all. Should this be supported at all in Microsoft ASP.NET OData implementation? Or is this supported only if I define explicit methods with correct routes for every single property like Username? Any suggestions to fix this? I have tried explicit {*rest} routes etc.

Upvotes: 1

Views: 379

Answers (1)

John Gathogo
John Gathogo

Reputation: 4655

AFAIK, the built-in routing conventions don't include one for property access. You'd be required to add many actions for every property access.

However, based on this resource here, it's not all that difficult to add a custom routing convention to handle the property access path template: ~/entityset/key/property

Here's a custom routing convention adapted from the link I shared above

Assembly used: Microsoft.AspNet.OData 7.4.1 - the approach would be the same for any other OData Web API library you might be using

Class used for illustration

public class Product
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
}

Add routing convention for property access

// Usings
using Microsoft.AspNet.OData.Routing;
using Microsoft.AspNet.OData.Routing.Conventions;
using System;
using System.Linq;
using System.Web.Http.Controllers;
// ...

public class CustomPropertyRoutingConvention : NavigationSourceRoutingConvention
{
    private const string ActionName = "GetProperty";

    public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
    {
        if (odataPath == null || controllerContext == null || actionMap == null)
        {
            return null;
        }

        if (odataPath.PathTemplate == "~/entityset/key/property" ||
            odataPath.PathTemplate == "~/entityset/key/cast/property" ||
            odataPath.PathTemplate == "~/singleton/property" ||
            odataPath.PathTemplate == "~/singleton/cast/property")
        {
            var segment = odataPath.Segments.OfType<Microsoft.OData.UriParser.PropertySegment>().LastOrDefault();

            if (segment != null)
            {
                string actionName = FindMatchingAction(actionMap, ActionName);

                if (actionName != null)
                {
                    if (odataPath.PathTemplate.StartsWith("~/entityset/key", StringComparison.Ordinal))
                    {
                        var keySegment = odataPath.Segments.OfType<Microsoft.OData.UriParser.KeySegment>().FirstOrDefault();
                        if (keySegment == null || !keySegment.Keys.Any())
                            throw new InvalidOperationException("This link does not contain a key.");

                        controllerContext.RouteData.Values[ODataRouteConstants.Key] = keySegment.Keys.First().Value;
                    }

                    controllerContext.RouteData.Values["propertyName"] = segment.Property.Name;

                    return actionName;
                }
            }
        }

        return null;
    }

    public static string FindMatchingAction(ILookup<string, HttpActionDescriptor> actionMap, params string[] targetActionNames)
    {
        foreach (string targetActionName in targetActionNames)
        {
            if (actionMap.Contains(targetActionName))
            {
                return targetActionName;
            }
        }

        return null;
    }
}

Add single method in your controller to handle request for any property

public class ProductsController : ODataController
{
    // ...
    [HttpGet]
    public IHttpActionResult GetProperty(int key, string propertyName)
    {
        var product = _db.Products.FirstOrDefault(d => d.Id.Equals(key));
        if (product == null)
        {
            return NotFound();
        }

        PropertyInfo info = typeof(Product).GetProperty(propertyName);

        object value = info.GetValue(product);

        return Ok(value, value.GetType());
    }

    private IHttpActionResult Ok(object content, Type type)
    {
        var resultType = typeof(OkNegotiatedContentResult<>).MakeGenericType(type);
        return Activator.CreateInstance(resultType, content, this) as IHttpActionResult;
    }
    // ...
}

In your WebApiConfig.cs (or equivalent place where you configure the service)

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Product>("Products");

var routingConventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting("odata", configuration);
routingConventions.Insert(0, new CustomPropertyRoutingConvention());

configuration.MapODataServiceRoute("odata", "odata", modelBuilder.GetEdmModel(), new DefaultODataPathHandler(), routingConventions);
configuration.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
configuration.EnsureInitialized();

Request for Name property: /Products(1)/Name

Request for Id property: /Products(1)/Id

Upvotes: 1

Related Questions