Adding SerializableError - a serializable container for the purpose of output conneg.

This commit is contained in:
sornaks 2015-01-05 16:26:29 -08:00
parent 51567194dc
commit 5262dfd577
12 changed files with 582 additions and 29 deletions

View File

@ -0,0 +1,33 @@
// 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 Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// An <see cref="ObjectResult"/> that when executed will produce a Bad Request (400) response.
/// </summary>
public class BadRequestObjectResult : ObjectResult
{
/// <summary>
/// Creates a new <see cref="BadRequestObjectResult"/> instance.
/// </summary>
/// <param name="error">Contains the errors to be returned to the client.</param>
public BadRequestObjectResult(object error)
: base(error)
{
StatusCode = 400;
}
/// <summary>
/// Creates a new <see cref="BadRequestObjectResult"/> instance.
/// </summary>
/// <param name="modelState"><see cref="ModelStateDictionary"/> containing the validation errors.</param>
public BadRequestObjectResult([NotNull] ModelStateDictionary modelState)
: base(new SerializableError(modelState))
{
StatusCode = 400;
}
}
}

View File

@ -0,0 +1,104 @@
// 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.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Defines a serializable container for storing ModelState information.
/// This information is stored as key/value pairs.
/// </summary>
[XmlRoot("Error")]
public sealed class SerializableError : Dictionary<string, object>, IXmlSerializable
{
/// <summary>
/// Initializes a new instance of the <see cref="SerializableError"/> class.
/// </summary>
public SerializableError()
: base(StringComparer.OrdinalIgnoreCase)
{
}
/// <summary>
/// Creates a new instance of <see cref="SerializableError"/>.
/// </summary>
/// <param name="modelState"><see cref="ModelState"/> containing the validation errors.</param>
public SerializableError([NotNull] ModelStateDictionary modelState)
: this()
{
if (modelState.IsValid)
{
return;
}
foreach (var keyModelStatePair in modelState)
{
var key = keyModelStatePair.Key;
var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0)
{
var errorMessages = errors.Select(error =>
{
return string.IsNullOrEmpty(error.ErrorMessage) ?
Resources.SerializableError_DefaultError : error.ErrorMessage;
}).ToArray();
Add(key, errorMessages);
}
}
}
// <inheritdoc />
public XmlSchema GetSchema()
{
return null;
}
// <inheritdoc />
public void ReadXml(XmlReader reader)
{
if (reader.IsEmptyElement)
{
reader.Read();
return;
}
reader.ReadStartElement();
while (reader.NodeType != XmlNodeType.EndElement)
{
var key = XmlConvert.DecodeName(reader.LocalName);
var value = reader.ReadInnerXml();
Add(key, value);
reader.MoveToContent();
}
reader.ReadEndElement();
}
// <inheritdoc />
public void WriteXml(XmlWriter writer)
{
foreach (var keyValuePair in this)
{
var key = keyValuePair.Key;
var value = keyValuePair.Value;
writer.WriteStartElement(XmlConvert.EncodeLocalName(key));
if (value != null)
{
writer.WriteValue(value);
}
writer.WriteEndElement();
}
}
}
}

View File

@ -627,6 +627,26 @@ namespace Microsoft.AspNet.Mvc
return new BadRequestResult();
}
/// <summary>
/// Creates an <see cref="BadRequestObjectResult"/> that produces a Bad Request (400) response.
/// </summary>
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
[NonAction]
public virtual BadRequestObjectResult HttpBadRequest(object error)
{
return new BadRequestObjectResult(error);
}
/// <summary>
/// Creates an <see cref="BadRequestObjectResult"/> that produces a Bad Request (400) response.
/// </summary>
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
[NonAction]
public virtual BadRequestObjectResult HttpBadRequest([NotNull] ModelStateDictionary modelState)
{
return new BadRequestObjectResult(modelState);
}
/// <summary>
/// Creates a <see cref="CreatedResult"/> object that produces a Created (201) response.
/// </summary>

View File

