Перехват запросов - удаление диагностических слушателей - PullRequest
0 голосов
/ 04 января 2019

Мы используем DiagnosticListeners для изменения текста команды SQL, создаваемого EF Core. Проблема заключается в том, что наши слушатели должны изменять команду SQL на основе некоторых пользовательских данных, которые поступают в наш API через HttpRequests. Наше текущее решение чрезвычайно хакерское и может вызвать проблемы в будущем. Мы регистрируем нового слушателя каждый раз, когда создается 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 позволяет создать иллюзию отображения 1: 1 между слушателем и данным экземпляром DbContext.

Что происходит, однако, так это то, что количество экземпляров слушателя накапливается в течение всего времени жизни API, и единственный раз, когда экземпляры уходят, это когда пулы приложений останавливаются или перезагружаются.

Можно ли как-то избавиться от этих экземпляров слушателя и как? Я также открыт к другому подходу для изменения команд SQL (диагностические прослушиватели были единственными жизнеспособными, которые мы нашли для EF Core).

EDIT: Я изменяю только команды SELECT. Я опустил детали, но DbCommandAdapter создается с префиксом, специфичным для пользователя, который отличается в зависимости от пользователя, пытающегося получить доступ к API.

Например, если запрос:

SELECT FIELD1, FIELD2 FROM EMPLOYEES

и пользовательский префикс USER_SOMENUMBER, тогда измененный запрос заканчивается:

SELECT FIELD1, FIELD2 FROM USER_SOMENUMBER_EMPLOYEES

Я понимаю, что это хрупко, но мы гарантируем, что схема изменяемого нами имени таблицы идентична, и это не проблема.

1 Ответ

0 голосов
/ 04 января 2019

Если вы не можете избавиться от слушателей, почему бы не объединить их и не использовать повторно? Объединение в пул является хорошим программным шаблоном, когда его размещение или создание обходится дорого. Предотвращение бесконечного роста - также разумное использование.

Ниже приведен только псевдокод. Требуется знать, когда была завершена транзакция адаптера, чтобы ее можно было пометить как доступную и использовать повторно. Также предполагается, что обновленный 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;
    }
}

ПРИМЕЧАНИЕ: Я сам не пробовал. Иван Стоев рекомендует внедрить зависимость для ICurrentDbContext в CustomSqlServerQuerySqlGeneratorFactory, который затем доступен внутри CustomSqlServerQuerySqlGenerator. см .: Ef-Core - какое регулярное выражение я могу использовать для замены имен таблиц именами nolock в Db Interceptor

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...