مطالب
منسوخ شده‌ها در نگارش‌های جدید SQL server

با تکامل SQL server و بهبودهای حاصل شده، یک سری از ویژگی‌های موجود صرفا جهت حفظ سازگاری با نگارش‌های قبلی ارائه می‌شوند. لیست کامل آنها را در آدرس زیر می‌توان مشاهده نمود:

Deprecated Database Engine Features in SQL Server 2008

لیست بلند بالایی است. اما در یک محیط کاری، نوع‌های زیر از سایر موارد ذکر شده بیشتر مورد استفاده قرار می‌گیرند:
منسوخ شده‌ها: text ، ntext و image . جایگزین‌ها : varchar ، nvarchar و varbinary از نوع max دار

عموما علت استفاده از نوع‌های text یا ntext (نمونه یونیکد text) ، مشخص نبودن تعداد کاراکتری است که کاربر قرار است وارد کند. برای مثال یک سایت خبری ایجاد کرده‌اید و طول محتوای خبر ثبت شده در بانک اطلاعاتی از یک خبر به خبر دیگر کاملا متفاوت است. در اینجا برای حل این مشکل از نوع‌های text یا ntext استفاده می‌شد (این مورد تا اس‌کیوال سرور 2000 توصیه می‌شود).
varchar max تا 2,147,483,648 کاراکتر را می‌تواند ذخیره کند، یعنی تا 2 GB و nvarchar max تا نصف این مقدار را. در اس کیوال سرور 2000 محدودیت 8000 کاراکتر برای نوع vrachar وجود داشت (و نوع nvrachar تا 4000 کاراکتر).

مزایای استفاده از نوع‌های max دار (از اس کیوال سرور 2005 به بعد) :
  • بهبود کارآیی کوئری‌های جستجو نسبت به نوع‌های Text‌
  • اگر مطلب تشخیص کمبود ایندکس‌ها را دنبال کرده باشید، در آنجا ذکر شد که در قسمت included columns نمی‌توان از text و ntext‌ استفاده کرد اما نوع‌های max دار متنی مجازند.
  • امکان استفاده از فیلدهای max دار برای مرتب سازی کوئری مجاز است. (به شخصه با این مورد زیاد برخورد داشتم. برای مثال امکان سورت کردن یک گرید را در ASP.Net فراهم کرده‌اید و کاربر با کلیک بر روی سر ستون فیلدی از نوع ntext با یک خطا متوقف خواهد شد)
  • امکان استفاده از نوع‌های Text به‌عنوان متغیر در رویه‌های ذخیره شده یا توابع T-SQL مهیا نیست اما این محدودیت در نوع‌های max دار برطرف شده است.
  • نوع‌های text را در توابع REPLACE ، CHARINDEX و SUBSTRINGنمی‌توان بکار برد (برخلاف نوع‌های متنی max دار).
بعد از این توضیحات شاید علاقمند شده باشید که نوع‌های فیلدهای قدیمی را به نوع‌های جدید تبدیل کنید (مراجعه به management studio ، تغییر نوع فیلد و کلیک بر روی دکمه ذخیره در نوار ابزار آن). اگر تعداد رکوردها بالا باشد، بدون شک با خطای زیر متوقف خواهید شد:

Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.

برای حل این مشکل می‌توان مقدار پیش فرض timeout را مطابق تصویر زیر تغییر داد (منوی tools گزینه options) :



بعد از این تغییر به سادگی می‌توان عملیات ارتقاء را بدون نگرانی از بروز خطای فوق انجام داد.

مطالب
رفع تداخل jQuery با کتابخانه‌های مشابه

قبل از شروع، یک خبر!
VsDoc for jQuery 1.3.1 (جهت فعال سازی intellisense آخرین نگارش جی کوئری در VS.Net)


اگر سعی کنید jQuery را به همراه سایر کتابخانه‌های جاوا اسکریپتی دیگر به صورت همزمان استفاده کنید (مثلا mootools یا ASP.Net Ajax و امثال آن)، احتمالا قسمتی و یا تمامی کدهای جاوا اسکریپتی شما کار نخواهند کرد. برای مثال update panel شما در ASP.Net Ajax از کار می‌افتد، یا کدهای mootools شما دیگر کار نمی‌کنند. علت اینجا است که تمامی این کتابخانه‌ها از نشانه $ به عنوان متغیری عمومی که بیانگر نام مستعار کتابخانه مربوطه است استفاده می‌کنند و در نهایت تمام این‌ها با هم تداخل خواهند کرد.

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

<script type="text/javascript" language="javascript" src="jquery.min.js"></script>
<script type="text/javascript">
jQuery.noConflict();
jQuery(document).ready(function($) {
//tip-1
$("select > option").each(function() {
var obj = $(this);
obj.attr("title", obj.attr("value"));
});
//tip-1
});
</script>

