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

Fixes #2911, Add APIs to register custom uri functions for certain model #3149

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
static Microsoft.OData.UriParser.CustomUriFunctions.AddCustomUriFunction(this Microsoft.OData.Edm.IEdmModel model, string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature) -> void
static Microsoft.OData.UriParser.CustomUriFunctions.RemoveCustomUriFunction(this Microsoft.OData.Edm.IEdmModel model, string functionName) -> bool
static Microsoft.OData.UriParser.CustomUriFunctions.RemoveCustomUriFunction(this Microsoft.OData.Edm.IEdmModel model, string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature) -> bool
static Microsoft.OData.UriParser.CustomUriFunctions.TryGetCustomFunction(this Microsoft.OData.Edm.IEdmModel model, string functionCallToken, out System.Collections.Generic.IList<System.Collections.Generic.KeyValuePair<string, Microsoft.OData.UriParser.FunctionSignatureWithReturnType>> nameSignatures, bool enableCaseInsensitive = false) -> bool
82 changes: 60 additions & 22 deletions src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,25 @@ internal static KeyValuePair<string, FunctionSignatureWithReturnType> MatchSigna
/// <param name="enableCaseInsensitive">Optional flag for whether case insensitive match is enabled.</param>
/// <returns>The signatures which match the supplied function name.</returns>
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "need to use lower characters for built-in functions.")]
internal static IList<KeyValuePair<string, FunctionSignatureWithReturnType>> GetUriFunctionSignatures(string functionCallToken, bool enableCaseInsensitive = false)
internal static IList<KeyValuePair<string, FunctionSignatureWithReturnType>> GetUriFunctionSignatures(string functionCallToken, IEdmModel model = null, bool enableCaseInsensitive = false)
{
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> customUriFunctionsNameSignatures = null;
string builtInUriFunctionName = null;
FunctionSignatureWithReturnType[] builtInUriFunctionsSignatures = null;
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> builtInUriFunctionsNameSignatures = null;

// Try to find the function in the user custom functions
bool customFound = CustomUriFunctions.TryGetCustomFunction(functionCallToken, out customUriFunctionsNameSignatures,
enableCaseInsensitive);
// Try to find the function in the user custom functions for the specified model.
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> customUriFunctionsNameSignaturesInModel = null;
bool customInModelFound = false;
if (model != null)
{
customInModelFound = model.TryGetCustomFunction(functionCallToken, out customUriFunctionsNameSignaturesInModel, enableCaseInsensitive);
}

// Try to find the function in the user custom functions (From global static dictionary, we will remove it in the next major release)
bool customFound = CustomUriFunctions.TryGetCustomFunction(functionCallToken, out customUriFunctionsNameSignatures, enableCaseInsensitive);

bool builtInFound = BuiltInUriFunctions.TryGetBuiltInFunction(functionCallToken, enableCaseInsensitive, out builtInUriFunctionName,
out builtInUriFunctionsSignatures);
bool builtInFound = BuiltInUriFunctions.TryGetBuiltInFunction(functionCallToken, enableCaseInsensitive, out builtInUriFunctionName, out builtInUriFunctionsSignatures);

// Populate the matched names found for built-in function
if (builtInFound)
Expand All @@ -167,25 +173,57 @@ internal static IList<KeyValuePair<string, FunctionSignatureWithReturnType>> Get
builtInUriFunctionsSignatures.Select(sig => new KeyValuePair<string, FunctionSignatureWithReturnType>(builtInUriFunctionName, sig)).ToList();
}

if (!customFound && !builtInFound)
{
// Not found in both built-in and custom.
throw new ODataException(Error.Format(SRResources.MetadataBinder_UnknownFunction, functionCallToken));
}

if (!customFound)
if (builtInFound)
{
Debug.Assert(builtInUriFunctionsNameSignatures != null, "No Built-in functions found");
return builtInUriFunctionsNameSignatures;
if (customInModelFound)
{
if (customFound)
{
return builtInUriFunctionsNameSignatures.Concat(customUriFunctionsNameSignaturesInModel).Concat(customUriFunctionsNameSignatures).ToArray();
}
else
{
return builtInUriFunctionsNameSignatures.Concat(customUriFunctionsNameSignaturesInModel).ToArray();
}
}
else
{
if (customFound)
{
return builtInUriFunctionsNameSignatures.Concat(customUriFunctionsNameSignatures).ToArray();
}
else
{
return builtInUriFunctionsNameSignatures;
}
}
}

if (!builtInFound)
else
{
Debug.Assert(customUriFunctionsNameSignatures != null, "No Custom functions found");
return customUriFunctionsNameSignatures;
if (customInModelFound)
{
if (customFound)
{
return customUriFunctionsNameSignaturesInModel.Concat(customUriFunctionsNameSignatures).ToArray();
}
else
{
return customUriFunctionsNameSignaturesInModel;
}
}
else
{
if (customFound)
{
return customUriFunctionsNameSignatures;
}
else
{
// Not found in both built-in and custom.
throw new ODataException(Error.Format(SRResources.MetadataBinder_UnknownFunction, functionCallToken));
}
}
}

return builtInUriFunctionsNameSignatures.Concat(customUriFunctionsNameSignatures).ToArray();
}

