Home > Software design >  Cannot access a disposed context instance with N-layer architecture
Cannot access a disposed context instance with N-layer architecture

Time:09-23

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 onl oadMoreNewsAsync(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?

CodePudding user response:

There are few problems in your code.

  1. 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  = onl oadCallBackAsync;
        _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.

  1. 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  = onl oadCallBack;
        _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
    }

    ...
}
  • Related