کد مثال فوق، به تمامی آیتم‌های drop down list‌ های شما در یک صفحه، بر اساس value هر آیتم موجود در آن‌ها، یک tooltip اضافه می‌کند. (با IE7 به بعد و فایرفاکس سازگار است)
در اینجا ابتدا jQuery.noConflict فراخوانی شده و سپس document ready متداول هم باید اندکی مطابق کد فوق تغییر کند. مابقی کدهای شما از این پس نیازی به تغییر نخواهند داشت. (روش‌های دیگری هم برای تغییر نام $ وجود دارند که در مستندات مربوطه قابل مشاهده است)

مطالب
نمایش بلادرنگ اعلامی به تمام کاربران در هنگام درج یک رکورد جدید
در ادامه می‌خواهیم اعلام عمومی نمایش افزوده شدن یک پیام جدید را بعد از ثبت رکوردی جدید، به تمامی کاربران متصل به سیستم ارسال کنیم. پیش نیاز مطلب جاری موارد زیر می‌باشند:
namespace ShowAlertSignalR.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public float Price { get; set; }
        public Category Category { get; set; }

    }

    public enum Category
    {
        [Display(Name = "دسته بندی اول")]
        Cat1,
        [Display(Name = "دسته بندی دوم")]
        Cat2,
        [Display(Name = "دسته بندی سوم")]
        Cat3
    }
}
در اینجا مدل ما شامل عنوان، توضیح، قیمت و یک enum برای دسته‌بندی یک محصول ساده می‌باشد.
کلاس context نیز به صورت زیر می‌باشد:
namespace ShowAlertSignalR.Models
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext() : base("productSample")
        {
            Database.Log = sql => Debug.Write(sql);
        }
        public DbSet<Product> Products { get; set; }
    }
}
همانطور که در ابتدا عنوان شد، می‌خواهیم بعد از ثبت یک رکورد جدید، پیامی عمومی به تمامی کاربران متصل به سایت نمایش داده شود. در کد زیر اکشن متد Create را مشاهده می‌کنید: 
[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Product product)
        {
            if (ModelState.IsValid)
            {
                db.Products.Add(product);
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            return View(product);
        }
می‌توانیم از ViewBag برای اینکار استفاده کنیم؛ به طوریکه یک پارامتر از نوع bool برای متد Index تعریف کرده و سپس مقدار آن را درون این شیء ViewBag انتقال دهیم، این متغییر بیانگر حالتی است که آیا اطلاعات جدیدی برای نمایش وجود دارد یا خیر؟ بنابراین اکشن متد Index را به اینصورت تعریف می‌کنیم:
public ActionResult Index(bool notifyUsers = false)
        {
            ViewBag.NotifyUsers = notifyUsers;
            return View(db.Products.ToList());
        }
در اینجا مقدار پیش‌فرض این متغیر، false می‌باشد. یعنی اطلاعات جدیدی برای نمایش موجود نمی‌باشد. در نتیجه اکشن متد Create را به صورتی تغییر می‌دهیم که بعد از درج رکورد موردنظر و هدایت کاربر به صفحه‌ی Index، مقدار این متغییر به true تنظیم شود:
[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Product product)
        {
            if (ModelState.IsValid)
            {
                db.Products.Add(product);
                db.SaveChanges();
                return RedirectToAction("Index", new { notifyUsers = true });
            }

            return View(product);
        }
قدم بعدی ایجاد یک هاب SignalR می‌باشد:
namespace ShowAlertSignalR.Hubs
{
    public class NotificationHub : Hub
    {
        public void SendNotification()
        {
            Clients.Others.ShowNotification();
        }
    }
}
در ادامه کدهای سمت کلاینت را برای هاب فوق، داخل ویوی Index اضافه می‌کنیم:
@section scripts
{
    
    <script src="~/Scripts/jquery.signalR-2.0.2.min.js"></script>
    <script src="~/signalr/hubs"></script>
    <script>

        var notify = $.connection.notificationHub;
        notify.client.showNotification = function() {
            $('#result').append("<div class='alert alert-info alert-dismissable'>" +
                "<button type='button' class='close' data-dismiss='alert' aria-hidden='true'>&times;</button>" +
            "رکورد جدیدی هم اکنون ثبت گردید، برای مشاهده آن صفحه را بروزرسانی کنید" + "</div>");
        };
        $.connection.hub.start().done(function() {
            @{
                if (ViewBag.NotifyUsers)
                {
                    <text>notify.server.sendNotification();</text>
                }
            }
        });
    </script>
}
همانطور که در کدهای فوق مشاهده می‌کنید، بعد از اینکه اتصال با موفقیت برقرار شد (درون متد done) شرط چک کردن متغییر NotifyUsers را بررسی کرده‌ایم. یعنی در این حالت اگر مقدار آن true بود، متد درون هاب را فراخوانی کرده‌ایم. در نهایت پیام به یک div با آی‌دی result اضافه شده است.
لازم به ذکر است برای حالت‌های حذف و به‌روزرسانی نیز روال کار به همین صورت می‌باشد.
سورس مثال جاری : ShowAlertSignalR.zip
مطالب
Blazor 5x - قسمت 23 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 3 - کار با نقش‌های کاربران
در قسمت قبل، روش یکپارچه سازی context مربوط به ASP.NET Core Identity را با یک برنامه‌ی Blazor Server، بررسی کردیم. در این قسمت می‌خواهیم محدود کردن دسترسی‌ها را بر اساس نقش‌های کاربران و همچنین کدنویسی مستقیم، بررسی کنیم.


کار با Authentication State از طریق کدنویسی

فرض کنید در کامپوننت HotelRoomUpsert.razor نمی‌خواهیم دسترسی‌ها را به کمک اعمال ویژگی attribute [Authorize]@ محدود کنیم؛ می‌خواهیم اینکار را از طریق کدنویسی مستقیم انجام دهیم:
// ...

@*@attribute [Authorize]*@


@code
{
    [CascadingParameter] public Task<AuthenticationState> AuthenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        var authenticationState = await AuthenticationState;
        if (!authenticationState.User.Identity.IsAuthenticated)
        {
            var uri = new Uri(NavigationManager.Uri);
            NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}");
        }
        // ...
- در اینجا در ابتدا اعمال ویژگی Authorize را کامنت کردیم.
- سپس یک پارامتر ویژه را از نوع CascadingParameter، به نام AuthenticationState تعریف کردیم. این خاصیت از طریق کامپوننت CascadingAuthenticationState که در قسمت قبل به فایل BlazorServer.App\App.razor اضافه کردیم، تامین می‌شود.
- در آخر در روال رویدادگردان OnInitializedAsync، بر اساس آن می‌توان به اطلاعات User جاری وارد شده‌ی به سیستم دسترسی یافت و برای مثال اگر اعتبارسنجی نشده بود، با استفاده از NavigationManager، او را به صفحه‌ی لاگین هدایت می‌کنیم.
- در اینجا روش ارسال آدرس صفحه‌ی فعلی را نیز مشاهده می‌کنید. این امر سبب می‌شود تا پس از لاگین، کاربر مجددا به همین صفحه هدایت شود.

authenticationState، امکانات بیشتری را نیز در اختیار ما قرار می‌دهد؛ برای مثال با استفاده از متد ()authenticationState.User.IsInRole آن می‌توان دسترسی به قسمتی را بر اساس نقش‌های خاصی محدود کرد.


ثبت کاربر ادمین Identity

در ادامه می‌خواهیم دسترسی به کامپوننت‌های مختلف را بر اساس نقش‌ها، محدود کنیم. به همین جهت نیاز است تعدادی نقش و یک کاربر ادمین را به بانک اطلاعاتی برنامه اضافه کنیم. برای اینکار به پروژه‌ی BlazorServer.Common مراجعه کرده و تعدادی نقش ثابت را تعریف می‌کنیم:
namespace BlazorServer.Common
{
    public static class ConstantRoles
    {
        public const string Admin = nameof(Admin);
        public const string Customer = nameof(Customer);
        public const string Employee = nameof(Employee);
    }
}
علت قرار دادن این کلاس در پروژه‌ی Common، نیاز به دسترسی به آن در پروژه‌ی اصلی Blazor Server و همچنین در پروژه‌ی سرویس‌های برنامه‌است. فضای نام این کلاس را نیز در فایل imports.razor_ قرار می‌دهیم.

سپس در فایل BlazorServer.App\appsettings.json، مشخصات ابتدایی کاربر ادمین را ثبت می‌کنیم:
{
  "AdminUserSeed": {
    "UserName": "vahid@dntips.ir",
    "Password": "123@456#Pass",
    "Email": "vahid@dntips.ir"
  }
}
جهت دریافت strongly typed این تنظیمات در برنامه، کلاس معادل AdminUserSeed را به پروژه‌ی Models اضافه می‌کنیم:
namespace BlazorServer.Models
{
    public class AdminUserSeed
    {
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }
    }
}
که به صورت زیر در فایل BlazorServer.App\Startup.cs به سیستم تزریق وابستگی‌های برنامه معرفی می‌شود:
namespace BlazorServer.App
{
    public class Startup
    {

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions<AdminUserSeed>().Bind(Configuration.GetSection("AdminUserSeed"));
            // ...
اکنون می‌توان سرویس افزودن نقش‌ها و کاربر ادمین را در پروژه‌ی BlazorServer.Services تکمیل کرد:
using System;
using System.Linq;
using System.Threading.Tasks;
using BlazorServer.Common;
using BlazorServer.DataAccess;
using BlazorServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace BlazorServer.Services
{
    public class IdentityDbInitializer : IIdentityDbInitializer
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly UserManager<IdentityUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;
        private readonly IOptions<AdminUserSeed> _adminUserSeedOptions;

        public IdentityDbInitializer(
            ApplicationDbContext dbContext,
            UserManager<IdentityUser> userManager,
            RoleManager<IdentityRole> roleManager,
            IOptions<AdminUserSeed> adminUserSeedOptions)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager));
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _adminUserSeedOptions = adminUserSeedOptions ?? throw new ArgumentNullException(nameof(adminUserSeedOptions));
        }

        public async Task SeedDatabaseWithAdminUserAsync()
        {
            if (_dbContext.Roles.Any(role => role.Name == ConstantRoles.Admin))
            {
                return;
            }

            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Admin));
            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Customer));
            await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Employee));

            await _userManager.CreateAsync(
                new IdentityUser
                {
                    UserName = _adminUserSeedOptions.Value.UserName,
                    Email = _adminUserSeedOptions.Value.Email,
                    EmailConfirmed = true
                },
                _adminUserSeedOptions.Value.Password);

            var user = await _dbContext.Users.FirstAsync(u => u.Email == _adminUserSeedOptions.Value.Email);
            await _userManager.AddToRoleAsync(user, ConstantRoles.Admin);
        }
    }
}
این سرویس، با استفاده از دو سرویس توکار UserManager و RoleManager کتابخانه‌ی Identity، ابتدا سه نقش ادمین، مشتری و کارمند را ثبت می‌کند. سپس بر اساس اطلاعات AdminUserSeed تعریف شده، کاربر ادمین را ثبت می‌کند. البته این کاربر در این مرحله، یک کاربر معمولی بیشتر نیست. در مرحله‌ی بعد است که با انتساب نقش ادمین به او، می‌توان کاربر او را بر اساس این نقش ویژه، شناسایی کرد. کلاس‌های IdentityRole و IdentityUser، کلاس‌های پایه‌ی نقش‌ها و کاربران کتابخانه‌ی Identity هستند.