internal static FunctionSignatureWithReturnType[] ExtractSignatures(
Expand Down Expand Up @@ -300,7 +338,7 @@ private QueryNode BindAsUriFunction(FunctionCallToken functionCallToken, List<Qu
}

// Do some validation and get potential Uri functions that could match what we saw
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> nameSignatures = GetUriFunctionSignatures(functionCallToken.Name,
IList<KeyValuePair<string, FunctionSignatureWithReturnType>> nameSignatures = GetUriFunctionSignatures(functionCallToken.Name, state.Model,
this.state.Configuration.EnableCaseInsensitiveUriFunctionIdentifier);

SingleValueNode[] argumentNodeArray = ValidateArgumentsAreSingleValue(functionCallToken.Name, argumentNodes);
Expand Down
174 changes: 169 additions & 5 deletions src/Microsoft.OData.Core/UriParser/CustomUriFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.OData.UriParser
#region NameSpaces

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand All @@ -17,6 +18,17 @@ namespace Microsoft.OData.UriParser

#endregion

/// <summary>
/// Class represents the annotation for custom functions
/// </summary>
internal class CustomUriFunctionsAnnotation
{
/// <summary>
/// Dictionary of the name of the custom function and all the signatures.
/// </summary>
public ConcurrentDictionary<string, FunctionSignatureWithReturnType[]> CustomFunctions { get; } = new ConcurrentDictionary<string, FunctionSignatureWithReturnType[]>(StringComparer.Ordinal);
}

/// <summary>
/// Class represents functions signatures of custom uri functions.
/// </summary>
Expand All @@ -36,6 +48,158 @@ private static readonly Dictionary<string, FunctionSignatureWithReturnType[]> Cu

#region Public Methods

/// <summary>
/// Add a custom uri function to extend uri functions for the certain model.
/// In case the function name already exists as a custom function, the signature will be added as an another overload.
/// </summary>
/// <param name="model">The given Edm model.</param>
/// <param name="functionName">The new custom function name.</param>
/// <param name="functionSignature">The new custom function signature.</param>
public static void AddCustomUriFunction(this IEdmModel model, string functionName, FunctionSignatureWithReturnType functionSignature)
{
// Parameters validation
ExceptionUtils.CheckArgumentNotNull(model, "model");
ExceptionUtils.CheckArgumentStringNotNullOrEmpty(functionName, "functionName");
ExceptionUtils.CheckArgumentNotNull(functionSignature, "functionSignature");

ValidateFunctionWithReturnType(functionSignature);

// Check if the function does already exists in the Built-In functions
FunctionSignatureWithReturnType[] existingBuiltInFunctionOverload;
if (BuiltInUriFunctions.TryGetBuiltInFunction(functionName, out existingBuiltInFunctionOverload))
{
// Function name exists, check if full signature exists among the overloads.
if (existingBuiltInFunctionOverload.Any(builtInFunction =>
AreFunctionsSignatureEqual(functionSignature, builtInFunction)))
{
throw new ODataException(Error.Format(SRResources.CustomUriFunctions_AddCustomUriFunction_BuiltInExistsFullSignature, functionName));
}
}

CustomUriFunctionsAnnotation funAnnotations = model.GetOrSetCustomUriFunctionAnnotation();
AddCustomFunction(funAnnotations.CustomFunctions, functionName, functionSignature);
}

/// <summary>
/// Removes the specific function overload from the custom uri functions.
/// </summary>
/// <param name="model">The given Edm model.</param>
/// <param name="functionName">Custom function name to remove.</param>
/// <param name="functionSignature">The specific signature overload of the function to remove.</param>
/// <returns>'False' if custom function signature doesn't exist. 'True' if function has been removed successfully.</returns>
/// <exception cref="ArgumentNullException">Arguments are null, or function signature return type is null.</exception>
public static bool RemoveCustomUriFunction(this IEdmModel model, string functionName, FunctionSignatureWithReturnType functionSignature)
{
ExceptionUtils.CheckArgumentNotNull(model, "model");
ExceptionUtils.CheckArgumentStringNotNullOrEmpty(functionName, "functionName");
ExceptionUtils.CheckArgumentNotNull(functionSignature, "functionSignature");

ValidateFunctionWithReturnType(functionSignature);

CustomUriFunctionsAnnotation funAnnotations = model.GetOrSetCustomUriFunctionAnnotation();
ConcurrentDictionary<string, FunctionSignatureWithReturnType[]> customFunctions = funAnnotations.CustomFunctions;

FunctionSignatureWithReturnType[] existingCustomFunctionOverloads;
if (!customFunctions.TryGetValue(functionName, out existingCustomFunctionOverloads))
{
return false;
}

// Get all function sigature overloads without the overload which is requested to be removed
FunctionSignatureWithReturnType[] customFunctionOverloadsWithoutTheOneToRemove =
existingCustomFunctionOverloads.SkipWhile(funcOverload => AreFunctionsSignatureEqual(funcOverload, functionSignature)).ToArray();

// Nothing was removed - Requested overload doesn't exist
if (customFunctionOverloadsWithoutTheOneToRemove.Length == existingCustomFunctionOverloads.Length)
{
return false;
}

// No overloads have left in this function name. Delete the function name
if (customFunctionOverloadsWithoutTheOneToRemove.Length == 0)
{
return customFunctions.Remove(functionName, out _);
}
else
{
// Requested overload has been removed.
// Update the custom functions to the overloads without that one requested to be removed
customFunctions[functionName] = customFunctionOverloadsWithoutTheOneToRemove;
return true;
}
}

/// <summary>
/// Removes all the function overloads from the custom uri functions.
/// </summary>
/// <param name="model">The given Edm model.</param>
/// <param name="functionName">The custom function name.</param>
/// <returns>'False' if custom function signature doesn't exist. 'True' if function has been removed successfully</returns>
/// <exception cref="ArgumentNullException">Arguments are null, or function signature return type is null</exception>
public static bool RemoveCustomUriFunction(this IEdmModel model, string functionName)
{
ExceptionUtils.CheckArgumentNotNull(model, "model");
ExceptionUtils.CheckArgumentStringNotNullOrEmpty(functionName, "functionName");

CustomUriFunctionsAnnotation funAnnotations = model.GetOrSetCustomUriFunctionAnnotation();
return funAnnotations.CustomFunctions.Remove(functionName, out _);
}

/// <summary>
/// Returns a list of name-signature pairs for a function name.
/// </summary>
/// <param name="model">The given Edm model.</param>
/// <param name="functionCallToken">The name of the function to look for.</param>
/// <param name="nameSignatures">
/// Output for the list of signature objects for matched function names, with canonical name of the function;
/// null if no matches found.
/// </param>
/// <param name="enableCaseInsensitive">Whether to perform case-insensitive match for function name.</param>
/// <returns>true if the function was found, or false otherwise.</returns>
public static bool TryGetCustomFunction(this IEdmModel model, string functionCallToken, out IList<KeyValuePair<string, FunctionSignatureWithReturnType>> nameSignatures,
bool enableCaseInsensitive = false)
{
ExceptionUtils.CheckArgumentNotNull(model, "model");

nameSignatures = null;
CustomUriFunctionsAnnotation funAnnotations = model.GetAnnotationValue<CustomUriFunctionsAnnotation>(model);
if (funAnnotations == null)
{
return false;
}

IList<KeyValuePair<string, FunctionSignatureWithReturnType>> bufferedKeyValuePairs
= new List<KeyValuePair<string, FunctionSignatureWithReturnType>>();

foreach (KeyValuePair<string, FunctionSignatureWithReturnType[]> func in funAnnotations.CustomFunctions)
{
if (func.Key.Equals(functionCallToken, enableCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
foreach (FunctionSignatureWithReturnType sig in func.Value)
{
bufferedKeyValuePairs.Add(new KeyValuePair<string, FunctionSignatureWithReturnType>(func.Key, sig));
}
}
}

// Setup the output values.
nameSignatures = bufferedKeyValuePairs.Count != 0 ? bufferedKeyValuePairs : null;

return nameSignatures != null;
}

private static CustomUriFunctionsAnnotation GetOrSetCustomUriFunctionAnnotation(this IEdmModel model)
{
CustomUriFunctionsAnnotation annotation = model.GetAnnotationValue<CustomUriFunctionsAnnotation>(model);
if (annotation == null)
{
annotation = new CustomUriFunctionsAnnotation();
model.SetAnnotationValue(model, annotation);
}

return annotation;
}

/// <summary>
/// Add a custom uri function to extend uri functions.
/// In case the function name already exists as a custom function, the signature will be added as an another overload.
Expand Down Expand Up @@ -71,7 +235,7 @@ public static void AddCustomUriFunction(string functionName, FunctionSignatureWi
}
}

AddCustomFunction(functionName, functionSignature);
AddCustomFunction(CustomFunctions, functionName, functionSignature);
}
}

