Skip to content

Commit

Permalink
Adding ModelStateError if there is no input formatter selected.
Browse files Browse the repository at this point in the history
  • Loading branch information
harshgMSFT committed Aug 22, 2014
1 parent 9faca78 commit 313a537
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 25 deletions.
16 changes: 11 additions & 5 deletions src/Microsoft.AspNet.Mvc.Core/FilterActionInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,20 @@ internal async Task<IDictionary<string, object>> GetActionArguments(ModelStateDi
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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
{
Expand All @@ -19,13 +15,8 @@ public IInputFormatter SelectFormatter(InputFormatterContext context)
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;
}
}
}
16 changes: 16 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@
<data name="InputFormatterNoEncoding" xml:space="preserve">
<value>No encoding found for input formatter '{0}'. There must be at least one supported encoding registered in order for the formatter to read content.</value>
</data>
<data name="UnsupportedContentType" xml:space="preserve">
<value>Unsupported content type '{0}'.</value>
</data>
<data name="OutputFormatterNoMediaType" xml:space="preserve">
<value>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.</value>
</data>
Expand Down
71 changes: 71 additions & 0 deletions test/Microsoft.AspNet.Mvc.Core.Test/ReflectedActionInvokerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand Down Expand Up @@ -1454,6 +1455,64 @@ public async Task GetActionArguments_AddsActionArgumentsToModelStateDictionary_I
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<ParameterDescriptor>
{
new ParameterDescriptor
{
Name = "bodyParam",
BodyParameterInfo = new BodyParameterInfo(typeof(Person))
}
},
FilterDescriptors = new List<FilterDescriptor>()
};

var context = new DefaultHttpContext();
var routeContext = new RouteContext(context);
var actionContext = new ActionContext(routeContext,
actionDescriptor);
var bindingContext = new ActionBindingContext(actionContext,
Mock.Of<IModelMetadataProvider>(),
Mock.Of<IModelBinder>(),
Mock.Of<IValueProvider>(),
Mock.Of<IInputFormatterSelector>(),
Enumerable.Empty<IModelValidatorProvider>());

var actionBindingContextProvider = new Mock<IActionBindingContextProvider>();
actionBindingContextProvider.Setup(p => p.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
.Returns(Task.FromResult(bindingContext));
var controllerFactory = new Mock<IControllerFactory>();
controllerFactory.Setup(c => c.CreateController(It.IsAny<ActionContext>()))
.Returns(new TestController());
var inputFormattersProvider = new Mock<IInputFormattersProvider>();
inputFormattersProvider.SetupGet(o => o.InputFormatters)
.Returns(new List<IInputFormatter>());
var invoker = new ReflectedActionInvoker(actionContext,
actionBindingContextProvider.Object,
Mock.Of<INestedProviderManager<FilterProviderContext>>(),
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()
{
Expand Down Expand Up @@ -1524,6 +1583,18 @@ public JsonResult ThrowingActionMethod()
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)
Expand Down
39 changes: 30 additions & 9 deletions test/Microsoft.AspNet.Mvc.FunctionalTests/InputFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Newtonsoft.Json;
using Xunit;

namespace Microsoft.AspNet.Mvc.FunctionalTests
Expand Down Expand Up @@ -60,23 +61,43 @@ public async Task JsonInputFormatter_IsSelectedForJsonRequest(string requestCont
}

[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<InvalidOperationException>
(() => 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<FormatterWebSite.ErrorInfo>(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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
18 changes: 18 additions & 0 deletions test/WebSites/FormatterWebSite/Models/ErrorInfo.cs
Original file line number Diff line number Diff line change
@@ -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<string> Errors { get; set; }
}
}
39 changes: 39 additions & 0 deletions test/WebSites/FormatterWebSite/ValidateBodyParameterAttribute.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

0 comments on commit 313a537

Please sign in to comment.