مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور
یکی از نیازهای مهم هر برنامه‌ای، امکانات گزارشگیری و نمایش لیستی از اطلاعات است. به همین جهت در طی چند قسمت، قصد داریم یک گرید ساده را به همراه امکانات نمایش، صفحه بندی و مرتب سازی اطلاعات، تنها به کمک امکانات توکار Angular و ASP.NET Core تهیه کنیم.




تهیه مقدمات سمت سرور

مدلی که در تصویر فوق نمایش داده شده‌است، در سمت سرور چنین ساختاری را دارد:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Product
    {
        public int ProductId { set; get; }
        public string ProductName { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}

همچنین یک منبع ساده درون حافظه‌ای را نیز جهت بازگشت 1500 محصول تهیه کرده‌ایم. علت اینجا است که ساختار نهایی اطلاعات آن شبیه به ساختار اطلاعات حاصل از ORMها باشد و همچنین به سادگی قابلیت اجرا و بررسی را داشته باشد:
using System.Collections.Generic;

namespace AngularTemplateDrivenFormsLab.Models
{
    public static class ProductDataSource
    {
        private static readonly IList<Product> _cachedItems;
        static ProductDataSource()
        {
            _cachedItems = createProductsDataSource();
        }

        public static IList<Product> LatestProducts
        {
            get { return _cachedItems; }
        }

        private static IList<Product> createProductsDataSource()
        {
            var list = new List<Product>();
            for (var i = 0; i < 1500; i++)
            {
                list.Add(new Product
                {
                    ProductId = i + 1,
                    ProductName = "نام " + (i + 1),
                    IsAvailable = (i % 2 == 0),
                    Price = 1000 + i
                });
            }
            return list;
        }
    }
}


مشخص کردن قرارداد اطلاعات دریافتی از سمت کلاینت

زمانیکه کلاینت Angular برنامه، اطلاعاتی را به سمت سرور ارسال می‌کند، یک چنین ساختاری را دریافت خواهیم کرد:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
درخواست، به یک اکشن متد مشخص ارسال شده‌است و حاوی یک سری کوئری استرینگ مشخص کننده‌ی نام خاصیت یا فیلدی که قرار است مرتب سازی بر اساس آن صورت گیرد، صعودی و نزولی بودن این مرتب سازی، شماره صفحه‌ی درخواستی و تعداد آیتم‌های در هر صفحه، می‌باشد.
بنابراین اینترفیسی را دقیقا بر اساس نام کلیدهای همین کوئری استرینگ‌ها تهیه می‌کنیم:
    public interface IPagedQueryModel
    {
        string SortBy { get; set; }
        bool IsAscending { get; set; }
        int Page { get; set; }
        int PageSize { get; set; }
    }


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

با تعریف این اینترفیس چند هدف را دنبال خواهیم کرد:
الف) استاندارد سازی نام خواصی که مدنظر هستند و اعمال یک دست آن‌ها به ViewModelهایی که قرار است از سمت کلاینت دریافت شوند:
    public class ProductQueryViewModel : IPagedQueryModel
    {
        // ... other properties ...

        public string SortBy { get; set; }
        public bool IsAscending { get; set; }
        public int Page { get; set; }
        public int PageSize { get; set; }
    }
برای مثال در اینجا یک ViewModel مخصوص Product را ایجاد کرده‌ایم که می‌تواند شامل یک سری فیلد دیگر نیز باشد. اما یک سری خواص مرتب سازی و صفحه بندی آن، یک دست و مشخص هستند.

ب) امکان استفاده‌ی از این قرارداد در متدهای کمکی که نوشته خواهند شد:
    public static class IQueryableExtensions
    {
        public static IQueryable<T> ApplyPaging<T>(
          this IQueryable<T> query, IPagedQueryModel model)
        {
            if (model.Page <= 0)
            {
                model.Page = 1;
            }

            if (model.PageSize <= 0)
            {
                model.PageSize = 10;
            }

            return query.Skip((model.Page - 1) * model.PageSize).Take(model.PageSize);
        }
    }
در حین ارائه‌ی اطلاعات نهایی صفحه بندی شده به کلاینت، همیشه یک قسمت Skip و Take وجود خواهند داشت. این متدها نیز باید بر اساس یک سری خاصیت و مقدار مشخص، مانند صفحه شماره صفحه‌ی جاری و تعداد ردیف‌های در هر صفحه کار کنند. اکنون که قرارداد IPagedQueryModel را تهیه کرده‌ایم و ViewModel ما نیز آن‌را پیاده سازی می‌کند، مطمئن خواهیم بود که می‌توان به سادگی به این خواص دسترسی یافت و همچنین این کد تکراری صفحه بندی را توانسته‌ایم به یک متد الحاقی کمکی منتقل و حجم کدهای نهایی را کاهش دهیم.
همچنین دراینجا بجای صدور استثناء در حین دریافت مقادیر غیرمعتبر شماره صفحه یا تعداد ردیف‌های هر صفحه، از حالت «بخشنده» بجای حالت «تدافعی» استفاده شده‌است. برای مثال در حالت «بخشنده» اگر شماره صفحه منفی بود، همان صفحه‌ی اول اطلاعات نمایش داده می‌شود؛ بجای صدور یک استثناء (یا حالت «تدافعی و defensive programming»).


