Is there a way I can hide a custom property of the route in Swagger JSON definition? I have another application that invokes the API and it reads Swagger to get more information.
For example, I want to indicate that route requires Active Directory. The calling application will execute a validation to make sure AD is available.
I looked at OperationsFilter but that inserts a Parameter into the Swagger definition. I also considered using Tag but it doesn't accomplish what I need, and it messes with UI.
We use Swagger Annotations in .NET Core 2.0 project. Ideally, I want to do this through annotations but haven't found anything I can use.
[HttpGet]
[Route("api/")]
[SwaggerOperation(
Summary = "Some Service",
Description = "Some Service",
OperationId = "getMyStuff",
Tags = new[] { "MyStuff" }
)]
[SwaggerResponse(200, "Response object returned")]
[SwaggerResponse(400, "Response object returned")]
public ActionResult<Response> GetStuff()
{...code here...}
I found an answer. Basically, I am adding an extension that is route specific.
Define custom attribute class
// allow mutliple attributes specified in data annotations
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
// model for custom attribute class
public class CustomAttribute : Attribute
{
public string ParameterName { get; set; }
public string Value { get; set; }
public string Description { get; set; }
public CustomAttribute(string parameterName, string value, string description = null)
{
this.ParameterName = parameterName;
this.Value = value;
this.Description = description;
}
}
Create swagger operations filter
public class CustomAttributeOperationFilter : IOperationFilter
{
// variables
MethodInfo _methodInfo;
public void Apply(Operation operation, OperationFilterContext context)
{
context.ApiDescription.TryGetMethodInfo(out _methodInfo);
var attributes = _methodInfo.GetCustomAttributes<CustomAttribute>();
foreach (var atrib in attributes)
operation.Extensions.Add(atrib.ParameterName, atrib.Value);
}
}
In startups.cs update swagger registration to include custom operations filter
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("helloworld", new Info {Title = "HelloWorld API"});
c.EnableAnnotations();
c.OperationFilter<CustomAttributeOperationFilter>();
});
Finally, specify custom attributes on the API definition
[HttpGet]
[CustomAttribute("x-accept-apples, "yes")]
[CustomAttribute("x-accept-pears, "no")]
public ActionResult<Response> Get()
{
return Ok();
}
Related
I am looking into adding an OpenAPI spec to my web API project, but I am running into various obstacles that I am not able to resolve.
API endpoint: /api/some_controller/some_method/id
The content body needs to come from the http body, but I do not want automatic binding using [FromBody] as I need to stream and process the data as-is (auditing, etc).
I added swagger to my project but as expected it does not show a body parameter.
The following DOES generate a proper swagger API definition:
public void some_method([FromBody]MyType mytype);
But as stated before, I need the raw data without model binding.
I am at a loss on how to solve this. Do I need to augment the API explorer somehow? Do I need to add options to swagger? Is there some way to have the [FromBody] attribute that does not actually bind? How can I do this?
I managed to get this to work using an extra custom attribute and an IOperationFilter
[AttributeUsage(AttributeTargets.Method)]
public class OpenApiRequestBodyType: Attribute
{
public Type BodyType { get; }
public string [] ContentTypes { get; }
public OpenApiRequestBodyType(Type type, string[] contentTypes = null)
{
BodyType = type;
ContentTypes = contentTypes;
}
}
public class SwaggerBodyTypeOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var bodyTypeAttribute = context.ApiDescription.CustomAttributes().OfType<OpenApiRequestBodyType>().FirstOrDefault();
if (bodyTypeAttribute != null)
{
var schema = context.SchemaGenerator.GenerateSchema(bodyTypeAttribute.BodyType, context.SchemaRepository);
operation.RequestBody = new OpenApiRequestBody();
string[] contentTypes;
if (bodyTypeAttribute.ContentTypes != null)
contentTypes = bodyTypeAttribute.ContentTypes;
else
contentTypes = operation.Responses.Where(x => x.Key =="200").SelectMany(x=>x.Value.Content).Select(x=>x.Key).ToArray();
foreach (var contentType in contentTypes)
{
operation.RequestBody.Content.Add(KeyValuePair.Create(contentType, new OpenApiMediaType { Schema = schema }));
}
}
}
}
Then I simply tag the method:
[OpenApiRequestBodyType(typeof(my_custom_type))]
and in the Startup:
services.AddSwaggerGen(c =>
{
c.OperationFilter<SwaggerBodyTypeOperationFilter>();
}
I am still not sure if there is no better way to do this.... but at least it works for me...
I'm building a restful server to handle post requests. However, there is a dot (.) in one of the parameter names which I don't know how to handle since C# does not allow dot (.) in their variable names. The parameter name is "data.json" without the quotation.
I read some posts about C# converting dots (.) into underscores (_), so I tried to name the variable "data_json", which doesn't work, the string is empty.
Object Class:
public class Lead {
public string data_json { get; set; }
public string page_id { get; set; }
public string page_url { get; set; }
}
Post Handler:
public HttpResponseMessage Post(Lead value) {
try {
Log.CreatePostLog(page_id + value.data_json);
} catch (Exception e) {
return Request.CreateResponse(HttpStatusCode.BadRequest, e.Message);
}
return Request.CreateResponse(HttpStatusCode.OK, "Done!");
}
Post Request Body (Cannot be changed):
page_url=http://ramdomurl/
&page_id=123456
&data.json={"time_submitted":["04:34 PM UTC"],"full_name":["John Doe"]}
When the request is made, the log shows page_id but nothing after.
It should show page_id and the Json string after it.
One possible solution is to create a custom model binder, which handles fields whose names contain the "." character, and apply this binder to the model class.
The code of the binder:
// this binder assigns form fields with dots to properties with underscores:
// e.g. data.json -> data_json
public class Dot2UnderscoreModelBinder : IModelBinder
{
// for regular fields, we will use the default binder
private readonly DefaultModelBinder _default = new DefaultModelBinder();
public object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
// handle the regular fields
var model = _default.BindModel(controllerContext, bindingContext);
// handle the special fields
if (model != null)
{
var modelType = model.GetType();
var form = controllerContext.HttpContext.Request.Form;
foreach (var key in form.AllKeys)
{
if (key.Contains(".")) // special field
{
// model property must be named by the convention "." -> "_"
var propertyName = key.Replace(".", "_");
var propertyInfo = modelType.GetProperty(propertyName);
propertyInfo?.SetValue(model, form[key]);
}
}
}
return model;
}
}
Note that this is a simplistic implementation, it only supports string properties, and its performance is not optimal. But it is a working starting point.
Now you need to apply the above binder to the model class:
[ModelBinder(typeof(Dot2UnderscoreModelBinder))]
public class Lead
{
//... properties
}
It worth noting that the controller must derive from Controller in System.Web.Mvc namespace, and not ApiController in System.Web.Http, because that latter doesn't trigger model binders:
using System.Web.Mvc;
....
public class MyController : Controller
{
[HttpPost]
public ActionResult Post(Lead value)
{
//... do some stuff
return base.Content("Done!");
}
}
ASP.NET Core
Just as a side note, in ASP.NET Core the same can be achieved in a very simple way, by applying FromForm attribute:
public class Lead
{
[FromForm(Name = "data.json")] // apply this attribute
public string data_json { get; set; }
//... other properties
}
Use NewtonsoftJson PropertyName attribute:
public class Lead
{
[JsonProperty(PropertyName = "data.json")]
public string data_json { get; set; }
public string page_id { get; set; }
public string page_url { get; set; }
}
Add nuget package:
https://www.nuget.org/packages/Newtonsoft.Json/
Tools
Visual Studio 2017
ASP.NET Core 2.2
Postman v7.2.0
What I'm trying to do
Send FormData from Postman to an ASP.NET Core controller and have the data from the request bind to to a command class that has properties with private setters.
I've sent JSON data using the same setup (private setters) with no problem. The FromBody attribute deserialises the JSON string to the model without errors.
The Problem
Properties that are primitive types do not bind if the model has a private setter. However, complex types do regardless of the access modifier.
Controller
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> CreateItemAsync([FromForm]CreateItemCommand command)
{
bool result = false;
commandResult = await _mediator.Send(command);
if (!commandResult)
{
return BadRequest();
}
return Ok();
}
Command
Note: The Title property has been left with a public setter deliberately to illustrate the behviour
[DataContract]
public class CreateItemCommand
:IRequest<bool>
{
[DataMember]
public string Title { get; set; }
[DataMember]
public string Description { get; private set; }
[DataMember]
public int Count { get; private set; }
[DataMember]
public HashSet<string> Tags { get; private set; }
[DataMember]
public string ItemDate { get; private set; }
[DataMember]
public List<IFormFile> Documents { get; private set; }
public CreateItemCommand()
{
Skills = new HashSet<string>();
Systems = new HashSet<string>();
}
public CreateItemCommand(string title, string description,
int count, HashSet<string> tags, string itemDate,
List<IFormFile> documents)
: this()
{
Title = title;
Description = description;
Count = count
Tags = tags;
ItemDate = itemDate;
Documents = documents;
}
}
In Postman I now setup the request as follows:
I've had to obfuscate some of the information, but you can see that the primitive types with private setters are not set.
Questions
Why does the property access modifier only affect properties with primitive types?
Why does this happens when the parameter attribute is set to FromForm but not when it's set to FromBody
Why does the property access modifier only affect properties with primitive types?
For Asp.Net Core ModelBinder, it will check whether the property is private access setter by ComplexTypeModelBinder code below:
protected virtual object CreateModel(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
// If model creator throws an exception, we want to propagate it back up the call stack, since the
// application developer should know that this was an invalid type to try to bind to.
if (_modelCreator == null)
{
// The following check causes the ComplexTypeModelBinder to NOT participate in binding structs as
// reflection does not provide information about the implicit parameterless constructor for a struct.
// This binder would eventually fail to construct an instance of the struct as the Linq's NewExpression
// compile fails to construct it.
var modelTypeInfo = bindingContext.ModelType.GetTypeInfo();
if (modelTypeInfo.IsAbstract || modelTypeInfo.GetConstructor(Type.EmptyTypes) == null)
{
var metadata = bindingContext.ModelMetadata;
switch (metadata.MetadataKind)
{
case ModelMetadataKind.Parameter:
throw new InvalidOperationException(
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForParameter(
modelTypeInfo.FullName,
metadata.ParameterName));
case ModelMetadataKind.Property:
throw new InvalidOperationException(
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForProperty(
modelTypeInfo.FullName,
metadata.PropertyName,
bindingContext.ModelMetadata.ContainerType.FullName));
case ModelMetadataKind.Type:
throw new InvalidOperationException(
Resources.FormatComplexTypeModelBinder_NoParameterlessConstructor_ForType(
modelTypeInfo.FullName));
}
}
_modelCreator = Expression
.Lambda<Func<object>>(Expression.New(bindingContext.ModelType))
.Compile();
}
return _modelCreator();
}
Why does this happens when the parameter attribute is set to FromForm but not when it's set to FromBody
For FromBody, it is used JsonInputFormatter to bind the model from the body request, it's used JsonConvert.DeserializeObject to deserilize the object and Newtonsoft.Json support deserize the object which contains private setter from json string.
I am working on an ASP.NET Core 2.2 API that is implementing OData via Microsoft.AspNetCore.Odata v7.1.0 NuGet. I had everything working fine so I decided to add API Versioning via the Microsoft.AspNetCore.OData.Versioning v3.1.0.
Now, my GET and GET{id} methods in my controller work correctly with versioning. For example, I can get to the GET list endpoint method by using the URL
~/api/v1/addresscompliancecodes
or
~/api/addresscompliancecodes?api-version=1.0
However when I try to create a new record, the request routes to the correct method in the controller but now the request body content is not being passed to the POST controller method
I have been following the examples in the Microsoft.ApsNetCore.OData.Versioning GitHub
There is the HttpPost method in my controller;
[HttpPost]
[ODataRoute()]
public async Task<IActionResult> CreateRecord([FromBody] AddressComplianceCode record, ODataQueryOptions<AddressComplianceCode> options)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_context.Add(record);
await _context.SaveChangesAsync();
return Created(record);
}
When I debug, the request routes to the controller method properly but the "record" variable is now null, whereas, before adding the code changes for API Versioning, it was correctly populated.
I suspect it is how I am using the model builder since that code changed to support API versioning.
Before trying to implement API Versioning, I was using a model builder class as shown below;
public class AddressComplianceCodeModelBuilder
{
public IEdmModel GetEdmModel(IServiceProvider serviceProvider)
{
var builder = new ODataConventionModelBuilder(serviceProvider);
builder.EntitySet<AddressComplianceCode>(nameof(AddressComplianceCode))
.EntityType
.Filter()
.Count()
.Expand()
.OrderBy()
.Page() // Allow for the $top and $skip Commands
.Select();
return builder.GetEdmModel();
}
}
And a Startup.cs --> Configure method like what is shown in the snippet below;
// Support for OData $batch
app.UseODataBatching();
app.UseMvc(routeBuilder =>
{
// Add support for OData to MVC pipeline
routeBuilder
.MapODataServiceRoute("ODataRoutes", "api/v1",
modelBuilder.GetEdmModel(app.ApplicationServices),
new DefaultODataBatchHandler());
});
And it worked with [FromBody] in the HttpPost method of the controller.
However, in following the examples in the API Versioning OData GitHub, I am now using a Configuration class like what is shown below, rather than the model builder from before;
public class AddressComplianceCodeModelConfiguration : IModelConfiguration
{
private static readonly ApiVersion V1 = new ApiVersion(1, 0);
private EntityTypeConfiguration<AddressComplianceCode> ConfigureCurrent(ODataModelBuilder builder)
{
var addressComplianceCode = builder.EntitySet<AddressComplianceCode>("AddressComplianceCodes").EntityType;
addressComplianceCode
.HasKey(p => p.Code)
.Filter()
.Count()
.Expand()
.OrderBy()
.Page() // Allow for the $top and $skip Commands
.Select();
return addressComplianceCode;
}
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion)
{
if (apiVersion == V1)
{
ConfigureCurrent(builder);
}
}
}
And my Startup.cs --> Configure method is changed as shown below;
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
VersionedODataModelBuilder modelBuilder)
{
// Support for OData $batch
app.UseODataBatching();
app.UseMvc(routeBuilder =>
{
// Add support for OData to MVC pipeline
var models = modelBuilder.GetEdmModels();
routeBuilder.MapVersionedODataRoutes("odata", "api", models);
routeBuilder.MapVersionedODataRoutes("odata-bypath", "api/v{version:apiVersion}", models);
});
}
If it is relevant, I have the following code in my Startup.cs -> ConfigureServices;
// Add Microsoft's API versioning
services.AddApiVersioning(options =>
{
// reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;
});
// Add OData 4.0 Integration
services.AddOData().EnableApiVersioning();
services.AddMvc(options =>
{
options.EnableEndpointRouting = false; // TODO: Remove when OData does not causes exceptions anymore
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(opt =>
{
opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
I feel the issue is with the model is somehow not matching up correctly but I cannot see exactly why it isn't
UPDATE 3/18/19 - Additional Information
Here is my entity class;
[Table("AddressComplianceCodes")]
public class AddressComplianceCode : EntityBase
{
[Key]
[Column(TypeName = "char(2)")]
[MaxLength(2)]
public string Code { get; set; }
[Required]
[Column(TypeName = "varchar(150)")]
[MaxLength(150)]
public string Description { get; set; }
}
and the EntityBase class;
public class EntityBase : IEntityDate
{
public bool MarkedForRetirement { get; set; }
public DateTimeOffset? RetirementDate { get; set; }
public DateTimeOffset? LastModifiedDate { get; set; }
public string LastModifiedBy { get; set; }
public DateTimeOffset? CreatedDate { get; set; }
public string CreatedBy { get; set; }
public bool Delete { get; set; }
public bool Active { get; set; }
}
And here is the request body from Postman;
{
"#odata.context": "https://localhost:44331/api/v1/$metadata#AddressComplianceCodes",
"Code": "Z1",
"Description": "Test Label - This is a test for Z1",
"Active": true
}
Any ideas?
As it turned out, the problem was because I was not using camel case as my property names in the Postman request body. This was not an issue with Microsoft.AspNetCore.Odata alone but once I added the Microsoft.AspNetCore.Odata.Versioning NuGet package, it failed with the upper case starting character of the property names. It seems that Microsoft.AspNetCore.Odata.Versioning uses it's own MediaTypeFormatter that enables lower camel case. I discovered this in the following GitHub post; https://github.com/Microsoft/aspnet-api-versioning/issues/310
There is no custom MediaTypeFormatter, but the behavior did change in 3.0 as using camel casing is seemingly the default for most JSON-based APIs. This is easy to revert back however.
modelBuilder.ModelBuilderFactory = () => new ODataConventionModelBuilder();
// as opposed to the new default:
// modelBuilder.ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase();
The is also the place were you'd perform or change any other setup related to model builders. The factory method is called to create a new model builder per API version.
It's worth pointing out that you do not need to map routes twice. For demonstraton purposes, the by query string and by URL path are configured. You should choose one or the other and remove the one that isn't used.
I hope that helps.
Currently I'm trying to create a web api based on asp.net core 2.0 and I'd like to create a nested route. In case of a put request it sends a part of the information in the route and another part in the body.
Requirements
The desired url to call would be
https://localhost/api/v1/master/42/details
If we'd like to create a new detail below our master 42 I would expect to send the data of the details in the body while the id of the master comes out of the route.
curl -X POST --header 'Content-Type: application/json' \
--header 'Accept: application/json' \
-d '{ \
"name": "My detail name", \
"description": "Just some kind of information" \
}' 'https://localhost/api/v1/master/42/details'
The outcoming response of the request would be
{
"name": "My detail name",
"description": "Just some kind of information",
"masterId": 42,
"id": 47
}
and a location url within the response header like
{
"location": "https://localhost/api/v1/master/42/details/47
}
Work done so far
To get this to work I created this controller:
[Produces("application/json")]
[Route("api/v1/master/{masterId:int}/details")]
public class MasterController : Controller
{
[HttpPost]
[Produces(typeof(DetailsResponse))]
public async Task<IActionResult> Post([FromBody, FromRoute]DetailCreateRequest request)
{
if(!ModelState.IsValid)
return BadRequest(ModelState);
var response = await Do.Process(request);
return CreatedAtAction(nameof(Get), new { id = response.Id }, response);
}
}
Which uses these classes:
public class DetailCreateRequest
{
public int MasterId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class DetailResponse
{
public int Id { get; set; }
public int MasterId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
The problem
So far most of the stuff works as expected. The only thing that really doesn't work is merging the MasterId from the route into the DetailCreateRequest that comes from the body.
First try: Use two attributes on the parameter
I tried to combine these two things by this action call:
public async Task<IActionResult> Post([FromBody, FromRoute]DetailCreateRequest request)
But the incoming object only had a MasterId of zero. If I change the order of the two attributes, then only the id from the route will be taken and all values within the body are ignored (so seems to be first attribute wins).
Second try: Use two different parameters in action
Another approach that I tried was this action call:
public async Task<IActionResult> Post([FromRoute]int masterId, [FromBody]DetailCreateRequest request)
In the first spot this looks okay, cause now I have both values within the controller action. But my big problem with this approach is the model validation. As you can see in the above code I check ModelState.IsValid which was filled through some checks from FluentValidation, but these checks can't be really done, cause the object wasn't build up correctly due to the missing master id.
(Not-working) Idea: Create own attribute with merge parameters
Tried to implement something like this:
public async Task<IActionResult> Post([FromMultiple(Merge.FromBody, Merge.FromRoute)]DetailCreateRequest request)
If we already would have something like this, that would be great. The order of the arguments within the attribute would give out the order in which the merge (and possible overwrites) would happen.
I already started with implementing this attribute and creating the skeleton for the needed IValueProvider and IValueProviderFactory. But it seems to be a quite lot of work. Especially finding all the nifty details to make this work seamlessly with the whole pipeline of asp.net core and other libraries I'm using (like swagger through swashbuckle).
So my question would be, if there already exists some mechanism within asp.net core to achieve such a merge or if anybody is aware about an already existing solution or about a good example on how to implement such a beast.
Solution so far: Custom ModelBinder
After getting the answer from Merchezatter I look into how to create a custom model binder and came up with this implementation:
public class MergeBodyAndValueProviderBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var body = bindingContext.HttpContext.Request.Body;
var type = bindingContext.ModelMetadata.ModelType;
var instance = TryCreateInstanceFromBody(body, type, out bool instanceChanged);
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
var setters = type.GetProperties(bindingFlags).Where(property => property.CanWrite);
foreach (var setter in setters)
{
var result = bindingContext.ValueProvider.GetValue(setter.Name);
if (result != ValueProviderResult.None)
{
try
{
var value = Convert.ChangeType(result.FirstValue, setter.PropertyType);
setter.SetMethod.Invoke(instance, new[] { value });
instanceChanged = true;
}
catch
{ }
}
}
if (instanceChanged)
bindingContext.Result = ModelBindingResult.Success(instance);
return Task.CompletedTask;
}
private static object TryCreateInstanceFromBody(Stream body, Type type, out bool instanceChanged)
{
try
{
using (var reader = new StreamReader(body, Encoding.UTF8, false, 1024, true))
{
var data = reader.ReadToEnd();
var instance = JsonConvert.DeserializeObject(data, type);
instanceChanged = true;
return instance;
}
}
catch
{
instanceChanged = false;
return Activator.CreateInstance(type);
}
}
}
It tries to deserialize the body into the desired object type and afterwards tries to apply further values from the available value providers. To get this model binder to work I had to decorate the destination class with the ModelBinderAttribute and made the MasterId internal, so that swagger doesn't announce it and JsonConvert doesn't deserialize it:
[ModelBinder(BinderType = typeof(MergeBodyAndValueProviderBinder))]
public class DetailCreateRequest
{
internal int MasterId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
Within my controller the action method parameters are still containing the [FromBody] flag, cause it is used by swagger to announce on how the method can be called, but it never will be called, cause my model binder has a higher priority.
public async Task<IActionResult> Post([FromBody]DetailCreateRequest request)
So it is not perfect, but works good so far.
That is looks like a right choice:
[HttpPost]
[Produces(typeof(DetailsResponse))]
public async Task<IActionResult> Post([FromRoute]int masterId, [FromBody]DetailCreateRequest request) {
//...
}
But if you have some problems with domain model validation, create custom Dto object without master Id.
Otherwise you can use custom model binder, and then work with arguments from action and binding contexts.
I'm not sure if this works in Asp.Net-Core 2.0, but we use the following in 3.1 to have a single request object which gets its properties from multiple locations:
// Annotate the action parameter with all relevant attributes
public async Task<IActionResult> Post([FromBody][FromRoute][FromQuery]DetailCreateRequest request) { ... }
// Annotate each property separately, so the binder(s) don't overwrite
public class DetailCreateRequest
{
[FromRoute]
public int MasterId { get; set; }
[FromBody]
public string Name { get; set; }
[FromQuery]
public string Description { get; set; }
}
It works with .Net 6:
[HttpPost]
[Route("{id}")]
public async Task<ActionResult<CustomerResponse>> Post([FromRoute, FromBody] CustomerPostRequest request)
{
return Ok();
}
public class CustomerPostRequest
{
[FromRoute(Name = "id")]
public int Id { get; set; }
[FromBody]
public string Name { get; set; }
}
Set the your required "source" attributes on the single request object parameter, and inside this object add each property the relevant "source" attribute.
Make sure the FromBody is the last one (it didn't work when I switched them).