پس از تعریف این سرویس، نیاز است آن‌را به سیستم تزریق وابستگی‌های برنامه اضافه کرد:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IIdentityDbInitializer, IdentityDbInitializer>();
            // ...
مرحله‌ی آخر، اعمال و اجرای سرویس IIdentityDbInitializer، در زمان آغاز برنامه‌است و محل توصیه شده‌ی آن، در متد Main برنامه‌ی اصلی، پیش از اجرای برنامه‌است. به همین جهت، نیاز است BlazorServer.DataAccess\Utils\MigrationHelpers.cs را به صورت زیر ایجاد کرد:
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Polly;

namespace BlazorServer.DataAccess.Utils
{
    public static class MigrationHelpers
    {
        public static void MigrateDbContext<TContext>(
                this IServiceProvider serviceProvider,
                Action<IServiceProvider> postMigrationAction
                ) where TContext : DbContext
        {
            using var scope = serviceProvider.CreateScope();
            var scopedServiceProvider = scope.ServiceProvider;
            var logger = scopedServiceProvider.GetRequiredService<ILogger<TContext>>();
            using var context = scopedServiceProvider.GetService<TContext>();

            logger.LogInformation($"Migrating the DB associated with the context {typeof(TContext).Name}");

            var retry = Policy.Handle<Exception>().WaitAndRetry(new[]
                {
                    TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(15)
                });

            retry.Execute(() =>
                {
                    context.Database.Migrate();
                    postMigrationAction(scopedServiceProvider);
                });

            logger.LogInformation($"Migrated the DB associated with the context {typeof(TContext).Name}");
        }
    }
}
در مورد این متد و استفاده از Polly جهت تکرار عملیات شکست خورده پیشتر در مطلب «اضافه کردن سعی مجدد به اجرای عملیات Migration در EF Core» بحث شده‌است.
کار متد الحاقی فوق، دریافت یک IServiceProvider است که به سرویس‌های اصلی برنامه اشاره می‌کند. سپس بر اساس آن، یک Scoped ServiceProvider را ایجاد می‌کند تا درون آن بتوان با Context برنامه در طی مدت کوتاهی کار کرد و در پایان آن، سرویس‌های ایجاد شده را Dispose کرد.
در این متد ابتدا Database.Migrate فراخوانی می‌شود تا اگر مرحله‌ای از Migrations برنامه هنوز به بانک اطلاعاتی اعمال نشده، کار اجرا و اعمال آن انجام شود. سپس یک متد سفارشی را از فراخوان دریافت کرده و اجرا می‌کند. برای مثال توسط آن می‌توان IIdentityDbInitializer در فایل BlazorServer.App\Program.cs به صوت زیر فراخوانی کرد:
public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    host.Services.MigrateDbContext<ApplicationDbContext>(
     scopedServiceProvider =>
            scopedServiceProvider.GetRequiredService<IIdentityDbInitializer>()
                                 .SeedDatabaseWithAdminUserAsync()
                                 .GetAwaiter()
                                 .GetResult()
    );
    host.Run();
}
تا اینجا اگر برنامه را اجرا کنیم، سه نقش پیش‌فرض، به بانک اطلاعاتی برنامه اضافه شده‌اند:


