Provide exceptions for wcf webhttpbinding - wcf

I have to change a binding for wcf webservices from tcpbinding to webhttpbinding with basic authentication and ssl.
Webservices are self hosted in a console application and in a windows service for production version. Some of local services are with named pipe binding, just if a service call another service.
All works perfectly but not the global error manager (a class that implement IErrorHandler interface)
Some of DAL or business methods throw an exception with a custom message and this message was correctly provide to client (unit test for a while). But since I change binding, exceptions caught in unit test are always a 500 error, internal server error and custom messages are not in exception object.
Server code :
// Création de l'URI
var baseAddress = new Uri($"https://localhost/blablabla/{typeof(TBusiness).Name}");
// Création du Host avec le type de la classe Business
var host = new ServiceHost(typeof(TBusiness), baseAddress);
// Liaison WebHttpBinding sécurité transport
var binding = new WebHttpBinding
{
MaxBufferSize = 2147483647,
MaxReceivedMessageSize = 2147483647,
Security = new WebHttpSecurity
{
Mode = WebHttpSecurityMode.Transport
},
};
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
// Permet de renvoyer du xml et du json
var webBehavior = new WebHttpBehavior
{
AutomaticFormatSelectionEnabled = true
};
var ep = host.AddServiceEndpoint(typeof(TContracts), binding, "");
ep.Behaviors.Add(webBehavior);
var sdb = host.Description.Behaviors.Find<ServiceDebugBehavior>();
sdb.HttpHelpPageEnabled = false;
// Activation https
var smb = new ServiceMetadataBehavior
{
HttpGetEnabled = false,
HttpsGetEnabled = true,
};
host.Description.Behaviors.Add(smb);
// Ajout de l'authentification
var customAuthenticationBehavior = new ServiceCredentials();
customAuthenticationBehavior.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
customAuthenticationBehavior.UserNameAuthentication.CustomUserNamePasswordValidator = new SessionAuthentication();
host.Description.Behaviors.Add(customAuthenticationBehavior);
// Démarrage du host
host.Open();
Business method that throw exception :
public TOUser GetUserByLogin(string login)
{
using (var service = new ServiceProviderNamedPipe<IBFSessionManager, BSSessionManager>())
{
// Récupération de la DALUsers
var dal = service.Channel.GetDALUsers(OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name);
var user = dal.GetUserByLogin(login);
if (user == null) throw new FaultException(Errors.DALUsers_Err001);
return BMToolsEntitiesToTO.UserToTOUser(user);
}
}
Error global manager :
public class GlobalErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
// Empèche la propagation de l'erreur
return true;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
var msg = error.Message;
// Création de l'exception de retour
var newEx = new FaultException(msg);
var msgFault = newEx.CreateMessageFault();
fault = Message.CreateMessage(version, msgFault, newEx.Action);
}
}
Unit test :
public void GetUserByLoginWithUnknownLoginTest()
{
TOUser user = null;
using (var service = new ServiceProviderHTTP<IBFUsers, BSUsers>(_user))
{
try
{
user = service.Channel.GetUserByLogin("1234");
}
catch (Exception e)
{
// e.message always provide "Internal server error instead of custom message (Errors.DALUsers_Err001)
Assert.AreEqual(Errors.DALUsers_Err001, e.Message);
}
Assert.IsNull(user);
}
}
All unit tests that catch exception failed since I change binding.
Thank you for your help.

