In my IRC app, the application receives content from an IRC server. The content is sent in to a factory and the factory spits out an IMessage
object that can be consumed by the presentation layer of the application. The IMessage
interface and a single implementation is shown below.
public interface IMessage
{
object GetContent();
}
public interface IMessage<out TContent> : IMessage where TContent : class
{
TContent Content { get; }
}
public class ServerMessage : IMessage<string>
{
public ServerMessage(string content)
{
this.Content = content;
}
public string Content { get; private set; }
public object GetContent()
{
return this.Content;
}
}
To receive the IMessage
object, the presentation layer subscribes to notifications that are published within my domain layer. The notification system iterates over a collection of subscribers to a specified IMessage
implementation and fires a callback method to the subscriber.
public interface ISubscription
{
void Unsubscribe();
}
public interface INotification<TMessageType> : ISubscription where TMessageType : class, IMessage
{
void Register(Action<TMessageType, ISubscription> callback);
void ProcessMessage(TMessageType message);
}
internal class Notification<TMessage> : INotification<TMessage> where TMessage : class, IMessage
{
private Action<TMessage, ISubscription> callback;
public void Register(Action<TMessage, ISubscription> callbackMethod)
{
this.callback = callbackMethod;
}
public void Unsubscribe()
{
this.callback = null;
}
public void ProcessMessage(TMessage message)
{
this.callback(message, this);
}
}
public class NotificationManager
{
private ConcurrentDictionary<Type, List<ISubscription>> listeners =
new ConcurrentDictionary<Type, List<ISubscription>>();
public ISubscription Subscribe<TMessageType>(Action<TMessageType, ISubscription> callback) where TMessageType : class, IMessage
{
Type messageType = typeof(TMessageType);
// Create our key if it doesn't exist along with an empty collection as the value.
if (!listeners.ContainsKey(messageType))
{
listeners.TryAdd(messageType, new List<ISubscription>());
}
// Add our notification to our listener collection so we can publish to it later, then return it.
var handler = new Notification<TMessageType>();
handler.Register(callback);
List<ISubscription> subscribers = listeners[messageType];
lock (subscribers)
{
subscribers.Add(handler);
}
return handler;
}
public void Publish<T>(T message) where T : class, IMessage
{
Type messageType = message.GetType();
if (!listeners.ContainsKey(messageType))
{
return;
}
// Exception is thrown here due to variance issues.
foreach (INotification<T> handler in listeners[messageType])
{
handler.ProcessMessage(message);
}
}
}
In order to demonstrate how the above code works, I have a simple Console application that subscribes to notifications from the above ServerMessage
type. The console app first publishes by passing the ServerMessage
object in to the Publish<T>
method directly. This works without any issues.
The 2nd example has the app creating an IMessage instance using a factory method. The IMessage instance is then passed in to the Publish<T>
method, causing my variance issue to throw an InvalidCastException
.
class Program
{
static void Main(string[] args)
{
var notificationManager = new NotificationManager();
ISubscription subscription = notificationManager.Subscribe<ServerMessage>(
(message, sub) => Console.WriteLine(message.Content));
notificationManager.Publish(new ServerMessage("This works"));
IMessage newMessage = MessageFactoryMethod("This throws exception");
notificationManager.Publish(newMessage);
Console.ReadKey();
}
private static IMessage MessageFactoryMethod(string content)
{
return new ServerMessage(content);
}
}
The exception states that I can not cast an INotification<IMessage>
(what the Publish method is understands the message being published to be) in to an INotification<ServerMessage>
.
I have tried to mark the INotification interface generic as contravariant, like INotification<in TMessageType>
but can't do that because I'm consuming TMessageType
as a parameter to the Register
method's callbacks. Should I split the interface in to two separate interfaces? One that can register and one that can consume? Is that the best alternative?
Any additional help on this would be great.
The basic issue here is that you are trying to use your types in a variant way, but that's not supported for the syntax you are trying to use. Thanks to your updated and now complete (and nearly minimal) code example, it's also clear that you simply can't do this the way you have it written now.
The interface in question, and in particular the method you want to use (i.e. ProcessMessage()
, could in fact be declared as a covariant interface (if you split the Register()
method into a separate interface). But doing so would not solve your problem.
You see, the issue is that you are trying to assign an implementation of INotification<ServerMessage>
to a variable typed as INotification<IMessage>
. Note that once that implementation is assigned to the variable of that type, the caller could pass any instance of IMessage
to the method, even one that is not an instance of ServerMessage
. But the actual implementation expects (nay, demands!) an instance of ServerMessage
.
In other words, the code you are trying to write simply is not statically safe. It has no way at compile time to guarantee that the types match, and that's just not something C# is willing to do.
One option would be to weaken the type-safety of the interface, by making it non-generic. I.e. just have it always accept an IMessage
instance. Then each implementation would have to cast according to its needs. Coding errors would be caught only at run-time, with an InvalidCastException
, but correct code would run fine.
Another option would be to set the situation up so that the full type parameter is known. For example, make PushMessage()
generic method too, so that it can call Publish()
using the type parameter of ServerMessage
instead of IMessage
:
private void OnMessageProcessed(IrcMessage message, IrcCommand command, ICommandFormatter response)
{
this.OnMessageProcessed(message);
ServerMessage formattedMessage = (ServerMessage)response.FormatMessage(message, command);
this.PushMessage(formattedMessage);
}
private void PushMessage<T>(T notification) where T : IMessage
{
this.notificationManager.Publish(notification);
}
That way, the type parameter T
would match exactly in the foreach
loop where you're having the problem.
Personally, I prefer the second approach. I realize that in your current implementation, this doesn't work. But IMHO it could be worth revisiting the broader design to see if you can accomplish the same feature while preserving the generic type throughout, so that it can be used to ensure compile-time type safety.
Long stretch here, from fiddling around with the code you provided...
Using breakpoints, may I know what the method thinks T is and what the type of listeners[messageType] is?
foreach (Notification<T> handler in listeners[messageType])
{
handler.ProcessMessage(message);
}
Because if indeed it's Notification<IMessage>
on one side and Notification<ServerMessage>
on the other, then yes this is an assignment compatibility issue.
There's a solution, but you have not shown code for how Notification is built. I'll extrapolate from your current code base. This should be all you need.
public interface INotification<in T> { /* interfacy stuff */ }
public class Notification<T>: INotification<T> { /* classy stuff */ }
Then modify the code in such a way that essentially this is called:
foreach (INotification<T> handler in listeners[messageType]) { /* loop stuff */ }
Where listeners[messageType] must be INotification.
This should prevent the need to cast your Notification to Notification explicitly like the compiler is complaining.
The magic occurs in the interface declaration for INotification, the in T key phrase (bad terminology, sorry), lets the compiler know that T is contravariant (by default, if you omit out, T is invariant, as in types must match).
EDIT: Per the comments, I have updated the answer to reflect the code that's actually written as opposed to what I thought was written. This mainly means declaring INotification as contravariant (in T) as opposed to covariant (out T).
I solved this by adding another level of indirection. Having followed advice from @PeterDuniho, I broke apart the INotification<TMessage>
interface in to two separate interfaces. By adding the new INotificationProcessor
interface, I can change my collection of listeners from an ISubscription
to an INotificationProcessor
and then iterate over my collection of listeners as an INotificationProcessor.
public interface ISubscription
{
void Unsubscribe();
}
public interface INotification<TMessageType> : ISubscription where TMessageType : class, IMessage
{
void Register(Action<TMessageType, ISubscription> callback);
}
public interface INotificationProcessor
{
void ProcessMessage(IMessage message);
}
The INotificationProcessor
implementation, implements both INotificationProcessor
and INotification<TMessageType>
. This allows the Notification
class below to cast the IMessage provided in to the appropriate generic type for publication.
internal class Notification<TMessage> : INotificationProcessor, INotification<TMessage> where TMessage : class, IMessage
{
private Action<TMessage, ISubscription> callback;
public void Register(Action<TMessage, ISubscription> callbackMethod)
{
this.callback = callbackMethod;
}
public void Unsubscribe()
{
this.callback = null;
}
public void ProcessMessage(IMessage message)
{
// I can now cast my IMessage to T internally. This lets
// subscribers use this and not worry about handling the cast themselves.
this.callback(message as TMessage, this);
}
}
My NotificationManager
can now hold a collection of INotificationProcessor
types instead of ISubscription
and invoke the ProcessMessage(IMessage)
method regardless if what comes in to it is an IMessage
or a ServerMessage
.
public class NotificationManager
{
private ConcurrentDictionary<Type, List<INotificationProcessor>> listeners =
new ConcurrentDictionary<Type, List<INotificationProcessor>>();
public ISubscription Subscribe<TMessageType>(Action<TMessageType, ISubscription> callback) where TMessageType : class, IMessage
{
Type messageType = typeof(TMessageType);
// Create our key if it doesn't exist along with an empty collection as the value.
if (!listeners.ContainsKey(messageType))
{
listeners.TryAdd(messageType, new List<INotificationProcessor>());
}
// Add our notification to our listener collection so we can publish to it later, then return it.
var handler = new Notification<TMessageType>();
handler.Register(callback);
List<INotificationProcessor> subscribers = listeners[messageType];
lock (subscribers)
{
subscribers.Add(handler);
}
return handler;
}
public void Publish<T>(T message) where T : class, IMessage
{
Type messageType = message.GetType();
if (!listeners.ContainsKey(messageType))
{
return;
}
// Exception is thrown here due to variance issues.
foreach (INotificationProcessor handler in listeners[messageType])
{
handler.ProcessMessage(message);
}
}
}
The original app example now works without issue.
class Program
{
static void Main(string[] args)
{
var notificationManager = new NotificationManager();
ISubscription subscription = notificationManager.Subscribe<ServerMessage>(
(message, sub) => Console.WriteLine(message.Content));
notificationManager.Publish(new ServerMessage("This works"));
IMessage newMessage = MessageFactoryMethod("This works without issue.");
notificationManager.Publish(newMessage);
Console.ReadKey();
}
private static IMessage MessageFactoryMethod(string content)
{
return new ServerMessage(content);
}
}
Thanks everyone for the help.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With