Shaun Xu

The Sheep-Pen of the Shaun


News

logo

Shaun, the author of this blog is a semi-geek, clumsy developer, passionate speaker and incapable architect with about 10 years’ experience in .NET and JavaScript. He hopes to prove that software development is art rather than manufacturing. He's into cloud computing platform and technologies (Windows Azure, Amazon and Aliyun) and right now, Shaun is being attracted by JavaScript (Angular.js and Node.js) and he likes it.

Shaun is working at Worktile Inc. as the chief architect for overall design and develop worktile, a web-based collaboration and task management tool, and lesschat, a real-time communication aggregation tool.

MVP

My Stats

  • Posts - 122
  • Comments - 576
  • Trackbacks - 0

Tag Cloud


Recent Comments


Recent Posts


Archives


Post Categories


.NET


 

In the previous post I discussed about the basic usage of WCF Discovery in 4.0. I implemented a managed discovery service by inheriting from System.ServiceModel.Discovery.DiscoveryProxy. I utilized a concurrent dictionary as the endpoint repository. I created a WCF service which have the ServiceDiscoveryBehavior attached. It can send the online and offline message to the discovery service announcement endpoint. I also created a client which invoke the discovery service’s probe endpoint to retrieve the service endpoint by providing the service contract. So that the client don’t need to know where the service is.

This is a very basic usage of discovery service. As I mentioned at the end of my previous post there are something I was figured out.

1, In sample code I hard coded the binding on client. This is because by default the WCF Discovery endpoint metadata doesn’t contain binding information. So I need to make sure that service and client used the same binding. As we know when we want to call a WCF service we must understand its “ABC”, which is address, binding and contract. WCF Discovery supports “AC” but no “B”. So how can we make our discovery service store the service binding.

2, I was using a concurrent dictionary as the endpoint repository. This is good for a test project, but not for production. In most cases we might be going to save the endpoint information in database. So how to store the service endpoint metadata in a database. This might need some serialization and deserialization work.

3, By default, WCF Discovery will return all proper endpoints. But normally the client only need one endpoint to invoke. How can we leverage this feature to build a load balance enabled discovery service.

4, When we want a service to be discoverable, in previous post I need to add the ServiceDiscoveryBehavior before the service host was opened. How can we make this more flexible so that we can use an attribute on top of each discoverable service. And how about using this attribute to add more flexibility.

In this post I will discuss and demonstrate them one by one.

 

Add Binding Information in Endpoint Metadata

The build-in endpoint metadata doesn’t support binding. This is because binding was not in WS-Discovery and the WCF Discovery is following this specification. But WCF gives us a chance to add customized data into the endpoint metadata, which is the EndpointDiscoveryMetadata.Extensions property. It is a collection of XElement which means we can add any data here as a list of XML element. The only thing we need to do is how to serialize the value.

So now what we need is to retrieve the discoverable service’s binding, serialize it and append the value into the Extensions property when it’s online. And when client probed, we will received the binding and desterilize.

EndpointDiscoveryBehavior is the place we can put our customized data into the metadata extensions property. We need to prepare the data and insert them into the EndpointDiscoveryBehavior.Extensions property. Then add it into each service endpoint’s behavior. So when the service goes online and sends the online message, each endpoint will perform our EndpointDiscoveryBehavior and inject our customized data into the endpoint metadata extensions.

image

In the code below I created a service behavior. When WCF applies it, each endpoint of the service will retrieve the binding object and binding type name, serialized them to JSON, and created two XML elements and added to the EndpointDiscoveryBehavior.Extensions.

Since we cannot desterilize the binding JSON string back to an abstract Binding object, we must put the actual binding type name within the metadata extensions.

   1: public class DiscoverableServiceBehavior : IServiceBehavior
   2: {
   3:     private const string CST_XELEMNAME_BINDINGTYPENAME = "bindingTypeName";
   4:     private const string CST_XELEMNAME_BINDING = "binding";
   5:  
   6:     public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
   7:     {
   8:     }
   9:  
  10:     public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  11:     {
  12:         foreach (var endpoint in serviceDescription.Endpoints)
  13:         {
  14:             var binding = endpoint.Binding;
  15:             var endpointDiscoveryBehavior = new EndpointDiscoveryBehavior();
  16:             var bindingType = binding.GetType().AssemblyQualifiedName;
  17:             var bindingJson = JsonConvert.SerializeObject(binding);
  18:             endpointDiscoveryBehavior.Extensions.Add(new XElement(CST_XELEMNAME_BINDINGTYPENAME, bindingType));
  19:             endpointDiscoveryBehavior.Extensions.Add(new XElement(CST_XELEMNAME_BINDING, bindingJson));
  20:             endpoint.Behaviors.Add(endpointDiscoveryBehavior);
  21:         }
  22:     }
  23:  
  24:     public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  25:     {
  26:     }
  27: }

Add this new service behavior in the discoverable service host process.

   1: static void Main(string[] args)
   2: {
   3:     var baseAddress = new Uri(string.Format("net.tcp://localhost:11001/stringservice/{0}/", Guid.NewGuid().ToString()));
   4:  
   5:     using (var host = new ServiceHost(typeof(StringService), baseAddress))
   6:     {
   7:         host.Opened += (sender, e) =>
   8:         {
   9:             Console.WriteLine("Service opened at {0}", host.Description.Endpoints.First().ListenUri);
  10:         };
  11:  
  12:         host.AddServiceEndpoint(typeof(IStringService), new NetTcpBinding(), string.Empty);
  13:  
  14:         var announcementAddress = new EndpointAddress(ConfigurationManager.AppSettings["announcementEndpointAddress"]);
  15:         var announcementBinding = Activator.CreateInstance(Type.GetType(ConfigurationManager.AppSettings["bindingType"], true, true)) as Binding;
  16:         var announcementEndpoint = new AnnouncementEndpoint(announcementBinding, announcementAddress);
  17:         var discoveryBehavior = new ServiceDiscoveryBehavior();
  18:         discoveryBehavior.AnnouncementEndpoints.Add(announcementEndpoint);
  19:         host.Description.Behaviors.Add(discoveryBehavior);
  20:  
  21:         var discoverableServiceBehavior = new DiscoverableServiceBehavior();
  22:         host.Description.Behaviors.Add(discoverableServiceBehavior);
  23:  
  24:         host.Open();
  25:  
  26:         Console.WriteLine("Press any key to exit.");
  27:         Console.ReadKey();
  28:     }
  29: }

Then on the client side, when a client found the endpoint metadata from discovery service it will retrieve the binding information from the extensions. What the client need to do is to desterilize the binding object from JSON and apply to the channel factory.

   1: private const string CST_XELEMNAME_BINDINGTYPENAME = "bindingTypeName";
   2: private const string CST_XELEMNAME_BINDING = "binding";
   3:  
   4: static Tuple<EndpointAddress, Binding> FindServiceEndpoint()
   5: {
   6:     var probeEndpointAddress = new EndpointAddress(ConfigurationManager.AppSettings["probeEndpointAddress"]);
   7:     var probeBinding = Activator.CreateInstance(Type.GetType(ConfigurationManager.AppSettings["bindingType"], true, true)) as Binding;
   8:     var discoveryEndpoint = new DiscoveryEndpoint(probeBinding, probeEndpointAddress);
   9:  
  10:     EndpointAddress address = null;
  11:     Binding binding = null;
  12:     FindResponse result = null;
  13:     using (var discoveryClient = new DiscoveryClient(discoveryEndpoint))
  14:     {
  15:         result = discoveryClient.Find(new FindCriteria(typeof(IStringService)));
  16:     }
  17:  
  18:     if (result != null && result.Endpoints.Any())
  19:     {
  20:         var endpointMetadata = result.Endpoints.First();
  21:         address = endpointMetadata.Address;
  22:         var bindingTypeName = endpointMetadata.Extensions
  23:             .Where(x => x.Name == CST_XELEMNAME_BINDINGTYPENAME)
  24:             .Select(x => x.Value)
  25:             .FirstOrDefault();
  26:         var bindingJson = endpointMetadata.Extensions
  27:             .Where(x => x.Name == CST_XELEMNAME_BINDING)
  28:             .Select(x => x.Value)
  29:             .FirstOrDefault();
  30:         var bindingType = Type.GetType(bindingTypeName, true, true);
  31:         binding = JsonConvert.DeserializeObject(bindingJson, bindingType) as Binding;
  32:     }
  33:     return new Tuple<EndpointAddress, Binding>(address, binding);
  34: }

Then we will use the address and binding from discovery service to construct our channel factory. This will ensure that the client utilizes the same binding as the service it’s calling.

   1: static void Main(string[] args)
   2: {
   3:     Console.WriteLine("Say something...");
   4:     var content = Console.ReadLine();
   5:     while (!string.IsNullOrWhiteSpace(content))
   6:     {
   7:         Console.WriteLine("Finding the service endpoint...");
   8:         var addressAndBinding = FindServiceEndpoint();
   9:         if (addressAndBinding == null || addressAndBinding.Item1 == null || addressAndBinding.Item2 == null)
  10:         {
  11:             Console.WriteLine("There is no endpoint matches the criteria.");
  12:         }
  13:         else
  14:         {
  15:             var address = addressAndBinding.Item1;
  16:             var binding = addressAndBinding.Item2;
  17:             Console.WriteLine("Found the endpoint {0} use binding {1}", address.Uri, binding.Name);
  18:  
  19:             var factory = new ChannelFactory<IStringService>(binding, address);
  20:             factory.Opened += (sender, e) =>
  21:             {
  22:                 Console.WriteLine("Connecting to {0}.", factory.Endpoint.ListenUri);
  23:             };
  24:             var proxy = factory.CreateChannel();
  25:             using (proxy as IDisposable)
  26:             {
  27:                 Console.WriteLine("ToUpper: {0} => {1}", content, proxy.ToUpper(content));
  28:             }
  29:         }
  30:  
  31:         Console.WriteLine("Say something...");
  32:         content = Console.ReadLine();
  33:     }
  34: }

