Cannot access a disposed object in Task.Run - c#

I am using .NET Core 3.1. I want to run some background processing without user having to wait for it to finish (it takes about 1 minute). Therefore, I used Task.Run like this:
public class MyController : Controller
{
private readonly IMyService _myService;
public MyController(IMyService myService)
{
_myService = myService;
}
public async Task<IActionResult> Create(...)
{
await _myService.CreatePostAsync(...);
return View();
}
}
public class MyService : IMyService
{
private readonly MyDbContext _dbContext;
private readonly IServiceScopeFactory _scopeFactory;
public MyService(MyDbContext dbContext, IServiceScopeFactory scopeFactory)
{
_dbContext = dbContext;
_scopeFactory = scopeFactory;
}
public async Task CreatePostAsync(Post post)
{
...
string username = GetUsername();
DbContextOptions<MyDbContext> dbOptions = GetDbOptions();
Task.Run(() => SaveFiles(username, dbOptions, _scopeFactory));
}
private void SaveFiles(string username, DbContextOptions<MyDbContext> dbOptions, IServiceScopeFactory scopeFactory)
{
using (var scope = scopeFactory.CreateScope())
{
var otherService = scope.ServiceProvider.GetRequiredService<IOtherService>();
var cntxt = new MyDbContext(dbOptions, username);
Post post = new Post("abc", username);
cntxt.Post.Add(post); <----- EXCEPTION
cntxt.SaveChanges();
}
}
}
I recieve the following exception in marked line:
System.ObjectDisposedException: 'Cannot access a disposed object. Object name: 'IServiceProvider'.'
Why does this happen? I used custom constructor (and not scope.ServiceProvider.GetRequiredService<MyDbContext>()) for MyDbContext because I need to save one additional propery (username) for later use in overriden methods.
public partial class MyDbContext
{
private string _username;
private readonly DbContextOptions<MyDbContext> _options;
public DbContextOptions<MyDbContext> DbOptions { get { return _options; } }
public MyDbContext(DbContextOptions<MyDbContext> options, string username) : base(options)
{
_username = username;
_options = options;
}
... other overriden methods
}
What am I doing wrong?

