مطالب
دریافت اطلاعات از پایگاه داده بواسطه Stored Procedure در EF Core 2.0
همواره در تکنولوژی  EF CodeFirst، چه در ASP.NET MVC و چه در ASP.NET Core، استفاده از امکانات بومی پایگاه‌های داده با محدودیت‌هایی مواجه بوده‌است. یکی از این اشکالات، عدم توانایی این تکنولوژی در گرفتن لیستی از اطلاعات که منطبق بر بیشتر از یک مدل می‌باشد، هست. در این مقاله تمرکز بر روی رفع این اشکال، بدون نیاز به اضافه کردن مدخل جدیدی به پروژه می‌باشد. بنابراین پیشنیاز ضروری این مبحث، مطالعه «شروع به کار با EF Core 1.0» ، مخصوصا «استفاده از امکانات بومی بانک‌های اطلاعاتی» است.

Stored Procedure چیست ؟

Stored Procedure  یا  SP  یا به زبان فارسی «رویه‌های ذخیره شده» اشیایی اجرا پذیر در بانک اطلاعاتی SQL Server هستند که شامل یک یا چندین دستور SQL می‌شوند. این رویه‌ها می‌توانند پارامتر‌های ورودی و خروجی داشته باشند؛ همچنین می‌توانند لیستی از موجودیت‌ها را نیز برگردانند و یا می‌توان داخل این رویه‌ها به زبان T-SQL برنامه نویسی کرد.
مهم‌ترین کاربر این رویه‌ها، ذخیره کردن دستورات Select , Insert , Update , Delete هست و یا ترکیبی از این‌ها .


اشکال راه حل‌های پیش فرض مبتنی بر Context

برای استفاده از راه حل‌های پیش فرض  مبتنی بر Context، همانطور که در مقاله «استفاده از امکانات بومی بانک‌های اطلاعاتی» به آن پرداخته شده، سه روش کلی برای استفاده از Stored Procedure  پیشنهاد شده‌است:
- روش اول استفاده از متد fromsql است. اشکال این متد، محدودیت استفاده برای یک موجودیت برنامه  است و به زبان ساده نمی‌توان در کوئری پایگاه داده از join  استفاده کرد.
- روش دوم استفاده از متد ExecuteSqlCommand موجود در context برنامه است . اشکال این متد void بودن آن است که باعث می‌شود بازگشتی از پایگاه داده حاصل نشود.
- روش سوم استفاده از متد ExecuteScalar  موجود در Context برنامه است. اشکالی که به این متد گرفته می‌شود، Scalar  بودن مقدار بازگشتی از آن است که باعث می‌شود نتوانیم لیستی از موجودیت‌ها را به ViewModel مورد نظر نگاشت کنیم.

راه حل این مشکل

برای حل این مشکلات که بسیار هم مهم هستند، اول باید قطعه کد زیر را به Context برنامه اضافه نمود:
public void OpenConnection()
{
   Database.OpenConnection();
}

