مطالب
ارسال PingBack در ASP.NET
Pingback یکی از روش‌های اطلاع رسانی به سایت‌های دیگر در مورد لینک دادن به آن‌ها در سایت خود است. برای مثال من لینکی از یکی از مطالب شما را در متن جاری خودم قرار می‌دهم. سپس به وسیله‌ی ارسال یک ping، در مورد انجام اینکار به شما اطلاع رسانی می‌کنم. حاصل آن عموما قسمت معروف ping-backs سایت‌ها است. این مورد نیز یکی از روش‌های مؤثر SEO در گرفتن backlink است و تبلیغ محتوا.
کار کردن با پروتکل Ping-back آنچنان ساده نیست؛ از این جهت که تبادل ارتباطات آن با پروتکل XML-RPC انجام می‌شود. XML-RPC نیز توسط PHP کارها بیشتر مورد استفاده قرار می‌گیرد؛ بجای استفاده از پروتکل‌های استاندارد وب سرویس‌ها مانند Soap و امثال آن. پیاده سازی‌های ابتدایی Pingback نیز مرتبط است به Wordpress معروف که با PHP تهیه شده‌است. در ادامه نگاهی خواهیم داشت به جزئیات پیاده سازی ارسال ping-back توسط برنامه‌های ASP.NET.


یافتن آدرس وب سرویس سایت پذیرای Pingback

اولین قدم در پیاده سازی Pingback، یافتن آدرسی است که باید اطلاعات مورد نظر را به آن ارسال کرد. این آدرس عموما به دو طریق ارائه می‌شود:
الف) در هدری به نام x-pingback و یا pingback
ب) در قسمتی از کدهای HTML صفحه به شکل
 <link rel="pingback" href="pingback server">
برای مثال اگر به وبلاگ‌های MSDN دقت کنید، هدر x-pingback را می‌توانید در خروجی وب سرور آن‌ها مشاهده کنید:


همانطور که ملاحظه می‌کنید، نیاز است Response header را آنالیز کنیم.
        private Uri findPingbackServiceUri()
        {
            var request = (HttpWebRequest)WebRequest.Create(_targetUri);
            request.UserAgent = UserAgent;
            request.Timeout = Timeout;
            request.ReadWriteTimeout = Timeout;
            request.Method = WebRequestMethods.Http.Get;
            request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
            using (var response = request.GetResponse() as HttpWebResponse)
            {
                if (response == null) return null;

                var url = extractPingbackServiceUriFormHeaders(response);
                if (url != null)
                    return url;

                if (!isResponseHtml(response))
                    return null;

                using (var reader = new StreamReader(response.GetResponseStream()))
                {
                    return extractPingbackServiceUriFormPage(reader.ReadToEnd());
                }
            }
        }

        private static Uri extractPingbackServiceUriFormHeaders(WebResponse response)
        {
            var pingUrl = response.Headers.AllKeys.FirstOrDefault(header =>
                                header.Equals("x-pingback", StringComparison.OrdinalIgnoreCase) ||
                                header.Equals("pingback", StringComparison.OrdinalIgnoreCase));

            return getValidAbsoluteUri(pingUrl);
        }

        private static Uri extractPingbackServiceUriFormPage(string content)
        {
            if (string.IsNullOrWhiteSpace(content)) return null;
            var regex = new Regex(@"(?s)<link\srel=""pingback""\shref=""(.+?)""", RegexOptions.IgnoreCase);
            var match = regex.Match(content);
            return (!match.Success || match.Groups.Count < 2) ? null : getValidAbsoluteUri(match.Groups[1].Value);
        }

        private static Uri getValidAbsoluteUri(string url)
        {
            Uri absoluteUri;
            return string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out absoluteUri) ? null : absoluteUri;
        }

        private static bool isResponseHtml(WebResponse response)
        {
            var contentTypeKey = response.Headers.AllKeys.FirstOrDefault(header =>
                                        header.Equals("content-type", StringComparison.OrdinalIgnoreCase));
            return !string.IsNullOrWhiteSpace(contentTypeKey) &&
                    response.Headers[contentTypeKey].StartsWith("text/html", StringComparison.OrdinalIgnoreCase);
        }
نحوه‌ی استخراج آدرس سرویس Pingback یک سایت را در کدهای فوق ملاحظه می‌کنید.
targetUri، آدرسی است از یک سایت دیگر که در سایت ما درج شده‌است. زمانیکه این صفحه را درخواست می‌کنیم، response.Headers.AllKeys حاصل می‌تواند حاوی کلید x-pingback باشد یا خیر. اگر بلی، همینجا کار پایان می‌یابد. فقط باید مطمئن شد که این آدرس مطلق است و نه نسبی. به همین جهت در متد getValidAbsoluteUri، بررسی بر روی UriKind.Absolute انجام شده‌است.
اگر هدر فاقد کلید x-pingback باشد، قسمت ب را باید بررسی کرد. یعنی نیاز است محتوای Html صفحه را برای یافتن link rel=pingback بررسی کنیم. همچنین باید دقت داشت که پیش از اینکار نیاز است حتما بررسی isResponseHtml صورت گیرد. برای مثال در سایت شما لینکی به یک فایل 2 گیگابایتی SQL Server درج شده‌است. در این حالت نباید ابتدا 2 گیگابایت فایل دریافت شود و سپس بررسی کنیم که آیا محتوای آن حاوی link rel=pingback است یا خیر. اگر محتوای ارسالی از نوع text/html بود، آنگاه کار دریافت محتوای لینک انجام خواهد شد.


ارسال Ping به آدرس سرویس Pingback

اکنون که آدرس سرویس pingback یک سایت را یافته‌ایم، کافی است ping ایی را به آن ارسال کنیم:
        public void Send()
        {
            var pingUrl = findPingbackServiceUri();
            if (pingUrl == null)
                throw new NotSupportedException(string.Format("{0} doesn't support pingback.", _targetUri.Host));

            sendPing(pingUrl);
        }

        private void sendPing(Uri pingUrl)
        {
            var request = (HttpWebRequest)WebRequest.Create(pingUrl);
            request.UserAgent = UserAgent;
            request.Timeout = Timeout;
            request.ReadWriteTimeout = Timeout;
            request.Method = WebRequestMethods.Http.Post;
            request.ContentType = "text/xml";
            request.ProtocolVersion = HttpVersion.Version11;
            makeXmlRpcRequest(request);
            using (var response = (HttpWebResponse)request.GetResponse())
            {
                response.Close();
            }
        }

        private void makeXmlRpcRequest(WebRequest request)
        {
            var stream = request.GetRequestStream();
            using (var writer = new XmlTextWriter(stream, Encoding.ASCII))
            {
                writer.WriteStartDocument(true);
                writer.WriteStartElement("methodCall");
                writer.WriteElementString("methodName", "pingback.ping");
                writer.WriteStartElement("params");

                writer.WriteStartElement("param");
                writer.WriteStartElement("value");
                writer.WriteElementString("string", Uri.EscapeUriString(_sourceUri.ToString()));
                writer.WriteEndElement();
                writer.WriteEndElement();

                writer.WriteStartElement("param");
                writer.WriteStartElement("value");
                writer.WriteElementString("string", Uri.EscapeUriString(_targetUri.ToString()));
                writer.WriteEndElement();
                writer.WriteEndElement();

                writer.WriteEndElement();
                writer.WriteEndElement();
            }
        }
