Giter Site home page Giter Site logo

Comments (5)

sakno avatar sakno commented on June 16, 2024 1

This is a limitation of ASP.NET Core Model Binding, not an issue of Optional<T>. You need to have your own Type Converter for Optional<T> as described here.

from dotnext.

sakno avatar sakno commented on June 16, 2024 1

Moreover, the binding architecture of ASP.NET Core passes empty string to TryParse for the type with custom binding. In case of Optional<string> it means that you'll get non-empty Optional container with empty string instead of None.

from dotnext.

sakno avatar sakno commented on June 16, 2024 1

According to docs, TryParse is the only way of binding for non-complex types. I see no benefits of Optional<T> for query parameters. Even nullable string is redundant because ASP.NET Core never passes null to the parameter.

from dotnext.

michaelbmorris avatar michaelbmorris commented on June 16, 2024 1

Thanks for the help. Good point that string? can't be null if the parameter is included in the query string. I ended up going with the solution below. It works well enough for my case. Hopefully someone else will find it useful too if they need optional query parameters. Sadly it doesn't make use of the Optional<T> struct though.

public readonly struct OptionalString(string? value)
{
    public static OptionalString Undefined => new()
    {
        IsUndefined = true
    };

    public bool IsUndefined { get; private init; } = false;

    public string? Value { get; } = value;
}

public class OptionalStringModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            bindingContext.Result = ModelBindingResult.Success(OptionalString.Undefined);
            return Task.CompletedTask;
        }

        if (valueProviderResult.Values.Count > 1)
        {
            bindingContext.ModelState.TryAddModelError(modelName, "Only a single value is allowed.");
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }

        OptionalString optionalString;

        if (string.IsNullOrEmpty(valueProviderResult.FirstValue))
        {
            // As pointed out, the value will never actually be null here so an empty string will have to suffice.
            // I'm not sure how I'd handle a scenario where the string could be undefined, null, empty, whitespace, or non-empty and non-whitespace.
            // I think it would require binding an additional parameter value.
            optionalString = new OptionalString(null);
        }
        else
        {
            optionalString = new OptionalString(valueProviderResult.FirstValue);
        }

        bindingContext.Result = ModelBindingResult.Success(optionalString);
        return Task.CompletedTask;
    }
}

public class QueryParameters
{
    // Could of course be registered globally via a ModelBinderProvider instead of an attribute.
    [ModelBinder(BinderType = typeof(OptionalStringModelBinder))]
    public OptionalString SomeString { get; init; }
}

[ApiController]
public class QueryController : ControllerBase
{
    [HttpGet("query")]
    public IActionResult Query([FromQuery] QueryParameters parameters)
    {
        // Do stuff here
    }
}

GET /query : Undefined
GET /query?SomeString : null
GET /query?SomeString=value : "value"

I have another model binder that I'm using for long?s as well.

// This is identical to OptionalString except for the value type.
// It's a shame generics don't work with model binding, but if they did I could just use Optional<T> instead of making these knockoffs.
public readonly struct OptionalLong(long? value)
{
    public static OptionalLong Undefined => new()
    {
        IsUndefined = true
    };

    public bool IsUndefined { get; private init; } = false;

    public long? Value { get; } = value;
}

public class OptionalLongModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            bindingContext.Result = ModelBindingResult.Success(OptionalLong.Undefined);
            return Task.CompletedTask;
        }

        if (valueProviderResult.Values.Count > 1)
        {
            bindingContext.ModelState.TryAddModelError(modelName, "Only a single value is allowed.");
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }

        var stringValue = valueProviderResult.FirstValue;

        if (string.IsNullOrWhiteSpace(stringValue))
        {
            bindingContext.Result = ModelBindingResult.Success(new OptionalLong(null));
        }
        else if (long.TryParse(stringValue, out var value))
        {
            bindingContext.Result = ModelBindingResult.Success(new OptionalLong(value));
        }
        else
        {
            bindingContext.ModelState.TryAddModelError(modelName, "Value must be null or a long.");
            bindingContext.Result = ModelBindingResult.Failed();
        }

        return Task.CompletedTask;
    }
}

from dotnext.

michaelbmorris avatar michaelbmorris commented on June 16, 2024

Agreed regarding your comment about TryParse not being helpful for this situation. I thought I'd be able to use an IModelBinder but I'm still getting the same error:

public class OptionalStringBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            bindingContext.Result = ModelBindingResult.Success(Optional.None<string>());
            return Task.CompletedTask;
        }

        var value = valueProviderResult.FirstValue;
        bindingContext.Result = ModelBindingResult.Success(new Optional<string>(value));
        return Task.CompletedTask;
    }
}

Is this something worth asking about on the aspnetcore project, or would you advise that I go back to using nullable strings for my scenario?

from dotnext.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.