نام این جدول را با درنظر گرفتن شرایط موجود میتوان Resources گذاشت.
ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.
ستون Key برای نگهداری کلید ورودی منبع استفاده میشود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره میشود.
ستون Culture برای ذخیره کالچر ورودی منبع به کار میرود. این مقدار میتواند برای کالچر پیشفرض برنامه برابر رشته خالی باشد.
ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده میشود.
برای ستون Id میتوان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.
نکته: برای امنیت بیشتر میتوان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.
برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش میدهد دقت کنید:
اصلاح کلاس DbResourceProviderFactory
برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید بهصورت زیر تغییر کند:
public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { if (!string.IsNullOrEmpty(virtualPath)) { virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/' } return new LocalDbResourceProvider(virtualPath); }
ارتباط با دیتابیس
خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راههای زیادی وجود دارد. برای پیادهسازی آن مثلا میتوان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا بهکارگیری IoC، نمونه مناسبی از پیادهسازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیادهسازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.
پس از پیادهسازی کلاسهای مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با دادهها استفاده میشود که نمونهای ابتدایی از آن در زیر آورده شده است:
using System.Collections.Generic; using System.Linq; using DbResourceProvider.Models; namespace DbResourceProvider.Data { public class ResourceData { private readonly string _resourceName; public ResourceData(string resourceName) { _resourceName = resourceName; } public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList(); } } } }
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { private readonly string _resourceName; private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture; public DbResourceManager(string resourceName) { _resourceName = resourceName; _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>(); } public object GetObject(string resourceKey, CultureInfo culture) { return GetCachedObject(resourceKey, culture.Name); } private object GetCachedObject(string resourceKey, string cultureName) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResource = _resourceCacheByCulture[cultureName]; lock (this) { if (!cachedResource.ContainsKey(resourceKey)) { var data = new ResourceData(_resourceName); var dbResource = data.GetResource(resourceKey, cultureName); if (dbResource == null) return null; var cachedResources = _resourceCacheByCulture[cultureName]; cachedResources.Add(dbResource.Key, dbResource.Value); } } return cachedResource[resourceKey]; } } }
private object GetCachedObject(string resourceKey, string cultureName) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) { _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResources = _resourceCacheByCulture[cultureName]; var data = new ResourceData(_resourceName); var dbResources = data.GetResources(cultureName); foreach (var dbResource in dbResources) { cachedResources.Add(dbResource.Key, dbResource.Value); } } } var cachedResource = _resourceCacheByCulture[cultureName]; return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey]; }
using System.Collections; using System.Collections.Generic; using System.Globalization; namespace DbResourceProvider { public class CultureFallbackProvider : IEnumerable<CultureInfo> { private readonly CultureInfo _startingCulture; private readonly CultureInfo _neutralCulture; private readonly bool _tryParentCulture; public CultureFallbackProvider(CultureInfo startingCulture = null, CultureInfo neutralCulture = null, bool tryParentCulture = true) { _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture; _neutralCulture = neutralCulture; _tryParentCulture = tryParentCulture; } #region Implementation of IEnumerable<CultureInfo> public IEnumerator<CultureInfo> GetEnumerator() { var reachedNeutralCulture = false; var currentCulture = _startingCulture; do { if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name) { yield return CultureInfo.InvariantCulture; reachedNeutralCulture = true; break; } yield return currentCulture; currentCulture = currentCulture.Parent; } while (_tryParentCulture && !HasInvariantCultureName(currentCulture)); if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture) yield break; yield return CultureInfo.InvariantCulture; } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion private bool HasInvariantCultureName(CultureInfo culture) { return culture.Name == CultureInfo.InvariantCulture.Name; } } }
public object GetObject(string resourceKey, CultureInfo culture) { foreach (var currentCulture in new CultureFallbackProvider(culture)) { var value = GetCachedObject(resourceKey, currentCulture.Name); if (value != null) return value; } throw new KeyNotFoundException("The specified 'resourceKey' not found."); }
ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.
دادههای نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:
<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" /> <asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):
<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
میبینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.
کار تولید یک پرووایدر منابع سفارشی دیتابیسی به اتمام رسید. تا اینجا اصول کلی تولید یک پرووایدر سفارشی شرح داده شد. بدین ترتیب میتوان برای هر حالت خاص دیگری نیز پرووایدرهای سفارشی مخصوص ساخت تا مدیریت منابع به آسانی تحت کنترل برنامه نویس قرار گیرد.
اما نکتهای را که باید به آن توجه کنید این است که در پیادهسازیهای نشان داده شده با توجه به نحوه کششدن مقادیر ورودیها، اگر این مقادیر در دیتابیس تغییر کنند، تا زمانیکه سایت ریست نشود این تغییرات در برنامه اعمال نخواهد شد. زیرا همانطور که اشاره شد، مدیریت نمونههای تولیدشده از پرووایدرهای منابع برای هر منبع درخواستی درنهایت برعهده ASP.NET است. بنابراین باید مکانیزمی پیاده شود تا کلاس DbResourceManager از بهروزرسانی ورودیهای کششده اطلاع یابد تا آنها را ریفرش کند.
در ادامه درباره روشهای مختلف نحوه پیادهسازی قابلیت بهروزرسانی ورودیهای منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راهحلهای مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژههای MVC شرح داده میشود.
البته مباحث پیشرفتهتری چون تزریق وابستگی برای پیادهسازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.
منابع
http://msdn.microsoft.com/en-us/library/aa905797.aspx
http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx
http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs
http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider
زمانیکه نیاز است موجودیت A با هیچ و یا حداکثر یک وهله از موجودیت B در ارتباط باشد، به یک چنین رابطهای One-to-Zero-or-One میگویند. برای اینکه یک چنین ارتباطی را تشکیل دهیم، نیاز است کلید اصلی یک جدول، در جدول مرتبط به آن، هم به عنوان کلید اصلی و هم به عنوان کلید خارجی معرفی شود؛ همانند شکل زیر که در آن CartableId، همزمان به صورت PK و FK تعریف شدهاست که به آن one-to-one association with shared primary key نیز میگویند:
الف) مدلسازی رابطهی One-to-Zero-or-One توسط Fluent API
در اینجا دو موجودیت را ملاحظه میکنید که توسط دو navigation property به هم متصل شدهاند:
public class UserProfile { public int UserProfileId { get; set; } public string UserName { get; set; } public virtual Cartable Cartable { get; set; } }
public class Cartable { public int CartableId { get; set; } public virtual UserProfile UserProfile { get; set; } }
public class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=testdb2;Integrated Security = true"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Cartable>(entity => { entity.Property(e => e.CartableId).ValueGeneratedNever(); entity.HasOne(d => d.UserProfile) .WithOne(p => p.Cartable) .HasForeignKey<Cartable>(d => d.CartableId); }); } public virtual DbSet<Cartable> Cartables { get; set; } public virtual DbSet<UserProfile> UserProfiles { get; set; } }
همچنین بر روی CartableId، فراخوانی متد ValueGeneratedNever را مشاهده میکنید. این متد را در قسمت «روشهای مختلف تولید خودکار مقادیر خواص» مطلب «شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکسها» پیشتر بررسی کردیم. هدف آن این است که به بانک اطلاعاتی اعلام کند، این فیلد یک کلید اصلی از نوع خود افزایش یابنده نیست و مقدار آن توسط برنامه به صورت صریح تنظیم میشود (چون کلید خارجی نیز هست و به کلید اصلی جدول سمت دیگر رابطه اشاره میکند).
ب) مدلسازی رابطهی One-to-Zero-or-One توسط Data Annotations
برای تنظیم این رابطه توسط ویژگیها نیاز است DatabaseGenerated به None تنظیم شود تا کلید اصلی CartableId به صورت خودکار توسط بانک اطلاعاتی تولید نشود. همچنین این کلید اصلی نیز باید به صورت کلید خارجی نیز معرفی شود. به علاوه توسط InversePropertyها، باید هر دو سطر به هم مرتبط، ذکر شوند:
public class Cartable { [DatabaseGenerated(DatabaseGeneratedOption.None)] public int CartableId { get; set; } [ForeignKey("CartableId")] [InverseProperty("Cartable")] public virtual UserProfile UserProfile { get; set; } }
public class UserProfile { public int UserProfileId { get; set; } public string UserName { get; set; } [InverseProperty("UserProfile")] public virtual Cartable Cartable { get; set; } }
بررسی رابطهی One-to-One
تشکیل رابطهی One-to-One که در آن برخلاف رابطهی One-to-Zero-or-One، وجود هر دو سر رابطه اجباری هستند، در SQL Server میسر نیست (زیرا زمانیکه یک چنین رابطهای تشکیل شود، نمیتوان رکوردی را Insert کرد! چون زمانیکه هنوز یک سر رابطه ثبت نشدهاست، چگونه میتوان Id آنرا در سر دیگری به اجبار ثبت کرد؟!). SQL Server این رابطه را نیز به صورت همان One-to-Zero-or-One تشکیل میدهد. تنظیمات آن نیز با قبل تفاوتی ندارد. در این حالت اجباری بودن و یا نبودن یک سر رابطه همانند نکات قسمت «تعیین اجباری بودن یا نبودن ستونها در EF Core» در مطلب «شروع به کار با EF Core 1.0 - قسمت 6 - تعیین نوعهای داده و ویژگیهای آنها» است و این تنظیمات در اینجا صرفا از دیدگاه EF Core مفهوم دارند.
جهت تکمیل بحث، روش تشکیل رابطهی One-to-One بدون استفاده از روش به اشتراک گذاری کلید اصلی (one-to-one association with shared primary key) به صورت زیر است:
همانطور که مشاهده میکنید، در اینجا هر بلاگ حداکثر یک تصویر را میتواند داشته باشد. علت آن نیز به ذکر MyBlogForeignKey بر میگردد که یک کلید خارجی نال نپذیر است.
public class MyBlog { public int MyBlogId { get; set; } public string Url { get; set; } public MyBlogImage MyBlogImage { get; set; } } public class MyBlogImage { public int MyBlogImageId { get; set; } public byte[] Image { get; set; } public string Caption { get; set; } public int MyBlogForeignKey { get; set; } public MyBlog MyBlog { get; set; } }
modelBuilder.Entity<MyBlog>() .HasOne(p => p.MyBlogImage) .WithOne(i => i.MyBlog) .HasForeignKey<MyBlogImage>(b => b.MyBlogForeignKey);
این روزها با وجود ORMs ، کوئری SQL نوشتن شبیه به دورانی شده که با وجود زبانهای سطح بالا، عدهای علاقمند هستند با استفاده از زبان اسمبلی برنامه نویسی کنند! WCF RIA Services به صورت پیش فرض از entity framework استفاده میکند (هر چند میتوان از سایر ORMs هم استفاده کرد)، بنابراین عنوان صحیحتر بحث این خواهد بود: چگونه خروجی SQL تولید شده توسط Entity framework را بررسی کنیم؟
الف) استفاده از SQL Server profiler
اولین برنامهای که از سالها قبل، حتی پیش از ظهور ORMs وجود داشته، برنامهی SQL server profiler است، که عموما در مسیر ذیل قابل دستیابی است:
نکته مهم:
حین کار با SQL Server profiler ، ممکن است انبوهی از کوئریهای دیگر مثلا مرتبط با SQL Server agent یا reporting services و غیره نیز لاگ شوند. اما الان ما تنها به کوئریهای برنامهی خود نیاز داریم. برای این منظور به کانکشن استرینگ خود، گزینهی Application Name=My Application Name را نیز اضافه کنید:
<connectionStrings>
<add name="dmEntities" connectionString="metadata=res://*/Models.dmDataModel.csdl|res://*/Models.dmDataModel.ssdl|res://*/Models.dmDataModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=(local);Initial Catalog=dm;Integrated Security=True;Application Name=My Application Name;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />
</connectionStrings>
اکنون اگر برنامه را با پروفایلر مورد بررسی قرار دهید خروجی به صورت زیر خواهد بود:
برای فیلتر کردن Application Name مورد نظر، در ابتدای کار که یک سشن جدید را آغاز میکنید به برگهی events selection مراجعه کرده و بر روی دکمهی column filter کلیک کنید. گزینهی application name را در صفحهی باز شده انتخاب نموده و در قسمت Like آن مطابق تصویر زیر ، نام برنامهی خود را وارد نمائید:
ب) استفاده از IntelliTrace در VS.NET 2010
برنامه را در حالت دیباگ در VS.NET 2010 اجرا کنید. در هر لحظهای میتوان روی گزینهی Break all کلیک کرد و خروجی SQL تولید شده را نیز علاوه بر اطلاعات دیگر مشاهده نمود:
ج) استفاده از برنامهی حرفهای entity framework profiler
این برنامه از هر دو مورد قبل کاملتر بوده و اساسا برای لاگ کردن کوئریها، مدت زمان اجرا، گزارشگیری از وضعیت برنامه، کدامیک از کوئریها سنگینتر هستند، حتی از طریق کدام متد فراخوانی شدهاند، ارائهی گزارشات و راهنماییهایی در مورد چگونگی بهبود کارآیی برنامهی تهیه شده و امثال آن کاربرد دارد.
استفاده از آن هم بسیار ساده است. ابتدا ارجاعی را به اسمبلی HibernatingRhinos.Profiler.Appender.v4.0 به پروژهی ASP.NET خود اضافه کنید (همان پروژهی هوست مربوط به WCF RIA Service ما). سپس به فایل Global.asax.cs برنامه مراجعه کرده و یک سطر ذیل را اضافه کنید:
protected void Application_Start(object sender, EventArgs e)
{
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
}
از این پس تنها کافی است برنامهی پروفایلر در حال اجرا بوده و برنامه شما نیز اجرا شود. کلیهی تبادلات با دیتابیس لاگ خواهند شد.
کار با کلیدهای اصلی و خارجی در EF Code first
- روش صحیح سؤال پرسیدن این است که مشخص کنید چه مدلی، چه کدی، چه مقادیری در حال استفاده هستند تا این خطا رخ داده. (امکان باز تولید یک استثناء رو باید بتونید فراهم کنید)
- این خطا زمانی رخ میده که مقادیر کلید اصلی و کلید خارجی در حال ثبت یکی نباشند.
پیشتر در مورد 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(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>
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)
معرفی پروژه NotifyPropertyWeaver
اگر به Wiki آن مراجعه نمائید، لیست افزونههای قابل توجهی را در مورد آن خواهید یافت که PropertyChanged تنها یکی از آنها است.
پیشنیازها
الف) صفحه پروژه در GitHub
ب) دریافت از طریق نوگت
روش استفاده
پس از نصب بسته نوگت پروژه PropertyChanged.Fody
PM> Install-Package PropertyChanged.Fody
using PropertyChanged; namespace AOP02 { [ImplementPropertyChanged] public class Person { public string Id { set; get; } public string Name { set; get; } } }
------ Build started: Project: AOP02, Configuration: Debug x86 ------ Fody (version 1.13.6.1) Executing Finished Fody 287ms. AOP02 -> D:\Prog\AOP02\bin\Debug\AOP02.exe ========== Build: 1 succeeded or up-to-date, 0 failed, 0 skipped ==========
using System; using System.ComponentModel; using System.Runtime.CompilerServices; namespace AOP02 { public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public string Id { [System.Runtime.CompilerServices.CompilerGenerated] get { return this.<Id>k__BackingField; } [System.Runtime.CompilerServices.CompilerGenerated] set { if (string.Equals(this.<Id>k__BackingField, value, System.StringComparison.Ordinal)) { return; } this.<Id>k__BackingField = value; this.OnPropertyChanged("Id"); } } public string Name { [System.Runtime.CompilerServices.CompilerGenerated] get { return this.<Name>k__BackingField; } [System.Runtime.CompilerServices.CompilerGenerated] set { if (string.Equals(this.<Name>k__BackingField, value, System.StringComparison.Ordinal)) { return; } this.<Name>k__BackingField = value; this.OnPropertyChanged("Name"); } } public virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler propertyChanged = this.PropertyChanged; if (propertyChanged != null) { propertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
using System.Collections.Generic; using System.Drawing; using iTextSharp.text; using PdfRpt.Core.Contracts; namespace PdfReportSamples.HexDump { public class GrayTemplate : ITableTemplate { public HorizontalAlignment HeaderHorizontalAlignment { get { return HorizontalAlignment.Center; } } public BaseColor AlternatingRowBackgroundColor { get { return new BaseColor(Color.WhiteSmoke); } } public BaseColor CellBorderColor { get { return new BaseColor(Color.LightGray); } } public IList<BaseColor> HeaderBackgroundColor { get { return new List<BaseColor> { new BaseColor(ColorTranslator.FromHtml("#990000")), new BaseColor(ColorTranslator.FromHtml("#e80000")) }; } } public BaseColor RowBackgroundColor { get { return null; } } public IList<BaseColor> PreviousPageSummaryRowBackgroundColor { get { return new List<BaseColor> { new BaseColor(Color.LightSkyBlue) }; } } public IList<BaseColor> SummaryRowBackgroundColor { get { return new List<BaseColor> { new BaseColor(Color.LightSteelBlue) }; } } public IList<BaseColor> PageSummaryRowBackgroundColor { get { return new List<BaseColor> { new BaseColor(Color.Yellow) }; } } public BaseColor AlternatingRowFontColor { get { return new BaseColor(ColorTranslator.FromHtml("#333333")); } } public BaseColor HeaderFontColor { get { return new BaseColor(Color.White); } } public BaseColor RowFontColor { get { return new BaseColor(ColorTranslator.FromHtml("#333333")); } } public BaseColor PreviousPageSummaryRowFontColor { get { return new BaseColor(Color.Black); } } public BaseColor SummaryRowFontColor { get { return new BaseColor(Color.Black); } } public BaseColor PageSummaryRowFontColor { get { return new BaseColor(Color.Black); } } public bool ShowGridLines { get { return true; } } } }
.MainTableTemplate(template => { template.CustomTemplate(new GrayTemplate()); })
- در کتابخانه iTextSharp، کلاس رنگ توسط BaseColor تعریف شده است. به همین جهت خروجی رنگها را در اینجا نیز بر اساس BaseColor مشاهده میکنید. اگر نیاز داشتید رنگهای تعریف شده در فضای نام استاندارد System.Drawing را به BaseColor تبدیل کنید، فقط کافی است آنرا به سازنده کلاس BaseColor ارسال نمائید.
- اگر علاقمند هستید که معادل رنگهای HTML ایی را در اینجا داشته باشید، میتوان از متد توکار ColorTranslator.FromHtml استفاده کرد.
- برای تعریف رنگی به صورت شفاف (transparent) آنرا مساوی null قرار دهید.
- در اینترفیس فوق، تعدادی از خروجیها به صورت IList است. در این موارد میتوان یک یا دو رنگ را حداکثر معرفی کرد. اگر دو رنگ را معرفی کنید یک گرادیان خودکار از این دو رنگ، تشکیل خواهد شد.
- اگر قالب جدید زیبایی را طراحی کردید، لطفا در این پروژه مشارکت کرده و آنرا به صورت یک وصله ارائه دهید!
تهیه یک منبع داده ناشناس
مثال زیر را در نظر بگیرید. در اینجا قصد داریم معادل Ascii اطلاعات Hex را تهیه کنیم:
using System; using System.Collections; using System.Linq; namespace PdfReportSamples.HexDump { public static class PrintHex { public static char ToSafeAscii(this int b) { if (b >= 32 && b <= 126) { return (char)b; } return '_'; } public static IEnumerable HexDump(this byte[] data) { int bytesPerLine = 16; return data .Select((c, i) => new { Char = c, Chunk = i / bytesPerLine }) .GroupBy(c => c.Chunk) .Select(g => new { Hex = g.Select(c => String.Format("{0:X2} ", c.Char)).Aggregate((s, i) => s + i), Chars = g.Select(c => ToSafeAscii(c.Char).ToString()).Aggregate((s, i) => s + i) }) .Select((s, i) => new { Offset = String.Format("{0:d6}", i * bytesPerLine), Hex = s.Hex, Chars = s.Chars }); } } }
مفهوم فوق از دات نت 3 به بعد تحت عنوان anonymous types در دسترس است. توسط این قابلیت میتوان یک شیء را بدون نیاز به تعریف ابتدایی آن ایجاد کرد. این نوعهای ناشناس توسط واژههای کلیدی new و var تولید میشوند. کامپایلر به صورت خودکار برای هر anonymous type یک کلاس ایجاد میکند.
نکتهای مهم حین کار با کلاسهای ناشناس:
کلاسهای ناشناس به صورت خودکار توسط کامپایلر تولید میشوند و ... از نوع internal هم تعریف خواهند شد. به عبارتی در اسمبلیهای دیگر قابل استفاده نیستند. البته میتوان توسط ویژگی assembly:InternalsVisibleTo ، تعاریف internal یک اسمبلی را دراختیار اسمبلی دیگری نیز گذاشت. ولی درکل باید به این موضوع دقت داشت و اگر قرار است منبع دادهای به این نحو تعریف شود، بهتر است داخل همان اسمبلی تعاریف گزارش باشد.
برای نمایش این نوع اطلاعات حاصل از کوئریهای LINQ میتوان از منبع داده پیش فرض AnonymousTypeList به نحو زیر استفاده کرد:
using System; using System.Text; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.HexDump { public class HexDumpPdfReport { public IPdfReportData CreatePdfReport() { return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.LeftToRight); doc.Orientation(PageOrientation.Portrait); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" }); }) .DefaultFonts(fonts => { fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\COUR.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.TTF"); }) .PagesFooter(footer => { footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("Hex Dump"); }); }) .MainTableTemplate(template => { template.CustomTemplate(new GrayTemplate()); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); }) .MainTableDataSource(dataSource => { var data = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog."); var list = data.HexDump(); dataSource.AnonymousTypeList(list); }) .MainTableColumns(columns => { columns.AddColumn(column => { column.PropertyName("Offset"); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(0); column.Width(0.5f); column.HeaderCell("Offset"); }); columns.AddColumn(column => { column.PropertyName("Hex"); column.CellsHorizontalAlignment(HorizontalAlignment.Left); column.IsVisible(true); column.Order(1); column.Width(2.5f); column.HeaderCell("Hex"); }); columns.AddColumn(column => { column.PropertyName("Chars"); column.CellsHorizontalAlignment(HorizontalAlignment.Left); column.IsVisible(true); column.Order(2); column.Width(1f); column.HeaderCell("Chars"); }); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\HexDumpSampleRpt.pdf")); } } }
توضیحات:
در اینجا منبع داده بر اساس کلاسهای کمکی که تعریف کردیم، به نحو زیر مشخص شده است:
.MainTableDataSource(dataSource => { var data = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog."); var list = data.HexDump(); dataSource.AnonymousTypeList(list); })
column.PropertyName("Offset"); //... column.PropertyName("Hex"); //... column.PropertyName("Chars");
نکتهای در مورد خواص تودرتو:
در حین استفاده از AnonymousTypeList امکان تعریف خواص تو در تو نیز وجود دارد. برای مثال فرض کنید که Select نهایی به شکل زیر تعریف شده است و در اینجا OrderInfoData نیز خود یک شیء است:
.Select(x => new { OrderInfo = x.OrderInfoData })
column.PropertyName("OrderInfo.Price");
با توجه به ابتدای آموزش، کدهای زیر را در ترمینال وارد میکنیم:
sudo sh -c 'echo "deb [arch=amd64] https://apt-mo.trafficmanager.net/repos/dotnet/ trusty main" > /etc/apt/sources.list.d/dotnetdev.list' sudo apt-key adv --keyserver apt-mo.trafficmanager.net --recv-keys 417A0893 sudo apt-get update
در خط دوم، کلید اختصاصی این پکیج جهت اعتبارسنجی معرفی میگردد که در سایت جاری قبلا به آن پرداخته شده است. در خط آخر هم مخازن موجود را به روزرسانی میکنیم تا آماده استفاده شود.
در سایت مایکروسافت از شما خواسته میشود که کد زیر را وارد کنید:
sudo apt-get install dotnet-dev-1.0.0-preview1-002702
sudo apt-get install dotnet-dev-1.0.0-preview2-003096
The following packages have unmet dependencies: dotnet-dev-1.0.0-preview1-002702 : Depends: dotnet-sharedframework-microsoft.netcore.app-1.0.0-rc2-3002702 but it is not going to be installed E: Unable to correct problems, you have held broken packages.
sudo dpkg -i libicu52_52.1-3ubuntu0.4_amd64.deb
sudo apt-get install dotnet-sharedframework-microsoft.netcore.app-1.0.0-rc2-3002702 sudo apt-get install dotnet-dev-1.0.0-preview2-003096
dotnet --version
1.0.0-preview2-003096