Используйте MiniProfiler с DevExpress XPO (ORM)

Я пытаюсь настроить свой проект так, чтобы MiniProfiler мог профилировать вызовы XPO SQL. Это должно было быть очень простым усилием, так как MiniProfiler просто обертывает обычное соединение, но этот простой подход не работает. Вот код, который должен был работать:

protected void Button1_Click(object sender, EventArgs e) {
    var s = new UnitOfWork();
    IDbConnection conn = new ProfiledDbConnection(new SqlConnection(Global.ConnStr), MiniProfiler.Current);
    s.Connection = conn; 
    for (int i = 0; i < 200; i++) {
        var p = new Person(s) {
            Name = $"Name of {i}",
            Age = i,
        };
        if (i % 25 == 0)
            s.CommitChanges();
    }
    s.CommitChanges();
}

Этот код просто оборачивает SqlConnection в ProfiledDbConnection, а затем устанавливает свойство Session/UnitOfWork.Connection для этого соединения.

Все компилируется просто отлично, но во время выполнения возникает следующее исключение:

DevExpress.Xpo.Exceptions.CannotFindAppropriateConnectionProviderException
  HResult=0x80131500
  Message=Invalid connection string specified: 'ProfiledDbConnection(Data Source=.\SQLEXPRESS;Initial Catalog=sample;Persist Security Info=True;Integrated Security=SSPI;)'.
  Source=<Cannot evaluate the exception source>
  StackTrace:
   em DevExpress.Xpo.XpoDefault.GetConnectionProvider(IDbConnection connection, AutoCreateOption autoCreateOption)
   em DevExpress.Xpo.XpoDefault.GetDataLayer(IDbConnection connection, XPDictionary dictionary, AutoCreateOption autoCreateOption, IDisposable[]& objectsToDisposeOnDisconnect)
   em DevExpress.Xpo.Session.ConnectOldStyle()
   em DevExpress.Xpo.Session.Connect()
   em DevExpress.Xpo.Session.get_Dictionary()
   em DevExpress.Xpo.Session.GetClassInfo(Type classType)
   em DevExpress.Xpo.XPObject..ctor(Session session)
   em WebApplication1.Person..ctor(Session s) na C:\Users\USER\source\repos\WebApplication2\WebApplication1\Person.cs:linha 11
   em WebApplication1._Default.Button1_Click(Object sender, EventArgs e) na C:\Users\USER\source\repos\WebApplication2\WebApplication1\Default.aspx.cs:linha 28
   em System.Web.UI.WebControls.Button.OnClick(EventArgs e)
   em System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument)
   em System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
   em System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument)
   em System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)
   em System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

Мне удалось найти эту проблему в Центре поддержки DevExpress: https://www.devexpress.com/Support/Center/Question/Details/Q495411/hooks-to-time-and-log-xpo-sql

Но ответ поверхностный, и он просто говорит своему клиенту написать класс, реализующий интерфейс IDataStore, и обратиться к исходному коду DataStoreLogger для примера... Поскольку у меня нет исходников, поскольку моя подписка не включала их, я в недоумении, как это реализовать.


person Loudenvier    schedule 14.03.2019    source источник


Ответы (1)


Через 9 дней я придумал решение с низким трением, хотя и не идеальное, которое состоит из двух новых классов, наследуемых от SimpleDataLayerи ThreadSafeDataLayer:

ProfiledThreadSafeDataLayer.cs

using DevExpress.Xpo.DB;
using DevExpress.Xpo.Metadata;
using StackExchange.Profiling;
using System.Reflection;

namespace DevExpress.Xpo
{
    public class ProfiledThreadSafeDataLayer : ThreadSafeDataLayer
    {
        public MiniProfiler Profiler { get { return MiniProfiler.Current; } }

        public ProfiledThreadSafeDataLayer(XPDictionary dictionary, IDataStore provider, params Assembly[] persistentObjectsAssemblies) 
            : base(dictionary, provider, persistentObjectsAssemblies) { }

