Saturday, September 2, 2017

Microsoft 70-487: Configure WCF services by using the API

Exam Objectives

Configure service behaviors; configure service endpoints; configure binding; specify a service contract; expose service metadata (XSDs, WSDL, and metadata exchange); WCF routing and discovery features


Quick Overview of Training Materials

Exam Ref 70-487 - Chapter 3.3
[Book] Programming WCF Services (in particular, Appendix C: Discovery)
[MSDN] Configuring WCF Services in Code (.NET 4.5+)


Trade-off of Programmatic Configuration


The book includes a note on programmatic vs "administrative" (i.e. config file based) configuration [Safari books].  The author notes that a config file gives one the ability the change the behavior of the service without the need to rebuild or redeploy (though you'd have to restart it).  The drawback being that editing a text based config file is potentially error prone.  The fourth edition of the book notes that newer versions of Visual Studio include functionality to validate config changes... which is helpful to the programmer testing the service, not so much to the admin trying to tweak the deployment.

Programmatic configuration allows the developer to fix aspects of the configuration that specifically should not be changed by the system admin, and also allows the system to be modified dynamically at runtime.  While I thought I saw somewhere something about type safety with programmatic configuration... I can't seem to find the reference.   

The book doesn't spend a lot of time focused on configuration specifically.  The distinction is made in the first chapter, and specific examples given for some endpoint and client config... thereafter it seems to focus on the actual functionality.



Service Behaviors, Endpoints, Bindings, and Contracts


The fact that I'm able to demonstrate all of these facets of the service (actually, all these plus the metadata, really) just goes to illustrate how simple programmatic config can really be.  Here is the code, which creates the functional equivalent of the chat server I played with in the "Create A WCF Service" post:



static void Main(string[] args)
{
   using (ServiceHost host = new ServiceHost(typeof(ChatManagerService), 
                                   new Uri("http://localhost:8080/chatmgr")))
   {
      ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
      smb.HttpGetEnabled = true;

      ProfanityInterceptorBehavior pib = new ProfanityInterceptorBehavior();

      host.Description.Behaviors.Add(smb);
      host.Description.Behaviors.Add(pib);

      //Manually create the service endpoint, and add to host collection
      Binding binding = new WSDualHttpBinding();
      EndpointAddress address = new EndpointAddress(host.BaseAddresses[0] + "/duplex");
      ContractDescription description = 
         ContractDescription.GetContract(typeof(IChatManager));
      ServiceEndpoint endpoint = new ServiceEndpoint(description, binding, address);
      host.AddServiceEndpoint(endpoint);

      //Add the metadata endpoint directly
      host.AddServiceEndpoint(typeof(IMetadataExchange), 
                        MetadataExchangeBindings.CreateMexHttpBinding(), 
                        "mex");

      host.Open();
      PrintServiceDescription(host);
      Console.ReadKey();
   }
}

The ServiceMetadata and ProfanityInterceptor behaviors are instantiated directly, and any configuration performed by changing their properties (such as setting HttpGetEnabled to "true").  These are then added to the service's "Description" object, in the "Behaviors" collection.

I explored two different ways of specifying an endpoint:  manually creating the ServiceEndpoint object, and the other using the AddServiceEndpoint method on the host.  For the manual process, creating the binding was simply a matter of creating a new instance.  The address requires a full, valid URL, so I had to concatenate the host's base address myself.  Finally, the ContractDescription was a bit tricky.  At first I tried to new up the ContractDescription object myself, but it was throwing a null pointer exception when I tried to add it to the service, and it wasn't showing any operations.  When I used the static method on the ContractDescription class, it worked as expected.  Digging into the code, it seems this method is using reflection to put together the full description.

In contrast, adding the metadata endpoint with AddServiceEndpoint is a one-liner.  I'm guessing under the covers, the method is calling the same GetContract() method using the contract type, and appending the address string to the baseAddress (if it exists).

The resulting ServiceHost behaves just as the previous config-file based version:





Expose Service Metadata


So, after playing with it a bit, I did find one minor issue with the above code.  It doesn't actually publish the service metadata.  Whoops.  For whatever reason, whenever I tried to hit the configured metadata endpoint, I would get an HTTP 400 error in return.  When I configured the HttpGetUrl property on the ServiceMetadataBehavior instance, it worked.  I did also need to modify the message inspector to ignore requests not bound for the duplex endpoint (making me wonder if that wouldn't be better implemented as an endpoint behavior instead... hmm...).

The only other interesting bit about publishing the metadata was that the endpoint was created using a static method on the MetadataExchangeBindings class.

As an aside, I did end up applying the profanity inspector behavior as an endpoint behavior rather than a service behavior.  The behavior needed a couple small tweaks to implement the IEndpointBehavior class, but nothing major.



Routing


The primary resources I used for routing are the MSDN articles on routing in WCF, and the routing module in WCF Power Topics (PluralSight).

A routing service acts as an intermediary between the client and one or more service endpoints that can service the requests of that client.  Interjecting a router between a client and a service starts relatively straightforward:

  • Change the outbound address on the client to the router, but do not change the contract
  • In the router, add the service as a client endpoint
  • Configure a filter to match any incoming request to the service endpoint

Both the documentation and the PluralSight course note a number of use cases where this kind of routing is valuable, such as service aggregation, bridging between protocols, versioning, fail over, and broadcasting (among others).  The first step in creating a routing service... is creating the routing service, which means implementing a service contract.  In the case of WCF routing, there are several service contracts out of the box:
  • IRequestReplyRouter - provides standard request/reply semantics.  Requests are sent only to the first matching endpoint.
  • ISimplexDatagramRouter and ISimplexSessionRouter - one-way routers
  • IDuplexSessionRouter - creates a duplex channel with a callback contract, allows transactions.


The next piece of the puzzle is the message filter.  This is what determines which endpoint actually receives an incoming message.  There are a number of built in message filters available:
  • Action - checks the Action header of the message against a table.
  • EndpointAddress - checks the To header and matches against a target endpoint.
  • PrefixEndpointAddress - checks the To header for the longest matching prefix.
  • And - takes two other filters as arguments, and matches when both are true
  • Custom - a class extending MessageFilter that implements custom matching logic
  • EndpointName - matches based on the name of the endpoint (from Message.Properties)
  • MatchAll - satisfied by any non-null message
  • MatchNone - satisfied only by null message
  • XPath - uses an XPath query to determine if a message contains a specific XML construct


Message filters are grouped into a filter table, which is used by the routing service behavior to evaluate incoming messages.  Filters in the table may be of different types, can include a priority (the higher the better) to give filters different precedence, and may also include a backup list.  This backup list is nothing more than a list of endpoint names that are tried if the matched endpoint fails to respond.  I tried to map it out here a little:



Configuring routing programmatically is a bit different than the config file based setup.  The biggest difference I noticed is that there is not a "backup list" notion in the programmatic setup.  However, if you add multiple endpoints to the same filter, they will be tried in order.  The following code snippet creates a service that behaves almost identically to the one using the above config file:


        static void Main(string[] args)
        {
            using (ServiceHost host = new ServiceHost(
                typeof(System.ServiceModel.Routing.RoutingService),
                new Uri("http://localhost:14552/router")))
            {
                ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
                smb.HttpGetEnabled = true;
                smb.HttpGetUrl = new Uri(host.BaseAddresses[0] + "/mex");
                host.Description.Behaviors.Add(smb);

                var routingConfig = new RoutingConfiguration();
                var filter = new MatchAllMessageFilter();
                var endpoints = new List<ServiceEndpoint>();
                endpoints.Add(getEchoClient(15560));
                endpoints.Add(getEchoClient(33344));
                endpoints.Add(getEchoClient(22455));
                routingConfig.FilterTable.Add(filter, endpoints, 2);

                RoutingBehavior routingBehavior = new RoutingBehavior(routingConfig);


                host.Description.Behaviors.Add(routingBehavior);

                host.AddServiceEndpoint(typeof(IRequestReplyRouter), 
                    new BasicHttpBinding(), "");

                host.Open();
                PrintServiceDescription(host);
                Console.ReadKey();
            }
        }

        private static ServiceEndpoint getEchoClient(int port)
        {
            Binding binding = new BasicHttpBinding();
            binding.ReceiveTimeout = TimeSpan.FromMilliseconds(100);
            binding.SendTimeout    = TimeSpan.FromMilliseconds(100);
            binding.CloseTimeout   = TimeSpan.FromMilliseconds(100);
            binding.OpenTimeout    = TimeSpan.FromMilliseconds(100);
            EndpointAddress address = 
                new EndpointAddress("http://localhost:" + port + "/print");
            ContractDescription description = 
                ContractDescription.GetContract(typeof(IPrintService));
            return new ServiceEndpoint(description, binding, address);
        }


One change I made for this sample was to make all the timeouts really low.  I noticed that as I started to take services offline, the wait on the client became longer and longer.  I think this is due to the fact that the router was cycling through all the endpoints in order, waiting for each to timeout or otherwise fail before moving on to the next.  By making the timeouts short, it still behaved as expected without taking half of forever to respond.

Another difference is that the routing behavior takes a "RoutingConfiguration" object as an argument. This routing config object is where the filter tables are set up, as well as other config such as ensuring ordered dispatch, enabling SOAP processing, and restricting routing to headers.

This sample is on GitHub: Routing Demo.

The docs include a  number of demos:

  • using XPath filters to route based on versions [link]
  • using EndpointName filters to route based on service levels [link]
  • using MatchAll filter with a backup list for failure handling [link]



One last note on routing.  It is possible to programmatically change the routing configuration at runtime.  By creating a new RoutingConfiguration and calling ApplyConfiguration on the RoutingExtension on the service host, routing can be changed dynamically.  The docs also include a demo of this process [link].  The key piece is this line of code:


// Use ApplyConfiguration to update the Routing Service
serviceHost.Extensions.Find<RoutingExtension>().ApplyConfiguration(rc); 




Discovery


The aim of WCF Discovery is to enable services to "broadcast" or "publish" their availability to clients configured to look for them.  This means the the address and port of a service may be completely unknown to a client, but can be "discovered" using this functionality.  It is actually quite simple to configure.

As a demo, I modified the above routing demo to use discoverability, so rather than three service endpoints with fixed ports, I created one service that listens on a random port, so I can create as many instances as I want (well... technically I could create like 60,000 but you get what I mean).  To make a service discoverable, it is only necessary to 1) add the Service Discovery behavior and 2) add the Udp Discovery endpoint.  Something like this:

//Make host discoverable with built in behavior, and add endpoint
host.Description.Behaviors.Add(new ServiceDiscoveryBehavior());
host.AddServiceEndpoint(new UdpDiscoveryEndpoint());


These are built in to WCF, and it is literally that easy.  A service with these two lines of code is now discoverable.  It is also possible to configure discoverability with a config file, as explained in the MSDN article Configuring Discovery in a Configuration File.  Basically, you add an endpoint to the service like this:

    <services>
      <service name="RoutingShared.EchoService">
        <endpoint kind="udpDiscoveryEndpoint" />
      </service>
    </services>


Then it is just a matter of adding the service behavior:

    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceDiscovery/>
        </behavior>
      </serviceBehaviors>
    </behaviors>


On the client side, we can take advantage of this discoverability by using the built in DiscoveryClient.  The following snipped of code will look for a maximum of 2 seconds for any services that implement the IEchoService contract, returning a list of EndpointAddress objects:

static IEnumerable<EndpointAddress> FindServiceAddresses()
{
    // Create DiscoveryClient  
    DiscoveryClient discoveryClient = new DiscoveryClient(new UdpDiscoveryEndpoint());
    Console.WriteLine("Scanning for endpoints...");
    FindResponse findResponse = discoveryClient.Find(
        new FindCriteria(typeof(IEchoService)) {
            Duration = TimeSpan.FromSeconds(2)
        });
    return findResponse.Endpoints.Select(x => x.Address);
}


These EndpointAddress objects are then used to create a new RoutingConfiguration object.  This routing configuration can then be dynamically substituted for the existing routing config.  Running this search, update, replace workflow in a separate thread periodically allows the routing service to automatically detect new service instances and add them to the endpoint collection.



Discovery also supports the notion of "Announcements", which in the above service could largely replace the polling mechanism.  With announcements, the service will send "Hello" and "Bye" messages in a multicast fashion, and any announcement clients will receive these messages.  One drawback of this approach is that for it to be effective (at least with service shutdown, or "Bye" messages), the service needs to shut down gracefully.  If the process is killed, then the "Bye" message will not be sent and clients may believe the service is still available.  The WCF Power Topics course includes a demo of announcements.

Like the discovery functionality, the announcement logic is all included out of the box, it is simply a matter of a little wiring.  The service itself need only add an announcement endpoint to the discovery endpoint behavior:


//Make host discoverable with built in behavior, and add endpoint
var sdb = new ServiceDiscoveryBehavior();
sdb.AnnouncementEndpoints.Add(new UdpAnnouncementEndpoint());
host.Description.Behaviors.Add(sdb);
host.AddServiceEndpoint(new UdpDiscoveryEndpoint());


The client has a little more work to do, but not much.  The client must create an announcement service, add event handlers for the Online and Offline announcement events, add a built in endpoint, and open the service:

// Create an AnnouncementService instance  
AnnouncementService announcementService = new AnnouncementService();

// Subscribe the announcement events  
announcementService.OnlineAnnouncementReceived += (s,e) => {
    Console.WriteLine("New service online, updating endpoints...");
    UpdateEndpoints(host);
};
announcementService.OfflineAnnouncementReceived += (s, e) => {
    Console.WriteLine("Existing service offline, updating endpoints...");
    UpdateEndpoints(host);
};

ServiceHost announcementServiceHost = new ServiceHost(announcementService);
// Listen for the announcements sent over UDP multicast  
announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
announcementServiceHost.Open();
Console.WriteLine("\n");
PrintServiceDescription(announcementServiceHost);


And voila, when new service come online, the routing service will automatically update the endpoints.  As always these demos are available on GitHub: Routing Discovery Demo.





No comments:

Post a Comment