کاهش کدهای تکراری مرتب سازی اطلاعات در سمت سرور

همانطور که عنوان شد، از سمت کلاینت، چنین لینکی را دریافت خواهیم کرد:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
در اینجا، هربار sortBy و isAscending می‌توانند متفاوت باشند و در نهایت به یک چنین کدهایی خواهیم رسید:
if(model.SortBy == "f1")
{
   query = !model.IsAscending ? query.OrderByDescending(x => x.F1) : query.OrderBy(x => x.F1);
}
امکان نوشتن این نوع کوئری‌ها توسط قابلیت تعریف زنجیره‌وار کوئری‌های LINQ میسر است و در نهایت زمانیکه ToList نهایی فراخوانی می‌شود، آنگاه است که کوئری SQL معادل این‌ها تولید خواهد شد.
اما در این حالت نیاز است به ازای تک تک فیلدها، یکبار if/else یافتن فیلد و سپس بررسی صعودی و نزولی بودن آن‌ها صورت گیرد که در نهایت ظاهر خوشایندی را نخواهند داشت.

یک نمونه از مزیت‌های تهیه‌ی قرارداد IPagedQueryModel را در حین نوشتن متد ApplyPaging مشاهده کردید. نمونه‌ی دیگر آن کاهش کدهای تکراری مرتب سازی اطلاعات است:
namespace AngularTemplateDrivenFormsLab.Utils
{
    public static class IQueryableExtensions
    {
        public static IQueryable<T> ApplyOrdering<T>(
          this IQueryable<T> query,
          IPagedQueryModel model,
          IDictionary<string, Expression<Func<T, object>>> columnsMap)
        {
            if (string.IsNullOrWhiteSpace(model.SortBy) || !columnsMap.ContainsKey(model.SortBy))
            {
                return query;
            }

            if (model.IsAscending)
            {
                return query.OrderBy(columnsMap[model.SortBy]);
            }
            else
            {
                return query.OrderByDescending(columnsMap[model.SortBy]);
            }
        }
    }
}
در اینجا متد الحاقی ApplyOrdering، کار دریافت و بررسی خواص مدنظر را از طریق یک دیکشنری انجام می‌دهد و مابقی کدهای تکراری نوشته شده، حذف خواهند شد. برای نمونه، مثالی از نحوه‌ی استفاده‌ی از این متد الحاقی را در ذیل مشاهده می‌کنید:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
query = query.ApplyOrdering(queryModel, columnsMap);
ابتدا نگاشتی بین خواص رشته‌ای دریافتی از سمت کلاینت، با خواص شیء Product برقرار شده‌است. سپس این نگاشت به متد ApplyOrdering ارسال شده‌است. به این ترتیب از نوشتن تعداد زیادی if/else یا switch بر اساس خاصیت SortBy بی‌نیاز شده‌ایم، حجم کدهای نهایی تولیدی کاهش پیدا می‌کنند و برنامه نیز خواناتر می‌شود.


تهیه قرارداد ساختار اطلاعات بازگشتی از سمت سرور به سمت کلاینت

تا اینجا قرارداد اطلاعات دریافتی از سمت کلاینت را مشخص کردیم. همچنین از آن برای ساده سازی عملیات مرتب سازی و صفحه بندی اطلاعات کمک گرفتیم. در ادامه نیاز است مشخص کنیم چگونه می‌خواهیم این اطلاعات را به سمت کلاینت ارسال کنیم:
using System.Collections.Generic;

namespace AngularTemplateDrivenFormsLab.Models
{
    public class PagedQueryResult<T>
    {
        public int TotalItems { get; set; }
        public IEnumerable<T> Items { get; set; }
    }
}
عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتم‌های تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیف‌های صفحه‌ی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی می‌تواند باشد، بهتر است آن‌را جنریک تعریف کنیم.


پایان کار بازگشت اطلاعات سمت سرور با تهیه اکشن متد GetPagedProducts

در اینجا اکشن متدی را مشاهده می‌کنید که اطلاعات نهایی مرتب سازی شده و صفحه بندی شده را بازگشت می‌دهد:
    [Route("api/[controller]")]
    public class ProductController : Controller
    {
        [HttpGet("[action]")]
        public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
        {
            var pagedResult = new PagedQueryResult<Product>();

            var query = ProductDataSource.LatestProducts
                                         .AsQueryable();

            //TODO: Apply Filtering ... .where(p => p....) ...

            var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
            query = query.ApplyOrdering(queryModel, columnsMap);

            pagedResult.TotalItems = query.Count();
            query = query.ApplyPaging(queryModel);
            pagedResult.Items = query.ToList();
            return pagedResult;
        }
    }
