StructureMap .Net Core Windows Service Nested Containers
Asked Answered
A

1

7

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");
    }
}
Averyaveryl answered 4/12, 2017 at 19:7 Comment(3)
Just call _serviceProvider.CreateScope() to create new scope and use that?Scheldt
Forgot to note that CreateScope is extension method in Microsoft.Extensions.DependencyInjection namespace. And after you create scope (using (var scope = _serviceProvider.CreateScope())) - use container of that scope to resolve dependencies: scope.ServiceProvider.GetService<IUpgradeOpportunityService>().Scheldt
@Scheldt that worked perfectly! Thank you. Can you please submit it as an answer?Averyaveryl
S
3

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>‌​();
}
Scheldt answered 21/12, 2017 at 13:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.