نظرات مطالب
Blazor 5x - قسمت 14 - کار با فرم‌ها - بخش 2 - تعریف فرم‌ها و اعتبارسنجی آن‌ها
یک نکته‌ی تکمیلی: روش سازگار کردن اعتبارسنجی فرم‌های استاندارد Blazor با کلاس‌های CSS بوت استرپ 4 و 5
زمانیکه از EditForm و کامپوننت‌های توکار Blazor استفاده می‌کنیم، اگر کامپوننتی در وضعیت اعتبارسنجی شده قرار داشته باشد، با کلاس valid:
class="modified valid form-control"
و اگر در وضعیت شکست اعتبارسنجی قرارگیرد، با کلاس invalid مزین می‌شود:
class="modified invalid form-control"
اما برای یکپارچه سازی آن با کلاس‌های اعتبارسنجی بوت استرپ 4 و 5، نیاز است از کلاس‌های is-valid و is-invalid بجای valid و invalid استفاده شود. این تغییر نیاز به استفاده از «یک نکته‌ی تکمیلی: امکان اعتبارسنجی دستی فرم‌ها در Blazor» را دارد؛ چون با دسترسی به EditContext است که می‌توان CSS provider آن‌را سفارشی سازی کرد؛ برای مثال:
EditContext = new EditContext(Model);
EditContext.SetFieldCssClassProvider(new BootstrapFieldCssClassProvider());
که سفارشی ساز مخصوص بوت استرپ، به صورت زیر قابل تعریف است:
using System;
using System.Linq;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorComponents
{
    /// <summary>
    /// Supplies CSS class names for form fields to represent their validation state or other state information from an EditContext.
    /// </summary>
    public class BootstrapFieldCssClassProvider : FieldCssClassProvider
    {
        /// <summary>
        /// Gets a string that indicates the status of the specified field as a CSS class.
        /// </summary>
        public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
        {
            if (editContext == null)
            {
                throw new ArgumentNullException(nameof(editContext));
            }

            var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();

            if (editContext.IsModified(fieldIdentifier))
            {
                return isValid ? "is-valid" : "is-invalid";
            }
            return isValid ? "" : "is-invalid";
        }
    }
}
در اینجا در ابتدا بررسی می‌شود که آیا فیلد جاری معتبر است یا خیر و همچنین آیا ویرایش شده‌است یا خیر؟ سپس بر این اساس، کلاس‌های ویژه‌ی بوت استرپ، بجای کلاس‌های پیش‌فرض ارائه خواهند شد.
مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت اول
تمام اپلیکیشن‌ها را نمی‌توان در یک پروسس بسته بندی کرد، بدین معنا که تمام اپلیکیشن روی یک سرور فیزیکی قرار گیرد. در عصر حاظر معماری بسیاری از اپلیکیشن‌ها چند لایه است و هر لایه روی سرور مجزایی توزیع می‌شود. بعنوان مثال یک معماری کلاسیک شامل سه لایه نمایش (presentation)، اپلیکیشن (application) و داده (data) است. لایه بندی منطقی (logical layering) یک اپلیکیشن می‌تواند در یک App Domain واحد پیاده سازی شده و روی یک کامپیوتر میزبانی شود. در این صورت لازم نیست نگران مباحثی مانند پراکسی ها، مرتب سازی (serialization)، پروتوکل‌های شبکه و غیره باشیم. اما اپلیکیشن‌های بزرگی که چندین کلاینت دارند و در مراکز داده میزبانی می‌شوند باید تمام این مسائل را در نظر بگیرند. خوشبختانه پیاده سازی چنین اپلیکیشن هایی با استفاده از Entity Framework و دیگر تکنولوژی‌های مایکروسافت مانند WCF, Web API ساده‌تر شده است. منظور از n-Tier معماری اپلیکیشن هایی است که لایه‌های نمایش، منطق تجاری و دسترسی داده هر کدام روی سرور مجزایی میزبانی می‌شوند. این تفکیک فیزیکی لایه‌ها به بسط پذیری، مدیریت و نگهداری اپلیکیشن‌ها در دراز مدت کمک می‌کند، اما معمولا تاثیری منفی روی کارایی کلی سیستم دارد. چرا که برای انجام عملیات مختلف باید از محدوده ماشین‌های فیریکی عبور کنیم.

معماری N-Tier چالش‌های بخصوصی را برای قابلیت‌های change-tracking در EF اضافه می‌کند. در ابتدا داده‌ها توسط یک آبجکت EF Context بارگذاری می‌شوند اما این آبجکت پس از ارسال داده‌ها به کلاینت از بین می‌رود. تغییراتی که در سمت کلاینت روی داده‌ها اعمال می‌شوند ردیابی (track) نخواهند شد. هنگام بروز رسانی، آبجکت Context جدیدی برای پردازش اطلاعات ارسالی باید ایجاد شود. مسلما آبجکت جدید هیچ چیز درباره Context پیشین یا مقادیر اصلی موجودیت‌ها نمی‌داند.

در نسخه‌های قبلی Entity Framework توسعه دهندگان با استفاده از قالب ویژه ای بنام Self-Tracking Entities می‌توانستند تغییرات موجودیت‌‌ها را ردیابی کنند. این قابلیت در نسخه EF 6 از رده خارج شده است و گرچه هنوز توسط ObjectContext پشتیبانی می‌شود، آبجکت DbContext از آن پشتیبانی نمی‌کند.

در این سری از مقالات روی عملیات پایه CRUD تمرکز می‌کنیم که در اکثر اپلیکیشن‌های n-Tier استفاده می‌شوند. همچنین خواهیم دید چگونه می‌توان تغییرات موجودیت‌ها را ردیابی کرد. مباحثی مانند همزمانی (concurrency) و مرتب سازی (serialization) نیز بررسی خواهند شد. در قسمت یک این سری مقالات، به بروز رسانی موجودیت‌های منفصل (disconnected) توسط سرویس‌های Web API نگاهی خواهیم داشت.


بروز رسانی موجودیت‌های منفصل با Web API

سناریویی را فرض کنید که در آن برای انجام عملیات CRUD از یک سرویس Web API استفاده می‌شود. همچنین مدیریت داده‌ها با مدل Code-First پیاده سازی شده است. در مثال جاری یک کلاینت Console Application خواهیم داشت که یک سرویس Web API را فراخوانی می‌کند. توجه داشته باشید که هر اپلیکیشن در Solution مجزایی قرار دارد. تفکیک پروژه‌ها برای شبیه سازی یک محیط n-Tier انجام شده است.

فرض کنید مدلی مانند تصویر زیر داریم.

همانطور که می‌بینید مدل جاری، سفارشات یک اپلیکیشن فرضی را معرفی می‌کند. می‌خواهیم مدل و کد دسترسی به داده‌ها را در یک سرویس Web API پیاده سازی کنیم، تا هر کلاینتی که از HTTP استفاده می‌کند بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe1.Service تغییر دهید.
  • کنترلر جدیدی از نوع WebApi Controller با نام OrderController به پروژه اضافه کنید.
  • کلاس جدیدی با نام Order در پوشه مدل‌ها ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Order
{
    public int OrderId { get; set; }
    public string Product { get; set; }
    public int Quantity { get; set; }
    public string Status { get; set; }
    public byte[] TimeStamp { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • حال کلاسی با نام Recipe1Context ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Recipe1Context : DbContext
{
    public Recipe1Context() : base("Recipe1ConnectionString") { }
    
    public DbSet<Order> Orders { get; set; }
    
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().ToTable("Orders");
        // Following configuration enables timestamp to be concurrency token
        modelBuilder.Entity<Order>().Property(x => x.TimeStamp)
            .IsConcurrencyToken()
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
    }
}

  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings>
  <add name="Recipe1ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به آن اضافه نمایید. این کد بررسی Entity Framework Compatibility را غیرفعال می‌کند.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);
    ...
}
  • در آخر کد کنترلر Order را با لیست زیر جایگزین کنید.
public class OrderController : ApiController
{
    // GET api/order
    public IEnumerable<Order> Get()
    {
        using (var context = new Recipe1Context())
        {
            return context.Orders.ToList();
        }
    }

    // GET api/order/5
    public Order Get(int id)
    {
        using (var context = new Recipe1Context())
        {
            return context.Orders.FirstOrDefault(x => x.OrderId == id);
        }
    }

    // POST api/order
    public HttpResponseMessage Post(Order order)
    {
        // Cleanup data from previous requests
        Cleanup();
        
        using (var context = new Recipe1Context())
        {
            context.Orders.Add(order);
            context.SaveChanges();
            // create HttpResponseMessage to wrap result, assigning Http Status code of 201,
            // which informs client that resource created successfully
            var response = Request.CreateResponse(HttpStatusCode.Created, order);
            // add location of newly-created resource to response header
            response.Headers.Location = new Uri(Url.Link("DefaultApi",
                new { id = order.OrderId }));
            return response;
        }
    }

    // PUT api/order/5
    public HttpResponseMessage Put(Order order)
    {
        using (var context = new Recipe1Context())
        {
            context.Entry(order).State = EntityState.Modified;
            context.SaveChanges();
            // return Http Status code of 200, informing client that resouce updated successfully
            return Request.CreateResponse(HttpStatusCode.OK, order);
        }
    }

