NET6+ and WCF headers

While creating a new frontend, written in C# on NET6+ (dotnet core) I stumbled across an issue talking to the old .NET Framework backend. The backend is exposed via WCF, and requires authentication headers to be sent. This is a bit tricky using NET6+, and does not work out of the box when adding the backend as a Connected Service (via Visual Studio). The out of the box solutions sends the headers as parameters in the SOAP body instead of the SOAP header.

WCF services added in Service References in Visual Studio

The structure of the WCF API

The WCF project and its services looks something like this

[SoapHeader(nameof(TransactionHeader), Direction = SoapHeaderDirection.In)]
[SoapHeader(nameof(AuthenticationHeader), Direction = SoapHeaderDirection.In)]
public string MyWCFApiMethod(string input)
{
    return ""; 
}Code language: C# (cs)

The SoapHeader attribute dictates that there should be a SOAP header present with the given names. The names corresponds to properties in a parent class (that the services inherits). The properties are POCOs, inheriting System.Web.Services.Protocols.SoapHeader, and contains a few string and GUID parameters.

In this case, the TransactionHeader is used to identify the caller (suppling IP-address, session IDs and such.

The AuthenticationHeader takes an object with username and password, or token.

Every method in the WCF API checks different parts of these headers, depending on the security level of the call.

The issue with NET6+

When a WCF reference is added to a modern dotnet core project, headers like this are not recognised correctly, leading to request DTOs with the headers as properties. This makes every method call to a function ugly, and requires setting these properties every time. This also leads to access errors, as the properties are not sent as headers.

The Authentication and Transaction objects are created (in the References.cs file) as any other class. They don’t implement any header interface or base class.

In order to get this to work, we must say that they are headers and also set them on every request. We begin with the header classes:

public partial class Authentication : System.ServiceModel.Channels.MessageHeader
{
    // UserName and Password are declared in the other partial class file

    public override string Name => nameof(Authentication);
    public override string Namespace => "http://our.namespace.com/";

    protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
    {
        writer.WriteStartElement(nameof(UserName));
        writer.WriteString(UserName);
        writer.WriteEndElement();

        writer.WriteStartElement(nameof(Password));
        writer.WriteString(Password);
        writer.WriteEndElement();
    }
}Code language: C# (cs)

We do the corresponding to our Transaction header class.

We then create an implementation of the IClientMessageInspector interface. This is what will be setting the header values.

public class AuthenticationMessageInspector : IClientMessageInspector
{
    private readonly ILogger<AuthenticationMessageInspector> _logger;

    public AuthenticationMessageInspector(ILogger<AuthenticationMessageInspector> logger)
    {
        _logger = logger;
    }

    public void AfterReceiveReply(ref Message reply, object correlationState)
    { }

    public object? BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        _logger.LogInformation("adding headers to request");
        request.Headers.Add(new Authentication
        {
            UserName = "username",
            Password = "password"
        });

        request.Headers.Add(new Transaction());

        return null;
    }
}Code language: C# (cs)

Wiring it all up

We need to write an IEndpointBehavior which ties all this together.

public class MyWCFEndpointBehavior : IEndpointBehavior
{
    private readonly AuthenticationMessageInspector _authenticationMessageInspector;
    private readonly ILogger<MyWCFEndpointBehavior> _logger;

    public MyWCFEndpointBehavior(AuthenticationMessageInspector authenticationMessageInspector, ILogger<MyWCFEndpointBehavior> logger)
    {
        _authenticationMessageInspector = authenticationMessageInspector;
        _logger = logger;
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        _logger.LogDebug("Applying client behavior");
        clientRuntime.ClientMessageInspectors.Clear();
        clientRuntime.ClientMessageInspectors.Add(_authenticationMessageInspector);
        // Feel free to add more inspectors, perhaps to log certain responses/requests
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }

    public void Validate(ServiceEndpoint endpoint) { }
}Code language: C# (cs)

Extend our MyWCFSoapClient (which be generated as a partial class) by creating a new public partial MyWCFSoapClient in the same namespace:

public partial class MyWCFSoapClient
{
    public MyWCFSoapClient(EndpointConfiguration endpointConfiguration, string remoteAddress, IEndpointBehavior endpointBehavior)
        : base(GetBindingForEndpoint(endpointConfiguration), new System.ServiceModel.EndpointAddress(remoteAddress))
    {
        Endpoint.EndpointBehaviors.Add(endpointBehavior);
    }
}Code language: C# (cs)

In Program.cs, we add the following

builder.Services.AddTransient<MyWCFEndpointBehavior>();
builder.Services.AddTransient<AuthenticationMessageInspector>();

builder.Services.AddTransient<MyWCFSoap, MyWCFSoapClient>(provider =>
{
    var baseUrl = builder.Configuration.GetSection("MyWCF").GetValue<string>("BaseUrl");
    return new MyWCFSoapClient(
        MyWCFSoapClient.EndpointConfiguration.MyWCFSoap,
        $"{baseUrl}/OFS.asmx",
        provider.GetRequiredService<MyWCFEndpointBehavior>()
    );
});Code language: C# (cs)

Now you can use them via dependency injection in your controllers or services

public class MyWCFService
{
    private readonly MyWCFSoap _myWCF;
    public MyWCFService(MyWCFSoap myWCF) => _myWCF = myWCF;

    public async Task<string> GetValue()
    {
        return await _myWCF.GetValueAsync();
    }
}Code language: C# (cs)

All in all, this took me a while to figure out. The code above is not tested, as I’ve copied and modified (minified) my working code (which I wrote for a customer). The original code has way more dependency injections which fetches information about the currently logged on user among other things. The original code also must fetch a token via the very same client before generating a new client with the IEndpointBehavior we created (username and password is supplied to get a token, which is used for subsequent requests, and IEndpointBehaviors can’t be added once the client has been created…).

This should however explain the gist on how to do it.

If I get my thumbs out, I might even make a sample project for this.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *