مطالب
آزمایش ساده‌تر 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 را در خط فرمان اجرا کنیم تا آزمون‌های ما به ترتیب اجرا شوند:


فایل نهایی این آزمایش را در اینجا می‌توانید مشاهده می‌کنید.
مطالب
به روز رسانی ساده‌تر اجزاء ارتباطات در EF Code first به کمک GraphDiff
دو نوع حالت کلی کارکردن با EF وجود دارند: متصل و منقطع.
در حالت متصل مانند برنامه‌های متداول دسکتاپ، Context مورد استفاده در طول عمر صفحه‌ی جاری زنده نگه داشته می‌شود. در این حالت اگر شیءایی اضافه شود، حذف شود یا تغییر کند، توسط EF ردیابی شده و تنها با فراخوانی متد SaveChanges، تمام این تغییرات به صورت یکجا به بانک اطلاعاتی اعمال می‌شوند.
در حالت غیرمتصل مانند برنامه‌های وب، طول عمر Context در حد طول عمر یک درخواست است. پس از آن از بین خواهد رفت و دیگر فرصت ردیابی تغییرات سمت کاربر را نخواهد یافت. در این حالت به روز رسانی کلیه تغییرات انجام شده در خواص و همچنین ارتباطات اشیاء موجود، کاری مشکل و زمانبر خواهد بود.
برای حل این مشکل، کتابخانه‌ای به نام GraphDiff طراحی شده‌است که صرفا با فراخوانی متد UpdateGraph آن، به صورت خودکار، محاسبات تغییرات صورت گرفته در اشیاء منقطع و اعمال آن‌ها به بانک اطلاعاتی صورت خواهد گرفت. البته ذکر متد SaveChanges پس از آن نباید فراموش شود.


اصطلاحات بکار رفته در GraphDiff

برای کار با GraphDiff نیاز است با یک سری اصطلاح آشنا بود:

Aggregate root
گرافی است از اشیاء به هم وابسته که مرجع تغییرات داده‌ها به شمار می‌رود. برای مثال یک سفارش و آیتم‌های آن‌را درنظر بگیرید. بارگذاری آیتم‌های سفارش، بدون سفارش معنایی ندارند. بنابراین در اینجا سفارش aggregate root است.

AssociatedCollection/AssociatedEntity
حالت‌های Associated به GraphDiff اعلام می‌کنند که اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root نباید به روز رسانی شوند. در این حالت تنها ارجاعات به روز رسانی خواهند شد.
اگر خاصیت راهبری از نوع ICollection است، حالت AssociatedCollection و اگر صرفا یک شیء ساده است، از AssociatedEntity استفاده خواهد شد.

OwnedCollection/OwnedEntity
حالت‌های Owned به GraphDiff اعلام می‌کنند که جزئیات و همچنین ارجاعات اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root باید به روز رسانی شوند.


دریافت و نصب GraphDiff

برای نصب خودکار کتابخانه‌ی GraphDiff می‌توان از دستور نیوگت ذیل استفاده کرد:
 PM> Install-Package RefactorThis.GraphDiff


بررسی GraphDiff در طی یک مثال

مدل‌های برنامه آزمایشی، از سه کلاس ذیل که روابط many-to-many و one-to-many با یکدیگر دارند، تشکیل شده‌است:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace GraphDiffTests.Models
{
    public class BlogPost
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public virtual ICollection<Tag> Tags { set; get; } // many-to-many

        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int UserId { get; set; }

        public BlogPost()
        {
            Tags = new List<Tag>();
        }
    }

    public class Tag
    {
        public int Id { set; get; }

        [StringLength(maximumLength: 450), Required]
        public string Name { set; get; }

        public virtual ICollection<BlogPost> BlogPosts { set; get; } // many-to-many

        public Tag()
        {
            BlogPosts = new List<BlogPost>();
        }
    }

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; } // one-to-many
    }
}
- یک مطلب می‌تواند چندین برچسب داشته باشد و هر برچسب می‌تواند به چندین مطلب انتساب داده شود.
- هر کاربر می‌تواند چندین مطلب ارسال کند.

در این حالت، Context برنامه چنین شکلی را خواهد یافت:
using System;
using System.Data.Entity;
using GraphDiffTests.Models;

namespace GraphDiffTests.Config
{
    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<BlogPost> BlogPosts { get; set; }
        public DbSet<Tag> Tags { get; set; }


        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
}
به همراه تنظیمات به روز رسانی ساختار بانک اطلاعاتی به صورت خودکار:
using System.Data.Entity.Migrations;
using System.Linq;
using GraphDiffTests.Models;

namespace GraphDiffTests.Config
{
    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            if(context.Users.Any())
                return;

            var user1 = new User {Name = "User 1"};
            context.Users.Add(user1);

            var tag1 = new Tag { Name = "Tag1" };
            context.Tags.Add(tag1);

            var post1 = new BlogPost { Title = "Title...1", Content = "Content...1", User = user1};
            context.BlogPosts.Add(post1);

            post1.Tags.Add(tag1);

            base.Seed(context);
        }
    }
}
در متد Seed آن یک سری اطلاعات ابتدایی ثبت شده‌اند؛ یک کاربر، یک برچسب و یک مطلب.




در این تصاویر به Id هر کدام از رکوردها دقت کنید. از آن‌ها در ادامه استفاده خواهیم کرد.
در اینجا نمونه‌ای از نحوه‌ی استفاده از GraphDiff را جهت به روز رسانی یک Aggregate root ملاحظه می‌کنید:
            using (var context = new MyContext())
            {
                var user1 = new User { Id = 1, Name = "User 1_1_1" };
                var post1 = new BlogPost { Id = 1, Title = "Title...1_1", Content = "Body...1_1",
                    User = user1, UserId = user1.Id };
                var tags = new List<Tag>
                {
                    new Tag {Id = 1, Name = "Tag1_1"},
                    new Tag {Id=12, Name = "Tag2_1"},
                    new Tag {Name = "Tag3"},
                    new Tag {Name = "Tag4"},
                };
                tags.ForEach(tag => post1.Tags.Add(tag));

                context.UpdateGraph(post1, map => map
                    .OwnedEntity(p => p.User)
                    .OwnedCollection(p => p.Tags)
                    );

                context.SaveChanges();
            }
پارامتر اول UpdateGraph، گرافی از اشیاء است که قرار است به روز رسانی شوند.
پارامتر دوم آن، همان مباحث Owned و Associated بحث شده در ابتدای مطلب را مشخص می‌کنند. در اینجا چون می‌خواهیم هم برچسب‌ها و هم اطلاعات کاربر مطلب اول به روز شوند، نوع رابطه را Owned تعریف کرده‌ایم.
در حین کار با متد UpdateGraph، ذکر Idهای اشیاء منقطع از Context بسیار مهم هستند. اگر دستورات فوق را اجرا کنیم به خروجی ذیل خواهیم رسید:




- همانطور که مشخص است، چون id کاربر ذکر شده و همچنین این Id در post1 نیز درج گردیده است، صرفا نام او ویرایش گردیده است. اگر یکی از موارد ذکر شده رعایت نشوند، ابتدا کاربر جدیدی ثبت شده و سپس رابطه‌ی مطلب و کاربر به روز رسانی خواهد شد (userId آن به userId آخرین کاربر ثبت شده تنظیم می‌شود).
- در حین ثبت برچسب‌ها، چون Id=1 از پیش در بانک اطلاعاتی موجود بوده، تنها نام آن ویرایش شده‌است. در سایر موارد، برچسب‌های تعریف شده صرفا اضافه شده‌اند (چون Id مشخصی ندارند یا Id=12 در بانک اطلاعاتی وجود خارجی ندارد).
- چون Id مطلب مشخص شده‌است، فیلدهای عنوان و محتوای آن نیز به صورت خودکار ویرایش شده‌اند.

