مطالب
Serialization #2

مطابق آنچه در قسمت قبل  گفته شد برای آن‌که بتوان از مدل News برای سریالی‌کردن استفاده کرد، باید آن را به شکل ذیل پیاده‌سازی کرد:

[DataContract]
public class News
{
     [DataMember] public int Id;
     [DataMember] public string Body;
     [DataMember] public DateTime NewsDate;
}

با Override  کردن  [DataContract]به صورت [("DataContract(Name=”MyCustomNews] می‌توان نام ریشه XML فایل را به MyCustomNews  تغییر داد. همچنین با Override  کردن [DataMember]  بصورت [("DataMember(Name=”MyCustomFieldName]  می‌شود به هر فیلدی عنوان دلخواهی داد و همچنین  با تعیین عبارت  NameSpace به صورت [("DataContract(Name = "MyCustomNews", Namespace = "http://www.my.com] می‌شود فضای نام را تغییر داد که با این تغییرات، خروجی زیر حاصل می‌شود: 

<?xml version="1.0" encoding="utf-8"?>
<MyCustomNews xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.my.com">
  <Body>NewsBody</Body>
  <MyCustomFieldName>111</MyCustomFieldName>
  <NewsDate>2012-10-04T00:00:00</NewsDate>
</MyCustomNews>


ویژگی [DataMember] هم ازفیلدها و هم از propertyها، پشتیبانی می‌کند، خواه عمومی باشند یا خصوصی و  نوع فیلد یا Property  می‌تواند به یکی از اشکال زیر باشد:

  1. انواع اولیه .
  2. انواع DateTime ،TimeSpan، Guid ،Uri و انواع Enum
  3. انواع پوچ پذیر هر کدام از موارد بالا
  4. نوع byte[]
  5. انواع تعریف شده توسط کاربر که توسط صفت [DataContract] محصور شده‌اند.
  6. هر نوع IEnumerable
  7. هر نوعی که با صفت [Serializable] محصور شود و یا اینترفیس ISerializable را پیاده سازی کند.
  8. هر نوعی که اینترفیس IXmlSerializble را پیاده سازی نماید.

تعیین فرمت باینری برای سریالی‌کردن:

برای سریالی کننده‌های  DataContractSerializer و NetDataContractSerializer می‌توان به روش زیر  فرمت خروجی را به شکل فرمت باینری درآورد که خروجی آن تاحد زیادی کوچک‌تر و کم حجم‌تر می‌شود:

var s = new MemoryStream();
using (XmlDictionaryWriter w=XmlDictionaryWriter.CreateBinaryWriter(s))
{
         ds.WriteObject(w,news);
}

و برای Deserialize کردن آن به شیوه زیر عمل می‌کنیم:

var s2 = new MemoryStream(s.ToArray());
News deserializednews;
using (XmlDictionaryReader r=XmlDictionaryReader.CreateBinaryReader(s2,XmlDictionaryReaderQuotas.Max))
{
     deserializednews = (News)ds.ReadObject(r);
}

که در آن از ویژگی Max  کلاس XmlDictionaryReaderQuotas  برای به دست آوردن حداکثر سهمیه فضای دیسک مربوط به  XmlDictionaryReaders  استفاده می‌شود. 

نظرات مطالب
سفارشی کردن ASP.NET Identity در MVC 5
لازم نیست تمام این آبجکت‌ها رو به context نگاشت کنید. قالب پروژه‌های VS 2013 بصورت خودکار در پوشه Models کلاسی بنام IdentityModels میسازه. این کلاس شامل کلاسی بنام ApplicationDbContext میشه که تعریفی مانند لیست زیر داره:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext() : base("DefaultConnection") { }
}

این کلاس رو کلا حذف کنید، چون قراره از یک DbContext برای تمام موجودیت‌ها استفاده کنید.
کلاس ApplicationUser که معرف موجودیت کاربران هست رو در لایه دامنه‌ها تعریف کنید و دقت کنید که باید از IdentityUser ارث بری کنه، حال با نام پیش فرض یا با نام دلخواه. سپس باید کلاسی بسازید که از <UserManager<T مشتق میشه. با استفاده از این کلاس می‌تونید به موجودیت‌های کاربران دسترسی داشته باشید. بعنوان مثال:
public class AppUserManager : UserManager<AppUser>{
    public AppUserManager() : base(new UserStore<AppUser>(new ShirazBilitDbContext())) { }
}

همونطور که می‌بینید کلاس موجودیت کاربر در اینجا AppUser نام داره، پس هنگام استفاده از UserManager نوع داده رو بهش نگاشت می‌کنیم. کد کلاس AppUser هم مطابق لیست زیر خواهد بود.
public class AppUser : IdentityUser
{
    public string Email { get; set; }
    public string ConfirmationToken { get; set; }
    public bool IsConfirmed { get; set; }
}

همونطور که مشخصه کلاس کاربران سفارشی سازی شده و سه فیلد به جدول کاربران اضافه کردیم. فیلدهای بیشتر یا موجودیت پروفایل کاربران هم باید به همین کلاس افزوده بشن. اگر پست‌ها رو بیاد بیارید گفته شد که ASP.NET Identity با مدل EF Code-First کار میکنه.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
در مطلب قبلی «استفاده از تامین کننده‌های هویت خارجی»، نحوه‌ی استفاده از اکانت‌های ویندوزی کاربران یک شبکه، به عنوان یک تامین کننده‌ی هویت خارجی بررسی شد. در ادامه می‌خواهیم از اطلاعات اکانت گوگل و IDP مبتنی بر OAuth 2.0 آن به عنوان یک تامین کننده‌ی هویت خارجی دیگر استفاده کنیم.


ثبت یک برنامه‌ی جدید در گوگل

اگر بخواهیم از گوگل به عنوان یک IDP ثالث در IdentityServer استفاده کنیم، نیاز است در ابتدا برنامه‌ی IDP خود را به آن معرفی و در آنجا ثبت کنیم. برای این منظور مراحل زیر را طی خواهیم کرد:

1- مراجعه به developer console گوگل و ایجاد یک پروژه‌ی جدید
https://console.developers.google.com
در صفحه‌ی باز شده، بر روی دکمه‌ی select project در صفحه و یا لینک select a project در نوار ابزار آن کلیک کنید. در اینجا دکمه‌ی new project و یا create را مشاهده خواهید کرد. هر دوی این مفاهیم به صفحه‌ی زیر ختم می‌شوند:


در اینجا نامی دلخواه را وارد کرده و بر روی دکمه‌ی create کلیک کنید.

2- فعالسازی API بر روی این پروژه‌ی جدید


در ادامه بر روی لینک Enable APIs And Services کلیک کنید و سپس google+ api را جستجو نمائید.
پس از ظاهر شدن آن، این گزینه را انتخاب و در صفحه‌ی بعدی، آن‌را با کلیک بر روی دکمه‌ی enable، فعال کنید.

3- ایجاد credentials


در اینجا بر روی دکمه‌ی create credentials کلیک کرده و در صفحه‌ی بعدی، این سه گزینه را با مقادیر مشخص شده، تکمیل کنید:
• Which API are you using? – Google+ API
• Where will you be calling the API from? – Web server (e.g. node.js, Tomcat)
• What data will you be accessing? – User data
سپس در ذیل این صفحه بر روی دکمه‌ی «What credentials do I need» کلیک کنید تا به صفحه‌ی پس از آن هدایت شوید. اینجا است که مشخصات کلاینت OAuth 2.0 تکمیل می‌شوند. در این صفحه، سه گزینه‌ی آن‌را به صورت زیر تکمیل کنید:
• نام: همان مقدار پیش‌فرض آن
• Authorized JavaScript origins: آن‌را خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آن‌را با آدرس زیر مقدار دهی خواهیم کرد.
https://localhost:6001/signin-google
این آدرس، به آدرس IDP لوکال ما اشاره می‌کند و مسیر signin-google/ آن باید به همین نحو تنظیم شود تا توسط برنامه شناسایی شود.
سپس در ذیل این صفحه بر روی دکمه‌ی «Create OAuth 2.0 Client ID» کلیک کنید تا به صفحه‌ی «Set up the OAuth 2.0 consent screen» بعدی هدایت شوید. در اینجا دو گزینه‌ی آن‌را به صورت زیر تکمیل کنید:
- Email address: همان آدرس ایمیل واقعی شما است.
- Product name shown to users: یک نام دلخواه است. نام برنامه‌ی خود را برای نمونه ImageGallery وارد کنید.
برای ادامه بر روی دکمه‌ی Continue کلیک نمائید.

4- دریافت credentials
در پایان این گردش کاری، به صفحه‌ی نهایی «Download credentials» می‌رسیم. در اینجا بر روی دکمه‌ی download کلیک کنید تا ClientId  و ClientSecret خود را توسط فایلی به نام client_id.json دریافت نمائید.
سپس بر روی دکمه‌ی Done در ذیل صفحه کلیک کنید تا این پروسه خاتمه یابد.


تنظیم برنامه‌ی IDP برای استفاده‌ی از محتویات فایل client_id.json

پس از پایان عملیات ایجاد یک برنامه‌ی جدید در گوگل و فعالسازی Google+ API در آن، یک فایل client_id.json را دریافت می‌کنیم که اطلاعات آن باید به صورت زیر به فایل آغازین برنامه‌ی IDP اضافه شود:
الف) تکمیل فایل src\IDP\DNT.IDP\appsettings.json
{
  "Authentication": {
    "Google": {
      "ClientId": "xxxx",
      "ClientSecret": "xxxx"
    }
  }
}
در اینجا مقادیر خواص client_secret و client_id موجود در فایل client_id.json دریافت شده‌ی از گوگل را به صورت فوق به فایل appsettings.json اضافه می‌کنیم.
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
   // ... 

            services.AddAuthentication()
                .AddGoogle(authenticationScheme: "Google", configureOptions: options =>
                {
                    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    options.ClientId = Configuration["Authentication:Google:ClientId"];
                    options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
                });
        }
خود ASP.NET Core از تعریف اطلاعات اکانت‌های Google, Facebook, Twitter, Microsoft Account و  OpenID Connect پشتیبانی می‌کند که در اینجا نحوه‌ی تنظیم اکانت گوگل آن‌را مشاهده می‌کنید.
- authenticationScheme تنظیم شده باید یک عبارت منحصربفرد باشد.
- همچنین SignInScheme یک چنین مقداری را در اصل دارد:
 public const string ExternalCookieAuthenticationScheme = "idsrv.external";
از این نام برای تشکیل قسمتی از نام کوکی که اطلاعات اعتبارسنجی گوگل در آن ذخیره می‌شود، کمک گرفته خواهد شد.


آزمایش اعتبارسنجی کاربران توسط اکانت گوگل آن‌ها

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


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


پس از آن، از طرف گوگل به صورت خودکار به IDP (همان آدرسی که در فیلد Authorized redirect URIs وارد کردیم)، هدایت شده و callback رخ‌داده، ما را به سمت صفحه‌ی ثبت اطلاعات کاربر جدید هدایت می‌کند. این تنظیمات را در قسمت قبل ایجاد کردیم:
namespace DNT.IDP.Controllers.Account
{
    [SecurityHeaders]
    [AllowAnonymous]
    public class ExternalController : Controller
    {
        public async Task<IActionResult> Callback()
        {
            var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
            var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";

            var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
            if (user == null)
            {
                // user = AutoProvisionUser(provider, providerUserId, claims);
                
                var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl });
                var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" ,
                    new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId });
                return Redirect(continueWithUrl);
            }
