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

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

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

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

所以,我试图按照每个调用(async,wcf生活方式)注册这个,但因为这是一个Windows服务,它不会关闭,容器没有正确的范围.

在这种情况下我可以做些什么来使它工作?

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

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

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;

最佳答案

the ServiceHosts, which are created in context mode Single

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

TIP: Use InstanceContextMode.PerCall for all WCF services. This prevents any hard to detect problems caused by WCF services outliving a single request.

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

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

>全球静态.应用程序中的所有线程和请求都可以访问这些字段.这不适合您的场景.
>线程静态.从单个线程访问该字段的任何地方,返回相同的值,但其他线程获得自己的值.由于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并设置数据库:

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

应用程序的其余部分可以简单地依赖于ISecurityContext并检索其数据库值.

点赞