Используйте MiniProfiler с DevExpress XPO (ORM) - PullRequest
0 голосов
/ 15 марта 2019

Я пытаюсь настроить свой проект так, чтобы MiniProfiler мог профилировать вызовы SQL XPO. Это должно было быть очень простым делом, поскольку 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 ... Так как у меня нет источников в качестве моей подписки Я не знаю, как это реализовать.

1 Ответ

0 голосов
/ 23 марта 2019

Через 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()));
    }
}

1020 * USAGE *

Один из способов использования указанных выше слоев данных - установить свойство 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 queries inside MiniProfiler UI

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

MiniProfiler detecting duplicate XPO calls


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

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

Показанное выше решение потомков уровня данных решает только часть проблемы. С одной стороны, он не регистрирует прямые вызовы SQL, вызовы хранимых процедур и методы Session, что может быть дорогостоящим (в конце концов, он даже не регистрирует гидратацию объектов, извлеченных из базы данных). XPO реализует два (возможно, три) различных механизма трассировки. Один протоколирует операторы и результаты SQL (количество строк, тайминги, параметры и т. Д.), Используя стандартную трассировку .NET, а другие методы сеанса журнала и операторы SQL (без результатов), используя класс DevExpress 'LogManager. 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");
...