مطالب
دریافت اطلاعات بیشتر از Social Provider ها در VS 2013
هنگامی که یک پروژه جدید ASP.NET را در VS 2013 می‌سازید و متد احراز هویت آن را Individual User Accounts انتخاب می‌کنید، قالب پروژه، امکانات لازم را برای استفاده از تامین کنندگان ثالث، فراهم می‌کند، مثلا مایکروسافت، گوگل، توییتر و فیسبوک. هنگامی که توسط یکی از این تامین کننده‌ها کاربری را احراز هویت کردید، می‌توانید اطلاعات بیشتری درخواست کنید. مثلا عکس پروفایل کاربر یا لیست دوستان او. سپس اگر کاربر به اپلیکیشن شما سطح دسترسی کافی داده باشد می‌توانید این اطلاعات را دریافت کنید و تجربه کاربری قوی‌تر و بهتری ارائه کنید.

در این پست خواهید دید که چطور می‌شود از تامین کننده Facebook اطلاعات بیشتری درخواست کرد. پیش فرض این پست بر این است که شما با احراز هویت فیسبوک و سیستم کلی تامین کننده‌ها آشنایی دارید. برای اطلاعات بیشتر درباره راه اندازی احراز هویت فیسبوک به  این لینک  مراجعه کنید.

برای دریافت اطلاعات بیشتر از فیسبوک مراحل زیر را دنبال کنید.

  • یک اپلیکیشن جدید ASP.NET MVC با تنظیمات Individual User Accounts بسازید.
  • احراز هویت فیسبوک را توسط کلید هایی که از Facebook دریافت کرده اید فعال کنید. برای اطلاعات بیشتر در این باره می‌توانید به این لینک مراجعه کنید.
  • برای درخواست اطلاعات بیشتر از فیسبوک، فایل Startup.Auth.cs را مطابق لیست زیر ویرایش کنید.
 List<string> scope = newList<string>() { "email", "user_about_me", "user_hometown", "friends_about_me", "friends_photos" };
 var x = newFacebookAuthenticationOptions();
 x.Scope.Add("email");
 x.Scope.Add("friends_about_me");
 x.Scope.Add("friends_photos");
 x.AppId = "636919159681109";
 x.AppSecret = "f3c16511fe95e854cf5885c10f83f26f";
 x.Provider = newFacebookAuthenticationProvider()
{
    OnAuthenticated = async context =>
    {
         //Get the access token from FB and store it in the database and
        //use FacebookC# SDK to get more information about the user
        context.Identity.AddClaim(
        new System.Security.Claims.Claim("FacebookAccessToken",
                                             context.AccessToken));
    }
};
 x.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie;
 app.UseFacebookAuthentication(x);

در خط 1 مشخص می‌کنیم که چه scope هایی از داده را می‌خواهیم درخواست کنیم.
از خط 10 تا 17 رویداد OnAuthenticated را مدیریت می‌کنیم که از طرف Facebook OWIN authentication اجرا می‌شود. این متد هر بار که کاربری با فیسبوک خودش را احراز هویت می‌کند فراخوانی می‌شود. پس از آنکه کاربر احراز هویت شد و به اپلیکیشن سطح دسترسی لازم را اعطا کرد، تمام داده‌ها در FacebookContext ذخیره می‌شوند. 
خط 14 شناسه FacebookAccessToken را ذخیره می‌کند. ما این آبجکت را از فیسبوک دریافت کرده و از آن برای دریافت لیست دوستان کاربر استفاده می‌کنیم.
نکته: در این مثال تمام داده‌ها بصورت Claims ذخیره می‌شوند، اما اگر بخواهید می‌توانید از ASP.NET Identity برای ذخیره آنها در دیتابیس استفاده کنید.
در قدم بعدی لیست دوستان کاربر را از فیسبوک درخواست می‌کنیم. ابتدا فایل Views/Shared/_LoginPartial.cshtml را باز کنید و لینک زیر را به آن بیافزایید.
 <li>
      @Html.ActionLink("FacebookInfo", "FacebookInfo","Account")
</li>

 هنگامی که کاربری وارد سایت می‌شود و این لینک را کلیک می‌کند، ما لیست دوستان او را از فیسبوک درخواست می‌کنیم و بهمراه عکس‌های پروفایل شان آنها را لیست می‌کنیم.
تمام Claim‌ها را از UserIdentity بگیرید و آنها را در دیتابیس ذخیره کنید. در این قطعه کد ما تمام Claim هایی که توسط OWIN دریافت کرده ایم را می‌خوانیم، و شناسه FacebookAccessToken را در دیتابیس عضویت ASP.NET Identity ذخیره می‌کنیم.
//
        // GET: /Account/LinkLoginCallback
        publicasyncTask<ActionResult> LinkLoginCallback()
        {
            var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId());
            if (loginInfo == null)
            {
                return RedirectToAction("Manage", new { Message = ManageMessageId.Error });
            }
            var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login);
            if (result.Succeeded)
            {
                var currentUser = await UserManager.FindByIdAsync(User.Identity.GetUserId());
                //Add the Facebook Claim
                await StoreFacebookAuthToken(currentUser);
                return RedirectToAction("Manage");
            }
            return RedirectToAction("Manage", new { Message = ManageMessageId.Error });
        }
خط 14-15 شناسه FacebookAccessToken را در دیتابیس ذخیره می‌کند.
StoreFacebookAuthToken تمام اختیارات (claim)‌های کاربر را از UserIdentity می‌گیرد و Access Token را در قالب یک User Claim در دیتابیس ذخیره می‌کند. اکشن LinkLoginCallback هنگامی فراخوانی می‌شود که کاربر وارد سایت شده و یک تامین کننده دیگر را می‌خواهد تنظیم کند.
اکشن ExternalLoginConfirmation هنگام اولین ورود شما توسط تامین کنندگان اجتماعی مانند فیسبوک فراخوانی می‌شود.
در خط 26 پس از آنکه کاربر ایجاد شد ما یک FacebookAccessToken را بعنوان یک Claim برای کاربر ذخیره می‌کنیم.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl)
        {
            if (User.Identity.IsAuthenticated)
            {
                return RedirectToAction("Manage");
            }
 
            if (ModelState.IsValid)
            {
                // Get the information about the user from the external login provider
                var info = await AuthenticationManager.GetExternalLoginInfoAsync();
                if (info == null)
                {
                    return View("ExternalLoginFailure");
                }
                var user = newApplicationUser() { UserName = model.Email };
                var result = await UserManager.CreateAsync(user);
                if (result.Succeeded)
                {
                    result = await UserManager.AddLoginAsync(user.Id, info.Login);
                    if (result.Succeeded)
                    {
                        await StoreFacebookAuthToken(user);
                        await SignInAsync(user, isPersistent: false);
                        return RedirectToLocal(returnUrl);
                    }
                }
                AddErrors(result);
            }
 
            ViewBag.ReturnUrl = returnUrl;
            return View(model);
        }


اکشن ExternalLoginCallback هنگامی فراخوانی می‌شود که شما برای اولین بار یک کاربر را به یک تامین کننده اجتماعی اختصاص می‌دهید. در خط 17 شناسه دسترسی فیسبوک را بصورت یک claim برای کاربر ذخیره می‌کنیم.
//
        // GET: /Account/ExternalLoginCallback
        [AllowAnonymous]
        publicasyncTask<ActionResult> ExternalLoginCallback(string returnUrl)
        {
            var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
            if (loginInfo == null)
            {
                return RedirectToAction("Login");
            }
 
            // Sign in the user with this external login provider if the user already has a login
            var user = await UserManager.FindAsync(loginInfo.Login);
            if (user != null)
            {
                //Save the FacebookToken in the database if not already there
                await StoreFacebookAuthToken(user);
                await SignInAsync(user, isPersistent: false);
                return RedirectToLocal(returnUrl);
            }
            else
            {
                // If the user does not have an account, then prompt the user to create an account
                ViewBag.ReturnUrl = returnUrl;
                ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
                return View("ExternalLoginConfirmation", newExternalLoginConfirmationViewModel { Email = loginInfo.Email });
            }
        }
