مطالب
نحوه‌ی نگاشت فیلدهای فرمول در Fluent NHibernate

اگر با SQL Server کار کرده باشید حتما با مفهوم و امکان Computed columns (فیلدهای محاسبه شده) آن آشنایی دارید. چقدر خوب می‌شد اگر این امکان برای سایر بانک‌های اطلاعاتی که از تعریف فیلدهای محاسبه شده پشتیبانی نمی‌کنند، نیز مهیا می‌شد. زیرا یکی از اهداف مهم استفاده‌ی صحیح از ORMs ، مستقل شدن برنامه از نوع بانک اطلاعاتی است. برای مثال امروز می‌خواهیم با MySQL‌ کار کنیم، ماه بعد شاید بخواهیم یک نسخه‌ی سبک‌تر مخصوص کار با SQLite را ارائه دهیم. آیا باید قسمت دسترسی به داده برنامه را از نو بازنویسی کرد؟ اینکار در NHibernate فقط با تغییر نحوه‌ی اتصال به بانک اطلاعاتی میسر است و نه بازنویسی کل برنامه (و صد البته شرط مهم و اصلی آن هم این است که از امکانات ذاتی خود NHibernate استفاده کرده باشید. برای مثال وسوسه‌ی استفاده از رویه‌های ذخیره شده را فراموش کرده و به عبارتی ORM مورد استفاده را به امکانات ویژه‌ی یک بانک اطلاعاتی گره نزده باشید).
خوشبختانه در NHibernate امکان تعریف فیلدهای محاسباتی با کمک تعریف نگاشت خواص به صورت فرمول مهیا است. برای توضیحات بیشتر لطفا به مثال ذیل دقت بفرمائید:
در ابتدا کلاس کاربر تعریف می‌شود:

using System;
using NHibernate.Validator.Constraints;

namespace FormulaTests.Domain
{
public class User
{
public virtual int Id { get; set; }

[NotNull]
public virtual DateTime JoinDate { set; get; }

[NotNullNotEmpty]
[Length(450)]
public virtual string FirstName { get; set; }

[NotNullNotEmpty]
[Length(450)]
public virtual string LastName { get; set; }

[Length(900)]
public virtual string FullName { get; private set; } //از طریق تعریف فرمول مقدار دهی می‌گردد

public virtual int DayOfWeek { get; private set; }//از طریق تعریف فرمول مقدار دهی می‌گردد
}
}
در این کلاس دو خاصیت FullName و DayOfWeek به صورت فقط خواندنی به کمک private set ذکر شده، تعریف گردیده‌اند. قصد داریم روی این دو خاصیت فرمول تعریف کنیم:

using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;

namespace FormulaTests.Domain
{
public class UserCustomMappings : IAutoMappingOverride<User>
{
public void Override(AutoMapping<User> mapping)
{
mapping.Id(u => u.Id).GeneratedBy.Identity(); //ضروری است
mapping.Map(x => x.DayOfWeek).Formula("DATEPART(dw, JoinDate) - 1");
mapping.Map(x => x.FullName).Formula("FirstName + ' ' + LastName");
}
}
}
نحوه‌ی انتساب فرمول‌های مبتنی بر SQL را در نگاشت فوق ملاحظه می‌نمائید. برای مثال FullName از جمع دو فیلد نام و نام خانوادگی حاصل خواهد شد و DayOfWeek از طریق فرمول SQL دیگری که ملاحظه می‌نمائید (یا هر فرمول SQL دلخواه دیگری که صلاح می‌دانید).
اکنون اگر Fluent NHibernate را وادار به تولید اسکریپت متناظر با این دو کلاس کنیم حاصل به صورت زیر خواهد بود:
    create table Users (
UserId INT IDENTITY NOT NULL,
JoinDate DATETIME not null,
FirstName NVARCHAR(450) not null,
LastName NVARCHAR(450) not null,
primary key (UserId)
)
همانطور که ملاحظه می‌کنید در اینجا خبری از دو فیلد محاسباتی تعریف شده نیست. این فیلدها در تعاریف نگاشت‌ها به صورت خودکار ظاهر می‌شوند:
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
default-access="property" auto-import="true" default-cascade="none" default-lazy="true">
<class xmlns="urn:nhibernate-mapping-2.2" mutable="true"
name="FormulaTests.Domain.User, FormulaTests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" table="Users">
<id name="Id" type="System.Int32" unsaved-value="0">
<column name="UserId" />
<generator class="identity" />
</id>
<property name="DayOfWeek" formula="DATEPART(dw, JoinDate) - 1" type="System.Int32" />
<property name="FullName" formula="FirstName + ' ' + LastName" type="System.String" />
<property name="JoinDate" type="System.DateTime">
<column name="JoinDate" />
</property>
<property name="FirstName" type="System.String">
<column name="FirstName" />
</property>
<property name="LastName" type="System.String">
<column name="LastName" />
</property>
</class>
</hibernate-mapping>
اکنون اگر کوئری زیر را در برنامه اجرا نمائیم:
var list = session.Query<User>.ToList();
foreach (var item in list)
{
Console.WriteLine("{0}:{1}", item.FullName, item.DayOfWeek);
}
به صورت خودکار به SQL ذیل ترجمه خواهد شد و اکنون نحوه‌ی بکارگیری فیلدهای فرمول، بهتر مشخص می‌گردد:
select
user0_.UserId as UserId0_,
user0_.JoinDate as JoinDate0_,
user0_.FirstName as FirstName0_,
user0_.LastName as LastName0_,
DATEPART(user0_.dw, user0_.JoinDate) - 1 as formula0_, --- همان فرمول تعریف شده است
user0_.FirstName + ' ' + user0_.LastName as formula1_ ---از طریق فرمول تعریف شده حاصل گردیده است
from
Users user0_

مطالب
استفاده از افزونه‌های Owin مخصوص سایر نگارش‌های ASP.NET در ASP.NET Core
همانطور که اطلاع دارید یکسری از کتابخانه‌های کمکی و ثالث ASP.NET Core همچون OData و SignalR ، Thinktecture IdentityServer هنوز در حال تکمیل هستند و از آنجایی که هر روزه محبوبیت ASP.NET Core در بین برنامه نویسان در حال افزایش است و خیلی از پروژه‌های نرم افزاری که امروزه start میخورند، از این فریم ورک جدید استفاده میکنند، پس خیلی به اهمیت این مقوله افزوده میشود که بتوان از تکنولوژی‌های فوق در پروژه‌های جدید نیز استفاده کرد و یکی از معقول‌ترین راه‌های ممکن آن، پیاده سازی Owin  در کنار ASP.NET Core Pipeline میباشد. بدین معنا که ما قصد نداریم owin pipeline را جایگزین آن نماییم؛ همانطور که در ادامه‌ی این مقاله گفته خواهد شد، از هر دو معماری (افزونه‌های Owin مخصوص نگارش کامل دات نت و همچنین خود ASP.NET Core) در کنار هم استفاده خواهیم کرد.
پیشنیاز این مقاله آشنایی با ASP.NET Core و MiddleWare و همچنین آشنایی با Owin میباشد. همانطور که عرض کردم اگر مقاله‌ی پیاده سازی OData را مطالعه کرد‌ه‌اید و هم اکنون قصد و یا علاقه‌ی به استفاده‌ی از آن را در پروژه‌ی ASP.NET Core خود دارید، میتوانید این مقاله را دنبال نمایید.
پس تا کنون متوجه شدیم که هدف از این کار، توانایی استفاده از ابزارهایی است که با Owin سازگاری کامل را دارند و همچنین به نسخه‌ی پایدار خود رسیده‌اند. بطور مثال IdentityServer 4 در حال تهیه است که با ASP.NET Core سازگار است. اما هنوز چند نسخه‌ی beta از آن بیرون آمده و به پایداری کامل نرسیده است. همچنین زمان توزیع نهایی آن نیز دقیقا مشخص نیست.
شما برای استفاده از ASP.NET Core RTM 1.0 احتیاج به Visual studio 2015 update 3 دارید. ابتدا یک پروژه‌ی #C از نوع Asp.Net Core Web Application  را «به همراه full .Net Framework» به نام OwinCore میسازیم.
در حال حاضر بدلیل اینکه پکیج‌هایی مانند OData, SignalR هنوز به نسخه‌ی Net Coreی خود آماده نشده‌اند (در حال پیاده سازی هستند)، پس مجبور به استفاده از full .Net framework هستیم و خوب مسلما برنامه در این حالت چند سکویی نخواهد بود. اما به محض اینکه آن‌ها آماده شدند، میتوان full .Net را کنار گذاشته و از NET Core. استفاده نمود و از آنجایی که Owin فقط یک استاندارد هست، هیچ مشکلی با NET Core. نداشته و برنامه را میتوان بدان منتقل کرد و از مزایای چند سکویی بودن آن نیز بهره برد. پس مشکل در حال حاضر Owin نبوده و مجبور به منتظر بودن برای آماده شدن این پکیج‌ها خواهیم بود. مثال عملی این قضیه نیز Nancy است که نسخه‌ی NET Core.ی آن با استفاده از Owin در ASP.NET Core پیاده سازی شده است. در اینجا مثال عملی آن را میتوانید پیدا کنید.


در قسمت بعد، قالب را هم از نوع empty انتخاب مینماییم.


در ادامه فایل project.json را باز کرده و در قسمت dependencies، تغییرات زیر را اعمال نمایید.

قبل از اینکه شما را از این همه وابستگی نگران کنم، باید عرض کنم فقط Microsoft.Owin , Microsoft.AspNetCore.Owin، پکیج‌های اجباری هستند؛ باقی آن‌ها برای نشان دادن انعطاف پذیری بالای این روش میباشند:

"dependencies": {
    "Microsoft.AspNet.OData": "5.9.1",
    "Microsoft.AspNet.SignalR": "2.2.1",
    "Microsoft.AspNet.WebApi.Client": "5.2.3",
    "Microsoft.AspNet.WebApi.Core": "5.2.3",
    "Microsoft.AspNet.WebApi.Owin": "5.2.3",
    "Microsoft.AspNetCore.Diagnostics": "1.0.0",
    "Microsoft.AspNetCore.Hosting": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.AspNetCore.Owin": "1.0.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Net.Http": "2.2.29",
    "Microsoft.Owin": "3.0.1",
    "Microsoft.Owin.Diagnostics": "3.0.1",
    "Microsoft.Owin.FileSystems": "3.0.1",
    "Microsoft.Owin.StaticFiles": "3.0.1",
    "Newtonsoft.Json": "9.0.1"
  },
//etc...

بعد از ذخیره کردن این فایل، در پنجره‌ی Output خود شاهد دانلود شدن این پکیج‌ها خواهید بود. در اینجا پکیج‌های مربوط به Owin, Odata, SignalR را مشاهد می‌کنید. ضمن اینکه در کنار آن، AspNetCore.Mvc را نیز مشاهده میفرمایید. دلیل این کار این است که این دو نوع متفاوت قرار است در کنار هم کار کنند و هیچ مشکلی با دیگری ندارند.

در مسیر اصلی پروژه‌ی خود کلاسی به نام OwinExtensions را با محتوای زیر بسازید:

namespace OwinCore
{
    public static class OwinExtensions
    {
        public static IApplicationBuilder UseOwinApp(
            this IApplicationBuilder aspNetCoreApp,
            Action<IAppBuilder> configuration)
        {
            return aspNetCoreApp.UseOwin(setup => setup(next =>
            {
                AppBuilder owinAppBuilder = new AppBuilder();

                IApplicationLifetime aspNetCoreLifetime = (IApplicationLifetime)aspNetCoreApp.ApplicationServices.GetService(typeof(IApplicationLifetime));

                AppProperties owinAppProperties = new AppProperties(owinAppBuilder.Properties);

                owinAppProperties.OnAppDisposing = aspNetCoreLifetime?.ApplicationStopping ?? CancellationToken.None;

                owinAppProperties.DefaultApp = next;

                configuration(owinAppBuilder);

                return owinAppBuilder.Build<Func<IDictionary<string, object>, Task>>();
            }));
        }
    }
}

یک Extension Method به نام UseOwinApp اضافه شده به IApplicationBuilder که مربوط به ASP.NET Core میباشد و درون آن نیز AppBuilder را که مربوط به Owin pipeline میباشد، نمونه سازی کرده‌ایم که باعث میشود Owin pipeline بر روی ASP.NET Core pipeline سوار شود.

حال میخواهیم یک Middleware سفارشی را با استفاده از Owin نوشته و در Startup پروژه، آن را فراخوانی نماییم. کلاسی به نام AddSampleHeaderToResponseHeadersOwinMiddleware را با محتوای زیر تولید مینماییم:

namespace OwinCore
{
    public class AddSampleHeaderToResponseHeadersOwinMiddleware : OwinMiddleware
    {
        public AddSampleHeaderToResponseHeadersOwinMiddleware(OwinMiddleware next)
            : base(next)
        {
        }
        public async override Task Invoke(IOwinContext context)
        {
            //throw new InvalidOperationException("ErrorTest");

            context.Response.Headers.Add("Test", new[] { context.Request.Uri.ToString() });

            await Next.Invoke(context);
        }
    }
}

کلاسی است که از owinMiddleware ارث بری کرده و در متد override شده‌ی Invoke نیز با استفاده از IOwinContext، به پیاده سازی Middleware خود میپردازیم. Exception مربوطه را comment کرده (بعدا در مرحله‌ی تست از آن نیز استفاده مینماییم) و در خط بعدی در هدر response هر request، یک شیء را به نام Test و با مقدار Uri آن request، میسازیم.

خط بعدی هم اعلام میدارد که به Middleware بعدی برود.

در ادامه فایل Startup.cs را باز کرده و اینگونه متد Configure را تغییر دهید:

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }

                owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });
        }

مشاهده میفرمایید با استفاده از UserOwinApp میتوانیم Middleware‌های Owinی خود را register نماییم و نکته‌ی قابل توجه این است که در کنار آن نیز می‌توانیم از IHostingEnviroment مربوط به ASP.NET core استفاده نماییم. owinApp.UseErrorPage از Microsoft.Owin.Diagnostics گرفته شده است و در خط بعدی نیز Middleware شخصی خود را register کرده‌ایم. پروژه را run کرده و در response این را مشاهد مینمایید.

اکنون اگر در Middleware سفارشی خود، آن Exception را از حالت comment در بیاوریم، در صورتیکه در حالت development باشیم، با این صفحه مواجه خواهیم شد:

Exception مربوطه را به حالت comment گذاشته و ادامه میدهیم.

برای اینکه نشان دهیم Owin و ASP.NET Core pipeline در کنار هم میتوانند کار کنند، یک Middleware را از نوع ASP.NET Core نوشته و آن را register مینماییم. کلاسی جدیدی را به نام AddSampleHeaderToResponseHeadersAspNetCoreMiddlware با محتوای زیر میسازیم:

namespace OwinCore
{
    public class AddSampleHeaderToResponseHeadersAspNetCoreMiddlware
    {
        private readonly RequestDelegate Next;

        public AddSampleHeaderToResponseHeadersAspNetCoreMiddlware(RequestDelegate next)
        {
            Next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            //throw new InvalidOperationException("ErrorTest");

            context.Response.Headers.Add("Test2", new[] { "some text" });

            await Next.Invoke(context);
        }
    }
}

متد Configure در Startup.cs را نیز اینگونه تغییر میدهیم

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }

                owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });

            aspNetCoreApp.UseMiddleware<AddSampleHeaderToResponseHeadersAspNetCoreMiddlware>();
        }

اکنون AddSampleHeaderToResponseHeadersAspNetCoreMiddlware رجیستر شده است و بعد از run کردن پروژه و بررسی header response باید این را ببینیم

میبینید که به ترتیب اجرای Middleware‌ها، ابتدا Test مربوط به Owin و بعد آن Test2 مربوط به ASP.NET Core تولید شده است.

حال اجازه دهید Odata را با استفاده از Owin پیاده سازی نماییم. ابتدا کلاسی را به نام Product با محتوای زیر تولید نمایید:

namespace OwinCore
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

حال کلاسی را به نام ProductsController با محتوای زیر میسازیم:

namespace OwinCore
{
    public class ProductsController : ODataController
    {
        [EnableQuery]
        public IQueryable<Product> Get()
        {
            return new List<Product>
            {
                 new Product { Id = 1, Name = "Test" , Price = 10 }
            }
            .AsQueryable();
        }
    }
}

اگر مقاله‌ی پیاده سازی Crud با استفاده از OData را مطالعه کرده باشید، قاعدتا با این کد‌ها آشنا خواهید بود. ضمن اینکه پرواضح است که OData هیچ وابستگی به entity framework ندارد.

برای config آن نیز در Startup.cs پروژه و متد Configure، تغییرات زیر را اعمال مینماییم.

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            //aspNetCoreApp.UseMvc();

            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }
                // owinApp.UseFileServer(); as like as asp.net core static files middleware
                // owinApp.UseStaticFiles(); as like as asp.net core static files middleware
                // owinApp.UseWebApi(); asp.net web api / odata / web hooks

                HttpConfiguration webApiConfig = new HttpConfiguration();
                ODataModelBuilder odataMetadataBuilder = new ODataConventionModelBuilder();
                odataMetadataBuilder.EntitySet<Product>("Products");
                webApiConfig.MapODataServiceRoute(
                    routeName: "ODataRoute",
                    routePrefix: "odata",
                    model: odataMetadataBuilder.GetEdmModel());
                owinApp.UseWebApi(webApiConfig);

                owinApp.MapSignalR();

                //owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });

            //aspNetCoreApp.UseMiddleware<AddSampleHeaderToResponseHeadersAspNetCoreMiddlware>();
        }

برای config مخصوص Odata، به HttpConfiguration نیاز داریم. بنابراین instanceی از آن گرفته و برای مسیریابی Odata از آن استفاده مینماییم.

با استفاده از پیاده سازی که از استاندارد Owin انجام دادیم، مشاهده کردید که Odata را همانند یک پروژه‌ی معمولی asp.netی، config نمودیم. در خط بعدی هم SignalR را مشاهده مینمایید.

اکنون اگر آدرس زیر را در مرورگر خود وارد نمایید، پاسخ زیر را از Odata دریافت خواهید کرد:

http://localhost:YourPort/odata/Products

بعد از فرستادن request فوق، باید response زیر را دریافت نمایید:

{ 
 "@odata.context":"http://localhost:4675/odata/$metadata#Products","value":[
    {
      "Id":1,"Name":"Test","Price":10
    }
  ]
}

