From d236d4ffde1f830dacd4ede323ea78cd5b6d7180 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Thu, 12 Feb 2015 17:37:25 -0800 Subject: [PATCH] Added CanReadType to InputFormatter --- .../Formatters/InputFormatter.cs | 15 ++++++ ...XmlDataContractSerializerInputFormatter.cs | 41 +++++++++++++++- ...mlDataContractSerializerOutputFormatter.cs | 29 ++++++++++- .../XmlSerializerInputFormatter.cs | 41 +++++++++++++++- .../XmlSerializerOutputFormatter.cs | 30 +++++++++++- ...ataContractSerializerInputFormatterTest.cs | 30 +++++++++++- ...taContractSerializerOutputFormatterTest.cs | 27 ++++++++++ .../XmlSerializerInputFormatterTest.cs | 49 +++++++++++++++++++ .../XmlSerializerOutputFormatterTest.cs | 28 +++++++++++ 9 files changed, 281 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs b/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs index 9962de7a39..769bad4837 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Formatters/InputFormatter.cs @@ -43,6 +43,11 @@ namespace Microsoft.AspNet.Mvc /// public virtual bool CanRead(InputFormatterContext context) { + if (!CanReadType(context.ModelType)) + { + return false; + } + var contentType = context.ActionContext.HttpContext.Request.ContentType; MediaTypeHeaderValue requestContentType; if (!MediaTypeHeaderValue.TryParse(contentType, out requestContentType)) @@ -54,6 +59,16 @@ namespace Microsoft.AspNet.Mvc .Any(supportedMediaType => supportedMediaType.IsSubsetOf(requestContentType)); } + /// + /// Returns a value indicating whether or not the given type can be read by this serializer. + /// + /// The type of object that will be read. + /// true if the type can be read, otherwise false. + protected virtual bool CanReadType(Type type) + { + return true; + } + /// public virtual async Task ReadAsync(InputFormatterContext context) { diff --git a/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerInputFormatter.cs b/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerInputFormatter.cs index 810fdabec5..62c4689336 100644 --- a/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerInputFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; @@ -19,6 +20,7 @@ namespace Microsoft.AspNet.Mvc.Xml public class XmlDataContractSerializerInputFormatter : InputFormatter { private DataContractSerializerSettings _serializerSettings; + private ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); /// @@ -93,7 +95,7 @@ namespace Microsoft.AspNet.Mvc.Xml { var type = GetSerializableType(context.ModelType); - var serializer = CreateSerializer(type); + var serializer = GetCachedSerializer(type); var deserializedObject = serializer.ReadObject(xmlReader); @@ -111,6 +113,12 @@ namespace Microsoft.AspNet.Mvc.Xml } } + /// + protected override bool CanReadType([NotNull] Type type) + { + return GetCachedSerializer(GetSerializableType(type)) != null; + } + /// /// Called during deserialization to get the . /// @@ -142,7 +150,36 @@ namespace Microsoft.AspNet.Mvc.Xml /// The used during deserialization. protected virtual DataContractSerializer CreateSerializer([NotNull] Type type) { - return new DataContractSerializer(type, _serializerSettings); + try + { + // If the serializer does not support this type it will throw an exception. + return new DataContractSerializer(type, _serializerSettings); + } + catch (Exception) + { + // We do not surface the caught exception because if CanRead returns + // false, then this Formatter is not picked up at all. + return null; + } + } + + /// + /// Gets the cached serializer or creates and caches the serializer for the given type. + /// + /// The instance. + protected virtual DataContractSerializer GetCachedSerializer(Type type) + { + object serializer; + if (!_serializerCache.TryGetValue(type, out serializer)) + { + serializer = CreateSerializer(type); + if (serializer != null) + { + _serializerCache.TryAdd(type, serializer); + } + } + + return (DataContractSerializer)serializer; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerOutputFormatter.cs index 285d8416d5..51dad77720 100644 --- a/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Xml/XmlDataContractSerializerOutputFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; @@ -19,6 +20,7 @@ namespace Microsoft.AspNet.Mvc.Xml public class XmlDataContractSerializerOutputFormatter : OutputFormatter { private DataContractSerializerSettings _serializerSettings; + private ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); /// /// Initializes a new instance of @@ -115,8 +117,12 @@ namespace Microsoft.AspNet.Mvc.Xml protected override bool CanWriteType(Type declaredType, Type runtimeType) { var type = ResolveType(declaredType, runtimeType); + if (type == null) + { + return false; + } - return CreateSerializer(GetSerializableType(type)) != null; + return GetCachedSerializer(GetSerializableType(type)) != null; } /// @@ -183,11 +189,30 @@ namespace Microsoft.AspNet.Mvc.Xml obj = wrapperProvider.Wrap(obj); } - var dataContractSerializer = CreateSerializer(wrappingType); + var dataContractSerializer = GetCachedSerializer(wrappingType); dataContractSerializer.WriteObject(xmlWriter, obj); } return Task.FromResult(true); } + + /// + /// Gets the cached serializer or creates and caches the serializer for the given type. + /// + /// The instance. + protected virtual DataContractSerializer GetCachedSerializer(Type type) + { + object serializer; + if (!_serializerCache.TryGetValue(type, out serializer)) + { + serializer = CreateSerializer(type); + if (serializer != null) + { + _serializerCache.TryAdd(type, serializer); + } + } + + return (DataContractSerializer)serializer; + } } } diff --git a/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerInputFormatter.cs b/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerInputFormatter.cs index bc661b4f91..e8c34b0587 100644 --- a/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerInputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerInputFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -18,6 +19,7 @@ namespace Microsoft.AspNet.Mvc.Xml /// public class XmlSerializerInputFormatter : InputFormatter { + private ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); /// @@ -72,7 +74,7 @@ namespace Microsoft.AspNet.Mvc.Xml { var type = GetSerializableType(context.ModelType); - var serializer = CreateSerializer(type); + var serializer = GetCachedSerializer(type); var deserializedObject = serializer.Deserialize(xmlReader); @@ -90,6 +92,12 @@ namespace Microsoft.AspNet.Mvc.Xml } } + /// + protected override bool CanReadType([NotNull] Type type) + { + return GetCachedSerializer(GetSerializableType(type)) != null; + } + /// /// Gets the type to which the XML will be deserialized. /// @@ -120,7 +128,36 @@ namespace Microsoft.AspNet.Mvc.Xml /// The used during deserialization. protected virtual XmlSerializer CreateSerializer(Type type) { - return new XmlSerializer(type); + try + { + // If the serializer does not support this type it will throw an exception. + return new XmlSerializer(type); + } + catch (Exception) + { + // We do not surface the caught exception because if CanRead returns + // false, then this Formatter is not picked up at all. + return null; + } + } + + /// + /// Gets the cached serializer or creates and caches the serializer for the given type. + /// + /// The instance. + protected virtual XmlSerializer GetCachedSerializer(Type type) + { + object serializer; + if (!_serializerCache.TryGetValue(type, out serializer)) + { + serializer = CreateSerializer(type); + if (serializer != null) + { + _serializerCache.TryAdd(type, serializer); + } + } + + return (XmlSerializer) serializer; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerOutputFormatter.cs b/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerOutputFormatter.cs index d485d268f9..db7423ec13 100644 --- a/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerOutputFormatter.cs +++ b/src/Microsoft.AspNet.Mvc.Xml/XmlSerializerOutputFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -18,6 +19,8 @@ namespace Microsoft.AspNet.Mvc.Xml /// public class XmlSerializerOutputFormatter : OutputFormatter { + private ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); + /// /// Initializes a new instance of /// with default XmlWriterSettings. @@ -93,8 +96,12 @@ namespace Microsoft.AspNet.Mvc.Xml protected override bool CanWriteType(Type declaredType, Type runtimeType) { var type = ResolveType(declaredType, runtimeType); + if (type == null) + { + return false; + } - return CreateSerializer(GetSerializableType(type)) != null; + return GetCachedSerializer(GetSerializableType(type)) != null; } /// @@ -159,11 +166,30 @@ namespace Microsoft.AspNet.Mvc.Xml obj = wrapperProvider.Wrap(obj); } - var xmlSerializer = CreateSerializer(wrappingType); + var xmlSerializer = GetCachedSerializer(wrappingType); xmlSerializer.Serialize(xmlWriter, obj); } return Task.FromResult(true); } + + /// + /// Gets the cached serializer or creates and caches the serializer for the given type. + /// + /// The instance. + protected virtual XmlSerializer GetCachedSerializer(Type type) + { + object serializer; + if (!_serializerCache.TryGetValue(type, out serializer)) + { + serializer = CreateSerializer(type); + if (serializer != null) + { + _serializerCache.TryAdd(type, serializer); + } + } + + return (XmlSerializer)serializer; + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs index 0785d58fe4..8f09180515 100644 --- a/test/Microsoft.AspNet.Mvc.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Xml.Test/XmlDataContractSerializerInputFormatterTest.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using System.Xml; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ModelBinding; -using Microsoft.AspNet.Mvc.Xml; using Microsoft.AspNet.Testing; using Moq; using Xunit; @@ -79,6 +78,24 @@ namespace Microsoft.AspNet.Mvc.Xml Assert.Equal(expectedCanRead, result); } + [Fact] + public void XmlDataContractSerializer_CachesSerializerForType() + { + // Arrange + var input = "" + + "10"; + var formatter = new TestXmlDataContractSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); + + // Act + formatter.CanRead(context); + formatter.CanRead(context); + + // Assert + Assert.Equal(1, formatter.createSerializerCalledCount); + } + [Fact] public void HasProperSuppportedMediaTypes() { @@ -485,5 +502,16 @@ namespace Microsoft.AspNet.Mvc.Xml httpContext.SetupGet(c => c.Request).Returns(request.Object); return httpContext.Object; } + + private class TestXmlDataContractSerializerInputFormatter : XmlDataContractSerializerInputFormatter + { + public int createSerializerCalledCount = 0; + + protected override DataContractSerializer CreateSerializer(Type type) + { + createSerializerCalledCount++; + return base.CreateSerializer(type); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Xml.Test/XmlDataContractSerializerOutputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Xml.Test/XmlDataContractSerializerOutputFormatterTest.cs index 95915ac472..111cd544f6 100644 --- a/test/Microsoft.AspNet.Mvc.Xml.Test/XmlDataContractSerializerOutputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Xml.Test/XmlDataContractSerializerOutputFormatterTest.cs @@ -111,6 +111,22 @@ namespace Microsoft.AspNet.Mvc.Xml Assert.True(outputFormatterContext.ActionContext.HttpContext.Response.Body.CanRead); } + [Fact] + public void XmlDataContractSerializer_CachesSerializerForType() + { + // Arrange + var input = new DummyClass { SampleInt = 10 }; + var formatter = new TestXmlDataContractSerializerOutputFormatter(); + var context = GetOutputFormatterContext(input, typeof(DummyClass)); + + // Act + formatter.CanWriteResult(context, MediaTypeHeaderValue.Parse("application/xml")); + formatter.CanWriteResult(context, MediaTypeHeaderValue.Parse("application/xml")); + + // Assert + Assert.Equal(1, formatter.createSerializerCalledCount); + } + [Fact] public void DefaultConstructor_ExpectedWriterSettings_Created() { @@ -552,5 +568,16 @@ namespace Microsoft.AspNet.Mvc.Xml httpContext.SetupGet(c => c.Response).Returns(response.Object); return new ActionContext(httpContext.Object, routeData: null, actionDescriptor: null); } + + private class TestXmlDataContractSerializerOutputFormatter : XmlDataContractSerializerOutputFormatter + { + public int createSerializerCalledCount = 0; + + protected override DataContractSerializer CreateSerializer(Type type) + { + createSerializerCalledCount++; + return base.CreateSerializer(type); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Xml.Test/XmlSerializerInputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Xml.Test/XmlSerializerInputFormatterTest.cs index 4f1dab7dc2..1e8103b3b0 100644 --- a/test/Microsoft.AspNet.Mvc.Xml.Test/XmlSerializerInputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Xml.Test/XmlSerializerInputFormatterTest.cs @@ -3,11 +3,13 @@ #if ASPNET50 using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; +using System.Xml.Serialization; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.Xml; @@ -64,6 +66,42 @@ namespace Microsoft.AspNet.Mvc.Xml Assert.Equal(expectedCanRead, result); } + [Theory] + [InlineData(typeof(Dictionary), false)] + [InlineData(typeof(string), true)] + public void CanRead_ReturnsFalse_ForAnyUnsupportedModelType(Type modelType, bool expectedCanRead) + { + // Arrange + var formatter = new XmlSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes("content"); + + var context = GetInputFormatterContext(contentBytes, modelType); + + // Act + var result = formatter.CanRead(context); + + // Assert + Assert.Equal(expectedCanRead, result); + } + + [Fact] + public void XmlSerializer_CachesSerializerForType() + { + // Arrange + var input = "" + + "10"; + var formatter = new TestXmlSerializerInputFormatter(); + var contentBytes = Encoding.UTF8.GetBytes(input); + var context = GetInputFormatterContext(contentBytes, typeof(DummyClass)); + + // Act + formatter.CanRead(context); + formatter.CanRead(context); + + // Assert + Assert.Equal(1, formatter.createSerializerCalledCount); + } + [Fact] public void HasProperSuppportedMediaTypes() { @@ -374,6 +412,17 @@ namespace Microsoft.AspNet.Mvc.Xml httpContext.SetupGet(c => c.Request).Returns(request.Object); return httpContext.Object; } + + private class TestXmlSerializerInputFormatter : XmlSerializerInputFormatter + { + public int createSerializerCalledCount = 0; + + protected override XmlSerializer CreateSerializer(Type type) + { + createSerializerCalledCount++; + return base.CreateSerializer(type); + } + } } } #endif diff --git a/test/Microsoft.AspNet.Mvc.Xml.Test/XmlSerializerOutputFormatterTest.cs b/test/Microsoft.AspNet.Mvc.Xml.Test/XmlSerializerOutputFormatterTest.cs index 4ba727cd2d..634c7d2940 100644 --- a/test/Microsoft.AspNet.Mvc.Xml.Test/XmlSerializerOutputFormatterTest.cs +++ b/test/Microsoft.AspNet.Mvc.Xml.Test/XmlSerializerOutputFormatterTest.cs @@ -13,6 +13,7 @@ using Microsoft.Net.Http.Headers; using Microsoft.AspNet.Mvc.Xml; using Moq; using Xunit; +using System.Xml.Serialization; namespace Microsoft.AspNet.Mvc.Xml { @@ -68,6 +69,22 @@ namespace Microsoft.AspNet.Mvc.Xml Assert.True(outputFormatterContext.ActionContext.HttpContext.Response.Body.CanRead); } + [Fact] + public void XmlSerializer_CachesSerializerForType() + { + // Arrange + var input = new DummyClass { SampleInt = 10 }; + var formatter = new TestXmlSerializerOutputFormatter(); + var context = GetOutputFormatterContext(input, typeof(DummyClass)); + + // Act + formatter.CanWriteResult(context, MediaTypeHeaderValue.Parse("application/xml")); + formatter.CanWriteResult(context, MediaTypeHeaderValue.Parse("application/xml")); + + // Assert + Assert.Equal(1, formatter.createSerializerCalledCount); + } + [Fact] public void DefaultConstructor_ExpectedWriterSettings_Created() { @@ -355,5 +372,16 @@ namespace Microsoft.AspNet.Mvc.Xml httpContext.SetupGet(c => c.Response).Returns(response.Object); return new ActionContext(httpContext.Object, routeData: null, actionDescriptor: null); } + + private class TestXmlSerializerOutputFormatter : XmlSerializerOutputFormatter + { + public int createSerializerCalledCount = 0; + + protected override XmlSerializer CreateSerializer(Type type) + { + createSerializerCalledCount++; + return base.CreateSerializer(type); + } + } } } \ No newline at end of file