C# 查询侦听-处理诊断侦听器

C# 查询侦听-处理诊断侦听器,c#,entity-framework-core,C#,Entity Framework Core,我们正在使用DiagnosticListeners来修改EF Core生成的SQL命令文本。问题是,我们的监听器需要根据一些通过HttpRequests进入Api的用户特定数据修改SQL命令。我们目前的解决方案非常粗糙,将来可能会出现问题。每次创建DbContext时,我们都会注册一个新的侦听器: public class MyContext : DbContext { private readonly HReCommandAdapter _adapter; public M

我们正在使用
DiagnosticListeners
来修改EF Core生成的SQL命令文本。问题是,我们的监听器需要根据一些通过HttpRequests进入Api的用户特定数据修改SQL命令。我们目前的解决方案非常粗糙,将来可能会出现问题。每次创建
DbContext
时,我们都会注册一个新的侦听器:

public class MyContext : DbContext
{
    private readonly HReCommandAdapter _adapter;


    public MyContext(DbContextOptions options) : base(options)
    {
        _adapter = new DbCommandAdapter();

        var listener = this.GetService<DiagnosticSource>();
        (listener as DiagnosticListener).SubscribeWithAdapter(_adapter);
    }   

    public override void Dispose()
    {
        _adapter.Dispose();
        base.Dispose();
    }

    //DbSets and stuff
}
如您所见,我们当前的方法是使用
connectionId
,每次创建
DbContext
时,它都会有所不同。使用这种黑客方法的原因是,即使每次处理
HttpRequest
时都会调用
DbContext.Dispose()
,侦听器实例也不会被释放。因此,
connectionId
基本上允许在侦听器和给定的
DbContext
实例之间有1:1映射的错觉

然而,在api的整个生命周期中,侦听器实例的数量一直在堆积,而实例消失的唯一时间是应用程序池停止或回收时

是否有可能以某种方式处置这些侦听器实例?如何处置?为了修改SQL命令,我也愿意采用不同的方法(诊断侦听器是我们为EF Core找到的唯一可行的侦听器)

编辑: 我只修改
SELECT
命令。我省略了细节,但是创建的
DbCommandAdapter
带有一个特定于用户的前缀,该前缀根据试图访问API的用户而有所不同

例如,如果查询是:

从员工中选择字段1、字段2

并且用户特定前缀为
user\u SOMENUMBER
,则修改后的查询结束:

从用户\u SOMENUMBER\u员工中选择字段1、字段2


我理解这是脆弱的,但我们保证我们更改的表名的模式是相同的,这不是一个问题。

如果您不能处理侦听器,为什么不将它们合并并重新使用呢?当处理或构造成本很高时,池是一种很好的软件模式。防止无限增长也是一种合理的使用

以下只是伪代码。它需要知道适配器事务何时完成,以便将其标记为可用并重用。它还假设更新后的myDbContext将具有执行工作所需的内容

public static class DbCommandAdapterPool
{
    private static ConcurrentBag<DbCommandAdapter> _pool = new ConcurrentBag<DbCommandAdapter>();

    public static DbCommandAdapter SubscribeAdapter(MyContext context)
    {
        var adapter = _pool.FirstOrDefault(a => a.IsAvailable);
        if (adapter == null)
        {
            adapter = new DbCommandAdapter(context);
            _pool.Add(adapter);
        }
        else adapter.Reuse(context);

        return adapter;
    }
}

public class MyContext : DbContext
{
    private readonly HReCommandAdapter _adapter;


    public MyContext(DbContextOptions options) : base(options)
    {
        //_adapter = new DbCommandAdapter();

        //var listener = this.GetService<DiagnosticSource>();
        //(listener as DiagnosticListener).SubscribeWithAdapter(_adapter);

        DbCommandAdapterPool.SubscribeAdapter(this);
    }

    public override void Dispose()
    {
        _adapter.Dispose();
        base.Dispose();
    }

    //DbSets and stuff
}

public class DbCommandAdapter : IDisposable
{
    private bool _hasExecuted = false;
    private Guid? _lastExecId = null;
    private MyContext _context;
    private DiagnosticListener _listener;//added for correlation

    public bool IsAvailable { get; } = false;//Not sure what constitutes a complete transaction.

    public DbCommandAdapter(MyContext context)
    {
        this._context = context;
        this._listener = context.GetService<DiagnosticSource>();
    }


    ...

