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 bottommost interface in the hierarchy:
<service name = "MyCalculator"> <endpoint address = "http://localhost:8001/MyCalculator/" binding = "basicHttpBinding" contract = "IScientificCalculator" /> </service>
When a client imports the metadata of a service endpoint
whose contract is part of an interface hierarchy, the resulting
contract on the client side will 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 ReplyAction
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 in Example 2-3, Example 2-4 shows the imported contract and the generated proxy class.
Example 2-4. Client-side flattened hierarchy
[ServiceContract] 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); } 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] interface ISimpleCalculator { [OperationContract] int Add(int arg1,int arg2); } class SimpleCalculatorClient : ClientBase<ISimpleCalculator>,ISimpleCalculator
{ public int Add(int arg1,int arg2) { return Channel.Add(arg1,arg2); } //Rest of the proxy } [ServiceContract] interface IScientificCalculator :ISimpleCalculator
{ [OperationContract] int Multiply(int arg1,int arg2); } 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 Reply
Action
properties, and you can safely remove them
all. Next, manually add the interface to the inheritance chain as
required:
[ServiceContract]
interface IScientificCalculator : ISimpleCalculator
{...}
Even though the service may have exposed just a single endpoint for the bottommost 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. The ISci
entific
Calculator
interface derives from
ISimpleCalculator
, but 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, illustrated in
Example 2-6.
Example 2-6. Proxy chaining
class SimpleCalculatorClient : ClientBase<IScientificCalculator
>,ISimpleCalculator { public int Add(int arg1,int arg2) { return Channel.Add(arg1,arg2); } //Rest of the proxy } 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 topmost base contract
derives directly from Client
Base<T>
, providing it as a type
parameter with the bottommost 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 enabling code reuse. Anyone on the client side
that expects a reference to SimpleCalculatorClient
can now be given 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);
3.144.131.62