// Copyright (c) .NET Foundation. 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.Concurrent; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; using System.Xml; using Microsoft.AspNetCore.Mvc.Formatters.Xml; using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.Formatters { /// /// This class handles serialization of objects /// to XML using /// public class XmlDataContractSerializerOutputFormatter : TextOutputFormatter { private readonly ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); private readonly ILogger _logger; private DataContractSerializerSettings _serializerSettings; /// /// Initializes a new instance of /// with default . /// public XmlDataContractSerializerOutputFormatter() : this(FormattingUtilities.GetDefaultXmlWriterSettings()) { } /// /// Initializes a new instance of /// with default . /// /// The . public XmlDataContractSerializerOutputFormatter(ILoggerFactory loggerFactory) : this(FormattingUtilities.GetDefaultXmlWriterSettings(), loggerFactory) { } /// /// Initializes a new instance of . /// /// The settings to be used by the . public XmlDataContractSerializerOutputFormatter(XmlWriterSettings writerSettings) : this(writerSettings, loggerFactory: null) { } /// /// Initializes a new instance of . /// /// The settings to be used by the . /// The . public XmlDataContractSerializerOutputFormatter(XmlWriterSettings writerSettings, ILoggerFactory loggerFactory) { if (writerSettings == null) { throw new ArgumentNullException(nameof(writerSettings)); } SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationXml); SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax); WriterSettings = writerSettings; _serializerSettings = new DataContractSerializerSettings(); WrapperProviderFactories = new List(); WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories)); WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory()); _logger = loggerFactory?.CreateLogger(GetType()); } /// /// Gets the list of to /// provide the wrapping type for serialization. /// public IList WrapperProviderFactories { get; } /// /// Gets the settings to be used by the XmlWriter. /// public XmlWriterSettings WriterSettings { get; } /// /// Gets or sets the used to configure the /// . /// public DataContractSerializerSettings SerializerSettings { get => _serializerSettings; set { if (value == null) { throw new ArgumentNullException(nameof(value)); } _serializerSettings = value; } } /// /// Gets the type to be serialized. /// /// The original type to be serialized /// The original or wrapped type provided by any s. protected virtual Type GetSerializableType(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type)); } var wrapperProvider = WrapperProviderFactories.GetWrapperProvider(new WrapperProviderContext( type, isSerialization: true)); return wrapperProvider?.WrappingType ?? type; } /// protected override bool CanWriteType(Type type) { if (type == null) { return false; } return GetCachedSerializer(GetSerializableType(type)) != null; } /// /// Create a new instance of for the given object type. /// /// The type of object for which the serializer should be created. /// A new instance of protected virtual DataContractSerializer CreateSerializer(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type)); } try { // Verify that type is a valid data contract by forcing the serializer to try to create a data contract FormattingUtilities.XsdDataContractExporter.GetRootElementName(type); // If the serializer does not support this type it will throw an exception. return new DataContractSerializer(type, _serializerSettings); } catch (Exception ex) { _logger?.FailedToCreateDataContractSerializer(type.FullName, ex); // We do not surface the caught exception because if CanWriteResult returns // false, then this Formatter is not picked up at all. return null; } } /// /// Creates a new instance of using the given and /// . /// /// /// The underlying which the should write to. /// /// /// The . /// /// A new instance of public virtual XmlWriter CreateXmlWriter( TextWriter writer, XmlWriterSettings xmlWriterSettings) { if (writer == null) { throw new ArgumentNullException(nameof(writer)); } if (xmlWriterSettings == null) { throw new ArgumentNullException(nameof(xmlWriterSettings)); } // We always close the TextWriter, so the XmlWriter shouldn't. xmlWriterSettings.CloseOutput = false; return XmlWriter.Create(writer, xmlWriterSettings); } /// /// Creates a new instance of using the given and /// . /// /// The formatter context associated with the call. /// /// The underlying which the should write to. /// /// /// The . /// /// A new instance of . public virtual XmlWriter CreateXmlWriter( OutputFormatterWriteContext context, TextWriter writer, XmlWriterSettings xmlWriterSettings) { return CreateXmlWriter(writer, xmlWriterSettings); } /// public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (selectedEncoding == null) { throw new ArgumentNullException(nameof(selectedEncoding)); } var writerSettings = WriterSettings.Clone(); writerSettings.Encoding = selectedEncoding; // Wrap the object only if there is a wrapping type. var value = context.Object; var wrappingType = GetSerializableType(context.ObjectType); if (wrappingType != null && wrappingType != context.ObjectType) { var wrapperProvider = WrapperProviderFactories.GetWrapperProvider(new WrapperProviderContext( declaredType: context.ObjectType, isSerialization: true)); value = wrapperProvider.Wrap(value); } var dataContractSerializer = GetCachedSerializer(wrappingType); using (var textWriter = context.WriterFactory(context.HttpContext.Response.Body, writerSettings.Encoding)) { using (var xmlWriter = CreateXmlWriter(context, textWriter, writerSettings)) { dataContractSerializer.WriteObject(xmlWriter, value); } // Perf: call FlushAsync to call WriteAsync on the stream with any content left in the TextWriter's // buffers. This is better than just letting dispose handle it (which would result in a synchronous // write). await textWriter.FlushAsync(); } } /// /// Gets the cached serializer or creates and caches the serializer for the given type. /// /// The instance. protected virtual DataContractSerializer GetCachedSerializer(Type type) { if (!_serializerCache.TryGetValue(type, out var serializer)) { serializer = CreateSerializer(type); if (serializer != null) { _serializerCache.TryAdd(type, serializer); } } return (DataContractSerializer)serializer; } } }