I'm working on a small reusable package för .NET Core applications that will help with automatic migrations on application startup.
It will basically run Database.Migrate() on every DbContext.
But the thing here is that I only want to run it on DbContexts that has been "marked" for auto-migration. I'm thinking that I could extend AddDbContext, and somehow tell the IServiceCollection to keep track of the particular DbContext. Something like this:
public static IServiceCollection AddDbContextWithMigration<TContext>(this IServiceCollection serviceCollection, Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext
{
//TODO: Somehow remember that this DbContext should be migrated.
return serviceCollection.AddDbContext<TContext, TContext>(optionsAction, contextLifetime, optionsLifetime);
}
Usage:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddDbContextWithMigration<DbContext1>();
services.AddDbContext<DbContext2>();
services.AddDbContextWithMigration<DbContext3>();
}
Then I thought that I either could use IStartupFilter or create an extensionmethod for IApplicationBuilder.
With extensionmethod:
public static IApplicationBuilder RunMigrations(this IApplicationBuilder app)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
var contexts = app.ApplicationServices.GetService();
foreach (DbContext context in contexts)
{
context.Database.Migrate();
}
return app;
}
With IStartupFilter:
public class MigrationsFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
var contexts = builder.ApplicationServices.GetService();
foreach (DbContext context in contexts)
{
context.Database.Migrate();
}
next(builder);
};
}
}
So I basically have two questions.
How can I keep track of which DbContexts that should be migrated?
Is this a "correct" usage of IStartupFilter?
I solved it by registering a wrapper-class (IContextMigrator) for my migrations.
First, my extension methods:
public static IApplicationBuilder RunMigrations(this IApplicationBuilder app)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
IEnumerable<IContextMigrator> migrations = app.ApplicationServices.GetServices<IContextMigrator>();
foreach (IContextMigrator migration in migrations)
{
migration.Migrate();
// Other logic for dispose...
}
return app;
}
public static IServiceCollection AddDbContextWithMigration<TContext>(this IServiceCollection serviceCollection, Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext
{
// By simply registering a transient of a wrapper-class I can resolve it later.
serviceCollection.AddTransient<IContextMigrator, ContextMigrator<TContext>>();
return serviceCollection.AddDbContext<TContext, TContext>(optionsAction, contextLifetime, optionsLifetime);
}
Then my class for actually migrating:
public interface IContextMigrator : IDisposable
{
void Migrate();
}
/// <summary>
/// Class responsible for migration of a DbContext.
/// Used by ApplicationBuilderExtensions to perform migrations.
/// </summary>
/// <typeparam name="T"></typeparam>
internal sealed class ContextMigrator<T> : IContextMigrator
where T : DbContext
{
private readonly T context;
public ContextMigrator(T context)
{
this.context = context;
}
public void Migrate()
{
try
{
this.context.Database.Migrate();
}
catch (Exception e)
{
throw new MigrationException(context.GetType().Name, e);
}
}
public void Dispose()
{
}
}
Related
I am trying to use dependency injection for a .Net Core Console application using the built in DI.
I have 2 following Methods :
private static void RegisterServices()
{
var collection = new ServiceCollection();
//repositories
collection.AddScoped<IAccountDataRepository, AccountDataRepository>();
collection.AddScoped<IClientDataRepository, ClientDataRepository>();
collection.AddScoped<IAddressDataRepository, AddressDataRepository>();
collection.AddScoped<IClientAccountDataRepository, ClientAccountDataRepository>();
//services
collection.AddScoped<IAccountDataService, AccountDataService>();
collection.AddScoped<IClientDataService, ClientDataService>();
collection.AddScoped<IAddressDataService, AddressDataService>();
collection.AddScoped<IClientAccountDataService, ClientAccountDataService>();
_serviceProvider = collection.BuildServiceProvider();
}
private static void DisposeServices()
{
if (_serviceProvider == null)
{
return;
}
if (_serviceProvider is IDisposable)
{
((IDisposable)_serviceProvider).Dispose();
}
}
I can get this to work in the main method by using this:
private static IServiceProvider _serviceProvider;
private static IClientDataRepository _clientDataRepository;
static void Main(string[] args)
{
RegisterServices();
_clientDataRepository = _serviceProvider.GetService<IClientDataRepository>();
However I need to inject the repository to the service and the service to main but I can t use the following in the service class :
_clientDataRepository = _serviceProvider.GetService<IClientDataRepository>();
Here is service:
public class ClientDataService : IClientDataService
{
private readonly ILogger _logger;
private readonly IClientDataRepository _clientDataRepository;
public ClientDataService(ILogger logger, IClientDataRepository clientDataRepository)
{
_logger = logger;
_clientDataRepository = clientDataRepository;
}
If I use
_clientDataRepository = _serviceProvider.GetService<IClientDataRepository>();
will give an error
Just resolve the service and the service provider will inject the repository into the service when building the object graph of the requested object
Based on the provided ClientDataService you would also need to make sure that all dependencies are registered with the service collection.
As it is current shown, ClientDataService also depends on ILogger which does not appear to be registered with the service collection.
services.AddLogging();
The following example uses the originally provided code and refactors to run the main using dependency injection.
public class Program
private readoonly IClientDataService service;
public Program(IClientDataService service) {
this.service = service;
}
public void SomeMethod() {
//...
}
//entry
public static void Main(string[] args) {
IServiceProvider serviceProvider = RegisterServices();
Program program = serviceProvider.GetService<Program>();
program.SomeMethod();
DisposeServices(serviceProvider);
}
//Support
private static IServiceProvider RegisterServices() {
var services = new ServiceCollection();
//repositories
services.AddScoped<IAccountDataRepository, AccountDataRepository>();
services.AddScoped<IClientDataRepository, ClientDataRepository>();
services.AddScoped<IAddressDataRepository, AddressDataRepository>();
services.AddScoped<IClientAccountDataRepository, ClientAccountDataRepository>();
//services
services.AddScoped<IAccountDataService, AccountDataService>();
services.AddScoped<IClientDataService, ClientDataService>();
services.AddScoped<IAddressDataService, AddressDataService>();
services.AddScoped<IClientAccountDataService, ClientAccountDataService>();
services.AddLogging(); //<-- LOGGING
//main
services.AddScoped<Program>(); //<-- NOTE THIS
return services.BuildServiceProvider();
}
private static void DisposeServices(IServiceProvider serviceProvider) {
if (serviceProvider == null) {
return;
}
if (serviceProvider is IDisposable sp) {
sp.Dispose();
}
}
}
I have created a basic DatabaseContext which handles the communication with a SQLite-Database:
public class DatabaseContext : DbContext
{
public DbSet<GearComponent> GearComponents{ get; set; }
public DatabaseContext() { }
public DatabaseContext(DbContextOptions options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Filename = database.db");
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<GearComponent>().HasKey(m => m.Id);
base.OnModelCreating(builder);
}
}
I registered this DatabaseContext in Startup.cs like this:
services.AddDbContext<DatabaseContext>(options => options.UseSqlite("Filename=database.db"));
I created a database with this command:
dotnet ef migrations add testMigration
I also auto-created a controller to access the database via HTTP-GET/POST/PUT.
This controller gets an instance of the DatabaseContext:
public class GearComponentsController : ControllerBase
{
private readonly DatabaseContext _context;
public GearComponentsController(DatabaseContext context)
{
_context = context;
}
//...
}
This GearComponentsController mainly is for the frontend to receive the database entries. For the backend I don't want to go with HTTP-POST/GET etc. but instead I want to directly access the DatabaseContext - but how?
I tried this in Program.cs:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
using (var db = new DatabaseContext())
{
db.GearComponents.Add(new GearComponent("Text", "Descr.", "08.12.2018"));
db.SaveChanges();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
But my database.db never receives this entry.
Edit: For everyone who is interested in how I got around this, look here.
Since you asked how to work with DatabaseContext outside your controller to perform some business logic. You can use straightforward approach with repository pattern. Will simply demonstrate for inserting data. Assuming you have model GearComponent created and EntityFramework already seted up.
Create file which contains interface and class which implements this interface:
public interface IGearComponentRepository
{
void Create(GearComponent obj)
}
public class GearComponentRepository : IGearComponentRepository
{
private readonly DatabaseContext _context;
public GearComponentRepository (DatabaseContext context)
{
_context = context;
}
public void Create(GearComponent data)
{
_context.Add(data);
_context.SaveChanges();
}
}
You need to register this service via IoC container in you Startup.cs file
public void ConfigureServices(IServiceCollection services)
{
..
services.AddMvc();
services.AddTransient<IGearComponentRepository, GearComponentRepository>();
..
}
Then you can use Repositories from your Controller
public class GearComponentsController : ControllerBase
{
private readonly IGearComponentRepository _gearComponentRepository;
public GearComponentsController(IGearComponentRepository
_gearComponentRepository)
{
_gearComponentRepository = gearComponentRepository;
}
[HttpPost]
public IActionResult Create(GearComponent data)
{
_dataRepository.Create(data);
return Ok();
}
}
in program.cs:
public static void Main(string[] args)
{
var hostBuilder = CreateWebHostBuilder(args);
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (!string.IsNullOrEmpty(env) && env.Equals("Production"))
{
hostBuilder.ConfigureLogging((context, builder) =>
{
// Read GelfLoggerOptions from appsettings.json
builder.Services.Configure<GelfLoggerOptions>(context.Configuration.GetSection("Graylog"));
// Optionally configure GelfLoggerOptions further.
builder.Services.PostConfigure<GelfLoggerOptions>(options =>
options.AdditionalFields["machine_name"] = Environment.MachineName);
// Read Logging settings from appsettings.json and add providers.
builder.AddConfiguration(context.Configuration.GetSection("Logging"))
.AddConsole()
//.AddDebug()
.AddGelf();
});
}
var host = hostBuilder.Build();
using (var scope = host.Services.CreateScope())
{
try
{
// Retrieve your DbContext isntance here
var dbContext = scope.ServiceProvider.GetRequiredService<NozomiDbContext>();
if (env != null && !env.Equals("Production"))
{
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
}
else
{
dbContext.Database.SetCommandTimeout((int)TimeSpan.FromMinutes(10).TotalSeconds);
dbContext.Database.Migrate();
}
// place your DB seeding code here
//DbSeeder.Seed(dbContext);
}
catch (Exception ex)
{
Console.WriteLine(ex);
// Continue
}
}
host.Run();
}
Focusing on:
using (var scope = host.Services.CreateScope())
and
var dbContext = scope.ServiceProvider.GetRequiredService<NozomiDbContext>();
You will be able to access your dbContext just like that.
As in the docs described, the method CreateWebhostbuilder is used to differ between build host and run host.
To run the host means, the code after is as reachable as after a throw statement.
Try this:
public class Program
{
public static void Main(string[] args)
{
//use var host to build the host
var host = CreateWebHostBuilder(args).Build();
using (var db = new DatabaseContext())
{
db.GearComponents.Add(new GearComponent("Text", "Descr.", "08.12.2018"));
db.SaveChanges();
}
// Run host after seeding
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Edit:
As far as I understand your issue now, you have a frontend, which should only consume the content of the database, if the refresh-button is clicked.
Along with that, your backend should consume other webservices and insert the consumed gearcomponents into the database.
In cause of the fact, that you don’t want another application do that job, which could be a windows-service (easy to handle intervals for updating the database),
the only way to trigger the Updates is to start them from the GearComponentsController or in an actionFilter. This way you can update your database and provide the data to the frontend. I hope, performance doesn’t matter.
Error: No parameterless constructor for AutoMapperConfiguration
I am using the nuget package automapper DI
public class AutoMapperConfiguration : Profile
{
private readonly ICloudStorage _cloudStorage;
public AutoMapperConfiguration(ICloudStorage cloudStorage)
{
_cloudStorage = cloudStorage;
// Do mapping here
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ICloudStorage, AzureStorage>();
services.AddAutoMapper(); // Errors here
}
How do I use the automapper DI with parameters?
I don't think you are able to add DI parameters to Profiles. Part of the logic behind this may be that these are only instantianted once, so services registered through AddTransient would not behave as expected.
One option would be to inject it into an ITypeConverter:
public class AutoMapperConfiguration : Profile
{
public AutoMapperConfiguration()
{
CreateMap<SourceModel, DestinationModel>().ConvertUsing<ExampleConverter>();
}
}
public class ExampleConverter : ITypeConverter<SourceModel, DestinationModel>
{
private readonly ICloudStorage _storage;
public ExampleCoverter(ICloudStorage storage)
{
// injected here
_storage = storage;
}
public DestinationModel Convert(SourceModel source, DestinationModel destination, ResolutionContext context)
{
// do conversion stuff
return new DestinationModel();
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ICloudStorage, AzureStorage>();
services.AddAutoMapper();
}
you may want to try this on your Startup.cs, if AddAutoMapper is an an extension that you built, then add the code below to your extension.
public void ConfigureServices(IServiceCollection services)
{
var mapperConfiguration = new MapperConfiguration(mc =>
{
IServiceProvider provider = services.BuildServiceProvider();
mc.AddProfile(new AutoMapperConfiguration (provider.GetService<ICloudStorage>()));
});
services.AddSingleton(mapperConfiguration.CreateMapper());
}
I found one solution to resolve this.
Create one list of types before add profile and pass it in the parameter.
public class AutoMapperConfiguration : Profile
{
private readonly ICloudStorage _cloudStorage;
public AutoMapperConfiguration(ICloudStorage cloudStorage)
{
_cloudStorage = cloudStorage;
// Do mapping here
}
}
public void ConfigureServices(IServiceCollection services)
{
var types = new List<Type>();
services.AddSingleton<ICloudStorage, AzureStorage>();
services.AddAutoMapper((provider, cfg) =>
{
var storage = new AutoMapperConfiguration(provider.GetService<ICloudStorage>());
types.Add(storage.GetType());
cfg.AddProfile(storage);
//others profiles
}, types);
}
In one of our app I am already using the dependency injection of AppTenant class like follows
public void ConfigureServices(IServiceCollection services)
{
services.AddMultitenancy<AppTenant, CachingAppTenantResolver>();
services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMultitenancy<AppTenant>();
}
and in controller i am able to access it easily as follows
public AccountController(AppTenant tenant)
{
this.tenant = tenant;
}
Now, I want to access the same AppTenant OR HttpContext in other project class in the same solution.
So, I have tried like this
public SqlStringLocalizerFactory(
AppTenant tenant)
{
_tenant = tenant;
}
But it is coming null, so what I need to do, to get the AppTenant OR HttpContext in the other project class ?
For SqlStringLocalizerFactory class the services are written in ConfigureServices method like follows
public static class SqlLocalizationServiceCollectionExtensions
{
public static IServiceCollection AddSqlLocalization(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
return AddSqlLocalization(services, setupAction: null);
}
public static IServiceCollection AddSqlLocalization(
this IServiceCollection services,
Action<SqlLocalizationOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAdd(new ServiceDescriptor(
typeof(IStringExtendedLocalizerFactory),
typeof(SqlStringLocalizerFactory),
ServiceLifetime.Singleton));
services.TryAdd(new ServiceDescriptor(
typeof(IStringLocalizerFactory),
typeof(SqlStringLocalizerFactory),
ServiceLifetime.Singleton));
services.TryAdd(new ServiceDescriptor(
typeof(IStringLocalizer),
typeof(SqlStringLocalizer),
ServiceLifetime.Singleton));
if (setupAction != null)
{
services.Configure(setupAction);
}
return services;
}
}
I have even tried with IHttpContextAccessor, but still not getting any success.
Any help on this appreciated !
Edit-2
New Solution:
public SqlStringLocalizerFactory(IHttpContextAccessor _accessor)
{
_accessor= accessor;
}
public void SomeMethod()
{
var tenant = _accessor.HttpContext.RequestServices
.GetRequiredService<AppTenant>();
}
Edit : IServiceProvider way doesn't work as i expect. See #Sock's solution
First, i assumes the problem occurs because of captive dependency as pointed by #qujck. To avoid captive dependency:
If lifetime of SqlStringLocalizerFactory must be singleton(some cases must be), in this
case use IServiceProvider:
public SqlStringLocalizerFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void SomeMethod()
{
var tenant = _serviceProvider.GetService<AppTenant>();
}
Otherwise using AddScoped seems reasonable for your case.
You have two options, the best option, if the SqlStringLocalizerFactory can be a scoped dependency (you get a new instance for every request) then you can register it as a scoped dependency:
services.TryAdd(new ServiceDescriptor(
typeof(IStringLocalizerFactory),
typeof(SqlStringLocalizerFactory),
ServiceLifetime.Scoped));
If the SqlStringLocalizerFactory must be a a Singleton dependency, then you need to make sure you resolve a scoped dependency for the Tenant by using a ServiceSope:
public class SqlStringLocalizerFactory
{
private readonly IServiceProvider _serviceProvider;
public SqlStringLocalizerFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void SomeMethod()
{
using (var serviceScope = _serviceProvider
.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var tenant = serviceScope.ServiceProvider.GetService<AppTenant>();
// do something with tenant...
}
}
}
i'd like to create a Plugin Enviroment for my ASP.Net 5.0 / MVC 6 Application. I'm using Autofac as IOC Container and i like to load the Plugins (Class Libraries) from the build in DNX LibraryManager. The goal of using the Library Manager is, that i don't have to care about NuGet Packages and Frameworks.
The Problem i have is the LifeCycle, i have to build the IOC Container before the instance of the LibraryManager is available. Because the Autofac Container provides his own IServiceProvider Instance which i have to inject within the ConfigureService() Method call (AddAutofac).
Does anyone know how to get this working?
Update: I have fixed my problem with Davids help and updated the code to get it working with the release candidates. Also i have added support for configuration.
In my DNX Class Library i implemented a Class for Self-Registration:
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.Register(c => new SimpleService())
.As<IService>()
.InstancePerLifetimeScope();
}
}
In my MVC WebApplication i have added the Class Library as Dependency.
Startup.cs
public IConfiguration Configuration { get; set; }
public class Startup
{
public Startup( IApplicationEnvironment applicationEnvironment )
{
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.SetBasePath( applicationEnvironment.ApplicationBasePath );
configurationBuilder.AddJsonFile( "appsettings.json" );
configurationBuilder.AddJsonFile( "autofac.json" );
configurationBuilder.AddEnvironmentVariables();
this.Configuration = configurationBuilder.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDependencies();
}
public void Configure(IApplicationBuilder applicationBuilder, IHostingEnvironment hostingEnvironment)
{
applicationBuilder.UseDependencies( this.Configuration );
applicationBuilder.UseStaticFiles();
applicationBuilder.UseMvc();
}
}
I have created an DependencyResolver to keep the ContainerBuilder instance.
DependencyResolver.cs
public class DependencyResolver : IDependencyResolver
{
private IContainer container;
private readonly ContainerBuilder builder;
public DependencyResolver()
{
this.builder = new ContainerBuilder();
}
public void RegisterModule( IModule module )
{
this.builder.RegisterModule( module );
}
public void RegisterModules( IEnumerable<Assembly> assemblies )
{
this.builder.RegisterAssemblyModules(assemblies.ToArray());
}
public void Populate( IServiceCollection services)
{
this.builder.Populate( services );
}
public void Build()
{
this.container = this.builder.Build();
}
public T Resolve<T>() where T : class
{
return this.container?.Resolve<T>();
}
}
IDependencyResolver.cs
public interface IDependencyResolver
{
void RegisterModule( IModule module );
void RegisterModules( IEnumerable<Assembly> assemblies );
void Populate(IServiceCollection services);
void Build();
T Resolve<T>() where T : class;
}
Last but not least i have created an Extension Class
DependencyResolverExtensions.cs
public static class DependencyResolverExtensions
{
public static IServiceCollection AddDependencies( this IServiceCollection services )
{
DependencyResolver dependencyResolver = new DependencyResolver();
dependencyResolver.Populate(services);
ServiceDescriptor serviceDescriptor = new ServiceDescriptor(typeof ( IDependencyResolver ), dependencyResolver );
services.TryAdd(serviceDescriptor);
return services;
}
public static IApplicationBuilder UseDependencies(this IApplicationBuilder applicationBuilder, IConfiguration configuration)
{
IDependencyResolver dependencyResolver = applicationBuilder.GetService<IDependencyResolver>();
if (dependencyResolver == null) return applicationBuilder;
ILibraryManager libraryManager = applicationBuilder.GetService<ILibraryManager>();
if (libraryManager == null) return applicationBuilder;
IEnumerable<Assembly> assemblies = libraryManager.GetLoadableAssemblies();
dependencyResolver.RegisterModules(assemblies);
ConfigurationModule configurationModule = new ConfigurationModule( configuration );
dependencyResolver.RegisterModule( configurationModule );
dependencyResolver.Build();
IServiceProvider serviceProvider = dependencyResolver.Resolve<IServiceProvider>();
applicationBuilder.ApplicationServices = serviceProvider;
return applicationBuilder;
}
public static IEnumerable<Assembly> GetLoadableAssemblies(this ILibraryManager libraryManager)
{
List<Assembly> result = new List<Assembly>();
IEnumerable<Library> libraries = libraryManager.GetLibraries();
IEnumerable<AssemblyName> assemblyNames = libraries.SelectMany(e => e.Assemblies).Distinct();
assemblyNames = Enumerable.Where(assemblyNames, e => e.Name.StartsWith("MyLib."));
foreach (AssemblyName assemblyName in assemblyNames)
{
Assembly assembly = Assembly.Load(assemblyName);
result.Add(assembly);
}
return result;
}
public static T GetService<T>(this IApplicationBuilder applicationBuilder) where T : class
{
return applicationBuilder.ApplicationServices.GetService(typeof (T)) as T;
}
}
If you need to switch between different implementations, like mock and real data you can use the Autofac Configuration.
autofac.json
{
"components": [
{
"type": "MyLib.Data.EF.EntitiesData, MyLib.Data.EF",
"services": [
{
"type": "MyLib.Abstractions.IDataRepository, MyLib.Abstractions"
}
]
}
]
}
It's a shame that ConfigureServices is not injectable, that would make this a lot easier.
Looking at the code you should be safe to replace the IServiceProvider inside Configure(...) instead of inside ConfigureServices(...) and get the intended behavior. ApplicationServices is setable.
In your UseAutofac method you should be able to do something like:
public static IApplicationBuilder UseAutofac( [NotNull] this IApplicationBuilder applicationBuilder )
{
IAutofacResolver autofacResolver = applicationBuilder.GetService<IAutofacResolver>();
ILibraryManager libraryManager = applicationBuilder.GetService<ILibraryManager>();
autofacResolver.RegisterLibraryModules( libraryManager);
applicationBuilder.ApplicationServices = autofacResolver.Resolve();
return applicationBuilder;
}
I've come up with a solution that uses part of this, but also uses a ComponentContainer that addresses the potential memory leaks in the DependencyResolver. This also works with RC1. Not sure yet about RC2 as it's not complete enough for me to test.
The ComponentContainer looks like this:
public static class ComponentContainer {
static IContainer _container;
static ContainerBuilder _containerBuilder;
public static ContainerBuilder Builder {
get {
if (_containerBuilder == null)
_containerBuilder = new ContainerBuilder();
return _containerBuilder;
}
}
public static IServiceProvider ServiceProvider {
get {
if (_container == null)
_container = _containerBuilder.Build();
return _container.Resolve<IServiceProvider>();
}
}
public static ComponentFactory<TObject> Component<TObject>() => new ComponentFactory<TObject>(_container);
public static void RegisterAssembly(Assembly assembly) {
if (assembly == null) return;
foreach (var obj in assembly.GetTypes().Where(t => t.GetCustomAttribute<ExportAttribute>() != null)) {
ExportAttribute att = obj.GetCustomAttribute<ExportAttribute>();
if (att.ContractType != null) {
_containerBuilder.RegisterType(obj).As(att.ContractType);
} else {
foreach (var intf in obj.GetInterfaces())
_containerBuilder.RegisterType(obj).As(intf);
}
}
}
}
public class ComponentFactory<TObject> : IDisposable {
protected TObject CurrentObject;
protected ILifetimeScope CurrentScope;
public TObject Current => (TObject)CurrentObject;
public ComponentFactory(IContainer container) {
CurrentScope = container.BeginLifetimeScope();
CurrentObject = CurrentScope.Resolve<TObject>();
}
public TObject Component => CurrentObject;
public void Dispose() {
(CurrentObject as IDisposable)?.Dispose();
CurrentScope.Dispose();
}
}
Then in Startup.cs I do the following:
public virtual IServiceProvider ConfigureServices(IServiceCollection services) {
services.AddMvc();
services.AddOptions();
services.AddSession();
services.AddCaching();
var assemblyLoadContextAccessor = services.FirstOrDefault(s => s.ServiceType == typeof(IAssemblyLoadContextAccessor)).ImplementationInstance as IAssemblyLoadContextAccessor;
var libraryManager = services.FirstOrDefault(s => s.ServiceType == typeof(ILibraryManager)).ImplementationInstance as ILibraryManager;
var loadContext = assemblyLoadContextAccessor.Default;
foreach(var library in libraryManager.GetLibraries()) {
var assembly = loadContext.Load(library.Name);
if(assembly != null) {
var module = assembly.GetTypes().FirstOrDefault(t => t == typeof(IModule));
if(module != null)
ComponentContainer.Builder.RegisterAssemblyModules(assembly);
else
ComponentContainer.RegisterAssembly(assembly);
}
}
ComponentContainer.Builder.Populate(services);
return ComponentContainer.ServiceProvider;
}
To export modules within an assembly, I either mark them with an ExportAttribute or add a class to the assembly that implements Autofac's IModule. The code in ConfigureServices will enumerate through the application's modules and feed them to the static Builder in ComponentContainer. Once the container has been built, you can either resolve modules through injection in a constructor or you can request a specific type by:
(using var myComponentFactory = ComponentContainer.Component<IMyModule>()) {
//You can now access your component through myComponentFactory.Component
//Once it passes out of scope of using, it will be properly disposed of
//along with the scope from which it was created.
}
Edit: With the release of RC2, this code is no longer valid as the enumeration of assemblies and classes will fail. I haven't come up with a good solution yet. If anyone else has any suggestions for enumerating assemblies in RC2, please let me know.