اینبار HttpWebRequest تشکیل شده از نوع post است و نه get. همچنین مقداری را که باید ارسال کنیم نیاز است مطابق پروتکل XML-RPC باشد. برای کار با XML-RPC در دات نت یا می‌توان از کتابخانه‌ی Cook Computing's XML-RPC.Net استفاده کرد و یا مطابق کدهای فوق، دستورات آن‌را توسط یک XmlTextWriter کنار هم قرار داد و نهایتا در درخواست Post ارسالی درج کرد.
در اینجا sourceUri آدرس صفحه‌ای در سایت ما است که targetUri ایی (آدرسی از سایت دیگر) در آن درج شده‌است. در یک pinback، صرفا این دو آدرس به سرویس دریافت کننده‌ی pingback ارسال می‌شوند.
سپس سایت دریافت کننده‌ی ping، ابتدا sourceUri را دریافت می‌کند تا عنوان آن‌را استخراج کند و همچنین بررسی می‌کند که آیا targetUri، در آن درج شده‌است یا خیر (آیا spam است یا خیر)؟
تا اینجا اگر این مراحل را کنار هم قرار دهیم به کلاس Pingback ذیل خواهیم رسید:
Pingback.cs


نحوه‌ی استفاده از کلاس Pingback تهیه شده

کار ارسال Pingback عموما به این نحو است: هر زمانیکه مطلبی یا یکی از نظرات آن، ثبت یا ویرایش می‌شوند، نیاز است Pingbackهای آن ارسال شوند. بنابراین تنها کاری که باید انجام شود، استخراج لینک‌های خارجی یک صفحه و سپس فراخوانی متد Send کلاس فوق است.
یافتن لینک‌های یک محتوا را نیز می‌توان مانند متد extractPingbackServiceUriFormPage فوق، توسط یک Regex انجام داد و یا حتی با استفاده از کتابخانه‌ی معروف HTML Agility Pack:
var doc = new HtmlWeb().Load(url);
var linkTags = doc.DocumentNode.Descendants("link");
var linkedPages = doc.DocumentNode.Descendants("a")
                                  .Select(a => a.GetAttributeValue("href", null))
                                  .Where(u => !String.IsNullOrEmpty(u));
مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت سوم - قالب پذیر ساختن گرید
در قسمت دوم، قالب نمایش ردیف‌های جدول، ثابت است و درون جدول به صورت مستقیمی درج و تعریف شده‌است. در ادامه می‌خواهیم این گرید را به نحوی تغییر دهیم که به ازای حالت‌های مختلفی مانند نمایش اطلاعات و یا ویرایش اطلاعات هر ردیف، از قالب‌های خاص آن‌ها استفاده شود.
قابلیتی که در ادامه از آن برای «قالب پذیر ساختن گرید» استفاده خواهیم کرد، همان نکته‌ی «امکان تعویض پویای قالب‌های یک دربرگیرنده» است که در مطلب «امکان تعریف قالب‌ها در Angular با دایرکتیو ng-template» به آن پرداختیم.


تعریف قالب‌های نمایش و ویرایش اطلاعات یک ردیف در گرید طراحی شده

پس از آشنایی با دایرکتیوهای تعریف و کار با قالب‌ها در Angular، اکنون تبدیل بدنه‌ی ثابت جدول، به دو قالب نمایش و ویرایش، ساده‌است.
در قسمت دوم این سری، کار رندر بدنه‌ی اصلی گرید توسط همین چند سطر، در قالب آن مدیریت می‌شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
        <td class="text-center">{{ item.productId }}</td>
        <td class="text-center">{{ item.productName }}</td>
        <td class="text-center">{{ item.price | number:'.0' }}</td>
        <td class="text-center">
          <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
            disabled="disabled" />
        </td>
      </tr>
    </tbody>
  </table>

در ادامه قسمت داخلی ngFor را تبدیل به یک ng-container می‌کنیم تا قالب پذیر شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <ng-container [ngTemplateOutlet]="loadTemplate(item)"
                 [ngOutletContext]="{ $implicit: item, idx: i }"></ng-container>
      </tr>
    </tbody>
کار دایرکتیو ngOutletContext، تنظیم شیء context هر قالب است. به این ترتیب شیء متناظر با هر ردیف و همچنین ایندکس آن‌را به هر قالب ارجاع می‌دهیم. خاصیت implicit$ به این معنا است که اگر منبع داده‌ی متغیر ورودی مشخص نشد، از مقدار item استفاده شود.
در اینجا ngTemplateOutlet این امکان را می‌دهد تا بتوان توسط کدهای برنامه، قالب هر ردیف را مشخص کرد. متد loadTemplate در کدهای کامپوننت متناظر فراخوانی شده و بر اساس وضعیت هر ردیف، یکی از دو قالب ذیل را بازگشت می‌دهد:

الف) قالب نمایش معمولی و فقط خواندنی رکوردها


<!--The Html Template for Read-Only Rows-->
<ng-template #readOnlyTemplate let-item let-i="idx">
  <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
  <td class="text-center">{{ item.productId }}</td>
  <td class="text-center">{{ item.productName }}</td>
  <td class="text-center">{{ item.price | number:'.0' }}</td>
  <td class="text-center">
    <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
      disabled="disabled" />
  </td>
  <td>
    <input type="button" value="Edit" class="btn btn-default btn-xs" (click)="editItem(item)"
    />
  </td>
  <td>
    <input type="button" value="Delete" (click)="deleteItem(item)" class="btn btn-danger btn-xs"
    />
  </td>
</ng-template>
همانطور که ملاحظه می‌کنید، در اینجا بدنه‌ی ngFor را به یک ng-template مشخص شده‌ی با readOnlyTemplate# انتقال داده‌ایم. همچنین دو متغیر ورودی item و i را توسط -let تعریف کرده‌ایم. چون عبارت منبع داده item مشخص نشده‌است، از همان خاصیت implicit$ شیء context استفاده می‌کند.
این قالب در کدهای کامپوننت آن به صورت ذیل قابل دسترسی و انتخاب شده‌است:
 @ViewChild("readOnlyTemplate") readOnlyTemplate: TemplateRef<any>;

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



شبیه به همان کاری را که برای نمایش ردیف‌های فقط خواندنی انجام دادیم، در مورد قالب ویرایش هر ردیف نیز تکرار می‌کنیم. در اینجا فقط امکان ویرایش نام محصول، قیمت آن و موجود بودن آن‌را توسط یک‌سری input box مهیا کرده‌ایم:
<!--The Html Template for Editable Rows-->
<ng-template #editTemplate let-item let-i="idx">
  <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
  <td class="text-center">{{ item.productId }}</td>
  <td class="text-center">
    <input type="text" [(ngModel)]="selectedItem.productName" class="form-control" />
  </td>
  <td class="text-center">
    <input type="text" [(ngModel)]="selectedItem.price" class="form-control" />
  </td>
  <td class="text-center">
    <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
      [(ngModel)]="selectedItem.isAvailable" />
  </td>
  <td>
    <input type="button" value="Save" (click)="saveItem()" class="btn btn-success btn-xs"
    />
  </td>
  <td>
    <input type="button" value="Cancel" (click)="cancel()" class="btn btn-warning btn-xs"
    />
  </td>
</ng-template>
به این قالب نیز با توجه به template reference variable آن که editTemplate# نام دارد، به صورت ذیل در کامپوننت متناظر دسترسی خواهیم یافت.
 @ViewChild("editTemplate") editTemplate: TemplateRef<any>;

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


خواص عمومی مورد نیاز جهت کار با قالب‌ها و ویرایش‌های درون ردیفی

