From aa57065d2abc9e26d5e26277dc4c14d8463cd352 Mon Sep 17 00:00:00 2001 From: Daniel Renninghoff Date: Mon, 12 Feb 2024 11:51:38 +0100 Subject: [PATCH] Add option to generate soapAction without contract name By default, the soapAction that is generated if it is not specified in the [OperationContract(Action = "...")] attribute, follows the format {namespace}/{contractName}/{methodName}. This seems to be different than the default soapAction that is generated in ASMX web services in .NET Framework. The new option makes it possible to omit the contractName and instead generate soapActions with the format {namespace}/{methodName}. The default for this new option is false. This means that the behavior for existing users of SoapCore does not change. --- .../OperationDescriptionTests.cs | 61 ++++++++++++------- .../ServiceContractTests.cs | 4 +- src/SoapCore.Tests/Wsdl/WsdlTests.cs | 2 +- .../AuthorizeOperationMessageProcessor.cs | 15 +++-- .../ServiceModel/ContractDescription.cs | 4 +- .../ServiceModel/OperationDescription.cs | 17 +++++- .../ServiceModel/ServiceDescription.cs | 4 +- src/SoapCore/SoapCoreOptions.cs | 7 +++ src/SoapCore/SoapEndpointMiddleware.cs | 2 +- src/SoapCore/SoapOptions.cs | 5 +- 10 files changed, 85 insertions(+), 36 deletions(-) diff --git a/src/SoapCore.Tests/OperationDescription/OperationDescriptionTests.cs b/src/SoapCore.Tests/OperationDescription/OperationDescriptionTests.cs index 6375c327..ca6effdd 100644 --- a/src/SoapCore.Tests/OperationDescription/OperationDescriptionTests.cs +++ b/src/SoapCore.Tests/OperationDescription/OperationDescriptionTests.cs @@ -11,14 +11,14 @@ public class OperationDescriptionTests [Fact] public void TestProperUnrappingOfGenericResponses() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract)); - ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute()); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract), false); + ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute(), false); System.Reflection.MethodInfo method = typeof(IServiceWithMessageContract).GetMethod(nameof(IServiceWithMessageContract.GetMyClass)); OperationContractAttribute contractAttribute = new OperationContractAttribute(); - ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute); + ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, false); Assert.True(operationDescription.IsMessageContractResponse); } @@ -26,14 +26,14 @@ public void TestProperUnrappingOfGenericResponses() [Fact] public void TestSupportXmlRootForParameterName() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract)); - ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute()); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract), false); + ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute(), false); System.Reflection.MethodInfo method = typeof(IServiceWithMessageContract).GetMethod(nameof(IServiceWithMessageContract.GetClassWithXmlRoot)); OperationContractAttribute contractAttribute = new OperationContractAttribute(); - ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute); + ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, false); Assert.Equal("test", operationDescription.AllParameters.FirstOrDefault()?.Name); } @@ -41,14 +41,14 @@ public void TestSupportXmlRootForParameterName() [Fact] public void TestSupportXmlRootForParameterNameWithEmptyStringAsRootElementNameUsesParameterInfoToExtractAName() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContractAndEmptyXmlRoot)); - ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContractAndEmptyXmlRoot), new ServiceContractAttribute()); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContractAndEmptyXmlRoot), false); + ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContractAndEmptyXmlRoot), new ServiceContractAttribute(), false); System.Reflection.MethodInfo method = typeof(IServiceWithMessageContractAndEmptyXmlRoot).GetMethod(nameof(IServiceWithMessageContractAndEmptyXmlRoot.GetClassWithEmptyXmlRoot)); OperationContractAttribute contractAttribute = new OperationContractAttribute(); - ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute); + ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, false); Assert.Equal("classWithXmlRoot", operationDescription.AllParameters.FirstOrDefault()?.Name); } @@ -56,14 +56,14 @@ public void TestSupportXmlRootForParameterNameWithEmptyStringAsRootElementNameUs [Fact] public void TestProperUnrappingOfNonGenericResponses() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract)); - ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute()); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract), false); + ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute(), false); System.Reflection.MethodInfo method = typeof(IServiceWithMessageContract).GetMethod(nameof(IServiceWithMessageContract.GetMyOtherClass)); OperationContractAttribute contractAttribute = new OperationContractAttribute(); - ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute); + ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, false); Assert.True(operationDescription.IsMessageContractResponse); } @@ -71,14 +71,14 @@ public void TestProperUnrappingOfNonGenericResponses() [Fact] public void TestProperUnrappingOfNonMessageContractResponses() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract)); - ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute()); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract), false); + ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute(), false); System.Reflection.MethodInfo method = typeof(IServiceWithMessageContract).GetMethod(nameof(IServiceWithMessageContract.GetMyStringClass)); OperationContractAttribute contractAttribute = new OperationContractAttribute(); - ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute); + ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, false); Assert.False(operationDescription.IsMessageContractResponse); } @@ -86,14 +86,14 @@ public void TestProperUnrappingOfNonMessageContractResponses() [Fact] public void TestProperUnwrappingOfSoapFaults() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract)); - ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute()); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract), false); + ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute(), false); System.Reflection.MethodInfo method = typeof(IServiceWithMessageContract).GetMethod(nameof(IServiceWithMessageContract.ThrowTypedFault)); OperationContractAttribute contractAttribute = new OperationContractAttribute(); - ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute); + ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, false); var faultInfo = Assert.Single(operationDescription.Faults); Assert.Equal("TypedSoapFault", faultInfo.Name); @@ -106,17 +106,36 @@ public void TestProperUnwrappingOfSoapFaults() [Fact] public void TestProperNamingOfAsyncMethods() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract)); - ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute()); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract), false); + ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute(), false); System.Reflection.MethodInfo method = typeof(IServiceWithMessageContract).GetMethod(nameof(IServiceWithMessageContract.GetMyAsyncClassAsync)); OperationContractAttribute contractAttribute = new OperationContractAttribute(); - ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute); + ServiceModel.OperationDescription operationDescription = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, false); Assert.True(operationDescription.IsMessageContractResponse); Assert.Equal("GetMyAsyncClass", operationDescription.Name); } + + [Fact] + public void TestGeneratedSoapActionName() + { + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithMessageContract), true); + ContractDescription contractDescription = new ContractDescription(serviceDescription, typeof(IServiceWithMessageContract), new ServiceContractAttribute(), true); + + System.Reflection.MethodInfo method = typeof(IServiceWithMessageContract).GetMethod(nameof(IServiceWithMessageContract.GetMyOtherClass)); + + OperationContractAttribute contractAttribute = new OperationContractAttribute(); + + ServiceModel.OperationDescription operationDescription1 = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, true); + + Assert.Equal("http://tempuri.org/GetMyOtherClass", operationDescription1.SoapAction); + + ServiceModel.OperationDescription operationDescription2 = new ServiceModel.OperationDescription(contractDescription, method, contractAttribute, false); + + Assert.Equal("http://tempuri.org/IServiceWithMessageContract/GetMyOtherClass", operationDescription2.SoapAction); + } } } diff --git a/src/SoapCore.Tests/ServiceDescription/ServiceContractTests.cs b/src/SoapCore.Tests/ServiceDescription/ServiceContractTests.cs index 47897229..70568c45 100644 --- a/src/SoapCore.Tests/ServiceDescription/ServiceContractTests.cs +++ b/src/SoapCore.Tests/ServiceDescription/ServiceContractTests.cs @@ -11,7 +11,7 @@ public class ServiceContractTests [Fact] public void TestFallbackOfServiceNameToTypeName() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithoutName)); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithoutName), false); Assert.Equal("IServiceWithoutName", serviceDescription.ServiceName); } @@ -19,7 +19,7 @@ public void TestFallbackOfServiceNameToTypeName() [Fact] public void TestExplicitlySetServiceName() { - ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithName)); + ServiceDescription serviceDescription = new ServiceDescription(typeof(IServiceWithName), false); Assert.Equal("MyServiceWithName", serviceDescription.ServiceName); } diff --git a/src/SoapCore.Tests/Wsdl/WsdlTests.cs b/src/SoapCore.Tests/Wsdl/WsdlTests.cs index 537c7b3a..9972abc3 100644 --- a/src/SoapCore.Tests/Wsdl/WsdlTests.cs +++ b/src/SoapCore.Tests/Wsdl/WsdlTests.cs @@ -1156,7 +1156,7 @@ private string GetWsdlFromAsmx() private async Task GetWsdlFromMetaBodyWriter(SoapSerializer serializer, string bindingName = null, string portName = null, bool useMicrosoftGuid = false) { - var service = new ServiceDescription(typeof(T)); + var service = new ServiceDescription(typeof(T), false); var baseUrl = "http://tempuri.org/"; var xmlNamespaceManager = Namespaces.CreateDefaultXmlNamespaceManager(useMicrosoftGuid); var defaultBindingName = !string.IsNullOrWhiteSpace(bindingName) ? bindingName : "BasicHttpBinding"; diff --git a/src/SoapCore/Extensibility/AuthorizeOperationMessageProcessor.cs b/src/SoapCore/Extensibility/AuthorizeOperationMessageProcessor.cs index 4cf2cf28..626d1e19 100644 --- a/src/SoapCore/Extensibility/AuthorizeOperationMessageProcessor.cs +++ b/src/SoapCore/Extensibility/AuthorizeOperationMessageProcessor.cs @@ -24,13 +24,20 @@ public class AuthorizeOperationMessageProcessor : ISoapMessageProcessor /// private readonly Dictionary _pathTypes; + /// + /// Whether to generate the soapAction without the contract name. This is also specified in the SoapOptions. + /// + private readonly bool _generateSoapActionWithoutContractName; + /// /// Initializes a new instance of the class. /// /// A dictionary that has the path of the endpoint as Key and the corresponding type as Value. Similar to using the UseSoapEndpoint extension function. - public AuthorizeOperationMessageProcessor(Dictionary pathAndTypes) + /// Whether to generate the soapAction without the contract name. Default is false. + public AuthorizeOperationMessageProcessor(Dictionary pathAndTypes, bool generateSoapActionWithoutContractName = false) { _pathTypes = pathAndTypes; + _generateSoapActionWithoutContractName = generateSoapActionWithoutContractName; } public async Task ProcessMessage(Message requestMessage, HttpContext httpContext, Func> next) @@ -43,7 +50,7 @@ public async Task ProcessMessage(Message requestMessage, HttpContext ht throw new ArgumentException("Unable to handle request without a valid action parameter. Please supply a valid soap action."); } - if (!_pathTypes.TryGetValue(httpContext.Request.Path.Value.ToLowerInvariant(), out Type serviceType) || !TryGetOperation(soapAction, serviceType, out var operation)) + if (!_pathTypes.TryGetValue(httpContext.Request.Path.Value.ToLowerInvariant(), out Type serviceType) || !TryGetOperation(soapAction, serviceType, _generateSoapActionWithoutContractName, out var operation)) { throw new InvalidOperationException($"No operation found for specified action: {soapAction}"); } @@ -141,9 +148,9 @@ public async Task ProcessMessage(Message requestMessage, HttpContext ht } } - private static bool TryGetOperation(string methodName, Type serviceType, out OperationDescription operation) + private static bool TryGetOperation(string methodName, Type serviceType, bool generateSoapActionWithoutContractName, out OperationDescription operation) { - var service = new ServiceDescription(serviceType); + var service = new ServiceDescription(serviceType, generateSoapActionWithoutContractName); operation = service.Operations.FirstOrDefault(o => o.SoapAction.Equals(methodName, StringComparison.Ordinal) || o.Name.Equals(HeadersHelper.GetTrimmedSoapAction(methodName), StringComparison.Ordinal) || methodName.Equals(HeadersHelper.GetTrimmedSoapAction(o.Name), StringComparison.Ordinal)); diff --git a/src/SoapCore/ServiceModel/ContractDescription.cs b/src/SoapCore/ServiceModel/ContractDescription.cs index 4e1a70ca..546cc254 100644 --- a/src/SoapCore/ServiceModel/ContractDescription.cs +++ b/src/SoapCore/ServiceModel/ContractDescription.cs @@ -7,7 +7,7 @@ namespace SoapCore.ServiceModel { public class ContractDescription { - public ContractDescription(ServiceDescription service, Type contractType, ServiceContractAttribute attribute) + public ContractDescription(ServiceDescription service, Type contractType, ServiceContractAttribute attribute, bool generateSoapActionWithoutContractName) { Service = service; ContractType = contractType; @@ -20,7 +20,7 @@ public ContractDescription(ServiceDescription service, Type contractType, Servic { foreach (var operationContract in operationMethodInfo.GetCustomAttributes()) { - operations.Add(new OperationDescription(this, operationMethodInfo, operationContract)); + operations.Add(new OperationDescription(this, operationMethodInfo, operationContract, generateSoapActionWithoutContractName)); } } diff --git a/src/SoapCore/ServiceModel/OperationDescription.cs b/src/SoapCore/ServiceModel/OperationDescription.cs index 6e19adf4..5af21dd8 100644 --- a/src/SoapCore/ServiceModel/OperationDescription.cs +++ b/src/SoapCore/ServiceModel/OperationDescription.cs @@ -12,11 +12,24 @@ namespace SoapCore.ServiceModel { public class OperationDescription { - public OperationDescription(ContractDescription contract, MethodInfo operationMethod, OperationContractAttribute contractAttribute) + public OperationDescription(ContractDescription contract, MethodInfo operationMethod, OperationContractAttribute contractAttribute, bool generateSoapActionWithoutContractName) { Contract = contract; Name = contractAttribute.Name ?? GetNameByAction(contractAttribute.Action) ?? GetNameByMethod(operationMethod); - SoapAction = contractAttribute.Action ?? $"{contract.Namespace.TrimEnd('/')}/{contract.Name}/{Name}"; + + if (contractAttribute.Action != null) + { + SoapAction = contractAttribute.Action; + } + else if (generateSoapActionWithoutContractName) + { + SoapAction = $"{contract.Namespace.TrimEnd('/')}/{Name}"; + } + else + { + SoapAction = $"{contract.Namespace.TrimEnd('/')}/{contract.Name}/{Name}"; + } + IsOneWay = contractAttribute.IsOneWay; DispatchMethod = operationMethod; diff --git a/src/SoapCore/ServiceModel/ServiceDescription.cs b/src/SoapCore/ServiceModel/ServiceDescription.cs index 4822f0d1..16d0c4ec 100644 --- a/src/SoapCore/ServiceModel/ServiceDescription.cs +++ b/src/SoapCore/ServiceModel/ServiceDescription.cs @@ -8,7 +8,7 @@ namespace SoapCore.ServiceModel { public class ServiceDescription { - public ServiceDescription(Type serviceType) + public ServiceDescription(Type serviceType, bool generateSoapActionWithoutContractName) { ServiceType = serviceType; ServiceKnownTypes = serviceType.GetCustomAttributes(inherit: false); @@ -21,7 +21,7 @@ public ServiceDescription(Type serviceType) { foreach (var serviceContract in contractType.GetTypeInfo().GetCustomAttributes()) { - var contractDescription = new ContractDescription(this, contractType, serviceContract); + var contractDescription = new ContractDescription(this, contractType, serviceContract, generateSoapActionWithoutContractName); contracts.Add(contractDescription); diff --git a/src/SoapCore/SoapCoreOptions.cs b/src/SoapCore/SoapCoreOptions.cs index 74e98875..ed1654e8 100644 --- a/src/SoapCore/SoapCoreOptions.cs +++ b/src/SoapCore/SoapCoreOptions.cs @@ -136,5 +136,12 @@ public class SoapCoreOptions /// Sets additional namespace declaration attributes in envelope /// public Dictionary AdditionalEnvelopeXmlnsAttributes { get; set; } + + /// + /// By default, the soapAction that is generated if not explicitely specified is + /// {namespace}/{contractName}/{methodName}. If set to true, the service name will + /// be omitted, so that the soapAction will be {namespace}/{methodName}. + /// + public bool GenerateSoapActionWithoutContractName { get; set; } = false; } } diff --git a/src/SoapCore/SoapEndpointMiddleware.cs b/src/SoapCore/SoapEndpointMiddleware.cs index afa0e7d6..942e279c 100644 --- a/src/SoapCore/SoapEndpointMiddleware.cs +++ b/src/SoapCore/SoapEndpointMiddleware.cs @@ -65,7 +65,7 @@ public SoapEndpointMiddleware(ILogger> logger, _serializerHelper = new SerializerHelper(options.SoapSerializer); _pathComparisonStrategy = options.CaseInsensitivePath ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - _service = new ServiceDescription(options.ServiceType); + _service = new ServiceDescription(options.ServiceType, options.GenerateSoapActionWithoutContractName); if (options.EncoderOptions is null) { diff --git a/src/SoapCore/SoapOptions.cs b/src/SoapCore/SoapOptions.cs index 660e2eb8..e55b67cd 100644 --- a/src/SoapCore/SoapOptions.cs +++ b/src/SoapCore/SoapOptions.cs @@ -70,6 +70,8 @@ public class SoapOptions public WsdlFileOptions WsdlFileOptions { get; set; } public Dictionary AdditionalEnvelopeXmlnsAttributes { get; set; } + public bool GenerateSoapActionWithoutContractName { get; set; } = false; + [Obsolete] public static SoapOptions FromSoapCoreOptions(SoapCoreOptions opt) { @@ -99,7 +101,8 @@ public static SoapOptions FromSoapCoreOptions(SoapCoreOptions opt, Type serviceT WsdlFileOptions = opt.WsdlFileOptions, AdditionalEnvelopeXmlnsAttributes = opt.AdditionalEnvelopeXmlnsAttributes, CheckXmlCharacters = opt.CheckXmlCharacters, - UseMicrosoftGuid = opt.UseMicrosoftGuid + UseMicrosoftGuid = opt.UseMicrosoftGuid, + GenerateSoapActionWithoutContractName = opt.GenerateSoapActionWithoutContractName, }; #pragma warning disable CS0612 // Type or member is obsolete