مطالب دوره‌ها
ایجاد یک اسمبلی جدید توسط Reflection.Emit
مطابق استاندارد ECMA-335 قسمت دوم آن، یک اسمبلی از یک یا چند ماژول تشکیل می‌شود. هر ماژول از تعدادی نوع، enum و delegate تشکیل خواهد شد و هر نوع دارای تعدادی متد، فیلد، خاصیت و غیره می‌باشد. به همین جهت در حین کار با Reflection.Emit نیز این مراحل رعایت می‌شوند. ابتدا یک اسمبلی (AppDomain.DefineDynamicAssembly) ایجاد خواهد شد (یا از اسمبلی موجود استفاده می‌شود). سپس یک ماژول (AssemblyBuilder.DefineDynamicModule) را باید به آن اضافه کنیم (یا از ماژول اسمبلی جاری استفاده نمائیم). در ادامه یک Type باید به این ماژول اضافه شود (ModuleBuilder.DefineType) و اکنون می‌توان به این نوع جدید، سازنده (TypeBuilder.DefineConstructor)، متد (TypeBuilder.DefineMethod)، فیلد (TypeBuilder.DefineField)، خاصیت (TypeBuilder.DefineProperty) و رخداد (TypeBuilder.DefineEvent) اضافه کرد.


using System;
using System.Reflection;
using System.Reflection.Emit;

namespace FastReflectionTests
{
    class Program
    {
        static void Main(string[] args)
        {
            var name = "HelloWorld.exe";
            var assemblyName = new AssemblyName(name);
            // ایجاد یک اسمبلی جدید با قابلیت ذخیره سازی آن
            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
                                            name: assemblyName,
                                            access: AssemblyBuilderAccess.RunAndSave);
            // افزودن یک ماژول به اسمبلی
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(name);
            // تعریف یک کلاس در این ماژول
            var programmClass = moduleBuilder.DefineType("Program", TypeAttributes.Public);
            // افزودن یک متد به این کلاس
            // این متد خروجی ندارد اما ورودی آن شبیه به متد اصلی یک برنامه کنسول است
            var mainMethod = programmClass.DefineMethod(name: "Main",
                                            attributes: MethodAttributes.Public | MethodAttributes.Static,
                                            returnType: null,
                                            parameterTypes: new Type[] { typeof(string[]) });
            // تعیین بدنه متد اصلی برنامه
            var il = mainMethod.GetILGenerator();
            il.Emit(OpCodes.Ldstr, "Hello World!");
            il.Emit(OpCodes.Call, (typeof(Console)).GetMethod("WriteLine", new Type[] { typeof(string) }));
            il.Emit(OpCodes.Call, (typeof(Console)).GetMethod("ReadKey", new Type[0]));
            il.Emit(OpCodes.Pop);
            il.Emit(OpCodes.Ret);

            // تکمیل کار ایجاد نوع جدید
            programmClass.CreateType();

            // تعیین نقطه شروع فایل اجرایی برنامه کنسول تهیه شده
            assemblyBuilder.SetEntryPoint(((Type)programmClass).GetMethod("Main"));

            // ذخیره سازی این اسمبلی بر روی دیسک سخت
            assemblyBuilder.Save(name);
        }
    }
}
مراحلی را که توضیح داده شد، در کدهای فوق ملاحظه می‌کنید. انتخاب حالت دسترسی AssemblyBuilderAccess.RunAndSave سبب می‌شود تا بتوان نتیجه حاصل را ذخیره کرد. فایل Exe نهایی را اگر در برنامه ILSpy باز کنیم چنین شکلی دارد:
using System;
public class Program
{
    public static void Main(string[] array)
    {
       Console.WriteLine("Hello World!");
       Console.ReadKey();
    }
}
مطالب
پردازش داده‌های جغرافیایی به کمک SQL Server و Entity framework
پشتیبانی SQL Server از Spatial data

از SQL Server 2008 به بعد، نوع داده جدیدی به نام geography به نوع‌های قابل تعریف ستون‌ها اضافه شده‌است. در این نوع ستون‌ها می‌توان طول و عرض جغرافیایی یک نقطه را ذخیره کرد و سپس به کمک توابع توکاری از آن‌ها کوئری گرفت.


در اینجا نمونه‌ای از نحوه‌ی تعریف و همچنین مقدار دهی این نوع ستون‌ها را مشاهده می‌کنید:
 CREATE TABLE [Geo](
[id] [int] IDENTITY(1,1) NOT NULL,
[Location] [geography] NULL
)

 insert into Geo( Location , long, lat ) values
( geography::STGeomFromText ('POINT(-121.527200 45.712113)', 4326))
متد geography::STGeoFromText یک SQL CLR function است. این متد در مثال فوق، مختصات یک نقطه را دریافت کرده‌است. همچنین نیاز دارد بداند که این نقطه توسط چه نوع سیستم مختصاتی ارائه می‌شود. عدد 4326 در اینجا یک SRID یا Spatial Reference System Identifier استاندارد است. برای نمونه اطلاعات ارائه شده توسط Google و یا Bing توسط این استاندارد ارائه می‌شوند.
در اینجا متدهای توکار دیگری مانند geography::STDistance برای یافتن فاصله مستقیم بین نقاط نیز ارائه شد‌ه‌اند. خروجی آن بر حسب متر است.


پشتیبانی از Spatial Data در Entity framework

پشتیبانی از نوع مخصوص geography، در EF 5 توسط نوع داده‌ای DbGeography ارائه شد. این نوع داده‌ای immutable است. به این معنا که پس از نمونه سازی، دیگر مقدار آن قابل تغییر نیست.
در اینجا برای نمونه مدلی را مشاهده می‌کنید که از نوع داده‌ای DbGeography استفاده می‌کند:
using System.Data.Entity.Spatial;

namespace EFGeoTests.Models
{
    public class GeoLocation
    {
        public int Id { get; set; }
        public DbGeography Location { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }

        public override string ToString()
        {
            return string.Format("Name:{0}, Location:{1}", Name, Location);
        }
    }
}
به همراه یک Context، تا کلاس GeoLocation در معرض دید EF قرار گیرد:
using System;
using System.Data.Entity;
using EFGeoTests.Models;

