From d253c804f35956460df79740a7aebc5e98338418 Mon Sep 17 00:00:00 2001 From: ankitkmrpatel Date: Thu, 19 Oct 2023 13:05:24 +0530 Subject: [PATCH 1/8] feat: Added Optional Custom Serializer in Soap Core #968 --- README.md | 18 +++- samples/Sample.sln | 4 +- .../Serializer/ISoapCoreSerializer.cs | 12 +++ .../{ => Serializer}/SerializerHelper.cs | 38 ++++---- src/SoapCore/SoapEndpointExtensions.cs | 15 +++ src/SoapCore/SoapEndpointMiddleware.cs | 94 ++++++++++--------- 6 files changed, 117 insertions(+), 64 deletions(-) create mode 100644 src/SoapCore/Serializer/ISoapCoreSerializer.cs rename src/SoapCore/{ => Serializer}/SerializerHelper.cs (88%) diff --git a/README.md b/README.md index 66c8b1f9..0cd162cf 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,22 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF } ``` +### Using with custom implementation of Serialization + +There is an optional feature included where you can implment the ISoapCoreSerializer to built your own custom serializar for body. + +In Startup.cs: + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddSoapCore(); + services.TryAddSingleton(); + services..AddCustomSoapMessageSerializer(); + services.AddMvc(); +} +``` + ### Using with legacy WCF/WS It is possible to use SoapCore with .NET legacy WCF and Web Services, both as client and service. @@ -151,7 +167,7 @@ services.AddSoapMessageProcessor(async (message, httpcontext, next) => //finish by returning the modified message. return responseMessage; -} +}); ``` #### How to get custom HTTP header in SoapCore service diff --git a/samples/Sample.sln b/samples/Sample.sln index 4ce0a45b..5d3921c3 100644 --- a/samples/Sample.sln +++ b/samples/Sample.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30717.126 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34031.279 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Client\Client.csproj", "{EBFDF95D-F7CC-457F-8924-3CB6069FBA3B}" EndProject diff --git a/src/SoapCore/Serializer/ISoapCoreSerializer.cs b/src/SoapCore/Serializer/ISoapCoreSerializer.cs new file mode 100644 index 00000000..30397f1c --- /dev/null +++ b/src/SoapCore/Serializer/ISoapCoreSerializer.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Xml; + +namespace SoapCore.Serializer +{ + public interface ISoapCoreSerializer + { + object DeserializeInputParameter(XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs, ICustomAttributeProvider customAttributeProvider, IEnumerable knownTypes = null); + } +} diff --git a/src/SoapCore/SerializerHelper.cs b/src/SoapCore/Serializer/SerializerHelper.cs similarity index 88% rename from src/SoapCore/SerializerHelper.cs rename to src/SoapCore/Serializer/SerializerHelper.cs index f30ed73a..e7342bbe 100644 --- a/src/SoapCore/SerializerHelper.cs +++ b/src/SoapCore/Serializer/SerializerHelper.cs @@ -1,18 +1,18 @@ using System; using System.CodeDom; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using System.Xml; +using System.Xml; using System.Xml.Serialization; using Microsoft.CSharp; -namespace SoapCore +namespace SoapCore.Serializer { - internal class SerializerHelper + internal class SerializerHelper : ISoapCoreSerializer { private readonly SoapSerializer _serializer; @@ -22,7 +22,7 @@ public SerializerHelper(SoapSerializer serializer) } public object DeserializeInputParameter( - System.Xml.XmlDictionaryReader xmlReader, + XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs, @@ -30,7 +30,7 @@ public object DeserializeInputParameter( IEnumerable knownTypes = null) { // Advance past any whitespace. - while (xmlReader.NodeType == System.Xml.XmlNodeType.Whitespace && xmlReader.Read()) + while (xmlReader.NodeType == XmlNodeType.Whitespace && xmlReader.Read()) { } @@ -39,7 +39,7 @@ public object DeserializeInputParameter( xmlReader.MoveToStartElement(parameterName, parameterNs); if (xmlReader.IsStartElement(parameterName, parameterNs)) - { + { switch (_serializer) { case SoapSerializer.XmlSerializer: @@ -74,7 +74,7 @@ public object DeserializeInputParameter( return null; } - private static object DeserializeObject(System.Xml.XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs) + private static object DeserializeObject(XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs) { // see https://referencesource.microsoft.com/System.Xml/System/Xml/Serialization/XmlSerializer.cs.html#c97688a6c07294d5 var elementType = parameterType.GetElementType(); @@ -90,21 +90,21 @@ private static object DeserializeObject(System.Xml.XmlDictionaryReader xmlReader { xmlReader.Read(); return new MemoryStream(xmlReader.ReadContentAsBase64(), false); - } - - if (elementType == typeof(XmlElement) || elementType == typeof(XmlNode)) - { - var xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(xmlReader.ReadInnerXml()); - var xmlNode = xmlDoc.FirstChild; - return xmlNode; + } + + if (elementType == typeof(XmlElement) || elementType == typeof(XmlNode)) + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xmlReader.ReadInnerXml()); + var xmlNode = xmlDoc.FirstChild; + return xmlNode; } return serializer.Deserialize(xmlReader); } private static object DeserializeDataContract( - System.Xml.XmlDictionaryReader xmlReader, + XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs, @@ -124,7 +124,7 @@ private static object DeserializeDataContract( return serializer.ReadObject(xmlReader, verifyObjectName: true); } - private XmlElementAttribute ChoiceElementToSerialize(System.Xml.XmlDictionaryReader xmlReader, XmlElementAttribute[] xmlElementAttributes, string parameterNs) + private XmlElementAttribute ChoiceElementToSerialize(XmlDictionaryReader xmlReader, XmlElementAttribute[] xmlElementAttributes, string parameterNs) { if (xmlElementAttributes != null && xmlElementAttributes.Length > 0) { @@ -140,7 +140,7 @@ private XmlElementAttribute ChoiceElementToSerialize(System.Xml.XmlDictionaryRea return null; } - private object DeserializeArrayXmlSerializer(System.Xml.XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs, ICustomAttributeProvider customAttributeProvider) + private object DeserializeArrayXmlSerializer(XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs, ICustomAttributeProvider customAttributeProvider) { var xmlArrayAttributes = customAttributeProvider.GetCustomAttributes(typeof(XmlArrayItemAttribute), true); XmlArrayItemAttribute xmlArrayItemAttribute = xmlArrayAttributes.FirstOrDefault() as XmlArrayItemAttribute; diff --git a/src/SoapCore/SoapEndpointExtensions.cs b/src/SoapCore/SoapEndpointExtensions.cs index db764a53..147b861a 100644 --- a/src/SoapCore/SoapEndpointExtensions.cs +++ b/src/SoapCore/SoapEndpointExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using SoapCore.Extensibility; using SoapCore.Meta; +using SoapCore.Serializer; #if NETCOREAPP3_0_OR_GREATER using Microsoft.AspNetCore.Routing; @@ -448,5 +449,19 @@ public static IServiceCollection AddSoapMessageProcessor(this IServi serviceCollection.Add(new ServiceDescriptor(typeof(ISoapMessageProcessor), typeof(TProcessor), lifetime)); return serviceCollection; } + + + public static IServiceCollection AddCustomSoapMessageSerializer(this IServiceCollection serviceCollection, ISoapCoreSerializer messageSerializer) + { + serviceCollection.AddSingleton(messageSerializer); + return serviceCollection; + } + + public static IServiceCollection AddCustomSoapMessageSerializer(this IServiceCollection serviceCollection, ServiceLifetime lifetime = ServiceLifetime.Singleton) + where TProcessor : class, ISoapCoreSerializer + { + serviceCollection.Add(new ServiceDescriptor(typeof(ISoapCoreSerializer), typeof(TProcessor), lifetime)); + return serviceCollection; + } } } diff --git a/src/SoapCore/SoapEndpointMiddleware.cs b/src/SoapCore/SoapEndpointMiddleware.cs index 510eb9b0..f8e1c6ac 100644 --- a/src/SoapCore/SoapEndpointMiddleware.cs +++ b/src/SoapCore/SoapEndpointMiddleware.cs @@ -1,3 +1,17 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using SoapCore.DocumentationWriter; +using SoapCore.Extensibility; +using SoapCore.MessageEncoder; +using SoapCore.Meta; +using SoapCore.Serializer; +using SoapCore.ServiceModel; using System; using System.Collections.Generic; using System.Diagnostics; @@ -10,22 +24,8 @@ using System.Security.Authentication; using System.ServiceModel; using System.ServiceModel.Channels; -using System.Text; using System.Threading.Tasks; using System.Xml; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using SoapCore.DocumentationWriter; -using SoapCore.Extensibility; -using SoapCore.MessageEncoder; -using SoapCore.Meta; -using SoapCore.ServiceModel; namespace SoapCore { @@ -35,42 +35,51 @@ public class SoapEndpointMiddleware private readonly ILogger> _logger; private readonly RequestDelegate _next; private readonly SoapOptions _options; + private readonly IServiceProvider _serviceProvider; private readonly ServiceDescription _service; private readonly StringComparison _pathComparisonStrategy; private readonly SoapMessageEncoder[] _messageEncoders; - private readonly SerializerHelper _serializerHelper; + private readonly ISoapCoreSerializer _serializerHelper; [Obsolete] - public SoapEndpointMiddleware(ILogger> logger, RequestDelegate next, Type serviceType, string path, SoapEncoderOptions[] encoderOptions, SoapSerializer serializer, bool caseInsensitivePath, ISoapModelBounder soapModelBounder, Binding binding, bool httpGetEnabled, bool httpsGetEnabled) - : this(logger, next, new SoapOptions() - { - ServiceType = serviceType, - Path = path, - EncoderOptions = encoderOptions ?? binding?.ToEncoderOptions(), - SoapSerializer = serializer, - CaseInsensitivePath = caseInsensitivePath, - SoapModelBounder = soapModelBounder, - UseBasicAuthentication = binding.HasBasicAuth(), - HttpGetEnabled = httpGetEnabled, - HttpsGetEnabled = httpsGetEnabled - }) + public SoapEndpointMiddleware(ILogger> logger, RequestDelegate next, IServiceProvider serviceProvider, Type serviceType, string path, SoapEncoderOptions[] encoderOptions, SoapSerializer serializer, bool caseInsensitivePath, ISoapModelBounder soapModelBounder, Binding binding, bool httpGetEnabled, bool httpsGetEnabled) + : this( + logger, + next, + new SoapOptions() + { + ServiceType = serviceType, + Path = path, + EncoderOptions = encoderOptions ?? binding?.ToEncoderOptions(), + SoapSerializer = serializer, + CaseInsensitivePath = caseInsensitivePath, + SoapModelBounder = soapModelBounder, + UseBasicAuthentication = binding.HasBasicAuth(), + HttpGetEnabled = httpGetEnabled, + HttpsGetEnabled = httpsGetEnabled + }, + serviceProvider) { } - public SoapEndpointMiddleware(ILogger> logger, RequestDelegate next, SoapOptions options) + public SoapEndpointMiddleware( + ILogger> logger, + RequestDelegate next, + SoapOptions options, + IServiceProvider serviceProvider) { _logger = logger; _next = next; _options = options; + _serviceProvider = serviceProvider; + + _serializerHelper = _serviceProvider.GetService(); + _serializerHelper ??= new SerializerHelper(options.SoapSerializer); - _serializerHelper = new SerializerHelper(options.SoapSerializer); _pathComparisonStrategy = options.CaseInsensitivePath ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; _service = new ServiceDescription(options.ServiceType); - if (options.EncoderOptions is null) - { - options.EncoderOptions = new[] { new SoapEncoderOptions() }; - } + options.EncoderOptions ??= new[] { new SoapEncoderOptions() }; _messageEncoders = new SoapMessageEncoder[options.EncoderOptions.Length]; @@ -81,9 +90,9 @@ public SoapEndpointMiddleware(ILogger> logger, } } - public async Task Invoke(HttpContext httpContext, IServiceProvider serviceProvider) + public async Task Invoke(HttpContext httpContext) { - var trailPathTuner = serviceProvider.GetService(); + var trailPathTuner = _serviceProvider.GetService(); trailPathTuner?.ConvertPath(httpContext); @@ -116,7 +125,7 @@ public async Task Invoke(HttpContext httpContext, IServiceProvider serviceProvid { if (!string.IsNullOrWhiteSpace(remainingPath)) { - await ProcessHttpOperation(httpContext, serviceProvider, remainingPath.Value.Trim('/')); + await ProcessHttpOperation(httpContext, _serviceProvider, remainingPath.Value.Trim('/')); } else if (httpContext.Request.Query.ContainsKey("xsd") && _options.WsdlFileOptions != null) { @@ -147,11 +156,11 @@ public async Task Invoke(HttpContext httpContext, IServiceProvider serviceProvid return; } - await ProcessHttpOperation(httpContext, serviceProvider, remainingPath.Value.Trim('/')); + await ProcessHttpOperation(httpContext, _serviceProvider, remainingPath.Value.Trim('/')); } else { - await ProcessOperation(httpContext, serviceProvider); + await ProcessOperation(httpContext, _serviceProvider); } } } @@ -587,9 +596,6 @@ private Message CreateResponseMessage( AdditionalEnvelopeXmlnsAttributes = _options.AdditionalEnvelopeXmlnsAttributes, NamespaceManager = xmlNamespaceManager }; - responseMessage.Headers.Action = operation.ReplyAction; - responseMessage.Headers.RelatesTo = requestMessage.Headers.MessageId; - responseMessage.Headers.To = requestMessage.Headers.ReplyTo?.Uri; } else { @@ -602,6 +608,10 @@ private Message CreateResponseMessage( }; } + responseMessage.Headers.Action = operation.ReplyAction; + responseMessage.Headers.RelatesTo = requestMessage.Headers.MessageId; + responseMessage.Headers.To = requestMessage.Headers.ReplyTo?.Uri; + if (responseObject != null) { var messageHeaderMembers = responseObject.GetType().GetMembersWithAttribute(); From 4b5d237dec408b5586e0809ae7870b3f81975f32 Mon Sep 17 00:00:00 2001 From: ankitkmrpatel Date: Thu, 19 Oct 2023 15:44:06 +0530 Subject: [PATCH 2/8] fix: Updated Test Cases for SoapEndpointMiddleware Constructor Arguments --- src/SoapCore.Tests/InvalidXMLTests.cs | 4 ++-- .../TrailingServicePathTunerTests.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/SoapCore.Tests/InvalidXMLTests.cs b/src/SoapCore.Tests/InvalidXMLTests.cs index dceb66ef..c7aa1bec 100644 --- a/src/SoapCore.Tests/InvalidXMLTests.cs +++ b/src/SoapCore.Tests/InvalidXMLTests.cs @@ -43,7 +43,7 @@ public async Task MissingNamespace() SoapSerializer = SoapSerializer.DataContractSerializer }; - var soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.CompletedTask, options); + var soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.CompletedTask, options, serviceCollection.BuildServiceProvider()); var context = new DefaultHttpContext(); context.Request.Path = new PathString("/Service.svc"); @@ -63,7 +63,7 @@ public async Task MissingNamespace() context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(request), false); context.Request.ContentType = "text/xml; charset=utf-8"; - await soapCore.Invoke(context, serviceCollection.BuildServiceProvider()); + await soapCore.Invoke(context); // Assert Assert.IsTrue(context.Response.Body.Length > 0); diff --git a/src/SoapCore.Tests/TrailingServicePathTuner/TrailingServicePathTunerTests.cs b/src/SoapCore.Tests/TrailingServicePathTuner/TrailingServicePathTunerTests.cs index 3a9a4a41..dae92d3f 100644 --- a/src/SoapCore.Tests/TrailingServicePathTuner/TrailingServicePathTunerTests.cs +++ b/src/SoapCore.Tests/TrailingServicePathTuner/TrailingServicePathTunerTests.cs @@ -41,7 +41,7 @@ public async Task TrailingServicePath_WritesMessage_True() SoapSerializer = SoapSerializer.DataContractSerializer }; - SoapEndpointMiddleware soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.FromResult(TaskStatus.RanToCompletion), options); + SoapEndpointMiddleware soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.FromResult(TaskStatus.RanToCompletion), options, new MockServiceProvider(true)); var context = new DefaultHttpContext(); context.Request.Path = new PathString("/DynamicPath/Service.svc"); @@ -50,7 +50,7 @@ public async Task TrailingServicePath_WritesMessage_True() // Act // MockServiceProvider(false) simulates registering the TrailingServicePathTuner in app startup - await soapCore.Invoke(context, new MockServiceProvider(true)); + await soapCore.Invoke(context); // Assert Assert.IsTrue(context.Response.Body.Length > 0); @@ -84,7 +84,7 @@ public async Task TrailingServicePath_WritesMessage_False() SoapSerializer = SoapSerializer.DataContractSerializer }; - SoapEndpointMiddleware soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.FromResult(TaskStatus.RanToCompletion), options); + SoapEndpointMiddleware soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.FromResult(TaskStatus.RanToCompletion), options, new MockServiceProvider(true)); var context = new DefaultHttpContext(); context.Request.Path = new PathString("/DynamicPath/Service.svc"); @@ -92,7 +92,7 @@ public async Task TrailingServicePath_WritesMessage_False() // Act // MockServiceProvider(false) simulates registering the TrailingServicePathTuner in app startup - await soapCore.Invoke(context, new MockServiceProvider(true)); + await soapCore.Invoke(context); // Assert Assert.IsFalse(context.Response.Body.Length > 0); @@ -126,7 +126,7 @@ public async Task FullPath_WritesMessage_True() SoapSerializer = SoapSerializer.DataContractSerializer }; - SoapEndpointMiddleware soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.FromResult(TaskStatus.RanToCompletion), options); + SoapEndpointMiddleware soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.FromResult(TaskStatus.RanToCompletion), options, new MockServiceProvider(false)); var context = new DefaultHttpContext(); context.Request.Path = new PathString("/v1/Service.svc"); @@ -135,7 +135,7 @@ public async Task FullPath_WritesMessage_True() // Act // MockServiceProvider(false) simulates not registering the TrailingServicePathTuner in app startup - await soapCore.Invoke(context, new MockServiceProvider(false)); + await soapCore.Invoke(context); // Assert Assert.IsTrue(context.Response.Body.Length > 0); From aa9c438a11af5553adbe9fd5c3a1a3b540b02873 Mon Sep 17 00:00:00 2001 From: ankitkmrpatel Date: Thu, 19 Oct 2023 16:22:43 +0530 Subject: [PATCH 3/8] chore: Added Test Cases for Default Serializer and Custom XML Serializer --- src/SoapCore.Tests/SerializerProviderTests.cs | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/SoapCore.Tests/SerializerProviderTests.cs diff --git a/src/SoapCore.Tests/SerializerProviderTests.cs b/src/SoapCore.Tests/SerializerProviderTests.cs new file mode 100644 index 00000000..d48f3a9d --- /dev/null +++ b/src/SoapCore.Tests/SerializerProviderTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Serialization; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SoapCore.Serializer; + +namespace SoapCore.Tests +{ + [TestClass] + public class SerializerProviderTests + { + [Timeout(500)] + [TestMethod] + public async Task UsingDefaultSerializer() + { + // Arrange + var logger = NullLoggerFactory.Instance.CreateLogger>(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(); + + var options = new SoapOptions() + { + Path = "/Service.svc", + EncoderOptions = new[] + { + new SoapEncoderOptions + { + MessageVersion = MessageVersion.Soap11, + WriteEncoding = Encoding.UTF8, + ReaderQuotas = XmlDictionaryReaderQuotas.Max + } + }, + ServiceType = typeof(DenialOfServiceProofOfConcept), + SoapModelBounder = new MockModelBounder(), + SoapSerializer = SoapSerializer.DataContractSerializer + }; + + var soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.CompletedTask, options, serviceCollection.BuildServiceProvider()); + + var context = new DefaultHttpContext(); + context.Request.Path = new PathString("/Service.svc"); + context.Request.Method = "POST"; + context.Response.Body = new MemoryStream(); + + // Act + var request = @" + + + + a + b + + +"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(request), false); + context.Request.ContentType = "text/xml; charset=utf-8"; + + await soapCore.Invoke(context); + + // Assert + Assert.IsTrue(context.Response.Body.Length > 0); + } + + [Timeout(500)] + [TestMethod] + public async Task UsingCustomXMLSerializer() + { + // Arrange + var logger = NullLoggerFactory.Instance.CreateLogger>(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddCustomSoapMessageSerializer(); + + var options = new SoapOptions() + { + Path = "/Service.svc", + EncoderOptions = new[] + { + new SoapEncoderOptions + { + MessageVersion = MessageVersion.Soap11, + WriteEncoding = Encoding.UTF8, + ReaderQuotas = XmlDictionaryReaderQuotas.Max + } + }, + ServiceType = typeof(DenialOfServiceProofOfConcept), + SoapModelBounder = new MockModelBounder(), + SoapSerializer = SoapSerializer.DataContractSerializer + }; + + var soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.CompletedTask, options, serviceCollection.BuildServiceProvider()); + + var context = new DefaultHttpContext(); + context.Request.Path = new PathString("/Service.svc"); + context.Request.Method = "POST"; + context.Response.Body = new MemoryStream(); + + // Act + var request = @" + + + + a + b + + +"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(request), false); + context.Request.ContentType = "text/xml; charset=utf-8"; + + await soapCore.Invoke(context); + + // Assert + Assert.IsTrue(context.Response.Body.Length > 0); + } + + [ServiceContract(Namespace = "https://dos.brianfeucht.com/")] + public class DenialOfServiceProofOfConcept + { + [OperationContract] + public Task SpinTheThread(string a, string b) + { + return Task.FromResult("Hello World"); + } + } + + public class DenialOfServiceProofOfConceptRequestSerializer : ISoapCoreSerializer + { + public object DeserializeInputParameter(XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs, ICustomAttributeProvider customAttributeProvider, IEnumerable knownTypes = null) + { + var serializer = new DataContractSerializer(parameterType, parameterName, parameterNs, (IEnumerable)knownTypes); + return serializer.ReadObject(xmlReader, true); + } + } + } +} From 4c5ed475968fed9afb9dbcf19047219d295d31f4 Mon Sep 17 00:00:00 2001 From: ankitkmrpatel Date: Fri, 20 Oct 2023 23:14:45 +0530 Subject: [PATCH 4/8] fix: Added The SoapCore Service into CustomSerializer Test, Fixed the Tests --- src/SoapCore.Tests/SerializerProviderTests.cs | 1 + src/SoapCore/SoapEndpointMiddleware.cs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/SoapCore.Tests/SerializerProviderTests.cs b/src/SoapCore.Tests/SerializerProviderTests.cs index d48f3a9d..0f33152c 100644 --- a/src/SoapCore.Tests/SerializerProviderTests.cs +++ b/src/SoapCore.Tests/SerializerProviderTests.cs @@ -81,6 +81,7 @@ public async Task UsingCustomXMLSerializer() var logger = NullLoggerFactory.Instance.CreateLogger>(); var serviceCollection = new ServiceCollection(); + serviceCollection.AddSoapCore(); serviceCollection.AddSingleton(); serviceCollection.AddCustomSoapMessageSerializer(); diff --git a/src/SoapCore/SoapEndpointMiddleware.cs b/src/SoapCore/SoapEndpointMiddleware.cs index f8e1c6ac..08868759 100644 --- a/src/SoapCore/SoapEndpointMiddleware.cs +++ b/src/SoapCore/SoapEndpointMiddleware.cs @@ -596,6 +596,10 @@ private Message CreateResponseMessage( AdditionalEnvelopeXmlnsAttributes = _options.AdditionalEnvelopeXmlnsAttributes, NamespaceManager = xmlNamespaceManager }; + + responseMessage.Headers.Action = operation.ReplyAction; + responseMessage.Headers.RelatesTo = requestMessage.Headers.MessageId; + responseMessage.Headers.To = requestMessage.Headers.ReplyTo?.Uri; } else { @@ -608,10 +612,6 @@ private Message CreateResponseMessage( }; } - responseMessage.Headers.Action = operation.ReplyAction; - responseMessage.Headers.RelatesTo = requestMessage.Headers.MessageId; - responseMessage.Headers.To = requestMessage.Headers.ReplyTo?.Uri; - if (responseObject != null) { var messageHeaderMembers = responseObject.GetType().GetMembersWithAttribute(); From 9d53df900c4a6f0bf9cd144ab07781fb2f7323d2 Mon Sep 17 00:00:00 2001 From: ankitkmrpatel Date: Thu, 26 Oct 2023 19:32:40 +0530 Subject: [PATCH 5/8] feat: Added Custom Serializer Resolver ----Added delegate To Get the Custom Service if Added Into SoapOptions ----Added Exception if Custom Impl NOT Found --- .../Serializer/ISoapCoreSerializer.cs | 2 + src/SoapCore/SoapCoreOptions.cs | 14 ++++++- src/SoapCore/SoapEndpointExtensions.cs | 13 +++++-- src/SoapCore/SoapEndpointMiddleware.cs | 37 +++++++++++-------- src/SoapCore/SoapOptions.cs | 7 +++- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/SoapCore/Serializer/ISoapCoreSerializer.cs b/src/SoapCore/Serializer/ISoapCoreSerializer.cs index 30397f1c..b8eb1bc3 100644 --- a/src/SoapCore/Serializer/ISoapCoreSerializer.cs +++ b/src/SoapCore/Serializer/ISoapCoreSerializer.cs @@ -5,6 +5,8 @@ namespace SoapCore.Serializer { + public delegate ISoapCoreSerializer ISoapCoreSerializerResolver(Type identifier); + public interface ISoapCoreSerializer { object DeserializeInputParameter(XmlDictionaryReader xmlReader, Type parameterType, string parameterName, string parameterNs, ICustomAttributeProvider customAttributeProvider, IEnumerable knownTypes = null); diff --git a/src/SoapCore/SoapCoreOptions.cs b/src/SoapCore/SoapCoreOptions.cs index 5ce33504..44351c58 100644 --- a/src/SoapCore/SoapCoreOptions.cs +++ b/src/SoapCore/SoapCoreOptions.cs @@ -1,8 +1,9 @@ -using SoapCore.Extensibility; using System; using System.Collections.Generic; using System.ServiceModel.Channels; using System.Xml; +using SoapCore.Extensibility; +using SoapCore.Serializer; namespace SoapCore { @@ -124,9 +125,20 @@ public class SoapCoreOptions public WsdlFileOptions WsdlFileOptions { get; set; } + /// + /// Get or sets a value indicating the use of custom serializer, use for if multiple custom serializer used to services + /// + internal Type SerializerIdentifier { get; set; } + /// /// Sets additional namespace declaration attributes in envelope /// public Dictionary AdditionalEnvelopeXmlnsAttributes { get; set; } + + public void UseCustomSerializer() + where TCustomSerializer : class, ISoapCoreSerializer + { + SerializerIdentifier = typeof(TCustomSerializer); + } } } diff --git a/src/SoapCore/SoapEndpointExtensions.cs b/src/SoapCore/SoapEndpointExtensions.cs index 147b861a..550b8ed6 100644 --- a/src/SoapCore/SoapEndpointExtensions.cs +++ b/src/SoapCore/SoapEndpointExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.ServiceModel.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -355,10 +356,7 @@ public static IEndpointConventionBuilder UseSoapEndpoint(this IEndpointRouteB public static IServiceCollection AddSoapCore(this IServiceCollection serviceCollection) { - serviceCollection.TryAddSingleton(); - serviceCollection.TryAddSingleton>(); - - return serviceCollection; + return AddSoapCore(serviceCollection); } public static IServiceCollection AddSoapCore(this IServiceCollection serviceCollection) @@ -367,6 +365,13 @@ public static IServiceCollection AddSoapCore(this IServiceCollection serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton>(); + serviceCollection.AddSingleton(serviceProvider => serviceType => + { + var serrailizes = serviceProvider.GetServices(); + var serializer = serrailizes.FirstOrDefault(x => x.GetType() == serviceType); + return serializer; + }); + return serviceCollection; } diff --git a/src/SoapCore/SoapEndpointMiddleware.cs b/src/SoapCore/SoapEndpointMiddleware.cs index 08868759..f473ace0 100644 --- a/src/SoapCore/SoapEndpointMiddleware.cs +++ b/src/SoapCore/SoapEndpointMiddleware.cs @@ -1,17 +1,3 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using SoapCore.DocumentationWriter; -using SoapCore.Extensibility; -using SoapCore.MessageEncoder; -using SoapCore.Meta; -using SoapCore.Serializer; -using SoapCore.ServiceModel; using System; using System.Collections.Generic; using System.Diagnostics; @@ -20,12 +6,27 @@ using System.Net; using System.Net.Http.Headers; using System.Reflection; +using System.Resources; using System.Runtime.CompilerServices; using System.Security.Authentication; using System.ServiceModel; using System.ServiceModel.Channels; using System.Threading.Tasks; using System.Xml; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using SoapCore.DocumentationWriter; +using SoapCore.Extensibility; +using SoapCore.MessageEncoder; +using SoapCore.Meta; +using SoapCore.Serializer; +using SoapCore.ServiceModel; namespace SoapCore { @@ -73,7 +74,13 @@ public SoapEndpointMiddleware( _options = options; _serviceProvider = serviceProvider; - _serializerHelper = _serviceProvider.GetService(); + var serializerResolver = _serviceProvider.GetService(); + if (serializerResolver != null && _options.SerializerIdentifier != null) + { + _serializerHelper = serializerResolver(_options.SerializerIdentifier); + _ = _serializerHelper ?? throw new InvalidOperationException("custom serializer implementation not found."); + } + _serializerHelper ??= new SerializerHelper(options.SoapSerializer); _pathComparisonStrategy = options.CaseInsensitivePath ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; diff --git a/src/SoapCore/SoapOptions.cs b/src/SoapCore/SoapOptions.cs index 62adbad5..0d952b80 100644 --- a/src/SoapCore/SoapOptions.cs +++ b/src/SoapCore/SoapOptions.cs @@ -1,9 +1,9 @@ -using SoapCore.Extensibility; -using SoapCore.Meta; using System; using System.Collections.Generic; using System.ServiceModel.Channels; using System.Xml; +using SoapCore.Extensibility; +using SoapCore.Meta; namespace SoapCore { @@ -50,6 +50,8 @@ public class SoapOptions /// public bool HttpsPostEnabled { get; set; } = true; + public Type SerializerIdentifier { get; set; } + public bool OmitXmlDeclaration { get; set; } = true; public bool? StandAloneAttribute { get; set; } = null; @@ -89,6 +91,7 @@ public static SoapOptions FromSoapCoreOptions(SoapCoreOptions opt, Type serviceT HttpGetEnabled = opt.HttpGetEnabled, HttpPostEnabled = opt.HttpPostEnabled, HttpsPostEnabled = opt.HttpsPostEnabled, + SerializerIdentifier = opt.SerializerIdentifier, OmitXmlDeclaration = opt.OmitXmlDeclaration, StandAloneAttribute = opt.StandAloneAttribute, IndentXml = opt.IndentXml, From acad054be3d49b685d038fd1aad389574cbc74ce Mon Sep 17 00:00:00 2001 From: ankitkmrpatel Date: Thu, 26 Oct 2023 19:38:54 +0530 Subject: [PATCH 6/8] docs: Updated Custom Serializer Implementation Guide --- README.md | 69 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 0cd162cf..526572d6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ There are 2 different ways of adding SoapCore to your ASP.NET Core website. If y In Startup.cs: - ```csharp public void ConfigureServices(IServiceCollection services) { @@ -42,7 +41,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseEndpoints(endpoints => { endpoints.UseSoapEndpoint("/ServicePath.asmx", new SoapEncoderOptions(), SoapSerializer.DataContractSerializer); }); - + } ``` @@ -70,11 +69,26 @@ In Startup.cs: ```csharp public void ConfigureServices(IServiceCollection services) { + ... services.AddSoapCore(); services.TryAddSingleton(); - services..AddCustomSoapMessageSerializer(); - services.AddMvc(); + services.AddCustomSoapMessageSerializer(); //Add Your Custom Implementation or Extend Default Serializer + + services.AddMvc(); + ... } + +public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) +{ + app.UseSoapEndpoint(soapCoreOptions => + { + soapCoreOptions.Path = "/ServicePath.asmx"; + soapCoreOptions.UseCustomSerializer(); //Specify the Service to Use Service Soap Message Serializer + soapCoreOptions.SoapSerializer = SoapSerializer.DataContractSerializer; + ... + }); +} + ``` ### Using with legacy WCF/WS @@ -103,21 +117,19 @@ To use it, add a setting like this to appsettings } ``` -* UrlOverride - can be used to override the URL in the service description. This can be useful if you are behind a firewall. -* VirualPath - can be used if you like to add a path between the base URL and service. -* WebServiceWSDLMapping - * UrlOverride - can be used to override the URL for a specific WSDL mapping. This can be useful if you want to host different services under different folder. - * Service.asmx - is the endpoint of the service you expose. You can have more than one. - * WsdlFile - is the name of the WSDL on disc. - * SchemaFolder - if you import XSD from WSDL, this is the folder where the Schemas are stored on disc. - * WsdlFolder - is the folder that the WSDL file is stored on disc. - +- UrlOverride - can be used to override the URL in the service description. This can be useful if you are behind a firewall. +- VirualPath - can be used if you like to add a path between the base URL and service. +- WebServiceWSDLMapping + - UrlOverride - can be used to override the URL for a specific WSDL mapping. This can be useful if you want to host different services under different folder. + - Service.asmx - is the endpoint of the service you expose. You can have more than one. + - WsdlFile - is the name of the WSDL on disc. + - SchemaFolder - if you import XSD from WSDL, this is the folder where the Schemas are stored on disc. + - WsdlFolder - is the folder that the WSDL file is stored on disc. To read the setting you can do the following In Startup.cs: - ```csharp var settings = Configuration.GetSection("FileWSDL").Get(); @@ -134,21 +146,23 @@ If the WsdFileOptions parameter is supplied then this feature is enabled / used. ### References -* [stackify.com/soap-net-core](https://stackify.com/soap-net-core/) +- [stackify.com/soap-net-core](https://stackify.com/soap-net-core/) ### Tips and Tricks #### Extending the pipeline In your ConfigureServices method, you can register some additional items to extend the pipeline: -* services.AddSoapMessageInspector() - add a custom MessageInspector. This function is similar to the `IDispatchMessageInspector` in WCF. The newer `IMessageInspector2` interface allows you to register multiple inspectors, and to know which service was being called. -* services.AddSingleton() - add a custom OperationInvoker. Similar to WCF's `IOperationInvoker` this allows you to override the invoking of a service operation, commonly to add custom logging or exception handling logic around it. -* services.AddSoapMessageProcessor() - add a custom SoapMessageProcessor. Similar to ASP.NET Cores middlewares, this allows you to inspect the message on the way in and out. You can also short-circuit the message processing and return your own custom message instead. Inspecting and modifying HttpContext is also possible + +- services.AddSoapMessageInspector() - add a custom MessageInspector. This function is similar to the `IDispatchMessageInspector` in WCF. The newer `IMessageInspector2` interface allows you to register multiple inspectors, and to know which service was being called. +- services.AddSingleton() - add a custom OperationInvoker. Similar to WCF's `IOperationInvoker` this allows you to override the invoking of a service operation, commonly to add custom logging or exception handling logic around it. +- services.AddSoapMessageProcessor() - add a custom SoapMessageProcessor. Similar to ASP.NET Cores middlewares, this allows you to inspect the message on the way in and out. You can also short-circuit the message processing and return your own custom message instead. Inspecting and modifying HttpContext is also possible #### Using ISoapMessageProcessor() + ```csharp //Add this to ConfigureServices in Startup.cs - + services.AddSoapMessageProcessor(async (message, httpcontext, next) => { var bufferedMessage = message.CreateBufferedCopy(int.MaxValue); @@ -164,7 +178,7 @@ services.AddSoapMessageProcessor(async (message, httpcontext, next) => var responseMessage = await next(message); //Inspect and modify the contents of returnMessage in the same way as the incoming message. - //finish by returning the modified message. + //finish by returning the modified message. return responseMessage; }); @@ -176,9 +190,10 @@ Use interface IServiceOperationTuner to tune each operation call. Create class that implements IServiceOperationTuner. Parameters in Tune method: -* httpContext - current HttpContext. Can be used to get http headers or body. -* serviceInstance - instance of your service. -* operation - information about called operation. + +- httpContext - current HttpContext. Can be used to get http headers or body. +- serviceInstance - instance of your service. +- operation - information about called operation. ```csharp public class MyServiceOperationTuner : IServiceOperationTuner @@ -238,8 +253,11 @@ public class MyService : IMyServiceService } } ``` + #### Additional namespace declaration attributes in envelope + Adding additional namespaces to the **SOAP Envelope** can be done by populating `SoapEncoderOptions.AdditionalEnvelopeXmlnsAttributes` parameter. + ```csharp .... endpoints.UseSoapEndpoint(opt => @@ -253,7 +271,9 @@ endpoints.UseSoapEndpoint(opt => }); ... ``` + This code will put `xmlns:myNS="...` and `xmlns:arr="...` attributes in `Envelope` and message will look like: + ```xml ... @@ -263,7 +283,9 @@ This code will put `xmlns:myNS="...` and `xmlns:arr="...` attributes in `Envelop ... ``` + instead of: + ```xml ... @@ -279,6 +301,7 @@ instead of: See [Contributing guide](CONTRIBUTING.md) ### Contributors + From 58afbb9b5d7582da6e4c189859c77f7822a5d23e Mon Sep 17 00:00:00 2001 From: ankitkmrpatel Date: Wed, 13 Dec 2023 16:49:20 +0530 Subject: [PATCH 7/8] fix: Resolve IMessageInspector2 Using Scope Resolver ----IMessageInsector2 is being Added as Scope Service, However, it is being resolve as Sington Service --- src/SoapCore/SoapEndpointMiddleware.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/SoapCore/SoapEndpointMiddleware.cs b/src/SoapCore/SoapEndpointMiddleware.cs index 510eb9b0..961ee6f7 100644 --- a/src/SoapCore/SoapEndpointMiddleware.cs +++ b/src/SoapCore/SoapEndpointMiddleware.cs @@ -460,10 +460,15 @@ private async Task ProcessMessage(Message requestMessage, SoapMessageEn throw new ArgumentException($"Unable to handle request without a valid action parameter. Please supply a valid soap action."); } - var messageInspector2s = serviceProvider.GetServices(); var correlationObjects2 = default(List<(IMessageInspector2 inspector, object correlationObject)>); - correlationObjects2 = messageInspector2s.Select(mi => (inspector: mi, correlationObject: mi.AfterReceiveRequest(ref requestMessage, _service))).ToList(); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + var messageInspector2s = scope.ServiceProvider.GetServices(); + correlationObjects2 = messageInspector2s.Select(mi => + (inspector: mi, correlationObject: mi.AfterReceiveRequest(ref requestMessage, _service))) + .ToList(); + } // for getting soapaction and parameters in (optional) body // GetReaderAtBodyContents must not be called twice in one request From bf1a3942a427b173098a5a4d28128d55762d6244 Mon Sep 17 00:00:00 2001 From: ankitsflhub Date: Fri, 5 Jan 2024 19:03:08 +0530 Subject: [PATCH 8/8] fix error SA1028: Code should not contain trailing whitespace --- .../TrailingServicePathTuner/TrailingServicePathTunerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SoapCore.Tests/TrailingServicePathTuner/TrailingServicePathTunerTests.cs b/src/SoapCore.Tests/TrailingServicePathTuner/TrailingServicePathTunerTests.cs index 72317dc3..32a08fa1 100644 --- a/src/SoapCore.Tests/TrailingServicePathTuner/TrailingServicePathTunerTests.cs +++ b/src/SoapCore.Tests/TrailingServicePathTuner/TrailingServicePathTunerTests.cs @@ -125,7 +125,7 @@ public async Task FullPath_WritesMessage_True() SoapModelBounder = new MockModelBounder(), SoapSerializer = SoapSerializer.DataContractSerializer }; - + SoapEndpointMiddleware soapCore = new SoapEndpointMiddleware(logger, (innerContext) => Task.CompletedTask, options, new MockServiceProvider(false)); var context = new DefaultHttpContext();