I am taking over a legacy SOAP project where I need to replace the SOAP Service with a .NET Core solution. We cannot have the SOAP client change, so we cannot look at REST, GRPC, etc. I have looked at SoapCore and CoreWCF and have both working with the SOAP header username/password auth demo, however, I'm going to use CoreWCF for the time being.
The existing SOAP service uses custom http status code responses, for example, 202 is returned after the service has been processed and in some situations, where a SOAP Fault occurs. I realize this is incorrect, however, we need to maintain the existing business logic.
My questions are:
- How can I configure my service to respond with a http status code 202 after the service is completed or when a certain condition is met? IsOneWay=True OperationContract will not work as this returns immediately.
- How would I configure a SOAP Fault to respond with a custom http status code?
There are many old SO posts mentioning WebOperationContext, however, I cannot seem to access this within my service. OperationContext doesn't seem to have control of the HttpStatusCode. Maybe I'm missing something. i.e.:
WebOperationContext ctx = WebOperationContext.Current;
ctx.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.BadRequest;
Here's my sample project breakdown:
Program.cs
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;
namespace CoreWcf.Samples.Http
{
public class Program
{
public const int HTTP_PORT = 8088;
public const int HTTPS_PORT = 8443;
static void Main(string[] args)
{
IWebHost host = CreateWebHostBuilder(args).Build();
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel(options =>
{
options.ListenLocalhost(HTTP_PORT);
options.ListenLocalhost(HTTPS_PORT, listenOptions =>
{
listenOptions.UseHttps();
if (Debugger.IsAttached)
{
listenOptions.UseConnectionLogging();
}
});
})
.UseStartup<BasicHttpBindingStartup>();
}
}
Startup.cs
using CoreWCF;
using CoreWCF.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace CoreWcf.Samples.Http
{
public class BasicHttpBindingStartup
{
public void ConfigureServices(IServiceCollection services)
{
//Enable CoreWCF Services, with metadata (WSDL) support
services.AddServiceModelServices()
.AddServiceModelMetadata();
}
public void Configure(IApplicationBuilder app)
{
var wsHttpBindingWithCredential = new BasicHttpBinding(CoreWCF.Channels.BasicHttpSecurityMode.TransportWithMessageCredential);
wsHttpBindingWithCredential.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName;
app.UseServiceModel(builder =>
{
// Add the Demo Service
builder.AddService<DemoService>(serviceOptions =>
{
// Set a base address for all bindings to the service, and WSDL discovery
serviceOptions.BaseAddresses.Add(new Uri($"http://localhost:{Program.HTTP_PORT}/DemoService"));
serviceOptions.BaseAddresses.Add(new Uri($"https://localhost:{Program.HTTPS_PORT}/DemoService"));
})
// Add BasicHttpBinding endpoint
.AddServiceEndpoint<DemoService, IDemo>(wsHttpBindingWithCredential, "/wsHttpUserPassword", ep => { ep.Name = "AuthenticatedDemoEP"; });
builder.ConfigureServiceHostBase<DemoService>(CustomUserNamePasswordValidator.AddToHost);
// Configure WSDL to be available over http & https
var serviceMetadataBehavior = app.ApplicationServices.GetRequiredService<CoreWCF.Description.ServiceMetadataBehavior>();
serviceMetadataBehavior.HttpGetEnabled = serviceMetadataBehavior.HttpsGetEnabled = true;
});
}
}
}
IDemo.cs (Service Interface):
using CoreWCF;
namespace CoreWcf.Samples.Http
{
// Define a service contract.
[ServiceContract]
public interface IDemo
{
//[OperationContract(IsOneWay = true)]
[OperationContract]
string DemoRequest(string tagid, string readerid, string datetime);
}
}
Demo.cs (Service):
using CoreWCF.Channels;
using Microsoft.AspNetCore.Http;
using System.Net;
namespace CoreWcf.Samples.Http
{
public class DemoService : IDemo
{
public string DemoRequest(string tagid, string readerid, string datetime)
{
return $"Received tagid: {tagid}; readerid: {readerid}; datetime: {datetime}";
}
}
}
CustomUserNamePasswordValidator.cs:
using CoreWCF;
using System.Threading.Tasks;
namespace CoreWcf.Samples.Http
{
internal class CustomUserNamePasswordValidator : CoreWCF.IdentityModel.Selectors.UserNamePasswordValidator
{
public override ValueTask ValidateAsync(string userName, string password)
{
bool valid = userName.ToLowerInvariant().EndsWith("valid")
&& password.ToLowerInvariant().EndsWith("valid");
if (!valid)
{
throw new FaultException("Unknown Username or Incorrect Password");
}
return new ValueTask();
}
public static void AddToHost(ServiceHostBase host)
{
var srvCredentials = new CoreWCF.Description.ServiceCredentials();
srvCredentials.UserNameAuthentication.UserNamePasswordValidationMode = CoreWCF.Security.UserNamePasswordValidationMode.Custom;
srvCredentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNamePasswordValidator();
host.Description.Behaviors.Add(srvCredentials);
}
}
}
Many thanks in advance for any assistance. cheers.
CoreWCF has it's own WebOperationContext in nuget package CoreWCF.WebHttp and you can set outgoing StatusCode there so to return custom status code you can just do
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.YourDesiredStatusCode;