From 313a537ea1f398cecce74054c0454a49b1ee5e62 Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Wed, 20 Aug 2014 15:04:16 -0700 Subject: [PATCH] Adding ModelStateError if there is no input formatter selected. --- .../FilterActionInvoker.cs | 16 +++-- .../DefaultInputFormatterSelector.cs | 13 +--- .../Properties/Resources.Designer.cs | 16 +++++ src/Microsoft.AspNet.Mvc.Core/Resources.resx | 3 + .../ReflectedActionInvokerTest.cs | 71 +++++++++++++++++++ .../InputFormatterTests.cs | 39 +++++++--- .../Controllers/InputFormatterController.cs | 40 +++++++++++ .../FormatterWebSite/Models/ErrorInfo.cs | 18 +++++ .../ValidateBodyParameterAttribute.cs | 39 ++++++++++ 9 files changed, 230 insertions(+), 25 deletions(-) create mode 100644 test/WebSites/FormatterWebSite/Controllers/InputFormatterController.cs create mode 100644 test/WebSites/FormatterWebSite/Models/ErrorInfo.cs create mode 100644 test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs 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); + } + } +}