مطالب
C# 8.0 - Default implementations in interfaces
اگر مطلب «تفاوت بین Interface و کلاس Abstract در چیست؟» را مطالعه کرده باشید، به این نتیجه می‌رسید که طراحی یک کتابخانه‌ی عمومی با اینترفیس‌ها، بسیار شکننده‌است. اگر عضو جدیدی را به یک اینترفیس عمومی اضافه کنیم، تمام پیاده سازی کننده‌های آن‌را از درجه‌ی اعتبار ساقط می‌کند و آن‌ها نیز باید این عضو را حتما پیاده سازی کنند تا برنامه‌ای که پیش از این به خوبی کار می‌کرده، باز هم بدون مشکل کامپایل شده و کار کند. هدف از ویژگی جدید «پیاده سازی‌های پیش‌فرض در اینترفیس‌ها» در C# 8.0، پایان دادن به این مشکل مهم است. با استفاده از این ویژگی جدید، می‌توان یک عضو جدید را با پیاده سازی پیش‌فرضی داخل خود اینترفیس قرار داد. به این ترتیب تمام برنامه‌هایی که از کتابخانه‌های عمومی شما استفاده می‌کنند، با به روز رسانی آن، به یکباره از کار نخواهند افتاد.
همچنین مزیت دیگر آن، انتقال ساده‌تر کدهای جاوا به سی‌شارپ است؛ از این لحاظ که ویژگی مشابهی در زبان جاوا تحت عنوان «Default Methods» سال‌ها است که وجود دارد.


یک مثال از ویژگی «پیاده سازی‌های پیش‌فرض در اینترفیس‌ها»

interface ILogger
{
    void Log(string message);
}

class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}
فرض کنید کتابخانه‌ی شما، اینترفیس ILogger را ارائه داده‌است و در برنامه‌ای دیگر، استفاده کننده، کلاس ConsoleLogger را بر مبنای آن پیاده سازی و استفاده کرده‌است.
مدتی بعد بر اساس نیازمندی‌های مشخصی به این نتیجه خواهید رسید که بهتر است overload دیگری را برای متد Log در اینترفیس ILogger، درنظر بگیریم. مشکلی که این تغییر به همراه دارد، کامپایل نشدن کلاس ConsoleLogger در یک برنامه‌ی ثالث است و این کلاس باید الزاما این overload جدید را پیاده سازی کند؛ در غیراینصورت قادر به کامپایل برنامه‌ی خود نخواهد شد. اکنون در C# 8.0 می‌توان برای این نوع تغییرات، در همان اینترفیس اصلی، یک پیاده سازی پیش‌فرض را نیز قرار داد:
interface ILogger
{
    void Log(string message);
    void Log(Exception exception) => Console.WriteLine(exception);
}
به این ترتیب استفاده کنندگان از این اینترفیس، برای کامپایل برنامه‌ی خود به مشکلی برنخواهند خورد و اگر از این overload جدید استفاده کنند، از همان پیاده سازی پیش‌فرض آن بهره خواهند برد. بدیهی است هنوز هم پیاده سازی کننده‌های اینترفیس ILogger می‌توانند پیاده سازی‌های سفارشی خودشان را در مورد این overload جدید ارائه دهند. در این حالت از پیاده سازی پیش‌فرض صرفنظر خواهد شد.


ویژگی «پیاده سازی‌های پیش‌فرض در اینترفیس‌ها» چگونه پیاده سازی شده‌است؟

واقعیت این است که امکان پیاده سازی این ویژگی، سال‌ها است که در سطح کدهای IL دات نت وجود داشته (از زمان دات نت 2) و اکنون از طریق کدهای برنامه با بهبود کامپایلر آن، قابل دسترسی شده‌است.


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

مثال زیر را درنظر بگیرید:
    interface IDeveloper
    {
        void LearnNewLanguage(string language, DateTime dueDate);

        void LearnNewLanguage(string language)
        {
            // default implementation
            LearnNewLanguage(language, DateTime.Now.AddMonths(6));
        }
    }

    class BackendDev : IDeveloper // compiles OK
    {
        public void LearnNewLanguage(string language, DateTime dueDate)
        {
            // Learning new language...
        }
    }
در اینجا اینترفیس IDeveloper، به همراه یک پیاده سازی پیش‌فرض است و بر این اساس، کلاس BackendDev پیاده سازی کننده‌ی آن، دیگر نیازی به پیاده سازی اجباری متد LearnNewLanguage ای که تنها یک رشته را می‌پذیرد، ندارد.
سؤال: به نظر شما اکنون کدامیک از کاربردهای زیر از کلاس BackendDev، کامپایل می‌شود و کدامیک خیر؟
IDeveloper dev1 = new BackendDev();
dev1.LearnNewLanguage("Rust");

var dev2 = new BackendDev();
dev2.LearnNewLanguage("Rust");
پاسخ: فقط مورد اول. مورد دوم با خطای کامپایلر زیر مواجه خواهد شد:
 There is no argument given that corresponds to the required formal parameter 'dueDate' of 'BackendDev.LearnNewLanguage(string, DateTime)' (CS7036) [ConsoleApp]
به این معنا که اگر کلاس BackendDev را به خودی خود (دقیقا از نوع BackendDev) و بدون معرفی آن از نوع اینترفیس IDeveloper، بکار بگیریم، فقط همان متدهایی که داخل این کلاس تعریف شده‌اند، قابل دسترسی می‌باشند و نه متدهای پیش‌فرض تعریف شده‌ی در اینترفیس مشتق شده‌ی از آن.


ارث‌بری چندگانه چطور؟

