Service contract interfaces can derive from each other, enabling you to define a hierarchy of contracts. However, the ServiceContract
attribute is not inheritable:
[AttributeUsage(Inherited = false
,...)]
public sealed class ServiceContractAttribute : Attribute
{...}
Consequently, every level in the interface hierarchy must explicitly have the ServiceContract
attribute, as shown in Example 2-3.
Example 2-3. Service-side contract hierarchy
[ServiceContract]
interface ISimpleCalculator
{
[OperationContract]
int Add(int arg1,int arg2);
}[ServiceContract]
interface IScientificCalculator : ISimpleCalculator
{
[OperationContract]
int Multiply(int arg1,int arg2);
}
When it comes to implementing a contract hierarchy, a single service class can implement the entire hierarchy, just as with classic C# programming:
class MyCalculator : IScientificCalculator { public int Add(int arg1,int arg2) { return arg1 + arg2; } public int Multiply(int arg1,int arg2) { return arg1 * arg2; } }
The host can expose a single endpoint for the bottom most interface in the hierarchy:
<service name = "MyCalculator"> <endpoint address = "http://localhost:8001/MyCalculator/" binding = "basicHttpBinding" contract = "IScientificCalculator" /> </service>
When the client imports the metadata of a service endpoint whose contract is part of an interface hierarchy, the resulting contract on the client side does not maintain the original hierarchy. Instead it will include a flattened hierarchy in the form of a single contract named after the endpoint’s contract. The single contract will have a union of all the operations from all the interfaces leading down to it in the hierarchy, including itself. However, the imported interface definition will maintain, in the Action
and ResponseAction
properties of the OperationContract
attribute, the name of the original contract that defined each operation:
[AttributeUsage(AttributeTargets.Method)] public sealed class OperationContractAttribute : Attribute { public string Action {get;set;} public string ReplyAction {get;set;} //More members }
Finally, a single proxy class will implement all methods in the imported contract. Given the definitions of Example 2-3, Example 2-4 shows the imported contract and the generated proxy class.
Example 2-4. Client-side flattened hierarchy
[ServiceContract] public interface IScientificCalculator { [OperationContract(Action = ".../ISimple
Calculator/Add", ReplyAction = ".../ISimple
Calculator/AddResponse")] int Add(int arg1,int arg2); [OperationContract(Action = ".../IScientific
Calculator/Multiply", ReplyAction = ".../IScientific
Calculator/MultiplyResponse")] int Multiply(int arg1,int arg2); } public partial class ScientificCalculatorClient : ClientBase<IScientificCalculator>, IScientificCalculator { public int Add(int arg1,int arg2) {...} public int Multiply(int arg1,int arg2) {...} //Rest of the proxy }
The client can manually rework the proxy and the imported contract definitions to restore the contract hierarchy as shown in Example 2-5.
Example 2-5. Client-side contract hierarchy
[ServiceContract]
public interface ISimpleCalculator
{
[OperationContract]
int Add(int arg1,int arg2);
}
public partial class SimpleCalculatorClient : ClientBase<ISimpleCalculator>,ISimpleCalculator
{
public int Add(int arg1,int arg2)
{
return Channel.Add(arg1,arg2);
}
//Rest of the proxy
}
[ServiceContract]
public interface IScientificCalculator : ISimpleCalculator
{
[OperationContract]
int Multiply(int arg1,int arg2);
}
public partial class ScientificCalculatorClient :
ClientBase<IScientificCalculator>,IScientificCalculator
{
public int Add(int arg1,int arg2)
{
return Channel.Add(arg1,arg2);
}
public int Multiply(int arg1,int arg2)
{
return Channel.Multiply(arg1,arg2);
}
//Rest of the proxy
}
Using the value of the Action
property in the various operations, the client can factor out the definitions of the comprising contracts in the service contract hierarchy and provide interface and proxy definitions, for example. ISimpleCalculator
and SimpleCalculatorClient
in Example 2-5. There is no need to set the Action
and ResponseAction
properties, and you can safely remove them all. Next, manually add the interface to the inheritance chain as required:
[ServiceContract]
public interface IScientificCalculator :ISimpleCalculator
{...}
Even though the service may have exposed just a single endpoint for the bottom-most interface in the hierarchy, the client can view it as different endpoints with the same address, where each endpoint corresponds to a different level in the contract hierarchy:
<client> <endpoint name = "SimpleEndpoint"address = "http://localhost:8001/MyCalculator/" binding = "basicHttpBinding" contract = "ISimple
Calculator" /> <endpoint name = "ScientificEndpoint" address = "http://localhost:8001/MyCalculator/" binding = "basicHttpBinding" contract = "IScientific
Calculator" /> </client>
The client can now write the following code, taking full advantage of the contract hierarchy:
SimpleCalculatorClient proxy1 = new SimpleCalculatorClient( );
proxy1.Add(1,2);
proxy1.Close( );
ScientificCalculatorClient proxy2 = new ScientificCalculatorClient( );proxy2.Add(3,4);
proxy2.Multiply(5,6);
proxy2.Close( );
The advantage of the proxy refactoring in Example 2-5 is that each level in the contract is kept separately and decoupled from the levels underneath it. Anyone on the client side that expects a reference to ISimpleCalculator
can now be given a reference to IScientificCalculator
:
void UseCalculator(ISimpleCalculator calculator) {...} ISimpleCalculator proxy1 = new SimpleCalculatorClient( ); ISimpleCalculator proxy2 = new ScientificCalculatorClient( ); IScientificCalculator proxy3 = new ScientificCalculatorClient( ); SimpleCalculatorClient proxy4 = new SimpleCalculatorClient( ); ScientificCalculatorClient proxy5 = new ScientificCalculatorClient( ); UseCalculator(proxy1); UseCalculator(proxy2); UseCalculator(proxy3); UseCalculator(proxy4); UseCalculator(proxy5);
However, there is no Is-A relationship between the proxies. Even though the IScientificCalculator
interface derives from ISimpleCalculator
, a ScientificCalculatorClient
is not a SimpleCalculatorClient
. In addition, you have to repeat the implementation of the base contract in the proxy for the subcontract. You can rectify that by using a technique I call proxy chaining, shown in Example 2-6.
Example 2-6. Proxy chaining
public partial class SimpleCalculatorClient : ClientBase<IScientificCalculator
>, ISimpleCalculator { public int Add(int arg1,int arg2) { return Channel.Add(arg1,arg2); } //Rest of the proxy } public partial class ScientificCalculatorClient :SimpleCalculatorClient
, IScientificCalculator { public int Multiply(int arg1,int arg2) { return Channel.Multiply(arg1,arg2); } //Rest of the proxy }
Only the proxy that implements the top most base contract derives directly from ClientBase<T>
, providing it as a type parameter with the bottom most subinterface. All the other proxies derive from the proxy immediately above them and the respective contract.
Proxy chaining gives you an Is-A relationship between the proxies as well as code reuse. Anyone on the client side that expects a reference to SimpleCalculatorClient
can be given now a reference to ScientificCalculatorClient
:
void UseCalculator(SimpleCalculatorClient calculator) {...} SimpleCalculatorClient proxy1 = new SimpleCalculatorClient( ); SimpleCalculatorClient proxy2 = new ScientificCalculatorClient( ); ScientificCalculatorClient proxy3 = new ScientificCalculatorClient( ); UseCalculator(proxy1); UseCalculator(proxy2); UseCalculator(proxy3);