public DbCommand Command()
{
   DbCommand cmd = Database.GetDbConnection().CreateCommand();
   return cmd;
}
سپس در اینترفیس IUnitOfWork  که در مطلب «لایه بندی و تزریق وابستگی‌ها» در مورد آن بحث شده، متد OpenConnection و Command را اضافه می‌کنیم:
void OpenConnection();
DbCommand Command();
حال کلاس و اینترفیس جدیدی را برای پیاده سازی سرویس اتصال به Stored Procedure ایجاد کرده و  در کلاس آغازین برنامه، به‌صورت AddScopped این سرویس را برای استفاده از تزریق وابستگی توکار ASP.NET Core  معرفی می‌کنیم:
public void ConfigureServices(IServiceCollection services)
{
     services.AddScoped<IUnitOfWork, ApplicationDbContext>();
     services.AddScoped<ISpReader, SpReader>();
}
سپس در سازنده کلاس این سرویس، اینترفیس IUnitOfWork را تزریق کرده تا بتوانیم از متد‌های نوشته شده در Context استفاده کنیم. حال اقدام به پیاده سازی متد GetFromSp بصورت زیر می‌کنیم :
public List<ViewModel> GetFromSp <ViewModel>(string[,] Parametr, string NameSp) where ViewModel  : new()
        {
            _uow.OpenConnection();
            DbCommand cmd = _uow.Command();
            cmd.CommandText = NameSp;
            cmd.CommandType = CommandType.StoredProcedure;
            var countParametr = Parametr.GetLength(0);

            for (int i = 0; i < countParame tr; i++)
            {
                cmd.Parameters.Add(new SqlParameter { ParameterName = Parametr[i, 0], Value = Parametr[i, 1] });
            }

            List<ViewModel> list = new List<ViewModel >();
            using (var reader = cmd.ExecuteReader())
            {
                if (reader != null && reader.HasRows)
                {
                    var entity = typeof(ViewModel);
                    var propDict = new Dictionary<string, PropertyInfo>();
                    var props = entity.GetProperties
           (BindingFlags.Instance | BindingFlags.Public);
                    propDict = props.ToDictionary(p => p.Name.ToUpper(), p => p);
                    while (reader.Read())
                    {
                        ViewModel  newobject = new ViewModel ();

                        for (int index = 0; index < reader.FieldCount; index++)
                        {
                            if (propDict.ContainsKey(reader.GetName(index).ToUpper()))
                            {
                                var info = propDict[reader.GetName(index).ToUpper()];
                                if ((info != null) && info.CanWrite)
                                {
                                    var val = reader.GetValue(index);
                                    info.SetValue(newobject, (val == DBNull.Value) ? null : val, null);
                                }
                            }
                        }
                        list.Add(newobject);
                    }

                }
                return list;
            }
در این متد، اول با استفاده از OpenConnection، اتصالی را به پایگاه داده، باز کرده سپس شیئ از DbCommand را می‌سازیم و نام Stored Procedure و نوع کوئری ارسالی را معین می‌کنیم. حال با استفاده از  حلقه for، نام و مقدار پارامتر‌های ارسال شده به متد را به شیئ cmd اضافه می‌کنیم. در مرحله بعد، لیستی را از کلاس مدلی که باید مقادیر بازگشتی به آن نگاشت شوند و بعنوان کلاس جنریک به متد ارسال شده است، می‌سازیم. با متد ExecuteReader که در شیئ ساخته شده از Command موجود می‌باشد، اقدام به خواندن اطلاعات از Stored Procedure کرده و در شیئ Reader نگه داری می‌کنیم و سپس اطلاعات خوانده شده را با استفاده از Dictionary و متد Add به لیست ساخته شده اضافه می‌کنیم. در آخر لیست ساخته شده در حلقه While را بعنوان نتیجه متد باز می‌گردانیم.

همچنین می‌توان برای استفاده این متد برای رویه‌های بدون پارامتر ورودی، از OverLoad این متد، با حذف قطعات کد زیر:
var countParametr = Parametr.GetLength(0);
for (int i = 0; i < countParametr; i++)
{
     cmd.Parameters.Add(new SqlParameter { ParameterName = Parametr[i, 0], Value = Parametr[i, 1] });
}
و حذف آرایه string[,] Parameter  از ورودی متد، استفاده نمود .

روش استفاده از این متد

برای استفاده از این متد، لازم است چند نکته رعایت شوند:
1- خروجی Stored Procedure دقیقا منطبق بر ViewModel ارسالی به متد جهت تشکیل لیست باشد.
2- لیست پارامتر‌ها باید بصورت آرایه دوبعدی باشد که اندازه بعد اول، تعداد پارامتر‌ها و اندازه بعد دوم 2 باشد.
3- در ماتریسی که از این پارامتر‌ها ساخته می‌شود، ستون اول نام پارامتر و ستون دوم مقدار پارامتر ست می‌شود.

بطور مثال Stored Procedure  زیر حاوی سه پارامتر است :
CREATE PROCEDURE [dbo].[isRelation](
@TableName as varchar(50),
@FieldOfRelation as varchar(70),
@ValueOfField as int)
برای دسترسی به این رویه ابتدا در سرویس استفاده کننده، ISpReader را تزریق می‌کنیم و سپس بصورت زیر مقدمات استفاده از این سرویس را فراهم می‌کنیم:

public class EntityServices : IEntityService
    {
        private ISpreader _Reader;
        public EntityServices( ISpreader reader)
        {
            _Reader = reader;
        }

        public List<StoreProcedureResultViewModel>  IsRelation(string tableName , int keyValue, string keyFieldName)
        {
            List<StoreProcedureResultViewModel> IsContact;
            try
            {
                string[,] Parametr = new string[3, 2];
                Parametr[0, 0] = "@TableName";
                Parametr[0, 1] = tableName ;
                Parametr[1, 0] = "@ValueOfField";
                Parametr[1, 1] = keyValue.ToString().Trim();
                Parametr[2, 0] = "@FieldOfRelation";
                Parametr[2, 1] = keyFieldName.Trim();
                IsContact = _Reader.GetSp<StoreProcedureResultViewModel>(Parametr, "IsRelation");
                return IsContact;
            }
            catch (Exception ex)
            {
            }
        }
    }
بدین ترتیب با استفاده از این متد توانستیم لیستی از ViewModel منطبق بر خروجی Stored Procedure  را بدست آوریم.  
نظرات مطالب
مدیریت محل اعمال Google analytics در ASP.NET MVC
با تشکر.
من دقیقاً روش بالا را استفاده کرده ام. در کلاینت خوب جواب می‌دهد ولی وقتی بر روی سرور آپلود میکنم ، نتیجه ای حاصل نمی‌شود.
string username = ConfigurationManager.AppSettings["GoogleAnalyticsUsername"];
        string password = ConfigurationManager.AppSettings["GoogleAnalyticsPassword"];
        string profileId = ConfigurationManager.AppSettings["GoogleAnalyticsProfileId"];
        string yesterdayDate = DateTime.Today.AddDays(-1).ToString("yyyy-MM-dd");
var service = new AnalyticsService("WebSiteAnalytics");

                service.setUserCredentials(username, password);
                var dataQuery = new DataQuery()
                {
                    Ids = profileId,
                    Metrics = "ga:pageviews",
                    Sort = "ga:pageviews",
                };
                DataFeed dataFeed = null;

                //--- total visit
                dataQuery.GAStartDate = new DateTime(2012, 3, 1).ToString("yyyy-MM-dd");
                dataQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd");
                dataFeed = service.Query(dataQuery);
                var totalEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" :
                    (dataFeed.Entries[0] as DataEntry).Metrics[0].Value;

                //--- yesterday visit
                dataQuery.GAStartDate = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd");
                dataQuery.GAEndDate = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd");
                dataFeed = service.Query(dataQuery);
                var yesterdayEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" :
                    (dataFeed.Entries[0] as DataEntry).Metrics[0].Value;

                //--- today visit
                dataQuery.GAStartDate = DateTime.Now.ToString("yyyy-MM-dd");
                dataQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd");
                dataFeed = service.Query(dataQuery);
                var todayEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" :
                    (dataFeed.Entries[0] as DataEntry).Metrics[0].Value;

                //--- last week visit
                dataQuery.GAStartDate = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");
                dataQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd");
                dataFeed = service.Query(dataQuery);
                var lastWeekEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" :
                    (dataFeed.Entries[0] as DataEntry).Metrics[0].Value;

                //--- last month visit
                dataQuery.GAStartDate = DateTime.Now.AddDays(-30).ToString("yyyy-MM-dd");
                dataQuery.GAEndDate = DateTime.Now.ToString("yyyy-MM-dd");
                dataFeed = service.Query(dataQuery);
                var lastMonthEntry = (dataFeed.Entries == null || dataFeed.Entries.Count == 0) ? "0" :
                    (dataFeed.Entries[0] as DataEntry).Metrics[0].Value;

مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت هشتم- تعریف سطوح دسترسی پیچیده
تعریف نقش‌ها، ساده‌ترین روش محدود کردن دسترسی به منابع است؛ برای نمونه مشخص می‌کنیم که کاربران دارای نقش PayingUser، امکان سفارش دادن نگارش قاب شده‌ی تصاویر را داشته باشند. اما می‌خواهیم منطق دسترسی به منابع مختلف را پیچیده‌تر کنیم. برای مثال می‌خواهیم بررسی کنیم اگر منبعی واقعا متعلق به کاربر جاری سیستم است، به آن دسترسی داشته باشد. برای مثال هرچند کاربر جاری دارای نقش PayingUser است، اما آیا باید اجازه‌ی ویرایش تصاویر تمام کاربران، برای او صادر شده باشد؟ برای پیاده سازی یک چنین موارد پیچیده‌ای که فراتر از مفهوم نقش‌ها هستند، ویژگی جدیدی به نام Authorization policies به ASP.NET Core اضافه شده‌است که آن‌را در این قسمت بر اساس امکانات IdentityServer 4 بررسی می‌کنیم.


مقایسه تعریف سطوح دسترسی «مبتنی بر نقش‌ها» با سطوح دسترسی «مبتنی بر سیاست‌های امنیتی»

- در سطوح دسترسی «مبتنی بر نقش‌ها»
یک‌سری نقش از پیش تعریف شده وجود دارند؛ مانند PayingUser و یا FreeUser که کاربر توسط هر نقش، به یکسری دسترسی‌های خاص نائل می‌شود. برای مثال PayingUser می‌تواند نگارش قاب شده‌ی تصاویر را سفارش دهد و یا تصویری را به سیستم اضافه کند.

- در سطوح دسترسی «مبتنی بر سیاست‌های امنیتی»
سطوح دسترسی بر اساس یک سری سیاست که بیانگر ترکیبی از منطق‌های دسترسی هستند، اعطاء می‌شوند. این منطق‌ها نیز از طریق ترکیب User Claims حاصل می‌شوند و می‌توانند منطق‌های پیچیده‌تری را به همراه داشته باشند. برای مثال اگر کاربری از کشور A است و نوع اشتراک او B است و اگر در بین یک بازه‌ی زمانی خاصی متولد شده باشد، می‌تواند به منبع خاصی دسترسی پیدا کند. به این ترتیب حتی می‌توان نیاز به ترکیب چندین نقش را با تعریف یک سیاست امنیتی جدید جایگزین کرد. به همین جهت نسبت به روش بکارگیری مستقیم کار با نقش‌ها ترجیح داده می‌شود.


جایگزین کردن بررسی سطوح دسترسی توسط نقش‌ها با روش بکارگیری سیاست‌های دسترسی

در ادامه می‌خواهیم بجای بکارگیری مستقیم نقش‌ها جهت محدود کردن دسترسی به قسمت‌های خاصی از برنامه‌ی کلاینت، تنها کاربرانی که از کشور خاصی وارد شده‌اند و نیز سطح اشتراک خاصی را دارند، بتوانند دسترسی‌های ویژه‌ای داشته باشند؛ چون برای مثال امکان ارسال مستقیم تصاویر قاب شده را به کشور دیگری نداریم.

تنظیم User Claims جدید در برنامه‌ی IDP
برای تنظیم این سیاست امنیتی جدید، ابتدا دو claim جدید subscriptionlevel و country را به خواص کاربران در کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    Username = "User 1",
                    // ...

                    Claims = new List<Claim>
                    {
    // ...
                        new Claim("subscriptionlevel", "PayingUser"),
                        new Claim("country", "ir")
                    }
                },
                new TestUser
                {
                    Username = "User 2",
// ...

                    Claims = new List<Claim>
                    {
    // ...
                        new Claim("subscriptionlevel", "FreeUser"),
                        new Claim("country", "be")
                    }
                }
            };
        }