اگر خروجی متد FindUserFromExternalProvider آن null باشد، یعنی کاربری که از سمت تامین کننده‌ی هویت خارجی/گوگل به برنامه‌ی ما وارد شده‌است، دارای اکانتی در سمت IDP نیست. به همین جهت او را به سمت صفحه‌ی ثبت نام کاربر هدایت می‌کنیم.
در اینجا نحوه‌ی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحه‌ی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده می‌کنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره می‌کند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد و به برنامه با این هویت جدید وارد می‌شود.


اتصال کاربر وارد شده‌ی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است

تا اینجا اگر کاربری از طریق یک IDP خارجی به برنامه وارد شود، او را به صفحه‌ی ثبت نام کاربر هدایت کرده و پس از دریافت اطلاعات او، اکانت خارجی او را به اکانتی جدید که در IDP خود ایجاد می‌کنیم، متصل خواهیم کرد. به همین جهت بار دومی که این کاربر به همین ترتیب وارد سایت می‌شود، دیگر صفحه‌ی ثبت نام و تکمیل اطلاعات را مشاهده نمی‌کند. اما ممکن است کاربری که برای اولین بار از طریق یک IDP خارجی به سایت ما وارد شده‌است، هم اکنون دارای یک اکانت دیگری در سطح IDP ما باشد؛ در اینجا فقط اتصالی بین این دو صورت نگرفته‌است. بنابراین در این حالت بجای ایجاد یک اکانت جدید، بهتر است از همین اکانت موجود استفاده کرد و صرفا اتصال UserLogins او را تکمیل نمود.
به همین جهت ابتدا نیاز است لیست Claims بازگشتی از گوگل را بررسی کنیم:
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);  
foreach (var claim in claims)
{
   _logger.LogInformation($"External provider[{provider}] info-> claim:{claim.Type}, value:{claim.Value}");
}
در اینجا پس از فراخوانی FindUserFromExternalProvider، لیست Claims بازگشت داده شده‌ی توسط IDP خارجی را لاگ می‌کنیم که در حالت استفاده‌ی از گوگل چنین خروجی را دارد:
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, value:Vahid N.
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname, value:Vahid
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname, value:N.
External provider[Google] info-> claim:urn:google:profile, value:https://plus.google.com/105013528531611201860
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, value:my.name@gmail.com
بنابراین اگر بخواهیم بر اساس این claims بازگشتی از گوگل، کاربر جاری در بانک اطلاعاتی خود را بیابیم، فقط کافی است اطلاعات claim مخصوص emailaddress آن‌را مورد استفاده قرار دهیم:
        [HttpGet]
        public async Task<IActionResult> Callback()
        {
            // ...

            var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
            if (user == null)
            {
                // user wasn't found by provider, but maybe one exists with the same email address?  
                if (provider == "Google")
                {
                    // email claim from Google
                    var email = claims.FirstOrDefault(c =>
                        c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
                    if (email != null)
                    {
                        var userByEmail = await _usersService.GetUserByEmailAsync(email.Value);
                        if (userByEmail != null)
                        {
                            // add Google as a provider for this user
                            await _usersService.AddUserLoginAsync(userByEmail.SubjectId, provider, providerUserId);

                            // redirect to ExternalLoginCallback
                            var continueWithUrlAfterAddingUserLogin =
                                Url.Action("Callback", new {returnUrl = returnUrl});
                            return Redirect(continueWithUrlAfterAddingUserLogin);
                        }
                    }
                }


                var returnUrlAfterRegistration = Url.Action("Callback", new {returnUrl = returnUrl});
                var continueWithUrl = Url.Action("RegisterUser", "UserRegistration",
                    new {returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId});
                return Redirect(continueWithUrl);
            }
در اینجا ابتدا بررسی شده‌است که آیا کاربر جاری واکشی شده‌ی از بانک اطلاعاتی نال است؟ اگر بله، اینبار بجای هدایت مستقیم او به صفحه‌ی ثبت کاربر و تکمیل مشخصات او، مقدار email این کاربر را از لیست claims بازگشتی او از طرف گوگل، استخراج می‌کنیم. سپس بر این اساس اگر کاربری در بانک اطلاعاتی وجود داشت، تنها اطلاعات تکمیلی UserLogin او را که در اینجا خالی است، به اکانت گوگل او متصل می‌کنیم. به این ترتیب دیگر کاربر نیازی نخواهد داشت تا به صفحه‌ی ثبت اطلاعات تکمیلی هدایت شود و یا اینکه بی‌جهت رکورد User جدیدی را مخصوص او به بانک اطلاعاتی اضافه کنیم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
نظرات مطالب
انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
برای استفاده از متد‌های Async، آیا روش زیر صحیح است:
        public override Task RunAsync()
        {
            MainAsync().Wait();
            return base.RunAsync();
        }

        private static async Task MainAsync()
        {
            var smsService = ObjectFactory.Container.GetInstance<ISmsService>();
            await smsService.CheckForDelivery();
        }
 روش بالا جواب میده، ولی از درست بودنش مطمئن نیستم! 
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت سوم
پیشنهاد می‌کنم قسمت‌های قبل را مطالعه کنید تا با اصطلاحات استفاده شده در ادامه مقالات آشنا باشید. در مقالات آتی، مباحث کمی قابل بحث‌تر خواهند بود.

Class Coupling and Cohesion

تعدادی از قواعد شهودی هم، با Coupling و Cohesion به ترتیب مابین و درون کلاس‌ها، سروکار دارند. تلاش ما در راستای افزایش Cohesion درون کلاس‌ها و سست کردن و کاهش Coupling مابین کلاس‌ها می‌باشد. این قواعد شهودی همین اهداف را در پارادایم action-oriented، در ارتباط با توابع دارند. هدف از Tight Cohesion (انسجام و چسبندگی قوی) در توابع، انسجام بالا و ارتباط نزدیک مابین کدهای موجود در تابع، می‌باشد. هدفی که Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) در بین توابع دنبال می‌کند، اشاره دارد به اینکه اگر تابعی قصد استفاده از تابع دیگری را داشته باشد، باید وارد شدن و خروج از آن، از یک نقطه صورت گیرد. این مباحث منجربه مطرح شدن قواعد شهودی از جمله: «یک تابع باید طوری سازماندهی شود که تنها یک دستور return  داشته باشد»، در پارادایم action-oriented می‌شود.
ما در پارادایم شیء گرا، اهداف خود از Loose Coupling و Tight Cohesion را در سطح کلاس مطرح می‌کنیم. 5 شکل اصلی Coupling مابین کلاس‌ها به شرح زیر می‌باشد:

  •  Ni Coupling
  •  Export Coupling
  •  Overt Coupling
  •  Covert Coupling
  •  Surreptitious Coupling
Nil Coupling

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

Export Coupling

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

Overt Coupling

این نوع اتصال زمانی رخ می‌دهد که یک کلاس از جزئیات پیاده سازی کلاس دیگر با داشتن اجازه دسترسی از جانب آن، استفاده کند. به عنوان مثال، مکانیزم کلاس‌های friend در زبان سی پلاس پلاس، که امکان این را می‌دهد کلاس X اجازه دوستی به کلاس Y را اعطا کند و در این صورت کلاس Y می‌تواند به جزئیات پیاده سازی خصوصی کلاس X دسترسی داشته باشد.

Cover Coupling

این نوع اتصال هم به مانند Overt می‌باشد؛ با این تفاوت که هیچ اجازه دسترسی به کلاس Y داده نشده است. اگر زبانی داشته باشیم که به کلاس Y اجازه دهد خود را به عنوان دوست کلاس X معرفی کند، در این صورت نوع اتصال بین دو کلاس X و Y از نوع Covert می‌باشد. به عنوان مثال واقعی، می‌توان به استفاده از Reflection در دات نت اشاره کرد.

Surreptitious Coupling (اتصال پنهان)

آخرین نوع اتصال که بدترین حالت هم محسوب می‌شود، مربوط است به زمانیکه کلاس X به هر طریقی که شده از جزئیات داخلی کلاس Y آگاه باشد و از اعضای عمومی داده‌ای (public data member) آن کلاس استفاده کند. منظور این است که با تغییر این داده‌های کلاس متوجه میشود که بر روی عملیات b از کلاس چه تأثیری می‌گذارد و با اتکاء به این دستاورد، جزئیات داخلی خود را پیاده سازی می‌کند و یک اتصال پنهان را با کلاس Y ایجاد کرده است. در این حالت یک وابستگی قوی به صورت پنهان مابین رفتاری از کلاس Y و پیاده سازی کلاس X ایجاد شده است.

قاعده شهودی 2.7
اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
بجز این دو نوع اتصال، بقیه شکل‌های اتصال به طریقی اجازه دسترسی به جزئیات پیاده سازی کلاس‌ها را اعطا می‌کنند. در نتیجه باعث ایجاد وابستگی مابین پیاده سازی دو کلاس می‌شوند. این وابستگی ما بین پیاده سازی‌ها به محض نیاز به تغییر پیاده سازی یکی از کلاس‌ها ، باعث به وجود آمدن مشکلات نگهداری خواهند شد.
Cohesion درون کلاس‌ها سعی بر این دارد که مطمئن شود تمام اجزای یک کلاس به شدت باهم مرتبط هستند. تعدادی از قواعد شهودی نیز در ادامه بر این خصوصیت دلالت دارند.

قاعده شهودی 2.8
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)
یک Key Abstraction به عنوان یک Entity در Domain Model تعریف می‌شود و اغلب در غالب اسم در اسناد و مشخصات نیازمندی‌ها ظاهر می‌شوند. هر کدام از آنها باید فقط به یک کلاس نگاشت پیدا کنند. اگر این نگاشت به بیش از یک کلاس انجام گیرد، در نتیجه احتمالا طراح هر تابع را به عنوان یک کلاس تسخیر کرده است. اگر بیش از یک Key Abstraction به یک کلاس نگاشت پیدا کرده باشد، پس احتمالا طراح یک سیستم متمرکز را طراحی کرده است. این کلاس‌ها Vague Classes نامیده می‌شوند و باید آنها در دو کلاس یا بیشتر، تسخیر شوند.