namespace EFGeoTests.Config
{
    public class MyContext : DbContext
    {
        public DbSet<GeoLocation> GeoLocations { get; set; }

        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
}
برای مقدار دهی خاصیت Location از نوع DbGeography می‌توان از متد ذیل استفاده کرد که بسیار شبیه به متد geography::STGeoFromText عمل می‌کند:
   private static DbGeography createPoint(double longitude, double latitude,  int coordinateSystemId = 4326)
  {
       var text = string.Format(CultureInfo.InvariantCulture.NumberFormat,"POINT({0} {1})", longitude, latitude);
       return DbGeography.PointFromText(text, coordinateSystemId);
  }


تهیه منبع داده‌ی جغرافیایی

برای تدارک یک مثال واقعی جغرافیایی، نیاز به اطلاعاتی دقیق داریم. این نوع اطلاعات عموما توسط یک سری فایل مخصوص به نام Shapefiles که حاوی اطلاعات برداری جغرافیایی هستند ارائه می‌شوند. برای نمونه اطلاعات جغرافیایی به روز ایران را از آدرس ذیل می‌توانید دریافت کنید:
http://download.geofabrik.de/asia/iran.html
http://download.geofabrik.de/asia/iran-latest.shp.zip

پس از دریافت این فایل، به تعدادی فایل با پسوندهای shp، shx و dbf خواهیم رسید.
فایل‌های shp بیانگر فرمت اشکال ذخیره شده هستند. فایل‌های shx یک سری ایندکس بوده و فایل‌های dbf از نوع بانک اطلاعاتی dBase IV می‌باشند.
همچنین اگر فایل‌های prj را باز کنید، یک چنین اطلاعاتی در آن موجودند:
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
نکته‌ی مهمی که در اینجا باید مدنظر داشت، استاندارد GCS_WGS_1984 آن است. این استاندارد معادل است با استاندارد EPSG 4326. عدد 4326 آن جهت ثبت این اطلاعات در یک بانک اطلاعاتی SQL Server حائز اهمیت است (پارامتر coordinateSystemId در متد createPoint) و ممکن است از هر فایلی به فایل دیگر متفاوت باشد.



خواند‌ن فایل‌های shp در دات نت

پس از دریافت فایل‌های shp و بانک‌های اطلاعاتی مرتبط با اطلاعات جغرافیایی ایران، اکنون نوبت به پردازش این فایل‌های مخصوص با فرمت بانک اطلاعاتی فاکس پرو مانند، رسیده‌است. برای این منظور می‌توان از پروژه‌ی سورس باز ذیل استفاده کرد:

این پروژه در خواندن فایل‌های shp بدون نقص عمل می‌کند اما توانایی خواندن نام‌های فارسی وارد شده در این نوع بانک‌های اطلاعاتی را ندارد. برای رفع این مشکل، سورس آن را از Codeplex دریافت کنید. سپس فایل Shapefile.cs را گشوده و ابتدای خاصیت Current آن‌را به نحو ذیل تغییر دهید:
        /// <summary>
        /// Gets the current shape in the collection
        /// </summary>
        public Shape Current
        {
            get 
            {
                if (_disposed) throw new ObjectDisposedException("Shapefile");
                if (!_opened) throw new InvalidOperationException("Shapefile not open.");
               
                // get the metadata
                StringDictionary metadata = null;
                if (!RawMetadataOnly)
                {
                    metadata = new StringDictionary();
                    for (int i = 0; i < _dbReader.FieldCount; i++)
                    {
                        string value = _dbReader.GetValue(i).ToString();
                        if (_dbReader.GetDataTypeName(i) == "DBTYPE_WVARCHAR")
                        {
                            // برای نمایش متون فارسی نیاز است
                            value = Encoding.UTF8.GetString(Encoding.GetEncoding(720).GetBytes(value));
                        }
                        metadata.Add(_dbReader.GetName(i),
                            value);
                    }
                }
در اینجا فقط سطر استفاده از Encoding خاصی با شماره 720 و تبدیل آن به UTF8 اضافه شده‌است. پس از آن بدون مشکل می‌توان برچسب‌های فارسی را از فایل‌های dBase IV این نوع بانک‌های اطلاعاتی استخراج کرد (اصلاح شده‌ی آن در فایل پیوست مطلب موجود است).
using System.Collections.Generic;
using System.Linq;
using Catfood.Shapefile;

namespace EFGeoTests
{
    public class MapPoint
    {
        public Dictionary<string, string> Metadata { set; get; }
        public double X { set; get; }
        public double Y { set; get; }
    }

    public static class ShapeReader
    {
        public static IList<MapPoint> ReadShapeFile(string path)
        {
            var results = new List<MapPoint>();

            using (var shapefile = new Shapefile(path))
            {
                foreach (var shape in shapefile)
                {
                    if (shape.Type != ShapeType.Point)
                        continue;

                    var shapePoint = shape as ShapePoint;
                    if (shapePoint == null)
                        continue;


                     var metadataNames = shape.GetMetadataNames();
                    if(!metadataNames.Any())
                        continue;

                    var metadata = new Dictionary<string, string>();
                    foreach (var metadataName in metadataNames)
                    {
                        metadata.Add(metadataName,shape.GetMetadata(metadataName));
                    }

                    results.Add(new MapPoint
                    {
                        Metadata = metadata,
                        X = shapePoint.Point.X,
                        Y = shapePoint.Point.Y
                    });
                }
            }

            return results;
        }
    }
}
در کدهای فوق به کمک کتابخانه‌ی C# Esri Shapefile Reader، اطلاعات نقاط بانک اطلاعاتی shape files را خوانده و به صورت لیست‌هایی از MapPoint بازگشت می‌دهیم. نکته‌ی مهم آن، Metadata است که از هر فایلی به فایل دیگر می‌توان متفاوت باشد. به همین جهت این اطلاعات را به شکل ویژگی‌های key/value در این نوع بانک‌های اطلاعاتی ذخیره می‌کنند.


افزودن اطلاعات جغرافیایی به بانک اطلاعاتی SQL Server به کمک Entity framework

فایل places.shp را در مجموعه فایل‌هایی که در ابتدای بحث عنوان شدند، می‌توانید مشاهده کنید. قصد داریم اطلاعات نقاط آن‌را به مدل GeoLocation انتساب داده و سپس ذخیره کنیم:
            var points = ShapeReader.ReadShapeFile("IranShapeFiles\\places.shp");
            using (var context = new MyContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false;
                context.Configuration.ProxyCreationEnabled = false;
                context.Configuration.ValidateOnSaveEnabled = false;

                if (context.GeoLocations.Any())
                    return;

                foreach (var point in points)
                {
                    context.GeoLocations.Add(new GeoLocation
                    {
                        Name = point.Metadata["name"],
                        Type = point.Metadata["type"],
                        Location = createPoint(point.X, point.Y)
                    });
                }

                context.SaveChanges();
            }
تعریف متد createPoint را که بر اساس X و Y نقاط، معادل قابل پذیرش آن‌را جهت SQL Server تهیه می‌کند، در ابتدای بحث مشاهده کردید.
در فایل‌های مرتبط با places.shp، متادیتا name، معادل نام شهرهای ایران است و type آن بیانگر شهر، روستا و امثال آن می‌باشد.
پس از اینکه اطلاعات مکان‌های ایران، در SQL Server ذخیره شدند، نمایش بصری آن‌ها را در management studio نیز می‌توان مشاهده کرد:



کوئری گرفتن از اطلاعات جغرافیایی

فرض کنید می‌خواهیم مکان‌هایی را با فاصله کمتر از 5 کیلومتر از تهران پیدا کنیم:
            var tehran = createPoint(51.4179604, 35.6884243);

            using (var context = new MyContext())
            {
                // find any locations within 5 kilometers ordered by distance
                var locations = context.GeoLocations
                    .Where(loc => loc.Location.Distance(tehran) < 5000)
                    .OrderBy(loc => loc.Location.Distance(tehran))
                    .ToList();

                foreach (var location in locations)
                {
                    Console.WriteLine(location.Name);
                }
            }
همانطور که پیشتر نیز عنوان شد، متد Distance بر اساس متر کار می‌کند. به همین جهت برای تعریف 5 کیلومتر به نحو فوق عمل شده‌است. همچنین نحوه‌ی مرتب سازی اطلاعات نیز بر اساس فاصله از یک مکان مشخص صورت گرفته‌است.
و یا اگر بخواهیم دقیقا بر اساس مختصات یک نقطه، مکانی را بیابیم، می‌توان از متد SpatialEquals استفاده کرد:
            var tehran = createPoint(51.4179604, 35.6884243);
            using (var context = new MyContext())
            {
                // find any locations within 5 kilometers ordered by distance
                var tehranLocation = context.GeoLocations.FirstOrDefault(loc => loc.Location.SpatialEquals(tehran));
                if (tehranLocation != null)
                {
                    Console.WriteLine(tehranLocation.Type);
                }
            }

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
EFGeoTests.zip
 
اشتراک‌ها
پیاده سازی یک Filter سفارشی برای نگاشت استثنای همزمانی به خطاهای MadelState
 public class HandleConcurrencyExceptionAttribute : FilterAttribute, IExceptionFilter
    {
        private PropertyMatchingMode _propertyMatchingMode;
        /// <summary>
        /// This defines when the concurrencyexception happens, 
        /// </summary>
        public enum PropertyMatchingMode
        {
            /// <summary>
            /// Uses only the field names in the model to check against the entity. This option is best when you are using 
            /// View Models with limited fields as opposed to an entity that has many fields. The ViewModel (or model) field names will
            /// be used to check current posted values vs. db values on the entity itself.
            /// </summary>
            UseViewModelNamesToCheckEntity = 0,
            /// <summary>
            /// Use any non-matching value fields on the entity (except timestamp fields) to add errors to the ModelState.
            /// </summary>
            UseEntityFieldsOnly = 1,
            /// <summary>
            /// Tells the filter to not attempt to add field differences to the model state.
            /// This means the end user will not see the specifics of which fields caused issues
            /// </summary>
            DontDisplayFieldClashes = 2
        }


        public HandleConcurrencyExceptionAttribute()
        {
            _propertyMatchingMode = PropertyMatchingMode.UseViewModelNamesToCheckEntity;
        }

        public HandleConcurrencyExceptionAttribute(PropertyMatchingMode propertyMatchingMode)
        {
            _propertyMatchingMode = propertyMatchingMode;
        }


        /// <summary>
        /// The main method, called by the mvc runtime when an exception has occured.
        /// This must be added as a global filter, or as an attribute on a class or action method.
        /// </summary>
        /// <param name="filterContext"></param>
        public void OnException(ExceptionContext filterContext)
        {
            if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException)
            {
                //Get original and current entity values
                DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception;
                var entry = ex.Entries.Single();
                //problems with ef4.1/4.2 here because of context/model in different projects.
                //var databaseValues = entry.CurrentValues.Clone().ToObject();
                //var clientValues = entry.Entity;
                //So - if using EF 4.1/4.2 you may use this workaround
                var clientValues = entry.CurrentValues.Clone().ToObject();
                entry.Reload();
                var databaseValues = entry.CurrentValues.ToObject();

                List<string> propertyNames;

                filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. The "
                        + "edit operation was canceled and the current values in the database "
                        + "have been displayed. If you still want to edit this record, click "
                        + "the Save button again to cause your changes to be the current saved values.");
                PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);

                if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity)
                {
                    //We dont have access to the model here on an exception. Get the field names from modelstate:
                    propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList();
                }
                else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly)
                {
                    propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList();
                }
                else
                {
                    filterContext.ExceptionHandled = true;
                    UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
                    filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
                    return;
                }



                UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);

                //Get all public properties of the entity that have names matching those in our modelstate.
                foreach (var propertyInfo in entityFromDbProperties)
                {

                    //If this value is not in the ModelState values, don't compare it as we don't want
                    //to attempt to emit model errors for fields that don't exist.

                    //Compare db value to the current value from the entity we posted.

                    if (propertyNames.Contains(propertyInfo.Name))
                    {
                        if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
                        {
                            var currentValue = propertyInfo.GetValue(databaseValues, null);
                            if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
                            {
                                currentValue = "Empty";
                            }

                            filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Current value: "
                                 + currentValue);
                        }
                    }

                    //TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved?
                    //The problem here is we may only have a few fields used in the viewmodel, but many in the entity
                    //so we could have a problem here with that.
                    //object o = propertyInfo.GetValue(myObject, null);
                }

                filterContext.ExceptionHandled = true;

                filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
            }
        }
پیاده سازی یک Filter سفارشی برای نگاشت استثنای همزمانی به خطاهای MadelState
مطالب
ASP.NET MVC #15

فیلترها در ASP.NET MVC

پایه قسمت‌های بعدی مانند مباحث امنیت، اعتبار سنجی کاربران، caching و غیره، مبحثی است به نام فیلترها در ASP.NET MVC. تابحال با سه فیلتر به نام‌های ActionName، NonAction و AcceptVerbs آشنا شده‌ایم. به این‌ها Action selector filters هم گفته می‌شود. زمانیکه قرار است یک درخواست رسیده به متدی در یک کنترلر خاص نگاشت شود،‌ فریم ورک ابتدا به متادیتای اعمالی به متدها توجه کرده و بر این اساس درخواست را به متدی صحیح هدایت خواهد کرد. ActionName، نام پیش فرض یک متد را بازنویسی می‌کند و توسط AcceptVerbs اجرای یک متد، به افعالی مانند POST، GET، DELETE و امثال آن محدود می‌شود که در قسمت‌های قبل در مورد آن‌ها بحث شد.
علاوه بر این‌ها یک سری فیلتر دیگر نیز در ASP.NET MVC وجود دارند که آن‌ها نیز به شکل متادیتا به متدهای کنترلرها اعمال شده و کار نهایی‌اشان تزریق کدهایی است که باید پیش و پس از اجرای یک اکشن متد،‌ اجرا شوند. 4 نوع فیلتر در ASP.NET MVC وجود دارند:
الف) IAuthorizationFilter
این نوع فیلترها پیش از اجرای هر متد یا فیلتر دیگری در کنترلر جاری اجرا شده و امکان لغو اجرای آن‌را فراهم می‌کنند. پیاده سازی پیش‌فرض آن توسط کلاس AuthorizeAttribute در فریم ورک وجود دارد.
بدیهی است این نوع اعمال را مستقیما داخل متدهای کنترلرها نیز می‌توان انجام داد (بدون نیاز به هیچگونه فیلتری). اما به این ترتیب حجم کدهای تکراری در سراسر برنامه به شدت افزایش می‌یابد و نگهداری آن‌را در طول زمان مشکل خواهد ساخت.