I doubt whether your service is running correctly. Do you bind the certificate to the default port 443 due to transport layer security (using HTTPS)? Please use the below statement to bind a certificate to the 443 port.
netsh http add sslcert ipport=0.0.0.0:443 certhash=c20ed305ea705cc4e36b317af6ce35dc03cfb83d appid={c9670020-5288-47ea-70b3-5a13da258012}
please refer to this link.
https://learn.microsoft.com/en-us/windows/win32/http/add-sslcert
Here is a relevant discussion.
How to disable credentials input for HTTPS call to my WCF hosted in windows service
Besides, I didn’t see you apply the GlobalErrorHandler to the self-hosted service. This is usually implemented by service endpoint behavior.
ServiceEndpoint se = sh.AddServiceEndpoint(typeof(IService),new WebHttpBinding(), "");
MyEndpointBehavior bhv = new MyEndpointBehavior();
se.EndpointBehaviors.Add(bhv);
I wrote an example, wish it is useful to you.
class Program
{
static void Main(string[] args)
{
//I have already bound a certificate to the 21011 port.
var baseAddress = new Uri($"https://localhost:21011");
var host = new ServiceHost(typeof(MyService), baseAddress);
var binding = new WebHttpBinding
{
MaxBufferSize = 2147483647,
MaxReceivedMessageSize = 2147483647,
Security = new WebHttpSecurity
{
Mode = WebHttpSecurityMode.Transport
},
};
//basic authentication use windows login account located on the server-side instead of the below configuration(UserNamePasswordValidationMode.Custom)
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
// Permet de renvoyer du xml et du json
var webBehavior = new WebHttpBehavior
{
AutomaticFormatSelectionEnabled=true
};
var ep = host.AddServiceEndpoint(typeof(IService), binding, "");
ep.Behaviors.Add(webBehavior);
MyEndpointBehavior bhv = new MyEndpointBehavior();
ep.EndpointBehaviors.Add(bhv);
var sdb = host.Description.Behaviors.Find<ServiceDebugBehavior>();
sdb.HttpHelpPageEnabled = false;
// Activation https
var smb = new ServiceMetadataBehavior
{
HttpGetEnabled = true,
HttpsGetEnabled = true,
};
host.Description.Behaviors.Add(smb);
// Ajout de l'authentification
//var customAuthenticationBehavior = new ServiceCredentials();
//customAuthenticationBehavior.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
//customAuthenticationBehavior.UserNameAuthentication.CustomUserNamePasswordValidator = new SessionAuthentication();
//host.Description.Behaviors.Add(customAuthenticationBehavior);
// Démarrage du host
host.Open();
Console.WriteLine("service is running....");
Console.ReadLine();
Console.WriteLine("Closing.....");
host.Close();
}
}
[ServiceContract(ConfigurationName = "isv")]
public interface IService
{
[OperationContract]
[WebGet]
string Delete(int value);
}
[ServiceBehavior(ConfigurationName = "sv")]
public class MyService : IService
{
public string Delete(int value)
{
if (value <= 0)
{
throw new ArgumentException("Parameter should be greater than 0");
}
return "Hello";
}
}
public class MyError
{
public string Details { get; set; }
public string Error { get; set; }
}
public class MyCustomErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
return true;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
MyError myerror = new MyError()
{
Details = error.Message,
Error = "An error occured"
};
fault = Message.CreateMessage(version, "messsagefault", myerror);
}
}
public class MyEndpointBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
return;
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
return;
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
MyCustomErrorHandler myCustomErrorHandler = new MyCustomErrorHandler();
endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(myCustomErrorHandler);
}
public void Validate(ServiceEndpoint endpoint)
{
return;
}
}
Result.
Feel free to let me know if there is anything I can help with.

After several search, I saw that a lot of people have same problem.
Here is my solution :
On server side, always throw a WebFaultException like this with correct HTTP Status code :
throw new WebFaultException<string>(myStringMessage, HttpStatusCode.NotFound);
On client side (only for unit tests or MVC project), cast exception to call GetResponseStream on Response object to get custom message :
var err = (WebException)e;
using (Stream respStream = err.Response.GetResponseStream())
{
using (var reader = new StreamReader(respStream))
{
var serializer = new XmlSerializer(typeof(string));
var response = reader.ReadToEnd();
return response.Substring(response.IndexOf('>') + 1).Replace("</string>", "");
}
}
In ProvideFault method from IErrorHandler, I just add code to write errors in a file but not create a message with Message.CreateMessage method.
It works correctly but generate an EndPointNotFoundException after ProvideFault, in some other posts I saw that a ProtocolException could be thrown.
Thank you for your remarks.

Related

WCF: Notify Clent Server Connction in both sides (Service Self Hosted as console application)

