مطالب
پلاگین جستجو با jquery و twitter bootstrap
در این مطلب با نحوه استفاده از پلاگین جستجوی سفارشی searchboxmvc.js آشنا خواهید شد. 

قبلاً در اینجا با نحوه ایجاد پلاگین jQuey آشنا شدید. روشی دیگری نیز برای ایجاد این نوع پلاگین‌ها وجود دارد و آن استفاده از widget factory موجود در پلاگین jQuery UI می‌باشد. 
برای استفاده از این پلاگین که کدهای کامل آن در فایل پیوست موجود است، ابتدا باید فایل‌های لازم را به پروژه خود اضافه کنیم:
    <link rel="stylesheet" href="@Url.Content("~/Content/bootstrap-rtl.css")" type="text/css" />
    <script type="text/javascript" src="@Url.Content("~/scripts/jquery-2.0.2.min.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/scripts/jquery-ui-1.10.3.min.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/scripts/bootstrap-rtl.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/scripts/searchboxmvc.js")"></script>
سپس در کنترلر خود یک Action بصورت زیر ایجاد کنید:
 [HttpPost]
        public virtual ActionResult LoadData(string fieldName, string value, string stringFilterMode = "startWith")
        {
            Thread.Sleep(2000);
            var models = MakePersons();
            if (fieldName == "Id")
            {
                models = models.Where(p => p.Id == int.Parse(value)).Take(1).ToList();
            }
            else if (fieldName == "FirstName")
            {
                models = models.Where(p => p.FirstName.StartsWith(value)).ToList();
            }

            return Json(new { Status = "OK", Records = models });
        }
        private List<Person> MakePersons()
        {
            var lst = new List<Person>();
            lst.Add(new Person() { Id = 1, Code = "Uytffs-098", FirstName = "احمدرضا", LastName = "عابدزاده" });
            lst.Add(new Person() { Id = 2, Code = "fTuuuw-652", FirstName = "کریم", LastName = "باقری" });
            lst.Add(new Person() { Id = 3, Code = "Lopapo-123", FirstName = "خداداد", LastName = "عزیزی" });
            lst.Add(new Person() { Id = 4, Code = "Utppq-981", FirstName = "علی", LastName = "دایی" });
            lst.Add(new Person() { Id = 5, Code = "zttsn-471", FirstName = "علی", LastName = "کریمی" });
            lst.Add(new Person() { Id = 6, Code = "poiud-901", FirstName = "مهدی", LastName = "مهدوی کیا" });
            lst.Add(new Person() { Id = 7, Code = "wqrPoP-391", FirstName = "علیرضا", LastName = "منصوریان" });
            return lst;
        }
در ادامه در ویوی مورد نظر خود یک div ایجاد کنید. همین div خام با اعمال پلاگین بر روی آن ، بصورت یک پلاگین جستجو عمل خواهد کرد.
حال کدهای جاوا اسکریپت مورد نظر را برای اعمال پلاگین و تنظیمات موردنیاز آن به div ایجاد شده می‌نویسیم:
...
<div id="div_SearchBoxContainer">
</div>
...
@section scripts{
    <script type="text/javascript">
        $("#div_SearchBoxContainer").searchboxmvc({
            loadUrl: '@Url.Action(actionName: "LoadData", controllerName: "Home")',
            defaultStringFilterMode: "startWith",
            loadDataOnLeave: true,
            displayClass: "",
            displayNoResultClass: "",
            display: function (element, record) {
                $(element).html(record.FirstName + "  " + record.LastName);
            },
            listItemsDisplay: function (element, record, index) {
                return record.LastName + " " + record.FirstName + "(" + record.Code + ")";
            },
            fields: [
                {
                    fieldName: "Id",
                    fieldTitle: "شناسه",
                    width: 100,
                    defaultValueField: true
                },
                {
                    fieldName: "FirstName",
                    fieldTitle: "نام",
                    width: 200,
                    defaultDisplayField: true,
                    filter: true,
                    isStringType: true
                },
                {
                    fieldName: "LastName",
                    fieldTitle: "نام خانوادگی",
                    filter: false,
                    isStringType: true
                }
            ]
        });
    </script>
}

