Creating a WCF 4 dynamic locator service part 2! Discovery extensions

Welcome back again! It has been a while since my last post. Last time I blogged about creating a locator service with WCF 4 discovery features. That article now resides here https://accblogs.infosupport.com/author/chrisb/. It will be shown right below this article. In that post I described how to use WCF 4 discovery to create a locator service that other services will use to announce themselves and a client can use to discover a address and binding based on a given contract.

The only problem with that solution was that discovery doesn’t provide us with Binding information of a service. It will only provide us with an address and the xml qualified name of a contract. To get the binding information I used dynamic meta data resolving, but that meant that all the services that want to be discoverable also needed to provide meta data via http get.

In this article I wan to explore another feature of discovery: Extensions. The discovery mechanism allows us to add extra data from endpoints to the discovery information. You can do this via the EndPointDiscoveryBehavior. This is an endpoint behavior which you can apply in your config file like below:

image

The endpointDiscovery behavior defines an extensions tag. Within this tag you are free to apply whatever extra information you want! Pretty cool right? What would be even cooler? If we could add this extra information dynamically, via code. This can be done by defining a new behavior. We could define a service behavior that in code applies this EndPointDiscovery behavior and fills the extension collection of this discovery behavior with runtime information. What kind of information you might wonder? Well how about the binding information! We can use this to place the assembly qualified typename of the binding that an endpoint is using, we can then use this information on the client side to dynamically instantiate the correct binding! First let’s define a service behavior that will add this binding information to the EndpointDiscoveryBehavior’s extensions collection. The code:

 1: [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]

 

 2:    public class DiscoveryBindingAttribute : Attribute,IServiceBehavior

 

 3:    {

 

 4:        public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)

&nbsp;

 5:        {

&nbsp;

 6:

&nbsp;

 7:        }

&nbsp;

 8:        public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)

&nbsp;

 9:        {

&nbsp;

 10:            foreach (var endpoint in serviceHostBase.Description.Endpoints)

&nbsp;

 11:            {

&nbsp;

 12:                EndpointDiscoveryBehavior discoBehavior= new EndpointDiscoveryBehavior();

&nbsp;

 13:                discoBehavior.Extensions.Add(new System.Xml.Linq.XElement("Binding",endpoint.Binding.GetType().AssemblyQualifiedName));

&nbsp;

 14:                endpoint.Behaviors.Add(discoBehavior);

&nbsp;

 15:            }

&nbsp;

 16:        }

&nbsp;

 17:        public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)

&nbsp;

 18:        {

&nbsp;

 19:

&nbsp;

 20:        }

&nbsp;

 21:    }

The interesting part begins on line 8. Here we iterate over all the endpoints in the service this behavior is applied to. We instantiate a new EndPointDiscoveryBehavior and we add a new XElement to its extensions collection. This XElement represent the custom information we saw earlier in our config file, this is the way to fill this information in code. This is all there is to it! to use this behavior we must apply it to a discoverable service. Like our Calculator service, and our Locator service, both will be discoverable, the Calculator via announcements, the Locator service via UDP.

Here is the Calculator Service code:

 1: using System;

&nbsp;

 2: using System.Collections.Generic;

&nbsp;

 3: using System.Linq;

&nbsp;

 4: using System.Runtime.Serialization;

&nbsp;

 5: using System.ServiceModel;

&nbsp;

 6: using System.ServiceModel.Web;

&nbsp;

 7: using System.Text;

&nbsp;

 8: using Contracts;

&nbsp;

 9:

&nbsp;

 10: namespace CalculatorService

&nbsp;

 11: {

&nbsp;

 12:     [DiscoveryExtensions.DiscoveryBinding]

&nbsp;

 13:     public class CalculatorService : ICalculator,ICalculatorMaintenance

&nbsp;

 14:     {

&nbsp;

 15:

&nbsp;

 16:         public int Add(int i, int i2)

&nbsp;

 17:         {

&nbsp;

 18:             return i + i2; ;

&nbsp;

 19:         }

&nbsp;

 20:

&nbsp;

 21:         public bool StatusOk()

&nbsp;

 22:         {

&nbsp;

 23:             //perform checks, everything ok

&nbsp;

 24:             return true;

&nbsp;

 25:         }

&nbsp;

 26:     }

&nbsp;

 27: }

Now we have the extra binding information exposed via discovery, so we don’t need meta data exchange anymore, here is the updated web.config file without meta data exposure!

image

Here we can see two endpoints. And we can see that this service announces itself to an endpoint. That endpoint is an endpoint on our locator service. When the locator service receives an announcement, it will store the data that comes with it. This data now included our extra binding information that we added in our extensions!