ب) IActionFilter
ActionFilterها پیش (OnActionExecuting) و پس از (OnActionExecuted) اجرای متدهای کنترلر جاری اجرا می‌شوند و همچنین پیش از ارائه خروجی نهایی متدها. به این ترتیب برای مثال می‌توان نحوه رندر یک View را تحت کنترل گرفت. این اینترفیس توسط کلاس ActionFilterAttribute در فریم ورک پیاده سازی شده است.

ج) IResultFilter
ResultFilter بسیار شبیه به ActionFilter است با این تفاوت که تنها پیش از (OnResultExecuting) بازگرداندن نتیجه متد و همچنین پس از (OnResultExecuted) اجرای متد، فراخوانی می‌گردد. کلاس ActionFilterAttribute موجود در فریم ورک، پیاده سازی پیش فرضی از آن‌‌را ارائه می‌دهد.

د) IExceptionFilter
ExceptionFilterها پس از اجرای تمامی فیلترهای دیگر، همواره اجرا خواهند شد؛ صرفنظر از اینکه آیا در این بین استثنایی رخ داده است یا خیر. بنابراین یکی از کاربردهای آن‌ها می‌تواند ثبت وقایع مرتبط با استثناهای رخ‌داده باشد. پیاده سازی پیش فرض آن توسط کلاس HandleErrorAttribute در فریم ورک موجود است.

علت معرفی 4 نوع فیلتر متفاوت هم به مسایل امنیتی بر می‌گردد. می‌شد تنها موارد ب و ج معرفی شوند اما از آنجائیکه نیاز است مورد الف همواره پیش از اجرای متدی و همچنین تمامی فیلترهای دیگر فراخوانی شود، احتمال بروز اشتباه در نحوه و ترتیب معرفی این فیلترها وجود داشت. به همین دلیل روش معرفی صریح مورد الف در پیش گرفته شد. برای مثال فرض کنید که اگر از روی اشتباه فیلتر کش شدن اطلاعات پیش از فیلتر اعتبار سنجی کاربر جاری اجرا می‌شد چه مشکلات امنیتی ممکن بود بروز کند.

مثالی جهت درک بهتر ترتیب و نحوه اجرای فیلترها:

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس فیلتر سفارشی زیر را به برنامه اضافه نمائید:

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class LogAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Log("OnActionExecuting", filterContext);
}

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
Log("OnActionExecuted", filterContext);
}

public override void OnResultExecuting(ResultExecutingContext filterContext)
{
Log("OnResultExecuting", filterContext);
}

public override void OnResultExecuted(ResultExecutedContext filterContext)
{
Log("OnResultExecuted", filterContext);
}

private void Log(string stage, ControllerContext ctx)
{
ctx.HttpContext.Response.Write(
string.Format("{0}:{1} - {2} < br/> ",
ctx.RouteData.Values["controller"], ctx.RouteData.Values["action"], stage));
}
}
}

مرسوم است برای ایجاد فیلترهای سفارشی، همانند مثال فوق با ارث بری از پیاده سازی‌های توکار اینترفیس‌های چهارگانه یاد شده، کار شروع شود.
سپس یک کنترلر جدید را به همراه دو متد، به برنامه اضافه نمائید. برای هر کدام از متدها هم یک View خالی را ایجاد کنید. اکنون این ویژگی جدید را به هر کدام از این متدها اعمال نموده و برنامه را اجرا کنید.

using System.Web.Mvc;
using MvcApplication12.CustomFilters;

namespace MvcApplication12.Controllers
{
public class HomeController : Controller
{
[Log]
public ActionResult Index()
{
return View();
}

[Log]
public ActionResult Test()
{
return View();
}
}
}

سپس ویژگی Log را از متدها حذف کرده و به خود کنترلر اعمال کنید:
[Log]
public class HomeController : Controller

در این حالت ویژگی اعمالی، پیش از اجرای متد درخواستی جاری اجرا خواهد شد یا به عبارتی به تمام متدهای قابل دسترسی کنترلر اعمال می‌گردد.


تقدم و تاخر اجرای فیلترهای هم‌خانواده

همانطور که عنوان شد، همیشه ابتدا AuthorizationFilter اجرا می‌شود و در آخر ExceptionFilter. سؤال: اگر در این بین مثلا دو نوع ActionFilter متفاوت به یک متد اعمال شدند، کدامیک ابتدا اجرا می‌شود؟
تمام فیلترها از کلاسی به نام FilterAttribute مشتق می‌شوند که دارای خاصیتی است به نام Order. بنابراین جهت مشخص سازی ترتیب اجرای فیلترها تنها کافی است این خاصیت مقدار دهی شود. برای مثال جهت اعمال دو فیلتر سفارشی زیر:

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class AuthorizationFilterA : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
Debug.WriteLine("OnAuthorization : AuthorizationFilterA");
}
}
}

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class AuthorizationFilterB : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
Debug.WriteLine("OnAuthorization : AuthorizationFilterB");
}
}
}

خواهیم داشت:
using System.Web.Mvc;
using MvcApplication12.CustomFilters;

namespace MvcApplication12.Controllers
{
public class HomeController : Controller
{
[AuthorizationFilterA(Order = 2)]
[AuthorizationFilterB(Order = 1)]
public ActionResult Index()
{
return View();
}
}
}

در اینجا با توجه به مقادیر order، ابتدا AuthorizationFilterB اجرا می‌گردد و سپس AuthorizationFilterA.
علاوه بر این‌ها محدوده اجرای فیلترها نیز بر بر این حق تقدم اجرایی تاثیر گذار هستند. برای مثال در پشت صحنه زمانیکه قرار است یک فیلتر جدید اجرا شود، وهله سازی آن به نحوه زیر است که بر اساس مقادیر order و FilterScope صورت می‌گیرد:
var filter = new Filter(actionFilter, FilterScope, order);

مقادیر FilterScope را در ادامه ملاحظه می‌نمائید:
namespace System.Web.Mvc { 
public enum FilterScope {
First = 0,
Global = 10,
Controller = 20,
Action = 30,
Last = 100,
}
}

به صورت پیش فرض، ابتدا فیلتری با محدوده اجرای کمتر، اجرا خواهد شد. در اینجا Global به معنای اجرای شدن در تمام کنترلرها است.


تعریف فیلترهای سراسری

برای اینکه فیلتری را عمومی و سراسری تعریف کنیم، تنها کافی است آن‌را در متد Application_Start فایل Global.asax.cs به نحو زیر معرفی نمائیم:

GobalFilters.Filters.Add(new AuthorizationFilterA() { Order = 2});

به این ترتیب AuthorizationFilterA، به تمام کنترلرها و متدهای قابل دسترسی آن‌ها در برنامه به صورت خودکار اعمال خواهد شد.
یکی از کاربردهای فیلترهای سراسری، نوشتن برنامه‌های پروفایلر است. برنامه‌هایی که برای مثال مدت زمان اجرای متدها را ثبت کرده و بر این اساس بهتر می‌توان کارآیی قسمت‌های مختلف برنامه را دقیقا زیرنظر قرار داد.


یک نکته
کلاس کنترلر در ASP.NET MVC نیز یک فیلتر است:
public abstract class Controller : ControllerBase, IActionFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter

به همین دلیل، امکان تحریف متدهای OnActionExecuting، OnActionExecuted و امثال آن که پیشتر ذکر شد، در یک کنترلر نیز وجود دارد.
کلاس کنترلر دارای محدوده اجرایی First و Order ایی مساوی Int32.MinValue است. به این ترتیب کنترلرها پیش از اجرای هر فیلتر دیگری اجرا خواهند شد.


ASP.NET MVC دارای یک سری فیلتر و متادیتای توکار مانند OutputCache، HandleError، RequireHttps، ValidateInpute و غیره است که توضیحات بیشتر آن‌ها به قسمت‌های بعد موکول می‌گردد.

مطالب
ایجاد کپچایی (captcha) سریع و ساده در ASP.NET MVC 5

در این مثال به کمک MVC5، یک کپچای ساده و قابل فهم را تولید و استفاده خواهیم کرد. این نوشته بر اساس این مقاله  ایجاد شده و جزئیات زیادی برای درک افراد مبتدی به آن افزوده شده است که امیدوارم راهنمای مفیدی برای علاقمندان باشد.

با کلیک راست بر روی پوشه کنترلر، یک کنترلر به منظور ایجاد کپچا بسازید و اکشن متد زیر را در آن کنترلر ایجاد کنید: 

public class CaptchaController : Controller
    {
        public ActionResult CaptchaImage(string prefix, bool noisy = true)
        {
            var rand = new Random((int)DateTime.Now.Ticks);
            //generate new question
            int a = rand.Next(10, 99);
            int b = rand.Next(0, 9);
            var captcha = string.Format("{0} + {1} = ?", a, b);

            //store answer
            Session["Captcha" + prefix] = a + b;

            //image stream
            FileContentResult img = null;

            using (var mem = new MemoryStream())
            using (var bmp = new Bitmap(130, 30))
            using (var gfx = Graphics.FromImage((Image)bmp))
            {
                gfx.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
                gfx.SmoothingMode = SmoothingMode.AntiAlias;
                gfx.FillRectangle(Brushes.White, new Rectangle(0, 0, bmp.Width, bmp.Height));

                //add noise
                if (noisy)
                {
                    int i, r, x, y;
                    var pen = new Pen(Color.Yellow);
                    for (i = 1; i < 10; i++)
                    {
                        pen.Color = Color.FromArgb(
                        (rand.Next(0, 255)),
                        (rand.Next(0, 255)),
                        (rand.Next(0, 255)));

                        r = rand.Next(0, (130 / 3));
                        x = rand.Next(0, 130);
                        y = rand.Next(0, 30);

                        gfx.DrawEllipse(pen, x - r, y - r, r, r);
                    }
                }

                //add question
                gfx.DrawString(captcha, new Font("Tahoma", 15), Brushes.Gray, 2, 3);

                //render as Jpeg
                bmp.Save(mem, System.Drawing.Imaging.ImageFormat.Jpeg);
                img = this.File(mem.GetBuffer(), "image/Jpeg");
            }

            return img;
        }

همانطور که از کد فوق پیداست، دو مقدار a و b، به شکل اتفاقی ایجاد می‌شوند و حاصل جمع آنها در یک Session نگهداری خواهد شد. سپس تصویری بر اساس تصویر a+b ایجاد می‌شود (مثل 3+4). این تصویر خروجی این اکشن متد است. به سادگی می‌توانید این اکشن را بر اساس خواسته خود اصلاح کنید؛ مثلا به جای حاصل جمع دو عدد، از کاربرد چند حرف یا عدد که بصورت اتفاقی تولید کرده‌اید، استفاده نمائید.

فرض کنید می‌خواهیم کپچا را هنگام ثبت نام استفاده کنیم.

در فایل AccountViewModels.cs در پوشه مدل‌ها در کلاس RegisterViewModel  خاصیت زیر را اضافه کنید:

[Required(ErrorMessage = "لطفا {0} را وارد کنید")]
         [Display(Name = "حاصل جمع")]
         public string Captcha { get; set; }

حالا در پوشه View/Account به فایل Register.Cshtml خاصیت فوق را اضافه کنید:

<div class="form-group">
                        <input type="button" value="" id="refresh" />

                        @Html.LabelFor(model => model.Captcha)

                        <img alt="Captcha" id="imgcpatcha" src="@Url.Action("CaptchaImage","Captcha")" style="" />
                    </div>

وظیفه این بخش، نمایش کپچاست. تگ img دارای آدرسی است که توسط اکشن متدی که در ابتدای این مقاله ایجاد نموده‌ایم تولید می‌شود. این آدرس تصویر کپچاست.  یک دکمه هم با شناسه refresh برای به روز رسانی مجدد تصویر در نظر گرفته‌ایم. 

حالا کد ایجکسی برای آپدیت کپچا توسط دکمه refresh را  به شکل زیر بنویسید (من در پایین ویوی Register، اسکریپت زیر را قرار دادم): 

<script type="text/javascript">
    $(function () {
        $('#refresh').click(function () {


            $.ajax({
                url: '@Url.Action("CaptchaImage","Captcha")',
                type: "GET",
                data: null
            })
            .done(function (functionResult) {
                $("#imgcpatcha").attr("src", "/Captcha/CaptchaImage?" + functionResult);
            });

        });
    });
</script>

آنچه در url نوشته شده است، شاید اصولی‌ترین شکل فراخوانی یک اکشن متد باشد. این اکشن در ابتدای مقاله تحت کنترلری به نام Captcha معرفی شده بود و خروجی آن آدرس یک فایل تصویری است. نوع ارتباط، Get است و هیچ اطلاعاتی به اکشن متد فرستاده نمیشود، اما اکشن متد ما آدرسی را به ما برمی‌گرداند که تحت نام FunctionResult آن را دریافت کرده و به کمک کد جی کوئری، مقدارش را در ویژگی src تصویر موجود در صفحه جاری جایگزین می‌کنیم. دقت کنید که برای دسترسی به تصویر، لازم است جایگزینی آدرس، در ویژگی src به شکل فوق صورت پذیرد.*

تنها کار باقیمانده اضافه کردن کد زیر به ابتدای اکشن متد Register درون کنترلر Account است. 

if (Session["Captcha"] == null || Session["Captcha"].ToString() != model.Captcha)
            {
                ModelState.AddModelError("Captcha", "مجموع اشتباه است");
            }

واضح است که اینکار پیش از شرط if(ModelState.IsValidate) صورت میگیرد و وظیفه شرط فوق، بررسی ِ برابریِ مقدار Session تولید شده در اکشن CaptchaImage  (ابتدای این مقاله) با مقدار ورودی کاربر است. (مقداری که از طریق خاصیت تولیدی خودمان  به آن دسترسی داریم) . بدیهی‌است اگر این دو مقدار نابرابر باشند، یک خطا به ModelState اضافه می‌شود و شرط ModelState.IsValid که در اولین خط بعد از کد فوق وجود دارد، برقرار نخواهد بود و پیغام خطا در صفحه ثبت نام نمایش داده خواهد شد.

تصویر زیر نمونه‌ی نتیجه‌ای است که حاصل خواهد شد  :


* اصلاح : دقت کنید بدون استفاده از ایجکس هم میتوانید تصویر فوق را آپدیت کنید:

  $('#refresh').click(function () {
         
            var d = new Date();
            $("#imgcpatcha").attr("src", "Captcha/CaptchaImage?" + d.getTime());

        });

رویداد کلیک را با کد فوق جایگزین کنید؛ دو نکته در اینجا وجود دارد :

یک. استفاده از زمان در انتهای آدرس به خاطر مشکلاتیست که فایرفاکس یا IE با اینگونه آپدیت‌های تصویری دارند. این دو مرورگر (بر خلاف کروم) تصاویر را نگهداری میکنند و آپدیت به روش فوق به مشکل برخورد میکند مگر آنکه آدرس را به کمک اضافه کردن زمان آپدیت کنید تا مرورگر متوجه داستان شود

دو. همانطور که میبینید آدرس تصویر در حقیقت خروجی یک اکشن است. پس نیازی نیست هر بار این اکشن را به کمک ایجکس صدا بزنیم و روش فوق در مرورگرهای مختلف جواب خواهد داد.

بازخوردهای پروژه‌ها
تشخیص بات یا سایر خزنده‌ها
ضمن تشکر از شما بابت به اشتراک گذاشتن پروژه، به نظرم تنها استفاده از context.Request.Browser.Crawler برای تشخیص اینکه آیا درخواست رسیده، خزنده هست یا خیر، کافی نیست. شاید بتوان در کنار و علاوه بر این کنترل، یک شرط دیگر هم به نحو زیر اضافه کرد و در نظر گرفت:
private static readonly List<string> KnownCrawlers = new List<string>
{
    "bot","crawler","baiduspider","80legs","ia_archiver","ahrefsBot","twitterbot",
    "yoozbot","yandexBot","bitlybot","other", "sogou web spider", "python requests",
    "voyager","curl","wget","yahoo! slurp","mediapartners-google", "mj12bot",
    "seznamBot", "Sogou web spider", "360Spider", "sogouwebspider"
};
public static bool IsBotOrCrawler(string agent)
{
    agent = agent.ToLower();
    return KnownCrawlers.Any(crawler => agent.Contains(crawler) || agent.Equals(crawler));
}
می‌توان به مرور، لیست خزنده‌ها را تکمیل کرد.
مطالب
روش ذخیره‌ی لاگ‌های ILogger در پایگاه داده در Blazor

مقدمه

همانطور که می‌دانید، Blazor دارای یک سیستم لاگ گیری توکار است که می‌توان از آن توسط تزریق ILogger در کامپوننت‌ها بهره برد. این سیستم لاگ گیری در زمان توسعه‌ی نرم افزار، در قالب یک کنسول، لاگ‌ها را به توسعه دهنده نشان می‌دهد. اما پس از تولید و پابلیش اپلیکیشن، دیگر این کنسول وجود ندارد. برای ذخیره‌ی لاگ‌ها در یک فایل متنی بر روی سرور هاست، می‌توان از Serilog بهره برد که روش آن در اینجا  توضیح داده شده است. حال اگر بخواهیم این لاگ‌ها را در یک پایگاه داده ذخیره کنیم چطور؟

ایجاد کلاس لاگ

برای این منظور ابتدا کلاسی را برای ذخیره‌ی لاگ‌ها در پایگاه داده به شکل زیر ایجاد می‌نماییم:
public class DBLog
    {
        public int  DBLogId { get; set; }
        public string? LogLevel { get; set; }
        public string? EventName { get; set; }
        public string? Message { get; set; }
        public string? StackTrace { get; set; }
        public DateTime CreatedDate { get; set; }=DateTime.Now;
    }


ایجاد دیتابیس لاگر

کلاس DBLogger از اینترفیس ILogger ارث بری می‌کند و دارای سه متد است که مهمترین آنها متد Log می‌باشد که درحقیقت با هر بار فراخوانی Logger در برنامه فراخوانی می‌شود. برای مطالعه‌ی بیشتر در رابطه با دو متد دیگر می‌توانید به اینجا مراجعه نمایید.
public class DBLogger:ILogger

    {
        private bool _isDisposed;
        private readonly ApplicationDbContext _dbContext;
        public DBLogger(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }


        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        {
            var dblLogItem = new DBLog()
            {
                EventName = eventId.Name,
                LogLevel = logLevel.ToString(),
                Message = exception?.Message,
                StackTrace=exception?.StackTrace                
            }; 
            _dbContext.DBLogs.Add(dblLogItem);
            _dbContext.SaveChanges();
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return true;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }
    }


ایجاد یک لاگ پروایدر سفارشی

حال باید یک لاگ پروایدر سفارشی را ایجاد کنیم تا بتوان یک نمونه از دیتابیس لاگر سفارشی بالا (DBLogger) را ایجاد کرد.

public class DbLoggerProvider:ILoggerProvider
    {
        private bool _isDisposed;
        private readonly ApplicationDbContext _dbContext;
        public DbLoggerProvider(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new D‌BLogger(_dbContext);
        }

        public void Dispose()
        {
        }
    }  

همانطور که ملاحظه می‌نمایید، این لاگ پروایدر، از اینترفیس ILoggerProvider ارث بری کرده‌است که دارای متد CreateLogger می‌باشد ئ این متد با شروع برنامه، یک نمونه از دیتابیس لاگر سفارشی ما را ایجاد می‌کند. در سازنده‌ی این کلاس، DatabaseContext را مقدار دهی نموده‌ایم تا آنرا به کلاس DBLogger ارسال نماییم.

در انتها کافیست در کلاس Startup.cs این لاگ پروایدر سفارشی (DbLoggerProvider  ) را صدا بزنیم.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {
            .
            .
            .
            #region CustomLogProvider
            var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
            var appDbContext = serviceProvider.GetRequiredService<ApplicationDbContext>();
            loggerFactory.AddProvider(new DbLoggerProvider(appDbContext));
            #endregion
            .
            .
            .
در اینجا ابتدا DBContext پروژه را گرفته سپس DbLoggerProvider   را صدا زده و مقدار DBContext را به آن پاس می‌دهیم.


مشکل!

منطق کدهای بالا کاملا صحیح می‌باشد، اما با اجرای یک اپلیکیشن واقعی، در ابتدای کار اینقدر تعداد فراخوانی ثبت لاگ‌ها در پایگاه داده بالا می‌رود که اپلیکیشن هنگ می‌کند. برای حل این مشکل باید یک صف همزمانی برای ثبت لاگ‌ها تشکیل شود. برای این منظور من از این مطلب پروژه‌ی DNTIdentity بهره بردم. بنابراین باید پروایدر را به شکل زیر تصحیح کنیم:
public class DbLoggerProvider:ILoggerProvider
    {
        private readonly CancellationTokenSource _cancellationTokenSource = new();
        private readonly IList<DBLog> _currentBatch = new List<DBLog>();
        private readonly TimeSpan _interval = TimeSpan.FromSeconds(2);

        private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>());

        private readonly Task _outputTask;
        private readonly ApplicationDbContext _dbContext;
        private bool _isDisposed;

        public DbLoggerProvider(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _outputTask = Task.Run(ProcessLogQueue);
        }
        public ILogger CreateLogger(string categoryName)
        {
            return new DBLogger(this,categoryName);
        }
        private async Task ProcessLogQueue()
        {
            while (!_cancellationTokenSource.IsCancellationRequested)
            {
                while (_messageQueue.TryTake(out var message))
                {
                    try
                    {
                        _currentBatch.Add(message);
                    }
                    catch
                    {
                        //cancellation token canceled or CompleteAdding called
                    }
                }

                await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token);
                _currentBatch.Clear();

                await Task.Delay(_interval, _cancellationTokenSource.Token);
            }
        }
        internal void AddLogItem(DBLog appLogItem)
        {
            if (!_messageQueue.IsAddingCompleted)
            {
                _messageQueue.Add(appLogItem, _cancellationTokenSource.Token);
            }
        }
        private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken)
        {
            try
            {
                if (!items.Any())
                {
                    return;
                }

                // We need a separate context for the logger to call its SaveChanges several times,
                // without using the current request's context and changing its internal state.
                foreach (var item in items)
                {
                    var addedEntry = _dbContext.DbLogs.Add(item);
                }

                await _dbContext.SaveChangesAsync(cancellationToken);

            }
            catch
            {
                // don't throw exceptions from logger
            }
        }

        [SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception",
            Justification = "don't throw exceptions from logger")]
        private void Stop()
        {
            _cancellationTokenSource.Cancel();
            _messageQueue.CompleteAdding();

            try
            {
                _outputTask.Wait(_interval);
            }
            catch
            {
                // don't throw exceptions from logger
            }
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                try
                {
                    if (disposing)
                    {
                        Stop();
                        _messageQueue.Dispose();
                        _cancellationTokenSource.Dispose();
                        _dbContext.Dispose();
                    }
                }
                finally
                {
                    _isDisposed = true;
                }
            }
        }
    }  