@ViewChild("readOnlyTemplate") readOnlyTemplate: TemplateRef<any>;
@ViewChild("editTemplate") editTemplate: TemplateRef<any>;
selectedItem: AppProduct;
isNewRecord: boolean;
برای اینکه بتوانیم قالب‌ها را به صورت پویا تعویض کنیم، نیاز است در کدهای کامپوننت، به آن‌ها دسترسی داشت. اینکار را توسط تعریف ViewChildهایی با همان نام template reference variable قالب‌ها انجام داده‌ایم.
به علاوه اگر به قالب editTemplate دقت کنید، مقدار ویرایش شده به [(ngModel)]="selectedItem.productName" انتساب داده می‌شود. به همین جهت شیء selectedItem نیز تعریف شده‌است.
همچنین نیاز است بدانیم اکنون در حال ویرایش یک ردیف هستیم یا این ردیف، کاملا ردیف جدیدی است. به همین جهت پرچم isNewRecord نیز تعریف شده‌است.


فعالسازی قالب ویرایش هر ردیف

در انتهای هر ردیف، دکمه‌ی ویرایش نیز قرار دارد که به (click) آن، رخداد editItem متصل است:
editItem(item: AppProduct) {
  this.selectedItem = item;
}
در اینجا Item انتخابی را به selectedItem انتساب می‌دهیم. همین مساله سبب محاسبه‌ی مجدد ردیف می‌شود. یعنی متد loadTemplate داخل حلقه‌ی ngFor مجددا فراخوانی می‌شود:
  loadTemplate(item: AppProduct) {
    if (this.selectedItem && this.selectedItem.productId === item.productId) {
      return this.editTemplate;
    } else {
      return this.readOnlyTemplate;
    }
  }
در اینجا بررسی می‌کنیم که آیا در حال ویرایش اطلاعات هستیم؟ آیا selectedItem  مقدار دهی شده‌است و نال نیست؟ اگر بله، قالب editTemplate را بازگشت می‌دهیم. اگر خیر، قالب نمایش ردیف‌های فقط خواندنی بازگشت داده می‌شود. به این ترتیب می‌توان در کدهای برنامه به صورت پویا، در مورد نمایش قالبی خاص تصمیم‌گیری کرد.


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

دکمه‌ی افزودن یک ردیف جدید به صورت ذیل به قالب اضافه شده‌است:
<div class="panel">
  <input type="button" value="Add new product" class="btn btn-primary" (click)="addItem()"
  />
</div>
بنابراین نیاز است رخ‌داد addItem آن‌را به صورت ذیل تعریف کرد:
  addItem() {
    this.selectedItem = new AppProduct(0, "", 0, false);
    this.isNewRecord = true;

    this.queryResult.items.push(this.selectedItem);
    this.queryResult.totalItems++;
  }
در اینجا برخلاف حالت ویرایش که selectedItem را به item انتخابی ردیف جاری تنظیم کردیم، آن‌را به یک شیء جدید و تازه تنظیم می‌کنیم. همچنین پرچم isNewRecord  را نیز true خواهیم کرد. سپس این آیتم را به لیست رکوردهای موجود گرید نیز اضافه می‌کنیم. همینقدر تغییر، سبب محاسبه‌ی مجدد loadTemplate و بارگذاری قالب ویرایشی آن می‌شود.


مدیریت لغو ویرایش هر ردیف

برای اینکه ویرایش هر ردیف را لغو کنیم و قالب آن‌‌را به حالت فقط خواندنی بازگشت دهیم، فقط کافی است selectedItem را به نال تنظیم کنیم:
cancel() {
  this.selectedItem = null;
}
با این تنظیم و محاسبه‌ی خودکار و مجدد متد loadTemplate، قسمت return this.readOnlyTemplate فعال می‌شود که سبب نمایش عادی یک ردیف خواهد شد.


مدیریت حذف هر ردیف

در اینجا با پیاده سازی متد رخ‌دادگردان deleteItem و ارسال id هر ردیف به سرور، کار حذف هر ردیف را انجام خواهیم داد:
  deleteItem(item: AppProduct) {
    this.productsService
      .deleteAppProduct(item.productId)
      .subscribe((resp: Response) => {
        this.getPagedProductsList();
      });
  }


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

آخرین عملیاتی که باید مدیریت شود، بررسی پرچم isNewRecord است. اگر true بود، کار افزودن یک ردیف جدید صورت گرفته و سپس این پرچم false می‌شود. اگر false بود، به معنای درخواست به روز رسانی ردیفی مشخص است. در پایان هر دو عملیات selectedItem را نیز true می‌کنیم و این پایان عملیات باید داخل قسمت دریافت پاسخ از سرور مدیریت شود و نه پس از فراخوانی این متدها؛ چون متدهای subscribe غیرهمزمان بوده و ردیف‌های پس از آن‌ها بلافاصله اجرا می‌شوند.
  saveItem() {
    if (this.isNewRecord) {
      this.productsService
        .addAppProduct(this.selectedItem)
        .subscribe((resp: AppProduct) => {
          this.selectedItem.productId = resp.productId;
          this.isNewRecord = false;
          this.selectedItem = null;
        });
    } else {
      this.productsService
        .updateAppProduct(this.selectedItem.productId, this.selectedItem)
        .subscribe((resp: AppProduct) => {
          this.selectedItem = null;
        });
    }
  }


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

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

چرخه انتشار نرم افزار:
 چرخه انتشار نرم افزار از زمان شروع کد نویسی تا عرضه نسخه نهایی می‌باشد که شامل چندین مرحله و عرضه نرم افزار می‌باشد.
  1. Pre-alpha
    این مرحله شامل تمام فعالیت‌های انجام شده قبل از مرحله تست می‌باشد. در این دوره آنالیز نیازمندیها، طراحی نرم افزار، توسعه نرم افزار و حتی تست واحد باشد. در نرم افزار‌های سورس باز چندین نسخه قبل از آلفا ممکن است عرضه شوند.
  2. Alpha
    این مرحله شامل همه فعالیت‌ها از زمان شروع تست می‌باشد. البته منظور از تست، تست تیمی و تست خود نرم افزار می‌باشد. نرم افزار‌های آلفا هنوز ممکن است خطا و اشکالاتی داشته باشند و ممکن است اطلاعات شما از بین رود. در این مرحله امکانات جدیدی مرتبا به نرم افزار اضافه می‌گردد.
  3. Beta
    نرم افزار بتا، همه قابلیت‌های آن تکمیل شده و خطا‌های زیادی برای کامل شدن نرم افزار وجود دارد. در این مرحله بیشتر به تست کاهش تاثیرات به کاربران و تست کارایی دقت می‌شود. نسخه بتا، اولین نسخه‌ای خواهد بود که بیرون شرکت و یا سازمان در دسترس قرار می‌گیرد. برخی توسعه دهندگان به این مرحله preview، technical preview یا early access نیز می‌گویند.
  4. Release candidate
    در این مرحله نرم افزار، آماده عرضه به مصرف کنندگان است و نرم افزارهایی مثل سیستم عامل‌های ویندوز در دسترس تولید کنندگان قرار گرفته تا با جدید‌ترین سخت افزار خود یکپارچه شوند.
  5.  General availability (GA)
    در این مرحله، عرضه عمومی نرم افزار و بازاریابی و فروش نرم افزار مد نظر است و علاوه بر این تست امنیتی و در نرم افزار‌های خیلی بزرگ عرضه جهانی صورت می‌گیرد 
مراحلی همچون عرضه در وب و پشتیبانی نیز وجود دارند.