توضیحات تکمیلی

امضای این اکشن متد، شامل دو مورد مهم است:
 public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
الف) ViewModel ایی که پیاده سازی کننده‌ی IPagedQueryModel است. به این ترتیب می‌توان به ساختار استانداردی از مقادیر مورد نیاز برای صفحه بندی و مرتب سازی رسید و همچنین این ViewModel می‌تواند حاوی خواص اضافی ویژه‌ی خود نیز باشد.
ب) خروجی آن از نوع PagedQueryResult است که در مورد آن توضیح داده شد. بنابراین باید به همراه تعداد کل رکوردهای جدول محصولات و همچنین تنها آیتم‌های صفحه‌ی جاری درخواستی باشد.

در ابتدای کار، دسترسی به منبع داده‌ی درون حافظه‌ای ابتدای برنامه را مشاهده می‌کنید. برای اینکه کارکرد آن‌را شبیه به کوئری‌های ORMها کنیم، یک AsQueryable نیز به انتهای آن اضافه شده‌است.
 var query = ProductDataSource.LatestProducts
  .AsQueryable();

//TODO: Apply Filtering ... .where(p => p....) ...
اینجا دقیقا جائی است که در صورت نیاز می‌توان کار فیلتر اطلاعات و اعمال متد where را انجام داد.

پس از مشخص شدن منبع داده و فیلتر آن در صورت نیاز، اکنون نوبت به مرتب سازی اطلاعات است:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
query = query.ApplyOrdering(queryModel, columnsMap);
توضیحات این مورد را پیشتر مطالعه کردید و هدف از آن، تهیه یک نگاشت ساده‌ی بین خواص رشته‌ای رسیده‌ی از سمت کلاینت به خواص مدل متناظر با آن است و سپس ارسال آن‌ها به همراه queryModel دریافتی از کاربر، برای اعمال مرتب سازی نهایی.

در آخر مطابق ساختار PagedQueryResult بازگشتی، ابتدا تعداد کل آیتم‌های منبع داده محاسبه شده‌است و سپس صفحه بندی به آن اعمال گردیده‌است. این ترتیب نیز مهم است و گرنه TotalItems دقیقا به همان تعداد ردیف‌های صفحه‌ی جاری محاسبه می‌شود:
var pagedResult = new PagedQueryResult<Product>();
pagedResult.TotalItems = query.Count();
query = query.ApplyPaging(queryModel);
pagedResult.Items = query.ToList();
return pagedResult;


در قسمت بعد، نحوه‌ی نمایش این اطلاعات را در سمت Angular بررسی خواهیم کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا
برای افزودن لیست Claims کاربر موجود در بانک اطلاعاتی به لیست Claims کاربر وارد شده‌ی توسط اکتیو دایرکتوری، باید از یک IClaimsTransformation سفارشی استفاده کنید تا این نگاشت را انجام دهد (نمونه‌اش در مطلب « سفارشی سازی ASP.NET Core Identity - قسمت چهارم - User Claims » به نحو دیگری استفاده شده‌است):
public class ApplicationClaimsTransformation : IClaimsTransformation
{
}
پیاده سازی کامل آن در اینجا
و برای ثبت آن:
services.AddScoped<IClaimsTransformation, ApplicationClaimsTransformation>();
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت چهارم - به روز رسانی خودکار توکن‌ها
در قسمت قبل، عملیات ورود به سیستم و خروج از آن‌را تکمیل کردیم. پس از ورود شخص به سیستم، هربار انقضای توکن دسترسی او، سبب خواهد شد تا وقفه‌ای در کار جاری کاربر، جهت لاگین مجدد صورت گیرد. برای این منظور، قسمتی از مطالب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»  و یا «پیاده سازی JSON Web Token با ASP.NET Web API 2.x» به تولید refresh_token در سمت سرور اختصاص دارد که از نتیجه‌ی آن در اینجا استفاده خواهیم کرد. عملیات به روز رسانی خودکار توکن دسترسی (access_token در اینجا) سبب خواهد شد تا کاربر پس از انقضای آن، نیازی به لاگین دستی مجدد نداشته باشد. این به روز رسانی در پشت صحنه و به صورت خودکار صورت می‌گیرد. refresh_token یک guid است که به سمت سرور ارسال می‌شود و برنامه‌ی سمت سرور، پس از تائید آن (بررسی صحت وجود آن در بانک اطلاعاتی و سپس واکشی اطلاعات کاربر متناظر با آن)، یک access_token جدید را صادر می‌کند.


ایجاد یک تایمر برای مدیریت دریافت و به روز رسانی توکن دسترسی