و همچنین کاربر پیش‌فرض سیستم را نیز می‌توان مشاهده کرد:


که نقش ادمین و کاربر پیش‌فرض، به این صورت به هم مرتبط شده‌اند (یک رابطه‌ی many-to-many برقرار است):



محدود کردن دسترسی کاربران بر اساس نقش‌ها

پس از ایجاد کاربر ادمین و تعریف نقش‌های پیش‌فرض، اکنون محدود کردن دسترسی به کامپوننت‌های برنامه بر اساس نقش‌ها، ساده‌است. برای این منظور فقط کافی است لیست نقش‌های مدنظر را که می‌توانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننت‌ها معرفی کرد:
@attribute [Authorize(Roles = ConstantRoles.Admin)]
و یا از طریق کدنویسی به صورت زیر نیز قابل اعمال است:
    protected override async Task OnInitializedAsync()
    {
        var authenticationState = await AuthenticationState;
        if (!authenticationState.User.Identity.IsAuthenticated ||
            !authenticationState.User.IsInRole(ConstantRoles.Admin))
        {
            var uri = new Uri(NavigationManager.Uri);
            NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}");
        }


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-23.zip
مطالب
ثبت جزئیات استثناهای Entity framework توسط ELMAH
در حین بروز استثناهای Entity framework، می‌توان توسط ابزارهای Logging متنوعی مانند ELMAH، جزئیات متداول آن‌ها را برای بررسی‌های آتی ذخیره کرد. اما این جزئیات فاقد SQL نهایی تولیدی و همچنین پارامترهای ورودی توسط کاربر یا تنظیم شده توسط برنامه هستند. برای اینکه بتوان این جزئیات را نیز ثبت کرد، می‌توان یک IDbCommandInterceptor جدید را طراحی کرد.