نسخه بندی نرم افزار:
برنامه‌های ویندوزی یا وب در ویژوال استادیو یک فایل AssemblyInfo دارند که در قسمت آخر آن، اطلاعات مربوط به نسخه نرم افزار ذخیره می‌شود. هر نسخه نرم افزار شامل چهار عدد می‌باشد که با نقطه از هم جدا شده است.
  • Major Version
    وقتی افزایش می‌یابد که تغییرات قابل توجهی در نرم افزار ایجاد شود
  • Minor Version
    وقتی افزایش یابد که ویژگی جزئی یا اصلاحات قابل توجهی به نرم افزار ایجاد شود.
  • Build Number
    به ازای هر بار ساخته شدن پروژه افزایش می‌یابد.
  • Revision
    وقتی افزایش می‌یابد که نواقص و باگ‌های کوچکی رفع شوند. 
وقتی که major یا minor افزایش یابد می‌تواند با کلماتی همچون alpha، beta یا release candidate همراه شود.در اکثر برنامه‌های تجاری اولین شمارهٔ انتشار یک محصول از نسحهٔ شمارهٔ یک شروع می‌شود. ترتیب نسخه بندی هم ممکن است تغییر یابد
major.minor[.build[.reversion]]
یا
major.minor[.maintenance[.build]]
نسخه بندی مایکروسافت:
اگر به نسخه برنامه Office توجه کرده باشید مثلا Office 2013 نسخه 15.0.4481.1508 می‌باشد که در این روش از تاریخ شروع پروژه و تعداد ماه‌ها یا روز‌ها و یا ثانیه‌ها با یک الگوریتم خاص برای تولید نسخه نرم افزار استفاده می‌شود.
نسخه بندی معنایی:

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

علیرغم آن، مهم است که این API، روشن و دقیق باشد. هنگامیکه API عمومی خود را تعیین کردید، تغییرات برنامه شما بر روی نسخه API عمومی تاثیر خواهد داشت و آنرا افزایش خواهد داد. بر این اساس، این مدل نسخه‌بندی را در نظر بگیرید: X.Y.Z یعنی (Major.Minor.Patch).

رفع حفره‌هایی که بر روی API عمومی تاثیر نمی‌گذارند، مقدار Patch را افزایش می‌دهند، تغییرات جدیدی که سازگار با نسخه قبلی است، مقدار Minor را افزایش می‌دهند و تغییرات جدیدی که کاملا بدیع هستند و به نحوی با تغییرات قبلی سازگار نیستند مقدار Major را افزایش می‌دهند. 

  1. نرم‌افزارهایی که از نسخه بندی معنایی استفاده می‌کنند، باید یک API عمومی داشته باشند. این API می‌تواند در خود کد یا و یا به طور صریح در مستندات باشد که باید دقیق و جامع باشد.
  2. یک شماره نسخه صحیح باید به شکل X.Y.Z باشد که در آن X،Y و Z اعداد صحیح غیر منفی هستند. X نسخه‌ی Major می‌باشد، Y نسخه‌ی Minor و Z نسخه‌ی Patch می‌باشد. هر عنصر باید یک به یک و بصورت عددی افزایش پیدا کند. به عنوان مثال: 1.9.0 -> 1.10.0 -> 1.11.0
  3. هنگامی که به یک نسخه‌ی Major یک واحد اضافه می‌شود، نسخه‌ی Minor و Patch باید به حالت 0 (صفر) تنظیم مجدد گردد. هنگامی که به شماره نسخه‌ی Minor یک واحد اضافه می‌شود، نسخه‌ی Patch باید به حالت 0 (صفر) تنظیم مجدد شود. به عنوان مثال: 1.1.3 -> 2.0.0 و 2.1.7 -> 2.2.0
  4. هنگامیکه یک نسخه از یک کتابخانه منتشر می‌شود، محتوای کتابخانه مورد نظر نباید به هیچ وجه تغییری داشته باشد. هر گونه تغییر جدیدی باید در قالب یک نسخه جدید انتشار پیدا کند.
  5. نسخه‌ی Major صفر (0.Y.Z) برای توسعه‌ی اولیه است. هر چیزی ممکن است در هر زمان تغییر یابد. API عمومی را نباید پایدار در نظر گرفت.
  6. نسخه 1.0.0 در حقیقت API عمومی را تعریف می‌کند. چگونگی تغییر و افزایش هر یک از نسخه‌ها بعد از انتشار این نسخه، وابسته به API عمومی و تغییرات آن می‌باشد.
  7. نسخه Patch یا (x.y.Z | x > 0) فقط در صورتی باید افزایش پیدا کند که تغییرات ایجاد شده در حد برطرف کردن حفره‌های نرم‌افزار باشد. برطرف کردن حفره‌های نرم‌افزار شامل اصلاح رفتارهای اشتباه در نرم‌افزار می‌باشد.
  8. نسخه Minor یا (x.Y.z | x > 0) فقط در صورتی افزایش پیدا خواهد کرد که تغییرات جدید و سازگار با نسخه قبلی ایجاد شود. همچنین این نسخه باید افزایش پیدا کند اگر بخشی از فعالیت‌ها و یا رفتارهای قبلی نرم‌افزار به عنوان فعالیت منقرض شده اعلام شود. همچنین این نسخه می‌تواند افزایش پیدا کند اگر تغییرات مهم و حیاتی از طریق کد خصوصی ایجاد و اعمال گردد. تغییرات این نسخه می‌تواند شامل تغییرات نسخه Patch هم باشد. توجه به این نکته ضروری است که در صورت افزایش نسخه Minor، نسخه Patch باید به 0 (صفر) تغییر پیدا کند.
  9. نسخه Major یا (X.y.z | X > 0) در صورتی افزایش پیدا خواهد کرد که تغییرات جدید و ناهمخوان با نسخه فعلی در نرم‌افزار اعمال شود. تغییرات در این نسخه می‌تواند شامل تغییراتی در سطح نسخه Minor و Patch نیز باشد. باید به این نکته توجه شود که در صورت افزایش نسخه Major، نسخه‌های Minor و Patch باید به 0 (صفر) تغییر پیدا کنند.
  10. یک نسخه قبل از انتشار می‌تواند توسط یک خط تیره (dash)، بعد از نسخه Patch (یعنی در انتهای نسخه) که انواع با نقطه (dot) از هم جدا می‌شوند، نشان داده شود. نشان‌گر نسخه قبل از انتشار باید شامل حروف، اعداد و خط تیره باشد [0-9A-Za-z-]. باید به این نکته دفت داشت که نسخه‌های قبل از انتشار خود به تنهایی یک انتشار به حساب می‌آیند اما اولویت و اهمیت نسخه‌های عادی را ندارد. برای مثال: 1.0.0-alpha ، 1.0.0-alpha.1 ، 1.0.0-0.3.7 ، 1.0.0-x.7.z.92
  11. یک نسخه Build می‌تواند توسط یک علامت مثبت (+)، بعد از نسخه Patch یا نسخه قبل از انتشار (یعنی در انتهای نسخه) که انواع آن با نقطه (dot) از هم جدا می‌شوند، نشان داده شود. نشان‌گر نسخه Build باید شامل حروف، اعداد و خط تیره باشد [0-9A-Za-z-]. باید به این نکته دقت داشت که نسخه‌های Build خود به تنهایی یک انتشار به حساب می‌آیند و اولویت و اهمیت بیشتری نسبت به نسخه‌های عادی دارند. برای مثال: 1.0.0+build.1 ، 1.3.7+build.11.e0f985a
  12. اولویت‌بندی نسخه‌ها باید توسط جداسازی بخش‌های مختلف یک نسخه به اجزای تشکیل دهنده آن یعنی Minor، Major، Patch، نسخه قبل از انتشار و نسخه Build و ترتیب اولویت بندی آن‌ها صورت گیرد. نسخه‌های Minor، Major و Patch باید بصورت عددی مقایسه شوند. مقایسه نسخه‌های قبل از انتشار و نسخه Build باید توسط بخش‌های مختلف که توسط جداکننده‌ها (نقطه‌های جداکننده) تفکیک شده است، به این شکل سنجیده شود:

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