و ... تمام این کارها صرفا با فراخوانی متدهای UpdateGraph و سپس SaveChanges رخ داده‌است.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
GraphDiffTests.zip
مطالب
ایجاد یک DbContext مشترک بین entityهای پروژه‌های متفاوت
فر ض کنید پروژه بزرگی دارید که هر قسمت را به یک برنامه نویس می‌سپارید تا آن قسمت را در پروژه مجزایی طراحی و برنامه نویسی کند. هر برنامه نویس Entity‌های خاص خود را در لایه‌های مربوط به پروژه خود تعریف می‌کند و از آنها استفاده می‌کند. حال یکی از برنامه نویس‌ها می‌خواهد از Entity های پروژه دیگر استفاده کند. در این صورت اگر از دو Context شیء‌ایی را بسازد و آنها را با یکدیگر Join  بزند، خطایی مربوط به تعلق داشتن دو  Entity به دو Context متفاوت را می‌گیرد.

در پروژه‌های کوچک، کل تیم بر روی ماژول‌های مختلف یک پروژه کار می‌کنند و یک DbContext مشترک دارند. اما راه حل این مشکل در پروژه‌های بزرگ چیست؟ 
یکی از راه‌های پیشنهادی، استفاده از یک کلاس DbContextBase است که همه پروژه‌ها بایستی Context خود را از این کلاس به ارث ببرند که در این صورت باز هم مشکل ساخت چند DbContext وجود خواهد داشت که فقط می‌توان از Entity‌های موجود در DbContextBase و DbContext پروژه جاری استفاده کرد. اما در شرکت‌های بزرگ که پروژه‌هایی مانندERP دارند، روش دیگری استفاده می‌شود که در ادامه خواهیم دید.
روش مورد استفاده به این صورت است که در زمان اجرا یک DbContext برای همه Entity‌های پروژه‌های مختلف ساخته می‌شود. اجازه بدهید همراه با مثال، این پروژه را پیش برویم. فرض کنید دو تیم برنامه نویسی داریم که هر کدام بر روی پروژه‌های مجزای SampleProject1 و SampleProject2 کار میکنند که Entity‌های هر کدام در لایه‌های Common قرار گرفته‌اند.

در SampleProject1 مدل Product را داریم:

public partial class Product : Entity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Nullable<byte> ProductTypeId { get; set; }
    }
و در SampleProject2، مدل ProductType را داریم که هر دو Entity از کلاس Entity ارث بری می‌کند: 
 public partial class ProductType : Entity
    {
        public byte Id { get; set; }
        public string Name { get; set; }
    }
همه پروژه‌ها را در پروژه‌ی SampleProject1.Console، به عنوان رفرنس اضافه می‌کنیم؛ بجز SampleProject2.Console و Output path همه پروژه‌ها را به یک پوشه مشترک هدایت می‌کنیم. در ادامه برای بدست آوردن Entity‌ها از کد زیر استفاده می‌کنیم:
            List<Assembly> allAssemblies = new List<Assembly>();
            string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

            foreach (string dll in Directory.GetFiles(path, "*.Common.dll"))
                allAssemblies.Add(Assembly.LoadFile(dll));

            var type = typeof(Entity);
      
            List<Type> types = allAssemblies
             .SelectMany(s => s.GetTypes())
             .Where(p => type.IsAssignableFrom(p)).ToList();

            List<string> entities = new List<string>();
            foreach (var item in types)
            {
                entities.Add(item.Name);
            }

            types.Add(typeof(Entity));
