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);
});
}
Related
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>();
});
}
As far as I have been able to do is to add new services though the IServiceCollection
Is there a way to add and remove these services at run time?
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMyService, MyService>();
var serviceProvider = services.BuildServiceProvider();
var service = serviceProvider.GetService<IMyService>();
AzureMultiTenantServiceBuilders.Build(services);
}
I am trying to figure out how to configure additional AzureAd for our tenants at run time without having to restart the system.
public static class AzureMultiTenantServiceBuilders
{
public static void Build(IServiceCollection services)
{
foreach (var tenant in Tenant.GetAll())
{
services.AddAuthentication()
.AddAzureADTenanted(options =>
{
options.ClientId = tenant.ClientId;
options.TenantId = tenant.TenantId;
options.Instance = "https://login.microsoftonline.com";
});
}
I found this Installing a new middleware at runtime in ASP.Net Core which was close as its adding middleware but what i am trying to do is adding a service at runtime so this isnt helping.
public static class RuntimeMiddlewareExtensions
{
public static IServiceCollection AddRuntimeMiddleware(this IServiceCollection services, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
services.Add(new ServiceDescriptor(typeof(RuntimeMiddlewareService), typeof(RuntimeMiddlewareService), lifetime));
AzureMultiTenantServiceBuilders.Build(services);
return services;
}
public static IApplicationBuilder UseRuntimeMiddleware(this IApplicationBuilder app, Action<IApplicationBuilder> defaultAction = null)
{
var service = app.ApplicationServices.GetRequiredService<RuntimeMiddlewareService>();
service.Use(app);
if (defaultAction != null)
{
service.Configure(defaultAction);
}
return app;
}
}
I have a console application which works quit like a web api.
At the Program.cs I register
var collection = new ServiceCollection();
collection.AddScoped<IInfoBusinessComponent, InfoBusinessComponent>();
The InfoBusinessComponent need also a dependency injection which I do before adding the InfoBusinessComponent. Also I register my ILogger.
At my InfoController I use the di like that:
public InfoController(IInfoBusinessComponent businessComponent, ILogger<InfoController> logger)
When I call now that endpoint, I get immediately a 500 response.
When I erase the arguments from the controller, than the process is going into the constructor and controller. But that's not what I want.
public InfoController()
Why is the constructor not getting the dependency injection or why is the constructor not called?
public class Program
{
#region fields and propetries
public IConfiguration Configuration { get; }
//# if DEBUG
//#endif
public static IConnection Connection { get; set; }
public static ITimeSeriesBusinessComponent TimeSeriesBusinessComponent { get; set; }
public static IInfoBusinessComponent InfoBusinessComponent { get; set; }
private static int counter;
#endregion fields and propetries
public static void Main(string[] args)
{
IConfiguration config = GetConfigurations();
ILogger logger = GetLogger();
ServiceProvider appServiceProvider = GetServiceProvider(config);
Parallel.Invoke
(
() =>
{
BuildWebHost(args).Build().Run();
},
() =>
{
//...
}
);
}
private static IConfiguration GetConfigurations()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json");
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", true, true)
.Build();
return config;
}
private static ILogger GetLogger()
{
ILogger logger = new LoggerFactory().AddNLog().CreateLogger<Program>();
return logger;
}
private static ServiceProvider GetServiceProvider(IConfiguration config)
{
var collection = new ServiceCollection();
collection.AddLogging(configuration => configuration.AddNLog());
//...
collection.AddScoped<IInfoRepository>(serviceProvider =>
{
return new InfoRepository(
config["ConnectionStrings:MainConnection"],
config["ConnectionStrings:MetaDataConnection"],
config["InfoFunctionName"],
config["UserName"],
config["Password"],
config["VirtualHost"],
config["ConnectionHostName"]);
});
collection.AddScoped<IInfoBusinessComponent, InfoBusinessComponent>();
var appServiceProvider = collection.BuildServiceProvider();
return appServiceProvider;
}
public static IWebHostBuilder BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseApplicationInsights()
.UseUrls("http://0.0.0.0:5003")
.UseNLog();
}
Here the Startup.cs:
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Title = "My CLI"
});
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My CLI");
c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
c.RoutePrefix = string.Empty;
});
app.UseMvc();
}
}
The problem is that the endpoint you create with BuildWebHost uses its own instance of ServiceProvider. The instance of ServiceProvider that you create doesn't get into the pipeline.
Why: ServiceCollection doesn't use any kind of singleton registry, so it's not enough to register services through some instance of ServiceCollection and build some instance of ServiceProvider. You have to make the endpoint use your specific instance of ServiceCollection/ServiceProvider. Or you can copy your ServiceCollection into one that's used by the endpoint - that's how I'd solve it.
So, let's use a ServiceCollection to register your services (as it is now). Then, instead of doing collection.BuildServiceProvider(), let's use that ServiceCollection in the Startup, to copy all registrations into the service collection used by the pipeline.
First, let's expose your ServiceCollection to be accessible from Startup:
class Program
{
public static ServiceCollection AppServices { get; set; }
public static void Main(string[] args)
{
// ...other stuff...
AppServices = GetServiceCollection(config);
// ...other stuff...
}
// renamed from GetServiceProvider
private static ServiceCollection GetServiceCollection(IConfiguration config)
{
var collection = new ServiceCollection();
// ... register services...
return collection;
}
}
Then in the Startup class, use Program.AppServices in ConfigureServices() as follows:
EDIT: pay attention to the usings in Startup.cs
// make sure these usings are present:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
....
public class Startup
{
// ... other members ...
public void ConfigureServices(IServiceCollection services)
{
// ... the usual stuff like services.AddMvc()...
// add this line:
services.TryAdd(Program.AppServices);
}
// ... other members ...
}
I am developing asp.net core 2.0 webapi and want a background task to process message from kafka message bus. I read some document realted to IHostedService and created a custom Background service. I am implementing CQRS with MediatR.
I have registered the MediatR module in Autofac. I need the Meditatr object to be available in the custom Hosted service. Can anyone please help me how to achieve this?
autofac.json
{
"modules": [
{
"type": "Producer.Infrastructure.Modules.MediatRModule",
"properties": {
}
}
]
}
Autofac module:
namespace Producer.Infrastructure.Modules
{
using Autofac;
using Autofac.Features.Variance;
using Producer.Application.Commands.Blogs;
using MediatR;
using System.Collections.Generic;
using System.Reflection;
public class MediatRModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterSource(new ContravariantRegistrationSource());
builder
.RegisterType<Mediator>()
.As<IMediator>()
.InstancePerLifetimeScope();
builder
.Register<SingleInstanceFactory>(ctx => {
var c = ctx.Resolve<IComponentContext>();
return t => { object o; return c.TryResolve(t, out o) ? o : null; };
})
.InstancePerLifetimeScope();
builder
.Register<MultiInstanceFactory>(ctx => {
var c = ctx.Resolve<IComponentContext>();
return t => (IEnumerable<object>)c.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
})
.InstancePerLifetimeScope();
builder.RegisterAssemblyTypes(typeof(CreateBlogCommand).GetTypeInfo().Assembly).AsImplementedInterfaces(); // via assembly scan
}
}
}
program.cs
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args)
{
return WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureAppConfiguration((builderContext, config) =>
{
IHostingEnvironment env = builderContext.HostingEnvironment;
config.AddJsonFile("autofac.json");
})
.ConfigureServices(services => services.AddAutofac())
.Build();
}
}
Startup.cs
IServiceProvider serviceProvider;
public IServiceProvider ConfigureServices(IServiceCollection services)
{
var brokerList = Configuration.GetSection("Kafka").GetValue<string>("BrokerList");
var topic = Configuration.GetSection("Kafka").GetValue<string>("Topic");
//Add framework services
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
services.AddSingleton<IHostedService>(s => new BackgroundService(brokerList, topic));
// Create an Autofac Container and push the framework services
var containerBuilder = new ContainerBuilder();
containerBuilder.Populate(services);
//Register your own services within Autofac
containerBuilder.RegisterModule(new ConfigurationModule(Configuration));
var container = containerBuilder.Build();
serviceProvider = container.Resolve<IServiceProvider>();
return serviceProvider;
}
Background service
public class BackgroundService : HostedService
{
public readonly string brokerList;
public readonly string topic;
public BackgroundService(string brokerList, string topic)
{
this.brokerList = brokerList;
this.topic = topic;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
//I need to access the Mediatr here???
}
}
}
Thanks
There are lots of articles talking about how to use Structure Map with ASP.NET Core, but not very many talking about console applications or windows services. The default behavior in ASP.Net Core is that StructureMap creates a Nested Container per HTTPRequest so that a concrete class will be instantiated only once per HTTP Request.
I am creating a .Net Core Windows Service using the PeterKottas.DotNetCore.WindowsService nuget package. I setup StructureMap using this article: https://andrewlock.net/using-dependency-injection-in-a-net-core-console-application/
My windows service is setup on a Timer and performs an action every X number of seconds. I want each of these actions to use a nested container similar to how ASP.NET does it. In other words, I want everything created for polling pass #1 to be disposed of once that polling pass completes. When polling pass #2 starts I want all new instances of objects to be instantiated. However, within the scope of a single polling pass I only want one instance of each object to be created.
What is the proper way to do this?
Here is my program class
public class Program
{
public static ILoggerFactory LoggerFactory;
public static IConfigurationRoot Configuration;
static void Main(string[] args)
{
var applicationBaseDirectory = AppContext.BaseDirectory;
string environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (string.IsNullOrWhiteSpace(environment))
throw new ArgumentNullException("Environment not found in ASPNETCORE_ENVIRONMENT");
ConfigureApplication(applicationBaseDirectory, environment);
var serviceCollection = ConfigureServices();
var serviceProvider = ConfigureIoC(serviceCollection);
ConfigureLogging(serviceProvider);
var logger = LoggerFactory.CreateLogger<Program>();
logger.LogInformation("Starting Application");
ServiceRunner<IWindowsService>.Run(config =>
{
var applicationName = typeof(Program).Namespace;
config.SetName($"{applicationName}");
config.SetDisplayName($"{applicationName}");
config.SetDescription($"Service that matches Previous Buyers to Vehicles In Inventory to Fine Upgrade Opportunities.");
config.Service(serviceConfig =>
{
serviceConfig.ServiceFactory((extraArgs, microServiceController) =>
{
return serviceProvider.GetService<IWindowsService>();
});
serviceConfig.OnStart((service, extraArgs) =>
{
logger.LogInformation($"Service {applicationName} started.");
service.Start();
});
serviceConfig.OnStop((service =>
{
logger.LogInformation($"Service {applicationName} stopped.");
service.Stop();
}));
serviceConfig.OnError(error =>
{
logger.LogError($"Service {applicationName} encountered an error with the following exception:\n {error.Message}");
});
});
});
}
private static void ConfigureApplication(string applicationBaseDirectory, string environment)
{
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
var builder = new ConfigurationBuilder()
.SetBasePath(applicationBaseDirectory)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environment}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
private static IServiceCollection ConfigureServices()
{
var serviceCollection = new ServiceCollection().AddLogging().AddOptions();
serviceCollection.AddDbContext<JandLReportingContext>(options => options.UseSqlServer(Configuration.GetConnectionString("JandLReporting")), ServiceLifetime.Transient);
//serviceCollection.AddDbContext<JLMIDBContext>(options => options.UseSqlServer(Configuration.GetConnectionString("JLMIDB")), ServiceLifetime.Scoped);
serviceCollection.Configure<TimerSettings>(Configuration.GetSection("TimerSettings"));
serviceCollection.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
return serviceCollection;
}
private static IServiceProvider ConfigureIoC(IServiceCollection serviceCollection)
{
//Setup StructureMap
var container = new Container();
container.Configure(config =>
{
config.Scan(scan =>
{
scan.AssemblyContainingType(typeof(Program));
scan.AssembliesFromApplicationBaseDirectory();
scan.AssembliesAndExecutablesFromApplicationBaseDirectory();
scan.WithDefaultConventions();
});
config.Populate(serviceCollection);
});
return container.GetInstance<IServiceProvider>();
}
private static void ConfigureLogging(IServiceProvider serviceProvider)
{
LoggerFactory = serviceProvider.GetService<ILoggerFactory>()
.AddConsole(Configuration.GetSection("Logging"))
.AddFile(Configuration.GetSection("Logging"))
.AddDebug();
}
}
Here is my WindowsService class:
public class WindowsService : MicroService, IWindowsService
{
private readonly ILogger _logger;
private readonly IServiceProvider _serviceProvider;
private readonly TimerSettings _timerSettings;
public WindowsService(ILogger<WindowsService> logger, IServiceProvider serviceProvider, IOptions<TimerSettings> timerSettings)
{
_logger = logger;
_serviceProvider = serviceProvider;
_timerSettings = timerSettings.Value;
}
public void Start()
{
StartBase();
Timers.Start("ServiceTimer", GetTimerInterval(), async () =>
{
await PollingPassAsyc();
},
(error) =>
{
_logger.LogCritical($"Exception while starting the service: {error}\n");
});
}
private async Task PollingPassAsyc()
{
using (var upgradeOpportunityService = _serviceProvider.GetService<IUpgradeOpportunityService>())
{
await upgradeOpportunityService.FindUpgradeOpportunitiesAsync();
}
}
private int GetTimerInterval()
{
return _timerSettings.IntervalMinutes * 60 * 1000;
}
public void Stop()
{
StopBase();
_logger.LogInformation($"Service has stopped");
}
}
There is extension method CreateScope for IServiceProvider in Microsoft.Extensions.DependencyInjection namespace. What it does is resolve special interface (IServiceScopeFactory) from current DI container, which is responsible for creating new scopes, and creates new scope using this factory. StructureMap registers implementation of this interface, so when you call CreateScope - StructureMap will create nested container. Sample usage:
using (var scope = _serviceProvider.CreateScope()) {
// use scope.ServiceProvider, not _serviceProvider to resolve instance
var service = scope.ServiceProvider.GetService<IUpgradeOpportunityService>();
}