شرح پارامترهای افزونه searchboxmvc.js 
loadUrl : آدرس اکشن متدی است که بصورت ajax ای فراخوانی شده و نتایج حاصل را بازگشت میدهد.
 نتایج حاصله باید با فرمت json بازگشت داده شوند. اگر نتایج موفقیت باشد باید بصورت  ({Json(new { Status = "OK", Records = models بازگشت داده شوند و اگر خطایی در این بین صورت گرفت مقدار Status نباید مقدار OK باشد.
پارامترهای مورد نیاز این اکشن نیز باید به ترتیب با نام های fieldName و value باشند که fieldName نام فیلدی است که جستجو بر اساس آن صورت می‌گیرد و value همان مقدار وارد شده توسط کاربر است. 
defaultStringFilterMode : اگر فیلد مورد جستجو از نوع رشته ای باشد (یعنی isStringType  آن برابر true باشد) آنگاه پارامتر سوم اکشن متد بطور خودکار مقداردهی خواهد شد. مقادیر این خاصیت میتواند startWith  یا contains و یا equal باشد.
loadDataOnLeave : اگر برابر false باشد، هربار که متن input تغییر کرد بلافاصله یک تقاضا برای یافتن مقادیر به سرور فرستاده میشود و نیازی نیست که فوکوس از کنترل خارج شود.
displayClass : نام کلاس css است که به div 3 اعمال خواهد شد.
displayNoResultClass : در صورتیکه جستجو نتیجه ای نداشته باشد این کلاس به div 3 اعمال خواهد شد.
display : یک فانکشن که برای ایجاد خروجی html برای نمایش در div 3 بکار می‌رود.
listItemsDisplay : یک فانکشن که برای ایجاد خروجی html برای آیتم‌ها بکار می‌رود.
fields : یک آرایه از فیلدهای موردنیاز پلاگین .
خاصیت‌های فیلد نیز بصورت زیر است:
fieldName : نام فیلد
fieldTitle : عنوان فیلد
defaultValueField : فیلد پیش فرض که جستجو بر اساس آن صورت می‌گیرد. اگر تعیین نشود فیلد اول آرایه به عنوان فیلد پیش فرض انتخاب خواهد شد.
defaultDisplayField : فیلد پیش فرض که برای نمایش متن div 3 بکار می‌رود(البته اگر پارامتر display تعیین نشود)
filter : اگر برابر true باشد این فیلد در لیست فیلدهای جستجو خواهد آمد و کاربر می‌تواند بر اساس آن جستجو انجام دهد.
isStringType : اگر برابر true باشد ، پارامتر سوم اکشن متد بطور خودکار مقداردهی خواهد شد.
لازم به ذکر است که این پلاگین کامل نیست و فقط برای ارائه مثال اینجا آورده شده است. هر یک از دوستان می‌توانند محتوای پلاگین را به سلیقه خود تغییر داده و پلاگین را کاملتر کنند.
sample_mvc.zip
مطالب
نحوه تولید پویای صفحات از طریق دیتابیس در ASP.NET MVC
گاهی نیاز داریم صفحات را در دیتابیس ذخیره کنیم تا علاوه بر قابلیت جستجوی پیشرفته‌ی صفحات از طریق Full Text Search، بتوانیم از پویایی صفحات کامپایل شونده نیز استفاده کنیم.
جهت پیاده سازی این مثال ما از Entity Framework استفاده می‌کنیم.
بنابراین ابتدا کلاس Page را جهت ذخیره آدرس و محتوی صفحات مجازی در دیتابیس، پیاده سازی می‌کنیم: 
public class Page
{
    public int Id { get; set; }

    public string Path { get; set; }

    public string Content { get; set; }
}  
سپس کلاس VirtualPathProvider را سفارشی سازی می‌کنیم:
public class CustomVirtualPathProvider : VirtualPathProvider
{
    public override bool FileExists(string virtualPath)
    {
        return base.FileExists(virtualPath) || FileExistsInDatabase(virtualPath);
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        return base.FileExists(virtualPath)
            ? base.GetFile(virtualPath)
            : new CustomVirtualFile(virtualPath, GetFileFromDatabase(virtualPath));
    }

    private bool FileExistsInDatabase(string virtualPath)
    {
        virtualPath = virtualPath.Replace("~", "");

        return new DatabaseContext().Pages.Any(v => v.Path == virtualPath);
    }

    private byte[] GetFileFromDatabase(string virtualPath)
    {
         virtualPath = virtualPath.Replace("~", "");

         return Encoding.UTF8.GetBytes(new DatabaseContext().Pages.First(v => v.Path == virtualPath).Content);
    }
}  
تابع FileExists ابتدا وجود فایل مورد نظر را در مسیر داده شده، بررسی می‌کند و در صورت عدم وجود آن، دیتابیس را به دنبال آن جستجو می‌کند.
تابع GetFile در صورتیکه صفحه به صورت فایل موجود باشد، روال همیشگی را جهت نمایش طی می‌کند. اما اگر نباشد یک نمونه از کلاس سفارشی سازی شده‌ی CustomVirtualFile را ایجاد کرده و بر می‌گرداند.
کلاس CustomVirtualFile به صورت زیر سفارشی سازی شده:
public class CustomVirtualFile : VirtualFile
{
    private readonly byte[] _content;

    public CustomVirtualFile(string virtualPath, byte[] content)
        : base(virtualPath)
    {
        _content = content;
    }

    public override Stream Open()
    {
        return new MemoryStream(_content);
    }
}  
تابع Open، محتوای ارائه شده را به صورت یک استریم بر می‌گرداند.

حال نوبت ثبت کلاس CustomVirtualPathProvider جهت استفاده‌ی خودکار از آن می‌باشد. برای این کار در تابع Application_Start موجود در فایل Global.asax.cs دستور زیر را اضافه می‌نماییم:
protected void Application_Start()
{
    HostingEnvironment.RegisterVirtualPathProvider(new CustomVirtualPathProvider());
    //...
}
و تمام!
همه چیز به صورت خودکار اجرا شده و در صورت عدم وجود فایل در آدرس‌های ارسال شده، صفحات ما از طریق جدول Pages موجود در دیتابیس بارگزاری می‌شوند.
مطالب
کامپوننت‌ها در AngularJS 1.5 - قسمت دوم - مسیریابی
در این قسمت به معرفی سیستم مسیریاب در Angular 1.5 خواهیم پرداخت. قبل از معرفی این سیستم ابتدا سیستم مسیریاب اصلی در Angular را بررسی خواهیم کرد.

مروری بر مسیریابی در AngularJS
برای استفاده از مسیریاب اصلی Angular کافی است از دایرکتیو ویژه‌ایی با نام ng-view به همراه یکسری تنظیمات پیکربندی استفاده کنیم. به عنوان مثال اگر آدرس صفحه با home/ مطابقت داشته باشد، تمپلیت home.html توسط دایرکتیو ng-view بارگذاری خواهد شد. برای فعال‌سازی این سیستم ابتدا باید پکیج angular-route را به پروژه مثال قسمت قبل اضافه کنید:
bower install angular-route --save
در ادامه لازم است وابستگی فوق را به صفحه‌ی index.html اضافه نمائید:
<script src="bower_components/angular-route/angular-route.min.js" type="text/javascript"></script>
اکنون درون صفحه به جای نمایش مستقیم کامپوننت، از دایرکتیو ng-view استفاده خواهیم کرد:
<div class="col-md-9">
                <ng-view></ng-view>
</div>

 همچنین مقدار فیلد url کامپوننت dntWidget را به صورت زیر تغییر دهید:
      model.panel = {
          title: "Panel Title",
          items: [
              {
                  title: "Home", url: "#/home"
              },
              {
                  title: "Articles", url: "#/articles"
              },
              {
                  title: "Authors", url: "#/authors"
              }
          ]
      };

در ادامه باید سیستم مسیریاب را به عنوان یک وابستگی به اپلیکیشن معرفی کنیم:
var module = angular.module("dntModule", ["ngRoute"]);
اکنون می‌توانیم پیکربندی موردنظر جهت هدایت آدرس‌ها به تمپلیت‌های مربوطه را بنویسیم:
module.config(function ($routeProvider) {
        $routeProvider
            .when("/home", { template: "<app-home></app-home>" })
            .when("/articles", { template: "<app-articles></app-articles>" })
            .when("/authors", { template: "<app-authors></app-authors>" })
            .otherwise({ redirectTo: "/home" });
    });

همانطور که مشاهده می‌کنید به route provider اعلام کرده‌ایم که در صورت مطابقت داشتن آدرس URL با هر کدام از حالت‌های فوق، تمپلیت متناسب با آن را نمایش بدهد. در نهایت توسط otherwise اگر آدرس، با هیچکدام از حالت‌های تعریف شده مطابقت نداشت، کاربر به آدرس home/ هدایت خواهد شد. 
نکته‌ایی که در کد فوق وجود دارد این است که سیستم مسیریاب اصلی Angular تا اینجا هیچ اطلاعی از وجود کامپوننت‌ها ندارد، اما می‌داند یک تمپلیت چیست. بنابراین از تمپلیت، جهت نمایش یک کامپوننت استفاده خواهد کرد.
برای ایجاد کامپوننت‌های فوق نیز می‌توانید آن را به صورت زیر ایجاد کنید:
module.component("appHome", {
        template: `
        <hr><div>
            <div>Panel heading = HomePage</div>
            <div>
                HomePage
            </div>
        </div>`
    });
    module.component("appArticles", {
        template: `
        <hr><div>
            <div>Panel heading = Articles</div>
            <div>
                Articles
            </div>
        </div>`
    });
    module.component("appAuthors", {
        template: `
        <hr><div>
            <div>Panel heading = Authors</div>
            <div>
                Authors
            </div>
        </div>`
    });

اکنون اگر برنامه را اجرا کنید، خواهید دید که به صورت پیش‌فرض به آدرس home/# هدایت خواهیم شد. زیرا آدرسی برای root، درون route configuration تعریف نکرده‌ایم:

اکنون توسط لینک‌های تعریف شده می‌توانیم به راحتی درون تمپلیت‌ها، پیمایش کنیم. همانطور که عنوان شد تا اینجا مسیریاب پیش‌فرض Angular هیچ اطلاعی از کامپوننت‌ها ندارد؛ بلکه آنها را با کمک template، به صورت غیر مستقیم، درون صفحه نمایش داده‌ایم.


معرفی Component Router

مزیت این روتر این است که به صورت اختصاصی برای کار با کامپوننت‌ها طراحی شده است. بنابراین دیگر نیازی به استفاده از template درون route configuration نیست. برای استفاده از این روتر ابتدا باید پکیج آن را نصب کنیم:

bower install angular-component-router --save

سپس وابستگی فوق را با روتر پیش‌فرضی که در مثال قبل بررسی کردیم، جایگزین خواهیم کرد:

<script src="bower_components/angular-component-router/angular_1_router.js"></script>

همچنین درون فایل module.js به جای وابستگی ngRoute از ngComponentrouter استفاده خواهیم کرد:

var module = angular.module("dntModule", ["ngComponentRouter"]);

در ادامه به جای تمامی route configurations قبلی، اینبار یک کامپوننت جدید را به صورت زیر ایجاد خواهیم کرد:

module.component("appHome", {
        template: `
        <hr>
        <div>
            <div>Panel heading = HomePage</div>
            <div>
                HomePage
            </div>
        </div>`
    });

همانطور که مشاهده می‌کنید برای پاسخ‌گویی به تغییرات URL، مقدار routeConfig$ را مقداردهی کرده‌ایم. در اینجا به جای بارگذاری تمپلیت، خود کامپوننت، در هر یک از ruleهای فوق بارگذاری خواهد شد. برای حالت otherwise نیز از سینتکس **/ استفاده کرده‌ایم.

تمپلیت کامپوننت فوق نیز به صورت زیر است:

<div class="container">
    <div class="row">
        <div class="col-md-3">
            <hr>
            <dnt-widget></dnt-widget>
        </div>
        <div class="col-md-9">
            <ng-outlet></ng-outlet>
        </div>
    </div>
</div>

لازم به ذکر است دیگر نباید از دایرکتیو  ng-view استفاده کنیم؛ زیرا این دایرکتیو برای استفاده از روتر اصلی طراحی شده است. به جای آن از دایرکتیو ng-outlet استفاده شده است. این کامپوننت به عنوان یک کامپوننت top level عمل خواهد کرد. بنابراین درون صفحه‌ی index.html از کامپوننت فوق استفاده خواهیم کرد:

<html ng-app="dntModule">
<head>
    <meta charset="UTF-8">
    <title>Using Angular 1.5 Component Router</title>
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
    <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css">
</head>
<body>
    
    <dnt-app></dnt-app>

    <script src="bower_components/angular/angular.js" type="text/javascript"></script>
    <script src="bower_components/angular-component-router/angular_1_router.js"></script>
    <script src="scripts/module.js" type="text/javascript"></script>
    <script src="scripts/dnt-app.component.js"></script>
    <script src="scripts/dnt-widget.component.js"></script>
</body>
</html>

در نهایت باید جهت فعال‌سازی سیستم مسیریابی جدید، سرویس زیر را همراه با نام کامپوننت فوق ریجستر کنیم:

module.value("$routerRootComponent", "dntApp");

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

کدهای این قسمت را نیز از اینجا می‌توانید دریافت کنید. 

مطالب
روش ذخیره‌ی لاگ‌های 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 - قسمت دوم، علاوه برلاگ‌های معمولی، می‌توان تمامی استثناءهای یک اپلیکیشن را نیز به راحتی در پایگاه داده ذخیره نمود.
مطالب
breeze js به همراه ایجاد سایت آگهی قسمت سوم
در مطلب قبلی، پیش نیازهای مربوطه را نصب کردیم. در این قسمت به ساخت صفحات ورود و خروج، ثبت نام کاربران و تغییر رمز عبور خواهیم پرداخت.
در اینجا ما از Account Controller پیش فرض Asp.net Mvc استفاده میکنیم که متدهای مورد استفاده ما در آن قرار دارد و به BreezeController مزین شده است.
 [BreezeController]
    public class AccountController : ApiController
    {
      ...
    }

 اینترفیس IAuthService:
module Interfaces {
    export interface IAuthService {
        user: Models.IUserToken
        getUserInfo(accessToken);
        login(data);
        logOut();
        register(data);
        changePassword(data);
        accessToken(accessToken, data);
    }
}

سرویس AuthService 
پیاده سازی اینترفیس IAuthService را برعهده دارد. در سازنده آن، وابستگی‌های آن مقداردهی شده‌است و همچنین تنظیمات manager را انجام داده‌ایم.
متد accessToken: وظیفه ارسال توکن  را به سرور و همچنین نگهداری آن‌را در local storage، برعهده دارد.
متد getUserInfo: اطلاعات کاربر لاگین شده را  از سرور دریافت مینماید.
متد login: فرمت مورد قبول سرور به نحو زیر میباشد. در صورت موفقیت آمیز بودن، توکن را به متد accessToken  پاس میدهیم و آبجکت user را با مقادیر دریافتی پر می‌نماییم.
"grant_type=password & username=myusername & password=mypassword";
برای فراخوانی متدهای post، همانطوری که در مطلب ارسال کوئری‌های پست به آن اشاره شده‌است، عمل می‌نماییم. در ابتدا فایل breeze.ajaxpost.js را اضافه می‌کنیم سپس در فایل breeze.angular  قطعه کد زیر را در متد useNgHttp اضافه می‌کنیم.
var ajaxAdapter = breeze.config.getAdapterInstance("ajax");
breeze.ajaxpost(ajaxAdapter);
با این تنظیمات میتوانیم توسط withParameters، متدهای post را فراخوانی کنیم. 
.withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                    $data: newData
                }
متد logOut: خروج از برنامه را برای ما انجام می‌دهد و درصورت موفقیت آمیز بودن، به صفحه اصلی هدایت می‌شود.
متد register: ثبت نام کاربران را بر عهده دارد.
متد changePassword: تغییر رمز عبور کاربران را برعهده دارد.
module AdApps {
    var securityUrls = {
        site: '/',
        login: '/token',
        logout: 'logout',
        register: 'register',
        userInfo: 'getUserInfo',
        changePassword: 'changePassword',
    }
    export class AuthService implements Interfaces.IAuthService {
        private manager: breeze.EntityManager;

        constructor(
            private _breeze: typeof breeze,
            private $http: ng.IHttpProvider,
            private toaster: ngtoaster.IToasterService,
            private $location: ng.ILocationService) {

            var dataService = new _breeze.DataService({
                serviceName: "/breeze/Account",
                hasServerMetadata: false
            });
            var metadataStore = new _breeze.MetadataStore({
                namingConvention: _breeze.NamingConvention.camelCase
            });
            this.manager = new _breeze.EntityManager({
                dataService: dataService,
                metadataStore: metadataStore,
                saveOptions: new _breeze.SaveOptions({
                    allowConcurrentSaves: true, tag: [{}]
                })
            });
        }

        user: Models.IUserToken;
        accessToken(accessToken, data): string {
            if (accessToken === 'clear') {
                localStorage.removeItem('accessToken');
                delete this.$http.defaults.headers.common.Authorization;
            }
            else {
                window.localStorage.setItem("accessToken", accessToken);
                this.$http.defaults.headers.common.Authorization = 'Bearer ' + accessToken;
            }
            return accessToken;
        }
        getUserInfo(): ng.IPromise<any> {
            var query = this._breeze.EntityQuery.from(securityUrls.userInfo);
            return this.manager.executeQuery(query).then(data => {
                return data.results[0];
            });
        }
        login(data: any): ng.IPromise<any> {
            var newData = "grant_type=password&username=" + data.userName + "&password=" + data.password;
            var query = this._breeze.EntityQuery.from(securityUrls.login)
                .withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                    $data: newData
                });
            return this.manager.executeQuery(query).then(data => {
                var self = this;
                var result = data.results[0] as any;
                self.accessToken(result.access_token, data.results[0]);
                self.user = <Models.IUserToken>{};
                self.user = <Models.IUserToken>result;
                return result;
            });
        }
        logOut(): ng.IPromise<any> {
            var query = this._breeze.EntityQuery.from(securityUrls.logout)
                .withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                });
            return this.manager.executeQuery(query).then(data => {
                this.user = null;
                this.accessToken('clear', null);
                this.$location.path("/");
            });
        }
        register(data: Object): ng.IPromise<any> {
            var query = this._breeze.EntityQuery.from(securityUrls.register)
                .withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                    $data: data
                });
            return this.manager.executeQuery(query).then(data => { });
        }
        changePassword(data: Object): ng.IPromise<any> {
            var query = this._breeze.EntityQuery.from(securityUrls.changePassword)
                .withParameters({
                    $method: 'POST',
                    $encoding: 'JSON',
                    $data: data
                });
            return this.manager.executeQuery(query).then(data => { });
        }
    }
}

سرویس HttpInterceptor 
: رهگیری و پیگیری کردن نتیجه درخواست‌های http را بر عهده دارد.
درrequest : توکن امنیتی را به هدر درخواست‌ها اضافه میکنیم.
در response : در صورت موفقیت درخواست http، پیغام مناسبی را نمایش میدهیم.
در responseError : در صورت عدم موفقیت درخواست http، پیغام مناسبی را نمایش میدهیم.
module AdApps {
    export class HttpInterceptor {
        private static _toaster: ngtoaster.IToasterService;
        private static _$q: ng.IQService;
        constructor(
            private $q: ng.IQService,
            private toaster: ngtoaster.IToasterService,
            private $location: ng.ILocationService) {
            HttpInterceptor._toaster = toaster;
            HttpInterceptor._$q = $q;
        }
        request(config): string {
            config.headers = config.headers || {};
            var authData = window.localStorage.getItem("accessToken");
            if (authData) {
                config.headers.Authorization = "Bearer " + authData;
            }
            return config;
        };
        response(response): ng.IPromise<any> {
            if (response.data && response.data.message && response.status === 200) {
                HttpInterceptor._toaster.success(response.data.message)
            }
            return HttpInterceptor._$q.resolve(response);
        };
        responseError(response): ng.IPromise<any> {
            var self = this;
            var data = response.data;
            var title = "خطا";
            var messages = [];
            if (data) {
                if (data.error) {
                    title = data.error;
                }
                if (data.message) {
                    messages.push(data.message);
                }
                if (data.Message) {
                    messages.push(data.Message);
                }
                if (data.ModelState) {
                    angular.forEach(data.ModelState, function (errors, key) {
                        if (key.substr(0, 1) != "$")
                        { messages.push(errors); }

                    });
                }
                if (data.exceptionMessage) {
                    messages.push(data.exceptionMessage);
                }
                if (data.ExceptionMessage) {
                    messages.push(data.ExceptionMessage);
                }
                if (data.error_description) {
                    messages.push(data.error_description);
                }
                if (messages.length > 0) {
                    HttpInterceptor._toaster.error(title, messages.join("<br/>"));
                }
                if (response.status === "401") {
                    self.$location.path("/ورود");
                }
            }
            return HttpInterceptor._$q.reject(response);
        }
    }
}

معرفی کردن مسیرهای ورود، ثبت نام و تغییر رمز عبور به انگولار
module AdApps {
    class SecurityCtrl {
        constructor(private $scope: Interfaces.IAuthScope, private authService: AuthService) {
            $scope.authService = authService;
            if (window.localStorage.getItem("accessToken") != null) {
                authService.getUserInfo().then(function (data) {
                    $scope.authService.user = data;
                });
            }
            $scope.logOut = function () {
                return authService.logOut().then(function () { });
            }
        }
    }
    define(["angularAmd", "angular", "factory/AuthService", "factory/httpInterceptor"], (angularAmd, ng) => {
        angularAmd = angularAmd.__proto__;
        var app = ng.module("AngularTypeScript", ['ngRoute', 'breeze.angular', 'toaster']);
        var viewPath = "app/views/";
        var controllerPath = "app/controller/";
        app.config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) {
            $httpProvider.interceptors.push("HttpInterceptor");
            $routeProvider
                .when("/", angularAmd.route({
                    templateUrl: viewPath + "home.html",
                    controllerUrl: controllerPath + "home.js"
                }))
                .when("/login", angularAmd.route({
                    templateUrl: viewPath + "login.html",
                    controllerUrl: controllerPath + "login.js"
                }))
                .when("/register", angularAmd.route({
                    templateUrl: viewPath + "register.html",
                    controllerUrl: controllerPath + "register.js"
                }))
                .when("/changePassword", angularAmd.route({
                    templateUrl: viewPath + "change-password.html",
                    controllerUrl: controllerPath + "changePassword.js"
                }))
                .otherwise({ redirectTo: '/' });
        }
        ]);
        app.service('AuthService', ['breeze', '$http', 'toaster', '$location', AuthService]);
        app.service("HttpInterceptor", ["$q", "toaster", "$location", HttpInterceptor]);
        app.controller('SecurityCtrl', ['$scope', 'AuthService', SecurityCtrl]);
        return angularAmd.bootstrap(app);
    });
}

ایجاد کنترلر .login.ts و ارسال سرویس‌های لازم به کلاس LoginCtrl
در صورت صحیح بودن نام کاربری و رمز عبور به صفحه اصلی هدایت خواهد شد.
module AdApps {
    define(['app'], function (app) {
        app.controller('LoginCtrl', ["$scope", "AuthService", "$location", LoginCtrl]);
    });
   export  class LoginCtrl {
        constructor($scope: Interfaces.ILoginScope, authService: AuthService, $location: ng.ILocationService) {
           $scope.submit = function () {
               authService.login(angular.copy($scope.form))
                    .then(function (data) {
                        this.$location.path("/");
                    })
            };
        }
    }
}

ایجاد login.html
<div  ng-controller="LoginCtrl">
    <div>
        <i></i>
        <span>ورود</span>
        <div>
            <div>
            </div>
        </div>
    </div>
    <div>
        <div>
            <div>
                <form name="Form" id="form1">
                    <fieldset>
                        <div>
                            <div>
                                <input
                                       name="username"
                                       ng-model="form.userName"
                                       placeholder="نام کاربری"
                                       required>
                                <span>
                                    <i></i>
                                </span>
                            </div>
                        </div>
                        <div>
                            <div>
                                <input name="password"
                                       type="password"
                                       ng-model="form.password"
                                       placeholder="{{'Password'}}"
                                       validator="required">
                                <span>
                                    <i></i>
                                </span>
                            </div>
                        </div>

                    </fieldset>
                    <div>
                        <button type="submit" ng-click="submit()">ورود</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
فایل‌های مربوط به ثبت نام و تغییر رمز عبور، مشابه لاگین می‌باشد و از ذکر آن خودداری می‌نماییم و فایل‌های مربوطه در پروژه قرار دارند.
با تغییرات بالا، فایل main.ts دارای محتویات زیر میباشد:
requirejs.config({
    paths: {
        "app": "app",
        "angularAmd": "/Scripts/angularAmd",
        "angular": "/Scripts/angular",
        "breezeAjaxpost": "/Scripts/breeze/breeze.ajaxpost",
        "breeze": "/Scripts/breeze/breeze.debug",
        "breezeAngular": "/Scripts/breeze/breeze.angular",
        "bootstrap": "/Scripts/bootstrap",
        "angularRoute": "/Scripts/angular-route",
        "jquery": "/Scripts/jquery-2.2.2",
        "entityManagerService": "factory/entityManagerService",
        "toaster": "/Scripts/toaster",
    },
    waitSeconds: 0,
    shim: {
        "angular": { exports: "angular" },
        "angularRoute": { deps: ["angular"] },
        "bootstrap": { deps: ["jquery"] },
        "breeze": { deps: ["jquery"] },
        "breezeAngular": {
            deps: ["angular", "breeze"]
        },
        "toaster": { deps: ["angular"] },
        "app": {
            deps: ["bootstrap", "angularRoute", "toaster", "breezeAngular", "breezeAjaxpost"]
        }
    }
});
require(["app"]);
فایل پروژه :AngularTypeScript.zip
در قسمت‌های بعدی به ثبت و نمایش آگهی در سایت خواهیم پرداخت.  
مطالب
شروع به کار با EF Core 1.0 - قسمت 9 - بررسی رابطه‌ی One-to-One
بررسی رابطه‌ی One-to-Zero-or-One

زمانیکه نیاز است موجودیت 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; }
    }
برای اینکه بتوان CartableId را هم به عنوان PK و هم FK معرفی کرد، نیاز است از Fluent API به نحو ذیل استفاده کنیم:
    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; }
    }
در اینجا رابطه‌ی یک به یک، توسط متدهای HasOne و WithOne معرفی شده‌است. به علاوه FK بودن CartableId به صورت صریح توسط متد HasForeignKey نیز مشخص گردیده‌است.
همچنین بر روی 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);
در اینجا جدول MyBlogImage هنوز Id خود افزاینده‌ی خود را دارد، اما ثبت رکورد آن بدون وجود کلید خارجی MyBlog میسر نیست.
مطالب دوره‌ها
تزریق خودکار وابستگی‌ها در SignalR
فرض کنید لایه سرویس برنامه دارای اینترفیس و کلاس‌های زیر است:
namespace SignalR02.Services
{
    public interface ITestService
    {
        int GetRecordsCount();
    }
}

namespace SignalR02.Services
{
    public class TestService : ITestService
    {
        public int GetRecordsCount()
        {
            return 10; // It's just a sample to test IOC's.
        }
    }
}
قصد داریم از این لایه، توسط تزریق وابستگی‌ها در Hub برنامه استفاده کنیم:
    [HubName("chat")]
    public class ChatHub : Hub
    {
        //جهت آزمایش تزریق خودکار وابستگی‌ها
        private readonly ITestService _testService;
        public ChatHub(ITestService testService)
        {
            _testService = testService;
        }

        public void SendMessage(string message)
        {
            var msg = string.Format("{0}:{1}", Context.ConnectionId, message);
            Clients.All.hello(msg);

            Clients.All.hello(string.Format("RecordsCount: {0}", _testService.GetRecordsCount()));
برنامه، همان برنامه‌ای است که در دوره جاری تکمیل گردیده است. فقط در اینجا سازنده کلاس اضافه شده و سپس اینترفیس ITestService به عنوان پارامتر آن تعریف گردیده است. در ادامه می‌خواهیم کار وهله سازی و تزریق نمونه مرتبط را توسط StructureMap به صورت خودکار انجام دهیم.
برای این منظور یک کلاس جدید را به نام StructureMapDependencyResolver به برنامه اضافه کنید:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.SignalR;
using StructureMap;

namespace SignalR02.Utils
{
    public class StructureMapDependencyResolver : DefaultDependencyResolver
    {
        private readonly IContainer _container;
        public StructureMapDependencyResolver(IContainer container)
        {
            if (container == null)
            {
                throw new ArgumentNullException("container");
            }
            _container = container;
        }
        public override object GetService(Type serviceType)
        {
            return !serviceType.IsAbstract && !serviceType.IsInterface && serviceType.IsClass
                               ? _container.GetInstance(serviceType)
                               : (_container.TryGetInstance(serviceType) ?? base.GetService(serviceType));
        }
        public override IEnumerable<object> GetServices(Type serviceType)
        {
            return _container.GetAllInstances(serviceType).Cast<object>().Concat(base.GetServices(serviceType));
        }
    }
}
کار این کلاس، تعویض DefaultDependencyResolver توکار SignalR با StructureMap است. از این جهت که برای مثال در سراسر برنامه از StructureMap جهت تزریق وابستگی‌ها استفاده شده است و قصد داریم در قسمت Hub آن نیز یکپارچگی کار حفظ گردد.
برای استفاده از این کلاس تعریف شده فقط کافی است Application_Start فایل Global.asax.cs برنامه هاب را به نحو ذیل تغییر دهیم:
using System;
using System.Web;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;
using SignalR02.Services;
using SignalR02.Utils;
using StructureMap;

namespace SignalR02
{
    public class Global : HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            ObjectFactory.Initialize(cfg => 
            {
                cfg.For<IDependencyResolver>().Singleton().Add<StructureMapDependencyResolver>(); 
                // the rest ...
                cfg.For<ITestService>().Use<TestService>();
            });
            GlobalHost.DependencyResolver = ObjectFactory.GetInstance<IDependencyResolver>();

            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs(new HubConfiguration
            {
                EnableCrossDomain = true
            });            
        }
    }
}
در اینجا در ابتدای کار IDependencyResolver توکار StructureMap با کلاس StructureMapDependencyResolver وهله سازی می‌گردد. سپس تعاریف متداول تنظیمات کلاس‌ها و اینترفیس‌های لایه سرویس برنامه اضافه می‌شوند. همچنین نیاز است GlobalHost.DependencyResolver توکار SignalR نیز به نحوی که ملاحظه می‌کنید مقدار دهی گردد.

اینبار اگر برنامه را اجرا کنید و سپس یکی از کلاینت‌‌های آن‌را فراخوانی نمائید، می‌توان مشاهده کرد که کار وهله سازی و تزریق وابستگی سرویس مورد استفاده به صورت خودکار انجام گردیده است:

نظرات مطالب
طراحی ValidationAttribute دلخواه و هماهنگ سازی آن با ASP.NET MVC
با بازنویسی متد FormatErrorMessage مربوط به RequiredAttribute هم میتوانید به نتیجه مشابه دست پیدا کنید. به شکل زیر:
    public class LocalizedRequiredAttribute : RequiredAttribute, IClientValidatable
    {
        private readonly string _resourceKey;
        public string SourceName { get; set; } = LocalizationSourceNames.Default;

        public LocalizedRequiredAttribute(string resourceKey)
        {
            _resourceKey = resourceKey;
        }

        public override string FormatErrorMessage(string name)
        {
            return LocalizationHelper.GetString(SourceName, _resourceKey);
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            yield return new ModelClientValidationRequiredRule(FormatErrorMessage(null));
        }
    }
مطالب
تخمین مدت زمان خوانده شدن یک مطلب
پس از انتشار مطلب «Pro Agile .NET Development With Scrum - قسمت اول» شاید این سؤال در ابتدای کار برای خواننده پیش بیاید که ... چقدر باید برای خواندن آن وقت بگذارم؟ برای پاسخ به این سؤال باید درنظر داشت که یک انسان معمولی، می‌تواند بین 200 تا 250 کلمه را در دقیقه، مطالعه کند. بنابراین در ابتدا باید محاسبه کرد که یک متن، چه تعدادی کلمه دارد؟
شاید عنوان کنید که کافی است متن ورودی را بر اساس فاصله‌ی بین کلمات تقسیم بندی کرده و سپس تعداد کلمات بدست آمده را محاسبه کنیم:
 var words = text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
return words.Length;
این روش با آزمون زیر کار نکرده و با شکست مواجه می‌شود:
[TestMethod]
public void TestInvalidChars()
{
    const string data = "To be . ! < > ( ) ! ! , ; : ' ? + -";
    Assert.AreEqual(2, data.WordsCount());
}
در اینجا ! ، و امثال آن نیز یک کلمه درنظر گرفته می‌شوند. برای حل این مشکل کافی است آرایه‌ی split را کمی تکمیل‌تر کنیم تا حروف غیرمجاز را درنظر نگیرد:
 var words = text.Split(
    new[] { ' ', ',', ';', '.', '!', '"', '(', ')', '?', ':', '\'', '«' , '»', '+', '-' },
    StringSplitOptions.RemoveEmptyEntries);
return words.Length;
تا اینجا مشکل !، >< حل شد، اما در مورد متن ذیل چطور؟
[TestMethod]
public void TestSimpleHtmlSpacesWithNewLine()
{
    const string data = "<b>this is&nbsp;a&nbsp;&nbsp;test.</b>\n\r<b>this is&nbsp;a&nbsp;&nbsp;test.</b>";
    Assert.AreEqual(8, data.WordsCount());
}
مطالب ثبت شده، عموما توسط HTML Editorها ثبت می‌شوند. بنابراین دارای انواع و اقسام تگ‌ها بوده و همچنین ممکن است در این بین new line هم وجود داشته باشد که در این حالت، test\n\rtest باید دو کلمه محاسبه شود و نه یک کلمه.
اگر این موارد را در نظر بگیریم، به کلاس ذیل خواهیم رسید:
using System;
using System.Text.RegularExpressions;
 
namespace ReadingTime
{
    public static class CalculateWordsCount
    {
        private static readonly Regex _matchAllTags =
            new Regex(@"<(.|\n)*?>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 
        public static int WordsCount(this string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                return 0;
            }
 
            text = text.cleanTags().Trim();
            text = text.Replace("\t", " ");
            text = text.Replace("\n", " ");
            text = text.Replace("\r", " ");
 
            var words = text.Split(
                new[] { ' ', ',', ';', '.', '!', '"', '(', ')', '?', ':', '\'', '«' , '»', '+', '-' },
                StringSplitOptions.RemoveEmptyEntries);
            return words.Length;
        }
 
        private static string cleanTags(this string data)
        {
            return data.Replace("\n", "\n ").removeHtmlTags();
        }
 
        private static string removeHtmlTags(this string text)
        {
            return string.IsNullOrEmpty(text) ?
                        string.Empty :
                        _matchAllTags.Replace(text, " ").Replace("&nbsp;", " ");
        }
    }
}
در اینجا حذف تگ‌های HTML و همچنین پردازش خطوط جدید و حروف غیرمجاز درنظر گرفته شده‌اند.

پس از اینکه موفق به شمارش تعداد کلمات یک متن HTML ایی شدیم، اکنون می‌توان این تعداد را تقسیم بر 180 (یک عدد معمول و متداول) کرد تا زمان خواندن کل متن بدست آید. سپس با استفاده از متد toReadableString می‌توان آن‌را به شکل قابل خواندن‌تری نمایش داد.
using System;
 
namespace ReadingTime
{
    public static class CalculateReadingTime
    {
        public static string MinReadTime(this string text, int wordsPerMinute = 180)
        {
            var wordsCount = text.WordsCount();
            var minutes = wordsCount / wordsPerMinute;
            return minutes == 0 ? "کمتر از یک دقیقه" : TimeSpan.FromMinutes(minutes).toReadableString();
        }
 
        private static string toReadableString(this TimeSpan span)
        {
            var formatted = string.Format("{0}{1}{2}{3}",
                span.Duration().Days > 0 ? string.Format("{0:0} روز و ", span.Days) : string.Empty,
                span.Duration().Hours > 0 ? string.Format("{0:0} ساعت و ", span.Hours) : string.Empty,
                span.Duration().Minutes > 0 ? string.Format("{0:0} دقیقه و ", span.Minutes) : string.Empty,
                span.Duration().Seconds > 0 ? string.Format("{0:0} ثانیه", span.Seconds) : string.Empty);
 
            if (formatted.EndsWith("و "))
            {
                formatted = formatted.Substring(0, formatted.Length - 2);
            }
 
            if (string.IsNullOrEmpty(formatted))
            {
                formatted = "0 ثانیه";
            }
            return formatted.Trim();
        }
    }
}

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
ReadingTime.zip
 
اشتراک‌ها
پیاده سازی صحیح الگوی PRG در ASP.NET MVC

خلاصه کار استفاده از دو اکشن فیلتر زیر برروی اکشن‌های GET و POST میباشد:

public abstract class ModelStateTransfer : ActionFilterAttribute  
{
    protected static readonly string Key = typeof(ModelStateTransfer).FullName;
}

public class ExportModelStateAttribute : ModelStateTransfer  
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        //Only export when ModelState is not valid
        if (!filterContext.Controller.ViewData.ModelState.IsValid)
        {
            //Export if we are redirecting
            if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult))
            {
                filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

public class ImportModelStateAttribute : ModelStateTransfer  
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;

        if (modelState != null)
        {
            //Only Import if we are viewing
            if (filterContext.Result is ViewResult)
            {
                filterContext.Controller.ViewData.ModelState.Merge(modelState);
            }
            else
            {
                //Otherwise remove it.
                filterContext.Controller.TempData.Remove(Key);
            }
        }

        base.OnActionExecuted(filterContext);
    }
}
پیاده سازی صحیح الگوی PRG در ASP.NET MVC