کلاس EfExceptionsInterceptor

در اینجا نمونه‌ای از یک پیاده سازی اینترفیس IDbCommandInterceptor را مشاهده می‌کنید. همچنین طراحی یک متد عمومی که می‌تواند به جزئیات SQL نهایی و پارامترهای آن دسترسی داشته باشد، در اینترفیس IEfExceptionsLogger ذکر شده‌است.
public interface IEfExceptionsLogger
{
    void LogException<TResult>(DbCommand command,
        DbCommandInterceptionContext<TResult> interceptionContext);
}

using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
 
namespace ElmahEFLogger
{
    public class EfExceptionsInterceptor : IDbCommandInterceptor
    {
        private readonly IEfExceptionsLogger _efExceptionsLogger;
 
        public EfExceptionsInterceptor(IEfExceptionsLogger efExceptionsLogger)
        {
            _efExceptionsLogger = efExceptionsLogger;
        }
 
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
 
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            _efExceptionsLogger.LogException(command, interceptionContext);
        }
    }
}


تهیه یک پیاده سازی سفارشی از IEfExceptionsLogger توسط ELMAH

اکنون که ساختار کلی IDbCommandInterceptor سفارشی برنامه مشخص شد، می‌توان پیاده سازی خاصی از آن‌را جهت استفاده از ELMAH به نحو ذیل ارائه داد:
using System;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using Elmah;
 
