Multiple vhost connection inside same project (Subscribe / Publish) - c#

Our requirement is like below :-
Exchange 1 is a topic exchange and queue 1 is bound to it. It is on VHOST 1.
Application is subscribed to the queue 1. It processes the message of queue 1. After processing of queue 1 message, we want to publish next message to different exchange which is on VHOST 2 (different rabbit connection)
I have below questions :-
a) Is it possible to implement this without federation ?a
b) In a same application, Can I maintain 2 different rabbit connections ?
We are using using EasynetQ as a client to connect with rabbitmq.
Can you please share some sample on this.
Thanks in advance.

a) Yes, you can also create a shovel between vhosts, which is simpler than a federation
b) Yes I don't see a problem with creating multiple IBus instances, as long as you use different DI (sub) containers per bus instance, so there's an added complexity.

Here is how I handle multiple connections. I can't find a solution directly from EasyNetQ. I don't use the default DI adaptors for MS DI. And I only use the advance api and inject the service I need manually. So far it seems working but it needs more test for sure.
in startup.cs / ConfigureServices
services.AddBusStation(busStationBuilder =>
{
// inject IBusStation and get the bus thru name
appSettings.RabbitMQSettings.Connections.ForEach(c =>
{
var taskQueueBus = RabbitHutch.CreateBus(c.ConnectionString, CustomServiceRegister.ServiceRegisterAction());
c.Exchanges.ForEach(async e =>
{
await taskQueueBus.Advanced.ExchangeDeclareAsync(e.Name, e.Type, e.Durable, e.AutoDelete);
});
busStationBuilder.Add(c.Name, taskQueueBus.Advanced);
busStationBuilder.AddDefaultBus(taskQueueBus);
});
});
public interface IBusStation
{
IBus DefualtBus { get; }
IAdvancedBus Get(string busName);
void Add(string busName, IAdvancedBus advancedBus);
void Add(IBus bus);
}
public class BusStation : IBusStation
{
private Dictionary<string, IAdvancedBus> BusList { get; set; } = new Dictionary<string, IAdvancedBus>();
public IBus DefualtBus { get; private set; }
public IAdvancedBus Get(string busName)
{
if (BusList.TryGetValue(busName, out IAdvancedBus advancedBus))
{
return advancedBus;
}
return null;
}
public void Add(string busName, IAdvancedBus advancedBus)
{
BusList.Add(busName, advancedBus);
}
public void Add(IBus bus)
{
this.DefualtBus = bus;
}
}
public class BusStationBuilder
{
private readonly IBusStation _BusStation;
public BusStationBuilder(IServiceCollection services, IBusStation busStation)
{
this._BusStation = busStation;
services.AddSingleton(busStation);
}
public BusStationBuilder Add(string busName, IAdvancedBus advancedBus)
{
_BusStation.Add(busName, advancedBus);
return this;
}
public BusStationBuilder AddDefaultBus(IBus bus)
{
_BusStation.Add(bus);
return this;
}
}
public static class DependencyExtensions
{
public static IServiceCollection AddBusStation(this IServiceCollection services, Action<BusStationBuilder> builder)
{
var busStationBuilder = new BusStationBuilder(services, new BusStation());
builder(busStationBuilder);
return services;
}
}
appsettings.json
"RabbitMQSettings": {
"DefaultQueue": "task.main",
"Connections": [
{
"Name": "Task_Queue",
"ConnectionString": "host=192.168.123.123;virtualHost=/;username=admin;password=password123;prefetchCount=1;persistentMessages=true;publisherConfirms=true",
"Exchanges": [
{
"Name": "Direct_Task_Queue",
"Type": "direct",
"Passive": false,
"Durable": true,
"AutoDelete": false,
"Internal": false,
"AlternateExchange": null,
"Delayed": false
}
]
}
]
},

Related

WebApplicationFactory is running the Api with the old IConnectionMultiplexer which fails to connect instead of first overriding configuration