Let’s have a test. As you can see the client retrieve the binding the service specified and initialized the corresponding binding object.

image

Later we will see how to use the extensions to implement more features. But next, let have a look at the backend discovery endpoint repository.

 

Use Database as the Endpoint Repository

In our discovery service we utilized a in-memory concurrent dictionary as the endpoint repository to store all services’ endpoint metadata. This is OK as an example and could provide high performance. But in some cases, especially when we have more than one discovery service instances, we’d better put the repository to another place. Database is one of the choice. Now let’s begin to refactor our code and make the endpoint repository changeable.

From our discovery service we will find that the endpoint repository has three main functions.

1, Add a new endpoint metadata.

2, Remove an existing endpoint metadata.

3, Find the proper metadata based on the criteria.

Hence I will create an interface to cover them and then create sub classes for each repository. The interface would be like this.

   1: public interface IEndpointMetadataProvider
   2: {
   3:     void AddEndpointMetadata(EndpointDiscoveryMetadata metadata);
   4:  
   5:     void RemoveEndpointMetadata(EndpointDiscoveryMetadata metadata);
   6:  
   7:     EndpointDiscoveryMetadata MatchEndpoint(FindCriteria criteria);
   8: }

And moved our in-memory dictionary stuff from the discovery service to a separated class.

   1: public class InProcEndpointMetadataProvider : IEndpointMetadataProvider
   2: {
   3:     private ConcurrentDictionary<EndpointAddress, EndpointDiscoveryMetadata> _endpoints;
   4:  
   5:     public InProcEndpointMetadataProvider()
   6:     {
   7:         _endpoints = new ConcurrentDictionary<EndpointAddress, EndpointDiscoveryMetadata>();
   8:     }
   9:  
  10:     public void AddEndpointMetadata(EndpointDiscoveryMetadata metadata)
  11:     {
  12:         _endpoints.AddOrUpdate(metadata.Address, metadata, (key, value) => metadata);
  13:     }
  14:  
  15:     public void RemoveEndpointMetadata(EndpointDiscoveryMetadata metadata)
  16:     {
  17:         EndpointDiscoveryMetadata value = null;
  18:         _endpoints.TryRemove(metadata.Address, out value);
  19:     }
  20:  
  21:     public EndpointDiscoveryMetadata MatchEndpoint(FindCriteria criteria)
  22:     {
  23:         var endpoints = _endpoints
  24:             .Where(meta => criteria.IsMatch(meta.Value))
  25:             .Select(meta => meta.Value)
  26:             .ToList();
  27:         return endpoints.FirstOrDefault();
  28:     }
  29: }

Then we can change our discovery service code to make it use the abstract endpoint provider interface instead of the actual repository.

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
   2: public class ManagedProxyDiscoveryService : DiscoveryProxy
   3: {
   4:     private IEndpointMetadataProvider _metadataProvider;
   5:  
   6:     public ManagedProxyDiscoveryService(IEndpointMetadataProvider metadataProvider)
   7:     {
   8:         _metadataProvider = metadataProvider;
   9:     }
  10:  
  11:     protected override IAsyncResult OnBeginOnlineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
  12:     {
  13:         _metadataProvider.AddEndpointMetadata(endpointDiscoveryMetadata);
  14:         return new OnOnlineAnnouncementAsyncResult(callback, state);
  15:     }
  16:  
  17:     protected override void OnEndOnlineAnnouncement(IAsyncResult result)
  18:     {
  19:         OnOnlineAnnouncementAsyncResult.End(result);
  20:     }
  21:  
  22:     protected override IAsyncResult OnBeginOfflineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
  23:     {
  24:         _metadataProvider.RemoveEndpointMetadata(endpointDiscoveryMetadata);
  25:         return new OnOfflineAnnouncementAsyncResult(callback, state);
  26:     }
  27:  
  28:     protected override void OnEndOfflineAnnouncement(IAsyncResult result)
  29:     {
  30:         OnOfflineAnnouncementAsyncResult.End(result);
  31:     }
  32:  
  33:     protected override IAsyncResult OnBeginFind(FindRequestContext findRequestContext, AsyncCallback callback, object state)
  34:     {
  35:         var endpoint = _metadataProvider.MatchEndpoint(findRequestContext.Criteria);
  36:         findRequestContext.AddMatchingEndpoint(endpoint);
  37:         return new OnFindAsyncResult(callback, state);
  38:     }
  39:  
  40:     protected override void OnEndFind(IAsyncResult result)
  41:     {
  42:         OnFindAsyncResult.End(result);
  43:     }
  44:  
  45:     protected override IAsyncResult OnBeginResolve(ResolveCriteria resolveCriteria, AsyncCallback callback, object state)
  46:     {
  47:         throw new NotImplementedException();
  48:     }
  49:  
  50:     protected override EndpointDiscoveryMetadata OnEndResolve(IAsyncResult result)
  51:     {
  52:         throw new NotImplementedException();
  53:     }
  54: }

If we using the InProcEndpointMetadataProvider as the provider then our discovery service should work as before. Just change the discovery service hosting code like this.

   1: static void Main(string[] args)
   2: {
   3:     IEndpointMetadataProvider metadataProvider = new InProcEndpointMetadataProvider();
   4:  
   5:     using (var host = new ServiceHost(new ManagedProxyDiscoveryService(metadataProvider)))
   6:     {
   7:         ... ...
   8:         ... ...
   9:     }
  10: }

 

Next, let’s start to use database as the endpoint repository. First of all we need to define the schema of the table where the endpoint metadata will be stored. Normally one service will be host at one endpoint so use the service contract name as the primary key column would be a good choice. But consider the scaling-out situation, one service might have more than one instance in the system, so that it might be more than one endpoints per service contract. So for extensibility we will use the endpoint address URI as the primary key, but add an index on the service contract column.

I also created three store procedures to add, remove and find the endpoint metadata as well. The database schema would be like this.

image

And here is the database creation script.

   1: /****** Object:  StoredProcedure [dbo].[GetServiceEndpointMetadata]    Script Date: 7/4/2012 3:03:01 PM ******/
   2: SET ANSI_NULLS ON
   3: GO
   4: SET QUOTED_IDENTIFIER ON
   5: GO
   6: CREATE PROCEDURE [dbo].[GetServiceEndpointMetadata]
   7:     @contractType nvarchar(512)
   8: AS
   9: BEGIN
  10:  
  11:     SELECT Address, BindingType, Binding, UpdatedOn FROM EndpointMetadata WHERE ContractType = @contractType
  12:  
  13: END
  14:  
  15: GO
  16: /****** Object:  StoredProcedure [dbo].[RegisterServiceEndpointMetadata]    Script Date: 7/4/2012 3:03:01 PM ******/
  17: SET ANSI_NULLS ON
  18: GO
  19: SET QUOTED_IDENTIFIER ON
  20: GO
  21: CREATE PROCEDURE [dbo].[RegisterServiceEndpointMetadata]
  22:     @uri nvarchar(256),
  23:     @contractType nvarchar(256),
  24:     @address nvarchar(1024),
  25:     @bindingType nvarchar(512),
  26:     @binding nvarchar(1024)
  27: AS
  28: BEGIN
  29:  
  30:     BEGIN TRAN
  31:  
  32:         EXEC UnRegisterServiceEndpointMetadata @uri, @contractType, @address, @bindingType
  33:  
  34:         INSERT INTO EndpointMetadata (Uri, ContractType, Address, BindingType, Binding) VALUES (@uri, @contractType, @address, @bindingType, @binding)
  35:  
  36:     COMMIT TRAN
  37:  
  38: END
  39:  
  40: GO
  41: /****** Object:  StoredProcedure [dbo].[UnRegisterServiceEndpointMetadata]    Script Date: 7/4/2012 3:03:01 PM ******/
  42: SET ANSI_NULLS ON
  43: GO
  44: SET QUOTED_IDENTIFIER ON
  45: GO
  46: CREATE PROCEDURE [dbo].[UnRegisterServiceEndpointMetadata]
  47:     @uri nvarchar(256),
  48:     @contractType nvarchar(256),
  49:     @address nvarchar(1024),
  50:     @bindingType nvarchar(512)
  51: AS
  52: BEGIN
  53:  
  54:     DELETE FROM EndpointMetadata WHERE Uri = @uri AND ContractType = @contractType AND Address = @address AND BindingType = @bindingType
  55:  
  56: END
  57:  
  58: GO
  59: /****** Object:  Table [dbo].[EndpointMetadata]    Script Date: 7/4/2012 3:03:01 PM ******/
  60: SET ANSI_NULLS ON
  61: GO
  62: SET QUOTED_IDENTIFIER ON
  63: GO
  64: CREATE TABLE [dbo].[EndpointMetadata](
  65:     [Uri] [nvarchar](256) NOT NULL,
  66:     [ContractType] [nvarchar](256) NOT NULL,
  67:     [Address] [nvarchar](1024) NOT NULL,
  68:     [BindingType] [nvarchar](512) NOT NULL,
  69:     [Binding] [nvarchar](1024) NOT NULL,
  70:     [UpdatedOn] [datetime] NOT NULL,
  71:  CONSTRAINT [PK_EndpointMetadata] PRIMARY KEY CLUSTERED 
  72: (
  73:     [Uri] ASC
  74: )WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF)
  75: )
  76:  
  77: GO
  78: SET ANSI_PADDING ON
  79:  
  80: GO
  81: /****** Object:  Index [IX_ContractType]    Script Date: 7/4/2012 3:03:01 PM ******/
  82: CREATE NONCLUSTERED INDEX [IX_ContractType] ON [dbo].[EndpointMetadata]
  83: (
  84:     [ContractType] ASC
  85: )WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF)
  86: GO
  87: ALTER TABLE [dbo].[EndpointMetadata] ADD  CONSTRAINT [DF_EndpiointMetadata_UpdatedOn]  DEFAULT (getutcdate()) FOR [UpdatedOn]
  88: GO

 

