Question

UPDATE: Created a Simple Solution

As Eric said below, I was being a little generic happy and it still didn't really solve my problem. I've figured out a much better solution for removing the MassTransit dependency from my Core assemblies.

I've posted the answer in detail below.



Original Question

Is there a way to put a constraint on a generic argument that says it must be the type of the concrete implementation?

For now I'm using a unit test that does reflection to make sure all implementations are formed correctly, but I'd love to have it fail by the compiler.

Example:

Abstract base

// this abstract class extends another generic type that needs to know 
// the type of the concrete implementation "TConsumer"
public abstract class ConsumerBase<TMessage,TConsumer> : 
    Consumes<AssignedMessage<TMessage,TConsumer>>.All
    where TMessage : IMessage
    where TConsumer : {{The class that's implementing me}}
{

}

Should Succeed

// this class passes it's own type as a generic arg to it's base
// and it works great!
public class TestConsumer : ConsumerBase<TestMessage, TestConsumer>
{  }

Should Fail

// this class passes a different type as a generic arg to its base
// and should FAIL!!! but how?
public class FailConsumer : ConsumerBase<TestMessage, TestConsumer>
{  }

UPDATE: Further Explanation

To explain further WHY I'm trying to do this...

I'm trying to create an abstraction over MassTransit (easier said than done). Mass transit requires a consumer to implement Consume<TMessage>.All.

In my case, TMessage is an AssignedMessage<TRawMessage,TAssignee>.

Having the need to implement MassTransit's generic Consumes<TMessage>.All with an AssignedMessage<TRawMessage,TAssignee> means that I need to pass the generic TAssignee parameter along.

That being said, it still doesn't fully abstract out MassTransit. It gives me a base class under mass transit that sort of hides the fact that I'm implementing a MassTransit interface, but it's still a MassTransit dependency.

Was it helpful?

Solution 2

I managed to solve the MassTransit dependency problem in a fairly simple way without overly complex generics or runtime emits.

In the example below we are using the following Nuget packages: MassTransit, MassTransit.RabbitMq, MassTransit.Ninject


Example

Our Domain's Simple Consumer (Assembly: OurDomain.Core)

Our Core assembly has no reference or dependency on MassTransit. Furthermore, any other assemblies that want to implement a consumer will not need reference to MassTransit either.

public interface IConsume<T> where T : MessageBase
{
    int RetryAllotment { get; }
    void Consume(T message);
}

public class ExampleConsumer : IConsume<ExampleMessage>
{
    public int RetryAllotment { get { return 3; } }
    public void Consume(ExampleMessage message){...}
}

Internal MassTransitConsumer (Assembly: OurDomain.Messaging)

The Messaging assembly is the only assembly with reference to MassTransit. It also has reference to the Core assembly

The purpose of this class is to simply provide a wrapper over our domain's consumers that can be used to hook up a MassTransit subscription

internal class MassTransitConsumer<TMessage, TConsumer> : Consumes<TMessage>.Context
    where TMessage : MessageBase
    where TConsumer : IConsume<TMessage>
{
    private readonly TConsumer _consumer;

    public MassTransitConsumer(TConsumer consumer)
    {
        _consumer = consumer;
    }

    public void Consume(IConsumeContext<TMessage> ctx)
    {
        try
        {
            _consumer.Consume(ctx.Message);
        }
        catch
        {
            if (ctx.RetryCount >= _consumer.RetryAllotment)
                throw;

            ctx.RetryLater();
        }
    }
}

Configurator (Assembly: OurDomain.Messaging)

Pay special attention to the GetMassTransitConsumer method

public class MassTransitConfigurator
{
    private readonly string _rabbitMqConnection;
    private readonly Queue _queue;
    private readonly IKernel _kernel;
    private readonly IEnumerable<Type> _consumers;
    private readonly IEnumerable<Type> _massTransitConsumers;

    public enum Queue
    {
        Worker,
        WorkerProducer,
    }

    public MassTransitConfigurator(string rabbitMqConnection, Queue queue, IKernel kernel,
        IEnumerable<Type> consumers)
    {
        _rabbitMqConnection = rabbitMqConnection;
        _queue = queue;
        _kernel = kernel;
        _consumers = consumers;
        if (_queue == Queue.Worker)
            _massTransitConsumers = _consumers.Select(GetMassTransitConsumer);
    }

    private static Type GetMassTransitConsumer(Type domainConsumer)
    {
        var interfaceType = domainConsumer.GetInterface(typeof (IConsume<>).Name);
        if (interfaceType == null)
            throw new ArgumentException("All consumers must implement IConsume<>.", "domainConsumer");

        var messageType = interfaceType.GetGenericArguments().First();

        var massTransitConsumer = typeof (MassTransitConsumer<,>).MakeGenericType(messageType, domainConsumer);

        return massTransitConsumer;
    }

    public void Configure()
    {
        if (_queue == Queue.Worker)
        {
            foreach (var consumer in _consumers)
                _kernel.Bind(consumer).ToSelf();
            foreach (var massTransitConsumer in _massTransitConsumers)
                _kernel.Bind(massTransitConsumer).ToSelf();
        }

        var massTransitServiceBus = ServiceBusFactory.New(ConfigureMassTransit);
        var ourServiceBus = new MassTransitServiceBus(massTransitServiceBus);
        _kernel.Bind<OurDomain.IServiceBus>().ToConstant(ourServiceBus);
    }

    private void ConfigureMassTransit(ServiceBusConfigurator config)
    {
        config.UseRabbitMq();
        var queueConnection = _rabbitMqConnection + "/" + _queue;
        config.ReceiveFrom(queueConnection);

        if (_queue == Queue.Worker)
        {
            foreach (var massTransitConsumer in _massTransitConsumers)
            {
                var consumerType = massTransitConsumer;
                config.Subscribe(s => s.Consumer(consumerType, t => _kernel.Get(t)));
            }
        }
    }
}

Example Program for Subscribing (Assembly: OurDomain.Services.Worker)

The Worker Assembly has reference to the Core assembly and whatever assemblies our IConsume implementations happen to be defined in.

However, it DOES NOT have reference to MassTransit. Instead, it references our Messaging assembly which internally hooks up MassTransit.

internal class Program
{
    private static void Main(string[] args)
    {
        var kernel = new StandardKernel();
        var rabbitMqConnection = "rabbitmq://localhost";
        var consumers = new[] { typeof(ExampleConsumer) };

        var configurator = new MassTransitConfigurator(
                rabbitMqConnection,
                MassTransitConfigurator.Queue.Worker,
                kernel,
                consumers);

        configurator.Configure();
    }
}

OTHER TIPS

As Lee correctly points out, this concept does not exist in the C# type system. You can get close, but not quite get there. My article on the subject is here:

http://blogs.msdn.com/b/ericlippert/archive/2011/02/03/curiouser-and-curiouser.aspx

I would suggest to you that you are suffering from Genericity Happiness Disease, a condition which causes developers to try to capture all their business logic in a complex interplay of generic types and constraints, regardless of how confusing that is for users of the type.

The generic type system was designed to solve the problem "I have a list of apples and a list of dinosaurs and a list of prices; I'd like all of those lists to share the same implementation". Your type:

public abstract class ConsumerBase<TMessage,TConsumer> : 
    Consumes<AssignedMessage<TMessage,TConsumer>>

is an abuse of the generic type system. Suppose you asked someone "what's a consumer of messages and consumers?" Would anyone who did not suffer from genericity happiness disease say "a consumer of messages and consumers is a kind of consuming thing that consumes asssigned messages of messages and consumers"?

My advice is that you massively simplify your system. You're trying to capture way too much in the type system here.

I think the closest you can get is:

where TConsumer : ConsumerBase<TMessage, TConsumer>

recurring templates can get unwieldy quickly and it's still possible to be bypassed e.g.

public class LyingConsumer<TestMessage, TestConsumer> { ... }

another alternative is to change your design so that you can put the generic parameters outside the classes themselves e.g.

    public void SomeMethod<TConsumer, TMessage>(TConsumer consumer, TMessage message)
        where TConsumer : IConsumer<TMessage>
        where TMessage : IMessage
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top