First of all, don't hide a thread-pool operation away in your service; let the calling coded decide whether to run the operation on the thread-pool or not:
As you are using dependency injection, the framework is disposing your DbContext at the end of the HTTP request.
You need to inject your service scope factory into your controller, and request the service from there:
public class MyController : Controller
{
private readonly IMyService _myService;
private readonly IServiceScopeFactory _scopeFactory;
public MyController(IMyService myService, IServiceScopeFactory scopeFactory)
{
_myService = myService;
_scopeFactory = scopeFactory;
}
public async Task<IActionResult> Create(...)
{
HostingEnvironment.QueueBackgroundWorkItem(SaveInBackground);
return View();
}
private async Task SaveInBackground(CancellationToken ct)
{
using (var scope = scopeFactory.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<IMyService>();
await scopedService.CreatePostAsync(...);
}
}
}
HostingEnvironment.QueueBackgroundWorkItem works in a similar way to Task.Run, except it ensures that the app doesn't shut down until all background work items have completed.
Your service would need to be something like this:
public class MyService : IMyService
{
private readonly MyDbContext _dbContext;
public MyService(MyDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task CreatePostAsync(Post post)
{
_dbContext.Post.Add(post);
await _dbContext.SaveChangesAsync();
}
}
UPDATE
To pass additional parameters to SaveInBackground:
private async Task SaveInBackground(YourParam param)
Then call like:
HostingEnvironment.QueueBackgroundWorkItem(cancellationToken => SaveInBackground(yourParam));

You shoud create a Service with a Singleton lifecycle and inject a DBContext inside and queue all tasks inside

Related

XUnit integration Test with IDbContextFactory for a Generic Data Service

tl;dr;
How can I inject the IDbContext factory in a Integration XUnit test?
I´m working on a Blazor Server project and I am creating a service that uses IDbContextFactory instead of the normal DbContext. The service uses EntityFrameworkCore to communicate with the DB. I need to create integration tests for this service that use the real test database, so I won´t Moq the factory.
This is the basic structure of my service.
public class CatalogService<T> : IEntityService<T> where T : CatalogBase
{
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
private readonly ILogger<T> _logger;
//The factory is injected via constructor
public CatalogService(IDbContextFactory<ApplicationDbContext> contextFactory, ILogger<T> logger)
{
_contextFactory = contextFactory;
_logger = logger;
}
//... All the functions
}
I also have a fixture where some seed data can be created
public class TestDatabaseFixture
{
private const string ConnectionString = #"my_connection";
private static readonly object _lock = new();
private static bool _databaseInitialized;
public TestDatabaseFixture()
{
lock (_lock)
{
if (!_databaseInitialized)
{
using (var context = CreateContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
//Create Seed Data for Brands
for (int i = 0; i < 100; i++) {
context.PcMarcas.Add(new PcBrand { Description = $"Brand {(i + 1).ToString("0000") }" });
}
context.SaveChanges();
}
_databaseInitialized = true;
}
}
}
public ApplicationDbContext CreateContext()
{
return new ApplicationDbContext(
new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(ConnectionString)
.Options);
}
public IDbContextFactory<ApplicationDbContext> CreateDbContextFactory() {
//How do I return the context factory? <---
}
}
I know how to moq my logger and create a normal DBContext, but I´m not sure how to inject the factory in the test
[Fact]
public async Task GetAsync_ShouldReturnAnItem()
{
//Setup ------------------------------------------------------
var contextFactory = Fixture.CreateDbContextFactory(); //from the fixture
var logger = Mock.Of<ILogger<PcMarca>>();
var service = new CatalogService<PcMarca>(contextFactory, logger);
//Act -------------------------------------------------------
//Assert ----------------------------------------------------
}
You'll need to implement a IDbContextFactory<ApplicationDbContext> yourself.
To reuse your existing code to create an ApplicationDbContext instance, you can choose to implement that IDbContextFactory<ApplicationDbContext> as an inner class of your TestDatabaseFixture.
For brevity below code doesn't include the constructor.
public class TestDatabaseFixture
{
public class ApplicationDbContextFactory : IDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext()
{
return CreateApplicationDbContext();
}
}
private const string ConnectionString = #"my_connection";
public ApplicationDbContext CreateContext()
{
return CreateApplicationDbContext();
}
public IDbContextFactory<ApplicationDbContext> CreateDbContextFactory()
{
return new ApplicationDbContextFactory();
}
private static ApplicationDbContext CreateApplicationDbContext()
{
return new ApplicationDbContext(
new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(ConnectionString)
.Options);
}
}

Cannot access a disposed context instance with N-layer architecture