قاعده شهودی 2.9
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
در واقع هدفی که این قاعده به دنبال آن می‌باشد این است که هر دو جزء تشکیل دهنده یک Key Abstraction ، یعنی همان داده و رفتار، باید توسط فقط یک کلاس تسخیر شوند. با نقض این قاعده، توسعه دهنده باید با قرار داد (Convention) خاصی برنامه نویسی کند. 
راه شناسایی
طراح باید کلاس‌هایی را که مرتبا داده‌های مورد نیاز خود را با متدهای get از سایر کلاس‌ها دریافت می‌کنند، شناسایی کند. زیرا این نوع کلاس‌ها این قاعده شهودی را نقض کرده‌اند.

مثال واقعی
استفاده از الگوی Domain Model ارائه شده توسط اقای Martin Fowler که دقیقا اشاره به این قاعده دارد.
یا برعکس آن ضد الگوی Anemic Domain Model که ناقض این قاعده می‌باشد. 
در قسمت اول اشاره کردیم این قواعد را به راحتی می‌توان در صورت نیاز نقض کرد. بعضی از مواقع نیاز به طراحی فیزیکی است که باعث تغییر در طراحی منطقی شده و چه بسا می‌تواند باعث نقض هر کدام از این قواعد شهودی نیز شود. اگر به بخش پروژه‌های سایت رجوع کنید این نقض کاملا مشهود (DomainClasses و ServiceLayer موجود در طراحی فیزیکی آنها) می‌باشد (بیشتر از Anemic Domain Model استفاده شده است)؛ ولی نمی‌توان گفت که این کار اشتباه است.