        public override ModificationResult ModifyData(params ModificationStatement[] dmlStatements) {
            if (Profiler != null) using (Profiler.CustomTiming("xpo", dmlStatements.ToSql(), nameof(ModifyData))) {
                return base.ModifyData(dmlStatements);
            }
            return base.ModifyData(dmlStatements);
        }

        public override SelectedData SelectData(params SelectStatement[] selects) {
            if (Profiler != null) using (Profiler.CustomTiming("xpo", selects.ToSql(), nameof(SelectData))) {
                return base.SelectData(selects);
            }
            return base.SelectData(selects);
        }
    }
}

ProfiledDataLayer.cs

using DevExpress.Xpo.DB;
using DevExpress.Xpo.Metadata;
using StackExchange.Profiling;

namespace DevExpress.Xpo
{
    public class ProfiledSimpleDataLayer : SimpleDataLayer
    {
        public MiniProfiler Profiler { get { return MiniProfiler.Current; } }

        public ProfiledSimpleDataLayer(IDataStore provider) : this(null, provider) { }

        public ProfiledSimpleDataLayer(XPDictionary dictionary, IDataStore provider) : base(dictionary, provider) { }

        public override ModificationResult ModifyData(params ModificationStatement[] dmlStatements) {
            if (Profiler != null) using (Profiler.CustomTiming("xpo", dmlStatements.ToSql(), nameof(ModifyData))) {
                return base.ModifyData(dmlStatements);
            }
            return base.ModifyData(dmlStatements);
        }

        public override SelectedData SelectData(params SelectStatement[] selects) {
            if (Profiler != null) using (Profiler.CustomTiming("xpo", selects.ToSql(), nameof(SelectData))) {
                return base.SelectData(selects);
            }
            return base.SelectData(selects);
        }
    }
}

И методы расширения .ToSql():

using DevExpress.Xpo.DB;
using System.Data;
using System.Linq;

namespace DevExpress.Xpo
{
    public static class StatementsExtensions
    {
        public static string ToSql(this SelectStatement[] selects) => string.Join("\r\n", selects.Select(s => s.ToString()));
        public static string ToSql(this ModificationStatement[] dmls) => string.Join("\r\n", dmls.Select(s => s.ToString()));
    }
}

ИСПОЛЬЗОВАНИЕ

Один из способов использования описанных выше слоев данных — настроить свойство XpoDefault.DataLayer при настройке XPO для вашего приложения:

XpoDefault.Session = null;
XPDictionary dict = new ReflectionDictionary();
IDataStore store = XpoDefault.GetConnectionProvider(connectionString, AutoCreateOption.SchemaAlreadyExists);
dict.GetDataStoreSchema(typeof(Some.Class).Assembly, typeof(Another.Class).Assembly);
// It's here that we setup the profiled data layer
IDataLayer dl = new ProfiledThreadSafeDataLayer(dict, store); // or ProfiledSimpleDataLayer if not an ASP.NET app
XpoDefault.DataLayer = dl; 

РЕЗУЛЬТАТЫ

Теперь вы можете просматривать (некоторые из них — подробнее об этом позже) запросы к базе данных XPO, аккуратно распределенные по категориям в пользовательском интерфейсе MiniProfiler:

Запросы XPO внутри пользовательского интерфейса MiniProfiler

С дополнительным преимуществом обнаружения повторяющихся вызовов следующим образом :-):

MiniProfiler обнаруживает повторяющиеся вызовы XPO


ЗАКЛЮЧИТЕЛЬНЫЕ МЫСЛИ