و سپس برای Generate کردن کلاس DbContext از کلاس زیر استفاده می‌کنیم:
public class ContextGenerator
    {
        public void Generate(List<string> entities, params Type[] types)
        {
            StringBuilder code = new StringBuilder();

            code.AppendLine(@"
           using System.Data.Entity;
           using System.Data.Entity.Core.EntityClient;
           using SampleProject1.Common.Models;
           using SampleProject1.Common.Models.Mapping;
           using SampleProject2.Common.Models;
           using SampleProject2.Common.Models.Mapping;

           namespace DbContextGenerator
           {
                public partial class TestContext : DbContext
                {
                    static TestContext()
                    {
                        Database.SetInitializer<TestContext>(null);
                    }

                    public TestContext()
                        : base(""Data Source=.;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True"")
                    {
                        }
                ");

            var pluralizeHelper = new PluralizeHelper();

            foreach (var entity in entities)
            {
                code.AppendLine($@"public DbSet<{entity}> {pluralizeHelper.Pluralize(entity)} {{ get; set; }}");
            }

            code.AppendLine(@"protected override void OnModelCreating(DbModelBuilder modelBuilder)");
            code.AppendLine(@"{");

            foreach (var entity in entities)
            {
                code.AppendLine($@"modelBuilder.Configurations.Add(new {entity}Map());");
            }
            code.AppendLine(@"}");
            code.AppendLine(@"}");
            code.AppendLine(@"}");
           
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters parameters = new CompilerParameters();

            parameters.ReferencedAssemblies.Add("System.Drawing.dll");
            parameters.ReferencedAssemblies.Add("System.Data.dll");
            parameters.ReferencedAssemblies.Add("System.Data.Entity.dll");
            parameters.ReferencedAssemblies.Add("System.ComponentModel.dll");

            foreach (var type in types)
            {
                parameters.ReferencedAssemblies.Add(type.Assembly.Location);
            }

            parameters.ReferencedAssemblies.Add(typeof(DbSet).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(DbContext).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(IQueryable).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(IQueryable<>).Assembly.Location);
            parameters.ReferencedAssemblies.Add(typeof(System.ComponentModel.IListSource).Assembly.Location);

            parameters.GenerateExecutable = false;
            parameters.GenerateInMemory = false;
            parameters.OutputAssembly = "ProjectContext.dll";

            CompilerResults results = provider.CompileAssemblyFromSource(parameters, code.ToString());

            if (results.Errors.HasErrors)
            {
                StringBuilder sb = new StringBuilder();

                foreach (CompilerError error in results.Errors)
                {
                    sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText));
                }

                throw new InvalidOperationException(sb.ToString());
            }
        }

    }
و نحوه فراخوانی آن:
 new ContextGenerator().Generate(entities, types.ToArray()); // generate dbContext
همانطور که مشاهده می‌کنید، برای تولید کد، از کلاس CSharpCodeProvider استفاده میکنیم که نتیجه اجرای کد بالا، ساخت DLLی به نام ProjectContext.dll است. با مشاهده DLL ساخته شده توسط نرم افزار ILSpy، کد جنریت شده به صورت زیر خواهد بود: 

حال برای استفاده از Context تولید شده، به صورت زیر شیءایی را ساخته:

 static DbContext _dbContext=null;
        public static DbContext GetDbContextInstance()
        {
            if (_dbContext == null)
            {
                string path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
                var dllversionAssm = Assembly.LoadFile(path + "\\ProjectContext.dll");
                Type type = dllversionAssm.GetType("DbContextGenerator.TestContext");
                _dbContext = (DbContext)Activator.CreateInstance(type);
            }
            return _dbContext;
        }

و سپس برای ساخت DbSet از هر Entity به کد زیر نیاز خواهیم داشت:

public static System.Data.Entity.DbSet<T> Get<T>() where T : class
        {
            var set = GetDbContextInstance().Set<T>();
            return set;
        }

هم اکنون می‌توان رکوردهای Entity‌ها را واکشی کرده و یا آن‌ها را با یکدیگر Join بزنیم:

            var products = Get<Product>().ToList();

            var productTypes = Get<ProductType>().ToList();


            var query = from p in Get<Product>()
                        join pt in Get<ProductType>() on p.ProductTypeId equals pt.Id
                        select new
                        {
                            Id = p.Id,
                            Name = p.Name,
                            ProductType = pt.Name

                        };

            var JoinResult = query.ToList();

و نتیجه واکشی ها 


کد کامل این پروژه  

مطالب
بررسی خطای cycles or multiple cascade paths و یا cyclical reference در EF Code first
ابتدا مثال کامل این قسمت را با شرح زیر درنظر بگیرید؛ در اینجا هر کاربر، یک کارتابل می‌تواند داشته باشد (رابطه یک به صفر یا یک) و تعدادی سند منتسب به او (رابطه یک به چند).  همچنین روابط بین کارتابل و اسناد نیز چند به چند است:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.ModelConfiguration;

namespace EF_General.Models.Ex18
{
    public class UserProfile
    {
        public int UserProfileId { set; get; }
        public string UserName { set; get; }

        [ForeignKey("CartableId")]
        public virtual Cartable Cartable { set; get; } // one-to-zero-or-one
        public int? CartableId { set; get; }

        public virtual ICollection<Doc> Docs { set; get; } // one-to-many
    }

    public class Doc
    {
        public int DocId { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }

        [ForeignKey("UserProfileId")]
        public virtual UserProfile UserProfile { set; get; }
        public int UserProfileId { set; get; }

        public virtual ICollection<Cartable> Cartables { set; get; } // many-to-many
    }

    public class Cartable
    {
        public int CartableId { set; get; }

        [ForeignKey("UserProfileId")]
        public virtual UserProfile UserProfile { set; get; }
        public int UserProfileId { set; get; }

        public virtual ICollection<Doc> Docs { set; get; } // many-to-many
    }

    public class UserProfileMap : EntityTypeConfiguration<UserProfile>
    {
        public UserProfileMap()
        {
            this.HasOptional(x => x.Cartable)
                .WithRequired(x => x.UserProfile)
                .WillCascadeOnDelete();
        }
    }

    public class MyContext : DbContext
    {
        public DbSet<UserProfile> UserProfiles { get; set; }
        public DbSet<Doc> Docs { get; set; }
        public DbSet<Cartable> Cartables { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new UserProfileMap());
            base.OnModelCreating(modelBuilder);
        }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            using (var context = new MyContext())
            {
                var user = context.UserProfiles.Find(1);
                if (user != null)
                    Console.WriteLine(user.UserName);
            }
        }
    }
}
اگر این مثال را اجرا کنیم، به خطای ذیل برخواهیم خورد:
Introducing FOREIGN KEY constraint 'FK_DocCartables_Cartables_Cartable_CartableId' 
on table 'DocCartables' may cause cycles or multiple cascade paths. Specify 
ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.
علت اینجا است که EF به صورت پیش فرض ویژگی cascade delete را برای حالات many-to-many و یا کلیدهای خارجی غیرنال پذیر اعمال می‌کند.
این دو مورد در کلاس‌های Doc و Cartable با هم وجود دارند که در نهایت سبب بروز circular cascade delete (حذف آبشاری حلقوی) می‌شوند و بیشتر مشکل SQL Server است تا EF؛ از این لحاظ که SQL Server در این حالت نمی‌تواند در مورد نحوه حذف خودکار رکوردهای وابسته درست تصمیم‌گیری و عمل کند. برای رفع این مشکل تنها کافی است کلید خارجی تعریف شده در دو کلاس Doc و کارتابل را nullable تعریف کرد تا cascade delete اضافی پیش فرض را لغو کند:
public int? UserProfileId { set; get; }
راه دیگر، استفاده از تنظیمات Fluent و تنظیم WillCascadeOnDelete به false است که به صورت پیش فرض در حالات ذکر شده (روابط چند به چند و یا کلید خارجی غیرنال پذیر)، true است.

شبیه به همین خطا نیز زمانی رخ خواهد داد که در یک کلاس حداقل دو کلید خارجی تعریف شده باشند:
The referential relationship will result in a cyclical reference that is not allowed. [ Constraint name =  ]
در اینجا نیز با نال پذیر تعریف کردن این کلیدهای خارجی، خطای cyclical reference برطرف خواهد شد.
مطالب
#Defensive Code in C - قسمت اول

Defensive Coding به معنی است که شما با انجام یکسری کار‌ها و در نظر گرفتن یکسری زیر ساخت‌ها در توسعه‌ی نرم افزار خود، به اهداف ذیل دست پیدا کنید:

1. Quality (کیفیت)

2. Comprehensible (جامعیت)

3. Predictable  (قابلیت پیش بینی)

دستیابی به هر کدام از این اهداف و روش‌های اعمال آنها بر روی یک پروژه‌ی نرم افزاری، در ادامه بحث خواهند شد. 

1. Clean Code

یکی از اهداف Defensive Coding که در ابتدای مقاله بحث شد جامعیت یا Comprehension بود. برای رسید به این هدف از مفهومی به نام Clean Code  استفاده می‌شود. Clean Code علاوه بر این مسئله، در پی ساده کردن ساختار بندی پشتیبانی و کاهش باگ‌های نرم افزار نیز هست. ویژگی‌های Clean Code در بالا با  توجه به شکل ذیل تشریح می‌شوند: 

· Easy to read

یک کد Clean  قابلیت خوانایی بالایی دارد. بسیاری از برنامه نویسان در سطوح مختلف با اهمیت این مسئله در توسعه نرم افزار آشنایی دارند. ولی بسیاری از همین برنامه نویسان این اصول را رعایت نمی‌کنند و سعی نمی‌کنند با اصول پیاده سازی آن در نرم افزارآشنا شوند.

اگر قابلیت خوانایی یک کد بالا باشد:

§ شما می‌توانید Pattern ‌های موجود در کد خود را که می‌توانید به عنوان نامزدهایی جهت Refactoring  هستند، تشخیص دهید.

§ برنامه نویسان دیگر به راحتی قصد و اهداف ( intent ) شما را از نوشتن یک کد خاص درک خواهند کرد و در طول زمان با خطا‌های زیادی روبرو نمی‌شوند.

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

· Clear intent

یک کد Clear دارای اهداف روشن و قابل فهمی می‌باشد.

· Simple

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

· Minimal

کد باید به گونه‌ای باشد که تنها یک چیز را انجام داده و آن را به درستی انجام دهد. همچنین وابستگی بین اجزای کد باید در کمترین حد ممکن باشند.

· Thoughtful

یک کد Clean  کدی است که ساختار آن متفکرانه طراحی شده باشد. از نحوه‌ی طراحی یک کلاس گرفته تا layering و Tiering پروژه باید کاملا هوشمندانه و با توجه به پارامتر‌های موجود باشند. همچنین خطا‌های خطرناک و استثناء‌ها باید کاملا هندل شوند. 

