rebus-org / Rebus

:bus: Simple and lean service bus implementation for .NET
https://mookid.dk/category/rebus
Other
2.26k stars 355 forks source link

Create extensibility point for topics #692

Closed mookid8000 closed 6 years ago

mookid8000 commented 6 years ago

in the form of a ITopicNameConvention service, most likely looking like this:

public interface ITopicNameConvention
{
    string GetTopic(Type eventType);
}

This way, it can be used by Rebus when subscribing/unsubscribing to types:

public async Task Subscribe<TEvent>()
{
    var topic = _topicNameConvention.GetTopic(typeof(TEvent));

    await InnerSubscribe(topic);
}

and when publishing messages

public async Task Publish(object eventMessage)
{
    var topic = _topicNameConvention.GetTopic(eventMessage.GetType());

    await InnerPublish(topic, eventMessage);
}

Thoughts?

mookid8000 commented 6 years ago

PS: I started out with the idea of having the string GetTopic(object eventMessage) signature on the interface too, because I thought it would be neat to have to ability to maybe look at certain messages and come up with topics from their contents.

But then I realized that what we're abstracting away here is actually JUST the type-to-topic-name logic, and nothing else.

More advanced things can still easily be had by taking advantage of the raw topics API.

heberop commented 6 years ago

What about write a method in OptionsConfigurer class? Something like this:

.Options(options =>
{
    options.SetTopicNameConvention(new MyConvention());
})

Or

.Options(options =>
{
    options.SetTopicNameConvention(topic => topic.MyExtensionMethod());
})
mookid8000 commented 6 years ago

To begin with, I think it should simply live its life quiet, like many of the other Rebus extensibility points, most of which are configured in the Start method of RebusConfigurer (using the PossiblyRegisterDefault to make a registration unless someone already made one).

"The Rebus Way" then, would be, e.g. for you @heberop – as a developer wanting to use this extensibility point – to create an appropriate extension method on OptionsConfigurer in your own code, possibly looking like this:

public static class ShortAndReadableTopicsRebusConfigurationExtensions
{
    public static void UseShortTopicNames(this OptionsConfigurer configurer)
    {
        configurer.Register<ITopicNameConvention>(c => new ShortTopicNamesConvention());
    }

    class ShortTopicNamesConvention : ITopicNameConvention
    {
        public string GetTopic(Type type) => type.Name;
    }
}

which would then make your configuration code look neat like this:

Configure.With(...)
    .(...)
    .Options(o => {
        o.UseShortTopicNames();
    })
    .Start();

This way we can avoid trying to predict how it's going to be used.

What do you think about this?

heberop commented 6 years ago

I like it ;) I will work on it and send you a pull request ;)

mookid8000 commented 6 years ago

Excellent – I'm looking forward to it 👍

mookid8000 commented 6 years ago

Fixed by #693 which is out in Rebus 5.0.0-b09

dariogriffo commented 4 years ago

Working with AzureServiceBus, and using the ITopicnameConvention and for some reason my topic names have some "cleanup" after the GetTopic() method. They all end up in lowercase and '.' are replaced with '_' Am I missing something? I'm using the namespace and other combination of things to create topic names and I want them to be something like MyDomain.Events and I'm getting mydomain_evetns

mookid8000 commented 4 years ago

Which version of Rebus.AzureServiceBus are you using?

Some changes were made in the latest version that makes it possible to change names to anything you feel like. From version 7 (which is only out as a prerelease right now) the default should be to allow '.' and all other special characters, which Rebus.AzureServiceBus used to "sanitize" away.

dariogriffo commented 4 years ago

Latest stable one. I will give a try the prerelease version. Basically I want rebus to work with an existing established queue/topic convention that might be created for some messages, but might be required to be create by rebus in some cases. There is an existing in-house service bus implementation and I doing a POC to see how much I can make rebus compatible with it (hopefully 110% since there are features in rebus I want to use not available at the moment in-house)

dariogriffo commented 4 years ago

Ok, upgrading to 7.0.0-a16 created an immediate issue with Autofac and the registration (which is working on the latest stable version).

