Search code examples
c#.netasp.net-coreasp.net-web-api.net-8.0

How to create custom parameter attribute FromActionArguments in C# like FromQuery?


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. ...

Solution

  • 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. */ }