Sumit Maingi
Sumit Maingi

Reputation: 2263

OData v4 - Select specific field in an Entity

I am using OData v4 with Web API 2.2.

I have an Entity called "Person" with Composite keys of "FirstName" and "LastName". Looks like this:

public class Person {
    public string FirstName {get; set;}
    public string LastName {get; set;}
    public double Age {get; set;}
}

In order to support composite keys, I have added a uri conventions on top of the default one, it looks like this:

public class CompositeKeyRoutingConvention : EntityRoutingConvention
{
    public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
    {
        var action = base.SelectAction(odataPath, controllerContext, actionMap);

        if (action != null)
        {
            var routeValues = controllerContext.RouteData.Values;
            if (routeValues.ContainsKey(ODataRouteConstants.Key))
            {
                var keyRaw = (string)routeValues[ODataRouteConstants.Key];

                var compoundKeyPairs = keyRaw.Split(',');

                if (!compoundKeyPairs.Any())
                {
                    return action;
                }

                foreach (var compoundKeyPair in compoundKeyPairs)
                {
                    var pair = compoundKeyPair.Split('=');
                    if (pair.Length != 2)
                    {
                        continue;
                    }
                    var keyName = pair[0].Trim();
                    var keyValue = pair[1].Trim();

                    routeValues.Add(keyName, keyValue);
                }
            }
        }

        return action;
    }

My calling code is trying to access the age of a person like so:

http://localhost:46028/Person(firstName='Blah',LastName='Blu')/Age

I get this error:

{ "error":{ "code":"","message":"No HTTP resource was found that matches the request URI 'http://:46028/Person(firstName='Blah',LastName='Blu')/Age'.","innererror":{ "message":"No routing convention was found to select an action for the OData path with template '~/entityset/key/property'.","type":"","stacktrace":"" } } }

my controller has two methods:

    public IQueryable<Person> Get()
    {
        return _db.People;
    }

    public Person Get([FromODataUri] string firstName, [FromODataUri] string lastName)
    {
        var person = _db.People
            .FirstOrDefault(x => x.FirstName == firstName && x.LastName== lastName);

        if (person == null)
        {
            throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
        }

        return person;
    }

Upvotes: 0

Views: 1712

Answers (1)

Sumit Maingi
Sumit Maingi

Reputation: 2263

Turns out there is a simple solution. Web API does not support composite keys well, they have a PropertyRoutingConvention which does exactly what I'm looking for except it doesn't work for composite keys.

Fixed it by creating a "CompositeKeyPropertyRoutingConvention" given below:

public class CompositeKeyPropertyRoutingConvention : PropertyRoutingConvention
{
    public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
    {
        var action = base.SelectAction(odataPath, controllerContext, actionMap);

        return new CompositeKeyRoutingBehaviour().SelectAction(action, odataPath, controllerContext, actionMap);
    }
}

public class CompositeKeyRoutingBehaviour
{
    public string SelectAction(string action, ODataPath odataPath, HttpControllerContext controllerContext,
                               ILookup<string, HttpActionDescriptor> actionMap)
    {
        if (action != null)
        {
            var routeValues = odataPath.Segments.FirstOrDefault(x => x.SegmentKind == ODataRouteConstants.Key);

            if (routeValues != null)
            {
                var keyRaw = routeValues.ToString();

                var formatter = new KeyValueFormatter();

                var keyPairs = formatter.FormatRawKey(keyRaw);

                if (!keyPairs.Any())
                {
                    return action;
                }

                foreach (var pair in keyPairs)
                {
                    controllerContext.RouteData.Values.Add(pair.Key, pair.Value);
                }
            }
        }

        return action;
    }
}

public class KeyValueFormatter
{
    public IDictionary<string, string> FormatRawKey(string rawKey)
    {
        var formattedKeys = new Dictionary<string, string>();

        if (string.IsNullOrWhiteSpace(rawKey))
            return formattedKeys;

        var keyBuilder = new StringBuilder();
        var valueBuilder = new StringBuilder();

        var keyBuilding = true;

        var keys = new List<string>();
        var values = new List<string>();

        for (var i = 0; i < rawKey.Length; i++)
        {
            var currentChar = rawKey[i];
            var nextChar = i < rawKey.Length - 1 ? rawKey[i + 1] : (char?)null;
            var prevChar = i > 0 ? rawKey[i - 1] : (char?)null;

            if (currentChar == '=' && keyBuilding)
            {
                keyBuilding = false;

                keys.Add(keyBuilder.ToString());

                keyBuilder.Clear();

                continue;
            }

            if (!keyBuilding && currentChar == ',' && prevChar.HasValue && prevChar.Value == '\'')
            {
                keyBuilding = true;

                values.Add(valueBuilder.ToString());

                valueBuilder.Clear();

                continue;
            }

            if (keyBuilding)
            {
                keyBuilder.Append(currentChar);
            }
            else
            {
                valueBuilder.Append(currentChar);
            }

            if (!keyBuilding && !nextChar.HasValue)
            {
                values.Add(valueBuilder.ToString());

                valueBuilder.Clear();
            }
        }

        if (keys.Count != values.Count)
        {
            throw new InvalidDataException("The key could not be formatted into valid pairs. key was: " + rawKey);
        }

        for (var i = 0; i < keys.Count; i++)
        {
            formattedKeys.Add(keys[i].Trim(), values[i].Trim());
        }

        return formattedKeys;
    }
}

The last class does some mumbo jumbo to handle "," inside the data of the keys themselves, haven't tested with "'" inside the data yet, I recommend you do that.

Upvotes: 0

Related Questions