我们使用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
}
简化的侦听器代码如下:
public class DbCommandAdapter : IDisposable
{
private bool _hasExecuted = false;
private Guid? _lastExecId = null;
[DiagnosticName("Microsoft.EntityFrameworkCore.Database.Command.CommandExecuting")]
public void OnCommandExecuting(DbCommand command, DbCommandMethod executeMethod, Guid commandId, Guid connectionId, bool async, DateTimeOffset startTime)
{
if (!_lastExecId.HasValue)
_lastExecId = connectionId;
if (_lastExecId != connectionId)
return;
//We are modifying command text here
}
[DiagnosticName("Microsoft.EntityFrameworkCore.Database.Command.CommandExecuted")]
public void OnCommandExecuted(object result, bool async)
{
}
[DiagnosticName("Microsoft.EntityFrameworkCore.Database.Command.CommandError")]
public void OnCommandError(Exception exception, bool async)
{
}
public void Dispose() { //No code in here }
}
如您所见,我们当前的方法是使用connectionId
,每次创建DbContext
时,它们都会有所不同。这种骇人听闻的方法的原因是,即使每次处理DbContext.Dispose()
都调用HttpRequest
时,也不会处理侦听器实例。因此,connectionId
基本上可以使侦听器与给定的DbContext
实例之间具有1:1映射的错觉。
但是,发生的事情是,在api的整个生命周期中,侦听器实例的数量不断堆积,并且实例消失的唯一时间是应用程序池停止或回收。
是否有可能以某种方式处置这些侦听器实例?我也愿意采用其他方法来修改SQL命令(诊断侦听器是我们在EF Core中发现的唯一可行的侦听器)。
编辑:
我仅修改SELECT
命令。我省略了详细信息,但是DbCommandAdapter
的创建是根据用户特定的前缀创建的,该前缀根据尝试访问API的用户而有所不同。
例如,如果查询为:
SELECT FIELD1, FIELD2 FROM EMPLOYEES
,并且用户特定的前缀为USER_SOMENUMBER
,则修改后的查询结束:
SELECT FIELD1, FIELD2 FROM USER_SOMENUMBER_EMPLOYEES
我知道这很脆弱,但是我们保证更改的表名的架构是相同的,因此不必担心。
答案 0 :(得分:1)
如果您无法处置侦听器,为什么不合并它们并重用它们呢?当配置或构建非常昂贵时,合并是一种很好的软件模式。防止无限增长也是一种合理的用法。
以下仅为伪代码。它需要知道适配器事务何时完成,以便可以将其标记为可用并可以重用。它还假定更新的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;
}
}
注意::我自己还没有尝试过。 Ivan Stoev建议将对ICurrentDbContext的依赖项注入到CustomSqlServerQuerySqlGeneratorFactory中,然后在CustomSqlServerQuerySqlGenerator中可用。参见:Ef-Core - What regex can I use to replace table names with nolock ones in Db Interceptor
答案 1 :(得分:0)
如何在启动中一次创建订阅并将其保留到应用程序末尾呢?
问题是,订阅基于由ServiceProvider生成的DiagnosticSource对象(在我的情况下为EntityFramework)。
因此,每次在代码中创建MyContext时,都会向DiagnosticSource添加另一个适配器和另一个订阅。 适配器与订阅一起存在,并且订阅不被处置(将与DiagnosticSource一起处置,或者至少在处置Source时变得无用)。
因此,侦听器实例在api的整个生命周期中都不断堆积。
我建议在构建主机之后并运行它之前初始化一次订阅。 您需要获取稍后在应用程序中使用的DiagnosticSource,这就是为什么需要主机的原因。否则,订阅将在另一个DiagnosticSource对象上,并且永远不会被调用。
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;
}
}
}