Last updated .
Chapter 1 showcased an example of a very simple WCF service and introduced the basic concepts involved. This chapter goes on with an in-depth treatment of one of these concepts,
namely the concept of
The metadata describes the contract. These are the requirements of the service. If a client wishes to communicate with the service it is bound to go along with that contract. A service contract can be broken down into three parts:
The service contract is the term used for the interface that the service implements. It also describes the message format used by the service. Thus, a service contract is a set of (presumably related) operations (methods) that a client can invoke. The metadata for an operation includes information about the names and types of parameters and return values. A WCF service implements an arbitrary number of service contracts, although at least one.
In .Net, a type is defined as being a WCF service by applying the
Name | Type | Description |
---|---|---|
CallbackContract | Type | Defines another service contract to be used as a callback contract, enabling asynchronous calls from service to client. |
ConfigurationName | String | Gets or sets the name of the |
Name | String | The name of the service. It defaults to the .Net name of the interface (or class), but the |
Namespace | String | The namespace of the service. It defaults to http://tempuri.org, so it should definitely be set to some other, unique value to disambiguate from other services. |
ProtectionLevel | ProtectionLevel (enum) | Defaults to |
HasProtectionLevel | Bool | Returns if |
SessionMode | SessionMode (enum) | Specifies whether the contract requires a binding that supports sessions. A session is a
way of correlating a set of messages exchanged between endpoints. Possible values are |
It is recommended that you always specify
A public method of a service contract becomes part of the service interface when it is adorned with an
Name | Type | Description |
---|---|---|
Action | String | Provides fine-grained control over the |
ReplyAction | String | Provides fine-grained control over the |
Name | String | Gets or sets the name of the operation, which defaults to the name of the implementing method. |
ProtectionLevel | ProtectionLevel (enum) | Can be used to override the|
HasProtectionLevel | Bool | Returns if |
IsTerminating | Bool | Indicates whether the service operation causes the server to close the session after the replying. |
IsInitiating | Bool | Indicates whether operation will initiate a server session (if using sessions). |
IsOneWay | Bool | Indicates whether the operations sends a reply message. |
AsyncPattern | Bool | Denotes if the operation adheres to the |
Setting
The names given in the SOAP message to parameters and the return value can be controlled with the
Name | Type | Description |
---|---|---|
Name | String | Gets or sets the name of the parameter or return value. |
In order to exercise a bit the attributes described above, I made a new solution for testing. It is just the standard Visual Studio WCF test solution, consisting of:
Before writing any specific code, I saved the solution for use as a solution template to use in future WCF test work. The Visual Studio 2015 solution comes with the required references between projects
and a bit of scaffold configuration and code. You can download the template as a Zip file.
I also wanted to see how things would work with multiple contracts and bindings for a service, so I constructed three simple interfaces in the
IContractOne.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
using System.ServiceModel;
namespace ServiceContract
{
[ServiceContract(Name = "ContractOneName",
Namespace = "http://mycompany.org/api/sampleservice/2016/01",
ConfigurationName = "ContractOneConfigName",
SessionMode = SessionMode.Allowed)]
public interface IContractOne
{
[OperationContract(Name = "SayHelloTo")]
[return: MessageParameter(Name = "GreetingResponse")]
string SayHello([MessageParameter(Name = "GreetingName")] string name);
[OperationContract]
string SayGoodbye(string name);
}
}
As you can see, values for the
I also defined this second interface:
IContractTwo.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
using System.ServiceModel;
namespace ServiceContract
{
[ServiceContract(Namespace = "http://mycompany.org/api/sampleservice/2016/01")]
public interface IContractTwo
{
[OperationContract]
string SayHelloAgain(string name);
}
}
And a third one:
IContractThree.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
using System.ServiceModel;
namespace ServiceContract
{
[ServiceContract(Namespace = "http://mycompany.org/api/sampleservice/2016/01")]
public interface IContractThree
{
[OperationContract]
string SayHelloThirdTime(string someName);
}
}
As you might expect, the service implementations are really simple. First, there is this class that implements the
ContractOneService.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
using ServiceContract;
namespace ServiceImplementation
{
public class ContractOneService : IContractOne
{
public string SayGoodbye(string name)
{
return string.Format("Goodbye, {0}!", name);
}
public string SayHello(string name)
{
return string.Format("Hello, {0}!", name);
}
}
}
The other service class implements both
ContractTwoThreeService.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
using System;
using ServiceContract;
namespace ServiceImplementation
{
public class ContractTwoThreeService : IContractTwo, IContractThree
{
public string SayHelloAgain(string name)
{
return string.Format("Hello second time to {0}!", name);
}
public string SayHelloThirdTime(string someName)
{
return string.Format("Hello third time to {0}!", someName);
}
}
}
Now, the services can be configured. I have implemented two service classes, so I'll need two
App.config
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="mexBehavior">
<serviceMetadata />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service name="ServiceImplementation.ContractOneService" behaviorConfiguration="mexBehavior">
<endpoint address="ContractOneServiceAddress" name="tcpEndpoint"
binding="netTcpBinding" contract="ContractOneConfigName" />
<endpoint address="ContractOneServiceAddress" name="httpEndpoint"
binding="basicHttpBinding" contract="ContractOneConfigName" />
<endpoint binding="mexHttpBinding" name="mexHttp" address="mex" contract="IMetadataExchange" />
<endpoint binding="mexTcpBinding" name="mexTcp" address="mex" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:9002/api"/>
<add baseAddress="http://localhost:8001/api"/>
</baseAddresses>
</host>
</service>
<service name="ServiceImplementation.ContractTwoThreeService" behaviorConfiguration="mexBehavior">
<endpoint address="ServiceTwoThree" name="IContractTwoEndpoint"
binding="basicHttpBinding" contract="ServiceContract.IContractTwo" />
<endpoint address="ServiceTwoThree" name="IContractThreeEndpoint"
binding="basicHttpBinding" contract="ServiceContract.IContractThree" />
<endpoint binding="mexHttpBinding" name="mex" address="mex" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:8002/api"/>
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
The first service, named
As for the second service, named
Note that the three baseaddresses are using different ports - only one endpoint can use the same port at the same time.
To get this going I also needed a host. Again, I constructed my own host executable in the form of a console app. Only this time it is the host of two services, rather than one, so it is
no longer practical to use the C#
Program.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
using System;
using ServiceImplementation;
namespace ServiceHost
{
class Program
{
static void Main(string[] args)
{
System.ServiceModel.ServiceHost host1 = null;
System.ServiceModel.ServiceHost host2 = null;
try {
host1 = new System.ServiceModel.ServiceHost(typeof(ContractOneService));
host1.Open();
host2 = new System.ServiceModel.ServiceHost(typeof(ContractTwoThreeService));
host2.Open();
Console.WriteLine("Services have been started.");
Console.WriteLine("Press Enter to terminate service hosts.");
Console.ReadLine();
}
finally {
if (null != host1)
((IDisposable) host1).Dispose();
if (null != host2)
((IDisposable) host2).Dispose();
}
}
}
}
Now, the host can be started. When the host is running there is a
svcutil /t:metadata http://localhost:8002/api/mex
Note that if you target a net.tcp endpoint, you will have to run svcutil with administrator privileges. The above command causes
Although looking at wsdl files is not terribly exciting, it is still instructive to examine precisely which information is available for a client. Please note that services are completely independent. Metadata for one service has nothing to do with metadata for the other service, even though, in this case, they are hosted by the same app. Let's start with one of the more basic files:
schemas.microsoft.com.2003.10.Serialization.xsd
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:tns="http://schemas.microsoft.com/2003/10/Serialization/"
attributeFormDefault="qualified"
elementFormDefault="qualified"
targetNamespace="http://schemas.microsoft.com/2003/10/Serialization/"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="anyType" nillable="true" type="xs:anyType" />
<xs:element name="anyURI" nillable="true" type="xs:anyURI" />
<xs:element name="base64Binary" nillable="true" type="xs:base64Binary" />
<xs:element name="boolean" nillable="true" type="xs:boolean" />
<xs:element name="byte" nillable="true" type="xs:byte" />
<xs:element name="dateTime" nillable="true" type="xs:dateTime" />
<xs:element name="decimal" nillable="true" type="xs:decimal" />
<xs:element name="double" nillable="true" type="xs:double" />
<xs:element name="float" nillable="true" type="xs:float" />
<xs:element name="int" nillable="true" type="xs:int" />
<xs:element name="long" nillable="true" type="xs:long" />
<xs:element name="QName" nillable="true" type="xs:QName" />
<xs:element name="short" nillable="true" type="xs:short" />
<xs:element name="string" nillable="true" type="xs:string" />
<xs:element name="unsignedByte" nillable="true" type="xs:unsignedByte" />
<xs:element name="unsignedInt" nillable="true" type="xs:unsignedInt" />
<xs:element name="unsignedLong" nillable="true" type="xs:unsignedLong" />
<xs:element name="unsignedShort" nillable="true" type="xs:unsignedShort" />
<xs:element name="char" nillable="true" type="tns:char" />
<xs:simpleType name="char">
<xs:restriction base="xs:int" />
</xs:simpleType>
<xs:element name="duration" nillable="true" type="tns:duration" />
<xs:simpleType name="duration">
<xs:restriction base="xs:duration">
<xs:pattern value="\-?P(\d*D)?(T(\d*H)?(\d*M)?(\d*(\.\d*)?S)?)?" />
<xs:minInclusive value="-P10675199DT2H48M5.4775808S" />
<xs:maxInclusive value="P10675199DT2H48M5.4775807S" />
</xs:restriction>
</xs:simpleType>
<xs:element name="guid" nillable="true" type="tns:guid" />
<xs:simpleType name="guid">
<xs:restriction base="xs:string">
<xs:pattern value="[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}" />
</xs:restriction>
</xs:simpleType>
<xs:attribute name="FactoryType" type="xs:QName" />
<xs:attribute name="Id" type="xs:ID" />
<xs:attribute name="Ref" type="xs:IDREF" />
</xs:schema>
There is nothing related to the two services in this file. It is just a standard
mycompany.org.api.sampleservice.2016.01.xsd
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:tns="http://mycompany.org/api/sampleservice/2016/01"
elementFormDefault="qualified"
targetNamespace="http://mycompany.org/api/sampleservice/2016/01"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="SayHelloAgain">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" name="name" nillable="true" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="SayHelloAgainResponse">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" name="SayHelloAgainResult" nillable="true" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="SayHelloThirdTime">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" name="someName" nillable="true" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="SayHelloThirdTimeResponse">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" name="SayHelloThirdTimeResult" nillable="true" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
This file describes the types of parameters and return values of the service operations. Since this service exposes two methods, each of which takes one parameter and returns a string,
this gives four entities in total, all of which happen to be
The next
mycompany.org.api.sampleservice.2016.01.wsdl
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions xmlns:wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy"
xmlns:wsa10="http://www.w3.org/2005/08/addressing"
xmlns:tns="http://mycompany.org/api/sampleservice/2016/01"
xmlns:msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://mycompany.org/api/sampleservice/2016/01"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
<wsdl:types>
<xsd:schema targetNamespace="http://mycompany.org/api/sampleservice/2016/01/Imports">
<xsd:import namespace="http://mycompany.org/api/sampleservice/2016/01" />
<xsd:import namespace="http://schemas.microsoft.com/2003/10/Serialization/" />
</xsd:schema>
</wsdl:types>
<wsdl:message name="IContractTwo_SayHelloAgain_InputMessage">
<wsdl:part name="parameters" element="tns:SayHelloAgain" />
</wsdl:message>
<wsdl:message name="IContractTwo_SayHelloAgain_OutputMessage">
<wsdl:part name="parameters" element="tns:SayHelloAgainResponse" />
</wsdl:message>
<wsdl:message name="IContractThree_SayHelloThirdTime_InputMessage">
<wsdl:part name="parameters" element="tns:SayHelloThirdTime" />
</wsdl:message>
<wsdl:message name="IContractThree_SayHelloThirdTime_OutputMessage">
<wsdl:part name="parameters" element="tns:SayHelloThirdTimeResponse" />
</wsdl:message>
<wsdl:portType name="IContractTwo">
<wsdl:operation name="SayHelloAgain">
<wsdl:input wsaw:Action="http://mycompany.org/api/sampleservice/2016/01/IContractTwo/SayHelloAgain"
message="tns:IContractTwo_SayHelloAgain_InputMessage" />
<wsdl:output wsaw:Action="http://mycompany.org/api/sampleservice/2016/01/IContractTwo/SayHelloAgainResponse"
message="tns:IContractTwo_SayHelloAgain_OutputMessage" />
</wsdl:operation>
</wsdl:portType>
<wsdl:portType name="IContractThree">
<wsdl:operation name="SayHelloThirdTime">
<wsdl:input wsaw:Action="http://mycompany.org/api/sampleservice/2016/01/IContractThree/SayHelloThirdTime"
message="tns:IContractThree_SayHelloThirdTime_InputMessage" />
<wsdl:output wsaw:Action="http://mycompany.org/api/sampleservice/2016/01/IContractThree/SayHelloThirdTimeResponse"
message="tns:IContractThree_SayHelloThirdTime_OutputMessage" />
</wsdl:operation>
</wsdl:portType>
</wsdl:definitions>
In the above
The
The final file describes the service from a more technical viewpoint, listing the available endpoints, their operations and addresses:
tempuri.org.wsdl
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions xmlns:wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy"
xmlns:wsa10="http://www.w3.org/2005/08/addressing"
xmlns:tns="http://tempuri.org/"
xmlns:msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:i0="http://mycompany.org/api/sampleservice/2016/01"
xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/"
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
name="ContractTwoThreeService"
targetNamespace="http://tempuri.org/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
<wsdl:import namespace="http://mycompany.org/api/sampleservice/2016/01" location="" />
<wsdl:types />
<wsdl:binding name="IContractTwoEndpoint" type="i0:IContractTwo">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="SayHelloAgain">
<soap:operation soapAction="http://mycompany.org/api/sampleservice/2016/01/IContractTwo/SayHelloAgain" style="document" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:binding name="IContractThreeEndpoint" type="i0:IContractThree">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="SayHelloThirdTime">
<soap:operation soapAction="http://mycompany.org/api/sampleservice/2016/01/IContractThree/SayHelloThirdTime" style="document" />
<wsdl:input>
<soap:body use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body use="literal" />
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="ContractTwoThreeService">
<wsdl:port name="IContractTwoEndpoint" binding="tns:IContractTwoEndpoint">
<soap:address location="http://localhost:8002/api/ServiceTwoThree" />
</wsdl:port>
<wsdl:port name="IContractThreeEndpoint" binding="tns:IContractThreeEndpoint">
<soap:address location="http://localhost:8002/api/ServiceTwoThree" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
In the above document, the two
Every service type has an associated description in the form of metadata files, as we have just seen above. Everything a client needs to connect to and call a service is contained in the metadata.
In chapter one of this series, I made the code and configuration for a client proxy by running the service and then calling
svcutil *.wsdl *.xsd /config:app.config /namespace:*,ContractTwoThreeService
This run of
Before calling
So far, I have generated metadata and client proxy files for just the one service. I repeated the whole procedure for the
app.config
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="IContractTwoEndpoint" />
<binding name="IContractThreeEndpoint" />
<binding name="httpEndpoint" />
</basicHttpBinding>
<netTcpBinding>
<binding name="tcpEndpoint" />
</netTcpBinding>
</bindings>
<client>
<endpoint address="http://localhost:8002/api/ServiceTwoThree"
binding="basicHttpBinding" bindingConfiguration="IContractTwoEndpoint"
contract="ContractTwoThreeService.IContractTwo" name="IContractTwoEndpoint" />
<endpoint address="http://localhost:8002/api/ServiceTwoThree"
binding="basicHttpBinding" bindingConfiguration="IContractThreeEndpoint"
contract="ContractTwoThreeService.IContractThree" name="IContractThreeEndpoint" />
<endpoint address="net.tcp://localhost:9002/api/ContractOneServiceAddress"
binding="netTcpBinding" bindingConfiguration="tcpEndpoint"
contract="ContractOneService.ContractOneName" name="tcpEndpoint" />
<endpoint address="http://localhost:8001/api/ContractOneServiceAddress"
binding="basicHttpBinding" bindingConfiguration="httpEndpoint"
contract="ContractOneService.ContractOneName" name="httpEndpoint" />
</client>
</system.serviceModel>
</configuration>
This is pretty straightforward. There are four endpoints involved here, three of which uses
Now, at last, the client app can be implemented, exercising the total of four operations offered by three interfaces, implemented by two service classes:
Program.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
using System;
namespace Client
{
class Program
{
static void Main(string[] args)
{
ContractOneService.ContractOneName serviceOne = new ContractOneService.ContractOneNameClient("tcpEndpoint");
ContractTwoThreeService.IContractTwo serviceTwo = new ContractTwoThreeService.ContractTwoClient();
ContractTwoThreeService.IContractThree serviceThree = new ContractTwoThreeService.ContractThreeClient();
Console.WriteLine(serviceOne.SayHelloTo("Bill"));
Console.WriteLine(serviceOne.SayGoodbye("Bill"));
Console.WriteLine(serviceTwo.SayHelloAgain("Alice"));
Console.WriteLine(serviceThree.SayHelloThirdTime("Bob"));
Console.WriteLine("Press Enter to quit");
Console.ReadLine();
}
}
}
Notice that in constructing the proxy for the
This example clearly shows that, in WCF, the interface is the service. In the service implementation, I implemented the serviceTwo = (ContractTwoThreeService.IContractTwo) serviceThree
. Well,
you could, but you would get a runtime exception.
The names I invented for the endpoints in the host
If you've read so far as here, through
Get the code for this chapter's first example as a Zip file (Visual Studio 2015).
If you've published a service contract that clients have started using, it is likely that you will soon find a need to modify or extend the contract. Perhaps clients have uttered change requests, or maybe new business processes lead to new requirements. In any case, while it is important to be able to improve services and the contracts they depend on, it's just as important that you don't inadvertently break the service for existing clients. This section discusses how to accomplish both aims at the same time.
In type-safe languages such as C# we are used to the fact that making even the smallest change to a method will break the code that calls the method. WCF services are much more lenient, in fact service contracts are change tolerant by default. The table below lists the possible changes you might make to a service contract and what the consequences are.
Type of change | Impact on clients |
---|---|
Add a parameter | Client unaffected. New parameters are initialized to default values at the service end. |
Remove a parameter | Client unaffected. Values for unknown parameters are discarded and lost at the service end. |
Change a parameter type | The incoming value will be converted to the appropiate type if possible, otherwise an exception will be thrown. |
Change a return type | The returned value will be converted to the appropiate type if possible, otherwise an exception will be thrown. |
Add an operation | Client unaffected. The client will not know about the new operation. |
Remove an operation | The client will get an exception. |
It's perfectly possible to rely on the lenient nature of WCF service contracts, but it is also a bit dangerous. For example, it is questionable whether it's "ethical" to lose data that the client is sending and type conversion may work, but the conversion result may still be unexpected. For these reasons, it is recommended that you version the service contract, i.e. make a new one to live side by side with existing ones. There are a couple of different strategies you can adopt in versioning.
Probably the most commonly needed type of modification is the addition of operations. Let's say you have this existing service contract that you want to update:
ISomeService.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
using System.ServiceModel;
namespace ServiceContract
{
[ServiceContract(Namespace = "http://mycompany.org/api/sampleservice/2016/01")]
public interface ISomeService
{
[OperationContract]
string SayHello([MessageParameter(Name = "name")] string name);
}
}
Now, you want to update the interface in two ways:
The first change can be accomplished by simply adding the parameter; this will not break exisiting clients, only they will not be passing any value for that parameter, so it will always have its default value for "old" clients.
ISomeService.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
using System.ServiceModel;
namespace ServiceContract
{
[ServiceContract(Namespace = "http://mycompany.org/api/sampleservice/2016/01")]
public interface ISomeService
{
[OperationContract]
string SayHello([MessageParameter(Name = "name")] string name,
[MessageParameter(Name = "upperCase")] bool upperCase = false);
}
}
As for the second required change, a new interface with a new
ISomeServiceExtension.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
using System.ServiceModel;
namespace ServiceContract
{
[ServiceContract(Namespace = "http://mycompany.org/api/sampleservice/2016/02")]
public interface ISomeServiceExtension : ISomeService
{
[OperationContract]
string SayGoodbye([MessageParameter(Name = "name")] string name);
}
}
Notice that this new interface has a different namespace than the "old" one, to indicate that this is new version. Now, the service implementation can be modified.
The new parameter can be added to the existing operation and
instead of implementing the "old"
SomeService.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
using ServiceContract;
namespace ServiceImplementation
{
public class SomeService : ISomeServiceExtension
{
public string SayGoodbye(string name)
{
return string.Format("Goodbye {0}!", name);
}
public string SayHello(string name, bool upperCase = false)
{
return upperCase ? string.Format("Hello {0}!", name).ToUpper() : string.Format("Hello {0}!", name);
}
}
}
Now there will be two endpoints in the configuration file for the service; one for the version one interface and one for the new interface. The two endpoints have different addresses, names and contracts:
app.config
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="mexBehavior">
<serviceMetadata />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service name="ServiceImplementation.SomeService" behaviorConfiguration="mexBehavior">
<endpoint address="SomeService" name="SomeServiceHttpEndpoint"
binding="basicHttpBinding" contract="ServiceContract.ISomeService" />
<endpoint address="SomeServiceV2" name="SomeServiceV2HttpEndpoint"
binding="basicHttpBinding" contract="ServiceContract.ISomeServiceExtension" />
<endpoint binding="mexHttpBinding" address="mex" name="mex" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:8001/api"/>
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
The following code for a client app is a composite of possible code in an existing client and in a client that uses the new contract:
program.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
using System;
namespace Client
{
class Program
{
static void Main(string[] args)
{
// Old client using version 1:
var client = new SomeServiceProxy.SomeServiceClient();
Console.WriteLine(client.SayHello("Bent"));
// Newer client using version 2:
var client2 = new SomeServiceV2Proxy.SomeServiceExtensionClient();
Console.WriteLine(client2.SayHello("Bent", true));
Console.WriteLine(client2.SayGoodbye("Bent"));
Console.WriteLine("Press Enter to quit");
Console.ReadLine();
}
}
}
An alternative to the strategy outlined above is to simply make a completely new service for the new version while keeping the existing one around as long as someone is still using it. Continuing the above example, let's imagine we need to make a version 3 of the service with one operation removed and a new operation added in its place. Instead of inheriting from an existing interface we are going to make a new interface that does not inherit from anything and then implement it in a service that is unrelated to the previous ones. The interface might look like this:
ISomeServiceV3.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
using System.ServiceModel;
namespace ServiceContract
{
[ServiceContract(Namespace = "http://mycompany.org/api/sampleservice/2016/03")]
public interface ISomeServiceV3
{
[OperationContract]
string SayGoodbye([MessageParameter(Name = "name")] string name);
[OperationContract]
string SayGoodMorning([MessageParameter(Name = "name")] string name);
}
}
Compared with version 2, one method has been removed while another has been added. Again, the use of a different namespace signals that this is a different version. A new implementation is then needed:
SomeServiceV3.cs
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
using ServiceContract;
namespace ServiceImplementation
{
public class SomeServiceV3 : ISomeServiceV3
{
public string SayGoodbye(string name)
{
return string.Format("Goodbye {0}!", name);
}
public string SayGoodMorning(string name)
{
return string.Format("Good morning {0}!", name);
}
}
}
A whole new
This latter approach of making a completely new service interface and implementation has the benefit of providing a whole new slate with every possibility for adding, removing and changing operations and parameters. The downside is that the service code base grows for every new version and there is a risk of code duplication.
Get the code for this chapter's second example showcasing contract versioning as a Zip file (Visual Studio 2015).