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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -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,7 +20,10 @@ 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; }
}
}