Next, we will create a class implemented the IEndpointMetadataProvider interface for database. And I’m using a delegation to let the consumer pass the database connection object creator in, so that it will not bounded at an actual database.

   1: public class DbEndpointMetadataProvider : IEndpointMetadataProvider
   2: {
   3:     private string _connectionString;
   4:     private Func<string, IDbConnection> _connectionCreator;
   5:  
   6:     public DbEndpointMetadataProvider(Func<string, IDbConnection> connectionCreator, string connectionString)
   7:     {
   8:         _connectionCreator = connectionCreator;
   9:         _connectionString = connectionString;
  10:     }
  11:  
  12:     public void AddEndpointMetadata(EndpointDiscoveryMetadata metadata)
  13:     {
  14:         throw new NotImplementedException();
  15:     }
  16:  
  17:     public void RemoveEndpointMetadata(EndpointDiscoveryMetadata metadata)
  18:     {
  19:         throw new NotImplementedException();
  20:     }
  21:  
  22:     public EndpointDiscoveryMetadata MatchEndpoint(FindCriteria criteria)
  23:     {
  24:         throw new NotImplementedException();
  25:     }
  26: }

When need to add endpoint metadata into database, we will invoke the RegisterServiceEndpointMetadata store procedure, specify the endpoint address, contract type name, binding type, binding (in JSON format) and update date time. There are two things need to be noticed. The first one is, a service may implement more than one contracts. So that for each contract we need to add a record for its endpoint metadata. The second one is, the endpoint address and binding should be serialized before saved to database. Currently we will use JSON format.

   1: public void AddEndpointMetadata(EndpointDiscoveryMetadata metadata)
   2: {
   3:     var uri = metadata.Address.Uri.AbsoluteUri;
   4:     var addressJson = JsonConvert.SerializeObject(metadata.Address);
   5:     var contractTypeNames = metadata.ContractTypeNames.Select(nm => string.Format("{0}, {1}", nm.Name, nm.Namespace)).ToList();
   6:     var bindingTypeName = metadata.Extensions
   7:         .Where(x => x.Name == CST_XELEMNAME_BINDINGTYPENAME)
   8:         .Select(x => x.Value)
   9:         .FirstOrDefault();
  10:     var bindingJson = metadata.Extensions
  11:         .Where(x => x.Name == CST_XELEMNAME_BINDING)
  12:         .Select(x => x.Value)
  13:         .FirstOrDefault();
  14:     using (var conn = _connectionCreator.Invoke(_connectionString))
  15:     {
  16:         var cmd = conn.CreateCommand();
  17:         cmd.CommandType = CommandType.StoredProcedure;
  18:         cmd.CommandText = "RegisterServiceEndpointMetadata";
  19:         cmd.AddParameter("uri", uri)
  20:            .AddParameter("contractType", null)
  21:            .AddParameter("address", addressJson)
  22:            .AddParameter("bindingType", bindingTypeName)
  23:            .AddParameter("binding", bindingJson);
  24:  
  25:         conn.Open();
  26:         cmd.Transaction = conn.BeginTransaction();
  27:         foreach (var contractTypeName in contractTypeNames)
  28:         {
  29:             cmd.GetParameter("contractType").Value = contractTypeName;
  30:             cmd.ExecuteNonQuery();
  31:         }
  32:         cmd.Transaction.Commit();
  33:     }
  34: }

I also added an internal helper class for make the database parameter operation easy.

   1: internal static class DatabaseHelpers
   2: {
   3:     public static IDbCommand AddParameter(this IDbCommand cmd, string name, object value)
   4:     {
   5:         var param = cmd.CreateParameter();
   6:         param.ParameterName = name;
   7:         param.Value = value;
   8:         cmd.Parameters.Add(param);
   9:         return cmd;
  10:     }
  11:  
  12:     public static IDbDataParameter GetParameter(this IDbCommand cmd, string parameterName)
  13:     {
  14:         return cmd.Parameters[parameterName] as IDbDataParameter;
  15:     }
  16: }

Similarly, when a service goes offline, we need to remove all records based on its endpoint metadata for each contract types.

   1: public void RemoveEndpointMetadata(EndpointDiscoveryMetadata metadata)
   2: {
   3:     var uri = metadata.Address.Uri.AbsoluteUri;
   4:     var addressJson = JsonConvert.SerializeObject(metadata.Address);
   5:     var contractTypeNames = metadata.ContractTypeNames.Select(nm => string.Format("{0}, {1}", nm.Name, nm.Namespace)).ToList();
   6:     var bindingTypeName = metadata.Extensions
   7:         .Where(x => x.Name == CST_XELEMNAME_BINDINGTYPENAME)
   8:         .Select(x => x.Value)
   9:         .FirstOrDefault();
  10:     using (var conn = _connectionCreator.Invoke(_connectionString))
  11:     {
  12:         var cmd = conn.CreateCommand();
  13:         cmd.CommandType = CommandType.StoredProcedure;
  14:         cmd.CommandText = "UnRegisterServiceEndpointMetadata";
  15:         cmd.AddParameter("uri", uri)
  16:             .AddParameter("contractType", null)
  17:             .AddParameter("address", addressJson)
  18:             .AddParameter("bindingType", bindingTypeName);
  19:  
  20:         conn.Open();
  21:         cmd.Transaction = conn.BeginTransaction();
  22:         foreach (var contractTypeName in contractTypeNames)
  23:         {
  24:             cmd.GetParameter("contractType").Value = contractTypeName;
  25:             cmd.ExecuteNonQuery();
  26:         }
  27:         cmd.Transaction.Commit();
  28:     }
  29: }

The last one is the find method, which should select the endpoint metadata and return back to the discovery service. When using in-memory dictionary this is very easy since we can store the endpoint metadata in the dictionary and return back. But when using database all information must be serialized. So now we need to find the proper endpoint records, deserialized them back to the endpoint metadata.

We have been implemented the deserialization code for binding in previous part. Now what we need to do is to desterilize the address. This is a little bit tricky. EndpointAddress class contains some readonly property which cannot be initialized directly by the JSON converter. So I have to create some wrapper classes to make it easy to desterilize.

   1: internal class AddressHeaderWrapper
   2: {
   3:     public string Name { get; set; }
   4:  
   5:     public string Namespace { get; set; }
   6:  
   7:     public AddressHeader ToAddressHeader()
   8:     {
   9:         return AddressHeader.CreateAddressHeader(Name, Namespace, null);
  10:     }
  11: }
  12:  
  13: internal class EndpointIdentityWrapper
  14: {
  15:     public Claim IdentityClaim { get; set; }
  16:  
  17:     public EndpointIdentity EndpointIdentity
  18:     {
  19:         get
  20:         {
  21:             return EndpointIdentity.CreateIdentity(IdentityClaim);
  22:         }
  23:     }
  24: }
  25:  
  26: internal class EndpointAddressWrapper
  27: {
  28:     public AddressHeaderWrapper[] Headers { get; set; }
  29:  
  30:     public EndpointIdentityWrapper Identity { get; set; }
  31:  
  32:     public bool IsAnonymous { get; set; }
  33:  
  34:     public bool IsNone { get; set; }
  35:  
  36:     public Uri Uri { get; set; }
  37:  
  38:     public EndpointAddress EndpointAddress
  39:     {
  40:         get
  41:         {
  42:             var headerWrappers = Headers ?? new AddressHeaderWrapper[] { };
  43:             var headers = headerWrappers.Select(h => h.ToAddressHeader()).ToArray();
  44:             if (Identity == null)
  45:             {
  46:                 return new EndpointAddress(Uri, headers);
  47:             }
  48:             else
  49:             {
  50:                 return new EndpointAddress(Uri, Identity.EndpointIdentity, headers);
  51:             }
  52:         }
  53:     }
  54: }

