I know how to use [FaultException] and throw Customized [DataContract] in WCF
I know how to return soap message with Customized Headers and Body Tags using [MessageContract]
However, I'm going to ask: How to throw a [FaultException] in WCF with customized Headers and Body? Because when I use [FaultException] to throw [MessageContract], it always wraps my headers and body tags into body tag.
This is not what I want. Most of the SOAP clients do not understand WCF [FaultException].
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header />
<s:Body>
<s:Fault>
<faultcode>s:Client</faultcode>
<faultstring xml:lang="en-CH" />
<detail>
<CustomizedError xmlns="http://schemas.datacontract.org/2004/07/PlayWcfFault" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<BodyTag1>Hello BodyTag1</BodyTag1>
<HeaderTag1>Hello HeaderTag1</HeaderTag1>
</CustomizedError>
</detail>
</s:Fault>
</s:Body>
</s:Envelope>
[MessageContract]
public class CustomizedError
{
[MessageHeader] public string HeaderTag1;
[MessageBodyMember] public string BodyTag1;
}
[ServiceContract]
public interface IService1
{
[OperationContract]
[FaultContract(typeof(CustomizedError))]
CustomizedError GetData();
}
public class Service1 : IService1
{
public CustomizedError GetData()
{
CustomizedError fault = new CustomizedError
{
HeaderTag1 = "Hello HeaderTag1",
BodyTag1 = "Hello BodyTag1",
};
throw new FaultException<CustomizedError>(fault, "");
// return fault;
}
}
The soap message I want is
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header />
<HeaderTag1>Hello HeaderTag1</HeaderTag1>
</s:Header >
<s:Body>
<s:Fault>
<BodyTag1>Hello BodyTag1</BodyTag1>
</detail>
</s:Fault>
</s:Body>
</s:Envelope>
I am confused that why you define the soap message structure in that format. But as far as I know, the IErrorHandler interface is able to custom the fault message when capturing the communication error. Here is an example used to capture the errors during communication.
Hadler Error EndPoint in my host - WCF - Behaviour
We are capable of adding the custom message header in the ProvideFault method. Please consider the below code.
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
FaultException faultException = new FaultException(error.Message);
MessageFault messageFault = faultException.CreateMessageFault();
fault = Message.CreateMessage(version, messageFault, error.Message);
MessageHeader mh = MessageHeader.CreateHeader("itemname", "apptest", "34");
fault.Headers.Add(mh);
}
Result.
Feel free to let me know if there is anything I can help with.
Updated.
class Program
{
static void Main(string[] args)
{
Uri uri = new Uri("http://localhost:4386");
WSHttpBinding binding = new WSHttpBinding();
binding.Security.Mode = SecurityMode.None;
using (ServiceHost sh = new ServiceHost(typeof(TestService), uri))
{
sh.AddServiceEndpoint(typeof(ITestService), binding, "");
ServiceMetadataBehavior smb;
smb = sh.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (smb == null)
{
smb = new ServiceMetadataBehavior()
{
HttpGetEnabled = true
};
sh.Description.Behaviors.Add(smb);
}
Binding mexbinding = MetadataExchangeBindings.CreateMexHttpBinding();
sh.AddServiceEndpoint(typeof(IMetadataExchange), mexbinding, "mex");
sh.Opened += delegate
{
Console.WriteLine("service is ready");
};
sh.Closed += delegate
{
Console.WriteLine("service is closed");
};
sh.Open();
Console.ReadLine();
sh.Close();
}
}
}
[ServiceContract]
public interface ITestService
{
[OperationContract]
int Div(int x, int y);
}
[ServiceBehavior]
public class TestService : ITestService
{
public int Div(int x, int y)
{
if (y == 0)
{
FaultReason reason = new FaultReason("The divisor cannot be Zero");
FaultCode code = new FaultCode("NotZero", "mynamespace",new FaultCode("mysubcode"));
throw new FaultException(reason,code);
}
return x / y;
}
}
In order to create ActivityID field, we should enable tracing and message logging on the server-side.
<system.diagnostics>
<sources>
<source name="System.ServiceModel" propagateActivity="true" switchValue="Information, ActivityTracing">
<listeners>
<add type="System.Diagnostics.XmlWriterTraceListener" name="xmlLog" initializeData="myLogs.svclog"/>
</listeners>
</source>
</sources>
</system.diagnostics>
<system.serviceModel>
<diagnostics>
<messageLogging logEntireMessage="true" logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="false"/>
</diagnostics>
</system.serviceModel>
Link.
About ActivityID field
Configure tracing and message logging
When the invocation on the client-side passes a parameter 0. We capture the http request by using fiddler.
Related
Is there way to change the wcf contract from.
This
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="http://schemas.datacontract.org/2004/07/">
<soapenv:Header/>
<soapenv:Body>
**<AddCustomer>**
<!--Optional:-->
<Data>
<ns:AddressLine1>?</ns:AddressLine1>
<ns:AddressLine2>?</ns:AddressLine2>
<ns:City>?</ns:City>
<ns:Country>?</ns:Country>
<ns:Email>?</ns:Email>
<ns:FirstName>?</ns:FirstName>
<ns:HomePhone>?</ns:HomePhone>
<ns:ID>?</ns:ID>
<ns:LastName>?</ns:LastName>
<ns:MobilePhone>?</ns:MobilePhone>
<ns:State>?</ns:State>
<ns:Suburb>?</ns:Suburb>
</Data>
**</AddCustomer>**
To
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:biz="http://BizTalk.Test001.Customer">
<soapenv:Header/>
<soapenv:Body>
<biz:Data>
<ID>?</ID>
<FirstName>?</FirstName>
<LastName>?</LastName>
<MobilePhone>?</MobilePhone>
<HomePhone>?</HomePhone>
<Email>?</Email>
<AddressLine1>?</AddressLine1>
<AddressLine2>?</AddressLine2>
<Suburb>?</Suburb>
<City>?</City>
<State>?</State>
<Country>?</Country>
</biz:Data>
Basically i want to get rid of "AddCustomer" from the request. Can we do something using ServiceContract or OperationContract.
You have to implement IClientMessageInspector interface.So before sending a request or after receving the response you need to implement your own C# methods to override your request /response SOAP messages like,
public class ClientMessageInspector : IClientMessageInspector
{
public string LastRequestXml { get; private set; }
public string LastResponseXml { get; private set; }
public void AfterReceiveReply(ref Message reply, object correlationState)
{
LastResponseXml = reply.ToString();
}
public object BeforeSendRequest(ref Message request, System.ServiceModel.IClientChannel channel)
{
LastRequestXml = request.ToString();
var xmlPayload = ChangeMessage();
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
writer.Write(xmlPayload);
writer.Flush();
ms.Position = 0;
var reader = XmlReader.Create(ms);
request = Message.CreateMessage(reader, int.MaxValue, request.Version);
var payload = request.ToString();
return request;
}
/// Manipulate the SOAP message
private string ChangeMessage()
{
//manipulation code here to remove AddCustomer.
}
}
For detail implementation check out this blog.
https://cmatskas.com/changing-soap-message-data-and-namespaces-with-c/
I am working on WCF Rest application I need to implement the token based authentication in it.Please suggest me a perfect way to implement the token based authentication WCF Rest.
I was able to implement AAD Token based authentication in a WCF based SOAP service.
For this I leveraged WCF extensibility features - Message Inspector and Custom Invoker in the following way
Message Inspector : Using the message inspector, we extract the Bearer Token from the Authorization Header of the incoming request. Post this we perform the token validation using OIDC library to get the keys and config for Microsoft AAD. If token is validated, the operation is invoked and we get the response on the client side.
If the Token validation fails, we stop the request processing by using Custom Invoker and return 401 Unauthorized response to the caller with a custom error message.
public class BearerTokenMessageInspector : IDispatchMessageInspector
{
/// Method called just after request is received. Implemented by default as defined in IDispatchMessageInspector
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
WcfErrorResponseData error = null;
var requestMessage = request.Properties["httpRequest"] as HttpRequestMessageProperty;
if (request == null)
{
error = new WcfErrorResponseData(HttpStatusCode.BadRequest, string.Empty, new KeyValuePair<string, string>("InvalidOperation", "Request Body Empty."));
return error;
}
var authHeader = requestMessage.Headers["Authorization"];
try
{
if (string.IsNullOrEmpty(authHeader))
{
error = new WcfErrorResponseData(HttpStatusCode.Unauthorized, string.Empty, new KeyValuePair<string, string>("WWW-Authenticate", "Error: Authorization Header empty! Please pass a Token using Bearer scheme."));
}
else if (this.Authenticate(authHeader))
{
return null;
}
}
catch (Exception e)
{
error = new WcfErrorResponseData(HttpStatusCode.Unauthorized, string.Empty, new KeyValuePair<string, string>("WWW-Authenticate", "Token with Client ID \"" + clientID + "\" failed validation with Error Messsage - " + e.Message));
}
if (error == null) //Means the token is valid but request must be unauthorized due to not-allowed client id
{
error = new WcfErrorResponseData(HttpStatusCode.Unauthorized, string.Empty, new KeyValuePair<string, string>("WWW-Authenticate", "Token with Client ID \"" + clientID + "\" failed validation with Error Messsage - " + "The client ID: " + clientID + " might not be in the allowed list."));
}
//This will be checked before the custom invoker invokes the method, if unauthorized, nothing is invoked
OperationContext.Current.IncomingMessageProperties.Add("Authorized", false);
return error;
}
/// Method responsible for validating the token and tenantID Claim.
private bool Authenticate(string authHeader)
{
const string bearer = "Bearer ";
if (!authHeader.StartsWith(bearer, StringComparison.InvariantCultureIgnoreCase)) { return false; }
var jwtToken = authHeader.Substring(bearer.Length);
PopulateIssuerAndKeys();
var validationParameters = GenerateTokenValidationParameters(_signingKeys, _issuer);
return ValidateToken(jwtToken, validationParameters);
}
/// Method responsible for validating the token against the validation parameters. Key Rollover is
/// handled by refreshing the keys if SecurityTokenSignatureKeyNotFoundException is thrown.
private bool ValidateToken(string jwtToken, TokenValidationParameters validationParameters)
{
int count = 0;
bool result = false;
var tokenHandler = new JwtSecurityTokenHandler();
var claimsPrincipal = tokenHandler.ValidateToken(jwtToken, validationParameters, out SecurityToken validatedToken);
result = (CheckTenantID(validatedToken));
return result;
}
/// Method responsible for sending proper Unauthorized reply if the token validation failed.
public void BeforeSendReply(ref Message reply, object correlationState)
{
var error = correlationState as WcfErrorResponseData;
if (error == null) return;
var responseProperty = new HttpResponseMessageProperty();
reply.Properties["httpResponse"] = responseProperty;
responseProperty.StatusCode = error.StatusCode;
var headers = error.Headers;
if (headers == null) return;
foreach (var t in headers)
{
responseProperty.Headers.Add(t.Key, t.Value);
}
}
}
NOTE - Please refer to this gist for complete Message Inspector code.
Custom Invoker - The job of custom Invoker is to stop the request flow to further stages of WCF Request processing pipeline if the Token is invalid. This is done by setting the Authorized flag as false in the current OperationContext (set in Message Inspector) and reading the same in Custom Invoker to stop the request flow.
class CustomInvoker : IOperationInvoker
{
public object Invoke(object instance, object[] inputs, out object[] outputs)
{
// Check the value of the Authorized header added by Message Inspector
if (OperationContext.Current.IncomingMessageProperties.ContainsKey("Authorized"))
{
bool allow = (bool)OperationContext.Current.IncomingMessageProperties["Authorized"];
if (!allow)
{
outputs = null;
return null;
}
}
// Otherwise, go ahead and invoke the operation
return defaultInvoker.Invoke(instance, inputs, out outputs);
}
}
Here's the complete gist for Custom Invoker.
Now you need to inject the Message Inspector and the Custom Invoker to your WCF Pipeline using Endpoint Behavior Extension Element. Here are the gists for the class files to do this and a few other needed helper classes:
BearerTokenEndpointBehavior
BearerTokenExtensionElement
MyOperationBehavior
OpenIdConnectCachingSecurityTokenProvider
WcfErrorResponseData
The job is not done yet! You need to write a custom binding and expose a custom AAD endpoint in your web.config apart from adding the AAD Config Keys -
<!--List of AAD Settings-->
<appSettings>
<add key="AADAuthority" value="https://login.windows.net/<Your Tenant ID>"/>
<add key="AADAudience" value="your service side AAD App Client ID"/>
<add key="AllowedTenantIDs" value="abcd,efgh"/>
<add key="ValidateIssuer" value="true"/>
<add key="ValidateAudience" value="true"/>
<add key="ValidateIssuerSigningKey" value="true"/>
<add key="ValidateLifetime" value="true"/>
<add key="useV2" value="true"/>
<add key="MaxRetries" value="2"/>
</appSettings>
<bindings>
<wsHttpBinding>
<!--wsHttpBinding needs client side AAD Token-->
<binding name="wsHttpBindingCfgAAD" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647" closeTimeout="00:30:00" openTimeout="00:30:00" receiveTimeout="00:30:00" sendTimeout="00:30:00">
<readerQuotas maxDepth="26214400" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
<security mode="Transport">
<transport clientCredentialType="None"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
<services>
<!--Exposing a new baseAddress/wssecureAAD endpoint which will support AAD Token Validation-->
<service behaviorConfiguration="ServiceBehaviorCfg" name="Service">
<!--wshttp endpoint with client AAD Token based security-->
<endpoint address="wsSecureAAD" binding="wsHttpBinding" bindingConfiguration="wsHttpBindingCfgAAD" name="ServicewsHttpEndPointAAD" contract="ServiceContracts.IService" behaviorConfiguration="AADEnabledEndpointBehavior"/>
</service>
</services>
<behaviors>
<endpointBehaviors> <!--Injecting the Endpoint Behavior-->
<behavior name="AADEnabledEndpointBehavior">
<bearerTokenRequired/>
</behavior>
</endpointBehaviors>
</behaviors>
<extensions>
<behaviorExtensions> <!--Linking the BearerTokenExtensionElement-->
<add name="bearerTokenRequired" type="TokenValidator.BearerTokenExtensionElement, TokenValidator"/>
</behaviorExtensions>
</extensions>
Your WCF service should now accept AAD Tokens on this custom AAD endpoint and your tenants will be able to consume the same by just changing the binding and endpoint from their side. Note that you will need to add the tenant's client ID in the allowedTenantIDs list in web.config so as to authorize the tenant to hit your service.
Final Note - Though I have implemented Microsoft's AAD Based Authentication, you should be able to reuse the whole code to implement any OAuth based Identity Provider's Token Validation. You just need to change the respective keys for AADAuthority in web.config.
You can implement Bearer token authentication
using Microsoft.Owin;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Http;
[assembly: OwinStartup(typeof(ns.Startup))]
namespace ns
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
config.MessageHandlers.Add(new LogRequestAndResponseHandler());
}
Configure using of OAuthBearerAuthentication:
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/TokenService"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(3),
Provider = new SimpleAuthorizationServerProvider()
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
And finally set the identity claims
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
try
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, "Name"));
identity.AddClaim(new Claim(ClaimTypes.Sid, "Sid"));
identity.AddClaim(new Claim(ClaimTypes.Role, "Role"));
context.Validated(identity);
}
catch (System.Exception ex)
{
context.SetError("Error....");
context.Response.Headers.Add("X-Challenge", new[] { ((int)HttpStatusCode.InternalServerError).ToString() });
}
}
}
}
}
It's the easiest solution and works like a charm!
I have a WCF behavior extension that I would like to add to a WCF client. However, the client is constructed programmatically. The endpoint address may vary, but I know the type. I could add the behavior programmatically or in the config file (preferred), but I need to pass some configuration in the config file only.
I don't want this in Common behaviors (machine.config).
I can add the behavior programmatically
endpoint.Behaviors.Add(new MyCustomBehavior())
But I'd rather do it in config, so I can configure the extension there as well.
Is it possible to declaratively add and configure an endpoint behavior extension to a programmatically constructed endpoint knowing only the the type or interface while leaving the client endpoint to be constructed programmatically?
<system.serviceModel>
<client>
<!-- Created programmatically -->
</client>
<extensions>
<behaviorExtensions>
<add name="MyCustomBehavior" type="namespace.CustomBehaviors", MyAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<behaviors>
<endpointBehaviors>
<behavior name="MyCustomBehavior">
<MyCustomBehavior MyImportantBehaviorParam1="foo" />
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
Of course I could put the config in another section, and have my behavior read it there, but I'd rather use the WCF facilities if possible.
To do that you need to create a behavior configuration extension for your endpoint. For more information on how to do that, check https://learn.microsoft.com/en-us/archive/blogs/carlosfigueira/wcf-extensibility-behavior-configuration-extensions.
Update: I see your issue now. There's no direct way to add to an endpoint created via code a behavior declared in configuration. You can still do it, though, but you'll need to use some reflection to access the CreateBehavior method of the behavior configuration extension (the method is protected) to actually create the endpoint behavior to add it to the endpoint created via code. The code below shows how this can be done.
public class StackOverflow_10232385
{
public class MyCustomBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
}
public void Validate(ServiceEndpoint endpoint)
{
Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
}
}
public class MyCustomBehaviorExtension : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(MyCustomBehavior); }
}
protected override object CreateBehavior()
{
return new MyCustomBehavior();
}
}
[ServiceContract]
public interface ITest
{
[OperationContract]
string Echo(string text);
}
public class Service : ITest
{
public string Echo(string text)
{
return text;
}
}
public static void Test()
{
string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
var configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
ServiceModelSectionGroup smsg = configuration.GetSectionGroup("system.serviceModel") as ServiceModelSectionGroup;
EndpointBehaviorElement endpointBehaviorElement = smsg.Behaviors.EndpointBehaviors["MyCustomBehavior_10232385"];
foreach (BehaviorExtensionElement behaviorElement in endpointBehaviorElement)
{
MethodInfo createBehaviorMethod = behaviorElement.GetType().GetMethod("CreateBehavior", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, Type.EmptyTypes, null);
IEndpointBehavior behavior = createBehaviorMethod.Invoke(behaviorElement, new object[0]) as IEndpointBehavior;
endpoint.Behaviors.Add(behavior);
}
host.Open();
Console.WriteLine("Host opened");
ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new BasicHttpBinding(), new EndpointAddress(baseAddress));
ITest proxy = factory.CreateChannel();
Console.WriteLine(proxy.Echo("Hello"));
((IClientChannel)proxy).Close();
factory.Close();
Console.Write("Press ENTER to close the host");
Console.ReadLine();
host.Close();
}
}
And the configuration for this code:
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="myCustomBehavior_10232385" type="QuickCode1.StackOverflow_10232385+MyCustomBehaviorExtension, QuickCode1"/>
</behaviorExtensions>
</extensions>
<behaviors>
<endpointBehaviors>
<behavior name="MyCustomBehavior_10232385">
<myCustomBehavior_10232385/>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
Situation
I have modified the WCF Router sample to remove things that I'm not currently using, support clientVia rather than direct connections, and support HTTP and HTTPS. When I use the router as an intermediary to an MLS-enabled service, everything works fine. When I use the router as an intermediate to a TLS- and MLS-enabled service, I am failing on setting up the secure conversation.
I am connecting to the router using HTTP and trying to route to the SSL-enabled service using HTTPS. If I try to use TLS to the router, then the router has to be on HTTPS.. and then I'd need the certificate to match, which is why I'm trying to setup the Client ==(HTTP)==> Router ==(HTTPS)==> Service relationship.
The message sent to establish TLS in the case of a direct connection (which works) is very different from the message sent to establish TLS when going through the router. It looks like the client is dictating what's being sent, but shouldn't the client just be sending the MLS message and the router be establishing TLS and then sending the MLS message?
Code and config info follow, and the resultant traffic (direct and through router) as well. Since no relevant changes have been made to the RouterBindingElement class, I've removed that from the code below (to save space).
Any suggestions as to how to get the SSL working would be greatly appreciated.
Code/Config
RouterBinding class
public enum RouterTransport
{
Http = 0,
Tcp = 1,
NamedPipe = 2,
Https = 3
}
public enum MessageEncoding
{
Text,
Binary,
Mtom,
}
public class RouterBinding : Binding, IBindingRuntimePreferences
{
MessageEncoding messageEncoding;
RouterTransport transport;
HttpTransportBindingElement httpTransport;
HttpsTransportBindingElement httpsTransport;
TcpTransportBindingElement tcpTransport;
NamedPipeTransportBindingElement namedPipeTransport;
TextMessageEncodingBindingElement textEncoding;
MtomMessageEncodingBindingElement mtomEncoding;
BinaryMessageEncodingBindingElement binaryEncoding;
public RouterBinding()
: base()
{
Initialize();
}
public RouterBinding(string configurationName)
: this()
{
ApplyConfiguration(configurationName);
}
public RouterBinding(RouterTransport transport)
: this()
{
this.Transport = transport;
if (transport == RouterTransport.NamedPipe || transport == RouterTransport.Tcp)
{
this.MessageEncoding = MessageEncoding.Binary;
}
}
public RouterTransport Transport
{
get { return this.transport; }
set
{
this.transport = value;
}
}
public MessageEncoding MessageEncoding
{
get { return this.messageEncoding; }
set
{
this.messageEncoding = value;
}
}
public HostNameComparisonMode HostNameComparisonMode
{
get { return this.tcpTransport.HostNameComparisonMode; }
set
{
this.tcpTransport.HostNameComparisonMode = value;
this.namedPipeTransport.HostNameComparisonMode = value;
this.httpTransport.HostNameComparisonMode = value;
this.httpsTransport.HostNameComparisonMode = value;
}
}
public int ListenBacklog
{
get { return this.tcpTransport.ListenBacklog; }
set { this.tcpTransport.ListenBacklog = value; }
}
public long MaxBufferPoolSize
{
get { return this.tcpTransport.MaxBufferPoolSize; }
set
{
this.tcpTransport.MaxBufferPoolSize = value;
this.namedPipeTransport.MaxBufferPoolSize = value;
}
}
public int MaxBufferSize
{
get { return this.tcpTransport.MaxBufferSize; }
set
{
this.tcpTransport.MaxBufferSize = value;
this.namedPipeTransport.MaxBufferSize = value;
}
}
public int MaxConnections
{
get { return this.tcpTransport.ConnectionPoolSettings.MaxOutboundConnectionsPerEndpoint; }
set
{
this.tcpTransport.MaxPendingConnections = value;
this.namedPipeTransport.MaxPendingConnections = value;
this.tcpTransport.ConnectionPoolSettings.MaxOutboundConnectionsPerEndpoint = value;
this.namedPipeTransport.ConnectionPoolSettings.MaxOutboundConnectionsPerEndpoint = value;
}
}
public long MaxReceivedMessageSize
{
get { return this.tcpTransport.MaxReceivedMessageSize; }
set
{
this.tcpTransport.MaxReceivedMessageSize = value;
this.namedPipeTransport.MaxReceivedMessageSize = value;
this.httpTransport.MaxReceivedMessageSize = value;
this.httpsTransport.MaxReceivedMessageSize = value;
}
}
public bool PortSharingEnabled
{
get { return this.tcpTransport.PortSharingEnabled; }
set { this.tcpTransport.PortSharingEnabled = value; }
}
public TransferMode TransferMode
{
get { return this.tcpTransport.TransferMode; }
set
{
this.tcpTransport.TransferMode = value;
this.namedPipeTransport.TransferMode = value;
this.httpTransport.TransferMode = value;
this.httpsTransport.TransferMode = value;
}
}
bool IBindingRuntimePreferences.ReceiveSynchronously
{
get { return false; }
}
public override string Scheme
{
get { return this.TransportElement.Scheme; }
}
void Initialize()
{
this.httpTransport = new HttpTransportBindingElement();
this.httpsTransport = new HttpsTransportBindingElement();
this.tcpTransport = new TcpTransportBindingElement();
this.namedPipeTransport = new NamedPipeTransportBindingElement();
this.textEncoding = new TextMessageEncodingBindingElement();
this.mtomEncoding = new MtomMessageEncodingBindingElement();
this.binaryEncoding = new BinaryMessageEncodingBindingElement();
this.httpTransport.ManualAddressing = true;
this.httpsTransport.ManualAddressing = true;
this.tcpTransport.ManualAddressing = true;
this.namedPipeTransport.ManualAddressing = true;
this.transport = RouterTransport.Http;
this.messageEncoding = MessageEncoding.Text;
}
void ApplyConfiguration(string configurationName)
{
RouterBindingCollectionElement bindingCollectionElement = RouterBindingCollectionElement.GetBindingCollectionElement();
RouterBindingElement element = bindingCollectionElement.Bindings[configurationName];
if (element == null)
{
throw new ConfigurationErrorsException(string.Format("ConfigInvalidBindingConfigurationName", configurationName, bindingCollectionElement.BindingName));
}
else
{
element.ApplyConfiguration(this);
}
}
TransportBindingElement TransportElement
{
get
{
switch (this.transport)
{
case RouterTransport.Http:
return this.httpTransport;
case RouterTransport.Https:
return this.httpsTransport;
case RouterTransport.Tcp:
return this.tcpTransport;
case RouterTransport.NamedPipe:
return this.namedPipeTransport;
}
return null;
}
}
MessageEncodingBindingElement EncodingElement
{
get
{
switch (this.messageEncoding)
{
case MessageEncoding.Text:
return this.textEncoding;
case MessageEncoding.Mtom:
return this.mtomEncoding;
case MessageEncoding.Binary:
return this.binaryEncoding;
}
return null;
}
}
public override BindingElementCollection CreateBindingElements()
{
BindingElementCollection elements = new BindingElementCollection();
elements.Add(this.EncodingElement);
elements.Add(this.TransportElement);
return elements;
}
}
public partial class RouterBindingCollectionElement : StandardBindingCollectionElement<RouterBinding, RouterBindingElement>
{
// Removed for space
}
Router Class
class SoapRouterExtension : IExtension<ServiceHostBase>
{
IDictionary<string, Binding> bindings = new Dictionary<string, Binding>(2);
public SoapRouterExtension()
{
this.bindings.Add("http", new RouterBinding("HttpTextSoap12RouterBinding"));
this.bindings.Add("https", new RouterBinding("HttpsTextSoap12RouterBinding"));
}
public IDictionary<string, Binding> Bindings
{
get { return this.bindings; }
}
public void Attach(ServiceHostBase owner)
{ }
public void Detach(ServiceHostBase owner)
{ }
}
sealed class SoapRouterServiceBehavior : Attribute, IServiceBehavior
{
void IServiceBehavior.Validate(ServiceDescription description, ServiceHostBase serviceHostBase)
{ }
void IServiceBehavior.AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters)
{ }
void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
{
SoapRouterExtension extension = new SoapRouterExtension();
serviceHostBase.Extensions.Add(extension);
}
}
[SoapRouterServiceBehavior]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Multiple, ValidateMustUnderstand = false, AddressFilterMode = AddressFilterMode.Any)]
public sealed class SoapRouter : IRequestReplyDatagramRouter, IDisposable
{
SoapRouterExtension extension;
public SoapRouter()
{
ServiceHostBase host = OperationContext.Current.Host;
this.extension = host.Extensions.Find<SoapRouterExtension>();
}
#region SoapIntermediary Request-Reply Datagram
Message IRequestReplyDatagramRouter.ProcessMessage(Message message)
{
EndpointAddress to = new EndpointAddress(message.Headers.To.AbsoluteUri);
IRequestReplyDatagramRouter forwardingChannel = null;
try
{
ChannelFactory<IRequestReplyDatagramRouter> factory = new ChannelFactory<IRequestReplyDatagramRouter>(this.extension.Bindings[to.Uri.Scheme], to);
factory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
forwardingChannel = factory.CreateChannel();
Console.WriteLine("Forwarding request " + message.Headers.Action + "...");
Message response = forwardingChannel.ProcessMessage(message);
Console.WriteLine("Forwarding response " + response.Headers.Action + "...");
return response;
}
finally
{
if (forwardingChannel != null)
{
IClientChannel channel = forwardingChannel as IClientChannel;
if (channel.State == CommunicationState.Faulted)
channel.Abort();
else
channel.Close();
}
}
}
#endregion
void IDisposable.Dispose()
{
}
}
public class ServiceDriver
{
public static void Main(string[] args)
{
ServiceHost serviceHost = new ServiceHost(typeof(SoapRouter));
serviceHost.Open();
Console.ReadLine();
}
}
Service Bindings
The HTTPS binding's configuration is applied to the outgoing TLS connection.
<routerBinding>
<binding name="HttpTextSoap12RouterBinding"
transport="Http"
messageEncoding="Text"
messageVersion="Soap12WSAddressing10"
closeTimeout="01:00:00"
openTimeout="01:00:00"
receiveTimeout="01:00:00"
sendTimeout="01:00:00">
</binding>
<binding name="HttpsTextSoap12RouterBinding"
transport="Https"
messageEncoding="Text"
messageVersion="Soap12WSAddressing10"
closeTimeout="01:00:00"
openTimeout="01:00:00"
receiveTimeout="01:00:00"
sendTimeout="01:00:00">
</binding>
</routerBinding>
Traffic
The places I have enclosed in {} have been modified, but no changes were made to the overall meaning.
Direct Connect
CONNECT {provider.com:443} HTTP/1.1
Host: {provider.com}
Proxy-Connection: Keep-Alive
Direct TLS Establishment
POST {https://provider.com/service.svc} HTTP/1.1
Content-Type: application/soap+xml; charset=utf-8
Host: {provider.com}
Content-Length: 4379
Expect: 100-continue
Connection: Keep-Alive
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT</a:Action>
<a:MessageID>urn:uuid:cfd9ec29-5e55-4154-8737-69f9b8b8bbb7</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1" u:Id="_1">{https://provider.com/service.svc}</a:To>
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<u:Timestamp u:Id="_0">
<u:Created>2011-03-04T14:06:27.993Z</u:Created>
<u:Expires>2011-03-04T14:11:27.993Z</u:Expires>
</u:Timestamp>
<o:BinarySecurityToken u:Id="uuid-526477b6-8ed4-4873-bba5-7997589cd63c-1" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"> {wFNcPIXogDSsEJYhXyu/H3NbS1oam7quaeDScz+MdDANBgkqhkiG9w0BAQUFADCBujEfMB0GA1UEChMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazEXMBUGA1UECxMOVmVyaVNpZ24sIEluYy4xMzAxBgNVBAsTKlZlcmlTaWduIEludGVybmF0aW9uYWwgU2VydmVyIENBIC0gQ2xhc3MgMzFJMEuIDPoxyyRVZOBT9HmFHglzaWduLmNvbS9DUFMgSW5jb3JwLmJ5IFJlZi4gTElBQklMSVRZIExURC4oYyk5NyBWZXJpU2lnbjAeFw0xMDA4MjYwMDAwMDBaFw0xMTA4MjYyMzU5NTlaMIGXMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExETAPBgNVBAcUCFJpY2htb25kMSswKQYDVQQKFCJBZmZpbGlhdGVkIENvbXB1dGVyIFNlcnZpY2VzLCBJbmMuMSQwIgYDVQQLFBtIZWFsdGggTWFuYWdlbWVudCBTb2x1dGlvbnMxDzANBgNVBAMUBkFDU0hJRTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArpBNMAnbIJ5MLafu2mYd8CJ1239FQ8LJlNfGwyVDkKHE8Quzdml28hIZ8XnLvUI0T9YWymf020SbqhwWKXt9eNH6IaRBZzIaT35NyAaUa/FA9rpCO/djt0z+wOukX75ZLmYtLHpskNtFfBtS9E+k2N8QEz7V+VJhHcWCVKESWBcCAwEAAaOCAa0wggGpMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9TVlJJbnRsLWNybC52ZXJpc2lnbi5jb20vU1ZSSW50bC5jcmwwRAYDVR0gBD0wOzA5BgtghkgBhvhFAQcXAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy52ZXJpc2lnbi5jb20vcnBhMCgGA1UdJQQhMB8GCWCGSAGG+EIEAQYIKwYBBQUHAwEGCCsGAQUFBwMCMHEGCCsGAQUFBwEBBGUwYzAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AudmVyaXNpZ24uY29tMDsGCCsGAQUFBzAChi9oaiEP2i8vU1ZSSW50bC1haWEudmVyaXNpZ24uY29tL1NWUkludGwtYWlhLmNlcjBuBggrBgEFBQcBDARiMGChXqBcMFowWDBWFglpbWFnZS9naWYwITAfMAcGBSsOAwIaBBRLa7kolgYMu9BSOJsprEsHiyEFGDAmFiRodHRwOi8vbG9nby52ZXJpc2lnbi5jb20vdnNsb2dvMS5naWYwDQYJKoZIhvcNAQEFCB03AYEAXx9ZBgH5iWAnU8eOh2tNS286TFIcnAMJRiR3lvNs+2bi8NNl4a3AUcPhcfy+ybSHiL0Qu7wbpSnZ67FIT2SDa+h3v1JYhAu9hUrkJF9UGOWe8QOVUZuhjt5Uzs+YyRcI30FPRBjvsEqrnO+5ckoKqFEJVwV3FKMyMF5/gvZZszo=}</o:BinarySecurityToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#_0">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>{aepZFE9EXqlXmuAf3RwcA6vXThQ=}</DigestValue>
</Reference>
<Reference URI="#_1">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>{G9/ctKlSyWbRU78aQSLZmEbgdPw=}</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>{JAGbae324PdpRWOaIzihZygSAQVm3CJfOWbP6gsc0UJAGbae324PmYyqYMsgIMuCAlSHIj4yrEfbEL2XHt/nWlBfF0FgfhyqgcsEhc5vHR4kSmS7uKEoOZg8iMSDTGgk86YN5Z+UdB9ysIwe7KpxqrPmJAGbae324PdW8E2GWzY=}</SignatureValue>
<KeyInfo>
<o:SecurityTokenReference>
<o:Reference ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" URI="#uuid-526477b6-8ed4-4873-bba5-7997589cd63c-1"/>
</o:SecurityTokenReference>
</KeyInfo>
</Signature>
</o:Security>
</s:Header>
<s:Body>
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:Entropy>
<t:BinarySecret u:Id="uuid-18de0e52-6a66-442a-8b18-41e4037b5139-1" Type="http://schemas.xmlsoap.org/ws/2005/02/trust/Nonce">{gjBI9ZhnJzJAGbae324P+APERNf9gqoJAGbae324PCA=}</t:BinarySecret>
</t:Entropy>
<t:KeySize>256</t:KeySize>
</t:RequestSecurityToken>
</s:Body>
</s:Envelope>
Client Post
POST http://localhost.:8000/services/soap12/text HTTP/1.1
Content-Type: application/soap+xml; charset=utf-8
Host: localhost.:8000
Content-Length: 1146
Expect: 100-continue
Connection: Keep-Alive
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
<a:MessageID>urn:uuid:1f5e02f6-ce41-4b66-a3a8-eb4014d5d1cb</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">{https://provider.com/service.svc}</a:To>
</s:Header>
<s:Body>
<t:RequestSecurityToken Context="uuid-ffc85f7f-3ffa-4bc7-9174-5ab16948ec78-1" xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:KeySize>256</t:KeySize>
<t:BinaryExchange ValueType=" http://schemas.xmlsoap.org/ws/2005/02/trust/tlsnego" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">{FgMBAEgBAABEAwFNcPIXogDSsEJYhXyu/adf4eFAe436TWvHqqv6ZN+FSQAAFgAEAAUACgAJAGbae324PYAEwASAGMBAAAF/wEAAQA=}</t:BinaryExchange>
</t:RequestSecurityToken>
</s:Body>
</s:Envelope>
Router Connect
CONNECT {provider.com:443} HTTP/1.1
Host: {provider.com}
Proxy-Connection: Keep-Alive
Router TLS Establishment
POST {https://provider.com/service.svc} HTTP/1.1
Content-Type: application/soap+xml; charset=utf-8
VsDebuggerCausalityData: uIDPoxyyRVZOBT9HmFHghQdaliEAAAAAAdkPtiI0y021hKG+IPwkNqyhfujS37tMnxoFJUL1/zoACQAA
Host: {provider.com}
Content-Length: 1146
Expect: 100-continue
Connection: Keep-Alive
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action>
<a:MessageID>urn:uuid:1f5e02f6-ce41-4b66-a3a8-eb4014d5d1cb</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">{https://provider.com/service.svc}</a:To>
</s:Header>
<s:Body>
<t:RequestSecurityToken Context="uuid-ffc85f7f-3ffa-4bc7-9174-5ab16948ec78-1" xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:KeySize>256</t:KeySize>
<t:BinaryExchange ValueType=" http://schemas.xmlsoap.org/ws/2005/02/trust/tlsnego" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">{FgMBAEgBAABEAMIIEhzCCA/CgAwIBAgIQadf4eFAe436TWvHqqv6ZN+FSQAAFgAEAAUACgAJAGbae324PYAEwASAGMBAAAF/wEAAQA=}</t:BinaryExchange>
</t:RequestSecurityToken>
</s:Body>
</s:Envelope>
Error
Secure channel cannot be opened because security negotiation with the remote endpoint has failed. This may be due to absent or incorrectly specified EndpointIdentity in the EndpointAddress used to create the channel. Please verify the EndpointIdentity specified or implied by the EndpointAddress correctly identifies the remote endpoint.
We have a very similar scenario, except for the fact that we are using the default basicHttpBindings rather than our custom ones and configuration. Never-the-less, its still protocol bridging from HTTP to HTTPS.
One thing that isn't obvious, is where your client configuration is. The client service is specifiying a Security Mode of Tansport and so should the router. Here is what our, albeit simple, router configuration looks like.
<services>
<service name="System.ServiceModel.Routing.RoutingService" behaviorConfiguration="RoutingServiceBehaviour">
<endpoint
address=""
binding="basicHttpBinding"
name="reqReplyEndpoint"
contract="System.ServiceModel.Routing.IRequestReplyRouter"/>
</service>
</services>
Our client config:
<client>
<endpoint
address="https://localhost/Service.svc"
binding="basicHttpBinding"
bindingConfiguration="SecureHttpBinding"
contract="*"
name="ServiceClientEndpoint"/>
</client>
The SecureHttpBinding is that specified by the client service
<bindings>
<basicHttpBinding>
<binding name="SecureHttpBinding">
<security mode="Transport">
<transport clientCredentialType="None"/>
</security>
</binding>
</basicHttpBinding>
</bindings>
Our routing table determines what service to invoke depending on the criteria set in the filters.
Also, if your using self-signed certificates in your development environment, make sure they are registered properly. If your using two different machines you will have to export the service certificate to the router service machine. In that instance, your self signed certs will need to be registered using the machine name rather than localhost.
HTH.
My goal is to reach a WCF service behind a firewall with no incoming ports opened.
The solution I chose is to host a duplex WCF service on the public side, that has as callback the same contract that was used if no firewall was involved.
It worked if I used netTcpBinding but since I need streamed communication I had to use the custom binding.
Everything works fine until I raise up the firewall. At that point, the direct call (from behind the firewall out) works fine, but the callback does not (firewall stops it).
The question is WHY? Shoudn't they use the same channel as for the predefined netTcpBinding?
Here is my channels stack in app.config:
<customBinding>
<binding name="ReversedServiceBinding">
<compositeDuplex />
<oneWay />
<binaryMessageEncoding />
<tcpTransport transferMode="Streamed" />
</binding>
</customBinding>
Actual behavior is correct. You have used compositeDuplex binding element - the meaning of composite word is exactly what you get - two separate channels each working for one direction. Composite duplex is needed only for transport channel which doesn't support duplex communication by default. That is not the case for TCP transport channel - check remarks. If you don't use compositeDuplex it should work like you have expected. Also there is no need to define whole new custom binding to allow streamed transport. NetTcpBinding also has TransportMode property which can be specified in configuration.
EDIT:
I have prepared working example with Net.TCP duplex communication. It doesn't use streaming. You are right that streaming is not possible when duplex TCP communication is used. You can try to combine duplex communication with chunking channel or check WCF Xtensions (commercial product).
Shared contracts
namespace NetTcpDuplexContracts
{
[ServiceContract(SessionMode = SessionMode.Required,
CallbackContract = typeof(IDuplexServiceCallback))]
public interface IDuplexService
{
[OperationContract(IsOneWay = true)]
void DoAction(string message);
}
public interface IDuplexServiceCallback
{
[OperationContract(IsOneWay = true)]
void ConfirmAction(string message);
}
}
Service and host
namespace NetTcpDuplexService
{
public class DuplexService : IDuplexService
{
public void DoAction(string message)
{
Console.WriteLine("DoAction: " + message);
var callbackChannel =
OperationContext.Current.GetCallbackChannel<IDuplexServiceCallback>();
callbackChannel.ConfirmAction("Ping back " + message);
}
}
class Program
{
public static void Main(string[] args)
{
try
{
using (var host = new ServiceHost(typeof(DuplexService)))
{
host.Open();
Console.ReadLine();
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Console.ReadLine();
}
}
}
}
Service configuration
<configuration>
<system.serviceModel>
<services>
<service name="NetTcpDuplexService.DuplexService">
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:8800/NetTcpDuplexService"/>
</baseAddresses>
</host>
<endpoint address="" binding="netTcpBinding" contract="NetTcpDuplexContracts.IDuplexService" />
</service>
</services>
</system.serviceModel>
</configuration>
Callback and client
namespace NetTcpDuplexClient
{
public class DuplexServiceCallback : IDuplexServiceCallback
{
public void ConfirmAction(string message)
{
Console.WriteLine(message);
}
}
class Program
{
public static void Main(string[] args)
{
DuplexChannelFactory<IDuplexService> factory = null;
try
{
var callbackService = new DuplexServiceCallback();
var context = new InstanceContext(callbackService);
factory = new DuplexChannelFactory<IDuplexService>(context, "IDuplexService_NetTcp");
var channel = factory.CreateChannel();
channel.DoAction("Hello world");
factory.Close();
Console.ReadLine();
}
catch (Exception e)
{
if (factory != null && factory.State != CommunicationState.Closed)
{
factory.Abort();
}
Console.WriteLine(e.Message);
Console.ReadLine();
}
}
}
}
Client configration
<configuration>
<system.serviceModel>
<client>
<endpoint name="IDuplexService_NetTcp" address="net.tcp://localhost:8800/NetTcpDuplexService" binding="netTcpBinding"
contract="NetTcpDuplexContracts.IDuplexService" />
</client>
</system.serviceModel>
</configuration>