قاعده شهودی 2.10
اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior) 

هدف از این قاعده این است که اگر کلاسی داریم که یکسری از متدهایش با بخشی از داده و یکسری دیگر با بخش دیگر داده‌ها کار میکنند، در واقع شما دو Key Abstraction را به یک کلاس نگاشت کرده اید (Vague Class) و باید آنها را به کلاس‌های جدا نگاشت کنید.

شکل 2.6 A class with noncommunicating behavior

مثال واقعی

به کلاس Dictionary در تصویر زیر توجه کنید.
برای تعداد کمی داده، بهترین پیاده سازی با استفاده از List و در مقابل برای تعداد داده زیاد بهترین پیاده سازی، استفاده از HashTable می‌باشد. هر یک از این پیاده سازی‌ها، به متدهایی برای add و find کلمات نیاز دارند. طراحی سمت چپ تصویر نشان از نقض این قاعده شهودی دارد.

شکل 2.7 (Noncommunicating behavior (real-world example

در طرح سمت چپ، استفاده کننده باید بداند که چقدر داده وارد کند. از طرفی نمایش جزئیات پیاده سازی در نام کلاس هم ایده خوبی نیست (طرح سمت چپ). بهترین راه حل که در مقالات آینده به آنها خواهیم رسید، بحث استفاده از ارث بری می‌باشد. به این ترتیب، با استفاده از  یک کلاس Dictionary که نمایش جزئیات داخلی خود را مخفی کرده و در شرایط لازم نمایش جزئیات داخلی خود را تغییر دهد. منظور این است که استفاده کننده درگیر جزئیات داخلی آن نشود و این جزئیات که کدام نوع PDict یا HDict استفاده خواهد شد، از دید او مخفی باشد.

مطالب
فراخوانی سرویس های WCF به صورت Async
هنگام تولید و توسعه سیستم‌های مبتنی بر WCF حتما نیاز به سرویس هایی داریم که متد‌ها را به صورت Async اجرا کنند. در دات نت 4.5 از Async&Await استفاده می‌کنیم(^). ولی در پروژه هایی که تحت دات نت 4 هستند این امکان وجود ندارد(البته می‌تونید Async&Await CTP رو برای دات نت 4 هم نصب کنید(^ )). فرض کنید پروژه ای داریم تحت دات نت 3.5 یا 4 و قصد داریم یکی از متد‌های سرویس WCF آن را به صورت Async پیاده سازی کنیم. ساده‌ترین روش این است که هنگام Add Service Reference از پنجره Advanced  به صورت زیر عمل کنیم:


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

 در این پست قصد دارم پیاده سازی این متد رو بدون استفاده از Async&Await و Code Generation توکار دات نت شرح بدم که با دات نت 3.5 هم سازگار است.

ابتدا یک پروژه از نوع WCF Service Application ایجاد کنید.
یک ClassLibrary جدید به نام Model بسازید و کلاس زیر را به عنوان مدل در آن قرار دهید.(این اسمبلی باید هم به پروژه‌های کلاینت و هم به پروژه‌های سرور رفرنس داده شود)
    [DataContract]
    public class Book
    {
        [DataMember]
        public int Code { get; set; }

        [DataMember]
        public string Title { get; set; }

        [DataMember]
        public string Author { get; set; }
    }
حال  پیاده سازی سرویس و Contract مربوطه را شروع می‌کنیم.
#Class Library به نام Contract بسازید. قصد داریم از این لایه به عنوان قرارداد‌های سمت کلاینت و سرور استفاده کنیم. اینترفیس زیر را به عنوان BookContract در آن بسازید.
   [ServiceContract]
    public interface IBookService
    {
        [OperationContract( AsyncPattern = true )]
        IAsyncResult BeginGetAllBook( AsyncCallback callback, object state );

        IEnumerable<Book> EndGetAllBook( IAsyncResult asyncResult ); 
    }
برای پیاده سازی متد‌های Async به این روش باید دو متد داشته باشیم. یکی به عنوان شروع عملیات و دیگری اتمام. دقت کنید نام گذاری به صورت Begin و End کاملا اختیاری است و برای خوانایی بهتر از این روش نام گذاری استفاده می‌کنم. متدی که به عنوان شروع عملیات استفاده می‌شود باید حتما OperationContractAttribute رو داشته باشد و مقدار خاصیت AsyncPattern اون هم true باشد. همان طور که می‌بیند این متد دارای 2 آرگومان وروردی است. یکی از نوع AsyncCallback و دیگری از نوع object. تمام متد‌های Async به این روش باید این دو آرگومان ورودی را حتما داشته باشند. خروجی این متد حتما باید از نوع IAsyncResult باشد. متد دوم که به عنوان اتمام عملیات استفاده می‌شود نباید OperationContractAttribute را داشته باشد. ورودی اون هم فقط یک آرگومان از نوع IAsyncResult است. خروجی اون هم هر نوعی که سمت کلاینت احتیاج دارید می‌تونه باشه . در صورت عدم رعایت نکات فوق، هنگام ساخت  ChannelFactory یا خطا روبرو خواهید شد. اگر نیاز به پارامتر دیگری هم داشتید باید آن‌ها را قبل از این دو پارامتر قرار دهید. برای مثال:
[OperationContract]
IEnumerable<Book> GetAllBook(int code , AsyncCallback callback, object state );
قبل از پیاده سازی سرویس باید ابتدا یک AsyncResult سفارشی بسازیم. ساخت AsyncResult سفارشی بسیار ساده است. کافی است کلاسی بسازیم که اینترفیس IAsyncResult را به ارث ببرد.
 public class CompletedAsyncResult<TEntity> : IAsyncResult where TEntity : class , new()
    {
        public IList<TEntity> Result
        {
            get
            {
                return _result;
            }
            set
            {
                _result = value;
            }
        }
        private IList<TEntity> _result;

        public CompletedAsyncResult( IList<TEntity> data )
        {
            this.Result = data;
        }

        public object AsyncState
        {
            get
            {
                return ( IList<TEntity> )Result;
            }
        }

        public WaitHandle AsyncWaitHandle
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public bool CompletedSynchronously
        {
            get
            {
                return true;
            }
        }

        public bool IsCompleted
        {
            get
            {
                return true;
            }
        }
    }
در کلاس بالا یک خاصیت به نام Result درنظر گرفتم که لیستی از نوع TEntity است.(TEntityبه صورت generic تعریف شده و نوع ورودی آن هر نوع کلاس غیر abstract می‌تواند باشد). این کلاس برای تمام سرویس‌های Async یک پروژه مورد استفاده قرار خواهد گرفت برای همین ورودی آن به صورت generic در نظر گرفته شده است.
#پیاده سازی سرویس
 public class BookService : IBookService
    {
        public BookService()
        {
            ListOfBook = new List<Book>();
        }

        public List<Book> ListOfBook
        {
            get;
            private set;
        }

        private List<Book> CreateListOfBook()
        {
            Parallel.For( 0, 10000, ( int counter ) =>
            {
                ListOfBook.Add( new Book()
                {
                    Code = counter,
                    Title = String.Format( "Book {0}", counter ),
                    Author = "Masoud Pakdel"
                } );
            } );

            return ListOfBook;
        }

        public IAsyncResult BeginGetAllBook( AsyncCallback callback, object state )
        {
            var result = CreateListOfBook();
            return new CompletedAsyncResult<Book>( result );
        }

        public IEnumerable<Book> EndGetAllBook( IAsyncResult asyncResult )
        {
            return ( ( CompletedAsyncResult<Book> )asyncResult ).Result;
        }
    }
*در متد BeginGetAllBook ابتدا به تعداد 10,000 کتاب در یک لیست ساخته می‌شوند و بعد این لیست در کلاس CompletedAsyncResult که ساختیم به عنوان ورودی سازنده پاس داده می‌شوند. چون CompletedAsyncResult از IAsyncResult ارث برده است پس return آن به عنوان خروجی مانعی ندارد. با فراخوانی متد EndGetAllBook سمت کلاینت  مقدار asyncResult به عنوان خروجی برگشت داده می‌شود. به عملیات casting برای دستیابی به مقدار Result در CompletedAsyncResult دقت کنید.
#کد‌های سمت کلاینت:
اکثر برنامه نویسان با استفاده از روش AddServiceReference یک سرویس کلاینت در اختیار خواهند داشت که با وهله سازی از این کلاس یک ChannelFactory ایجاد می‌شود. در این پست به جای استفاده از Code Generation توکار دات نت برای ساخت ChannelFactory از روش دیگری استفاده خواهیم کرد. به عنوان برنامه نویس باید بدانیم که در پشت پرده عملیات ساخت ChannelFactory چگونه است.
 روش AddServiceReference
بعد از اضافه شدن سرویس سمت کلاینت کد‌های زیر برای سرویس Book به صورت زیر تولید می‌شود.
[System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
    public partial class BookServiceClient : System.ServiceModel.ClientBase<UI.BookService.IBookService>, UI.BookService.IBookService {
        
        public BookServiceClient() {
        }
        
        public BookServiceClient(string endpointConfigurationName) : 
                base(endpointConfigurationName) {
        }
        
        public BookServiceClient(string endpointConfigurationName, string remoteAddress) : 
                base(endpointConfigurationName, remoteAddress) {
        }
        
        public BookServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : 
                base(endpointConfigurationName, remoteAddress) {
        }
        
        public BookServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : 
                base(binding, remoteAddress) {
        }
        
        public UI.BookService.Book[] BeginGetAllBook() {
            return base.Channel.BeginGetAllBook();
        }
    }
همانطور که می‌بینید سرویس بالا از کلاس ClientBase ارث برده است. ClientBase دارای خاصیتی به نام ChannelFactory است که فقط خواندنی می‌باشد. با استفاده از مقادیر EndPointConfiguration یک وهله از کلاس ChannelFactory با توجه به مقدار generic کلاس clientBase ایجاد خواهد شد. در کد زیر مقدار TChannel برابر IBookService است:
System.ServiceModel.ClientBase<UI.BookService.IBookService>
وهله سازی از ChannelFactory به صورت دستی
یک پروژه ConsoleApplication سمت کلاینت ایجاد کنید. برای فراخوانی متد‌های سرویس سمت سرور باید ابتدا تنظیمات EndPoint رو به درستی انجام دهید. سپس با استفاده از EndPoint به راحتی می‌توانیم Channel مربوطه را بسازیم.
کلاسی به نام ServiceMapper ایجاد می‌کنیم که وظیفه آن ساخت ChannelFactory به ازای درخواست‌ها است.
public class ServiceMapper<TChannel>
    {
        public static TChannel CreateChannel()
        {
            TChannel proxy;

            var endPointAddress = new EndpointAddress( "http://localhost:7000/" + typeof( TChannel ).Name.Remove( 0, 1 ) + ".svc" );

            var httpBinding = new BasicHttpBinding();
            
            ChannelFactory<TChannel> factory = new ChannelFactory<TChannel>( httpBinding, endPointAddress );

            proxy = factory.CreateChannel();

            return proxy;
        }
    }
در متد CreateChannel یک وهله از کلاس EndPointAddress ایجاد شده است. پارامتر ورودی آن آدرس سرویس هاست شده می‌باشد برای مثال:

"http://localhost:7000/" +  "BookService.svc"
دستور Remove برای حذف I از ابتدای نام سرویس است. پارامتر‌های ورودی برای سازنده کلاس ChannelFactory ابتدا یک نمونه از کلاس BasicHttpBinding می‌باشد. می‌توان از WSHttpBinding یا NetTCPBinding یا WSDLHttpBinding هم استفاده کرد. البته هر کدام از انواع Binding‌ها تنظیمات خاص خود را می‌طلبد که در مقاله ای جداگانه بررسی خواهم کرد. پارامتر دوم هم EndPoint ساخته شده می‌باشد. در نهایت با دستور CreateChannel عملیات ساخت Channel به پایان می‌رسد.

بعد از اعمال تغییرات زیر در فایل Program پروژه Console و اجرای آن، خروجی به صورت زیر می‌باشد.
  var channel = ServiceMapper<Contract.IBookService>.CreateChannel();
            channel.BeginGetAllBook( new AsyncCallback( ( asyncResult ) => 
            {
                channel.EndGetAllBook( asyncResult ).ToList().ForEach( _record => 
                {
                    Console.WriteLine( _record.Title );
                } );
            } ) , null );
            Console.WriteLine( "Loading..." );
            Console.ReadLine();
همان طور که می‌بینید ورودی متد BeginGtAllBook یک AsyncCallback است که در داخل آن متد EndGetAllBook صدا زده شده است. مقدار برگشتی متد EndGetAllBook خروجی مورد نظر ماست.
خروجی : 


نکته: برای اینکه مطمئن شوید که سرویس مورد نظر در آدرس "http"//localhost:7000/" هاست شده است(یعنی همان آدرسی که در EndPointAddress از آن استفاه کردیم) کافیست از پنجره Project Properties  برای پروژه سرویس وارد برگه Web شده و از بخش Servers گزینه Use Visual Studio Development Server و Specific Port 7000 رو انتخاب کنید.

 
نظرات مطالب
بررسی بهبودهای ProblemDetails در ASP.NET Core 7x
در ASP.NET Core 8.0 نیز امکان مدیریت استثناءها به صورت سراسری با پیاده‌سازی اینترفیس  IExceptionHandler مسیر شده است:
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

internal sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext context,
        Exception exception,
        CancellationToken cancellationToken = default)
    {
        _logger.LogError(exception, "An unhandled exception has occurred while executing the request.");
        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Server Error",
        };

        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await context.Response.WriteAsJsonAsync(problemDetails);

        return true;
    }
}