With these wrapper classes we can easily construct the endpoint metadata from database records.

   1: public EndpointDiscoveryMetadata MatchEndpoint(FindCriteria criteria)
   2: {
   3:     var endpoints = new List<EndpointDiscoveryMetadata>();
   4:     var contractTypeNames = criteria.ContractTypeNames.Select(nm => string.Format("{0}, {1}", nm.Name, nm.Namespace)).ToList();
   5:     using (var conn = _connectionCreator.Invoke(_connectionString))
   6:     {
   7:         var cmd = conn.CreateCommand();
   8:         cmd.CommandType = CommandType.StoredProcedure;
   9:         cmd.CommandText = "GetServiceEndpointMetadata";
  10:         cmd.AddParameter("contractType", null);
  11:  
  12:         conn.Open();
  13:         foreach (var contractTypeName in contractTypeNames)
  14:         {
  15:             cmd.GetParameter("contractType").Value = contractTypeName;
  16:             using (var reader = cmd.ExecuteReader())
  17:             {
  18:                 while (reader.Read())
  19:                 {
  20:                     var addressJson = (string)reader["Address"];
  21:                     var bindingTypeName = (string)reader["BindingType"];
  22:                     var bindingJson = (string)reader["Binding"];
  23:                     var updatedOn = (DateTime)reader["UpdatedOn"];
  24:                     var address = JsonConvert.DeserializeObject<EndpointAddressWrapper>(addressJson).EndpointAddress;
  25:                     var metadata = new EndpointDiscoveryMetadata();
  26:                     metadata.Address = address;
  27:                     metadata.Extensions.Add(new XElement(CST_XELEMNAME_BINDINGTYPENAME, bindingTypeName));
  28:                     metadata.Extensions.Add(new XElement(CST_XELEMNAME_BINDING, bindingJson));
  29:                     metadata.Extensions.Add(new XElement(CST_XELEMNAME_UPDATEDON, JsonConvert.SerializeObject(updatedOn)));
  30:                     endpoints.Add(metadata);
  31:                 }
  32:             }
  33:         }
  34:     }
  35:     return endpoints.FirstOrDefault();
  36: }

Changed the discovery service host program and have a try.

   1: var connectionString = ConfigurationManager.ConnectionStrings["localhost"].ConnectionString;
   2: IEndpointMetadataProvider metadataProvider = new DbEndpointMetadataProvider(connString => new SqlConnection(connString), connectionString);
   3:  
   4: using (var host = new ServiceHost(new ManagedProxyDiscoveryService(metadataProvider)))
   5: {
   6:     ... ...
   7: }

When the discoverable service was started, in database we can see the endpoint information was added.

image

And our client can invoke the service through the discovery service.

image

And when we close the discoverable service its endpoint will be deleted from database.

 

Dispatcher Service Scaling-out Mode

In my Scaling-out Your Services by Message Bus-based WCF Transport Extension series I talked a lot about how to use service bus to make your service scalable. In that post I compared two scaling-out mode: dispatcher mode and pooling mode. Now with discovery service we can implement the dispatcher mode very easy.

The discovery service takes the responsible for retrieving the proper endpoint for a service by its contract. And in the discovery service procedure it’s possible that one service contract can have more than one endpoint addresses associated. This means, we can have more than one instance for a service and when the client need to invoke this service, the discovery service can find one of the endpoint of this service. This is the dispatcher mode I mentioned.

image

If we review our endpoint provider code, DbEndpointMetadataProvider and InProcEndpointMetadataProvider returns the first matched endpoint back. And this is where we can implement the changeable endpoint selector. We can let the discovery service return the endpoint which registered latest, or randomly select one endpoint which is the core of dispatcher mode.

First, I created an interface named IEndpointSelector which only has one method that return one endpoint metadata from a list of metadata.

   1: public interface IEndpointSelector
   2: {
   3:     EndpointDiscoveryMetadata Select(IEnumerable<EndpointDiscoveryMetadata> endpoints);
   4: }

Then I created an abstract base class for all sub selectors. In this base class I just implemented a common feature. If the metadata list is null or empty, it will return null. If there’s only one endpoint metadata in the list, it will return directly. Otherwise, only when there are more than one candidate endpoints I will invoke the sub class OnSelect method.

   1: public abstract class EndpointSelectorBase : IEndpointSelector
   2: {
   3:     public EndpointDiscoveryMetadata Select(IEnumerable<EndpointDiscoveryMetadata> endpoints)
   4:     {
   5:         // when no endpoints or only one endpoint then no need to invoke the actual selector, just return null or the only one
   6:         var endpointCount = endpoints.Count();
   7:         if (endpointCount <= 0)
   8:         {
   9:             return null;
  10:         }
  11:         else
  12:         {
  13:             if (endpointCount > 1)
  14:             {
  15:                 return OnSelect(endpoints, endpointCount);
  16:             }
  17:             else
  18:             {
  19:                 return endpoints.First();
  20:             }
  21:         }
  22:     }
  23:  
  24:     protected abstract EndpointDiscoveryMetadata OnSelect(IEnumerable<EndpointDiscoveryMetadata> endpoints, int endpointCount);
  25: }

Then we can create our own endpoint selector. For example the randomly endpoint selector.

   1: public class RandomEndpointSelector : EndpointSelectorBase
   2: {
   3:     private Random _rnd;
   4:  
   5:     public RandomEndpointSelector()
   6:     {
   7:         _rnd = new Random();
   8:     }
   9:  
  10:     protected override EndpointDiscoveryMetadata OnSelect(IEnumerable<EndpointDiscoveryMetadata> endpoints, int endpointCount)
  11:     {
  12:         var index = _rnd.Next(0, endpointCount - 1);
  13:         return endpoints.ElementAt(index);
  14:     }
  15: }

Then I will inject the endpoint selector into the endpoint provider so that it will invoke the selector to return the best endpoint to the client. Create a new abstract class from the IEndpointMetadataProvider and added the selector variant.

   1: public abstract class EndpointMetadataProviderBase : IEndpointMetadataProvider
   2: {
   3:     private IEndpointSelector _endpointSelector;
   4:  
   5:     protected EndpointMetadataProviderBase(IEndpointSelector endpointSelector)
   6:     {
   7:         _endpointSelector = endpointSelector;
   8:     }
   9:  
  10:     public abstract void AddEndpointMetadata(System.ServiceModel.Discovery.EndpointDiscoveryMetadata metadata);
  11:  
  12:     public abstract void RemoveEndpointMetadata(System.ServiceModel.Discovery.EndpointDiscoveryMetadata metadata);
  13:  
  14:     public EndpointDiscoveryMetadata MatchEndpoint(FindCriteria criteria)
  15:     {
  16:         EndpointDiscoveryMetadata endpoint = null;
  17:         var endpoints = OnMatchEndpoints(criteria);
  18:         endpoint = _endpointSelector.Select(endpoints);
  19:         return endpoint;
  20:     }
  21:  
  22:     protected abstract IEnumerable<EndpointDiscoveryMetadata> OnMatchEndpoints(FindCriteria criteria);
  23: }

Then we need to change the database endpoint provider and in process endpoint provider code. They now should be inherited from this base class.

The updated code can be downloaded from the link at the bottom of this post.

Now we can start the discovery service and start three instances of our discoverable service. In database we will find three records with the same service contract but different endpoint address. And as you can see my client connected different service instances.

image

 

Attribute and More Controls

Now we came to the last topic. From the sample code I provided above we can see that when we need to make a service to be discoverable, we should append the related service behavior before it was being hosted and opened. If we are using the console application to host the discoverable service we need to initialize and add the ServiceDiscoveryBehavior and DiscoverableServiceBehavior before open the host.

image

But this is not convenient, especially when we don’t have the service host explicitly. For example when hosting the service through IIS. As you may know, the service behavior could be specified through an attributed on the service class definition. So Let’s begin to create the attribute so that we can mark the services which we want them to be discoverable.