I have a WCF service, self hosted on a console application. I can successfully connect and communicate.
I need to push some data to Client from server when a connection is established. How could I understand that a client has been connected and vice-versa (client knows when server connection established).
//Create a URI to serve as the base address
Uri httpUrl = new Uri("http://localhost:8090/TAOInventoryService/InventoryManager");
//Create ServiceHost
ServiceHost host = new ServiceHost(typeof(TAOInventoryService.InventoryManager), httpUrl);
//Add a service endpoint
host.AddServiceEndpoint(typeof(TAOInventoryService.IInventoryManager ), new WSHttpBinding(), "");
//Enable metadata exchange
ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
host.Description.Behaviors.Add(smb);
//Start the Service
host.Open();
Console.WriteLine("Service is host at " + DateTime.Now.ToString());
Console.WriteLine("Host is running... Press key to stop");
Console.ReadLine();
I might not fully understand your issue. In my opinion. It’s a bit like online and offline notifications. When the server is online, it can actively push an announcement message to the client.
Here is an example I ever wrote, wish it is useful to you.
Server.
static void Main(string[] args)
{
Uri uri = new Uri("http://localhost:21011");
BasicHttpBinding binding = new BasicHttpBinding();
binding.Security.Mode = BasicHttpSecurityMode.None;
using (ServiceHost sh = new ServiceHost(typeof(MyService), uri))
{
sh.AddServiceEndpoint(typeof(IService), binding, "");
ServiceMetadataBehavior smb;
smb = sh.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (smb == null)
{
smb = new ServiceMetadataBehavior()
{
HttpGetEnabled = true
};
sh.Description.Behaviors.Add(smb);
}
ServiceDiscoveryBehavior sdb = new ServiceDiscoveryBehavior();
sdb.AnnouncementEndpoints.Add(new UdpAnnouncementEndpoint());
sh.Description.Behaviors.Add(sdb);
sh.AddServiceEndpoint(new UdpDiscoveryEndpoint());
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 clsoed");
};
sh.Open();
Console.ReadLine();
//pause
sh.Close();
Console.ReadLine();
}
}
}
[ServiceContract]
public interface IService
{
[OperationContract]
Int32 Add(Int32 Num1, Int32 Num2);
}
[ServiceBehavior]
public class MyService : IService
{
public int Add(int Num1, int Num2)
{
return Num1 + Num2;
}
}
Client.
static void Main(string[] args)
{
Console.Title = "Start client first";
AnnouncementService annsvc = new AnnouncementService();
annsvc.OnlineAnnouncementReceived += OnlineRec;
annsvc.OfflineAnnouncementReceived += OfflineRec;
using (ServiceHost host = new ServiceHost(annsvc))
{
host.AddServiceEndpoint(new UdpAnnouncementEndpoint());
host.Open();
Console.Read();
}
}
private static void OfflineRec(object sender, AnnouncementEventArgs e)
{
Console.WriteLine($"\nService is offline, service address:{e.EndpointDiscoveryMetadata.Address.Uri}");
}
private static void OnlineRec(object sender, AnnouncementEventArgs e)
{
Console.WriteLine($"\nService is online, service address:{e.EndpointDiscoveryMetadata.Address.Uri}");
}
Result.
Official document.
https://learn.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/dd483353(v=vs.100)
Feel free to let me know if there is anything I can help with.

How to use SoapCore in Asp.net Core project for exposing wsdl at project route folder

