using System; using System.Globalization; using System.IO; using System.Runtime.Serialization; using System.ServiceModel; using System.ServiceModel.Channels; using System.Xml; namespace SoapCore { public class FaultBodyWriter : BodyWriter { private const string Soap12Namespace = "http://www.w3.org/2003/05/soap-envelope"; private const string Soap11Namespace = "http://schemas.xmlsoap.org/soap/envelope/"; private readonly MessageVersion _version; private readonly Exception _exception; private readonly string _faultStringOverride; public FaultBodyWriter(Exception exception, MessageVersion version, bool isBuffered = true, string faultStringOverride = null) : base(isBuffered) { _version = version; _exception = exception; _faultStringOverride = faultStringOverride; } protected override void OnWriteBodyContents(XmlDictionaryWriter writer) { if (_version.Envelope == EnvelopeVersion.Soap12) { WriteSoap12Fault(writer); } else if (_version.Envelope == EnvelopeVersion.Soap11) { WriteSoap11Fault(writer); } else { // We will default to the oldest SOAP format instead of // breaking everything WriteSoap11Fault(writer); } } private void WriteSoap12Fault(XmlDictionaryWriter writer) { // NOTE: This default culture is a hack until a better localisation solution is // built. At this stage it assumes the current thread culture var defaultCulture = CultureInfo.CurrentCulture; var faultString = _faultStringOverride ?? (_exception.InnerException != null ? _exception.InnerException.Message : _exception.Message); var faultDetail = ExtractFaultDetailsAsXmlElement(_exception); var prefix = writer.LookupPrefix(Soap12Namespace) ?? "s"; writer.WriteStartElement(prefix, "Fault", Soap12Namespace); writer.WriteStartElement(prefix, "Code", Soap12Namespace); writer.WriteStartElement(prefix, "Value", Soap12Namespace); writer.WriteString(prefix + ":Sender"); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteStartElement(prefix, "Reason", Soap12Namespace); writer.WriteStartElement(prefix, "Text", Soap12Namespace); writer.WriteAttributeString("xml:lang", defaultCulture.IetfLanguageTag); writer.WriteString(faultString); writer.WriteEndElement(); writer.WriteEndElement(); if (faultDetail != null) { writer.WriteStartElement(prefix, "Detail", Soap12Namespace); faultDetail.WriteTo(writer); writer.WriteEndElement(); } writer.WriteEndElement(); } private void WriteSoap11Fault(XmlDictionaryWriter writer) { var faultString = _faultStringOverride ?? (_exception.InnerException != null ? _exception.InnerException.Message : _exception.Message); var faultDetail = ExtractFaultDetailsAsXmlElement(_exception); writer.WriteStartElement("Fault", Soap11Namespace); /* SUPPORT FOR SPECIFYING CUSTOM FAULTCODE AND NAMESPACE For Example, this would result in the response below: throw new System.ServiceModel.FaultException(new FaultReason("faultString1"), new FaultCode("faultCode1", "faultNamespace1"), "action1"); <s:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <s:Fault> <a:faultcode xmlns:a="faultNamespace1">a:faultCode1</a:faultcode> <faultstring>faultString1</faultstring> </s:Fault> </s:Body> </s:Envelope> For Example, this would result in the response below: throw new System.ServiceModel.FaultException(new FaultReason("faultString1"), new FaultCode("faultCode1"), "action1"); <s:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <s:Fault> <faultcode>s:faultCode1</faultcode> <faultstring>faultString1</faultstring> </s:Fault> </s:Body> </s:Envelope> */ if (_exception is FaultException) { var faultException = (FaultException)_exception; if (faultException != null && faultException.Code != null && !string.IsNullOrEmpty(faultException.Code.Name)) { if (!string.IsNullOrEmpty(faultException.Code.Namespace)) { writer.WriteElementString("a", "faultcode", faultException.Code.Namespace, "a:" + faultException.Code.Name); } else { writer.WriteElementString("faultcode", "s:" + faultException.Code.Name); } } else { writer.WriteElementString("faultcode", "s:Client"); } } else { writer.WriteElementString("faultcode", "s:Client"); } writer.WriteElementString("faultstring", faultString); if (faultDetail != null) { writer.WriteStartElement("detail"); faultDetail.WriteTo(writer); writer.WriteEndElement(); } writer.WriteEndElement(); } private XmlElement ExtractFaultDetailsAsXmlElement(Exception ex) { var detailObject = ExtractFaultDetail(ex); if (detailObject == null) { return null; } using (var ms = new MemoryStream()) { var serializer = new DataContractSerializer(detailObject.GetType()); serializer.WriteObject(ms, detailObject); ms.Position = 0; var doc = new XmlDocument(); doc.Load(ms); return doc.DocumentElement; } } /// <summary> /// Helper to extract object of a detailed fault. /// </summary> /// <param name="exception"> /// The exception that caused the failure. /// </param> /// <returns> /// Returns instance of T if the exception (or its InnerExceptions) is of type FaultException<T>. /// otherwise returns null /// </returns> private object ExtractFaultDetail(Exception exception) { try { var currentException = exception; while (currentException != null) { var type = currentException.GetType(); if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(FaultException<>)) { var detailInfo = type.GetProperty("Detail"); var value = detailInfo?.GetValue(currentException); if (value != null) { return value; } } currentException = currentException.InnerException; } } catch { return null; } return null; } } }