در مطلب «ایجاد تایمرها در برنامه‌های Angular» با روش کار با تایمرهای reactive آشنا شدیم. در اینجا قصد داریم از این امکانات جهت پیاده سازی به روز کننده‌ی خودکار access_token استفاده کنیم. در مطلب «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی»، زمان انقضای توکن را به کمک کتابخانه‌ی jwt-decode، از آن استخراج کردیم:
  getAccessTokenExpirationDateUtc(): Date {
    const decoded = this.getDecodedAccessToken();
    if (decoded.exp === undefined) {
      return null;
    }
    const date = new Date(0); // The 0 sets the date to the epoch
    date.setUTCSeconds(decoded.exp);
    return date;
  }
اکنون از این زمان در جهت تعریف یک تایمر خود متوقف شونده استفاده می‌کنیم:
  private refreshTokenSubscription: Subscription;

  scheduleRefreshToken() {
    if (!this.isLoggedIn()) {
      return;
    }

    this.unscheduleRefreshToken();

    const expiresAtUtc = this.getAccessTokenExpirationDateUtc().valueOf();
    const nowUtc = new Date().valueOf();
    const initialDelay = Math.max(1, expiresAtUtc - nowUtc);
    console.log("Initial scheduleRefreshToken Delay(ms)", initialDelay);
    const timerSource$ = Observable.timer(initialDelay);
    this.refreshTokenSubscription = timerSource$.subscribe(() => {
      this.refreshToken();      
    });
  }

  unscheduleRefreshToken() {
    if (this.refreshTokenSubscription) {
      this.refreshTokenSubscription.unsubscribe();
    }
  }
کار متد scheduleRefreshToken، شروع تایمر درخواست توکن جدید است.
- در ابتدا بررسی می‌کنیم که آیا کاربر لاگین کرده‌است یا خیر؟ آیا اصلا دارای توکنی هست یا خیر؟ اگر خیر، نیازی به شروع این تایمر نیست.
-  سپس تایمر قبلی را در صورت وجود، خاتمه می‌دهیم.
- در مرحله‌ی بعد، کار محاسبه‌ی میزان زمان تاخیر شروع تایمر Observable.timer را انجام می‌دهیم. پیشتر زمان انقضای توکن موجود را استخراج کرده‌ایم. اکنون این زمان را از زمان جاری سیستم برحسب UTC کسر می‌کنیم. مقدار حاصل، initialDelay این تایمر خواهد بود. یعنی این تایمر به مدت initialDelay صبر خواهد کرد و سپس تنها یکبار اجرا می‌شود. پس از اجرا، ابتدا متد refreshToken ذیل را فراخوانی می‌کند تا توکن جدیدی را دریافت کند.

در متد unscheduleRefreshToken کار لغو تایمر جاری در صورت وجود انجام می‌شود.

متد درخواست یک توکن جدید بر اساس refreshToken موجود نیز به صورت ذیل است:
  refreshToken() {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    const model = { refreshToken: this.getRawAuthToken(AuthTokenType.RefreshToken) };
    return this.http
      .post(`${this.appConfig.apiEndpoint}/${this.appConfig.refreshTokenPath}`, model, { headers: headers })
      .finally(() => {
        this.scheduleRefreshToken();
      })
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        console.log("RefreshToken Result", result);
        this.setLoginSession(result);
      });
  }
در اینجا هرزمانیکه تایمر اجرا شود، این متد فراخوانی شده و مقدار refreshToken فعلی را به سمت سرور ارسال می‌کند. سپس سرور این مقدار را بررسی کرده و در صورت تعیین اعتبار، یک access_token و refresh_token جدید را به سمت کلاینت ارسال می‌کند که نتیجه‌ی آن به متد setLoginSession جهت ذخیره سازی ارسال خواهد شد.
در آخر چون این تایمر، خود متوقف شونده‌است (متد Observable.timer بدون پارامتر دوم آن فراخوانی شده‌است)، یکبار دیگر کار زمانبندی دریافت توکن جدید بعدی را در متد finally انجام می‌دهیم (متد scheduleRefreshToken را مجددا فراخوانی می‌کنیم).


تغییرات مورد نیاز در سرویس Auth جهت زمانبندی دریافت توکن دسترسی جدید

تا اینجا متدهای مورد نیاز شروع زمانبندی دریافت توکن جدید، خاتمه‌ی زمانبندی و دریافت و به روز رسانی توکن جدید را پیاده سازی کردیم. محل قرارگیری و فراخوانی این متدها در سرویس Auth، به صورت ذیل هستند:
الف) در سازنده‌ی کلاس:
  constructor(
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
    private browserStorageService: BrowserStorageService,
    private http: HttpClient,
    private router: Router
  ) {
    this.updateStatusOnPageRefresh();
    this.scheduleRefreshToken();
  }
این مورد برای مدیریت حالتی که کاربر صفحه را refresh کرده‌است و یا پس از مدتی مجددا از ابتدا برنامه را بارگذاری کرده‌است، مفید است.