بخش‌های عددی همواره اولویت پایین‌تری نسبت به بخش‌های غیر عددی دارند. برای مثال:

1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0-rc.1+build.1 < 1.0.0 < 1.0.0+0.3.7 < 1.3.7+build < 1.3.7+build.2.b8f12d7 < 1.3.7+build.11.e0f985a

منبع نسخه بندی معنایی : semver.org

 
مطالب
استفاده از MembershipProvider و RoleProvider در windows Application
برای استفاده از  سیستم مدیریت کاربران و نقش‌های آنها به یک پیاده سازی از کلاس انتزاعی MembershipProvider نیاز داریم. SQL Membership Provider تو کار دات نت، انتخاب پیش فرض ماست ولی به دلیل طراحی در دات نت 2 و نیاز سنجی قدیمی اون و همچنین گره زدن برنامه با sql server  (استفاده از stored procedure و... ) انتخاب مناسبی نیست. پیشنهاد خود مایکروسافت استفاده از SimpleMembership است که این پیاده سازی قابلیت‌های بیشتری از MembershipProvider پایه رو دارد. این قابلیت‌های بیشتر با استفاده از کلاس انتزاعی ExtendedMembershipProvider که خود از از MembershipProvider مشتق شده است میسر شده است.
برای این آموزش ما از SimpleMembership  استفاده می‌کنیم اگر  شما دوست ندارید از SimpleMembership  استفاده کنید می‌تونید از Provider  های دیگه ای استفاده کنید و حتی می‌تونید یک پروایدر سفارشی برای خودتون بنویسید.
برای شروع یک پروژه ConsoleApplication  تعریف کنید و رفرنس‌های زیر رو اضافه کنید.
System.Web.dll 
System.Web.ApplicationServices.dll
 خاصیت  Copy Local دو کتابخانه زیر رو true  ست کنید.
C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies\WebMatrix.Data.dll
C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies\WebMatrix.WebData.dll
- در صورتیکه یک پروژه Asp.Net MVC 4 به همراه تمپلت Internet Application بسازید بصورت خودکار SimpleMembership  و رفرنس‌های آن به پروژه اضافه می‌شود.
 یک فایل App.config با محتویات زیر به پروژه اضافه کنید  و تنظیمات ConnectionString را مطابق با دیتابیس موجود در کامپیوتر خود تنظیم کنید:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <connectionStrings>
    <add name="DefaultConnection" 
         connectionString="Data Source=.;Initial Catalog=SimpleMembershipProviderDB;Integrated Security=True;"
         providerName="System.Data.SqlClient"/>
  </connectionStrings>
  
  <system.web>
    <roleManager enabled="true" defaultProvider="SimpleRoleProvider">
      <providers>
        <clear/>
        <add name="SimpleRoleProvider" type="WebMatrix.WebData.SimpleRoleProvider, WebMatrix.WebData"/>
      </providers>
    </roleManager>
    <membership defaultProvider="SimpleMembershipProvider">
      <providers>
        <clear/>
        <add name="SimpleMembershipProvider" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData"/>
      </providers>
    </membership>

  </system.web>
</configuration>
محتویات فایل Program.cs :
using System; 
using System.Security.Principal; 
using System.Web.Security;
using WebMatrix.WebData;

namespace MemberShipConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            WebSecurity.InitializeDatabaseConnection("DefaultConnection",
                "UserProfile", "UserId", "UserName", 
                autoCreateTables: true);

            AddUserAndRolSample();
            Login();
            if (System.Threading.Thread.CurrentPrincipal.Identity.IsAuthenticated)
                RunApp();

        }

        static void AddUserAndRolSample()
        {
            if (WebSecurity.UserExists("iman"))
                return;

            //  No implements in SimpleMembershipProvider :
            //     Membership.CreateUser("iman", "123");
            WebSecurity.CreateUserAndAccount("iman", "123");
            Roles.CreateRole("admin");
            Roles.CreateRole("User");
            Roles.AddUserToRole("iman", "admin");
        }

        static void Login()
        {
            for (int i = 0; i < 3; i++)
            {
                Console.Write("UserName: ");
                var userName = Console.ReadLine();
                Console.Write("Password: ");
                var password = Console.ReadLine();
                if (Membership.ValidateUser(userName, password))
                {

                    var user = Membership.GetUser(userName);
                    var identity = new GenericIdentity(user.UserName);
                    var principal = new RolePrincipal(identity);
                    System.Threading.Thread.CurrentPrincipal = principal;

                    Console.Clear();
                    return;
                }


                Console.WriteLine("User Name Or Password Not Valid.");
            }

        }
        static void RunApp()
        {
            Console.WriteLine("Welcome To MemberShip. User Name: {0}",
                System.Threading.Thread.CurrentPrincipal.Identity.Name);

            if (System.Threading.Thread.CurrentPrincipal.IsInRole("admin"))
                Console.WriteLine("Hello Admin User!");

            Console.Read();
        }
    }
}
در ابتدا با فراخوانی متد InitializeDatabaseConnection  تنظیمات اولیه simpleMembership را مقدار دهی می‌کنیم. این متد حتما باید یکبار اجرا شود.
InitializeDatabaseConnection(string connectionStringName, 
                             string userTableName, 
                             string userIdColumn, 
                             string userNameColumn, 
                             bool autoCreateTables)                            
ملاحظه می‌کنید پارامتر آخر متد مربوط به ساخت جداول مورد نیاز این پروایدر است. در صورتی که بخواهیم در پروژه از EntityFramework استفاده کنیم می‌تونیم موجودیت‌های معادل جدول‌های مورد نیاز SimpleMembership  رو در EF  بسازیم و در این متد AutoCreateTables رو False مقدار دهی کنیم. برای بدست آوردن موجودیت‌های معادل جدول‌های این پروایدر با ابزار Entity Framework Power Tools و روش مهندسی معکوس ، تمام موجودیت‌های یک دیتابیس ساخته شده رو استخراج می‌کنیم.
 - SimpleMembership  از تمام خانواده microsoft sql پشتبانی می‌کنه (SQL Server, SQL Azure, SQL Server CE, SQL Server Express,LocalD)   برای سایر دیتابیس‌ها به علت تفاوت در دستورات ساخت تیبل‌ها و ... مایکروسافت تضمینی نداده ولی اگر خودمون جدول‌های مورد نیاز SimpleMembership  رو ساخته باشیم احتمالا در سایر دیتابیس‌ها هم قابل استفاده است.
 در ادامه برنامه بالا یک کاربر و دو نقش تعریف کردیم و نقش admin  رو به کاربر نسبت دادیم. در متد login در صورت معتبر بودن کاربر  ، اون رو به ترد اصلی برنامه معرفی می‌کنیم. هر جا خواستیم می‌تونیم نقشهای کاربر رو چک کنیم و نکته آخر با اولین چک کردن نقش یک کاربر تمام نقش‌های اون در حافظه سیستم کش می‌شود و تنها مرتبه اول با دیتابیس ارتباط برقرار می‌کند.

مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت دوم - اجرای قواعد اعتبارسنجی تعریف شده
در قسمت قبل، روش تعریف قواعد اعتبارسنجی را با استفاده از کتابخانه‌ی Fluent Validation بررسی کردیم. در این قسمت می‌خواهیم این قواعد را به صورت خودکار به یک برنامه‌ی ASP.NET Core معرفی کرده و سپس از آن‌ها استفاده کنیم.


روش اول: استفاده‌ی دستی از اعتبارسنج کتابخانه‌ی Fluent Validation

روش‌های زیادی برای استفاده‌ی از قواعد تعریف شده‌ی توسط کتابخانه‌ی Fluent Validation وجود دارند. اولین روش، فراخوانی دستی اعتبارسنج، در مکان‌های مورد نیاز است. برای اینکار در ابتدا نیاز است با اجرای دستور «dotnet add package FluentValidation.AspNetCore»، این کتابخانه را در پروژه‌ی وب خود نیز نصب کنیم تا بتوانیم از کلاس‌ها و متدهای آن استفاده نمائیم. پس از آن، روش دستی کار با کلاس RegisterModelValidator که در قسمت قبل آن‌را تعریف کردیم، به صورت زیر است:
using FluentValidationSample.Models;
using Microsoft.AspNetCore.Mvc;

namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult RegisterValidateManually(RegisterModel model)
        {
            var validator = new RegisterModelValidator();
            var validationResult = validator.Validate(model);
            if (!validationResult.IsValid)
            {
                return BadRequest(validationResult.Errors[0].ErrorMessage);
            }

            // TODO: Save the model

            return Ok();
        }
    }
}
ساده‌ترین روش کار با RegisterModelValidator تعریف شده، ایجاد یک وهله‌ی جدید از آن و سپس فراخوانی متد Validate آن شیء است. در این حالت می‌توان کنترل کاملی را بر روی قالب پیام نهایی بازگشت داده شده داشت. برای مثال در اینجا اولین خطای بازگشت داده شده، به اطلاع کاربر رسیده‌است. حتی می‌توان کل شیء Errors را نیز بازگشت داد.

یک نکته: متد الحاقی AddToModelState که در فضای نام FluentValidation.AspNetCore قرار دارد، امکان تبدیل نتیجه‌ی اعتبارسنجی حاصل را به ModelState استاندارد ASP.NET Core نیز میسر می‌کند:
public IActionResult RegisterValidateManually(RegisterModel model)
{
    var validator = new RegisterModelValidator();
    var validationResult = validator.Validate(model);
    if (!validationResult.IsValid)
    {
       validationResult.AddToModelState(ModelState, null);
       return BadRequest(ModelState);
    }

    // TODO: Save the model

    return Ok();
}


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

بجای وهله سازی دستی RegisterModelValidator و ایجاد وابستگی مستقیمی به آن، می‌توان از روش تزریق وابستگی‌های آن نیز استفاده کرد. در این حالت اعتبارسنج RegisterModelValidator با طول عمر Transient به سیستم تزریق وابستگی‌ها معرفی شده:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IValidator<RegisterModel>, RegisterModelValidator>();
            services.AddControllersWithViews();
        }
 و پس از آن با تزریق <IValidator<RegisterModel به سازنده‌ی کنترلر مدنظر، می‌توان به امکانات آن همانند روش اول، دسترسی یافت:
namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IValidator<RegisterModel> _registerModelValidator;

        public HomeController(IValidator<RegisterModel> registerModelValidator)
        {
            _registerModelValidator = registerModelValidator;
        }

        [HttpPost]
        public IActionResult RegisterValidatorInjection(RegisterModel model)
        {
            var validationResult = _registerModelValidator.Validate(model);
            if (!validationResult.IsValid)
            {
                return BadRequest(validationResult.Errors[0].ErrorMessage);
            }

            // TODO: Save the model

            return Ok();
        }
    }
}
به این ترتیب new RegisterModelValidator را با وهله‌ای از <IValidator<RegisterModel، تعویض کردیم. کار با این روش بسیار انعطاف پذیر بوده و همچنین قابلیت آزمون پذیری بالایی را نیز دارد.


روش سوم: خودکار سازی اجرای یک تک اعتبارسنج تعریف شده

اگر متد الحاقی AddFluentValidation را به صورت زیر به سیستم معرفی کنیم:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IValidator<RegisterModel>, RegisterModelValidator>();
            services.AddControllersWithViews().AddFluentValidation();
        }
سبب اجرای خودکار تمام IValidatorهای اضافه شده‌ی به سیستم، پیش از اجرای اکشن متد مرتبط با آن‌ها می‌شود. برای مثال اگر اکشن متدی دارای پارامتری از نوع RegisterModel بود، چون IValidator مخصوص به آن به سیستم تزریق وابستگی‌ها معرفی شده‌است، متد الحاقی AddFluentValidation، کار وهله سازی خودکار این IValidator و سپس فراخوانی متد Validate آن‌را به صورت خودکار انجام می‌دهد. به این ترتیب، قطعه کدهایی را که تاکنون نوشتیم، به صورت زیر خلاصه خواهند شد که در آن‌ها اثری از بکارگیری کتابخانه‌ی FluentValidation مشاهده نمی‌شود:
namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        [HttpPost]
        public IActionResult RegisterValidatorAutomatically(RegisterModel model)
        {
            if (!ModelState.IsValid)
            {
                // re-render the view when validation failed.
                return View(model);
            }

            // TODO: Save the model

            return Ok();
        }
    }
}
زمانیکه model به سمت اکشن متد فوق ارسال می‌شود، زیرساخت model-binding موجود در ASP.NET Core، اینبار کار اعتبارسنجی آن‌را توسط RegisterModelValidator به صورت خودکار انجام داده و نتیجه‌ی آن‌را به ModelState اضافه می‌کند که برای مثال در اینجا سبب رندر مجدد فرم شده که تمام مباحث tag-helper‌های استانداردی مانند asp-validation-summary و asp-validation-for پس از آن به صورت متداولی و همانند قبل، قابل استفاده خواهند بود.

نکته 1: تنظیمات فوق برایASP.NET Web Pages و PageModels نیز یکی است. فقط با این تفاوت که اعتبارسنج‌ها را فقط می‌توان به مدل‌هایی که به صورت خواص یک page model تعریف شده‌اند، اعمال کرد و نه به کل page model.

نکته 2: اگر کنترلر شما به ویژگی [ApiController] مزین شده باشد:
namespace FluentValidationSample.Web.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class HomeController : Controller
    {
        [HttpPost]
        public IActionResult RegisterValidatorAutomatically(RegisterModel model)
        {
            // TODO: Save the model

            return Ok();
        }
    }
}
در این حالت دیگر نیازی به ذکر if (!ModelState.IsValid) نیست و خطای حاصل از شکست اعتبارسنجی، به صورت خودکار توسط FluentValidation تشکیل شده و بازگشت داده می‌شود (پیش از رسیدن به بدنه‌ی اکشن متد فوق) و برای نمونه یک چنین شکل و خروجی خودکاری را پیدا می‌کند:
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|84df05e2-41e0d4841bb61293.",
    "errors": {
        "FirstName": [
            "'First Name' must not be empty."
        ]
    }
}
اگر علاقمند به سفارشی سازی این خروجی خودکار هستید، باید به این صورت با تنظیم ApiBehaviorOptions و مقدار دهی نحوه‌ی تشکیل ModelState نهایی، عمل کرد:
        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            // override modelstate
            services.Configure<ApiBehaviorOptions>(options =>
            {
                options.InvalidModelStateResponseFactory = context =>
                {
                    var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p => p.ErrorMessage)).ToList();
                    return new BadRequestObjectResult(new
                    {
                        Code = "00009",
                        Message = "Validation errors",
                        Errors = errors
                    });
                };
            });
        }


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