I'm working on an integration test for a Web API which communicates through Redis, so I tried to replace the Redis Server with a containerized one and run some tests.
The issue is that it is first running the Api with project's appsettings.Development.json configuration and the old IConnectionMultiplexer instance which obviously won't connect because the hostname is offline. The question is how do I make it run the project with the new IConnectionMultiplexer that uses the containerized Redis Server? Basically the sequence is wrong there. What I did is more like run the old IConnectionMultiplexer and replace it with the new one but it wouldn't connect to the old one, so that exception prevents me from continuing. I commented the line of code where it throws the exception but as I said it's obvious because it's first running the Api with the old configuration instead of first overriding the configuration and then running the Api.
I could have done something like the following but I'm DI'ing other services based on configuration as well, meaning I must override the configuration first and then run the actual API code.
try
{
var redis = ConnectionMultiplexer.Connect(redisConfig.Host);
serviceCollection.AddSingleton<IConnectionMultiplexer>(redis);
}
catch
{
// We discard that service if it's unable to connect
}
Api
public static class RedisConnectionConfiguration
{
public static void AddRedisConnection(this IServiceCollection serviceCollection, IConfiguration config)
{
var redisConfig = config.GetSection("Redis").Get<RedisConfiguration>();
serviceCollection.AddHostedService<RedisSubscription>();
serviceCollection.AddSingleton(redisConfig);
var redis = ConnectionMultiplexer.Connect(redisConfig.Host); // This fails because it didn't override Redis:Host
serviceCollection.AddSingleton<IConnectionMultiplexer>(redis);
}
}
Integration tests
public class OrderManagerApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
private const string Password = "Test1234!";
private readonly TestcontainersContainer _redisContainer;
private readonly int _externalPort = Random.Shared.Next(10_000, 60_000);
public OrderManagerApiFactory()
{
_redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("redis:alpine")
.WithEnvironment("REDIS_PASSWORD", Password)
.WithPortBinding(_externalPort, 6379)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
.Build();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Development");
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
});
builder.ConfigureAppConfiguration(config =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
{ "Redis:Host", $"localhost:{_externalPort},password={Password},allowAdmin=true" },
{ "Redis:Channels:Main", "main:new:order" },
});
});
builder.ConfigureTestServices(services =>
{
services.RemoveAll(typeof(IConnectionMultiplexer));
services.AddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect($"localhost:{_externalPort},password={Password},allowAdmin=true"));
});
}
public async Task InitializeAsync()
{
await _redisContainer.StartAsync();
}
public new async Task DisposeAsync()
{
await _redisContainer.DisposeAsync();
}
}
public class OrderManagerTests : IClassFixture<OrderManagerApiFactory>, IAsyncLifetime
{
private readonly OrderManagerApiFactory _apiFactory;
public OrderManagerTests(OrderManagerApiFactory apiFactory)
{
_apiFactory = apiFactory;
}
[Fact]
public async Task Test()
{
// Arrange
var configuration = _apiFactory.Services.GetRequiredService<IConfiguration>();
var redis = _apiFactory.Services.GetRequiredService<IConnectionMultiplexer>();
var channel = configuration.GetValue<string>("Redis:Channels:Main");
// Act
await redis.GetSubscriber().PublishAsync(channel, "ping");
// Assert
}
public Task InitializeAsync()
{
return Task.CompletedTask;
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
}
Problem solved.
If you override WebApplicationFactory<T>.CreateHost() and call IHostBuilder.ConfigureHostConfiguration() before calling base.CreateHost() the configuration you add will be visible between WebApplication.CreateBuilder() and builder.Build().
The following two links might help someone:
https://github.com/dotnet/aspnetcore/issues/37680
https://github.com/dotnet/aspnetcore/issues/9275
public sealed class OrderManagerApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
private const string Password = "Test1234!";
private const int ExternalPort = 7777; // Random.Shared.Next(10_000, 60_000);
private readonly TestcontainersContainer _redisContainer;
public OrderManagerApiFactory()
{
_redisContainer = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage("redis:alpine")
.WithEnvironment("REDIS_PASSWORD", Password)
.WithPortBinding(ExternalPort, 6379)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379))
.Build();
}
public async Task InitializeAsync()
{
await _redisContainer.StartAsync();
}
public new async Task DisposeAsync()
{
await _redisContainer.DisposeAsync();
}
protected override IHost CreateHost(IHostBuilder builder)
{
builder.ConfigureHostConfiguration(config =>
config.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Redis:Host", $"localhost:{ExternalPort},password={Password},allowAdmin=true"),
new KeyValuePair<string, string>("Redis:Channels:Main", "main:new:order")
}));
return base.CreateHost(builder);
}
}

ASP.NET Core Web App Dependency Injection with Multiple Implementations