namespace ElmahEFLogger.CustomElmahLogger
{
    public class ElmahEfExceptionsLogger : IEfExceptionsLogger
    {
        /// <summary>
        /// Manually log errors using ELMAH
        /// </summary>
        public void LogException<TResult>(DbCommand command,
            DbCommandInterceptionContext<TResult> interceptionContext)
        {
            var ex = interceptionContext.OriginalException;
            if (ex == null)
                return;
 
            var sqlData = CommandDumper.LogSqlAndParameters(command, interceptionContext);
            var contextualMessage = string.Format("{0}{1}OriginalException:{1}{2} {1}", sqlData, Environment.NewLine, ex);
 
 
            if (!string.IsNullOrWhiteSpace(contextualMessage))
            {
                ex = new Exception(contextualMessage, new ElmahEfInterceptorException(ex.Message));
            }
 
            try
            {
                ErrorSignal.FromCurrentContext().Raise(ex);
            }
            catch
            {
                ErrorLog.GetDefault(null).Log(new Error(ex));
            }
        }
    }
}
در اینجا شیء Command به همراه SQL نهایی تولید و پارامترهای مرتبط است. همچنین interceptionContext.OriginalException جزئیات عمومی استثنای رخ داده را به همراه دارد. می‌توان این اطلاعات را پس از اندکی نظم بخشیدن، به متد Raise مربوط به ELMAH ارسال کرد تا جزئیات بیشتری از استثنای رخ داده شده، در لاگ‌های آن ظاهر شوند.


استفاده از ElmahEfExceptionsLogger جهت طراحی یک Interceptor عمومی

   public class ElmahEfInterceptor : EfExceptionsInterceptor
    {
        public ElmahEfInterceptor()
            : base(new ElmahEfExceptionsLogger())
        { }
    }
برای استفاده از ElmahEfExceptionsLogger و تهیه یک Interceptor عمومی، می‌توان با ارث بری از کلاس Interceptor ابتدای بحث شروع کرد و وهله‌ای از ElmahEfExceptionsLogger را به سازنده‌ی آن تزریق نمود (یکی از چندین روش ممکن). سپس برای استفاده از آن کافی است به ابتدای متد Application_Start فایل Global.asax.cs مراجعه و در ادامه سطر ذیل را اضافه نمود:
 DbInterception.Add(new ElmahEfInterceptor());

پس از آن جزئیات کلیه استثناهای EF در لاگ‌های نهایی ELMAH به نحو ذیل ظاهر خواهند شد:




کدهای کامل این پروژه را از اینجا می‌توانید دریافت کنید:
ElmahEFLogger
مطالب
به روز رسانی فیلدهای XML در SQL Server - قسمت دوم

قسمت اول را در این آدرس می‌توانید مطالعه نمائید.

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

DECLARE @tblTest AS TABLE (xmlField XML)

INSERT INTO @tblTest
(
xmlField
)
VALUES
(
'<Sample>
<Node1>Value1</Node1>
<Node2>Value2</Node2>
<Node3>OldValue</Node3>
</Sample>'
)

DECLARE @Name NVARCHAR(50)
SELECT @Name = 'Vahid'

UPDATE @tblTest
SET xmlField.modify(
'insert element Node4 {sql:variable("@Name")} as last into
(/Sample)[1]'
)

SELECT tt.xmlField
FROM @tblTest tt

که حاصل آن (افزوده شدن یک المان جدید به نام Node4 بر اساس مقدار متغیر Name در انتهای لیست) به صورت زیر می‌باشد:

<Sample>
<Node1>Value1</Node1>
<Node2>Value2</Node2>
<Node3>OldValue</Node3>
<Node4>Vahid</Node4>
</Sample>

سؤال 1 :
اگر بخواهیم نام Node4 نیز متغیر باشد به چه صورتی باید مساله را حل کرد؟
در این حالت باید از کوئری‌های داینامیک استفاده کرد. باید یک رشته را ایجاد (کل عبارت update باید یک رشته شود) و سپس از دستور exec کمک گرفت و البته باید دقت داشت در این حالت کار encoding کارکترهای غیرمجاز در XML را باید خودمان انجام دهیم.

سؤال 2:
اگر بخواهیم نام نودها و مقادیر آن‌ها را به صورت یک جدول معمولی بازگشت دهیم به چه صورتی باید عمل کرد؟

DECLARE @XML AS XML

SELECT @XML = tt.xmlField
FROM @tblTest tt

SELECT t.c.value('local-name(..)', 'varchar(max)') AS ParentNodeName,
t.c.value('local-name(.)', 'varchar(max)') AS NodeName,
t.c.value('text()[1]', 'varchar(max)') AS NodeText

FROM @XML.nodes('/*/*') AS t(c)

که پس از اجرای آن خواهیم داشت:

ParentNodeName - NodeName - NodeText
Sample Node1 Value1
Sample Node2 Value2
Sample Node3 OldValue
Sample Node4 Vahid

مطالب
سطح دوم کش در Nhibernate 4
 Second Level Cache In NHibernate 4

