The problem
We have the following code:
public abstract class MessageHandler
{
public abstract Message Poll();
}
public abstract class Message
{
protected Message(string id, string correlationId, object data)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
CorrelationId = correlationId ?? throw new ArgumentNullException(nameof(correlationId));
Data = data ?? throw new ArgumentNullException(nameof(data));
}
public string Id { get; }
public string CorrelationId { get; }
public object Data { get; }
}
public class Message<T> : Message where T : class
{
public Message(string id, string correlationId, T data) : base(id, correlationId, data)
{
}
}
We implement our MessageHandler like this:
public class HamburgerMessageHandler : MessageHandler
{
public override Message Poll()
{
var message = new Message<HamburgerMessageBody>(
id: Guid.NewGuid().ToString(),
correlationId: Guid.NewGuid().ToString(),
new HamburgerMessageBody("Triple Smash"));
return message;
}
}
And then we consume it like this:
[Fact]
public void MessageDataShouldBeOfHamburgerMessageBodyType()
{
var message = _sut.Poll();
message.Data.ShouldBeOfType<HamburgerMessageBody>();
// Since message.Data is of type object, we need to cast it.
var hamburgerMessageBody = (HamburgerMessageBody)message.Data;
hamburgerMessageBody.Name.ShouldBe("Triple Smash");
}
The problem here is that we need to cast the message.Data
to HamburgerMessageBody before we can access the Name property. This is because the return type of MessageHandler is Message.
Let's see if we can do some changes that helps us avoiding the cast.
The solution
This will only work if you are using c# 9 or later, otherwise you will get the following compiler warning:
Feature 'covariant returns' is not available in C# 8.0. Please use language version 9.0 or greater.
First, we change our Message classes to the following:
public abstract class Message
{
protected Message(string id, string correlationId, object data)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
CorrelationId = correlationId ?? throw new ArgumentNullException(nameof(correlationId));
Data = data ?? throw new ArgumentNullException(nameof(data));
}
public string Id { get; }
public string CorrelationId { get; }
public virtual object Data { get; }
}
public class Message<T> : Message where T : class
{
public Message(string id, string correlationId, T data) : base(id, correlationId, data)
{
}
public override T Data => (T)base.Data;
}
The Data property is now virtual, which will allow us to override the return type in our generic Message class. Covariant returns works with both virtual and abstract properties.
In the generic Message class we override the type to be of type T
instead of object
. We also cast the Data
property (from the base class) to T
.
This is our first usage of covariant return types.
We will also update our HamburgerMessageHandler like this:
public class HamburgerMessageHandler : MessageHandler
{
public override Message<HamburgerMessageBody> Poll()
{
var message = new Message<HamburgerMessageBody>(
id: Guid.NewGuid().ToString(),
correlationId: Guid.NewGuid().ToString(),
new HamburgerMessageBody("Triple Smash"));
return message;
}
}
Notice that we changed the return type from Message to Message
We can now consume the message like this:
[Fact]
public void MessageDataShouldBeOfHamburgerMessageBodyType()
{
var message = _sut.Poll();
message.Data.ShouldBeOfType<HamburgerMessageBody>();
message.Data.Name.ShouldBe("Triple Smash");
}
Pretty sweat, right??