برای ریجستر کردن آن نیز:
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
// other code
app.UseExceptionHandler();
مطالب دوره‌ها
نگاهی به SignalR Clients
در قسمت قبل موفق به ایجاد اولین Hub خود شدیم. در ادامه، برای تکمیل برنامه نیاز است تا کلاینتی را نیز برای آن تهیه کنیم.
مصرف کنندگان یک Hub می‌توانند انواع و اقسام برنامه‌های کلاینت مانند jQuery Clients و یا حتی یک برنامه کنسول ساده باشند و همچنین Hubهای دیگر نیز قابلیت استفاده از این امکانات Hubهای موجود را دارند. تیم SignalR امکان استفاده از Hubهای آن‌را در برنامه‌های دات نت 4 به بعد، برنامه‌های WinRT، ویندوز فون 8، سیلورلایت 5، jQuery و همچنین برنامه‌های CPP نیز مهیا کرده‌اند. به علاوه گروه‌های مختلف نیز با توجه به سورس باز بودن این مجموعه، کلاینت‌های iOS Native، iOS via Mono و Android via Mono را نیز به این لیست اضافه کرده‌اند.


بررسی کلاینت‌های jQuery

با توجه به پروتکل مبتنی بر JSON سیگنال‌آر، استفاده از آن در کتابخانه‌های جاوا اسکریپتی همانند jQuery نیز به سادگی مهیا است. برای نصب آن نیاز است در کنسول پاور شل نوگت، دستور زیر را صادر کنید:
 PM> Install-Package Microsoft.AspNet.SignalR.JS
