Профайлер для Entity Framework
Приветствую!
Продолжу серию статей про программирование. На это раз я хочу поговорить о профилировании и логировании по отношению с Entity Framework-у (далее EF) c 4-й по 6-ю версию, а также о некоторых примененных мной решениях, таких как декомпилирование .NET кода на лету, форма для обработки исключению и прочее.
Подробности под катом.
Введение Когда я начинал изучение EF, передо мной стал вопрос в профилировании запросов которые генерирует EF. После некоторого поиска я с неудовольствием обнаружил, что штатных средств EF не предоставляет, соответственно, приходилось пользоваться, на мой взгляд, крайне неудобной в использовании программой SQL Server Profiler или купить достаточно дорогой сторонний инструмент Entity Framework Profiler 2.0, за $389.00, а я жадный.С появлением EF версии 6, ситуация с логированием команд изменилась. Появился механизм, который позволяет добавить свой интерцептор:
//Регистрируем при старте программы свой IDbCommandInterceptor
DbInterception.Add (new LogFormatter ());
public class LogFormatter: IDbCommandInterceptor
{
private readonly Stopwatch _stopwatch = new Stopwatch ();
public void NonQueryExecuting (DbCommand command, DbCommandInterceptionContext
// Задает IP профалера в случаях его отличие от »127.0.0.1» EFloggerFor6.SetProfilerClientIP (»192.168.10.50″);
// Посылает текстовое сообщение в профайлер EFloggerFor6.WriteMessage («Text message»);
// Стартует посылку логов профайлер (уже запущен по умолчанию после инициализации) EFloggerFor6.StartSendToClient ();
// Останавливает посылку логов профайлер EFloggerFor6.StopSendToClient ();
// Стартует логирование в файл EFlogger.log приложения EFloggerFor6.StartSaveToLogFile ();
// Останавливает логирование в файл EFlogger.log приложения EFloggerFor6.StopSaveToLogFile ();
//Очищает лог в профайлере и удаляет файл лога, если включен EFloggerFor6.ClearLog ();
// Отключает декомпиляцию кода EFloggerFor6.DisableDecompiling ();
// Добавляет сборку для игнорирования при построении Stack Trace-a EFloggerFor6.ExcludeAssembly («MyAssemly»); Как это все работает Механизм внедрения в EF 4-й версии сильно отличается от EF 6-й версии.Штатные механизмов подмены DbProviderFactory в EF 4 отсутствуют, поэтому приходится идти на подмену штатной фабрики на свою через рефлекцию.Сам процесс выглядит так: // проверяем что все фабрики загруженны DbProviderFactories.GetFactoryClasses ();
// получаем тип DbProviderFactories Type type = typeof (DbProviderFactories);
DataTable table = null; //получаем значение DataSet статического поля _configTable или _providerTable object setOrTable = (type.GetField (»_configTable», BindingFlags.NonPublic | BindingFlags.Static) ? type.GetField (»_providerTable», BindingFlags.NonPublic | BindingFlags.Static)).GetValue (null);
var set = setOrTable as DataSet; if (set!= null) table = set.Tables[«DbProviderFactories»];
table = (DataTable)setOrTable;
// для каждой записи в DataTable подставляем свою фабрику
foreach (DataRow row in table.Rows.Cast
var profType = EFProviderUtilities.ResolveFactoryType (factory.GetType ());
if (profType!= null)
{
DataRow profiled = table.NewRow ();
profiled[«Name»] = row[«Name»];
profiled[«Description»] = row[«Description»];
profiled[«InvariantName»] = row[«InvariantName»];
profiled[«AssemblyQualifiedName»] = profType.AssemblyQualifiedName;
table.Rows.Remove (row);
table.Rows.Add (profiled);
}
}
Для интеграции в EF 6-й версии можно воспользоваться штатным механизмом подстановки сервисов:
DbConfiguration.Loaded += (_, a) => a.ReplaceService
var methods = new List
// проходим по всем фреймам foreach (StackFrame t in frames) { // получаем метод var method = t.GetMethod ();
// получаем сборку и проверяем нужно ли ее пропустить var assembly = method.Module.Assembly.GetName ().Name; if (ShouldExcludeType (method) || AssembliesToExclude.Contains (assembly) || MethodsToExclude.Contains (method.Name)) continue;
// находим первый по стеку фрейм и считаем что именно он сгенерировал команду, если нет нужно добавить имя сборки в список на пропуск if (outStackFrame == null) { outStackFrame = t; } methods.Add (method.DeclaringType.FullName + »:» + method.Name); } return string.Join (»\r\n», methods); } private static string GetMethodBody (StackFrame stackFrame) { MethodBase methodBase = stackFrame.GetMethod (); return Decompiler.GetSourceCode (methodBase.Module.FullyQualifiedName, methodBase.DeclaringType.Name, methodBase.Name); } Decompiler using System; using System.IO; using System.Linq; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Ast; using ICSharpCode.Decompiler.Ast.Transforms; using Mono.Cecil;
namespace Common { public static class Decompiler { public static string GetSourceCode (string pathToAssembly, string className, string methodName) { try { var assemblyDefinition = AssemblyDefinition.ReadAssembly (pathToAssembly);
TypeDefinition assembleDefenition = assemblyDefinition.MainModule.Types.First (type => type.Name == className); MethodDefinition methodDefinition = assembleDefenition.Methods.First (method => method.Name == methodName); var output = new StringWriter (); var plainTextOutput = new PlainTextOutput (output); DecompileMethod (methodDefinition, plainTextOutput); return output.ToString (); } catch (Exception exception) {
return string.Format («Exception in decompling. \r\n Message:{0}, \r\n Inner Exception:{1}, \r\n StackTrace:{2}», exception.Message, exception.InnerException, exception.StackTrace); } }
private static void DecompileMethod (MethodDefinition method, ITextOutput output) { AstBuilder codeDomBuilder = CreateAstBuilder (currentType: method.DeclaringType, isSingleMember: true); if (method.IsConstructor && ! method.IsStatic && ! method.DeclaringType.IsValueType) { AddFieldsAndCtors (codeDomBuilder, method.DeclaringType, method.IsStatic); RunTransformsAndGenerateCode (codeDomBuilder, output); } else { codeDomBuilder.AddMethod (method); RunTransformsAndGenerateCode (codeDomBuilder, output); } }
private static AstBuilder CreateAstBuilder (ModuleDefinition currentModule = null, TypeDefinition currentType = null, bool isSingleMember = false) { if (currentModule == null) currentModule = currentType.Module; var settings = new DecompilerSettings (); if (isSingleMember) { settings = settings.Clone (); settings.UsingDeclarations = false; } return new AstBuilder ( new DecompilerContext (currentModule) { CurrentType = currentType, Settings = settings }); }
private static void AddFieldsAndCtors (AstBuilder codeDomBuilder, TypeDefinition declaringType, bool isStatic) { foreach (var field in declaringType.Fields) { if (field.IsStatic == isStatic) codeDomBuilder.AddField (field); } foreach (var ctor in declaringType.Methods) { if (ctor.IsConstructor && ctor.IsStatic == isStatic) codeDomBuilder.AddMethod (ctor); } }
private static void RunTransformsAndGenerateCode (AstBuilder astBuilder, ITextOutput output, IAstTransform additionalTransform = null) { astBuilder.GenerateCode (output); } } } Пример рабочего проекта декомпиляции на яндекс файлах: yadi.sk/d/AzBGet5-Nwns2Окно обработки исключительной ситуации Никому не секрет, что приложения периодически падают, это может произойти по самым разным причинам и задача разработчика показать какое-то вменяемое окно для сообщения об ошибке.Мой вариант формы выглядит так: Форма предлагает пользователю добавить информацию об ошибке, шаги для воспроизведения и свой email для ответа.При нажатии на кнопку Отправить, на почту разработчику приходить письмо со всем данными.
[STAThread] [STAThread] static void Main () { SetupLogger (); // если дебагер не подключен if (! Debugger.IsAttached) { // отлавливаем все необработанные ошибки Application.ThreadException += (sender, e) => HandleError (e.Exception); AppDomain.CurrentDomain.UnhandledException += (sender, e) => HandleError ((Exception)e.ExceptionObject); }
Application.EnableVisualStyles (); Application.SetCompatibleTextRenderingDefault (false); Application.Run (new MainForm ()); } private static void HandleError (Exception exception) { try { // запускаем контроллер формы обернутый try/catch на случай если в контроллере тоже произойдет ошибка new ErrorHandlerController (exception).Run (); } catch (Exception e) { MessageBox.Show («Error processing exception. Please send log file EFlogger.log to developer:» + Settings.Default.ProgrammerEmail + » \r\n Exception:» + e); // записываем ошибку в лог файл Logger.Error (e); if (MessageBox.Show («Attach debugger? \n Only for developer!!!», «Debugging…», MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { Debugger.Launch (); throw; } } finally { // обязательно принудительно завершаем приложение чтобы Windows не отображала стандартное окно ошибки приложения Environment.Exit (1); } }
//new ErrorHandlerController (exception).Run (); public void Run () {
// формируем текст ошибки string exceptionInfoText = string.Format ( «An unexpected error occurred: {0}» + Environment.NewLine + «Time: {1} » + Environment.NewLine + »{2}» + Environment.NewLine + «InnerException: \n {3}» + Environment.NewLine + «InnerException StackTrace: \n {4}» + Environment.NewLine, _exception.Message, DateTime.Now, _exception, _exception.InnerException, _exception.InnerException!= null ? _exception.InnerException.StackTrace : string.Empty ); // записываем ошибку с лог файл Program.Logger.Error (exceptionInfoText); _view.ExceptionInfoText = exceptionInfoText;
// показываем форму, вызвав метод ShowDialog, в противном случае покажется стандартное окно ошибки приложения _view.ShowDialog (); } Пример рабочего проекта с формой обработки ошибок можете забрать на яндекс файлах: yadi.sk/d/7y4i_cz7NwtE3Послесловие На случай если сайт упадет, вы можете сказать архив профайлера опять же, на яндекс файлах: yadi.sk/d/trLmkJnkNwtg7Уверен, багов еще много, но я постараюсь все ошибки исправлять максимально быстро. Так же, я хочу спросить совета у уважаемого хабра сообщества как поступить с этим профайлером, так с одной стороны хочется сделать бесплатный продукт, но в то же время и немного заработать. В настоящее время я думаю о том чтобы сделать профайлер полностью бесплатным для личного использования и платным для компаний, но опять же по минимальной цене баксов в 20–30. Ну или просто оставить бесплатным с кнопочной Donate.Всем спасибо за внимание. Надеюсь этот инструмент будет полезен.
PS: По всем вопросам, баг репортам и прочее можете писать на почту: developer@ef-logger.com или st.glushak@gmail.com. Также, автор ищет подработку, ибо ипотека. Спасибо за понимание.