I'm trying to make a N-layer architecture for my Telegram Bot. I created DAL, BLL and PL. I would like to add entity News to my DB. But I have some issue with my context.
My DB Context:
public class ApplicationContext : DbContext
{
public DbSet<News> News { get; set; }
public DbSet<User> Users { get; set; }
public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<News>().Property(tn => tn.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<User>().Property(tn => tn.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<News>().Property(tn => tn.Title).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Href).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Image).IsRequired();
modelBuilder.Entity<News>().Property(tn => tn.Date).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.UserId).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.UserName).IsRequired();
modelBuilder.Entity<User>().Property(tn => tn.DateOfStartSubscription).IsRequired();
base.OnModelCreating(modelBuilder);
}
}
Interface UoW:
public interface IUnitOfWork : IDisposable
{
INewsRepository News { get; }
IUserRepository Users { get; }
int Complete();
}
Class UoW:
public class UnitOfWork : IUnitOfWork
{
public IUserRepository Users { get; }
public INewsRepository News { get; }
private readonly ApplicationContext _context;
public UnitOfWork(ApplicationContext context)
{
_context = context;
Users = new UserRepository.UserRepository(_context);
News = new NewsRepository.NewsRepository(_context);
}
public int Complete() => _context.SaveChanges();
public void Dispose() => _context.Dispose();
}
My DAL Generic Repository:
async Task IGenericRepository<T>.AddAsync(T entity) => await _context.Set<T>().AddAsync(entity);
DAL Injection:
public static class DALInjection
{
public static void Injection(IServiceCollection services)
{
services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddTransient<IUserRepository, UserRepository.UserRepository>();
services.AddTransient<INewsRepository, NewsRepository.NewsRepository>();
services.AddTransient<IUnitOfWork, UnitOfWork.UnitOfWork>();
}
}
My BLL Service class:
public class ParserService : IParser
{
private IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public ParserService(IUnitOfWork unitOfWork, IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
private async Task SaveArticles(IEnumerable<NewsDTO> articlesDTO)
{
var articles = _mapper.Map<IEnumerable<NewsDTO>, IEnumerable<News>>(articlesDTO);
await _unitOfWork.News.AddAsync(articles.First());
_unitOfWork.Complete();
}
BLL Injection:
public static class BLLInjection
{
public static void Injection(IServiceCollection services)
{
DALInjection.Injection(services);
services.AddTransient<IParser, ParserService>();
services.AddTransient<IArticleService, ArticleService>();
services.AddAutoMapper(typeof(CommonMappingProfile));
}
}
My PL:
private static async Task SendArticleAsync(long chatId, int offset, int count)
{
var articles = await _parser.MakeHtmlRequest(offset, count);
foreach (var article in articles)
{
var linkButton = KeyboardGoOver("Перейти", article.Href);
await _client.SendPhotoAsync(chatId: chatId, photo: article.Image,
caption: $"*{article.Title}*", parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown, replyMarkup: linkButton);
}
await OnLoadMoreNewsAsync(chatId, offset + count, count);
}
PL Startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ApplicationContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"),
b => b.MigrationsAssembly(typeof(ApplicationContext).Assembly.FullName)));
BLLInjection.Injection(services);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "TelegramBot.WebApi", Version = "v1" });
});
}
When I tried to debug, I had this error but I could not resolve this issue.
_context = Database = {"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 o...
Could someone help me with this issue?
There are few problems in your code.
Controllers are scoped entities, their instances created per http request and disposed after request is finished. It means controller is not good place to subscribe to events. When you call /start endpoint you create an instance of TelegramController and TelegramBotClient, but once the request is finished, the controller and all its non-singleton dependencies (IParser in your case) are disposed. But you subscribed for TelegramBotClient events that captured reference to IParser. It means all events that will arrive after request is finished will try to access disposed IParser instance and this is the reason for your exception.
For event based messages it's better to use IHostedService. You will need to use IServiceScopeFactory to create a scope for each message and resolve your dependencies from this scope.
public class TelegramHostedService : IHostedService
{
private IServiceScopeFactory _scopeFactory;
public TimedHostedService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_client = new TelegramBotClient(_token);
_client.OnMessage += OnMessageHandlerAsync;
_client.OnCallbackQuery += OnLoadCallBackAsync;
_client.StartReceiving();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken stoppingToken)
{
// TODO: Unsubscribe from events
return Task.CompletedTask;
}
public static async void OnMessageHandlerAsync(object sender, MessageEventArgs e)
{
using var scope = _scopeFactory.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<MessageHandler>();
await handler.Handle(TODO: pass required args); // Move the logic to separate handler class to keep hosted service clean
}
...
}
I moved call to _client.StartReceiving(); after event subscription otherwise there is a chance for race condition when you receive event but you don't yet have subscribers and this event will be lost.
The second issue is as #PanagiotisKanavos said: async void can't be awaited, hence once your code hit first true async method (like DB access, http request, file read or any other I/O operation) the control is returned to the point where async void method was called and continues execution without waiting for operation completion. The whole app can even crash if you throw unhandled exception from such method, hence async void should be avoided. To prevent these problems wrap your async event handlers with sync methods that will block the execution with Wait() method:
public class TelegramHostedService : IHostedService
{
private IServiceScopeFactory _scopeFactory;
public TimedHostedService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_client = new TelegramBotClient(_token);
_client.OnMessage += OnMessageHandler;
_client.OnCallbackQuery += OnLoadCallBack;
_client.StartReceiving();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken stoppingToken)
{
// TODO: Unsubscribe from events
return Task.CompletedTask;
}
public static void OnMessageHandler(object sender, MessageEventArgs e)
{
OnMessageHandlerAsync(sender, e).Wait();
}
public static async Task OnMessageHandlerAsync(object sender, MessageEventArgs e)
{
using var scope = _scopeFactory.CreateScope();
var handler = scope.ServiceProvider.GetRequiredService<MessageHandler>();
await handler.Handle(TODO: pass required args); // Move the logic to separate handler class to keep hosted service clean
}
...
}

