۱۳۸۹/۰۵/۲۰

يكپارچه كردن ELMAH با WCF RIA Services


پيشتر در مورد ELMAH مطلبي را منتشر كرده بودم و اگر برنامه نويس ASP.NET هستيد و با ELMAH آشنايي نداريد،‌ جدا نيمي از عمر كاري شما بر فنا است!
هاست پيش فرض يك WCF RIA Service هم يك برنامه‌ي ASP.NET است. بنابراين كليه‌ي خطاهاي رخ داده در سمت سرور را بايد بتوان به نحوي لاگ كرد تا بعدا با مطالعه‌ي آن‌ها اطلاعات ارزشمندي را از نقايص برنامه در عمل و پيش از گوشزد شدن آن‌ها توسط كاربران، دريافت، بررسي و رفع كرد.
كليه خطاها را لاگ مي‌كنم تا:
- بدانم معناي جمله‌ي "برنامه كار نمي‌كنه" چي هست.
- بدون روبرو شدن با كاربران يا حتي سؤال و جوابي از آن‌ها بدانم دقيقا مشكل از كجا ناشي شده.
- بدانم رفتارهاي عمومي كاربران كه منجر به بروز خطا مي‌شوند كدام‌ها هستند.
- بدانم در كداميك از قسمت‌هاي برنامه تعيين اعتبار ورودي كاربران يا انجام نشده يا ضعيف و ناكافي است.
- بدانم زمانيكه دوستي (!) قصد پايين آوردن برنامه را با تزريق SQL داشته، دقيقا چه چيزي را وارد كرده، در كجا و چه زماني؟
- بتوانم Remote worker خوبي باشم.

ELMAH هم براي لاگ كردن خطاهاي مديريت نشده‌ي يك برنامه‌ي ASP.NET ايجاد شده است. بنابراين بايد بتوان اين دو (WCF RIA Services و ELMAH) را به نحوي با هم سازگار كرد. براي اينكار نياز است تا يك مديريت كننده‌ي خطاي سفارشي را با پياده سازي اينترفيس IErrorHandler تهيه كنيم (تا خطاهاي مديريت نشده‌ي حاصل را به سمت ELMAH هدايت كند) و سپس آن‌را به كمك يك ويژگي يا Attribute به DomainService خود جهت لاگ كردن خطاها اعمال نمائيم. روش تعريف اين Attribute را در كدهاي بعد ملاحظه خواهيد نمود (در اينجا نياز است تا دو ارجاع را به اسمبلي‌هاي Elmah.dll كه دريافت كرده‌ايد و اسمبلي استاندارد System.ServiceModel نيز به پروژه اضافه نمائيد):

//add a reference to "Elmah.dll"
using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Web;

namespace ElmahWcf
{
public class HttpErrorHandler : IErrorHandler
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
return false;
}

public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error == null)
return;

if (HttpContext.Current == null) //In case we run outside of IIS
return;

Elmah.ErrorSignal.FromCurrentContext().Raise(error);
}
#endregion
}
}

//add a ref to "System.ServiceModel" assembly
using System;
using System.Collections.ObjectModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace ElmahWcf
{
public class ServiceErrorBehaviorAttribute : Attribute, IServiceBehavior
{
Type errorHandlerType;
public ServiceErrorBehaviorAttribute(Type errorHandlerType)
{
this.errorHandlerType = errorHandlerType;
}

#region IServiceBehavior Members

public void AddBindingParameters(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters)
{ }

public void ApplyDispatchBehavior(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
IErrorHandler errorHandler;
errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType);
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher cd = cdb as ChannelDispatcher;
cd.ErrorHandlers.Add(errorHandler);
}
}

public void Validate(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{ }
#endregion
}
}
اكنون پس از تعريف ويژگي ServiceErrorBehavior، نوبت به اعمال آن مي‌رسد. به فايل DomainService خود مراجعه كرده و يك سطر زير را به آن اضافه نمائيد:
    [ServiceErrorBehavior(typeof(HttpErrorHandler))] //Integrating with ELMAH