احتمالا حدس زده‌اید که این قابلیت ممکن است ارث‌بری چندگانه را که در سی‌شارپ ممنوع است، میسر کند. تا C# 8.0، یک کلاس تنها از یک کلاس دیگر می‌تواند مشتق شود؛ اما این محدودیت در مورد اینترفیس‌ها وجود ندارد. به علاوه تاکنون اینترفیس‌ها مانند کلاس‌ها، امکان تعریف پیاده سازی خاصی را نداشتند و صرفا یک قرارداد بیشتر نبودند. بنابراین اکنون این سؤال مطرح می‌شود که آیا می‌توان با ارائه‌ی پیاده سازی پیش‌فرض متدها در اینترفیس‌ها، ارث‌بری چندگانه را در سی‌شارپ پیاده سازی کرد؛ مانند مثال زیر؟!
using System;

namespace ConsoleApp
{
    public interface IDev
    {
        void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a default way.");
    }

    public interface IBackendDev : IDev
    {
        void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a backend way.");
    }

    public interface IFrontendDev : IDev
    {
        void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a frontend way.");
    }

    public interface IFullStackDev : IBackendDev, IFrontendDev { }

    public class Dev : IFullStackDev { }
}
سؤال: کد فوق بدون مشکل کامپایل می‌شود. اما در فراخوانی زیر، دقیقا از کدام متد LearnNewLanguage استفاده خواهد شد؟ آیا پیاده سازی آن از IBackendDev فراهم می‌شود و یا از IFrontendDev؟
IFullStackDev dev = new Dev();
dev.LearnNewLanguage("TypeScript");
پاسخ: هیچکدام! برنامه با خطای زیر کامپایل نخواهد شد:
The call is ambiguous between the following methods or properties: 'IBackendDev.LearnNewLanguage(string)' and 'IFrontendDev.LearnNewLanguage(string)' (CS0121)
کامپایلر سی‌شارپ در این مورد خاص از قانونی به نام «the most specific override rule» استفاده می‌کند. یعنی اگر برای مثال در IFullStackDev متد LearnNewLanguage به صورت صریحی بازنویسی و تامین شد، آنگاه امکان استفاده‌ی از آن وجود خواهد داشت. یا حتی می‌توان این پیاده سازی را در کلاس Dev نیز ارائه داد و از نوع آن (بجای نوع اینترفیس) استفاده کرد.


تفاوت امکانات کلاس‌های Abstract با متدهای پیش‌فرض اینترفیس‌ها چیست؟

اینترفیس‌ها هنوز نمی‌توانند مانند کلاس‌ها، سازنده‌ای را تعریف کنند. نمی‌توانند متغیرها/فیلدهایی را در سطح اینترفیس داشته باشند. همچنین در اینترفیس‌ها همه‌چیز public است و امکان تعریف سطح دسترسی دیگری وجود ندارد.
بنابراین باید بخاطر داشت که هدف از تعریف اینترفیس‌ها، ارائه‌ی «یک رفتار» است و هدف از تعریف کلاس‌ها، ارائه «یک حالت».


یک نکته: در نگارش‌های پیش از C# 8.0 هم می‌توان ویژگی «متدهای پیش‌فرض» را شبیه سازی کرد

واقعیت این است که توسط ویژگی «متدهای الحاقی»، سال‌ها است که امکان افزودن «متدهای پیش‌فرضی» به اینترفیس‌ها در زبان سی‌شارپ وجود دارد:
namespace MyNamespace
{
    public interface IMyInterface
    {
        IList<int> Values { get; set; }
    }

    public static class MyInterfaceExtensions
    {
        public static int CountGreaterThan(this IMyInterface myInterface, int threshold)
        {
            return myInterface.Values?.Where(p => p > threshold).Count() ?? 0;
        }
    }
}
و در این حالت هرچند به نظر اینترفیس IMyInterface دارای متدی نیست، اما فراخوانی زیر مجاز است:
var myImplementation = new MyInterfaceImplementation();
// Note that there's no typecast to IMyInterface required
var countGreaterThanFive = myImplementation.CountGreaterThan(5);
مطالب
مدیریت اعمال آغازین در برنامه‌های Angular
در برنامه‌های کاربردی بر پایه Angular گاها نیاز است اعمالی را قبل از بارگذاری آغازین نرم افزار انجام دهید. این موارد می‌توانند خواندن اطلاعات پیکربندی از یک فایل json. باشند و یا گرفتن داده‌هایی از سرور بک‌اند و استفاده از آنها برای ایجاد محدودیت در بعضی از قسمتها.
از +Angular 4 با بکار گیری توکن APP_INITIALIZER در ماژول آغازین برنامه (app.module)، امکان معرفی و ثبت تابعی را جهت اجرا، در ابتدای چرخه حیات برنامه، خواهیم داشت. برای روشن شدن مطلب، مثالی از خواندن اطلاعات پیکربندی واقع در یک فایل جیسون را در ذیل پیاده سازی می‌کنیم.
در بسیاری موارد نیاز داریم که پس از بیلد پروژه، امکان تغییر آدرس نهایی هاست سمت سرور را داشته باشیم و بر اساس آن بتوانیم Web Api Url‌ها را در سرویس‌های Angular به روز کنیم. برای این کار فایلی را به نام config.json با محتویات ذیل ایجاد می‌کنیم (آدرس هاست را مطابق سرور خود، تغییر دهید): 
{
  "host": "http://localhost:8008"
}

