jpcrrs
jpcrrs

Reputation: 63

Core 3.1 - Binding some properties from Body and some others from Header

I have the following model:

public class Network {
    public string Network { get; set; }
    public string Cidr { get; set; }
    public string TenantId { get; set; }
}

And the following endpoint:

public async Task<ActionResult<Network>> PostNetwork(Network network)
{
    ...
}

And would like to know if it's possible to bind Network and Cidr with values from my post Body, and the TenantId with the value from a specific header.

I don't wan't to put entity.TenantId = HttpContext.Request.Headers["TenantId"]; all over my controllers endpoint because this pattern is necessary to several entities/endpoints.

I've tried to create a custom middle-ware to edit my Body content, but the changes wasn't reflected in my controller. Also tried to use a custom DataBinder but without success.

Upvotes: 2

Views: 1978

Answers (3)

Prisoner ZERO
Prisoner ZERO

Reputation: 14166

I prefer this solution because I feel it's simpler & easier to read and maintain.

That said...
This should work 95% of the time.
However, truly "Complex Objects" may need their own JSON Deserializer class.

[HttpPost]
public IActionResult Save([ModelBinder(typeof(EmployeeModelBinder))] Employee entity)
{
    // Do awesome stuff here
}

[HttpPost]
public IActionResult Find([ModelBinder(typeof(EmployeeModelBinder))] EmployeeFilter filter)
{
    // Do awesome stuff here
}

public class EmployeeModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
            throw new ArgumentNullException(nameof(bindingContext));

        var key = bindingContext.ModelName;
        var result = bindingContext.ValueProvider.GetValue(key);

        // Exists?
        if (result.Length == 0)
            return Task.CompletedTask;

        // Deserialize
        var json = result.ToString();
        switch (key)
        {
            case "entity":
                var entity = JsonConvert.DeserializeObject<Employee>(json);
                bindingContext.Result = ModelBindingResult.Success(entity);
                break;
                
            case "filter":
                var filter = JsonConvert.DeserializeObject<EmployeeFilter>(json);
                bindingContext.Result = ModelBindingResult.Success(filter);
                break;
        }

        return Task.CompletedTask;
    }
}

Upvotes: 0

Belyansky Ilya
Belyansky Ilya

Reputation: 381

My task was to fill the property of each object which implements the specific interface from a header. I resolved it by decorating the default ModelBinderFactory.

public sealed class CustomModelBinderFactory : ModelBinderFactory, IModelBinderFactory
{
    public CustomModelBinderFactory(
        IModelMetadataProvider metadataProvider,
        IOptions<MvcOptions> options,
        IServiceProvider serviceProvider)
        : base(metadataProvider, options, serviceProvider)
    {
    }

    public new IModelBinder CreateBinder(ModelBinderFactoryContext context)
    {
        var binder = base.CreateBinder(context);

        return new NodeIdBinder(binder);
    }

    /// <summary>
    /// Wrapper on the resolved by <see cref="ModelBinderFactory"/> binder.
    /// </summary>
    private sealed class NodeIdBinder : IModelBinder
    {
        private readonly IModelBinder _originalBinder;
               
        public NodeIdBinder(IModelBinder originalBinder)
        {
            _originalBinder = originalBinder;
        }
        
        public async Task BindModelAsync(ModelBindingContext bindingContext)
        {
            await _originalBinder.BindModelAsync(bindingContext);

            if (bindingContext.Result.Model is IQueryWithNode model)
            {
                var nodeId = bindingContext.HttpContext.Request.Headers["X-Node"];
                model.NodeId = nodeId;
            }
        }
    }
}

The interface example

public interface IQueryWithNode
{
    public string NodeId { get; set; }
}

This factory should be added to a container. I didn't find an official way to do it, so I used

services.AddSingleton<IModelBinderFactory, CustomModelBinderFactory>();

Now any request which consumes the object that implements this interface will have the NodeId property filled.

Upvotes: 1

George
George

Reputation: 6739

So you'll need to create a custom model binder and add an attribute to your class.

The model binder will inherit from IModelBinder and assuming your data is JSON

public class NetworkModelBinder : IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        Network model;

        string bodyAsText = await new StreamReader(bindingContext.HttpContext.Request.Body).ReadToEndAsync();
        model = JsonConvert.DeserializeObject<Network>(bodyAsText);
        model.TenantId = bindingContext.HttpContext.Request.Headers["TenantId"];
        bindingContext.Result = ModelBindingResult.Success(model);
    }
}

Then add the ModelBinder attribute to the class to tell it to use that model binder

[ModelBinder(typeof(NetworkModelBinder))]
public class Network
{
    public string Network { get; set; }
    public string Cidr { get; set; }
    public string TenantId { get; set; }
}

Upvotes: 1

Related Questions