[EnableClientAccess()]
public partial class MyDomainService : LinqToEntitiesDomainService<myEntities>

در ادامه نحوه‌ي افزودن تعاريف متناظر با ELMAH به Web.Config برنامه ذكر شده است. اين تعاريف براي IIS6 و 7 به بعد هم تكميل گرديده است. خطاها هم به صورت فايل‌هاي XML در پوشه‌اي به نام Errors كه به ريشه‌ي سايت اضافه خواهيد نمود (يا هر پوشه‌ي دلخواه ديگري)، لاگ مي‌شوند.
به نظر من اين روش، از ذخيره سازي اطلاعات لاگ‌ها در ديتابيس بهتر است. چون اساسا زمانيكه خطايي رخ مي‌دهد شايد مشكل اصلي همان ارتباط با ديتابيس باشد.
قسمت ارسال خطاها به صورت ايميل نيز comment شده است كه در صورت نياز مي‌توان آن‌را فعال نمود:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
<section name="errorTweet" requirePermission="false" type="Elmah.ErrorTweetSectionHandler, Elmah"/>
</sectionGroup>
</configSections>

<elmah>
<security allowRemoteAccess="1" />
<errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/Errors" />
<!-- <errorMail
from="errors@site.net"
to="nasiri@site.net"
subject="prj-error"
async="true"
smtpPort="25"
smtpServer="mail.site.net"
noYsod="true" /> -->
</elmah>

<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="DomainServiceModule"
preCondition="managedHandler"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</modules>
<validation validateIntegratedModeConfiguration="false" />
<handlers>
<add name="Elmah" verb="POST,GET,HEAD" path="myelmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</handlers>
</system.webServer>
<system.web>
<globalization
requestEncoding="utf-8"
responseEncoding="utf-8"
/>
<authentication mode="Forms">
<!--one month ticket-->
<forms name=".403AuthV"
cookieless="UseCookies"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200" />
</authentication>
<httpHandlers>
<add verb="POST,GET,HEAD" path="myelmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</httpHandlers>
<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="DomainServiceModule"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</assemblies>
</compilation>
</system.web>
<connectionStrings>
</connectionStrings>
<system.serviceModel>
<serviceHostingEnvironment
aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
</system.serviceModel>
</configuration>
اكنون براي مثال به يكي از متدهاي DomainService خود سطر زير را اضافه كرده و برنامه را آزمايش كنيد:
throw new Exception("This is an ELMAH test");

سپس به آدرس http://localhost/myelmah.axd مراجعه نموده و اطلاعات لاگ شده حاصل را بررسي كنيد:


اين روش با WCF Services هاي متداول هم كار مي‌كند. فقط در اين سرويس‌ها بايد aspNetCompatibilityEnabled مطابق تگ‌هاي ذكر شده‌ي system.serviceModel فوق در web.config لحاظ شوند (اين مورد به صورت پيش فرض در WCF RIA Services وجود دارد). همچنين ويژگي زير نيز بايد به سرويس شما اضافه گردد:
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]

منابع مورد استفاده:
Integrating ELMAH for a WCF Service
Making WCF and ELMAH play nice together
Getting ELMAH to work with WCF services



پ.ن.
اگر به خطاهاي ASP.NET دقت كرده باشيد كه به yellow screen of death هم مشهور هستند (در مقابل صفحات آبي ويندوز!)، ابتداي آن خيلي بزرگ نوشته شده Server Error و سپس ادامه‌ي خطا. همين مورد دقيقا يادم هست كه هر بار سبب بازخواست مديران شبكه بجاي برنامه نويس‌ها مي‌شد! (احتمالا اين هم يك نوع بدجنسي تيم ASP.NET براي گرفتن حال ادمين‌هاي شبكه است! و گرنه مثلا مي‌توانستند همان ابتدا بنويسند program/application error بجاي server error)