همچنین دیتابیس لاگر نیز به شکل زیر تغییر خواهد کرد:
public class DBLogger:ILogger
    {
        private readonly LogLevel _minLevel;
        private readonly DbLoggerProvider _loggerProvider;
        private readonly string _categoryName;

        public DBLogger(
            DbLoggerProvider loggerProvider,
            string categoryName
        )
        {
            _loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider));
            _categoryName= categoryName;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return new NoopDisposable();
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel >= _minLevel;
        }

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter == null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            var message = formatter(state, exception);

            if (exception != null)
            {
                message = $"{message}{Environment.NewLine}{exception}";
            }

            if (string.IsNullOrEmpty(message))
            {
                return;
            }

            var dblLogItem = new DBLog()
            {
                EventName = eventId.Name,
                LogLevel = logLevel.ToString(),
                Message = $"{_categoryName}{Environment.NewLine}{message}",
                StackTrace=exception?.StackTrace
            };
            _loggerProvider.AddLogItem(dblLogItem);
        }

      

        private class NoopDisposable : IDisposable
        {
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
            }
        }
    }  
خلاصه تغییرات اینگونه‌است که بجای ذخیره لاگ‌ها در دیتابیس در داخل کلاس DBLogger، آنها را در یک صف همزمانی اضافه می‌کنیم و پس از اتمام پروسه، ذخیره سازی لاگ‌ها را در لاگ پروایدر انجام می‌دهیم.
توسط ترکیب روش توضیح داده شده در این مقاله با مدیریت استثناءها در Blazor Server - قسمت دوم، علاوه برلاگ‌های معمولی، می‌توان تمامی استثناءهای یک اپلیکیشن را نیز به راحتی در پایگاه داده ذخیره نمود.
مطالب
نحوه استفاده از Text template ها در دات نت - قسمت دوم
بعد از ایجاد فایل Text template که در جلسه قبل با آن آشنا شدید، برای شروع قواعد زیر را در نظر بگیرید :
- تنظیمات مربوط به فایل Text template و نحوه تولید خروجی در ابتدای فایل و بین علامت <#@ و  #> قرار میگیرد.
- هر متنی که بصورت معمول در فایل tt نوشته شود، به همان صورت در فایل خروجی قرار می‌گیرد.
- هر دستوری که در بین علامت‌های  <#=  و #> قرار گیرد هنگام کامپایل اجرا شده و معادل آن در همان مکان متن قرار میگیرد.
- هر دستوری که بین علامت‌های  <#  و #>  قرار گیرد، هنگام کامپایل اجرا می‌شود. در این صورت دستورات نوشته شده در این قسمت فقط اجرا می‌گردد و معمولا برای استفاده در قسمتهای دیگر، داخل بلوک <#= #> نوشته می‌شود . 
- برای تعریف کلاس یا متد جدید جهت استفاده در فایل tt می‌توانیم کلاس را در بین علامت <#+ و  #> قرار دهیم. در این صورت کلاس و متد‌های نوشته شده در قسمتهای دیگر، داخل بلوک  <#= #> و یا  <# #> مورد استفاده قرار میگیرند.

