Our integration third parties use SOAP request in their existing system. In order to integrate with them, we are required to have a SOAP service endpoint that the third parties can call. When we looked at building this 18 months ago, we wanted to build this web service in .Net Core. However, .Net Core only supports SOAP client to make SOAP request but not SOAP web service. We came across a library called SoapCore but at the time there was no release 1.0.0 yet so we built the SOAP web service in JAVA instead.
They finally released v1.0.0 mid December last year so we thought to give it a go. More information on the library can be found on their github page here.
The current setup
We currently have 2 endpoints which our third parties can call similar to the sample below
https://somedomain.com.au/integration-service/v2_17/ActionA
https://somedomain.com.au/integration-service/v2_17/ActionB
We have another 2 similar endpoints but with version v2_16 in the route as we are required to support 2 versions at all time.
We have a couple xsd files from third parties that we used to generate request and response classes in JAVA. A sample request looks similar to below
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Header/>
<env:Body>
<ns2:ActionARequest xmlns:ns2="http://sample.namespace.com/service/2.19/">
<Id xmlns="http://sample.namespace.com/schema/2.19/">132-abc</Id>
<Name xmlns="http://sample.namespace.com/schema/2.19/">B</Name>
</ns2:ActionARequest>
</env:Body>
</env:Envelope>
This is what the response looks like for a successful request
<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>
<ActionAResponse xmlns="http://sample.namespace.com/service/2.19" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Status xmlns="http://sample.namespace.com/schema/2.19/">
<MessageStatusCode>200</MessageStatusCode>
<MessageStatusReason>OK</MessageStatusReason>
</Status>
</ActionAResponse>
</s:Body>
</s:Envelope>
Web service in .NET Core with SoapCore
After creating a blank project and install SoapCore nuget package, the request and response models can be generated using xsd.exe command. I tried using the svcutils tool with data contract only flag /dconly but it failed with the error message “Error: Invalid type specified. Type with name ‘someEnumType’ not found in schema with namespace ‘http://sample.namespace.com/schema/2.19/’.” and a warning message
If you are using the /dataContractOnly option to import data contract types and are getting this error message, consider using xsd.exe instead. Types generated by xsd.exe may be used in the Windows Communication Foundation after applying the XmlSerializerFormatAttribute attribute on your service contract. Alternatively, consider using the /importXmlTypes option to import these types as XML types to use with DataContractFormatAttribute attribute on your service contract.
So I generate the models using the following command
xsd.exe -c actionA.xsd actionB.xsd /n:SampleSoapWebService.Models.CommonTypes
The generated models for action A looks like below
// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true, Namespace = "http://sample.namespace.com/schema/2.19/")]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "http://sample.namespace.com/schema/2.19/", IsNullable = false)]
public partial class ActionARequest
{
private string idField;
private someEnumType nameField;
/// <remarks/>
public string Id
{
get
{
return this.idField;
}
set
{
this.idField = value;
}
}
/// <remarks/>
public someEnumType Name
{
get
{
return this.nameField;
}
set
{
this.nameField = value;
}
}
}
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://sample.namespace.com/schema/2.19/")]
public enum someEnumType
{
/// <remarks/>
A,
/// <remarks/>
B,
/// <remarks/>
C,
}
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true, Namespace = "http://sample.namespace.com/schema/2.19/")]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "http://sample.namespace.com/schema/2.19/", IsNullable = false)]
public partial class ActionAResponse
{
private messageStatusType statusField;
/// <remarks/>
public messageStatusType Status
{
get
{
return this.statusField;
}
set
{
this.statusField = value;
}
}
}
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://sample.namespace.com/schema/2.19/")]
public partial class messageStatusType
{
private int messageStatusCodeField;
private string messageStatusReasonField;
/// <remarks/>
public int MessageStatusCode
{
get
{
return this.messageStatusCodeField;
}
set
{
this.messageStatusCodeField = value;
}
}
/// <remarks/>
public string MessageStatusReason
{
get
{
return this.messageStatusReasonField;
}
set
{
this.messageStatusReasonField = value;
}
}
}
Then, the service contract needs to be created.
[ServiceContract, XmlSerializerFormat]
public interface ICallbackService
{
[OperationContract]
Task<ActionAResponse> Process(ActionARequest request);
}
Notice that it has XmlSerializerFormat attribute. This is due to the warning message when I generate the models using xsd.exe which suggests applying XmlSerializerFormat attribute on the service contract so it can be used with WCF.
The implementation for the service contract looks like below
public class CallbackService : ICallbackService
{
public Task<ActionAResponse> Process(ActionARequest request)
{
// Business logics go here
return Task.FromResult(new ActionAResponse
{
Status = new messageStatusType
{
MessageStatusCode = 200,
MessageStatusReason = "OK"
}
});
}
}
In Startup.cs, I register my service and SOAP endpoint as followed:
public void ConfigureServices(IServiceCollection services)
{
// Other configurations go here
services.AddScoped<ICallbackService, CallbackService>();
services.AddSingleton<IFaultExceptionTransformer, DefaultFaultExceptionTransformer>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Other configurations go here
app.UseSoapEndpoint<ICallbackService>("/integration-service/v2_17/ActionA", new BasicHttpBinding(), SoapSerializer.XmlSerializer);
}
Launching the app and go to endpoint “/integration-service/v2_17/ActionA” will take you to the wsdl definition for service contract.
I use this Chrome extension called Wizdler to show me what the SOAP request I have to send looks like
As you can see, the format of the request does not match the current request that our third parties send to the current endpoints in JAVA. It looks like it takes the method name and the parameter name from the service contract as the request body name. It also does not have any namespaces. This would cause the request model to not be deserialised correctly. In the screenshot below, the Id is null and the Name is the default enum which are not what is in the request. The response also does not look like what the existing service returns.
After a bit of research and playing around with different things, to get the namespace correctly shown on the request so that the request will be serialised correctly, the Namespace property on the Service Contract needs to be set. Also, the MessageParameter attribute with Name property set to the name of request class needs to be added on the method parameter. This allows changing the name of tag request to ActionARequest. Finally, the Action property on the OperationContract attribute needs to be set to ActionARequest. The service contract now looks like below
[ServiceContract(Namespace = "http://sample.namespace.com/service/2.19/"), XmlSerializerFormat]
public interface ICallbackServiceV2
{
[OperationContract(Action = "ActionARequest")]
Task<ActionAResponse> Process([MessageParameter(Name = "ActionARequest")]ActionARequest request);
}
To not include the Process tag, the both the request and response classes will require MessageContract attribute and all their public properties will need MessageBodyMember attribute. The request and response classes now look like below
// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true, Namespace = "http://sample.namespace.com/schema/2.19/")]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "http://sample.namespace.com/schema/2.19/", IsNullable = false)]
[MessageContract]
public partial class ActionARequest
{
private string idField;
private someEnumType nameField;
/// <remarks/>
[MessageBodyMember]
public string Id
{
get
{
return this.idField;
}
set
{
this.idField = value;
}
}
/// <remarks/>
[MessageBodyMember]
public someEnumType Name
{
get
{
return this.nameField;
}
set
{
this.nameField = value;
}
}
}
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://sample.namespace.com/schema/2.19/")]
public enum someEnumType
{
/// <remarks/>
A,
/// <remarks/>
B,
/// <remarks/>
C,
}
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true, Namespace = "http://sample.namespace.com/schema/2.19/")]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "http://sample.namespace.com/schema/2.19/", IsNullable = false)]
[MessageContract]
public partial class ActionAResponse
{
private messageStatusType statusField;
/// <remarks/>
[MessageBodyMember]
public messageStatusType Status
{
get
{
return this.statusField;
}
set
{
this.statusField = value;
}
}
}
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://sample.namespace.com/schema/2.19/")]
public partial class messageStatusType
{
private int messageStatusCodeField;
private string messageStatusReasonField;
/// <remarks/>
public int MessageStatusCode
{
get
{
return this.messageStatusCodeField;
}
set
{
this.messageStatusCodeField = value;
}
}
/// <remarks/>
public string MessageStatusReason
{
get
{
return this.messageStatusReasonField;
}
set
{
this.messageStatusReasonField = value;
}
}
}
The structure of both the request and response now looks like below
There is still one issue with the response. The namespace on the response is “http://sample.namespace.com/schema/2.19/” instead of “http://sample.namespace.com/service/2.19/”. To fix this, in the response class, change the namespace of the XmlRootAttribute to “http://sample.namespace.com/service/2.19/”.
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true, Namespace = "http://sample.namespace.com/schema/2.19/")]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "http://sample.namespace.com/service/2.19/", IsNullable = false)]
[MessageContract]
public partial class ActionAResponse
{
private messageStatusType statusField;
/// <remarks/>
[MessageBodyMember]
public messageStatusType Status
{
get
{
return this.statusField;
}
set
{
this.statusField = value;
}
}
}
Everything now looks correct.
To add the endpoint for ActionB and another version of endpoints ActionA and ActionB, simply repeat the whole process again to create the models and service contracts. Then in the Startup.cs, register the new SOAP endpoints.
public void ConfigureServices(IServiceCollection services)
{
// Other configurations go here
services.AddScoped<ICallbackService, CallbackService>();
services.AddScoped<ICallbackServiceActionB, CallbackServiceActionB>();
services.AddScoped<v2_18.ICallbackService, v2_18.CallbackService>();
services.AddScoped<v2_18.ICallbackServiceActionB, v2_18.CallbackServiceActionB>();
services.AddSingleton<IFaultExceptionTransformer, DefaultFaultExceptionTransformer>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Other configurations go here
app.UseSoapEndpoint<ICallbackService>("/integration-service/v2_17/ActionA", new BasicHttpBinding(), SoapSerializer.XmlSerializer);
app.UseSoapEndpoint<ICallbackServiceActionB>("/integration-service/v2_17/ActionB", new BasicHttpBinding(), SoapSerializer.XmlSerializer);
app.UseSoapEndpoint<v2_18.ICallbackService>("/integration-service/v2_18/ActionA", new BasicHttpBinding(), SoapSerializer.XmlSerializer);
app.UseSoapEndpoint<v2_18.ICallbackServiceActionB>("/integration-service/v2_18/ActionB", new BasicHttpBinding(), SoapSerializer.XmlSerializer);
}