Saturday, April 10, 2010

WCF 4.0 Service Discovery - Adding the binding to the discovery

With WCF 4.0 a wonderful new feature is added. Service Discovery allows us to see what services are running and where they are. However, when I started to look into this in detail, I noticed the information you get on services only contains the A (Address) and C (Contract) of the ABC, The Address and the Contract - but not the binding. So in this article I will show how to add the binding to the discovery information with little ease.

Note: All the code is developed on Visual Studio 2010 RC

EndpointDiscoveryMetadata



Within the discovery this class is probably one of the most important ones. This is the information we get when services go up and down. Below you can see that there is no binding information about the service.

Notice you can get the contact names, using:
Collection ContractTypeNames { get; }

and you can get the address using:
public EndpointAddress Address { get; set; }

But, no binding information!

When I posted on the forum asking why was it done this way, they told me that the binding are not part of the discovery standard (WS Discovery), they also told me that you can get the binding without problems by simply discovering the Mex endpoint and then issuing a call to get the metadata of the service. But, that is not ideal, you would need to discovery the service, and then get its metadata just to know the full ABC of an endpoint (2 network trips).
(http://social.msdn.microsoft.com/Forums/en/wcfprerelease/thread/ca6bc4be-bc46-4740-ba1e-dca8cf39aa5f)

One key field that EndpointDiscoveryMetadata has is Extensions. So right away I decided to see how I can fill up these extensions and add my own custom information in there. In this case it would be binding information.

The code


The example below is based on the WCF / WF code examples from Microsoft. The code has been modified to add binding information to the discovery metadata. I have used the Service Proxy example for this prototype.

You can get the Microsoft sample code here: http://www.microsoft.com/downloads/details.aspx?FamilyID=35ec8682-d5fd-4bc3-a51a-d8ad115a8792&displaylang=en


Adding Extensions to the EndpointDiscoveryMetadata



To add an extension to the Discovery Metadata, you need to create a new Endpoint Behavior and add it to the Endpoint.Behaviors collection. The behavior is a special behavior that belongs to the Discovery sub-system, it is called EndpointDiscoveryBehavior. Once you create this behavior you can add extensions to it using the Extension collection.
endpointDiscoveryBehavior.Extensions.Add

To make sure that the binding information is attached to the discovery metadata of each endpoint, the best approach was to create a new ServiceBehavior, and add a EndpointDiscoveryBehavior to each endpoint.

Below is the code of the Service Behavior:


public class BindingDiscoveryServiceBehavior : Attribute, IServiceBehavior
{
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection endpoints, BindingParameterCollection bindingParameters)
{

}

public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
var endpoints = serviceDescription.Endpoints;

foreach (ServiceEndpoint endpoint in endpoints)
{
var endpointDiscoveryBehavior = new EndpointDiscoveryBehavior();

StringBuilder sb = new StringBuilder();

sb.Append(endpoint.Address);
sb.Append(Environment.NewLine);
sb.Append(endpoint.Binding.Scheme);
sb.Append(Environment.NewLine);
sb.Append(endpoint.Binding.Name);

string bindingInfo = sb.ToString();
string largeData = String.Empty;

StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < 3000000; i++)
sb2.Append("Lots of data " + i.ToString() + Environment.NewLine);

largeData = sb2.ToString();

// add the binding information to the endpoint
endpointDiscoveryBehavior.Extensions.Add(
new XElement(
"root",
new XElement("BindingData", bindingInfo),
new XElement("LargeData", largeData)));

// add the extension
endpoint.Behaviors.Add(endpointDiscoveryBehavior);

}
}

public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{

}
}


Note that I have also added a very large data to the extension, just to show that you can pass large amount of data in the Extensions. (but you need to increase the limits on the discovery proxy TCP binding).


The next step is simple, you just need to add your Service Behavior to your service. You can do this by configuration, or simply applying it as an attribute on the service:


[BindingDiscoveryServiceBehavior]
public class CalculatorService : ICalculatorService



Getting the information when a service is "Discovered"


To see the binding information when a service is discovered, I have modified the Service Proxy from the WCF discovery example:

When a service is discovered, the OnBeginOnlineAnnouncement is called..

Below is the modified implementation:


[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class DiscoveryProxyService : DiscoveryProxy
{
// Repository to store EndpointDiscoveryMetadata. A database or a flat file could also be used instead.
Dictionary onlineServices;

public DiscoveryProxyService()
{
this.onlineServices = new Dictionary();
}

// OnBeginOnlineAnnouncement method is called when a Hello message is received by the Proxy
protected override IAsyncResult OnBeginOnlineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
{
this.AddOnlineService(endpointDiscoveryMetadata);
return new OnOnlineAnnouncementAsyncResult(callback, state);
}

protected override void OnEndOnlineAnnouncement(IAsyncResult result)
{
OnOnlineAnnouncementAsyncResult.End(result);
}

// OnBeginOfflineAnnouncement method is called when a Bye message is received by the Proxy
protected override IAsyncResult OnBeginOfflineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
{
this.RemoveOnlineService(endpointDiscoveryMetadata);
return new OnOfflineAnnouncementAsyncResult(callback, state);
}

protected override void OnEndOfflineAnnouncement(IAsyncResult result)
{
OnOfflineAnnouncementAsyncResult.End(result);
}

// OnBeginFind method is called when a Probe request message is received by the Proxy
protected override IAsyncResult OnBeginFind(FindRequestContext findRequestContext, AsyncCallback callback, object state)
{
this.MatchFromOnlineService(findRequestContext);
return new OnFindAsyncResult(callback, state);
}

protected override void OnEndFind(IAsyncResult result)
{
OnFindAsyncResult.End(result);
}

// OnBeginFind method is called when a Resolve request message is received by the Proxy
protected override IAsyncResult OnBeginResolve(ResolveCriteria resolveCriteria, AsyncCallback callback, object state)
{
return new OnResolveAsyncResult(this.MatchFromOnlineService(resolveCriteria), callback, state);
}

protected override EndpointDiscoveryMetadata OnEndResolve(IAsyncResult result)
{
return OnResolveAsyncResult.End(result);
}

// The following are helper methods required by the Proxy implementation
void AddOnlineService(EndpointDiscoveryMetadata endpointDiscoveryMetadata)
{
lock (this.onlineServices)
{
this.onlineServices[endpointDiscoveryMetadata.Address] = endpointDiscoveryMetadata;
}

PrintDiscoveryMetadata(endpointDiscoveryMetadata, "Adding");
// show the binding information
PrintBindingInformation(endpointDiscoveryMetadata);
}

private void PrintBindingInformation(EndpointDiscoveryMetadata endpointDiscoveryMetadata)
{
// Get the binding data
XElement element = endpointDiscoveryMetadata.Extensions.Elements("BindingData").FirstOrDefault();
string bindingInfo = element.Value;

Console.WriteLine("Binding Data");
Console.WriteLine(bindingInfo);
}

void RemoveOnlineService(EndpointDiscoveryMetadata endpointDiscoveryMetadata)
{
if (endpointDiscoveryMetadata != null)
{
lock (this.onlineServices)
{
this.onlineServices.Remove(endpointDiscoveryMetadata.Address);
}

PrintDiscoveryMetadata(endpointDiscoveryMetadata, "Removing");
}
}

void MatchFromOnlineService(FindRequestContext findRequestContext)
{
lock (this.onlineServices)
{
foreach (EndpointDiscoveryMetadata endpointDiscoveryMetadata in this.onlineServices.Values)
{
if (findRequestContext.Criteria.IsMatch(endpointDiscoveryMetadata))
{
findRequestContext.AddMatchingEndpoint(endpointDiscoveryMetadata);
}
}
}
}

EndpointDiscoveryMetadata MatchFromOnlineService(ResolveCriteria criteria)
{
EndpointDiscoveryMetadata matchingEndpoint = null;
lock (this.onlineServices)
{
foreach (EndpointDiscoveryMetadata endpointDiscoveryMetadata in this.onlineServices.Values)
{
if (criteria.Address == endpointDiscoveryMetadata.Address)
{
matchingEndpoint = endpointDiscoveryMetadata;
}
}
}
return matchingEndpoint;
}

void PrintDiscoveryMetadata(EndpointDiscoveryMetadata endpointDiscoveryMetadata, string verb)
{
Console.WriteLine("\n**** " + verb + " service of the following type from cache. ");
foreach (XmlQualifiedName contractName in endpointDiscoveryMetadata.ContractTypeNames)
{
Console.WriteLine("** " + contractName.ToString());
break;
}
Console.WriteLine("**** Operation Completed");
}

sealed class OnOnlineAnnouncementAsyncResult : AsyncResult
{
public OnOnlineAnnouncementAsyncResult(AsyncCallback callback, object state)
: base(callback, state)
{
this.Complete(true);
}

public static void End(IAsyncResult result)
{
AsyncResult.End(result);
}
}

sealed class OnOfflineAnnouncementAsyncResult : AsyncResult
{
public OnOfflineAnnouncementAsyncResult(AsyncCallback callback, object state)
: base(callback, state)
{
this.Complete(true);
}

public static void End(IAsyncResult result)
{
AsyncResult.End(result);
}
}

sealed class OnFindAsyncResult : AsyncResult
{
public OnFindAsyncResult(AsyncCallback callback, object state)
: base(callback, state)
{
this.Complete(true);
}

public static void End(IAsyncResult result)
{
AsyncResult.End(result);
}
}

sealed class OnResolveAsyncResult : AsyncResult
{
EndpointDiscoveryMetadata matchingEndpoint;

public OnResolveAsyncResult(EndpointDiscoveryMetadata matchingEndpoint, AsyncCallback callback, object state)
: base(callback, state)
{
this.matchingEndpoint = matchingEndpoint;
this.Complete(true);
}

public static EndpointDiscoveryMetadata End(IAsyncResult result)
{
OnResolveAsyncResult thisPtr = AsyncResult.End(result);
return thisPtr.matchingEndpoint;
}
}
}


Notice that I am getting the binding information in the code below:
XElement element = endpointDiscoveryMetadata.Extensions.Elements("BindingData").FirstOrDefault();
string bindingInfo = element.Value;


Now I have the binding information, so when I ask for a specific contract, I can get the binding as well as the address back to the client that is requesting the endpoint information. Now we can discover the full A B C and not just the A and C.

3 comments:

Unknown said...

Hello Peretz,,

I would like to know how do i modify probe match message at runtime.i have requirment that i have to added element to the probe match brefore sening to the client,

I have tryed this using message inpector but it not working during probe match the message null, not sure why this is happeing

you have pointer please let me know

Anonymous said...

Great idea!

I extended your solution to include the complete metadata of an endpoint in the Extensions by using WsdlExporter/WsdlImporter. This reduces the problems you run into when trying to instantiate the Binding on the client side.

Cheers

Sebastian

Julien Enno said...

Great article Mike! Nice read :)