Unity中的每次调用上下文(Web请求)单例模式

45
几天前,我在ASP.Net线程方面遇到了问题。我想要每个Web请求一个单例对象。我实际上需要这个来进行工作单元。我想要在每个Web请求中实例化一个工作单元,以便标识映射在整个请求期间有效。这样我就可以使用IoC将我的IUnitOfWork注入到我的存储库类中,而且我可以使用相同的实例来查询和更新我的实体。
由于我使用Unity,我错误地使用了PerThreadLifeTimeManager。我很快意识到ASP.Net线程模型不支持我想要实现的内容。基本上它使用一个线程池并回收线程,这意味着我每个线程只得到一个UnitOfWork!!然而,我想要的是每个Web请求一个工作单元。
通过一些搜索,我找到了这篇绝佳的文章。那正是我想要的,除了Unity部分,这相当容易实现。
以下是我对Unity的PerCallContextLifeTimeManager的实现:
public class PerCallContextLifeTimeManager : LifetimeManager
{
    private const string Key = "SingletonPerCallContext";

    public override object GetValue()
    {
        return CallContext.GetData(Key);
    }

    public override void SetValue(object newValue)
    {
        CallContext.SetData(Key, newValue);
    }

    public override void RemoveValue()
    {
    }
}

当然,我使用类似以下代码的方式将我的工作单元注册:

unityContainer
            .RegisterType<IUnitOfWork, MyDataContext>(
            new PerCallContextLifeTimeManager(),
            new InjectionConstructor());

希望它能为某人节省一些时间。


不错的解决方案。如果可以的话,我建议将其重命名为“CallContextLifetimeManager”,因为Web请求可能只是潜在应用之一。 - Derek Greer
没错,我已经更新了文本和代码以反映这一点。谢谢。 - Mehdi Khalili
使用PerResolveLifetimeManager有什么问题? - Sleeper Smith
这不是一个问题! - brumScouse
3
FYI,这不是该问题的“正确”答案/解决方案。在ASP.NET中,单个请求可以(而且在高负载情况下通常会)在线程之间跳转。当它这样做时,调用上下文没有被传递,只有HttpContext被迁移。如果你想在ASP.NET可靠地(在负载下)使用它,你需要将CallContext更改为HttpContext.Current.Items。 - Micah Zoltu
3个回答

25

方案不错,但每个 LifetimeManager 的实例应使用唯一的键而不是常量:

private string _key = string.Format("PerCallContextLifeTimeManager_{0}", Guid.NewGuid());
否则,如果您在PerCallContextLifeTimeManager中注册了多个对象,则它们共享同一个键来访问CallContext,您将无法获取期望的对象。
另外,值得实现RemoveValue以确保清理对象:
public override void RemoveValue()
{
     CallContext.FreeNamedDataSlot(_key);
}

-1 CallContext 在每个请求上都会被重新创建。在您的每个请求单例的不同实例之间,键不必是唯一的。 - Igor Zevaka
8
实际上,它必须像sixeyed所说的那样。如果您不指定唯一键,则所有对象都将注册在单个键下,会出现混乱的情况。 - Élodie Petit
-1 查看Steven Robbins的答案和Micah Zoltu在问题下的评论:在负载下,单个请求的CallContext未被保留。 - Frédéric

21

虽然将其称为 PerCallContextLifeTimeManager 可以接受,但我非常确定这不能被认为是一个安全的 ASP.Net Per-request LifeTimeManager。

如果 ASP.Net 进行线程转换,则通过 CallContext 带入新线程的 唯一内容 是当前 HttpContext,而在 CallContext 中存储的任何其他内容都将消失。这意味着在高负载下,上述代码可能会产生意想不到的结果 - 我想追踪问题会非常麻烦!

唯一“安全”的方法是使用 HttpContext.Current.Items,或执行类似以下操作:

public class PerCallContextOrRequestLifeTimeManager : LifetimeManager
{
    private string _key = string.Format("PerCallContextOrRequestLifeTimeManager_{0}", Guid.NewGuid());

    public override object GetValue()
    {
      if(HttpContext.Current != null)
        return GetFromHttpContext();
      else
        return GetFromCallContext();
    }

    public override void SetValue(object newValue)
    {
      if(HttpContext.Current != null)
        return SetInHttpContext();
      else
        return SetInCallContext();
    }

    public override void RemoveValue()
    {
    }
}

显然这意味着要依赖于System.Web :-(

更多信息,请参见:

http://piers7.blogspot.com/2005/11/threadstatic-callcontext-and_02.html


4

感谢您的贡献,

但是所提出的实现方案存在两个缺点,其中一个是严重的错误,如Steven Robbins在他的答案和Micah Zoltu在评论中已经指出。

  1. Asp.Net不能保证为单个请求保留调用上下文。在负载下,它可能会切换到另一个上下文,导致所提出的实现方案失败。
  2. 它不能处理在请求结束时释放依赖项。

目前,Unity.Mvc Nuget软件包提供了一个PerRequestLifetimeManager来完成这项工作。不要忘记在引导代码中注册其关联的UnityPerRequestHttpModule,否则也无法处理依赖项的释放。

使用引导

DynamicModuleUtility.RegisterModule(typeof(UnityPerRequestHttpModule));

或在web.config文件的system.webServer/modules节点中。
<add name="UnityPerRequestHttpModule" type="Microsoft.Practices.Unity.Mvc.UnityPerRequestHttpModule, Microsoft.Practices.Unity.Mvc" preCondition="managedHandler" />

看起来它当前的实现也适用于Web表单。而且它甚至不依赖于MVC。不幸的是,由于它包含一些其他类,所以它的程序集就有了依赖。

请注意,在使用已解析的依赖项的自定义http模块时,它们可能已经在模块EndRequest中被释放。这取决于模块执行顺序。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接