همه‌ی ما با دیدن کد بالا سریعا مفهوم اسپاگتی کد به ذهنمان خطور می‌کند. تغییر، توسعه و پشتیبانی نرم افزارهایی که کد آنها به این صورت نوشته شده است، بسیار سخت و پر هزینه می‌باشد. در این حالت تغییر هر یک از اجزاء ممکن است بر سایر قسمت‌های دیگر تاثیرات مختلفی داشته باشد. راه کاری که در این حالت ارائه می‌شود، Refactoring می‌باشد. در این روش کد را به کلاس‌ها و متدهایی بر حسب عملکرد تقسیم خواهیم کرد. در نهایت کد تولید شده دارای کمترین تاثیر بر سایر قسمت‌ها خواهد بود. توجه داشته باشید که با انجام این کار، قدمی به سوی SOC یا Separation Of Concern برداشته‌اید.

1. Testable Code & Unit Test

یکی دیگر از اهداف Defensive Coding افزایش کیفیت یا Quality می‌باشد که برای رسیدن به این هدف از مفهوم Testable Code & Unit Test استفاده می‌شود. بسیاری از ویژگی‌های Testable Code و Clean Code با هم مشابه می‌باشند. برای مثال Refactor کردن هر متد به متد‌های کوچکتر، تست آن را ساده‌تر خواهند کرد. در نتیجه نوشتن کد‌های Testable ، با نوشتن کد‌های clean شروع می‌شود.

در این قسمت اشاره‌ای به Unit Test شده است؛ اما این مفهوم می‌تواند به یک مفهوم گسترده‌تر به نام  Automated Code testing، تعمیم داده شود. به این دلیل که تست فقط به Unit Testing محدود نمی‌شود و می‌تواند شامل سایر انواع تست‌ها مانند  integration test نیز باشد.

برای مثال شکل ذیل را در نظر بگیرید. در انتهای این سناریو یک Page جدید اضافه شده است. خوب؛ برای تست کد اضافه شده، مجبورید برنامه را اجرا کنید، login کنید، داده‌های مورد نظر را در فرم وارد کرده و در نهایت شرایط لازم را جهت تست، فراهم کنید تا بتوانید کد جدید را تست کنید. در این بین با خطایی مواجه می‌شوید. پس برنامه را متوقف می‌کنید و تغییرات لازم را اعمال می‌کنید. حال فرض کنید این خطا به این زودی‌ها رفع نشود. در این حالت باید فرآیند بالا را چندین و چند بار انجام دهید. نتیجه اینکه این روش بسیار زمان بر و پر هزینه خواهد بود. البته میزان هزینه و زمان رابطه‌ی نزدیکی با وسعت تغییرات دارند. برای رفع مسائلی از این دست مایکروسافت زیرساختی به نام MS Test ارائه داده است که می‌توان با آن سناریوهای تست متفاوتی را پیاده سازی و اجرا نمود. متاسفانه این مسئله در بسیار از جوامع توسعه نرم افزار رعایت نمی‌شود و در بسیاری از این جوامع، نیروی انسانی، این فرآیند و فرآیندهایی از این دست را انجام می‌دهند. درحالیکه چنین فرآیندهایی به راحتی توسط ابزارهای ارائه شده‌ی توسط شرکت‌های مختلف قابل مدیریت است.

 


1. Predictability

یکی دیگر از اهداف Defensive Coding، قابلیت پیش بینی یا Predictability می‌باشد. فرآیند تشخیص و پیش بینی خطا‌ها را Predictability می‌گویند. با درنظر گرفتن امکان وقوع خطاهای مختلف و تصمیم گرفتن در مورد اینکه در هنگام رخ دادن این خطا باید چه کاری صورت بگیرد، می‌توان در رسیدن به این هدف قدم بزرگی برداشت. 

برای رسیدن به این هدف باید اصل Trust but Verify را دنبال کنیم. برای مثال این اصل به ما می‌گوید که در هنگام تعریف متد‌های public باید یکسری موارد را در نظر بگیریم. یک متد باید از یکسری قرارداد‌ها پیروی کند. یک متد قرارداد می‌کند که یکسری پارامتر‌ها را با یک data type خاص به عنوان ورودی دریافت کند. قرارداد می‌کند که یک مقدار خاص با یک data type خاص را به عنوان نوع بازگشتی بازگرداند یا اینکه هیچ مقداری را باز نگرداند و در نهایت یک متد متعهد می‌شود که یکسری Exception ‌تعریف شده و پیش بینی شده را صادر کند. اما برای اینکه مطمئن شویم یک application واقعا قابل پیش بینی است و این اصل را به درستی پیاده سازی کرده است، اعتماد می‌کنیم اما Verify را هم انجام می‌دهیم. برای verify کردن باید پارامترها، دیتا‌های متغیر، مقادیر بازگشتی و استثناء‌ها به گونه‌ای بررسی شوند که مطمئن شویم انتظارت ما را برآورده کرده‌اند. 

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


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


چگونه می‌توان رخ‌دادهایی از نوع Observable را ایجاد کرد؟

کلاس Subject پاسخی است به این پرسش. Subjectها Observableهایی هستند که می‌توانند چندین مشترک داشته باشند و رخ‌دادهایی را به مشترکین خود صادر کنند. برای کار با آن‌ها باید یک private Subject را در سرویس خود ایجاد کرد و سپس جریان منتقل شده‌ی توسط آن‌را توسط یک public Observable در اختیار مصرف کنندگان قرار داد. با فراخوانی متد next یک Subject، رخ‌دادی به مشترکین آن منتقل می‌شود.
import { Subject } from “rxjs/Subject”;

public countdown: number = 0;

private countdownEndSource = new Subject<void>();
public countdownEnd$ = this.countdownEndSource.asObservable();
مرسوم است نام Observableهایی را که قرار است رخ‌دادی را صادر کنند به $ ختم می‌کنند.
استفاده کنندگان نیز مشترک این $countdownEnd شده و هر بار که در طرف سرویس، متد next آن فراخوانی می‌شود، از به روز رسانی آن مطلع خواهند شد.


چرا مستقیما از مقدار countdown استفاده نکنیم؟

در قسمتی از سرویس فوق که ملاحظه می‌کنید، می‌توان مقدار countdown را مستقیما نیز در کامپوننت‌ها مورد استفاده قرار داد. اما این روش بهینه نیست. از این جهت که Angular باید مدام تغییرات این خاصیت را رصد کند و به آن واکنش نشان دهد. آیا بهتر نیست ما به Angular اعلام کنیم که مقدار آن تغییر کرده‌است و اکنون بهتر است View را به روز رسانی کنی؟ با ارائه‌ی مقادیر جدیدی توسط یک Observable، اکنون Angular صرفا به تغییرات آن واکنش نشان خواهد داد و دیگری نیاز به بررسی مداوم تغییرات مقدار countdown ندارد.


یک مشکل! Subject تعریف شده، مقادیر را تنها در زمان فراخوانی متد next ارائه می‌دهد و نه به صورت دیگری.

پیشتر با دسترسی مستقیم به خاصیت countdown، همواره به مقادیر آن هم دسترسی داشتیم. اما با استفاده از یک Subject، تنها زمانیکه متد next آن فراخوانی شود می‌توان به این مقدار دسترسی یافت. برای رفع این مشکل یک Subject ویژه به نام BehaviorSubject طراحی شده‌است که به محض مشترک شدن به آن، اولین و یا آخرین مقدار آن‌را می‌توان دریافت کرد.


تفاوت Subject با BehaviorSubject

BehaviorSubject مانند یک Subject است؛ با این تفاوت که همواره از وضعیت خود آگاه می‌باشد. یک BehaviorSubject:
- همواره دارای مقداری است. حتی در زمان وهله سازی، باید مقدار اولیه‌ای را برای آن مشخص کرد.
- در زمان اشتراک به آن، می‌توان آخرین مقدار موجود در آن را که ممکن است اولین مقدار آن نیز باشد، دریافت کرد.
- همواره می‌توان مقدار آن‌را توسط متد getValue بدست آورد.