    public void Reuse(MyContext context)
    {
        this.IsAvailable = false;
        this._context = context;
    }
}
公共静态类DbCommandAdapterPool
{
私有静态ConcurrentBag_pool=新ConcurrentBag();
公共静态DbCommandAdapter SubscribeAdapter(MyContext上下文)
{
var adapter=\u pool.FirstOrDefault(a=>a.IsAvailable);
if(适配器==null)
{
adapter=新的DbCommandAdapter(上下文);
_pool.Add(适配器);
}
重用(上下文);
返回适配器;
}
}
公共类MyContext:DbContext
{
专用只读HReCommandAdapter\u适配器;
公共MyContext(DbContextOptions):基本(选项)
{
//_adapter=新的DbCommandAdapter();
//var listener=this.GetService();
//(作为诊断侦听器的侦听器)。订阅适配器(_适配器);
DbCommandAdapterPool.SubscribeAdapter(此);
}
公共覆盖无效处置()
{
_adapter.Dispose();
base.Dispose();
}
//数据库集和其他
}
公共类DbCommandAdapter:IDisposable
{
private boolu hassecuted=false;
私有Guid?\u lastExecId=null;
私有MyContext_上下文;
private DiagnosticListener _listener;//为关联添加
public bool IsAvailable{get;}=false;//不确定什么构成完整事务。
公共DbCommandAdapter(MyContext上下文)
{
这._context=context;
这是。_listener=context.GetService();
}
...
公共void重用(MyContext上下文)
{
this.IsAvailable=false;
这._context=context;
}
}

注意:我自己没有试过。Ivan Stoev建议将ICurrentDbContext的依赖项注入CustomSqlServerQuerySqlGeneratorFactory,该工厂随后在CustomSqlServerQuerySqlGenerator中可用。请参阅:

如何在启动时创建一次订阅,并让它一直运行到应用程序结束

问题是,订阅是基于由ServiceProvider生成的DiagnosticSource对象(在我的例子中是EntityFramework)

因此,每次在代码中创建MyContext时,都会向DiagnosticsSource添加另一个适配器和另一个订阅。 适配器与订阅一起存在,订阅未被释放(它将与DiagnosticSource一起释放,或者至少在释放源时变得无用)

因此,侦听器实例在api的整个生命周期中不断堆积

我建议在构建主机之后和运行主机之前初始化订阅一次。 您需要获取稍后在应用程序中使用的诊断源,这就是您需要主机的原因。否则,订阅将位于另一个DiagnosticSource对象上,并且永远不会被调用

var host = new WebHostBuilder()
     .UseConfiguration(config)
      // ...
     .Build();

using (var subscription = InitializeSqlSubscription(host))
{
  host.Run();
}
私有静态IDisposable初始化QLSubscription(IWebHost主机)
{
//TODO:删除使用.Net Core 3.1的变通方法
//我们需要服务提供者的作用域
使用(var serviceScope=host.Services.CreateScope())
{
var services=serviceScope.ServiceProvider;
尝试
{
var adapter=new DbCommandAdapter();
//获取DiagnosticsSource(EntityFramework)所需的上下文
var myContext=services.GetRequiredService();
//DiagnosticsSource在ServiceProvider中是单例的(我猜),并且跨越范围
//->范围的处置对DiagnosticSource或其订阅没有影响
var diagnosticSource=myContext.GetService();
var host = new WebHostBuilder()
     .UseConfiguration(config)
      // ...
     .Build();

using (var subscription = InitializeSqlSubscription(host))
{
  host.Run();
}
private static IDisposable InitializeSqlSubscription(IWebHost host)
{
   // TODO: remove Workaround with .Net Core 3.1
   // we need a scope for the ServiceProvider
   using (var serviceScope = host.Services.CreateScope())
   {
      var services = serviceScope.ServiceProvider;

      try
      {
         var adapter = new DbCommandAdapter();

         // context needed to get DiagnosticSource (EntityFramework)
         var myContext = services.GetRequiredService<MyContext>();

         // DiagnosticSource is Singleton in ServiceProvider (i guess), and spanning across scopes
         // -> Disposal of scope has no effect on DiagnosticSource or its subscriptions
         var diagnosticSource = myContext.GetService<DiagnosticSource>();

         // adapter Object is referenced and kept alive by the subscription
         // DiagnosticListener is Disposable, but was created (and should be disposed) by ServiceProvider
         // subscription itself is returned and can be disposed at the end of the application lifetime
         return (diagnosticSource as DiagnosticListener).SubscribeWithAdapter(adapter);
      }
      catch (Exception ex)
      {
         var logger = services.GetRequiredService<ILogger<Startup>>();
         logger.LogError(ex, "An error occurred while initializing subscription");
         throw;
      }
   }
}