تعداد زیادی Owin Middleware موجود همانند Thinktecture IdentityServer, NWebSec, Nancy, Facebook OAuth , ... هم با همان آموزش راه اندازی بر روی Owin که دارند میتوانند در ASP.NET Core نیز استفاده شوند و زمانی که نسخه‌ی ASP.NET Core اینها به آمادگی کامل رسید، با کمترین تغییری میتوان از آنها استفاده نمود.
اگر در پیاده سازی مقاله‌ی فوق با مشکل رو به رو شدید (اگر مراحل به ترتیب اجرا شوند، مشکلی نخواهید داشت) میتوانید از Github ، کل این پروژه را clone کرده و همچنین طبق commit‌های منظم از طریق history آن، مراحل جلو رفتن پروژه را دنبال نمایید.
مطالب
امکان رمزنگاری اطلاعات شخصی کاربران در ASP.NET Core Identity 2.1
از نگارش ASP.NET Core Identity 2.1 به بعد، ویژگی جدید ProtectedPersonalData در تعاریف موجودیت کاربران سیستم مشاهده می‌شود:
public class IdentityUser<TKey> where TKey : IEquatable<TKey>
{
    [ProtectedPersonalData]
    public virtual string UserName { get; set; }

    [ProtectedPersonalData]
    public virtual string Email { get; set; }
این ویژگی در حقیقت یک نشانه‌گذار است. کار آن اعلام نیاز به ذخیره سازی رمزنگاری شده‌ی اینگونه اطلاعات در بانک اطلاعاتی، جهت محافظت از اطلاعات شخصی کاربران سیستم، در حین دسترسی‌های غیرمجاز می‌باشد.


روش فعالسازی ذخیره سازی رمزنگاری شده‌ی اطلاعات شخصی کاربران

اگر برنامه‌ی پیشین خود را به نگارش‌های جدیدتر ASP.NET Core Identity ارتقاء داده باشید، احتمالا متوجه وجود یک چنین قابلیتی نشده‌اید؛ چون به صورت پیش‌فرض غیرفعال است. برای فعالسازی آن، می‌توان به صورت زیر عمل کرد:
services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
   options.Stores.ProtectPersonalData = true;
   // ...
})


ویژگی ProtectedPersonalData چگونه به صورت خودکار پردازش می‌شود؟

پیشنیاز درک نحوه‌ی پردازش ویژگی ProtectedPersonalData، مطلب «رمزنگاری خودکار فیلدها توسط Entity Framework Core» است. در اینجا نیز دقیقا یک «تبدیلگر مقدار» برای رمزنگاری و رمزگشایی خودکار فیلدهای مزین به ProtectedPersonalData تدارک دیده شده‌است:
private class PersonalDataConverter : ValueConverter<string, string>
{
  public PersonalDataConverter(IPersonalDataProtector protector) :
      base(s => protector.Protect(s), s => protector.Unprotect(s), default)
      { }
}

سپس در IdentityUserContext تعریف شده و متد OnModelCreating آن، ابتدا بررسی می‌کند که آیا پیشتر ProtectPersonalData را به true تنظیم کرده‌اید یا خیر؟ اگر بله، تمام خواصی را که با ویژگی ProtectedPersonalDataAttribute مزین شده‌اند، یافته و سپس توسط متد HasConversion به آن‌ها تبدیلگر مقداری از نوع PersonalDataConverter را اضافه می‌کند:
        protected override void OnModelCreating(ModelBuilder builder)
        {
            var encryptPersonalData = storeOptions?.ProtectPersonalData ?? false;

            builder.Entity<TUser>(b =>
            {
                if (encryptPersonalData)
                {
                    converter = new PersonalDataConverter(this.GetService<IPersonalDataProtector>());
                    var personalDataProps = typeof(TUser).GetProperties().Where(
                                    prop => Attribute.IsDefined(prop, typeof(ProtectedPersonalDataAttribute)));
                    foreach (var p in personalDataProps)
                    {
                        if (p.PropertyType != typeof(string))
                        {
                            throw new InvalidOperationException(Resources.CanOnlyProtectStrings);
                        }
                        b.Property(typeof(string), p.Name).HasConversion(converter);
                    }
                }
// ...


سرویس پیش‌فرض IPersonalDataProtector در ASP.NET Core Identity

اگر در کدهای فوق، به جائیکه new PersonalDataConverter نوشته شده دقت کنید؛ یک سرویس از نوع IPersonalDataProtector را دریافت می‌کند که توسط آن دو متد Protect و Unprotect به کلاس خصوصی PersonalDataConverter تزریق می‌شوند:
public interface IPersonalDataProtector
{
   string Protect(string data);
   string Unprotect(string data);
}
 پیاده سازی پیش‌فرض این سرویس رمزنگاری را در اینجا می‌توانید مشاهده کنید.


تاثیر فعالسازی ProtectPersonalData بر روی سایر سرویس‌های ASP.NET Core Identity

اگر options.Stores.ProtectPersonalData را به true تنظیم کنید، سرویس UserManager اینبار انتظار خواهد داشت که IUserStore ای که به آن تزریق می‌شود، از نوع جدید IProtectedUserStore باشد؛ در غیراینصورت یک استثناء را صادر می‌کند:
if (Options.Stores.ProtectPersonalData)
 {
     if (!(Store is IProtectedUserStore<TUser>))
     {
         throw new InvalidOperationException(Resources.StoreNotIProtectedUserStore);
     }
     if (services.GetService<ILookupProtector>() == null)
     {
         throw new InvalidOperationException(Resources.NoPersonalDataProtector);
     }
 }
که البته اگر از UserStore ای با امضای زیر استفاده می‌کنید، IProtectedUserStore را هم پیاده سازی کرده‌است:
 public class UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> : ...

همچنین تمام متدهای سرویس UserManager که با خواص مزین شده‌ی به [ProtectedPersonalData] کار می‌کنند، جهت استفاده‌ی از متد ProtectPersonalData به روز رسانی شده‌اند:
public virtual async Task UpdateNormalizedUserNameAsync(TUser user)
{
    var normalizedName = NormalizeName(await GetUserNameAsync(user));
    normalizedName = ProtectPersonalData(normalizedName);
    await Store.SetNormalizedUserNameAsync(user, normalizedName, CancellationToken);
}


چگونه می‌توان بجای DefaultPersonalDataProtector پیش‌فرض، از یک نمونه‌ی سفارشی استفاده کرد؟

برای انجام اینکار نیاز است ILookupProtector خودتان را نوشته و به سیستم معرفی کنید. یک نمونه از پیاده سازی سفارشی آن‌را در پروژه‌ی AspNetCoreIdentityEncryption می‌توانید مشاهده نمائید.
نظرات مطالب
بخش دوم - بررسی امکانات (کلاس ها و متدهای) کتابخانه Gridify
من قصد پیاده سازی یک صفحه برای جستجوی پیشرفته بین همه جداول رو دارم. برای این کار یک کلاس تعریف کردم و در ورودی تابع یک لیست از این نوع را بهش پاس میدم. این کلاس اینطور تعریف شده:
public class SearchDTO
{
    public string TableName { get; set; }
    public string ColumnName { get; set; }
    public string searchPhrase { get; set; } = string.Empty;
    public DateTimeOffset? searchDateFrom { get; set; }
    public DateTimeOffset? searchDateTo { get; set; }
    public int Include { get; set; } = 1;
}
کاربر اسم جدول و اون ستونی که میخواد شرط رو براش اعمال کنه رو انتخاب میکنه و بعدش عبارت یا محدوده تاریخ رو وارد میکنه. این که شامل بشه یا نشه رو هم با Include میتونه مشخص کنه. 
من هر چی داکومنت رو خوندم توی همشون اسم جدول رو نمیشد به صورت string وارد کرد. اگه اشتباه میکنم لطفا اصلاح کنید.
رویه ای که مدنظرم هست اینه که داخل یه حلقه for یا foreach یک کوئری بنویسم که همه‌ی جداولی که کاربر انتخاب کرده رو با هم join کنه. بعدش توی یه حلقه دیگه شرط‌ها رو روی ستون هایی که انتخاب کرده اعمال کنم.
در نهایت ستون‌های نتیحه نهایی رو Select کنم تا اون ستون هایی که مجاز هستند به سمت کلاینت برگشت داده بشه. اسامی این ستون‌ها رو توی یه فایل .resx ذخیره کردم.
یعنی یه چیزی شبیه به این کد:(ولی این کد درست و قابل اجرا نیست)
// Join the tables dynamically based on the table names
for (int i = 1; i < filterList.Count; i++)
{
    var joinEntityType = entityTypes.FirstOrDefault(t => t.ClrType.Name == filterList[0].TableName)?.ClrType;
    if (entityType == null)
    {
        return (null, 0, 0);
    }
    var joinEntityQuery = (IQueryable<object>)Activator.CreateInstance(typeof(DbSet<>).MakeGenericType(joinEntityType), _dbContext);
    query = query.Join(joinEntityQuery.ToList(),
                                    x => x.GetType().GetProperty($"{filterList[i - 1].TableName}.{filterList[i - 1].TableName}Id").GetValue(x),
                                    y => y.GetType().GetProperty($"{filterList[i].TableName}.{filterList[i].TableName}Id").GetValue(y),
                                    (x, y) => x);
}

// Apply the conditions dynamically based on the column names and conditions
for (int i = 0; i < filterList.Count; i++)
{
    if (!string.IsNullOrEmpty(filterList[i].searchPhrase))
    {
        var parameter = Expression.Parameter(entityType, "x");
        var condition = Expression.Call(
            typeof(string).GetMethod("Contains", new[] { typeof(string) }),
            Expression.PropertyOrField(parameter, filterList[i].ColumnName),
            Expression.Constant(filterList[i].searchPhrase)
        );
        var lambda = Expression.Lambda<Func<object, bool>>(condition, parameter);

        query = query.Where(lambda);
    }
    if (filterList[i].searchDateFrom.HasValue)
    {
       //must write expression for date constraint
    }
}

// Select the specified columns dynamically
ResourceManager resourceManager = new ResourceManager(typeof(TablePropertiesResources));
var columnNames = resourceManager.GetResourceSet(CultureInfo.CurrentCulture, true, true)
    .OfType<DictionaryEntry>()
    .Select(entry => entry.Key.ToString())
    .ToList();
var selectColumns = columnNames.ToArray();
var selectedData = query
    .Select(x => new
    {
        // Dynamically select the desired properties
        Result = selectColumns.ToDictionary(column => column, column => x.GetType().GetProperty(column).GetValue(x))
    })
    .ToList();
آیا اینکار با gridify امکان پذیر می‌باشد؟
نظرات مطالب
مروری بر کاربردهای Action و Func - قسمت دوم
یک نکته‌ی تکمیلی: امضای نگارش‌های Task دار و Async این متدها

در حالت اول، Task فراخوانی شده یک خروجی را باز می‌گرداند و در حالت دوم، خروجی آن void است:
    private async Task<T> doSomethingAsync<T>(Func<Task<T>> task)
    {
        var result = default(T);
        try
        {
            result = await task();
        }
        catch (Exception ex)
        {
            // todo: log
        }
        return result;
    }

    private async Task doSomethingAsync(Func<Task> task)
    {
        await task();
    }
با یک چنین کاربردهای نمونه‌ای
    public async Task ExampleAsync()
    {
        await doSomethingAsync<string>(() => Task.FromResult("...."));
        await doSomethingAsync(() => Task.Delay(1000));
    }
نظرات مطالب
PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند
سلام خدمت دوستان
بنده PersianDatePicker رو در پروژه خودم به کار گرفتم اما ظاهرا زمانی که بجای استفاده از Post Model از پیاده سازی PostViewModel استفاده می‌کنیم، datepicker به درستی عمل نمیکنه، البته در نمایش تقویم و انتخاب تاریخ مشکلی نیست بلکه در تبدیل شمسی به میلادی و ارسال به سرور مشکل داره و نهایتا فیلد addDate مقدار {01/01/0001 12:00:00 ق.ظ} رو میگیره.

Post ViewModel به صورت:
namespace MvcAppPersianDatePicker.ViewModels
{
    public class PostViewModel
    {
        public Post post { get; set; }
    }
}

و Home کنترلر:

namespace MvcAppPersianDatePicker.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            var model = new PostViewModel();
            model.post = new Post() { AddDate = DateTime.Now.AddDays(-1), Title = "تست" };
            return View(model);
        }

        [HttpPost]
        public ActionResult Index(PostViewModel model)
        {
            if (ModelState.IsValid)
            {
                // todo: save data
                return RedirectToAction("Index");
            }

            return View(model);
        }
    }
}