The locator service now returns two types of info when queried for an contract. It will send back the address of a service that implements that contract, and it will send back the assembly qualified typestring of the binding that that service is using. Here is the code of the locator service:

 1: [DiscoveryBindingAttribute]

&nbsp;

 2:    [ServiceBehavior(ConcurrencyMode=ConcurrencyMode.Multiple,InstanceContextMode=InstanceContextMode.Single,IncludeExceptionDetailInFaults=true)]

&nbsp;

 3:    public class LocatorService :AnnouncementService, IServiceLocator

&nbsp;

 4:    {

&nbsp;

 5:        private System.Collections.Concurrent.ConcurrentDictionary<XmlQualifiedName, EndpointDiscoveryMetadata> _services= new System.Collections.Concurrent.ConcurrentDictionary<XmlQualifiedName,EndpointDiscoveryMetadata>();

&nbsp;

 6:        public LocatorService()

&nbsp;

 7:        {

&nbsp;

 8:            this.OnlineAnnouncementReceived += new EventHandler<AnnouncementEventArgs>(anService_OnlineAnnouncementReceived);

&nbsp;

 9:            this.OfflineAnnouncementReceived += new EventHandler<AnnouncementEventArgs>(anService_OfflineAnnouncementReceived);

&nbsp;

 10:        }

&nbsp;

 11:

&nbsp;

 12:        void anService_OfflineAnnouncementReceived(object sender, AnnouncementEventArgs e)

&nbsp;

 13:        {

&nbsp;

 14:            EndpointDiscoveryMetadata ed;

&nbsp;

 15:            _services.TryRemove(e.EndpointDiscoveryMetadata.ContractTypeNames[0], out ed);

&nbsp;

 16:        }

&nbsp;

 17:

&nbsp;

 18:        void anService_OnlineAnnouncementReceived(object sender, AnnouncementEventArgs e)

&nbsp;

 19:        {

&nbsp;

 20:            // Note this will be called for every endpoint and every endpoint will have 1 contract, a service has more contracts

&nbsp;

 21:            if (!_services.ContainsKey(e.EndpointDiscoveryMetadata.ContractTypeNames[0]))

&nbsp;

 22:            {

&nbsp;

 23:                _services[e.EndpointDiscoveryMetadata.ContractTypeNames[0]]= e.EndpointDiscoveryMetadata;

&nbsp;

 24:            }

&nbsp;

 25:        }

&nbsp;

 26:

&nbsp;

 27:        public ServiceDiscoData Discover(XmlQualifiedName contractname)

&nbsp;

 28:        {

&nbsp;

 29:            ServiceDiscoData data = null;

&nbsp;

 30:            EndpointDiscoveryMetadata meta=_services[contractname];

&nbsp;

 31:            if (meta!=null)

&nbsp;

 32:            {

&nbsp;

 33:                data = new ServiceDiscoData() { Adres = meta.Address.ToString(), BindingTypeName = meta.Extensions[0].Value };

&nbsp;

 34:            }

&nbsp;

 35:            return data;

&nbsp;

 36:        }

&nbsp;

 37:    }

The cool stuff begins on line 27. Here the service will retrieve its stored announcements information by using the contract name of a service. From this info he will get the address and the binding typestring, it will then wrap this in a response and send it back to the client.

As in my earlier post, the client is still a dll, this dll will pack a class that will handle the queries to the locator service for us. All a client application needs to do, is to reference our dll and use the DynamicChannelFactory class to create a runtime proxy, all the client application needs to pass is a generic type parameter for the contract type. Our dll will discover the locator service, query it for the information it needs, and then use the WCF ChannelFactory class to instantiate a proxy. To the ChannelFactory class it will pass the binding, and the address. Here is the code of the DynamicChannelFactory class.

 1: public static class DynamicChannelFactory

