I have the following Program.cs
public class Program
{
public async static Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ProductDbContext>();
if (context.Database.IsSqlServer())
{
context.Database.Migrate();
}
await ProductDbContextSeed.SeedSampleDataAsync(context);
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while migrating or seeding the database.");
throw;
}
}
await host.RunAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webBuilder =>
webBuilder.UseStartup<Startup>());
}
By adding the following line: (as I already have in the code above):
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
My code runs how I want.
However if I remove .UseServiceProviderFactory(new AutofacServiceProviderFactory())
And simply have:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
webBuilder.UseStartup<Startup>());
I get the following error:
Cannot resolve 'ProductAPI.IntegrationEvents.EventHandling.OrderCreatedIntegrationEventHandler'
from root provider because it requires scoped service
'RetailInMotion.Services.Inventory.ProductAPI.Infrastructure.Persistence.ProductDbContext'.
Here is how I have configured the ProductDbContext:
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<ProductDbContext>(options =>
options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ProductDbContext).Assembly.FullName)));
services.AddDbContext<EventLogContext>(options =>
{
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(typeof(ProductDbContext).Assembly.FullName);
sqlOptions.EnableRetryOnFailure(10, TimeSpan.FromSeconds(30), null);
});
});
services.AddAuthentication();
services.AddAuthorization();
return services;
}
}
I register my EventBus as follows:
private void RegisterEventBus(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IEventBus, EventBusRabbitMq>(sp =>
{
var subscriptionClientName = configuration["SubscriptionClientName"];
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQConnectionManagementService>();
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMq>>();
//var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
var retryCount = 5;
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(configuration["EventBusRetryCount"]);
}
//return new EventBusRabbitMq(eventBusSubcriptionsManager, rabbitMQPersistentConnection, iLifetimeScope, logger, retryCount, subscriptionClientName);
return new EventBusRabbitMq(eventBusSubcriptionsManager, rabbitMQPersistentConnection, sp, logger, retryCount, subscriptionClientName);
});
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
services.AddTransient<OrderCreatedIntegrationEventHandler>();
services.AddTransient<RemoveProductStockIntegrationEventHandler>();
}
Here is my EventBusRabbitMq class:
`
public class EventBusRabbitMq : IEventBus, IDisposable
{
const string BROKER_NAME = "retailInMotion_event_bus";
const string AUTOFAC_SCOPE_NAME = "retailInMotion_event_bus";
private readonly IEventBusSubscriptionsManager _subsManager;
private readonly IRabbitMQConnectionManagementService _rabbitMQConnectionManagementService;
private readonly IServiceProvider _serviceProvider;
//private readonly ILifetimeScope _autofac;
private readonly ILogger<EventBusRabbitMq> _logger;
private readonly int _retryCount;
private IModel _consumerChannel;
private string _queueName;
public EventBusRabbitMq(
IEventBusSubscriptionsManager subsManager,
IRabbitMQConnectionManagementService rabbitMQConnectionManagementService,
IServiceProvider serviceProvider,
//ILifetimeScope autofac,
ILogger<EventBusRabbitMq> logger,
int retryCount = 5,
string queueName = null)
{
_subsManager = subsManager;
_rabbitMQConnectionManagementService = rabbitMQConnectionManagementService;
_serviceProvider = serviceProvider;
//_autofac = autofac;
_logger = logger;
_retryCount = retryCount;
_queueName = queueName;
_consumerChannel = CreateConsumerChannel();
_subsManager.OnEventRemoved += SubsManager_OnEventRemoved;
}
private void SubsManager_OnEventRemoved(object? sender, string eventName)
{
if (!_rabbitMQConnectionManagementService.IsConnected)
{
_rabbitMQConnectionManagementService.TryConnect();
}
using (var channel = _rabbitMQConnectionManagementService.CreateModel())
{
channel.QueueUnbind(queue: _queueName,
exchange: BROKER_NAME,
routingKey: eventName);
if (_subsManager.IsEmpty)
{
_queueName = string.Empty;
_consumerChannel.Close();
}
}
}
private IModel? CreateConsumerChannel()
{
if (!_rabbitMQConnectionManagementService.IsConnected)
{
_rabbitMQConnectionManagementService.TryConnect();
}
_logger.LogTrace("Creating RabbitMQ consumer channel");
var channel = _rabbitMQConnectionManagementService.CreateModel();
channel.ExchangeDeclare(exchange: BROKER_NAME,
type: "direct");
channel.QueueDeclare(queue: _queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.CallbackException += (sender, ea) =>
{
_logger.LogWarning(ea.Exception, "Recreating RabbitMQ consumer channel");
_consumerChannel.Dispose();
_consumerChannel = CreateConsumerChannel();
StartBasicConsume();
};
return channel;
}
public void Publish(IntegrationEvent #event)
{
if (!_rabbitMQConnectionManagementService.IsConnected)
{
_rabbitMQConnectionManagementService.TryConnect();
}
var policy = RetryPolicy.Handle<BrokerUnreachableException>()
.Or<SocketException>()
.WaitAndRetry(_retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) =>
{
_logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s ({ExceptionMessage})", #event.Id, $"{time.TotalSeconds:n1}", ex.Message);
});
var eventName = #event.GetType().Name;
//var jsonMessage = JsonConvert.SerializeObject(#event);
_logger.LogTrace("Creating RabbitMQ channel to publish event: {EventId} ({EventName})", #event.Id, eventName);
using (var channel = _rabbitMQConnectionManagementService.CreateModel())
{
_logger.LogTrace("Declaring RabbitMQ exchange to publish event: {EventId}", #event.Id);
channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct");
var body = JsonSerializer.SerializeToUtf8Bytes(#event, #event.GetType(), new JsonSerializerOptions
{
WriteIndented = true
});
//var body = Encoding.UTF8.GetBytes(jsonMessage);
policy.Execute(() =>
{
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2; // persistent
_logger.LogTrace("Publishing event to RabbitMQ: {EventId}", #event.Id);
channel.BasicPublish(
exchange: BROKER_NAME,
routingKey: eventName,
mandatory: true,
basicProperties: properties,
body: body);
});
}
}
//public void Setup()
//{
// throw new NotImplementedException();
//}
public void Subscribe<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>
{
var eventName = _subsManager.GetEventKey<T>();
DoInternalSubscription(eventName);
_logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).Name);
_subsManager.AddSubscription<T, TH>();
StartBasicConsume();
}
private void DoInternalSubscription(string eventName)
{
var containsKey = _subsManager.HasSubscriptionsForEvent(eventName);
if (!containsKey)
{
if (!_rabbitMQConnectionManagementService.IsConnected)
{
_rabbitMQConnectionManagementService.TryConnect();
}
_consumerChannel.QueueBind(queue: _queueName,
exchange: BROKER_NAME,
routingKey: eventName);
}
}
public void Unsubscribe<T, TH>()
where T : IntegrationEvent
where TH : IIntegrationEventHandler<T>
{
var eventName = _subsManager.GetEventKey<T>();
_logger.LogInformation("Unsubscribing from event {EventName}", eventName);
_subsManager.RemoveSubscription<T, TH>();
}
private void StartBasicConsume()
{
_logger.LogTrace("Starting RabbitMQ basic consume");
if (_consumerChannel != null)
{
var consumer = new AsyncEventingBasicConsumer(_consumerChannel);
consumer.Received += Consumer_Received;
_consumerChannel.BasicConsume(
queue: _queueName,
autoAck: false,
consumer: consumer);
}
else
{
_logger.LogError("StartBasicConsume can't call on _consumerChannel == null");
}
}
private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventArgs)
{
var eventName = eventArgs.RoutingKey;
var message = Encoding.UTF8.GetString(eventArgs.Body.Span);
try
{
if (message.ToLowerInvariant().Contains("throw-fake-exception"))
{
throw new InvalidOperationException($"Fake exception requested: \"{message}\"");
}
await ProcessEvent(eventName, message);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message);
}
// Even on exception we take the message off the queue.
// in a REAL WORLD app this should be handled with a Dead Letter Exchange (DLX).
// For more information see: https://www.rabbitmq.com/dlx.html
_consumerChannel.BasicAck(eventArgs.DeliveryTag, multiple: false);
}
private async Task ProcessEvent(string eventName, string message)
{
_logger.LogTrace("Processing RabbitMQ event: {EventName}", eventName);
if (_subsManager.HasSubscriptionsForEvent(eventName))
{
//using (var scope = _autofac.BeginLifetimeScope(AUTOFAC_SCOPE_NAME)) // can do it this way with autofac or like directly below with built in .net core DI
using (var scope = _serviceProvider.CreateScope())
{
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
{
var handler = _serviceProvider.GetRequiredService(subscription.HandlerType); //CHRIS INVESTIGATE!!
//var handler = scope.ResolveOptional(subscription.HandlerType);
if (handler == null) continue;
var eventType = _subsManager.GetEventTypeByName(eventName);
//var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
//dynamic json = Newtonsoft.Json.JsonConvert.DeserializeObject(message);
//using dynamic eventData = JsonDocument.Parse(message);
var integrationEvent = JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true}); //JsonSerializer.Deserialize(message, eventType, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
await (Task)concreteType.GetMethod("HandleAsync").Invoke(handler, new object[] { integrationEvent });
}
}
}
else
{
_logger.LogWarning("No subscription for RabbitMQ event: {EventName}", eventName);
}
}
public void Dispose()
{
if (_consumerChannel != null)
{
_consumerChannel.Dispose();
}
_subsManager.Clear();
}
}
`
Here is the stack trace for the error I am receiving:
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) at EventBus.EventBusRabbitMq.<ProcessEvent>d__18.MoveNext() in C:\Users\porterc\Documents\My Version of DDD\RetailInMotion-master\EventBus\EventBusRabbitMq.cs:line 260 at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at EventBus.EventBusRabbitMq.<Consumer_Received>d__17.MoveNext() in C:\Users\porterc\Documents\My Version of DDD\RetailInMotion-master\EventBus\EventBusRabbitMq.cs:line 235
My question is why? If anyone can point me in the right direction or tell me what does this one line actually does I would be very grateful.
As the exception message says, your ProductAPI.IntegrationEvents.EventHandling.OrderCreatedIntegrationEventHandler is not resolved in a scope or is not registered as a scoped service.
Whatever event dispatcher you use ought to begin a scope before resolving the event handler and handling events. And the OrderCreatedIntegrationEventHandler must be registered as a service scoped too.
My guess is that Autofac does not perform any lifetime mismatch checks at startup and that's why it does not throw when you plug in Autofac. This does not mean, that it actually works, only that you hide a potential error. It may come up as an exception the first time you handle an event, or it may be even more severe in that it just uses an the same ProductDbContext over the lifetime of the event handler, which might be singleton.
But note that I havn't used Autofac, only other containers, so I don't know how exactly this library handles such issues.
EDIT 1
This line (260): var handler = _serviceProvider.GetRequiredService(subscription.HandlerType); in the provided source for EventBusRabbitMq needs to be changed to: var handler = scope.ServiceProvicer.GetRequiredService(subscription.HandlerType);.
Inside the scope you should resolve your services using the scope (as opposed to from the root service provider _serviceProvider from which you create your scope).
Try this, I think this might be to do more with the order it is constructed.
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => {
webBuilder.UseStartup<Startup>();
})
.UseServiceProviderFactory(new AutofacServiceProviderFactory());
Related
there are 2 apps which are work as consumers and producers.
app1 and app2. both of them are asp.net core web api.
app1 or app2 publish a message and both app1 and app2 consume the message.
but sometimes app1 consumes the same message many times ( from 2 to 478) but the same message is consuming by app2 just one time as expected. the consume method is very simple just one select and one insert operation.
at first, I thought after insert operation some exceptions occurred so masstransit enqueued it and tried to consume it again. but there is no code to make exceptions after insert operation. even if there is, we send exceptions to the sentry but there is nothing about exception in sentry.
according to aws sqs logs there are abnormal message counts in a specific period of time
app2's queue is normal
there are my configurations and codes below. so what could cause this?
configurations
app1
masstransit configuration
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
services.AddTransient<OrderCompletedIntegrationEvent>();
var options = configuration.GetSection("AmazonSqs").Get<AmazonSqsOptions>();
var massOptions = configuration.GetSection("Masstransit").Get<MasstransitOptions>();
IBusControl CreateBus(IServiceProvider serviceProvider)
{
var secretManagerService = serviceProvider.GetService<ISecretManagerService>();
var secrets = secretManagerService.GetSecrets();
var bus = Bus.Factory.CreateUsingAmazonSqs(cfg =>
{
cfg.Host(options.Region, h =>
{
h.AccessKey(secrets.AccessKeyId);
h.SecretKey(secrets.SecretKey);
});
cfg.Durable = true;
cfg.WaitTimeSeconds = 10;
cfg.ReceiveEndpoint(options.QueueName, q =>
{
// RetryCount: 1
// RetryWaitIntervalInSeconds: 1
q.UseMessageRetry(r => r.Interval(massOptions.RetryCount, TimeSpan.FromSeconds(massOptions.RetryWaitIntervalInSeconds)));
q.ConfigureConsumer<OrderCompletedIntegrationEventHandler>(serviceProvider);
});
});
return bus;
}
// local function to configure consumers
void ConfigureMassTransit(IServiceCollectionConfigurator configurator)
{
configurator.AddConsumer<OrderCompletedIntegrationEventHandler>();
}
// configures MassTransit to integrate with the built-in dependency injection
services.AddMassTransit(configurator =>
{
configurator.AddBus(CreateBus);
ConfigureMassTransit(configurator);
});
services.AddMassTransitHostedService();
return services;
}
consumer
public class OrderCompletedIntegrationEventHandler : IConsumer<OrderCompletedIntegrationEvent>
{
private readonly MasstransitOptions options;
private readonly IServiceProvider serviceProvider;
private readonly ICoinService coinService;
private readonly IAccountService accountService;
private readonly ILogger<OrderCompletedIntegrationEventHandler> _logger;
public OrderCompletedIntegrationEventHandler(
IConfiguration configuration,
IServiceProvider serviceProvider,
ICoinService coinService,
IAccountService accountService)
{
options = configuration.GetSection("Masstransit").Get<MasstransitOptions>();
this.serviceProvider = serviceProvider;
this.coinService = coinService;
this.accountService = accountService;
}
public async Task Consume(ConsumeContext<OrderCompletedIntegrationEvent> context)
{
var #event = context.Message;
var retry = context.GetRetryAttempt();
try
{
await Task.Run(() =>
{
var coin = coinService.TlToCoin(#event.Total);
var coinToAdd = coin * 25 / 1000;
var user = accountService.FindByEmail(#event.Email);
if (user != null) coinService.AddBonus(user.Id, coinToAdd);
});
}
catch (Exception ex)
{
if (retry == options.RetryCount)
{
using (var scope = serviceProvider.CreateScope())
{
var hub = scope.ServiceProvider.GetService<IHub>();
hub.CaptureException(new IntegrationEventException(data: #event, ex));
}
}
throw ex;
}
}
}
app2
masstransit configuration
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<OrderCompletedIntegrationEventHandler>();
var options = configuration.GetSection("AmazonSqs").Get<AmazonSqsOptions>();
var massOptions = configuration.GetSection("Masstransit").Get<MasstransitOptions>();
IBusControl CreateBus(IServiceProvider serviceProvider)
{
return Bus.Factory.CreateUsingAmazonSqs(cfg =>
{
cfg.UseAmazonSqsMessageScheduler();
cfg.Host(options.Region, h =>
{
h.AccessKey(options.AccessKeyId);
h.SecretKey(options.SecretKey);
});
cfg.Durable = true;
cfg.WaitTimeSeconds = 20;
cfg.ReceiveEndpoint(options.QueueName, q =>
{
// RetryCount: 5
// RetryWaitIntervalInSeconds: 300
q.UseMessageRetry(r => r.Interval(massOptions.RetryCount, TimeSpan.FromSeconds(massOptions.RetryWaitIntervalInSeconds)));
q.ConfigureConsumer<OrderCompletedIntegrationEventHandler>(serviceProvider);
});
});
}
// local function to configure consumers
void ConfigureMassTransit(IServiceCollectionConfigurator configurator)
{
configurator.AddConsumer<OrderCompletedIntegrationEventHandler>();
}
//configures MassTransit to integrate with the built -in dependency injection
services.AddMassTransit(x =>
{
x.AddBus(CreateBus);
ConfigureMassTransit(x);
});
services.AddMassTransitHostedService();
return services;
}
consumer
public class OrderCompletedIntegrationEventHandler : IConsumer<OrderCompletedIntegrationEvent>
{
private readonly MasstransitOptions options;
private readonly IServiceProvider serviceProvider;
private IUserService _userService;
private IScoreService _scoreService;
public OrderCompletedIntegrationEventHandler(
IConfiguration configuration,
IServiceProvider serviceProvider, IUserService userService, IScoreService scoreService)
{
options = configuration.GetSection("Masstransit").Get<MasstransitOptions>();
this.serviceProvider = serviceProvider;
this._userService = userService;
this._scoreService = scoreService;
}
public async Task Consume(ConsumeContext<OrderCompletedIntegrationEvent> context)
{
var #event = context.Message;
var retry = context.GetRetryAttempt();
try
{
var user = _userService.GetUserByEmail(#event.Email);
if (user != null)
{
var activity = _scoreService.GetActivityTypeByName("buy-book");
var scoreToAdd = Convert.ToInt32(#event.Total * 150);
var coinToAdd = Convert.ToInt32(#event.Total * 100 * 2.5M / 100);
var scoreModel = new ScoreModel
{
ActivityType = new ActivityType
{
Id = activity.Id
},
Name = activity.Name,
Description = $"{#event.Total:F2} TL'lik bir sipariş verildi. Sipariş no: {#event.OrderId}",
UserId = (int)user.ID,
BranchId = null,
GainedScore = scoreToAdd,
GainedCoins = coinToAdd,
CreateDate = DateTime.UtcNow.TurkeyTime()
};
await _scoreService.AddActivity(scoreModel);
}
}
catch (System.Exception ex)
{
if (retry == options.RetryCount)
{
using (var scope = serviceProvider.CreateScope())
{
var hub = scope.ServiceProvider.GetService<IHub>();
hub.CaptureException(ex);
}
}
throw ex;
}
}
}
Based on that link I'm trying to create EasyNetQ Dispatcher for my messages. For some reason my Consumer is not triggered when my message appears in queue and I've no idea what the reason might be.
public class Program
{
static void Main(string[] args)
{
var config = LoadConfiguration();
ConfigureServices(config);
Console.ReadLine();
}
public static IConfiguration LoadConfiguration()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
return builder.Build();
}
private static void ConfigureServices(IConfiguration config)
{
var services = new ServiceCollection()
.AddSingleton(config)
.AddEasyNetQ("host=127.0.0.1:5672;username=guest;password=guest")
.AddSingleton<AutoSubscriber>(provider =>
{
// When I put breakpoint below - is never reached. Is that correct behavior?
var subscriber = new AutoSubscriber(provider.GetRequiredService<IBus>(), "SomePrefix")
{
AutoSubscriberMessageDispatcher = provider.GetRequiredService<IAutoSubscriberMessageDispatcher>()
};
subscriber.Subscribe(new Assembly[] { Assembly.GetExecutingAssembly() });
subscriber.SubscribeAsync(new Assembly[] { Assembly.GetExecutingAssembly() });
return subscriber;
});
services.BuildServiceProvider();
}
}
Below is the rest of nested code, though it seems working fine - so probably the problem is in Program.cs
EasyNetQExtension
public static class EasyNetQExtension
{
private static void InternalInitEasyNetQ(IServiceCollection service, string rabbitMqConnection)
{
service.AddSingleton(RabbitHutch.CreateBus(rabbitMqConnection));
service.AddSingleton<IAutoSubscriberMessageDispatcher, ConsumerMessageDispatcher>(serviceProvider => new ConsumerMessageDispatcher(serviceProvider));
var consumerTypes = Assembly.GetExecutingAssembly().GetTypes()
.Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface)
.Where(x => x.GetInterfaces().Any(t => t.Name == typeof(IConsume<>).Name));
foreach (var consumerType in consumerTypes)
{
service.AddTransient(consumerType);
}
// My consumer is found here, so this works properly
var consumerAsyncTypes = Assembly.GetExecutingAssembly().GetTypes()
.Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface)
.Where(x => x.GetInterfaces().Any(t => t.Name == typeof(IConsumeAsync<>).Name));
foreach (var consumerAsyncType in consumerAsyncTypes)
{
service.AddTransient(consumerAsyncType);
}
}
public static IServiceCollection AddEasyNetQ(this IServiceCollection service, string rabbitMqConnectionString)
{
InternalInitEasyNetQ(service, rabbitMqConnectionString);
return service;
}
}
ConsumerMessageDispatcher
public class ConsumerMessageDispatcher : IAutoSubscriberMessageDispatcher
{
private readonly IServiceProvider _serviceProvider;
public ConsumerMessageDispatcher(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Dispatch<TMessage, TConsumer>(TMessage message, CancellationToken cancellationToken = new CancellationToken()) where TMessage : class where TConsumer : class, IConsume<TMessage>
{
try
{
TConsumer consumer = _serviceProvider.GetRequiredService<TConsumer>();
consumer.Consume(message);
}
catch (Exception exception)
{
throw;
}
}
public async Task DispatchAsync<TMessage, TConsumer>(TMessage message, CancellationToken cancellationToken = new CancellationToken()) where TMessage : class where TConsumer : class, IConsumeAsync<TMessage>
{
try
{
TConsumer consumer = _serviceProvider.GetRequiredService<TConsumer>();
await consumer.ConsumeAsync(message);
}
catch (Exception exception)
{
throw;
}
}
}
First, you have to implement IConsumer<> interface in your console app, so that it can be registered via InternalInitEasyNetQ method.
using EasyNetQ.AutoSubscribe;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
namespace MyApp
{
public class ConsumeTextMessage : IConsume<string>
{
private readonly ILogger _logger;
public ConsumeTextMessage(ILogger<ConsumeTextMessage> logger)
{
_logger = logger;
}
public void Consume(string message, CancellationToken cancellationToken = default)
{
_logger.LogInformation("Logging the message: " + message);
Console.WriteLine("Reading the message: " + message);
}
}
}
Second, you are missing part where you write message using IAutoSubscriberMessageDispatcher. You can resolve IAutoSubscriberMessageDispatcher interface using IServiceProvider or dependency injection. Something along those lines:
var dispatcher = _provider.GetRequiredService<IAutoSubscriberMessageDispatcher>();
dispatcher.Dispatch<string, ConsumeTextMessage>("Dispatch my message - " + DateTime.Now);
I fixed that by moving Autosubscriber to separate method so the correct looks:
public class Program
{
static void Main(string[] args)
{
var config = LoadConfiguration();
var provider = ConfigureServices(config);
ConfigureConsumers(provider);
Console.ReadLine();
}
public static IConfiguration LoadConfiguration()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
return builder.Build();
}
private static ServiceProvider ConfigureServices(IConfiguration configuration)
{
var services = new ServiceCollection()
.AddTransient<IEmailSender, EmailSender>()
.Configure<AuthMessageSenderOptions>(options => configuration.GetSection("SendGridEmailSettings").Bind(options))
.AddEasyNetQ(configuration["QueueConnectionData"]);
return services.BuildServiceProvider();
}
private static void ConfigureConsumers(ServiceProvider provider)
{
var autoSubscriber = new AutoSubscriber(provider.GetRequiredService<IBus>(), "SomePrefix")
{
AutoSubscriberMessageDispatcher = provider.GetRequiredService<IAutoSubscriberMessageDispatcher>()
};
autoSubscriber.Subscribe(new[] { Assembly.GetExecutingAssembly() });
autoSubscriber.SubscribeAsync(new[] { Assembly.GetExecutingAssembly() });
}
}
I have a website Angular frontend and WebAPI on the backend with all my controllers, I also have a service (C# class) that I call as a singleton as a long running task to listen for incoming Azure service bus messages.
FYI - I can't pass any scoped services (DbContext) to a singleton (ServiceBusConsumer), so I can't pass in my DB context to this service.
QUESTION - Once I receive an incoming service bus message, how do I call up my DB and use it?
Here is my service listening for and receiving messages.
Startup.cs
services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();
Program.cs -> in Main() I start the service
var bus = services.GetRequiredService<IServiceBusConsumer>();
bus.RegisterOnMessageHandlerAndReceiveMessages();
ServiceBusConsumer.cs
public class ServiceBusConsumer : IServiceBusConsumer
{
private readonly IConfiguration _config;
private readonly ServiceBusClient _queueClient;
private readonly ServiceBusProcessor _processor;
// private readonly DataContext _context;
public ServiceBusConsumer(IConfiguration config,
// DataContext context)
{
_config = config;
// _context = context;
_queueClient = new ServiceBusClient(_config["ServiceBus:Connection"]);
_processor = _queueClient.CreateProcessor(_config["ServiceBus:Queue"], new ServiceBusProcessorOptions());
}
public void RegisterOnMessageHandlerAndReceiveMessages() {
_processor.ProcessMessageAsync += MessageHandler;
_processor.ProcessErrorAsync += ErrorHandler;
_processor.StartProcessingAsync();
}
private async Task MessageHandler(ProcessMessageEventArgs args)
{
string body = args.Message.Body.ToString();
JObject jsonObject = JObject.Parse(body);
var eventStatus = (string)jsonObject["EventStatus"];
await args.CompleteMessageAsync(args.Message);
// _context is disposed
// want to connect to DB here but don't know how!
// var ybEvent = _context.YogabandEvents.Where(p => p.ServiceBusSequenceNumber == args.Message.SequenceNumber).FirstOrDefault();
}
private Task ErrorHandler(ProcessErrorEventArgs args)
{
var error = args.Exception.ToString();
return Task.CompletedTask;
}
}
Error
Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.\nObject name: 'DataContext'.
Here is Program.cs
public class Program
{
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var context = services.GetRequiredService<DataContext>();
var userManager = services.GetRequiredService<UserManager<User>>();
var roleManager = services.GetRequiredService<RoleManager<Role>>();
var bus = services.GetRequiredService<IServiceBusConsumer>();
bus.RegisterOnMessageHandlerAndReceiveMessages();
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occured during migration");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Here is Startup.cs -> just the ConfigureServices method
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper(typeof(MappingEvents));
services.AddAutoMapper(typeof(MappingMembers));
services.AddAutoMapper(typeof(MappingUsers));
services.AddAutoMapper(typeof(MappingYogabands));
services.AddAutoMapper(typeof(MappingReviews));
// objects being passed back to the UI. Before I was passing User/Photo/etc and they
// had loops/refrences back to the user objects
services.AddControllers().AddNewtonsoftJson(opt =>
{
opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Error;
});
services.AddDbContext<DataContext>(x =>
// x.UseSqlite(_config.GetConnectionString("DefaultConnection"), y => y.UseNetTopologySuite()));
x.UseSqlServer(_config.GetConnectionString("SqlServerConnection"), y => y.UseNetTopologySuite()));
services.Configure<AuthMessageSenderOptions>(_config.GetSection("SendGrid"));
services.Configure<AuthMessageSenderOptionsNew>(_config.GetSection("SendGrid"));
services.Configure<ConfirmationOptions>(_config.GetSection("Confirmation"));
services.Configure<CloudinarySettings>(_config.GetSection("CloudinarySettings"));
services.AddApplicationServices();
services.AddIdentityServices(_config);
services.AddSwaggerDocumentation();
services.AddCors(opt =>
{
opt.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("https://localhost:4200");
});
});
}
Here is AddApplicationServices()
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// scoped - better option when you want to maintain state within a request
// services.AddScoped<IEventConsumer, EventConsumer>();
services.AddScoped<IServiceBusProducer, ServiceBusProducer>();
services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();
services.AddScoped<IEmailSender, EmailSender>();
services.AddScoped<IEmailSender, EmailSenderNew>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped(typeof(IGenericRepository<>), (typeof(GenericRepository<>)));
services.AddScoped<LogUserActivity>();
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
var errors = actionContext.ModelState
.Where(e => e.Value.Errors.Count > 0)
.SelectMany(x => x.Value.Errors)
.Select(x => x.ErrorMessage).ToArray();
var errorResponse = new ApiValidationErrorResponse
{
Errors = errors
};
return new BadRequestObjectResult(errorResponse);
};
});
return services;
}
It seems your problem is with DI.
Your ServiceBusConsumer service is a singleton but you inject a DbContext as a constructor. This is generally the recommendation but in this case, it can't work.
You inject a DbContext in the constructor and "save" a "link" to it. But then it gets disposed, so that "link" won't work.
Instead, you should inject a DbContextFactory. With a factory, you can create DbContext instances on demand.
private readonly IDbContextFactory<DataContext> _contextFactory;
public ServiceBusConsumer(IConfiguration config, IDbContextFactory<DataContext> contextFactory)
{
// Add this line
_contextFactory = contextFactory;
}
private async Task MessageHandler(ProcessMessageEventArgs args)
{
// With the new C# 8 syntax you can do
using var db = _contextFactory.CreateDbContext();
// Otherwise, wrap it up
using (var db = _contextFactory.CreateDbContext())
{
}
}
Here's a link to a docs where they show how it can be used: https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor
You just need to register it:
public void ConfigureServices(IServiceCollection services)
{
// Add this line to register a context factory
services.AddDbContextFactory<DataContext>(
options =>
.UseSqlServer(_config.GetConnectionString("SqlServerConnection"), y => y.UseNetTopologySuite()));
}
You can't use the same DI as with controllers, since they're usually not singletons, therefore won't run into this problem. AFAIK the DbContextFactory was created exactly for this purpose (with Blazor in mind). If the service you needed was not a DbContext you would need to inject the service provider in the constructor and then request the service directly, although Microsoft doesn't recommend that.
I solved same problem avoiding using statment, instead declare scope variable within Main task. You want to keep alive the scope created for queue message handlers so your Program.cs should be like this:
public class Program
{
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
// This variable is working within ServiceBus threads so it need to bee keept alive until Main ends
var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var context = services.GetRequiredService<DataContext>();
var userManager = services.GetRequiredService<UserManager<User>>();
var roleManager = services.GetRequiredService<RoleManager<Role>>();
var bus = services.GetRequiredService<IServiceBusConsumer>();
bus.RegisterOnMessageHandlerAndReceiveMessages();
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occured during migration");
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
I'm creating a project that is based on the eShopOnContainers Microservices architecture
I Made a few changes to program.cs and startup.cs according to .NET Core 3+
Program.cs:
public static IHostBuilder CreateHostBuilder(IConfiguration configuration, string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
Startup.cs:
// ConfigureContainer is where you can register things directly
// with Autofac. This runs after ConfigureServices so the things
// here will override registrations made in ConfigureServices.
// Don't build the container; that gets done for you by the factory.
public void ConfigureContainer(ContainerBuilder builder)
{
//configure autofac
// Register your own things directly with Autofac, like:
builder.RegisterModule(new MediatorModule());
builder.RegisterModule(new ApplicationModule(Configuration));
}
Now in Startup.cs the AddCustomIntegrations() method Registers the IRabbitMQPersistentConnection which returns the DefaultRabbitMQPersistentConnection with IConnectionFactory configured
public static IServiceCollection AddCustomIntegrations(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IIdentityService, IdentityService>();
services.AddTransient<IVehicleManagementIntegrationEventService, VehicleManagementIntegrationEventService>();
services.AddTransient<Func<DbConnection, IIntegrationEventLogService>>(
sp => (DbConnection c) => new IntegrationEventLogService(c));
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new ConnectionFactory()
{
HostName = configuration["EventBusConnection"],
DispatchConsumersAsync = true
};
if (!string.IsNullOrEmpty(configuration["EventBusUserName"]))
{
factory.UserName = configuration["EventBusUserName"];
}
if (!string.IsNullOrEmpty(configuration["EventBusPassword"]))
{
factory.Password = configuration["EventBusPassword"];
}
var retryCount = 5;
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(configuration["EventBusRetryCount"]);
}
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
});
return services;
}
public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
var subscriptionClientName = configuration["SubscriptionClientName"];
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
{
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
var retryCount = 5;
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(configuration["EventBusRetryCount"]);
}
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount);
});
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
return services;
}
When I run the application I get the following error:
Autofac.Core.DependencyResolutionException: An exception was thrown while activating IFMS.GMT.BuildingBlocks.Infrastructure.Events.EventBusRabbitMQ.EventBusRabbitMQ -> IFMS.GMT.BuildingBlocks.Infrastructure.Events.EventBusRabbitMQ.DefaultRabbitMQPersistentConnection.
---> Autofac.Core.DependencyResolutionException: None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'IFMS.GMT.BuildingBlocks.Infrastructure.Events.EventBusRabbitMQ.DefaultRabbitMQPersistentConnection' can be invoked with the available services and parameters:
Cannot resolve parameter 'RabbitMQ.Client.IConnectionFactory connectionFactory' of constructor 'Void .ctor(RabbitMQ.Client.IConnectionFactory, Microsoft.Extensions.Logging.ILogger`1[IFMS.GMT.BuildingBlocks.Infrastructure.Events.EventBusRabbitMQ.DefaultRabbitMQPersistentConnection], Int32)'.
Autofac cant seem to find the service registered with AddCustomIntegrations()
I Moved all the code from AddCustomIntegrations() and AddEventBus() to a separate Module class that inherites from Autofac.Module class and it worked
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<InMemoryEventBusSubscriptionsManager>()
.As<IEventBusSubscriptionsManager>()
.InstancePerLifetimeScope();
builder.Register<IRabbitMQPersistentConnection>(fff =>
{
var logger = fff.Resolve<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new ConnectionFactory()
{
HostName = Configuration["EventBusConnection"],
DispatchConsumersAsync = true
};
if (!string.IsNullOrEmpty(Configuration["EventBusUserName"]))
{
factory.UserName = Configuration["EventBusUserName"];
}
if (!string.IsNullOrEmpty(Configuration["EventBusPassword"]))
{
factory.Password = Configuration["EventBusPassword"];
}
var retryCount = 5;
if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"]))
{
retryCount = int.Parse(Configuration["EventBusRetryCount"]);
}
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
});
}
I'm trying to do dependency injection via the module constructor, and it seems to work because I can successfully use the class inside of the constructor, but whenever I run a command it breaks. This is what I get when I try to run a command:
---> System.InvalidOperationException: Failed to create "AnimeCalendarBot.Modules.InfoModule", dependency "TestService" was not found.
at Discord.Commands.ReflectionUtils.GetMember(CommandService commands, IServiceProvider services, Type memberType, TypeInfo ownerType)
at Discord.Commands.ReflectionUtils.<>c__DisplayClass2_0`1.<CreateBuilder>b__0(IServiceProvider services)
at Discord.Commands.ModuleClassBuilder.<>c__DisplayClass6_0 <<BuildCommand>g__ExecuteCallback|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at Discord.Commands.CommandInfo.ExecuteInternalAsync(ICommandContext context, Object[] args, IServiceProvider services)
--- End of inner exception stack trace ---
But before running a command, everything is fine.
Program:
public class Program
{
public static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();
private readonly DiscordSocketClient _client;
private readonly CommandService _commands;
private readonly CommandHandler _commandHandler;
private readonly IServiceProvider _services;
private Program()
{
_client = new DiscordSocketClient(new DiscordSocketConfig
{
LogLevel = LogSeverity.Info,
});
_commands = new CommandService(new CommandServiceConfig
{
LogLevel = LogSeverity.Info,
CaseSensitiveCommands = false,
});
_services = ConfigureServices();
_commandHandler = new CommandHandler(_client, _commands, _services);
_client.Log += Log;
_commands.Log += Log;
}
private static IServiceProvider ConfigureServices()
{
var map = new ServiceCollection()
.AddSingleton(new TestService());
return map.BuildServiceProvider();
}
private static Task Log(LogMessage message)
{
switch (message.Severity)
{
case LogSeverity.Critical:
case LogSeverity.Error:
Console.ForegroundColor = ConsoleColor.Red;
break;
case LogSeverity.Warning:
Console.ForegroundColor = ConsoleColor.Yellow;
break;
case LogSeverity.Info:
Console.ForegroundColor = ConsoleColor.White;
break;
case LogSeverity.Verbose:
case LogSeverity.Debug:
Console.ForegroundColor = ConsoleColor.DarkGray;
break;
}
Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message} {message.Exception}");
Console.ResetColor();
return Task.CompletedTask;
}
public async Task MainAsync()
{
await _commandHandler.InstallCommandsAsync();
_client.Log += Log;
await _client.LoginAsync(TokenType.Bot,
Environment.GetEnvironmentVariable("AnimeCalendarBotToken"));
await _client.StartAsync();
// Block this task until the program is closed.
await Task.Delay(-1);
}
}
CommandHandler:
public class CommandHandler
{
private readonly DiscordSocketClient _client;
private readonly CommandService _commands;
private readonly IServiceProvider _services;
public CommandHandler(DiscordSocketClient client, CommandService commands, IServiceProvider services)
{
_client = client;
_commands = commands;
_services = services;
}
public async Task InstallCommandsAsync()
{
_client.MessageReceived += HandleCommandAsync;
await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(),
services: _services);
}
private async Task HandleCommandAsync(SocketMessage messageParam)
{
// Don't process the command if it was a system message
var message = messageParam as SocketUserMessage;
if (message == null) return;
// Create a number to track where the prefix ends and the command begins
int argPos = 0;
// Determine if the message is a command based on the prefix and make sure no bots trigger commands
if (!(message.HasCharPrefix('!', ref argPos) ||
message.HasMentionPrefix(_client.CurrentUser, ref argPos)) ||
message.Author.IsBot)
return;
// Create a WebSocket-based command context based on the message
var context = new SocketCommandContext(_client, message);
// Execute the command with the command context we just
// created, along with the service provider for precondition checks.
// Keep in mind that result does not indicate a return value
// rather an object stating if the command executed successfully.
var result = await _commands.ExecuteAsync(
context: context,
argPos: argPos,
services: null);
}
}
InfoModule:
public class InfoModule : ModuleBase<SocketCommandContext>
{
private readonly TestService _testService;
public InfoModule(TestService testService)
{
_testService = testService;
_testService.Start(); // this works
}
[Command("startTest")]
[Summary("Starts test service.")]
public Task StartTestService()
{
_testService.Start();
return Task.CompletedTask;
}
[Command("stopTest")]
[Summary("Stops test service.")]
public Task StopTestService()
{
_testService.Stop();
return Task.CompletedTask;
}
}
TestService:
public class TestService
{
private readonly Timer _timer;
public TestService()
{
_timer = new Timer(1000) { AutoReset = true };
_timer.Elapsed += TimerElapsed;
}
private void TimerElapsed(object sender, ElapsedEventArgs e)
{
String currentTime = DateTime.Now.ToString();
Console.WriteLine(currentTime);
}
public void Start()
{
_timer.Start();
}
public void Stop()
{
_timer.Stop();
}
}
I ran into this same issue before. The problem is that the services are not injected by the CommandHandler.
So first of all I would assign your service like in the answer of #EnderIce2.
var map = new ServiceCollection()
.AddSingleton<TestService>()
After that you need to go the the CommandHandler and change the ending InstallCommandsAsync method:
await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(),
services: null);
To
await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(),
services: _services);
The same goes for HandleCommandAsync; at the end change:
await _commands.ExecuteAsync(
context: context,
argPos: argPos,
services: null);
To:
await _commands.ExecuteAsync(
context: context,
argPos: argPos,
services: _services);
Try this on Program at ConfigureServices:
private static IServiceProvider ConfigureServices()
{
var map = new ServiceCollection()
.AddSingleton<TestService>()
return map.BuildServiceProvider();
}