Не удается получить доступ к удаленному экземпляру контекста с архитектурой N-уровня

#c# #asp.net-core #entity-framework-core

Вопрос:

Я пытаюсь создать N-слойную архитектуру для своего Телеграмм-бота. Я создал DAL, BLL и PL. Я хотел бы добавить новости сущностей в свою базу данных. Но у меня есть некоторые проблемы с моим контекстом.

Мой контекст БД:

 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);
        }
    }
 

Интерфейс UoW:

 public interface IUnitOfWork : IDisposable
    {
        INewsRepository News { get; }
        IUserRepository Users { get; }
        int Complete();
    }
 

Класс 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();
    }
 

Мой универсальный репозиторий DAL:

 async Task IGenericRepository<T>.AddAsync(T entity) => await _context.Set<T>().AddAsync(entity);
 

Инъекция ДАЛЯ:

  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));
        }
    }
 

Мой ПЛ:

    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:

 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" });
            });
        }
 

Когда я попытался выполнить отладку, у меня возникла эта ошибка, но я не смог решить эту проблему.

_context = База данных = {«Не удается получить доступ к удаленному экземпляру контекста. Распространенной причиной этой ошибки является удаление экземпляра контекста, который был устранен в результате внедрения зависимостей, а затем последующая попытка использовать тот же экземпляр контекста в другом месте приложения. Это может быть о…

Может ли кто-нибудь помочь мне с этим вопросом?

Комментарии:

1. Ошибка в коде, который вы не опубликовали. Для начала не используйте антипаттерн «универсальный репозиторий». DbContext уже является рабочей единицей, набор баз данных уже является хранилищем. ORM, подобные EF Core, уже абстрагируют механизм сохранения. Это AddAsync не делает того, что вы думаете, и в этом нет необходимости. В этом случае отсутствующий код каким-то образом пытался использовать одноэлементный экземпляр DbContext.

2. Начните с простого старого кода ядра EF вместо того, чтобы пытаться использовать «лучшие практики». Прямо сейчас никто не знает, что происходит, кроме как сказать, что классы «репозиторий» и «единица работы» не работают. Правильно настроенный DbContest работает просто отлично. Использование AddDbContext работает нормально. Если вы добавите свой DbContext в качестве зависимости к контроллеру, он будет работать нормально и сохранит изменения только при одном вызове SaveChanges в самом конце действия. Если вы этого не сделаете, все изменения будут отменены. Это У-у-У из коробки

3. Не могли бы вы объяснить, почему «универсальный репозиторий» является антипаттерном? Если у меня будет много репозиториев, это будет полезно для того, чтобы не повторять мой код. Какой код я мог бы опубликовать, который мог бы помочь вам понять мою проблему?

4. Зачем вам нужен хотя бы один репозиторий? Набор базы данных-это репозиторий. DbContext — это единица работы. Зачем вообще писать этот AddAsync метод? Что касается кода, который вам нужно опубликовать, это код, в котором хранится DbContext экземпляр _context . Очевидно, что вы используете одноэлементный экземпляр, когда вам не следует этого делать. Вероятно, вам потребуется опубликовать полный код UoW и код действия, потому что все, что делает этот код, не является стандартным или распространенным

5. Что касается того, что в целом не так, нет необходимости в репозиториях, и подразделение работы с ядром Entity Framework подробно объясняет это.

Ответ №1:

В вашем коде немного проблем.

  1. Контроллеры-это объекты с областью действия, их экземпляры создаются по http-запросу и удаляются после завершения запроса. Это означает, что контроллер-неподходящее место для подписки на события. Когда вы вызываете /start конечную точку , вы создаете экземпляр TelegramController и TelegramBotClient , но как только запрос завершен, контроллер и все его не одноэлементные зависимости ( IParser в вашем случае) удаляются. Но вы подписались на TelegramBotClient события, на которые есть ссылка IParser . Это означает, что все события, которые появятся после завершения запроса, будут пытаться получить доступ к удаленному IParser экземпляру, и это является причиной вашего исключения.
    Для сообщений на основе событий лучше использовать IHostedService. Вам нужно будет IServiceScopeFactory создать область для каждого сообщения и разрешить зависимости от этой области.
 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
    }

    ...
}
 

Я переехал _client.StartReceiving(); в подписку на звонок после события, в противном случае есть шанс на состояние гонки, когда вы получаете событие, но у вас еще нет подписчиков, и это событие будет потеряно.

  1. Второй вопрос-как @PanagiotisKanavos сказал: async void нельзя подождать, следовательно, как только ваш код нажмите первого истинного асинхронного метода (например, доступ к БД, HTTP-запросов, чтение файла или любой другой операции ввода-вывода) управление возвращается в точку, где async void метод был вызван и продолжает выполняться, не дожидаясь завершения операции. Все приложение может даже аварийно завершить работу, если вы создадите необработанное исключение из такого метода, следовательно async void , этого следует избегать. Чтобы предотвратить эти проблемы, оберните обработчики асинхронных событий методами синхронизации, которые заблокируют выполнение с Wait() помощью метода:
 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
    }

    ...
}