مطالب
Roslyn #7
معرفی Workspace API

Workspace، در حقیقت نمایش اجزای یک Solution در ویژوال استودیو است و یک Solution متشکل است از تعدادی پروژه به همراه وابستگی‌های بین آن‌ها. هدف از وجود Workspace API در Roslyn، دسترسی به اطلاعات لازم جهت انجام امور Refactoring در سطح یک Solution است. برای مثال اگر قرار است نام خاصیتی تغییر کند و این خاصیت در چندین پروژه‌ی دیگر در حال استفاده است، این نام باید در سراسر Solution جاری یافت شده و تغییر یابد. همچنین برفراز Workspace API تعدادی سرویس زبان مانند فرمت کننده‌های کدها، تغییرنام دهنده‌های سیمبل‌ها و توصیه کننده‌ها نیز تهیه شده‌اند.
همچنین این سرویس‌ها و API تهیه شده، منحصر به ویژوال استودیو نیستند و VS 2015 تنها از آن‌ها استفاده می‌کند. برای مثال نگارش‌های جدیدتر mono-develop لینوکسی نیز شروع به استفاده‌ی از Roslyn کرده‌اند.


نمایش اجزای یک Solution

 در ادامه مثالی را مشاهده می‌کنید که توسط آن نام Solution و سپس تمام پروژه‌های موجود در آن‌ها به همراه نام فایل‌های مرتبط و همچنین ارجاعات آن‌ها در صفحه نمایش داده می‌شوند:
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Print the root of the solution.
Console.WriteLine(Path.GetFileName(sln.FilePath));
 
 
// Get dependency graph to perform a sort.
var g = sln.GetProjectDependencyGraph();
var ps = g.GetTopologicallySortedProjects();
 
 
// Print all projects, their documents, and references.
foreach (var p in ps)
{
    var proj = sln.GetProject(p);
 
    Console.WriteLine("> " + proj.Name);
 
    Console.WriteLine("  > References");
    foreach (var r in proj.ProjectReferences)
    {
        Console.WriteLine("    - " + sln.GetProject(r.ProjectId).Name);
    }
 
    foreach (var d in proj.Documents)
    {
        Console.WriteLine("  - " + d.Name);
    }
}
در ابتدا نیاز است یک وهله از MSBuildWorkspace را ایجاد کرد. اکنون با استفاده از این Workspace می‌توان solution خاصی را گشود و آنالیز کرد. قسمتی از خروجی آن چنین شکلی را دارد:
 Roslyn.sln
> Roslyn01
  > References
  - Program.cs
  - AssemblyInfo.cs
  - .NETFramework,Version=v4.6.AssemblyAttributes.cs


ایجاد یک Syntax highlighter با استفاده از Classification service

هدف از Classification service، رندر کردن فایل‌ها در ادیتور جاری است. برای این منظور نیاز است بتوان واژه‌های کلیدی، کامنت‌ها، نام‌های نوع‌ها و امثال آن‌ها را به صورت کلاسه شده در اختیار داشت و سپس برای مثال هرکدام را با رنگی مجزا نمایش داد و رندر کرد.
در ادامه مثالی از آن‌را ملاحظه می‌کنید:
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;

// Get the Tests\Bar.cs document.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var test = proj.Documents.Single(d => d.Name == "Bar.cs");
 
var tree = test.GetSyntaxTreeAsync().Result;
var root = tree.GetRootAsync().Result;
 
// Get all the spans in the document that are classified as language elements.
var spans = Classifier.GetClassifiedSpansAsync(test, root.FullSpan).Result.ToDictionary(c => c.TextSpan.Start, c => c);
 
// Print the source text with appropriate colorization.
var txt = tree.GetText().ToString();
 
var i = 0;
foreach (var c in txt)
{
    var span = default(ClassifiedSpan);
    if (spans.TryGetValue(i, out span))
    {
        var color = ConsoleColor.Gray;
 
        switch (span.ClassificationType)
        {
            case ClassificationTypeNames.Keyword:
                color = ConsoleColor.Cyan;
                break;
            case ClassificationTypeNames.StringLiteral:
            case ClassificationTypeNames.VerbatimStringLiteral:
                color = ConsoleColor.Red;
                break;
            case ClassificationTypeNames.Comment:
                color = ConsoleColor.Green;
                break;
            case ClassificationTypeNames.ClassName:
            case ClassificationTypeNames.InterfaceName:
            case ClassificationTypeNames.StructName:
            case ClassificationTypeNames.EnumName:
            case ClassificationTypeNames.TypeParameterName:
            case ClassificationTypeNames.DelegateName:
                color = ConsoleColor.Yellow;
                break;
            case ClassificationTypeNames.Identifier:
                color = ConsoleColor.DarkGray;
                break;
        }
 
        Console.ForegroundColor = color;
    }
 
    Console.Write(c);
 
    i++;
}
با این خروجی:


توضیحات:
در اینجا نیز کار با ایجاد یک Workspace و سپس گشودن Solution ایی مشخص در آن آغاز می‌شود. سپس در آن به دنبال پروژه‌ای به نام Roslyn04.Tests می‌گردیم. این پروژه حاوی تعدادی کلاس، جهت بررسی و آزمایش هستند. برای مثال در اینجا فایل Bar.cs آن قرار است آنالیز شود. پس از یافتن آن، ابتدا syntax tree آن دریافت می‌گردد و سپس به سرویس Classifier.GetClassifiedSpansAsync ارسال خواهد شد. خروجی آن شامل لیستی از Classified Spans است؛ مانند کلمات کلیدی، رشته‌ها، کامنت‌ها و غیره. در ادامه این لیست تبدیل به یک دیکشنری می‌شود که کلید آن محل آغاز این span و مقدار آن، مقدار span است. سپس متن syntax tree دریافت شده و حرف به حرف آن در طی یک حلقه بررسی می‌شود. در این حلقه، مقدار i به محل حروف جاری مورد آنالیز اشاره می‌کند. اگر این محل در دیکشنری Classified Spans وجود داشت، یعنی یک span جدید شروع شده‌است و بر این اساس، نوع آن span را می‌توان استخراج کرد و سپس بر اساس این نوع، رنگ متفاوتی را در صفحه نمایش داد.


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

