ثبت جزئیات استثناهای Entity framework توسط ELMAH
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: دو دقیقه

در حین بروز استثناهای Entity framework، می‌توان توسط ابزارهای Logging متنوعی مانند ELMAH، جزئیات متداول آن‌ها را برای بررسی‌های آتی ذخیره کرد. اما این جزئیات فاقد SQL نهایی تولیدی و همچنین پارامترهای ورودی توسط کاربر یا تنظیم شده توسط برنامه هستند. برای اینکه بتوان این جزئیات را نیز ثبت کرد، می‌توان یک IDbCommandInterceptor جدید را طراحی کرد.


کلاس EfExceptionsInterceptor

در اینجا نمونه‌ای از یک پیاده سازی اینترفیس IDbCommandInterceptor را مشاهده می‌کنید. همچنین طراحی یک متد عمومی که می‌تواند به جزئیات SQL نهایی و پارامترهای آن دسترسی داشته باشد، در اینترفیس IEfExceptionsLogger ذکر شده‌است.
public interface IEfExceptionsLogger
{
    void LogException<TResult>(DbCommand command,
        DbCommandInterceptionContext<TResult> interceptionContext);
}

using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
 
namespace ElmahEFLogger
{
    public class EfExceptionsInterceptor : IDbCommandInterceptor
    {
        private readonly IEfExceptionsLogger _efExceptionsLogger;
 
        public EfExceptionsInterceptor(IEfExceptionsLogger efExceptionsLogger)
        {
            _efExceptionsLogger = efExceptionsLogger;
        }
 
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
    }
}


تهیه یک پیاده سازی سفارشی از IEfExceptionsLogger توسط ELMAH

اکنون که ساختار کلی IDbCommandInterceptor سفارشی برنامه مشخص شد، می‌توان پیاده سازی خاصی از آن‌را جهت استفاده از ELMAH به نحو ذیل ارائه داد:
using System;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using Elmah;
 
namespace ElmahEFLogger.CustomElmahLogger
{
    public class ElmahEfExceptionsLogger : IEfExceptionsLogger
    {
        /// <summary>
        /// Manually log errors using ELMAH
        /// </summary>
        public void LogException<TResult>(DbCommand command,
            DbCommandInterceptionContext<TResult> interceptionContext)
        {
            var ex = interceptionContext.OriginalException;
            if (ex == null)
                return;
 
            var sqlData = CommandDumper.LogSqlAndParameters(command, interceptionContext);
            var contextualMessage = string.Format("{0}{1}OriginalException:{1}{2} {1}", sqlData, Environment.NewLine, ex);
 
 
            if (!string.IsNullOrWhiteSpace(contextualMessage))
            {
                ex = new Exception(contextualMessage, new ElmahEfInterceptorException(ex.Message));
            }
 
            try
            {
                ErrorSignal.FromCurrentContext().Raise(ex);
            }
            catch
            {
                ErrorLog.GetDefault(null).Log(new Error(ex));
            }
        }
    }
}
در اینجا شیء Command به همراه SQL نهایی تولید و پارامترهای مرتبط است. همچنین interceptionContext.OriginalException جزئیات عمومی استثنای رخ داده را به همراه دارد. می‌توان این اطلاعات را پس از اندکی نظم بخشیدن، به متد Raise مربوط به ELMAH ارسال کرد تا جزئیات بیشتری از استثنای رخ داده شده، در لاگ‌های آن ظاهر شوند.


استفاده از ElmahEfExceptionsLogger جهت طراحی یک Interceptor عمومی

   public class ElmahEfInterceptor : EfExceptionsInterceptor
    {
        public ElmahEfInterceptor()
            : base(new ElmahEfExceptionsLogger())
        { }
    }
برای استفاده از ElmahEfExceptionsLogger و تهیه یک Interceptor عمومی، می‌توان با ارث بری از کلاس Interceptor ابتدای بحث شروع کرد و وهله‌ای از ElmahEfExceptionsLogger را به سازنده‌ی آن تزریق نمود (یکی از چندین روش ممکن). سپس برای استفاده از آن کافی است به ابتدای متد Application_Start فایل Global.asax.cs مراجعه و در ادامه سطر ذیل را اضافه نمود:
 DbInterception.Add(new ElmahEfInterceptor());

پس از آن جزئیات کلیه استثناهای EF در لاگ‌های نهایی ELMAH به نحو ذیل ظاهر خواهند شد:




کدهای کامل این پروژه را از اینجا می‌توانید دریافت کنید:
ElmahEFLogger
  • #
    ‫۹ سال و ۹ ماه قبل، سه‌شنبه ۹ دی ۱۳۹۳، ساعت ۰۰:۱۷
    با سلام و تشکر از مطلب شما
    در صورت استفاده از پکیج بالا باید فیلتر کلی که در پست آموزش MVC قسمت 16 توضیح دادین برای ثبت استثناهای elmah حذف بشه؟
    • #
      ‫۹ سال و ۹ ماه قبل، سه‌شنبه ۹ دی ۱۳۹۳، ساعت ۰۰:۵۵
      خیر. کار این Interceptor صرفا لاگ کردن سراسری و نامرئی ریز جزئیات استثناهای EF در کل برنامه است. استثناهای رخ داده، در ادامه از فیلتر ELMAH اصلی تنظیم شده رد خواهند شد و یکبار هم در آنجا لاگ می‌شوند. این مورد چون منحصر به EF نیست، بنابراین ضرورتی به حذف آن هم نیست.
  • #
    ‫۳ سال و ۸ ماه قبل، شنبه ۱۳ دی ۱۳۹۹، ساعت ۱۸:۰۳
    یک نکته‌ی تکمیلی: معادل این مطلب با EF Core 5x

    کدهای Interceptor به این صورت اصلاح شده و تطابق خواهند یافت:
    using System;
    using System.Data;
    using System.Data.Common;
    using System.Globalization;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.EntityFrameworkCore.Diagnostics;
    using Microsoft.Extensions.Logging;
    
    namespace Utils
    {
        public class EfExceptionsInterceptor : DbCommandInterceptor
        {
            private readonly ILogger<EfExceptionsInterceptor> _logger;
    
            public EfExceptionsInterceptor(ILogger<EfExceptionsInterceptor> logger)
            {
                _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            }
    
            public override void CommandFailed(DbCommand command, CommandErrorEventData eventData)
            {
                logError(command, eventData);
            }
    
            public override Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = default)
            {
                logError(command, eventData);
                return Task.CompletedTask;
            }
    
            private void logError(DbCommand command, CommandErrorEventData eventData)
            {
                if (command == null || eventData == null)
                {
                    return;
                }
    
                var ex = eventData.Exception;
                if (ex == null)
                {
                    return;
                }
    
                var sqlData = logSqlAndParameters(command);
                var contextualMessage = $"{sqlData}{Environment.NewLine}OriginalException:{Environment.NewLine}{ex} {Environment.NewLine}";
                _logger.LogError(contextualMessage);
            }
    
            private static string logSqlAndParameters(DbCommand command)
            {
                // -- Name: [Value] (Type = {}, Direction = {}, IsNullable = {}, Size = {}, Precision = {} Scale = {})
                var builder = new StringBuilder();
    
                var commandText = command.CommandText ?? "<null>";
                builder.AppendFormat(CultureInfo.InvariantCulture, "{0}Command: {0}{1}", Environment.NewLine, commandText)
                    .AppendLine();
    
                var parameters = command.Parameters.OfType<DbParameter>().ToList();
    
                if (parameters.Any())
                {
                    builder.AppendFormat(CultureInfo.InvariantCulture, "{0}Parameters: ", Environment.NewLine)
                        .AppendLine();
                }
    
                foreach (var parameter in parameters)
                {
                    builder.Append("-- ")
                        .Append(parameter.ParameterName)
                        .Append(": '")
                        .Append((parameter.Value == null || parameter.Value == DBNull.Value) ? "null" : parameter.Value)
                        .Append("' (Type = ")
                        .Append(parameter.DbType);
    
                    if (parameter.Direction != ParameterDirection.Input)
                    {
                        builder.Append(", Direction = ").Append(parameter.Direction);
                    }
    
                    if (!parameter.IsNullable)
                    {
                        builder.Append(", IsNullable = false");
                    }
    
                    if (parameter.Size != 0)
                    {
                        builder.Append(", Size = ").Append(parameter.Size);
                    }
    
                    if (((IDbDataParameter)parameter).Precision != 0)
                    {
                        builder.Append(", Precision = ").Append(((IDbDataParameter)parameter).Precision);
                    }
    
                    if (((IDbDataParameter)parameter).Scale != 0)
                    {
                        builder.Append(", Scale = ").Append(((IDbDataParameter)parameter).Scale);
                    }
    
                    builder.Append(')').Append(Environment.NewLine);
                }
    
                return builder.ToString();
            }
        }
    }
    سپس برای ثبت آن، چون یک سرویس را به صورت تزریق وابستگی‌ها دریافت کرده، یکبار باید آن‌را به سیستم تزریق وابستگی‌ها معرفی کرد:
    services.AddScoped<EfExceptionsInterceptor>();
    و بعد نحوه‌ی معرفی آن به AddDbContextPool به روش خاص زیر است که از سرویس‌پروایدر استفاده می‌کند:
    services.AddDbContextPool<ApplicationDbContext>((serviceProvider, optionsBuilder) =>
                        optionsBuilder
                            .UseSqlServer(
                                connectionString,
                                sqlServerOptionsBuilder =>
                                {
                                  // ...
                                })
    .AddInterceptors(serviceProvider.GetRequiredService<EfExceptionsInterceptor>()));
    این نکته‌ای است که جهت ثبت Interceptorهای دارای تزریق وابستگی، مورد استفاده قرار می‌گیرد.