I renamed the DiscoverableServiceBehavior class to DiscoverableAttribute and it’s now inherited from the Attribute class, which means I can put it onto any service classes that I want it to be discoverable. Then in order to make it apply the ServiceDiscoveryBehavior I added a local variant of ServiceDiscoveryBehavior, initialized in the constructor, and perform its operations in each methods of the IServiceBehavior interface.

   1: [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
   2: public class DiscoverableAttribute : Attribute, IServiceBehavior
   3: {
   4:     private const string CST_XELEMNAME_BINDINGTYPENAME = "bindingTypeName";
   5:     private const string CST_XELEMNAME_BINDING = "binding";
   6:  
   7:     private const string CST_CONFIGKEY_ANNOUNCEMENTENDPOINT = "announcementEndpointAddress";
   8:     private const string CST_CONFIGKEY_ANNOUNCEMENTBINDING = "bindingType";
   9:  
  10:     private IServiceBehavior _serviceDiscoveryBehavior;
  11:  
  12:     public DiscoverableAttribute(string announcementEndpoint, Binding announcementBinding)
  13:     {
  14:         var endpoint = new AnnouncementEndpoint(announcementBinding, new EndpointAddress(announcementEndpoint));
  15:         var serviceDiscoveryBehavior = new ServiceDiscoveryBehavior();
  16:         serviceDiscoveryBehavior.AnnouncementEndpoints.Add(endpoint);
  17:         _serviceDiscoveryBehavior = serviceDiscoveryBehavior;
  18:     }
  19:  
  20:     public DiscoverableAttribute()
  21:         : this(ConfigurationManager.AppSettings[CST_CONFIGKEY_ANNOUNCEMENTENDPOINT],
  22:                Activator.CreateInstance(Type.GetType(ConfigurationManager.AppSettings[CST_CONFIGKEY_ANNOUNCEMENTBINDING], true, true)) as Binding)
  23:     {
  24:     }
  25:  
  26:     public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
  27:     {
  28:         // apply the service discovery behavior opertaion
  29:         _serviceDiscoveryBehavior.AddBindingParameters(serviceDescription, serviceHostBase, endpoints, bindingParameters);
  30:     }
  31:  
  32:     public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  33:     {
  34:         // apply the service discovery behavior opertaion
  35:         _serviceDiscoveryBehavior.ApplyDispatchBehavior(serviceDescription, serviceHostBase);
  36:         // add the additional information into the endpoint metadata extension through the endpoint discovery binding for each endpoints
  37:         foreach (var endpoint in serviceDescription.Endpoints)
  38:         {
  39:             var binding = endpoint.Binding;
  40:             var endpointDiscoveryBehavior = new EndpointDiscoveryBehavior();
  41:             var bindingType = binding.GetType().AssemblyQualifiedName;
  42:             var bindingJson = JsonConvert.SerializeObject(binding);
  43:             endpointDiscoveryBehavior.Extensions.Add(new XElement(CST_XELEMNAME_BINDINGTYPENAME, bindingType));
  44:             endpointDiscoveryBehavior.Extensions.Add(new XElement(CST_XELEMNAME_BINDING, bindingJson));
  45:             endpoint.Behaviors.Add(endpointDiscoveryBehavior);
  46:         }
  47:     }
  48:  
  49:     public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
  50:     {
  51:         // apply the service discovery behavior opertaion
  52:         _serviceDiscoveryBehavior.Validate(serviceDescription, serviceHostBase);
  53:     }
  54: }

Then in my discoverable service I just need to add this attribute at the service class definition and changed the service hosting code accordingly.

   1: [Discoverable]
   2: public class StringService : IStringService
   3: {
   4:     public string ToUpper(string content)
   5:     {
   6:         return content.ToUpper();
   7:     }
   8: }
   1: static void Main(string[] args)
   2: {
   3:     var baseAddress = new Uri(string.Format("http://localhost:11001/stringservice/{0}/", Guid.NewGuid().ToString()));
   4:  
   5:     using (var host = new ServiceHost(typeof(StringService), baseAddress))
   6:     {
   7:         host.Opened += (sender, e) =>
   8:         {
   9:             Console.WriteLine("Service opened at {0}", host.Description.Endpoints.First().ListenUri);
  10:         };
  11:  
  12:         host.AddServiceEndpoint(typeof(IStringService), new BasicHttpBinding(), string.Empty);
  13:  
  14:         host.Open();
  15:  
  16:         Console.WriteLine("Press any key to exit.");
  17:         Console.ReadKey();
  18:     }
  19: }

 

Finally let’s add a new feature of our own discovery service. By default when a service went online it will register all its endpoints for all contracts it implemented. But in some cases we don’t want all endpoints and contracts are discoverable. For example, I have IStringService and ICalculateService contracts and my MixedService class implemented both of them.

   1: [Discoverable]
   2: public class MixedService : IStringService, ICalculateService
   3: {
   4:     public string ToUpper(string content)
   5:     {
   6:         return content.ToUpper();
   7:     }
   8:  
   9:     public int Add(int x, int y)
  10:     {
  11:         return x + y;
  12:     }
  13: }

And when hosting this service I also added a service metadata endpoint.

   1: using (var host = new ServiceHost(typeof(MixedService), baseAddress))
   2: {
   3:     host.Opened += (sender, e) =>
   4:     {
   5:         host.Description.Endpoints.All((ep) =>
   6:         {
   7:             Console.WriteLine(ep.Contract.Name + ": " + ep.ListenUri);
   8:             return true;
   9:         });
  10:     };
  11:  
  12:     var serviceMetadataBehavior = new ServiceMetadataBehavior();
  13:     serviceMetadataBehavior.HttpGetEnabled = true;
  14:     serviceMetadataBehavior.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
  15:     host.Description.Behaviors.Add(serviceMetadataBehavior);
  16:  
  17:     host.AddServiceEndpoint(typeof(IStringService), new BasicHttpBinding(), "string");
  18:     host.AddServiceEndpoint(typeof(ICalculateService), new BasicHttpBinding(), "calculate");
  19:     host.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName, MetadataExchangeBindings.CreateMexHttpBinding(), "mex");
  20:  
  21:     host.Open();
  22:  
  23:     Console.WriteLine("Press any key to exit.");
  24:     Console.ReadKey();
  25: }

So when this service was opened it will register three endpoints into my discovery service: the IStringService, ICalculateService and the IMetadataExchange. How can I not to expose the ICalculateService and the metadata endpoint.

This can be achieved by adding some more information from the attribute to the endpoint discovery behavior, then went to the discovery service. In the attribute I added two parameters which the user can specify which contract types and endpoints they don’t want to be exposed.

   1: private IEnumerable<string> _ignoredContractTypeNames;
   2: private IEnumerable<string> _ignoredEndpoints;
   3:  
   4: public DiscoverableAttribute(string announcementEndpoint, Binding announcementBinding, string[] ignoredContractTypeNames, string[] ignoredEndpoints)
   5: {
   6:     _ignoredContractTypeNames = ignoredContractTypeNames;
   7:     _ignoredEndpoints = ignoredEndpoints;
   8:  
   9:     var endpoint = new AnnouncementEndpoint(announcementBinding, new EndpointAddress(announcementEndpoint));
  10:     var serviceDiscoveryBehavior = new ServiceDiscoveryBehavior();
  11:     serviceDiscoveryBehavior.AnnouncementEndpoints.Add(endpoint);
  12:     _serviceDiscoveryBehavior = serviceDiscoveryBehavior;
  13: }
  14:  
  15: public DiscoverableAttribute(string[] ignoredContractTypeNames, string[] ignoredEndpoints)
  16:     : this(ConfigurationManager.AppSettings[CST_CONFIGKEY_ANNOUNCEMENTENDPOINT],
  17:            Activator.CreateInstance(Type.GetType(ConfigurationManager.AppSettings[CST_CONFIGKEY_ANNOUNCEMENTBINDING], true, true)) as Binding,
  18:            ignoredContractTypeNames, ignoredEndpoints)
  19: {
  20: }
  21:  
  22: public DiscoverableAttribute()
  23:     : this(ConfigurationManager.AppSettings[CST_CONFIGKEY_ANNOUNCEMENTENDPOINT],
  24:            Activator.CreateInstance(Type.GetType(ConfigurationManager.AppSettings[CST_CONFIGKEY_ANNOUNCEMENTBINDING], true, true)) as Binding,
  25:            new string[] { }, new string[] { })
  26: {
  27: }

And then when we need to add the endpoint discovery behavior we will firstly check if this endpoint was in the ignored endpoint list, or the contract of this endpoint was in the ignored contract list. If yes then we will add a XML element into the metadata extension. Otherwise we will add the binding information.

   1: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
   2: {
   3:     // apply the service discovery behavior opertaion
   4:     _serviceDiscoveryBehavior.ApplyDispatchBehavior(serviceDescription, serviceHostBase);
   5:     // add the additional information into the endpoint metadata extension through the endpoint discovery binding for each endpoints
   6:     foreach (var endpoint in serviceDescription.Endpoints)
   7:     {
   8:         var endpointDiscoveryBehavior = new EndpointDiscoveryBehavior();
   9:         // check if the contract or endpoint should be ignored
  10:         var contractTypeName = endpoint.Contract.Name;
  11:         var endpointAddress = endpoint.Address.Uri;
  12:         if (_ignoredContractTypeNames.Any(ctn => string.Compare(ctn, contractTypeName, true) == 0) ||
  13:             _ignoredEndpoints.Any(ep => string.Compare(ep, endpointAddress.Segments.Last(), true) == 0))
  14:         {
  15:             endpointDiscoveryBehavior.Extensions.Add(new XElement(CST_XELEMNAME_IGNORED, true));
  16:         }
  17:         else
  18:         {
  19:             endpointDiscoveryBehavior.Extensions.Add(new XElement(CST_XELEMNAME_IGNORED, false));
  20:             // add the binding infomation
  21:             var binding = endpoint.Binding;
  22:             var bindingType = binding.GetType().AssemblyQualifiedName;
  23:             var bindingJson = JsonConvert.SerializeObject(binding);
  24:             endpointDiscoveryBehavior.Extensions.Add(new XElement(CST_XELEMNAME_BINDINGTYPENAME, bindingType));
  25:             endpointDiscoveryBehavior.Extensions.Add(new XElement(CST_XELEMNAME_BINDING, bindingJson));
  26:         }
  27:         endpoint.Behaviors.Add(endpointDiscoveryBehavior);
  28:     }
  29: }

So when the service sent the announcement message to the discovery service we will know if it should be ignored. In the discovery service we just need to simply check the ignored information from the metadata extension and then perform the register and unregister process accordingly.

   1: protected override IAsyncResult OnBeginOnlineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
   2: {
   3:     var ignored = false;
   4:     bool.TryParse(
   5:         endpointDiscoveryMetadata.Extensions
   6:             .Where(x => x.Name == CST_XELEMNAME_IGNORED)
   7:             .Select(x => x.Value)
   8:             .FirstOrDefault(),
   9:         out ignored);
  10:     if (!ignored)
  11:     {
  12:         _metadataProvider.AddEndpointMetadata(endpointDiscoveryMetadata);
  13:     }
  14:     return new OnOnlineAnnouncementAsyncResult(callback, state);
  15: }
  16:  
  17: protected override IAsyncResult OnBeginOfflineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
  18: {
  19:     var ignored = false;
  20:     bool.TryParse(
  21:         endpointDiscoveryMetadata.Extensions
  22:             .Where(x => x.Name == CST_XELEMNAME_IGNORED)
  23:             .Select(x => x.Value)
  24:             .FirstOrDefault(),
  25:         out ignored);
  26:     if (!ignored)
  27:     {
  28:         _metadataProvider.RemoveEndpointMetadata(endpointDiscoveryMetadata);
  29:     }
  30:     return new OnOfflineAnnouncementAsyncResult(callback, state);
  31: }