How do you register services in an enumerable way? For example I'm working on a solution similar to OpenLM. I would like to have a configuration file that defines the inputs. For example:
"Collectors": {
"Frequency": "2",
"FlexLM": {
"ExePath": "Resources\\lmutil.exe",
"Servers": [
{
"Server": "port#server",
"Product": "product"
}
],
},
"SentinelRMS": {
"ExePath": "Resources\\lsmon.exe",
"Servers": [
{
"Server": "server",
"Product": "product 2"
}
]
}
This requires two services, one knows how to communicate with FlexLM and the other with Sentinel RMS. I've implemented it a non-IoC way like this
internal static IEnumerable<IDataCollector> Build()
{
var time = DateTime.Now;
var collectors = new List<IDataCollector>();
AddSection("Collectors:FlexLM", collectors);
AddSection("Collectors:SentinelRMS", collectors);
return collectors;
}
private static void AddSection(string section, List<IDataCollector> collectors)
{
if (ConfigurationAdapter.Sections.Contains(section))
{
var exePath = ConfigurationAdapter.Config.GetSection($"{section}:ExePath").Get<string>();
var children = ConfigurationAdapter.Config.GetSection(section + ":Servers").Get<ServerConfig[]>();
if (children != null)
{
foreach (var item in children)
{
if (section == "Collectors:FlexLM")
{
collectors.Add(new FlexLmDataCollector(item.Server, item.Product, exePath));
}
else if (section == "Collectors:SentinelRMS")
{
collectors.Add(new SentinelRmsDataCollector(item.Server, item.Product, exePath));
}
}
}
}
}
Later in the process these services are consumed in parallel like this
Parallel.ForEach(collectors, new ParallelOptions { MaxDegreeOfParallelism = 99 }, (collector) =>
{
collector.ReadLicenses();
});
Creating a service for communicating with
I created one service for each type of license server. Each service is responsible for multiple servers, which are passed in a IOptions. First I configured the services.
builder.Services.Configure<SentinelRmsServiceOptions>(
builder.Configuration.GetSection(SentinelRmsServiceOptions.Section));
builder.Services.Configure<FlexLmServiceOptions>(
builder.Configuration.GetSection(FlexLmServiceOptions.Section));
builder.Services.Configure<DataCollectorsOptions>(
builder.Configuration.GetSection(DataCollectorsOptions.Section));
Register multiple services
builder.Services.AddSingleton<IDataCollectorService, SentinelRmsDataService>();
builder.Services.AddSingleton<IDataCollectorService, FlexLmDataService>();
The constructor for the service that calls the server specific services looks like this
public DataCollectorsService(
ILogger<DataCollectorsService> logger,
IEnumerable<IDataCollectorService> services,
IOptions<DataCollectorsOptions> options)
The options class matches the appsettings.json
public class FlexLmServiceOptions
{
public static string Section => "Collectors:FlexLM";
public string ExePath { get; set; }
public IEnumerable<ServerInfo> Servers { get; set; }
}
The service communicates with the servers using the original classes
Parallel.ForEach(options.Servers, item =>
{
var collector = new FlexLmDataCollector(item.Server, item.Product, options.ExePath, enrichers);
collector.ReadLicenses();
});

Azure WebJobs access configuration from within a static function

I'm creating a webjob in .net core 3.1. In this project I have a function that is timer activated which should read the number of messages in a queue Q1 and if empty, put a message in Q2 as well as trigger a rest call to an API.
In order to check how many messages are in the API I need to access the AzureWebJobsStorage in my appsettings.json and then the url which is also in the settings.
Program.cs
class Program
{
static async Task Main()
{
var builder = new HostBuilder();
builder.ConfigureWebJobs(b =>
{
b.AddAzureStorageCoreServices();
b.AddAzureStorage();
b.AddTimers();
});
builder.ConfigureLogging((context, b) =>
{
b.AddConsole();
});
builder.ConfigureAppConfiguration((context, b) =>
{
b.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
});
builder.ConfigureServices((context, services) =>
{
var mySettings = new MySettings
{
AzureWebJobsStorage = context.Configuration.GetValue<string>("AzureWebJobsStorage"),
AzureWebJobsDashboard = context.Configuration.GetValue<string>("AzureWebJobsDashboard"),
url = context.Configuration.GetValue<string>("url"),
};
services.AddSingleton(mySettings);
});
var host = builder.Build();
using (host)
{
await host.RunAsync();
}
}
}
Fuctions.cs
public class Functions
{
public static void UpdateChannels([QueueTrigger("Q1")] string message, ILogger logger)
{
logger.LogInformation(message);
}
public static void WhatIsThereToUpdate([QueueTrigger("Q2")] string message, ILogger logger)
{
logger.LogInformation(message);
}
public static void CronJob([TimerTrigger("0 * * * * *")] TimerInfo timer, [Queue("Q2")] out string message, ILogger logger, MySettings mySettings)
{
message = null;
// Get the connection string from app settings
string connectionString = mySettings.AzureWebJobsStorage;
logger.LogInformation("Connection String: " + connectionString);
// Instantiate a QueueClient which will be used to create and manipulate the queue
QueueClient queueClient = new QueueClient(connectionString, "Q1");
if (queueClient.Exists())
{
QueueProperties properties = queueClient.GetProperties();
// Retrieve the cached approximate message count.
int cachedMessagesCount = properties.ApproximateMessagesCount;
// Display number of messages.
logger.LogInformation($"Number of messages in queue: {cachedMessagesCount}");
if (cachedMessagesCount == 0)
message = "Hello world!" + System.DateTime.Now.ToString(); //here I would call the REST API as well
}
logger.LogInformation("Cron job fired!");
}
}
appsettings.json
{
"AzureWebJobsStorage": "constr",
"AzureWebJobsDashboard": "constr",
"url": "url"
}
My Settings
public class MySettings
{
public string AzureWebJobsStorage { get; set; }
public string AzureWebJobsDashboard { get; set; }
public string url { get; set; }
}
However when I run this I get the following error:
Error indexing method 'Functions.CronJob'
Microsoft.Azure.WebJobs.Host.Indexers.FunctionIndexingException: Error indexing method 'Functions.CronJob'
---> System.InvalidOperationException: Cannot bind parameter 'mySettings' to type MySettings. Make sure the parameter Type is supported by the binding. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
In addition to what is shown in the above codes I also tried using ConfigurationManager and Environment.GetEnvironmentVariable, both methods gave me null when I tried to read the values. For example ConfigurationManager.AppSettings.GetValues("AzureWebJobsStorage").
I also tried to register IConfiguration as a service services.AddSingleton(context.Configuration); and inject it in the parameters (instead of MySettings), but it also gave me the same binding error.
I'm really at a loss here, I've scoured the SO archives trying to find a solution and I think I tried everything I saw gave people positive results, but unfortunately I wasn't as lucky as the other posters.
Any guidance is much appreciated.
Edited to add my packages
In case it helps anyone, I'm using the following
Azure.Storage.Queues (12.4.0)
Microsoft.Azure.WebJobs.Extensions (3.0.6)
Microsoft.Azure.WebJobs.Extensions.Storage (4.0.2)
Microsoft.Extensions.Logging.Console (3.1.7)
When using DI, I suggest you use non-static method and constructor inject.
Here is the Functions.cs:
public class Functions
{
private readonly MySettings mySettings;
public Functions(MySettings _mySettings)
{
mySettings = _mySettings;
}
public void ProcessQueueMessage([TimerTrigger("0 */1 * * * *")] TimerInfo timer, [Queue("queue")] out string message, ILogger logger)
{
message = null;
string connectionString = mySettings.AzureWebJobsStorage;
logger.LogInformation("Connection String: " + connectionString);
}
}
No code change in other .cs file.
Here is the test result:

Register multiple singletons with same interface but different constructor parameters values

I got stuck and need some advice or pointer to a solution.
A web API with ASP.NET Core 3.1
Startup.cs
services.AddSingleton<ITopicClient>(s => new TopicClient({connectionstring},{topic}));
TopicRepository.cs
public class TopicRepository : ITopicRepository
{
private readonly ITopicClient _topicClient1;
private readonly ITopicClient _topicClient2;
public TopicRepository (ITopicClient topicClient1, ITopicClient topicClient2)
{
_topicClient1 = topicClient1;
_topicClient2 = topicClient2;
}
public async Task<Response> SendToTopicAsync(string message, string topic)
{
if( topic == "topic1")
await _topicClient1.send(message);
else if (topic == "topic2")
await _topicClient2.send(message);
}
}
TopicClient.cs in a shared library
public TopicClient(string serviceBusConnectionString, string topicName)
{
_topicClient = new TopicClient(_serviceBusConnectionString,topicName);
}
I need to send message to different topics. I would like to register services with different topic names in startup.cs. I want to reuse topicClient connection.
services.AddSingleton(s => new TopicClient({connectionstring},{topic1}));
services.AddSingleton(s => new TopicClient({connectionstring},{topic2}));
How can I achieve this by registering singleton instances of same type using same interface ?
Thank you in advance!
You could use a client resolver that holds your registered clients with a wrapper around the client.
First create a wrapper around your client with a name or enum for how to resolve it. As I'm not a fan of magic strings I decided to go with an enum in the example.
// Wrapper for your TopicClients
public interface ICustomTopicClient
{
public ITopicClient TopicClient { get; }
public TopicName TopicName { get; }
}
// Implement the ICustomTopicClient interface
public class CustomTopicClient : ICustomTopicClient
{
public ITopicClient TopicClient { get; }
public TopicName TopicName { get; }
public CustomTopicClient(ITopicClient topicClient, TopicName topicName)
{
TopicClient = topicClient;
TopicName = topicName;
}
}
// Enum for how to resolve the requested TopicClient
public enum TopicName
{
Topic1 = 0,
Topic2 = 1
}
// Register all ICustomTopicClients in your container
services.AddSingleton<ICustomTopicClient>(s => new CustomTopicClient(new TopicClient({connectionstring},{topic}), TopicName.Topic1));
services.AddSingleton<ICustomTopicClient>(s => new CustomTopicClient(new TopicClient({connectionstring},{topic2}), TopicName.Topic2));
Then you create a resolver that holds all custom clients.
You inject the collection of clients from the container and create a dictionary with a public method to resolve the clients.
public interface IMessageClientResolver
{
ITopicClient ResolveClient(TopicName name);
}
public class MessageClientResolver : IMessageClientResolver
{
private readonly Dictionary<TopicName, ITopicClient> topicClients;
public MessageClientResolver(IEnumerable<ICustomTopicClient> clients)
{
topicClients = clients.ToDictionary(k => k.TopicName, v => v.TopicClient);
}
public ITopicClient ResolveClient(TopicName name)
{
topicClients.TryGetValue(name, out var client);
if (client is null)
throw new ArgumentException(nameof(client));
return client;
}
}
Register the resolver to the container.
services.AddSingleton<IMessageClientResolver, MessageClientResolver>();
And then use it like this:
public class Foo
{
private readonly ITopicClient topicClient;
private readonly ITopicClient topicClient2;
public Foo(IMessageClientResolver clientResolver)
{
topicClient = clientResolver.ResolveClient(TopicName.Topic1);
topicClient2 = clientResolver.ResolveClient(TopicName.Topic2);
}
}
You can use the same pattern and extend the resolver with IQueueClients. And add a resolve method to return the IQueueClient by a QueueName enum.
You can already register multiple instances as the same interface, so when you do:
services.AddSingleton<ITopicClient>(_ => new TopicClient("topic1"));
services.AddSingleton<ITopicClient>(_ => new TopicClient("topic2"));
you already added two instances to the container.
It is just when you resolve interface ITopicClient, you always get the last added instance. For example, if you resolve:
// instance = topic2
var instance = container.GetService<ITopicClient>();
If you need all instances, you should resolve / inject IEnumerable<ITopicClient>.
class TopicRepository
{
public TopicRepository(IEnumerable<ITopicClient> clients)
{
// clients contains topic1 and topic2
}
}

NServiceBus.IStartableBus is not registered in the container. NServiceBus 4.6.3

I am attempting a very simple application with NServiceBus and Ninject.
I am attempting to use Ninject as the container for NServiceBus, but I am getting the following error - "NServiceBus.IStartableBus is not registered in the container."
I'm sure the answer is quite obvious... just not to me!
My code is as follows
public class StartApp : IWantCustomInitialization, IWantToRunWhenBusStartsAndStops
{
private static IKernel _kernel;
public IBus Bus { get; set; }
public void Init()
{
Configure.Serialization.Json();
}
public void Start()
{
_kernel = new StandardKernel();
Configure.With()
.NinjectBuilder(_kernel)
.CreateBus()
.Start();
Bus.Send(new TestMessage {Id = Guid.NewGuid(), MessageText = "Bloop"});
}
public void Stop()
{
}
}
namespace NServiceBus_Ninject_Simple
{
public class EndpointConfig : IConfigureThisEndpoint, AsA_Server
{ }
}
This google groups discussion is about the same issue.
It seems you are creating the configuration in the wrong place.
It should look like this:
public abstract class DefaultEndpointConfig
: IConfigureThisEndpoint
, IWantCustomInitialization
{
IWantCustomInitialization.Init()
{
Configure
.With()
.NinjectBuilder();
// + any other config;
// Call optional endpoint specific config
Init();
}
public virtual void Init()
{
}
}
See here (Johannes Gustafsson)
It needs to be done in the EndPoint-Configuration (for every endpoint, this is why he suggests using a base class) and it needs to implement IConfigureThisEndpoint.

Categories