سپس باید تعاریف این claims جدید را به متد GetIdentityResources افزود تا به صورت scopeهای جدید از طرف کلاینت‌ها قابل درخواست باشند و چون این claimها استاندارد نیستند، برای تعریف آن‌ها از IdentityResource استفاده می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // identity-related resources (scopes)
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
   // ...     
                new IdentityResource(
                    name: "country",
                    displayName: "The country you're living in",
                    claimTypes: new List<string> { "country" }),
                new IdentityResource(
                    name: "subscriptionlevel",
                    displayName: "Your subscription level",
                    claimTypes: new List<string> { "subscriptionlevel" })
            };
        }
همچنین باید مطمئن شد که کلاینت مدنظر ما قادر است این scopeهای تعریف شده را درخواست کند و IDP مجاز است تا آن‌ها را بازگشت دهد. برای این منظور آن‌ها را به لیست AllowedScopes تعریف کلاینت، اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
// ...
                    AllowedScopes =
                    {
    // ...
                        "country",
                        "subscriptionlevel"
                    }
// ...
                }
             };
        }
    }

استفاده‌ی از User Claims جدید در برنامه‌ی MVC Client
در ادامه به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client مراجعه کرده و دو scope جدیدی را که در سمت IDP تعریف کردیم، در اینجا در تنظیمات متد AddOpenIdConnect، درخواست می‌دهیم:
options.Scope.Add("subscriptionlevel");
options.Scope.Add("country");
به این ترتیب برنامه‌ی کلاینت می‌تواند دسترسی به این دو claim جدید را از طریق IDP، پیدا کند.
البته همانطور که در قسمت‌های قبل نیز ذکر شد، اگر claim ای در لیست نگاشت‌های تنظیمات میان‌افزار OpenID Connect مایکروسافت نباشد، آن‌را در لیست this.User.Claims ظاهر نمی‌کند. به همین جهت همانند claim role که پیشتر MapUniqueJsonKey را برای آن تعریف کردیم، نیاز است برای این دو claim نیز نگاشت‌های لازم را به سیستم افزود:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
options.ClaimActions.MapUniqueJsonKey(claimType: "subscriptionlevel", jsonKey: "subscriptionlevel");
options.ClaimActions.MapUniqueJsonKey(claimType: "country", jsonKey: "country");

ایجاد سیاست‌های دسترسی در برنامه‌ی MVC Client

برای تعریف یک سیاست دسترسی جدید در کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی MVC Client، به متد ConfigureServices آن مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthorization(options =>
            {
                options.AddPolicy(
                   name: "CanOrderFrame",
                   configurePolicy: policyBuilder =>
                    {
                        policyBuilder.RequireAuthenticatedUser();
                        policyBuilder.RequireClaim(claimType: "country", requiredValues: "ir");
                        policyBuilder.RequireClaim(claimType: "subscriptionlevel", requiredValues: "PayingUser");
                    });
            });
در اینجا نحوه‌ی تعریف یک Authorization Policy جدید را مشاهده می‌کنید. ابتدا یک نام برای آن تعریف می‌شود که در قسمت‌های دیگر برنامه جهت ارجاع به آن مورد استفاده قرار می‌گیرد. سپس تنظیمات این سیاست دسترسی جدید را مشاهده می‌کنید که در آن نیاز است کاربر مدنظر حتما اعتبارسنجی شده باشد. از کشور ir بوده و همچنین سطح اشتراک او PayingUser باشد. در اینجا پارامتر requiredValues، یک آرایه را می‌پذیرد. بنابراین اگر برای مثال کشورهای دیگری نیز مدنظر هستند، می‌توان لیست آن‌ها را در اینجا اضافه کرد.
به علاوه policyBuilder شامل متد RequireRole نیز هست. به همین جهت است که این روش تعریف سطوح دسترسی، روش قدیمی مبتنی بر نقش‌ها را جایگزین کرده و در برگیرنده‌ی آن نیز می‌شود؛ چون در این سیستم، role نیز تنها یک claim است، مانند country و یا subscriptionlevel فوق.


بررسی نحوه‌ی استفاده‌ی از Authorization Policy تعریف شده و جایگزین کردن آن با روش بررسی نقش‌ها

