'SoapCore Asp.net core 3.1 Header

Any idea how I can add a header for my calls using SoapCore?

what I have so far:

at startup.cs:
app.UseSoapEndpoint<IMyService>("/MyService.svc", new BasicHttpBinding(), SoapSerializer.DataContractSerializer);

in IMyService

[ServiceContract]
    public interface IMyService
    {      

        [OperationContract]
        public List<SOADataGetService> GetService(string ServiceType, string ServiceName, string ServiceVersion);
        
    }

then my soap ends up like that:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tem="http://tempuri.org/">
   <soapenv:Header/>
   <soapenv:Body>
      <tem:GetService>
         <tem:ServiceType>?</tem:ServiceType>
         <tem:ServiceName>?</tem:ServiceName>
         <tem:ServiceVersion>?</tem:ServiceVersion>
      </tem:GetService>
   </soapenv:Body>
</soapenv:Envelope>

I need to get in <soapenv:Header/> like user and password



Solution 1:[1]

You can access the header in SoapCore by implementing and registering a custom IServiceOperationTuner as described in the docs.

e.g.

public class MyServiceOperationTuner : IServiceOperationTuner
{
    public void Tune(HttpContext httpContext, object serviceInstance, SoapCore.ServiceModel.OperationDescription operation)
    {
        if (operation.Name.Equals(nameof(MyService.SomeOperationName)))
        {
            MyService service = serviceInstance as MyService;
            service.SetHttpRequest(httpContext.Request);
        }
    }
}
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddSingleton<IMyService, MyService>();
        services.TryAddSingleton<IServiceOperationTuner>(provider => new MyServiceOperationTuner());
    }
}
public class MyService : IMyService
{
    private ThreadLocal<HttpRequest> _httpRequest = new ThreadLocal<HttpRequest>() { Value = null };

    public void SetHttpRequest(HttpRequest request)
    {
        _httpRequest.Value = request;
    }

    public string SomeOperationName()
    {
        var soapHeader = GetHeaderFromRequest(_httpRequest.Value)
        return $"SOAP Header: {soapHeader}";
    }

    private XmlNode GetHeaderFromRequest(HttpRequest request)
    {
        var bytes = (request.Body as MemoryStream)?.ToArray();
        if (bytes == null)
        {
            // Body missing from request
            return null;
        }

        var envelope = new XmlDocument();
        envelope.LoadXml(Encoding.UTF8.GetString(bytes));

        return envelope.DocumentElement?.ChildNodes.Cast<XmlNode>().FirstOrDefault(n => n.LocalName == "Header");
    }
}

Solution 2:[2]

I hope this helps someone. I'm using SoapCore 1.1.0.28 with .Net Core 6. I tried the Tune method listed by @wolfyuk, but Core always returned bytes as null, so I was never able to get past the null check.

The most straightforward way I found is to use IMessageInspector2 from SoapCore to create middleware to intercept the SOAP request on the way in and intercept the SOAP response on the way out. Your class that implements IMessageInspector2 has access to the message so you can extract headers on the way in (that's what I needed), and add headers on the way out. I needed the request headers to be included in my response (a requirement of the system I'm communicating with).

public class AuthMessageFilter : IMessageInspector2
{
    private const string WsNamespaceSecurityUri = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
    private const string WsUserNameTokenNodeName = "UsernameToken";
    private const string WsSecurityNodeName = "Security";
    private const string WsTimestampNodeName = "Timestamp";
    private readonly IMyService _service;
    private readonly IHttpContextAccessor _acc;
    private readonly ILogger _logger;
    private MessageHeaders _messageHeaders;

    public AuthMessageFilter(IHttpContextAccessor acc, IMyService service, ILogger logger)
    {
        _acc = acc;
        _service = service;
        _logger = logger;
    }

    public object AfterReceiveRequest(ref Message message, ServiceDescription serviceDescription)
    {
        ValidateSoapAction();
        var token = GetUserNameToken(message);
        var userIsAuthenticated = _service.ValidateUser(token.Username, token.Password.Value).GetAwaiter().GetResult();
        if (userIsAuthenticated) 
        {
            _messageHeaders = message.Headers; // use in response.
            return null;
        }
        
        const string msg = "The user credentials did not authenticate.";
        _logger.LogEntry(msg);
        throw new AuthenticationFailedException(msg);
    }
    
    private void ValidateSoapAction()
    {
        try
        {
            var soapAction = _acc.HttpContext?.Request.Headers["SOAPAction"].FirstOrDefault()?.Replace("\"", "");
            if (soapAction == null)
            {
                throw new Exception(
                    "Error: Could not extract SoapAction from HttpContext.Request.Headers. Aborting SOAP operation.");
            }
        }
        catch (Exception ex)
        {
            _logger.LogEntry("No SOAP Action found.", ex);
        }
    }

    private WsUsernameToken GetUserNameToken(Message message)
    {
        WsUsernameToken wsUsernameToken = null;
        for (var i = 0; i < _messageHeaders.Count; i++)
        {
            if (!_messageHeaders[i].Name.Equals(WsSecurityNodeName, StringComparison.OrdinalIgnoreCase)) 
                continue;
            
            using var reader = _messageHeaders.GetReaderAtHeader(i);
            while (reader.Read())
            {
                if (reader.IsStartElement() &&
                    reader.NamespaceURI.Equals(WsNamespaceSecurityUri, StringComparison.OrdinalIgnoreCase) &&
                    reader.LocalName.Equals(WsUserNameTokenNodeName, StringComparison.OrdinalIgnoreCase))
                {
                    var serializer = new XmlSerializer(typeof(WsUsernameToken));
                    wsUsernameToken = (WsUsernameToken)serializer.Deserialize(reader);
                    break;
                }
            }
            break;
        }

        if (wsUsernameToken == null)
        {
            var ex = new SecurityException("An exception occurred when verifying security for the message.");
            _logger.LogEntry(LoggingCategory.Service, LoggingLevel.Error, ex.Message, ex);
            throw ex;
        }

        return wsUsernameToken;
    }
    public void BeforeSendReply(ref Message reply, ServiceDescription serviceDescription, object correlationState)
    {
        for (var i = 0; i < _messageHeaders.Count; i++)
        {
            if (!_messageHeaders[i].Name.Equals(WsSecurityNodeName, StringComparison.OrdinalIgnoreCase)) 
                continue;
            
            using var reader = _messageHeaders.GetReaderAtHeader(i);
            while (reader.Read())
            {
                if (reader.IsStartElement() &&
                    reader.NamespaceURI.Equals(WsNamespaceSecurityUri, StringComparison.OrdinalIgnoreCase) &&
                    reader.LocalName.Equals(WsTimestampNodeName, StringComparison.OrdinalIgnoreCase))
                {
                    reply.Headers.Add(_messageHeaders[i] as MessageHeader);
                    break;
                }
            }
            break;
        }
    }
}

Solution 3:[3]

Do not use SoapCore it is outdated, try to use SmartSoap: https://github.com/Raffa50/SmartSoap

it is also available as a nugetPackage: https://www.nuget.org/packages/Aldrigos.SmartSoap.AspNet/

Have a look at it, try it and if you need further support I will be pleased to help you!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 wolfyuk
Solution 2
Solution 3 raffa50