1. 程式人生 > >dotnet core在Task中使用依賴注入的Service/EFContext

dotnet core在Task中使用依賴注入的Service/EFContext

C#:在Task中使用依賴注入的Service/EFContext

dotnet core時代,依賴注入基本已經成為標配了,這就不多說了.

前幾天在做某個功能的時候遇到在Task中使用EF DbContext的問題,學藝不精的我被困擾了不短的一段時間,

於是有了這個文章.

先說一下程式碼結構和場景.

首先有一個HouseDbContext,程式碼大概是下面這樣:

public class HouseDbContext : DbContext
{
    public HouseDbContext(DbContextOptions<HouseDbContext> options)
        : base(options)
    {
    }
    public DbSet<Notice> Notices { get; set; }
}

接著已經在StarUp.cs中初始化並注入了,注入程式碼是這樣的:


services.AddDbContextPool<HouseDbContext>(options =>
{
    options.UseLoggerFactory(loggerFactory);
    options.UseMySql(Configuration["MySQLString"].ToString());
});

有一個NoticeService.cs 會用到HouseDbContext 進行增刪查改

public class NoticeService
{

    private readonly HouseDbContext _dataContext;

    public NoticeService(HouseDbContext dataContext)
    {
        _dataContext = dataContext;
    }

    public Notice FindNotice(long id)
    {
        var notice = _dataContext.Notices.FirstOrDefault(n =>n.Id == id);
        return notice;
    }
}

當然我們也需要把NoticeService注入到容器中,類似這樣


services.AddScoped<NoticeService,NoticeService>();

現在一切都是很美好的,也能正常查詢出Notice

然後某一天來了,有個需求是Update Notice之後需要把Notice同步到另外一個地方,例如Elasticsearch?

程式碼如下:


public class NoticeService
{

    private readonly HouseDbContext _dataContext;

    public NoticeService(HouseDbContext dataContext)
    {
        _dataContext = dataContext;
    }

    public Notice FindNotice(long id)
    {
        var notice = _dataContext.Notices.FirstOrDefault(n =>n.Id == id);
        return notice;
    }

    public void Save(Notice notice)
    {
        _dataContext.Notices.Add(notice);
        _dataContext.SaveChanges();
        Task.Run(() =>
        {
            try
            {
                var one = _dataContext.Notices.FirstOrDefault(n =>n.Id == notice.Id);
                // write to other
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        });
    }
}


然後一跑...

程式碼炸了...

恭喜你獲得跨執行緒使用EF 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: 'HouseDbContext'.

估計現在整個人都不好了.

這個撒意思呢?


無法訪問被釋放的物件。

這種錯誤的一個常見原因是使用從依賴注入中解決的上下文,然後在應用程式的其他地方嘗試使用相同的上下文例項。

如果您在上下文上呼叫Dispose(),或者在using語句中包裝上下文,可能會發生這種情況。如果使用依賴項注入,則應該讓依賴項注入容器處理上下文例項。

用人話來說是什麼意思呢?

這裡的HouseDbContext是依賴注入進來的,生命週期由容器本身管理;

在Task.Run中再次使用HouseDbContext例項中由於已經切換了執行緒了,

HouseDbContext例項已經被釋放掉了,無法再繼續使用同一個例項,我們應該自己初始化HouseDbContext來用.

到這裡的話,上次我做的時候心生一計:

既然我們不能直接從建構函式注入的HouseDbContext例項的話,我們是不是可以直接從依賴注入容器中拿一個例項回來呢?

那在dotnet core裡面可以用個什麼從容器中取出例項呢?

答案是:IServiceProvider

程式碼如下:


public class NoticeService
    {

        private readonly HouseDbContext _dataContext;

        private readonly IServiceProvider _serviceProvider;

        public NoticeService(HouseDbContext dataContext,IServiceProvider serviceProvider)
        {
            _dataContext = dataContext;
            _serviceProvider = serviceProvider;
        }

        public Notice FindNotice(long id)
        {
            var notice = _dataContext.Notices.FirstOrDefault(n => n.Id == id);
            Task.Run(() =>
            {
                try
                {
                    var context = _serviceProvider.GetService<HouseDbContext>();
                    var one = context.Notices.FirstOrDefault(n => n.Id == notice.Id);
                    Console.WriteLine(notice.Id);
                    // write to other
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            });
            return notice;
        }

        public void Save(Notice notice)
        {
            _dataContext.Notices.Add(notice);
            _dataContext.SaveChanges();
            Task.Run(() =>
            {
                try
                {
                    var one = _dataContext.Notices.FirstOrDefault(n => n.Id == notice.Id);
                    // write to other
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            });
        }
    }

跑一下看看...

然而事實告訴我,例項是能拿得到,然而還是會炸,錯誤是一樣的.

原因其實還是一樣的,這裡已經不受依賴注入託管了,人家的上下文你別想用了.

那咋辦呢...

在EF6,還可以直接new HouseDbContext 一個字串進去初始化,在EF Core這裡,已經不能這樣玩了.

那可咋辦呢?

翻了好多資料都沒看到有人介紹過咋辦,最後居然還是在官網教程裡面找到了樣例.

先看程式碼...

 Task.Run(() =>
{
    try
    {
        var optionsBuilder = new DbContextOptionsBuilder<HouseDbContext>();
        // appConfiguration.MySQLString appConfiguration是配置類,MySQLString為連線字串
        optionsBuilder.UseMySql(appConfiguration.MySQLString);
        using (var context = new HouseDbContext(optionsBuilder.Options))
        {
            var one = context.Notices.FirstOrDefault(n => n.Id == notice.Id);
            // 當然你也可以直接初始化其他的Service
            var nService = new NoticeService(context,null);
            var one =nService.FindOne(notice.Id);
        }

    }catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
});

教程程式碼在:Configuring a DbContext

大功告成...