تا کنون از روش بررسی سطوح دسترسی‌ها بر اساس نقش‌های کاربران در دو قسمت استفاده کرده‌ایم:
الف) اصلاح Views\Shared\_Layout.cshtml برای استفاده‌ی از Authorization Policy
در فایل Layout با بررسی نقش PayingUser، منوهای مرتبط با این نقش را فعال می‌کنیم:
@if(User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
برای جایگزین کردن آن جهت استفاده‌ی از سیاست دسترسی جدید CanOrderFrame، ابتدا نیاز است در این View به سرویس IAuthorizationService دسترسی پیدا کنیم که روش تزریق آن‌را در ذیل مشاهده می‌کنید:
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
پس از آن، روش استفاده‌ی از این سرویس را در ذیل مشاهده می‌کنید:
@if (User.IsInRole("PayingUser"))
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
}
@if ((await AuthorizationService.AuthorizeAsync(User, "CanOrderFrame")).Succeeded)
{
  <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
اکنون لینک منوی درخواست نگارش قاب شده‌ی یک تصویر، صرفا به کاربران تامین کننده‌ی سیاست دسترسی CanOrderFrame نمایش داده می‌شود.

ب) اصلاح کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs برای استفاده‌ی از Authorization Policy
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Policy = "CanOrderFrame")]
        public async Task<IActionResult> OrderFrame()
        {
در اینجا فیلتر Authorize امکان پذیرش نام یک Policy را نیز به همراه دارد.

اکنون برای آزمایش برنامه یکبار از آن خارج شده و سپس توسط اکانت User 1 که از نوع PayingUser در کشور ir است، به آن وارد شوید.
ابتدا به قسمت IdentityInformation آن وارد شوید. در اینجا لیست claims جدید را می‌توانید مشاهده کنید. همچنین لینک سفارش تصویر قاب شده نیز نمایان است و می‌توان به آدرس آن نیز وارد شد.


استفاده از سیاست‌های دسترسی در سطح برنامه‌ی Web API

در سمت برنامه‌ی Web API، در حال حاضر کاربران می‌توانند به متدهای Get ،Put و Delete ای که رکوردهای آن‌ها الزاما متعلق به آن‌ها نیست دسترسی داشته باشند. بنابراین نیاز است از ورود کاربران به متدهای تغییرات رکوردهایی که OwnerID آن‌ها با هویت کاربری آن‌ها تطابقی ندارد، جلوگیری کرد. در این حالت Authorization Policy تعریف شده نیاز دارد تا با سرویس کاربران و بانک اطلاعاتی کار کند. همچنین نیاز به دسترسی به اطلاعات مسیریابی جاری را برای دریافت ImageId دارد. پیاده سازی یک چنین سیاست دسترسی پیچیده‌ای توسط متدهای RequireClaim و RequireRole میسر نیست. خوشبختانه امکان بسط سیستم Authorization Policy با پیاده سازی یک IAuthorizationRequirement سفارشی وجود دارد. RequireClaim و RequireRole، جزو Authorization Requirementهای پیش‌فرض و توکار هستند. اما می‌توان نمونه‌های سفارشی آن‌ها را نیز پیاده سازی کرد:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace ImageGallery.WebApi.Services
{
    public class MustOwnImageRequirement : IAuthorizationRequirement
    {
    }

    public class MustOwnImageHandler : AuthorizationHandler<MustOwnImageRequirement>
    {
        private readonly IImagesService _imagesService;
        private readonly ILogger<MustOwnImageHandler> _logger;

        public MustOwnImageHandler(
            IImagesService imagesService,
            ILogger<MustOwnImageHandler> logger)
        {
            _imagesService = imagesService;
            _logger = logger;
        }

        protected override async Task HandleRequirementAsync(
            AuthorizationHandlerContext context, MustOwnImageRequirement requirement)
        {
            var filterContext = context.Resource as AuthorizationFilterContext;
            if (filterContext == null)
            {
                context.Fail();
                return;
            }

            var imageId = filterContext.RouteData.Values["id"].ToString();
            if (!Guid.TryParse(imageId, out Guid imageIdAsGuid))
            {
                _logger.LogError($"`{imageId}` is not a Guid.");
                context.Fail();
                return;
            }

            var subClaim = context.User.Claims.FirstOrDefault(c => c.Type == "sub");
            if (subClaim == null)
            {
                _logger.LogError($"User.Claims don't have the `sub` claim.");
                context.Fail();
                return;
            }

            var ownerId = subClaim.Value;
            if (!await _imagesService.IsImageOwnerAsync(imageIdAsGuid, ownerId))
            {
                _logger.LogError($"`{ownerId}` is not the owner of `{imageIdAsGuid}` image.");
                context.Fail();
                return;
            }

            // all checks out
            context.Succeed(requirement);
        }
    }
}
در پروژه‌ی ImageGallery.WebApi.Services ابتدا یک Authorization Requirement و سپس پیاده سازی کننده‌ی آن که Authorization Handler نام دارد را تعریف کرده‌ایم. این پروژه نیاز به وابستگی‌های ذیل را دارد تا کامپایل شود.
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.1.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.1.0" />
  </ItemGroup>
</Project>

پیاده سازی سیاست‌های پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class MustOwnImageRequirement : IAuthorizationRequirement
{
}
ابتدا نیاز است یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی، خالی است و صرفا به عنوان نشانه‌ای جهت یافت AuthorizationHandler استفاده کننده‌ی از آن استفاده می‌شود. در اینجا در صورت نیاز می‌توان یک سری خاصیت اضافه را تعریف کرد تا آن‌ها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.

2- پیاده سازی یک AuthorizationHandler استفاده کننده‌ی از نیازمندی دسترسی تعریف شده
که کدهای کامل آن‌را در کلاس MustOwnImageHandler مشاهده می‌کنید. کار آن با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن توسط ASP.NET Core استفاده می‌شود. بنابراین در اینجا MustOwnImageRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آن‌را بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری می‌تواند به منبع درخواستی بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت می‌کند.
در این پیاده سازی از filterContext.RouteData برای یافتن Id تصویر مورد نظر استفاده شده‌است. همچنین Id شخص جاری نیز از sub claim موجود استخراج گردیده‌است. اکنون این اطلاعات را به سرویس تصاویر ارسال می‌کنیم تا توسط متد IsImageOwnerAsync آن مشخص شود که آیا کاربر جاری سیستم، همان کاربری است که تصویر را در بانک اطلاعاتی ثبت کرده‌است؟ اگر بله، با فراخوانی context.Succeed به سیستم Authorization اعلام خواهیم کرد که این سیاست دسترسی و نیازمندی مرتبط با آن با موفقیت پشت سر گذاشته شده‌است.

3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحله‌ی زیر است:
مراجعه‌ی به کلاس ImageGallery.WebApi.WebApp\Startup.cs و افزودن نیازمندی آن:
namespace ImageGallery.WebApi.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthorization(authorizationOptions =>
            {
                authorizationOptions.AddPolicy(
                   name: "MustOwnImage",
                   configurePolicy: policyBuilder =>
                    {
                        policyBuilder.RequireAuthenticatedUser();
                        policyBuilder.AddRequirements(new MustOwnImageRequirement());
                    });
            });
            services.AddScoped<IAuthorizationHandler, MustOwnImageHandler>();
ابتدا باید MustOwnImageHandler تهیه شده را به سیستم تزریق وابستگی‌ها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه MustOwnImage تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه می‌کنیم. همانطور که ملاحظه می‌کنید یک وهله‌ی جدید از MustOwnImageRequirement در اینجا ثبت شده‌است. همین وهله به متد HandleRequirementAsync نیز ارسال می‌شود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، می‌توان خواص مرتبطی را به کلاس MustOwnImageRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را می‌توان ثبت کرد و نه Handler آن‌را. این Handler از سیستم تزریق وابستگی‌ها بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا می‌شود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آن‌را به سیستم تزریق وابستگی‌ها معرفی کرده‌اید).

پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy ="MustOwnImage")]
به صورت خودکار توسط MustOwnImageHandler مدیریت می‌شود.


اعمال سیاست دسترسی پویای تعریف شده به Web API