اجازه دهید با یک مثال ساده قواعد اولیه را بررسی کنیم :  
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".txt" #>

<# var T = DateTime.Now; #>

The Time is : <#= T #>

The Time is : <#=  DateTime.Now #>
در این مثال، T در واقع متغیری است که در  بلوک <# #> تعریف گردیده و در بلوک <#= #> مقدار آن استفاده میشود. خروجی فایل چیزی شبیه به دو خط زیر خواهد بود:

The Time is : 02/16/2014 14:17:39
The Time is : 02/16/2014 14:17:39
به عنوان یک مثال دیگر که قواعد توضیح داده شده را پوشش دهد به مثال زیر توجه کنید :
<#@ template debug="true" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>

using System;
using System.Text;

<# string ClassName = "DotnetTips"; #>
public class <#= ClassName + "_" + new MyTestClass().Str #>
{
}

<#+
public class MyTestClass
{
public string  Str { get{return new DateTime().DayOfWeek.ToString() ;} }
}
#>
خروجی  Text template   بالا فایل Cs    با  محتوی شبیه کد زیر خواهد بود:  (روز نگارش مطلب البته دوشنبه است) 
using System;
using System.Text;

public class DotnetTips_Monday
{
}

به عنوان یک مثال ساده دیگر برای فهم بیشتر به کد زیر جهت تولید Table  در Html  توجه کنید:  

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".html" #>