Я копался в этом уже 9 дней. Я изучил декомпилированный код XPO с помощью JustDecompile от Telerik и перепробовал слишком много разных подходов к передаче данных профилирования из XPO в MiniProfiler с минимальными трудностями. насколько это возможно. Я попытался создать провайдера подключения XPO, наследующий от XPO MSSqlConnectionProvider, и переопределить метод, который он использует для выполнения запросов, но отказался, поскольку этот метод не является виртуальным (на самом деле он частный), и я бы для репликации всего исходного кода для этого класса, который зависит от многих других исходных файлов из DevExpress. Затем я попытался написать потомок Xpo.Session, чтобы переопределить все его методы управления данными, отложив вызов базового метода класса Session, окруженный вызовом MiniProfiler.CustomTiming. К моему удивлению, ни один из этих вызовов не был виртуальным (класс UnitOfWork, который наследуется от Session, кажется скорее хаком, чем правильным классом-потомком), поэтому я столкнулся с той же проблемой, что и с подходом провайдера соединения. Затем я попытался подключиться к другим частям фреймворка, даже к его собственному механизму трассировки. Это было плодотворно, в результате появилось два аккуратных класса: XpoNLogLogger и XpoConsoleLogger, но в конечном итоге я не смог показать результаты внутри MiniProfiler, поскольку он предоставлял уже профилированные и синхронизированные результаты, которые я не нашел способа включить/вставить в шаг MiniProfiler/настраиваемое время.

Показанное выше решение для потомков уровня данных решает только часть проблемы. Во-первых, он не регистрирует прямые вызовы SQL, вызовы хранимых процедур и методы сеанса, которые могут быть дорогостоящими (в конце концов, он даже не регистрирует гидратацию объектов, извлеченных из базы данных). XPO реализует два (возможно, три) различных механизма отслеживания. Один регистрирует операторы SQL и результаты (количество строк, тайминги, параметры и т. д.) с использованием стандартной трассировки .NET, а другие методы сеанса журнала и операторы SQL (без результатов) — с использованием класса LogManager DevExpress. LogManager — единственный метод, который не считается устаревшим. Третий метод, имитирующий класс DataStoreLogger, страдает теми же ограничениями, что и наш собственный подход.

В идеале мы должны иметь возможность просто предоставить ProfiledDbConnection любому объекту XPO Session, чтобы получить все возможности SQL-профилирования MiniProfiler.

Я все еще ищу способ обернуть или наследовать некоторые классы инфраструктуры XPO, чтобы обеспечить более полное/лучшее профилирование с помощью MiniProfiler для проектов на основе XPO. Я обновлю это дело, если найду что-нибудь полезное.

КЛАССЫ РЕГИСТРАЦИИ XPO

Исследуя это, я создал два очень полезных класса:

XpoNLogLogger.cs

using DevExpress.Xpo.Logger;
using NLog;
using System;

namespace Simpax.Xpo.Loggers
{
    public class XpoNLogLogger: DevExpress.Xpo.Logger.ILogger
    {
        static Logger logger = NLog.LogManager.GetLogger("xpo");

        public int Count => int.MaxValue;

        public int LostMessageCount => 0;

        public virtual bool IsServerActive => true;

        public virtual bool Enabled { get; set; } = true;

        public int Capacity => int.MaxValue;

        public void ClearLog() { }

        public virtual void Log(LogMessage message) {
            logger.Debug(message.ToString());
        }

        public virtual void Log(LogMessage[] messages) {
            if (!logger.IsDebugEnabled) return;
            foreach (var m in messages)
                Log(m);
        }
    }
}

XpoConsoleLogger.cs

using DevExpress.Xpo.Logger;
using System;

namespace Simpax.Xpo.Loggers
{
    public class XpoConsoleLogger : DevExpress.Xpo.Logger.ILogger
    {
        public int Count => int.MaxValue;

        public int LostMessageCount => 0;

        public virtual bool IsServerActive => true;

        public virtual bool Enabled { get; set; } = true;

        public int Capacity => int.MaxValue;

        public void ClearLog() { }

        public virtual void Log(LogMessage message) => Console.WriteLine(message.ToString());

        public virtual void Log(LogMessage[] messages) {
            foreach (var m in messages)
                Log(m);
        }
    }
}

Чтобы использовать эти классы, просто установите XPO LogManager.Transport следующим образом:

DevExpress.Xpo.Logger.LogManager.SetTransport(new XpoNLogLogger(), "SQL;Session;DataCache");
person Loudenvier    schedule 23.03.2019