Also tried updating all the packages to the latest pre release versions, and the same error.

Rebus stable + autofac stable + azure prerelease = error Rebus prerelease + autofac prerelease + azure prerelease = error

Should I create a Tikcet on the Rebus.AzureServiceBus repository?

mookid8000 commented 4 years ago

Is it a compile error?

dariogriffo commented 4 years ago

Runnning, I uploaded the example to this repo RebusAzureNamingTest

mookid8000 commented 4 years ago

Could you show me the full stack trace or another description of the error?

dariogriffo commented 4 years ago
Unhandled Exception: Autofac.Core.DependencyResolutionException: An error occurred while attempting to automatically activate registration 'Activator = AutofacHandlerActivator (ProvidedInstanceActivator), Services = [Rebus.Autofac.AutofacHandlerActivator, AutoActivate], Lifetime = Autofac.Core.Lifetime.RootScopeLifetime, Sharing = Shared, Ownership = OwnedByLifetimeScope'. See the inner exception for information on the source of the failure. ---> An exception was thrown while executing a resolve operation. See the InnerException for details. ---> Could not start Rebus (See inner exception for details.) (See inner exception for details.) ---> Autofac.Core.DependencyResolutionException: An exception was thrown while executing a resolve operation. See the InnerException for details. ---> Could not start Rebus (See inner exception for details.) ---> Rebus.Exceptions.RebusConfigurationException: Could not start Rebus ---> Autofac.Core.DependencyResolutionException: An error occurred during the activation of a particular registration. See the inner exception for details. Registration: Activator = IBus (DelegateActivator), Services = [Rebus.Bus.IBus], Lifetime = Autofac.Core.Lifetime.RootScopeLifetime, Sharing = Shared, Ownership = OwnedByLifetimeScope ---> Attempted to register primary -> Rebus.Topic.ITopicNameConvention, but a primary registration already exists: primary -> Rebus.Topic.ITopicNameConvention (See inner exception for details.) ---> System.InvalidOperationException: Attempted to register primary -> Rebus.Topic.ITopicNameConvention, but a primary registration already exists: primary -> Rebus.Topic.ITopicNameConvention
   at Rebus.Injection.Injectionist.Register[TService](Func`2 resolverMethod, Boolean isDecorator, String description) in C:\projects-rebus\Rebus\Rebus\Injection\Injectionist.cs:line 121
   at Rebus.Injection.Injectionist.Register[TService](Func`2 resolverMethod, String description) in C:\projects-rebus\Rebus\Rebus\Injection\Injectionist.cs:line 74
   at RebusAzureNamingTest.RebusConfigurationExtensions.MyTopics(OptionsConfigurer configurer) in C:\Users\Dario\source\repos\RebusAzureNamingTest\RebusAzureNamingTest\RebusConfigurationExtensions.cs:line 12
   at RebusAzureNamingTest.Program.<>c.<Main>b__0_2(OptionsConfigurer x) in C:\Users\Dario\source\repos\RebusAzureNamingTest\RebusAzureNamingTest\Program.cs:line 25
   at Rebus.Config.RebusConfigurer.Options(Action`1 configurer) in C:\projects-rebus\Rebus\Rebus\Config\RebusConfigurer.cs:line 140
   at RebusAzureNamingTest.Program.<>c.<Main>b__0_0(RebusConfigurer configurer, IComponentContext context) in C:\Users\Dario\source\repos\RebusAzureNamingTest\RebusAzureNamingTest\Program.cs:line 21
   at Rebus.Config.ContainerBuilderExtensions.<>c__DisplayClass1_0.<RegisterRebus>b__0(RebusConfigurer configurer, IComponentContext context)
   at Rebus.Autofac.AutofacHandlerActivator.<>c__DisplayClass3_0.<.ctor>b__1(IComponentContext context)
   at Autofac.Builder.RegistrationBuilder.<>c__DisplayClass0_0`1.<ForDelegate>b__0(IComponentContext c, IEnumerable`1 p)
   at Autofac.Core.Activators.Delegate.DelegateActivator.ActivateInstance(IComponentContext context, IEnumerable`1 parameters)
   at Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters)
   --- End of inner exception stack trace ---
   at Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters)
   at Autofac.Core.Lifetime.LifetimeScope.GetOrCreateAndShare(Guid id, Func`1 creator)
   at Autofac.Core.Resolving.InstanceLookup.Execute()
   at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, IComponentRegistration registration, IEnumerable`1 parameters)
   at Autofac.ResolutionExtensions.TryResolveService(IComponentContext context, Service service, IEnumerable`1 parameters, Object& instance)
   at Autofac.ResolutionExtensions.ResolveService(IComponentContext context, Service service, IEnumerable`1 parameters)
   at Autofac.ResolutionExtensions.Resolve[TService](IComponentContext context, IEnumerable`1 parameters)
   at Rebus.Autofac.AutofacHandlerActivator.<>c.<.ctor>b__3_0(IActivatedEventArgs`1 e)
   --- End of inner exception stack trace ---
   at Rebus.Autofac.AutofacHandlerActivator.<>c.<.ctor>b__3_0(IActivatedEventArgs`1 e)
   at Autofac.Core.Resolving.InstanceLookup.Complete()
   at Autofac.Core.Resolving.ResolveOperation.CompleteActivations()
   at Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, IComponentRegistration registration, IEnumerable`1 parameters)
   at Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration registration, IEnumerable`1 parameters)
   --- End of inner exception stack trace ---
   at Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration registration, IEnumerable`1 parameters)
   at Autofac.ContainerBuilder.StartStartableComponents(IComponentContext componentContext)
   --- End of inner exception stack trace ---
   at Autofac.ContainerBuilder.StartStartableComponents(IComponentContext componentContext)
   at Autofac.ContainerBuilder.Build(ContainerBuildOptions options)
   at RebusAzureNamingTest.Program.Main(String[] args) in C:\Users\Dario\source\repos\RebusAzureNamingTest\RebusAzureNamingTest\Program.cs:line 30