Expand Down Expand Up @@ -184,14 +348,14 @@ IList<KeyValuePair<string, FunctionSignatureWithReturnType>> bufferedKeyValuePai

#region Private Methods

private static void AddCustomFunction(string customFunctionName, FunctionSignatureWithReturnType newCustomFunctionSignature)
private static void AddCustomFunction(IDictionary<string, FunctionSignatureWithReturnType[]> customFunctions, string customFunctionName, FunctionSignatureWithReturnType newCustomFunctionSignature)
{
FunctionSignatureWithReturnType[] existingCustomFunctionOverloads;

// In case the function doesn't already exist
if (!CustomFunctions.TryGetValue(customFunctionName, out existingCustomFunctionOverloads))
if (!customFunctions.TryGetValue(customFunctionName, out existingCustomFunctionOverloads))
{
CustomFunctions.Add(customFunctionName, new FunctionSignatureWithReturnType[] { newCustomFunctionSignature });
customFunctions.Add(customFunctionName, new FunctionSignatureWithReturnType[] { newCustomFunctionSignature });
}
else
{
Expand All @@ -207,7 +371,7 @@ private static void AddCustomFunction(string customFunctionName, FunctionSignatu
}

// Add the custom function as an overload to the same function name
CustomFunctions[customFunctionName] =
customFunctions[customFunctionName] =
existingCustomFunctionOverloads.Concat(new FunctionSignatureWithReturnType[] { newCustomFunctionSignature }).ToArray();
}
}
Expand Down
Loading