و مهم‌ترین مزیت آن نسبت به Subject، همان مورد دوم است. اگر مشترک یک Subject شویم، تا متد next آن فراخوانی نشود، مقداری را دریافت نمی‌کنیم. اما همان لحظه که مشترک BehaviorSubject می‌شویم، آخرین مقدار موجود در آن‌را دریافت خواهیم کرد.
برای مثال فرض کنید کامپوننتی را دارید که به خاصیت isLoggedIn از نوع Observable یک Subject گوش فرا می‌دهد. اما اشتراک آن پس از فراخوانی متد next در این سرویس بوده‌است. از این رو این کامپوننت هیچگاه متوجه تغییر و یا مقدار نهایی isLoggedIn نخواهد شد. به همین جهت است که به BehaviorSubject نیاز داریم. در این بین مهم نیست که چه زمانی مشترک آن می‌شویم؛ همواره در زمان اشتراک، آخرین و یا اولین مقدار موجود در آن‌را دریافت خواهیم کرد.


یک مثال: بررسی عملکرد BehaviorSubject

در ادامه یک ماژول را به همراه 4 کامپوننت و یک سرویس سفارشی ایجاد می‌کنیم:
ng g m ServiceComponentCommunication -m app.module --routing
ng g c ServiceComponentCommunication/First
ng g c ServiceComponentCommunication/Second
ng g c ServiceComponentCommunication/Third
ng g c ServiceComponentCommunication/Final
ng g s ServiceComponentCommunication/Sample


هدف این است که سه کامپوننت اول، دوم و سوم را در کامپوننت final، همانند تصویر فوق نمایش دهیم.
در این بین یک سرویس انتشار اطلاعات نیز طراحی شده‌است:
import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs/BehaviorSubject";

@Injectable()
export class SampleService {

  private msgSource = new BehaviorSubject<string>("default service value");

  telecast$ = this.msgSource.asObservable();

  constructor() { }

  editMsg(newMsg: string) {
    this.msgSource.next(newMsg);
  }

}
کار این سرویس ارائه یک پیام از نوع BehaviorSubject از طریق خاصیت عمومی $telecast آن است که به صورت Observable در معرض دید کامپوننت‌های مشترک به آن قرار خواهد گرفت. هدف این است که کامپوننت‌ها مدام تغییرات msg را بررسی نکنند و فقط به آخرین تغییر صادر شده‌ی توسط کامپوننت که از طریق فراخوانی متد next در متد editMsg صورت می‌گیرد، واکنش نشان دهند.

در کامپوننت اول، نحوه‌ی اشتراک به این سرویس را مشاهده می‌کنید:
import { SampleService } from "./../sample.service";
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

@Component({
  selector: "app-first",
  templateUrl: "./first.component.html",
  styleUrls: ["./first.component.css"]
})
export class FirstComponent implements OnInit, OnDestroy {

  editedMsg: string;
  sampleSubscription: Subscription;

  constructor(private sampleService: SampleService) { }

  ngOnInit() {
    this.sampleSubscription = this.sampleService.telecast$.subscribe(message => {
      this.editedMsg = message;
    });
  }

  editMsg() {
    this.sampleService.editMsg(this.editedMsg);
  }

  ngOnDestroy() {
    this.sampleSubscription.unsubscribe();
  }
}
کار اشتراک در این کامپوننت در متد ngOnInit انجام شده‌است. بسیار مهم است جهت عدم بروز نشتی حافظه، در متد ngOnDestroy کار unsubscribe بر روی این اشتراک نیز صورت گیرد.
در اینجا هر زمانیکه متد next در سرویس فراخوانی شود، this.editedMsg مقدار جدیدی را دریافت می‌کند.
با این قالب:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">First Component</h2>
  </div>
  <div class="panel-body">
    <p> {{editedMsg}}</p>
    <input class="form-control" type="text" [(ngModel)]="editedMsg">
    <button (click)="editMsg()" class="btn btn-primary">Change</button>
  </div>
</div>


اما اگر به تصویر دقت کنید، this.editedMsg هم اکنون دارای مقدار است (در اولین بار اجرای این کامپوننت). علت آن به داشتن مقدار اولیه‌ای در BehaviorSubject تعریف شده بر می‌گردد که در اولین بار اشتراک به آن، در اختیار مشترک قرار خواهد گرفت. این مورد، مهم‌ترین تفاوت BehaviorSubject با Subject است.
در این کامپوننت اگر کاربر مقداری را در textbox وارد کند و سپس بر روی دکمه‌ی Change کلیک نماید، این تغییر از طریق سرویس، به تمام مشترکین آن صادر خواهد شد.

کامپوننت دوم نیز مانند کامپوننت اول است، فقط یک textbox ورود اطلاعات را به همراه ندارد.


همانطور که ملاحظه می‌کنید، این کامپوننت نیز دارای مقدار اولیه‌ی BehaviorSubject است.

کامپوننت سوم، اندکی متفاوت است:
import { SampleService } from "./../sample.service";
import { Component, OnInit, OnDestroy } from "@angular/core";
import { Subscription } from "rxjs/Subscription";

@Component({
  selector: "app-third",
  templateUrl: "./third.component.html",
  styleUrls: ["./third.component.css"]
})
export class ThirdComponent implements OnInit, OnDestroy {

  message: string;
  sampleSubscription: Subscription;

  constructor(private sampleService: SampleService) { }

  ngOnInit() {
  }

  subscribe() {
    this.sampleSubscription = this.sampleService.telecast$.subscribe(message => {
      this.message = message;
    });
  }

  ngOnDestroy() {
    if (this.sampleSubscription) {
      this.sampleSubscription.unsubscribe();
    }
  }
}
در اینجا کار اشتراک در متد subscribe فراخوانی شده‌ی توسط قالب آن صورت می‌گیرد:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">Third Component</h2>
  </div>
  <div class="panel-body">
    <p>{{message}}</p>
    <button (click)="subscribe()" class="btn btn-success">Subscribe</button>
  </div>
</div>


و چون این متد پس از ngOnInit قرار است توسط کاربر فراخوانی شود، مقدار message این کامپوننت هنوز خالی است.
اکنون اگر بر روی دکمه‌ی Subscribe آن کلیک کنیم، بلافاصله در لحظه‌ی اشتراک، اولین/آخرین مقدار موجود در BehaviorSubject را دریافت خواهیم کرد:


کامپوننت Final نیز تمام کامپوننت‌ها را در صفحه نمایش می‌دهد:
<div class="row">
  <div class="col-md-4">
    <app-first></app-first>
  </div>
  <div class="col-md-4">
    <app-second></app-second>
  </div>
  <div class="col-md-4">
    <app-third></app-third>
  </div>
</div>

و اگر در textbox کامپوننت اول، مقدار Test را وارد کنیم و سپس بر روی دکمه‌ی Change آن کلیک نمائیم، این مقدار به تمام کامپوننت‌های مشترک به BehaviorSubject سرویس برنامه، منتشر خواهد شد:


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