<html><body>
<table>
    <# for (int i = 1; i <= 10; i++)
       { #>
         <tr>
 <td>Test name <#= i #> </td>
             <td>Test value <#= i * i #> </td> 
 </tr>
    <# } #>
 </table>
</body></html>
فکر می‌کنم این 3 مثال ساده، تا حد زیادی قواعد اولیه T4 Text Template  را برای شما روشن کرده باشد. در قسمت بعدی برخی قواعد تکمیلی را در این مورد خدمتتون ارائه میدم.
مطالب
تبادل داده ها بین لایه ها- قسمت اول

معماری لایه بندی شده، یک معماری بسیار همه گیر می‌باشد. به این خاطر که به راحتی SOC ، decoupling و قدرت درک کد را بسیار بالا می‌برد. امروزه کمتر برنامه نویس و فعال حوضه‌ی نرم افزاری است که با لایه‌های کلی و وظایف آنها آشنا نباشد ( UI layer آنچه که ما می‌بینیم، middle layer   برای مقاصد منطق کاری، data access layer برای هندل کردن دسترسی به داده‌ها). اما مسئله ای که بیشتر برنامه نویسان و توسعه دهندگان نرم افزار با استاندارد‌های آن آشنا نیستند، راه‌های تبادل داده‌ها مابین layer ‌ها می‌باشد. در این مقاله سعی داریم راه‌های تبادل داده‌ها را مابین لایه‌ها، تشریح کنیم. 

Layers  و Tiers با هم متفاوت هستند

Layer  با Tier متفاوت است. هنگامیکه در مورد مفهوم layer و Tier دچار شک شدید، دیاگرام ذیل می‌تواند به شما بسیار کمک کند. layer به مجزاسازی منطقی کد و Tier هم به مجزا سازی فیزیکی در ماشین‌های مختلف اطلاق می‌شود. توجه داشته باشید که این نکته یک شفاف سازی کلی در مورد یک مسئله مهم بود.


داده‌های وارد شونده( incoming ) و خارج شونده( outgoing )

ما باید تبادل داده‌ها را از دو جنبه مورد بررسی قرار دهیم؛ اول اینکه داده‌ها چگونه به سمت لایه Data Access می‌روند، دوم اینکه داده‌ها چگونه به لایه UI پاس می‌شوند، در ادامه شما دلیل این مجزا سازی را درک خواهید کرد. 

روش اول: Non-uniform

این روش اولین روش و احتمالا عمومی‌ترین روش می‌باشد. خوب، اجازه دهید از لایه‌ی UI به لایه DAL شروع کنیم. داده‌ها از لایه UI به Middle با استفاده از getter ‌ها و setter ‌ها ارسال خواهد شد. کد ذیل این مسئله را به روشنی نمایش می‌دهد.

Customer objCust = new Customer();
objCust.CustomerCode = "c001";
objCust.CustomerName = "Shivprasad";

بعد از آن، از لایه Middle  به لایه Data Access داده‌ها با استفاده از مجزاسازی به وسیله comma و آرایه‌ها و سایر روش‌های  non-uniform پاس داده می‌شوند. در کد ذیل به متد Add دقت کنید که چگونه فراخوانی به لایه Data Access را با استفاده از پارامتر‌های ورودی انجام می‌دهد. 

public class Customer
{
    private string _CustomerName = "";
    private string _CustomerCode = "";
    public string CustomerCode
    {
        get { return _CustomerCode; }
        set { _CustomerCode = value; }
    }
    public string CustomerName
    {
        get { return _CustomerName; }
        set { _CustomerName = value; }
    }
    public void Add()
    {
        CustomerDal obj = new CustomerDal();
        obj.Add(_CustomerName,_CustomerCode);
    }
}

کد ذیل، متد add در لایه Data Access را با استفاده از دو متد نمایش می‌دهد. 

public class CustomerDal
{
    public bool Add(string CustomerName,string CustomerCode)
    {
        // Insert data in to DB
    }
}

بنابراین اگر بخواهیم  به صورت خلاصه نحوه پاس دادن داده‌ها را در روش non-uniform  بیان کنیم، شکل ذیل به زیبایی این مسئله را نشان می‌دهد. 

· از لایه UI به لایه Middle  با استفاده از setter و getter

· از لایه Middle به لایه  data access با استفاده از comma ، input ، array 

حال نوبت این است بررسی کنیم که چگونه داده‌ها از DAL به UI   در روش non-uniform پاس خواهند شد. بنابراین اجازه دهید که اول از UI شروع کنیم. از لایه UI داده‌ها با استفاده از object ‌های لایه Middle  واکشی می‌شوند.   

Customer obj = new Customer();
List<Customer> oCustomers = obj.getCustomers();

از لایه Middle هم داده‌ها با استفاده از dataset ، datatable و xml پاس خواهند شد. مهمترین مسئله برای لایه middle ، loop بر روی dataset و تبدیل آن به strong type object  ها می‌باشد. برای مثال می‌توانید کد تابع getCustomers که بر روی dataset ، loop می‌زند و یک لیست از Customer ‌ها را آماده می‌کند در ذیل مشاهده کنید. این تبدیل باید انجام شود، به این دلیل که UI به کلاس‌های strongly typed دسترسی دارد. 

public class Customer
{
    private string _CustomerName = "";
    private string _CustomerCode = "";
    public string CustomerCode
    {
        get { return _CustomerCode; }
        set { _CustomerCode = value; }
    }
    public string CustomerName
    {
        get { return _CustomerName; }
        set { _CustomerName = value; }
    }
    public List<Customer> getCustomers()
    {
        CustomerDal obj = new CustomerDal();
        DataSet ds = obj.getCustomers();
        List<Customer> oCustomers = new List<Customer>();
        foreach (DataRow orow in ds.Tables[0].Rows)
        {
            // Fill the list
        }
        return oCustomers;
    }
}

با انجام این تبدیل به یکی از بزرگترین اهداف معماری لایه بندی شده می‌رسیم؛ یعنی اینکه « UI نمی‌تواند به طور مستقیم به کامپوننت‌های لایه Data Access مانند ADO.NET ، OLEDB و غیره دستیابی داشته باشد. با این روش اگر ما در ادامه متدولوژی Data Access را تغییر دهیم تاثیری بر روی لایه UI نمی‌گذارد.» آخرین مسئله اینکه کلاس CustomerDal یک Dataset  را با استفاده از ADO.NET بر می‌گرداند و Middle از آن استفاده می‌کند. 

public class CustomerDal
{
    public DataSet getCustomers()
    {
        // fetch customer records
        return new DataSet();
    }
}

حال اگر بخواهیم حرکت داده‌ها را به لایه UI، به صورت خلاصه بیان کنیم، شکل ذیل کامل این مسئله را نشان می‌دهد. 

· داده‌ها از لایه DAL به لایه  Middle با استفاده از Dataset ، Datareader ،  XML ارسال خواهند شد. 

· از لایه Middle به UI  از strongly typed classes استفاده می‌شود. 


مزایا و معایب روش non-uniform

یکی از مزایای non-uniform

· به راحتی قابل پیاده سازی می‌باشد، در مواردی که روش data access تغییر نمی‌کند این روش کارآیی لازم را دارد.

تعدادی از معایب  این روش

· به خاطر اینکه یک ساختار uniform نداریم، بنابراین نیاز داریم که همیشه در هنگام عبور از یک لایه به یک لایه‌ی دیگر از یک ساختار به یک ساختار دیگر تبدیل را انجام دهیم.

· برنامه نویسان از روش‌های خودشان برای پاس دیتا استفاده می‌کنند؛ بنابراین این مسئله خود باعث پیچیدگی می‌شود.

· اگر برای مثال شما بخواهید متدولوژی Data Access خود را تغییر دهید، تغییرات بر تمام لایه‌ها تاثیر می‌گذارد.