C# Dependency Injection : Injecting multiple interfaces into other services

I'd like to inject a number of interfaces to another service.
Let's take a look at 2 services that I want to have their dependency injected.
Inside Term.cs
private readonly IWSConfig WSConfig;
private readonly IMemoryCache MemCache;
public Term(IWSConfig wsConfig, IMemoryCache memoryCache)
{
WSConfig = wsConfig;
MemCache = memoryCache;
}
public async Task LoadData()
{
List<ConfigTerm> configTerm = await WSConfig.GetData(); // This is a web service call
...
}
Inside Person.cs
private readonly PersonRepo PersonRepository;
private readonly IMemoryCache MemCache;
private readonly ITerm Term;
private readonly IWSLoadLeave LoadLeave;
private readonly IWSLoadPartics LoadPartics;
public Person(PersonRepo personRepository, IMemoryCache memCache, ITerm term, IWSLoadLeave loadLeave, IWSLoadPartics loadPartics)
{
PersonRepository = personRepository;
MemCache = memCache;
Term = term;
LoadLeave = loadLeave;
LoadPartics = loadPartics;
}
Code in Startup.cs
services.AddDbContext<DBContext>(opts => opts.UseOracle(RegistryReader.GetRegistryValue(RegHive.HKEY_LOCAL_MACHINE, Configuration["AppSettings:RegPath"], "DB.ConnectionString", RegWindowsBit.Win64)));
services.AddTransient<ILogging<ServiceLog>, ServiceLogRepo>();
services.AddSingleton<IMemoryCache, MemoryCache>();
services.AddHttpClient<IWSConfig, WSConfig>();
services.AddHttpClient<IWSLoadLeave, WSLoadLeave>();
services.AddHttpClient<IWSLoadPartics, WSLoadPartics>();
var optionsBuilder = new DbContextOptionsBuilder<DBContext>(); // Can we omit this one and just use the one in AddDbContext?
optionsBuilder.UseOracle(RegistryReader.GetRegistryValue(RegHive.HKEY_LOCAL_MACHINE, Configuration["AppSettings:RegPath"], "DB.ConnectionString", RegWindowsBit.Win64));
services.AddSingleton<ITerm, Term>((ctx) => {
WSConfig wsConfig = new WSConfig(new System.Net.Http.HttpClient(), new ServiceLogRepo(new DBContext(optionsBuilder.Options))); // Can we change this to the IWSConfig and the ILogging<ServiceLog>
IMemoryCache memoryCache = ctx.GetService<IMemoryCache>();
return new Term(wsConfig, memoryCache);
});
services.AddSingleton<IPerson, Person>((ctx) => {
PersonRepo personRepo = new PersonRepo(new DBContext(optionsBuilder.Options)); // Can we change this?
IMemoryCache memoryCache = ctx.GetService<IMemoryCache>();
ITerm term = ctx.GetService<ITerm>();
WSLoadLeave loadLeave = new WSLoadLeave(new System.Net.Http.HttpClient(), new ServiceLogRepo(new DBContext(optionsBuilder.Options))); // Can we change this?
WSLoadPartics loadPartics = new WSLoadPartics(new System.Net.Http.HttpClient(), new ServiceLogRepo(new DBContext(optionsBuilder.Options))); // Can we change this?
return new Person(personRepo, memoryCache, term, loadLeave, loadPartics);
});
But there are some duplication here and there. I've marked as the comments in the code above.
How to correct it ?
[UPDATE 1]:
If I change the declaration from singleton with the following:
services.AddScoped<ITerm, Term>();
services.AddScoped<IPerson, Person>();
I'm getting the following error when trying to insert a record using the DbContext.
{System.ObjectDisposedException: Cannot access a disposed object. A
common cause of this error is disposing a context 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, or wrapping the context in a
using statement. If you are using dependency injection, you should let
the dependency injection container take care of disposing context
instances. Object name: 'DBContext'.
In my WSConfig, it will inherit a base class. This base class also have reference to the ServiceLogRepo, which will call the DbContext to insert a record to the database
In WSConfig
public class WSConfig : WSBase, IWSConfig
{
private HttpClient WSHttpClient;
public WSConfig(HttpClient httpClient, ILogging<ServiceLog> serviceLog) : base(serviceLog)
{
WSHttpClient = httpClient;
//...
}
//...
}
The WSBase class:
public class WSBase : WSCall
{
private readonly ILogging<ServiceLog> ServiceLog;
public WSBase(ILogging<ServiceLog> serviceLog) : base(serviceLog)
{
}
...
}
The WSCall class:
public class WSCall
{
private readonly ILogging<ServiceLog> ServiceLog;
public WSCall(ILogging<ServiceLog> serviceLog)
{
ServiceLog = serviceLog;
}
....
}
And the ServiceLogRepo code
public class ServiceLogRepo : ILogging<ServiceLog>
{
private readonly DBContext _context;
public ServiceLogRepo(DBContext context)
{
_context = context;
}
public async Task<bool> LogRequest(ServiceLog apiLogItem)
{
await _context.ServiceLogs.AddAsync(apiLogItem);
int i = await _context.SaveChangesAsync();
return await Task.Run(() => true);
}
}
I also have the following in Startup.cs to do the web service call upon application load.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, ITerm term)
{
....
System.Threading.Tasks.Task.Run(async () => await term.LoadData());
}
It seems when going into term.LoadData(), the DBContext is disposed already.
First properly register all the necessary dependencies in ConfigureServices using the appropriate liftetime scopes
services.AddDbContext<DBContext>(opts => opts.UseOracle(RegistryReader.GetRegistryValue(RegHive.HKEY_LOCAL_MACHINE, Configuration["AppSettings:RegPath"], "DB.ConnectionString", RegWindowsBit.Win64)));
services.AddTransient<ILogging<ServiceLog>, ServiceLogRepo>();
services.AddSingleton<IMemoryCache, MemoryCache>();
services.AddHttpClient<IWSConfig, WSConfig>();
services.AddHttpClient<IWSLoadLeave, WSLoadLeave>();
services.AddHttpClient<IWSLoadPartics, WSLoadPartics>();
services.AddScoped<ITerm, Term>();
services.AddScoped<IPerson, Person>();
Given the async nature of the method being called in Configure the DbContext is being disposed before you are done with it.
Now ideally given what you are trying to achieve you should be using a background service IHostedServive which will be started upon startup of the application.
public class TermHostedService : BackgroundService {
private readonly ILogger<TermHostedService> _logger;
public TermHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger) {
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
_logger.LogInformation("Term Hosted Service running.");
using (var scope = Services.CreateScope()) {
var term = scope.ServiceProvider.GetRequiredService<ITerm>();
await term.LoadData();
_logger.LogInformation("Data Loaded.");
}
}
public override async Task StopAsync(CancellationToken stoppingToken) {
_logger.LogInformation("Term Hosted Service is stopping.");
await Task.CompletedTask;
}
}
when registered at startup
services.AddHostedService<TermHostedService>();
Reference Background tasks with hosted services in ASP.NET Core

