Define custom string function in OData4

Viewed 183

I'm trying to implement a custom function for OData which should convert string -> string. But all sources in the internet including MS docs are very unclear about how can it be done.

Consider following registration:

private static ODataConventionModelBuilder AddOrder(this ODataConventionModelBuilder builder)
{
    var entity = builder.EntitySet<Order>(typeof(Order).Name.ToLower());
    entity.EntityType.HasKey(x => x.Id);
    var function = entity.EntityType.Function("ToLower2");
    function.IsComposable = true;
    function.SupportedInFilter = true;
    function.Parameter<string>("s");
    function.Returns<string>();
    return builder;
}

Here is an OData controller:

[ODataRoutePrefix("order")]
public sealed class OrdersODataController : ODataController
{
    [HttpGet]
    [EnableQuery(HandleNullPropagation = HandleNullPropagationOption.True)]
    [ODataRoute]
    public IQueryable<Order> Get()
    {
        return TestHelper.GetMyQueryable():
    }

    [HttpGet]
    public string ToLower2(string s)
    {
        return s;
    }
}

Now when calling it:

curl 'http://localhost:10100/api/odata/order?$filter=((contains(Default.ToLower2(SomeField),+%27gdgdg%27))&$select=Id&skip=0&$top=5'

The response is:

"message":"The query specified in the URI is not valid. An unknown function with name 'Default.ToLower2' was found. This may also be a function import or a key lookup on a navigation property, which is not allowed."

Is it possible to implement a custom function like this?

1 Answers

I've tried everything docs and examples was saying but it didn't work to me.

I ended up debugging and checking how OData determines which function should be called. Then I've checked APIs that allowed me to register required callbacks. This is what I've ended with:

  1. Declare static class with methods that should be registered:

    public static class CustomFunctions
    {
        public static bool ContainsIgnoreCase(string text, string toFind) => 
            throw ShouldBeTranslatedException;
    
        private static Exception ShouldBeTranslatedException => 
            new Exception("Should be translated to MongoDb/SQL");
    }
    
  2. Register it to OData:

    private static void RegisterCustomFunction(string functionName)
    {
        var methodInfo = typeof(CustomFunctions).GetMethod(functionName)!;
        var returnType = TypeToReference(methodInfo.ReturnType);
        var args = methodInfo.GetParameters()
            .Select(x => TypeToReference(x.ParameterType))
            .ToArray();
        var signature = new FunctionSignatureWithReturnType(returnType, args);
        ODataUriFunctions.AddCustomUriFunction(functionName, signature, methodInfo);
    }
    
    private static IEdmTypeReference TypeToReference(Type type)
    {
        var primitiveTypeKind = EdmCoreModel.Instance.GetPrimitiveTypeKind(type.Name);
        var isNullable = type.IsClass || Nullable.GetUnderlyingType(type) != null;
        return new EdmPrimitiveTypeReference(
            EdmCoreModel.Instance.GetPrimitiveType(primitiveTypeKind),
            isNullable);
    }
    
    ...
    
    RegisterCustomFunction(nameof(CustomFunctions.ContainsIgnoreCase));
    
  3. Now OData will be happy with it and you just need to write your translation code to convert it into SQL (or mongo query in my case)

Related