WCF supports allowing a service to call back to its clients. During a callback, in many respects the tables are turned: the service is the client, and the client becomes the service (see Figure 5-1). Callback operations can be used in a variety of scenarios and applications, but they are especially useful when it comes to events, or notifying the client(s) that some event has happened on the service side.
Callbacks are also commonly referred to as duplex operations. There are two immediate challenges to supporting duplex communication. First, how does the service know where the callback endpoint is? Second, how does the client facilitate hosting the callback object?
Not all bindings support callback operations. Only
bidirectional-capable bindings support callback operations. For example,
because of its connectionless nature, HTTP cannot be used for callbacks, and therefore you
cannot use callbacks over the BasicHttpBinding
or the WSHttpBinding
. The only two commonly used
bindings that offer callbacks are the NetTcpBinding
and the
NetNamedPipeBinding
,
because by their very nature, the TCP and the IPC protocols support
duplex communication.
Callback operations are part of the service contract,
and it is up to the service contract to define its own callback
contract. A service contract can have at most one callback contract.
Once defined, the clients are required to support the callback and
provide the callback endpoint to the service in every call. To define
a callback contract, the Service
Contract
attribute offers the CallbackContract
property of the type
Type
:
[AttributeUsage(AttributeTargets.Interface|AttributeTargets.Class)] public sealed class ServiceContractAttribute : Attribute { public Type CallbackContract {get;set;} //More members }
When you define a service contract with a callback contract, you
need to provide the ServiceContract
attribute with the type of the callback contract and the definition of
the callback contract, as shown in Example 5-1.
Example 5-1. Defining and configuring a callback contract
interface ISomeCallbackContract
{
[OperationContract]
void OnCallback();
}
[ServiceContract(CallbackContract = typeof(ISomeCallbackContract)
)]
interface IMyContract
{
[OperationContract]
void DoSomething();
}
Note that the callback contract need not be marked with the
ServiceContract
attribute—the ServiceContract
attribute is implied because
it is defined as a callback contract and will be included in the
service metadata. Of course, you still need to mark all the callback
interface methods with the OperationContract
attribute.
When the client imports the metadata of the callback contract,
the imported callback interface will not have the same name on the
client as in the original service-side definition. The name on the
client will be the name of the service contract interface, suffixed
with the word Callback
. For
example, a client that imports the definitions of Example 5-1 will end up with
these definitions instead:
interfaceIMyContractCallback
{ [OperationContract] void OnCallback(); } [ServiceContract(CallbackContract = typeof(IMyContractCallback
))] interface IMyContract { [OperationContract] void DoSomething(); }
For simplicity’s sake, I recommend using this naming
convention also on the service side (i.e., naming the callback
contract with the service contract interface name suffixed by
Callback
).
It is up to the client to host the callback object and
expose a callback endpoint. Recall from Chapter 1 that the innermost execution scope of the
service instance is the instance context. The InstanceContext
class
provides a constructor that takes the service instance to the
host:
public sealed class InstanceContext : CommunicationObject,...
{
public InstanceContext(object implementation
);
public object GetServiceInstance();
//More members
}
All the client needs to do to host a callback object is instantiate the callback object and construct a context around it:
class MyCallback : IMyContractCallback
{
public void OnCallback()
{...}
}
IMyContractCallback callback = new MyCallback();
InstanceContext context = new InstanceContext(callback
);
It is also worth mentioning that although the callback methods
are on the client side, they are WCF operations in every respect and
therefore have an operation call context, accessible via OperationContext.Current
.
Whenever a client is interacting with a service
endpoint whose contract defines a callback contract, the client must
use a proxy that will set up the bidirectional communication and
pass the callback endpoint reference to the service. To that end,
the proxy the client uses must derive from the specialized proxy
class DuplexClientBase<T>
, shown in Example 5-2.
Example 5-2. The DuplexClientBase<T> class
public interface IDuplexContextChannel : IContextChannel { InstanceContext CallbackInstance {get;set;} //More members } public abstract class DuplexClientBase<T> : ClientBase<T> where T : class { protected DuplexClientBase(InstanceContext callbackContext
); protected DuplexClientBase(InstanceContext callbackContext
,string endpointName); protected DuplexClientBase(InstanceContext callbackContext
,Binding binding, EndpointAddress remoteAddress); protected DuplexClientBase(object callbackInstance
); protected DuplexClientBase(object callbackInstance
,string endpointName); protected DuplexClientBase(object callbackInstance
,Binding binding, EndpointAddress remoteAddress); public IDuplexContextChannel InnerDuplexChannel {get;} //More members }
The client needs to provide the constructor of DuplexClientBase<T>
with the
instance context hosting the callback object (as well as the service
endpoint information, as with a regular proxy). The proxy will
construct an endpoint around the callback context, while inferring
the details of the callback endpoint from the service endpoint
configuration: the callback endpoint contract is the one defined by
the service contract callback type. The callback endpoint will use
the same binding (and transport) as the outgoing call. For the
address, WCF will use the client’s machine name. Simply passing the
instance context to the duplex proxy and using the proxy to call the
service will expose that client-side callback endpoint. To
streamline the process, DuplexClientBase<T>
also offers
constructors that accept the callback object directly and wrap it
internally with a context. If for any reason the client needs to
access that context, DuplexClientBase<T>
additionally
offers the InnerDuplexChannel
property of the type IDuplex
Context
Channel
, which provides access to the context via the Call
back
Instance
property.
When you use Visual Studio 2010 to generate a proxy class
targeting a service with a callback contract, the tool will generate
a class that derives from DuplexClientBase<T>
, as shown in
Example 5-3.
Example 5-3. VS 2010-generated duplex proxy
class MyContractClient : DuplexClientBase
<IMyContract>,IMyContract
{
public MyContractClient(InstanceContext callbackContext) : base(callbackContext)
{}
public MyContractClient(InstanceContext callbackContext,string endpointName) :
base(callbackContext,endpointName)
{}
public MyContractClient(InstanceContext callbackContext,Binding binding,
EndpointAddress remoteAddress) :
base(callbackContext,binding,remoteAddress)
{}
//More constructors
public void DoSomething()
{
Channel.DoSomething();
}
}
Using that derived proxy class, the client can construct a callback instance, host it in a context, create a proxy, and call the service, thus passing the callback endpoint reference:
class MyCallback : IMyContractCallback
{
public void OnCallback()
{...}
}
IMyContractCallback callback = new MyCallback();
InstanceContext context = new InstanceContext(callback);
MyContractClient proxy = new MyContractClient(context
);
proxy.DoSomething();
Note that as long as the client is expecting callbacks, the client cannot close the proxy. Doing so will close the callback endpoint and cause an error on the service side when the service tries to call back.
It is often the case that the client itself implements the callback contract, in which case the client will typically use a member variable for the proxy and close it when the client is disposed of, as shown in Example 5-4.
Example 5-4. Client implementing the callback contract
class MyClient : IMyContractCallback,IDisposable { MyContractClient m_Proxy; public void CallService() { InstanceContext context = new InstanceContext(this
); m_Proxy = new MyContractClient(context
); m_Proxy.DoSomething(); } public void OnCallback() {...} public void Dispose() { m_Proxy.Close(); } }
The generated proxy does not take advantage of the streamlined
constructors of DuplexClientBase<T>
that accept the
callback object directly, but you can rework the proxy manually to
add that support, as shown in Example 5-5.
Example 5-5. Using a reworked object-based proxy
class MyContractClient : DuplexClientBase<IMyContract>,IMyContract { public MyContractClient(object
callbackInstance) : base(callbackInstance) {} //More constructors public void DoSomething() { Channel.DoSomething(); } } class MyClient : IMyContractCallback,IDisposable { MyContractClient m_Proxy; public void CallService() { m_Proxy = new MyContractClient(this
); m_Proxy.DoSomething(); } public void OnCallback() {...} public void Dispose() { m_Proxy.Close(); } }
The client-side callback endpoint reference is passed
along with every call the client makes to the service, and it is part
of the incoming message. The OperationContext
class provides the service
with easy access to the callback reference via the generic method
GetCallbackChannel<T>()
:
public sealed class OperationContext : ... { public T GetCallbackChannel<T>(); //More members }
Exactly what the service does with the callback reference and when it decides to use it is completely at the discretion of the service. The service can extract the callback reference from the operation context and store it for later use, or it can use it during the service operation to call back to the client. Example 5-6 demonstrates the first option.
Example 5-6. Storing the callback references for later use
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract { static List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>(); public void DoSomething() { IMyContractCallback callback = OperationContext.Current. GetCallbackChannel<IMyContractCallback>(); if(m_Callbacks.Contains(callback) == false) { m_Callbacks.Add(callback); } } public static void CallClients() { Action<IMyContractCallback> invoke = callback => callback.OnCallback(); m_Callbacks.ForEach(invoke); } }
The service uses a static, generic linked list to store
references to interfaces of the type IMyContractCallback
. Because the service is
not aware of which client is calling it and whether or not the client
has called it already, in every call the service checks to see whether
the list already contains the passed-in callback reference. If the
list does not contain the reference, the service adds the callback to
the list.
The service class also offers the static method CallClients()
, which any party on the host
side can use to call back to the clients:
MyService.CallClients();
Here, the invoking party is using some host-side thread for the callback invocation. That thread is unrelated to any thread executing the incoming service call.
Example 5-6 (and similar examples in this chapter) does not synchronize access to the callback list. Obviously, real application code will need to do that. Concurrency management (and, in particular, synchronizing access to shared resources) is discussed in Chapter 8.
The service may also want to invoke the callback reference that’s passed in (or a saved copy of it) during the execution of a contract operation. However, such invocations are disallowed by default. The reason is the default service concurrency management. By default, the service class is configured for single-threaded access: the service instance context is associated with a lock, and only one thread at a time can own the lock and access the service instance inside that context. Calling out to the client during an operation call requires blocking the service thread and invoking the callback. The problem is that processing the reply message from the client on the same channel once the callback returns requires reentering the same context and negotiating ownership of the same lock, which will result in a deadlock. Note that the service may still invoke callbacks to other clients or call other services; it is the callback to its calling client that will cause the deadlock.
To prevent such a deadlock, if the single-threaded service
instance tries to call back to its client, WCF will throw an
InvalidOperationException
. There are three
possible solutions. The first is to configure the service for
multithreaded access. Callbacks to the calling client will then be
allowed because the service instance will not be associated with a
lock; however, this will increase the burden on the service
developer, because of the need to provide synchronization for the
service. The second solution is to configure the service for
reentrancy. When configured for reentrancy, the service instance
context is still associated with a lock, and only single-threaded
access is allowed. However, if the service is calling back to its
client, WCF will silently release the lock first. Chapter 8 is dedicated to the
synchronization modes and their implications on the programming
model. For now, all you need to know is that you can set the
concurrency behavior to either multithreaded or reentrant using the
ConcurrencyMode
property of the
ServiceBehavior
attribute:
public enum ConcurrencyMode { Single, //Default Reentrant, Multiple } [AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : ... { public ConcurrencyMode ConcurrencyMode {get;set;} //More members }
Example 5-7 demonstrates a service configured for reentrancy. During the operation execution, the service accesses the operation context, grabs the callback reference to its calling client, and invokes it. Control will only return to the service once the callback returns, and the service’s own thread will need to reacquire the lock.
Example 5-7. Configuring for reentrancy to allow callbacks
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{
[OperationContract]
void DoSomething();
}
interface IMyContractCallback
{
[OperationContract]
void OnCallback();
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant
)]
class MyService : IMyContract
{
public void DoSomething()
{
IMyContractCallback callback = OperationContext.Current.
GetCallbackChannel<IMyContractCallback>();
callback.OnCallback();
}
}
The third solution that allows the service to safely call back to the calling client is to configure the callback contract operations as one-way operations. Doing so will enable the service to call back even when the concurrency mode is set to single-threaded, because there will not be any reply message to contend for the lock. Example 5-8 demonstrates this configuration. Note that the service defaults to single-threaded concurrency mode.
Example 5-8. One-way callbacks are allowed by default
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{
[OperationContract]
void DoSomething();
}
interface IMyContractCallback
{
[OperationContract(IsOneWay = true
)]
void OnCallback();
}
class MyService : IMyContract
{
public void DoSomething()
{
IMyContractCallback callback = OperationContext.Current.
GetCallbackChannel<IMyContractCallback>();
callback.OnCallback();
}
}
The callback mechanism supplies nothing like a
higher-level protocol for managing the connection between the service
and the callback endpoint. It is up to the developer to come up with
some application-level protocol or a consistent pattern for managing
the lifecycle of the connection. As mentioned previously, the service
can only call back to the client if the client-side channel is still
open, which is typically achieved by not closing the proxy. Keeping
the proxy open will also prevent the callback object from being
garbage-collected. If the service maintains a reference on a callback
endpoint and the client-side proxy is closed or the client application
itself is gone, when the service invokes the callback it will get an
ObjectDisposedException
from the service
channel. It is therefore preferable for the client to inform the
service when it no longer wishes to receive callbacks or when the
client application is shutting down. To that end, you can add an
explicit Disconnect()
method to the
service contract. Since every method call carries the callback
reference with it, in the Disconnect()
method the service can remove
the callback reference from its internal store.
In addition, for symmetry’s sake, I recommend adding an explicit
Connect()
method. Having a Connect()
method will enable the client to
connect or disconnect multiple times, as well as provide a clearly
delineated point in time as to when to expect a callback (only after a
call to Connect()
). Example 5-9 demonstrates this
technique. In both the Connect()
and Disconnect()
methods, the
service needs to obtain the callback reference. In Connect()
, the service verifies that the
callback list does not already contain the callback reference before
adding it to the list (this makes multiple calls to Connect()
benign). In Disconnect()
, the service verifies that the
list contains the callback reference, and it throws an exception
otherwise.
Example 5-9. Explicit callback connection management
[ServiceContract(CallbackContract = typeof(IMyContractCallback))] interface IMyContract { [OperationContract] void DoSomething(); [OperationContract] void Connect(); [OperationContract] void Disconnect(); } interface IMyContractCallback { [OperationContract] void OnCallback(); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IMyContract {static
List<IMyContractCallback> m_Callbacks = new List<IMyContractCallback>(); public void Connect() { IMyContractCallback callback = OperationContext.Current. GetCallbackChannel<IMyContractCallback>(); if(m_Callbacks.Contains(callback) == false) { m_Callbacks.Add(callback); } } public void Disconnect() { IMyContractCallback callback = OperationContext.Current. GetCallbackChannel<IMyContractCallback>(); if(m_Callbacks.Contains(callback)) { m_Callbacks.Remove
(callback); } else { throw new InvalidOperationException("Cannot find callback"); } } public static void CallClients() { Action<IMyContractCallback> invoke = callback => callback.OnCallback(); m_Callbacks.ForEach(invoke); } public void DoSomething() {...} }
A per-call service can use a callback reference during
the operation call itself, or store it in some kind of a global
repository such as a static variable, as you have seen in the
examples so far. The per-call service must use some static variable
to store the reference, since any instance state the service may use
to store the reference will be gone when the operation returns.
Using a Disconnect()
-like method
is therefore especially required for per-call services, as without
it, the shared store will become bloated over time with dead
callback references. A similar need exists with a singleton service. The singleton lifetime has no end,
so it will accumulate an unlimited number of callback references,
and as time goes by most of them will become stale because the
callback clients will no longer be running. Having a Disconnect()
method will keep the
singleton connected only to the relevant alive clients.
Interestingly enough, a per-session service may get by without
a Disconnect()
method, as long as
it maintains the callback reference in some instance member
variable. The reason is that the service instance will automatically
be disposed of when the session ends (when the client closes the
proxy or times out), and there is no danger in keeping the reference
throughout the session, as it is guaranteed to always be valid.
However, if the sessionful service stores its callback reference in
some global repository for the use of other host-side parties or
across sessions, adding a Disconnect()
method is required in order
to remove the callback reference explicitly, because the callback
reference is not available during the call to Dispose()
.
You may also want to add the Connect()
and Disconnect()
pair on a sessionful service
simply as a feature, because it enables the client to decide when to
start or stop receiving callbacks during the session.
The WCF-provided DuplexClientBase<T>
is not strongly
typed to the callback interface used. The compiler will let you pass
in any object, including an invalid callback interface. The compiler
will even let you use for T
a
service contract type that has no callback contract defined at all. At
runtime, you can successfully instantiate the proxy. The
incompatibility will be discovered only when you try to use the proxy,
yielding an InvalidOperationException
. Much the same way, InstanceContext
is
object-based and is not verified at compile time to actually have a
valid callback contract instance. When it’s passed as a constructor
parameter to the duplex proxy, there is no compile-time check to
correlate the InstanceContext
with
the callback instance the duplex proxy expects, and the error will be
discovered only when you try to use the proxy. You can use generics to
compensate to some degree for these oversights and discover the error
at runtime, as soon as you declare the proxy.
First, define the type-safe, generic InstanceContext<T>
class, shown in
Example 5-10.
Example 5-10. The InstanceContext<T> class
public class InstanceContext<T> { public InstanceContext Context {get;private set;} public InstanceContext(T callbackInstance) { Context = new InstanceContext(callbackInstance); } public void ReleaseServiceInstance() { Context.ReleaseServiceInstance(); } public T ServiceInstance { get { return (T)Context.GetServiceInstance(); } } }
By using generics, you also provide type-safe access to the hosted callback object and capture the desired callback type.
Next, define a new type-safe, generic subclass of DuplexClientBase<T>
, as shown in Example 5-11.
Example 5-11. The DuplexClientBase<T,C>class
//T is the service contract and C is the callback contract
public abstract class DuplexClientBase<T,C> : DuplexClientBase<T> where T : class
{
protected DuplexClientBase(InstanceContext<C> context) : base(context.Context)
{}
protected DuplexClientBase(InstanceContext<C> context,string endpointName) :
base(context.Context,endpointName)
{}
protected DuplexClientBase(InstanceContext<C> context,Binding binding,
EndpointAddress remoteAddress) :
base(context.Context,binding,remoteAddress)
{}
protected DuplexClientBase(C callback) : base(callback)
{}
protected DuplexClientBase(C callback,string endpointName) :
base(callback,endpointName)
{}
protected DuplexClientBase(C callback,Binding binding,
EndpointAddress remoteAddress) :
base(callback,binding,remoteAddress)
{}
/* More constructors */
static
DuplexClientBase()
{
VerifyCallback();
}
internal static void VerifyCallback()
{
Type contractType = typeof(T);
Type callbackType = typeof(C);
object[] attributes = contractType.GetCustomAttributes(
typeof(ServiceContractAttribute),false);
if(attributes.Length == 0)
{
throw new InvalidOperationException("Type of " + contractType +
" is not a service contract");
}
ServiceContractAttribute serviceContractAttribute;
serviceContractAttribute = attributes[0] as ServiceContractAttribute;
if(callbackType != serviceContractAttribute.CallbackContract)
{
throw new InvalidOperationException("Type of " + callbackType +
" is not configured as callback contract for " + contractType);
}
}
}
The DuplexClientBase<T,C>
class uses two
type parameters: T
is used for the
service contract type parameter and C
is used for the callback contract type
parameter. The constructors of DuplexClientBase<T,C>
can accept
either a raw C
instance or an
instance of InstanceContext<C>
wrapping a C
instance. These enable the compiler to
ensure that only compatible contexts are used. However, .NET does not
support a way to constrain a declarative relationship between T
and C
.
The workaround is to perform a single runtime check before any use of
DuplexClientBase<T,C>
, and
abort the use of the wrong type immediately, before any damage can be
done. The trick is to place the runtime verification in the C# static
constructor. The static constructor of DuplexClientBase<T,C>
calls the static
helper method VerifyCallback()
.
VerifyCallback()
uses reflection to
first verify that T
is decorated
with the ServiceContract
attribute. Then it verifies that it has a type set for the callback
contract that is the type parameter C
. If not, an exception will be thrown in
the static constructor, enabling you to discover the error as soon as
possible at runtime.
Performing the callback contract verification in the static constructor is a technique applicable to any constraint that you cannot enforce at compile time, yet you have some programmatic way of determining and enforcing it at runtime.
Next, if you used Visual Studio 2010 to generate the proxy, you
need to rework the proxy to derive from the type-safe DuplexClientBase<T,C>
class:
class MyContractClient : DuplexClientBase<IMyContract,IMyContractCallback>, IMyContract { public MyContractClient(InstanceContext<IMyContractCallback> context) : base(context) {} public MyContractClient(IMyContractCallback callback) : base(callback) {} /* Rest of the constructors */ public void DoSomething() { Channel.DoSomething(); } }
You can provide the reworked proxy either with a type-safe instance context, or with the callback instance directly:
//Client code class MyCallback : IMyContractCallback {...} IMyContractCallback callback = new MyCallback(); MyContractClient proxy1 = new MyContractClient(callback); InstanceContext<IMyContractCallback> context = new InstanceContext<IMyContractCallback>(callback); MyContractClient proxy2 = new MyContractClient(context);
Either way, the compiler will verify that the type parameters provided to the proxy match the context type parameter or the callback instance, and the static constructor will verify the relationship between the service contract and the callback instance upon instantiation.
Similar to the ChannelFactory<T>
class, WCF also
offers DuplexChannelFactory<T>
, which can be
used for setting up duplex proxies programmatically:
public class DuplexChannelFactory<T> : ChannelFactory<T> { public DuplexChannelFactory(object callback); public DuplexChannelFactory(object callback,string endpointName); public DuplexChannelFactory(InstanceContext context,string endpointName); public T CreateChannel(InstanceContext context); public static T CreateChannel(object callback,string endpointName); public static T CreateChannel(InstanceContext context,string endpointName); public static T CreateChannel(object callback,Binding binding, EndpointAddress endpointAddress); public static T CreateChannel(InstanceContext context,Binding binding, EndpointAddress endpointAddress); //More members }
DuplexChannelFactory<T>
is used just like its base class, ChannelFactory<T>
, except its
constructors expect either a callback instance or a callback context.
Note again the use of object
for
the callback instance and the lack of type safety. Example 5-12 shows my
reworked DuplexChannelFactory<T,C>
class, which
provides both compile-time and runtime type safety (similar to the
fixed-up DuplexClientBase<T>
class presented in Example 5-11).
Example 5-12. The DuplexChannelFactory<T,C> class
public class DuplexChannelFactory<T,C> : DuplexChannelFactory<T> where T : class { static DuplexChannelFactory() { DuplexClientBase<T,C>.VerifyCallback(); } public static T CreateChannel(C callback,string endpointName) { return DuplexChannelFactory<T>.CreateChannel(callback,endpointName); } public static T CreateChannel(InstanceContext<C> context,string endpointName) { return DuplexChannelFactory<T>.CreateChannel(context.Context,endpointName); } public static T CreateChannel(C callback,Binding binding, EndpointAddress endpointAddress) { return DuplexChannelFactory<T>.CreateChannel(callback,binding, endpointAddress); } public static T CreateChannel(InstanceContext<C> context,Binding binding, EndpointAddress endpointAddress) { return DuplexChannelFactory<T>.CreateChannel(context,binding, endpointAddress); } public DuplexChannelFactory(C callback) : base(callback) {} public DuplexChannelFactory(C callback,string endpointName) : base(callback,endpointName) {} public DuplexChannelFactory(InstanceContext<C> context,string endpointName) : base(context.Context,endpointName) {} //More constructors }
As an example of utilizing the duplex channel factory, consider
Example 5-13, which adds
callback ability to the InProcFactory
static
helper class presented in Chapter 1.
Example 5-13. Adding duplex support to InProcFactory
public static class InProcFactory { public static I CreateInstance<S,I,C>(C callback) where I : class where S : class,I { InstanceContext<C> context = new InstanceContext<C>(callback); return CreateInstance<S,I,C>(context); } public static I CreateInstance<S,I,C>(InstanceContext<C> context) where I : class where S : class,I { HostRecord hostRecord = GetHostRecord<S,I>(); return DuplexChannelFactory<I,C>.CreateChannel(context,Binding, hostRecord.Address); } //More members } //Sample client IMyContractCallback callback = new MyCallback(); IMyContract proxy = InProcFactory.CreateInstance<MyService,IMyContract, IMyContractCallback>(callback); proxy.DoSomething(); InProcFactory.CloseProxy(proxy);
An interesting constraint on the design of callback contracts is that a service contract can designate a callback contract only if that contract is a sub-interface of all callback contracts defined by the contract’s own base contracts. For example, here is an invalid callback contract definition:
interface ICallbackContract1
{...}
interface ICallbackContract2
{...}
[ServiceContract(CallbackContract = typeof(ICallbackContract1))]
interface IMyBaseContract
{...}
//Invalid
[ServiceContract(CallbackContract = typeof(ICallbackContract2
))]
interface IMySubContract : IMyBaseContract
{...}
IMySubContract
cannot
designate ICallbackContract2
as a
callback contract because ICallbackContract2
is not a sub-interface of
ICallbackContract1
, which IMy
Base
Contract
(the base of
IMySubContract
) defines as its own
callback contract.
The reason for this constraint is obvious: if a client passes an
endpoint reference to a service implementation of IMySubContract
, that callback reference must
satisfy the callback type expected by IMyBaseContract
. WCF verifies the callback
contract hierarchy at service load time and throws an InvalidOperationException
in the case of a
violation.
The straightforward way to satisfy the constraint is to reflect the service contract hierarchy in the callback contract hierarchy:
interface ICallbackContract1
{...}
interface ICallbackContract2 : ICallbackContract1
{...}
[ServiceContract(CallbackContract = typeof(ICallbackContract1))]
interface IMyBaseContract
{...}
[ServiceContract(CallbackContract = typeof(ICallbackContract2))]
interface IMySubContract : IMyBaseContract
{...}
However, you can also use multiple interface inheritance by a single callback contract and avoid mimicking the service contract hierarchy:
interface ICallbackContract1
{...}
interface ICallbackContract2
{...}
interface ICallbackContract3 : ICallbackContract2,ICallbackContract1
{...}
[ServiceContract(CallbackContract = typeof(ICallbackContract1))]
interface IMyBaseContract1
{...}
[ServiceContract(CallbackContract = typeof(ICallbackContract2))]
interface IMyBaseContract2
{...}
[ServiceContract(CallbackContract = typeof(ICallbackContract3))]
interface IMySubContract : IMyBaseContract1,IMyBaseContract2
{...}
Note, also, that a service can implement its own callback contract:
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{...}
[ServiceContract]
interface IMyContractCallback
{...}
class MyService : IMyContract,IMyContractCallback
{...}
The service can even store a reference to itself in some callback store (if it wishes to be called back as if it were a client).
3.133.114.221