AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

Custom Method with nullable properties

Open bjelbo opened this issue 2 years ago • 10 comments

I am experiencing problems when creating Custom Methods that should handle nullable properties.

My database entity contains nullable properties. e.g.

public DateTime? SomeDateTime { get; set; }

My custom method also takes a nullable DateTime as argument. e.g.

public static Date? DateOnly(DateTime? date)
{
    if (date == null)
    {
        return null;
    }
    return date.Value.ToUniversalTime().Date;
}

An OData query with the following compute statement throws an exception.

$apply=compute(DateOnly(SomeDateTime) as date)

The Exception is:

"'Expression of type 'System.DateTime' cannot be used for parameter of type 'System.Nullable`1[System.DateTime]' of method 'System.Nullable`1[Microsoft.OData.Edm.Date] DateOnly(System.Nullable`1[System.DateTime])' (Parameter 'arg0')"

The problem lies in the method ExpressionBinderHelper.MakeFunctionCall

In this method line 228 the following method is called

// if the argument is of type Nullable<T>, then translate the argument to Nullable<T>.Value as none
// of the canonical functions have overloads for Nullable<> arguments.
functionCallArguments = ExtractValueFromNullableArguments(functionCallArguments);

Then when calling Expression.Call it will fail because 'System.DateTime' is not System.Nullable`1[System.DateTime]

bjelbo avatar Nov 03 '23 10:11 bjelbo

@SirTipzy Since you have a method call "DateOnly(...)", Can you share me your configuration about this method into the model? Otherwise, ODL doesn't know this method?

And, if you can share a repro using a github repository, it can help us to dig more for you. Thanks.

xuzhg avatar Nov 06 '23 19:11 xuzhg

@xuzhg I unfortunately do not have a github repo I can share.

I do not have problems for your OData Library to recognize and use my methods. I would not even get the exception I am getting if it did not know of the method.

Here is my class I use to add my methods. I call the UseCustomODataFunctions at startup

public static class CustomODataFunctionService
{
    public static void UseCustomODataFunctions(this IApplicationBuilder app)
    {
        var methodInfos = typeof(CustomODataFunctions).GetMethods(BindingFlags.Static | BindingFlags.Public);
        foreach (var methodInfo in methodInfos)
        {
            RegisterCustomFunction(methodInfo);
        }          
    }

    private static void RegisterCustomFunction(MethodInfo methodInfo)
    {
        var functionName = methodInfo.Name;
        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 underlyingType = Nullable.GetUnderlyingType(type) ?? type;
        var primitiveTypeKind = EdmCoreModel.Instance.GetPrimitiveTypeKind(underlyingType.Name);
        if(underlyingType == typeof(DateTime))
        {
            primitiveTypeKind = EdmPrimitiveTypeKind.DateTimeOffset;
        }
        var isNullable = type.IsClass || Nullable.GetUnderlyingType(type) != null;
        return new EdmPrimitiveTypeReference(
            EdmCoreModel.Instance.GetPrimitiveType(primitiveTypeKind),
            isNullable);
    }
}

I have similar methods that have string parameters. These functions work as they are not explicitly set as nullables. I have also tested with none nullable DateTimes. These work if I change my DateOnly parameter to a normal DateTime. But as all my DateTime entity attributes are nullable in my database, I need these custom methods to contain nullable parameters.

A solution for you is to make a new method called ExpressionBinderHelper.MakeCustomFunctionCall where this method looks somewhat like this

    public static Expression MakeCustomFunctionCall(MemberInfo member, ODataQuerySettings querySettings, params Expression[] arguments)
    {
        Contract.Assert(member.MemberType == MemberTypes.Property || member.MemberType == MemberTypes.Method);
        
        Expression functionCall;
        if (member.MemberType == MemberTypes.Method)
        {
            MethodInfo method = member as MethodInfo;
            if (method.IsStatic)
            {
                functionCall = Expression.Call(null, method, arguments);
            }
            else
            {
                functionCall = Expression.Call(arguments.First(), method, arguments.Skip(1));
            }
        }
        else
        {
            var functionCallArguments = ExtractValueFromNullableArguments(arguments);
            
            functionCall = Expression.Property(functionCallArguments.First(), member as PropertyInfo);
        }

        return functionCall;
    }

I have tested creating this method in a copy of your repository and it will work.

bjelbo avatar Nov 07 '23 08:11 bjelbo

@SirTipzy I see. Do you mind sharing a contribution based on your investigation?

By the way, OData spec has this built-in function at: https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_date, did you try to use 'date()' method directly? not use your customized method?

xuzhg avatar Nov 07 '23 17:11 xuzhg

@xuzhg Yes I know of this function, but it does not work in your library. Your library just returns the same datetime value as was passed to the function.

image

We also have other custom methods, DateOnly was just our most simple one I used as an example.

Yes, I can share a contribution.

bjelbo avatar Nov 08 '23 08:11 bjelbo

@bjelbo You can NOT override the corresponding virtual methods to customize your scenarios?

xuzhg avatar Nov 13 '23 04:11 xuzhg

@bjelbo You can NOT override the corresponding virtual methods to customize your scenarios?

I have done so for FilterBinder, but the same is not possible for ComputeBinder if I am not mistaking. Please correct me if I am wrong.

bjelbo avatar Nov 13 '23 10:11 bjelbo

@xuzhg I am still very much interested in getting some clarification on this bug. Also I have left a comment for you on the pull request I am still waiting for a reply on.

bjelbo avatar Dec 19 '23 15:12 bjelbo

@xuzhg @KenitoInc This is still an issue which I really hope will be resolved. Locally I have tested my fix in PR https://github.com/OData/AspNetCoreOData/pull/1090 thoroughly and it works as intended. @xuzhg I have a comment on your comment in the PR. Your comment in the PR "MakeFunctionCall does MORE than MakeCustomFunctionCall. So, it's enough and safe to change using MakeCustomFunctionCall?" From my tests is that custom function calls are applied after querying the database, and therefore, the MakeCustomFunctionCall does less than MakeFunctionCall.

bjelbo avatar Jan 09 '24 09:01 bjelbo

@bjelbo Can you share the working code for CustomFunctions. I also have same use case https://github.com/OData/AspNetCoreOData/discussions/1229

rbalagangadharan avatar May 14 '24 08:05 rbalagangadharan

@rbalagangadharan I have this PR for it that fixes the issue. Though I have not had the time to create the unit tests they are asking. https://github.com/OData/AspNetCoreOData/pull/1090

bjelbo avatar Jun 26 '24 08:06 bjelbo