همان طور که می‌دانیم کش در NHibernate در دو سطح قابل انجام می‌باشد:
- کش سطح اول که همان اطلاعات سشن، در تراکنش جاری هست و با اتمام تراکنش، محتویات آن خالی می‌گردد. این سطح همیشه فعال می‌باشد و در این بخش قصد پرداختن به آن را نداریم.
- کش سطح دوم که بین همه‌ی تراکنش‌ها مشترک و پایدار می‌باشد. این مورد به طور پیش فرض فعال نمی‌باشد و می‌بایستی از طریق کانفیگ برنامه فعال گردد.

جهت پیاده سازی باید قسمت‌های ذیل را در کانفیگ مربوط به NHibernate اضافه نمود:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
         <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate"/>
          <section name="syscache" type="NHibernate.Caches.SysCache.SysCacheSectionHandler, NHibernate.Caches.SysCache" requirePermission="false"/>
</configSections>

<hibernate-configuration  xmlns="urn:nhibernate-configuration-2.2" >
<session-factory>
        <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
      <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
      <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
      <property name="connection.connection_string_name">LocalSqlServer</property>
      <property name="show_sql">false</property>
      <property name="hbm2ddl.keywords">none</property>
      <property name="cache.use_second_level_cache">true</property>
      <property name="cache.use_query_cache" >true</property>      
      <property name="cache.provider_class">NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache</property>
</session-factory>
</hibernate-configuration>

<syscache>
       <cache region="LongExpire" expiration="3600" priority="5"/>
       <cache region="ShortExpire" expiration="600" priority="3"/>    
</syscache>
</configuration>

پیاده سازی Caching در NHibernate در سه مرحله قابل اعمال می‌باشد :
- کش در سطح Load موجودیت‌های مستقل
- کش در سطح Load موجودیت‌های وابسته Bag , List , Set , …
- کش در سطح Query ها

Providerهای مختلفی برای اعمال و پیاده سازی آن وجود دارند که معروف‌ترین آن‌ها SysCache بوده و ما هم از همان استفاده می‌نماییم.
- مدت زمان پیش فرض کش سطح دوم، ۵ دقیقه می‌باشد و در صورت نیاز به تغییر آن، باید تگ مربوط به SysCache را تنظیم نمود. محدودیتی در تعریف تعداد متفاوتی از زمان‌های خالی شدن کش وجود ندارد و مدت زمان آن بر حسب ثانیه مشخص می‌گردد. نحوه‌ی تخصیص زمان انقضای کش به هر مورد بدین شکل صورت می‌گیرد که region مربوطه در آن معرفی می‌گردد.

جهت اعمال کش در سطح Load موجودیت‌های مستقل، علاوه بر کانفیگ اصلی، می‌بایستی کدهای زیر را به Mapping موجودیت اضافه نمود مانند :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Core.Domain" namespace="Core.Domain.Model">
  <class name="Organization" table="Core_Enterprise_Organization">
    <cache usage="nonstrict-read-write" region="ShortExpire"/>                  
    <id name="Id" >
      <generator/>
    </id>
    <version name="Version"/>
    <property name="Title" not-null="true" unique="true"/>
    <property name="Code" not-null="true" unique="true"/>
  </class>
</hibernate-mapping>
این مورد برای موجودیت‌های وابسته هم نیز صادق است؛ به شکل کد زیر:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Core.Domain" namespace="Core.Domain.Model">
  <class name="Party" table="Core_Enterprise_Party">  
    <id name="Id" >
      <generator />
    </id>
    <version name="Version"/>
    <property name="Username" unique="true"/>
    <property name="DisplayName" not-null="true"/>
    <bag name="PartyGroups" inverse="true" table="Core_Enterprise_PartyGroup" cascade="all-delete-orphan">      
      <cache usage="nonstrict-read-write" region="ShortExpire"/>
      <key column="Party_id_fk"/>
      <one-to-many/>
    </bag>
  </class>
</hibernate-mapping>

ویژگی usage نیز با مقادیر زیر قابل تنظیم است:

- read-only : این مورد جهت موجودیت‌هایی مناسب است که امکان بروزرسانی آن‌ها توسط کاربر وجود ندارد. این مورد بهترین کارآیی را دارد.
- read-write : این مورد جهت موجودیت‌هایی بکار می‌رود که امکان بروزرسانی آن‌ها توسط کاربر وجود دارد. این مورد کارآیی پایین‌تری دارد.
- nonstrict-read-write : این مورد جهت موجودیت‌هایی مناسب می‌باشد که امکان بروزرسانی آن‌ها توسط کاربر وجود دارد؛ اما امکان همزمان بروز کردن آن‌ها توسط چندین کاربر وجود نداشته باشد. این مورد در قیاس، کارآیی بهتر و بهینه‌تری نسبت به مورد قبل دارد.