سپس  سرویسی که وظیفه‌ی خواندن اطلاعات را بر عهده دارد، ترجیحا در بخش Core Modules پروژه با کد ذیل، ایجاد می‌کنیم:
import { Observable } from 'rxjs/Observable';
import { Inject, Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class AppConfigService {
    constructor(private http: Http) {   }
  private config: Object = null;
    get apiRoot() {
      return this.getProperty('host'); // <--- THIS GETS CALLED FIRST
    }
    load(): Promise<any> {
      console.log('get user called');
      const promise = this.http.get('assets/config.json').map((res) => res.json()).toPromise();
      promise.then(config => {
        this.config = config;     // <--- THIS RESOLVES AFTER
        console.log(this.config);
      });
    return promise;
  }
    private getProperty(property: string): any {
      //noinspection TsLint
      if (!this.config) {
        throw new Error('Attempted to access configuration property before configuration data was loaded, please implemented.');
      }

      if (!this.config[property]) {
        throw new Error(`Required property ${property} was not defined within the configuration object. Please double check the
        assets/config.json file`);
      }

      return this.config[property];
    }

}

حال در app.module  توسط APP_INITIALIZER، متد Load سرویس را برای مقدار دهی خاصیت apiRoot سرویس صدا می‌زنیم. بنابراین ابتدا تابع init را تکمیل می‌کنیم: 
export function init(config: AppConfigService) {
  return () => {
    return  config.load(); 
  };
}

سپس در قسمت Providers ماژول آغازین (app.module)، مانند کد زیر اقدام می‌کنیم:
  Providers:[
…,
AppConfigService,
{
      provide: APP_INITIALIZER,
      useFactory: init,
      multi: true,
      deps: [AppConfigService]
   }
…,

]

حال می‌توانیم سرویس AppConfigService را در متد سازنده‌ی سرویس‌های خود تزریق کرده و از خاصیت apiRoot  جهت به روز رسانی آدرس‌های خود استفاده کنیم:
@Injectable()
export class DashboardService {
  private tagUrl = '';
  constructor(private http: Http,private AppConfig:AppConfigService) {
      this.tagtUrl = this.AppConfig.apiRoot+"/myApiUrl";
….
}

نکته تکمیلی: می‌توان چندین تابع را به همین روش در نقطه‌ی آغازین برنامه، جهت اجرا معرفی کرد. کافی است useFactory را به نام تابع مورد نظر خود و خاصیت deps را که اشاره به نوع پارامتر ورودی تابع دارد (در اینجا سرویس AppConfigService) مقدار دهی کنید: 
    {
      provide: APP_INITIALIZER,
      useFactory: init,
      multi: true,
      deps: [AppConfigService]
    },
  {
    provide: APP_INITIALIZER,
    useFactory: initIdentity,
    multi: true,
    deps: [IdentityService]
  },
  AppConfigService,
  IdentityService,

مطالب
آزمایش ساده‌تر Web APIs توسط strest
در سری کار با Postman، یک روش بسیار متداول آزمایش Web APIs را بررسی کردیم. اما ... برای کار آن با مدام نیاز است از این برگه به آن برگه مراجعه کرد و ارتباط دادن درخواست‌های متوالی در آن مشکل است. به همین منظور تابحال راه‌حل‌های زیادی برای جایگزین کردن postman ارائه شده‌اند که یکی از آن‌ها strest است. این ابزار خط فرمان:
- بسیار سبک ورزن است و تنها نیاز به نصب بسته‌ی npm آن‌را دارد.
- با فایل‌های متنی معمولی کار می‌کند که ویرایش و copy/paste در آن‌ها بسیار ساده‌است.
- قرار دادن فایل‌های نهایی متنی آن در ورژن کنترل بسیار ساده‌است.
- امکان نوشتن درخواست‌های به هم وابسته و آزمودن نتایج حاصل را دارا است.
- چون یک ابزار خط فرمان است، امکان استفاده‌ی از آن به سادگی در فرآینده‌های توسعه‌ی مداوم وجود دارد.
- ابزارهای npm، چندسکویی هستند.


نصب strest

در ادامه قصد داریم مطلب «آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT» را با استفاده از strest بازنویسی کنیم. به همین جهت در ابتدا نیاز است بسته‌ی npm آن‌را به صورت سراسری نصب کنیم:
npm i -g @strest/cli
پس از آن فایل جدید JWT.strest.yml را در پوشه‌ای ایجاد کرده و آن‌را تکمیل می‌کنیم. برای اجرای فرامین موجود در آن تنها کافی است دستور strest JWT.strest.yml را درخط فرمان صادر کنیم.


مرحله 1: خاموش کردن بررسی مجوز SSL برنامه
مرحله 2: ایجاد درخواست login و دریافت توکن‌ها

مجوز SSL آزمایشی برنامه‌ی ASP.NET Core ما، از نوع خود امضاء شده‌است. به همین جهت اگر سعی در اجرای strest را با درخواست‌های ارسالی به آن داشته باشیم، باشکست مواجه خواهند شد. بنابراین در ابتدا، خاصیت allowInsecure را به true تنظیم می‌کنیم:
version: 2

variables:
  baseUrl: https://localhost:5001/api
  logResponse: false

allowInsecure: true
- این تنظیمات با فرمت yaml نوشته می‌شوند. به همین جهت در اینجا تعداد spaceها مهم است.
- همچنین در ابتدای این تنظیمات، روش تعریف متغیرها را نیز مشاهده می‌کنید که برای مثال توسط آن‌ها baseUrl تعریف شده‌است.
درست در سطر پس از این تنظیمات، دستور اجرا و اعتبارسنجی درخواست Login را می‌نویسیم:
requests:
  loginRequest:
    request:
      url: <$ baseUrl $>/account/login
      method: POST
      postData:
        mimeType: application/json
        text:
          username: "Vahid"
          password: "1234"
    log: <$ logResponse $>
    validate:
      - jsonpath: content.access_token
        type: [string]
      - jsonpath: content.refresh_token
        type: [string]
توضیحات:
- درخواست‌ها با requests شروع می‌شوند. سپس ذیل آن می‌توان نام چندین درخواست یا request را ذکر کرد که برای مثال نام درخواست تعریف شده‌ی در اینجا loginRequest است. این نام مهم است؛ از این جهت که با اشاره‌ی به آن می‌توان به فیلدهای خروجی response حاصل، در درخواست‌های بعدی، دسترسی یافت.
- سپس، آدرس درخواست مشخص شده‌است. در اینجا روش کار با متغیرها را نیز مشاهده می‌کنید.
- نوع درخواست POST است.
- در ادامه جزئیات اطلاعات ارسالی به سمت سرور باید مشخص شوند. برای مثال در اینجا با فرمت application/json قرار است یک شیء تشکیل شده‌ی از username و password ارسال شوند.
- در سطر بعدی، خاصیت log با متغیر logResponse مقدار دهی شده‌است. اگر به true تنظیم شود، اصل خروجی response را توسط برنامه‌ی خط فرمان strest می‌توان مشاهده کرد. اگر اینکار خروجی را شلوغ کرد، می‌توان آن‌را به false تنظیم کرد و این خروجی را در فایل strest_history.json نهایی که حاصل از اجرای آزمایش‌های تعریف شده‌است، در کنار فایل JWT.strest.yml خود یافت و مشاهده کرد.
- سپس به قسمت آزمودن نتیجه‌ی درخواست می‌رسیم. در اینجا انتظار داریم که درخواست حاصل که با فرمت json است، دارای دو خاصیت رشته‌ای access_token و refresh_token باشد.


 مرحله‌ی 3: ذخیره سازی توکن‌های دریافتی در متغیرهای سراسری
 مرحله‌ی 3: ذخیره سازی مراحل انجام شده
در حین کار با strest نیازی به ذخیره سازی نتیجه‌ی حاصل از response، در متغیرهای خاصی نیست. برای مثال اگر بخواهیم به نتیجه‌ی حاصل از عملیات لاگین فوق در درخواست‌های بعدی دسترسی پیدا کنیم، می‌توان نوشت <$ loginRequest.content.access_token $>
در اینجا درج متغیرها توسط <$ $> صورت می‌گیرد. سپس loginRequest به نام درخواست مرتبط اشاره می‌کند. خاصیت content.access_token نیز مقدار خاصیت access_token شیء response را بر می‌گرداند.

همچنین ذخیره سازی مراحل انجام شده نیز نکته‌ی خاصی را به همراه ندارد. یک تک فایل متنی JWT.strest.yml وجود دارد که آزمایش‌های ما در آن درج می‌شوند.


مرحله‌ی 4: دسترسی به منابع محافظت شده‌ی سمت سرور

در ادامه روش تعریف دو درخواست جدید دیگر را در فایل JWT.strest.yml مشاهده می‌کنید که از نوع Get هستند و به اکشن متدهای محافظت شده ارسال می‌شوند:
  myProtectedApiRequest:
    request:
      url: <$ baseUrl $>/MyProtectedApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ loginRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content.title
        expect: "Hello from My Protected Controller! [Authorize]"

  mProtectedAdminApiRequest:
    request:
      url: <$ baseUrl $>/MyProtectedAdminApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ loginRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content.title
        expect: "Hello from My Protected Admin Api Controller! [Authorize(Policy = CustomRoles.Admin)]"
دو نکته‌ی جدید در اینجا قابل مشاهده‌است:
- چون نیاز است به همراه درخواست خود، هدر اعتبارسنجی مبتنی بر JWT را که به صورت Bearer value است نیز به سمت سرور ارسال کنیم، خاصیت headers را توسط یک name/value مشخص کرده‌ایم. همانطور که عنوان شد در فایل‌های yaml، فاصله‌ها و تو رفتگی‌ها مهم هستند و حتما باید رعایت شوند.
- سپس دومین آزمون نوشته شده را نیز مشاهده می‌کنید. در قسمت validate، مشخص کرده‌ایم که خاصیت title دریافتی از response باید مساوی مقدار خاصی باشد.

دقیقا همین نکات برای درخواست دوم به MyProtectedAdminApi تکرار شده‌اند.


مرحله‌ی 5: ارسال Refresh token و دریافت یک سری توکن جدید

اکشن متد account/RefreshToken در سمت سرور، نیاز دارد تا یک شیء جی‌سون با خاصیت refreshToken را دریافت کند. مقدار این خاصیت از طریق response متناظر با درخواست نام‌دار loginRequest استخراج می‌شود که در قسمت postData مشخص شده‌است:
  refreshTokenRequest:
    request:
      url: <$ baseUrl $>/account/RefreshToken
      method: POST
      postData:
        mimeType: application/json
        text:
          refreshToken: <$ loginRequest.content.refresh_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content.access_token
        type: [string]
      - jsonpath: content.refresh_token
        type: [string]
در آخر، به قسمت آزمودن نتیجه‌ی درخواست می‌رسیم. در اینجا انتظار داریم که درخواست حاصل که با فرمت json است، دارای دو خاصیت رشته‌ای access_token و refresh_token باشد که بیانگر صدور توکن‌های جدیدی هستند.


مرحله‌ی 6: آزمایش توکن جدید دریافتی از سرور

در قسمت قبل، توکن‌های جدیدی صادر شدند که اکنون برای کار با آن‌ها می‌توان از متغیر refreshTokenRequest.content.access_toke استفاده کرد:
  myProtectedApiRequestWithNewToken:
    request:
      url: <$ baseUrl $>/MyProtectedApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ refreshTokenRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content.title
        expect: "Hello from My Protected Controller! [Authorize]"
در اینجا با استفاده از توکن جدید درخواست نام‌دار refreshTokenRequest، آزمون واحد نوشته شده با موفقیت به پایان می‌رسد (یا باید برسد که اجرای نهایی آزمایش‌ها، آن‌را مشخص می‌کند).


مرحله‌ی 7: آزمایش منقضی شدن توکنی که در ابتدای کار پس از لاگین دریافت کردیم

اکنون که refresh token صورت گرفته‌است، دیگر نباید بتوانیم از توکن دریافتی پس از لاگین استفاده کنیم و برنامه باید آن‌را برگشت بزند:
  myProtectedApiRequestWithOldToken:
    request:
      url: <$ baseUrl $>/MyProtectedApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ loginRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: status
        expect: 401
به همین جهت، درخواستی ارسال شده که به نتیجه‌ی درخواست نام‌دار loginRequest اشاره می‌کند. در این حالت برای آزمایش عملیات، اینبار status بازگشتی از سرور که باید 401 باشد، بررسی شده‌است.


مرحله‌ی 8: آزمایش خروج از سیستم

در اینجا نیاز است به آدرس account/logout، یک کوئری استرینگ را با کلید refreshToken و مقدار ریفرش‌توکن دریافتی از درخواست نام‌دار refreshTokenRequest، به سمت سرور ارسال کنیم:
  logoutRequest:
    request:
      url: <$ baseUrl $>/account/logout
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ refreshTokenRequest.content.access_token $>
      queryString:
        - name: refreshToken
          value: <$ refreshTokenRequest.content.refresh_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content
        expect: true
خروجی آزمایش شده‌ی در اینجا، دریافت مقدار true از سمت سرور است.


مرحله‌ی 9: بررسی عدم امکان دسترسی به منابع محافظت شده‌ی سمت سرور، پس از logout

در مرحله‌ی قبل، از سیستم خارج شدیم. اکنون می‌خواهیم بررسی کنیم که آیا توکن دریافتی پیشین هنوز معتبر است یا خیر؟ آیا می‌توان هنوز هم به منابع محافظت شده دسترسی یافت یا خیر:
  myProtectedApiRequestWithNewTokenAfterLogout:
    request:
      url: <$ baseUrl $>/MyProtectedApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ refreshTokenRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: status
        expect: 401
به همین جهت هدر Authorization را با اکسس‌توکنی که در مرحله‌ی ریفرش‌توکن دریافت کردیم (پیش از logout)، مقدار دهی می‌کنیم و سپس درخواستی را به یک منبع محافظت شده ارسال می‌کنیم. نتیجه‌ی حاصل باید status code ای مساوی 401 داشته باشد که به معنای برگشت خوردن آن است


مرحله‌ی 10: اجرای تمام آزمون‌های واحد نوشته شده

همانطور که در ابتدای بحث نیز عنوان شد فقط کافی است دستور strest JWT.strest.yml را در خط فرمان اجرا کنیم تا آزمون‌های ما به ترتیب اجرا شوند:


فایل نهایی این آزمایش را در اینجا می‌توانید مشاهده می‌کنید.
اشتراک‌ها
موجودیت های مرتبط با مدیریت دسترسی های پویا

راه حل‌های مختلفی برای مدیریت دسترسی‌های پویا وجود دارد که باتوجه به Domain ای که در آن قرار داریم میتوانند مورد استفاده قرار بگیرند و در این رابطه مطالب زیادی هم ارائه شده است؛ در این راستا یکی از راه حل هایی که در یکی سیستم‌ها استفاده کرده ایم را در این gist می‌توانید مشاهده کنید.

در راه حل ارائه شده، علاوه بر این که امکان اعطای یکسری دسترسی برای گروه‌های کاربری وجود دارد، می‌توان دسترسی هایی را به صورت مستقیم برای کاربر خاصی نیز اعطاء کرد یا یکسری از دسترسی‌های کاربر را که از طریق گروه‌های کاربری خود دارا می‌باشد را از او گرفت و ...

public static class PermissionNames
{
    public const string Pages = nameof(Pages);
    public const string Pages_Administration = nameof(Pages_Administration);
    public const string Pages_Administration_Roles = nameof(Pages_Administration_Roles);
    public const string Pages_Administration_Roles_Create = nameof(Pages_Administration_Roles_Create);
    public const string Pages_Administration_Roles_Edit = nameof(Pages_Administration_Roles_Create);
    public const string Pages_Administration_Roles_Delete = nameof(Pages_Administration_Roles_Create);
    public const string Pages_Administration_Users = nameof(Pages_Administration_Users);
    public const string Pages_Administration_Users_Create = nameof(Pages_Administration_Users_Create);
    public const string Pages_Administration_Users_Edit = nameof(Pages_Administration_Users_Create);
    public const string Pages_Administration_Users_Delete = nameof(Pages_Administration_Users_Create);
    //...
}
موجودیت های مرتبط با مدیریت دسترسی های پویا
نظرات مطالب
اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
خیر. شیء this.User با اطلاعات جدول کاربران، تناظر یک به یک ندارد. از نگارش‌های پیشین ASP.NET ، هنوز هم اطلاعات شیء User مانند User.Identity.Name در ASP.NET Core نیز در دسترس هستند. به این ترتیب زمانیکه کاربری به سیستم وارد شد، دیگر نیازی نیست تا جهت یافتن Name او، از بانک اطلاعاتی کوئری گرفت. خاصیت Name یاد شده به صورت خودکار از کوکی رمزنگاری شده‌ی و یا در اینجا از توکن او دریافت شده و در اختیار برنامه قرار می‌گیرد. این Name در ASP.NET Core Identity، اصطلاحا یک User Claim پیش‌فرض نام دارد و به صورت خودکار ایجاد و مقدار دهی می‌شود. روش مقدار دهی اولیه‌ی آن هم در متد createAccessTokenAsync مشخص است. هر زمانیکه این توکن به سمت سرور ارسال می‌شود، پس از اعتبارسنجی توکن و پذیرش آن، این Claims هم پردازش شده و جزئی از اطلاعات شیء this.User می‌شوند.
مطالب
آشنایی با FileTable در SQL Server 2012 بخش 1
پیش از آشنایی با FileTable نیاز است که پیشینه‌ای از شیوه‌های ذخیره‌سازی فایل و یا بهتر بگویم BLOB در SQL Server را داشته باشیم. نخستین شیوه‌ی نگه‌داری فایل استفاده از Image است که در SQL Server 2000 کاربرد داشت و هم‌اکنون استفاده از آن به دلیل کاهش بسیار کارآیی منسوخ‌شده است. به دلایل مشکلات بسیار فراوان Image هم‌زمان بسیاری از طراحان پایگاه داده‌ها، جهت کاهش حجم جدول‌ها و پیروی آن حجم پایگاه داده‌ها، فایل را در سیستم‌فایل نگه‌داری می‌کردند و تنها مسیر آن را در فیلدی از نوع کاراکتری در پایگاه‌داده‌ها ذخیره می‌کردند. این روش هرچند از حجم پایگاه داده‌ها می‌کاست ولی به دلیل عدم دخالت SQL Server در مدیریت فایل‌ها مشکلات دیگری را به وجود آورد.
از SQL Server 2005 نوع داده‌ی varbinary(max) معرفی شد که برخی از چالش‌های به‌کاربری Image را کاست و درباره‌ی بسیاری از موارد مانند ذخیره‌ی عکس پرسنلی هنوز هم کاربرد دارد؛ ولی توجه داشته باشید که استفاده از این فیلد فقط برای فایل‌های کم‌تر از 256 کیلوبایت سفارش شده است و برای بالاتر از آن، کارآیی کاهش فراوانی خواهد یافت.
در  SQL Server 2008 نوع داده‌ی جدیدی به نام FileStream به وجود آمد به این شکل که یک FileGroup از نوع  Data FileStream به پایگاه‌داده افزوده می‌شود و در واقع با یک پوشه در سیستم فایل در پیوند است. از این پس هنگام ساخت یک جدول به جای استفاده از نوع داده‌ی varbinary از نوع FileStream استفاده می‌کنیم با مد نظر داشتن این نکته که حتماً باید یک فیلد از نوع Uniqueidentifier هم در آن جدول تعریف شده باشد. شیوه‌ی کار نیز به این صورت خواهد بود که خود رکورد در جدول ذخیره می‌شود و فقط محتوای فایل در آن مسیری از NTFS ذخیره می‌شود. برخلاف روش درج مسیر فایل در جدول که پس از حذف رکورد، فایل هم‌چنان در سیستم فایل می‌ماند؛ این بار با حذف رکورد فایل مربوطه نیز حذف خواهد شد. افزون بر این مدیریت پشتیبانی از فایل‌ها نیز برعهده‌ی پایگاه داده‌ها خواهد بود. اندازه‌ی فایل‌ها در FileStream محدودیت‌های پیشین را نخواهد داشت و شما به اندازه‌ی حجم درایو هارددیسک می‌توانید فایل در آن ذخیره کنید. نکته‌ی دیگر درباره‌ی فایل‌های با حجم سنگین که می‌توانید Stream مربوط به یک فایل را به صورت بخش‌بخش در سمت مشتری بارگذاری کنید و به او نشان دهید. در FileStream امنیت و تراکنش فایل‌ها برعهده‌ی SQL Server است و از این دیدگاه بسیار ساده‌تر و کارآتر از FileSystem است. (برای آشنایی بیشتر با FileStream، این نوشتار از مهندس وحید نصیری را مطالعه کنید.)
گونه‌ی FileTable از ویژگی‌های نوین SQL Server 2012 است که تکمیل‌کننده‌ی FileStream است. FileTable آمیزشی از FileStream با hierarchyid و سیستم فایل ویندوز برای ارائه‌ی توانایی‌های نوین مدیریت BLOB در  SQL Server است. FileTable همان‌گونه که از دو واژه‌ی تشکیل‌دهنده‌اش پیداست؛ هم‌زمان یک جدول و یک سیستم فایل معمولی است.
FileTable به هر روی یک جدول از پایگاه‌داده‌های SQL Server است با یک تفاوت که ساختار آن از پیش تعریف‌شده است. ستون‌های FileTable و نوع داده‌ی آن از پیش توسط SQL Server  مشخص شده است. ستون‌های تشکیل‌دهنده‌ی FileTable دربرگیرنده‌ی جدول زیر است:

هر ردیف از FileTable نماینده‌ی یک فایل یا پوشه در File System است. ستون path_locator که از نوع hierarchyid است نشان‌دهنده‌ی مسیر یک فایل یا پوشه است. hierarchyid که از SQL Server 2008 معرفی شده است؛ بهترین نوع داده برای نگه‌داری ارتباط ساختار سلسله‌مراتبی مانند چارت سازمانی، درخت تجهیزات یک کارخانه و یا در همین نمونه درخت فایل‌ها و پوشه‌ها است. پس می‌توانیم از همه‌ی امکانات hierarchyid در FileTable نیز برخوردار شویم. این‌که این فایل به ترتیب در چه پوشه‌هایی قرار گرفته است یا این‌که این پوشه شامل چه فایل‌ها یا پوشه‌هایی خواهد بود. این‌که پوشه‌های هم‌فرزند پوشه‌ی جاری کدام است و یا یا توابع مربوط به جابه‌جایی فایل‌ها و پوشه‌ها.

دنباله دارد...

مطالب
کوئری نویسی در EF Core - قسمت هفتم - کار با رشته‌‌ها
هدف از این سری مثال‌ها، آشنایی با متدها و توابعی است که در حین کار با خواص رشته‌ای در LINQ to Entities، مجاز به استفاده‌ی از آن‌ها هستیم و همچنین اگر تابعی در EF-Core هنوز تعریف نشده بود، راه حل چیست.


مثال 1: نام تمام کاربران را با قالب 'Surname, Firstname'  نمایش دهید.

var members = context.Members
                                    .Select(member => new { Name = member.Surname + ", " + member.FirstName })
                                    .ToList();
متد Select می‌تواند به همراه اعمال محاسباتی ساده‌ای نیز باشد که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.
با این خروجی:



مثال 2: تمام امکاناتی را که با Tennis شروع می‌شوند، لیست کنید.
این گزارش به همراه تمام ستون‌های جدول است.

var facilities = context.Facilities
                                        .Where(facility => facility.Name.StartsWith("Tennis"))
                                        .ToList();
متدهای استانداردی مانند StartsWith، EndsWith و Contains را می‌توان بر روی خواص رشته‌ای بکار برد.
با این خروجی:



مثال 3: تمام امکاناتی را که با tennis شروع می‌شوند، لیست کنید. این جستجو باید غیرحساس به بزرگی و کوچکی حروف باشد.
این گزارش به همراه تمام ستون‌های جدول است.

نیازی به انجام مجزای این تمرین نیست؛ چون پاسخ آن همان پاسخ مثال 2 است. Collation پیش‌فرض در SQL Server، غیرحساس به بزرگی و کوچکی حروف است. بنابراین چه tennis را جستجو کنیم و یا TeNnis را، تفاوتی نمی‌کند.


مثال 4: شماره تلفن‌های دارای پرانتز را لیست کنید.
این گزارش باید به همراه ستون‌های memid, telephone باشد.

روش اول: در اینجا دوبار از متد Contains استفاده شده‌است:
var members = context.Members
                                    .Select(member => new { member.MemId, member.Telephone })
                                    .Where(member => member.Telephone.Contains("(")
                                                    && member.Telephone.Contains(")"))
                                    .ToList();
با این خروجی:


روش دوم: اگر می‌خواهیم کنترل بیشتری را بر روی خروجی نهایی LIKE تولیدی داشته باشیم، می‌توان از متد سفارشی استاندارد EF.Functions.Like استفاده کرد که از حروف wild cards نیز پشتیبانی می‌کند:
members = context.Members
                                    .Select(member => new { member.MemId, member.Telephone })
                                    .Where(member => EF.Functions.Like(member.Telephone, "%[()]%"))
                                    .ToList();
با این خروجی:



مثال 5: کد پستی‌ها 5 رقمی هستند. گزارشی را تهیه کنید که در آن اگر کدپستی کمتر از 5 رقم بود، ابتدای آن با صفر شروع شود.
هدف اصلی از این مثال، اعمال متد PadLeft(5, '0') به خاصیت member.ZipCode است.

روش اول: EF-Core فعلا قابلیت ترجمه‌ی PadLeft(5, '0') را به معادل SQL آن‌را ندارد. به همین جهت مجبور هستیم ابتدا ZipCode‌ها را به صورت رشته‌ای بازگشت دهیم که در اینجا استفاده‌ی از Convert.ToString مجاز است.
با این خروجی:
SELECT   CONVERT (NVARCHAR (MAX), [m].[ZipCode]) AS [Zip]
FROM     [Members] AS [m]
ORDER BY CONVERT (NVARCHAR (MAX), [m].[ZipCode]);
 سپس می‌توان بر روی لیست آماده‌ی موجود در حافظه، از LINQ to Objects استفاده کرد و در این حالت دسترسی کاملی به تمام امکانات زبان #C وجود دارد:
var members = context.Members
                                    .Select(member => new { ZipCode = Convert.ToString(member.ZipCode) })
                                    .OrderBy(m => m.ZipCode)
                                    .ToList();
// Now using LINQ to Objects
members = members.Select(member => new { ZipCode = member.ZipCode.PadLeft(5, '0') })
                                                    .OrderBy(m => m.ZipCode)
                                                    .ToList();

روش دوم: SQL Server به همراه تابع استانداردی به نام Replicate است که از آن می‌توان برای شبیه سازی PadLeft، بدون متوسل شدن به LINQ to Objects، استفاده کرد. اما چون این تابع هنوز به EF-Core معرفی نشده‌است، نیاز است خودمان اینکار را انجام دهیم. در این روش، از متد SqlDbFunctionsExtensions.SqlReplicate استفاده می‌شود. روش تعریف این نوع متدها را در مطلب «امکان تعریف توابع خاص بانک‌های اطلاعاتی در EF Core» پیشتر بررسی کرده‌ایم که برای مثال در اینجا چنین شکلی را پیدا می‌کند:
namespace EFCorePgExercises.Utils
{
    public static class SqlDbFunctionsExtensions
    {
        public static string SqlReplicate(string expression, int count)
            => throw new InvalidOperationException($"{nameof(SqlReplicate)} method cannot be called from the client side.");

        private static readonly MethodInfo _sqlReplicateMethodInfo = typeof(SqlDbFunctionsExtensions)
            .GetRuntimeMethod(
                nameof(SqlDbFunctionsExtensions.SqlReplicate),
                new[] { typeof(string), typeof(int) }
            );


        public static void AddCustomSqlFunctions(this ModelBuilder modelBuilder)
        {
            modelBuilder.HasDbFunction(_sqlReplicateMethodInfo)
                .HasTranslation(args =>
                {
                    return SqlFunctionExpression.Create("REPLICATE",
                        args,
                        _sqlReplicateMethodInfo.ReturnType,
                        typeMapping: null);
                });
        }
    }
}
پس از آن فقط کافی است متد AddCustomSqlFunctions را به Context برنامه معرفی کنیم:
namespace EFCorePgExercises.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
         // ...

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
         // ...
            modelBuilder.AddCustomSqlFunctions();
         // ...
        }
    }
}
اکنون می‌توان از تابع SqlDbFunctionsExtensions.SqlReplicate جهت شبیه سازی PadLeft به صورت زیر استفاده کرد:
var newMembers = context.Members
                                        .Select(member => new
                                        {
                                            ZipCode =
                                                SqlDbFunctionsExtensions.SqlReplicate(
                                                    "0", 5 - Convert.ToString(member.ZipCode).Length)
                                                + member.ZipCode
                                        })
                        .OrderBy(m => m.ZipCode)
                        .ToList();