ب) پس از لاگین موفقیت آمیز
در متد لاگین، پس از دریافت یک response موفقیت آمیز و تنظیم و ذخیره سازی توکن‌های دریافتی، کار زمانبندی دریافت توکن دسترسی بعدی بر اساس refresh_token فعلی انجام می‌شود:
this.setLoginSession(response);
this.scheduleRefreshToken();

ج) پس از خروج از سیستم
در متد logout، پس از حذف توکن‌های کاربر از کش مرورگر، کار لغو تایمر زمانبندی دریافت توکن بعدی نیز صورت خواهد گرفت:
this.deleteAuthTokens();
this.unscheduleRefreshToken();

در این حالت اگر برنامه را اجرا کنید، یک چنین خروجی را که بیانگر دریافت خودکار توکن‌های جدید است، پس از مدتی در کنسول developer مرورگر مشاهده خواهید کرد:


ابتدا متد scheduleRefreshToken اجرا شده و تاخیر آغازین تایمر محاسبه شده‌است. پس از مدتی متد refreshToken توسط تایمر فراخوانی شده‌است. در آخر مجددا متد scheduleRefreshToken جهت شروع یک زمانبندی جدید، اجرا شده‌است.

اعداد initialDelay محاسبه شده‌ای را هم که ملاحظه می‌کنید، نزدیک به همان 2 دقیقه‌ی تنظیمات سمت سرور در فایل appsettings.json هستند:
  "BearerTokens": {
    "Key": "This is my shared key, not so secret, secret!",
    "Issuer": "http://localhost/",
    "Audience": "Any",
    "AccessTokenExpirationMinutes": 2,
    "RefreshTokenExpirationMinutes": 60
  }


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
مطالب
مدیریت خروجی نال از اکشن متدهای برنامه‌های ASP.NET Core
فرض کنید اکشن متد Web API شما قرار است اطلاعات رکوردی را بازگشت دهد:
using Microsoft.AspNetCore.Mvc;

namespace Core3xWebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return null;
        }
    }
}
و در این حالت خاص، خروجی کوئری مدنظر، نال است. اگر کلاینت سعی کند این اطلاعات را دریافت نماید، با تصویر/خروجی زیر مواجه خواهد شد:


همانطور که مشاهده می‌کنید، هیچ خروجی تولید نشده و تنها به تنظیم status-code مساوی 204 که به معنای no content است، اکتفا کرده‌است.


چرا از کشن متدهای ASP.NET Core نمی‌توان خروجی نال گرفت؟

فرمت خروجی اکشن متدها در ASP.NET Core بر اساس output formatter‌های متفاوتی مانند JSON ،XML و غیره، تعیین می‌شود. اما زمانیکه به نال می‌رسد، از یک output formatter خاص به نام HttpNoContentOutputFormatter کمک می‌گیرد. کار آن تبدیل null، به خروجی مخصوصی است که توضیح داده شد. به این معنا که هیچگاه خروجی نال برنامه، برای مثال به نحو متداولی تبدیل به یک خروجی استاندارد JSON نمی‌شود و این مساله برای Http Client‌های مختلفی که منتظر یک خروجی استاندارد هستند (مانند انواع برنامه‌های تک صفحه‌ای وب)، عموما مساله ساز است؛ چون هر status-code دیگری بجز 200 را به صورت بروز یک خطا تفسیر می‌کنند که در حالت فوق، فقط به معنای نبود اطلاعات است و نه بروز خطایی.


چگونه خروجی نال را به همان شکلی که هست بازگشت دهیم؟

HttpNoContentOutputFormatter به همراه خاصیت TreatNullValueAsNoContent نیز هست که اگر در ابتدای تنظیمات برنامه به false تنظیم شود، دیگر مقادیر نال را به خروجی no content تبدیل نمی‌کند:
namespace Core3xWebApi
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers(options =>
            {
                // remove formatter that turns nulls into 204 - No Content responses
                // this formatter breaks SPA's Http response JSON parsing
                options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>();
                options.OutputFormatters.Insert(0, new HttpNoContentOutputFormatter
                {
                    TreatNullValueAsNoContent = false
                });
            });
        }
پس از این تغییر، اگر برنامه‌ی سمت کلاینت، سعی در دریافت اطلاعاتی از API فوق کند، اینبار یک خروجی استاندارد JSON را که در آن null درج شده‌است، دریافت خواهد کرد؛ به همراه status-code مساوی 200 که در سمت کلاینت، به یک خطا نگاشت نخواهد شد (اگر کوئری ما خروجی ندارد، دلیل بر بروز خطایی نبوده؛ فقط اطلاعاتی برای نمایش نیست):

نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهنده‌ها
ارتقاء به ASP.NET Core 3.0