برای شناسایی ایرادات در کد و بهبود کیفیت کدها می‌توانید از ابزارهای دسته‌ی lint استفاده کنید که تعدادی از معروفترین این ابزارها (jslint ,cpplint ,eslint ,nodelint و ...) هستند.
برای نصب چنین ابزاری مثل lint می‌توانید به شکل زیر آن را نصب کنید:
npm install -g eslint
فلگ g در بالا به معنای global بوده و باعث می‌شود که eslint نصب شده، بر روی همه پروژه‌های node.js اجرا شود.
برای اولین بار نیاز است که آماده سازی ابتدایی برای این کتابخانه صورت بگیرد که با دستور زیر قابل انجام است:
eslint --init
سپس تعدادی سوال شخصی از شما پرسیده می‌شود که باید پاسخ داده شود که نمونه‌ای از آن‌را در زیر می‌بینید:
? How would you like to configure ESLint? Answer questions about your style
? Are you using ECMAScript 6 features? Yes
? Are you using ES6 modules? Yes
? Where will your code run? Browser, Node
? Do you use CommonJS? No
? Do you use JSX? No
? What style of indentation do you use? Spaces
? What quotes do you use for strings? Double
? What line endings do you use? Windows
? Do you require semicolons? Yes
? What format do you want your config file to be in? JSON
Successfully created .eslintrc.json file in D:\ali\electron
از این پس با دستور زیر، قادرید کیفیت کدهای خود را بهبود بخشید
eslint .
یا
eslint index.js

می‌توانید این دستور را در در خصوصیت test، در بخش اسکریپت، به شکل زیر وارد کنید:
{
  "name": "electron",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "electron .",
    "test":"eslint ."
  },
  "author": "",
  "license": "ISC"
}
تا از این پس با دستور زیر قابل اجرا باشد:
npm test
بعد از اجرای دستور و طبق سوالات پاسخ داده شده، بهبود کیفیت کد به شکل زیر پاسخ داده می‌شود:
D:\ali\electron\index.js
   1:26  error  Strings must use doublequote                            quotes
  16:8   error  Strings must use doublequote                            quotes
  19:3   error  Expected indentation of 4 space characters but found 2  indent
  22:3   error  Expected indentation of 4 space characters but found 2  indent

✖ 4 problems (4 errors, 0 warnings)

برای نامگذاری فایل‌های js، به جای استفاده از _ از - استفاده کنید. این قاعده‌ای است که گیت هاب در نامگذاری فایل‌های js انجام می‌دهد. یعنی به جای عبارت dotnet_tips.js از عبارت dotnet-tips.js استفاده کنید. خاطرتان جمع باشد که هیچ فایل جاوااسکرپیتی رسمی در الکترون، خارج از این قاعده نامگذاری نشده است.
سعی کنید از جدیدترین قواعد موجود در ES6 استفاده کنید مانند:
let برای تعریف متغیرها
const برای تعریف ثابت ها
توابع جهتی به جای نوشتن عبارت function
استفاده از template string‌ها به جای چسباندن رشته‌ها از طریق عملگر +

برای نامگذاری متغیرها از همان قوانین تعریف شده برای node.js استفاده کنید که شامل موارد زیر است:
- موقعی که یک ماژول را به عنوان یک کلاس استفاده می‌کنید از متد CamelCase استفاده کنید مثل BrowserWindow
- موقعی که از یک ماژول که حاوی مجموعه‌ای از دستورات و api‌ها است استفاده می‌کنید، از قانون mixedCase استفاده کنید مثل app یا globalShortcut
- موقعی که یک api به عنوان خصوصیت یک شیء مورد استفاده قرار میگیرد، از mixedCase استفاده کنید؛ به عنوان مثال win.webContents یا fs.readFileSync
- برای مابقی اشیا مثل دستورات process و یا تگ webview که به شکل متفاوت‌تری مورد استفاده قرار می‌گیرند، از همان عنوان‌های خودشان استفاده می‌کنیم.

موقعی که api جدیدی را می‌سازید و قصد تعریف متدی را دارید بهتر است دو متد برای get و set تعریف شود، نسبت به حالتی که در کتابخانه جی کوئری مورد استفاده قرار می‌گیرد یعنی عبارت‌های زیر
setText('test');
getText();
نسبت به عبارت
.text([text]);
ترجیح داده می‌شوند.
مطالب
نوشتن آزمون‌های واحد به کمک کتابخانه‌ی Moq - قسمت پنجم - نکات و مباحث تکمیلی
پس از بررسی مباحث و نکات پایه‌ای کار با کتابخانه‌ی Moq، در این قسمت تعدادی از نکات تکمیلی آن‌را بررسی خواهیم کرد.


حالت‌های عملکرد کتابخانه‌ی Moq

کتابخانه‌ی Moq، دو حالت عملکرد را دارد: Strict Mode و Loose mode. زمانیکه یک Mock object را نمونه سازی می‌کنیم، به صورت پیش‌فرض کتابخانه‌ی Moq، یک Loose mock را ایجاد می‌کند. در این حالت این شیء، مقادیر پیش‌فرض خواص و اشیاء را بازگشت می‌دهد و استثنائی را صادر نمی‌کند. اگر این موارد مدنظر نیستند، می‌توان به حالت Strict آن رجوع کرد که روش تنظیم آن به صورت زیر است:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(MockBehavior.Strict);
در این حالت اگر متد آزمون واحد را اجرا کنیم، با پیام زیر، با شکست مواجه خواهد شد:
Test method Loans.Tests.LoanApplicationProcessorShould.Accept threw exception:
Moq.MockException: IIdentityVerifier.Initialize() invocation failed with mock behavior Strict.
All invocations on the mock must have a corresponding setup.
در حالت Strict، تمام فراخوانی‌های شیء Mock شده باید دارای Setup باشند (نیازی به Setup تمام موارد نیست؛ فقط مواردی که در فراخوانی‌های آزمون واحد، مورد استفاده قرار می‌گیرند، حتما باید تنظیم شوند). برای نمونه در اینجا عنوان کرده‌است که در این آزمایش، تنظیمات متد Initialize انجام نشده‌است که با تعریف سطر زیر، این مشکل برطرف می‌شود:
mockIdentityVerifier.Setup(x => x.Initialize());

بنابراین هرچند کارکردن با حالت پیش‌فرض کتابخانه‌ی Moq ساده‌است، اما تنظیم حالت Strict سبب می‌شود تا تنظیمی را فراموش نکنیم و در نتیجه کیفیت آزمون واحد تهیه شده افزایش می‌یابد.


صدور استثناءها از طریق Mock objects

اگر در سیستم در حال آزمایش، قسمتی به بررسی خطاها اختصاص دارد، می‌توان توسط Mock objects استثناءهایی را تولید و به این ترتیب منطق بررسی خطاها را آزمایش کرد.
برای نمونه در متد Process کلاس LoanApplicationProcessor، یک try/catch را به قسمت CalculateScore اضافه می‌کنیم:
try
{
    _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address);
}
catch
{
    return application.IsAccepted;
}
زمانیکه کار فراخوانی متد CalculateScore صورت می‌گیرد، برای تنظیم آزمون واحد آن می‌توان از متد Throws، برای صدور یک استثناء استفاده کرد:
mockCreditScorer.Setup(x =>
                    x.CalculateScore(It.IsAny<string>(), It.IsAny<string>()))
                .Throws(new InvalidOperationException("Test Exception"));
صدور این استثناء سبب خواهد شد تا درخواست شخص، رد شود. بنابراین در آزمایش آن می‌توان این مساله را بررسی کرد و از رسیدن به این قسمت (رد شدن درخواست) اطمینان حاصل نمود:
Assert.IsFalse(application.IsAccepted);


صدور رخدادها از طریق Mock objects

فرض کنید یک EventArgs سفارشی را به صورت زیر تعریف:
using System;