برای نمونه به solution پروژه قبل، یک برنامه وب خالی دیگر را اضافه کرده و سپس دستور فوق را بر روی آن اجرا نمائید. در این حالت فقط باید دقت داشت که فرامین بر روی کدام پروژه اجرا می‌شوند:


با استفاده از افزونه SignalR jQuery، به دو طریق می‌توان به یک Hub اتصال برقرار کرد:
الف) استفاده از فایل proxy تولید شده آن (این فایل، در زمان اجرای برنامه تولید می‌شود و یا امکان استفاده از آن به کمک ابزارهای کمکی نیز وجود دارد)
نمونه‌ای از آن‌را در قسمت قبل ملاحظه کردید؛ همان فایل تولید شده در مسیر /signalr/hubs برنامه. به نوعی به آن Service contract نیز گفته می‌شود (ارائه متادیتا و قراردادهای کار با یک سرویس Hub). این فایل همانطور که عنوان شد به صورت پویا در زمان اجرای برنامه ایجاد می‌شود.
امکان تولید آن توسط برنامه کمکی signalr.exe نیز وجود دارد؛ برای دریافت آن می‌توان از طریق NuGet اقدام کرد (بسته Microsoft.AspNet.SignalR.Utils) که نهایتا در پوشه packages قرار خواهد گرفت. نحوه استفاده از آن نیز به صورت زیر است:
 Signalr.exe ghp http://localhost/
در این دستور ghp مخفف generate hub proxy است و نهایتا فایلی را به نام server.js تولید می‌کند.

ب) بدون استفاده از فایل proxy و به کمک روش late binding (انقیاد دیر هنگام)

برای کار با یک Hub از طریق jQuery مراحل ذیل باید طی شوند:
1) ارجاعی به Hub باید مشخص شود.
2) روال‌های رخدادگردان تنظیم گردند.
3) اتصال به Hub برقرار گردد.
4) متدی فراخوانی شود.

در اینجا باید دقت داشت که امکانات Hub به صورت خواص
 $.connection
در سمت کلاینت جی‌کوئری، در دسترس خواهند بود. برای مثال:
 $.connection.chatHub
و نام‌های بکارگرفته شده در اینجا مطابق روش‌های متداول نام گذاری در جاوا اسکریپت، camel case هستند.

خوب، تا اینجا فرض بر این است که یک پروژه خالی ASP.NET را آغاز و سپس فرمان نصب Microsoft.AspNet.SignalR.JS را نیز همانطور که عنوان شد، صادر کرده‌اید. در ادامه یک فایل ساده html را به نام chat.htm، به این پروژه جدید اضافه کنید (برای استفاده از کتابخانه جاوا اسکریپتی SignalR الزامی به استفاده از صفحات کامل پروژه‌های وب نیست).
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
    <script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {
            var chat;
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند
            chat = $.connection.chat; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است
            chat.client.hello = function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            };
            $.connection.hub.start(/*{ transport: 'longPolling' }*/); // فاز اولیه ارتباط را آغاز می‌کند

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                chat.server.sendMessage($("#txtMsg").val());
            });
        });
    </script>
</body>
</html>
کدهای آن‌را به نحو فوق تغییر دهید.
توضیحات:
همانطور که ملاحظه می‌کنید ابتدا ارجاعاتی به jquery و jquery.signalR-1.0.1.min.js اضافه شده‌اند. سپس نیاز است مسیر دقیق فایل پروکسی هاب خود را نیز مشخص کنیم. اینکار با تعریف مسیر signalr/hubs انجام شده است.
<script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
در ادامه توسط تنظیم connection.hub.logging سبب خواهیم شد تا اطلاعات بیشتری در javascript console مرورگر لاگ شود.
سپس ارجاعی به هاب تعریف شده، تعریف گردیده است. اگر از قسمت قبل به خاطر داشته باشید، توسط ویژگی HubName، نام chat را برگزیدیم. بنابراین connection.chat ذکر شده دقیقا به این هاب اشاره می‌کند.
سپس سطر chat.client.hello مقدار دهی شده است. متد hello، متدی dynamic و تعریف شده در سمت هاب برنامه است. به این ترتیب می‌توان به پیام‌های رسیده از طرف سرور گوش فرا داد. در اینجا، این پیام‌ها، به li ایی با id مساوی messages اضافه می‌شوند.
سپس توسط فراخوانی متد connection.hub.start، فاز negotiation شروع می‌شود. در اینجا حتی می‌توان نوع transport را نیز صریحا انتخاب کرد که نمونه‌ای از آن را به صورت کامنت شده جهت آشنایی با نحوه تعریف آن مشاهده می‌کنید. مقادیر قابل استفاده در آن به شرح زیر هستند:
 - webSockets
