Asked by:
Use Middleware to Set Model Values from Controller

Question
-
User-759426482 posted
Let's assume I have a controller with several actions in it all of which take simple POCO objects. Right now, I have to manually pull in the user's claims and set those claim values to a property in the object, see below.
[HttpPost] [Route("/announcements")] public async Task<IActionResult> PostAnnouncementAsync(PostAnnouncementCommand command) { command.AccountId = new Guid(((ClaimsIdentity) User.Identity).FindFirst(x => x.Type == "AccountId").Value); return Ok(await Mediator.Send(command)); } [HttpPost] [Route("/notifications")] public async Task<IActionResult> PostAnnouncementAsync(PostNotificationCommand command) { command.AccountId = new Guid(((ClaimsIdentity) User.Identity).FindFirst(x => x.Type == "AccountId").Value); return Ok(await Mediator.Send(command)); } [HttpPost] [Route("/users")] public async Task<IActionResult> PostUserAsync(PostUserCommand command) { command.AccountId = new Guid(((ClaimsIdentity) User.Identity).FindFirst(x => x.Type == "AccountId").Value); return Ok(await Mediator.Send(command)); }
What I'd like to do is remove the need to manually add that same line of code (that sets command.AccountId) for every single controller action, and instead put it in middleware. I'm trying to understand the right way to do this, since the middleware's httpcontext.Request seems to only provide a stream. Is there an efficient way for me to work with the Command POCO object that ASP.NET will have already deserialized for me somewhere else along the pipeline?
Tuesday, September 8, 2020 3:11 PM
All replies
-
User-474980206 posted
rather than middleware which can only access the request stream, you want a custom binder, that fills in the model:
https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-3.1
Tuesday, September 8, 2020 3:45 PM -
User-759426482 posted
Thanks for the reply, bruce. That makes sense based on the docs, however I'm struggling to find how to exactly implement this. Given that I can have more than one type of object that needs binding (PostAnnouncementCommand, PostNotificationCommand, PostUserCommand etc.) I figured I could trigger the binding provider based on their common assembly, so that I can catch all the commands with a single custom binder.
But it seems based on the docs that I have to return a full model/object in my IModelBinder, which implies to me that I'd need a binder for each type/model. Or am I just misinterpreting the docs?
Each of my three (and more) "Command" objects (models) all look similar to this (with other properties omitted):
public class PostAnnouncementCommand { public Guid AccountId { get; set; } //This property will be in ALL Command objects, and is what I want to modify public Guid AnnouncementId { get; set; } //Unique to this command
public string AnnouncementContent { get; set; } //Unique to this command }public class PostNotificationCommand { public Guid AccountId { get; set; } //This property will be in ALL Command objects, and is what I want to modify public Guid NotificationId { get; set; } //Unique to this command public string NotificationName { get; set;} //Unique to this command }
public class PostUserCommand { public Guid AccountId { get; set; } //This property will be in ALL Command objects, and is what I want to modify public string FirstName { get; set;} //Unique to this command }
So essentially, I just want to use the ModelBinder to set a value for AccountId only, and leave all the other values bound based on what was POST'd in from the body of the request. But I don't want to write logic for each and every command type.
Here is what I came up with so far:
public class MyCustomBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var modelType = bindingContext.ModelType; var accountId = new Guid(((ClaimsIdentity) bindingContext.HttpContext.User.Identity).FindFirst(x => x.Type == "AccountId").Value); //I think this is essentially what I need to do: bindingContext.Model.AccountId = accountId; //However, I also want to ensure that the rest of the model is still populated as well. Does using this custom model binder turn off the default binder? Or do they both execute on the model in the pipeline? return Task.CompletedTask; } } public class MyCustomBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } return context.Metadata.ModelType.Assembly == typeof(PostAnnouncementCommand).GetTypeInfo().Assembly ? new BinderTypeModelBinder(typeof(MyCustomBinder)) : null; } }
Tuesday, September 8, 2020 8:54 PM -
User-474980206 posted
you probably want just a property binder. see:
https://www.dotnetcurry.com/aspnet-mvc/1368/aspnet-core-mvc-custom-model-binding
Tuesday, September 8, 2020 10:41 PM