namespace Loans.Models
{
    public class CreditScoreResultArgs : EventArgs
    {
        public int Score { get; set; }
    }
}
و سپس رخدادی را به نحو زیر به ICreditScorer اضافه کرده‌ایم:
public interface ICreditScorer
{
   event EventHandler<CreditScoreResultArgs> ResultAvailable;
برای اینکه یک Mock object سبب بروز رخداد ResultAvailable شود (به صورت دستی و دقیقا در سطری که مشخص می‌کنیم)، می‌توان به صورت زیر عمل کرد:
mockCreditScorer.Raise(x => x.ResultAvailable += null, new CreditScoreResultArgs());
ابتدا توسط متد Raise، رخ‌داد مدنظر را ذکر می‌کنیم و سپس یک نمونه‌ی EventArgs را به آن ارسال خواهیم کرد.
روش دیگر انجام اینکار به صورت زیر است:
mockCreditScorer.Setup(x =>
                x.CalculateScore(It.IsAny<string>(), It.IsAny<string>()))
                .Raises(x => x.ResultAvailable += null, new CreditScoreResultArgs());
در این حالت با فراخوانی متد CalculateScore، رخداد ResultAvailable به صورت خودکار صادر می‌شود.


معرفی Partial Mocks

در اغلب آزمون‌های واحدی که تا اینجا بررسی شدند، ابتدا یک Mock object را ایجاد و سپس وهله‌ای از سرویس مدنظر را توسط آن تهیه می‌کنیم. در ادامه تعدادی از متدهای این سرویس را مانند متد Process کلاس LoanApplicationProcessor، فراخوانی می‌کنیم. اینکار سبب اجرای فعالیتی در این سیستم شده و به همراه آن تعاملی با اشیاء Mock شده نیز صورت می‌گیرد. در نهایت حالت و یا نتیجه‌ای را دریافت می‌کنیم و آن‌را با حالت یا نتیجه‌ای که انتظار داریم، مقایسه خواهیم کرد. در این روش پس از پایان اجرای سیستم در حال اجرا، حالت و نتیجه‌ی نهایی حاصل از عملکرد آن، مورد بررسی قرار می‌گیرد. این بررسی‌ها را نیز بر روی اینترفیس‌ها انجام دادیم. اگر بجای اینترفیس‌ها از یک class استفاده شود، به آن partial mock گفته می‌شود. عموما مواردی را که آزمایش آن‌ها سخت است، با Partial mocks پیاده سازی می‌کنند؛ مانند کار با فایل سیستم، کار با قطعه کدهای نامعین مانند DateTime.Now، اعداد اتفاقی و یا Guidها.

در مثال زیر، شبیه به متد آزمون واحد Accept که تاکنون آن‌را بررسی کردیم، از اشیاء Mock شده استفاده شده‌است؛ با یک تفاوت: بجای اینترفیس IIdentityVerifier، از کلاس پیاده سازی کننده‌ی آن که در اینجا IdentityVerifierServiceGateway است، استفاده شده:
namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {        
        [TestMethod]
        public void AcceptUsingPartialMock()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};

            var mockIdentityVerifier = new Mock<IdentityVerifierServiceGateway>();

            mockIdentityVerifier.Setup(x => x.CallService(applicant.Name, applicant.Age, applicant.Address))
                .Returns(true);

            var mockCreditScorer = new Mock<ICreditScorer>();
            mockCreditScorer.Setup(x => x.ScoreResult.ScoreValue.Score).Returns(110_000);

            var sut = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object);
            sut.Process(application);

            Assert.IsTrue(application.IsAccepted);
        }
    }
}
در اینجا برای اینکه بتوانیم متد CallService را که private بوده، بررسی و تنظیم کنیم، آن‌را به public virtual تبدیل کرده‌ایم تا توسط Moq قابل دسترسی و همچنین قابل بازنویسی شود:
public virtual bool CallService(string applicantName, int applicantAge, string applicantAddress)


تبدیل DateTime.Now به یک مقدار ثابت قابل آزمایش توسط Partial Mocks

در کلاس IdentityVerifierServiceGateway، یک چنین کدی را داریم که از DateTime.Now نامشخص استفاده می‌کند و آزمون واحد نوشتن برای آن مشکل است؛ چون DateTime.Now در هربار که آزمایش اجرا می‌شود، تغییر می‌کند:
public bool Validate(string applicantName, int applicantAge, string applicantAddress)
{
    Connect();
    var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
    LastCheckTime = DateTime.Now;
    Disconnect();

    return isValidIdentity;
}
برای بالابردن قابلیت آزمون نویسی این کلاس، آن‌را به صورت زیر Refactor می‌کنیم تا DateTime.Now را به صورت یک متد public virtual دریافت کند:
public bool Validate(string applicantName, int applicantAge, string applicantAddress)
{
    Connect();
    var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
    LastCheckTime = GetCurrentTime();
    Disconnect();

    return isValidIdentity;
}

public virtual DateTime GetCurrentTime()
{
    return DateTime.Now;
}
اکنون آزمون واحد نویسی برای این کلاس توسط Mock objects بسیار ساده‌است:
var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Setup(x => x.GetCurrentTime())
    .Returns(expectedTime);
// ...
Assert.AreEqual(expectedTime, mockIdentityVerifier.Object.LastCheckTime);
در اینجا خروجی متد GetCurrentTime بر روی Mock object تهیه شده، به یک مقدار ثابت تنظیم شده‌است که با هر بار اجرای آزمایش در زمان‌های مختلف، تغییری نمی‌کند و وابسته‌ی به DateTime.Now نامشخص، نیست.


استفاده از متدهای protected بجای استفاده از متدهای public virtual در Partial Mocks

همانطور که مشاهده کردید، برای کار با Partial Mocks نیاز است متدهای معرفی شده، از نوع public virtual باشند. برای نمونه حتی مجبور شدیم یک متد private را نیز public کنیم. اگر علاقمند به این نوع تغییرات نیستید، می‌توان بجای public کردن متدهای private، آن‌ها را protected تعریف کرد. به همین جهت دو متدی را که تاکنون public virtual تعریف کردیم، تبدیل به protected virtual می‌کنیم.
پس از آن در کلاسی که آزمون‌های واحد را تهیه کردیم، ابتدا using Moq.Protected را ذکر می‌کنیم تا بتوانیم به قابلیت‌های ویژه‌ی کار با متدهای Protected دسترسی پیدا کنیم.
سپس روش تنظیم این نوع متدهای protected، چون دسترسی مستقیمی به آن‌ها وجود ندارد، به صورت زیر، با ذکر نام رشته‌ای آن‌ها تغییر می‌کند:
mockIdentityVerifier.Protected().Setup<bool>(
        "CallService",applicant.Name, applicant.Age, applicant.Address)
    .Returns(true);

var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Protected().Setup<DateTime>("GetCurrentTime")
    .Returns(expectedTime);
ابتدا متد Protected شیء Mock شده ذکر می‌شود و پس از آن متد Setup باید دقیقا نوع بازگشتی متد در حال تنظیم را ذکر کند؛ چون دیگر دسترسی strongly typed ای به آن نداریم. پس ا‌ز آن، لیست پارامترهای متد، ذکر می‌شوند.