Your project, written with Asp.net Core, makes deploying Rest API. However, your customer wanted to communicate with soap. How to make an improvement
SoapCore has already done many things for us to support this situation. We apply step-by-step changes to our Asp.net Core project.
First in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
try
{
services.AddSoapServiceOperationTuner(new MyServiceOperationTuner());
services.Configure<XApiSettings>(options =>
{
options.baseurl = XApiOptions[nameof(XApi.baseurl)];
options.accesstokenroute = XApiOptions[nameof(XApi.accesstokenroute)];
options.secret = XApiOptions[nameof(XApi.secret)];
options.secretkey = XApiOptions[nameof(XApi.secretkey)];
options.grant_type = XApiOptions[nameof(XApi.grant_type)];
options.username = XApiOptions[nameof(XApi.username)];
options.password = XApiOptions[nameof(XApi.password)];
});
services.AddSoapCore();
services.AddSingleton<IRESAdapterService>(new RESAdapterService(
Xcontroller: new XApiController(
services.BuildServiceProvider().GetRequiredService<IOptions<XApi>>(),
_corendonLogger
)));
services.AddSoapExceptionTransformer((ex) => ex.Message);
}
catch (Exception ex)
{
Log.Logger.Error("ConfigureServices Message: " + ex.Message);
}
}
If you want your application to be accessible from the root directory at the address you deploy, you can type path '/' directly or name it as '/ XX'
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, ApplicationContext dbContext)
{
try
{
app.UseSoapEndpoint<IRESAdapterService>(path: "/", binding: new BasicHttpBinding(), SoapSerializer.XmlSerializer);
}
}
In the case of requests that are handled on the server side, data sent as xml will normally be null. We need to make the following improvement in SoapCore so that the server can resolve the request.
public class MyServiceOperationTuner : IServiceOperationTuner
{
public void Tune(HttpContext httpContext, object serviceInstance, SoapCore.OperationDescription operation)
{
RESAdapterService service = serviceInstance as RESAdapterService;
service.SetHttpRequest = httpContext.Request;
}
}
In addition, the interface to meet the incoming requests and services to make redirects to our controller should write as follows
[ServiceContract]
public interface IRESAdapterService
{
[OperationContract]
[XmlSerializerFormat(SupportFaults = true)]
Task<OTA_AirAvailRS> getAvailability([FromBody]HttpRequestMessage req);
[OperationContract]
Task<OTA_AirPriceRS> pricing([FromBody]HttpRequestMessage req);
}
public class RESAdapterService : IRESAdapterService
{
XApiController _controller;
public HttpRequest SetHttpRequest { get; set; }
public RESAdapterService(XApiController XApi)
{
_controller = XApi;
}
public Task<MyRequesterClass> Method1([FromBody]HttpRequestMessage req)
{
return _controller.Method1(SetHttpRequest);
}
public Task<MyDiffRequesterClass> Method2([FromBody]HttpRequestMessage req)
{
return _controller. Method2(SetHttpRequest);
}
}
The controller was catching requests from the Request object, but; now the Request object has to get through the router service for the future of null in this context. Therefore we can implement the code that reads XML as follows
Stream reqBody = Request?.Body;
if (Request == null)
reqBody = (MyRequesterClass as HttpRequest).Body;
Let's come to the client side, write a simple framework console project
Normally we offer wsdl visual studio add references by adding the portion of the proxy can create and walk. (Recommended case) But in my case I decided to post xml with webclient because I use a user certificate and I don't know the type of object to send. Sample use below:
static string xml = " <ReqObj xmlns='http://tempuri.org/'>"
+ "</ ReqObj >";
static string wrapbody = #"<?xml version=""1.0"" encoding=""utf-8""?>
<soap:Envelope xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/""
xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
<soap:Body>
#
</soap:Body>
</soap:Envelope>";
public static async Task<string> CreateSoapEnvelope()
{
HttpResponseMessage response = await PostXmlRequest("https://localhostorliveaddress.com");
string content = await response.Content.ReadAsStringAsync();
return content;
}
public static async Task<HttpResponseMessage> PostXmlRequest(string baseUrl)
{
X509Certificate2 clientCert = new X509Certificate2(#"D:\ccer\xxx.pfx", "password");
//X509Certificate2 clientCert = GetClientCertificate();
WebRequestHandler requestHandler = new WebRequestHandler();
requestHandler.ClientCertificates.Add(clientCert);
using (var httpClient = new HttpClient(requestHandler))
{
string wrpXmlContent = wrapbody.Replace("#", xml);
var httpContent = new StringContent(wrpXmlContent, Encoding.UTF8, "text/xml");
httpContent.Headers.Add("SOAPAction", "https://localhostorliveaddress.com/method1");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml"));
return await httpClient.PostAsync(baseUrl, httpContent);
}
}
Getting Client certificate from My User Personel Store
private static X509Certificate2 GetClientCertificate()
{
X509Store userCaStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
userCaStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificatesInStore = userCaStore.Certificates;
X509Certificate2Collection findResult = certificatesInStore.
Find(X509FindType.FindBySubjectName, "XRootCertificateOnMyUserPersonelStore", true);
X509Certificate2 clientCertificate = null;
if (findResult.Count == 1)
{
clientCertificate = findResult[0];
}
else
{
throw new Exception("Unable to locate the correct client certificate.");
}
return clientCertificate;
}
catch
{
throw;
}
finally
{
userCaStore.Close();
}
}

WCF REST Self-Hosted 400 Bad Request