    // DELETE api/order/5
    public HttpResponseMessage Delete(int id)
    {
        using (var context = new Recipe1Context())
        {
            var order = context.Orders.FirstOrDefault(x => x.OrderId == id);
            context.Orders.Remove(order);
            context.SaveChanges();
            // Return Http Status code of 200, informing client that resouce removed successfully
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }

    private void Cleanup()
    {
        using (var context = new Recipe1Context())
        {
            context.Database.ExecuteSqlCommand("delete from [orders]");
        }
    }
}

قابل ذکر است که هنگام استفاده از Entity Framework در MVC یا Web API، بکارگیری قابلیت Scaffolding بسیار مفید است. این فریم ورک‌های ASP.NET می‌توانند کنترلرهایی کاملا اجرایی برایتان تولید کنند که صرفه جویی چشمگیری در زمان و کار شما خواهد بود.

در قدم بعدی اپلیکیشن کلاینت را می‌سازیم که از سرویس Web API استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe1.Client تغییر دهید.
  • کلاس موجودیت Order را به پروژه اضافه کنید. همان کلاسی که در سرویس Web API ساختیم.

نکته: قسمت هایی از اپلیکیشن که باید در لایه‌های مختلف مورد استفاده قرار گیرند - مانند کلاس‌های موجودیت‌ها - بهتر است در لایه مجزایی قرار داده شده و به اشتراک گذاشته شوند. مثلا می‌توانید پروژه ای از نوع Class Library بسازید و تمام موجودیت‌ها را در آن تعریف کنید. سپس لایه‌های مختلف این پروژه را ارجاع خواهند کرد.

فایل program.cs را باز کنید و کد زیر را به آن اضافه نمایید.

private HttpClient _client;
private Order _order;

private static void Main()
{
    Task t = Run();
    t.Wait();
    
    Console.WriteLine("\nPress <enter> to continue...");
    Console.ReadLine();
}

private static async Task Run()
{
    // create instance of the program class
    var program = new Program();
    program.ServiceSetup();
    program.CreateOrder();
    // do not proceed until order is added
    await program.PostOrderAsync();
    program.ChangeOrder();
    // do not proceed until order is changed
    await program.PutOrderAsync();
    // do not proceed until order is removed
    await program.RemoveOrderAsync();
}

private void ServiceSetup()
{
    // map URL for Web API cal
    _client = new HttpClient { BaseAddress = new Uri("http://localhost:3237/") };
    // add Accept Header to request Web API content
    // negotiation to return resource in JSON format
    _client.DefaultRequestHeaders.Accept.
        Add(new MediaTypeWithQualityHeaderValue("application/json"));
}

private void CreateOrder()
{
    // Create new order
    _order = new Order { Product = "Camping Tent", Quantity = 3, Status = "Received" };
}

private async Task PostOrderAsync()
{
    // leverage Web API client side API to call service
    var response = await _client.PostAsJsonAsync("api/order", _order);
    Uri newOrderUri;
    
    if (response.IsSuccessStatusCode)
    {
        // Capture Uri of new resource
        newOrderUri = response.Headers.Location;
        // capture newly-created order returned from service,
        // which will now include the database-generated Id value
        _order = await response.Content.ReadAsAsync<Order>();
        Console.WriteLine("Successfully created order. Here is URL to new resource: {0}",  newOrderUri);
    }
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

private void ChangeOrder()
{
    // update order
    _order.Quantity = 10;
}

private async Task PutOrderAsync()
{
    // construct call to generate HttpPut verb and dispatch
    // to corresponding Put method in the Web API Service
    var response = await _client.PutAsJsonAsync("api/order", _order);
    
    if (response.IsSuccessStatusCode)
    {
        // capture updated order returned from service, which will include new quanity
        _order = await response.Content.ReadAsAsync<Order>();
        Console.WriteLine("Successfully updated order: {0}", response.StatusCode);
    }
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

private async Task RemoveOrderAsync()
{
    // remove order
    var uri = "api/order/" + _order.OrderId;
    var response = await _client.DeleteAsync(uri);

    if (response.IsSuccessStatusCode)
        Console.WriteLine("Sucessfully deleted order: {0}", response.StatusCode);
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

اگر اپلیکیشن کلاینت را اجرا کنید باید با خروجی زیر مواجه شوید:

Successfully created order: http://localhost:3237/api/order/1054
Successfully updated order: OK
Sucessfully deleted order: OK

شرح مثال جاری

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

حال اپلیکیشن کنسول را باز کنید. روی خط اول کد program.cs یک breakpoint تعریف کرده و اپلیکیشن را اجرا کنید. ابتدا آدرس سرویس Web API را پیکربندی کرده و خاصیت Accept Header را مقدار دهی می‌کنیم. با این کار از سرویس مورد نظر درخواست می‌کنیم که داده‌ها را با فرمت JSON بازگرداند. سپس یک آبجکت Order می‌سازیم و با فراخوانی متد PostAsJsonAsync آن را به سرویس ارسال می‌کنیم. این متد روی آبجکت HttpClient تعریف شده است. اگر به اکشن متد Post در کنترلر Order یک breakpoint اضافه کنید، خواهید دید که این متد سفارش جدید را بعنوان یک پارامتر دریافت می‌کند و آن را به لیست موجودیت‌ها در Context جاری اضافه می‌نماید. این عمل باعث می‌شود که آبجکت جدید بعنوان Added علامت گذاری شود، در این مرحله Context جاری شروع به ردیابی تغییرات می‌کند. در آخر با فراخوانی متد SaveChanges داده‌ها را ذخیره می‌کنیم. در قدم بعدی کد وضعیت 201 (Created) و آدرس منبع جدید را در یک آبجکت HttpResponseMessage قرار می‌دهیم و به کلاینت ارسال می‌کنیم. هنگام استفاده از Web API باید اطمینان حاصل کنیم که کلاینت‌ها درخواست‌های ایجاد رکورد جدید را بصورت POST ارسال می‌کنند. درخواست‌های HTTP Post بصورت خودکار به اکشن متد متناظر نگاشت می‌شوند.

در مرحله بعد عملیات بعدی را اجرا می‌کنیم، تعداد سفارش را تغییر می‌دهیم و موجودیت جاری را با فراخوانی متد PutAsJsonAsync به سرویس Web API ارسال می‌کنیم. اگر به اکشن متد Put در کنترلر سرویس یک breakpoint اضافه کنید، خواهید دید که آبجکت سفارش بصورت یک پارامتر دریافت می‌شود. سپس با فراخوانی متد Entry و پاس دادن موجودیت جاری بعنوان رفرنس، خاصیت State را به Modified تغییر می‌دهیم، که این کار موجودیت را به Context جاری می‌چسباند. حال فراخوانی متد SaveChanges یک اسکریپت بروز رسانی تولید خواهد کرد. در مثال جاری تمام فیلدهای آبجکت Order را بروز رسانی می‌کنیم. در شماره‌های بعدی این سری از مقالات، خواهیم دید چگونه می‌توان تنها فیلدهایی را بروز رسانی کرد که تغییر کرده اند. در آخر عملیات را با بازگرداندن کد وضعیت 200 (OK) به اتمام می‌رسانیم.

در مرحله بعد، عملیات نهایی را اجرا می‌کنیم که موجودیت Order را از منبع داده حذف می‌کند. برای اینکار شناسه (Id) رکورد مورد نظر را به آدرس سرویس اضافه می‌کنیم و متد DeleteAsync را فراخوانی می‌کنیم. در سرویس Web API رکورد مورد نظر را از دیتابیس دریافت کرده و متد Remove را روی Context جاری فراخوانی می‌کنیم. این کار موجودیت مورد نظر را بعنوان Deleted علامت گذاری می‌کند. فراخوانی متد SaveChanges یک اسکریپت Delete تولید خواهد کرد که نهایتا منجر به حذف شدن رکورد می‌شود.

در یک اپلیکیشن واقعی بهتر است کد دسترسی داده‌ها از سرویس Web API تفکیک شود و در لایه مجزایی قرار گیرد.

اشتراک‌ها
استفاده از فایل csv برای مقدار دهی اولیه‌ی جداول با استفاده از EF

protected override void Seed(SeedingDataFromCSV.Domain.LocationContext context)
{
    Assembly assembly = Assembly.GetExecutingAssembly();
    string resourceName = "SeedingDataFromCSV.Domain.SeedData.countries.csv";
    using (Stream stream = assembly.GetManifestResourceStream(resourceName))
    {
        using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
        {
            CsvReader csvReader = new CsvReader(reader);
            csvReader.Configuration.WillThrowOnMissingField = false;
            var countries = csvReader.GetRecords<Country>().ToArray();
            context.Countries.AddOrUpdate(c => c.Code, countries);
        }
    }

    resourceName = "SeedingDataFromCSV.Domain.SeedData.provincestates.csv";
    using (Stream stream = assembly.GetManifestResourceStream(resourceName))
    {
        using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
        {
            CsvReader csvReader = new CsvReader(reader);
            csvReader.Configuration.WillThrowOnMissingField = false;
            while (csvReader.Read())
            {
                var provinceState = csvReader.GetRecord<ProvinceState>();
                var countryCode = csvReader.GetField<string>("CountryCode");
                provinceState.Country = context.Countries.Local.Single(c => c.Code == countryCode);
                context.ProvinceStates.AddOrUpdate(p => p.Code, provinceState);
            }
        }
    }
}

یک کتابخانه مرتبط: EntityFramework.Seeder

استفاده از فایل csv برای مقدار دهی اولیه‌ی جداول با استفاده از EF
مطالب
کار با کوکی‌ها در ASP.NET Core
API کار با کوکی‌ها نیز در ASP.NET Core نسبت به نگارش‌های دیگر تغییریافته‌است که در ادامه این موارد را بررسی خواهیم کرد. همچنین با کمک مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» یک تامین کنند‌ه‌ی سفارشی کوکی‌های رمزنگاری شده را نیز ایجاد می‌کنیم.


خلاصه‌ای از روش‌های کار با کوکی‌ها در ASP.NET Core

ایجاد یک کوکی جدید
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class TestCookiesController : Controller
    {
        public IActionResult Index()
        {
            this.Response.Cookies.Append("key", "value", new CookieOptions
            {
                HttpOnly = true,
                Path = this.Request.PathBase.HasValue ? this.Request.PathBase.ToString() : "/",
                Secure = this.Request.IsHttps
            });
 
            return Content("OK!");
        }
    }
}
کوکی جدید را می‌توان توسط متد Append مجموعه‌ی کوکی‌ها، به Response اضافه کرد:


همانطور که در تصویر نیز مشخص است، طول عمر این کوکی، به سشن تنظیم شده‌است و با پایان سشن جاری مرورگر (بسته شدن کل مرورگر)، این کوکی نیز غیرمعتبر شده و به صورت خودکار حذف خواهد شد. برای تعیین عمر دقیق یک کوکی می‌توان از خاصیت Expires شیء CookieOptions که در مثال فوق مقدار دهی نشده‌است، استفاده کرد؛ مانند:
 Expires = DateTimeOffset.UtcNow.AddDays(2)

خواندن محتویات کوکی ذخیره شده

پس از ثبت کوکی در Response، خواندن آن در Request بعدی به شکل زیر است:
 var value = this.Request.Cookies["key"];
در این حالت اگر کلید درخواستی در مجموعه‌ی کوکی‌ها یافت نشد، نال بازگشت داده می‌شود.
شیء this.Request.Cookies از نوع IRequestCookieCollection است:
    public interface IRequestCookieCollection : IEnumerable<KeyValuePair<string, string>>, IEnumerable
    {
        string this[string key] { get; }
        ICollection<string> Keys { get; }
        bool ContainsKey(string key);
        bool TryGetValue(string key, out string value);
    }
و همانطور که ملاحظه می‌کنید، برای دریافت مقدار یک کوکی یا می‌توان از indexer آن مانند مثال فوق و یا از متد TryGetValue استفاده کرد.
در مستندات آن عنوان شده‌است که در حالت استفاده‌ی از indexer، درصورت یافت نشدن کلید، string.Empty بازگشت داده می‌شود (که آزمایشات null را نمایش می‌دهند). اما در حالت استفاده‌ی از TryGetValue بر اساس خروجی bool آن دقیقا می‌توان مشخص کرد که آیا این کوکی وجود داشته‌است یا خیر.
در اینجا همچنین متد ContainsKey نیز جهت بررسی وجود یک کلید، در مجموعه‌ی کلیدها نیز پیش بینی شد‌ه‌است.
بنابراین بهتر است جهت یافتن مقادیر کوکی‌ها از روش ذیل استفاده کرد:
string cookieValue;
if (this.Request.Cookies.TryGetValue(key, out cookieValue))
{
   // TODO: use the cookieValue
}
else
{
  // this cookie doesn't exist.
}

حذف کوکی‌های موجود

در اینجا متد Delete نیز پیش بینی شده‌است که باید بر روی کوکی‌های Response فراخوانی شود:
 this.Response.Cookies.Delete("key");
کار آن افزودن یک کوکی دیگر با همین کلید، اما منقضی شده‌است؛ تا مرورگر را مجبور به حذف آن کند. در اینجا در صورت نیاز می‌توان به عنوان پارامتر دوم، CookieOptions این کوکی جدید را نیز تنظیم کرد.


همانطور که در تصویر نیز مشخص است، در صورت عدم تنظیم CookieOptions، این کوکی جدید اضافه شده، دارای تاریخ انقضای 1970 است که سبب خواهد شد تا توسط مرورگر، غیرمعتبر درنظر گرفته شده و حذف شود.


طراحی یک تامین کننده‌ی کوکی‌های امن

پس از آشنایی با مقدمات کوکی‌ها و همچنین «بررسی تغییرات رمزنگاری اطلاعات در NET Core.»، اکنون می‌توان یک تامین کننده‌ی کوکی‌های رمزنگاری شده را برای ASP.NET Core به نحو ذیل طراحی کرد:
public interface ISecureCookiesProvider
{
    void Add(HttpContext context, string token, string value);
    bool Contains(HttpContext context, string token);
    string GetValue(HttpContext context, string token);
    void Remove(HttpContext context, string token);
}
 
public class SecureCookiesProvider : ISecureCookiesProvider
{
    private readonly IProtectionProvider _protectionProvider;
 
    public SecureCookiesProvider(IProtectionProvider protectionProvider)
    {
 
        _protectionProvider = protectionProvider;
    }
 
    public void Add(HttpContext context, string token, string value)
    {
        value = _protectionProvider.Encrypt(value);
        context.Response.Cookies.Append(token, value, getCookieOptions(context));
    }
 
    public bool Contains(HttpContext context, string token)
    {
        return context.Request.Cookies.ContainsKey(token);
    }
 
    public string GetValue(HttpContext context, string token)
    {
        string cookieValue;
        if (!context.Request.Cookies.TryGetValue(token, out cookieValue))
        {
            return null;
        }
        return _protectionProvider.Decrypt(cookieValue);
    }
 
    public void Remove(HttpContext context, string token)
    {
        if (context.Request.Cookies.ContainsKey(token))
        {
            context.Response.Cookies.Delete(token);
        }
    }
 
    /// <summary>
    /// Expires at the end of the browser's session.
    /// </summary>
    private CookieOptions getCookieOptions(HttpContext context)
    {
        return new CookieOptions
        {
            HttpOnly = true,
            Path = context.Request.PathBase.HasValue ? context.Request.PathBase.ToString() : "/",
            Secure = context.Request.IsHttps
        };
    }
}
- نکاتی را که در ابتدای مطلب در مورد ثبت و خواندن و حذف کوکی‌ها مطالعه کردید، به این کلاس اضافه شده‌اند. با این تغییر که پیش از ذخیر‌ه‌ی مقدار کوکی، این مقدار رمزنگاری می‌شود و همچنین پس از خواندن آن، رمزگشایی خواهد شد.
- در این تامین کننده‌ی کوکی‌های امن، IProtectionProvider تزریقی به سازنده‌ی کلاس را در مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» پیشتر ملاحظه کرده‌اید.
- در اینجا برای ثبت سرویس جدید، تنظیمات ابتدایی برنامه چنین شکلی را پیدا می‌کنند و پس از آن می‌توان سرویس ISecureCookiesProvider را به کنترلرهای برنامه تزریق و استفاده کرد:
public class Startup
{ 
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddSingleton<IProtectionProvider, ProtectionProvider>();
        services.TryAddSingleton<ISecureCookiesProvider, SecureCookiesProvider>();
- چون در کلاس SecureCookiesProvider، خاصیت Expires تنظیم نشده‌است، طول عمر این کوکی‌ها محدود است به مدت زمان باز بودن مرورگر. بنابراین در صورت نیاز این مورد را تغییر دهید.
مطالب
استفاده از Data Annotations جهت تعریف خواص ستون‌ها در PdfReport
در مطلب «تولید پویای ستون‌ها در PdfReport» عنوان شد که ذکر قسمت MainTableColumns و تمام تعاریف مرتبط با آن در PdfReports اختیاری است. همچنین به کمک متد MainTableAdHocColumnsConventions می‌توان بر اساس نوع‌های داده‌ای، بر روی نحوه نمایش ستون‌ها تاثیر گذاشت. برای مثال هرجایی DateTime مشاهده شد، به صورت خودکار تبدیل به تاریخ شمسی شود.
روش دیگری که این روزها در اکثر فریم‌های دات نتی مرسوم شده است، استفاده از Data Annotations جهت انتساب یک سری متادیتا به خاصیت‌های تعریف شده کلاس‌ها است. برای مثال ASP.NET MVC از این قابلیت زیاد استفاده می‌کند (در تولید پویای کد، یا اعتبار سنجی‌های سمت سرور و کاربر).
به همین جهت برای سازگاری بیشتر PdfReport با مدل‌های اینگونه فریم ورک‌ها، اکثر ویژگی‌ها و Data Annotations متداول را نیز می‌توان در PdfReport بکار برد. همچنین تعدادی ویژگی سفارشی نیز تعریف شده است، که در ادامه به بررسی آن‌ها خواهیم پرداخت.

آشنایی با مدل‌های بکار رفته در مثال جاری:
using System.ComponentModel;

namespace PdfReportSamples.Models
{
    public enum JobTitle
    {
        [Description("Grunt")]
        Grunt,

        [Description("Programmer")]
        Programmer,

        [Description("Analyst Programmer")]
        AnalystProgrammer,

        [Description("Project Manager")]
        ProjectManager,

        [Description("Chief Information Officer")]
        ChiefInformationOfficer,
    }
}
در اینجا یک enum، جهت تعیین سمت شغلی تعریف شده است. برای اینکه بتوان خروجی مطلوبی را در گزارشات شاهد بود، می‌توان از ویژگی Description، جهت تعیین مقدار نمایشی آن‌ها نیز استفاده کرد و این تعاریف در PdfReport خوانده و اعمال می‌شوند.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using PdfReportSamples.Models;
using PdfRpt.Aggregates.Numbers;
using PdfRpt.ColumnsItemsTemplates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.DataAnnotations;

namespace PdfReportSamples.DataAnnotations
{
    public class Person
    {
        [IsVisible(false)]
        public int Id { get; set; }

        [DisplayName("User name")]
        //Note: If you don't specify the ColumnItemsTemplate, a new TextBlockField() will be used automatically.
        [ColumnItemsTemplate(typeof(TextBlockField))]
        public string Name { get; set; }

        [DisplayName("Job title")]
        public JobTitle JobTitle { set; get; }

        [DisplayName("Date of birth")]
        [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
        public DateTime DateOfBirth { get; set; }

        [DisplayName("Date of death")]
        [DisplayFormat(NullDisplayText = "-", DataFormatString = "{0:MM/dd/yyyy}")]
        public DateTime? DateOfDeath { get; set; }

        [DisplayFormat(DataFormatString = "{0:n0}")]
        [CustomAggregateFunction(typeof(Sum))]
        public int Salary { get; set; }

        [IsCalculatedField(true)]
        [DisplayName("Calculated Field")]
        [DisplayFormat(DataFormatString = "{0:n0}")]
        [AggregateFunction(AggregateFunction.Sum)]
        public string CalculatedField { get; set; }

        [CalculatedFieldFormula("CalculatedField")]
        public static Func<IList<CellData>, object> CalculatedFieldFormula =
                                                    list =>
                                                    {
                                                        if (list == null) return string.Empty;
                                                        var salary = (int)list.GetValueOf<Person>(x => x.Salary);
                                                        return salary * 0.8;
                                                    };//Note: It's a static field, not a property.
    }
}
مدل فوق جهت مقدار دهی اطلاعات یک شخص تعریف شده است.
- اگر قصد ندارید خاصیتی در این بین، در گزارشات ظاهر شود، از ویژگی IsVisible با مقدار false استفاده کنید.
- از ویژگی DisplayName جهت تعیین برچسب‌های سرستون‌ها استفاده خواهد شد.
- ذکر ویژگی ColumnItemsTemplate اختیاری است و اگر عنوان نشود به صورت خودکار از TextBlockField استفاده خواهد شد. اما اگر نیاز به استفاده از قالب‌های ستون‌های سفارشی و یا حتی قالب‌های پیش فرض دیگری که متنی نیستند، وجود دارد، می‌توانید از ویژگی ColumnItemsTemplate به همراه نوع کلاس مورد نظر استفاده نمائید. کلاس‌های پیش فرض قالب‌های ستون‌ها در PdfReport در پوشه Lib\ColumnsItemsTemplates سورس آن قرار دارند.
- برای تعیین نحوه فرمت اطلاعات در اینجا می‌توان از ویژگی DisplayFormat استفاده کرد. این ویژگی در اسمبلی System.ComponentModel.DataAnnotations.dll دات نت تعریف شده است؛ که در اینجا نمونه‌ای از استفاده از آن‌را برای تعیین نحوه نمایش تاریخ، ملاحظه می‌کنید. توسط این ویژگی حتی می‌توان مشخص ساخت (توسط پارامتر NullDisplayText) که اگر اطلاعاتی null بود، بجای آن چه عبارتی نمایش داده شود.
- اگر علاقمند به اعمال تابعی تجمعی به ستونی خاص هستید، از ویژگی CustomAggregateFunction استفاده کنید. پارامتر آن نوع کلاس تابع مورد نظر است. یک سری تابع تجمعی پیش فرض در فضای نام PdfRpt.Aggregates.Numbers قرار دارند. البته امکان تهیه انواع سفارشی آن‌ها نیز پیش بینی شده است که در قسمت‌های بعد به آن خواهیم پرداخت.
- امکان تعریف خواص محاسباتی نیز پیش بینی شده است. برای این منظور دو کار را باید انجام داد:
الف) ویژگی IsCalculatedField را با مقدار true بر روی خاصیت مورد نظر اعمال کنید.
ب) هم نام خاصیت محاسباتی افزوده شده به کلاس جاری، ویژگی CalculatedFieldFormula را بر روی یک فیلد استاتیک عمومی در آن کلاس به نحوی که ملاحظه می‌کنید (مطابق امضای فیلد CalculatedFieldFormula فوق)، تعریف نمائید. (علت این است که نمی‌توان توسط ویژگی‌ها از delegates استفاده کرد و این محدودیت ذاتی وجود دارد)


در ادامه کدهای منبع داده فرضی مثال جاری ذکر شده است:
using System;
using System.Collections.Generic;
using PdfReportSamples.Models;

namespace PdfReportSamples.DataAnnotations
{
    public static class PersonnelDataSource
    {
        public static IList<Person> CreatePersonnelList()
        {
            return new List<Person>
            {
                new Person
                {
                    Id = 1,
                    Name = "Edward",
                    DateOfBirth = new DateTime(1900, 1, 1),
                    DateOfDeath = new DateTime(1990, 10, 15),
                    JobTitle = JobTitle.ChiefInformationOfficer,
                    Salary = 5000
                },
                new Person
                {
                    Id = 2,
                    Name = "Margaret", 
                    DateOfBirth = new DateTime(1950, 2, 9), 
                    DateOfDeath = null,
                    JobTitle = JobTitle.AnalystProgrammer,
                    Salary = 4000
                },
                new Person
                {
                    Id = 3,
                    Name = "Grant", 
                    DateOfBirth = new DateTime(1975, 6, 13), 
                    DateOfDeath = null,
                    JobTitle = JobTitle.Programmer,
                    Salary = 3500
                }
            };
        }
    }
}
در پایان، نحوه استفاده از منبع داده فوق جهت تامین یک گزارش، به نحو زیر می‌باشد:
using System;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.DataAnnotations
{
    public class DataAnnotationsPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("new rpt.");
                     defaultHeader.RunDirection(PdfRunDirection.LeftToRight);
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.ClassicTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.FitToContent);
             })
             .MainTableDataSource(dataSource =>
             {
                 dataSource.StronglyTypedList(PersonnelDataSource.CreatePersonnelList());
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("Total");
                 summary.PageSummarySettings("Page Summary");
                 summary.PreviousPageSummarySettings("Pervious Page Summary");
             })
             .MainTableAdHocColumnsConventions(adHocColumns =>
             {
                 adHocColumns.ShowRowNumberColumn(true);
                 adHocColumns.RowNumberColumnCaption("#");
             })
             .Export(export =>
             {
                 export.ToExcel();
                 export.ToXml();
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\DataAnnotationsSampleRpt.pdf"));
        }
    }
}
همانطور که مشخص است، از ذکر متد MainTableColumns به علت استفاده از DataAnnotations صرفنظر شده و PdfReport این تعاریف را بر اساس ویژگی‌های خواص کلاس شخص دریافت می‌کند. تنها از متد MainTableAdHocColumnsConventions جهت مشخص سازی اینکه نیاز به نمایش ستون ردیف می‌باشد، استفاده کرده‌ایم.


مطالب
خروجی Excel با حجم بالا در برنامه‌های ‌ASP.NET Core با استفاده از MiniExcel

امکان خروجی اکسل از گزارشات سیستم، یکی از بایدهای بیشتر سیستم‌های اطلاعاتی می‌باشد؛ یکی از چالش‌های اصلی در تولید این نوع خروجی، افزایش مصرف حافظه متناسب با افزایش حجم دیتا می‌باشد. از آنجایی‌که بیشتر راهکارهای موجود از جمله ClosedXml یا Epplus کل ساختار را ابتدا تولید کرده و اصطلاحا خروجی مورد نظر را بافر می‌کنند، برای حجم بالای اطلاعات مناسب نخواهند بود. راهکار برای خروجی CSV به عنوان مثال خیلی سرراست می‌باشد و می‌توان با چند خط کد، به نتیجه دلخواه از طریق مکانیزم Streaming رسید؛ ولی ساختار Excel به سادگی فرمت CSV نیست و برای مثال فرمت Excel Workbook با پسوند xlsx یک بسته Zip شده‌ای از فایل‌های XML می‌باشد.

معرفی MiniExcel

MiniExcel یک کتابخانه سورس باز با هدف به حداقل رساندن مصرف حافظه در زمان پردازش فایل‌های Excel در دات نت می‌باشد. در مقایسه با Aspose از منظر امکانات شاید حرفی برای گفتن نداشته باشد، ولی از جهت خواندن اطلاعات فایل‌های Excel با قابلیت پشتیبانی از ‌LINQ و Deferred Execution در کنار مصرف کم حافظه و جلوگیری از مشکل OOM خیلی خوب عمل می‌کند. در تصویر زیر مشخص است که برای عمده عملیات پیاده‌سازی شده، از استریم‌ها بهره برده شده است.