این سرویس کار فرمت خودکار کدهای بهم ریخته را انجام می‌دهد؛ مانند تنظیم فاصله‌های خالی و یا ایجاد indentation و امثال آن. در حقیقت Ctlr K+D در ویژوال استودیو، دقیقا از همین سرویس زبان استفاده می‌کند.
کار کردن با این سرویس از طریق برنامه نویسی به نحو ذیل است:
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Get the Tests\Qux.cs document.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var qux = proj.Documents.Single(d => d.Name == "Qux.cs");
 
Console.WriteLine("Before:");
Console.WriteLine();
Console.WriteLine(qux.GetSyntaxTreeAsync().Result.GetText());
 
Console.WriteLine();
Console.WriteLine();
 
 
// Apply formatting and print the result.
var res = Formatter.FormatAsync(qux).Result;
 
Console.WriteLine("After:");
Console.WriteLine();
Console.WriteLine(res.GetSyntaxTreeAsync().Result.GetText());
Console.WriteLine();
با این خروجی:
Before:

using System;

namespace Roslyn04.Tests
{
    class Qux {
        public void Baz()
        { Console.WriteLine(42);
            return;  }
    }
}


After:

using System;

namespace Roslyn04.Tests
{
    class Qux
    {
        public void Baz()
        {
            Console.WriteLine(42);
            return;
        }
    }
}
همانطور که ملاحظه می‌کنید، فایل Qux.cs که فرمت مناسبی ندارد. بنابراین باز شده و syntax tree آن به سرویس Formatter.FormatAsync جهت فرمت شدن ارسال می‌شود.


سرویس یافتن سیمبل‌ها

یکی دیگر از قابلیت‌هایی که در ویژوال استودیو وجود دارد، امکان یافتن سیمبل‌ها است. برای مثال این نوع یا کلاس خاص، در کجاها استفاده شده‌است و به آن ارجاعاتی وجود دارد. مواردی مانند Find all references، Go to definition و نمایش Call hierarchy از این سرویس استفاده می‌کنند.
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Get the Tests project.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
 
// Locate the symbol for the Bar.Foo method and the Bar.Qux property.
var comp = proj.GetCompilationAsync().Result;
 
var barType = comp.GetTypeByMetadataName("Roslyn04.Tests.Bar");
 
var fooMethod = barType.GetMembers().Single(m => m.Name == "Foo");
var quxProp = barType.GetMembers().Single(m => m.Name == "Qux");
 
 
// Find callers across the solution.
Console.WriteLine("Find callers of Foo");
Console.WriteLine();
 
var callers = SymbolFinder.FindCallersAsync(fooMethod, sln).Result;
foreach (var caller in callers)
{
    Console.WriteLine(caller.CallingSymbol);
    foreach (var location in caller.Locations)
    {
        Console.WriteLine("    " + location);
    }
}
 
Console.WriteLine();
Console.WriteLine();
 
// Find all references across the solution.
Console.WriteLine("Find all references to Qux");
Console.WriteLine();
 
var references = SymbolFinder.FindReferencesAsync(quxProp, sln).Result;
foreach (var reference in references)
{
    Console.WriteLine(reference.Definition);
    foreach (var location in reference.Locations)
    {
        Console.WriteLine("    " + location.Location);
    }
}
در این مثال، پروژه‌ی Roslyn04.Tests که حاوی کلاس‌های Foo و Qux است، جهت آنالیز باز شده‌است. در اینجا برای رسیدن به Symbols نیاز است ابتدا به Compilation API دسترسی یافت و سپس متادیتاها را بر اساس آن استخراج کرد. سپس متدهای Foo و خاصیت Qux آن یافت شده‌اند.
اکنون با استفاده از سرویس SymbolFinder.FindCallersAsync تمام فراخوان‌های متد Foo را در سراسر Solution جاری می‌یابیم.
سپس با استفاده از سرویس SymbolFinder.FindReferencesAsync تمام ارجاعات به خاصیت Qux را در Solution جاری نمایش می‌دهیم.


سرویس توصیه کننده

Intellisense در ویژوال استودیو از سرویس توصیه کننده‌ی Roslyn استفاده می‌کند.
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;

// Get the Tests\Foo.cs document.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var foo = proj.Documents.Single(d => d.Name == "Foo.cs");
 
 
// Find the 'dot' token in the first Console.WriteLine member access expression.
var tree = foo.GetSyntaxTreeAsync().Result;
var model = proj.GetCompilationAsync().Result.GetSemanticModel(tree);
var consoleDot = tree.GetRoot().DescendantNodes().OfType<MemberAccessExpressionSyntax>().First().OperatorToken;
 
 
// Get recommendations at the indicated cursor position.
//
//   Console.WriteLine
//           ^
var res = Recommender.GetRecommendedSymbolsAtPosition(

                    model, consoleDot.GetLocation().SourceSpan.Start + 1, ws).ToList();
 
foreach (var rec in res)
{
    Console.WriteLine(rec);
}
در این مثال سعی شده‌است لیست توصیه‌های ارائه شده در حین تایپ دات، توسط سرویس Recommender.GetRecommendedSymbolsAtPosition دریافت و نمایش داده شوند. در ابتدای کار، کلاس Foo گشوده شده و سپس Syntax tree و Semantic model آن استخراج می‌شود. این model پارامتر اول متد سرویس توصیه کننده‌است. سپس نیاز است محل مکانی را به آن معرفی کنیم تا کار توصیه کردن را بر اساس آن شروع کند. برای نمونه در اینجا OperatorToken در حقیقت همان دات مربوط به Console.WriteLine است. پس از یافتن این توکن، امکان دسترسی به مکان آن وجود دارد.
تعدادی از خروجی‌های مثال فوق به صورت زیر هستند:
 System.Console.Beep()
System.Console.Beep(int, int)
System.Console.Clear()


سرویس تغییر نام دادن

هدف از سرویس Renamer.RenameSymbolAsync، تغییر نام یک identifier در کل Solution است. نمونه‌ای از نحوه‌ی کاربرد آن‌را در مثال ذیل مشاهده می‌کنید:
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Get Tests\Bar.cs before making changes.
var oldProj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var oldDoc = oldProj.Documents.Single(d => d.Name == "Bar.cs");
 
Console.WriteLine("Before:");
Console.WriteLine();
 
var oldTxt = oldDoc.GetTextAsync().Result;
Console.WriteLine(oldTxt);
 
Console.WriteLine();
Console.WriteLine();
 
 
// Get the symbol for the Bar.Foo method.
var comp = oldProj.GetCompilationAsync().Result;
 
var barType = comp.GetTypeByMetadataName("Roslyn04.Tests.Bar");
var fooMethod = barType.GetMembers().Single(m => m.Name == "Foo");
 
 
// Perform the rename.
var newSln = Renamer.RenameSymbolAsync(sln, fooMethod, "Foo2", ws.Options).Result;
 
 
// Get Tests\Bar.cs after making changes.
var newProj = newSln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var newDoc = newProj.Documents.Single(d => d.Name == "Bar.cs");
 
Console.WriteLine("After:");
Console.WriteLine();
 
var newTxt = newDoc.GetTextAsync().Result;
Console.WriteLine(newTxt);
در این مثال، متد Foo کلاس Bar، قرار است به Foo2 تغییرنام یابد. به همین منظور ابتدا پروژه‌ی حاوی فایل Bar.cs باز شده و اطلاعات این کلاس استخراج می‌گردد. سپس اصل این کلاس تغییر نیافته نمایش داده می‌شود. در ادامه با استفاده از API کامپایل، به متادیتای متد Foo یا به عبارتی Symbol آن دسترسی پیدا می‌کنیم. سپس این Symbol به متد یا سرویس Renamer.RenameSymbolAsync ارسال می‌شود تا کار تغییر نام صورت گیرد. پس از اینکار مجددا متن کلاس تغییر یافته نمایش داده خواهد شد.


سرویس ساده کننده

هدف از سرویس ساده کننده، ساده‌کردن و کاهش کدهای ارائه شده، از دید Semantics است. برای مثال اگر فضای نامی در قسمت using ذکر شده‌است، دیگر نیازی نیست تا این فضای نام به ابتدای فراخوانی یک متد آن اضافه شود و می‌توان این قطعه از کد را ساده‌تر کرد و کاهش داد.
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Get the Tests\Baz.cs document.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var baz = proj.Documents.Single(d => d.Name == "Baz.cs");
 
Console.WriteLine("Before:");
Console.WriteLine();
Console.WriteLine(baz.GetSyntaxTreeAsync().Result.GetText());
 
Console.WriteLine();
Console.WriteLine();
 
var oldRoot = baz.GetSyntaxRootAsync().Result;

 
var memberAccesses = oldRoot.DescendantNodes().OfType<CastExpressionSyntax>();
var newRoot = oldRoot.ReplaceNodes(memberAccesses, (_, m) => m.WithAdditionalAnnotations(Simplifier.Annotation));
 
var newDoc = baz.WithSyntaxRoot(newRoot);
 
 
// Invoke the simplifier and print the result.
var res = Simplifier.ReduceAsync(newDoc).Result;
 