پس از تعریف سیاست دسترسی MustOwnImage که پویا عمل می‌کند، اکنون نوبت به استفاده‌ی از آن در کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs است:
namespace ImageGallery.WebApi.WebApp.Controllers
{
    [Route("api/images")]
    [Authorize]
    public class ImagesController : Controller
    {
        [HttpGet("{id}", Name = "GetImage")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> GetImage(Guid id)
        {
        }

        [HttpDelete("{id}")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> DeleteImage(Guid id)
        {
        }

        [HttpPut("{id}")]
        [Authorize("MustOwnImage")]
        public async Task<IActionResult> UpdateImage(Guid id, [FromBody] ImageForUpdateModel imageForUpdate)
        {
        }
    }
}
در اینجا در سه قسمت GetImage ،DeleteImage و UpdateImage با اعمال سیاست دسترسی پویای MustOwnImage، اگر کاربر جاری همان Owner تصویر درخواستی نباشد، دسترسی او به اجرای کدهای داخل این اکشن متدها به صورت خودکار بسته خواهد شد.




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
PowerShell 7.x - قسمت نهم - آشنایی با Crescendo
همانطور که در ابتدای این سری نیز اشاره شد، یکی از ویژگی‌های منحصربه‌فرد PowerShell، طراحی شیءگرای آن است، به‌طوریکه خروجی cmdletهای آن، به صورت آبجکت هستند. همچنین، در PowerShell امکان اجرای کامندهای native نیز وجود دارد. به عنوان مثال اگر کامند زیر را وارد کنید: 
git log --oneline
خروجی، همانطوری که در دیگر shellها انتظار میرود، نمایش داده خواهد شد؛ یعنی به صورت string. همچنین امکان intellisense را نیز برای پارامترهای کامند موردنظر نخواهیم داشت؛ چون در اصل، به اصطلاح یک legacy command است و نه یک cmdlet. برای بهره بردن از امکانات PowerShell میتوانیم این نوع کامندها را توسط یک wrapper به cmdlet تبدیل کنیم، اما آپدیت نگه‌داشتن این wrapper و نوشتن آن فرآیند سختی است. برای سهولت انجام اینکار، یک فریم‌ورک تحت عنوان Crescendo توسط مایکروسافت ارائه شده است.
یک مثال
فرض کنید میخواهیم کامند git log را به همراه تعدادی از دستورات آن به یک PowerShell cmdlet تبدیل کنیم؛ برای اینکار ابتدا نیاز است ماژول عنوان شده را نصب کنیم: 
Install-Module -Name Microsoft.PowerShell.Crescendo
بعد از نصب ماژول فوق، یکسری cmdlet به مجموعه کامندهای PowerShell اضافه خواهند شد. یکی از این کامندها New-CrescendoCommand است. با کمک این کامند، فایل JSON موردنیاز Crescendo را میتوانیم تولید کنیم: 
$Configuration = @{
    '$schema' = "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11"
    Commands  = @()
}
$parameters = @{
    Verb = "Get"
    Noun = "GitLog"
    OriginalName = "git"
}
$Configuration.Commands += New-CrescendoCommand @parameters

$Configuration | ConvertTo-Json -Depth 3 | Out-File ./git-ps.json
در اینجا تعیین کرده‌ایم که کامندی که میخواهیم برایمان تولید شود، چه ویژگی‌هایی باید داشته باشد. به عنوان مثال Verb آن Get و Noun آن باید GitLog باشد (براساس استانداری که مایکروسافت برای نامگذاری cmdletها پیشنهاد میدهد). در نهایت میتوانیم به صورت Get-GitLog از آن استفاده کنیم. همچنین legacy command اصلی که میخواهیم برای آن cmdlet ایجاد کنیم نیز توسط OriginalName تعیین شده‌است. لازم به ذکر است که در ویندوز باید مسیر کامل آن را وارد کنید. سپس با اجرای دستورات فوق، خروجی زیر برایمان تولید خواهد شد: 
{
  "Commands": [
    {
      "Verb": "Get",
      "Noun": "GitLog",
      "OriginalName": "git",
      "OriginalCommandElements": null,
      "Platform": [
        "Windows",
        "Linux",
        "MacOS"
      ],
      "Elevation": null,
      "Aliases": null,
      "DefaultParameterSetName": null,
      "SupportsShouldProcess": false,
      "ConfirmImpact": null,
      "SupportsTransactions": false,
      "NoInvocation": false,
      "Description": null,
      "Usage": null,
      "Parameters": [],
      "Examples": [],
      "OriginalText": null,
      "HelpLinks": null,
      "OutputHandlers": null
    }
  ],
  "$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11"
}
نکته: دقت داشته باشید که schema$ باید درون single quote نوشته شود؛ چون در غیراینصورت، key آن درون فایل تولید شده، خالی خواهد بود: 
"": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
با کمک این schema درون Visual Studio Code امکان Intelisense را نیز خواهیم داشت: 


اکنون باید این فایل Configuration را به Crescendo معرفی کنیم تا cmdlet را برایمان تولید کند. اینکار را توسط Export-CrescendoModule انجام خواهیم داد: 

Export-CrescendoModule -Configuration ./git-ps.json -ModuleName ./git-ps.psm1

با اجرای دستور فوق، فایل‌های git.psm1 و همچنین git.psd1 تولید خواهند شد. نیاز به بررسی فایل‌های جنریت شده نیست؛ چون تنها جایی که با آن باید در ارتباط باشیم، همان فایل JSON ابتدای بحث است که در ادامه آن را بررسی خواهیم کرد. اما قبل از آن اجازه دهید ماژول تولید شده را Import کنیم و دستور Get-GitLog را وارد کنیم: 

PP /> Import-Module ./git-ps.psd1
PS /> Get-GitLog

usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
           <command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
   clone     Clone a repository into a new directory
   init      Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)
   add       Add file contents to the index
   mv        Move or rename a file, a directory, or a symlink
   restore   Restore working tree files
   rm        Remove files from the working tree and from the index

examine the history and state (see also: git help revisions)
   bisect    Use binary search to find the commit that introduced a bug
   diff      Show changes between commits, commit and working tree, etc
   grep      Print lines matching a pattern
   log       Show commit logs
   show      Show various types of objects
   status    Show the working tree status

grow, mark and tweak your common history
   branch    List, create, or delete branches
   commit    Record changes to the repository
   merge     Join two or more development histories together
   rebase    Reapply commits on top of another base tip
   reset     Reset current HEAD to the specified state
   switch    Switch branches
   tag       Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)
   fetch     Download objects and refs from another repository
   pull      Fetch from and integrate with another repository or a local branch
   push      Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.

همانطور که مشاهده میکنید، خروجی دستور git، نمایش داده شده‌است. دلیل آن نیز این است که در فایل configuration، هیچ آرگومانی را به عنوان ورودی آن تعیین نکرده‌ایم. برای اضافه کردن آرگومان‌های موردنظر باید پراپرتی OrginalCommandElements را مقدار دهی کنیم: 

"OriginalCommandElements": ["log", "--oneline"],

بنابراین با فراخوانی دستور Get-GitLog، در اصل دستور git log —oneline فراخوانی خواهد شد:  

PS /> Get-GitLog

e9590e8 init

اما تا اینجا نیز خروجی به صورت رشته‌ایی است. برای داشتن یک خروجی Object، باید پراپرتی OutputHandlers را از Configuration، تغییر دهیم: 

"OutputHandlers": [
  {
    "ParameterSetName": "Default",
    "Handler": "$args[0] | ForEach-Object { $hash, $message = $_.Split(' ', 2) ; [PSCustomObject]@{ Hash = $hash; Message = $message } }"
  }
]

در اینجا توسط args$ به خروجی کامند اصلی دسترسی خواهیم داشت. این خروجی را سپس با کمک ForEach-Object، به یک شیء با پراپرتی‌های Hash و Message تبدیل کرده‌ایم. در اینجا فقط میخواستم روال تهیه یک آبجکت را از کامندهایی که خروجی JSON ندارند، نشان دهم؛ اما خوشبختانه توسط پرچم pretty در git log، امکان تهیه‌ی خروجی JSON را نیز داریم: 