همچنین در زیر مقایسه‌ای روی خروجی ۱ میلیون رکورد با تعداد ۱۰ ستون در هر ردیف انجام شده‌است که قابل توجه می‌باشد:

Logic : create a total of 10,000,000 "HelloWorld" excel
LibraryMethodMax Memory UsageMean
MiniExcel'MiniExcel Create Xlsx'15 MB11.53181 sec
Epplus'Epplus Create Xlsx'1,204 MB22.50971 sec
OpenXmlSdk'OpenXmlSdk Create Xlsx'2,621 MB42.47399 sec
ClosedXml'ClosedXml Create Xlsx'7,141 MB140.93992 sec

به شدت API خوش دستی برای استفاده دارد و شاید مطالعه سورس کد آن از جهت طراحی نیز درس آموزی داشته باشد. در ادامه چند مثال از مستندات آن را می‌توانید ملاحظه کنید:

var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
MiniExcel.SaveAs(path, new[] {
    new { Column1 = "MiniExcel", Column2 = 1 },
    new { Column1 = "Github", Column2 = 2}
});

// DataReader export multiple sheets (recommand by Dapper ExecuteReader)

using (var cnn = Connection)
{
    cnn.Open();
    var sheets = new Dictionary<string,object>();
    sheets.Add("sheet1", cnn.ExecuteReader("select 1 id"));
    sheets.Add("sheet2", cnn.ExecuteReader("select 2 id"));
    MiniExcel.SaveAs("Demo.xlsx", sheets);
}

طراحی یک ActionResult سفارشی برای استفاده از MiniExcel

برای این منظور نیاز است تا Stream مربوط به Response درخواست جاری را در اختیار این کتابخانه قرار دهیم و از سمت دیگر دیتای مورد نیاز را به نحوی که بافر نشود و از طریق مکانیزم Streaming در EF (استفاده از Deferred Execution و Enumerableها) مهیا کنیم. برای امکان تعویض پذیری (این سناریو در پروژه واقعی و باتوجه به جهت وابستگی‌ها می‌تواند ضروری باشد) از دو واسط زیر استفاده خواهیم کرد:

public interface IExcelDocumentFactory
{
    ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> headers, Stream stream);
}


public interface ILargeExcelDocument : IAsyncDisposable, IDisposable
{
    Task Write<T>(
        PaginatedEnumerable<T> items,
        int count,
        int sizeLimit,
        CancellationToken cancellationToken = default) where T : notnull;
}

متد CreateLargeDocument یک وهله از ILargeExcelDocument را در اختیار مصرف کننده قرار می‌دهد که قابلیت نوشتن روی آن از طریق متد Write را خواهد داشت. روش واکشی دیتا از طریق Delegate تعریف شده با نام PaginatedEnumerable به مصرف کننده محول شده‌است که در ادامه امضای آن را می‌توانید مشاهده کنید:

public delegate IEnumerable<T> PaginatedEnumerable<out T>(int page, int pageSize);

در ادامه پیاده‌سازی واسط ILargeExcelDocument برای MiniExcel به شکل زیر خواهد بود:

internal sealed class MiniExcelDocument(Stream stream, IEnumerable<ExcelColumn> columns) : ILargeExcelDocument
{
    private const int SheetLimit = 1_048_576;
    private bool _disposedValue;

    public async Task Write<T>(
        PaginatedEnumerable<T> items,
        int count,
        int sizeLimit,
        CancellationToken cancellationToken = default)
        where T : notnull
    {
        ThrowIfDisposed();
        
        // TODO: apply sizeLimit
        var properties = FastReflection.Instance.GetProperties(typeof(T))
            .ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);

        var sheets = new Dictionary<string, object>();
        var index = 1;
        while (count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();

            IEnumerable<Dictionary<string, object>> reader = items(index, SheetLimit)
                .Select(item =>
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    return columns.ToDictionary(h => h.Title, h => ValueOf(item, h.Name, properties));
                });

            sheets.Add($"sheet_{index}", reader);
            count -= SheetLimit;
            index++;
        }

        // This part is forward-only, and we are pretty sure that streaming will happen without buffering.
        await stream.SaveAsAsync(sheets, cancellationToken: cancellationToken);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            _disposedValue = true;
        }
    }

    ~MiniExcelDocument()
    {
        Dispose(disposing: false);
    }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        Dispose();
        await ValueTask.CompletedTask;
    }

    private void ThrowIfDisposed()
    {
        if (!_disposedValue) return;
        
        throw new ObjectDisposedException(nameof(MiniExcelDocument));
    }
    private static object ValueOf<T>(T record, string prop, IDictionary<string, FastPropertyInfo> properties)
        where T : notnull
    {
        var property = properties[prop] ??
                       throw new InvalidOperationException($"There is no property with given name [{prop}]");

        return NormalizeValue(property.GetValue?.Invoke(record));
    }

    private static object NormalizeValue(object? value)
    {
        if (value == null) return null!;

        return value switch
        {
            DateTime dateTime => dateTime.ToShortPersianDateTimeString(),
            TimeSpan time => time.ToString(@"hh\:mm\:ss"),
            DateOnly dateTime => dateTime.ToShortPersianDateString(false),
            TimeOnly time => time.ToString(@"hh\:mm\:ss"),
            bool boolean => boolean ? "بلی" : "خیر",
            IEnumerable<object> values => string.Join(',', values.Select(NormalizeValue).ToList()),
            Enum enumField => enumField.GetEnumStringValue(),
            _ => value
        };
    }
}

در بدنه متد Write باتوجه به تعداد کل رکوردها، یک کوئری برای هر شیت از طریق فراخوانی متد منتسب به پارامتر items اجرا خواهد شد؛ توجه کنید که اجرای این کوئری مشخصا به تعویق افتاده و تا زمان اولین MoveNext، اجرایی صورت نخواهد گرفت (مفهوم Deferred Execution). به این ترتیب باقی کارها از جمله فرمت کردن مقادیر در سمت برنامه و از طریق Linq To Object انجام خواهد شد. همچنین پیاده‌سازی Factory مرتبط با آن به شکل زیر خواهد بود:

internal sealed class ExcelDocumentFactory : IExcelDocumentFactory
{
    public ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> columns, Stream stream)
    {
        return new MiniExcelDocument(stream, columns);
    }
}

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

public class ExcelExportResult<T>(PaginatedEnumerable<T> items, int count, ExportMetadata metadata) : ActionResult
    where T : notnull
{
    private const string ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    private const string Extension = ".xlsx";
    private const int SizeLimit = int.MaxValue;

    private readonly IReadOnlyList<FastPropertyInfo> _properties = FastReflection.Instance.GetProperties(typeof(T));

    public override async Task ExecuteResultAsync(ActionContext context)
    {
        var sp = context.HttpContext.RequestServices;
        var factory = sp.GetRequiredService<IExcelDocumentFactory>();

        var disposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment);
        disposition.SetHttpFileName(MakeFilename());

        context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = disposition.ToString();
        context.HttpContext.Response.Headers.Append(HeaderNames.ContentType, ContentType);
        context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;

        //TODO: deal with exception, because our global exception handling cannot take into account while the response is started.

        await using var bodyStream = context.HttpContext.Response.BodyWriter.AsStream();
        await context.HttpContext.Response.StartAsync(context.HttpContext.RequestAborted);
        await using (var document = factory.CreateLargeDocument(MakeColumns(), bodyStream))
        {
            await document.Write(items, count, SizeLimit, context.HttpContext.RequestAborted);
        }

        await context.HttpContext.Response.CompleteAsync();
    }

    private string MakeFilename()
    {
        return
            $"{metadata.Title} - {DateTime.UtcNow.ToEpochSeconds()}{Extension}";
    }

    private IEnumerable<ExcelColumn> MakeColumns()
    {
        var types = _properties.ToDictionary(p => p.Name, p => p.PropertyType, StringComparer.OrdinalIgnoreCase);
        return metadata.Fields.Select(f =>
        {
            var type = types[f.Name];

            type = Nullable.GetUnderlyingType(type) ?? type;

            if (type.IsEnum ||
                type == typeof(DateOnly) ||
                type == typeof(TimeOnly) ||
                type == typeof(bool) ||
                type == typeof(TimeSpan) ||
                type == typeof(DateTime))
            {
                type = typeof(string);
            }

            return new ExcelColumn(f.Name, f.Title, type);
        });
    }
}

در اینجا از طریق ExportMetadata که از سمت کاربر تعیین می‌شود، مشخص خواهد شد که کدام فیلدها در فایل نهایی حضور داشته باشند. در بدنه متد ExecuteResultAsync یکسری هدر مرتبط با کار با فایل‌ها تنظیم شده‌است و سپس از طریق BodyWriter و متد AsStream به استریم مورد نظر دست یافته و در اختیار متد Write مربوط به document ایجاد شده، قرار داده‌ایم. یک نمونه استفاده از آن برای موجودیت فرضی مشتری می تواند به شکل زیر باشد:

[ApiController, Route("api/customers")]
public class CustomersController(IDbContext dbContext) : ControllerBase
{
    [HttpGet("export")]
    public async Task<ActionResult> ExportCustomers([FromQuery] ExportMetadata metadata,
        CancellationToken cancellationToken)
    {
        var count = await dbContext.Set<Customer>().CountAsync(cancellationToken);
        return this.Export(
            (page, pageSize) => dbContext.Set<Customer>()
                .OrderBy(c => c.Id)
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .AsNoTracking()
                .AsEnumerable(), // Enable streaming instead of buffering through deferred execution
            count,
            metadata);
    }
}

در اینجا از طریق Extension Method مهیا شده روش کوئری کردن برای هر شیت را مشخص کرده‌ایم؛ نکته مهم در ایجاد استفاده از ‌متد AsEnumerable می باشد که در عمل یک Type Casting انجام می دهد که باقی متدهای استفاده شده روی خروجی، از طریق Linq To Object اعمال شود و همچنین نیاز به استفاده از ToList و یا موارد مشابه را نخواهیم داشت. نمونه درخواست GET برای این API می تواند به شکل زیر باشد:

http://localhost:5118/api/customers/export?Title=Test&Fields[0].Name=FirstName&Fields[0].Title=First name&Fields[1].Name=LastName&Fields[1].Title=Last name&Fields[2].Name=BirthDate&Fields[2].Title=BirthDate

سورس کد مثال قابل اجرا از طریق مخزن زیر قابل دسترس می باشد:

https://github.com/rabbal/large-excel-streaming

در این مثال در زمان آغاز برنامه، ۱۰ میلیون رکورد در جدول Customer ثبت خواهد شد که در ادامه می توان از آن خروجی Excel تهیه کرد.

نکته مهم: توجه داشته باشید که استفاده از این روش قابلیت از سرگیری مجدد برای دانلود را نخواهد داشت و شاید بهتر است این فرآیند را از طریق یک Job انجام داده و با استفاده از قابلیت‌های Multipart Upload مربوط به یک BlobStroage مانند Minio، خروجی مورد نظر از قبل ذخیره کرده و لینک دانلودی را در اختیار کاربر قرار دهید.

مطالب
Image Annotations
می‌خواهیم با تغییر jQuery Image Annotation  این پلاگین و برای asp.net استفاده کنیم

ایجاد دیتابیس
ابتدا یک دیتابیس به نام Coordinates  ایجاد کنید و سپس جدول زیر رو ایجاد کنید
USE [Coordinates]
GO
CREATE TABLE [dbo].[Coords2](
[top] [int] NULL,
[left] [int] NULL,
[width] [int] NULL,
[height] [int] NULL,
[text] [nvarchar](50) NULL,
[id] [uniqueidentifier] NULL,
[editable] [bit] NULL
) ON [PRIMARY]
GO

ایجاد کلاس Coords برای خواندن و ذخیره اطلاعات
public class Coords
{
    public string top;
    public string left;
    public string width;
    public string height;
    public string text;
    public string id;
    public string editable;

    public Coords(string top, string left, string width, string height, string text, string id, string editable)
    {
        this.top = top;
        this.left = left;
        this.width = width;
        this.height = height;
        this.text = text;
        this.id = id;
        this.editable = editable;


    }
}
فرم اصلی برنامه شامل 3 وب سرویس به شرح زیر می‌باشد

1-GetDynamicContext 
این متد در زمان لود اطلاعات از دیتابیس استفاده می‌شود (وقتی که postback صورت می‌گیرد)
[WebMethod]
    public static List<Coords> GetDynamicContext(string entryId, string entryName)
    {
        List<Coords> CoordsList = new List<Coords>();

        string connect = "Connection String";
        using (SqlConnection conn = new SqlConnection(connect))
        {
            string query = "SELECT [top], [left], width, height, text, id, editable FROM  Coords2";
            using (SqlCommand cmd = new SqlCommand(query, conn))
            {
                conn.Open();
                using (SqlDataReader reader=cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        CoordsList.Add(new Coords(reader["top"].ToString(), reader["left"].ToString(),
                                                  reader["width"].ToString(), reader["height"].ToString(),
                                                  reader["text"].ToString(), reader["id"].ToString(),
                                                  reader["editable"].ToString()));
                    }
                }
               
               conn.Close();
            }
        }

         return CoordsList;


    }

2,3 -SaveCoordsو DeleteCoords 
این دو متد هم واسه ذخیره و حذف می‌باشند که نکته خاصی ندارند و خودتون بهینه اش کنید(در فایل ضمیمه موجودند)

تغییر فایل jquery.annotate.js  جهت فراخوانی وب سرویس ها
فقط لازمه که سه قسمت زیر رو در فایل اصلی تغییر بدید
$.fn.annotateImage.ajaxLoad = function (image) {
        ///<summary>
        ///Loads the annotations from the "getUrl" property passed in on the
        ///     options object.
        ///</summary>
      
        $.ajax({
            type: "POST",
            contentType: "application/json; charset=utf-8",
            url: "Default.aspx/GetDynamicContext",
            data: "{'entryId': '" + 1 + "','entryName': '" + 2 + "'}",
            dataType: "json",
            success: function (msg) {
                image.notes = msg.d;
                $.fn.annotateImage.load(image);
            }
        });
};

 $.fn.SaveCoords = function (note) {
        $.ajax({
            type: "POST",
            contentType: "application/json; charset=utf-8",
            url: "Default.aspx/SaveCoords",
            data: "{'top': '" + note.top + "','left': '" + note.left + "','width': '" + note.width + "','height': '" + note.height + "','text': '" + note.text + "','id': '" + note.id + "','editable': '" + note.editable + "'}",
            dataType: "json",
            success: function (msg) {
                note.id = msg.d;
             }
        });
 };
$.fn.annotateView.prototype.edit = function () {
///<summary>
///Edits the annotation.
///</summary>
if (this.image.mode == 'view') {
this.image.mode = 'edit';
var annotation = this;
// Create/prepare the editable note elements
var editable = new $.fn.annotateEdit(this.image, this.note);
$.fn.annotateImage.createSaveButton(editable, this.image, annotation);
// Add the delete button
var del = $('<a>حذف</a>');
del.click(function () {
var form = $('#image-annotate-edit-form form');
$.fn.annotateImage.appendPosition(form, editable)
if (annotation.image.useAjax) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "Default.aspx/DeleteCoords",
// url: annotation.image.deleteUrl,
// data: form.serialize(),
data: "{'id': '" + editable.note.id + "'}",
dataType: "json",
success: function (msg) {
// image.notes = msg.d;
// $.fn.annotateImage.load(image);
},
error: function (e) { alert("An error occured deleting that note.") }
});
}
annotation.image.mode = 'view';
editable.destroy();
annotation.destroy();
});
editable.form.append(del);
$.fn.annotateImage.createCancelButton(editable, this.image);
}
};
این پروژه شامل یه سری فایل css هم هست که می‌تونید کل پروژه رو از  اینجا دانلود کنید
مطالب
ایجاد Helper سفارشی جهت نمایش ویدئو در ASP.NET MVC
معرفی HTML Helpers
در صورتی که در مورد Helper‌ها در ASP.NET MVC اطلاعات بیشتری نیاز دارید پیشنهاد می‌کنم ابتدا این مطلب + را مطالعه کنید.
 
ایجاد یک HTML Helper سفارشی برای نمایش ویدئو‌های سایت آپارات
قبل از ایجاد هر Helper ی باید با خروجی نهایی آن آشنا بود. پس از بررسی خروجی نهایی کافیست بخش هایی از آن را Optional کنیم تا در زمان استفاده از آن، نسبت به مقادیر این دسته از پارامترها تصمیم گیری کنیم.
سایت آپارات نمونه‌ی فارسی سایت YouTube است و امکان اشتراک گذاری فایل‌های ویدئویی را برای کاربران مهیا کرده است. در این سایت در صفحه نمایش هر ویدئو، بخشی با عنوان "دریافت کد ویدئو" وجود دارد. با کمی بررسی در کدهای نهایی ایجاد شده توسط دستورات ذکر شده در این بخش، کد نهایی برای نمایش یک ویدئو به صورت زیر خواهد بود:

<embed height="400" width="500" 
flashvars="config=http://www.aparat.com//video/video/config/videohash/BA9Md/watchtype/embed" 
allowfullscreen="true" 
quality="high" name="aparattv_BA9Md" id="aparattv_BA9Md" 
src="http://host10.aparat.com/public/player/aparattv" type="application/x-shockwave-flash">
برای تولید پویای این کد Helper زیر می‌تواند مفید باشد:
using System.Web.Mvc;

