如何在与应用程序类型无关的NTier Api中提供适当作用域的DbContext

本文关键字:DbContext 作用域 Api NTier 应用程序 类型 | 更新日期: 2023-09-27 18:09:01

我正在使用EF Code First编写一个类库,我希望在所有类型的应用程序中使用,包括web,控制台和桌面。目前,我的API仅作为web API存在,它使用标准工厂模式来返回作用域为HttpContext的DbContext实例。

方法调用看起来像这样:

EntityDbContextFactory<MyDbContext>.GetInstance();

和实现:

public class EntityDbContextFactory<TContext>
    where TContext : class, IDisposable, new()
{
    private static TContext _dbContext;
    public static TContext GetInstance()
    {
        TContext context;
        if (HttpContext.Current != null)
        {
            var objectContextKey = HttpContext.Current.GetHashCode().ToString("x") +
                                      typeof(TContext).GetHashCode().ToString(CultureInfo.InvariantCulture);
            if (!HttpContext.Current.Items.Contains(objectContextKey))
            {
                context = new TContext();
                HttpContext.Current.Items.Add(objectContextKey, context);
            }
            else
            {
                context = HttpContext.Current.Items[objectContextKey] as TContext;
            }
        }
        else
        {
            if (_dbContext == null)
            {
                _dbContext = new TContext();
                context = _dbContext;
            }
            else
            {
                context = _dbContext;
            }
        }

        return context;
    }
}

非常标准的东西,我在网上很多地方看到过。我需要手动提供这种作用域,因为我不能保证使用我的API的程序员使用的是DI容器,即使我可以,我也不希望我的存储库跟踪状态(通过将DbContext注入到存储库构造函数中,就像通常看到的那样),也不希望调用我的服务的人关心我的数据访问方法。

现在的问题是,我如何将其扩展到主机和桌面应用?如果我在这些应用程序中使用上述内容,我必须参考System。Web,这看起来一点也不合适。

标准的单例模式可以适用于桌面和控制台,但如果其中任何一个应用程序长时间打开,那么缓存在上下文中的实体很可能会变得陈旧。我当然不想为应用的生命周期做完全的单例方法,但如何界定它们呢?如果可能的话,我希望能够仍然有效地创建DbContexts(以及服务和存储库创建),而不是每次都创建新的。并且不依赖于外部库

如何在与应用程序类型无关的NTier Api中提供适当作用域的DbContext

这看起来很傻,但我几乎已经在这么做了。正如评论中的链接所提到的,你根本不希望在控制台或桌面应用中保持上下文存活,因为这些应用是有状态的,上下文可能很快就会过时。对于那些实例,你应该在每次调用时使用一个新实例。

然而,在没有状态的web应用程序中,将DbContext作用域设置为HttpContext (Per web Request)是适当的和常见的做法。每个请求可能包含多个服务调用,因此每个请求只使用一个dbcontext实例是有意义的。几乎可以把它想象成一个交易。微软本身就有很多例子证明了这一点。

然而,我的方法有一个额外的好处。您在网上看到的大多数使用这种方法的NTier示例都将dbcontext存储为存储库类的成员。这使得你的NTier API是有状态的,然后根据需要(因为dbcontext不是线程安全的),你必须将你的存储库和服务类限定为每个请求一个实例。但是使用我的方法,你可以将你的服务和存储库类限定为单例,这更有效。

网上常见的场景:

public class UserRepository
{
    private MyDbContext _myDbContext;
    public UserRepository(MyDbContext myDbContext)
    {
        _myDbContext = myDbContext;
    }
}

DbContext不是线程安全的,因此你的应用中的每一层(通常是DbContext -> repository -> service)都必须限定在你的DI容器中的每个http请求的范围内。

我的方法:

public class UserRepository<TbContext>
{
    private DbContext Context
    {
        get { return EntityContextFactory<TDbContext>.Current(); }
    }
}

在工厂类的问题中使用上述代码。现在我的服务和存储库可以是单例的,不管我在什么样的应用程序中使用我的API,它都会适当地处理dbcontext的生命周期。唯一需要改变的是,如果HttpContext是空的,返回一个新的实例,不要将其存储为私有成员:

public class EntityContextFactory<TContext>
    where TContext: class, IDisposable, new()
{
    private static TContext _dbContext;
    public static TContext Current()
    {
        TContext context = null;
        if (HttpContext.Current != null)
        {
            string objectContextKey = HttpContext.Current.GetHashCode().ToString("x") +
                                      typeof (TContext).GetHashCode().ToString(CultureInfo.InvariantCulture);
            if (HttpContext.Current.Items.Contains(objectContextKey) == false)
            {
                context = new TContext();
                HttpContext.Current.Items.Add(objectContextKey, context);
            }
            else
            {
                context = HttpContext.Current.Items[objectContextKey] as TContext;
            }
        }
        else
        {
                context = new TContext();
        }

        return context;
    }

}