در آخر شناسه FacebookAccessToken را در دیتابیس ASP.NET Identity ذخیره کنید.
privateasyncTask StoreFacebookAuthToken(ApplicationUser user)
        {
            var claimsIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
            if (claimsIdentity != null)
            {
                // Retrieve the existing claims for the user and add the FacebookAccessTokenClaim
                var currentClaims = await UserManager.GetClaimsAsync(user.Id);
                var facebookAccessToken = claimsIdentity.FindAll("FacebookAccessToken").First();
                if (currentClaims.Count() <=0 )
                {
                    await UserManager.AddClaimAsync(user.Id, facebookAccessToken);
                }

پکیج Facebook C#SDK را نصب کنید. http://nuget.org/packages/Facebook
فایل AccountViewModel.cs را باز کنید و کد زیر را اضافه کنید.
    public class FacebookViewModel
     {
         [Required]
         [Display(Name = "Friend's name")]
         public string Name { get; set; }
  
        public string ImageURL { get; set; }
    }

کد زیر را به کنترلر Account اضافه کنید تا عکس‌های دوستان تان را دریافت کنید.
//GET: Account/FacebookInfo
[Authorize]
publicasyncTask<ActionResult> FacebookInfo()
{
    var claimsforUser = await UserManager.GetClaimsAsync(User.Identity.GetUserId());
    var access_token = claimsforUser.FirstOrDefault(x => x.Type == "FacebookAccessToken").Value;
    var fb = newFacebookClient(access_token);
    dynamic myInfo = fb.Get("/me/friends");
    var friendsList = newList<FacebookViewModel>();
    foreach (dynamic friend in myInfo.data)
    {
        friendsList.Add(newFacebookViewModel()
           {
               Name = friend.name,
               ImageURL = @"https://graph.facebook.com/" + friend.id + "/picture?type=large"
           });
    }
 
    return View(friendsList);
}

در پوشه Views/Account یک نمای جدید با نام FacebookInfo.cshtml بسازید و کد Markup آن را مطابق لیست زیر تغییر دهید.
@model IList<WebApplication96.Models.FacebookViewModel>
 @if (Model.Count > 0)
 {
     <h3>List of friends</h3>
     <div class="row">
             @foreach (var friend in Model)
             {
               <div class="col-md-3">
                <a href="#" class="thumbnail">
                  <img src=@friend.ImageURL alt=@friend.Name />
                 </a>
               </div>
              }
     </div>
 }
در این مرحله، شما می‌توانید لیست دوستان خود را بهمراه عکس‌های پروفایل شان دریافت کنید.
پروژه را اجرا کنید و توسط Facebook وارد سایت شوید. باید به سایت فیسبوک هدایت شوید تا احراز هویت کنید و دسترسی لازم را به اپلیکیشن اعطا کنید. پس از آن مجددا به سایت خودتان باید هدایت شوید.
حال هنگامی که روی لینک FacebookInfo کلیک می‌کنید باید صفحه ای مشابه تصویر زیر ببینید.

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

نظرات مطالب
اعتبارسنجی درخواست های http$ با استفاده از یک Interceptor

با سلام و احترام

ممنون بابت مقاله خوبتون

ولی این شرط

filterContext.HttpContext.Request.IsAjaxRequest

معنی

یعنی اعتبارسنجی نشده است

را نمی‌دهد.

شما در متد HandleUnauthorizedRequest هستید و بالطبع اعتبار درخواست رد شده است، کاری که می‌خواهید انجام دهید، اعتبارسنجی نیست، بلکه اصلاح Response درخواست‌های Ajax ای است که Status Code آنها ۲۰۰ است که نباید باشد.

این نوع دقت داشتن و شفافیت لازمه ایجاد یک کد صحیح است، چون الآن و در مرحله ای بالاتر، می‌توانیم بگویم که هر Response ای که وارد متد HandleUnauthorizedRequest شده است، اگر Status Code 401 ندارد، Status Code آن برابر ۴۰۱ قرار داده شود. فارغ از Ajax بودن یا نبودن.

و باز در مرحله ای بالاتر اگر در پروژه از Owin استفاده شده باشد، یک Owin Middleware و اگر نه یک ASP.NET Module جای مناسب‌تری برای نوشتن این چنین کدی است.

از این که IsAjaxRequest همیشه درست کار نمی‌کند و فقط وقتی درخواست Ajax ای بگوید که Ajax ای است، کار خواهد کرد نیز بگذریم.

موفق و پایدار باشید.

نظرات مطالب
تبدیل HTML به PDF با استفاده از کتابخانه‌ی iTextSharp
- در مورد فارسی نویسی در iTextSharp یک دیباگ مرحله به مرحله قبلا در سایت مطرح شده. اگر خروجی یونیکد نگرفتید یعنی قلم صحیحی در حال استفاده نیست. کدهایی که قبلا ارسال کرده بودم به این نحو است:
// روش صحیح تعریف فونت  
var systemRoot = Environment.GetEnvironmentVariable("SystemRoot");
FontFactory.Register(Path.Combine(systemRoot, "fonts\\tahoma.ttf"));
در کدهای شما به این نحو:
var systemRoot = Environment.GetEnvironmentVariable("SystemRoot");
FontFactory.Register(Path.Combine(systemRoot, "c:\\windows\\fonts\\tahoma.ttf"));
با توجه به استفاده از Path.Combine، مسیری را که معرفی کرده‌اید می‌شود چیزی مانند c:\\windows\\c:\\windows\\fonts\\tahoma.ttf . به همین جهت این فونت یافت نشده و ثبت نمی‌شود (چون دوبار system root در آن وجود دارد).
- بله؛ قدرت پردازش CSS در XML Worker آن خیلی بهتر است از HTML Worker.
- در مورد میزان چرخش جدول، RunDirection = PdfWriter.RUN_DIRECTION_RTL را با حالت LTR هم تست کنید (PdfWriter.RUN_DIRECTION_LTR ).
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 21 - بررسی تغییرات Bundling و Minification
زیرساخت یکی کردن و فشرده سازی اسکریپت‌ها و فایل‌های CSS نگارش پیشین ASP.NET MVC، به طور کامل از ASP.NET Core حذف شده‌است. در ابتدا (تا نگارش RC2)، روش استفاده‌ی از Gulp را توصیه کردند و در زمان ارائه‌ی نگارش RTM، توصیه‌ی رسمی آن‌ها به Bundler Minifier تغییر کرد (و دیگر Gulp را توصیه نمی‌کنند).


یکی کردن و فشرده سازی فایل‌های استاتیک در ASP.NET Core

هدف از یکی کردن و فشرده سازی فایل‌های استاتیک مانند اسکریپت‌ها و فایل‌های CSS، بهبود کارآیی برنامه با کاهش حجم نهایی ارائه‌ی آن و همچنین کاهش تعداد رفت و برگشت‌های به سرور برای دریافت فایل‌های متعدد مرتبط به آن است. در عملیات Bundling، چندین فایل، به یک تک فایل تبدیل می‌شوند تا اتصالات مرورگر به وب سرور، جهت دریافت آن‌ها به نحو چشمگیری کاهش پیدا کند و در عملیات Minification، مراحل متعددی بر روی کدهای نوشته شده صورت می‌گیرد تا حجم نهایی آن‌ها کاهش پیدا کنند. مایکروسافت در ASP.NET Core RTM، ابزاری را به نام BundlerMinifier.Core جهت برآورده کردن این اهداف ارائه کرده‌است. بنابراین اولین قدم، نصب وابستگی‌های آن است.
برای اینکار یک سطر ذیل  را به فایل project.json اضافه کنید. این بسته باید به قسمت tools اضافه شود تا قابلیت فراخوانی از طریق خط فرمان را نیز پیدا کند:
"tools": {
    "BundlerMinifier.Core": "2.1.258"
},
در غیر اینصورت (ذکر آن در قسمت dependencies) خطاهای ذیل را دریافت خواهید کرد:
No executable found matching command "dotnet-bundle"
Version for package `BundlerMinifier.Core` could not be resolved.


اسکریپت نویسی برای کار با BundlerMinifier.Core

روش‌های زیادی برای کار با ابزار BundlerMinifier.Core وجود دارند؛ منجمله انتخاب فایل‌ها در solution explorer و سپس کلیک راست بر روی فایل‌های انتخاب شده و انتخاب گزینه‌ی bundler & minifier برای یکی کردن و فشرده سازی خودکار این فایل‌ها. برای این منظور افزونه‌ی Bundler & Minifier را نیاز است نصب کنید.
اما روشی که قابلیت خودکارسازی را دارد، استفاده از فایل ویژه‌ی bundleconfig.json این ابزار است. برای این منظور فایل جدید bundleconfig.json را به ریشه‌ی پروژه اضافه کرده و سپس محتوای ذیل را به آن اضافه کنید:
[
    {
        "outputFileName": "wwwroot/css/site.min.css",
        "inputFiles": [
            "wwwroot/css/site.css"
        ]
    },
    {
        "outputFileName": "wwwroot/js/site.min.js",
        "inputFiles": [
            "bower_components/jquery/dist/jquery.min.js",
            "bower_components/jquery-validation/dist/jquery.validate.min.js",
            "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
        ],
        "minify": {
            "enabled": true,
            "renameLocals": true
        },
        "sourceMap": false
    }
]
فرمت این فایل بسیار خوانا است. برای مثال در یک مدخل آن، در ذیل خاصیت inputFiles، لیست فایل‌های css ذکر می‌شوند و سپس در outputFileName، محل نهایی فایل تولیدی باید ذکر شود. این محل نیز باید از پیش وجود داشته باشد. یعنی باید پوشه‌های js و css را در پوشه‌ی عمومی wwwroot پیشتر ایجاد کرده باشید.
با ذخیره سازی این فایل، کار یکی سازی و فشرده کردن مداخل آن به صورت خودکار صورت خواهد گرفت.


خودکار سازی فرآیند یکی کردن و فشرده سازی فایل‌های استاتیک

برای خودکار سازی این فرآیند، می‌توان به صورت زیر عمل کرد. فایل project.json را گشوده و قسمت scripts آن‌را به نحو ذیل تغییر دهید:
"scripts": {
    "precompile": [
        "dotnet bundle"
    ],
    "prepublish": [
        "bower install"
    ],
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
}
روش دستی کار با ابزار BundlerMinifier، مراجعه به خط فرمان و صدور دستور dotnet bundle است (ابتدا از طریق خط فرمان به ریشه‌ی پروژه وارد شده و سپس این دستور را صادر کنید). برای خودکار سازی آن می‌توان این دستور را در قسمت scripts فایل project.json نیز ذکر کرد تا پیش از کامپایل برنامه، کار یکی کردن، فشرده سازی و همچنین کپی فایل نهایی به پوشه‌ی wwwroot برنامه به صورت خودکار انجام شود.

یک نکته: به منوی Build گزینه‌ی Update all bundles نیز با نصب افزونه‌ی Bundler & Minifier اضافه می‌شود. همچنین اگر از منوی Tools گزینه‌ی Task runner explorer را انتخاب کنید، فایل bundleconfig.json توسط آن شناسایی شده و گزینه‌ی update all files را نیز در اینجا مشاهده خواهید کرد.



ساده سازی تعاریف فایل Layout برنامه

در یک چنین حالتی دیگر نباید در فایل layout شما، ارجاعات مستقیمی به پوشه‌ی مثلا bower_components وجود داشته باشند و یا در کلاس آغازین برنامه، نیازی نیست تا این پوشه را عمومی کنید. لیست مداخلی را که نیاز دارید، به ترتیب از پوشه‌های مختلفی تهیه و در فایل bundleconfig.json ذکر کنید تا یکی شده و خروجی js/site.min.js را تشکیل دهند. این مورد تنها مدخلی است که نیاز است در فایل layout برنامه ذکر شود (بجای چندین و چند مدخل مورد نیاز):
 <script src="~/js/site.min.js" asp-append-version="true" type="text/javascript"></script>
در مورد ویژگی asp-append-version نیز پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» بحث شد و به آن مکانیزم cache busting می‌گویند. این ویژگی سبب خواهد شد تا یک کوئری استرینگ v=xyz? مانند، به انتهای آدرس اسکریپت یا فایل css یا هر فایل استاتیک دیگری اضافه شود. با تغییر محتوای این فایل، قسمت xyz به صورت خودکار تغییر خواهد کرد و به این ترتیب مرورگر همواره آخرین نگارش این فایل را دریافت می‌کند.
مطالب
نحوه استفاده از TransactionFlow در WCF
شش مرحله برای ایجاد WCFTransactions  در WCF 
 مقدمه و هدف:

هدف از مطلب  فوق اجرا نمودن عملیات Insert، Update و غیرو... بوسیله چندین Connection  در یک Transaction  در زمان اجرای سرویسهای WCF  میباشد. برای پیاده سازی و شرح Transaction ، سه پروژه ایجاد می‌نماییم. دو پروژه WCF  سرویس و یک پروژهClient ، هر سه پروژه را در یک Solution  به نام WCFTransaction  اضافه می‌نماییم. در هر دو پروژه WCF  بطور جداگانه Connection  رویDatabase  ایجاد می‌نماییم. سپس سعی می‌کنیم بوسیله Transaction  عملیات Insert  هر دو Service  را کنترل نماییم. بطوریکه اگر یکی از Service ‌ها در زمان عملیات Insert  دچار مشکل شود. دیگری نیز Commit  نگردد. به عبارتی در قدیم نمی‌توانستیم بیش از یک Connection  در یک Transaction  ایجاد نماییم. اما بوسیله Transactionscope ، انجام عملیات Insert، Update و غیرو...  بوسیله چندین Connection   به یکDatabase  بطور همزمان در یک Transaction  فراهم شده است. برای نمایش دادن عملیات Rollback  نیز،به عمد خطایی ایجاد می‌کنیم،تا نحوه Rollback  شدن در Transaction  را مشاهده نماییم.

سعی شده است پیاده سازی و استفاده از  Transaction در شش مرحله انجام شود.

مرحله اول: ایجاد دو پروژه WCFService و یک پروژه Client جهت فراخوانی (Call) کردن سرویسها

در این مرحله همانطور که از قیل نیز توضیح داده شده است، دو پروژه WCF  به نامهای WCFService1  و WCFService2  ایجاد شده است و یک پروژه Client  به نام WCFTransactions  نیز ایجاد می‌کنیم.

مرحله دوم : افزودن   Attribute ی به نام   TransactionFlow به  Interface سرویسها.

در این مرحله در Interface  هریک از سرویس‌ها متد جدیدی به نام UpdateData  اضافه می‌نماییم. که عملیات Insert into  درون Database  را انجام می‌دهد. حال بالای متد UpdateData   از صفت TransactionFlow  استفاده می‌نماییم. تا قابلیت Transaction  برای متد فوق فعال گردد و متد فوق اجازه می‌یابد از Transaction  استفاده نماید.

<ServiceContract()> _
Public Interface IService1

    <OperationContract()> _
    Function GetData(ByVal value As Integer) As String

    <OperationContract()> _
    Function GetDataUsingDataContract(ByVal composite As CompositeType) As CompositeType

    <OperationContract()> _
    <TransactionFlow(TransactionFlowOption.Allowed)> _
     Sub UpdateData()

End Interface

مرحله سوم:

در این مرحله متد UpdateData  را پیاده سازی می‌نماییم. بطوریکه یک Insert Into  ساده در Database  انجام می‌دهیم.و بالای متد فوق نیز کد زیر را می‌افزاییم.

 <OperationBehavior(TransactionScopeRequired:=True)> 

کد متد UpdateData   

   <OperationBehavior(TransactionScopeRequired:=True)> _
    Public Sub UpdateData() Implements IService1.UpdateData
        Dim objConnection As SqlConnection = New SqlConnection(strConnection)
        objConnection.Open()
        Dim objCommand As SqlCommand = New SqlCommand("insert into T(ID,Age) values(10,10)", objConnection)
        objCommand.ExecuteNonQuery()
        objConnection.Close()
End Sub

مرحله دوم و سوم را برای Service دوم نیز تکرار می‌نماییم.

مرحله چهارم:

در این مرحله  TransactionFlow  را در Web.Config  دو سرویس فعال می‌نماییم. تا قابلیت استفاده از  TransactionFlow   برای سرویسها نیز فعال گردد. نحوه فعال نمودن بصورت زیر میباشد:

برای  WCFService1خواهیم داشت:

<bindings>
                <wsHttpBinding>
                                <binding name="TransactionalBind" transactionFlow="true"/>
                </wsHttpBinding>
</bindings>
و در ادامه داریم:
<endpoint address="" binding="wsHttpBinding" 
bindingConfiguration="TransactionalBind" 
contract="WcfService1.IService1">

برای  WCFService2نیز خواهیم داشت:

<bindings>
                <wsHttpBinding>
                                <binding name="TransactionalBind" transactionFlow="true"/>
                </wsHttpBinding>
</bindings>

و در ادامه داریم:

<endpoint address="" binding="wsHttpBinding" 
bindingConfiguration="TransactionalBind" 
contract="WcfService2.IService1">

مرحله پنجم:

در این مرحله دو سرویس فوق را به پروژه  WCFTransactions  اضافه نموده و قطعه کد زیر را درون فرم Load  می‌نویسیم.

Private Sub frmmain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        Using ts As New TransactionScope(TransactionScopeOption.Required)
            Try
                Dim obj As ServiceReference1.Service1Client = New ServiceReference1.Service1Client()
                obj.UpdateData()
                Dim obj1 As ServiceReference2.Service1Client = New ServiceReference2.Service1Client()
                obj1.UpdateData()
                ts.Complete()

            Catch ex As Exception
                ts.Dispose()
            End Try

        End Using
End Sub


پس از اجرای برنامه دو رکورد در جدول درج خواهد شد.

مرحله ششم:

حال برای RollBack   کردن کل عملیات و مشاهده آنها کافیست در یکی از متدهای UpdateData  یک  Throw Exception  ایجاد نماییم.

سعی می‌کنیم با کمی تغییر در متد UpdateData در WCFService2 ، خطایی ایجاد شود، تا نحوه RollBack را مشاهده نماییم.

Public Sub UpdateData() Implements IService1.UpdateData
        Throw New Exception()
        Dim objConnection As SqlConnection = New SqlConnection(strConnection)
        objConnection.Open()
        Dim objCommand As SqlCommand = New SqlCommand("insert into T(ID,Age) values(101,101)", objConnection)
        objCommand.ExecuteNonQuery()
        objConnection.Close()
End Sub

فقط کد زیر به متد UpdateData اضافه شده است:

Throw New Exception()

و در رویداد Load  فرم نیز پیاده سازی آن بشکل زیر خواهد بود:


Using ts As New TransactionScope(TransactionScopeOption.Required)
            Try
                Dim obj As ServiceReference1.Service1Client = New ServiceReference1.Service1Client()
                obj.UpdateData()
                Throw New Exception("There was Error")
                Dim obj1 As ServiceReference2.Service1Client = New ServiceReference2.Service1Client()
                obj1.UpdateData()
                ts.Complete()

            Catch ex As Exception
                ts.Dispose()
            End Try
 End Using 

وقتی برنامه را اجرا نمایید، مشاهده می‌کنید که هیچ رکوردی دورن دیتابیس درج نشده است.

بسبار مهم: برای اینکه بتوانید بصورت Distibuted  عملیات Transaction  را انجام دهید می‌بایست تنظیماتی را روی سرور که دیتایس و سرویسها و کامپیوتر کلاینت انجام دهید که بصورت زیر می‌باشد:

نحوه تنظیم:

1- سرویسDistribute Transaction Coordinator  را روی هر دو Server‌های WCFService ، Database و کامپیوتر کلاینت، Start می‌نماییم.    

البته در شرایطی که Service‌های WCF و برنامه Client و Database روی یک سیستم باشد، تنظیمات فوق فقط روی همان سیستم انجام می‌شود.

برای دسترسی به قسمت Service ‌های Windows  ابتدا Administrative Tools  و سپس Service   را باز نمایید و روی Start کلیک کنید.

2- در ادامه روی MY Computer کلیک راست نموده و تب MSDTC را انتخاب نمایید:

در ادامه روی Security Configuration  کلیک نمایید. تا فرم زیر نمایش داده شود.


مطمئن شوید که آیتمهای زیر انتخاب شده باشند:

· Network DTC Access

· Allow Remote Clients

· Allow Inbound

· Allow Outbound

· Enable Transaction Internet Protocol(TIP) Transactions 

سپس با OK کردن Service،سرویس بطور خودکار Restart می‌شود.
در ضمن اگر از SQL Server 2000 استفاده می‌نمایید. لازم است تنظیم زیر را انجام دهید.
روی SQL Server Service Manager کلیک نموده و کامبوی Service را Dropdown نمایید و Distribute Transaction Coordinator  را انتخاب کنید. اما برای ورژن‌های بالاتر از SQL Server 2000 نیاز به انتخاب Distribute Transaction Coordinator  نمی‌باشد.
امیدوارم مطلب فوق مفید واقع شود، چنانچه کم و کاستی مشاهده نمودید، اینجانب را از نظرات خود بهره مند سازید.
منبع:
مسیرراه‌ها
ASP.NET MVC
              مطالب
              استفاده از پلاگین DataTables کتابخانه jQuery در برنامه‌های ASP.NET Core
              datatable js، کتابخانه‌ای جهت ساخت جداول است و نسبت به رقیب اصلی خودش یعنی kendo telerik، از سادگی بیشتری برخوردار هست و امکانات خوبی هم دارد.

              اگر برای جداول صفحات خود، از کتابخانه‌ی جی‌کوئری datatable استفاده میکنید، بعد از مدتی که تعداد رکورد‌ها زیاد میشوند، شاهد کند شدن صفحه خود خواهید شد. برای رفع این مشکل نیاز به پیاده سازی pagination دارید که به صورت خیلی ساده‌ای قابل پیاده سازی هست و شما تغییر کمی را در سمت سرور اعمال میکنید و سایر موارد توسط خود کتابخانه انجام میشود.

              در ابتدا به بررسی کد‌ها و تغییرات سمت فرانت‌اند و صفحه‌ی cshtml می‌پردازیم:
              1- تابع Ajax ای که وظیفه‌ی دریافت اطلاعات را دارد، به کل پاک کنید. چون Ajax به صورت یک آبجکت، به درون خود دیتاتیبل منتقل خواهد شد.
              2- در صفحه خود، کد زیر را قرار دهید (جهت جلوگیری از 400 bad request) که این کار فقط برای هندلر‌های razor page و یا controller نیاز است و اگر از API استفاده میکنید، مسلما نیازی به این مدل تنظیمات نیست.
              @Html.AntiForgeryToken()
              3- سپس کد زیر را به startup خود اضافه کنید (در قسمتی که دارید اینترفیس‌ها را ثبت میکنید):
              //Post in Ajax
              services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
              4- حالا نوبت کانفیگ‌های دیتاتیبل هست:
              function initDataTables() {
                      table.destroy();
                      table = $("#tblJs").DataTable({
                        processing: true,
                        serverSide: true,
                        filter: true,
                        ajax: {
                          url: '@Url.Page("yourPage","yourHandler")',
                          beforeSend: function (xhr) {
                            xhr.setRequestHeader("XSRF-TOKEN",
                              $('input:hidden[name="__RequestVerificationToken"]').val());
                          },
                          type: "POST",
                          datatype: "json"
                        },
                        language: {
                          url: "/Persian.json"
                        },
                        responsive: true,
                        select: true,
              
                        columns: scheme,
                        select: true,
                      });
                    }
              5-کد بالا، کل تابعی را نشان میدهد که وظیفه‌ی ساخت دیتاتیبل را دارد؛ ولی شما تنها نیاز دارید قسمت زیر را اضافه کنید:
              processing: true,
                        serverSide: true,
                        filter: true,
                        ajax: {
                          url: '@Url.Page("yourPage","yourHandler  ")',
                          beforeSend: function (xhr) {
                            xhr.setRequestHeader("XSRF-TOKEN",
                              $('input:hidden[name="__RequestVerificationToken"]').val());
                          },
                          type: "POST",
                          datatype: "json"
                        },

              حالا باید کد‌های سمت سرور را بنویسیم. برای این منظور باید ابتدا مقادیری را که دیتاتیبل برای ما ارسال میکند، از ریکوئست دریافت کنیم.
              6- کل دیتایی که دیتا تیبل برای ما میفرستد، به مدل زیر خلاصه میشود:
              public class FiltersFromRequestDataTable
                  {
                      public string length { get; set; }
                      public string start { get; set; }
                      public string sortColumn { get; set; }
                      public string sortColumnDirection { get; set; }
                      public string sortColumnIndex { get; set; }
                      public string draw { get; set; }
                      public string searchValue { get; set; }
                      public int pageSize { get; set; }
                      public int skip { get; set; }
                  }
              نکته‌ی مهم این است که پراپرتی‌ها باید با اسم کوچک به سمت فرانت‌اند ارسال شوند.
              * (من از razor page  استفاده میکنم؛ ولی مسلما در controller هم به همین شکل و راحت‌تر خواهد بود) 
              7- سپس داده‌های ارسال شده‌ی توسط دیتاتیبل، به سمت سرور را با استفاده از متد زیر دریافت میکنیم:
               public static void GetDataFromRequest(this HttpRequest Request, out FiltersFromRequestDataTable filtersFromRequest)
                      {
                          //TODO: Make Strings Safe String
                          filtersFromRequest = new();
              
                          filtersFromRequest.draw = Request.Form["draw"].FirstOrDefault();
                          filtersFromRequest.start = Request.Form["start"].FirstOrDefault();
                          filtersFromRequest.length = Request.Form["length"].FirstOrDefault();
                          filtersFromRequest.sortColumn = Request.Form["columns[" + Request.Form["order[0][column]"].FirstOrDefault() + "][name]"].FirstOrDefault();
                          filtersFromRequest.sortColumnDirection = Request.Form["order[0][dir]"].FirstOrDefault();
                          filtersFromRequest.searchValue = Request.Form["search[value]"].FirstOrDefault();
                          filtersFromRequest.pageSize = filtersFromRequest.length != null ? Convert.ToInt32(filtersFromRequest.length) : 0;
                          filtersFromRequest.skip = filtersFromRequest.start != null ? Convert.ToInt32(filtersFromRequest.start) : 0;
                          filtersFromRequest.sortColumnIndex = Request.Form["order[0][column]"].FirstOrDefault();
              
                          filtersFromRequest.searchValue = filtersFromRequest.searchValue?.ToLower();
                      }
              8- نحوه‌ی استفاده از این متد در handler یا action مورد نظر:
              Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);

              9- با استفاده از متد زیر، مقادیر مورد نیاز دیتاتیبل را به آن ارسال می‌کنیم:
               public static PaginationDataTableResult<T> ToDataTableJs<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest)
                      {
                          int recordsTotal = source.Count();
                          CofingPaging(ref filtersFromRequest, recordsTotal);
                          var result = new PaginationDataTableResult<T>()
                          {
                              draw = filtersFromRequest.draw,
                              recordsFiltered = recordsTotal,
                              recordsTotal = recordsTotal,
                              data = source.OrderByIndex(filtersFromRequest).Skip(filtersFromRequest.skip).Take(filtersFromRequest.pageSize).ToList()
                          };
              
                          return result;
                      }
              
                      private static void CofingPaging(ref FiltersFromRequestDataTable filtersFromRequest, int recordsTotal)
                      {
                          if (filtersFromRequest.pageSize == -1)
                          {
                              filtersFromRequest.pageSize = recordsTotal;
                              filtersFromRequest.skip = 0;
                          }
                      }
              private static IEnumerable<T> OrderByIndex<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest)
                      {
                          var props = typeof(T).GetProperties();
                          string propertyName = "";
                          for (int i = 0; i < props.Length; i++)
                          {
                              if (i.ToString() == filtersFromRequest.sortColumnIndex)
                                  propertyName = props[i].Name;
                          }
              
                          System.Reflection.PropertyInfo propByName = typeof(T).GetProperty(propertyName);
                          if (propByName is not null)
                          {
                              if (filtersFromRequest.sortColumnDirection == "desc")
                                  source = source.OrderByDescending(x => propByName.GetValue(x, null));
                              else
                                  source = source.OrderBy(x => propByName.GetValue(x, null));
                          }
              
                          return source;
                      }
              که باهر دیتاتایپی کار می‌کند و خودش به صورت خودکار، عملیات مرتب‌سازی را انجام می‌دهد (ابدایی خودم)
              10- نحوه استفاده در هندلر یا اکشن:
              var result =  query.ToDataTableJs(filtersFromRequest);
              return new JsonResult(result);
              یک نکته: پراپرتی‌های شما باید باحروف کوچک باشد وگرنه در سمت جاوااسکریپت، خطای undefined را مشاهده خواهید کرد. در این حالت باید پراپرتی‌ها را با حروف کوچک شروع کنید؛ ولی اگر دارید با کتابخانه‌ی newtonSoft  و jsonCovert سریالایز میکنید، میتوانید از این attribute بالای پراپرتی‌ها استفاده کنید: [JsonProperty("name")]
              درکل باید یک iqueryrable را آماده و به متد ToDataTableJs ارسال کنید.

              - برای سرچ هم در column‌ها هم  میتوانید به شکل زیر عمل کنید.
              ابتدا دو متد زیر را به یک کلاس static اضافه کنید:
               public static IEnumerable<TSource> WhereSearchValue<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
                      {
                          return source.Where(predicate);
                      }
                      public static bool ContainsSearchValue(this string source, string toCheck)
                      {
                          return source != null && toCheck != null && source.IndexOf(toCheck, StringComparison.OrdinalIgnoreCase) >= 0;
                      }
              بعد به این شکل از آن‌ها بین ایجاد iqueryrable  و جایی که متد todatatableJs فراخوانی می‌شود، استفاده کنید:
              if (!string.IsNullOrEmpty(filtersFromRequest.searchValue))
              query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();
              برای افزایش کارآیی بهتر است مدل اصلی را به ویوو ارسال نکنید و از همان اول یک IQueryrable از جنس ویوومدل یا dto داشته باشید و این سرچ را هم بر روی همان انجام دهید.

              کد کامل هندلر یا action  (که ترکیب کد‌های بالا هستش):
               public JsonResult OnPostList()
                      {
                          Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);
                          var query = _Repo.GetQueryable().Select(x => new VmAdminList()
                          {
                              title = x.Title,
                          }
                          );
              
                          if (!string.IsNullOrEmpty(filtersFromRequest.searchValue))
                              query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();
              
                          var result =  query.ToDataTableJs(filtersFromRequest);
                          return new JsonResult(result);
                      }

              چند نکته:
              1- ممکن‌است که بخواهید یکسری فیلتر را بجز مقادیر پیش فرض به سمت سرور ارسال کنید. برای اینکار کد زیر را به قسمت Ajax  فرانت‌اند اضافه کنید:
               data: function (d) {
                                  d.parentId = parentID;
                                  d.StartDateTime= StartDateTime;
                              },
               و آن‌ها را به این شکل در سمت سرور دریافت کنید:
              if (!int.TryParse(Request.Form["parentId"].FirstOrDefault(), out int parentId))
                              throw new NullReferenceException();
              2- در کانفیگ‌های Ajax مربوط به دیتاتیبل، دیگر کلید Success را نداریم؛ ولی به این شکل میتوانید این قسمت را شبیه سازی کنید:
               dataSrc: function (json) {
                                  $("#count").val(json.data.length);
                                  var sum = 0;
                                  json.data.forEach(function (item) {
                                      if (!isNullOrEmpty(item.credit))
                                          sum += parseInt(item.credit);
                                  })
                                  $("#sum").val(separate(sum));
              
                                  return json.data;
                              }
              که return آن الزامی هست؛ وگرنه به خطا میخورید.

              تا به اینجا کار کاملا تمام شده؛ ولی من برای داینامیک کردن schema و column‌ها هم کلاسی را نوشته‌ام که فکر میکنم کار را راحت‌تر کند. چون شما برای تعداد ستون‌ها باید یک آبجکت را به شکل زیر تعریف کنید:
              columns: [
                      { data: 'name' },
                      { data: 'position' },
                      { data: 'salary' },
                      { data: 'office' }
                  ]
              در اینجا اگر کلید‌ها و یا ستون‌ها (<th>) جابجا باشند، خطا می‌دهد و توسعه را بعدا سخت می‌کند؛ چون بعد هر بار تغییر، باید دستی این آبجکت‌ها و ستون‌ها را هم جابجا کنید. ولی با استفاده از کد‌های زیر، خودش به صورت داینامیک تولید می‌شود. کدزیر این کار رو انجام می‌دهد:
              public class JsDataTblGeneretaor<T>
                  {
                      public readonly DataTableSchemaResult DataTableSchemaResult = new();
                      public JsDataTblGeneretaor<T> CreateTableSchema()
                      {
                          var props = typeof(T).GetProperties();
              
                          foreach (var prop in props)
                          {
                              DataTableSchemaResult.SchemaResult.Add(new()
                              {
                                  data = prop.Name,
                                  sortable = (prop.PropertyType == typeof(int)) || (prop.PropertyType == typeof(bool)) || (prop.PropertyType == typeof(DateTime)),
                                  width = "",
                                  visible = (prop.PropertyType != typeof(DateTime))
                              });
                          }
              
                          return this;
                      }
              
                      public JsDataTblGeneretaor<T> CreateTableColumns()
                      {
                          var props = typeof(T).GetProperties();
              
                          CustomAttributeData displayAttribute;
              
                          foreach (var prop in props)
                          {
                              string displayName = prop.Name;
                              displayAttribute = prop.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "DisplayAttribute");
                              if (displayAttribute != null)
                              {
                                  displayName = displayAttribute.NamedArguments.FirstOrDefault().TypedValue.Value.ToString();
                              }
              
                              DataTableSchemaResult.Colums.Add(displayName);
                          }
              
                          return this;
                      }
              
                      public JsDataTblGeneretaor<T> AddCustomSchema(string data, bool? sortable = null, bool? visible = null, string width = null, string className = null)
                      {
                          if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any())
                              return this;
              
                          foreach (var item in DataTableSchemaResult.SchemaResult.Where(x => x.data == data))
                          {
                              if (sortable != null)
                                  item.sortable = sortable.Value;
              
                              if (visible != null)
                                  item.visible = visible.Value;
              
                              if (width != null)
                                  item.width = width;
              
                              if (className != null)
                                  item.className = className;
                          }
              
                          return this;
                      }
                      public JsDataTblGeneretaor<T> SerializeSchema()
                      {
                          if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any())
                              return this;
              
                          DataTableSchemaResult.SerializedSchemaResult = JsonSerializer.Serialize(DataTableSchemaResult.SchemaResult);
              
                          return this;
                      }
                  }
                  public class DataTableSchema
                  {
                      public string data { get; set; }
                      public bool sortable { get; set; }
                      public string width { get; set; }
                      public bool visible { get; set; }
                      public string className { get; set; }
                  }
                  public class DataTableSchemaResult
                  {
                      public readonly List<DataTableSchema> SchemaResult = new();
                      public readonly List<string> Colums = new();
                      public string SerializedSchemaResult = "";
                  }
              متد CreateTableSchema، آبجکت هایی را که دیتاتیبل نیاز دارد، ایجاد می‌کند (فرقی ندارد مدلت شما از چه جنسی باشد) که شامل یک لیست از آبجکت‌هاست و شما میتوانید بااستفاده از متد AddCustomSchema، آن را سفارشی سازی کنید؛ مثلا بگوئید فلان کلید نمایش داده نشود و یا عرضش را مشخص کنید و ...  
              متد CreateTableColumns  خیلی ساده هست و فقط یک لیست از استرینگ‌ها را برمیگرداند.
              SerializeSchema هم که لیست آبجکت‌های مورد نیاز دیتاتیبل را سریالایز می‌کند.

              نحوه استفاده:
              در متد آغازین برنامه باید این کلاس را صدا بزنید و با هر روشی که دوست دارید، به view یا razor page ارسال کنید:
              public void OnGet()
                      {
                          //Create Data Table Js Schema and Columns Dynamicly
                          JsDataTblGeneretaor<yourVM> tblGeneretaor = new();
                          DataTableSchemaResult = tblGeneretaor.CreateTableColumns().CreateTableSchema().SerializeSchema().DataTableSchemaResult;
                      }

              نحوه سفارشی سازی:
              .AddCustomSchema("yourProperty",visible:false)
              که به سمت View ارسال می‌شودو حالا نحوه‌ی استفاده کردن از scheme ساخته شده:
              var scheme = JSON.parse('@Html.Raw(Model.DataTableSchemaResult.SerializedSchemaResult)')
              و استفاده‌ی از آن در گزینه‌های دیتاتیبل  columns: scheme,

              نحوه ساخت ستون‌ها در view:
              @foreach (var col in Model.DataTableSchemaResult.Colums)
                            {
                              <th>@col</th>
                            }

              مطالب
              به روز رسانی اطلاعات Master-Detail یا Master-Detail-DetailOfDetail با استفاده از EF Core

              یکی از چالش‌هایی که در طراحی زیرساخت برای Domain هایی که تعداد زیادی عملیات CRUD را در back office سیستم خود دارند، داشتن مکانیزمی برای ذخیره سازی اطلاعات Master-Detail یا چه بسا Master-Detail-DetailOfDetail می‌باشد. در ادامه نحوه برخورد با چنین سناریوهایی را در EF Core و همچنین با استفاده از AutoMapper و FluentValidation بررسی خواهیم کرد.


              موجودیت‌های فرضی

              public abstract class Entity : IHaveTrackingState
              {
                  public long Id { get; set; }
                  [NotMapped] public TrackingState TrackingState { get; set; }
              }
              
              public class Master : Entity
              {
                  public string Title { get; set; }
                  public ICollection<Detail> Details { get; set; }
              }
              
              public class Detail : Entity
              {
                  public string Title { get; set; }
              
                  public ICollection<DetailOfDetail> Details { get; set; }
                  public Master Master { get; set; }
                  public long MasterId { get; set; }
              }
              
              public class DetailOfDetail : Entity
              {
                  public string Title { get; set; }
                  public Detail Detail { get; set; }
                  public long DetailId { get; set; }
              }

              DbContext برنامه

              public class ProjectDbContext : DbContext
              {
                  public DbSet<Master> Masters { get; set; }
              
                  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
                  {
                      base.OnConfiguring(optionsBuilder);
              
                      optionsBuilder.UseInMemoryDatabase("SharedDatabaseName");
                  }
              }

              واسط IHaveTrackingState
              public interface IHaveTrackingState
              {
                  TrackingState TrackingState { get; set; }
                  //ICollection<string> ModifiedProperties { get; set; }
              }
              
              public enum TrackingState
              {
                  Unchanged = 0,
                  Added = 1,
                  Modified = 2,
                  Deleted = 3
              }

              با استفاده از پراپرتی TrackingState بالا، امکان مشخص کردن صریح State رکورد ارسالی توسط کلاینت مهیا می‌شود. قبلا نیز مطلبی در راستای STE یا همان Self-Tracking Entity تهیه شده است؛ و همچنین نظرات ارسالی این مطلب نیز می‌تواند مفید واقع شود. 


              DTO‌های متناظر با موجودیت‌های فرضی

              public abstract class Model : IHaveTrackingState
              {
                  public long Id { get; set; }
                  public TrackingState TrackingState { get; set; }
              }
              
              public class MasterModel : Model
              {
                  public string Title { get; set; }
                  public ICollection<DetailModel> Details { get; set; }
              }
              
              public class DetailModel : Model
              {
                  public string Title { get; set; }
                  public ICollection<DetailOfDetailModel> Details { get; set; }
              }
              
              public class DetailOfDetailModel : Model
              {
                  public string Title { get; set; }
              }

              تنظیمات نگاشت موجودیت‌ها و DTOها
              Mapper.Initialize(expression =>
              {
                  expression.CreateMap<MasterModel, Master>(MemberList.None).ReverseMap();
                  expression.CreateMap<DetailModel, Detail>(MemberList.None).ReverseMap();
                  expression.CreateMap<DetailOfDetailModel, DetailOfDetail>(MemberList.None).ReverseMap();
              });

              البته بهتر است این تنظیمات در درون Profile‌های مرتبط با AutoMapper کپسوله شوند و در زمان مورد نیاز نیز برای انجام نگاشت‌ها، واسط IMapper تزریق شده و استفاده شود.


              تهیه داده ارسالی فرضی توسط کلاینت

              var masterModel = new MasterModel
                  {
                      Title = "Master-Title",
                      TrackingState = TrackingState.Added,
                      Details = new List<DetailModel>
                      {
                          new DetailModel
                          {
                              Title = "Detail-Title",
                              TrackingState = TrackingState.Added,
                              Details = new List<DetailOfDetailModel>
                              {
                                  new DetailOfDetailModel
                                  {
                                      Title = "DetailOfDetail-Title",
                                      TrackingState = TrackingState.Added,
                                  }
                              }
                          }
                      }
                  };

              ذخیره سازی اطلاعات

              در EF Core، متد جدید context.ChangeTracker.TrackGraph برای به روز رسانی وضعیت یک گراف از اشیاء مشابه به اطلاعات ارسالی ذکر شده در بالا، اضافه شده است. این مکانیزم مفهوم کاملا جدیدی در EF Core می‌باشد که امکان کنترل نهایی برروی اشیایی را که قرار است توسط Context ردیابی شوند، مهیا می‌کند. با پیمایش یک گراف، امکان اجرای عملیات مورد نظر شما را برروی تک تک اشیاء، مهیا می‌سازد. 

              using (var context = new ProjectDbContext())
              {
                  Console.WriteLine("################ Create Master and Details and DetailsOfDetail ##################");
                  Print(masterModel);
              
                  var masterEntity = Mapper.Map<Master>(masterModel);
              
                  context.ChangeTracker.TrackGraph(
                      masterEntity,
                      n =>
                      {
                          var entity = (IHaveTrackingState) n.Entry.Entity;
                          n.Entry.State = entity.TrackingState.ToEntityState();
                      });
              
                  context.SaveChanges();
              }

              در تکه کد بالا، پس از انجام عملیات نگاشت، توسط متد TrackGraph به صورت صریح، وضعیت موجودیت‌ها مشخص شده است؛ این کار با تغییر State ارسالی توسط کلاینت به State قابل فهم توسط EF انجام شده‌است. برای این منظور دو متد الحاقی زیر را می‌توان در نظر گرفت:

              public static class TrackingStateExtensions
              {
                  public static EntityState ToEntityState(this TrackingState trackingState)
                  {
                      switch (trackingState)
                      {
                          case TrackingState.Added:
                              return EntityState.Added;
              
                          case TrackingState.Modified:
                              return EntityState.Modified;
              
                          case TrackingState.Deleted:
                              return EntityState.Deleted;
              
                          case TrackingState.Unchanged:
                              return EntityState.Unchanged;
              
                          default:
                              return EntityState.Unchanged;
                      }
                  }
              
                  public static TrackingState ToTrackingState(this EntityState state)
                  {
                      switch (state)
                      {
                          case EntityState.Added:
                              return TrackingState.Added;
              
                          case EntityState.Modified:
                              return TrackingState.Modified;
              
                          case EntityState.Deleted:
                              return TrackingState.Deleted;
              
                          case EntityState.Unchanged:
                              return TrackingState.Unchanged;
              
                          default:
                              return TrackingState.Unchanged;
                      }
                  }
              }

              شبیه سازی عملیات ویرایش
              //GetForEditAsync
              var masterModel = context.Masters
                  .ProjectTo<MasterModel>()
                  .AsNoTracking().Single(a => a.Id == 1);
              
              //Client
              var detail1 = masterModel.Details.First();
              detail1.Title = "Details-EditedTitle";
              detail1.TrackingState = TrackingState.Modified;
              
              foreach (var detail in detail1.Details)
              {
                  detail.TrackingState = TrackingState.Deleted;
                  //detail.Title = "DetailOfDetails-EditedTitle";
              }

              متدی تحت عنوان GetForEditAsync که یک MasterModel را بازگشت می‌دهد، در نظر بگیرید؛ کلاینت از طریق API، این Object Graph را دریافت می‌کند و تغییرات خود را اعمال کرده و همانطور که مشخص می‌باشد به دلیل اینکه تنظیمات نگاشت بین Detail و DetailModel در ابتدای بحث نیز انجام شده است، این بار دیگر نیاز به استفاده از متد Include نمی‌باشد و این عملیات توسط متد ProjectTo خودکار می‌باشد. در نهایت داده ارسالی توسط کلاینت را دریافت کرده و به شکل زیر عملیات به روز رسانی انجام می‌شود:

              using (var context = new ProjectDbContext())
              {
                  Console.WriteLine(
                      "################ Unchanged Master and Modified Details and Deleted DetailsOfDetail ##################");
                  Print(masterModel);
              
                  var masterEntity = Mapper.Map<Master>(masterModel);
              
                  context.ChangeTracker.TrackGraph(
                      masterEntity,
                      n =>
                      {
                          var entity = (IHaveTrackingState) n.Entry.Entity;
                          n.Entry.State = entity.TrackingState.ToEntityState();
                      });
              
                  context.SaveChanges();
              }

              با خروجی زیر:

              برای بحث اعتبارسنجی هم می‌توان به شکل زیر عمل کرد:

              public class MasterValidator : AbstractValidator<MasterModel>
              {
                  public MasterValidator()
                  {
                      RuleFor(a => a.Title).NotEmpty();
                      RuleForEach(a => a.Details).SetValidator(new DetailValidator());
                  }
              }
              
              public class DetailValidator : AbstractValidator<DetailModel>
              {
                  public DetailValidator()
                  {
                      RuleFor(a => a.Title).NotEmpty();
                      RuleForEach(a => a.Details).SetValidator(new DetailOfDetailValidator());
                  }
              }
              
              public class DetailOfDetailValidator : AbstractValidator<DetailOfDetailModel>
              {
                  public DetailOfDetailValidator()
                  {
                      RuleFor(a => a.Title).NotEmpty();
                  }
              }

              با استفاده از متد RuleForEach و SetValidator موجود در کتابخانه FluentValidation، امکان مشخص کردن اعتبارسنج برای Detail موجود در شیء Master را خواهیم داشت.

              همچنین با توجه به این که برای عملیات Create و Edit از یک مدل (DTO) استفاده خواهیم کرد، شاید لازم باشد اعتبارسنجی خاصی را فقط در زمان ویرایش لازم داشته باشیم، که در این صورت می‌توان از امکانات RuleSet استفاده کنید. در مطلب «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» با استفاده ValidateWithRuleAttribute امکان مشخص کردن RuleSet مورد نظر برای اعتبارسنجی ورودی متد سرویس نیز در نظر گرفته شده است.


              منابع تکمیلی

              کتابخانه کمکی

              کدهای کامل مطلب جاری را  از اینجا می‌توانید دریافت کنید.
              مطالب
              رشته ها و پردازش متن در دات نت به زبان ساده
              رشته، مجموعه‌ای از کاراکترهاست که پشت سرهم، در مکانی از حافظه قرار گرفته‌اند. هر کاراکتر حاوی یک شماره سریال در جدول یونیکد هست. به طور پیش فرض دات نت برای هر کاراکتر (نوع داده char) شانزده بیت در نظر گرفته است که برای 65536 کاراکتر کافی است.
              برای نگهداری از رشته‌ها و انجام عملیات بر روی آنها در دات نت از نوع system.string استفاده می‌کنیم:
              string greeting = "Hello, C#";

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

              اتفاقاتی که در داخل کلاس string رخ می‌دهد بسیار ساده است و ما را از تعریف []char بی‌نیاز می‌کند تا مجبور نشویم خانه‌های  آرایه را به ترتیب پر کنیم. از معایب استفاده از آرایه char میتوان موارد زیر را برشمارد:
              1. خانه‌های آن یک ضرب پر نمیشوند بلکه به ترتیب، خانه به خانه پر می‌شوند.
              2. قبل از انتساب متن باید باید از طول متن مطمئن شویم تا بتوانیم تعداد خانه‌ها را بر اساس آن ایجاد کنیم.
              3. همه عملیات آرایه‌ها از پر کردن ابتدای کار گرفته تا هر عملی، نیاز است به صورت دستی صورت بگیرد و تعداد خطوط کد برای هر کاری هم بالا می‌رود.
              البته استفاده از string هم راه حل نهایی برای کار با متون نیست. در انتهای این مطلب مورد دیگری را نیز بررسی خواهیم کرد. از ویژگی دیگر رشته‌ها این است که آن‌ها شباهت زیادی به آرایه‌ای از کاراکتر‌ها دارند؛ ولی اصلا شبیه آن‌ها نیستند و نمی‌توانید به صورت یک آرایه آن‌ها را مقداردهی کنید. البته کلاس string امکاناتی را با استفاده از indexer [] مهیا کرده است که میتوانید بر اساس اندیس‌ها به کاراکترها به صورت جداگانه دسترسی داشته باشید ولی نمی‌توانید آن‌ها را مقدار دهی کنید. این اندیس‌ها از 0 تا طول آن length-1 ادامه دارند.
              string str = "abcde";
              char ch = str[1]; // ch == 'b'
              str[1] = 'a'; // Compilation error!
              ch = str[50]; // IndexOutOfRangeException
              همانطور که میدانیم برای مقداردهی رشته‌ها از علامت‌های نقل قول "" استفاده میکنیم که باعث میشود اگر بخواهیم علامت " را در رشته‌ها داشته باشیم نتوانیم. برای حل این مشکل از علامت \ استفاده میکنیم که البته باعث استفاده از بعضی کاراکترهای خاص دیگر هم می‌شود:
              string a="Hello \"C#\"";
              string b="Hello \r\n C#"; //مساوی با اینتر
              string c="C:\\a.jpg"; //چاپ خود علامت  \ -مسیردهی
              البته اگر از علامت @ در قبل از رشته استفاده شود علامت \ بی اثر خواهد شد.
              string c=@"C:\a.jpg";// == "C:\\a.jpg"

              مقداردهی رشته‌ها و پایدار (تغییر ناپذیر) بودن آنها Immutable
              رشته‌ها ساختاری پایدار هستند؛ به این معنی که به صورت reference مقداردهی می‌شوند. موقعی که شما مقداری را به یک رشته انتساب می‌دهید، مقدار متغیر در  String pool یا لینک در Heap ذخیره می‌شوند و اگر همین متغیر را به یک متغیر دیگر انتساب دهیم، متغیر جدید مقدار آن را دیگر در حافظه پویا (داینامیک) Heap به عنوان مقدار جدید ذخیره نخواهد کرد؛ بلکه تنها یک pointer خواهد بود که به آدرس حافظه متغیر اولی اشاره می‌کند. به مثال زیر دقت کنید. متغیر source مقدار some source را ذخیره می‌کند و بعد همین متغیر، به متغیر assigned انتساب داده میشود؛ ولی مقداری جابجا نمی‌شود. بلکه متغیر assign به آدرسی در حافظه اشاره می‌کند که متغیر source اشاره می‌کند. هرگاه که در یکی از متغیرها، تغییری رخ دهد، همان متغیری که تغییر کرده است، به آدرس جدید با محتوای تغییر داده شده اشاره می‌کند.
              string source = "Some source";
              string assigned = source;

              این ویژگی نوع reference فقط برای ساختارهای Immutable به معنی پایدار رخ می‌دهد و نه برای ساختار‌های ناپایدار (تغییر پذیر)  mutable؛ به این خاطر که آن‌ها مقادیرشان را مستقیما تغییر میدهند و اشاره‌ای در حافظه صورت نمی‌گیرد. 
              string hel = "Hel";
              string hello = "Hello";
              string copy = hel + "lo";

              string hello = "Hello";
              string same = "Hello";

              برای اطلاعات بیشتر در این زمینه این لینک را مطالعه نمایید.


              مقایسه رشته‌ها
              برای مقایسه دو رشته میتوان از علامت == یا از متد Equals استفاده نماییم. در این حالت به خاطر اینکه کد حروف کوچک و بزرگ متفاوت است، مقایسه حروف هم متفاوت خواهد بود. برای اینکه حروف کوچک و بزرگ تاثیری بر مقایسه ما نگذارند و #c را با #C برابر بدانند باید از متد Equals به شکل زیر استفاده کنیم:
              Console.WriteLine(word1.Equals(word2,
                  StringComparison.CurrentCultureIgnoreCase));
              برای اینکه بزرگی و کوچکی اعداد را مشخص کنیم از علامت‌های < و > استفاده میکنیم ولی برای رشته‌ها از متد CompareTo بهره می‌بریم که چینش قرارگیری آن‌ها را بر اساس حروف الفبا مقایسه می‌کند و سه عدد، می‌تواند خروجی آن باشند. اگر 0 باشد یعنی برابر هستند، اگر -1 باشد رشته اولی قبل از رشته دومی است و اگر 1 باشد رشته دومی قبل از رشته اولی است.
              string score = "sCore";
              string scary = "scary";
               
              Console.WriteLine(score.CompareTo(scary));
              Console.WriteLine(scary.CompareTo(score));
              Console.WriteLine(scary.CompareTo(scary));
               
              // Console output:
              // 1
              // -1
              // 0
               اینبار هم برای اینکه حروف کوچک و بزرگ، دخالتی در کار نداشته باشند، میتوانید از داده شمارشی StringComparison در متد ایستای (string.Compare(s1,s2,StringComparison استفاده نمایید؛ یا از نوع داده‌ای boolean برای تعیین نوع مقایسه استفاده کنید.
              string alpha = "alpha";
              string score1 = "sCorE";
              string score2 = "score";
               
              Console.WriteLine(string.Compare(alpha, score1, false));
              Console.WriteLine(string.Compare(score1, score2, false));
              Console.WriteLine(string.Compare(score1, score2, true));
              Console.WriteLine(string.Compare(score1, score2,
                  StringComparison.CurrentCultureIgnoreCase));
              // Console output:
              // -1
              // 1
              // 0
              // 0
              نکته : برای مقایسه برابری  دو رشته از متد Equals یا == استفاده کنید و فقط برای تعیین کوچک یا بزرگ بودن از compare‌ها استفاده نمایید. دلیل آن هم این است که برای مقایسه از فرهنگ culture فعلی سیستم استفاده میشود و نظم جدول یونیکد را رعایت نمی‌کنند و ممکن است بعضی رشته‌های نابرابر با یکدیگر برابر باشند. برای مثال در زبان آلمانی دو رشته "SS" و "ß " با یکدیگر برابر هستند.

              عبارات با قاعده Regular Expression
              این عبارات الگوهایی هستند که قرار است عبارات مشابه الگویی را در رشته‌ها پیدا کنند. برای مثال الگوی +[A-Z0-9] مشخص می‌کند که رشته مورد نظر نباید خالی باشد و حداقل با یکی از حروف بزرگ یا اعداد پرشده باشد. این الگوها میتوانند برای واکشی داده‌ها یا قالب‌های خاص در رشته‌ها به کار بروند. برای مثال شماره تماس‌ها ، پست الکترونیکی و ...
              در اینجا میتواند نحوه‌ی الگوسازی را بیاموزید. کد زیر بر اساس یک الگو، شماره تماس‌های مورد نظر را یافته و البته با فیلتر گذاری آن‌ها را نمایش می‌دهد:
              string doc = "Smith's number: 0898880022\nFranky can be " +
                  "found at 0888445566.\nSteven's mobile number: 0887654321";
              string replacedDoc = Regex.Replace(
                  doc, "(08)[0-9]{8}", "$1********");
              Console.WriteLine(replacedDoc);
              // Console output:
              // Smith's number: 08********
              // Franky can be found at 08********.
              // Steven' mobile number: 08********
              سه شماره تماس در رشته‌ی بالا با الگوی ما همخوانی دارند که بعد با استفاده از متد replace در شی Regex عبارات دلخواه خودمان را جایگزین شماره تماس‌ها خواهیم کرد. الگوی بالا شماره تماس‌هایی را میابد که با 08 آغاز شده‌اند و بعد از آن 8 عدد دیگر از 0 تا 9 قرار گرفته‌اند. بعد از اینکه متن مطابق الگو یافت شد، ما آن را با الگوی ********1$ جایگزین می‌کنیم که علامت $ یک placeholder برای یک گروه است. هر عبارت () در عبارات با قاعده یک گروه حساب میشود و اولین پرانتر 1$ و دومین پرانتز یا گروه میشود 2$ که در عبارت بالا (08) میشود 1$ و به جای مابقی الگو، 8 علامت ستاره نمایش داده میشود.

              اتصال رشته‌ها در Loop
              برای اتصال رشته‌ها ما از علامت + یا متد ایستای string.concat استفاده می‌کنیم ولی استفاده‌ی از آن در داخل یک حلقه باعث کاهش کارآیی برنامه خواهد شد. برای همین بیایید ببینم در حین اتتقال رشته‌ها در حافظه چه اتفاقی رخ میدهد. ما در اینجا دو رشته str1 و str2 داریم که عبارات "super" و "star" را نگه داری می‌کنند و در واقع دو متغیر هستند که به حافظه‌ی پویای Heap اشاره می‌کنند. اگر این دو را با هم جمع کنیم و نتیجه را در متغیر result قرار دهیم، سه متغیر میشوند که هر کدام به حافظه‌ای جداگانه در heap اشاره می‌کنند. در واقع برای این اتصال، قسمت جدیدی از حافظه تخصصیص داده شده و مقدار جدید در آن نشسته‌است. در این حالت یک متغیر جدید ساخته شد که به آدرس آن اشاره می‌کند. کل این فرآیند یک فرآیند کاملا زمانبر است که با تکرار این عمل موجب از دست دادن کارآیی برنامه می‌شود؛ به خصوص اگر در یک حلقه این کار صورت بگیرد.
              سیستم دات نت همانطور که میدانید شامل GC یا سیستم خودکار پاکسازی حافظه است که برنامه نویس را از dispose کردن بسیاری از اشیاء بی نیاز می‌کند. موقعی‌که متغیری به قسمتی از حافظه اشاره می‌کند که دیگر بلا استفاده است، سیستم GC به صورت خودکار آنها را پاکسازی می‌کند که این عمل زمان بر هم خودش موجب کاهش کارآیی می‌شود. همچنین انتقال رشته‌ها از یک مکان حافظه به مکانی دیگر، باز خودش یک فرآیند زمانبر است؛ به خصوص اگر رشته مورد نظر طولانی هم باشد.
              مثال عملی: در تکه کد زیر قصد داریم اعداد 1 تا 20000 را در یک رشته الحاق کنیم:
               DateTime dt = DateTime.Now;
                          string s = "";
                      for (int index = 1; index <= 20000; index++)
                      {
                          s += index.ToString();
                      }
                          Console.WriteLine(s);
                          Console.WriteLine(dt);
                          Console.WriteLine(DateTime.Now);
                          Console.ReadKey();
              کد بالا تاز زمان نمایش کامل، بسته به قدرت سیستم ممکن است یکی دو ثانیه طول بکشد. حالا عدد را به 200000 تغییر دهید (یک صفر اضافه تر). برنامه را اجرا کنید و مجددا تست بزنید. در این حالت چند دقیقه ای بسته به قدرت سیستم زمان خواهد برد؛ مثلا دو دقیقه یا سه دقیقه یا کمتر و بیشتر.
              عملیاتی که در حافظه صورت میگیرد این چند گام را طی میکند:
              • قسمتی از حافظه به طور موقت برای این دور جدید حلقه، گرفته میشود که به آن بافر میگوییم.
              • رشته قبلی به بافر انتقال میابد که بسته به مقدار آن زمان بر و کند است؛ 5 کیلو یا 5 مگابایت یا 50 مگابایت و ...
              • شماره تولید شده جدید به بافر چسبانده میشود.
              • بافر به یک رشته تبدیل میشود وجایی برای خود در حافظه Heap میگیرد.
              • حافظه رشته قدیمی و بافر دیگر بلا استفاده شده‌اند و توسط GC پاکسازی میشوند که ممکن است عملیاتی زمان بر باشد.

              String Builder
              این کلاس ناپایدار و تغییر پذیر است. به کد و شکل زیر دقت کنید:
              string declared = "Intern pool";
              string built = new StringBuilder("Intern pool").ToString();

              این کلاس دیگر مشکل الحاق رشته‌ها یا دیگر عملیات پردازشی را ندارد. بیایید مثال قبل را برای این کلاس هم بررسی نماییم:
               StringBuilder sb = new StringBuilder();
                    sb.Append("Numbers: ");
              
                          DateTime dt = DateTime.Now;
                      for (int index = 1; index <= 200000; index++)
                      {
                          sb.Append(index);
                      }
                          Console.WriteLine(sb.ToString());
                          Console.WriteLine(dt);
                          Console.WriteLine(DateTime.Now);
                          Console.ReadKey();
              اکنون همین عملیات چند دقیقه‌ای قبل، در زمانی کمتر، مثلا دو ثانیه انجام میشود.
              حال این سوال پیش می‌آید مگر کلاس stringbuilder چه میکند که زمان پردازش آن قدر کوتاه است؟
              همانطور که گفتیم این کلاس mutable یا تغییر پذیر است و برای انجام عملیات‌های ویرایشی نیازی به ایجاد شیء جدید در حافظه ندارد؛ در نتیجه باعث کاهش انتقال غیرضروری داده‌ها برای عملیات پایه‌ای چون الحاق رشته‌ها میگردد.
              stringbuilder شامل یک بافر با ظرفیتی مشخص است (به طور پیش فرض 16 کاراکتر). این کلاس آرایه‌هایی از کاراکترها را پیاده سازی میکند که برای عملیات و پردازش‌هایش  از یک رابط کاربرپسند برای برنامه نویسان استفاده می‌کند. اگر تعداد کاراکترها کمتر از 16 باشد مثلا 5 ، فقط 5 خانه آرایه استفاده میشود و مابقی خانه‌ها خالی میماند و با اضافه شدن یک کاراکتر جدید، دیگر شیء جدیدی در حافظه درست نمی‌شود؛ بلکه در خانه ششم قرار می‌گیرد و اگر تعداد کاراکترهایی که اضافه می‌شوند باعث شود از 16 کاراکتر رد شود، مقدار خانه‌ها دو برابر میشوند؛ هر چند این عملیات دو برابر شدن resizing عملیاتی کند است ولی این اتفاق به ندرت رخ می‌دهد.
              کد زیر یک آرایه 15 کاراکتری ایجاد می‌کند و عبارت #Hello C را در آن قرار می‌دهد.
              StringBuilder sb = new StringBuilder(15);
              sb.Append("Hello, C#!");

              در شکل بالا خانه هایی خالی مانده است Unused و  جا برای کاراکترهای جدید به اندازه خانه‌های unused هست و اگر بیشتر شود همانطور که گفتیم تعداد خانه‌ها 2 برابر می‌شوند که در اینجا میشود 30.

              استفاده از متد ایستای string.Format
              از این متد برای نوشتن یک متن به صورت قالب و سپس جایگزینی مقادیر استفاده می‌شود:
              DateTime date = DateTime.Now;
              string name = "David Scott";
              string task = "Introduction to C# book";
              string location = "his office";
               
              string formattedText = String.Format(
                  "Today is {0:MM/dd/yyyy} and {1} is working on {2} in {3}.",
                  date, name, task, location);
              Console.WriteLine(formattedText);
              در کد بالا ابتدا ساختار قرار گرفتن تاریخ را بر اساس الگو بین {} مشخص می‌کنیم و متغیر date در آن قرار می‌گیرد و سپس برای {1},{2},{3} به ترتیب قرار گیری آن‌ها متغیرهای name,last,location قرار میگیرند.
              از ()ToString. هم می‌توان برای فرمت بندی خروجی استفاده کرد؛ مثل همین عبارت MM/dd/yyyy در خروجی نوع داده تاریخ و زمان.
              مطالب
              Attribute Routing در ASP.NET MVC 5
              Routing مکانیزم مسیریابی ASP.NET MVC است، که یک URI را به یک اکشن متد نگاشت می‌کند. MVC 5 نوع جدیدی از مسیر یابی را پشتیبانی میکند که Attribute Routing یا مسیریابی نشانه ای نام دارد. همانطور که از نامش پیداست، مسیریابی نشانه ای از Attribute‌ها برای این امر استفاده میکند. این روش به شما کنترل بیشتری روی URI‌های اپلیکیشن تان می‌دهد.
              مدل قبلی مسیریابی (conventional-routing) هنوز کاملا پشتیبانی می‌شود. در واقع می‌توانید هر دو تکنیک را بعنوان مکمل یکدیگر در یک پروژه استفاده کنید.
              در این پست قابلیت‌ها و گزینه‌های اساسی مسیریابی نشانه ای را بررسی میکنیم.
              • چرا مسیریابی نشانه ای؟
              • فعال سازی مسیریابی نشانه ای
              • پارامتر‌های اختیاری URI و مقادیر پیش فرض
              • پیشوند مسیر ها
              • مسیر پیش فرض
              • محدودیت‌های مسیر ها
                  • محدودیت‌های سفارشی
              • نام مسیر ها
              • ناحیه‌ها (Areas)


              چرا مسیریابی نشانه ای

              برای مثال یک وب سایت تجارت آنلاین بهینه شده اجتماعی، می‌تواند مسیرهایی مانند لیست زیر داشته باشد:
              • {productId:int}/{productTitle}
              نگاشت می‌شود به: (ProductsController.Show(int id
              • {username}
              نگاشت می‌شود به: (ProfilesController.Show(string username
              • {username}/catalogs/{catalogId:int}/{catalogTitle}
              نگاشت می‌شود به: (CatalogsController.Show(string username, int catalogId
              در نسخه قبلی ASP.NET MVC، قوانین مسیریابی در فایل RouteConfig.cs تعریف می‌شدند، و اشاره به اکشن‌های کنترلرها به نحو زیر انجام می‌شد:
              routes.MapRoute(
                  name: "ProductPage",
                  url: "{productId}/{productTitle}",
                  defaults: new { controller = "Products", action = "Show" },
                  constraints: new { productId = "\\d+" }
              );
              هنگامی که قوانین مسیریابی در کنار اکشن متدها تعریف می‌شوند، یعنی در یک فایل سورس و نه در یک کلاس پیکربندی خارجی، درک و فهم نگاشت URI‌ها به اکشن‌ها واضح‌تر و راحت می‌شود. تعریف مسیر قبلی، می‌تواند توسط یک attribute ساده بدین صورت نگاشت شود:
              [Route("{productId:int}/{productTitle}")]
              public ActionResult Show(int productId) { ... }

              فعال سازی Attribute Routing

              برای فعال سازی مسیریابی نشانه ای، متد MapMvcAttributeRoutes را هنگام پیکربندی فراخوانی کنید.
              public class RouteConfig
              {
                  public static void RegisterRoutes(RouteCollection routes)
                  {
                      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
               
                      routes.MapMvcAttributeRoutes();
                  }
              }
              همچنین می‌توانید مدل قبلی مسیریابی را با تکنیک جدید تلفیق کنید.
              public static void RegisterRoutes(RouteCollection routes)
              {
                  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
               
                  routes.MapMvcAttributeRoutes();
               
                  routes.MapRoute(
                      name: "Default",
                      url: "{controller}/{action}/{id}",
                      defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
                  );
              }

              پارامترهای اختیاری URI و مقادیر پیش فرض

              می توانید با اضافه کردن یک علامت سوال به پارامترهای مسیریابی، آنها را optional یا اختیاری کنید. برای تعیین مقدار پیش فرض هم از فرمت parameter=value استفاده می‌کنید.
              public class BooksController : Controller
              {
                  // eg: /books
                  // eg: /books/1430210079
                  [Route("books/{isbn?}")]
                  public ActionResult View(string isbn)
                  {
                      if (!String.IsNullOrEmpty(isbn))
                      {
                          return View("OneBook", GetBook(isbn));
                      }
                      return View("AllBooks", GetBooks());
                  }
               
                  // eg: /books/lang
                  // eg: /books/lang/en
                  // eg: /books/lang/he
                  [Route("books/lang/{lang=en}")]
                  public ActionResult ViewByLanguage(string lang)
                  {
                      return View("OneBook", GetBooksByLanguage(lang));
                  }
              }
              در این مثال، هر دو مسیر books/ و books/1430210079/ به اکشن متد "View" نگاشت می‌شوند، مسیر اول تمام کتاب‌ها را لیست میکند، و مسیر دوم جزئیات کتابی مشخص را لیست می‌کند. هر دو مسیر books/lang/ و books/lang/en/ به یک شکل نگاشت می‌شوند، چرا که مقدار پیش فرض این پارامتر en تعریف شده.



              پیشوند مسیرها (Route Prefixes)

              برخی اوقات، تمام مسیرها در یک کنترلر با یک پیشوند شروع می‌شوند. بعنوان مثال:
              public class ReviewsController : Controller
              {
                  // eg: /reviews
                  [Route("reviews")]
                  public ActionResult Index() { ... }
                  // eg: /reviews/5
                  [Route("reviews/{reviewId}")]
                  public ActionResult Show(int reviewId) { ... }
                  // eg: /reviews/5/edit
                  [Route("reviews/{reviewId}/edit")]
                  public ActionResult Edit(int reviewId) { ... }
              }
              همچنین می‌توانید با استفاده از خاصیت [RoutePrefix] یک پیشوند عمومی برای کل کنترلر تعریف کنید:
              [RoutePrefix("reviews")]
              public class ReviewsController : Controller
              {
                  // eg.: /reviews
                  [Route]
                  public ActionResult Index() { ... }
                  // eg.: /reviews/5
                  [Route("{reviewId}")]
                  public ActionResult Show(int reviewId) { ... }
                  // eg.: /reviews/5/edit
                  [Route("{reviewId}/edit")]
                  public ActionResult Edit(int reviewId) { ... }
              }
              در صورت لزوم، می‌توانید برای بازنویسی (override) پیشوند مسیرها از کاراکتر ~ استفاده کنید:
              [RoutePrefix("reviews")]
              public class ReviewsController : Controller
              {
                  // eg.: /spotlight-review
                  [Route("~/spotlight-review")]
                  public ActionResult ShowSpotlight() { ... }
               
                  ...
              }

              مسیر پیش فرض

              می توانید خاصیت [Route] را روی کنترلر اعمال کنید، تا اکشن متد را بعنوان یک پارامتر بگیرید. این مسیر سپس روی تمام اکشن متدهای این کنترلر اعمال می‌شود، مگر آنکه یک [Route] بخصوص روی اکشن‌ها تعریف شده باشد.
              [RoutePrefix("promotions")]
              [Route("{action=index}")]
              public class ReviewsController : Controller
              {
                  // eg.: /promotions
                  public ActionResult Index() { ... }
               
                  // eg.: /promotions/archive
                  public ActionResult Archive() { ... }
               
                  // eg.: /promotions/new
                  public ActionResult New() { ... }
               
                  // eg.: /promotions/edit/5
                  [Route("edit/{promoId:int}")]
                  public ActionResult Edit(int promoId) { ... }
              }

              محدودیت‌های مسیر ها

              با استفاده از Route Constraints می‌توانید نحوه جفت شدن پارامتر‌ها در قالب مسیریابی را محدود و کنترل کنید. فرمت کلی {parameter:constraint} است. بعنوان مثال:
              // eg: /users/5
              [Route("users/{id:int}"]
              public ActionResult GetUserById(int id) { ... }
               
              // eg: users/ken
              [Route("users/{name}"]
              public ActionResult GetUserByName(string name) { ... }
              در اینجا، مسیر اول تنها در صورتی انتخاب می‌شود که قسمت id در URI یک مقدار integer باشد. در غیر اینصورت مسیر دوم انتخاب خواهد شد.
              جدول زیر constraint‌ها یا محدودیت هایی که پشتیبانی می‌شوند را لیست می‌کند.
               مثال  توضیحات  محدودیت
               {x:alpha}  کاراکترهای الفبای لاتین را تطبیق (match) می‌دهد (a-z, A-Z).  alpha
               {x:bool}  یک مقدار منطقی را تطبیق می‌دهد.  bool
               {x:datetime}  یک مقدار DateTime را تطبیق می‌دهد.  datetime
               {x:decimal}  یک مقدار پولی را تطبیق می‌دهد.  decimal
               {x:double}  یک مقدار اعشاری 64 بیتی را تطبیق می‌دهد.  double
               {x:float}  یک مقدار اعشاری 32 بیتی را تطبیق می‌دهد.  float
               {x:guid}  یک مقدار GUID را تطبیق می‌دهد.  guid
               {x:int}  یک مقدار 32 بیتی integer را تطبیق می‌دهد.  int
               {(x:length(6}
              {(x:length(1,20}
               رشته ای با طول تعیین شده را تطبیق می‌دهد.  length
               {x:long}  یک مقدار 64 بیتی integer را تطبیق می‌دهد.  long
               {(x:max(10}  یک مقدار integer با حداکثر مجاز را تطبیق می‌دهد.  max
               {(x:maxlength(10}  رشته ای با حداکثر طول تعیین شده را تطبیق می‌دهد.  maxlength
               {(x:min(10}  مقداری integer با حداقل مقدار تعیین شده را تطبیق می‌دهد.  min
               {(x:minlength(10}  رشته ای با حداقل طول تعیین شده را تطبیق می‌دهد.  minlength
               {(x:range(10,50}  مقداری integer در بازه تعریف شده را تطبیق می‌دهد.  range
               {(${x:regex(^\d{3}-\d{3}-\d{4}  یک عبارت با قاعده را تطبیق می‌دهد.  regex

              توجه کنید که بعضی از constraint ها، مانند "min" آرگومان‌ها را در پرانتز دریافت می‌کنند.
              می توانید محدودیت‌های متعددی روی یک پارامتر تعریف کنید، که باید با دونقطه جدا شوند. بعنوان مثال:
              // eg: /users/5
              // but not /users/10000000000 because it is larger than int.MaxValue,
              // and not /users/0 because of the min(1) constraint.
              [Route("users/{id:int:min(1)}")]
              public ActionResult GetUserById(int id) { ... }
              مشخص کردن اختیاری بودن پارامتر ها، باید در آخر لیست constraints تعریف شود:
              // eg: /greetings/bye
              // and /greetings because of the Optional modifier,
              // but not /greetings/see-you-tomorrow because of the maxlength(3) constraint.
              [Route("greetings/{message:maxlength(3)?}")]
              public ActionResult Greet(string message) { ... }

              محدودیت‌های سفارشی

              با پیاده سازی قرارداد IRouteConstraint می‌توانید محدودیت‌های سفارشی بسازید. بعنوان مثال، constraint زیر یک پارامتر را به لیستی از مقادیر قابل قبول محدود می‌کند:
              public class ValuesConstraint : IRouteConstraint
              {
                  private readonly string[] validOptions;
                  public ValuesConstraint(string options)
                  {
                      validOptions = options.Split('|');
                  }
               
                  public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
                  {
                      object value;
                      if (values.TryGetValue(parameterName, out value) && value != null)
                      {
                          return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase);
                      }
                      return false;
                  }
              }
              قطعه کد زیر نحوه رجیستر کردن این constraint را نشان می‌دهد:
              public class RouteConfig
              {
                  public static void RegisterRoutes(RouteCollection routes)
                  {
                      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
               
                      var constraintsResolver = new DefaultInlineConstraintResolver();
               
                      constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint));
               
                      routes.MapMvcAttributeRoutes(constraintsResolver);
                  }
              }
              حالا می‌توانید این محدودیت سفارشی را روی مسیرها اعمال کنید:
              public class TemperatureController : Controller
              {
                  // eg: temp/celsius and /temp/fahrenheit but not /temp/kelvin
                  [Route("temp/{scale:values(celsius|fahrenheit)}")]
                  public ActionResult Show(string scale)
                  {
                      return Content("scale is " + scale);
                  }
              }

              نام مسیر ها

              می توانید به مسیرها یک نام اختصاص دهید، با این کار تولید URI‌ها هم راحت‌تر می‌شوند. بعنوان مثال برای مسیر زیر:
              [Route("menu", Name = "mainmenu")]
              public ActionResult MainMenu() { ... }
              می‌توانید لینکی با استفاده از Url.RouteUrl تولید کنید:
              <a href="@Url.RouteUrl("mainmenu")">Main menu</a>

              ناحیه‌ها (Areas)

              برای مشخص کردن ناحیه ای که کنترلر به آن تعلق دارد می‌توانید از خاصیت [RouteArea] استفاده کنید. هنگام استفاده از این خاصیت، می‌توانید با خیال راحت کلاس AreaRegistration را از ناحیه مورد نظر حذف کنید.
              [RouteArea("Admin")]
              [RoutePrefix("menu")]
              [Route("{action}")]
              public class MenuController : Controller
              {
                  // eg: /admin/menu/login
                  public ActionResult Login() { ... }
               
                  // eg: /admin/menu/show-options
                  [Route("show-options")]
                  public ActionResult Options() { ... }
               
                  // eg: /stats
                  [Route("~/stats")]
                  public ActionResult Stats() { ... }
              }
              با این کنترلر، فراخوانی تولید لینک زیر، رشته "Admin/menu/show-options/" را بدست میدهد:
              Url.Action("Options", "Menu", new { Area = "Admin" })
              به منظور تعریف یک پیشوند سفارشی برای یک ناحیه، که با نام خود ناحیه مورد نظر متفاوت است می‌توانید از پارامتر AreaPrefix استفاده کنید. بعنوان مثال:
              [RouteArea("BackOffice", AreaPrefix = "back-office")]
              اگر از ناحیه‌ها هم بصورت مسیریابی نشانه ای، و هم بصورت متداول (که با کلاس‌های AreaRegistration پیکربندی می‌شوند) استفاده می‌کنید باید مطمئن شوید که رجیستر کردن نواحی اپلیکیشن پس از مسیریابی نشانه ای پیکربندی می‌شود. به هر حال رجیستر کردن ناحیه‌ها پیش از تنظیم مسیرها بصورت متداول باید صورت گیرد. دلیل آن هم مشخص است، برای اینکه درخواست‌های ورودی بدرستی با مسیرهای تعریف شده تطبیق داده شوند، باید ابتدا attribute routes، سپس area registration و در آخر default route رجیستر شوند. بعنوان مثال:
              public static void RegisterRoutes(RouteCollection routes)
              {
                  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
               
                  routes.MapMvcAttributeRoutes();
               
                  AreaRegistration.RegisterAllAreas();
               
                  routes.MapRoute(
                      name: "Default",
                      url: "{controller}/{action}/{id}",
                      defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
                  );
              }