و در آخر بجای معرفی دستی تک تک اعتبارسنج‌های تعریف شده به سیستم تزریق وابستگی‌ها، می‌توان تمام آن‌ها را با فراخوانی متد RegisterValidatorsFromAssemblyContaining، به صورت خودکار از یک اسمبلی خاص استخراج نمود و با طول عمر Transient، به سیستم معرفی کرد. در این حالت متد ConfigureServices به صورت زیر خلاصه می‌شود:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews().AddFluentValidation(
                fv => fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>()
            );
        }
در اینجا امکان استفاده‌ی از متد fv.RegisterValidatorsFromAssembly نیز برای معرفی اسمبلی خاصی مانند ()Assembly.GetExecutingAssembly نیز وجود دارد.


سازگاری اجرای خودکار FluentValidation با اعتبارسنج‌های استاندارد ASP.NET Core

به صورت پیش‌فرض، زمانیکه FluentValidation اجرا می‌شود، اگر اعتبارسنج دیگری نیز در سیستم تعریف شده باشد، اجرا خواهد شد. به این معنا که برای مثال می‌توان FluentValidation و DataAnnotations attributes و IValidatableObject‌ها را با هم ترکیب کرد.
اگر می‌خواهید این قابلیت را غیرفعال کنید و فقط سبب اجرای خودکار FluentValidationها شوید، نیاز است تنظیم زیر را انجام دهید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews().AddFluentValidation(
       fv =>
       {
           fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>();
           fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
       }
    );
}
نظرات مطالب
Portable Class Library چیست و چگونه از آن استفاده کنیم؟
زمانی که شما از Add as Link استفاده می‌کنید در واقع معایب زیر رو هم قبول کردید.
  • اولا مبحث Refactoring برای Add as Link کار نمی‌کنه. یعنی اگه یک فایل یا یک متد رو Rename کنید در بقیه فایل‌ها این Rename انجام نمیشه. یا باید دوباره عملیات Add Link رو انجام بدید یا باید از گزینه RunCustomTools استفاده کنید.
  • زمانی که یک فایل لینک شده باز کنید و اگر اون فایل در یک پروژه دیگه باز باشه؛ Visual Studio به شما چنین پیغامی میده "This document is opened by another project ".
  • زمانی که در حال استفاده از فایل‌های لینک شده هستید intellisense   به شما API هایی رو نمایش می‌ده که ممکنه توی اون Platform  قایل استفاده نباشند.
  • زمانی که قصد داشته باشید که یک Library بسازید که بعدا از اون دوباره استفاده کنید یک Portable Library خیلی بهتر به ما کمک می‌کنه تا فایل هایی که Add as Link شده باشند.
  •  Add as Link برای مدیریت و نگهداری در پروژه‌ها با حجم و تعداد Porgrammer بالا مناسب نیست.
  • و...
با تشکر از توضیحات دوست عزیز-سعید
نظرات مطالب
نحوه صحیح تولید Url در ASP.NET MVC
یک نکته‌ی تکمیلی:
لزوم استفاده از Tag Helper‌ها در Asp.net Core جهت تولید آدرس‌های صحیح
 با یک مثال بررسی میکنیم. ابتدا نگاهی داشته باشیم به Route Template