با این خروجی:



مثال 6: اولین حرف نام خانوادگی کاربران در کل ردیف‌های جدول چندبار تکرار شده‌است؟
این گزارش باید به همراه ستون‌های letter,  count باشد.

var members = context.Members
                                    .Select(member => new { Letter = member.Surname.Substring(0, 1) })
                                    .GroupBy(m => m.Letter)
                                    .Select(g => new
                                    {
                                        Letter = g.Key,
                                        Count = g.Count()
                                    })
                                    .OrderBy(r => r.Letter)
                                    .ToList();
هدف از این مثال بیان مجاز بودن استفاده‌ی از متد Substring بر روی خواص رشته‌ای است که EF-Core امکان ترجمه‌ی آن‌ها را به کدهای SQL دارد.
با این خروجی:



مثال 7: حروف '-','(',')', ' ' را از شماره تلفن‌ها حذف کنید.
این گزارش باید به همراه ستون‌های memid, telephone باشد.

بانک اطلاعاتی PostgreSQL به همراه تابع استاندارد regexp_replace است و می‌توان از آن برای حل یک چنین مسایلی استفاده کرد:
select memid, regexp_replace(telephone, '[^0-9]', '', 'g') as telephone
from members
order by memid;
اما SQL Server هنوز هم به همراه یک چنین تابعی نیست. بنابراین از روش زیر نیز می‌توان مثال جاری را حل کرد:
var members = context.Members
                                .Select(member => new
                                {
                                    member.MemId,
                                    Telephone = member.Telephone.Replace("-", "")
                                                        .Replace("(", "")
                                                        .Replace(")", "")
                                                        .Replace(" ", "")
                                })
                                .OrderBy(r => r.MemId)
                                .ToList();