در کلاس آغازین برنامه‌های وب نگارش 3، بجای متد
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
اینبار متد زیر با معرفی IWebHostEnvironment مشاهده می‌شود (چون این نوع قبلی در دو اسمبلی Microsoft.Extensions.Hosting و Microsoft.AspNetCore.Hosting تعریف شده بوده، جهت رفع ابهام مجبور به این تغییرات شده‌اند):
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
و برای اینکه متدهای الحاقی مانند ()env.IsDevelopment  شناسایی شوند، نیاز به افزودن فضای نام زیر می‌باشد:
using Microsoft.Extensions.Hosting;
البته امضای قبلی، هنوز هم در نگارش 3 کار می‌کند؛ اما در نگارش‌های بعدی حذف خواهد شد.
به صورت خلاصه هرجائی در برنامه‌ی خود IHostingEnvironment دارید، باید به IWebHostEnvironment تبدیل شود. همچنین اگر از سرویس IApplicationLifetime در جائی استفاده کرده‌اید، باید به IHostApplicationLifetime تبدیل گردد.
نظرات مطالب
یافتن Contextهای Dispose نشده در Entity framework
- همه شاید از الگوی واحد کار استفاده نکنند.
- کسانی هم که از الگوی واحد کار استفاده می‌کنند شاید بد نباشد بررسی کنند که در پایان کار Context و Connection زنده‌ای هنوز وجود دارد یا خیر.
- همه جا امکان استفاده از الگوی واحد کاری که از یک Context در طول یک درخواست استفاده می‌کند، نیست. خصوصا در مکان‌هایی که وهله سازی آن‌ها را نمی‌توان تحت کنترل خودکار IoC Containerها در آورد؛ مثلا در یک Role Provider که راسا توسط ASP.NET وهله سازی می‌شود و یا یک وظیفه‌ی فعال پس زمینه.
- گزارشی که در انتهای کار روش فوق تهیه می‌شود، مستقل است از نحوه‌ی بکارگیری و مدیریت وهله‌های Context. همچنین مستقل است از Code-first یا Db first و غیره. قابلیت interceptor آن، بحثی است عمومی.
- «هدف مقاله فعلی پیدا کردن وهله‌های dispose نشده درون متدهای برنامه است»
نهایتا از هر روشی که استفاده کنید، در متدی مشخص، وهله سازی می‌شود و شاید در جایی Dispose و یا خیر. در اینجا می‌شود از این نوع مکان‌ها گزارش گرفت.
مطالب
آشنایی با Visual Studio 2012 Code Map
Code Map چیست؟
در نسخه Visual Studio 2012 Ultimate Update 1  قابلیتی به نام Code Map اضافه گردید که امکان تصویر سازی، روابط کد‌ها را فراهم می‌سازد.
در نسخه  Visual Studio 2012 Update 2 مایکروسافت Code Map را توسعه داد و با پشتیبانی از اشکال زدایی، با Code Map نیز می‌توانید قدم به قدم کد را اشکال زایی نمایید. به زبان ساده‌تر Code Map فلوچارت اجرای برنامه است که در آن ارتباط بین متود‌ها نمایش داده شده است.
چگونه  از Code Map Debugging استفاده نماییم؟

در خطی از برنامه Break Point گذاشته و برنامه را اجرا نمایید. وقتی برنامه به خط مورد نظر رسید، نوار ابزار Debug را فعال و گزینه Code Map را انتخاب کنید تا پنجره Code Map باز شده و فلوچارت کد مورد نظر را رسم نماید. نوار قرمز رنگ دور گره، قسمتی از کد که در حال حاضر در حال اشکال زدایی است را نشان می‌دهد. وقتی با کلید F11 به درون متودی می‌روید گره متناظر آن نیز رسم می‌شود.

اگر بر روی فیلد Summary راست کلیک و گزینه Find All References on Code Map را انتخاب کنید، همه ارجاع‌های که این فیلد در آنها صدا زده شده است را نمایش می‌دهد.

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

بررسی ویژگی‌های Code Map:
برای هر گره در صورت لزوم می‌توانید یادداشتی اضافه نماید برای این کار گره مورد نظر را انتخاب تا نوار ابزاری در کنار آن ظاهر شود و گزینه create new comment node را انتخاب و یادداشت خود را ثبت نمایید.

می توانید code map را با دیگران از طریق ایمیل به اشتراک گذارید یا آن را ذخیره نمایید. برای این کار در بالای پنجره code map گزینه share را انتخاب و یکی از آینم‌ها مورد نظر را کلیک نمایید.

علاوه بر این برای تعقیب کردن یک گره آن می‌توانید آن را با یک پرچم و تغییر رنگ آن در پنجره code map از دیگر گره‌ها مجزا و تعقیب نمایید. برای این کار بر روی گره مورد نظر راست کلیک و گزینه Flag for Follow Up را انتخاب نمایید. از گزینه Other Flag Colors رنگ پیش فرض آن را می‌توانید تغییر دهید.

بازبینی اشاره گر ها:

در کنار هر گره علامتی نمایان می‌شود که وضعیت code amp در آن لحظه برای آن گره را نمایش می‌دهد.

تغییر نمای دیاگرام:

برای تغییر نمای گراف code map و نمایش بهتر آن از گزینه layout یکی از حالات موجود را می‌توانید انتخاب نمایید. هر وقت گره ای  اضافه شود که قبلا اضافه شده باشد رنگ آن سبر می‌شود برای پاک کردن این چنین گره‌های گزینه clear Result Highlighting را انتخاب نمایید.

نظرات مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول
- زمانیکه پوشه‌های پروژه‌ها را جابجا می‌کنید، باید تمام فایل‌های csproj آن‌ها را باز کنید و سپس مسیرهای HintPath بسته‌های نیوگت را اصلاح کنید:
 <HintPath>..\..\..\packages\T4MVCExtensions.3.15.0\lib\net40\T4MVCExtensions.dll</HintPath>
اگر اینکار رخ ندهد، عملا کار بازیابی بسته‌ها پاسخ نخواهد داد چون HintPath‌های موجود به چند سطح بالاتر اشاره نمی‌کنند:
 <HintPath>..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll</HintPath>
- در پروژه‌ی RabbalShopCMS.DomainClasses شما به نظر یک سری کلاس‌ها نیستند و اضافه نشدند به سورس کنترل.
- قسمت post build event باید به صورت ذیل اصلاح شود:
 Copy "$(ProjectDir)$(OutDir)*.*" "$(SolutionDir)RabbalShopCMS.Web\bin\"
به این صورت تمام فایل‌های مرتبط کپی می‌شوند.
- در global.asax.cs پروژه‌ی اصلی باید این موارد را حذف کنید:
 ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine ());
Razor generator به ازای هر پلاگین دارای یک فایل RazorGeneratorMvcStart است که کارش ثبت یک ViewEngine مخصوص خواندن فایل‌های View از اسمبلی برنامه است که این موارد نباید حذف شوند و اگر حذف شوند، Viewهای پلاگین‌ها قابل مشاهده نخواهند بود.
- افزونه‌ی دارای Area نیازی نیست فایل layout داشته باشد. فقط باید دارای یک ViewStart باشد که به layout پروژه‌ی اصلی اشاره کند. این layout از پروژه‌ی پایه دریافت می‌شود و نه از افزونه. بنابراین فایل layout افزونه باید حذف شود و اضافی است.
- بعد در حالت solution چند پروژه‌ای اجرای دستور ذیل الزامی است: (خیلی مهم)
 PM> update-package
این مورد سبب خواهد شد تا تمام وابستگی‌های solution جاری به همراه تمام پروژه‌های مرتبط آن یکدست شوند.
- اگر با درخواست یک آدرس، فایل view پروژه‌ی دیگری بازگشت داده شد، ترتیب اضافه شدن PrecompiledMvcEngine را تغییر دهید. برای مثال در پروژه‌ی پلاگین:
 ViewEngines.Engines.Insert(0, engine);
در پروژه‌ی اصلی:
 ViewEngines.Engines.Add(engine);

نظرات مطالب
اجرای وظایف زمان بندی شده با Quartz.NET - قسمت اول
از ELMAH استفاده می‌کنید؟ متدهای Application_End و Application_Start را با آن لاگ کنید و همچنین خطاهای مدیریت نشده برنامه را. با لاگ کردن عنوان شده بررسی کنید برنامه چندبار ری استارت شده.
نظرات مطالب
معرفی و استفاده از DDL Triggers در SQL Server
با سلام و احترام؛ همانطور که در متن به عرض رسانده شده:
" از عبارت  ON  برای مشخص کردن محدوده Trigger در سطح SQL Instance (در این صورت ON All SERVER نوشته می‌شود) و یا در سطح Database (در این حالت ON DATABASE نوشته می‌شود)  استفاده می‌شود و از عبارت FOR  برای مشخص کردن رویداد یا گروه رویدادی که سبب فراخوانی  Trigger می‌شود، استفاده  خواهد شد.  "
در خصوص مثالی که اشاره کردید، به نظرم می‌رسد از Trigger برای این منظور استفاده نمی‌شود (در حوزه‌ی بکاپ و ریستور)، شاید اگر قصدتان به منظور ثبت log و ... بایست از Auditing استفاده کنید. به این منظور در Auditing با توجه به جدول زیر می‌توان اقدام به ثبت موارد نمود:
Server-Level Audit Action Groups  
Action group name 
 Event Class   Description 
 BACKUP_RESTORE_GROUP   Audit Backup/Restore  
 یک دستور Backup یا Restore صادر شود  
به طور مختصر Auditing به شرح زیر است:
 بررسی SQL Server Audit
 بازبینی (Auditing) شامل پیگیری و ثبت رویدادهایی است که در سطح SQL Instance و یا Database‌های روی یک سیستم اتفاق می‌افتد. چندین سطح برای Auditing در SQL Server وجود دارد که به صلاحدید و نیازمندی‌های نصب شما وابسته است. شما می‌توانید گروه اقدامات بازبینی سرویس دهنده (server audit action groups) را به ازای هر SQL Instance و گروه اقدامات بازبینی بانک اطلاعاتی (database audit action groups) را  به ازای هر بانک اطلاعاتی ثبت کنید. رویداد Audit هر زمان عملی که مورد رسیدگی قرار گرفته اتفاق افتد، رخ می‌دهد.