- forverFrame
- serverSentEvents
- longPolling
سپس به رویدادهای کلیک دکمه send گوش فرا داده و در این حین، اطلاعات TextBox ایی با id مساوی txtMsg را به متد SendMessage هاب خود ارسال می‌کنیم. همانطور که پیشتر نیز عنوان شد، در سمت کلاینت، تعریف متد SendMessage باید camel case باشد.

اکنون به صورت جداگانه یکبار برنامه hub را در مرورگر باز کنید. سپس بر روی فایل chat.htm کلیک راست کرده و گزینه مشاهده آن را در مرورگر نیز انتخاب نمائید (گزینه View in browser منوی کلیک راست).

خوب! پروژه کار نمی‌کند! برای اینکه مشکلات را بهتر بتوانید مشاهده کنید نیاز است به JavaScript Console مرورگر خود مراجعه نمائید. برای مثال در مرورگر کروم دکمه F12 را فشرده و برگه Console آن‌را باز کنید. در اینجا اعلام می‌کند که فاز negotiation قابل انجام نیست؛ چون مسیر پیش فرضی را که انتخاب کرده است، همین مسیر پروژه دومی است که اضافه کرده‌ایم (کلاینت ما در پروژه دوم قرار دارد و نه در همان پروژه اول هاب).
برای اینکه مسیر دقیق hub را در این حالت مشخص کنیم، سطر زیر را به ابتدای کدهای جاوا اسکریپتی فوق اضافه نمائید:
 $.connection.hub.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
اکنون اگر مجدا سعی کنید، باز هم برنامه کار نمی‌کند و پیام می‌دهد که امکان دسترسی به این سرویس از خارج از دومین آن میسر نیست. برای اینکه این مجوز را صادر کنیم نیاز است تنظیمات مسیریابی پروژه هاب را به نحو ذیل ویرایش نمائیم:
using System;
using System.Web;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;

namespace SignalR02
{
    public class Global : HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs(new HubConfiguration
            {
                EnableCrossDomain = true
            });
        }
    }
}
با تنظیم EnableCrossDomain به true اینبار فاز‌های آغاز ارتباط با سرور برقرار می‌شوند:
 SignalR: Auto detected cross domain url. jquery.signalR-1.0.1.min.js:10
SignalR: Negotiating with 'http://localhost:1072/signalr/negotiate'. jquery.signalR-1.0.1.min.js:10
SignalR: SignalR: Initializing long polling connection with server. jquery.signalR-1.0.1.min.js:10
SignalR: Attempting to connect to 'http://localhost:1072/signalr/connect?transport=longPolling&connectionToken…NRh72omzsPkKqhKw2&connectionData=%5B%7B%22name%22%3A%22chat%22%7D%5D&tid=3' using longPolling. jquery.signalR-1.0.1.min.js:10
SignalR: Longpolling connected jquery.signalR-1.0.1.min.js:10
مطابق این لاگ‌ها ابتدا فاز negotiation انجام می‌شود. سپس حالت long polling را به صورت خودکار انتخاب می‌کند.

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

یک نکته:
اگر از ویندوز 8 (یعنی IIS8) و VS 2012 استفاده می‌کنید، برای استفاده از حالت Web socket، ابتدا فایل وب کانفیگ برنامه را باز کرده و در قسمت httpRunTime، مقدار ویژگی targetFramework را بر روی 4.5 تنظیم کنید. اینبار اگر مراحل negotiation را بررسی کنید در همان مرحله اول برقراری اتصال، از روش Web socket استفاده گردیده است.


تمرین 1
به پروژه ساده و ابتدایی فوق یک تکست باکس دیگر به نام Room را اضافه کنید؛ به همراه دکمه join. سپس نکات قسمت قبل را در مورد الحاق به یک گروه و سپس ارسال پیام به اعضای گروه را پیاده سازی نمائید. (تمام نکات آن با مطلب فوق پوشش داده شده است و در اینجا باید صرفا فراخوانی متدهای عمومی دیگری در سمت هاب، صورت گیرد)

تمرین 2
در انتهای قسمت دوم به نحوه ارسال پیام از یک هاب به هابی دیگر اشاره شد. این MonitorHub را ایجاد کرده و همچنین یک کلاینت جاوا اسکریپتی را نیز برای آن تهیه کنید تا بتوان اتصال و قطع اتصال کلیه کاربران سیستم را مانیتور و مشاهده کرد.


پیاده سازی کلاینت jQuery بدون استفاده از کلاس Proxy

در مثال قبل، از پروکسی پویای مهیای در آدرس signalr/hubs استفاده کردیم. در اینجا قصد داریم، بدون استفاده از آن نیز کار برپایی کلاینت را بررسی کنیم.
بنابراین یک فایل جدید html را مثلا به نام chat_np.html به پروژه دوم برنامه اضافه کنید. سپس محتویات آن‌را به نحو زیر تغییر دهید:
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {                        
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند

            var connection = $.hubConnection();
            connection.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
            var proxy = connection.createHubProxy('chat');

            proxy.on('hello', function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            });

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                proxy.invoke('sendMessage', $("#txtMsg").val());
            });

            connection.start();
        });
    </script>
</body>
</html>
در اینجا سطر مرتبط با تعریف مسیر اسکریپت‌های پویای signalr/hubs را دیگر در ابتدای فایل مشاهده نمی‌کنید. کار تشکیل proxy اینبار از طریق کدنویسی صورت گرفته است. پس از ایجاد پروکسی، برای گوش فرا دادن به متدهای فراخوانی شده از طرف سرور از متد proxy.on و نام متد فراخوانی شده سمت سرور استفاده می‌کنیم و یا برای ارسال اطلاعات به سرور از متد proxy.invoke به همراه نام متد سمت سرور استفاده خواهد شد.


کلاینت‌های دات نتی SignalR
تا کنون Solution ما حاوی یک پروژه Hub و یک پروژه وب کلاینت جی‌کوئری است. به همین Solution، یک پروژه کلاینت کنسول ویندوزی را نیز اضافه کنید.
سپس در خط فرمان پاور شل نوگت دستور زیر را صادر نمائید تا فایل‌های مورد نیاز به پروژه کنسول اضافه شوند:
 PM> Install-Package Microsoft.AspNet.SignalR.Client