git log --pretty=format:'{"commit": "%h", "author": "%an", "date": "%ad", "message": "%s"}'

در نتیجه عملاً نیازی به split کردن نیست و بجای آن میتوانیم به صورت مستقیم، خروجی را توسط ConvertFrom-Json پارز کنیم: 

"OutputHandlers": [
  {
    "ParameterSetName": "Default",
    "Handler": "$args[0] | ConvertFrom-Json"
  }
]

همچنین درون فایل schema با کمک پراپرتی Parameters، امکان تعریف پارامتر را نیز برای کامند Get-GitLog خواهیم داشت. به عنوان مثال میتوانیم فلگ reverse را نیز به کامند اصلی از طریق PowerShell ارسال کنیم: 

"Parameters": [
  {
    "Name": "reverse",
    "OriginalName": "--reverse",
    "ParameterType": "switch",
    "Description": "Reverse the order of the commits in the output."
  }
],

دقت داشته باشیم که با هربار تغییر فایل schema باید توسط دستور Export-CrescendoModule ماژول موردنظر را تولید کنید:

Export-CrescendoModule -Configuration ./git-ps.json -ModuleName ./git-ps.psm1
Import-Module ./git-ps.psd1

در نهایت cmdletمان به این صورت قابل استفاده خواهد بود:

مطالب
ایده‌ی ثبت خودکار سرویس‌ها، به همراه تنظیمات؛ بدون نوشتن هیچ کدی در ConfigureServices با روش Installer
خودکارسازی، در قسمت‌های مختلف یک پروژه می‌تواند انجام شود. نمونه‌های مختلف این خودکارسازی‌ها که اکثرا توسط رفلکشن انجام می‌شوند شامل نگاشت خودکار Dto به Entity و بالعکس (توسط AutoMapper)، ثبت خودکار تمام Entityها در DbContext بدون نیاز به ثبت تک تک آن‌ها به صورت  public DbSet<Person> People { get; set; }  (که در این روش خودکار، اسم جداول می‌تواند به صورت جمع ثبت شود)، ثبت خودکار EntityTypeConfigurationها، ثبت خودکار کلیه‌ی کلاس‌های Profile برای کانفیگ AutoMapper و رجیستر خودکار DI سرویس‌ها، تا نیازی به نوشتن کدهای تکراری و مشابه   ;()<services.AddTransient<IUserService, UserService نداشته باشیم. 
 برای مشاهده‌ی عملی پیاده‌سازی این نمونه‌ها می‌توانید به پروژه‌ی ASP.NET Core WebAPI مراجعه کنید. در این مقاله می‌خواهیم همین سناریو را برای ثبت سرویس‌هایمان در متد ConfigureServices انجام دهیم، تا نیازی به نوشتن هیچ کدی برای آن‌ها نداشته باشیم. 

ثبت سرویس‌های مختلف، به همراه تنظیمات آن‌ها (مانند Authentication، Swagger، DbContext، ApiVersioning و ...) در استارتاپ می‌تواند به چندین صورت انجام شود.
روش اول اینکه به صورت دستی تمام کدهای مربوط به رجیستر کردن سرویس‌ها و تنظیمات آن‌ها، در متد ConfigureServices نوشته شود که خیلی جالب نیست و موجب شلوغ شدن سریع این متد می‌شود. نمونه‌ی این شیوه را برای ثبت سرویس مربوط به DbContext می‌بینیم:
public void ConfigureServices(IServiceCollection services) {
      // DbContext Service
      services.AddDbContext<AppDbContext>(options =>
            {
                options
             .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {     sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });

      // register other services ....

}


راه دوم روش استفاده از متدهای الحاقی است؛ طوریکه برای هر سرویس، یک متد الحاقی را تعریف کنیم و از آن، در این متد استفاده کنیم که حجم کدها را تا حد زیادی کم می‌کند. برای مثال ثبت سرویس بالا را می‌توانیم در کلاس دیگری با نام DbContextServiceCollectionExtensions.cs ثبت کنیم:
public static class DbContextServiceCollectionExtensions
    {
        public static void AddDbContext(this IServiceCollection services)
        {
              services.AddDbContext<AppDbContext>(options =>
            {
                options
                    .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {
                    sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });
        }
    }
و سپس در متد ConfigureServices می‌توان آن را به صورت زیر استفاده کرد:
public void ConfigureServices(IServiceCollection services) {
// Add DbContext
 services.AddDbContext();

//.... Register other services
}

ولی اگه پروژه‌ی ما متوسط به بالا باشد، کم‌کم تعداد سرویس‌های ما زیاد می‌شود (برای مثال چند نمونه از سرویس‌های رایج مورد استفاده، شامل سرویس‌های لاگ خطاها مثل Elmah و سرویس HttpClientFactory و AutoRegisterDi (توضیح در ادامه مقاله) و AutoMapper و Cache و EFSecondLevelCache و Hangfire و ....) می‌بینیم که تعداد این سرویس‌ها هم زیاد است و حتی به صورت اکستنشن هم به مرور زمان باعث شلوغ شدن استارتاپ می‌شوند. ضمن اینکه یک کار تکراری است که باید هر بار انجام شود.

راه سوم ثبت سرویس، استفاده از یک اینترفیس به نام IServiceInstaller و استفاده از آن در کلاس‌های مختلف مربوط به ثبت سرویس و بعد خواندن خودکار این تنظیمات با یک خط کد ساده‌ی رفلکشن است که در ادامه می‌بینیم: 
اینترفیس IServiceInstaller را تعریف می‌کنیم: 
public interface IServiceInstaller
    {
        void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly);
    }
توضیح: پارامتر appSettings کلاسی شامل کلیه‌ی مقادیر فایل appsettings.json است. شما می‌توانید بجای آن از IConfiguration استفاده کنید و مقدار آن را در Startup پاس دهید. پارامتر آخر برای حالتی است که این فایل‌ها را در لایه‌ی دیگری به غیر از لایه‌ی اصلی Api (مثل لایه‌ی WebFamewrk) پیاده سازی می‌کنید.
سپس کلاس‌های ثبت سرویس‌هایمان را با ارث بری از این اینترفیس می‌سازیم. برای نمونه رجیستر DbContext را با ایجاد کلاسی به نام DbContextInstaller انجام می‌دهیم:
public class DbContextInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            services.AddDbContext<AppDbContext>(options =>
            {
                options
               .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {
                    sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });
        }
    }

حالا برای ثبت این کلاس و کلاس‌های مشابه Installer، می‌آییم یک متد الحاقی را برای متد ConfigureServices می‌نویسیم که در آن از رفلکشن استفاده می‌کنیم: 
public static class ServiceInstallerExtensions
    {
        public static void InstallServicesInAssemblies(this IServiceCollection services, AppSettings appSettings)
        {
            var startupProjectAssembly = Assembly.GetCallingAssembly();
            var assemblies = new[] { startupProjectAssembly, Assembly.GetExecutingAssembly() };
            var installers = assemblies.SelectMany(a => a.GetExportedTypes())
                .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(IServiceInstaller).IsAssignableFrom(c))
                .Select(Activator.CreateInstance).Cast<IServiceInstaller>().ToList();
            installers.ForEach(i => i.InstallServices(services, appSettings, startupProjectAssembly));
        }
    }

