WCF中的IoC托管在Windows服务和每次调用的容器生存期中

时间:2018-04-19 11:27:34

标签: c# wcf unity-container soa simple-injector

我有一些客户端(网络和桌面应用)必须连接到一些服务,这些服务是Windows服务中托管的WCF服务。这些服务必须从一个或多个数据库获取数据,但是客户端确定从哪个数据库可以定义一个或多个来自相同类型的数据库。

我在服务中设置了datacontexts,存储库,业务经理和服务实现。 Datacontext由SimpleInjector容器注入(我也尝试使用Unity),当然,容器中的注册在创建ServiceHosts之前进行,ServiceHosts是在上下文模式Single(也是每个调用和每个会话尝试)中创建的。 / p>

我编写了IDispatchMessageInspector的实现,它将拦截所有SOAP消息并读取消息头,并根据消息头中的值设置datacontext的数据库连接字符串。 但是这会导致问题,因为它不是线程安全的,或者至少当一个调用还没有完成时,下一个调用可能会设置另一个连接字符串到同一个datacontext,搞乱一切。 / p>

所以,我试图按照呼叫(async,wcf lifestyle)注册,但因为这是一个Windows服务,它不会关闭,容器没有正确的范围。

在这种情况下我可以做些什么来使其发挥作用?

Case 1: no problem, cases 2 and 3: it must not interfere with each other

容器创建和服务主机启动:

var container = new Container();
//container.Options.DefaultLifestyle = new AsyncScopedLifestyle();
DIContainer.SetDI(container);

serviceHosts.Add(new ServiceHost(typeof(LoginService)));
serviceHosts.Add(new ServiceHost(typeof(IdentityService)));

foreach (var serviceHost in serviceHosts)
{
    serviceHost.Open();
}

注册DbContext:

container.RegisterInstance<CSI.AuthServices.DataAccess.EF.Interfaces.ISecurityContext>(new CSI.AuthServices.DataAccess.EF.SecurityContext());

用于读取SOAP消息头和设置数据库连接字符串的拦截器:

ISecurityContext securityContext = m_Container.GetInstance<ISecurityContext>();
var sqlConn = new SqlConnectionStringBuilder
{
    DataSource = @"DEV_TEST_SERVER\SQL2017",
    InitialCatalog = "COMMON",
    IntegratedSecurity = true,
    ConnectTimeout = 30
};
securityContext.Database.Connection.ConnectionString = sqlConn.ConnectionString;

1 个答案:

答案 0 :(得分:1)

  

ServiceHosts,在上下文模式Single

中创建

这是一个坏主意。集成页面states

  

提示:对所有WCF服务使用InstanceContextMode.PerCall。这可以防止任何难以检测到因WCF服务超出单个请求而导致的问题。

在为应用程序的某些部分提供上下文数据(例如受请求影响的连接字符串)时,有两种选择。您可以使用环境状态,也可以使用objec图表 flow 数据。

环境状态意味着您将数据存储在某个可用于特定上下文的可变状态中。以下是几个选项:

  • 全球静态。应用程序中的所有线程和请求都可以访问这些字段。这不适合您的情况。
  • 线程静态。从单个线程访问该字段的任何地方,返回相同的值,但其他线程获得自己的值。由于WCF请求可以异步执行多个线程,因此这两个选项也不合适。
  • 异步范围的状态。这允许单个逻辑异步操作流使用相同的数据作用域。数据在该范围内的任何地方都可用。此选项最适合您的需求。

注意:对于这个答案,我假设ISecurityContext定义如下:

public interface ISecurityContext
{
    public Database Database { get; }
}

使用第三个选项,您可以按如下方式实施SecurityContext

public sealed class SecurityContext : ISecurityContext
{
    private static readonly AsyncLocal<Database> db =
        new AsyncLocal<Database>();

    Database ISecurityContext.Database => db.Value;

    internal void SetDatabase(Database database) => db.Value = database;
}

由于使用了System.Threading.AsyncLocal,您可以在整个应用程序中重复使用相同的单个SecurityContext实例,注册为Singleton

IDispatchMessageInspector内,您可以通过调用SecurityContext.SetDatabase(db)来设置数据库。

另一种选择是使用对象图流动数据。在这种情况下,您将可变状态存储为上下文类(例如SecurityContext)中的私有字段,并将它们注册为Scoped。这样您就可以在请求开始时设置它们的值,这些值可以可以在请求中的任何地方重复使用,其中ISecuriryContext被注入,而另一个请求获得不同的SecurityContext实例。

您可以将SecurityContext更改为以下内容:

public sealed class SecurityContext : ISecurityContext
{
    // Just get/set with a private backing field. No ambient state
    public Database Database { get; set; }
}

注册如下:

container.Register<SecurityContext>(Lifestyle.Scoped);
container.Register<ISecurityContext, SecurityContext>(Lifestyle.Scoped);

IDispatchMessageInspector内,您可以解析SecurityContext并设置Database

container.GetInstance<SecurityContext>().Database = db;

您应用的其余部分可以完全依赖ISecurityContext并检索其Database值。