Reputation: 63
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
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
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
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