روش دیگری نیز برای تعریف متدهای protected وجود دارد که اینبار strongly typed است. بالای متد آزمون واحد، اینترفیس private زیر را تعریف می‌کنیم:
interface IIdentityVerifierServiceGatewayProtectedMembers
{
   DateTime GetCurrentTime();
   bool CallService(string applicantName, int applicantAge, string applicantAddress);
}
که در آن متدهای تعریف شده، با متدهای protected در حال بررسی، امضای یکسانی دارند (و همواره با هر تغییری در برنامه نیز باید این وضعیت حفظ شود). در ادامه تعاریف تنظیمات این متدها به صورت strongly typed زیر قابل انجام است:
mockIdentityVerifier.Protected()
    .As<IIdentityVerifierServiceGatewayProtectedMembers>()
    .Setup(x => x.CallService(It.IsAny<string>(),
        It.IsAny<int>(),
        It.IsAny<string>()))
    .Returns(true);

var expectedTime = new DateTime(2000, 1, 1);
mockIdentityVerifier.Protected()
    .As<IIdentityVerifierServiceGatewayProtectedMembers>()
    .Setup(x => x.GetCurrentTime())
    .Returns(expectedTime);


معرفی روش دیگری بجای استفاده از متدهای protected

اگر در کدهای خود نیاز به استفاده‌ی بیش از حد از متدهای protected را مشاهده کردید، این مورد می‌توان نشانه‌ی امکان Refactoring این قسمت از کدها به سرویس‌هایی مجزا باشند. برای مثال می‌توان یک اینترفیس INowProvider را به صورت زیر تعریف کرد:
using System;

namespace Loans.Services.Contracts
{
    public interface INowProvider
    {
        DateTime GetNow();
    }
}
و سپس آن‌را به سازنده‌ی کلاس IdentityVerifierServiceGateway تزریق کرد:
    public class IdentityVerifierServiceGateway : IIdentityVerifier
    {
        private readonly INowProvider _nowProvider;
        
        public DateTime LastCheckTime { get; private set; }

        public IdentityVerifierServiceGateway(INowProvider nowProvider)
        {
            _nowProvider = nowProvider;
        }
 و متد GetCurrentTime را حذف و آن‌را با متد GetNow این سرویس جایگزین نمود:
        public bool Validate(string applicantName, int applicantAge, string applicantAddress)
        {
            Connect();
            var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
            LastCheckTime = _nowProvider.GetNow();
            // ...
 به این ترتیب نیاز به تنظیم متد protected بازگشت زمان، حذف شده و می‌توان از این سرویس جدید استفاده کرد:
var mockNowProvider = new Mock<INowProvider>();
mockNowProvider.Setup(x => x.GetNow()).Returns(expectedTime);

var mockIdentityVerifier =  new Mock<IdentityVerifierServiceGateway>(mockNowProvider.Object);


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید: MoqSeries-05.zip
نظرات مطالب
Blazor 5x - قسمت 14 - کار با فرم‌ها - بخش 2 - تعریف فرم‌ها و اعتبارسنجی آن‌ها
یک نکته‌ی تکمیلی: روش ایجاد کامپوننت‌های ورودی سفارشی در Blazor


Blazor به صورت توکار به همراه تعدادی کنترل ورودی مانند InputText، InputTextArea، InputSelect، InputNumber، InputCheckbox و InputDate است که با سیستم اعتبارسنجی ورودی‌های آن نیز یکپارچه هستند.
در یک برنامه‌ی واقعی نیاز است divهایی مانند زیر را که به همراه روش تعریف این کامپوننت‌های ورودی است، صدها بار در قسمت‌های مختلف تکرار کرد:
<EditForm Model="NewPerson" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    
    <div class="form-group">
        <label for="firstname">First Name</label>
        <InputText @bind-Value="NewPerson.FirstName" class="form-control" id="firstname" />
        <ValidationMessage For="NewPerson.FirstName" />
    </div>
و خصوصا اگر نگارش بوت استرپ مورد استفاده تغییر کند، برای به روز رسانی برنامه نیاز خواهیم داشت تا تمام فرم‌های آن‌را تغییر دهیم. در یک چنین حالت‌هایی امکان ایجاد مخزنی از کامپوننت‌های سفارشی شده در برنامه‌های Blazor نیز پیش‌بینی شده‌است.
تمام کامپوننت‌های ورودی Blazor از کلاس پایه‌ی ویژه‌ای به نام <InputBase<T مشتق شده‌اند. این کلاس است که کار یکپارچگی با EditContext را جهت ارائه‌ی اعتبارسنجی‌های لازم، انجام می‌دهد. همچنین کار binding را نیز با ارائه‌ی پارامتر Value از نوع T انجام می‌دهد که نوشتن یک چنین کدهایی مانند "bind-Value="myForm.MyValue@ را میسر می‌کند. InputBase یک کلاس جنریک است که خاصیت Value آن از نوع T است. از آنجائیکه مرورگرها اطلاعات را به صورت رشته‌ای در اختیار ما قرار می‌دهند، این کامپوننت نیاز به روشی را دارد تا بتواند ورودی دریافتی را به نوع T تبدیل کند و اینکار را می‌توان با بازنویسی متد TryParseValueFromString آن انجام داد:
 protected abstract bool TryParseValueFromString(string value, out T result, out string validationErrorMessage);

یک مثال: کامپوننت جدید Shared\InputPassword.razor
@inherits InputBase<string>
<input type="password" @bind="@CurrentValue" class="@CssClass" />

@code {
    protected override bool TryParseValueFromString(string value, out string result, 
        out string validationErrorMessage)
    {
        result = value;
        validationErrorMessage = null;
        return true;
    }
}
در بین کامپوننت‌های پیش‌فرض Blazor، کامپوننت InputPassword را نداریم که نمونه‌ی سفارشی آن‌را می‌توان با ارث‌بری از InputBase، به نحو فوق طراحی کرد و نمونه‌ای از استفاده‌ی از آن می‌تواند به صورت زیر باشد:
<EditForm Model="userInfo" OnValidSubmit="CreateUser">
    <DataAnnotationsValidator />

    <InputPassword class="form-control" @bind-Value="@userInfo.Password" />
توضیحات:
- در این مثال CurrentValue و CssClass از کلاس پایه‌ی InputBase تامین می‌شوند.
- هربار که مقدار ورودی وارد شده‌ی توسط کاربر تغییر کند، متد TryParseValueFromString اجرا می‌شود.
- در متد TryParseValueFromString، مقدار validationErrorMessage به نال تنظیم شده؛ یعنی اعتبارسنجی خاصی مدنظر نیست. اولین پارامتر آن مقداری است که از کاربر دریافت شده و دومین پارامتر آن مقداری است که به کامپوننت ورودی که از آن ارث‌بری کرده‌ایم، ارسال می‌شود تا CurrentValue را تشکیل دهد (و یا خاصیت CurrentValueAsString نیز برای این منظور وجود دارد).
- اگر اعتبارسنجی اطلاعات ورودی در متد TryParseValueFromString با شکست مواجه شود، مقدار false را باید بازگشت داد.
نظرات مطالب
EF Code First #7
اینقدر باهاش ور رفتم تا درست شد (با استفاده از متد HasForeignKey). الان مشکلی که دارم در مورد Cascade Update هستش. اگر اینم تو قسمت های بعدی توضیح بدید عالی میشه.

 فقط یه خواهش دیگه که داشتم (البته مرتبط با این موضوع نیست)، اینکه اگر وقت کردید در مورد WPF و MVVM مطالب بیشتری بذارید چون این دو موضوع الان خیلی طرفدار دارن ولی یه خورده سخت هستن!.

من روزی چند بار به سایت شما میام و از مطالب مفیدتون استفاده میکنم. واقعاً ازتون ممنونم.