Console.WriteLine("After:");
Console.WriteLine();
Console.WriteLine(res.GetSyntaxTreeAsync().Result.GetText());
Console.WriteLine();
در این مثال نحوه‌ی ساده سازی cast‌های اضافی را ملاحظه می‌کنید. برای مثال اگر نوع متغیری int است، دیگر نیازی نیست در سراسر کد در کنار این متغیر، cast به int را هم ذکر کرد و می‌توان این کد را ساده‌تر نمود.


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید:
Roslyn-Samples.zip
مطالب
ذخیره‌ی سوابق کامل تغییرات یک رکورد در یک فیلد توسط Entity framework Core
در این مقاله، نوشته‌ی ایمان محمدی، ذخیره‌ی اطلاعات نظارتی هر Entity توسط دو فیلد CreatedSources و ModifiedSources به صورت JSON انجام می‌شود که در هر کدام از این فیلدها، اطلاعات مختلفی مانند ip کاربر، شناسه دستگاه، HostName، ClientName و یک سری اطلاعات دیگر ذخیره می‌شوند. بیایید به این اطلاعات متادیتا بگوییم. در این حالت اگر رکورد، چندین بار تغییر کند، متادیتای آخرین تغییرات در فیلد ModifiedSources ذخیره می‌شود. حالا اگر ما بخواهیم اطلاعات متادیتای همه‌ی تغییرات را داشته باشیم چه؟ اگر بخواهیم علاوه بر اطلاعات بالا، اینکه چه کسی و در چه زمانی این تغییرات را انجام داده است، نیز داشته باشیم چطور؟ اگر بخواهیم حتی اطلاعات متادیتای حذف یک رکورد را داشته باشیم چطور (در حالت soft-delete که رکورد واقعا پاک نمی‌شود)؟ سوال جالبتر اینکه اگر بخواهیم تمام تاریخچه‌ی مقادیر مختلف یک رکورد را از ابتدای ایجاد شدن داشته باشیم چطور؟ در این مقاله قصد داریم به همه‌ی این موارد اضافی برسیم؛ آن هم فقط با یک ستون در Entityهایمان، به اسم Audit!

ابتدا کلاس پایه موجودیت‌هایمان را تعریف می‌کنیم؛ تا بر روی Entityهایمان بتوانیم فیلد نظارتی Audit را اعمال کنیم:
public class BaseEntity : IBaseEntity
{
   [JsonIgnore]
   int Id { get; set; } 

   [JsonIgnore] 
    string? Audit { get; set; }
}
ویژگی [JsonIgnore]  به این منظور استفاده شده است تا از serialize کردن این فیلدها هنگام ایجاد Audit، جلوگیری شود؛ تا در نهایت حجم جیسن Audit کاهش یابد. با مطالعه‌ی ادامه‌ی مقاله، متوجه این قضیه خواهید شد.

دقیقا مانند مقاله‌ی اشاره شده (که خواندن آن توصیه می‌شود)، کلاس AuditSourceValues را ایجاد می‌کنیم:
public class AuditSourceValues
{
    [JsonProperty("hn")]
    public string? HostName { get; set; }

    [JsonProperty("mn")]
    public string? MachineName { get; set; }

    [JsonProperty("rip")]
    public string? RemoteIpAddress { get; set; }

    [JsonProperty("lip")]
    public string? LocalIpAddress { get; set; }

    [JsonProperty("ua")]
    public string? UserAgent { get; set; }

    [JsonProperty("an")]
    public string? ApplicationName { get; set; }

    [JsonProperty("av")]
    public string? ApplicationVersion { get; set; }

    [JsonProperty("cn")]
    public string? ClientName { get; set; }

    [JsonProperty("cv")]
    public string? ClientVersion { get; set; }

    [JsonProperty("o")]
    public string? Other { get; set; }
}
با تعریف کردن نام برای فیلد‌های JSON و نادیده گرفتن مقادیر نال، سعی کردیم حجم خروجی JSON پایین باشد.

اکنون کلاس EntityAudit را ایجاد می‌کنیم که شامل تمامی اطلاعات مورد نیاز ما برای ثبت تاریخچه‌ی کامل هر موجودیت است:
public class EntityAudit<TEntity>
{
    [JsonProperty("type")]
    [JsonConverter(typeof(StringEnumConverter))]
    public EntityEventType EventType { get; set; }

    [JsonProperty("user", NullValueHandling = NullValueHandling.Include)]
    public int? ActorUserId { get; set; }

    [JsonProperty("at")]
    public DateTime ActDateTime { get; set; }

    [JsonProperty("sources")]
    public AuditSourceValues? AuditSourceValues { get; set; }

    [JsonProperty("newValues", NullValueHandling = NullValueHandling.Include)]
    public TEntity NewEntity { get; set; } = default!;

    public string? SerializeJson()
    {
        return JsonSerializer.Serialize(this, 
            options: new JsonSerializerOptions { WriteIndented = false, IgnoreNullValues = true }); 
    }
}

دقت کنید که این کلاس به صورت جنریک تعریف شده است تا اگر بعدا بخواهیم آن را Deserialize کنیم و مثلا از آن API بسازیم، یا استفاده‌ی خاصی را از آن داشته باشیم، به‌راحتی به Entity مد نظر تبدیل شود. در این مقاله فقط به ذخیره‌ی آن پرداخته می‌شود و استفاده از این فیلد که به راحتی و با کمک DbFunctionها در Entity Framework قابل انجام است به خواننده واگذار می‌شود. 

همچنین اینام EntityEventType که تعریف آن در زیر می‌آید دارای ویژگی [JsonConverter(typeof(StringEnumConverter))]  می‌باشد تا مقدار رشته‌ای آن را بجای مقدار عددی، در خروجی جیسن داشته باشیم. این اینام، شامل  تمامی عملیاتی است که بر روی یک رکورد قابل انجام است و به این صورت تعریف می‌شود:
public enum EntityEventType
{
    Create = 0,
    Update = 1,
    Delete = 2
}

تامین اطلاعات کلاس AuditSourceValues به همان صورت است که در مقاله‌ی اشاره شده آمده‌است؛ ابتدا تعریف اینترفیس IAuditSourcesProvider و سپس ایجاد کلاس AuditSourcesProvider:
public interface IAuditSourcesProvider
{
    AuditSourceValues GetAuditSourceValues();
}
public class AuditSourcesProvider : IAuditSourcesProvider
{
    protected readonly IHttpContextAccessor HttpContextAccessor;

    public AuditSourcesProvider(IHttpContextAccessor httpContextAccessor)
    {
        HttpContextAccessor = httpContextAccessor;
    }

    public virtual AuditSourceValues GetAuditSourceValues()
    {
        var httpContext = HttpContextAccessor.HttpContext;

        return new AuditSourceValues
        {
            HostName = GetHostName(httpContext),
            MachineName = GetComputerName(httpContext),
            LocalIpAddress = GetLocalIpAddress(httpContext),
            RemoteIpAddress = GetRemoteIpAddress(httpContext),
            UserAgent = GetUserAgent(httpContext),
            ApplicationName = GetApplicationName(httpContext),
            ClientName = GetClientName(httpContext),
            ClientVersion = GetClientVersion(httpContext),
            ApplicationVersion = GetApplicationVersion(httpContext),
            Other = GetOther(httpContext)
        };
    }

    protected virtual string? GetUserAgent(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["User-Agent"].ToString();
    }

    protected virtual string? GetRemoteIpAddress(HttpContext httpContext)
    {
        return httpContext.Connection?.RemoteIpAddress?.ToString();
    }

    protected virtual string? GetLocalIpAddress(HttpContext httpContext)
    {
        return httpContext.Connection?.LocalIpAddress?.ToString();
    }

    protected virtual string GetHostName(HttpContext httpContext)
    {
        return httpContext.Request.Host.ToString();
    }

    protected virtual string GetComputerName(HttpContext httpContext)
    {
        return Environment.MachineName;
    }
    protected virtual string? GetApplicationName(HttpContext httpContext)
    {
        return Assembly.GetEntryAssembly()?.GetName().Name;
    }

    protected virtual string? GetApplicationVersion(HttpContext httpContext)
    {
        return Assembly.GetEntryAssembly()?.GetName().Version.ToString();
    }

    protected virtual string? GetClientVersion(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["client-version"];
    }
    protected virtual string? GetClientName(HttpContext httpContext)
    {
        return httpContext.Request?.Headers["client-name"];
    }

    protected virtual string? GetOther(HttpContext httpContext)
    {
        return null;
    }
}

حالا برای تامین اطلاعات کلاس EntityAudit کار مشابهی می‌کنیم. ابتدا اینترفیس IEntityAuditProvider را به صورت زیر تعریف می‌کنیم: 
public interface IEntityAuditProvider
{
    string? GetAuditValues(EntityEventType eventType, object? entity, string? previousJsonAudit = null);
}

  و سپس کلاس EntityAuditProvider را ایجاد می‌کنیم:
public class EntityAuditProvider : IEntityAuditProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IAuditSourcesProvider _auditSourcesProvider;

    #region Constructor Injections

    public EntityAuditProvider(IHttpContextAccessor httpContextAccessor, IAuditSourcesProvider auditSourcesProvider)
    {
        _httpContextAccessor = httpContextAccessor;
        _auditSourcesProvider = auditSourcesProvider;
    }

    #endregion

    public virtual string? GetAuditValues(EntityEventType eventType, object? newEntity, string? previousJsonAudit = null)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        int? userId;

        var user = httpContext.User;

        if (!user.Identity.IsAuthenticated)
            userId = null;
        else
            userId = user.Claims.Where(x => x.Type == "UserID").Select(x => x.Value).First().ToInt();

        var auditSourceValues = _auditSourcesProvider.GetAuditSourceValues();

        var auditJArray = new JArray();

        // Update & Delete
        if (eventType == EntityEventType.Update || eventType == EntityEventType.Delete)
        {
            auditJArray = JArray.Parse(previousJsonAudit!);
        }

        // Delete => No NewValues
        if (eventType == EntityEventType.Delete)
        {
            newEntity = null;
        }

        JObject newAuditJObject = JObject.FromObject(new EntityAudit<object?>
        {
            EventType = eventType,
            ActorUserId = userId,
            ActDateTime = DateTime.Now,
            AuditSourceValues = auditSourceValues,
            NewEntity = newEntity
        }, new JsonSerializer
        {
            NullValueHandling = NullValueHandling.Ignore,
            Formatting = Formatting.None
        });

        auditJArray.Add(newAuditJObject);

        return auditJArray.SerializeToJson(true);
    }
}
در این کلاس برای اینکه به جیسن قبلی Audit که تاریخچه‌ی قبلی رکورد می‌باشد یک آیتم را اضافه کنیم، از JArray و JObject پکیج Newtonsoft استفاده کرد‌ه‌ایم.

حالا همه چیز آماده است. مانند مقاله‌ی اشاره شده، از مفهوم Interceptor استفاده می‌کنیم. کلاس AuditSaveChangesInterceptor را که از کلاس SaveChangesInterceptor مشتق می‌شود، به صورت زیر ایجاد می‌کنیم: 
public class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
    private readonly IEntityAuditProvider _entityAuditProvider;

    #region Constructor Injections

    public AuditSaveChangesInterceptor(IEntityAuditProvider entityAuditProvider)
    {
        _entityAuditProvider = entityAuditProvider;
    }

    #endregion

    public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
    {
        ApplyAudits(eventData.Context.ChangeTracker);
        return base.SavingChanges(eventData, result);
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
        CancellationToken cancellationToken = new CancellationToken())
    {
        ApplyAudits(eventData.Context.ChangeTracker);
        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }

    private void ApplyAudits(ChangeTracker changeTracker)
    {
        ApplyCreateAudits(changeTracker);
        ApplyUpdateAudits(changeTracker);
        ApplyDeleteAudits(changeTracker);
    }

    private void ApplyCreateAudits(ChangeTracker changeTracker)
    {
        var addedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Added);

        foreach (var addedEntry in addedEntries)
        {
            if (addedEntry.Entity is IBaseEntity entity)
            {              
                entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Create, entity);
            }
        }
    }

    private void ApplyUpdateAudits(ChangeTracker changeTracker)
    {
        var modifiedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Modified);

        foreach (var modifiedEntry in modifiedEntries)
        {
            if (modifiedEntry.Entity is IBaseEntity entity)
            {
                var eventType = entity.IsArchived ? EntityEventType.Delete : EntityEventType.Update; // Maybe Soft Delete
                entity.Audit = _entityAuditProvider.GetAuditValues(eventType, entity, entity.Audit);
            }
        }
    }

    private void ApplyDeleteAudits(ChangeTracker changeTracker)
    {
        var deletedEntries = changeTracker.Entries()
            .Where(x => x.State == EntityState.Deleted);

        foreach (var modifiedEntry in deletedEntries)
        {
            if (modifiedEntry.Entity is IBaseEntity entity)
            {
                entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Delete, entity, entity.Audit);
            }
        }
    }

}


و سپس آن را به سیستم معرفی می‌کنیم:

services.AddDbContext<ATADbContext>((serviceProvider, options) =>
{
    options
        .UseSqlServer(...)

    // Interceptors
    var entityAuditProvider = serviceProvider.GetRequiredService<IEntityAuditProvider>();
    options.AddInterceptors(new AuditSaveChangesInterceptor(entityAuditProvider));

});

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

نمونه‌ی نتیجه‌ای را که از این روش بدست می‌آید، در این تصویر می‌بینید. اگر بخواهید به صورت نرم‌افزاری یا کدی از این دیتا استفاده کنید، باید آن را Deserialize کنید که همانطور که گفته شد با امکاناتی که SQL Server برای خواندن فیلدهای JSON دارد و معرفی آن به EF، قابل انجام است. در غیر اینصورت استفاده از این دیتا به صورت چشمی یا استفاده از Json Formatterها به‌راحتی امکان پذیر است. 

 
نمونه‌ی کامل فیلد Audit که در JsonFormatter قرار داده شده است، بعد از ایجاد شدن و یکبار آپدیت و سپس حذف نرم رکورد:
[
   {
      "type":"Create",
      "user":1,
      "at":"2020-11-24T23:05:54.2692711+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":{               
         "Name":"Farshad"
      }
   },
   {
      "type":"Update",
      "user":1,
      "at":"2020-11-24T23:06:20.0838188+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":{                 
         "Name":"Edited Farshad"
      }
   },
   {
      "type":"Delete",
      "user":null,
      "at":"2020-11-24T23:06:28.601837+03:30",
      "sources":{
         "hn":"localhost:44398",
         "mn":"DESKTOP-N1GAV2U",
         "rip":"::1",
         "lip":"::1",
         "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
         "an":"Server.Api",
         "av":"1.0.0.0"
      },
      "newValues":null
   }
]

یک روش مرسوم داشتن تاریخچه‌ی تغییرات رکورد که با جستجو در اینترنت نیز می‌توان به آن رسید، داشتن یک جدول جداگانه به اسم Audit است که با هر بار تغییر هر Entity، یک رکورد در آن ایجاد می‌شود. ساختار آن مانند تصاویر زیر است:


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

مطالب
دریافت و نمایش فایل‌های PDF در برنامه‌های Blazor WASM
زمانیکه قرار است با فایل‌های باینری واقع در سمت سرور کار کنیم، اگر اکشن متدهای ارائه دهنده‌ی آن‌ها محافظت شده نباشند، برای نمایش و یا دریافت آن‌ها تنها کافی است از آدرس مستقیم این منابع استفاده کرد و در این حالت نیازی به رعایت هیچ نکته‌ی خاصی نیست. اما اگر اکشن متدی در سمت سرور توسط فیلتر Authorize محافظت شده باشد و روش محافظت نیز مبتنی بر کوکی‌ها نباشد، یعنی این کوکی‌ها در طی درخواست‌های مختلف، به صورت خودکار توسط مرورگر به سمت سرور ارسال نشوند، آنگاه نیاز است با استفاده از HttpClient برنامه‌های Blazor WASM، درخواست دسترسی به منبعی را به همراه برای مثال JSON Web Tokens کاربر به سمت سرور ارسال کرد و سپس فایل باینری نهایی را به صورت آرایه‌ای از بایت‌ها دریافت نمود. در این حالت با توجه به ماهیت Ajax ای این این عملیات، برای نمایش و یا دریافت این فایل‌های محافظت شده در مرورگر، نیاز به دانستن نکات ویژه‌ای است که در این مطلب به آن‌ها خواهیم پرداخت.



کدهای سمت سرور دریافت فایل PDF

در اینجا کدهای سمت سرور برنامه، نکته‌ی خاصی را به همراه نداشته و صرفا یک فایل PDF ساده (محتوای باینری) را بازگشت می‌دهد:
using Microsoft.AspNetCore.Mvc;

namespace BlazorWasmShowBinaryFiles.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ReportsController : ControllerBase
    {
        [HttpGet("[action]")]
        public IActionResult GetPdfReport()
        {
            //TODO: create the `sample.pdf` report file on the server

            return File(virtualPath: "~/app_data/sample.pdf",
                        contentType: "application/pdf",
                        fileDownloadName: "sample.pdf");
        }
    }
}
که در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.

یک نکته: استفاده مستقیم از کتابخانه‌های تولید PDF در برنامه‌های سمت کاربر Blazor منطقی نیست؛ چون به ازای هر کاربر، گاهی از اوقات مجبور به ارسال بیش از 8 مگابایت اضافی مختص به فایل‌های dll. آن کتابخانه‌ی تولید PDF خواهیم شد. بنابراین بهتر است تولید PDF را در سمت سرور و در اکشن متدهای Web API انجام داد و سپس فایل نهایی تولیدی را در برنامه‌ی سمت کلاینت، دریافت و یا نمایش داد. به همین جهت در این مثال خروجی نهایی یک چنین عملیات فرضی را توسط یک اکشن متد Web API ارائه داده‌ایم که در بسیاری از موارد حتی می‌تواند توسط فیلتر Authorize نیز محافظت شده باشد.


ساخت URL برای دسترسی به اطلاعات باینری

تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی می‌کنند. این متد، شیء URL را از شیء window جاری دریافت می‌کند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید می‌کند. حاصل آن، یک URL ویژه‌است مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در برنامه‌های Blazor نیاز است اینکار را توسط JS Interop آن انجام داد؛ از این جهت که API تولید یک Blob URL، صرفا توسط کدهای جاوا اسکریپتی قابل دسترسی است. به همین جهت فایل جدید Client\wwwroot\site.js را با محتوای زیر ایجاد کرده و همچنین مدخل آن‌را در به انتهای فایل Client\wwwroot\index.html، پیش از بسته شدن تگ body، اضافه می‌کنیم:
window.JsBinaryFilesUtils = {
  createBlobUrl: function (byteArray, contentType) {
    // The byte array in .NET is encoded to base64 string when it passes to JavaScript.
    const numArray = atob(byteArray)
      .split("")
      .map((c) => c.charCodeAt(0));
    const uint8Array = new Uint8Array(numArray);
    const blob = new Blob([uint8Array], { type: contentType });
    return URL.createObjectURL(blob);
  },
  downloadFromUrl: function (fileName, url) {
    const anchor = document.createElement("a");
    anchor.style.display = "none";
    anchor.href = url;
    anchor.download = fileName;
    document.body.appendChild(anchor);
    anchor.click();
    document.body.removeChild(anchor);
  },
  downloadBlazorByteArray: function (fileName, byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.downloadFromUrl(fileName, blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
  printFromUrl: function (url) {
    const iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = url;
    document.body.appendChild(iframe);
    if (iframe.contentWindow) {
      iframe.contentWindow.print();
    }
  },
  printBlazorByteArray: function (byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.printFromUrl(blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
  showUrlInNewTab: function (url) {
    window.open(url);
  },
  showBlazorByteArrayInNewTab: function (byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.showUrlInNewTab(blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
};
توضیحات:
- زمانیکه در برنامه‌های Blazor با استفاده از متد ()HttpClient.GetByteArrayAsync آرایه‌ای از بایت‌های یک فایل باینری را دریافت می‌کنیم، ارسال آن به کدهای جاوااسکریپتی به صورت یک رشته‌ی base64 شده صورت می‌گیرد (JS Interop اینکار را به صورت خودکار انجام می‌دهد). به همین جهت در متد createBlobUrl روش تبدیل این رشته‌ی base64 دریافتی را به آرایه‌ای از بایت‌ها، سپس به یک Blob و در آخر به یک Blob URL، مشاهده می‌کنید.  این Blob Url اکنون آدرس موقتی دسترسی به آرایه‌ای از بایت‌های دریافتی توسط مرورگر است. به همین جهت می‌توان از آن به عنوان src بسیاری از اشیاء HTML استفاده کرد.
- متد downloadFromUrl، کار دریافت یک Url و سپس دانلود خودکار آن‌را انجام می‌دهد. اگر به یک anchor استاندارد HTML، ویژگی download را نیز اضافه کنیم، با کلیک بر روی آن، بجای گشوده شدن این Url، مرورگر آن‌را دریافت خواهد کرد. متد downloadFromUrl کار ساخت لینک و تنظیم ویژگی‌های آن و سپس کلیک بر روی آن‌را به صورت خودکار انجام می‌دهد. از متد downloadFromUrl زمانی استفاده کنید که منبع مدنظر، محافظت شده نباشد و Url آن به سادگی در مرورگر قابل گشودن باشد.
- متد downloadBlazorByteArray همان کار متد downloadFromUrl را انجام می‌دهد؛ با این تفاوت که Url مورد نیاز توسط متد downloadFromUrl را از طریق یک Blob Url تامین می‌کند.
- متد printFromUrl که جهت دسترسی به منابع محافظت نشده طراحی شده‌است، Url یک منبع را دریافت کرده، آن‌را به یک iframe اضافه می‌کند و سپس متد print را بر روی این iframe به صورت خودکار فراخوانی خواهد کرد تا سبب ظاهر شدن صفحه‌ی پیش‌نمایش چاپ شود.
- printBlazorByteArray همان کار متد printFromUrl را انجام می‌دهد؛  با این تفاوت که Url مورد نیاز توسط متد printFromUrl را از طریق یک Blob Url تامین می‌کند.


تهیه‌ی متدهایی الحاقی جهت کار ساده‌تر با JsBinaryFilesUtils

پس از تهیه‌ی JsBinaryFilesUtils فوق، می‌توان با استفاده از کلاس زیر که به همراه متدهایی الحاقی جهت دسترسی به امکانات آن است، کار با متدهای دریافت، نمایش و چاپ فایل‌های باینری را ساده‌تر کرد و از تکرار کدها جلوگیری نمود:
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace BlazorWasmShowBinaryFiles.Client.Utils
{
    public static class JsBinaryFilesUtils
    {
        public static ValueTask<string> CreateBlobUrlAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeAsync<string>("JsBinaryFilesUtils.createBlobUrl", byteArray, contentType);
        }

        public static ValueTask DownloadFromUrlAsync(this IJSRuntime JSRuntime, string fileName, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadFromUrl", fileName, url);
        }

        public static ValueTask DownloadBlazorByteArrayAsync(
            this IJSRuntime JSRuntime,
            string fileName, byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadBlazorByteArray",
                    fileName, byteArray, contentType);
        }

        public static ValueTask PrintFromUrlAsync(this IJSRuntime JSRuntime, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printFromUrl", url);
        }

        public static ValueTask PrintBlazorByteArrayAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printBlazorByteArray", byteArray, contentType);
        }

        public static ValueTask ShowUrlInNewTabAsync(this IJSRuntime JSRuntime, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showUrlInNewTab", url);
        }

        public static ValueTask ShowBlazorByteArrayInNewTabAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showBlazorByteArrayInNewTab", byteArray, contentType);
        }
    }
}


اصلاح Content Security Policy سمت سرور جهت ارائه‌ی محتوای blob

پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL می‌توان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرس‌هایی به صورت blob:http تولید می‌شوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob:
default-src 'self' blob:
object-src 'self' blob:
در غیراینصورت مرورگر، نمایش یک چنین تصاویر و یا اشیایی را سد خواهد کرد.


نمایش فایل PDF دریافتی از سرور، به همراه دکمه‌های دریافت، چاپ و نمایش آن در صفحه‌ی جاری

در ادامه کدهای کامل مرتبط با تصویری را که در ابتدای بحث مشاهده کردید، ملاحظه می‌کنید:
@page "/"

@using BlazorWasmShowBinaryFiles.Client.Utils

@inject IJSRuntime JSRuntime
@inject HttpClient HttpClient

<h1>Display PDF Files</h1>
<button class="btn btn-info" @onclick="handlePrintPdf">Print PDF</button>
<button class="btn btn-primary ml-2" @onclick="handleShowPdf">Show PDF</button>
<button class="btn btn-success ml-2" @onclick="handleDownloadPdf">Download PDF</button>

@if(!string.IsNullOrWhiteSpace(PdfBlobUrl))
{
    <section class="card mb-5 mt-3">
        <div class="card-header">
            <h4>using iframe</h4>
        </div>
        <div class="card-body">
            <iframe title="PDF Report" width="100%" height="600" src="@PdfBlobUrl" type="@PdfContentType"></iframe>
        </div>
    </section>

    <section class="card mb-5">
        <div class="card-header">
            <h4>using object</h4>
        </div>
        <div class="card-body">
            <object data="@PdfBlobUrl" aria-label="PDF Report" type="@PdfContentType" width="100%" height="100%"></object>
        </div>
    </section>

    <section class="card mb-5">
        <div class="card-header">
            <h4>using embed</h4>
        </div>
        <div class="card-body">
            <embed aria-label="PDF Report" src="@PdfBlobUrl" type="@PdfContentType" width="100%" height="100%">
        </div>
    </section>
}

@code
{
    private const string ReportUrl = "/api/Reports/GetPdfReport";
    private const string PdfContentType = "application/pdf";

    private string PdfBlobUrl;

    private async Task handlePrintPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `PrintFromUrlAsync` method.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        await JSRuntime.PrintBlazorByteArrayAsync(byteArray, PdfContentType);
    }

    private async Task handleDownloadPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `DownloadFromUrlAsync` method.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        await JSRuntime.DownloadBlazorByteArrayAsync("report.pdf", byteArray, PdfContentType);
    }

    private async Task handleShowPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `ReportUrl` as the `src` of the `iframe` directly.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        PdfBlobUrl = await JSRuntime.CreateBlobUrlAsync(byteArray, PdfContentType);
    }

    // Tips:
    // 1- How do I enable/disable the built-in pdf viewer of FireFox
    // https://support.mozilla.org/en-US/kb/disable-built-pdf-viewer-and-use-another-viewer
    // 2- How to configure browsers to use the Adobe PDF plug-in to open PDF files
    // https://helpx.adobe.com/acrobat/kb/pdf-browser-plugin-configuration.html
    // https://helpx.adobe.com/acrobat/using/display-pdf-in-browser.html
    // 3- Microsoft Edge is gaining new PDF reader features within the Windows 10 Fall Creator’s Update (version 1709).
}
توضیحات:
- پس از تهیه‌ی JsBinaryFilesUtils و متدهای الحاقی متناظر با آن، اکنون تنها کافی است با  استفاده از متد ()HttpClient.GetByteArrayAsync، فایل PDF ارائه شده‌ی توسط یک اکشن متد را به صورت آرایه‌ای از بایت‌ها دریافت و سپس به متدهای چاپ (PrintBlazorByteArrayAsync) و دریافت (DownloadBlazorByteArrayAsync) آن ارسال کنیم.
- در مورد نمایش آرایه‌ای از بایت‌های دریافتی، وضعیت کمی متفاوت است. ابتدا باید توسط متد CreateBlobUrlAsync، آدرس موقتی این آرایه را در مرورگر تولید کرد و سپس این آدرس را برای مثال به src یک iframe انتساب دهیم تا PDF را با استفاده از امکانات توکار مرورگر، نمایش دهد.


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

    }

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

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

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

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