dariogriffo commented 4 years ago

This is the bus configuration and start code

static void Main(string[] args)
{
    var containerBuilder = new ContainerBuilder();

    const string connString = "";

    containerBuilder.RegisterType<DepartmentUpdatedHandler>().AsImplementedInterfaces();
    containerBuilder.RegisterRebus((configurer, context) =>
    {
        return configurer
            .Transport(t => t.UseAzureServiceBus(connString, "RebusAzureNamingTest"))
            .Options(x => x.MyTopics())
            .Routing(c => c.MyRouting());
    });

    using (var container = containerBuilder.Build())
    using (var scope = container.BeginLifetimeScope())
    {
        scope.Resolve<IBus>().Subscribe<DepartmentUpdated>().GetAwaiter().GetResult();
        Thread.Sleep(TimeSpan.FromSeconds(10));
    }
}

This is the method to register the convention

public static void MyTopics(this OptionsConfigurer configurer)
{
    configurer.Register<ITopicNameConvention>(c => new MyTopicNamesConvention());
}
mookid8000 commented 4 years ago

Ok, the error reads

System.InvalidOperationException: Attempted to register 
primary -> Rebus.Topic.ITopicNameConvention, but a primary registration already exists:
primary -> Rebus.Topic.ITopicNameConvention

and the line

   at RebusAzureNamingTest.RebusConfigurationExtensions.MyTopics(OptionsConfigurer configurer) in C:\Users\Dario\source\repos\RebusAzureNamingTest\RebusAzureNamingTest\RebusConfigurationExtensions.cs:line 12

from the stacktrace reveals that MyTopics was where the registration was made.

I'm afraid this is because Rebus.AzureServiceBus makes its own registration of ITopicNameConvention.

Therefore you need to add your registration as a decorator, which means that you get to intercept all calls to it.

So, if you change your MyTopics implementation to

public static void MyTopics(this OptionsConfigurer configurer)
{
    configurer.Decorate<ITopicNameConvention>(c => new MyTopicNamesConvention());
}

then you should be good. 🙂

dariogriffo commented 4 years ago

Works perfect!