The whole logic works like this. The ignored contracts and endpoints were specified on the discoverable service side through the attribute. And then when the discoverable service was opened it will load the service behavior by the attribute and check the endpoints which should be ignored or not, and add the ignored flag into the metadata extension. The metadata will be sent to the discovery service and in the discovery service it will check this flag and determined whether to insert the endpoint into the repository or not.

image

The WCF discovery service behavior utilized the channel dispatcher to hook the event when a service channel was opened and closed to send the service online and offline message to the discovery service. If we can move the check logic into the underlying dispatcher so that it will NOT send message if the contract or endpoint was marked as ignored, which will reduce the network usage.

But unfortunately there’s no way to modify the channel dispatchers of the ServiceDiscoveryBehavior so that we have to send the ignore information to the discovery service.

Now let’s change our discoverable service as below, mark the ICalculateService and metadata endpoint not to be discoverable.

   1: [Discoverable(new string[] { "ICalculateService" }, new string[] { "mex" })]
   2: public class MixedService : IStringService, ICalculateService
   3: {
   4:     public string ToUpper(string content)
   5:     {
   6:         return content.ToUpper();
   7:     }
   8:  
   9:     public int Add(int x, int y)
  10:     {
  11:         return x + y;
  12:     }
  13: }

And as you can see even though there are three endpoints opened but there’s only one endpoint registered in the discovery service.

image

 

Summary

In this post I described how to add more features and flexibility to the discovery service. I demonstrated how to use database as the repository and how to make our discovery service work as a service dispatcher, which implement the dispatcher mode of service scaling-out.

I also demonstrated how to use the service behavior attribute to make the discoverable service easy to be configured. At the end I talked how to make the service contracts and endpoints configurable so that we can choose which of them should be discoverable.

Discovery service was introduced in WCF 4.0. With its functionalities we can build our service farm without having to configure all services information in each service. What we need to do is to call the discovery service and find the target endpoint, which reduced a lot of the service side configuration.

In fact when using discovery service, the service endpoints will not be changed frequently. So we can use cache before the client wanted to invoke a service. So it will firstly look for the local cache to find the endpoint. If yes then it will call the service directly. If not then it will go to the discovery service, get the endpoint, update local cache and invoke. But using cache means increased the complexity of the logic. How to deal with the cache expiration. How to deal with the service endpoint changing.

 

The code can be download here.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.

 

When designing a service oriented architecture (SOA) system, there will be a lot of services with many service contracts, endpoints and behaviors. Besides the client calling the service, in a large distributed system a service may invoke other services. In this case, one service might need to know the endpoints it invokes. This might not be a problem in a small system. But when you have more than 10 services this might be a problem. For example in my current product, there are around 10 services, such as the user authentication service, UI integration service, location service, license service, device monitor service, event monitor service, schedule job service, accounting service, player management service, etc..

 

Benefit of Discovery Service

Since almost all my services need to invoke at least one other service. This would be a difficult task to make sure all services endpoints are configured correctly in every service. And furthermore, it would be a nightmare when a service changed its endpoint at runtime.

image

Hence, we need a discovery service to remove the dependency (configuration dependency). A discovery service plays as a service dictionary which stores the relationship between the contracts and the endpoints for every service. By using the discovery service, when service X wants to invoke service Y, it just need to ask the discovery service where is service Y, then the discovery service will return all proper endpoints of service Y, then service X can use the endpoint to send the request to service Y. And when some services changed their endpoint address, all need to do is to update its records in the discovery service then all others will know its new endpoint.

image

In WCF 4.0 Discovery it supports both managed proxy discovery mode and ad-hoc discovery mode. In ad-hoc mode there is no standalone discovery service. When a client wanted to invoke a service, it will broadcast an message (normally in UDP protocol) to the entire network with the service match criteria. All services which enabled the discovery behavior will receive this message and only those matched services will send their endpoint back to the client.

The managed proxy discovery service works as I described above. In this post I will only cover the managed proxy mode, where there’s a discovery service. For more information about the ad-hoc mode please refer to the MSDN.

 

Service Announcement and Probe

The main functionality of discovery service should be return the proper endpoint addresses back to the service who is looking for. In most cases the consume service (as a client) will send the contract which it wanted to request to the discovery service. And then the discovery service will find the endpoint and respond. Sometimes the contract and endpoint are not enough. It also contains versioning, extensions attributes. This post I will only cover the case includes contract and endpoint.

image

When a client (or sometimes a service who need to invoke another service) need to connect to a target service, it will firstly request the discovery service through the “Probe” method with the criteria. Basically the criteria contains the contract type name of the target service.

Then the discovery service will search its endpoint repository by the criteria. The repository might be a database, a distributed cache or a flat XML file. If it matches, the discovery service will grab the endpoint information (it’s called discovery endpoint metadata in WCF) and send back. And this is called “Probe”.

Finally the client received the discovery endpoint metadata and will use the endpoint to connect to the target service.

Besides the probe, discovery service should take the responsible to know there is a new service available when it goes online, as well as stopped when it goes offline. This feature is named “Announcement”. When a service started and stopped, it will announce to the discovery service.

image

So the basic functionality of a discovery service should includes:

1, An endpoint which receive the service online message, and add the service endpoint information in the discovery repository.

2, An endpoint which receive the service offline message, and remove the service endpoint information from the discovery repository.

3, An endpoint which receive the client probe message, and return the matches service endpoints, and return the discovery endpoint metadata.

WCF 4.0 discovery service just covers all these features in it's infrastructure classes.

 

Discovery Service in WCF 4.0

WCF 4.0 introduced a new assembly named System.ServiceModel.Discovery which has all necessary classes and interfaces to build a WS-Discovery compliant discovery service. It supports ad-hoc and managed proxy modes. For the case mentioned in this post, what we need to build is a standalone discovery service, which is the managed proxy discovery service mode.

To build a managed discovery service in WCF 4.0 just create a new class inherits from the abstract class System.ServiceModel.Discovery.DiscoveryProxy. This class implemented and abstracted the procedures of service announcement and probe. And it exposes 8 abstract methods where we can implement our own endpoint register, unregister and find logic.

These 8 methods are asynchronized, which means all invokes to the discovery service are asynchronously, for better service capability and performance.

1, OnBeginOnlineAnnouncement, OnEndOnlineAnnouncement: Invoked when a service sent the online announcement message. We need to add the endpoint information to the repository in this method.

2, OnBeginOfflineAnnouncement, OnEndOfflineAnnouncement: Invoked when a service sent the offline announcement message. We need to remove the endpoint information from the repository in this method.

3, OnBeginFind, OnEndFind: Invoked when a client sent the probe message that want to find the service endpoint information. We need to look for the proper endpoints by matching the client’s criteria through the repository in this method.

4, OnBeginResolve, OnEndResolve: Invoked then a client sent the resolve message. Different from the find method, when using resolve method the discovery service will return the exactly one service endpoint metadata to the client. In our example we will NOT implement this method.

 