&nbsp;

 2:    {

&nbsp;

 3:        public const int MAXRETRIES = 1;

&nbsp;

 4:        // this will only happen once during te application,or more often when something goes wrong

&nbsp;

 5:        private static ServiceLocator.IServiceLocator _locatorProxy = CreateLocatorProxy();

&nbsp;

 6:

&nbsp;

 7:        public static TContract CreateChannel<TContract>()

&nbsp;

 8:        {

&nbsp;

 9:            var result = GetDiscoData(typeof(TContract));

&nbsp;

 10:            if (result != null)

&nbsp;

 11:            {

&nbsp;

 12:                return ChannelFactory<TContract>.CreateChannel(result.Item2, new EndpointAddress(result.Item1));

&nbsp;

 13:            }

&nbsp;

 14:            return default(TContract);

&nbsp;

 15:

&nbsp;

 16:        }

&nbsp;

 17:

&nbsp;

 18:

&nbsp;

 19:        public static Tuple<string, Binding> GetDiscoData(Type contract)

&nbsp;

 20:        {

&nbsp;

 21:            Binding b = null;

&nbsp;

 22:            bool error = false;

&nbsp;

 23:            int nrRetries = 0;

&nbsp;

 24:            ServiceDiscoData data = null;

&nbsp;

 25:            ContractDescription cd = ContractDescription.GetContract(contract);

&nbsp;

 26:            do

&nbsp;

 27:            {

&nbsp;

 28:                try

&nbsp;

 29:                {

&nbsp;

 30:

&nbsp;

 31:                    data = _locatorProxy.Discover(new XmlQualifiedName(cd.Name, cd.Namespace));

&nbsp;

 32:                    error = false;

&nbsp;

 33:                }

&nbsp;

 34:                catch (FaultException)

&nbsp;

 35:                {

&nbsp;

 36:                    // service was not found

&nbsp;

 37:                    return null;

&nbsp;

 38:                }

&nbsp;

 39:                catch (CommunicationException)

&nbsp;

 40:                {

&nbsp;

 41:                    // something infra structural went wrong, maybe the locator service moved? Lets discover it again and try again

&nbsp;

 42:                    CreateLocatorProxy();

&nbsp;

 43:                    error = true;

&nbsp;

 44:                    nrRetries++;

&nbsp;

 45:                }

&nbsp;

 46:

&nbsp;

 47:            } while (error && nrRetries < MAXRETRIES);

&nbsp;

 48:

&nbsp;

 49:            if (data != null)

&nbsp;

 50:            {

&nbsp;

 51:                b = Activator.CreateInstance(Type.GetType(data.BindingTypeName)) as Binding;

&nbsp;

 52:                return new Tuple<string, Binding>(data.Adres, b); ;

&nbsp;

 53:            }

&nbsp;

 54:            else

&nbsp;

 55:            {

&nbsp;

 56:                return null;

&nbsp;

 57:            }

&nbsp;

 58:

&nbsp;

 59:        }

&nbsp;

 60:

&nbsp;

 61:        /// <summary>

&nbsp;

 62:        /// Finding via udp is still relatively slow, this will happen only the first time, if there is a well known address of this service

&nbsp;

 63:        /// its better to use that instead of locating it dynamically, its also better to define a well known binding for its endpoint.

&nbsp;

 64:        /// </summary>

&nbsp;

 65:        /// <returns></returns>

&nbsp;

 66:        private static ServiceLocator.IServiceLocator CreateLocatorProxy()

&nbsp;

 67:        {

&nbsp;

 68:            Type locatorContract = typeof(ServiceLocator.IServiceLocator);

&nbsp;

 69:            DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint());

&nbsp;

 70:            FindCriteria criteria = new FindCriteria(locatorContract);

&nbsp;

 71:            //dont forget to set the max results to one, this is a huge performance improvement

&nbsp;

 72:            criteria.MaxResults = 1;

&nbsp;

 73:            FindResponse response = dc.Find(criteria);

&nbsp;

 74:            if (response.Endpoints.Count > 0)

&nbsp;

 75:            {

&nbsp;

 76:                var metaExchangeEndpointData = response.Endpoints[0];

&nbsp;

 77:

&nbsp;

 78:                // The Locator service only has one endpoint which support locating

&nbsp;

 79:                return new ServiceLocator.ServiceLocatorClient((Binding)Activator.CreateInstance(Type.GetType(metaExchangeEndpointData.Extensions[0].Value)), metaExchangeEndpointData.Address);

&nbsp;

 80:

&nbsp;

 81:            }

&nbsp;

 82:            return null;

&nbsp;

 83:        }

&nbsp;

 84:    }

Line 66 is first up. This method will create a locator proxy for us so that we can ask it for service addresses and bindings. The factory will create a locator proxy as soon as it is initialized. it will try to discover the locator service by using udp. Because we also annotated our locator service with our cool new extension attribute, a response by the locator service will also include our XElement with binding information. This information is used to instantiate a proxy that was earlier generated with svcUtil. No ChannelFactory here.

Later on the GetDiscoData of our DynamicChannelfactory will use the locator proxy to query for service data, You can see this method from line 19. It will try to query, if it fails it will try to generate a new locator proxy and then query again. This method returns a tuple of an adress and a binding. The CreateChannel method is the method that a client should call to dynamically generate a proxy, this will use the WCF ChannelFactory to generate a proxy, by first querying our Locator service for address and binding data.

That’s it! Pretty cool stuff! Now we only need to know contracts and the rest will be searched for by runtime! Ideal for mocking etc. By using the extension mechanism of Discovery we have now one less method call for every query, and the services don’t have to expose metadata anymore!