namespace MvcApplication1
{
    public static class AparatPlayerHelper
    {
        public static MvcHtmlString AparatPlayer(this HtmlHelper helper, string mediafile, int height, int width)
        {
            var player = @"<embed height=""{0}"" width=""{1}"" flashvars=""config=http://www.aparat.com//video/video/config/videohash/{2}/watchtype/embed"" 
                                allowfullscreen=""true"" 
                                quality=""high"" 
                                name=""aparattv_{2}"" id=""aparattv_{2}"""" src=""http://host10.aparat.com/public/player/aparattv"" 
                                type=""application/x-shockwave-flash"">";

            player = string.Format(player, height, width, mediafile);
            return new MvcHtmlString(player);
        }
    }
}
نحوه استفاده از این Helper:
@Html.AparatPlayer("BA9Md", 400, 500)
اگر به آدرس صفحات در سایت آپارات دقت کنید URL جاری به یک عبارت چند حرقی ختم می‌شود که از این عبارت به عنوان مقدار پارامتر mediafile و شناسه منحصر بفرد فایل ویدئو استفاده شده است.
ایجاد Helper سفارشی جهت نمایش ویدئو‌های YouTube:
با توجه به توضیحات فوق یک Helper سفارشی برای نمایش ویدئو‌های YouTube بصورت زیر است:
using System;
using System.Drawing;
using System.Web.Mvc;

namespace MvcApplication1
{

    public static class YouTubePlayerHelper
    {
        public static MvcHtmlString YouTubePlayer(this HtmlHelper helper, string playerId, string mediaFile, YouTubePlayerOption youtubePlayerOption)
        {

            const string baseURL = "http://www.youtube.com/v/";

            // YouTube Embedded Code
            var player = @"<div id=""YouTubePlayer_{7}""width:{1}px; height:{2}px;"">
                                 <object width=""{1}"" height=""{2}"">
                                 <param name=""movie"" value=""{6}{0}&fs=1&border={3}&color1={4}&color2={5}""></param>
                                 <param name=""allowFullScreen"" value=""true""></param>
                                 <embed src=""{6}{0}&fs=1&border={3}&color1={4}&color2={5}""
                                 type = ""application/x-shockwave-flash""
                                 width=""{1}"" height=""{2}"" allowfullscreen=""true""></embed>
                                 </object>
                             </div>";

            // Replace All The Value
            player = String.Format(player, mediaFile, youtubePlayerOption.Width, youtubePlayerOption.Height, (youtubePlayerOption.Border ? "1" : "0"), ConvertColorToHexa.ConvertColorToHexaString(youtubePlayerOption.PrimaryColor), ConvertColorToHexa.ConvertColorToHexaString(youtubePlayerOption.SecondaryColor), baseURL, playerId);

            //Retrun Embedded Code
            return new MvcHtmlString(player);
        }
    }

    public class YouTubePlayerOption
    {
        int _width = 425;
        int _height = 355;
        Color _color1 = Color.Black;
        Color _color2 = Color.Aqua;

        public YouTubePlayerOption()
        {
            Border = false;
        }

        public int Width { get { return _width; } set { _width = value; } }
        public int Height { get { return _height; } set { _height = value; } }
        public Color PrimaryColor { get { return _color1; } set { _color1 = value; } }
        public Color SecondaryColor { get { return _color2; } set { _color2 = value; } }
        public bool Border { get; set; }
    }

    public class ConvertColorToHexa
    {
        private static readonly char[] HexDigits =
            {
                '0', '1', '2', '3', '4', '5', '6', '7',
                '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
            };

        public static string ConvertColorToHexaString(Color color)
        {
            var bytes = new byte[3];
            bytes[0] = color.R;
            bytes[1] = color.G;
            bytes[2] = color.B;
            var chars = new char[bytes.Length * 2];
            for (int i = 0; i < bytes.Length; i++)
            {
                int b = bytes[i];
                chars[i * 2] = HexDigits[b >> 4];
                chars[i * 2 + 1] = HexDigits[b & 0xF];
            }
            return new string(chars);
        }


    }        

}
نحوه استفاده:
@Html.YouTubePlayer("Casablanca", "iLdqKUkkM6w", new YouTubePlayerOption()
                                {
                                    Border = true
                                })
در اینجا جهت مقداردهی پارمتر‌های پخش کننده ویدئو، از کلاس YouTubePlayerOption استفاده شده است.
مطالب
C# 8.0 - Using declarations
یکی دیگر از ویژگی‌های جدید C# 8.0، پشتیبانی از using declarations (اعلان‌های using) در مقابل using statements (عبارات using) پیشین است که سبب می‌شود بتوان کدهای کمتری را برای تعریف آن‌ها نوشت.


مثالی از using declarations

تا پیش از C# 8.0، روش متداول کار با عبارات using به صورت زیر است و به آن استفاده از using statements گفته می‌شود:
    class Program
    {
        static void UsingOld()
        {
            using (var file = new FileStream("input.txt", FileMode.Open))
            using (var reader = new StreamReader(file))
            {
                var s = reader.ReadToEnd();

                // Do something with data
            }
        }
که در نهایت پس از پایان این قطعه کد، هر دو شیء file و reader به صورت خودکار Dispose می‌شوند.
اکنون در C# 8.0 می‌توان قطعه کد فوق را به کمک using declarations به صورت زیر خلاصه کرد:
    class Program
    {
        static void UsingNew(string[] args)
        {
            using Stream file = new FileStream("input.txt", FileMode.Open);
            using StreamReader reader = new StreamReader(file);

            var s = reader.ReadToEnd();

            // Do something with data
        }
که در اینجا پرانتزها و همچنین {} ها، حذف شده‌اند.


میدان دید using declarations

پس از این تغییرات، سؤال مهمی که مطرح می‌شود این است: متغیرهایی که توسط using declaration تعریف می‌شوند، تا چه زمانی زنده نگه داشته می‌شوند. به عبارتی متد UsingOldScope آیا همانند متد UsingNewScope عمل می‌کند؟ آیا متغیر buffer آن همانند متد UsingOldScope خارج از میدان دید usingها قرار می‌گیرد؟
    class Program
    {
        static void UsingNewScope()
        {
            string buffer = null;
            using Stream file = new FileStream("input.txt", FileMode.Open);
            using StreamReader reader = new StreamReader(file);

            buffer = reader.ReadToEnd();

            // Do something with data

            buffer = null;
        }

        static void UsingOldScope(string[] args)
        {
            string buffer = null;

            using (var file = new FileStream("input.txt", FileMode.Open))
            using (var reader = new StreamReader(file))
            {
                buffer = reader.ReadToEnd();
            }

            // Do something with data

            buffer = null;
        }
زمانیکه از using statements استفاده می‌شود (مانند متد UsingOldScope)، توسط آن یک scope نیز تعریف می‌شود (داخل {} ها) که در پایان آن، کار فراخوانی متد Dispose اشیاء IDisposable ارجاعی، به صورت خودکار انجام می‌شود. این فراخوانی نیز توسط کامپایلر در داخل یک قطعه کد try/finally صورت می‌گیرد تا حتی اگر در این بین استثنائی نیز رخ داد، حتما متد Dispose فراخوانی گردد.
اما زمانیکه از using declarations استفاده می‌شود (مانند متد UsingNewScope)، دیگر این {} را نداریم. اینبار scope تعریف شده، تا «پایان متد» ادامه پیدا می‌کند و سپس متد Dispose اشیاء ارجاعی، فراخوانی می‌گردد. بدیهی است در اینجا نیز همانند قبل، همان قطعه کد try/finally توسط کامپایلر جهت فراخوانی متد Dispose، تشکیل خواهد شد. بنابراین اگر بخواهیم متد UsingNewScope را توسط using statements پیشین بازنویسی کنیم، به یک چنین قطعه کدی خواهیم رسید که scope پس از using declarations، تا آخر متد ادامه پیدا می‌کند:
    string buffer = null; 
    using (var file = new FileStream("input.txt", FileMode.Open)) 
    { 
        using (var reader = new StreamReader(file)) 
        { 
            buffer = reader.ReadToEnd(); 
            buffer = null; 
        } 
    }


سؤال: آیا امکان محدود کردن میدان دید using declarations وجود دارد؟

پاسخ: بله. می‌توان با تعریف یک {}، میدان دید متغیرهای ارجاعی توسط using declarations را محدود کرد:
private static void UsingDeclarationWithScope()
{
    {
        using var r1 = new AResource();
        r1.UseIt();
    }  // r1 is disposed here!
    Console.WriteLine("r1 is already disposed");
}
در اینجا جائیکه {} بسته می‌شود، متغیر r1 از میدان دید خارج شده و بلافاصله Dispose خواهد شد.


سؤال: آیا using declarations تمام قابلیت‌های using statements را ارائه می‌دهند؟

پاسخ: خیر. فرض کنید کلاس AResource از نوع IDisposable تعریف شده‌است:
    public class AResource : IDisposable
    {
        public void UseIt() => Console.WriteLine(nameof(UseIt));
        public void Dispose() => Console.WriteLine($"Dispose {nameof(AResource)}");
    }
و سپس متدی، وهله‌ای از این کلاس را باز می‌گرداند:
    class Program
    {
        public static AResource GetTheResource() => new AResource();
با استفاده از using statements، نوشتن چنین قطعه کدی بدون تعریف متغیری مجاز است:
using (GetTheResource())
{
   // do something here
}  // resource is disposed here
اما اگر اینکار را توسط using declarations انجام دهیم، به چندین خطای کامپایلر خواهیم رسید:
using GetTheResource(); // Compiler error
علت اینجا است که برخلاف using statements، ذکر متغیرهای scope برای using declarations اجباری است. برای رفع آن می‌توان از یک discard استفاده کرد:
using var _ = GetTheResource(); // Works fine
مطالب
آزمون واحد Entity Framework به کمک چارچوب تقلید
در باب ضرورت نوشتن کدهای تست پذیر، توسعه کلاس‌های کوچک تک مسئولیتی و اهمیت تزریق وابستگی‌ها بارها و بارها بحث شده و مطلب نوشته شده است. این روز‌ها کم پیش میاید که نرم افزاری توسعه داده شود و از پایگاه داده به جهت ذخیره و بازیابی داده‌ها استفاده نکند. با گسترش و رواج ORM ها، نوشتن کدهای دسترسی به داده‌ها سهولت یافته است و استفاده از ORM در لایه‌ی سرویس که نگهدارنده‌ی منطق تجاری برنامه است، امری اجتناب ناپذیر می‌باشد. 
در این مطلب نحوه‌ی نوشتن آزمون واحد برای کلاس سرویسی که وابسته به DbContext می‌باشد، به همراه محدودیت‌ها شرح داده می‌شود.
ابتدا یک روش که که در آن مستقیما از DbContext در سرویس استفاده شده را بررسی میکنیم. در مثال زیر کلاس ProductService وظیفه‌ی برگرداندن لیست کالاها را به ترتیب نام دارد. در آن DbContext مستقیما وهله سازی شده و از آن جهت انجام تراکنش‌های دیتابیس کمک گرفته شده است:
    public class ProductService
    {
        public IEnumerable<Product> GetOrderedProducts()
        {
            using (var ctx = new Entites())
            {
                return ctx.Products.OrderBy(x => x.Name).ToList();
            }
        }
    }

برای این کلاس نمی‌توان Unit Test نوشت چرا که یک وابستگی به شی DbContext دارد و این وابستگی مستقیما درون متد GetOrderedProducts  نمونه سازی شده است. در مطالب پیشین شرح داده شد که برای تست پذیر کردن کدها باید این وابستگی‌ها را از بیرون، در اختیار کلاس مورد نظر قرار داد.
برای نوشتن تست برای کلاس ProductService حداقل دو روش در اختیار است:
- نوشتن Integration Test:
یعنی کلاس جاری را به همین شکل نگاه داریم و در تست، مستقیما به یک پایگاه داده که به منظور تست فراهم شده وصل شویم. برای سهولت مدیریت پایگاه داده می‌توان عمل درج را در یک Transaction قرار داد و پس از پایان یافتن تست Transaction را RollBack کرد. این روش مورد بحث مطلب جاری نمی‌باشد، لطفا برای آشنایی این دو مطلب را مطالعه بفرمایید:
- بهره جستن از تزریق وابستگی و نوشتن Unit Test که وابستگی به دیتابیس ندارد
یکی از قانون‌های یک آزمون واحد این است که وابستگی به منابع خارجی مثل پایگاه داده نداشته باشد. این مطلب نحوه‌ی صحیح پیاده سازی الگوی Unit of Work را شرح داده است. بعد از پیاده سازی Unit Of Work، کلاس DbContext به شرح زیر می‌شود. همانطور که مشاهده می‌کنید، اکنون DbContext یک Interface را پیاده سازی کرده است.
    public interface IUnitOfWork
    {
        IDbSet<TEntity> Set<TEntity>() where TEntity : class;
        int SaveAllChanges();
    }

    public class Entites : DbContext, IUnitOfWork
    {
        public virtual DbSet<Product> Products { get; set; }  // This is virtual because Moq needs to override the behaviour 

        public new virtual IDbSet<TEntity> Set<TEntity>() where TEntity : class   // This is virtual because Moq needs to override the behaviour 
        {
            return base.Set<TEntity>();
        }

        public int SaveAllChanges()
        {
            return base.SaveChanges();
        }
    }
در این حالت می‌توان به جای وهله سازی مستقیم DbContext در ProductService آن را خارج از کلاس سرویس در اختیار استفاده کننده قرار داد:
    public class ProductService
    {
        private readonly IDbSet<Product> _products;
        private readonly IUnitOfWork _uow;
        public ProductService(IUnitOfWork uow)
        {
            _uow = uow;
            _products = _uow.Set<Product>();
        }
     public IEnumerable<Product> GetOrderedProducts()
        {
            return _products.OrderBy(x => x.Name).ToList();
        }
    }
همانطور که مشاهده می‌کنید، الان IUnitOfWork به کلاس سرویس تزریق شده و در متدها، خبری از وهله سازی یک وابستگی (DbContext) نمی‌باشد.
اکنون برای تست این سرویس می‌توان پیاده سازی دیگری را از IUnitOfWork انجام داد و در کدهای تست به سرویس مورد نظر تزریق کرد. برای سهولت این امر قصد داریم از moq به عنوان  چارچوب تقلید (Mocking framework) استفاده کنیم. برای  نصب moq  می توان از  بسته‌ی نیوگت آن بهره جست. پیشتر  مطلبی  در رابطه با چارچوب‌های تقلید در سایت نوشته شده است.
با توجه به اینکه PoductService به دیتابیس وابستگی دارد، مقصود این است که این وابستگی با ایجاد یک نمونه‌ی mock از IUnitOfWork حذف شود. برای این منظور در سازنده‌ی کلاس، تعدادی کالای درون حافظه ایجاد شده و به صورت IQueryable جایگزین DbSet شده است.
اگر به تعریف کلاس Entities که همان DbContext می‌باشد دقت کنید، مشاهده می‌شود که Products و تابع Set، هر دو به صورت Virtual تعریف شده اند. برای تغییر رفتار DbContext نیاز است در آزمون واحد، این دو با داده‌های درون حافظه کار کنند و رفتار آنها قرار است عوض شود. این تغییر رفتار از طریق چند ریختی (Polymorphism) خواهد بود.
کلاس تست در نهایت اینگونه تعریف می‌شود:
   [TestFixture]
    public class ProductServiceTest
    {
        private readonly ProductService _productService;
        public ProductServiceTest()
        {
            IQueryable<Product> data = GetRoadNetworks().AsQueryable();
            var mockSet = new Mock<DbSet<Product>>();
            mockSet.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(data.Provider);
            mockSet.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(data.Expression);
            mockSet.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(data.ElementType);
            mockSet.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
            var context = new Mock<Entites>();
            context.Setup(c => c.Products).Returns(mockSet.Object);
            context.Setup(m => m.Set<Product>()).Returns(mockSet.Object);
            _productService = new ProductService(context.Object);
        }
        private IEnumerable<Product> GetRoadNetworks()
        {
            return new List<Product>
            {
                new Product
                {
                    Id = 1,
                    Name = "A"
                },
                new Product
                {
                    Id = 2,
                    Name = "B"
                },
                new Product
                {
                    Id = 3,
                    Name = "C"
                }
            };
        }
        [Test]
        public void GetOrderedProductTest()
        {
            IEnumerable<Product> products = _productService.GetOrderedProducts();
            List<string> names = products.Select(x => x.Name).ToList();
            var expected = new List<string> {"A", "B", "C"};
            CollectionAssert.AreEqual(names, expected);
        }
    }
همانطور که مشاهده می‌شود، در سازنده‌ی کلاس تست، یک منبع داده‌ی درون حافظه‌ای به صورت IQueryable تولید شده و پیاده سازی‌های تقلیدی از DbContext به همراه تابع Set و همچنین DbSet کالا‌ها به کمک Moq ایجاد گردیده و در اختیار ProductService قرار داده شده است.
در نهایت، در یک تست تلاش شده است تا منطق متد GerOrderedProducts مورد آزمون قرار گیرد.
محدودیت این روش:
با اینکه LINQ یک روش و سینتکس یکتا برای دسترسی به منابع داده‌ای مختلف را محیا می‌کند، اما این الزامی برای یکسان بودن نتایج، هنگام استفاده از Provider‌های مختلف LINQ نمی‌باشد. در تست نوشته شده از LINQ To Objects برای کوئری گرفتن از منبع داده استفاده شده است؛ در صورتیکه در برنامه‌ی اصلی از LINQ To Entities استفاده می‌شود و الزامی نیست که یک کوئری LINQ در دو Provider متفاوت یک رفتار را داشته باشد.
این نکته در قسمت Limitations of EF in-memory test doubles این مطلب هم شرح داده شده است.
در نهایت این پرسش به وجود می‌آید که با وجود محدودیت ذکر شده، از این روش استفاده شود یا خیر؟ پاسخ این پرسش، بسته به هر سناریو، متفاوت است.
به عنوان نمونه اگر در یک سناریو داده‌ها با یک کوئری نه چندان پیچیده از منبع داده ای گرفته می‌شود و اعمال دیگری دیگری روی نتیجه‌ی کوئری درون حافظه انجام می‌شود می‌توان این روش را قابل اعتماد قلمداد کرد.
برای مطالعه‌ی بیشتر مطالب متعددی در سایت در رابطه با تزریق وابستگی و آزمون‌های واحد نوشته شده است.