2

We are using DiagnosticListeners in order to modify the SQL command text that EF Core produces. The problem is that our listeners need to be modifying the SQL command based on some user specific data that comes into our Api via HttpRequests. Our current solution is extremely hacky and likely to cause issues in the future. We register a new listener each time DbContext is created:

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
}

The simplified listener code looks like below:

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 }
} 

As you can see, our current approach is to use the connectionId which will be different each time DbContext is created. The reason for this hacky approach is because the listener instances are not disposed even though the DbContext.Dispose() is called every time the HttpRequest is processed. So connectionId allows to basically have an illusion of 1:1 mapping between a listener and a given DbContext instance.

What happens though is that the amount of listener instances keep piling up throughout the lifetime of the api and the only time the instances go away is when the application pools stop or recycle.

Is it possible to somehow dispose of these listener instances and how? I'm also open to a different approach in order to modify SQL commands (the diagnostic listeners was the only viable one we found for EF Core).

EDIT: I am modifying only SELECT commands. I omitted the details, but the DbCommandAdapter gets created with a user specific prefix that is different based on the user trying to access the API.

So for example if the query is:

SELECT FIELD1, FIELD2 FROM EMPLOYEES

and the user specific prefix is USER_SOMENUMBER, then the modified query ends up:

SELECT FIELD1, FIELD2 FROM USER_SOMENUMBER_EMPLOYEES

I understand that this is fragile but we guarantee that the schema of the tablename that we change is identical and it is not a concern.

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Vidmantas Blazevicius
  • 4,652
  • 2
  • 11
  • 30
  • Does `DbCommandAdapter.Dispose` really have no code in it? – mjwills Jan 04 '19 at 12:28
  • @mjwills yeah, what code should be in there? – Vidmantas Blazevicius Jan 04 '19 at 12:55
  • What type of SQL commands are you modifying (`SELECT` or modification)? Can you provide an example? I'm asking because there are certainly other (infrastructure or public) points where you can hook into, depending on what do you need to do. – Ivan Stoev Jan 04 '19 at 18:09
  • @IvanStoev I've updated the question. We only do this for `SELECT` – Vidmantas Blazevicius Jan 04 '19 at 18:29
  • Take a look at https://stackoverflow.com/questions/53078435/ef-core-what-regex-can-i-use-to-replace-table-names-with-nolock-ones-in-db-int/53082098#53082098. You can inject dependency to `ICurrentDbContext` (or some other your own service) to `CustomSqlServerQuerySqlGeneratorFactory ` which then you can use inside `CustomSqlServerQuerySqlGenerator` to replace the table name. – Ivan Stoev Jan 04 '19 at 18:34
  • This seems like a really complicated way of overriding table names based on what would appear to be a multi-tenant system. With code first you can simply specify the table name via `OnModelCreating()` based on something you can inject into the Context Constructor and you're done. – Erik Philips Jan 04 '19 at 19:11
  • @ErikPhilips But `OnModelCreating` is *not* called per each `DbContext` instance, so it can't really be tied to injected instance state. – Ivan Stoev Jan 05 '19 at 21:14
  • @IvanStoev your answer in the other SO post looks very interesting. Do you see any problems getting the CustomSqlQueryGenerator into the DI with injectable services that would provide the dynamic table prefix value? I will try this anyway, was just wondering on your thoughts. – Vidmantas Blazevicius Jan 07 '19 at 13:52
  • @VidmantasBlazevicius Honestly I haven't tried such scenario. But all EF Core services are following constructor injection pattern, and EF Core internal service provider is build for each `DbContext` instance. Also `SqlServerQuerySqlGeneratorFactory` class is has additional dependency `ISqlServerOptions` compared to the base `QuerySqlGeneratorFactoryBase`. So I assume that's the intended design. Can't guarantee w/o testing, but worth trying :) – Ivan Stoev Jan 07 '19 at 14:27

2 Answers2

1

If you can't dispose of the listeners why not pool them and reuse them? Pooling is a good software pattern when disposing or constructing is expensive. Preventing infinite growth is a reasonable usage too.

The following is only pseudo-code. It requires knowing when an adapter transaction has completed, so that it can be marked available and reused. It also assumes the updated myDbContext will have what you need to perform your work.

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;
    }
}

NOTE: I haven't tried this myself. Ivan Stoev recommends injecting dependency to ICurrentDbContext to CustomSqlServerQuerySqlGeneratorFactory which is then available inside CustomSqlServerQuerySqlGenerator. see: Ef-Core - What regex can I use to replace table names with nolock ones in Db Interceptor

N-ate
  • 6,051
  • 2
  • 40
  • 48
  • I made the adapter keep reference to its listener in a 1 to 1 relationship, but I'm not sure you actually need the listener for what you're doing. – N-ate Jan 04 '19 at 20:22
  • 1
    This is certainly something I haven't thought of when we originally tried to tackle with. Pooling and reusing the listeners could be a potential solution. We could release the listener in the Dispose() method, so that when DbContext is getting disposed it would call listener dispose where we mark it for reusability (with some flag) and essentially release back into the pool. – Vidmantas Blazevicius Jan 07 '19 at 13:55
  • Can you add a link to an alternative solution (https://stackoverflow.com/questions/53078435/ef-core-what-regex-can-i-use-to-replace-table-names-with-nolock-ones-in-db-int/53082098#53082098) that Ivan Stoev suggested in the question comments. I've tried your approach and it works fine, but if someone stumbles at a similar problem - they might be interested in the alternative. – Vidmantas Blazevicius Jan 08 '19 at 13:58
  • I'll take a look at it. Don't forget to accept my answer if it solves your problem. Upvote if you can. Thanks! – N-ate Jan 08 '19 at 19:23
0

How about creating the subscription once in the Startup and letting it live until the end of the application.

The thing is, that the subscription is based on the DiagnosticSource Object (EntityFramework in my case) which is generated by the ServiceProvider.

So everytime you create a MyContext in your code, you add another adapter and another subscription to the DiagnosticSource. The adapter lives with the subscription and the subscription is not disposed (it would be disposed with the DiagnosticSource or at least get useless when the Source is disposed).

Therefore the listener instances keep piling up throughout the lifetime of the api.

I suggest to initialize your subscription once after building your host and before running it. You need to get the DiagnosticSource that is used later on in your application, that's why you need the host. Otherwise the subscription would be on another DiagnosticSource object and would never be called.

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;
      }
   }
}