Dynamically add masstransit bus instance on event - c#

I was trying to configure a microservice that would be able to listen a 'NewTenant' event
On creation of the tenant a new vhost in rabbitmq is activated and the event is published on the main servicebus.
But when trying to add a new bus instance for this vhost I found out there is a interface needed for the registration.
Anybody have any ideas on how MSDI and masstransit can be configured for this purpose?
I want to listen to new events on the new tenant bus for all consumers, but have it scoped for the tenant or have a way to check the queue for tentant information.
But I also want to be able to publish on the correct bus, so maybe instead of injecting the IBus, injecting a ITenantbus or something like that?
All ideas are welcome
foreach (var tenant in TenantService.GetTenants())
{
services.AddMassTransit<ITenantBus, TenantBus>(x =>
{
x.AddConsumer<MyConsumer>();
x.SetKebabCaseEndpointNameFormatter();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("rabbitmq://my-tenant-vhost", o =>
{
o.Username("tenantusername");
o.Password("tenantpassword");
});
cfg.ConfigureEndpoints(context);
});
});
}
A way to configure this but being able to set the tenant id on the tenant bus instance would be great.

Related

how Multiple consumer in masstransit consume publish one event

I have my controller publishing events to azure service bus via masstransit.
And multiple instanses of .net core service consuming those event.I want ALL instances to consume the same events.
await _publishEndpoint.Publish<MyPublishEvent>(
new MyPublishEvent
{
Id = 1,
Description = "test"
});
here is one consumer from one microservice.
public async Task Consume(ConsumeContext<MyPublishEvent> context)
{
try
{
// Here add business logic to insert record in to Database 1.
}
await Task.CompletedTask;
}
and the configuration is here.
builder.Services.AddMassTransit(cfg =>
{
cfg.SetKebabCaseEndpointNameFormatter();
cfg.AddConsumersFromNamespaceContaining<Consumers>();
cfg.UsingAzureServiceBus((context, cfg) =>
{
cfg.Host($"Endpoint = endpoint");
cfg.ConfigureEndpoints(context);
});
});
I want ALL instances to consume the same events.
The first one consume the event from one microservice and the rest of the service instances don't get it to consume it. can this be fixed. Am I missing any configuration?Appreciate your help in finding the cause.

Integration Events in Modular Monolith

I am trying to implement a multitenant (using a database per tenant) modular monolith that uses integration events to communicate between modules.
I am attempting to implement the integration events functionality and have the following scenario:
A new integration event is raised via an HTTP request
The integration event is published to the event bus
An integration event handler receives the message from the bus and modifies a domain object
The modification to the domain object causes a domain event to be raised
A domain event handler receives the domain event and performs some read operations on the database
This all works fine if I ignore the multitenant requirement, I am struggling to understand how to implement this for multiple tenants. I am using Finbuckle in my API and constructor injecting ITenantInfo into my DbContext to retrieve the correct connection string for the current tenant.
When I try to move the scenario to multitenant:
Step 1 is easy enough – I can simply pass a tenant identifier in the integration event.
Step 2 is unaffected – the message is published to the event bus.
Step 3 is tricky because it is asynchronous. If it was within the scope of the HTTP request, I would be able to inject the domain repository into the constructor of my integration event handler and retrieve the domain object from the database. However, because it is triggered from the event bus, the tenant identifier must be extracted from the message and manually resolved. Once I have resolved the tenant, I can manually create an instance of the domain repository and get the domain object from the database.
Step 4 is not affected; the domain object is updated as usual.
Step 5 is where I am struggling. I am constructor injecting the DbContext into the domain event handler which, before I introduced integration events, worked because ITenantInfo was resolved by the Finbuckle middleware and DI. The problem I have now is that I do not know how to resolve the tenant when the domain event is triggered via an integration event. My domain event does not contain a tenant identifier and there is no HttpContext therefore I cannot resolve ITenantInfo to provide the database connect string.
To clarify, the final step of my scenario requires the domain event handler to read data from the database via a DbContext. This is possible within the scope of an HTTP request because Finbuckle will resolve the ITenantInfo to provide the connection string. When outside the scope of an HTTP request, I do not know how to resolve the tenant because my domain event does not contain any tenant specific information.
How can I identify the tenant in a domain event handler when the domain event is raised as a side effect of an integration event?
For anybody that sees this in the future I was overthinking the problem.
I was able to resolve ITenantInfo using Scoped Filters in MassTransit.
For example:
public class MyTenantInfoConsumeFilter<T>
: IFilter<ConsumeContext<T>>
where T : class
{
readonly MyTenantInfo _tenantInfo;
private readonly MultiTenantStoreDbContext _dbContext;
public MyTenantInfoConsumeFilter(MyTenantInfo tenantInfo, MultiTenantStoreDbContext dbContext)
{
_tenantInfo = tenantInfo;
_dbContext = dbContext;
}
public Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
if (context.Message is IntegrationEventBase message)
{
Map(_dbContext.TenantInfo.FirstOrDefault(tenant => tenant.Identifier == message.TenantIdentifier));
}
return next.Send(context);
}
public void Probe(ProbeContext context)
{
}
private void Map(MyTenantInfo? tenantInfo)
{
if (tenantInfo is null) return;
_tenantInfo.Id = tenantInfo.Id;
_tenantInfo.Identifier = tenantInfo.Identifier;
_tenantInfo.Name = tenantInfo.Name;
_tenantInfo.ConnectionString = tenantInfo.ConnectionString;
}
}
Then register the services using something like:
services.AddScoped<Finbuckle.MultiTenant.ITenantInfo>(sp => sp.GetService<MyTenantInfo>()!);
services.AddMassTransit(x =>
{
x.AddScoped<MyTenantInfo>();
x.UsingAzureServiceBus((context, cfg) =>
{
cfg.Host(hostContext.Configuration.GetConnectionString("AzureServiceBusConnection"));
cfg.ConfigureEndpoints(context);
cfg.UseConsumeFilter(typeof(MyTenantInfoConsumeFilter<>), context);
});
});