در نهایت متد ConfigureServices ما به صورت زیر خواهد بود (بعد از اضافه کردن تمام سرویس‌ها!):
public void ConfigureServices(IServiceCollection services)
        {
            //* HttpContextAccessor
            // services.AddHttpContextAccessor();

            //* Controllers
            services.AddControllers(options => { options.Filters.Add(new AuthorizeFilter()); })
                .AddNewtonsoftJson();

            //* Installers
            services.InstallServicesInAssemblies(_appSettings);
        }
کار تمام شد. حالا تمام سرویس‌های ما با ایجاد کلاس مرتبط و implement شدن از اینترفیس IServiceInstaller، به طور خودکار در استارتاپ و متد ConfigureServies ثبت خواهند شد.

فقط یک نکته آخر اینکه برای رجیستر خودکار DI سرویس‌ها (و ننوشتن کدهایی مانند   ()<services.AddTransient<IUserService, UserService برای رجیستر هر سرویس) می‌توانیم از Autofac استفاده کنیم (در پروژه‌ی بالا آمده است) و یا از پکیج AutoRegisterDi استفاده کنیم (متعلق به Jon P Smith) که از خود Container داخلی Core استفاده می‌کند و از Autofac سبکتر است. کلاسی می‌سازیم به نام RegisterServicesUsingAutoRegisterDiInstaller: 
public class RegisterServicesUsingAutoRegisterDiInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            var dataAssembly = typeof(SomeRepository).Assembly;
            var serviceAssembly = typeof(SomeService).Assembly;
            var webFrameworkAssembly = Assembly.GetExecutingAssembly();
            var startupAssembly = startupProjectAssembly;
            var assembliesToScan = new[] { dataAssembly, serviceAssembly, webFrameworkAssembly, startupAssembly };

            #region Generic Type Dependencies
            services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
            #endregion

            #region Scoped Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(IScopedDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Scoped);
            #endregion

            #region Singleton Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Singleton);
            #endregion

            #region Transient Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(ITransientDependency)))
                .AsPublicImplementedInterfaces(); // Default is Transient
            #endregion

            #region Register DIs By Name
            services.RegisterAssemblyPublicNonGenericClasses(dataAssembly)
                .Where(c => c.Name.EndsWith("Repository")
                            && !c.GetInterfaces().Contains(typeof(ITransientDependency))
                            && !c.GetInterfaces().Contains(typeof(IScopedDependency))
                            && !c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Scoped);

            services.RegisterAssemblyPublicNonGenericClasses(serviceAssembly)
                .Where(c => c.Name.EndsWith("Service")
                            && !c.GetInterfaces().Contains(typeof(ITransientDependency))
                            && !c.GetInterfaces().Contains(typeof(IScopedDependency))
                            && !c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces();
            #endregion
        }
    }
 (رجیستر در اینجا با اولویت اینترفیس‌های ITransiantDependency، IScopedDependency، ISingletonDependency و سپس اتمام نام سرویس با کلمه‌های Repository و Service انجام می‌شود که شما می‌توانید با منطق و نیاز خودتان آن‌ها را تغییر دهید)
مطالب
شروع کار با Dart - قسمت 1

Dart کتابخانه ای است که توسط شرکت گوگل ارائه شده است و گفته می‌شود، قرار است جایگزین جاوا اسکریپت گردد و از آدرس https://www.dartlang.org قابل دسترسی می‌باشد. این کتابخانه، دارای انعطاف پذیری فوق العاده بالایی است و کد نویسی Java Script را راحت‌تر می‌کند. در حال حاضر هیچ مرورگری به غیر از Chromium از این تکنولوژی پشتیبانی نمی‌کند و جهت تسهیل در کدنویسی، باید از ویرایشگر Dart Editor استفاده کنید. این ویرایشگر کدهای نوشته شده را به دو صورت Native و JavaScript Compiled در اختیار مرورگر قرار می‌دهد. در ادامه با نحوه‌ی کار و راه اندازی Dart آشنا خواهید شد.

ابتدا Dart و ویرایشگر مربوط به آن را توسط لینک‌های زیر دانلود کنید:

دانلود نسخه 64 بیتی دارت + ویرایشگر

دانلود نسخه 32 بیتی دارت + ویرایشگر

بعد از اینکه فایلهای فوق را از حالت فشرده خارج کردید، پوشه ای با نام dart ایجاد می‌نماید. وارد پوشه dart شده و DartEditor را اجرا کنید.

توجه: جهت اجرای dart به JDK 6.0 یا بالاتر نیاز دارید

در مرحله بعد نمونه کدهای Dart را از لینک زیر دانلود نمایید و از حالت فشرده خارج کنید. پوشه ای با نام one-hour-codelab ایجاد می‌گردد.

دانلود نمونه کدهای دارت

از منوی File > Open Existing Folder… پوشه one-hour-codelab را باز کنید .

توضیحات

- پوشه packages و همچنین فایلهای pubspec.yaml و pubspec.lock شامل پیش نیازها و Package هایی هستند که جهت اجرای برنامه‌های تحت Dart مورد نیاز هستند. Dart Editor این نیازمندی‌ها را به صورت خودکار نصب و تنظیم می‌کند.

توجه: اگر پوشه Packages را مشاهده نکردید و یا در سمت چپ فایلها علامت X قرمز رنگ وجود داشت، بدین معنی است که package ‌ها به درستی نصب نشده اند. برای این منظور بر روی pubspec.yaml کلیک راست نموده و گزینه Get Pub را انتخاب کنید. توجه داشته باید که بدلیل تحریم ایران توسط گوگل باید از ابزارهای عبور از تحریم استفاده کنید.

- 6 پوشه را نیز در تصویر فوق مشاهده می‌کنید که نمونه کد piratebadge را بصورت مرحله به مرحله انجام داده و به پایان می‌رساند.

- Dart SDK شامل سورس کد مربوط به تمامی توابع، متغیرها و کلاس هایی است که توسط کیت توسعه نرم افزاری Dart ارائه شده است.

- Installed Packages شامل سورس کد مربوط به تمامی توابع، متغیرها و کلاس‌های کتابخانه‌های اضافه‌تری است که Application به آنها وابسته است.


گام اول: اجرای یک برنامه کوچک

در این مرحله سورس کدهای آماده را مشاهده می‌کنید و با ساختار کدهای Dart و HTML آشنا می‌شوید و برنامه کوچکی را اجرا می‌نمایید.

در Dart Editor پوشه 1-blankbadge را باز کنید و فایلهای piratebadge.html و piratebadge.dart را مشاهده نمایید.

کد موجود در فایل piratebadge.html

<html>
  <head>
    <meta charset="utf-8">
    <title>Pirate badge</title>
    <meta name="viewport"
          content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="piratebadge.css">
  </head>
  <body>
    <h1>Pirate badge</h1>
    
    <div>
      TO DO: Put the UI widgets here.
    </div>
    <div>
      <div>
        Arrr! Me name is
      </div>
      <div>
        <span id="badgeName"> </span>
      </div>
    </div>

    <script type="application/dart" src="piratebadge.dart"></script>
    <script src="packages/browser/dart.js"></script>
  </body>