Let’s create our own discovery service, inherit the base System.ServiceModel.Discovery.DiscoveryProxy. We also need to specify the service behavior in this class. Since the build-in discovery service host class only support the singleton mode, we must set its instance context mode to single.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.ServiceModel.Discovery;
   6: using System.ServiceModel;
   7:  
   8: namespace Phare.Service
   9: {
  10:     [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
  11:     public class ManagedProxyDiscoveryService : DiscoveryProxy
  12:     {
  13:         protected override IAsyncResult OnBeginFind(FindRequestContext findRequestContext, AsyncCallback callback, object state)
  14:         {
  15:             throw new NotImplementedException();
  16:         }
  17:  
  18:         protected override IAsyncResult OnBeginOfflineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
  19:         {
  20:             throw new NotImplementedException();
  21:         }
  22:  
  23:         protected override IAsyncResult OnBeginOnlineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
  24:         {
  25:             throw new NotImplementedException();
  26:         }
  27:  
  28:         protected override IAsyncResult OnBeginResolve(ResolveCriteria resolveCriteria, AsyncCallback callback, object state)
  29:         {
  30:             throw new NotImplementedException();
  31:         }
  32:  
  33:         protected override void OnEndFind(IAsyncResult result)
  34:         {
  35:             throw new NotImplementedException();
  36:         }
  37:  
  38:         protected override void OnEndOfflineAnnouncement(IAsyncResult result)
  39:         {
  40:             throw new NotImplementedException();
  41:         }
  42:  
  43:         protected override void OnEndOnlineAnnouncement(IAsyncResult result)
  44:         {
  45:             throw new NotImplementedException();
  46:         }
  47:  
  48:         protected override EndpointDiscoveryMetadata OnEndResolve(IAsyncResult result)
  49:         {
  50:             throw new NotImplementedException();
  51:         }
  52:     }
  53: }

Then let’s implement the online, offline and find methods one by one. WCF discovery service gives us full flexibility to implement the endpoint add, remove and find logic. For the demo purpose we will use an internal dictionary to store the services’ endpoint metadata.

In the next post we will see how to serialize and store these information in database.

Define a concurrent dictionary inside the service class since our it will be used in the multiple threads scenario.

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
   2: public class ManagedProxyDiscoveryService : DiscoveryProxy
   3: {
   4:     private ConcurrentDictionary<EndpointAddress, EndpointDiscoveryMetadata> _services;
   5:  
   6:     public ManagedProxyDiscoveryService()
   7:     {
   8:         _services = new ConcurrentDictionary<EndpointAddress, EndpointDiscoveryMetadata>();
   9:     }
  10: }

Then we can simply implement the logic of service online and offline.

   1: protected override IAsyncResult OnBeginOnlineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
   2: {
   3:     _services.AddOrUpdate(endpointDiscoveryMetadata.Address, endpointDiscoveryMetadata, (key, value) => endpointDiscoveryMetadata);
   4:     return new OnOnlineAnnouncementAsyncResult(callback, state);
   5: }
   6:  
   7: protected override void OnEndOnlineAnnouncement(IAsyncResult result)
   8: {
   9:     OnOnlineAnnouncementAsyncResult.End(result);
  10: }
  11:  
  12: protected override IAsyncResult OnBeginOfflineAnnouncement(DiscoveryMessageSequence messageSequence, EndpointDiscoveryMetadata endpointDiscoveryMetadata, AsyncCallback callback, object state)
  13: {
  14:     EndpointDiscoveryMetadata endpoint = null;
  15:     _services.TryRemove(endpointDiscoveryMetadata.Address, out endpoint);
  16:     return new OnOfflineAnnouncementAsyncResult(callback, state);
  17: }
  18:  
  19: protected override void OnEndOfflineAnnouncement(IAsyncResult result)
  20: {
  21:     OnOfflineAnnouncementAsyncResult.End(result);
  22: }

Regards the find method, the parameter FindRequestContext.Criteria has a method named IsMatch, which can be use for us to evaluate which service metadata is satisfied with the criteria. So the implementation of find method would be like this.

   1: protected override IAsyncResult OnBeginFind(FindRequestContext findRequestContext, AsyncCallback callback, object state)
   2: {
   3:     _services.Where(s => findRequestContext.Criteria.IsMatch(s.Value))
   4:              .Select(s => s.Value)
   5:              .All(meta =>
   6:              {
   7:                  findRequestContext.AddMatchingEndpoint(meta);
   8:                  return true;
   9:              });
  10:     return new OnFindAsyncResult(callback, state);
  11: }
  12:  
  13: protected override void OnEndFind(IAsyncResult result)
  14: {
  15:     OnFindAsyncResult.End(result);
  16: }

As you can see, we checked all endpoints metadata in repository by invoking the IsMatch method. Then add all proper endpoints metadata into the parameter.

Finally since all these methods are asynchronized we need some AsyncResult classes as well. Below are the base class and the inherited classes used in previous methods.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Threading;
   6:  
   7: namespace Phare.Service
   8: {
   9:     abstract internal class AsyncResult : IAsyncResult
  10:     {
  11:         AsyncCallback callback;
  12:         bool completedSynchronously;
  13:         bool endCalled;
  14:         Exception exception;
  15:         bool isCompleted;
  16:         ManualResetEvent manualResetEvent;
  17:         object state;
  18:         object thisLock;
  19:  
  20:         protected AsyncResult(AsyncCallback callback, object state)
  21:         {
  22:             this.callback = callback;
  23:             this.state = state;
  24:             this.thisLock = new object();
  25:         }
  26:  
  27:         public object AsyncState
  28:         {
  29:             get
  30:             {
  31:                 return state;
  32:             }
  33:         }
  34:  
  35:         public WaitHandle AsyncWaitHandle
  36:         {
  37:             get
  38:             {
  39:                 if (manualResetEvent != null)
  40:                 {
  41:                     return manualResetEvent;
  42:                 }
  43:                 lock (ThisLock)
  44:                 {
  45:                     if (manualResetEvent == null)
  46:                     {
  47:                         manualResetEvent = new ManualResetEvent(isCompleted);
  48:                     }
  49:                 }
  50:                 return manualResetEvent;
  51:             }
  52:         }
  53:  
  54:         public bool CompletedSynchronously
  55:         {
  56:             get
  57:             {
  58:                 return completedSynchronously;
  59:             }
  60:         }
  61:  
  62:         public bool IsCompleted
  63:         {
  64:             get
  65:             {
  66:                 return isCompleted;
  67:             }
  68:         }
  69:  
  70:         object ThisLock
  71:         {
  72:             get
  73:             {
  74:                 return this.thisLock;
  75:             }
  76:         }
  77:  
  78:         protected static TAsyncResult End<TAsyncResult>(IAsyncResult result)
  79:             where TAsyncResult : AsyncResult
  80:         {
  81:             if (result == null)
  82:             {
  83:                 throw new ArgumentNullException("result");
  84:             }
  85:  
  86:             TAsyncResult asyncResult = result as TAsyncResult;
  87:  
  88:             if (asyncResult == null)
  89:             {
  90:                 throw new ArgumentException("Invalid async result.", "result");
  91:             }
  92:  
  93:             if (asyncResult.endCalled)
  94:             {
  95:                 throw new InvalidOperationException("Async object already ended.");
  96:             }
  97:  
  98:             asyncResult.endCalled = true;
  99:  
 100:             if (!asyncResult.isCompleted)
 101:             {
 102:                 asyncResult.AsyncWaitHandle.WaitOne();
 103:             }
 104:  
 105:             if (asyncResult.manualResetEvent != null)
 106:             {
 107:                 asyncResult.manualResetEvent.Close();
 108:             }
 109:  
 110:             if (asyncResult.exception != null)
 111:             {
 112:                 throw asyncResult.exception;
 113:             }
 114:  
 115:             return asyncResult;
 116:         }
 117:  
 118:         protected void Complete(bool completedSynchronously)
 119:         {
 120:             if (isCompleted)
 121:             {
 122:                 throw new InvalidOperationException("This async result is already completed.");
 123:             }
 124:  
 125:             this.completedSynchronously = completedSynchronously;
 126:  
 127:             if (completedSynchronously)
 128:             {
 129:                 this.isCompleted = true;
 130:             }
 131:             else
 132:             {
 133:                 lock (ThisLock)
 134:                 {
 135:                     this.isCompleted = true;
 136:                     if (this.manualResetEvent != null)
 137:                     {
 138:                         this.manualResetEvent.Set();
 139:                     }
 140:                 }
 141:             }
 142:  
 143:             if (callback != null)
 144:             {
 145:                 callback(this);
 146:             }
 147:         }
 148:  
 149:         protected void Complete(bool completedSynchronously, Exception exception)
 150:         {
 151:             this.exception = exception;
 152:             Complete(completedSynchronously);
 153:         }
 154:     }
 155: }
   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.ServiceModel.Discovery;
   6: using Phare.Service;
   7:  
   8: namespace Phare.Service
   9: {
  10:     internal sealed class OnOnlineAnnouncementAsyncResult : AsyncResult
  11:     {
  12:         public OnOnlineAnnouncementAsyncResult(AsyncCallback callback, object state)
  13:             : base(callback, state)
  14:         {
  15:             this.Complete(true);
  16:         }
  17:  
  18:         public static void End(IAsyncResult result)
  19:         {
  20:             AsyncResult.End<OnOnlineAnnouncementAsyncResult>(result);
  21:         }
  22:  
  23:     }
  24:  
  25:     sealed class OnOfflineAnnouncementAsyncResult : AsyncResult
  26:     {
  27:         public OnOfflineAnnouncementAsyncResult(AsyncCallback callback, object state)
  28:             : base(callback, state)
  29:         {
  30:             this.Complete(true);
  31:         }
  32:  
  33:         public static void End(IAsyncResult result)
  34:         {
  35:             AsyncResult.End<OnOfflineAnnouncementAsyncResult>(result);
  36:         }
  37:     }
  38:  
  39:     sealed class OnFindAsyncResult : AsyncResult
  40:     {
  41:         public OnFindAsyncResult(AsyncCallback callback, object state)
  42:             : base(callback, state)
  43:         {
  44:             this.Complete(true);
  45:         }
  46:  
  47:         public static void End(IAsyncResult result)
  48:         {
  49:             AsyncResult.End<OnFindAsyncResult>(result);
  50:         }
  51:     }
  52:  
  53:     sealed class OnResolveAsyncResult : AsyncResult
  54:     {
  55:         EndpointDiscoveryMetadata matchingEndpoint;
  56:  
  57:         public OnResolveAsyncResult(EndpointDiscoveryMetadata matchingEndpoint, AsyncCallback callback, object state)
  58:             : base(callback, state)
  59:         {
  60:             this.matchingEndpoint = matchingEndpoint;
  61:             this.Complete(true);
  62:         }
  63:  
  64:         public static EndpointDiscoveryMetadata End(IAsyncResult result)
  65:         {
  66:             OnResolveAsyncResult thisPtr = AsyncResult.End<OnResolveAsyncResult>(result);
  67:             return thisPtr.matchingEndpoint;
  68:         }
  69:     }
  70: }

Now we have finished the discovery service. The next step is to host it. The discovery service is a standard WCF service. So we can use ServiceHost on a console application, windows service, or in IIS as usual. The following code is how to host the discovery service we had just created in a console application.

   1: static void Main(string[] args)
   2: {
   3:     using (var host = new ServiceHost(new ManagedProxyDiscoveryService()))
   4:     {
   5:         host.Opened += (sender, e) =>
   6:         {
   7:             host.Description.Endpoints.All((ep) =>
   8:             {
   9:                 Console.WriteLine(ep.ListenUri);
  10:                 return true;
  11:             });
  12:         };
  13:  
  14:         try
  15:         {
  16:             // retrieve the announcement, probe endpoint and binding from configuration
  17:             var announcementEndpointAddress = new EndpointAddress(ConfigurationManager.AppSettings["announcementEndpointAddress"]);
  18:             var probeEndpointAddress = new EndpointAddress(ConfigurationManager.AppSettings["probeEndpointAddress"]);
  19:             var binding = Activator.CreateInstance(Type.GetType(ConfigurationManager.AppSettings["bindingType"], true, true)) as Binding;
  20:             var announcementEndpoint = new AnnouncementEndpoint(binding, announcementEndpointAddress);
  21:             var probeEndpoint = new DiscoveryEndpoint(binding, probeEndpointAddress);
  22:             probeEndpoint.IsSystemEndpoint = false;
  23:             // append the service endpoint for announcement and probe
  24:             host.AddServiceEndpoint(announcementEndpoint);
  25:             host.AddServiceEndpoint(probeEndpoint);
  26:  
  27:             host.Open();
  28:  
  29:             Console.WriteLine("Press any key to exit.");
  30:             Console.ReadKey();
  31:         }
  32:         catch (Exception ex)
  33:         {
  34:             Console.WriteLine(ex.ToString());
  35:         }
  36:     }
  37:  
  38:     Console.WriteLine("Done.");
  39:     Console.ReadKey();
  40: }

What we need to notice is that, the discovery service needs two endpoints for announcement and probe. In this example I just retrieve them from the configuration file. I also specified the binding of these two endpoints in configuration file as well.

   1: <?xml version="1.0"?>
   2: <configuration>
   3:   <startup>
   4:     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
   5:   </startup>
   6:   <appSettings>
   7:     <add key="announcementEndpointAddress" value="net.tcp://localhost:10010/announcement"/>
   8:     <add key="probeEndpointAddress" value="net.tcp://localhost:10011/probe"/>
   9:     <add key="bindingType" value="System.ServiceModel.NetTcpBinding, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
  10:   </appSettings>
  11: </configuration>

And this is the console screen when I ran my discovery service. As you can see there are two endpoints listening for announcement message and probe message.

image

 

Discoverable Service and Client

Next, let’s create a WCF service that is discoverable, which means it can be found by the discovery service. To do so, we need to let the service send the online announcement message to the discovery service, as well as offline message before it shutdown.

Just create a simple service which can make the incoming string to upper. The service contract and implementation would be like this.

   1: [ServiceContract]
   2: public interface IStringService
   3: {
   4:     [OperationContract]
   5:     string ToUpper(string content);
   6: }
   1: public class StringService : IStringService
   2: {
   3:     public string ToUpper(string content)
   4:     {
   5:         return content.ToUpper();
   6:     }
   7: }

Then host this service in the console application. In order to make the discovery service easy to be tested the service address will be changed each time it’s started.

   1: static void Main(string[] args)
   2: {
   3:     var baseAddress = new Uri(string.Format("net.tcp://localhost:11001/stringservice/{0}/", Guid.NewGuid().ToString()));
   4:  
   5:     using (var host = new ServiceHost(typeof(StringService), baseAddress))
   6:     {
   7:         host.Opened += (sender, e) =>
   8:         {
   9:             Console.WriteLine("Service opened at {0}", host.Description.Endpoints.First().ListenUri);
  10:         };
  11:  
  12:         host.AddServiceEndpoint(typeof(IStringService), new NetTcpBinding(), string.Empty);
  13:  
  14:         host.Open();
  15:  
  16:         Console.WriteLine("Press any key to exit.");
  17:         Console.ReadKey();
  18:     }
  19: }

Currently this service is NOT discoverable. We need to add a special service behavior so that it could send the online and offline message to the discovery service announcement endpoint when the host is opened and closed. WCF 4.0 introduced a service behavior named ServiceDiscoveryBehavior. When we specified the announcement endpoint address and appended it to the service behaviors this service will be discoverable.

   1: var announcementAddress = new EndpointAddress(ConfigurationManager.AppSettings["announcementEndpointAddress"]);
   2: var announcementBinding = Activator.CreateInstance(Type.GetType(ConfigurationManager.AppSettings["bindingType"], true, true)) as Binding;
   3: var announcementEndpoint = new AnnouncementEndpoint(announcementBinding, announcementAddress);
   4: var discoveryBehavior = new ServiceDiscoveryBehavior();
   5: discoveryBehavior.AnnouncementEndpoints.Add(announcementEndpoint);
   6: host.Description.Behaviors.Add(discoveryBehavior);

The ServiceDiscoveryBehavior utilizes the service extension and channel dispatcher to implement the online and offline announcement logic. In short, it injected the channel open and close procedure and send the online and offline message to the announcement endpoint.

 

On client side, when we have the discovery service, a client can invoke a service without knowing its endpoint. WCF discovery assembly provides a class named DiscoveryClient, which can be used to find the proper service endpoint by passing the criteria.

In the code below I initialized the DiscoveryClient, specified the discovery service probe endpoint address. Then I created the find criteria by specifying the service contract I wanted to use and invoke the Find method. This will send the probe message to the discovery service and it will find the endpoints back to me.

The discovery service will return all endpoints that matches the find criteria, which means in the result of the find method there might be more than one endpoints. In this example I just returned the first matched one back. In the next post I will show how to extend our discovery service to make it work like a service load balancer.

   1: static EndpointAddress FindServiceEndpoint()
   2: {
   3:     var probeEndpointAddress = new EndpointAddress(ConfigurationManager.AppSettings["probeEndpointAddress"]);
   4:     var probeBinding = Activator.CreateInstance(Type.GetType(ConfigurationManager.AppSettings["bindingType"], true, true)) as Binding;
   5:     var discoveryEndpoint = new DiscoveryEndpoint(probeBinding, probeEndpointAddress);
   6:  
   7:     EndpointAddress address = null;
   8:     FindResponse result = null;
   9:     using (var discoveryClient = new DiscoveryClient(discoveryEndpoint))
  10:     {
  11:         result = discoveryClient.Find(new FindCriteria(typeof(IStringService)));
  12:     }
  13:  
  14:     if (result != null && result.Endpoints.Any())
  15:     {
  16:         var endpointMetadata = result.Endpoints.First();
  17:         address = endpointMetadata.Address;
  18:     }
  19:     return address;
  20: }

Once we probed the discovery service we will receive the endpoint. So in the client code we can created the channel factory from the endpoint and binding, and invoke to the service.

When creating the client side channel factory we need to make sure that the client side binding should be the same as the service side. WCF discovery service can be used to find the endpoint for a service contract, but the binding is NOT included. This is because the binding was not in the WS-Discovery specification.

In the next post I will demonstrate how to add the binding information into the discovery service. At that moment the client don’t need to create the binding by itself. Instead it will use the binding received from the discovery service.

   1: static void Main(string[] args)
   2: {
   3:     Console.WriteLine("Say something...");
   4:     var content = Console.ReadLine();
   5:     while (!string.IsNullOrWhiteSpace(content))
   6:     {
   7:         Console.WriteLine("Finding the service endpoint...");
   8:         var address = FindServiceEndpoint();
   9:         if (address == null)
  10:         {
  11:             Console.WriteLine("There is no endpoint matches the criteria.");
  12:         }
  13:         else
  14:         {
  15:             Console.WriteLine("Found the endpoint {0}", address.Uri);
  16:  
  17:             var factory = new ChannelFactory<IStringService>(new NetTcpBinding(), address);
  18:             factory.Opened += (sender, e) =>
  19:             {
  20:                 Console.WriteLine("Connecting to {0}.", factory.Endpoint.ListenUri);
  21:             };
  22:             var proxy = factory.CreateChannel();
  23:             using (proxy as IDisposable)
  24:             {
  25:                 Console.WriteLine("ToUpper: {0} => {1}", content, proxy.ToUpper(content));
  26:             }
  27:         }
  28:  
  29:         Console.WriteLine("Say something...");
  30:         content = Console.ReadLine();
  31:     }
  32: }

Similarly, the discovery service probe endpoint and binding were defined in the configuration file.

   1: <?xml version="1.0"?>
   2: <configuration>
   3:   <startup>
   4:     <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
   5:   </startup>
   6:   <appSettings>
   7:     <add key="announcementEndpointAddress" value="net.tcp://localhost:10010/announcement"/>
   8:     <add key="probeEndpointAddress" value="net.tcp://localhost:10011/probe"/>
   9:     <add key="bindingType" value="System.ServiceModel.NetTcpBinding, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
  10:   </appSettings>
  11: </configuration>

OK, now let’s have a test. Firstly start the discovery service, and then start our discoverable service. When it started it will announced to the discovery service and registered its endpoint into the repository, which is the local dictionary. And then start the client and type something. As you can see the client asked the discovery service for the endpoint and then establish the connection to the discoverable service.

image

And more interesting, do NOT close the client console but terminate the discoverable service but press the enter key. This will make the service send the offline message to the discovery service. Then start the discoverable service again. Since we made it use a different address each time it started, currently it should be hosted on another address. If we enter something in the client we could see that it asked the discovery service and retrieve the new endpoint, and connect the the service.

image

 

Summary

In this post I discussed the benefit of using the discovery service and the procedures of service announcement and probe. I also demonstrated how to leverage the WCF Discovery feature in WCF 4.0 to build a simple managed discovery service.

For test purpose, in this example I used the in memory dictionary as the discovery endpoint metadata repository. And when finding I also just return the first matched endpoint back. I also hard coded the bindings between the discoverable service and the client.

In next post I will show you how to solve the problem mentioned above, as well as some additional feature for production usage.

You can download the code here.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.