locked
Use Middleware to Set Model Values from Controller RRS feed

  • 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