@ -1514,6 +1514,22 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousTypeMatch_Item"), p0, p1);
}
/// <summary>
/// The input was not valid.
/// </summary>
internal static string SerializableError_DefaultError
{
get { return GetString("SerializableError_DefaultError"); }
}
/// <summary>
/// The input was not valid.
/// </summary>
internal static string FormatSerializableError_DefaultError()
{
return GetString("SerializableError_DefaultError");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -409,4 +409,7 @@
<data name="ViewComponent_AmbiguousTypeMatch_Item" xml:space="preserve">
<value>Type: '{0}' - Name: '{1}'</value>
</data>
<data name="SerializableError_DefaultError" xml:space="preserve">
<value>The input was not valid.</value>
</data>
</root>

View File

@ -0,0 +1,35 @@
// 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 Xunit;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc
{
public class BadRequestObjectResultTests
{
[Fact]
public void BadRequestObjectResult_SetsStatusCodeAndValue()
{
// Arrange & Act
var obj = new object();
var badRequestObjecResult = new BadRequestObjectResult(obj);
// Assert
Assert.Equal(400, badRequestObjecResult.StatusCode);
Assert.Equal(obj, badRequestObjecResult.Value);
}
[Fact]
public void BadRequestObjectResult_ModelState_SetsStatusCodeAndValue()
{
// Arrange & Act
var badRequestObjecResult = new BadRequestObjectResult(new ModelStateDictionary());
// Assert
Assert.Equal(400, badRequestObjecResult.StatusCode);
var errors = Assert.IsType<SerializableError>(badRequestObjecResult.Value);
Assert.Equal(0, errors.Count);
}
}
}

View File

@ -0,0 +1,146 @@
// 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.Globalization;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;
using Microsoft.AspNet.Mvc.ModelBinding;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class SerializableErrorTests
{
[Fact]
public void ConvertsModelState_To_Dictionary()
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.AddModelError("key1", "Test Error 1");
modelState.AddModelError("key1", "Test Error 2");
modelState.AddModelError("key2", "Test Error 3");
// Act
var serializableError = new SerializableError(modelState);
// Assert
var arr = Assert.IsType<string[]>(serializableError["key1"]);
Assert.Equal("Test Error 1", arr[0]);
Assert.Equal("Test Error 2", arr[1]);
Assert.Equal("Test Error 3", (serializableError["key2"] as string[])[0]);
}
[Fact]
public void LookupIsCaseInsensitive()
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.AddModelError("key1", "x");
// Act
var serializableError = new SerializableError(modelState);
// Assert
var arr = Assert.IsType<string[]>(serializableError["KEY1"]);
Assert.Equal("x", arr[0]);
}
[Fact]
public void ConvertsModelState_To_Dictionary_AddsDefaultValuesWhenErrorsAreAbsent()
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.AddModelError("key1", "");
// Act
var serializableError = new SerializableError(modelState);
// Assert
var arr = Assert.IsType<string[]>(serializableError["key1"]);
Assert.Equal("The input was not valid.", arr[0]);
}
[Fact]
public void DoesNotThrowOnValidModelState()
{
// Arrange, Act & Assert (does not throw)
new SerializableError(new ModelStateDictionary());
}
[Fact]
public void DoesNotAddEntries_IfNoErrorsArePresent()
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.Add(
"key1",
new ModelState() { Value = new ValueProviderResult("foo", "foo", CultureInfo.InvariantCulture) });
modelState.Add(
"key2",
new ModelState() { Value = new ValueProviderResult("bar", "bar", CultureInfo.InvariantCulture) });
// Act
var serializableError = new SerializableError(modelState);
// Assert
Assert.Equal(0, serializableError.Count);
}
[Fact]
public void GetSchema_Returns_Null()
{
// Arrange
var modelState = new ModelStateDictionary();
// To make modelState invalid.
modelState.AddModelError("key1", "Test Error 1");
var serializableError = new SerializableError(modelState);
// Act & Assert
Assert.Null(serializableError.GetSchema());
}
[Fact]
public void WriteXml_WritesValidXml()
{
// Arrange
var modelState = new ModelStateDictionary();
modelState.AddModelError("key1", "Test Error 1");
modelState.AddModelError("key1", "Test Error 2");
modelState.AddModelError("key2", "Test Error 3");
var serializableError = new SerializableError(modelState);
var outputStream = new MemoryStream();
// Act
using (var xmlWriter = XmlWriter.Create(outputStream))
{
var dataContractSerializer = new DataContractSerializer(typeof(SerializableError));
dataContractSerializer.WriteObject(xmlWriter, serializableError);
}
outputStream.Position = 0;
var res = new StreamReader(outputStream, Encoding.UTF8).ReadToEnd();
// Assert
Assert.Equal("<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Error><key1>Test Error 1 Test Error 2</key1><key2>Test Error 3</key2></Error>", res);
}
[Fact]
public void ReadXml_ReadsSerializableErrorXml()
{
// Arrange
var serializableErrorXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<Error><key1>Test Error 1 Test Error 2</key1><key2>Test Error 3</key2></Error>";
var serializer = new DataContractSerializer(typeof(SerializableError));
// Act
var errors = (SerializableError)serializer.ReadObject(
new MemoryStream(Encoding.UTF8.GetBytes(serializableErrorXml)));
// Assert
Assert.Equal("Test Error 1 Test Error 2", errors["key1"]);
Assert.Equal("Test Error 3", errors["key2"]);
}
}
}

View File

@ -669,6 +669,38 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal(400, result.StatusCode);
}
[Fact]
public void BadRequest_SetsStatusCodeAndValue_Object()
{
// Arrange
var controller = new Controller();
var obj = new object();
// Act
var result = controller.HttpBadRequest(obj);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal(400, result.StatusCode);
Assert.Equal(obj, result.Value);
}
[Fact]
public void BadRequest_SetsStatusCodeAndValue_ModelState()
{
// Arrange
var controller = new Controller();
// Act
var result = controller.HttpBadRequest(new ModelStateDictionary());
// Assert
Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal(400, result.StatusCode);
var errors = Assert.IsType<SerializableError>(result.Value);
Assert.Equal(0, errors.Count);
}
[Theory]
[MemberData(nameof(PublicNormalMethodsFromController))]
public void NonActionAttribute_IsOnEveryPublicNormalMethodFromController(MethodInfo method)