How is Mediatr wired?

Update:
The Mediatr in the project is used without any customized logic for dispatching the messages. Can I say it's used as an event aggregator?
In the source code of https://github.com/JasonGT/NorthwindTraders, the Controller gets the Mediator from ControllerBase.
[ApiController]
[Route("api/[controller]/[action]")]
public abstract class BaseController : ControllerBase
{
private IMediator _mediator;
protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
}
In the controller, it calls Mediator.Send(...) to send the message to the mediator.
public class EmployeesController : BaseController
{
// ....
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<EmployeeDetailVm>> Get(int id)
{
return Ok(await Mediator.Send(new GetEmployeeDetailQuery { Id = id }));
}
And the method Handle() in the inner class GetEmployeeDetailQuery.GetEmployeeDetailQueryHandler will be called for query message GetEmployeeDetailQuery. How is this wired?
public class GetEmployeeDetailQuery : IRequest<EmployeeDetailVm>
{
public int Id { get; set; }
public class GetEmployeeDetailQueryHandler : IRequestHandler<GetEmployeeDetailQuery, EmployeeDetailVm>
{
private readonly INorthwindDbContext _context;
private readonly IMapper _mapper;
public GetEmployeeDetailQueryHandler(INorthwindDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<EmployeeDetailVm> Handle(GetEmployeeDetailQuery request, CancellationToken cancellationToken)
{
var vm = await _context.Employees
.Where(e => e.EmployeeId == request.Id)
.ProjectTo<EmployeeDetailVm>(_mapper.ConfigurationProvider)
.SingleOrDefaultAsync(cancellationToken);
return vm;
}
}
}
In the startup.cs of that project, there's a call to AddApplication, which is an extension method from the NorthwindTraders.Application project, and is defined in DependencyInjection.cs. This calls services.AddMediatR(Assembly.GetExecutingAssembly());, which scans the assembly for handlers and registers them.
In general, you can register MediatR for your own projects by calling services.AddMediatr(Assembly.GetExecutingAssembly()) in your web application's Startup.ConfigureServices method.

.NET Core IServiceCollection Replace not refreshing

I have an Address object that depends on an IState object inject in the constructor:
public class AddressService : BaseService, IAddressService
{
private readonly IStateRepository _stateRepository;
private readonly ICacheClass _cache;
private readonly ILogger<AddressService> _logger;
public const string StatesCacheKey = "StateListManagedByStateService";
public AddressService(IStateRepository stateRepository,
ICacheClass cache,
ILogger<AddressService> logger,
IUserContext userContext) : base(userContext)
{
_stateRepository = stateRepository;
_cache = cache;
_logger = logger;
}
public async Task<IReadOnlyList<T>> GetAllStatesAsync<T>() where T : IState
{
var list = await _cache.GetAsync<IReadOnlyList<T>>(StatesCacheKey);
if (list != null) return list;
var repoList = _stateRepository.GetAll().Cast<T>().ToList();
_logger.LogInformation("GetAllStates retrieved from repository, not in cache.");
await _cache.SetAsync(StatesCacheKey, repoList);
return repoList;
}
}
IStateRepository is injected by the ASP.NET Core web project with the folling lines in Startup:
services.AddTransient<IStateRepository, Local.StateRepository>();
services.AddTransient<IAddressService, AddressService>();
In my Controller I have two action methods. Depending on the one called, I want to change the IStateRepository object associated with DI:
private readonly IAddressService _addressService;
public HomeController(IConfiguration configuration, ILogger<HomeController> logger, IAddressService addressService) : base(configuration,logger)
{
_addressService = addressService;
}
public async Task<IActionResult> DistributedCacheAsync()
{
var services = new ServiceCollection();
services.Replace(ServiceDescriptor.Transient<IStateRepository, Repositories.Local.StateRepository>());
ViewBag.StateList = await _addressService.GetAllStatesAsync<State>();
return View();
}
public async Task<IActionResult> DapperAsync()
{
var services = new ServiceCollection();
services.Replace(ServiceDescriptor.Transient<IStateRepository, Repositories.Dapper.StateRepository>());
ViewBag.StateList = await _addressService.GetAllStatesAsync<State>();
return View();
}
The problem is that _addressService already has the Local.StateRepository associated from Startup, and updating it in DapperAsync using Replace doesn't have an effect on it.
How would be able to change IStateRepository in DI during runtime in the example above?
Changing services at runtime is not supported by Microsoft.Extensions.DependencyInjection.
I recommend creating a wrapper service that at runtime can choose between different implementations based on a condition.

Categories