'.Net 6 Nullable String Parameter set to "Microsoft.OData.ODataNullValue" instead of null

I'm in the process of upgrading an Entity Framework API project from .Net 5 to .Net 6 and I'm having issues with one of the functions when trying to pass a null value for a string parameter.

As part of the upgrade to .Net 6 all packages were updated to the latest version.
Microsoft.AspNetCore.OData went from v7.5.6 to 8.0.10
Microsoft.EntityFrameworkCore went from v5.0.3 to 6.0.4
If there are other package versions that might be relevant please let me know.

I'm trying to keep the API signature and results the same, to avoid needing to adjust a number of consumers. I'm aware I can define the controller action to be called as /odata/VPDElemCats(ElementId=1) and this is probably how it should have been defined and called when vpdNo needed to be null.

I have a few work arounds but I'm looking for the proper solution if possible. I have read Optional function argument in OData v4 web api but a number of the links are dead.

Function definition

var vpdElemCats = builder.Function("VPDElemCats")
  .ReturnsCollection<vPayrollElemCats>();
var paramElemCatVPDNo = vpdElemCats.Parameter<string>("VPDNo");
paramElemCatVPDNo.Optional();
paramElemCatVPDNo.HasDefaultValue(null);
paramElemCatVPDNo.Nullable = true;
vpdElemCats.Parameter<int>("ElementId");

Controller Action

[HttpGet("/odata/VPDElemCats(VPDNo={vpdNo},ElementId={elementId})")]
public IActionResult VPDElemCats([FromODataUri] string vpdNo, [FromODataUri] int elementId)
{
...

Prior to the upgrade /odata/VPDElemCats(VPDNo=null,ElementId=1) would set vpdNo to null, elementId to 1 and /odata/VPDElemCats(VPDNo='074',ElementId=1) would set vpdTo to "074" and elementId to 1.

After the upgrade the first API call is setting vpdNo to "Microsoft.OData.ODataNullValue". Several of the failed attempts I wasn't expecting to work but I was wondering if they might help get me headed in the correct direction.

I suspect my issue is either a lack of understanding of OData convention, rather than a bug with OData but I can't figure out why what worked in .Net 5 isn't working in .Net 6, or that this isn't supported as the correct way to pass a null value would be an optional parameter with a signature that excludes the parameter entirely.

The workarounds I have are:

Work Around 1

Change the [FromRoute] to [FromODataUri] - but this affects (I believe - not confirmed yet as swagger is hanging while trying to load at the moment) how the API is documented in swagger.

This works even if I remove the Optional(), HasDefaultValue(null) and Nullable = true lines from the function definition.

Work Around 2

Check if (vpdNo == "Microsoft.OData.ODataNullValue") and, if it does, set it to null.


I've tried the below without success:

Failed Attempt 1

Changing the controller action to:

[HttpGet("/odata/VPDElemCats(VPDNo={vpdNo},ElementId={elementId})")]
public IActionResult VPDElemCats([FromRoute] int elementId, [FromRoute] string vpdNo = null)

vpdNo is still set to "Microsoft.OData.ODataNullValue".

Failed Attempt 2

Changing the controller action to:

[HttpGet("/odata/VPDElemCats(VPDNo='{vpdNo}',ElementId={elementId})")]
[HttpGet("/odata/VPDElemCats(VPDNo=null,ElementId={elementId})")]
public IActionResult VPDElemCats([FromRoute] int elementId, [FromRoute] string vpdNo = null)

Successfully set vpdNo to null but the result set is not in the oData format, which means the consumers need to change to handle the different format. Example of non-OData format:

[
  {
    "PayrollId": null,
...

Example of OData format:

{"@odata.context":".../odata/$metadata#Collection(api.vPayrollElemCats)",
  "value":
    [
      {
        "PayrollId":165
...

Failed Attempt 3

Changing the controller action to:

[HttpGet("/odata/VPDElemCats(VPDNo={vpdNo},ElementId={elementId})")]
[HttpGet("/odata/VPDElemCats(VPDNo=null,ElementId={elementId})")]
public IActionResult VPDElemCats([FromRoute] int elementId, [FromRoute] string vpdNo = null)

and function definition to:

var vpdElemCats = builder.Function("VPDElemCats")
    .ReturnsCollection<vPayrollElemCats>();
var paramElemCatVPDNo = vpdElemCats.Parameter<string>("VPDNo");
paramElemCatVPDNo.Optional();
paramElemCatVPDNo.Nullable = false;
vpdElemCats.Parameter<int>("ElementId");

passing non-null VPD works but passing null gives:

ODataException: Type verification failed. Expected non-nullable type 'Edm.String' but received a null value.

Microsoft.OData.ODataUriConversionUtils.VerifyAndCoerceUriPrimitiveLiteral(object primitiveValue, string literalValue, IEdmModel model, IEdmTypeReference expectedTypeReference) ODataException: The parameter value (null) from request is not valid. The parameter value should be format of type 'Edm.String'.

Microsoft.AspNetCore.OData.Routing.Template.SegmentTemplateHelpers.Match(ODataTemplateTranslateContext context, IEdmFunction function, IDictionary<string, string> parameterMappings)

The fact this one is recognising the vpdNo as null rather than a string "Microsoft.OData.ODataNullValue" I find most confusing!

Failed Attempt 4

As attempt 3 except allowing leaving vpdNo as nullable i.e. paramElemCatVPDNo.Nullable = true;

Then calling for null vpdNo gives:

AmbiguousMatchException: The request matched multiple endpoints. Matches:

ES_ESROps.Controllers.ElementsController.VPDElemCats (ES_ESROps) ES_ESROps.Controllers.ElementsController.VPDElemCats (ES_ESROps)



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source