Reputation: 2263
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
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