Reading FromUri and FromBody at the same time
A post body is typically a URI string like this:
Message=foobar&TestingMode=true
You have to make sure that the HTTP header contains
Content-Type: application/x-www-form-urlencoded
EDIT: Because it's still not working, I created a full example myself.
It prints the correct data.
I also used .NET 4.5 RC.
// server-side
public class ValuesController : ApiController {
[HttpPost]
public string PushMessage([FromUri] string x, [FromUri] string y, [FromBody] Person p) {
return p.ToString();
}
}
public class Person {
public string Name { get; set; }
public int Age { get; set; }
public override string ToString() {
return this.Name + ": " + this.Age;
}
}
// client-side
public class Program {
private static readonly string URL = "http://localhost:6299/api/values/PushMessage?x=asd&y=qwe";
public static void Main(string[] args) {
NameValueCollection data = new NameValueCollection();
data.Add("Name", "Johannes");
data.Add("Age", "24");
WebClient client = new WebClient();
client.UploadValuesCompleted += UploadValuesCompleted;
client.Headers["Content-Type"] = "application/x-www-form-urlencoded";
Task t = client.UploadValuesTaskAsync(new Uri(URL), "POST", data);
t.Wait();
}
private static void UploadValuesCompleted(object sender, UploadValuesCompletedEventArgs e) {
Console.WriteLine(Encoding.ASCII.GetString(e.Result));
}
}
Are [FromBody] and [FromUri] mutually exclusive?
In your first implementation
public void PostInventoryItem([FromBody] string serialNum, [FromUri] InventoryItem ii)
{}
the value of serialNum is null as expected, because [FromBody] was trying to look for serialNum in the body of your message.
This is the definition of the attribute from MSDN:
FromBodyAttribute Class
An attribute that specifies that an action parameter comes only from
the entity body of the incoming HttpRequestMessage.
ASP.NET Core MVC Mixed Route/FromBody Model Binding & Validation
You can remove the [FromBody]
decorator on your input and let MVC binding map the properties:
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{
return Json(new
{
data.Id,
data.RootId,
data.Name,
data.Description,
Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
});
}
More info:
Model binding in ASP.NET Core MVC
UPDATE
Testing
UPDATE 2
@heavyd, you are right in that JSON data requires [FromBody]
attribute to bind your model. So what I said above will work on form data but not with JSON data.
As alternative, you can create a custom model binder that binds the Id
and RootId
properties from the url, whilst it binds the rest of the properties from the request body.
public class TestModelBinder : IModelBinder
{
private BodyModelBinder defaultBinder;
public TestModelBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory) // : base(formatters, readerFactory)
{
defaultBinder = new BodyModelBinder(formatters, readerFactory);
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
// callinng the default body binder
await defaultBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
var data = bindingContext.Result.Model as TestModel;
if (data != null)
{
var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;
int intValue = 0;
if (int.TryParse(value, out intValue))
{
// Override the Id property
data.Id = intValue;
}
value = bindingContext.ValueProvider.GetValue("RootId").FirstValue;
if (int.TryParse(value, out intValue))
{
// Override the RootId property
data.RootId = intValue;
}
bindingContext.Result = ModelBindingResult.Success(data);
}
}
}
}
Create a binder provider:
public class TestModelBinderProvider : IModelBinderProvider
{
private readonly IList<IInputFormatter> formatters;
private readonly IHttpRequestStreamReaderFactory readerFactory;
public TestModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
{
this.formatters = formatters;
this.readerFactory = readerFactory;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(TestModel))
return new TestModelBinder(formatters, readerFactory);
return null;
}
}
And tell MVC to use it:
services.AddMvc()
.AddMvcOptions(options =>
{
IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
});
Then your controller has:
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{...}
Testing
You can add an Id
and RootId
to your JSON but they will be ignored as we are overwriting them in our model binder.
UPDATE 3
The above allows you to use your data model annotations for validating Id
and RootId
. But I think it may confuse other developers who would look at your API code. I would suggest to just simplify the API signature to accept a different model to use with [FromBody]
and separate the other two properties that come from the uri.
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int id, int rootId, [FromBody]TestModelNameAndAddress testModelNameAndAddress)
And you could just write a validator for all your input, like:
// This would return a list of tuples of property and error message.
var errors = validator.Validate(id, rootId, testModelNameAndAddress);
if (errors.Count() > 0)
{
foreach (var error in errors)
{
ModelState.AddModelError(error.Property, error.Message);
}
}
Related Topics
Why Is 16 Byte the Recommended Size for Struct in C#
How to Get Current Regional Settings in C#
Why Does My .Net Application Crash When Run from a Network Drive
Download Feature Not Working Within Update Panel in ASP.NET
Windows Authentication in ASP.NET 5
ASP.NET C# Static Variables Are Global
An Integer Array as a Key for Dictionary
Convert This Linq Expression into Lambda
Process.Start to Open an Url, Getting an Exception
Does Foreach Automatically Call Dispose
Is the Sorting Algorithm Used by .Net's 'Array.Sort()' Method a Stable Algorithm
There Is No Viewdata Item of Type 'Ienumerable<Selectlistitem>' That Has the Key Country
Route Parameter with Slash "/" in Url
How to Get a Dimension (Slice) from a Multidimensional Array
Foo.Cmd Won't Output Lines in Process (On Website)
Maybe a C# Compiler Bug in Visual Studio 2015