1

I would like to create attribute that will tell that parameter should be filled from action arguments, not from query, route, header or body.

ASP.NET is already binding it correctly from ActionArguments on its own, but I want to prevent it from binding via other means.

I have ActionFilterAttribute for method that set

context.ActionArguments["set"] = filledSet;

So far I'm in my methods I have

[FillSet]
public void MyMethod([FromServices] HashSet<string> set = null)
{
    // Do stuff.
}

If I don't use = null I will get

System.InvalidOperationException: No service for type System.Collections.Generic.HashSet1[System.String]' has been registered.

I used FromService so it shouldn't try to bind from other sources.

I would like to have something like

[FillSet]
public void MyMethod([FromActionArguments] HashSet<string> set)
{
    // Do stuff.
}

EDIT1: I tried to change it to BindNeverAttribute but now I got

System.InvalidOperationException 'Test.Controllers.TestController.Get (Test)' has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. ...
3
  • Do you mean a custom model binder has the same effect as FromService? Commented Jul 22, 2024 at 9:47
  • After few days of sleep, and new vigor for searching again I found BindNeverAttribute, that I will try. Commented Jul 23, 2024 at 13:51
  • I want to disable binding on method's parameter so user will not be able to fill it. parameter will be set by my custom ActionFilterAttribute. Commented Jul 23, 2024 at 14:02

2 Answers 2

1

In the end I create Attribute with name SetDefault. I'm not sure, if there might be some bad case, but for now it seems to work.

Classes that I created:

/// <inheritdoc/>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class SetDefaultAttribute : Attribute, IBindingSourceMetadata
{
/// <inheritdoc/>
    public BindingSource BindingSource => BindingSource.Custom;
}
/// <inheritdoc/>
public class DefaultValueModelBinder : IModelBinder
{
    /// <inheritdoc/>
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        ArgumentNullException.ThrowIfNull(bindingContext);
        var modelType = bindingContext.ModelType;
        object defaultValue = GetDefaultValue(modelType);
        bindingContext.Result = ModelBindingResult.Success(defaultValue);
        return Task.CompletedTask;
    }
    private object GetDefaultValue(Type type)
    {
        if (type.IsValueType)
        {
            return Activator.CreateInstance(type);
        }
        return null;
    }
}
/// <inheritdoc/>
public class DefaultValueModelBinderProvider : IModelBinderProvider
{
    /// <inheritdoc/>
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        ArgumentNullException.ThrowIfNull(context);
        if ((context.Metadata is DefaultModelMetadata defaultModelMetadata
            && defaultModelMetadata.Attributes?.ParameterAttributes?.Any(x => x is SetDefaultAttribute) == true)
            || context.Metadata?.BinderType == typeof(DefaultValueModelBinder))
        {
            return new DefaultValueModelBinder();
        }
        return null;
    }
}

Added to startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new DefaultValueModelBinderProvider());
    });
}

Used like:

[FillSet]
[HttpPost("test")]
public void MyMethod([FromQuery] int id, [SetDefault] HashSet<string> set, [SetDefault] int someNumber)
{ /* Code here. */ }
Sign up to request clarification or add additional context in comments.

Comments

0

The OnActionExecuting actually runs after model binding. It means "context.ActionArguments["set"] = filledSet;" is not model binding at all. So you just need to create a model binder that prevent from any binding resource. You could try following sample.

    public class FillSet : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            context.ActionArguments["set"] = new HashSet<string>() { "from filter" };
        }
    }

MyModelBinder.cs

    public class MyModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var valueFromBinding= bindingContext.ValueProvider.GetValue("set").FirstValue;

            if (valueFromBinding != null)
            {
                //if there are value from any binding source, you could return a invalid response. 
                bindingContext.Result = ModelBindingResult.Failed();
            }
            // if there is no value from any binding source, we bind set to a empty value.
            bindingContext.Result = ModelBindingResult.Success(new HashSet<string>());
            return Task.CompletedTask;
        

Controller

        [FillSet]
        [HttpPost("test")]
        public void MyMethod([ModelBinder(typeof(MyModelBinder))] HashSet<string> set )
        {
            var result = set;
        }

Test result
enter image description here

1 Comment

But I want to add it to any type not only HashSet<string>. Also if it's not nullable type.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.