Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding first stage Alternate Key Support #219

Closed
wants to merge 12 commits into from
1 change: 1 addition & 0 deletions src/Microsoft.OData.Core/Microsoft.OData.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@
<Compile Include="UriParser\InternalErrorCodes.cs" />
<Compile Include="UriParser\KeyPropertyValue.cs" />
<Compile Include="UriParser\LiteralUtils.cs" />
<Compile Include="UriParser\Metadata\AlternateKeysODataUriResolver.cs" />
<Compile Include="UriParser\Metadata\StringAsEnumResolver.cs" />
<Compile Include="UriParser\Metadata\ODataUriResolver.cs" />
<Compile Include="UriParser\Metadata\UnqualifiedODataUriResolver.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ internal static QueryNode GetNavigationNode(IEdmNavigationProperty property, Sin
// Doing key lookup on the collection navigation property
if (namedValues != null)
{
return keyBinder.BindKeyValues(collectionNavigationNode, namedValues);
return keyBinder.BindKeyValues(collectionNavigationNode, namedValues, state.Model);
}

// Otherwise it's just a normal collection of entities
Expand Down
109 changes: 98 additions & 11 deletions src/Microsoft.OData.Core/UriParser/Binders/KeyBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,100 @@ internal KeyBinder(MetadataBinder.QueryTokenVisitor keyValueBindMethod)
/// </summary>
/// <param name="collectionNode">Already bound collection node.</param>
/// <param name="namedValues">The named value tokens to bind.</param>
/// <param name="model">The model to be used.</param>
/// <returns>The bound key lookup.</returns>
internal QueryNode BindKeyValues(EntityCollectionNode collectionNode, IEnumerable<NamedValue> namedValues)
internal QueryNode BindKeyValues(EntityCollectionNode collectionNode, IEnumerable<NamedValue> namedValues, IEdmModel model)
{
Debug.Assert(namedValues != null, "namedValues != null");
Debug.Assert(collectionNode != null, "CollectionNode != null");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug.Assert(model != null, "model != null");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added.


IEdmEntityTypeReference collectionItemType = collectionNode.EntityItemType;
List<KeyPropertyValue> keyPropertyValues = new List<KeyPropertyValue>();

IEdmEntityType collectionItemEntityType = collectionItemType.EntityDefinition();
QueryNode keyLookupNode;

if (TryBindToDeclaredKey(collectionNode, namedValues, model, collectionItemEntityType, out keyLookupNode) == true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== true is unncessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.

{
return keyLookupNode;
}
else if (TryBindToDeclaredAlternateKey(collectionNode, namedValues, model, collectionItemEntityType, out keyLookupNode) == true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

{
return keyLookupNode;
}
else
{
throw new ODataException(ODataErrorStrings.MetadataBinder_NotAllKeyPropertiesSpecifiedInKeyValues(collectionNode.ItemType.ODataFullName()));
}
}

/// <summary>
/// Tries to bind key values to a key lookup on a collection.
/// </summary>
/// <param name="collectionNode">Already bound collection node.</param>
/// <param name="namedValues">The named value tokens to bind.</param>
/// <param name="model">The model to be used.</param>
/// <param name="collectionItemEntityType">The type of a single item in a collection to apply the key value to.</param>
/// <param name="keyLookupNode">The bound key lookup.</param>
/// <returns>Returns true if binding succeeded.</returns>
private bool TryBindToDeclaredAlternateKey(EntityCollectionNode collectionNode, IEnumerable<NamedValue> namedValues, IEdmModel model, IEdmEntityType collectionItemEntityType, out QueryNode keyLookupNode)
{
IEnumerable<IDictionary<string, IEdmProperty>> alternateKeys = collectionItemEntityType.DeclaredAlternateKeys(model);
foreach (IDictionary<string, IEdmProperty> keys in alternateKeys)
{
if (TryBindToKeys(collectionNode, namedValues, model, collectionItemEntityType, keys, out keyLookupNode))
{
return true;
}
}

keyLookupNode = null;
return false;
}

/// <summary>
/// Tries to bind key values to a key lookup on a collection.
/// </summary>
/// <param name="collectionNode">Already bound collection node.</param>
/// <param name="namedValues">The named value tokens to bind.</param>
/// <param name="model">The model to be used.</param>
/// <param name="collectionItemEntityType">The type of a single item in a collection to apply the key value to.</param>
/// <param name="keyLookupNode">The bound key lookup.</param>
/// <returns>Returns true if binding succeeded.</returns>
private bool TryBindToDeclaredKey(EntityCollectionNode collectionNode, IEnumerable<NamedValue> namedValues, IEdmModel model, IEdmEntityType collectionItemEntityType, out QueryNode keyLookupNode)
{
Dictionary<string, IEdmProperty> keys = new Dictionary<string, IEdmProperty>();
foreach (IEdmStructuralProperty property in collectionItemEntityType.Key())
{
keys[property.Name] = property;
}

return TryBindToKeys(collectionNode, namedValues, model, collectionItemEntityType, keys, out keyLookupNode);
}

/// <summary>
/// Binds key values to a key lookup on a collection.
/// </summary>
/// <param name="collectionNode">Already bound collection node.</param>
/// <param name="namedValues">The named value tokens to bind.</param>
/// <param name="model">The model to be used.</param>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is an exception thrown from BindKeyValues to take care of scenario where segment cannot be bound to declared keys.

/// <param name="collectionItemEntityType">The type of a single item in a collection to apply the key value to.</param>
/// <param name="keys">Dictionary of aliases to structural property names for the key.</param>
/// <param name="keyLookupNode">The bound key lookup.</param>
/// <returns>Returns true if binding succeeded.</returns>
private bool TryBindToKeys(EntityCollectionNode collectionNode, IEnumerable<NamedValue> namedValues, IEdmModel model, IEdmEntityType collectionItemEntityType, IDictionary<string, IEdmProperty> keys, out QueryNode keyLookupNode)
{
List<KeyPropertyValue> keyPropertyValues = new List<KeyPropertyValue>();
HashSet<string> keyPropertyNames = new HashSet<string>(StringComparer.Ordinal);
foreach (NamedValue namedValue in namedValues)
{
KeyPropertyValue keyPropertyValue = this.BindKeyPropertyValue(namedValue, collectionItemEntityType);
KeyPropertyValue keyPropertyValue;

if (!this.TryBindKeyPropertyValue(namedValue, collectionItemEntityType, model, keys, out keyPropertyValue))
{
keyLookupNode = null;
return false;
}

Debug.Assert(keyPropertyValue != null, "keyPropertyValue != null");
Debug.Assert(keyPropertyValue.KeyProperty != null, "keyPropertyValue.KeyProperty != null");

Expand All @@ -70,15 +149,18 @@ internal QueryNode BindKeyValues(EntityCollectionNode collectionNode, IEnumerabl
if (keyPropertyValues.Count == 0)
{
// No key values specified, for example '/Customers()', do not include the key lookup at all
return collectionNode;
keyLookupNode = collectionNode;
return true;
}
else if (keyPropertyValues.Count != collectionItemEntityType.Key().Count())
{
throw new ODataException(ODataErrorStrings.MetadataBinder_NotAllKeyPropertiesSpecifiedInKeyValues(collectionNode.ItemType.ODataFullName()));
keyLookupNode = null;
return false;
}
else
{
return new KeyLookupNode(collectionNode, new ReadOnlyCollection<KeyPropertyValue>(keyPropertyValues));
keyLookupNode = new KeyLookupNode(collectionNode, new ReadOnlyCollection<KeyPropertyValue>(keyPropertyValues));
return true;
}
}

Expand All @@ -87,8 +169,11 @@ internal QueryNode BindKeyValues(EntityCollectionNode collectionNode, IEnumerabl
/// </summary>
/// <param name="namedValue">The named value to bind.</param>
/// <param name="collectionItemEntityType">The type of a single item in a collection to apply the key value to.</param>
/// <param name="model">The model to be used.</param>
/// <param name="keys">Dictionary of alias to keys.</param>
/// <param name="keyPropertyValue">The bound key property value node.</param>
/// <returns>The bound key property value node.</returns>
private KeyPropertyValue BindKeyPropertyValue(NamedValue namedValue, IEdmEntityType collectionItemEntityType)
private bool TryBindKeyPropertyValue(NamedValue namedValue, IEdmEntityType collectionItemEntityType, IEdmModel model, IDictionary<string, IEdmProperty> keys, out KeyPropertyValue keyPropertyValue)
{
// These are exception checks because the data comes directly from the potentially user specified tree.
ExceptionUtils.CheckArgumentNotNull(namedValue, "namedValue");
Expand All @@ -98,7 +183,7 @@ private KeyPropertyValue BindKeyPropertyValue(NamedValue namedValue, IEdmEntityT
IEdmProperty keyProperty = null;
if (namedValue.Name == null)
{
foreach (IEdmProperty p in collectionItemEntityType.Key())
foreach (IEdmProperty p in keys.Values)
{
if (keyProperty == null)
{
Expand All @@ -112,11 +197,12 @@ private KeyPropertyValue BindKeyPropertyValue(NamedValue namedValue, IEdmEntityT
}
else
{
keyProperty = collectionItemEntityType.Key().Where(k => string.CompareOrdinal(k.Name, namedValue.Name) == 0).SingleOrDefault();
keyProperty = keys.Where(k => string.CompareOrdinal(k.Key, namedValue.Name) == 0).SingleOrDefault().Value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keys.SingleOrDefault(...)


if (keyProperty == null)
{
throw new ODataException(ODataErrorStrings.MetadataBinder_PropertyNotDeclaredOrNotKeyInKeyValue(namedValue.Name, collectionItemEntityType.ODataFullName()));
keyPropertyValue = null;
return false;
}
}

Expand All @@ -129,11 +215,12 @@ private KeyPropertyValue BindKeyPropertyValue(NamedValue namedValue, IEdmEntityT
value = MetadataBindingUtils.ConvertToTypeIfNeeded(value, keyPropertyType);

Debug.Assert(keyProperty != null, "keyProperty != null");
return new KeyPropertyValue()
keyPropertyValue = new KeyPropertyValue()
{
KeyProperty = keyProperty,
KeyValue = value
};
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//---------------------------------------------------------------------
// <copyright file="AlternateKeysODataUriResolver.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

namespace Microsoft.OData.Core.UriParser.Metadata
{
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Microsoft.OData.Core.UriParser.Semantic;
using Microsoft.OData.Core.UriParser.TreeNodeKinds;
using Microsoft.OData.Edm;

/// <summary>
/// Implementation for resolving a literal value without qualified namespace to enum type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modify the summary

/// </summary>
public sealed class AlternateKeysODataUriResolver : ODataUriResolver
{
/// <summary>
/// Model to be used for resolving the alternate keys.
/// </summary>
private readonly IEdmModel model;

/// <summary>
/// Constructs a AlternateKeysODataUriResolver with the given edmModel to be used for resolving alternate keys
/// </summary>
/// <param name="model">The model to be used.</param>
public AlternateKeysODataUriResolver(IEdmModel model)
{
this.model = model;
}

/// <summary>
/// Resolve keys for certain entity set, this function would be called when key is specified as name value pairs. E.g. EntitySet(ID='key')
/// Enum value could omit type name prefix using this resolver.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

/// </summary>
/// <param name="type">Type for current entityset.</param>
/// <param name="namedValues">The dictionary of name value pairs.</param>
/// <param name="convertFunc">The convert function to be used for value converting.</param>
/// <returns>The resolved key list.</returns>
public override IEnumerable<KeyValuePair<string, object>> ResolveKeys(IEdmEntityType type, IDictionary<string, string> namedValues, Func<IEdmTypeReference, string, object> convertFunc)
{
IEnumerable<KeyValuePair<string, object>> convertedPairs;
try
{
convertedPairs = base.ResolveKeys(type, namedValues, convertFunc);
}
catch (ODataException ex)
{
if (!TryResolveUsingAlternateKeys(type, namedValues, convertFunc, out convertedPairs))
{
throw ex;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw;

}
}

return convertedPairs;
}

/// <summary>
/// Try to resolve alternate keys for certain entity set, this function would be called when key is specified as name value pairs. E.g. EntitySet(ID='key')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for certain entity type, ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// </summary>
/// <param name="type">Type for current entityset.</param>
/// <param name="namedValues">The dictionary of name value pairs.</param>
/// <param name="convertFunc">The convert function to be used for value converting.</param>
/// <param name="convertedPairs">The resolved key list.</param>
/// <returns>True if resolve succeeded.</returns>
private bool TryResolveUsingAlternateKeys(IEdmEntityType type, IDictionary<string, string> namedValues, Func<IEdmTypeReference, string, object> convertFunc, out IEnumerable<KeyValuePair<string, object>> convertedPairs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better as "TryResolveAlternateKeys()"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, done.

{
IEnumerable<IDictionary<string, IEdmProperty>> alternateKeys = type.DeclaredAlternateKeys(model);
foreach (IDictionary<string, IEdmProperty> keys in alternateKeys)
{
if (TryResolveUsingKeys(type, namedValues, keys, convertFunc, out convertedPairs))
{
return true;
}
}

convertedPairs = null;
return false;
}

/// <summary>
/// Try to resolve keys for certain entity set, this function would be called when key is specified as name value pairs. E.g. EntitySet(ID='key')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for certain entity type, ...

/// </summary>
/// <param name="type">Type for current entityset.</param>
/// <param name="namedValues">The dictionary of name value pairs.</param>
/// <param name="keyProperties">Dictionary of alias to key properties.</param>
/// <param name="convertFunc">The convert function to be used for value converting.</param>
/// <param name="convertedPairs">The resolved key list.</param>
/// <returns>True if resolve succeeded.</returns>
private bool TryResolveUsingKeys(IEdmEntityType type, IDictionary<string, string> namedValues, IDictionary<string, IEdmProperty> keyProperties, Func<IEdmTypeReference, string, object> convertFunc, out IEnumerable<KeyValuePair<string, object>> convertedPairs)
{
Dictionary<string, object> pairs = new Dictionary<string, object>(StringComparer.Ordinal);

foreach (KeyValuePair<string, IEdmProperty> kvp in keyProperties)
{
string valueText;

if (EnableCaseInsensitive)
{
var list = namedValues.Keys.Where(key => string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)).ToList();
if (list.Count > 1)
{
throw new ODataException(Strings.UriParserMetadata_MultipleMatchingKeysFound(kvp.Key));
}
else if (list.Count == 0)
{
convertedPairs = null;
return false;
}

valueText = namedValues[list.Single()];
}
else if (!namedValues.TryGetValue(kvp.Key, out valueText))
{
convertedPairs = null;
return false;
}

object convertedValue = convertFunc(kvp.Value.Type, valueText);
if (convertedValue == null)
{
convertedPairs = null;
return false;
}

pairs[kvp.Key] = convertedValue;
}

convertedPairs = pairs;
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ public virtual IEnumerable<KeyValuePair<string, object>> ResolveKeys(IEdmEntityT
throw ExceptionUtil.CreateSyntaxError();
}

//// Debug.Assert(property.Type.IsPrimitive() || property.Type.IsTypeDefinition(), "Keys can only be primitive or type definition");
object convertedValue = convertFunc(property.Type, valueText);
if (convertedValue == null)
{
Expand All @@ -316,7 +315,7 @@ public virtual IEnumerable<KeyValuePair<string, object>> ResolveKeys(IEdmEntityT
}

/// <summary>
/// Resolve an operatin parameter's name with case insensitive enabled
/// Resolve an operation parameter's name with case insensitive enabled
/// </summary>
/// <param name="operation">The operation.</param>
/// <param name="identifier">Name for the parameter.</param>
Expand All @@ -329,7 +328,7 @@ internal static IEdmOperationParameter ResolveOpearationParameterNameCaseInsensi
{
throw new ODataException(Strings.UriParserMetadata_MultipleMatchingParametersFound(identifier));
}

if (list.Count == 1)
{
return list.Single();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ private static KeySegment CreateKeySegment(ODataPathSegment segment, KeySegment
key = KeyFinder.FindAndUseKeysFromRelatedSegment(key, keyProperties, currentNavPropSegment.NavigationProperty, previousKeySegment);
}

// if we still didn't find any keys, then throw an error.
if (keyProperties.Count != key.ValueCount && resolver.GetType() == typeof(ODataUriResolver))
// if we still didn't find any keys, then throw an error unless resolver supports alternate keys in which case the validation is done during resolution
if (keyProperties.Count != key.ValueCount && resolver.GetType() == typeof(ODataUriResolver) && resolver.GetType() != typeof(AlternateKeysODataUriResolver))
{
throw ExceptionUtil.CreateBadRequestError(ErrorStrings.BadRequest_KeyCountMismatch(targetEntityType.FullName()));
}
Expand Down
Loading