MassTransit not subscribing to AzureServiceBus Topic

I'm currently trying to update application that was originally .NET Core 3.1 using MassTransit 6.3.2. It is now configured to use .NET 6.0 and MassTransit 7.3.0
Our application uses MassTransit to send messages via Azure Service Bus, publishing messages to Topics, which then have other Subscribers listening to those Topic.
Cut down, it was implemented like so:
// Program.cs
services.AddMassTransit(config =>
{
config.AddConsumer<AppointmentBookedMessageConsumer>();
config.AddBus(BusControlFactory.ConfigureAzureServiceBus);
});
// BusControlFactory.cs
public static class BusControlFactory
{
public static IBusControl ConfigureAzureServiceBus(IRegistrationContext<IServiceProvider> context)
{
var config = context.Container.GetService<AppConfiguration>();
var azureServiceBus = Bus.Factory.CreateUsingAzureServiceBus(busFactoryConfig =>
{
busFactoryConfig.Host("Endpoint=sb://REDACTED-queues.servicebus.windows.net/;SharedAccessKeyName=MyMessageQueuing;SharedAccessKey=MyKeyGoesHere");
busFactoryConfig.Message<AppointmentBookedMessage>(m => m.SetEntityName("appointment-booked"));
busFactoryConfig.SubscriptionEndpoint<AppointmentBookedMessage>(
"my-subscriber-name",
configurator =>
{
configurator.UseMessageRetry(r => r.Interval(5, TimeSpan.FromSeconds(60)));
configurator.Consumer<AppointmentBookedMessageConsumer>(context.Container);
});
return azureServiceBus;
}
}
}
It has now been changed and upgraded to the latest MassTransit and is implemented like:
// Program.cs
services.AddMassTransit(config =>
{
config.AddConsumer<AppointmentBookedMessageConsumer, AppointmentBookedMessageConsumerDefinition>();
config.UsingAzureServiceBus((context, cfg) =>
{
cfg.Host("Endpoint=sb://REDACTED-queues.servicebus.windows.net/;SharedAccessKeyName=MyMessageQueuing;SharedAccessKey=MyKeyGoesHere");
cfg.Message<AppointmentBookedMessage>(m => m.SetEntityName("appointment-booked"));
cfg.ConfigureEndpoints(context);
});
// AppointmentBookedMessageConsumerDefinition.cs
public class AppointmentBookedMessageConsumerDefinition: ConsumerDefinition<AppointmentBookedMessageConsumer>
{
public AppointmentBookedMessageConsumerDefinition()
{
EndpointName = "testharness.subscriber";
}
protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator<AppointmentBookedMessageConsumer> consumerConfigurator)
{
endpointConfigurator.UseMessageRetry(r => r.Interval(5, TimeSpan.FromSeconds(60)));
}
}
The issue if it can be considered one, is that I can't bind to a subscription that already exists.
In the example above, you can see that the EndpointName is set as "testharness.subscriber". There was already a subscription to the Topic "appointment-booked" from prior to me upgrading. However, when the application runs, it does not error, but it receives no messages.
If I change the EndpointName to "testharness.subscriber2". Another subscriber appears in the Azure Service Bus topic (via the Azure Portal) and I start receiving messages. I can see no difference in the names (other than the change that I placed, in this case: the "2" suffix).
Am I missing something here? Is there something else I need to do to get these to bind? Is my configuration wrong? Was it wrong? While I'm sure I can get around this by managing the release more closely and removing unneeded queues once they're using new ones - it feels like the wrong approach.
With Azure Service Bus, ForwardTo on a subscription can be a bit opaque.
While the subscription may indeed visually indicate that it is forwarding to the correctly named queue, it might be that the queue was deleted and recreated at some point without deleting the subscription. This results in a subscription that will build up messages, as it is unable to forward them to a queue that no longer exists.
Why? Internally, a subscription maintains the ForwardTo as an object id, which after the queue is deleted points to an object that doesn't exist – resulting in messages building up in the subscription.
If you have messages in the subscription, you may need to go into the portal and update that subscription to point to the new queue (even though it has the same name), at which point the messages should flow through to the queue.
If there aren't any messages in the subscription (or if they aren't important), you can just delete the subscription and it will be recreated by MassTransit when you restart the bus.

asp.net core 3.1 service lifetime and Observable sobscriptions in Signalrhub

I think I`m bit lost:
I have a signalr hub which gets a "scoped" service ("subscription-service") injected via DI.
This "subscription-service" service gets a hubxontext and additional "scoped" domain-specific services injected, and tries to subscribe to the different domain-specific observables, that the services provides.
In the individual callbacks from the observables, I want to call methods on the hubcontext, to be able to notify the client when CRUD operations has been done.
My proble is that the callbacks are only fired IF the injected domain-services are of "singletons" lifetime.
In my Startup.cs I have configured all my services as "scoped":
services.AddScoped<IMaterialTypeService, MaterialTypeService>();
services.AddScoped<IMaterialTypeRepository, MaterialTypeDbRepository>();
services.AddScoped<WebApiSubscriptionService>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<INotificationService, NotificationService>();
The method for wiring up the subscriptions (in the "subscription-service") is as following:
private void Startup()
{
_notificationService.Notifications.Subscribe(notification =>
{
_hubContext.Clients.All.Notify(notification);
});
_materialTypeService.MaterialTypesChanged.Subscribe(materialTypes =>
{
var matTypes = _mapper.Map<IList<MaterialTypeResponseDto>>(materialTypes);
_hubContext.Clients.All.MaterialTypesChanged(matTypes);
});
}
What am I missing here?
Why wouldnt the callback inside the subscription being called?
All services ar scoped and triggered by t he same web request.
My design is that I want to have n number of domain services, and when something happens (CRUD) inside any of the domain services, I want them to push an Observable message, som that the subscription-service mentioned above, can subscribe to this Observable, and in the callback, push the message via signalr.

ASP.NET Core service not creating RabbitMQ queue

Being a new user of MassTransit and RabbitMQ I'm currently trying to make my ASP.NET core service to work with MassTransit.
Taking this documentation section to configure MassTransit and ASP.NET Core I'm unable to get it working.
Currently (part of) the Startup.cs looks like
services.AddMassTransit(x =>
{
x.AddConsumer<MailConsumer>();
x.AddConsumer<MailFailedConsumer>();
x.AddBus(provider => ConfigureBus(provider, rabbitMqConfigurations));
});
private IBusControl ConfigureBus(
IServiceProvider provider,
RabbitMqConfigSection rabbitMqConfigurations) => Bus.Factory.CreateUsingRabbitMq(
cfg =>
{
var host = cfg.Host(
rabbitMqConfigurations.Host,
"/",
hst =>
{
hst.Username(rabbitMqConfigurations.Username);
hst.Password(rabbitMqConfigurations.Password);
});
cfg.ReceiveEndpoint(host, $"{typeof(MailSent).Namespace}.{typeof(MailSent).Name}", endpoint =>
{
endpoint.Consumer<MailConsumer>(provider);
});
cfg.ReceiveEndpoint(host, $"{typeof(MailSentFailed).Namespace}.{typeof(MailSentFailed).Name}", endpoint =>
{
endpoint.Consumer<MailFailedConsumer>(provider);
});
});
The exchange is created automatically in RabbitMQ on startup, but no queue is bind to the exchange which I would expect.
After invoking my API endpoint I can see activity on the exchange, but of course the consumers doing nothing as there is no queue.
What (obvious) part am I missing?
Ok, I found the issue. It worked as described in the docs at the moment the docs were written. There are several AddMassTransit extensions for the IServiceCollection interface, which is confusing.
AddMassTransit overload, which accepts the bus instance works as described.
AddMassTransit overload, which accepts the Action<IServiceCollectionConfigurator> only does necessary registrations.
You need to add one line:
services.AddMassTransitHostedService();
and your code will work.

Categories