تا پیش از SQL SERVER 2008، شما باید از خصیصه‌های متعددی برای انجام یک مجموعه کامل بازبینی (Auditing) برای نمونه DDL Trigger، DML Trigger و SQL Trace، بر روی یک SQL Instance استفاده می‌کردید.
SQL SERVER 2008، همه قابلیت‌های Auditing را روی یک audit specification ترکیب می‌کند. Audit Specification با تعریف یک شی بازبینی (audit object) در سطح سرویس دهنده برای ثبت (logging) یک دنباله بازبینی (audit trial) آغاز می‌شود. توجه شود که بایست یک شیء بازبینی ایجاد کنید پیش از اینکه یک Server Audit Specification و یا Database Audit Specification ایجاد کنید.
Server Audit Specification، گروه اقدامات در سطح سرویس دهنده را جمع آوری می‌کند که با رویدادهای وسیعی فعال می‌شوند، این گروه اقدامات تحت عنوان  Server-Level Audit Action Groups تشریح شده اند. شما می‌توانید یک Server Audit Specification را به ازای هر Audit ایجاد کنید چرا که هر دو در محدوده یک SQL Instance ایجاد می‌شوند.
Database Audit Specification، گروه اقدامات در سطح بانک اطلاعاتی را جمع آوری می‌کند که با رویدادهای وسیعی فعال می‌شود. این گروه اقدامات تحت عنوان‌های Database-Level Audit Action Groups و Database-Level Audit Actions تشریح شده اند.می توانید یک Database Audit Specification را به ازای هر Audit در بانک اطلاعاتی SQL Server ایجاد کنید.
همچنین می‌توانید هر گروه اقدامات بازبینی(audit action groups) یا رویدادهای بازبینی(audit events) را به یک Database Audit Specification اضافه کنید. گروه اقدامات بازبینی، گروه اقدامات از پیش تعریف شده ای هستند و رویدادهای بازبینی اقدامات تجزیه ناپذیری هستند که توسط موتور بانک اطلاعاتی مورد رسیدگی قرار می‌گیرند، هر دو در محدوده بانک اطلاعاتی (Database) هستند. این اقدامات برای Audit فرستاده می‌شوند تا در Target (که می‌تواند یک فایل، Windows Security Log و یا Windows Application Log باشد) ذخیره شوند. برای نوشتن در Windows Security Log لازم است که Service Account سرویس دهنده شما به Policy، Generate security audits اضافه شده باشد، به صورت پیش فرض Local System، Local Service و Network Service بخشی از این Policy می‌باشند. پس از اینکه Audit را ایجاد و فعال کردید، Target ورودی‌ها را دریافت خواهد کرد. 
Server-Level Audit Action Groups (گروه اقدامات بازبینی در سطح سرویس دهنده)
این گروه اقدامات به گروه رویداد Security Audit شبیه هستند. به طور خلاصه این گروه اقدامات، اقداماتی را که در یک SQL Instance شامل می‌شوند، در بر می‌گیرد. برای مثال اگر گروه اقدام مناسب با Server Audit Specification اضافه شده باشد هر شیء در هر Schema که مورد دستیابی قرار می‌گیرد، ثبت می‌شود. اقدامات در سطح سرویس دهنده به شما اجازه نمی‌دهد که جزئیات اقدامات در سطح بانک اطلاعاتی را فیلتر کنید. یک بازبینی در سطح بانک اطلاعاتی برای انجام به جزئیات دقیق فیلتر کردن نیاز دارد، برای مثال اجرای دستور Select روی جدول Customers برای login هایی که در گروه Employee هستند.
Database-Level Audit Action Groups (گروه اقدامات بازبینی در سطح بانک اطلاعاتی)
این گروه اعمال به کلاس‌های رویداد Security Audit  شبیه هستند.
Database-Level Audit Actions
اقدامات در سطح بانک اطلاعاتی، اقدامات بازبینی خاصی را به طور مستقیم روی Database، Schema و اشیاء Schema (از قبیل جداول، View ها، رویه‌های ذخیره شده، توابع و ... ) فراهم می‌کند. این اقدامات برای فیلدها (Columns) صدق نمی‌کنند.
Audit-Level Audit Action Groups
شما می‌توانید اقداماتی را که در فرآیند Auditing هستند، بازبینی کنید که می‌تواند در محدوده سرویس دهنده یا بانک اطلاعاتی باشد. در محدوده بانک اطلاعاتی تنها برای database audit specification رخ می‌دهد.
  جهت بررسی بیشتر به این لینک مراجعه شود.