I'm having a problem with a self-host WCF REST service.
When I try to issue a GET via browser or Fiddler, I get a 400 Bad Request. Tracing is reporting an inner exception of XmlException "The body of the message cannot be read because it is empty."
I don't have any configuration in app.config (do I need any?). I have tried changing WebServiceHost to ServiceHost, and WSDL is returned, but the operations still return 400.
What am I missing here?
// Add Reference to System.ServiceModel and System.ServiceModel.Web
using System;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
namespace WCFRESTTest
{
class Program
{
static void Main(string[] args)
{
var baseAddress = new Uri("http://localhost:8000/");
var host = new WebServiceHost(typeof(RestService), baseAddress);
try
{
host.AddServiceEndpoint(typeof(IRestService), new WSHttpBinding(), "RestService");
var smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
host.Description.Behaviors.Add(smb);
host.Open();
Console.WriteLine("Service Running. Press any key to stop.");
Console.ReadKey();
}
catch(CommunicationException ce)
{
host.Abort();
throw;
}
}
}
[ServiceContract]
public interface IRestService
{
[OperationContract]
[WebGet(UriTemplate = "Test")]
bool Test();
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class RestService : IRestService
{
public bool Test()
{
Debug.WriteLine("Test Called.");
return true;
}
}
}
When you use the WebServiceHost, you typically don't need to add a service endpoint - it will add one with all behaviors required to make it a "Web HTTP" (a.k.a. REST) endpoint (i.e., an endpoint which doesn't use SOAP and you can easily consume with a tool such as Fiddler, which seems to be what you want). Also, Web HTTP endpoints aren't exposed in the WSDL, so you don't need to add the ServiceMetadataBehavior either.
Now for why it doesn't work - sending a GET request to http://localhost:8000/Test should work - and in the code below it does. Try running this code, and sending the request you were sending before with Fiddler, to see the difference. That should point out what the issue you have.
public class StackOverflow_15705744
{
[ServiceContract]
public interface IRestService
{
[OperationContract]
[WebGet(UriTemplate = "Test")]
bool Test();
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class RestService : IRestService
{
public bool Test()
{
Debug.WriteLine("Test Called.");
return true;
}
}
public static void Test()
{
var baseAddress = new Uri("http://localhost:8000/");
var host = new WebServiceHost(typeof(RestService), baseAddress);
// host.AddServiceEndpoint(typeof(IRestService), new WSHttpBinding(), "RestService");
// var smb = new ServiceMetadataBehavior();
// smb.HttpGetEnabled = true;
// host.Description.Behaviors.Add(smb);
host.Open();
WebClient c = new WebClient();
Console.WriteLine(c.DownloadString(baseAddress.ToString().TrimEnd('/') + "/Test"));
Console.WriteLine("Service Running. Press any key to stop.");
Console.ReadKey();
}
}

Test WCF service and client

I am trying to write an integration test that runs the service and then connects a client to this service.
ConnectClientToTestService() throws error:
System.ServiceModel.Security.SecurityNegotiationException: 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. --->
System.ServiceModel.FaultException: The request for security token has
invalid or malformed elements.
Can you do this in the same exe? There are certificates involved which have been installed on my machine, but these also might be the issue.
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.ServiceModel;
using ECS.Services;
using ECS.App.Core.ECSDataService;
using System.ServiceModel.Description;
namespace ECS.Test.ClientSide
{
[TestClass]
public class TestValidUserIntegrationTest
{
private static TestContext context;
[ClassInitialize()]
public static void ClassInitialize(TestContext testContext)
{
context = testContext;
ResourcingServiceHost.StartService();
}
/// <summary>
/// Shut down the WCF service once all tests have been run
/// </summary>
[ClassCleanup()]
public static void MyClassCleanup()
{
ResourcingServiceHost.StopService();
}
//Point the client at the test ResourceingServiceHost service
[TestMethod]
public void ConnectClientToTestService()
{
WSHttpBinding myBinding = new WSHttpBinding();
EndpointAddress myEndpoint = new EndpointAddress("http://localhost:8733/ECS.Services/DataService/");
var factory = new ChannelFactory<ECS.App.Core.ECSDataService.IDataService>("debug", new EndpointAddress("http://localhost:8733/ECS.Services/DataService/"));//new ChannelFactory<ECS.App.Core.ECSDataService.IDataService>(myBinding, myEndpoint);//
{
ClientCredentials clientCredentials = new ClientCredentials();
clientCredentials.UserName.UserName = "admin";
clientCredentials.UserName.Password = "a";
factory.Endpoint.Behaviors.RemoveAll<ClientCredentials>();
factory.Endpoint.Behaviors.Add(clientCredentials);
ECS.App.Core.ECSDataService.IDataService client = factory.CreateChannel();
using (Channel.AsDisposable(client))
{
client.GetConnectionStrings();
}
}
}
}
internal class ResourcingServiceHost
{
internal static ServiceHost Instance = null;
internal static void StartService()
{
Instance = new ServiceHost(typeof(DataService));
WSHttpBinding wsBinding = new WSHttpBinding();
wsBinding.Security.Mode = SecurityMode.Message;
wsBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;
Instance.AddServiceEndpoint(typeof(ECS.Services.IDataService), wsBinding, "http://localhost:8733/ECS.Services/DataService/");
Instance.Open();
}
internal static void StopService()
{
if (Instance.State != CommunicationState.Closed)
{
Instance.Close();
}
}
}
//This allows us to see the inner exceptions from the WCF service
public class Channel : IDisposable
{
private ICommunicationObject _channel;
private Channel(ICommunicationObject channel)
{
_channel = channel;
}
public static IDisposable AsDisposable(object client)
{
return new Channel((ICommunicationObject)client);
}
public void Dispose()
{
bool success = false;
try
{
if (_channel.State != CommunicationState.Faulted)
{
_channel.Close(); success = true;
}
}
finally
{
if (!success)
{
_channel.Abort();
}
}
}
}
}

How to publish and subscribe to Message class messages using WCF

I have a small WCF pub/sup service running, and remote clients subscribe and send messages (tried will all sorts of complex objects) and that works fine. All interfaces reflect(ed) the type of object being used. Switching to another object type requires that the interfaces be adjusted to accommodate that object type. All subscribers get a copy of the message.
Now I am trying to do the same thing, but with Message class messages. Client creates a new message and encapsulates its object in the message, and sends it to the (remote) service, where it is received properly (inspected the object). However, when the server replies by resending (callback) the message back to the originating client, the client receives the following message:
“The server did not provide a meaningful reply; this might be caused by a contract mismatch, a premature session shutdown or an internal server error.”
Sequence of events (Client):
Client creates Message,
(DuplexChannelFactory)AddMessage,
-Catch above error
Sequence of events (Server):
Service host receives message,
Message inspected (copy and recreate),
Perform callback,
No errors.
Switching back to a basic, or user defined, type and all problems go away. I have been struggling with this for a week now and not closer to any solution. Tried manipulating headers, recreating the message, switching to Message Contracts, and trying to interpret the contents of the trace logs etc. Hope I will find some answers here.
Primary code used (stripped of most of the error handling):
Client interfaces:
namespace WCFSQL
{
public class ClientInterfaces
{
[ServiceContract(Namespace = "WCFServer", Name = "CallBacks")]
public interface IMessageCallback
{
[OperationContract(Name = "OnMessageAdded", Action = "WCFServer/IMessageCallback/OnMessageAdded", IsOneWay = true)]
void OnMessageAdded(Message SQLMessage, DateTime timestamp);
}
[ServiceContract(Namespace = "WCFServer", CallbackContract = typeof(IMessageCallback))]
public interface IMessage
{
[OperationContract(Name = "AddMessage", Action = "WCFServer/IMessage/AddMessage")]
void AddMessage(Message SQLMessage);
[OperationContract(Name = "Subscribe", Action = "WCFServer/IMessage/Subscribe")]
bool Subscribe();
[OperationContract(Name = "Unsubscribe", Action = "WCFServer/IMessage/Unsubscribe")]
bool Unsubscribe();
}
}
}
Server interfaces:
namespace WCFSQL
{
public class ServerInterfaces
{
[ServiceContract(Namespace = "WCFServer")]
public interface IMessageCallback
{
[OperationContract(Name = "OnMessageAdded", Action = "WCFServer/IMessageCallback/OnMessageAdded", IsOneWay = true)]
void OnMessageAdded(Message SQLMessage, DateTime timestamp);
}
[ServiceContract(Namespace = "WCFServer", CallbackContract = typeof(IMessageCallback), SessionMode = SessionMode.Required)]
public interface IMessage
{
[OperationContract(Name = "AddMessage", Action = "WCFServer/IMessage/AddMessage")]
void AddMessage(Message SQLMessage);
[OperationContract(Name = "Subscribe", Action = "WCFServer/IMessage/Subscribe")]
bool Subscribe();
[OperationContract(Name = "Unsubscribe", Action = "WCFServer/IMessage/Unsubscribe")]
bool Unsubscribe();
}
}
}
Message creation:
// client proxy instance created and opened before
public static bool WCFSqlLogger(string Program, WCFSQLErrorLogMessage SQLErrorMessage, WCFSqlClientProxy client)
{
MessageVersion ver = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.WSAddressing10);
Message Out = Message.CreateMessage(ver, "WCFServer/IMessage/AddMessage", SQLErrorMessage);
if (!client.SendMessage(Out))
{
Console.WriteLine("Client Main: Unable to send");
return false;
}
return true;
}
Client proxy:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, IncludeExceptionDetailInFaults = true)]
[CallbackBehavior(IncludeExceptionDetailInFaults = true, ConcurrencyMode = ConcurrencyMode.Single, UseSynchronizationContext = false)]
public class WCFSqlClientProxy : ClientInterfaces.IMessageCallback, IDisposable
{
public ClientInterfaces.IMessage pipeProxy = null;
DuplexChannelFactory<ClientInterfaces.IMessage> pipeFactory;
public bool Connect()
{
NetTcpBinding newBinding = new NetTcpBinding(SecurityMode.TransportWithMessageCredential);// NetTcpBinding newBinding = new NetTcpBinding(SecurityMode.Transport)
newBinding.Security.Message.ClientCredentialType = MessageCredentialType.Certificate;
EndpointAddress newEndpoint = new EndpointAddress(new Uri("net.tcp://host:8000/ISubscribe"), EndpointIdentity.CreateDnsIdentity("Domain"));
pipeFactory = new DuplexChannelFactory<ClientInterfaces.IMessage>(new InstanceContext(this), newBinding, newEndpoint);
pipeFactory.Credentials.Peer.PeerAuthentication.CertificateValidationMode = X509CertificateValidationMode.PeerOrChainTrust;
pipeFactory.Credentials.ServiceCertificate.Authentication.RevocationMode = X509RevocationMode.NoCheck;
pipeFactory.Credentials.ClientCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.TrustedPeople, X509FindType.FindByThumbprint, "somestring");
try
{
pipeProxy = pipeFactory.CreateChannel();
pipeProxy.Subscribe();
return true;
}
catch (Exception e)
{
Console.WriteLine("Error opening: {0}", e.Message);
return false;
}
}
public void Close()
{
pipeProxy.Unsubscribe();
}
public bool SendMessage(Message SQLMessage)
{
try
{
Console.WriteLine("Proxy Sending:");
pipeProxy.AddMessage(SQLMessage); // This is where the eror occurs !!!!!!!!!!!!!!!!!!
return true;
}
catch (Exception e)
{
Console.WriteLine("Client Proxy: Error sending: {0}", e.Message);
}
return false;
}
public void OnMessageAdded(Message SQLMessage, DateTime timestamp)
{
WCFSQLErrorLogMessage message = SQLMessage.GetBody<WCFSQLErrorLogMessage>();
Console.WriteLine(message.LogProgram + ": " + timestamp.ToString("hh:mm:ss"));
}
public void Dispose()
{
Console.WriteLine("Dispose: Unsubscribe");
pipeProxy.Unsubscribe();
}
}
Service:
namespace WCFSQL
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, IncludeExceptionDetailInFaults = true)]
[CallbackBehavior(IncludeExceptionDetailInFaults = true, ConcurrencyMode = ConcurrencyMode.Single, UseSynchronizationContext = false)] // or ConcurrencyMode.Reentrant
public class WCFSqlServerProxy : ServerInterfaces.IMessage
{
private static List<ServerInterfaces.IMessageCallback> subscribers = new List<ServerInterfaces.IMessageCallback>();
private static Uri target;
private static ServiceHost serviceHost;
public WCFSqlServerProxy(Uri Target) // Singleton
{
target = Target;
}
public bool Connect()
{
serviceHost = new ServiceHost(typeof(WCFSqlServerProxy), target);
NetTcpBinding newBinding = new NetTcpBinding(SecurityMode.TransportWithMessageCredential);
newBinding.Security.Transport.ClientCredentialType = TcpClientCredentialType.Certificate;
newBinding.Security.Message.ClientCredentialType = MessageCredentialType.Certificate;
serviceHost.Credentials.ClientCertificate.Authentication.RevocationMode = X509RevocationMode.NoCheck; // Non-domain members cannot follow the chain?
serviceHost.Credentials.ServiceCertificate.SetCertificate(StoreLocation.LocalMachine, StoreName.TrustedPeople, X509FindType.FindByThumbprint, "somestring");
serviceHost.Credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.PeerOrChainTrust;
serviceHost.AddServiceEndpoint(typeof(ServerInterfaces.IMessage), newBinding, "ISubscribe");
return true;
}
public bool Open()
{
serviceHost.Open();
return true;
}
public bool Close()
{
serviceHost.Close();
return true;
}
public bool Subscribe()
{
try
{
ServerInterfaces.IMessageCallback callback = OperationContext.Current.GetCallbackChannel<ServerInterfaces.IMessageCallback>();
if (!subscribers.Contains(callback))
{
subscribers.Add(callback);
return true;
}
else
{
return false;
}
}
catch (Exception e)
{
return false;
}
}
public bool Unsubscribe()
{
try
{
ServerInterfaces.IMessageCallback callback = OperationContext.Current.GetCallbackChannel<ServerInterfaces.IMessageCallback>();
if (subscribers.Contains(callback))
{
subscribers.Remove(callback);
return true;
}
return false;
}
catch (Exception e)
{
Console.WriteLine("WCFSqlServerProxy: Unsubscribe - Unsubscribe error {0}", e);
return false;
}
}
private string GetData()
{
MessageProperties messageProperties = ((OperationContext)OperationContext.Current).IncomingMessageProperties;
RemoteEndpointMessageProperty endpointProperty = messageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
string computerName = null;
try
{
string[] computer_name = Dns.GetHostEntry(endpointProperty.Address).HostName.Split(new Char[] { '.' });
computerName = computer_name[0].ToString();
}
catch (Exception e)
{
computerName = "NOTFOUND";
Console.WriteLine("WCFSqlServerProxy: Hostname error: {0}", e);
}
return string.Format("{0} - {1}:{2}", computerName, endpointProperty.Address, endpointProperty.Port);
}
public void AddMessage(Message SQLMessage) //Go through the list of connections and call their callback funciton
{
subscribers.ForEach(delegate(ServerInterfaces.IMessageCallback callback)
{
if (((ICommunicationObject)callback).State == CommunicationState.Opened)
{
MessageVersion ver = MessageVersion.CreateVersion(EnvelopeVersion.Soap12, AddressingVersion.WSAddressing10);
MessageBuffer buffer = SQLMessage.CreateBufferedCopy(4096);
Message msgCopy = buffer.CreateMessage();
//System.Xml.XmlDictionaryReader xrdr = msgCopy.GetReaderAtBodyContents();
WCFSQLErrorLogMessage p = msgCopy.GetBody<WCFSQLErrorLogMessage>();
SQLMessage = buffer.CreateMessage();
buffer.Close();
Message In = Message.CreateMessage(ver, "WCFServer/IMessage/AddMessage", p); // Tried recreating messsage, with same results
//Console.WriteLine("Message: Header To: {0}", In.Headers.To);
//Console.WriteLine("Message: Header From: {0}", In.Headers.From);
//Console.WriteLine("Message: Header Action: {0}", In.Headers.Action);
//Console.WriteLine("Message: Header ReplyTo: {0}", In.Headers.ReplyTo);
//Console.WriteLine("Message: IsFault: {0}", In.IsFault);
//Console.WriteLine("Message: Properties {0}", In.Properties);
//Console.WriteLine("Message: State {0}", In.State);
//Console.WriteLine("Message: Type {0}", In.GetType());
//Console.WriteLine("Proxy Sending: Copy created");
//Console.WriteLine("Remote: {0}, Hash: {1}", GetData(), callback.GetHashCode());
callback.OnMessageAdded(SQLMessage, DateTime.Now); // This should echo the message back with a timeslot.
}
else
{
Console.WriteLine("WCFSqlServerProxy:addmessage connected state: {0}", ((ICommunicationObject)callback).State == CommunicationState.Opened);
subscribers.Remove(callback);
}
});
}
}
I just got an answer to my question from Tanvir Huda, on the microsoft WCF forum.
"Using the Message Class in Operations
You can use the Message class as an input parameter of an operation, the return value of an operation, or both. If Message is used anywhere in an operation, the following restrictions apply:
•The operation cannot have anyoutorrefparameters.
•There cannot be more than oneinputparameter. If the parameter is present, it must be either Message or a message contract type.
•The return type must be either void,Message, or a message contract type."
I cannot beleive I missed that; must have read it at least three times, but never applied those rules to the callback. The callback in my interfaces described, do have a return type of void, but it has a Message and a DateTime parameter.
After removing the DateTime parameter, the callback did (try) to re-serialize the original Message, but failed because of on invalid action (action was still set for the AddMessage, while now it should be OnMessageAdded). After changing the action on the callback to Action="*" it workt perfectly. A (maybe annoying) detail it that i do not really require a Message type on the callback, but I was very frustrated that I did not get it to work