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());
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'm trying to send emails using Hangfire and Postal, but I'm having problems with the HTTPContext. If I execute the shooting through a controller the sending is done without problems, now if I execute a job via Hangfire an expection is generated.
Code startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(Configuration.GetConnectionString("default"), new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
UsePageLocksOnDequeue = true,
DisableGlobalLocks = true
}));
services.Configure<EmailSenderOptions>(Configuration.GetSection("EmailSender"));
services.AddPostal();
services.AddTransient<IEmailSenderEnhance, EmailSender>();
services.AddHttpContextAccessor();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
var options = new DashboardOptions
{
Authorization = new[] { new HangFireAuthorizationFilter() }
};
app.UseHangfireDashboard("/hangfire", options);
app.UseHangfireServer();
RecurringJob.AddOrUpdate<NotificationServices>("SendNotify-", m => m.Notify(1), Cron.Daily);
}
Code NotificationServices.cs
public class NotificationServices
{
private readonly IEmailSenderEnhance _emailSender;
private readonly Config _appConfig;
private readonly IHttpContextAccessor _httpContextAccessor;
public NotificationServices(IEmailSenderEnhance emailSender, IOptions<Config> optionsAccessor, IHttpContextAccessor httpContextAccessor)
{
_emailSender = emailSender;
if (optionsAccessor == null) throw new ArgumentNullException(nameof(optionsAccessor));
_appConfig = optionsAccessor.Value;
_httpContextAccessor = httpContextAccessor;
}
public void Notify(int messageId)
{
var listOSs = Task.Run(async () => await this.SendEmail(messageId));
}
public async Task SendEmail(int orderID)
{
var orderRy = new OrderRepository();
var order = orderRy.Get_By_ID(orderID);
try
{
var requestPath = new Postal.RequestPath
{
PathBase = _httpContextAccessor.HttpContext.Request.PathBase.ToString(),
Host = _httpContextAccessor.HttpContext.Request.Host.ToString(),
IsHttps = _httpContextAccessor.HttpContext.Request.IsHttps,
Scheme = _httpContextAccessor.HttpContext.Request.Scheme,
Method = _httpContextAccessor.HttpContext.Request.Method
};
var emailData = new Postal.Email("SendTest")
{
RequestPath = requestPath,
};
var emailsCopy = $"{order.Salesman.Email},{order.Salesman2.Email},";
emailData.ViewData["to"] = emailsCopy;
emailData.ViewData["Order"] = order;
await _emailSender.SendEmailAsync(emailData);
}
catch (Exception ex)
{
}
}
}
The error is in HttpContext:
_httpContextAccessor.HttpContext.Request.PathBase.ToString()
Is it possible to send e-mails via background with Postal?
I am attempting to run a background worker for a web app that I am developing. I am using Npgsql as my EF Core provider.
For clarification, I have injected my DbContext with a Transient lifetime, and have allowed Pooling in my connection string, however, whenever I try to test it I get the following error:
Npgsql.NpgsqlOperationInProgressException: A command is already in progress: [My Query Here]
I have my Program Class Set up as such:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
// Get the configuration
IConfiguration config = hostContext.Configuration;
// DbContext
services.AddDbContext<MyDbContext>(options => options.UseNpgsql(config.GetConnectionString("PostgreSQLString")), ServiceLifetime.Transient);
services.AddHostedService<Worker>();
services.AddScoped<IDTOService, BackgroundDTOService>();
});
}
Which then leads to my Worker class
public class Worker : BackgroundService
{
private Logger logger;
public Worker(IServiceProvider services, IConfiguration configuration)
{
this.Services = services;
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgreSQLString"));
var context = new StatPeekContext(optionsBuilder.Options);
this.logger = new Logger(new LogWriter(context));
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
this.logger.LogInformation("ExecuteAsync in Worker Service is running.");
await this.DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
using (var scope = Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
var dtoService = scope.ServiceProvider.GetRequiredService<IDTOService>();
await dtoService.ProcessJSON(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
this.logger.LogInformation("Worker service is stopping.");
await Task.CompletedTask;
}
}
which leads to my BackGroundDTOService
public class BackgroundDTOService : IDTOService
{
private int executionCount = 0;
private Logger logger;
private MyDbContext context;
private DbContextOptionsBuilder<MyDbContext> optionsBuilder;
public BackgroundDTOService(IConfiguration configuration, MyDbContext context)
{
this.optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
this.optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgreSQLString"));
this.logger = new Logger(new LogWriter(new MyDbContext(this.optionsBuilder.Options)));
this.context = context;
}
public async Task ProcessJSON(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
this.executionCount++;
this.logger.LogInformation($"DTO Service is working. Count: {this.executionCount}");
this.ProcessTeams();
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
}
public void ProcessTeams()
{
// Add Any Franchises that don't exist
var franchiseDumps = this.context.RequestDumps.Where(rd => rd.Processed == false && rd.DumpType == "leagueteams");
foreach (RequestDump teamDump in franchiseDumps)
{
var league = this.context.Leagues.Include(l => l.Franchises).FirstOrDefault(l => l.Id == teamDump.LeagueID);
var teams = Jeeves.GetJSONFromKey<List<DTOTeam>>(teamDump.JsonDump, "leagueTeamInfoList");
foreach (DTOTeam team in teams)
{
this.UpdateFranchise(team, league);
}
this.logger.LogInformation($"DTO Service Processed League Teams on count {this.executionCount}");
}
this.context.SaveChanges();
}
The error appears to occur immediately after snagging franchiseDumps when it tries to get league
Could you try materialising the query:
var franchiseDumps = this.context.RequestDumps.Where(rd => rd.Processed == false && rd.DumpType == "leagueteams").ToList();
I had the same issue and I realized that PostgreSQL doesn't support Multiple Active Result Sets (MARS) compare to MSSQL.
An alternative way that worked for me to fetch objects with foreign key:
IEnumerable<Expense> objList = _db.Expenses;
foreach (var obj in objList.ToList())
{
obj.ExpenseCategory = _db.ExpenseCategories.FirstOrDefault(u => u.Id == obj.ExpenseCategoryId);
}
I'm facing a strange issue since I created a view component in my app.
All my integration tests using an HttpClient start failing with response code "Internal Server Error".
Here the test configuration :
public class BaseTest<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
private readonly bool _hasUser;
private readonly HttpClient _client;
protected BaseTest(bool hasUser = false)
{
_hasUser = hasUser;
_client = CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
}
protected async Task<HttpResponseMessage> GetPageByPath(string path)
{
return await _client.GetAsync(path);
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
if (_hasUser)
{
services.AddScoped<IAuthenticationService, AuthenticationServiceStub>();
services.AddSingleton<IStartupFilter, FakeUserFilter>();
services.AddMvc(options =>
{
options.Filters.Add(new AllowAnonymousFilter());
options.Filters.Add(new AuthenticatedAttribute());
});
}
});
builder.ConfigureServices(services =>
{
// Create a new service provider.
ServiceProvider serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// Build the service provider.
var sp = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var logger = scopedServices
.GetRequiredService<ILogger<BaseTest<TStartup>>>();
}
});
}
}
}
And a usage example :
public class BasicTest : BaseTest<Startup>
{
public BasicTest() : base()
{
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/Users/SignOut")]
[Trait("Category", "Integration")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Act
var response = await GetPageByPath(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
If you need the component code let me know.
I already rollback code to check when this start happening, and it's start after the commit with the new Component called in several pages.