در اینجا نیز باید دقت داشت تا دستور بر روی default project صحیحی اجرا شود (حالت پیش فرض، اولین پروژه موجود در solution است).
پس از نصب آن اگر به پوشه packages مراجعه کنید، نگارش‌های مختلف آن‌را مخصوص سیلورلایت، دات نت‌های 4 و 4.5، WinRT و ویندوز فون8 نیز می‌توانید در پوشه Microsoft.AspNet.SignalR.Client ملاحظه نمائید. البته در ابتدای نصب، انتخاب نگارش مناسب، بر اساس نوع پروژه جاری به صورت خودکار صورت می‌گیرد.
مدل برنامه نویسی آن نیز بسیار شبیه است به حالت عدم استفاده از پروکسی در حین استفاده از jQuery که در قسمت قبل بررسی گردید و شامل این مراحل است:
1) یک وهله از شیء HubConnection را ایجاد کنید.
2) پروکسی مورد نیاز را جهت اتصال به Hub از طریق متد CreateProxy تهیه کنید.
3) رویدادگردان‌ها را همانند نمونه کدهای جاوا اسکریپتی قسمت قبل، توسط متد On تعریف کنید.
4) به کمک متد Start، اتصال را آغاز نمائید.
5) متدها را به کمک متد Invoke فراخوانی نمائید.

using System;
using Microsoft.AspNet.SignalR.Client.Hubs;

namespace SignalR02.WinClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var hubConnection = new HubConnection(url: "http://localhost:1072/signalr");
            var chat = hubConnection.CreateHubProxy(hubName: "chat");

            chat.On<string>("hello", msg => {
                Console.WriteLine(msg);
            });

            hubConnection.Start().Wait();

            chat.Invoke<string>("sendMessage", "Hello!");

            Console.WriteLine("Press a key to terminate the client...");
            Console.Read();
        }
    }
}
نمونه‌ای از این پیاده سازی را در کدهای فوق ملاحظه می‌کنید که از لحاظ طراحی آنچنان تفاوتی با نمونه ذهنی جاوا اسکریپتی ندارد.

نکته مهم
کلیه فراخوانی‌هایی که در اینجا ملاحظه می‌کنید غیرهمزمان هستند.
به همین جهت پس از متد Start، متد Wait ذکر شده‌است تا در این برنامه ساده، پس از برقراری کامل اتصال، کار invoke صورت گیرد و یا زمانیکه callback تعریف شده توسط متد chat.On فراخوانی می‌شود نیز این فراخوانی غیرهمزمان است و خصوصا اگر نیاز است رابط کاربری برنامه را در این بین به روز کنید باید به نکات به روز رسانی رابط کاربری از طریق یک ترد دیگر دقت داشت.
مطالب
استفاده از پروایدر SQLite در Entity Framework 7
Entity Framework در نگارش 7 خود از منابع داده‌ایی جدیدی پشتیبانی میکند(+) . یعنی از Windows Phone، Windows Store و همچنین ASP.NET 5 (اپلیکیشن‌هایی که از NET Core. استفاده می‌کنند) پشتیبانی خواهد کرد. در این نسخه از دیتابیس‌های non-relational نیز پشتیبانی می‌شود. پروایدر SQLite به صورت رسمی توسط تیم EF ارائه شده است که در ادامه نحوه‌ی استفاده از آن را در یک برنامه کنسول ساده بررسی خواهیم کرد.
کلاس‌های برنامه:
using Microsoft.Data.Entity;
using Microsoft.Data.Entity.Metadata;
using System.Collections.Generic;
using System.Linq;

namespace UsingEF7WithSQLite
{
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; set; }
    }

    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }
}
خب تا اینجا مدل‌های برنامه را تعریف کردیم، قدم بعدی افزودن پکیج مربوط به پروایدر SQLite به پروژه است، با دستور زیر این پکیج را نصب می‌کنیم:
PM> Install-Package EntityFramework.SQLite –Pre
اکنون کلاس کانتکست برنامه را به صورت زیر تعریف می‌کنیم:
namespace UsingEF7SQLiteProvider
{
    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptions builder)
        {
            builder.UseSQLite(@"Data Source=.\BloggingDatabae.db");
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<Blog>()
                .OneToMany(b => b.Posts, p => p.Blog)
                .ForeignKey(p => p.BlogId);

            // The EF7 SQLite provider currently doesn't support generated values
            // so setting the keys to be generated from developer code
            builder.Entity<Blog>()
                .Property(b => b.BlogId)
                .GenerateValueOnAdd(false);

            builder.Entity<Post>()
                .Property(b => b.BlogId)
                .GenerateValueOnAdd(false);
        }
    }
}

کار را با بازنویسی متد OnConfiguration شروع می‌کنیم، در این قسمت باید به EF بگوئیم که می‌خواهیم از SQLite استفاده کنیم برای اینکار از یک Extension Method با نام UseSQLite و پاس دادن کانکشتن استرینگ به آن استفاده می‌کنیم.
نکته: پروایدر فعلی SQLite در حال حاضر از Generated values پشتیبانی نمی‌کند، برای این منظور باید درون متد OnModelCreating این قابلیت را غیرفعال کنیم.
اکنون می‌توانیم از طریق پاورشل نیوگت دیتابیس را ایجاد کنیم، برای اینکار باید پکیج زیر را به پروژه اضافه کنید:
Install-Package EntityFramework.Commands -Pre
سپس دستورات زیر را اجرا می‌کنیم:
Add-Migration MyFirstMigration
Apply-Migration
توسط دستور Apply-Migrate دیتابیس برای شما ایجاد خواهد شد. البته این دستور زمانی استفاده می‌شود که برنامه شما یک اپلیکیشن دسکتاپ باشد. اگر اپلیکیشن شما یک Windows Phone Application است باید در زمان اجرای برنامه این کد را بنویسید:
using (var db = new BloggingContext())
{
   db.Database.AsMigrationsEnabled().ApplyMigrations();
}
در نهایت می‌توانید با دستور زیر از کانتکست برنامه استفاده کرده و خروجی را مشاهده کنید:
using (var db = new Models.BloggingContext())
{
     db.Blogs.Add(new Models.Blog { Url = "https://www.dntips.ir" });
     db.SaveChanges();

     foreach (var item in db.Blogs)
     {
         Console.WriteLine(item.Url);
     }
}
Console.ReadLine();
نظرات مطالب
نوشتن Middleware سفارشی در ASP.NET Core
یکی از روش‌های مقابله با مشکل فوق استفاده از کلاس SemaphoreSlim می باشد که در NET Framework 4.0 معرفی شده و در فضای نام  System.Threading در دسترس می‌باشد.
اگر اکشن متد‌های شما به صورت async await ایجاد کرده اید بهتر هست  ابتدا کلاس زیر را ایجاد نمایید:
using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace MyApp
{
    public class AsyncLock : IDisposable
    {
        private SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
 
        public async Task<AsyncLock> LockAsync()
        {
            await _semaphoreSlim.WaitAsync();
            return this;
        }
 
        public void Dispose()
        {
            _semaphoreSlim.Release();
        }
    }
}
سپس به صورت زیر از آن استفاده کنید:
private static readonly AsyncLock _mutex = new AsyncLock();
 
using(await _mutex.LockAsync())
{
    // Critical section... You can await here!
}
در این صورت تمامی درخواست‌های به سمت سرور به ترتیب اجرا خواهند شد و دیگر مشکل فوق را نخواهیم داشت.