جهت اعمال کش در کوئری‌ها نیز باید مراحل خاص خودش را انجام داد. به عنوان مثال برای یک کوئری Linq به شکل زیر خواهیم داشت:
public IList<Organization> Search(QueryOrganizationDto dto)
{
     var q = SessionInstance.Query<Organization>();            

     if (!String.IsNullOrEmpty(dto.Title))
          q = q.Where(x => x.Title.Contains(dto.Title));

     if (!String.IsNullOrEmpty(dto.Code))
          q = q.Where(x => x.Code.Contains(dto.Code));

     q = q.OrderBy(x => x.Title);

      q = q.CacheRegion("ShortExpire").Cacheable(); 
     return q.ToList();
}
در واقع کد اضافه شده به کوئری بالا، قابل کش بودن کوئری را مشخص می‌نماید و مدت زمان کش شدن آن نیز از طریق کانفیگ مربوطه مشخص می‌گردد. این نکته را هم درنظر داشته باشید که کش در سطح کوئری برای کوئری‌هایی که دقیقا مثل هم هستند اعمال می‌گردد و با افزوده یا کاسته شدن یک شرط جدید به کوئری، مجددا کوئری سمت پایگاه داده ارسال می‌گردد.


در انتها لینک‌های زیر هم جهت مطالعه بیشتر پیشنهاد می‌گردند:
http://www.nhforge.org/doc/nh/en/index.html#performance-cache-readonly
http://nhforge.org/blogs/nhibernate/archive/2009/02/09/quickly-setting-up-and-using-nhibernate-s-second-level-cache.aspx
http://www.klopfenstein.net/lorenz.aspx/using-syscache-as-secondary-cache-in-nhibernate
http://stackoverflow.com/questions/1837651/hibernate-cache-strategy
مطالب دوره‌ها
ارسال کوئری استرینگ‌ها به همراه عملیات Ajax در jQuery
فرض کنید اطلاعات صفحه‌ای بر اساس کوئری استرینگ دریافتی رندر می‌شوند. مثلا یک گرید و یا حتی یک صفحه ویرایش اطلاعات. در حین فراخوانی متد Ajax در jQuery اطلاعات کوئری استرینگ‌های موجود به سرور ارسال نخواهند شد و صرفا اطلاعاتی که در پارامتر data آن صریحا ذکر می‌شوند، به سرور ارسال می‌گردند.
برای رفع این نقیصه با استفاده از قطعه کد زیر می‌توان کلیه کوئری استرینگ‌های صفحه را یافت و به شیءایی به نام urlParams به صورت خاصیت اضافه کرد:
var urlParams;
(window.onpopstate = function () {
    var match,
        pl     = /\+/g,  // Regex for replacing addition symbol with a space
        search = /([^&=]+)=?([^&]*)/g,
        decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
        query  = window.location.search.substring(1);

urlParams = {};
    while (match = search.exec(query))
       urlParams[decode(match[1])] = decode(match[2]);
})();
سپس اگر قسمت ارسال اطلاعات متد ajax در حال فراخوانی چنین شکلی را داشته باشد:
      $.ajax({
                    type: "POST",
                    url: "@sortUrl",
                    data: JSON.stringify({ items: items, surveyId: surveyId }),
نیاز خواهیم داشت تا urlParams را با آن یکی کنیم. اینکار نیز توسط متد extend خود jQuery قابل انجام است:
function jsonConcat(defaults, options) {
 /* merge defaults and options, without modifying defaults */
 return $.extend({}, defaults, options);
}
بنابراین در نهایت قسمت ارسال اطلاعات ما برای الحاق کلیه کوئری استرینگ‌های صفحه چنین شکلی را خواهد یافت:
    var ajaxData = { items: items, surveyId: surveyId };
    $.ajax({
        type: "POST",
        url: "@sortUrl",
        data: JSON.stringify(jsonConcat(ajaxData,urlParams)),
و در سمت سرور امضای متدی که اطلاعات به آن ارسال می‌شوند، در این مثال خاص شامل items و surveyId به همراه نام کوئری استرینگ‌های مدنظر می‌تواند باشد و اطلاعات به صورت خودکار به آن‌ها بایند خواهند شد.
نظرات مطالب
مونیتور کردن میزان مصرف CPU در اس کیوال سرور
باید از روش‌های دیگری استفاده کرد. برای مثال در مطلب «گزارشگیری از تاریخچه‌ی اجرای کوئری‌ها در SQL Server» نمونه‌ای از آن مطرح شده‌است و یا نمونه‌ای دیگر در اینجا