Every service is associated with an address that defines where the service is, a binding that defines how to communicate with the service, and a contract that defines what the service does. This triumvirate governing the service is easy to remember as the ABC of the service. WCF formalizes this relationship in the form of an endpoint. The endpoint is the fusion of the address, contract, and binding (see Figure 1-8).
Every endpoint must have all three elements, and the host exposes the endpoint. Logically, the endpoint is the service’s interface and is analogous to a CLR or COM interface. Note the use of the traditional “lollipop” notation to denote an endpoint in Figure 1-8.
Note
Conceptually, even in C# or VB, there are endpoints: the address is the memory address of the type’s virtual table, the binding is CLR, and the contract is the interface itself. Because in classic .NET programming you never deal with addresses or bindings, you take them for granted, and you’ve probably gotten used to equating in your mind’s eye the interface (which is merely a programming construct) with all that it takes to interface with an object. The WCF endpoint is a true interface because it contains all the information required to interface with the object. In WCF, the address and the binding are not preordained and you must specify them.
Every service must expose at least one business endpoint, and each endpoint has exactly one contract. All endpoints on a service have unique addresses, and a single service can expose multiple endpoints. These endpoints can use the same or different bindings and can expose the same or different contracts. There is absolutely no relationship between the various endpoints a service provides.
It is important to point out that nothing in the service code pertains to its endpoints, and they are always external to the service code. You can configure endpoints either administratively (using a config file) or programmatically.
Configuring an endpoint administratively requires placing the endpoint details in the hosting process config file. For example, given this service definition:
namespace MyNamespace { [ServiceContract] interface IMyContract {...} class MyService : IMyContract {...} }
Example 1-6 shows the required entries in the config file. Under each service type, you list its endpoints.
Example 1-6. Administrative endpoint configuration
<system.serviceModel> <services> <service name = "MyNamespace.MyService"> <endpoint address = "http://localhost:8000/MyService" binding = "wsHttpBinding" contract = "MyNamespace.IMyContract" /> </service> </services> </system.serviceModel>
When you specify the service and the contract type, you need to
use fully qualified type names. I will omit the namespace in the examples throughout the remainder of
this book, but you should use a namespace when applicable. Note that
if the endpoint provides a base address, that address scheme must be consistent
with the binding, such as HTTP with WSHttpBinding
. A mismatch causes an
exception at service load time.
Example 1-7 shows a config file defining a single service that exposes multiple endpoints. You can configure multiple endpoints with the same base address as long as the URI is different.
Example 1-7. Multiple endpoints on the same service
<service name = "MyService"> <endpoint address = "http://localhost:8000/MyService" binding = "wsHttpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyService" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8002/MyService" binding = "netTcpBinding" contract = "IMyOtherContract" /> </service>
Administrative configuration is the option of choice in the majority of cases because it provides the flexibility to change the service address, binding, and even exposed contracts without rebuilding and redeploying the service.
In Example 1-7, each endpoint provided its own base address. When you provide an explicit base address, it overrides any base address the host may have provided.
You can also have multiple endpoints use the same base address, as long as the endpoint addresses differ in their URIs:
<service name = "MyService"> <endpoint address = "net.tcp://localhost:8001/MyService
" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyOtherService
" binding = "netTcpBinding" contract = "IMyContract" /> </service>
Alternatively, if the host provides a base address with a matching transport scheme, you can leave out the address. In this case, the endpoint address will be the same as the base address of the matching transport:
<endpoint binding = "wsHttpBinding" contract = "IMyContract" />
If the host does not provide a matching base address, loading the service host will fail with an exception.
When you configure the endpoint address, you can add just the relative URI under the base address:
<endpoint
address = "SubAddress"
binding = "wsHttpBinding"
contract = "IMyContract"
/>
The endpoint address in this case will be the matching base address plus the URI and, again, the host must provide a matching base address.
You can use the config file to customize the binding
used by the endpoint. To that end, add the bindingConfiguration
tag to the endpoint
section and name a customized
section in the bindings
section
of the config file. Example 1-8 demonstrates using
this technique to enable transaction propagation. Chapter 7 explains the function of the transactionFlow
tag.
Example 1-8. Service-side binding configuration
<system.serviceModel> <services> <service name = "MyService"> <endpoint address = "net.tcp://localhost:8000/MyService" bindingConfiguration = "TransactionalTCP" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyService" bindingConfiguration = "TransactionalTCP" binding = "netTcpBinding" contract = "IMyOtherContract" /> </service> </services> <bindings> <netTcpBinding> <binding name = "TransactionalTCP" transactionFlow = "true" /> </netTcpBinding> </bindings> </system.serviceModel>
As shown in Example 1-8, you can reuse the named binding configuration in multiple endpoints simply by referring to it.
WCF allows you to use a default binding that affects all endpoints of all services of the application that uses the config file. A default binding is simply a nameless binding section. For example, in the case of TCP:
<netTcpBinding> <binding transactionFlow = "true" /> </netTcpBinding>
The default binding implicitly configures all endpoints that do not explicitly reference a binding configuration.
For example, using a default binding, Example 1-8 is reduced to:
<system.serviceModel> <services> <service name = "MyService"> <endpoint address = "net.tcp://localhost:8000/MyService" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint address = "net.tcp://localhost:8001/MyService" binding = "netTcpBinding" contract = "IMyOtherContract" /> </service> </services> <bindings> <netTcpBinding> <binding transactionFlow = "true" /> </netTcpBinding> </bindings> </system.serviceModel>
You can only have at most one default binding configuration per binding type.
The problem with the default binding is that when you combine default bindings with named binding configurations, as shown in Figure 1-9, the config file may become difficult for humans to parse and understand.
While Figure 1-9 is a perfectly valid configuration, I recommend against mixing named and default bindings. Either have all your binding configurations named or use only the default configuration. Another advantage of a named configuration is that it allows you to weave a bit of documentation via the binding configuration name as to what that configuration is trying to achieve. Most if not all of the binding configurations in this book are named for precisely that reason.
Programmatic endpoint configuration is equivalent to
administrative configuration, but instead of resorting to a config
file, you rely on programmatic calls to add endpoints to the
ServiceHost
instance.
Again, these calls are always outside the scope of the service code.
ServiceHost
provides
overloaded versions of the AddServiceEndpoint()
method:
public class ServiceHost : ServiceHostBase { public ServiceEndpoint AddServiceEndpoint(Type implementedContract, Binding binding, string address); //Additional members }
You can provide AddServiceEndpoint()
methods with either
relative or absolute addresses,
just as with a config file. Example 1-9 demonstrates
programmatic configuration of the same endpoints as in Example 1-7.
Example 1-9. Service-side programmatic endpoint configuration
ServiceHost host = new ServiceHost(typeof(MyService)); Binding wsBinding = new WSHttpBinding(); Binding tcpBinding = new NetTcpBinding(); host.AddServiceEndpoint
(typeof(IMyContract),wsBinding, "http://localhost:8000/MyService"); host.AddServiceEndpoint
(typeof(IMyContract),tcpBinding, "net.tcp://localhost:8001/MyService"); host.AddServiceEndpoint
(typeof(IMyOtherContract),tcpBinding, "net.tcp://localhost:8002/MyService"); host.Open();
When you add an endpoint programmatically, the address is given
as a string, the contract as a Type
, and the binding as one of the
subclasses of the abstract class Binding
, as in:
public class NetTcpBinding : Binding,... {...}
To rely on the host base address, provide an empty string if you want to use only the base address, or just the URI to use the base address plus that URI:
Uri tcpBaseAddress = new Uri("net.tcp://localhost:8000/"); ServiceHost host = new ServiceHost(typeof(MyService),tcpBaseAddress); Binding tcpBinding = new NetTcpBinding(); //Use base address as address host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,""); //Add relative address host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,"MyService"
); //Ignore base address host.AddServiceEndpoint(typeof(IMyContract),tcpBinding, "net.tcp://localhost:8001/
MyService"); host.Open();
As with administrative configuration using a config file, the host must provide a matching base address; otherwise, an exception occurs. In fact, in terms of capabilities, there is no difference between programmatic and administrative configuration. When you use a config file, all WCF does is parse the file and execute the appropriate programmatic calls in its place.
You can programmatically set the properties of the binding used. For example, the following is the code required to enable transaction propagation (similar to Example 1-8):
ServiceHost host = new ServiceHost(typeof(MyService));NetTcp
Binding tcpBinding = newNetTcp
Binding(); tcpBinding.TransactionFlow
= true; host.AddServiceEndpoint(typeof(IMyContract),tcpBinding, "net.tcp://localhost:8000/MyService"); host.Open();
Note that when you’re dealing with specific binding
properties, you typically interact with a concrete binding subclass,
such as NetTcpBinding
,
rather than its abstract base class, Binding
(as was done in Example 1-9).
All the binding classes also offer a constructor that takes a string, for example:
public class NetTcpBinding : Binding,... { public NetTcpBinding(string configurationName); //More members }
You can use that constructor to programmatically initialize a
binding object based on settings found in a particular binding
section in the config file. You can also pass an empty string to
instruct WCF to use the default (nameless) binding configuration. If
the config file does not contain a default binding definition, you
will encounter a KeyNotFoundException
.
If the service host does not define any endpoints (neither in config nor programmatically) but does provide at least one base address, WCF will by default add endpoints to the service. These are called the default endpoints. WCF will add an endpoint per base address per contract, using the base address as the endpoint’s address. WCF will infer the binding from the scheme of the base address. For HTTP, WCF will use the basic binding. Note that the default bindings will affect the default endpoints. WCF will also name the endpoint by concatenating the binding name and the contract name.
For example, given this service definition:
[ServiceContract] interface IMyContract {...} [ServiceContract] interface IMyOtherContract {...} class MyService : IMyContract,IMyOtherContract {...}
for this hosting code:
Uri httpBaseAddress = new Uri("http://localhost:8000/"); Uri tcpBaseAddress = new Uri("net.tcp://localhost:9000/"); Uri ipcBaseAddress = new Uri("net.pipe://localhost/"); ServiceHost host = new ServiceHost(typeof(MyService), httpBaseAddress,tcpBaseAddress,ipcBaseAddress); host.Open();
Assuming no config file is used to define any additional endpoints, WCF will add these endpoints, as if they were defined in config:
<service name = "MyService"> <endpoint name = "BasicHttpBinding_IMyContract" address = "http://localhost:8000/" binding = "basicHttpBinding" contract = "IMyContract" /> <endpoint name = "NetTcpBinding_IMyContract" address = "net.tcp://localhost:9000" binding = "netTcpBinding" contract = "IMyContract" /> <endpoint name = "NetNamedPipeBinding_IMyContract" address = "net.pipe://localhost/" binding = "netNamedPipeBinding" contract = "IMyContract" /> <endpoint name = "BasicHttpBinding_IMyOtherContract" address = "http://localhost:8000/" binding = "basicHttpBinding" contract = "IMyOtherContract" /> <endpoint name = "NetTcpBinding_IMyOtherContract" address = "net.tcp://localhost:9000" binding = "netTcpBinding" contract = "IMyOtherContract" /> <endpoint name = "NetNamedPipeBinding_IMyOtherContract" address = "net.pipe://localhost/" binding = "netNamedPipeBinding" contract = "IMyOtherContract" /> </service>
Note that WCF will provide the same address multiple times to different endpoints. While this works as far as invocation (since the host monitors the incoming ports or pipes just once and just dispatches the message internally to the correct endpoint), this configuration will fail metadata publishing due to an internal limitation of WCF.
You can also add the default endpoints explicitly using the
AddDefaultEndpoints()
method of ServiceHost
:
public class ServiceHost : ... { public void AddDefaultEndpoints(); //More members }
You can add the default endpoints even if you have added other endpoints conventionally using a config file or programmatically. The only thing to watch for is conflict with other endpoints that use the base address as their address.
For the default endpoints, WCF will infer the binding to use from the scheme of the base
address. This inferring is called protocol
mapping. In the case of TCP, IPC, and MSMQ, there is only
a single mapping option. However, in the case of HTTP (or HTTPS),
WCF will default to the basic binding for mapping. If you like to
rely on the WS binding instead (as you should in most cases), you
need to override the default protocol mapping using the protocolMapping
section in the config
file:
<system.serviceModel> <protocolMapping> <add scheme = "http" binding = "wsHttpBinding" /> </protocolMapping> </system.serviceModel>
You can also specify a particular binding configuration to use:
<protocolMapping>
<add
scheme = "http"
binding = "wsHttpBinding"
bindingConfiguration = "..."
/>
</protocolMapping>
You must do protocol mapping administratively in the config file. There is no equivalent programmatic way.
Note
Protocol mapping is the only configuration option offered by WCF in the service model section that does not have a programmatic equivalent.
Get Programming WCF Services, 3rd Edition 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.