app.UseMvc(routes =>
{
   routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
حالا با وجود این الگوی مسیریابی می‌خواهیم لینکی تولید کنیم که با کلیک بر روی آن جزئیات کاربر را مشاهده کنیم:
 ایجاد لینک بدون استفاده از تگ هلپر
<a href="Home/Details/@student.Id">View Details</a>
ایجاد لینک با استفاده از تگ هلپر
<a asp-controller="Home" asp-action="Details" asp-route-id="@student.Id">View Details </a>
وقتی به کدهای HTML تولید شده نگاه کنید می‌بینید که تغییرات چندانی رخ نداده است:
 خروجی حاصل
<a href="Home/Details/1">View Details</a>
<a href="/Home/Details/1">View Details</a>
حال اگر کمی تغییر را در route template ایجاد کنیم:
app.UseMvc(routes =>
{
   routes.MapRoute("default", "uni/{controller=Home}/{action=Index}/{id?}");
});
و به آدرس localhost:56241/Uni مراجعه کنیم خواهید دید که خروجی‌های متفاوتی تولید شده است. برای حالت اول که از Tag Helpers استفاده نشده خواهیم داشت:
<a href="Home/Details/1">View Details</a>
و هنگام کلیک روی لینک تولید شده با خطای 404 مواجه خواهیم شد. ولی در حالت دوم که از Tag Helpers استفاده شده خواهیم داشت:
 <a href="/Uni/Home/Details/1">View Details</a>
و هدایت کاربر به اکشن متد Details به درستی انجام خواهد شد. بدین معنا که در آینده اگر لازم به ایجاد تغییراتی در الگوی مسیریابی باشد، این تغییرات بصورت اتوماتیک توسط Tag Helperها لحاظ خواهند شد و دیگر نیازی نیست که ما تمامی لینک‌های موجود را اصلاح کنیم.
مطالب
ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular
تمام برنامه‌های وب، از داده‌ها استفاده می‌کنند و امکان ذخیره سازی، به اشتراک گذاری و بازیابی آن‌ها حتی زمانیکه اتصال به شبکه برقرار نیست، بسیار حائز اهمیت است. به همین جهت مرورگرهای امروزی نیز به همراه قابلیت‌هایی هستند تا این امر را ساده‌تر کنند. این محل ذخیره سازی، درون مرورگر کاربر بوده و دسترسی به آن نیز بسیار سریع است. همچنین امکان دسترسی به آن در حالت آفلاین و بدون اتصال به شبکه نیز میسر است. البته باید دقت داشت که بسته به نوع ذخیره سازی اطلاعات محلی انتخاب شده، حداکثر 10 مگابایت بیشتر در اختیار برنامه قرار نمی‌گیرد. همچنین دسترسی این اطلاعات وابسته‌است به ماشین و وسیله‌ی مورد استفاده. برای مثال اگر کاربر از طریق سیستم و ماشین دیگری برنامه را مرور کند، دیگر دسترسی به اطلاعات محلی قبلی خود نخواهد داشت و یا اگر کاربر کش مرورگر را خالی کند، این اطلاعات نیز حذف می‌شوند.


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

Web Storage و یا Client-side storage در دو حالت کلی session storage و local storage قابل دسترسی است:
الف) session storage
در این حالت اطلاعات ذخیره شده‌ی در session storage، پس از بسته شدن مرورگر، به صورت خودکار حذف خواهند شد.

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

هر دو حالت فوق به صورت ایزوله ارائه می‌شوند؛ با محدودیت حجم 10 مگابایت (جمع حجم نهایی هر دو حالت با هم، محدود به 10 مگابایت است). به این معنا که برنامه‌های هر دومین، تنها به محل ذخیره سازی خاص همان دومین دسترسی خواهند داشت.
همچنین API دسترسی به آن‌ها synchronous است و کار کردن با آن‌ها ساده‌است.

البته Client-side storage به دو مورد فوق خلاصه نمی‌شود و شامل File Storage ،WebSQL ،IndexedDB و کوکی‌های مرورگر نیز هست.
- File Storage هنوز مراحل آزمایشی خودش را طی می‌کند و مناسب برنامه‌های دنیای واقعی نیست.
- WebSQL قرار بود بر اساس بانک اطلاعاتی معروف SQLite ارائه شود؛ اما W3C در سال 2010 این استاندارد را منسوخ شده اعلام کرد و با IndexedDB جایگزین شد. دسترسی به آن async است و می‌تواند موضوع بحثی مجزا باشد.
- کوکی‌های مرورگرها نیز یکی دیگر از روش‌های ذخیره سازی اطلاعات در مرورگرها هستند و تنها به ذخیره سازی حداکثر 4096 بایت اطلاعات محدود هستند. کوکی‌ها نیز همانند local storage پس از بسته شدن مرورگر باقی می‌مانند؛ اما برخلاف آن، دارای تاریخ انقضاء و همچنین قابلیت ارسال بین دومین‌ها را نیز دارا می‌باشند. اگر تاریخ انقضای یک کوکی تعیین نشود، همانند session storage، در پایان کار مرورگر و بسته شدن آن، حذف خواهد شد.


تهیه یک سرویس Angular برای کار با Web Storage

جهت کپسوله سازی نحوه‌ی کار با session storage و local storage می‌توان سرویسی را برای این‌کار تهیه کرد:
import { Injectable } from "@angular/core";

@Injectable()
export class BrowserStorageService {

  getSession(key: string): any {
    const data = window.sessionStorage.getItem(key);
    return JSON.parse(data);
  }

  setSession(key: string, value: any): void {
    const data = value === undefined ? null : JSON.stringify(value);
    window.sessionStorage.setItem(key, data);
  }

  removeSession(key: string): void {
    window.sessionStorage.removeItem(key);
  }

  removeAllSessions(): void {
    for (const key in window.sessionStorage) {
      if (window.sessionStorage.hasOwnProperty(key)) {
        this.removeSession(key);
      }
    }
  }

  getLocal(key: string): any {
    const data = window.localStorage.getItem(key);
    return JSON.parse(data);
  }

  setLocal(key: string, value: any): void {
    const data = value === undefined ? null : JSON.stringify(value);
    window.localStorage.setItem(key, data);
  }

  removeLocal(key: string): void {
    window.localStorage.removeItem(key);
  }

  removeAllLocals(): void {
    for (const key in window.localStorage) {
      if (window.localStorage.hasOwnProperty(key)) {
        this.removeLocal(key);
      }
    }
  }
}
دسترسی به local storage از طریق شیء window.localStorage انجام می‌شود و کار با آن در برنامه‌های Angular، نیاز به وابستگی خاص دیگری ندارد. این مورد برای کار با session storage از طریق شیء window.sessionStorage صورت می‌گیرد. هر دو حالت، دارای متدهای setItem برای ذخیره سازی اطلاعات، getItem برای دریافت اطلاعات، بر اساس کلیدی مشخص و removeItem برای حذف اطلاعات کلیدی معلوم، هستند.
در حالت setItem اطلاعاتی را که مرورگرها ذخیره می‌کنند باید رشته‌ای باشد. به همین جهت توسط متد JSON.stringify می‌توان یک شیء را تبدیل به رشته کرد و ذخیره نمود و در حالت getItem توسط متد JSON.parse، می‌توان این رشته را مجددا به همان شیء پیشین خود تبدیل کرد و بازگشت داد.


محل صحیح تعریف BrowserStorageService

همانطور که در مطلب «سازماندهی برنامه‌های Angular توسط ماژول‌ها» بررسی شد، محل صحیح تعریف این سرویس سراسری مشترک در بین کامپوننت‌ها و ماژول‌های برنامه، در CoreModule و پوشه‌ی src\app\core\browser-storage.service.ts است:
import { BrowserStorageService } from "./browser-storage.service";
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";

@NgModule({
  imports: [CommonModule, RouterModule],
  exports: [], // components that are used in app.component.ts will be listed here.
  declarations: [], // components that are used in app.component.ts will be listed here.
  providers: [BrowserStorageService] // singleton services of the whole app will be listed here.
})
export class CoreModule { };

و CoreModule نیز به AppModule اضافه می‌شود:
import { CoreModule } from "./core/core.module";

@NgModule({
  imports:      [
//...
    CoreModule,
//...
    RouterModule.forRoot(appRoutes)
  ],
//...
})
export class AppModule { }

بنابراین یکی دیگر از روش‌های به اشتراک گذاری اطلاعات در بین قسمت‌های مختلف برنامه، ذخیره سازی آن‌ها در session/local storage و سپس بازیابی آن‌ها بر اساس کلیدهای مشخص آن‌ها است.


مثالی از نحوه‌ی کاربرد BrowserStorageService

برای آزمایش سرویس تهیه شده، از کامپوننت و قالب ذیل استفاده خواهیم کرد. در اینجا سرویس BrowserStorageService به سازنده‌ی کلاس تزریق شده‌است و سپس دو حالت session storage و local storage مورد بررسی قرار گرفته‌اند:
import { BrowserStorageService } from "./../../core/browser-storage.service";
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-browser-storage-sample-test",
  templateUrl: "./browser-storage-sample-test.component.html",
  styleUrls: ["./browser-storage-sample-test.component.css"]
})
export class BrowserStorageSampleTestComponent implements OnInit {

  fromSessionStorage = "";
  fromLocalStorage = ""

  sessionStorageKey = "sessionStorageKey1";
  localStorageKey = "localStorageKey1"

  constructor(private browserStorage: BrowserStorageService) { }

  ngOnInit() {
  }

  sessionStorageSetItem() {
    this.browserStorage.setSession(this.sessionStorageKey, "Val1");
  }

  sessionStorageGetItem() {
    this.fromSessionStorage = this.browserStorage.getSession(this.sessionStorageKey);
  }

  localStorageSetItem() {
    this.browserStorage.setLocal(this.localStorageKey, { key1: "val1", key2: 2 });
  }

  localStorageGetItem() {
    this.fromLocalStorage = JSON.stringify(this.browserStorage.getLocal(this.localStorageKey));
  }
}
به همراه قالب:
<h1>Browser storage sample</h1>
<div class="panel">
  <button class="btn btn-primary" (click)="sessionStorageSetItem()" type="button">sessionStorage -> Set Item</button>
  <button class="btn btn-success" (click)="sessionStorageGetItem()" type="button">sessionStorage -> Get Item</button>
  <div class="alert alert-info" *ngIf="fromSessionStorage">
    {{fromSessionStorage}}
  </div>
</div>

<div class="panel">
  <button class="btn btn-warning" (click)="localStorageSetItem()" type="button">localStorage -> Set Item</button>
  <button class="btn btn-success" (click)="localStorageGetItem()" type="button">localStorage -> Get Item</button>
  <div class="alert alert-info" *ngIf="fromLocalStorage">
    {{fromLocalStorage}}
  </div>
</div>

در این حالت اگر برنامه را اجرا کنیم، یک چنین خروجی قابل مشاهده خواهد بود:


و اگر به برگه‌ی Application کنسول ابزارهای توسعه دهنده‌های مرورگرها نیز مراجعه کنیم، این مقادیر ثبت شده را در دو حالت استفاده‌ی از session storage و local storage، می‌توان مشاهده کرد:



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