简体   繁体   中英

Is it possible to combine [FromRoute] and [FromBody] in ASP.NET Core?

I have an action on API controller like this:

[HttpPost]
public async Task<IActionResult> StartDeployment(
    [FromQuery]Guid deploymentId,
    [FromRoute]RequestInfo requestInfo,
    [FromBody]DeploymenRequest deploymentRequest)
{
}

which is available by complex url ( requestInfo ) and receives HTTP POST request payload ( deploymentRequest ).

Is it possible to combine [FromRoute] and [FromBody] so I would have single request model:

public class UberDeploymentRequestInfo
{
    [FromQuery]public Guid DeploymentId { get; set; }

    [FromRoute]public RequestInfo RequestInfo { get; set; }

    [FromBody]public DeploymenRequest DeploymentRequest { get; set; }
}

so I could have single validator using Fluent Validation:

internal class UberDeploymentRequestInfoValidator : AbstractValidator<UberDeploymentRequestInfo>
{
    public UberDeploymentRequestInfoValidator()
    {
        // validation rules can access both url and payload
    }
}

It's doable by a custom model binder as mentioned in the comment. Here is a few code snippets to wire everything up, with the example you can send a http request with the following JSON body to an API /api/cats?From=james&Days=20

{
    "Name":"",
    "EyeColor":"Red"
}

A few classes, you can find them here as well: https://github.com/atwayne/so-51316269

// We read Cat from request body
public class Cat
{
    public string Name { get; set; }
    public string EyeColor { get; set; }
}

// AdoptionRequest from Query String or Route
public class AdoptionRequest
{
    public string From { get; set; }
    public string Days { get; set; }
}

// One class to merge them together
[ModelBinder(BinderType = typeof(CatAdoptionEntityBinder))]
public class CatAdoptionRequest
{
    public Cat Cat { get; set; }
    public AdoptionRequest AdoptionRequest { get; set; }
}


public class CatAdoptionEntityBinder : IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // Read Cat from Body
        var memoryStream = new MemoryStream();
        var body = bindingContext.HttpContext.Request.Body;
        var reader = new StreamReader(body, Encoding.UTF8);
        var text = reader.ReadToEnd();
        var cat = JsonConvert.DeserializeObject<Cat>(text);

        // Read Adoption Request from query or route
        var adoptionRequest = new AdoptionRequest();
        var properties = typeof(AdoptionRequest).GetProperties();
        foreach (var property in properties)
        {
            var valueProvider = bindingContext.ValueProvider.GetValue(property.Name);
            if (valueProvider != null)
            {
                property.SetValue(adoptionRequest, valueProvider.FirstValue);
            }
        }

        // Merge
        var model = new CatAdoptionRequest()
        {
            Cat = cat,
            AdoptionRequest = adoptionRequest
        };

        bindingContext.Result = ModelBindingResult.Success(model);
        return;
    }
}


// Controller
[HttpPost()]
public bool Post([CustomizeValidator]CatAdoptionRequest adoptionRequest)
{
    return ModelState.IsValid;
}

public class CatAdoptionRequestValidator : AbstractValidator<CatAdoptionRequest>
{
    public CatAdoptionRequestValidator()
    {
        RuleFor(profile => profile.Cat).NotNull();
        RuleFor(profile => profile.AdoptionRequest).NotNull();
        RuleFor(profile => profile.Cat.Name).NotEmpty();
    }
}

// and in our Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddFluentValidation();
    services.AddTransient<IValidator<CatAdoptionRequest>, CatAdoptionRequestValidator>();
}

I've customized the above ModelBinder further so that it's more generic and it works on many different contracts now. Figured I may as well share it here, where I found the majority of the code below.

public class BodyAndQueryAndRouteModelBinder<T> : IModelBinder where T : new()
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // Read Cat from Body
        var memoryStream = new MemoryStream();
        var body = bindingContext.HttpContext.Request.Body;
        var reader = new StreamReader(body);
        var text = await reader.ReadToEndAsync();
        var contract = JsonConvert.DeserializeObject<T>(text);

        var properties = typeof(T).GetProperties();
        foreach (var property in properties)
        {
            var valueProvider = bindingContext.ValueProvider.GetValue(property.Name);
            if (valueProvider.FirstValue.IsNotNullOrEmpty())
            {
                property.SetValue(contract, valueProvider.FirstValue);
            }
        }

        bindingContext.Result = ModelBindingResult.Success(contract);
    }
}

I then use the binder on the parent contract:

[ModelBinder(BinderType = typeof(BodyAndQueryAndRouteModelBinder<ConfirmStatusRequest>))]
    public class ConfirmStatusRequest
    {
        public string ShortCode { get; set; }
        public IEnumerable<DependantRequest> Dependants { get; set; }
        public IEnumerable<CheckinQuestionAnswer> Answers { get; set; }
    }

I have found an other solution with inject the IActionContextAccessor into the Validator. With this I can access the ROUTE paramerter without the need of a special model binding.

Startup.cs

services.AddHttpContextAccessor();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

CatValidator.cs

public class CatValidator : AbstractValidator<CatDto>
{
    public CatValidator(IActionContextAccessor actionContextAccessor)
    {
        RuleFor(item => item.Age)
            .MustAsync(async (context, age, propertyValidatorContext, cancellationToken) =>
            {
                var catId = (string)actionContextAccessor.ActionContext.RouteData.Values
                .Where(o => o.Key == "catId")
                .Select(o => o.Value)
                .FirstOrDefault();

                return true;
            });
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM