You’re about to be introduced to the WCF service. This lab isn’t your typical “Hello World”—it’s “Hello Indigo”! In this lab, you will learn how to build a new WCF service and in the process learn the minimum requirements of service development and consumption. Here’s a short list of things you’ll accomplish:
Create a new service contract and service implementation
Programmatically configure a service host, its endpoints, and bindings
Create a client application and open a client channel proxy to invoke the service
Now, before you start thinking “been there, done that,” this simple lab will be slightly
different because I’m going to give you some practical design tips that ensure
configurability and appropriate decoupling of service, host, and client. In addition, I’ll
be diving deeper into basic concepts such as services, service contracts, endpoints,
bindings, ServiceHost
, and channels.
In this first lab, you will create a new solution with three projects: a service, a host, and a client. When you run the service host, you’ll expose a single service endpoint. The client application will access service operations through that endpoint. You’ll host the service in a console application and invoke the service using a manually constructed proxy. This lab will teach you the basic requirements for creating, hosting, and consuming a service with WCF.
The first thing you will do is create a new service contract with a single operation and implement this contract on a new service type.
In this lab, everything begins from scratch, so you’ll start by creating a new Visual Studio solution. Open a new instance of Visual Studio 2008. Select File → New → Project, and from the New Project dialog, create a new Blank Solution in the <YourLearningWCFPath>\Labs\Chapter1 directory. Name the solution
ServiceFromScratch
. Click OK to create the empty solution.Create the service project. From Solution Explorer, right-click on the solution node and select Add → New Project. Select the Class Library template, name the project
HelloIndigo
, and make sure the location path matches the solution at <YourLearningWCFPath>\Labs\Chapter1\ServiceFromScratch. Click OK to create the new project.Now you will create your first service contract. From Solution Explorer, rename the project’s only class file to Service.cs. Open this file in the code window.
Add a new interface named
IHelloIndigoService
in Service.cs. Add a single method to the interface,HelloIndigo
, with the signature shown here:public interface IHelloIndigoService { string HelloIndigo(); }
Add a reference to the
System.ServiceModel
assembly. From Solution Explorer, right-click References and selectSystem.ServiceModel
from the list. You’ll also need to add the followingusing
statement to Service.cs:using System.ServiceModel;
To turn this interface into a service contract, you’ll need to explicitly decorate the interface with the
ServiceContractAttribute
. In addition, each method should be decorated with theOperationContractAttribute
to include it in the service contract. In this case, you’ll makeIHelloIndigoService
a service contract and exposeHelloIndigo()
as its only service operation by applying these attributes as shown here:[ServiceContract(Namespace="http://www.thatindigogirl.com/samples/2006/06")]
public interface IHelloIndigoService {[OperationContract]
string HelloIndigo(); }Tip
Providing a namespace for the
ServiceContractAttribute
reduces the possibility of naming collisions with other services. This will be dicussed in greater detail in Chapter 2.In the same file, create a service type to implement the service contract. You can modify the existing class definition, renaming it to
HelloIndigoService
. Then add theIHelloIndigoService
interface to the derivation list and implementHelloIndigo()
with the following code:public class HelloIndigoService : IHelloIndigoService { public string HelloIndigo() { return "Hello Indigo"; } }
Compile the service project.
At this point, you’ve created a service contract with a single operation and implemented it on a service type. The service is complete at this point, but to consume it from a client application, you will need to host it first.
Next, add a new console application to the solution. This will be the host
application. You’ll instantiate a ServiceHost
instance for the service type and configure a single endpoint.
Go to the Solution Explorer and add a new Console Application project to the solution. Name the new project
Host
.Add a reference to the
System.ServiceModel
assembly, and add the followingusing
statement to Program.cs:using System.ServiceModel;
You will be writing code to host the
HelloIndigoService
type. Before you can do this, you must add a reference to theHelloIndigo
project.Create a
ServiceHost
instance and endpoint for the service. Open Program.cs in the code window and modify theMain()
entry point, adding the code shown in Example 1-1. This code initializes aServiceHost
instance specifying the service type and a base address where relative service endpoints can be located. It also adds a single relative endpoint for the service. In this case, a base address is provided for HTTP protocol, and the relative endpoint uses one of the standard bindings,BasicHttpBinding
, based on HTTP protocol.Compile and run the host to verify that it works. From Solution Explorer, right-click on the
Host
project node and select “Set as Startup Project.” Run the project (F5), and you should see console output similar to that shown in Figure 1-17.Stop debugging and return to Visual Studio.
Example 1-1. Code to programmatically initialize the ServiceHost
static void Main(string[] args) { using (ServiceHost host = new ServiceHost(typeof(HelloIndigo.HelloIndigoService), new Uri("http://localhost:8000/HelloIndigo"))) { host.AddServiceEndpoint(typeof(HelloIndigo.IHelloIndigoService), new BasicHttpBinding(), "HelloIndigoService"); host.Open(); Console.WriteLine("Press <ENTER> to terminate the service host"); Console.ReadLine(); } }
You now have a host application for the service. When it is running, clients will be able to communicate with the service. The next step is to create a client application.
Now you will create a new console application to test the service. To do this, the client requires metadata from the service and information about its endpoint. This information will be used to initialize a client proxy that can invoke service operations.
Go to Solution Explorer and add a new Console Application to the solution. Name the new project
Client
.As you might expect, this project also requires a reference to
System.ServiceModel
. Add this reference and add the followingusing
statement to Program.cs:using System.ServiceModel;
Copy the service contract to the client. First, add a new class to the
Client
project, naming the file ServiceProxy.cs. Open this new file in the code window and add theIHelloIndigoService
contract metadata as shown in Example 1-2. This service contract supplies the necessary metadata to the client, describing namespaces and service operation signatures.Now you can add code to invoke the service endpoint. Open Program.cs and modify the
Main()
entry point by adding the code as shown in Example 1-3. This code uses theChannelFactory
to create a new channel to invoke the service. This strongly typed channel reference acts as a proxy. The code also initializes anEndpointAddress
with the correct address and binding expected by the service endpoint.Test the client and service. Compile the solution and run the
Host
project first, followed by theClient
project. TheClient
console output should look similar to that shown in Figure 1-18.
Example 1-2. Service contract metadata for the client
using System.ServiceModel; [ServiceContract(Namespace = "http://www.thatindigogirl.com/samples/2006/06")] public interface IHelloIndigoService { [OperationContract] string HelloIndigo(); }
Example 1-3. Code to invoke a service through its generated proxy
static void Main(string[] args) { EndpointAddress ep = new EndpointAddress("http://localhost:8000/HelloIndigo/HelloIndigoService"); IHelloIndigoService proxy = ChannelFactory<IHelloIndigoService>. CreateChannel(new BasicHttpBinding(), ep); string s = proxy.HelloIndigo(); Console.WriteLine(s); Console.WriteLine("Press <ENTER> to terminate Client."); Console.ReadLine(); }
In the next few sections, I will explain in more detail the steps you completed and the features you explored in the lab.
The first thing I’d like to touch on is the allocation of assemblies when you create a new solution that includes services. For example, in this lab you created a new solution with three projects: one for the service, another for the host, and another for the client. Note that the service definition is decoupled from the host project. This is an approach I always recommend because it allows you to host the same service in multiple environments. For example, you may need to expose a service behind the firewall over TCP, and yet also allow remote, interoperable clients to consume it over HTTP. These two approaches require distinct hosting environments (specifically, a Windows service and IIS, as I will discuss in Chapter 4 at length). For simplicity, many examples may couple service and host, but this is merely a convenience—not a practical approach. As such, at a minimum I recommend that you always create a separate project for service contracts and services.
Services are the window through which business functionality is invoked, but business logic has no place in the service assembly. Business functionality should never be coupled with the service implementation because it is possible that multiple services and applications may need to reuse the same business logic. Furthermore, while you may use services to reach that functionality in most cases, what if you needed to expose an Enterprise Service component to interoperate with a particular application or system? If business logic is stored in its own assemblies, this type of sharing is made easy.
Another reason to decouple business logic from service implementation is to improve manageability and versioning. The service tier may need to coordinate logging activities and exception handling around calls to business components, and the service tier may need to be versioned, while business components and associated functionality have not changed. For this reason, I always recommend that business components, data access components, and other dependencies of the business tier also represent a separate set of assemblies in your solution. Figure 1-19 illustrates this breakdown from a high level.
There may be times when it is desirable to share the service contracts with client applications. In that case, service contracts and service implementations may also be decoupled. This makes it possible to share the metadata of the service without sharing the implementation.
Tip
Later in this chapter, you’ll see a scenario in which the service contract and service are decoupled.
The first step in creating a service is to define a service contract. You create a
service contract by applying the ServiceContractAttribute
to an interface or type. Methods on the interface or
type will not be included in the service contract until the OperationContractAttribute
is applied. In a typical service contract, all
methods will be included in the contract—after all, the entire reason for defining a
service contract is to expose operations as part of a service. Business interfaces should
not be directly converted into service contracts. Likewise, business components should not
be directly converted to services. The service tier should instead be explicitly defined
with the sole purpose of exposing public functionality and should internally consume
business components, rather than embed business logic with the service
implementation.
When you implement a service contract as an interface, the service type implements
this interface. In this lab, the service implements a single service contract, IHelloIndigoService
. This contract exposes a single operation,
HelloIndigo()
.
An alternative to this approach is to apply both the ServiceContractAttribute
and the OperationContractAttribute
directly to the service type. Example 1-4 shows the
changes you would make to the lab to achieve this. Here is a summary of those
changes:
When you apply the
ServiceContractAttribute
to the service type, the service type name becomes the official name of the service contract. Thus, this is the name that must be provided when you create a new endpoint (seeAddServiceEndpoint()
).On the client side, the service contract can still be represented as an interface (the client only requires metadata) but the name of that interface (the service contract) must match the new service contract name,
HelloIndigoService
—instead ofIHelloIndigoService
. To update the lab, you can rename the interface at the client, or specify a value for theName
property of theServiceContactAttribute
as shown in Example 1-4. Service contracts are discussed in Chapter 2.
Tip
The following sample illustrates the coupling of service contracts with service type: <YourLearningWCFPath>\Sample\ServiceContracts\ServiceContractOnServiceType.
Example 1-4. Changes that support defining the service contract with the service type
// HelloIndigo Project - Service.cs[ServiceContract(Namespace = "http://www.thatindigogirl.com/samples/2006/06")] public class HelloIndigoService { [OperationContract] public string HelloIndigo() { return "Hello Indigo"; } } // Host Project - Program.cs host.AddServiceEndpoint(typeof(HelloIndigo.HelloIndigoService
), new BasicHttpBinding(), "HelloIndigoService"); // Client Project - ServiceProxy.cs [ServiceContract(Name="HelloIndigoService"
, Namespace = "http://www.thatindigogirl.com/samples/2006/06")] public interface IHelloIndigoService { [OperationContract] string HelloIndigo(); }
Any managed process can host services. Within that process, you can create one or more
ServiceHost
instances, each associated with a
particular service type and exposing one or more endpoints for that type. This lab shows
you how to host a service by creating an instance of the ServiceHost
type for the HelloIndigoService
type within a console application.
Before opening the ServiceHost
instance, you can
also provide it with base addresses if you are planning to create relative endpoints. In
order to reach the service, at least one endpoint is required. To programmatically supply
base addresses to the ServiceHost
, you can pass them to
the constructor. The ServiceHost
type also provides an
AddServiceEndpoint()
method to create endpoints as
shown here (from Example 1-1):
using (ServiceHost host = new ServiceHost(typeof(HelloIndigo.HelloIndigoService), new Uri("http://localhost:8000/HelloIndigo"))) { host.AddServiceEndpoint(typeof(HelloIndigo.IHelloIndigoService), new BasicHttpBinding(), "HelloIndigoService"); // other code }
In a simple scenario, the ServiceHost
need only
know its service type and associated endpoints where the service can be reached. This
information is used to create a server channel that can receive and process messages. The
channel is created when you call the Open()
method on
the ServiceHost
instance. This creates a channel
listener to receive messages for the service through its associated endpoints. The
receiving channel processes incoming messages, invokes service operations, and processes
responses. When the Close()
method is called, the
channel is gracefully disposed of after processing any remaining requests. In Example 1-1, Close()
is automatically called when code block associated
with the using
statement ends.
Endpoints expose service functionality at a particular address. Each endpoint is
associated with a particular contract and a set of protocols as defined by the binding
configuration. For each service, one or more endpoints may be exposed if multiple
contracts are present or if multiple protocols are desired to access service
functionality. Figure 1-20 illustrates how
the ServiceHost
instance exposes endpoints to clients
and how the proxy invokes service operations at a particular endpoint.
As the lab illustrates, to create a service endpoint you provide an address, a binding, and a contract.
The address can be a complete URI or
a relative address like that used in the lab. The following shows you how to initialize
an endpoint with a complete URI without supplying a base address to the ServiceHost
:
using (ServiceHost host = new ServiceHost(typeof(HelloIndigo.HelloIndigoService)))
{
host.AddServiceEndpoint(typeof(HelloIndigo.IHelloIndigoService),
new BasicHttpBinding(),"http://localhost:8000/HelloIndigo/HelloIndigoService"
);
// other code
}
If
you supply a relative address it is concatenated with the ServiceHost
base address for the matching protocol. The following
illustrates providing an HTTP base address to the ServiceHost
constructor and providing a relative address to AddServiceEndpoint()
:
using (ServiceHost host = new ServiceHost(typeof(HelloIndigo.HelloIndigoService), new Uri("http://localhost:8000/HelloIndigo"
))) { host.AddServiceEndpoint(typeof(HelloIndigo.IHelloIndigoService), new BasicHttpBinding(),"HelloIndigoService"
); // other code } // Resulting endpoint address http://localhost:8000/HelloIndigo/HelloIndigoService
In practice, a base address should be supplied for each transport protocol over which the service can be accessed—for example, HTTP, TCP, named pipes, or MSMQ. In the event an endpoint address includes a complete URI, the base address will be ignored.
Tip
Using relative endpoint addressing makes it possible to modify the base URI to move all associated relative endpoints to a new domain or port. This can simplify the deployment process.
The binding provided to an
endpoint can be any of the standard bindings supplied by the service model. In this
example, a new instance of the standard BasicHttpBinding
is used to initialize the endpoint:
host.AddServiceEndpoint(typeof(HelloIndigo.IHelloIndigoService),new BasicHttpBinding()
, "HelloIndigoService");
The
choice of binding defines the communication channel. For an endpoint, BasicHttpBinding
, for example, supports requests over HTTP
protocol sent in text format without any additional protocols for addressing, reliable
messaging, security, or transactions.
Tip
In this chapter, you will employ other standard bindings, but you should look to Chapter 3 for an in-depth discussion of bindings, channels, and overall service model architecture.
Clients use a proxy to consume a service endpoint. A proxy can be created manually using a channel factory, or it can be generated using tools. This lab explores the former and shows you the bare necessities required to communicate with a service:
The address of the service endpoint
The protocols required to communicate with the service endpoint, or the binding
The service contract metadata as described by the service contract associated with the endpoint
Essentially, the client proxy requires information about the service endpoint it
wishes to consume. In this lab, you learned how to manually create the proxy using
ChannelFactory<T>
, as shown here:
EndpointAddress ep = new EndpointAddress("http://localhost:8000/HelloIndigo/HelloIndigoService"); IHelloIndigoService proxy = ChannelFactory<IHelloIndigoService>. CreateChannel(new BasicHttpBinding(), ep);
ChannelFactory<T>
is a service model type
that can generate the client proxy and underlying channel stack. You provide the address,
binding, and service contract type and call CreateChannel()
to generate the channel stack discussed earlier. In this lab,
you made a copy of the service contract (not the implementation) in the client application
in order to supply it as the generic parameter type to ChannelFactory<T>
. The address and binding supplied matched those of
the service. The result is that the client proxy knows where to send messages, what
protocols to use, and which operations it can call.
In order for communication between client and service to succeed, the binding must be
equivalent to the binding specified for the service endpoint.
Equivalence means that the transport protocol is the same, the
message-encoding format is the same, and any additional messaging protocols used at the
service to serialize messages are also used at the client. This lab achieves this by
applying the same standard binding, BasicHttpBinding
,
at the client and service—thus, they are equivalent. Another requirement for successful
communication is that the service contract used to initialize the proxy has equivalent
operation signatures and namespace definitions. This is achieved in this lab by making an
exact copy of the service contract at the client.
Tip
You may be wondering: how can the client discover the correct address, binding, and contract associated with a service endpoint? In the next lab, you’ll learn how to generate client proxies and configuration to consume a service without having access to the service code base.
Get Learning WCF now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.