AutoSubscriber in EasyNetQ does not working - RabbitMQ .NET - c#

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() });
}
}

Related

.NET 6 Unit Test with DependencyResolverHelper

I was using my unit tests on .Net5(or lower) with DependencyResolverHelper like this below. This is my base test class
public abstract class BaseTestClass
{
protected DependencyResolverHelper _serviceProvider;
public BaseTestClass()
{
var webHost = WebHost.CreateDefaultBuilder()
.UseStartup<Startup>()
.Build();
_serviceProvider = new DependencyResolverHelper(webHost);
}
}
and my DependencyResolverHelper
public class DependencyResolverHelper
{
private readonly IWebHost _webHost;
/// <inheritdoc />
public DependencyResolverHelper(IWebHost webHost) => _webHost = webHost;
public T GetService<T>()
{
var serviceScope = _webHost.Services.CreateScope();
var services = serviceScope.ServiceProvider;
try
{
var scopedService = services.GetRequiredService<T>();
return scopedService;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
but im bit confused with new .NET 6 Dependency things. Does anyone tried it without startup class?
I tried to change it with
WebApplicationBuilder
but it gave the
No service for type 'MinimalAPI.Services.TokenService.ITokenService' has been registered. error.
Just Because this part of codes .UseStartup<Startup>() called startup class and registed the services for you,If you try with WebApplicationBuilder in .net 6,You have to regist the services yourself,
I tried as below:
public void Test1()
{
var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { });
builder.Services.AddSingleton<ISomeService,SomeService>();
var app = builder.Build();
var serviceProvider = new DependencyResolverHelper(app);
var someservice = serviceProvider.GetService<ISomeService>();
Assert.Equal(typeof(SomeService), someservice.GetType());
}
DependencyResolverHelper class:
public class DependencyResolverHelper
{
private readonly WebApplication _app;
/// <inheritdoc />
public DependencyResolverHelper(WebApplication app) => _app = app;
public T GetService<T>()
{
var serviceScope = _app.Services.CreateScope();
var services = serviceScope.ServiceProvider;
try
{
var scopedService = services.GetRequiredService<T>();
return scopedService;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
Result:

Cannot resolve from root provider because it requires scoped service

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());

a consumer consumes the same message many times - masstransit

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;
}
}
}

How to implement Dependency Injection in .Net core Console Application

I have a project .Net Core in React and, to save time, I would like to test a method to export files by a Console Application. But I can't to implement the DI in my console, here's the error:
Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger`1[MyPrj.Managers.Managers.Implementations.MyLogManager]' while attempting to activate 'MyPrj.Managers.Managers.Implementations.MyLogManager'.
This is a simplified version of my Program.cs:
class Program
{
public static IConfigurationRoot Configuration;
public static ServiceProvider serviceProvider;
public static void Main(string[] args)
{
var services = new ServiceCollection();
var builder = new ConfigurationBuilder()
.SetBasePath(Path.Combine(AppContext.BaseDirectory))
.AddJsonFile("appsettings.json", optional: true);
Configuration = builder.Build();
services.AddDbContext<MyDBContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.Configure<MailOptions>(Configuration.GetSection("EmailSettings"));
[... other options...]
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<ILogManager, MyLogManager>();
services.AddScoped<IMailManager, MailManager>();
[... other services ...]
services.AddScoped<IDocumentationManager, DocumentationManager>();
serviceProvider = services.BuildServiceProvider();
MainAsync().GetAwaiter().GetResult();
Console.WriteLine("END");
Console.ReadKey();
}
private static async Task MainAsync()
{
await ExportAsync();
}
public static async Task ExportAsync()
{
using (MyDBContext dbContext = serviceProvider.GetService<MyDBContext>())
{
List<User> user = dbContext.Users.ToList();
var logger = serviceProvider.GetService<ILogManager>();
var docManager = serviceProvider.GetService<IDocumentationManager>();
string userAnswer = Console.ReadLine();
var file = await docManager.GetUser(userAnswer);
File.WriteAllBytes("C:\tmp\user.txt", file.File);
}
}
}
Since you are creating your console app from scratch you have to manually configure the logger, otherwise it would not be injected.
var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter("System", LogLevel.Warning)
.AddFilter("LoggingConsoleApp.Program", LogLevel.Debug)
.AddConsole()
.AddEventLog();
});
ILogger logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Example log message");
services.AddSingleton<ILoggerFactory>(loggerFactory);
public class MyLogManager : ILogManager {
private readonly ILogger<MyLogManager> logger;
public MyLogManager(ILoggerFactory factory) {
logger = factory.CreateLogger<MyLogManager>();
}
}
you can find more here https://learn.microsoft.com/aspnet/core/fundamentals/logging/?view=aspnetcore-3.1
You can use a HostedService for encapsulating the StartAsync and StopAsync, making the code more elegancy.
public class HostedService : IHostedService
{
private readonly ISolrSeeder _seeder;
public HostedService(ISolrSeeder seeder)
{
_seeder = seeder;
}
public async Task StartAsync(CancellationToken cancellationToken)
=> await _seeder.SeedAsync(cancellationToken);
public async Task StopAsync(CancellationToken cancellationToken) =>
await Task.Run(() => Environment.Exit(Environment.ExitCode), cancellationToken);
}
And then, you can make your program.cs represents the startup from a console application, considering many environments.
internal class Program
{
private const string SettingsName = "appsettings";
private const string SettingsExtention = ".json";
private const string AppSettings = SettingsName + SettingsExtention;
public static async Task Main(string[] args)
=> await new HostBuilder()
.ConfigureHostConfiguration(configHost =>
{
configHost
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(AppSettings, true, true)
.AddEnvironmentVariables()
.AddCommandLine(args);
})
.ConfigureAppConfiguration((hostContext, configApp) =>
{
configApp
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(AppSettings, true, true)
.AddJsonFile(SettingsName + "." + hostContext.HostingEnvironment.EnvironmentName + SettingsExtention, true, true)
.AddEnvironmentVariables()
.AddCommandLine(args);
})
.ConfigureServices((hostContext, services) =>
{
services
.AddLogging()
.AddRepositories()
.AddDbContext(options => options.ConnectionString = hostContext.Configuration.GetConnectionString("DefaultConnection"))
.AddSolr(options =>
{
options.BaseAddress = hostContext.Configuration["Solr:BaseAddress"];
options.Core = hostContext.Configuration["Solr:Core"];
})
.AddScoped<ISolrSeeder, SolrSeeder>()
.AddHostedService<HostedService>();
})
.ConfigureLogging((hostContext, configLogging) =>
{
configLogging.AddConsole();
})
.UseConsoleLifetime()
.Build()
.RunAsync();
}
The rest of the code is available on this repository.
Thanks to all,
it has been enough adding services.AddLogging(), following #Steven 's advice, to solve my problem.

How to access a service in ASP.NET Core before building the WebHost?

Let's assume that we have the following Program.cs:
public static class Program
{
public static async Task Main(string[] args)
{
await CreateWebHostBuilder(args).Build().RunAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return WebHost
.CreateDefaultBuilder(args)
.ConfigureKestrel(options => { options.AllowSynchronousIO = false; })
.ConfigureFanciness()
.ConfigureLogging(ConfigureApplicationLogging)
.UseStartup<Startup>();
}
private static void ConfigureApplicationLogging(WebHostBuilderContext context, ILoggingBuilder loggingBuilder)
{
var loggingConfiguration = context.Configuration.GetSection("Logging");
loggingBuilder.AddConfiguration(loggingConfiguration);
// var fancyService = SomehowGet<IFancyService>();
// if (fancyService.IsEnabled)
// loggingBuilder.AddEventLog(loggingConfiguration);
}
public static IWebHostBuilder ConfigureFanciness(this IWebHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices(delegate (WebHostBuilderContext context, IServiceCollection services)
{
var fancinessConfiguration = context.Configuration.GetSection("Fanciness");
services.Configure<FancinessSettings>(fancinessConfiguration);
services.AddSingleton<IFancyService, FancyService>();
// var fancyService = SomehowGet<IFancyService>();
// fancyService.Initialize();
});
}
}
And the following FancyService.cs:
public sealed class FancyService : IFancyService
{
private readonly ILogger<FancyService> logger;
private readonly IOptions<FancinessSettings> settings;
public FancyService(ILogger<FancyService> logger, IOptions<FancinessSettings> settings)
{
this.logger = logger;
this.settings = settings;
}
public bool IsEnabled { get; private set; }
public void Initialize()
{
// do the initialization work and set IsEnabled to some value
}
}
As demonstrated in the example files, loggingBuilder.AddEventLog(loggingConfiguration) depends on IFancyService.IsEnabled that is set when IFancyService.Initialize() is called.
To do that, I need access to an instance of IFancyService; is there a way to achieve this?
Once services.AddSingleton<IFancyService, FancyService>() has been called, you can get the instance of FancyService by building a ServiceProvider:
private static void ConfigureApplicationLogging(WebHostBuilderContext context, ILoggingBuilder loggingBuilder)
{
var fancyService = loggingBuilder.Services.BuildServiceProvider().GetService<IFancyService>();
fancyService.Initialize();
var loggingConfiguration = context.Configuration.GetSection("Logging");
loggingBuilder.AddConfiguration(loggingConfiguration);
if (fancyService.IsEnabled)
loggingBuilder.AddEventLog(loggingConfiguration);
}
As discussed in comments, if your FancyService has no constructor dependency on ILogger<> :
public sealed class FancyService : IFancyService
{
public FancyService(IOptions<FancinessSettings> settings)
{ ... }
to initialize the IFancyService, simply use an implementation factory to create a IFancyService instance:
public static IWebHostBuilder ConfigureFanciness(this IWebHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices(delegate (WebHostBuilderContext context, IServiceCollection services)
{
var fancinessConfiguration = context.Configuration.GetSection("Fanciness");
services.Configure<FancinessSettings>(fancinessConfiguration);
services.AddSingleton<IFancyService, FancyService>(sp =>{
var fancy=ActivatorUtilities.CreateInstance(sp,typeof(FancyService)) as FancyService;
fancy.Initialize();
return fancy;
});
});
}
The same trick could also be used to register a logger provider. Since an ILogger<> service depends on IEnumerable<ILoggerProvider>, we could register an ILoggerProvider instance that provides an additional INullLogger in order to configure the logging behavior according to the current IFancyService :
private static void ConfigureApplicationLogging(WebHostBuilderContext context, ILoggingBuilder loggingBuilder)
{
var loggingConfiguration = context.Configuration.GetSection("Logging");
var descriptor=ServiceDescriptor.Singleton<ILoggerProvider,NullLoggerProvider>(sp =>{
var provider = NullLoggerProvider.Instance;
var fancy = sp.GetRequiredService<IFancyService>();
if(fancy.IsEnabled)
{
loggingBuilder.AddDebug();
loggingBuilder.AddEventLog(loggingConfiguration);
loggingBuilder.AddEventSourceLogger();
// ... add more configuration as you like
}else{
loggingBuilder.AddConsole();
}
return provider;
});
loggingBuilder.Services.TryAddEnumerable(descriptor);
loggingBuilder.AddConfiguration(loggingConfiguration);
}
As a side note, be aware once the logger has been built, don't change the IFancyService.IsEnabled. That's because the ILogger<> service is a registered as a singleton and never changed once created.

Categories