'FromBody, ApiController and BindProperty not working together - binder not binding
I have REST API in .net5.0.
My controller inherits ControllerBaseand has the ApiController attribute.
My simple model and I need to bind value from Claim to a single property.
My model looks like this:
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
[Required]
[JsonProperty("description"), JsonPropertyName("description")]
public string Description { get; set; }
[Required]
[JsonProperty("version"), JsonPropertyName("version")]
public string Version { get; set; }
[Required]
[JsonProperty("public"), JsonPropertyName("public")]
public bool Public { get; set; }
[BindProperty(BinderType = typeof(CreatedByModelBinder))]
[ModelBinder(BinderType = typeof(CreatedByModelBinder))]
[Newtonsoft.Json.JsonConverter(typeof(CreatedByConverter))]
[SwaggerIgnore]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
and a simple controller method:
[HttpPost]
[Produces("application/json")]
[ProducesResponseType(typeof(Plugin), (int)HttpStatusCode.OK)]
public async Task<IActionResult> Add(CreatePlugin plugin)
{
//some logic
return Ok();
}
My request looks like this:
{
"name": "test",
"description": "sample description",
"version": "1.0",
"public": true
}
Because of ApiController attribute, my model binder isn't working. The same happens when I remove ApiController attribute from my controller and add FromBody to Add method parameter (ref: Custom model binder not firing for a single property in ASP.NET Core 2).
When I remove both attributes, the binder works fine, but the rest of the dto is empty.
I've tried using a custom JsonConverter, but it only works if I pass something in the created_by field.
How can I bind a single property of the model to something that isn't coming from the request data (in my case to claims)?
My workaround for now is to manually assign property value in each method like so:
plugin.CreatedBy = _identityContext.UserId;
Most of my models have CreatedBy and EditedBy properties, and I'd like to automatically fill them with a specific value (in this case with a value from a Claim)
Solution 1:[1]
You can custom the model binding like below:
public class CreatedByModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var claim = int.Parse(bindingContext.HttpContext.User.FindFirst("KeyName").Value);
bindingContext.Result = ModelBindingResult.Success(claim);
return Task.CompletedTask;
}
}
Model:
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
//...
[NSwag.Annotations.SwaggerIgnore]
[ModelBinder(BinderType = typeof(CreatedByModelBinder))]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
Controller:
Not sure how do you add the claims, I just add the claim in cookie authentication.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAsync()
{
var claims = new List<Claim>
{
new Claim("KeyName","4")
};
var authProperties = new AuthenticationProperties
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1),
IsPersistent = false
};
const string authenticationType = "Cookies";
var claimsIdentity = new ClaimsIdentity(claims, authenticationType);
await HttpContext.SignInAsync(authenticationType, new ClaimsPrincipal(claimsIdentity), authProperties);
return Ok();
}
[HttpPost]
[Produces("application/json")]
public async Task<IActionResult> Add([FromForm]CreatePlugin plugin) //add FromForm....
{
//some logic
return Ok();
}
}
Result:
UPDATE:
If you must using FromBody, you can custom JsonConverter for the model:
public class CreatedByConverter : Newtonsoft.Json.JsonConverter
{
private readonly IHttpContextAccessor httpContextAccessor;
public CreatedByConverter(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(CreatePlugin));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
var claim = int.Parse(httpContextAccessor.HttpContext.User.FindFirst("KeyName").Value);
JObject obj = JObject.Load(reader);
CreatePlugin root = new CreatePlugin();
root.Name = (string)obj["name"];
root.Description = (string)obj["description"];
root.Version = (string)obj["version"];
root.Public = (bool)obj["public"];
root.CreatedBy = claim;
return root;
}
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
o.WriteTo(writer);
}
}
}
Model:
Note: If you get the claim from HttpContext, you need inject the service and cannot use JsonConverter attribute any more. You need register it in Startup.cs
public class CreatePlugin
{
[Required]
[JsonProperty("name"), JsonPropertyName("name")]
public string Name { get; set; }
//more property...
[NSwag.Annotations.SwaggerIgnore]
//[Newtonsoft.Json.JsonConverter(typeof(CreatedByConverter))]
[JsonProperty("created_by"), JsonPropertyName("created_by")]
public int CreatedBy { get; set; }
}
Controller:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAsync()
{
//add claims like the first answer..
return Ok();
}
[HttpPost]
[Produces("application/json")]
public async Task<IActionResult> Add(CreatePlugin plugin)
{
//some logic
return Ok(plugin);
}
}
Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var httpContextAccessor = new HttpContextAccessor();
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.Add(new CreatedByConverter(httpContextAccessor));
});
services.AddSingleton<IHttpContextAccessor>(httpContextAccessor);
services.AddSwaggerDocument();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => o.LoginPath = new PathString("/account/login"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseOpenApi();
app.UseSwaggerUi3();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |

