مطالب
معرفی Async Parallel.ForEach در دات نت 6
عموما زمانیکه می‌خواهیم تمام وظایف مدنظر، به صورت موازی اجرا شوند، آن‌ها را Task.WhenAll می‌کنیم. برای مثال 10 هزار درخواست HTTP را به صورت وظایفی، WhenAll می‌کنیم و ... در این حالت ... سرور ریموت، IP شما را خواهد بست! چون کنترلی بر روی تعداد وظیفه‌ی در حالت اجرای موازی وجود ندارد و یک چنین عملی، شبیه به یک حمله‌ی DDOS عمل می‌کند! برای مدیریت بهتر یک چنین مواردی، در دات نت 6 متدهای Parallel.ForEachAsync ارائه شده‌اند تا دیگر نیازی به استفاده از راه‌حل‌های ثالثی که عموما آنچنان بهینه هم نیستند، نباشد.
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
این مجموعه متدها از ValueTaskها بجای Taskها استفاده می‌کند تا سربار ایجاد Taskها در حلقه‌ها کاهش یابد. همچنین در اینجا degree of parallelism به صورت پیش‌فرض به تعداد هسته‌های سی‌پی تنظیم شده‌است (Environment.ProcessorCount)؛ چون عموما توسعه دهنده‌ها نمی‌دانند که چه عددی را باید برای آن انتخاب کنند. هر چند امکان تنظیم دستی آن‌ها هم وجود دارد (یکی از مهم‌ترین مشکلات کار با WhenAll).

یک مثال: در اینجا می‌خواهیم به صورت موازی، مشخصات کاربرانی از Github را توسط HttpClient دریافت کنیم. هر بار هم فقط می‌خواهیم سه وظیفه اجرا شوند و نه بیشتر
using System.Net.Http.Headers;
using System.Net.Http.Json;
 
var userHandlers = new []  { "users/VahidN", "users/shanselman", "users/jaredpar", "users/davidfowl" };
 
using HttpClient client = new()
{
    BaseAddress = new Uri("https://api.github.com"),
};
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("DotNet", "6"));
 
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = 3 };
 
await Parallel.ForEachAsync(userHandlers, parallelOptions, async (uri, token) =>
{
    var user = await client.GetFromJsonAsync<GitHubUser>(uri, token); 
    Console.WriteLine($"Name: {user.Name}\nBio: {user.Bio}\n");
});
 
public class GitHubUser
{
    public string Name { get; set; }
    public string  Bio { get; set; }
}
در این مثال، نمونه‌ای از کارکرد متد جدید Parallel.ForEachAsync را مشاهده می‌کنید که اینبار، MaxDegreeOfParallelism آن قابل تنظیم است. یعنی با تنظیم فوق، هربار فقط سه وظیفه به صورت موازی اجرا خواهند شد. البته تنظیم آن به منهای یک، همان حالت WhenAll را سبب خواهد شد؛ یعنی محدودیتی وجود نخواهد داشت.
متد Parallel.ForEachAsync، آرایه‌ای را که باید بر روی آن کار کند، دریافت می‌کند. سپس تنظیمات اجرای موازی آن‌ها را هم مشخص می‌کنیم. در ادامه آن‌ها را در دسته‌های مشخصی، به صورت موازی بر اساس منطقی که مشخص می‌کنیم، اجرا خواهد کرد.


وضعیت امکان اجرای موازی متدهای async همزمان، تا پیش از دات نت 6

<List<T به همراه متد الحاقی ForEach است که می‌تواند یک <Action<T را بر روی المان‌های این لیست، اجرا کند و ... عموما زمانیکه به وظایف async می‌رسیم، به اشتباه مورد استفاده قرار می‌گیرد:
customers.ForEach(c => SendEmailAsync(c));
مثال فوق، با اجرای حلقه‌ی زیر تفاوتی ندارد:
foreach(var c in customers)
{
    SendEmailAsync(c); // the return task is ignored
}
یعنی یک عملیات async، بدون await فراخوانی شده‌است و تا پایان عملیات مدنظر، صبر نخواهد شد. حداقل مشکل آن این است که اگر در این بین استثنایی رخ دهد، هیچگاه متوجه آن نخواهید شد و حتی می‌تواند کل پروسه‌ی برنامه را خاتمه دهد. شاید عنوان کنید که می‌شود این مشکل را به صورت زیر حل کرد:
customers.ForEach(async c => await SendEmailAsync(c));
اما ... این روش هم تفاوتی با قبل ندارد. از این لحاظ که متد ForEach یک <Action<T را دریافت می‌کند که خروجی آن void است. یعنی در نهایت با راه حل دوم، فقط یک async void ایجاد می‌شود که باز هم قابلیت صبر کردن تا پایان عملیات را ندارد. نکته‌ی مهم اینجا است که اجرای موازی آن‌ها توسط متد Parallel.ForEach نیز دقیقا همین مشکل را دارد.
تنها راه حل پذیرفته‌ی شده‌ی چنین عمل async ای، فراخوانی آن‌ها به صورت متداول زیر و بدون استفاده از متد ForEach است:
foreach(var c in customers)
{
   await SendEmailAsync(c);
}
و یا Task.WhenAll کردن آن‌ها، با علم به این موضوع که MaxDegreeOfParallelism آن قابل کنترل نیست (حداقل به صورت استاندارد و بدون نیاز به کتابخانه‌های جانبی). برای مثال بجای نوشتن:
foreach(var o in orders)
{
    await ProcessOrderAsync(o);
}
می‌توان آن‌را به صورت زیر درآورد:
var tasks = orders.Select(o => ProcessOrderAsync(o)).ToList();
await Task.WhenAll(tasks);
در این حالت عملیات ProcessOrderAsync را تبدیل به لیستی از وظایف مدنظر کرده و به متد Task.WhenAll ارسال می‌کنیم تا به صورت موازی اجرا شوند. اما ... اگر 10 هزار Task وجود داشته باشند، کنترلی بر روی تعداد وظایف در حال اجرای موازی وجود نخواهد داشت و این مورد نه تنها سبب بالا رفتن کارآیی نخواهد شد، بلکه می‌تواند سرور را هم با اخلال پردازشی، به علت کمبود منابع در دسترس مواجه کند.

دات نت 6، هم کنترل MaxDegreeOfParallelism را میسر کرده‌است و هم اینکه اینبار نگارش async واقعی Parallel.ForEachAsync را ارائه داده‌است تا دیگر همانند حالت قبلی Parallel.ForEach، به async void‌ها و مشکلات مرتبط با آن‌ها نرسیم.
مطالب
تخمین مدت زمان خوانده شدن یک مطلب
پس از انتشار مطلب «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
 
مطالب
جلوگیری از ورود نام Area های یکسان، در هنگام درج اطلاعات در برنامه‌های ASP.NET MVC 5x
در وب‌سایتی مثل آپارات، چنین آدرسی aparat.com/reporting به منزله‌ی آدرس دهی به کانال شخصیِ فردی است. حال اگر وب‌سایت ما نیز چنین سیستم آدرس دهی را داشته باشد و همچنین پیشتر یک Area با نام Reporting را نیز داشته باشیم، توسط چنین آدرسی (درحالت پیش فرض) به آن Area دسترسی خواهیم داشت:
mysite.com/reporting
حال اگر یکی از کاربران هنگام ساخت کانالی جدید (برای سناریوی بالا)، بخواهد آدرس کانالش Reporting باشد، با توجه به اینکه هم مسیر دسترسی به Area گزارشات (Reporting) و هم مسیر دسترسی به کانال این شخص از طریق Url بالا است، قطعا به مشکل خواهیم خورد.
برای رفع این مشکل میتوان یک فایل xml، txt و ... درست کرد و نام تمامی Area‌‌ها را در آن فایل ثبت کرد و بعد، هنگام ثبت کانال جدید (برای سناریوی بالا) توسط کاربر، فایل مذکور را خوانده و در صورتیکه نام آدرس وارد شده معادل یکی از Area‌‌های سایتمان بود و در لیست Area‌‌های از پیش ثبت شده در آن فایل قرار داشت، پیغام لازم را به کاربر نشان می‌دهیم و از ثبت و یا ویرایش اطلاعات، جلوگیری می‌کنیم.
روش فوق به درستی کار می‌کند و مشکلی ندارد، اما ضعف آن این است که به صورت دستی این عملیات باید انجام شود و در صورتیکه یک Area جدید اضافه شود، باید آن فایل ویرایش شود. اما می‌توان با استفاده از یک Attribute، این کار را انجام و تمامی عملیات را به صورت داینامیک انجام داد.
برای شروع، یک مدل برای کانال و یک منبع داده را برای آن در نظر می‌گیریم:
using System.ComponentModel.DataAnnotations;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        public string ChannelUrl { get; set; }
    }
}
using System.Collections.Generic;

namespace SampleProject.Models
{
    public static class ChannelDataSource
    {
        static ChannelDataSource() => Channels = new List<Channel>();
        public static List<Channel> Channels { get; private set; }
        public static void Add(Channel channel) => Channels.Add(channel);
    }
}
منبع داده، شامل یک خاصیت است که لیست تمامی کانال‌های از قبل اضافه شده را بر می‌گرداند و یک متد افزودن که به این لیست، یک کانال را اضافه می‌کند.
حال یک کنترلر به نام Channel را اضافه می‌کنیم:
using SampleProject.Models;
using System.Linq;
using System.Web.Mvc;

namespace SampleProject.Controllers
{
    public class ChannelController : Controller
    {
        // GET: Channel
        public ActionResult Index()
        {
            var channels = ChannelDataSource.Channels;
            return View(channels);
        }

        public ActionResult Channel(string channelUrl)
        {
            if (string.IsNullOrWhiteSpace(channelUrl))
            {
                return new HttpNotFoundResult("channel not found!");
            }
            var channel = ChannelDataSource.Channels.SingleOrDefault(ch => ch.ChannelUrl == channelUrl.ToLower());
            if (channel == null)
            {
                return new HttpNotFoundResult("channel not found!");
            }
            return View(channel);
        }

        public ActionResult Create() => View();

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Channel channel)
        {
            if (!ModelState.IsValid)
            {
                ModelState.AddModelError(string.Empty, "Please check your inputs!");
                return View(channel);
            }
            ChannelDataSource.Add(channel);
            TempData["Message"] = "Channel added successfully!";
            return RedirectToAction(nameof(Index));
        }
    }
}
در اکشن Index، لیستی از تمامی کانال‌های موجود را نمایش می‌دهیم. در اکشن Channel، آدرسی را که وارد شده است، در منبع داده به دنبال آن می‌گردیم و یک ویوو با Template جزئیات (Details)، از مدل کانال را به کاربر نمایش می‌دهیم؛ در غیر اینصورت صفحه 404 را نمایش می‌دهیم. در اکشن‌های Create، صفحه افزودن را به کاربر نمایش داده و در آن یکی اکشن، عمل افزودن را در صورتیکه اطلاعات وارد شده صحیح باشند، انجام می‌دهیم.
با توجه به اینکه میخواهیم سیستم مسیر دهی سایت برای کانال‌ها تغییر کند، فایل RouteConfig در پوشه‌ی App_Start را به شکل ذیل تغییر می‌دهیم:
using System.Web.Mvc;
using System.Web.Routing;

namespace SampleProject
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "ChannelUrls",
                url: "{channelurl}",
                defaults: new { controller = "Channel", action = "Channel", id = UrlParameter.Optional }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Channel", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
در مسیر دهی بالا اگر "نام سایت، اسلش، نام کانال" را وارد کند اولین سیستم مسیریابی فعال می‌شود و او را به اکشن Channel کنترلر Channel، راهنمایی می‌کند.
حال برای اینکه هنگام ساخت کانال جدید، نام تکراری یکی از Area‌ها را وارد نکند، به این ترتیب عمل می‌کنیم:
ابتدا یک متد کمکی را نوشته که لیست Area‌‌های پروژه‌مان را برگشت دهد ( + ):
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;

namespace SampleProject.Models
{
    public static class Utility
    {
        public static List<string> GetAllAreaNames()
        {
            var areaNames = RouteTable.Routes.OfType<Route>()
                            .Where(d => d.DataTokens != null)
                            .Where(d=> d.DataTokens.ContainsKey("area"))
                            .Select(r => r.DataTokens["area"].ToString().ToLower())
                            .ToList();
            return areaNames;
        }
    }
}
و بعد Attribute مورد نظر را ایجاد میکنیم:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using SampleProject.Models;

namespace SampleProject.CustomValidators
{
    public class CheckForAreaExisting : ValidationAttribute, IClientValidatable
    {
        public List<string> AreaNames
        {
            get
            {
                return Utility.GetAllAreaNames();
            }
        }
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ValidationType = "checkforareaexisting",
                ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
            };
            rule.ValidationParameters.Add("areanames", string.Join(",", AreaNames));
            yield return rule;
        }

        public override bool IsValid(object value)
        {
            if (value != null)
            {
                return Utility.GetAllAreaNames()
                   .SingleOrDefault(area => area == value.ToString().ToLower()) == null;
            }
            return true;
        }
    }
}
در کلاس بالا توسط متد IsValid بررسی میکنیم که آیا مقدار وارد شده ( Channel Url ) با یکی از نام‌های Area‌‌های پروژه‌مان تطابق دارد یا خیر، که اگر این چنین بود، مقدار false برگشت داده می‌شود.
توسط واسط IClientValidatable و متود GetClientValidationRules کارهای اعتبارسنجی سمت کلاینت را نیز انجام می‌دهیم ( + ). مقدار خاصیت ValidationType نام متدی است که در سمت کلاینت این کار را انجام می‌دهد. مقدار خاصیت ValidationParameters، مقداری است که به سمت کلاینت به عنوان param فرستاده می‌شود تا از آن جهت اینکه آیا مقدار وارد شده توسط کاربر، یکی از Area‌های سایت هست یا خیر، استفاده کرد. در اینجا نام Area‌‌‌ها را با یک رشته و با یک جداکننده، توسط این خاصیت به سمت کلاینت می‌فرستیم. 
حال در سمت کلاینت یک فایل Js را با نام CustomValidation و محتوای زیر ایجاد می‌کنیم:
jQuery.validator.addMethod("checkforareaexisting",
    function (value, element, param) {
        var isValueOneOfTheAreaNames = $.inArray(value.toLowerCase(), param.areaNames) === -1;
        return isValueOneOfTheAreaNames;
    });

$.validator.unobtrusive.adapters.add('checkforareaexisting', ['areanames'], function (options) {
    options.rules['checkforareaexisting'] = { areaNames: options.params.areanames.split(',') };
    options.messages['checkforareaexisting'] = options.message;
});
در بخش اول، نام متد که در بالا (Attribute) به آن اشاره شده است آمده است، و بعد بررسی می‌کنیم که آیا مقدار آمده توسط کاربر، یکی از نام‌های Area‌‌های موجود سایت است یا خیر که اگر این طور باشد، false برگشت داده می‌شود و پیغام خطا به کاربر نمایش داده می‌شود. در بخش Onubtrusive توسط پارامتری که در Attribute برای فرستادن نام Area‌ها نوشته بودیم (areanames)، نام‌های Area‌ها را می‌گیریم و بعد آن را Split و به Rule انتساب می‌دهیم و ErrorMessage ـی را که به خاصیت ChannelUrl مدلمان نسبت می‌دهیم، به عنوان پیغام خطا در نظر می‌گیریم.
فایل‌های Js در Layout باید به این صورت باشند:
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>_Layout</title>
    <style>

    </style>
</head>
<body>
    <div>
        @RenderBody()
    </div>
    <script src="~/Scripts/Jquery.js"></script>
    <script src="~/Scripts/jquery.validate.min.js"></script>
    <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
    <script src="~/Scripts/CustomValidation.js"></script>
</body>
</html>

حال کافی است به خاصیت ChannelUrl مدلمان این Attribute را نسبت دهیم:
using SampleProject.CustomValidators;
using System.ComponentModel.DataAnnotations;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        [CheckForAreaExisting(ErrorMessage = "You can't use this url for your channel!")]
        public string ChannelUrl { get; set; }
    }
}
اکنون نوبت آزمایش برنامه است. کافی است که یک یا چند Area جدید را با نام‌های متفاوت، اضافه کنید و الان اگر به صفحه افزودن کانال مراجعه کنید و نام یکی از Area‌‌های سایت را در قسمت Channel Url وارد کنید، پیغام خطا نمایش داده می‌شود.
نکته: در این حالت اسامی تمامی Area‌‌های سایت به کلاینت ارسال می‌شود. اگر از این بابت احساس رضایت نمی‌کنید، میتوانید از خاصیت Remote توکار MVC بهره ببرید.
برای اینکار این اکشن را به کنترلر Channel اضافه می‌کنیم:
[HttpPost]
public ActionResult CheckForAreaExisting(string channelUrl)
{
    var isValueOneOfTheAreaNames = Utility.GetAllAreaNames()
                                   .SingleOrDefault(area => area == channelUrl.ToLower()) == null;
    return Json(isValueOneOfTheAreaNames);
}  
و بعد مدل نیز به این صورت تغییر می‌کند:
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        [Remote("CheckForAreaExisting", "Channel",
            ErrorMessage = "You can't use this url for your channel!",
            HttpMethod = "Post")]
        public string ChannelUrl { get; set; }
    }
}
به این ترتیب هر بار درخواستی به سمت سرور ارسال و طی آن بررسی می‌شود که مقدار وارد شده یکی از Area‌‌‌‌های سایت هست یا خیر؟ بدیهی است که در این حالت، دیگر نیازی به واسط IClientValidatable در کلاس CheckForAreaExisting موجود در پوشه CustomValidators وجود ندارد.
مطالب
تغییر نام دسته جمعی تعدادی فایل PDF بر اساس متادیتای فایل‌ها
فرض کنید تعداد زیادی فایل PDF را با اسامی نامفهومی داریم. برای نظم بخشیدن و یافتن ساده‌تر مطالب شاید بهتر باشد این فایل‌ها را بر اساس عنوان اصلی ذخیره شده در فایل، تغییر نام دهیم.


امکان خواندن meta data فوق (البته در صورت وجود)، توسط iTextSharp وجود دارد. در ادامه قطعه کد ساده‌ای را ملاحظه می‌کنید که در یک پوشه، تمام فایل‌های PDF را یافته و بر اساس Title یا Subject آن‌ها، فایل موجود را تغییر نام می‌دهد:
using System.IO;
using iTextSharp.text.pdf;

namespace BatchRename
{
    class Program
    {
        private static string getTitle(PdfReader reader)
        {
            string title;
            reader.Info.TryGetValue("Title", out title); // Reading PDF file's meta data
            return string.IsNullOrWhiteSpace(title) ? string.Empty : title.Trim();
        }

        private static string getSubject(PdfReader reader)
        {
            string subject;
            reader.Info.TryGetValue("Subject", out subject); // Reading PDF file's meta data
            return string.IsNullOrWhiteSpace(subject) ? string.Empty : subject.Trim();
        }

        static void Main(string[] args)
        {
            var dir = @"D:\Path";
            if (!dir.EndsWith(@"\"))
                dir = dir + @"\";

            foreach (var file in Directory.GetFiles(dir, "*.pdf"))
            {
                var reader = new PdfReader(file);
                var title = getTitle(reader);
                var subject = getSubject(reader);
                reader.Close();

                string newFile = string.Empty;
                if (!string.IsNullOrWhiteSpace(title))
                {
                    newFile = dir + title + ".pdf";
                }
                else if (!string.IsNullOrWhiteSpace(subject))
                {
                    newFile = dir + subject + ".pdf";
                }

                if (!string.IsNullOrWhiteSpace(newFile))
                    File.Move(file, newFile);
            }
        }
    }
}

در قطعه کد فوق علت مراجعه به reader.Info، بر اساس ساختار یک فایل PDF است. در Dictionary به نام Info (تصویر فوق)، در یک سری کلید مشخص، اطلاعاتی مانند تهیه کننده، عنوان و غیره درج می‌شوند. به این ترتیب با استفاده از شیء PdfReader، فایل را گشوده، این متادیتا را خوانده و سپس بر اساس آن می‌توان فایل را تغییر نام داد.
 
مطالب
مدیریت سفارشی سطوح دسترسی کاربران در MVC
فرض کنید در پروژه‌ی جاری خودتون قصد دارید یک سیستم مدیریت سطوح دسترسی کاربران رو با انعطاف بالا پیاده سازی کنید . مثلا سیستم شما دارای صفحات مختلفی هستش که هر گروه کاربری اجازه دسترسی به هر صفحه رو نداره ... هدف اینه که شما این گروه‌های کاربری و سطوح دسترسی برای هر گروه رو برای سیستم مشخص کنید .
مثلا فقط کاربرانی که دسترسی admin دارن بتونن به صفحات مدیریتی دسترسی داشته باشن و ....
برای این منظور در دات نت کلاسی با نام Role Provider وجود داره که در ادامه‌ی این مبحث به کار با اون میپردازیم.
مثلا فرض کنید قرار بر اینه که سطوح دسترسی رو از بانک اطلاعاتی استخراج کنیم . کلاس مذکور در فضای نام System.Web.Security قرار گرفته . برای شروع ما نیاز داریم یک نمونه از این کلاس رو پیاده سازی کرده و چند تابع از اون رو بازنویسی کنیم . 

پیاده سازی کلاس به این صورته :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;

namespace Myproject.Security
{
    public class CustomRoleProvider : RoleProvider
    {

    }
}
 خب در مرحله‌ی بعد دو تابع از این کلاس رو بازنویسی میکنیم . اول تابع GetRolesForUser که در این مقاله وظیفه‌ی استخراج لیست مجوز‌ها برای هر کاربر رو از بانک اطلاعاتی داره که به شکل زیر پیاده سازی میشه .
public override string[] GetRolesForUser(string username)
        {
            using (DatabaseEntities db = new DatabaseEntities())
            {
                User user = db.Users.FirstOrDefault(u => u.UserName.Equals(username, StringComparison.CurrentCultureIgnoreCase));
 
                var roles = from ur in user.UserRoles
                            from r in db.Roles
                            where ur.RoleId == r.Id
                            select r.Name;
                if (roles != null)
                    return roles.ToArray();
                else
                    return new string[] {}; ;
            }
        }
 همونطور که میبینید در تابع بالا از کلاس CustomRoleProvider ما عملیات استخراج لیست مجوز‌های دسترسی مربوط به هر کاربر رو از بانک اطلاعاتی انجام دادیم . توجه داشته باشین که شما میتونید این واکشی رو از هر محیط دیگه ای جز بانک اطلاعاتی هم بسته به نوع کارتون انجام بدید .

تابع بعد IsUserInRole نام داره که با بازنویسی اون مشخص میکنیم که ایا یک کاربر دارای مجوز لازم برای دسترسی هست یا نه . اون رو به شکل زیر بازنویسی میکنیم.
public override bool IsUserInRole(string username, string roleName)
        {
            using (DatabaseEntities db = new DatabaseEntities())
            {
                User user = db.Users.FirstOrDefault(u => u.UserName.Equals(username, StringComparison.CurrentCultureIgnoreCase));
 
                var roles = from ur in user.UserRoles
                            from r in db.Roles
                            where ur.RoleId == r.Id
                            select r.Name;
                if (user != null)
                    return roles.Any(r => r.Equals(roleName, StringComparison.CurrentCultureIgnoreCase));
                else
                    return false;
            }
        }
 همونطور که شاهد هستید در تابع بالا بعد از واکشی لیست مجوز‌های ثبت شده برای هر کاربر بررسی انجام میشه که ایا اولا کاربر یک کاربر ثبت شده هست و ثانیا اینکه ایا درخواستی که ارسال کرده برای دسترسی به یک بخش مجوز اون رو داره یا خیر ... 

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

public override bool IsUserInRole(string username, string roleName)
        {
  return this.GetRolesForUser(username).Contains(roleName);
        }

خب میرسیم به بخش معرفی این Provider در web.config که به صورت زیر انجام میشه ...
<system.web>
    ...
    <rolemanager cacherolesincookie="true" defaultprovider="CustomRoleProvider" enabled="true">
        <providers>
            <clear />
            <add name="CustomRoleProvider" type="Myproject.Security.CustomRoleProvider" />
        </providers>
    </rolemanager>
    ...
</system.web>

 توجه داشته باشید که مجوز‌های هر کاربر با معرفی بالا بعد از یک بار واکشی در کوکی ذخیره میشه و دیگه هر بار، بار اضافه برای واکشی از بانک اطلاعاتی به برنامه تحمیل نمیشه ...

حالا به این صورت میتونیم مثلا یک Controller رو محافظت  کنیم در برابر درخواست از جانب کاربرانی که سطح دسترسی به اون رو ندارند .
using System;
using System.Web.Mvc;
 
namespace MyProject.Areas.Admin.Controllers
{
    [Authorize(Roles = "Administrators")]
    public class HomeController : Controller
    {
        //
        // GET: /Admin/Home/
 
        public ActionResult Index()
        {
            return View();
        }
 
    }
}
 توجه داشته باشید که کنترل مجوز برای بررسی  وجود مجوز در بخش‌های کوچکتر هم مانند اکشن‌ها و ... میتونه در نظر گرفته بشه .
مطالب
نمایش خطاهای اعتبارسنجی سمت کاربر ASP.NET MVC به شکل Tooltip به کمک Twitter bootstrap
این مطلب در ادامه بحث «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» می‌باشد. بنابراین تعاریف مدل و کنترلر آن به همراه توضیحات ذکر شده در آن، در ادامه مورد استفاده قرار خواهند گرفت.


اصول نمایش Tooltip در Bootstrap

Tooltip یکی دیگر از کامپوننت‌های جاوا اسکریپتی Bootstrap است.
<a rel="tooltip" title="یک سری توضیحات در اینجا" href="#">اطلاعات</a>

<script type="text/javascript">  
   $(document).ready(function () {  
     $("[rel='tooltip']").tooltip({placement:'top', trigger : 'hover'});  
   });  
</script>
نحوه استفاده از آن را در مثال فوق مشاهده می‌کنید.
برای المان مورد نظر، یک title تعریف کرده‌ایم. برای اینکه tooltip پیش فرض مرورگر در این حالت ظاهر نشود، یک rel=tooltip را در اینجا به المان اضافه کرده و سپس افزونه tooltip بوت استرپ، بر روی کلیه المان‌هایی با rel=tooltip فراخوانی شده است.
placement، محل قرارگیری tooltip را مشخص می‌کند؛ مانند بالا، پایین، چپ و راست. trigger مشخص می‌کند که این tooltip در چه زمانی ظاهر شود. برای مثال در حالت hover یا در حالت focus یک المان.
در اینجا بجای title، از ویژگی data-original-title نیز می‌توان استفاده کرد.



تبدیل خطاهای اعتبارسنجی ASP.NET MVC به Tooltip

هدف ما در اینجا، نهایتا رسیدن به شکل زیر می‌باشد:


همانطور که ملاحظه می‌کنید، اینبار بجای نمایش خطاها در یک برچسب مقابل کنترل متناظر، این خطا صرفا در حالت فوکوس کنترل، به شکل یک Tooltip در بالای آن ظاهر شده است.


کدهای کامل View برنامه

@model Mvc4TwitterBootStrapTest.Models.User
@{
    ViewBag.Title = "تعریف کاربر جدید";
}
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" })

    <fieldset class="form-horizontal">
        <legend>تعریف کاربر جدید</legend>
        <div class="control-group">
            @Html.LabelFor(model => model.Name, new { @class = "control-label" })
            <div class="controls">
                @Html.EditorFor(model => model.Name)
                @*@Html.ValidationMessageFor(model => model.Name, null, new { @class = "help-inline" })*@
            </div>
        </div>
        <div class="control-group">
            @Html.LabelFor(model => model.LastName, new { @class = "control-label" })
            <div class="controls">
                @Html.EditorFor(model => model.LastName)
                @*@Html.ValidationMessageFor(model => model.LastName, null, new { @class = "help-inline" })*@
            </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">
                ارسال</button>
        </div>
    </fieldset>
}
@section JavaScript
{
    <script type="text/javascript">
        $.validator.setDefaults({            
            showErrors: function (errorMap, errorList) {
                this.defaultShowErrors();
                //اگر المانی معتبر است نیاز به نمایش تول‌تیپ ندارد
                $("." + this.settings.validClass).tooltip("destroy");
                //افزودن تول‌تیپ‌ها
                for (var i = 0; i < errorList.length; i++) {
                    var error = errorList[i];
                    $(error.element).tooltip({ trigger: "focus" }) // فقط در حالت فوکوس نمایش داده شود
                                    .attr("data-original-title", error.message);
                }
            },
            // همانند قبل برای رنگی کردن کل ردیف در صورت عدم اعتبار سنجی و برعکس
            highlight: function (element, errorClass, validClass) {
                if (element.type === 'radio') {
                    this.findByName(element.name).addClass(errorClass).removeClass(validClass);
                } else {
                    $(element).addClass(errorClass).removeClass(validClass);
                    $(element).closest('.control-group').removeClass('success').addClass('error');
                }
                $(element).trigger('highlited');
            },
            unhighlight: function (element, errorClass, validClass) {
                if (element.type === 'radio') {
                    this.findByName(element.name).removeClass(errorClass).addClass(validClass);
                } else {
                    $(element).removeClass(errorClass).addClass(validClass);
                    $(element).closest('.control-group').removeClass('error').addClass('success');
                }
                $(element).trigger('unhighlited');
            }
        });
        //برای حالت پست بک از سرور عمل می‌کند
        $(function () {
            $('form').each(function () {
                $(this).find('div.control-group').each(function () {
                    if ($(this).find('span.field-validation-error').length > 0) {
                        $(this).addClass('error');
                    }
                });
            });
        });
    </script>
}
کدهای مدل و کنترلر همانند مطلب «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» می‌باشند و از تکرار مجدد آن‌ها در اینجا صرفنظر گردید.

توضیحات
- با توجه به اینکه دیگر نمی‌خواهیم خطاها به صورت برچسب در مقابل کنترل‌ها نمایش داده شوند، کلیه Html.ValidationMessageFor به صورت کامنت درآورده شده‌اند.
- تغییر دوم مطلب جاری، اضافه شدن متد showErrors به تنظیمات پیش فرض jQuery Validator است. در این متد، اگر المانی معتبر بود، Tooltip آن حذف می‌شود؛ یا در سایر حالات، المان‌هایی که نیاز به اعتبارسنجی سمت کلاینت دارند، یافت شده و سپس ویژگی data-original-title با مقداری معادل خطای اعتبارسنجی متناظر، به این المان افزوده شده و سپس متد tooltip بوت استرپ بر روی آن فراخوانی می‌گردد.
به عبارتی زمانیکه یک input box در ASP.NET MVC به همراه مقادیر مرتبط با اعتبارسنجی آن رندر می‌شود، چنین شکلی را خواهد داشت:
<input class="text-box single-line" data-val="true" data-val-required="لطفا نام را تکمیل کنید"
 id="Name" name="Name" type="text" value="" />
اما در اینجا به صورت پویا data-original-title نیز به آن افزوده می‌گردد:
<input class="text-box single-line input-validation-error" data-val="true" 
data-val-required="لطفا نام را تکمیل کنید" id="Name" name="Name" type="text" value="" 
data-original-title="لطفا نام را تکمیل کنید" title="">
این مقدار توسط افزونه tooltip بوت استرپ شناسایی شده و مورد استفاده قرار می‌گیرد.
مطالب
انجام پی در پی اعمال Async به کمک Iterators - قسمت دوم

در قسمت قبل ایده‌ی اصلی و مفاهیم مرتبط با استفاده از Iterators مطرح شد. در این قسمت به یک مثال عملی در این مورد خواهیم پرداخت.

چندین کتابخانه و کلاس جهت مدیریت Coroutines در دات نت تهیه شده که لیست آن‌ها به شرح زیر است:
1) Using C# 2.0 iterators to simplify writing asynchronous code
2) Wintellect's Jeffrey Richter's PowerThreading Library
3) Rob Eisenberg's Build your own MVVM Framework codes

و ...

مورد سوم که توسط خالق اصلی کتابخانه‌ی Caliburn (یکی از فریم ورک‌های مشهور MVVM برای WPF و Silverlight) در کنفرانس MIX 2010 ارائه شد، این روزها در وبلاگ‌های مرتبط بیشتر مورد توجه قرار گرفته و تقریبا به یک روش استاندارد تبدیل شده است. این روش از یک اینترفیس و یک کلاس به شرح زیر تشکیل می‌شود:

using System;

namespace SLAsyncTest.Helper
{
public interface IResult
{
void Execute();
event EventHandler Completed;
}
}

using System;
using System.Collections.Generic;

namespace SLAsyncTest.Helper
{
public class ResultEnumerator
{
private readonly IEnumerator<IResult> _enumerator;

public ResultEnumerator(IEnumerable<IResult> children)
{
_enumerator = children.GetEnumerator();
}

public void Enumerate()
{
childCompleted(null, EventArgs.Empty);
}

private void childCompleted(object sender, EventArgs args)
{
var previous = sender as IResult;

if (previous != null)
previous.Completed -= childCompleted;

if (!_enumerator.MoveNext())
return;

var next = _enumerator.Current;
next.Completed += childCompleted;
next.Execute();
}
}
}

توضیحات:
مطابق توضیحات قسمت قبل، برای مدیریت اعمال همزمان به شکلی پی در پی، نیاز است تا یک IEnumerable را به همراه yield return در پایان هر مرحله از کار ایجاد کنیم. در اینجا این IEnumerable را از نوع اینترفیس IResult تعریف خواهیم کرد. متد Execute آن شامل کدهای عملیات Async خواهند شد و پس از پایان کار رخداد Completed صدا زده می‌شود. به این صورت کلاس ResultEnumerator به سادگی می‌تواند یکی پس از دیگری اعمال Async مورد نظر ما را به صورت متوالی فراخوانی نمائید. با هر بار فراخوانی رخداد Completed، متد MoveNext صدا زده شده و یک مرحله به جلو خواهیم رفت.
برای مثال کدهای ساده WCF Service زیر را در نظر بگیرید.

using System.ServiceModel;
using System.ServiceModel.Activation;
using System.Threading;

namespace SLAsyncTest.Web
{
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode
= AspNetCompatibilityRequirementsMode.Allowed)]
public class TestService
{
[OperationContract]
public int GetNumber(int number)
{
Thread.Sleep(2000);//Simulating a log running operation
return number * 2;
}
}
}

قصد داریم در طی دو مرحله متوالی این WCF Service را در یک برنامه‌ی Silverlight فراخوانی کنیم. کدهای قسمت فراخوانی این سرویس بر اساس پیاده سازی اینترفیس IResult به صورت زیر درخواهند آمد:

using System;
using SLAsyncTest.Helper;

namespace SLAsyncTest.Model
{
public class GetNumber : IResult
{
public int Result { set; get; }
public bool HasError { set; get; }

private int _num;
public GetNumber(int num)
{
_num = num;
}

#region IResult Members
public void Execute()
{
var srv = new TestServiceReference.TestServiceClient();
srv.GetNumberCompleted += (sender, e) =>
{
if (e.Error == null)
Result = e.Result;
else
HasError = true;

Completed(this, EventArgs.Empty); //run the next IResult
};
srv.GetNumberAsync(_num);
}

public event EventHandler Completed;
#endregion
}
}
در متد Execute کار فراخوانی غیرهمزمان WCF Service به صورتی متداول انجام شده و در پایان متد Completed صدا زده می‌شود. همانطور که توضیح داده شد، این فراخوانی در کلاس ResultEnumerator یاد شده مورد استفاده قرار می‌گیرد.
اکنون قسمت‌های اصلی کدهای View Model برنامه به شکل زیر خواهند بود:

private void doFetch(object obj)
{
new ResultEnumerator(executeAsyncOps()).Enumerate();
}

private IEnumerable<IResult> executeAsyncOps()
{
FinalResult = 0;
IsBusy = true; //Show BusyIndicator

//Sequential Async Operations
var asyncOp1 = new GetNumber(10);
yield return asyncOp1;

//using the result of the previous step
if(asyncOp1.HasError)
{
IsBusy = false; //Hide BusyIndicator
yield break;
}

var asyncOp2 = new GetNumber(asyncOp1.Result);
yield return asyncOp2;

FinalResult = asyncOp2.Result; //Bind it to the UI

IsBusy = false; //Hide BusyIndicator
}
در اینجا یک IEnumerable از نوع IResult تعریف شده است و در طی دو مرحله‌ی متوالی اما غیرهمزمان کار دریافت اطلاعات از WCF Service صورت می‌گیرد. ابتدا عدد 10 به WCF Service ارسال می‌شود و خروجی 20 خواهد بود. سپس این عدد در مرحله‌ی بعد مجددا به WCF Service ارسال گردیده و حاصل نهایی که عدد 40 می‌باشد در اختیار سیستم Binding قرار خواهد گرفت.
اگر از این روش استفاده نمی‌شد ممکن بود به این جواب برسیم یا خیر. ممکن بود مرحله‌ی دوم ابتدا شروع شود و سپس مرحله‌ی اول رخ دهد. اما با کمک Iterators و yield return به همراه کلاس ResultEnumerator موفق شدیم تا عملیات دوم همزمان را در حالت تعلیق قرار داده و پس از پایان اولین عملیات غیر همزمان، مرحله‌ی بعدی فراخوانی را بر اساس مقدار حاصل شده از WCF Service آغاز کنیم.
این روش برای برنامه‌ نویس‌ها آشناتر است و همان سیستم فراخوانی A->B->C را تداعی می‌کند اما کلیه اعمال غیرهمزمان هستند و ترد اصلی برنامه قفل نخواهد شد.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.

مطالب
آماده سازی زیرساخت تهیه Integration Tests برای ServiceLayer

پیشنیاز

در این مطلب قصد داریم تست ServiceLayer را به جای تست درون حافظه‌ای که با ابزارهای Mocking در قالب Unit Testing انجام میگیرد، به کمک یک دیتابیس واقعی سبک وزن در قالب Integration Testing انجام دهیم.


قدم اول

یک پروژه تست را ایجاد کنید؛ بهتر است برای نظم دهی به ساختار Solution، پروژه‌های تست را در پوشه ای به نام Tests نگهداری کنید.



قدم دوم

بسته‌های نیوگت زیر را نصب کنید:

PM> install-package NUnit
PM> install-package Shouldly
PM> install-package EntityFramework
PM> install-package FakeHttpContext


قدم سوم

نسخه دیتابیس انتخابی برای تست خودکار، LocalDB می باشد. لازم است در ابتدای اجرای تست‌ها دیتابیس مربوط به Integration Test ایجاد شده و بعد از اتمام نیز دیتابیس مورد نظر حذف شود؛ برای این منظور از کلاس TestSetup استفاده خواهیم کرد.

[SetUpFixture]
public class TestSetup
{
    [OneTimeSetUp]
    public void SetUpDatabase()
    {
        DestroyDatabase();
        CreateDatabase();
    }

    [OneTimeTearDown]
    public void TearDownDatabase()
    {
        DestroyDatabase();
    }

   //...
}

با توجه به اینکه کلاس TestSetup با [SetUpFixture] تزئین شده است، Nunit قبل از اجرای تست‌ها سراغ این کلاس آمده و متد SetUpDatebase را به دلیل تزئین شدن با [OneTimeSetUp]، قبل از اجرای تست‌ها و متد TearDownDatabase را بدلیل تزئین شدن با [OneTimeTearDown]  بعد از اجرای تمام تست‌ها، اجرا خواهد کرد.


متد CreateDatabase

private static void CreateDatabase()
{
    ExecuteSqlCommand(Master, string.Format(SqlResource.DatabaseScript, FileName));

    //Use T-Sql Scripts For Create Database
    //ExecuteSqlCommand(MyAppTest, SqlResources.V1_0_0);

    var migration =
        new MigrateDatabaseToLatestVersion<ApplicationDbContext, DataLayer.Migrations.Configuration>();
    migration.InitializeDatabase(new ApplicationDbContext());

}

private static SqlConnectionStringBuilder Master =>
    new SqlConnectionStringBuilder
    {
        DataSource = @"(LocalDB)\MSSQLLocalDB",
        InitialCatalog = "master",
        IntegratedSecurity = true
    };

private static string FileName => Path.Combine(
    Path.GetDirectoryName(
        Assembly.GetExecutingAssembly().Location),
    "MyAppTest.mdf");

برای مدیریت محل ذخیره سازی فایل‌های دیتابیس، ابتدا دستورات ایجاد «دیتابیس تست» را برروی دیتابیس master اجرا می‌کنیم و در ادامه برای ساخت جداول از مکانیزم Migration خود EF استفاده شده است.

لازم است رشته اتصال به این دیتابیس ایجاد شده را در فایل App.config پروژه تست قرار دهید:

<connectionStrings>
  <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="Data Source=(LocalDB)\MSSQLLocalDb;Initial Catalog=MyAppTest;Integrated Security=True;" />
</connectionStrings>


متد DestroyDatabase 

private static void DestroyDatabase()
{
    var fileNames = ExecuteSqlQuery(Master, SqlResource.SelecDatabaseFileNames,
        row => (string)row["physical_name"]);

    if (!fileNames.Any()) return;

    ExecuteSqlCommand(Master, SqlResource.DetachDatabase);

    fileNames.ForEach(File.Delete);
}

در این متد ابتدا آدرس فایل‌های مرتبط با «دیتابیس تست» واکشی شده و در ادامه دستورات Detach دیتابیس انجام شده و فایل‌های مرتبط حذف خواهند شد. فایل‌های دیتابیس در مسیری شبیه به آدرس نشان داده شده‌ی در شکل زیر ذخیره خواهند شد.


قدم چهارم

برای جلوگیری از تداخل بین تست‌ها لازم است تک تک تست‌ها از هم ایزوله باشند؛ یکی از راه حل‌های موجود، استفاده از تراکنش‌ها می‌باشد. برای این منظور امکان AutoRollback را به صورت خودکار به متدهای تست با استفاده از Attribute زیر اعمال خواهیم کرد:

public class AutoRollbackAttribute : Attribute, ITestAction
{
    private TransactionScope _scope;

    public void BeforeTest(ITest test)
    {
        _scope = new TransactionScope(TransactionScopeOption.RequiresNew,new TransactionOptions {IsolationLevel = IsolationLevel.Snapshot});
    }

    public void AfterTest(ITest test)
    {
        _scope?.Dispose();
        _scope = null;
    }

    public ActionTargets Targets => ActionTargets.Test;
}

متدهای BeforTest و AfterTest به ترتیب قبل و بعد از اجرای متدهای تست تزئین شده با این Attribute اجرا خواهند شد. 


در مواقعی هم که به HttpConext نیاز دارید، می‌توانید از کتابخانه FakeHttpContext بهره ببرید. برای این مورد هم میتوان Attributeای را به مانند AutoRollback در نظر گرفت.

public class HttpContextAttribute:Attribute,ITestAction
{
    private FakeHttpContext.FakeHttpContext _httpContext;

    public void BeforeTest(ITest test)
    {
        _httpContext = new FakeHttpContext.FakeHttpContext();

    }

    public void AfterTest(ITest test)
    {
        _httpContext?.Dispose();
        _httpContext = null;
    }

    public ActionTargets Targets => ActionTargets.Test;
}

کاری که FakeHttpContext انجام می‌دهد، مقدار دهی HttpContext.Current با یک پیاده سازی ساختگی می‌باشد.


قدم پنجم

به عنوان مثال اگر بخواهیم برای سرویس «گروه کاربری»، Integration Test بنویسیم، به شکل زیر عمل خواهیم کرد:

namespace MyApp.IntegrationTests.ServiceLayer
{
    [TestFixture]
    [AutoRollback]
    [HttpContext]
    public class RoleServiceTests
    {
        private IRoleApplicationService _roleService;

        [SetUp]
        public void Init()
        {
        }

        [TearDown]
        public void Clean()
        {
        }

        [OneTimeSetUp]
        public void SetUp()
        {
            _roleService = IoC.Resolve<IRoleApplicationService>();

            using (var uow = IoC.Resolve<IUnitOfWork>())
            {
                RoleInitialDataBuilder.Build(uow);
            }
        }

        [OneTimeTearDown]
        public void TearDown()
        {
        }

        [Test]
        [TestCase("Role1")]
        public void Should_Create_New_Role(string role)
        {
            var viewModel = new RoleCreateViewModel
            {
                Name = role
            };

            _roleService.Create(viewModel);

            using (var context = IoC.Resolve<IUnitOfWork>())
            {
                var user = context.Set<Role>().FirstOrDefault(a => a.Name == role);
                user.ShouldNotBeNull();
            }
        }

        [Test]
        public void Should_Not_Create_New_Role_With_Admin_Name()
        {
            var viewModel = new RoleCreateViewModel
            {
                Name = "Admin"
            };

            Assert.Throws<DbUpdateException>(() => _roleService.Create(viewModel));
        }

        [Test]
        public void Should_AdminRole_Exists()
        {
            using (var context = IoC.Resolve<IUnitOfWork>())
            {
                var user = context.Set<Role>().FirstOrDefault(a => a.Name == "Admin");
                user.ShouldNotBeNull();
            }
        }

        [Test]
        public void Should_Not_Create_New_Role_Without_Name()
        {
            Assert.Throws<ValidationException>(() => _roleService.Create(new RoleCreateViewModel { Name = null }));
        }
    }
}

با این خروجی:



کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید. 
مطالب
استفاده از Kendo UI TreeView به همراه یک منبع داده راه دور
یکی دیگر از ویجت‌های Kendo UI، ویجت نمایش ساختارهای درختی است به نام TreeView. در ادامه قصد داریم با نحوه‌ی نمایش آن، به کمک اطلاعات JSON دریافتی از سرور آشنا شویم.



ساختار مورد نیاز یک Kendo UI Tree View

فرض کنید قصد دارید نظرات تو در توی مطلبی را توسط Kendo UI Tree View نمایش دهید. مدل خود ارجاع دهنده‌ی آن می‌تواند چنین شکلی را داشته باشد:
namespace KendoUI11.Models
{
    public class BlogComment
    {
        public int Id { set; get; }
 
        public string Body { set; get; }
 
        public int? ParentId { get; set; }
 
        // مخصوص کندو یو آی هستند
        public bool HasChildren { get; set; }
        public string imageUrl { get; set; }
    }
}
سه خاصیت اول این کلاس همواره در تمام کلاس‌های خود ارجاع دهنده حضور دارند؛ شماره ردیف، متن و شماره Id والد احتمالی.
چند خاصیت بعدی مانند HasChildren و imageUrl مخصوص Kendo UI هستند. از imageUrl اختیاری می‌توان جهت نمایش آیکنی در کنار یک آیتم استفاده کرد و HasChildren به این معنا است که آیا گره جاری دارای عناصر فرزندی می‌باشد یا خیر.


تهیه یک منبع داده نمونه

شکل ابتدای مطلب، از طریق منبع داده ذیل تهیه شده‌است:
using System.Collections.Generic;
 
namespace KendoUI11.Models
{
    /// <summary>
    /// منبع داده فرضی جهت سهولت دموی برنامه
    /// </summary>
    public static class BlogCommentsDataSource
    {
        private static readonly IList<BlogComment> _cachedItems;
        static BlogCommentsDataSource()
        {
            _cachedItems = createBlogCommentsDataSource();
        }
 
        public static IList<BlogComment> LatestComments
        {
            get { return _cachedItems; }
        }
 
        /// <summary>
        /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
        /// </summary>
        private static IList<BlogComment> createBlogCommentsDataSource()
        {
            var list = new List<BlogComment>();
 
            var comment1 = new BlogComment
            {
                Id = 1, Body = "نظر من این است که", HasChildren = true, ParentId = null
            };
            list.Add(comment1);
 
            var comment12 = new BlogComment
            {
                Id = 2, Body = "پاسخی به نظر اول", HasChildren = true, ParentId = 1
            };
            list.Add(comment12);
 
            var comment12A = new BlogComment
            {
                Id = 3, Body = "پاسخی دیگری به نظر اول", HasChildren = false, ParentId = 1
            };
            list.Add(comment12A);
 
            var comment121 = new BlogComment
            {
                Id = 4, Body = "پاسخی به پاسخ به نظر اول", HasChildren = false, ParentId = 2
            };
            list.Add(comment121);
 
            var comment2 = new BlogComment
            {
                Id = 5, Body = "نظر 2", HasChildren = true, ParentId = null, imageUrl= "images/search.png"
            };
            list.Add(comment2);
 
            var comment21 = new BlogComment
            {
                Id = 6, Body = "پاسخ به نظر 2", HasChildren = false, ParentId = 5
            };
            list.Add(comment21);
 
            return list;
        }
    }
}
در اینجا نحوه‌ی مقدار دهی ParentId و HasChildren را جهت تو در تو سازی اطلاعات، مشاهده می‌کنید.
در این لیست دو رکورد، دارای ParentId مساوی null هستند. از این null بودن‌ها جهت کوئری گرفتن و نمایش ریشه‌های TreeView در ادامه استفاده خواهیم کرد.


بازگشت نظرات با فرمت JSON به سمت کلاینت

در ادامه یک کنترلر ASP.NET MVC را مشاهده می‌کنید که توسط اکشن متد GetBlogComments، رکوردهای مورد نظر را با فرمت JSON به سمت کلاینت ارسال می‌کند:
using System.Linq;
using System.Web.Mvc;
using KendoUI11.Models;
 
namespace KendoUI11.Controllers
{
 
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View(); // shows the page.
        }
 
        [HttpGet]
        public ActionResult GetBlogComments(int? id)
        {
            if (id == null)
            {
                //دریافت ریشه‌ها
                return Json(
                    BlogCommentsDataSource.LatestComments
                        .Where(x => x.ParentId == null) // ریشه‌ها
                        .ToList(),
                    JsonRequestBehavior.AllowGet);
            }
            else
            {
                //دریافت فرزندهای یک ریشه
                return Json(
                    BlogCommentsDataSource.LatestComments
                              .Where(x => x.ParentId == id)
                              .ToList(),
                              JsonRequestBehavior.AllowGet);
            }
        }
    }
}
اگر از سمت Kendo UI، مقدار id تنظیم نشود، به معنای درخواست نمایش ریشه‌ها است. در این حالت رکوردها را بر اساس مواردی که دارای ParentId مساوی null هستند، فیلتر خواهیم کرد.
اگر مقدار id به سمت سرور ارسال شود، یعنی کاربر گره و نودی را گشوده‌است. بر این اساس، تمامی فرزندان این گره را یافته و بازگشت می‌دهیم.


کدهای سمت کاربر نمایش Kendo UI Tree View

برای کار با Kendo UI TreeView نیاز است از منبع داده خاصی به نام HierarchicalDataSource به نحو ذیل استفاده کنیم. در قسمت transport آن مشخص می‌کنیم که اطلاعات باید از چه آدرسی خوانده شوند که در اینجا به آدرس اکشن متد  GetBlogComments اشاره می‌کند.
همچنین نیاز است مشخص کنیم کدامیک از خواص مدل بازگردانده شده، همان hasChildren است که در مثال فوق دقیقا به همین نام نیز تنظیم شده‌است.
<!--نحوه‌ی راست به چپ سازی -->
<div class="k-rtl k-header demo-section">
    <div id="my-treeview"></div>
</div>
 
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            var dataSource = new kendo.data.HierarchicalDataSource({
                transport: {
                    read: {
                        url: "@Url.Action("GetBlogComments", "Home")",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                schema: {
                    model: {
                        id: "Id",
                        hasChildren: "HasChildren"
                    }
                }
            });
 
            $("#my-treeview").kendoTreeView({
                //استفاده از قالب در صورت نیاز
                template: kendo.template($("#treeview-template").html()),
                checkboxes: {
                    checkChildren: false
                },
                dataSource: dataSource,
                dataTextField: "Body",
                //رخدادها
                select: function (e) { console.log("Selecting: " + this.text(e.node)); },
                check: function (e) { console.log("Checkbox changed :: " + this.text(e.node)); },
                change: function (e) { console.log("Selection changed"); },
                collapse: function (e) { console.log("Collapsing " + this.text(e.node)); },
                expand: function (e) { console.log("Expanding " + this.text(e.node)); }
            });
        });
    </script>
 
    <script id="treeview-template" type="text/kendo-ui-template">
        <strong> #: item.Body # </strong>
    </script>
 
    <style scoped>
        .demo-section {
            width: 100%;
            height: 300px;
        }
    </style>
}
 پس از تنظیم remote data source، اکنون نوبت به تعریف و تنظیم kendoTreeView است.
- در ابتدا به ازای هر ردیف این TreeView، از یک قالب استفاده شده‌است. تعریف این مورد اختیاری است. اگر نیاز به سفارشی سازی نحوه‌ی نمایش هر آیتم را داشتید، می‌توان از قالب‌ها استفاده کرد.
- قسمت checkboxes مشخص می‌کند که آیا نیاز است در کنار هر آیتم یک checkbox نیز نمایش داده شود یا خیر.
- dataSource را به HierarchicalDataSource تنظیم کرده‌ایم.
- dataTextField مشخص می‌کند که کدام فیلد دربرگیرنده‌ی متن هر آیتم TreeView است.
- تعدادی رخداد منتسب به TreeView نیز تنظیم شده‌اند که خروجی آن‌ها را در console تصویر ابتدای بحث مشاهده می‌کنید.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب
نصب خودکار اطلاعات فایل‌های PFX در سیستم

در مورد نحوه رمزنگاری فایل‌های PDF به کمک روش Public-key encryption توسط iTextSharp مطلبی را پیشتر در این سایت مطالعه کرده‌اید.
این روش یک مشکل مهم دارد: «ارائه فایل PFX و همچنین کلمه عبور آن به کاربر نهایی»
خوب، این یعنی اینکه شما به راحتی می‌تونید اطلاعات را رمزگشایی کنید؛ چون همه چیز سخاوتمندانه در اختیارتان است. بنابراین ضرورت رمزنگاری آن در ابتدای امر زیر سؤال می‌رود.
اکنون این سؤال مطرح می‌شود که آیا می‌توان این اطلاعات را تا حد قابل قبولی مخفی کرد؟ مثلا یک برنامه را در اختیار کاربر قرار داد که اطلاعات فایل PFX را به همراه کلمه عبور آن در سیستم نصب کند.
پاسخ:
دات نت به صورت توکار از این نوع فایل‌های مجوز پشتیبانی می‌کند:

using System.Security.Cryptography.X509Certificates;

namespace InstallPfx
{
class Program
{
private static void InstallCertificate(string cerFileName, string password)
{
var certificate = new X509Certificate2(cerFileName, password, X509KeyStorageFlags.PersistKeySet);
var store = new X509Store(StoreName.My);
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
store.Close();
}

static void Main(string[] args)
{
InstallCertificate(@"D:\forTest\file.pfx", "123456");
}
}
}

پس از اجرای کد فوق، امکان مشاهده فایل‌های PDF رمزنگاری شده به کمک اطلاعات فایل file.pfx، میسر می‌شود.
برای مشاهده این مجوز نصب شده هم می‌توان در دیالوگ Run ویندوز نوشت : certmgr.msc تا کنسول مدیریتی مجوز‌های ویندوز ظاهر شود. سپس به قسمت personal certificates باید مراجعه کرد.