View File

@ -2,10 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using ActionResultsWebSite;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
@ -17,6 +20,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
{
private readonly IServiceProvider _provider = TestHelper.CreateServices("ActionResultsWebSite");
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
private const string sampleIntError = "The field SampleInt must be between 10 and 100.";
private const string sampleStringError =
"The field SampleString must be a string or array type with a minimum length of '15'.";
[Fact]
public async Task BadRequestResult_CanBeReturned()
@ -181,5 +187,97 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("http://localhost/foo/ActionResultsVerification/GetDummy/1", response.Headers.Location.OriginalString);
Assert.Equal("{\"SampleInt\":10,\"SampleString\":\"Foo\"}", await response.Content.ReadAsStringAsync());
}
[Theory]
[InlineData("http://localhost/Home/Index",
"application/json;charset=utf-8",
"{\"test.SampleInt\":[\"" + sampleIntError + "\"]," +
"\"test.SampleString\":" +
"[\"" + sampleStringError + "\"]}")]
[InlineData("http://localhost/Home/Index",
"application/xml;charset=utf-8",
"<Error><test.SampleInt>" + sampleIntError + "</test.SampleInt>" +
"<test.SampleString>" + sampleStringError +
"</test.SampleString></Error>")]
[InlineData("http://localhost/XmlSerializer/GetSerializableError",
"application/xml;charset=utf-8",
"<Error><test.SampleInt>" + sampleIntError + "</test.SampleInt>" +
"<test.SampleString>" + sampleStringError +
"</test.SampleString></Error>")]
public async Task SerializableErrorIsReturnedInExpectedFormat(string url, string outputFormat, string output)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<DummyClass xmlns=\"http://schemas.datacontract.org/2004/07/ActionResultsWebSite\">" +
"<SampleInt>2</SampleInt><SampleString>foo</SampleString></DummyClass>";
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(outputFormat));
request.Content = new StringContent(input, Encoding.UTF8, "application/xml");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal(output, await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task SerializableError_CanSerializeNormalObjects()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<DummyClass xmlns=\"http://schemas.datacontract.org/2004/07/ActionResultsWebSite\">" +
"<SampleInt>2</SampleInt><SampleString>foo</SampleString></DummyClass>";
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Home/GetCustomErrorObject");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;charset=utf-8"));
request.Content = new StringContent(input, Encoding.UTF8, "application/xml");
// Act
var response = await client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal("[\"Something went wrong with the model.\"]",
await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task SerializableError_ReadTheReturnedXml()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<DummyClass xmlns=\"http://schemas.datacontract.org/2004/07/ActionResultsWebSite\">" +
"<SampleInt>20</SampleInt><SampleString>foo</SampleString></DummyClass>";
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Home/Index");
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/xml;charset=utf-8"));
request.Content = new StringContent(input, Encoding.UTF8, "application/xml");
// Act
var response = await client.SendAsync(request);
var responseContent = await response.Content.ReadAsStringAsync();
// Deserializing Xml content
var serializer = new XmlSerializer(typeof(SerializableError));
var errors = (SerializableError)serializer.Deserialize(
new MemoryStream(Encoding.UTF8.GetBytes(responseContent)));
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal(
"<Error><test.SampleString>" + sampleStringError + "</test.SampleString></Error>",
responseContent);
Assert.Equal(sampleStringError, errors["test.SampleString"]);
}
}
}

View File

@ -0,0 +1,33 @@
// 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 Microsoft.AspNet.Mvc;
namespace ActionResultsWebSite
{
public class HomeController : Controller
{
public IActionResult Index([FromBody] DummyClass test)
{
if (!ModelState.IsValid)
{
return HttpBadRequest(ModelState);
}
return Content("Hello World!");
}
public IActionResult GetCustomErrorObject([FromBody] DummyClass test)
{
if (!ModelState.IsValid)
{
var errors = new List<string>();
errors.Add("Something went wrong with the model.");
return new BadRequestObjectResult(errors);
}
return Content("Hello World!");
}
}
}

View File

@ -0,0 +1,31 @@
// 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 Microsoft.AspNet.Mvc;
namespace ActionResultsWebSite
{
public class XmlSerializerController : Controller
{
public IActionResult GetSerializableError([FromBody] DummyClass test)
{
if (!ModelState.IsValid)
{
return HttpBadRequest(ModelState);
}
return Content("Success!");
}
public override void OnActionExecuted(ActionExecutedContext context)
{
var result = context.Result as ObjectResult;
if (result != null)
{
result.Formatters.Add(new XmlSerializerOutputFormatter());
}
base.OnActionExecuted(context);
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// 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.ComponentModel.DataAnnotations;
@ -7,8 +7,10 @@ namespace ActionResultsWebSite
{
public class DummyClass
{
[Range(10, 100)]
public int SampleInt { get; set; }
[MinLength(15)]
public string SampleString { get; set; }
}
}