diff --git a/src/Microsoft.AspNet.Mvc.Core/FilterActionInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/FilterActionInvoker.cs
index d30b8c7b71..59ae52fd2d 100644
--- a/src/Microsoft.AspNet.Mvc.Core/FilterActionInvoker.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/FilterActionInvoker.cs
@@ -237,14 +237,20 @@ namespace Microsoft.AspNet.Mvc
if (parameter.BodyParameterInfo != null)
{
var parameterType = parameter.BodyParameterInfo.ParameterType;
- var modelMetadata = metadataProvider.GetMetadataForType(
- modelAccessor: null,
- modelType: parameterType);
var formatterContext = new InputFormatterContext(actionBindingContext.ActionContext,
- modelMetadata.ModelType);
+ parameterType);
var inputFormatter = actionBindingContext.InputFormatterSelector.SelectFormatter(
formatterContext);
- parameterValues[parameter.Name] = await inputFormatter.ReadAsync(formatterContext);
+ if (inputFormatter == null)
+ {
+ var request = ActionContext.HttpContext.Request;
+ var unsupportedContentType = Resources.FormatUnsupportedContentType(request.ContentType);
+ ActionContext.ModelState.AddModelError(parameter.Name, unsupportedContentType);
+ }
+ else
+ {
+ parameterValues[parameter.Name] = await inputFormatter.ReadAsync(formatterContext);
+ }
}
else
{
diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultInputFormatterSelector.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultInputFormatterSelector.cs
index fb5ff4a577..97cc5ad771 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultInputFormatterSelector.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/DefaultInputFormatterSelector.cs
@@ -1,16 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System;
-using System.Globalization;
-
namespace Microsoft.AspNet.Mvc
{
public class DefaultInputFormatterSelector : IInputFormatterSelector
{
public IInputFormatter SelectFormatter(InputFormatterContext context)
{
- // TODO: https://github.com/aspnet/Mvc/issues/1014
var formatters = context.ActionContext.InputFormatters;
foreach (var formatter in formatters)
{
@@ -19,13 +15,8 @@ namespace Microsoft.AspNet.Mvc
return formatter;
}
}
-
- var request = context.ActionContext.HttpContext.Request;
-
- // TODO: https://github.com/aspnet/Mvc/issues/458
- throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
- "415: Unsupported content type {0}",
- request.ContentType));
+
+ return null;
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
index 6128205e7e..90327b5bbd 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs
@@ -1082,6 +1082,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("InputFormatterNoEncoding"), p0);
}
+ ///
+ /// Unsupported content type '{0}'.
+ ///
+ internal static string UnsupportedContentType
+ {
+ get { return GetString("UnsupportedContentType"); }
+ }
+
+ ///
+ /// Unsupported content type '{0}'.
+ ///
+ internal static string FormatUnsupportedContentType(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("UnsupportedContentType"), p0);
+ }
+
///
/// No supported media type registered for output formatter '{0}'. There must be at least one supported media type registered in order for the output formatter to write content.
///
diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
index cb350b487e..11507e0ba0 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx
@@ -318,6 +318,9 @@
No encoding found for input formatter '{0}'. There must be at least one supported encoding registered in order for the formatter to read content.
+
+ Unsupported content type '{0}'.
+
No supported media type registered for output formatter '{0}'. There must be at least one supported media type registered in order for the output formatter to write content.
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs
index dde7523440..62ee7a3e15 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs
@@ -9,6 +9,7 @@ using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Mvc.ModelBinding;
+using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
@@ -1454,6 +1455,64 @@ namespace Microsoft.AspNet.Mvc
Assert.Equal(value, result["foo"]);
}
+ [Fact]
+ public async Task GetActionArguments_NoInputFormatterFound_SetsModelStateError()
+ {
+ var actionDescriptor = new ReflectedActionDescriptor
+ {
+ MethodInfo = typeof(TestController).GetTypeInfo().GetMethod("ActionMethodWithDefaultValues"),
+ Parameters = new List
+ {
+ new ParameterDescriptor
+ {
+ Name = "bodyParam",
+ BodyParameterInfo = new BodyParameterInfo(typeof(Person))
+ }
+ },
+ FilterDescriptors = new List()
+ };
+
+ var context = new DefaultHttpContext();
+ var routeContext = new RouteContext(context);
+ var actionContext = new ActionContext(routeContext,
+ actionDescriptor);
+ var bindingContext = new ActionBindingContext(actionContext,
+ Mock.Of(),
+ Mock.Of(),
+ Mock.Of(),
+ Mock.Of(),
+ Enumerable.Empty());
+
+ var actionBindingContextProvider = new Mock();
+ actionBindingContextProvider.Setup(p => p.GetActionBindingContextAsync(It.IsAny()))
+ .Returns(Task.FromResult(bindingContext));
+ var controllerFactory = new Mock();
+ controllerFactory.Setup(c => c.CreateController(It.IsAny()))
+ .Returns(new TestController());
+ var inputFormattersProvider = new Mock();
+ inputFormattersProvider.SetupGet(o => o.InputFormatters)
+ .Returns(new List());
+ var invoker = new ReflectedActionInvoker(actionContext,
+ actionBindingContextProvider.Object,
+ Mock.Of>(),
+ controllerFactory.Object,
+ actionDescriptor,
+ inputFormattersProvider.Object);
+
+
+ var modelStateDictionary = new ModelStateDictionary();
+
+ // Act
+ var result = await invoker.GetActionArguments(modelStateDictionary);
+
+ // Assert
+ Assert.Empty(result);
+ Assert.DoesNotContain("bodyParam", result.Keys);
+ Assert.False(actionContext.ModelState.IsValid);
+ Assert.Equal("Unsupported content type '" + context.Request.ContentType + "'.",
+ actionContext.ModelState["bodyParam"].Errors[0].ErrorMessage);
+ }
+
[Fact]
public async Task Invoke_UsesDefaultValuesIfNotBound()
{
@@ -1524,6 +1583,18 @@ namespace Microsoft.AspNet.Mvc
throw _actionException;
}
+ public JsonResult ActionMethodWithBodyParameter([FromBody] Person bodyParam)
+ {
+ return new JsonResult(bodyParam);
+ }
+
+ public class Person
+ {
+ public string Name { get; set; }
+
+ public int Age { get; set; }
+ }
+
private sealed class TestController
{
public IActionResult ActionMethodWithDefaultValues(int value = 5)
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs
index 559bfb87e0..44c3f575c9 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs
@@ -5,6 +5,7 @@ using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
+using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
@@ -60,23 +61,43 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
}
[Theory]
- [InlineData("")]
- [InlineData(null)]
- [InlineData("invalid")]
- public async Task JsonInputFormatter_IsNotSelectedForNonJsonRequests(string requestContentType)
+ [InlineData("", true)]
+ [InlineData(null, true)]
+ [InlineData("invalid", true)]
+ [InlineData("application/custom", true)]
+ [InlineData("image/jpg", true)]
+ [InlineData("", false)]
+ [InlineData(null, false)]
+ [InlineData("invalid", false)]
+ [InlineData("application/custom", false)]
+ [InlineData("image/jpg", false)]
+ public async Task ModelStateErrorValidation_NoInputFormatterFound_ForGivenContetType(string requestContentType,
+ bool filterHandlesModelStateError)
{
// Arrange
+ var actionName = filterHandlesModelStateError ? "ActionFilterHandlesError" : "ActionHandlesError";
+ var expectedSource = filterHandlesModelStateError ? "filter" : "action";
+
var server = TestServer.Create(_services, _app);
var client = server.Handler;
var input = "{\"SampleInt\":10}";
// Act
- var ex = await Assert.ThrowsAsync
- (() => client.PostAsync("http://localhost/Home/CheckIfDummyIsNull", input, requestContentType));
+ var response = await client.PostAsync("http://localhost/InputFormatter/" + actionName,
+ input,
+ requestContentType,
+ (request) => request.Accept = "application/json");
- //Assert
- // TODO: Change the validation after https://github.com/aspnet/Mvc/issues/458 is fixed.
- Assert.Equal("415: Unsupported content type " + requestContentType, ex.Message);
+ var responseBody = await response.ReadBodyAsStringAsync();
+ var result = JsonConvert.DeserializeObject(responseBody);
+
+ // Assert
+ Assert.Equal(1, result.Errors.Count);
+ Assert.Equal("Unsupported content type '" + requestContentType + "'.",
+ result.Errors[0]);
+ Assert.Equal(actionName, result.ActionName);
+ Assert.Equal("dummy", result.ParameterName);
+ Assert.Equal(expectedSource, result.Source);
}
// TODO: By default XmlSerializerInputFormatter is called because of the order in which
diff --git a/test/WebSites/FormatterWebSite/Controllers/InputFormatterController.cs b/test/WebSites/FormatterWebSite/Controllers/InputFormatterController.cs
new file mode 100644
index 0000000000..11f80e5fa8
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/Controllers/InputFormatterController.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNet.Mvc;
+
+namespace FormatterWebSite.Controllers
+{
+ public class InputFormatterController : Controller
+ {
+ [HttpPost]
+ public object ActionHandlesError([FromBody] DummyClass dummy)
+ {
+ if (!ActionContext.ModelState.IsValid)
+ {
+ var parameterBindingErrors = ActionContext.ModelState["dummy"].Errors;
+ if (parameterBindingErrors.Count != 0)
+ {
+ return new ErrorInfo
+ {
+ ActionName = "ActionHandlesError",
+ ParameterName = "dummy",
+ Errors = parameterBindingErrors.Select(x => x.ErrorMessage).ToList(),
+ Source = "action"
+ };
+ }
+ }
+
+ return dummy;
+ }
+
+ [HttpPost]
+ [ValidateBodyParameter]
+ public object ActionFilterHandlesError([FromBody] DummyClass dummy)
+ {
+ return dummy;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/FormatterWebSite/Models/ErrorInfo.cs b/test/WebSites/FormatterWebSite/Models/ErrorInfo.cs
new file mode 100644
index 0000000000..aade6fbf3d
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/Models/ErrorInfo.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace FormatterWebSite
+{
+ public class ErrorInfo
+ {
+ public string Source { get; set; }
+
+ public string ActionName { get; set; }
+
+ public string ParameterName { get; set; }
+
+ public List Errors { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs b/test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs
new file mode 100644
index 0000000000..9a65f4b95e
--- /dev/null
+++ b/test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Linq;
+using Microsoft.AspNet.Mvc;
+
+namespace FormatterWebSite
+{
+ public class ValidateBodyParameterAttribute : ActionFilterAttribute
+ {
+ public override void OnActionExecuting(ActionExecutingContext context)
+ {
+ if (!context.ModelState.IsValid)
+ {
+ var bodyParameter = context.ActionDescriptor
+ .Parameters
+ .FirstOrDefault(parameter => parameter.BodyParameterInfo != null);
+ if (bodyParameter != null)
+ {
+ var parameterBindingErrors = context.ModelState[bodyParameter.Name].Errors;
+ if (parameterBindingErrors.Count != 0)
+ {
+ var errorInfo = new ErrorInfo
+ {
+ ActionName = context.ActionDescriptor.Name,
+ ParameterName = bodyParameter.Name,
+ Errors = parameterBindingErrors.Select(x => x.ErrorMessage).ToList(),
+ Source = "filter"
+ };
+
+ context.Result = new ObjectResult(errorInfo);
+ }
+ }
+ }
+
+ base.OnActionExecuting(context);
+ }
+ }
+}