با این خروجی:



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
بازخوردهای پروژه‌ها
عدم سازگاری با EF
با سلام و احترام
وقتی دیتا سورس از جنس اینتیتی تعریف می‌کنم بحتی هیچ ستونی هم اد نمی‌کنم بدون اینکه خطایی دریافت کنم فایل پی دی اف، read only  می‌مونه و حجم فایل 0kb باقی می‌مونه
 public static IPdfReportData CreatePdfReport(Order order, int languageId)
        {
            if (order == null)
                throw new ArgumentNullException("order");



            Language lang = IoC.Resolve<ILanguageService>().GetLanguageById(languageId);

            if (lang == null)
                throw new NopException("Language could not be loaded");

            var localizationManager = IoC.Resolve<ILocalizationManager>();
            var orderProductVariants = order.OrderProductVariants;
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "coponet", Application = "coponet eshop", Keywords = "Factor", Subject = "Factor", Title = "Factor" });
                doc.Compression(new CompressionSettings
                {
                    EnableCompression = true,
                    EnableFullCompression = true
                });
                doc.PrintingPreferences(new PrintingPreferences
                {
                    ShowPrintDialogAutomatically = false
                });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\arial.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(CalenderHelper.dateTimeParseToString("yyyy/MM/dd", order.CreatedOn, CalenderEnum.PersianCalender));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.RunDirection(PdfRunDirection.LeftToRight);
                    //defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("Our new rpt.");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.ClassicTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(1);
            })
            .MainTableDataSource(dataSource =>
            {
                //var listOfRows = new List<User>();
                //for (int i = 0; i < 40; i++)
                //{
                //    listOfRows.Add(new User {Id = i});
                //}
                //dataSource.StronglyTypedList(listOfRows);
                dataSource.StronglyTypedList(orderProductVariants);
            })
            )
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
           ).Generate(data => data.AsPdfFile(string.Format("{0}\\documents\\Temp\\Factor-{1}.pdf", HttpRuntime.AppDomainAppPath, order.OrderId)));
        }

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