</html>

توضیحات

- در کد HTML ، اولین تگ <script> ، فایل piratebadge.dart را جهت پیاده سازی دستورات dart به صفحه ضمیمه می‌نماید

- Dart Virtual Machine (Dart VM) کدهای Dart را بصورت Native یا بومی ماشین اجرا می‌کند. Dart VM کدهای خود را در Dartium که یک ویرایش ویژه از مرورگر Chromium می‌باشد اجرا می‌کند که می‌تواند برنامه‌های تحت Dart را بصورت Native اجرا کند.

- فایل packages/browser/dart.js پشتیبانی مرورگر از کد Native دارت را بررسی می‌کند و در صورت پشتیبانی، Dart VM را راه اندازی می‌کند و در غیر این صورت JavaScript کامپایل شده را بارگزاری می‌نماید.


کد موجود در piratebadge.dart

void main() {
  // Your app starts here.
}

- این فایل شامل تابع main می‌باشد که تنها نقطه ورود به application است. تگ <script> موجود در piratebadge.html برنامه را با فراخوانی این تابع راه اندازی می‌کند.

- تابع main() یک تابع سطح بالا یا top-level می‌باشد.

- متغیرها و توابع top-level عناصری هستند که خارج از ساختار تعریف کلاس ایجاد می‌شوند.

جهت اجرای برنامه در Dart Editor بر روی piratebadge.html کلیک راست نمایید و گزینه Run in Dartium را اجرا کنید. این فایل توسط Dartium اجرا می‌شود و تابع main() را فراخوانی می‌کند و صفحه ای همانند شکل زیر را نمایش می‌دهد.


گام دوم: افزودن فیلد input

توجه داشته باشید که در این مرحله یا می‌توانید تغییرات مورد نظر خود را در طی آموزش بر روی پوشه‌ی 1-blankbadge اعمال کنید و یا به پوشه‌های تهیه شده در نمونه کد موجود در همین پروژه مراجعه نمایید.

در این مرحله یک تگ <input> به تگ <div class=”widgets”> اضافه کنید.

...
<div>
  <div>
    <input type="text" id="inputName" maxlength="15">
  </div>
</div>
...

سپس کتابخانه dart:html را به ابتدای فایل piratebadge.dart اضافه کنید.

import 'dart:html';

توضیحات

- دستور فوق کلاس‌ها و Resource ‌های موجود در کتابخانه dart:html را اضافه می‌کند.

- از حجیم شدن کدهای خود نگران نباشید، زیرا فرایند کامپایل کدهای اضافی را حذف خواهد کرد.

- کتابخانه dart:html شامل کلاسهایی جهت کار با عناصر DOM و توابعی جهت دسترسی به این عناصر می‌باشد.

- در مباحث بعدی یاد می‌گیرید که با استفاده از کلمه کلیدی show فقط کلاسهایی را import کنید که به آن نیاز دارید.

- اگر کتابخانه ای در هیچ بخش کد استفاده نشود، خود Dart Editor به صورت warning اخطار می‌دهد و می‌توانید آن را حذف کنید.


دستور زیر را در تابع main بنویسید تا رویداد مربوط به ورود اطلاعات  در فیلد input را مدیریت نمایید.

void main() {
  querySelector('#inputName').onInput.listen(updateBadge);
}

توضیحات

- تابع querySelector() در کتابخانه dart:html تعریف شده است و یک المنت DOM را جستجو می‌نماید. پارامتر ورودی آن یک selector می‌باشد که در اینجا فیلد input را توسط #inputName جستجو نمودیم که یک ID Selector می‌باشد.

- نوع خروجی این متد یک شی از نوع DOM می‌باشد.

- تابع onInput.Listen() رویدادی را برای پاسخگویی به ورود اطلاعات در فیلد input تعریف می‌کند. زمانی که کاربر اطلاعاتی را وارد نماید، تابع updateBadge فراخوانی می‌گردد.

- رویداد input زمانی رخ می‌دهد که کاربر کلیدی را از صفحه کلید فشار دهد.

- رشته‌ها همانند جاوا اسکریپت می‌توانند در " یا '  قرار بگیرند.


تابع زیر را به صورت top-level یعنی خارج از تابع main تعریف کنید.

...

void updateBadge(Event e) { 
  querySelector('#badgeName').text = e.target.value;
}

توضیحات

- این تابع محتوای المنت badgeName را به محتوای وارد شده در فیلد input تغییر می‌دهد.

- پارامتر ورودی این تابع شی e از نوع Event می‌باشد و به همین دلیل می‌توانیم این تابع را یک Event Handler بنامیم.

- e.target به شی ای اشاره می‌کند که موجب رخداد رویداد شده است و در اینجا همان فیلد input می‌باشد

- با نوشتن کد فوق یک warning را مشاهده می‌کنید که بیان می‌کند ممکن است خصوصیت value برای e.target وجود نداشته باشد. برای حل این مسئله کد را بصورت زیر تغییر دهید.

...

void updateBadge(Event e) { 
  querySelector('#badgeName').text = (e.target as InputElement).value;
}

توضیحات

- کلمه کلیدی as به منظور تبدیل نوع استفاده می‌شود که e.target را به یک InputElement تبدیل می‌کند.

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

اشتراک‌ها
سری آموزشی #C برای تازه‌ کارها


C# for Beginners
Join Scott Hanselman and .NET Distinguished Engineer David Fowler as they teach us C# from the ground up! From Hello World to LINQ and more, Scott and David share C# lessons at a deep and leisurely pace. By the end you'll be ready to explore the Foundational C# Certification from FreeCodeCamp!

 

سری آموزشی #C برای تازه‌ کارها
اشتراک‌ها
بحثی در مورد آینده‌ی #C از Mads Torgersen

What’s Next in C#? - Mads Torgersen - Copenhagen DevFest 2023
NDC Conferences
Join Mads on a tour of upcoming language features in C#. While still very much in the works, C# 12 is starting to take shape. We touch on some of the ways, big and small, that C# is striving to make your life easier in the coming years. 

بحثی در مورد آینده‌ی #C از Mads Torgersen
اشتراک‌ها
چگونه یک برنامه‌ی Vue.js را به Blazor تبدیل کنیم؟

Burke learns Blazor by porting a Vue.js app to Blazor

This summer, Burke and Jon are porting theurlist.com to Blazor - a real world JavaScript application written in Vue.js. Join them each week as they use Visual Studio, Visual Studio Code and GitHub Copilot to rebuild this app and try to tackle every frontend issue you might encounter along the way.
 

چگونه یک برنامه‌ی Vue.js را به Blazor تبدیل کنیم؟
اشتراک‌ها
دوره مقدماتی Progressive Web Apps (PWA)

Progressive Web Apps (PWA) for Beginners
Welcome to PWA for Beginners! Join Beth Pan and her team in this spunky 4-chapter, 17-video tutorial that walks you through building your own Progressive Web Apps that can run cross platforms and combine the best of web and native features! 

دوره مقدماتی Progressive Web Apps (PWA)