اشتراک‌ها
C# در مرورگر با Blazor

 Blazor is the new Microsoft experimental framework that brings C# into any browser without a plug-in. It holds the promise of modern single-page applications, combined with the ability to use C# and its vast base-class library. Blazor takes C# development to a new level. It’s the final piece necessary to make the language a full-stack development tool. It will have all the power of the popular JavaScript frameworks, but based on the familiar languages, APIs and tooling of the Microsoft .NET Framework. 

C# در مرورگر با Blazor
نظرات مطالب
EF Code First #7
سلام.ممنون از توضیحات خوبتون.
من یک رابطه many-to-oneبین جداول Project و ProjectRow دارم که به این صورت map شده:
      HasOptional ( c => c.Project ).WithMany (c => c.ProjectRowCollection).HasForeignKey(c => c.ProjectID).WillCascadeOnDelete();

حالا وقتی میخوام یک ProjectRow  رو حذف کنم به این صورت عمل میکنم:
ProjectRowCollection.Remove(ProjectRowItem);
اما وقتی یک ردیف پروژه رو حذف میکنم به جای اینکه ردیف رو از جدول حذف کنه فقط کلید خارجی رو NULL میکنه مگر اینکه مستقیم از خود ProjectRow ردیف رو حذف کنم.مشکل از کجا میتونه باشه؟!
ممنون از اینکه وقت گذاشتید و خوندید. 
پاسخ به بازخورد‌های پروژه‌ها
همکاری
با سلام
لطفا ابتدا در مورد متدها توضیح دهید و کد آنها را ارسال کنید (یا یک patch تهیه کنید).
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 2 - بررسی ساختار جدید Solution
اگر یک پروژه‌ی خالی ASP.NET Core Web Application را شروع کنید (با طی مراحل زیر جهت ایجاد یک پروژه‌ی جدید):
 .NET Core -> ASP.NET Core Web Application (.NET Core) -> Select `Empty` Template
تغییرات ساختاری ASP.NET Core 1.0، با نگارش‌های قبلی ASP.NET، بسیار قابل ملاحظه هستند:


در اینجا نقش Solution همانند نگارش‌های قبلی ویژوال استودیو است: ظرفی است برای ساماندهی موارد مورد نیاز جهت تشکیل یک برنامه‌ی وب و شامل مواردی است مانند پروژه‌ها، تنظیمات آن‌ها و غیره. بنابراین هنوز در اینجا فایل sln. تشکیل می‌شود.


نقش فایل global.json

زمانیکه یک پروژه‌ی جدید ASP.NET Core 1.0 را آغاز می‌کنیم، ساختار پوشه‌های آن به صورت زیر هستند:


در اینجا هنوز فایل sln. قابل مشاهده است. همچنین در اینجا فایل جدیدی به نام global.json نیز وجود دارد، با این محتوا:
{
  "projects": [ "src", "test" ],
  "sdk": {
    "version": "1.0.0-preview2-003121"
  }
}
شماره نگارش ذکر شده‌ی در اینجا را در قسمت قبل بررسی کردیم.
خاصیت projects در اینجا به صورت یک آرایه تعریف شده‌است و بیانگر محل واقع شدن پوشه‌های اصلی پروژه‌ی جاری هستند. پوشه‌ی src یا source را در تصویر فوق مشاهده می‌کنید و محلی است که سورس‌های برنامه در آن قرار می‌گیرند. یک پوشه‌ی test نیز در اینجا ذکر شده‌است و اگر در حین ایجاد پروژه، گزینه‌ی ایجاد unit tests را هم انتخاب کرده باشید، این پوشه‌ی مخصوص نیز ایجاد خواهد شد.
نکته‌ی مهم اینجا است، هرکدی که درون پوشه‌های ذکر شده‌ی در اینجا قرار نگیرد، قابلیت build را نخواهد داشت. به عبارتی این نسخه‌ی از ASP.NET پوشه‌ها را قسمتی از پروژه به حساب می‌آورد. در نگارش‌های قبلی ASP.NET، مداخل تعریف فایل‌های منتسب به هر پروژه، درون فایلی با پسوند csproj. قرار می‌گرفتند. معادل این فایل در اینجا اینبار پسوند xproj را دارد و اگر آن‌را با یک ادیتور متنی باز کنید، فاقد تعاریف مداخل فایل‌های پروژه است.
در این نگارش جدید اگر فایلی را به پوشه‌ی src اضافه کنید یا حذف کنید، بلافاصله در solution explorer ظاهر و یا حذف خواهد شد.
یک آزمایش: به صورت معمول از طریق windows explorer به پوشه‌ی src برنامه وارد شده و فایل پیش فرض Project_Readme.html را حذف کنید. سپس به solution explorer ویژوال استودیو دقت کنید. مشاهده خواهید کرد که این فایل، بلافاصله از آن حذف می‌شود. در ادامه به recycle bin ویندوز مراجعه کرده و این فایل حذف شده را restore کنید تا مجددا به پوشه‌ی src برنامه اضافه شود. اینبار نیز افزوده شدن خودکار و بلافاصله‌ی این فایل را می‌توان در solution explorer مشاهده کرد.
بنابراین ساختار مدیریت فایل‌های این نگارش از ASP.NET در ویژوال استودیو، بسیار شبیه به ساختار مدیریت فایل‌های VSCode شده‌است که آن نیز بر اساس پوشه‌ها کار می‌کند و یک پوشه و تمام محتوای آن‌را به صورت پیش فرض به عنوان یک پروژه می‌شناسد. به همین جهت دیگر فایل csproj ایی در اینجا وجود ندارد و file system همان project system است.

یک نکته: در اینجا مسیرهای مطلق را نیز می‌توان ذکر کرد:
  "projects": [ "src", "test", "c:\\sources\\Configuration\\src" ],
اما در مورد هر مسیری که ذکر می‌شود، NET Core. باید بتواند یک سطح پایین‌تر از پوشه‌ی ذکر شده، فایل مهم project.json را پیدا کند؛ در غیراینصورت از آن صرفنظر خواهد شد. برای مثال برای مسیر نسبی src، مسیر src\MyProjectName\project.json را جستجو می‌کند و برای مسیر مطلق ذکر شده، این مسیر را c:\\sources\\Configuration\\src\\SomeName\\project.json


کامپایل خودکار پروژه در ASP.NET Core 1.0

علاوه بر تشخیص خودکار کم و زیاد شدن فایل‌های سیستمی پروژه، بدون نیاز به Add new item کردن آن‌ها در ویژوال استودیو، اگر سورس‌های برنامه را نیز تغییر دهید، فایل سورس جدیدی را اضافه کنید و یا فایل سورس موجودی را حذف کنید، کل پروژه به صورت خودکار کامپایل می‌شود و نیازی نیست این‌کار را به صورت دستی انجام دهید.
یک آزمایش: برنامه را از طریق منوی debug و گزینه‌ی start without debugging اجرا کنید. اگر برنامه را در حالت معمول debug->start debugging اجرا کنید، حالت کامپایل خودکار را مشاهده نخواهید کرد. در اینجا (پس از start without debugging) یک چنین خروجی را مشاهده خواهید کرد:


این خروجی حاصل اجرای کدهای درون فایل Startup.cs برنامه است:
 app.Run(async (context) =>
{
   await context.Response.WriteAsync("Hello World!");
});
اکنون در همین حال که برنامه در حال اجرا است و هنوز IIS Express خاتمه نیافته است، از طریق windows explorer، به پوشه‌ی src برنامه وارد شده و فایل Startup.cs را با notepad باز کنید. هدف این است که این فایل را در خارج از ویژوال استودیو ویرایش کنیم. اینبار سطر await دار را در notepad به نحو ذیل ویرایش کنید:
 await context.Response.WriteAsync("Hello DNT!");
پس از آن اگر مرورگر را refresh کنید، بلافاصله خروجی جدید فوق را مشاهده خواهید کرد که بیانگر کامپایل خودکار پروژه در صورت تغییر فایل‌های آن است.
این مساله قابلیت استفاده‌ی از ASP.NET Core را در سایر ادیتورهای موجود، مانند VSCode سهولت می‌بخشد.


نقش فایل project.json

فایل جدید project.json مهم‌ترین فایل تنظیمات یک پروژه‌ی ASP.NET Core است و مهم‌ترین قسمت آن، قسمت وابستگی‌های آن است:
"dependencies": {
  "Microsoft.NETCore.App": {
    "version": "1.0.0",
    "type": "platform"
  },
  "Microsoft.AspNetCore.Diagnostics": "1.0.0",
  "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
  "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
  "Microsoft.Extensions.Logging.Console": "1.0.0"
},
همانطور که در قسمت قبل نیز عنوان شد، در این نگارش از دات نت، تمام وابستگی‌های پروژه از طریق نیوگت تامین می‌شوند و دیگر خبری از یک دات نت «بزرگ» که شامل تمام اجزای این مجموعه‌است نیست. این مساله توزیع برنامه را ساده‌تر کرده و همچنین امکان به روز رسانی سریع‌تر این اجزا را توسط تیم‌های مرتبط فراهم می‌کند؛ بدون اینکه نیازی باشد تا منتظر یک توزیع «بزرگ» دیگر ماند.
در نگارش‌های قبلی ASP.NET، فایلی XML ایی به نام packages.config حاوی تعاریف مداخل بسته‌های نیوگت برنامه بود. این فایل در اینجا جزئی از محتوای فایل project.json در قسمت dependencies آن است.
با قسمت وابستگی‌های این فایل، به دو طریق می‌توان کار کرد:
الف) ویرایش مستقیم این فایل که به همراه intellisense نیز هست. با افزودن مداخل جدید به این فایل و ذخیره کردن آن، بلافاصله کار restore و دریافت و نصب آن‌ها آغاز می‌شود:


ب) از طریق NuGet package manager
روش دیگر کار با وابستگی‌ها، کلیک راست بر روی گره references و انتخاب گزینه‌ی manage nuget packages است:


برای نمونه جهت نصب ASP.NET MVC 6 این مراحل باید طی شوند:


ابتدا برگه‌ی browse را انتخاب کنید و سپس تیک مربوط به include prerelease را نیز انتخاب نمائید.
البته بسته‌ی اصلی MVC در اینجا Microsoft.AspNetCore.Mvc نام دارد و نه MVC6.

اینبار بسته‌هایی که restore می‌شوند، در مسیر اشتراکی C:\Users\user_name\.nuget\packages ذخیره خواهند شد.


یک نکته‌ی مهم:
قرار هست در نگارش‌های پس از RTM، فایل‌های project.json و xproj را جهت سازگاری با MSBuild، اندکی تغییر دهند (که این تغییرات به صورت خودکار توسط VS.NET انجام می‌شود). اطلاعات بیشتر


انتخاب فریم ورک‌های مختلف در فایل project.json

در قسمت قبل عنوان شد که ASP.NET Core را می‌توان هم برفراز NET Core. چندسکویی اجرا کرد و هم NET 4.6. مختص به ویندوز. این انتخاب‌ها در قسمت frameworks فایل project.json انجام می‌شوند:
"frameworks": {
  "netcoreapp1.0": {
    "imports": [
      "dotnet5.6",
      "portable-net45+win8"
    ]
  }
},
در اینجا، netcoreapp1.0 به معنای برنامه‌‌ای است که برفراز NET Core. اجرا می‌شود. نام پیشین آن dnxcore50 بود (اگر مقالات قدیمی‌تر پیش از RTM را مطالعه کنید).
در اینجا اگر علاقمند بودید که از دات نت کامل مخصوص ویندوز نیز استفاده کنید، می‌توانید آن‌را در لیست فریم ورک‌ها اضافه نمائید (در این مثال، دات نت کامل 4.5.2 نیز ذکر شده‌است):
 "frameworks": {
    "netcoreapp1.0": {
    },
    "net452": {
    }
  }
لیست کامل این اسامی را در مستندات NET Starndard می‌توانید مطالعه کنید و خلاصه‌ی آن به این صورت است:
- “netcoreapp1.0” برای معرفی و استفاده‌ی از NET Core 1.0. بکار می‌رود.
- جهت معرفی فریم ورک‌های کامل و ویندوزی دات نت، اسامی “net45”, “net451”, “net452”, “net46”, “net461” مجاز هستند.
-  “portable-net45+win8” برای معرفی پروفایل‌های PCL یا portable class libraries بکار می‌رود.
- “dotnet5.6”, “dnxcore50” برای معرفی نگارش‌های پیش نمایش NET Core.، پیش از ارائه‌ی نگارش RTM استفاده می‌شوند.
- “netstandard1.2”, “netstandard1.5” کار معرفی برنامه‌های NET Standard Platform. را انجام می‌دهند.

بر این مبنا، dotnet5.6 ذکر شده‌ی در قسمت تنظیمات نگارش RTM، به این معنا است که قادر به استفاده‌ی از بسته‌های نیوگت و کتابخانه‌های تولید شده‌ی با نگارش‌های RC نیز خواهید بود (هرچند برنامه از netcoreapp1.0 استفاده می‌کند).

یک مثال: قسمت فریم ورک‌های فایل project.json را به نحو ذیل جهت معرفی دات نت 4.6.1 تغییر دهید:
"frameworks": {
  "netcoreapp1.0": {
      "imports": [
          "dotnet5.6",
          "portable-net45+win8"
      ]
  },
  "net461": {
      "imports": [
          "portable-net45+win8"
      ],
      "dependencies": {
      }
  }
},
لیست وابستگی‌های خاص این فریم ورک در خاصیت dependencies آن قابل ذکر است.
در این حالت پس از ذخیره‌ی فایل و شروع خودکار بازیابی وابستگی‌ها، با پیام خطای Package Microsoft.NETCore.App 1.0.0 is not compatible with net461 متوقف خواهید شد.
برای رفع این مشکل باید وابستگی Microsoft.NETCore.App را حذف کنید، چون با net461 سازگاری ندارد
 "dependencies": {
 //"Microsoft.NETCore.App": {
 //  "version": "1.0.0",
 //  "type": "platform"
 //},

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


فایل project.lock.json چیست؟


ذیل فایل project.json، فایل دیگری به نام project.lock.json نیز وجود دارد. اگر به محتوای آن دقت کنید، این فایل حاوی لیست دقیق بسته‌های نیوگت مورد استفاده‌ی توسط برنامه است و الزاما با آن‌چیزی که در فایل project.json قید شده، یکی نیست. از این جهت که در فایل project.json، قید می‌شود netcoreapp1.0؛ ولی این netcoreapp1.0 دقیقا شامل چه بسته‌هایی است؟ لیست کامل آن‌ها را در این فایل می‌توانید مشاهده کنید.
در ابتدای این فایل یک خاصیت locked نیز وجود دارد که مقدار پیش فرض آن false است. اگر به true تنظیم شود، در حین restore وابستگی‌های برنامه، تنها از نگارش‌های ذکر شده‌ی در این فایل استفاده می‌شود. از این جهت که در فایل project.json می‌توان شماره نگارش‌ها را با * نیز مشخص کرد؛ مثلا *.1.0.0


پوشه‌ی جدید wwwroot و گره dependencies

یکی از پوشه‌های جدیدی که در ساختار پروژه‌ی ASP.NET Core معرفی شده‌است، wwwroot نام دارد:


از دیدگاه هاستینگ برنامه، این پوشه، پوشه‌ای است که در معرض دید عموم قرار می‌گیرد (وب روت). برای مثال فایل‌های ایستای اسکریپت، CSS، تصاویر و غیره باید در این پوشه قرار گیرند تا توسط دنیای خارج قابل دسترسی و استفاده شوند. بنابراین سورس کدهای برنامه خارج از این پوشه قرار می‌گیرند.
گره dependencies که ذیل پوشه‌ی wwwroot قرار گرفته‌است، جهت مدیریت این وابستگی‌های سمت کلاینت برنامه است. در اینجا می‌توان از NPM و یا Bower برای دریافت و به روز رسانی وابستگی‌های اسکریپتی و شیوه‌نامه‌های برنامه کمک گرفت (علاوه بر نیوگت که بهتر است صرفا جهت دریافت وابستگی‌های دات نتی استفاده شود).
یک مثال: فایل جدیدی را به نام bower.json به پروژه‌ی جاری با این محتوا اضافه کنید:
{
  "name": "asp.net",
  "private": true,
  "dependencies": {
    "bootstrap": "3.3.6",
    "jquery": "2.2.0",
    "jquery-validation": "1.14.0",
    "jquery-validation-unobtrusive": "3.2.6"
  }
}
این نام‌ها استاندارد هستند. برای مثال اگر قصد استفاده‌ی از npm مربوط به node.js را داشته باشید، نام این فایل packag.json است (با ساختار خاص خودش) و هر دوی این‌ها را نیز می‌توانید با هم اضافه کنید و از این لحاظ محدودیتی وجود ندارد.
پس از اضافه شدن فایل bower.json، بلافاصله کار restore بسته‌ها از اینترنت شروع می‌شود:


و یا با کلیک راست بر روی گره dependencies، گزینه‌ی restore packages نیز وجود دارد.
فایل‌های نهایی دریافت شده را در پوشه‌ی bower_components خارج از wwwroot می‌توانید مشاهده کنید.

در مورد نحوه‌ی توزیع و دسترسی به فایل‌های استاتیک یک برنامه‌ی ASP.NET Core 1.0، نکات خاصی وجود دارند که در قسمت‌های بعد، بررسی خواهند شد.

یک نکته: اگر خواستید نام پوشه‌ی wwwroot را تغییر دهید، فایل جدیدی را به نام hosting.json با این محتوا به پروژه اضافه کنید:
{
    "webroot":"AppWebRoot"
}
در اینجا AppWebRoot نام دلخواه پوشه‌ی جدیدی است که نیاز است به ریشه‌ی پروژه اضافه نمائید تا بجای wwwroot پیش فرض استفاده شود.


نقطه‌ی آغازین برنامه کجاست؟

اگر به فایل project.json دقت کنید، چنین تنظیمی در آن موجود است:
"buildOptions": {
  "emitEntryPoint": true,
  "preserveCompilationContext": true
},
true بودن مقدار تولید entry point به استفاده‌ی از فایلی به نام Program.cs بر می‌گردد؛ با این محتوا:
public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseIISIntegration()
        .UseStartup<Startup>()
        .Build();
 
    host.Run();
}
به این ترتیب، یک برنامه‌ی ASP.NET Core، دقیقا همانند سایر برنامه‌های NET Core. رفتار می‌کند و در اساس یک برنامه‌ی کنسول است.
 var outputKind = compilerOptions.EmitEntryPoint.GetValueOrDefault() ?
OutputKind.ConsoleApplication : OutputKind.DynamicallyLinkedLibrary;
این چند سطر، قسمتی از سورس کد ASP.NET Core 1.0 هستند. به این معنا که اگر مقدار خاصیت emitEntryPoint مساوی true بود، با این برنامه به شکل یک برنامه‌ی کنسول رفتار کن و در غیر اینصورت یک Dynamically Linked Library خواهد بود.
در فایل Program.cs تنظیماتی را مشاهده می‌کنید، در مورد راه اندازی Kestler که وب سرور بسیار سریع و چندسکویی کار با برنامه‌های ASP.NET Core 1.0 است و قسمت مهم دیگر آن به استفاده‌ی از کلاس Startup بر می‌گردد (()<UseStartup<Startup). این کلاس را در فایل جدید Startup.cs می‌توانید ملاحظه کنید که کار تنظیمات آغازین برنامه را انجام می‌دهد. اگر پیشتر با OWIN، در نگارش‌های قبلی ASP.NET کار کرده باشید، قسمتی از این فایل برای شما آشنا است:
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    }
}
در اینجا امکان دسترسی به تنظیمات برنامه، معرفی سرویس‌ها، middleware‌ها و غیره وجود دارند که هرکدام، در قسمت‌های آتی به تفصیل بررسی خواهند شد.


نقش فایل launchsetting.json


محتویات پیش فرض این فایل برای قالب empty پروژه‌های ASP.NET Core 1.0 به صورت ذیل است:
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:7742/",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "Core1RtmEmptyTest": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
همانطور که مشاهده می‌کنید، در اینجا تنظیمات IIS Express قابل تغییر هستند. برای مثال port پیش فرض برنامه 7742 تنظیم شده‌است.
پروفایل‌هایی که در اینجا ذکر شده‌اند، در تنظیمات پروژه نیز قابل مشاهده هستند: (کلیک راست بر روی پروژه و مشاهده‌ی properties آن و یا دوبار کلیک بر روی گره properties)


به علاوه امکان انتخاب این پروفایل‌ها در زمان آغاز برنامه نیز وجود دارند:


نکته‌ی مهم تمام این موارد به قسمت environment variable قابل مشاهده‌ی در تصاویر فوق بر می‌گردد. این متغیر محیطی می‌تواند سه مقدار Development ، Staging و Production را داشته باشد و بر اساس این متغیر و مقدار آن، می‌توان پروفایل جدیدی را تشکیل داد. زمانیکه برنامه بر اساس پروفایل خاصی بارگذاری می‌شود، اینکه چه متغیر محیطی انتخاب شده‌است، در کلاس Startup قابل استخراج و بررسی بوده و بر این اساس می‌توان اقدامات خاصی را انجام داد. برای مثال تنظیمات خاصی را بارگذاری کرد و یا صفحات ویژه‌ای را فعال و غیرفعال کرد (این موارد را در قسمت‌های بعدی مرور می‌کنیم).
همچنین در اینجا به ازای هر پروفایل مختلف می‌توان Url آغازین یا launch url و پورت آن‌را مجزا درنظر گرفت و یا از وب سرور دیگری استفاده کرد.
مسیرراه‌ها
ASP.NET MVC
              مطالب
              آزمون واحد Entity Framework به کمک چارچوب تقلید
              در باب ضرورت نوشتن کدهای تست پذیر، توسعه کلاس‌های کوچک تک مسئولیتی و اهمیت تزریق وابستگی‌ها بارها و بارها بحث شده و مطلب نوشته شده است. این روز‌ها کم پیش میاید که نرم افزاری توسعه داده شود و از پایگاه داده به جهت ذخیره و بازیابی داده‌ها استفاده نکند. با گسترش و رواج ORM ها، نوشتن کدهای دسترسی به داده‌ها سهولت یافته است و استفاده از ORM در لایه‌ی سرویس که نگهدارنده‌ی منطق تجاری برنامه است، امری اجتناب ناپذیر می‌باشد. 
              در این مطلب نحوه‌ی نوشتن آزمون واحد برای کلاس سرویسی که وابسته به DbContext می‌باشد، به همراه محدودیت‌ها شرح داده می‌شود.
              ابتدا یک روش که که در آن مستقیما از DbContext در سرویس استفاده شده را بررسی میکنیم. در مثال زیر کلاس ProductService وظیفه‌ی برگرداندن لیست کالاها را به ترتیب نام دارد. در آن DbContext مستقیما وهله سازی شده و از آن جهت انجام تراکنش‌های دیتابیس کمک گرفته شده است:
                  public class ProductService
                  {
                      public IEnumerable<Product> GetOrderedProducts()
                      {
                          using (var ctx = new Entites())
                          {
                              return ctx.Products.OrderBy(x => x.Name).ToList();
                          }
                      }
                  }

              برای این کلاس نمی‌توان Unit Test نوشت چرا که یک وابستگی به شی DbContext دارد و این وابستگی مستقیما درون متد GetOrderedProducts  نمونه سازی شده است. در مطالب پیشین شرح داده شد که برای تست پذیر کردن کدها باید این وابستگی‌ها را از بیرون، در اختیار کلاس مورد نظر قرار داد.
              برای نوشتن تست برای کلاس ProductService حداقل دو روش در اختیار است:
              - نوشتن Integration Test:
              یعنی کلاس جاری را به همین شکل نگاه داریم و در تست، مستقیما به یک پایگاه داده که به منظور تست فراهم شده وصل شویم. برای سهولت مدیریت پایگاه داده می‌توان عمل درج را در یک Transaction قرار داد و پس از پایان یافتن تست Transaction را RollBack کرد. این روش مورد بحث مطلب جاری نمی‌باشد، لطفا برای آشنایی این دو مطلب را مطالعه بفرمایید:
              - بهره جستن از تزریق وابستگی و نوشتن Unit Test که وابستگی به دیتابیس ندارد
              یکی از قانون‌های یک آزمون واحد این است که وابستگی به منابع خارجی مثل پایگاه داده نداشته باشد. این مطلب نحوه‌ی صحیح پیاده سازی الگوی Unit of Work را شرح داده است. بعد از پیاده سازی Unit Of Work، کلاس DbContext به شرح زیر می‌شود. همانطور که مشاهده می‌کنید، اکنون DbContext یک Interface را پیاده سازی کرده است.
                  public interface IUnitOfWork
                  {
                      IDbSet<TEntity> Set<TEntity>() where TEntity : class;
                      int SaveAllChanges();
                  }
              
                  public class Entites : DbContext, IUnitOfWork
                  {
                      public virtual DbSet<Product> Products { get; set; }  // This is virtual because Moq needs to override the behaviour 
              
                      public new virtual IDbSet<TEntity> Set<TEntity>() where TEntity : class   // This is virtual because Moq needs to override the behaviour 
                      {
                          return base.Set<TEntity>();
                      }
              
                      public int SaveAllChanges()
                      {
                          return base.SaveChanges();
                      }
                  }
              در این حالت می‌توان به جای وهله سازی مستقیم DbContext در ProductService آن را خارج از کلاس سرویس در اختیار استفاده کننده قرار داد:
                  public class ProductService
                  {
                      private readonly IDbSet<Product> _products;
                      private readonly IUnitOfWork _uow;
                      public ProductService(IUnitOfWork uow)
                      {
                          _uow = uow;
                          _products = _uow.Set<Product>();
                      }
                   public IEnumerable<Product> GetOrderedProducts()
                      {
                          return _products.OrderBy(x => x.Name).ToList();
                      }
                  }
              همانطور که مشاهده می‌کنید، الان IUnitOfWork به کلاس سرویس تزریق شده و در متدها، خبری از وهله سازی یک وابستگی (DbContext) نمی‌باشد.
              اکنون برای تست این سرویس می‌توان پیاده سازی دیگری را از IUnitOfWork انجام داد و در کدهای تست به سرویس مورد نظر تزریق کرد. برای سهولت این امر قصد داریم از moq به عنوان  چارچوب تقلید (Mocking framework) استفاده کنیم. برای  نصب moq  می توان از  بسته‌ی نیوگت آن بهره جست. پیشتر  مطلبی  در رابطه با چارچوب‌های تقلید در سایت نوشته شده است.
              با توجه به اینکه PoductService به دیتابیس وابستگی دارد، مقصود این است که این وابستگی با ایجاد یک نمونه‌ی mock از IUnitOfWork حذف شود. برای این منظور در سازنده‌ی کلاس، تعدادی کالای درون حافظه ایجاد شده و به صورت IQueryable جایگزین DbSet شده است.
              اگر به تعریف کلاس Entities که همان DbContext می‌باشد دقت کنید، مشاهده می‌شود که Products و تابع Set، هر دو به صورت Virtual تعریف شده اند. برای تغییر رفتار DbContext نیاز است در آزمون واحد، این دو با داده‌های درون حافظه کار کنند و رفتار آنها قرار است عوض شود. این تغییر رفتار از طریق چند ریختی (Polymorphism) خواهد بود.
              کلاس تست در نهایت اینگونه تعریف می‌شود:
                 [TestFixture]
                  public class ProductServiceTest
                  {
                      private readonly ProductService _productService;
                      public ProductServiceTest()
                      {
                          IQueryable<Product> data = GetRoadNetworks().AsQueryable();
                          var mockSet = new Mock<DbSet<Product>>();
                          mockSet.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(data.Provider);
                          mockSet.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(data.Expression);
                          mockSet.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(data.ElementType);
                          mockSet.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
                          var context = new Mock<Entites>();
                          context.Setup(c => c.Products).Returns(mockSet.Object);
                          context.Setup(m => m.Set<Product>()).Returns(mockSet.Object);
                          _productService = new ProductService(context.Object);
                      }
                      private IEnumerable<Product> GetRoadNetworks()
                      {
                          return new List<Product>
                          {
                              new Product
                              {
                                  Id = 1,
                                  Name = "A"
                              },
                              new Product
                              {
                                  Id = 2,
                                  Name = "B"
                              },
                              new Product
                              {
                                  Id = 3,
                                  Name = "C"
                              }
                          };
                      }
                      [Test]
                      public void GetOrderedProductTest()
                      {
                          IEnumerable<Product> products = _productService.GetOrderedProducts();
                          List<string> names = products.Select(x => x.Name).ToList();
                          var expected = new List<string> {"A", "B", "C"};
                          CollectionAssert.AreEqual(names, expected);
                      }
                  }
              همانطور که مشاهده می‌شود، در سازنده‌ی کلاس تست، یک منبع داده‌ی درون حافظه‌ای به صورت IQueryable تولید شده و پیاده سازی‌های تقلیدی از DbContext به همراه تابع Set و همچنین DbSet کالا‌ها به کمک Moq ایجاد گردیده و در اختیار ProductService قرار داده شده است.
              در نهایت، در یک تست تلاش شده است تا منطق متد GerOrderedProducts مورد آزمون قرار گیرد.
              محدودیت این روش:
              با اینکه LINQ یک روش و سینتکس یکتا برای دسترسی به منابع داده‌ای مختلف را محیا می‌کند، اما این الزامی برای یکسان بودن نتایج، هنگام استفاده از Provider‌های مختلف LINQ نمی‌باشد. در تست نوشته شده از LINQ To Objects برای کوئری گرفتن از منبع داده استفاده شده است؛ در صورتیکه در برنامه‌ی اصلی از LINQ To Entities استفاده می‌شود و الزامی نیست که یک کوئری LINQ در دو Provider متفاوت یک رفتار را داشته باشد.
              این نکته در قسمت Limitations of EF in-memory test doubles این مطلب هم شرح داده شده است.
              در نهایت این پرسش به وجود می‌آید که با وجود محدودیت ذکر شده، از این روش استفاده شود یا خیر؟ پاسخ این پرسش، بسته به هر سناریو، متفاوت است.
              به عنوان نمونه اگر در یک سناریو داده‌ها با یک کوئری نه چندان پیچیده از منبع داده ای گرفته می‌شود و اعمال دیگری دیگری روی نتیجه‌ی کوئری درون حافظه انجام می‌شود می‌توان این روش را قابل اعتماد قلمداد کرد.
              برای مطالعه‌ی بیشتر مطالب متعددی در سایت در رابطه با تزریق وابستگی و آزمون‌های واحد نوشته شده است.
              مطالب
              پشتیبانی آنلاین سایت با SignalR ،ASP.NET MVC و AngularJS
                پشتیبانی آنلاین سایت، روشی مناسب برای افزایش سطح تماس مشتریان با فروشندگان، برای جلوگیری از اتلاف وقت در برقراری تماس میباشد.
              قصد داریم در این بخش پشتیبانی آنلاین سایت را با استفاده از AngularJs /Asp.Net Mvc / Signalr تهیه کنیم.
              امکانات این برنامه:
              * امکان مکالمه متنی به همراه ارسال شکلک
              * امکان انتقال مکالمه
              * مشاهده آرشیو گفتگوها
              * امکان ارسال فایل (بزودی)
              * امکان ذخیره گفتگو و ارسال گفتگو به ایمیل  (بزودی)
              * امکان ارسال تیکت در صورت آفلاین بودن کارشناسان (بزودی) 
              * رعایت مسائل امنیتی(بزودی)

              مراحل نحوه اجرای برنامه:
              1-  باز کردن دو tab، یکی برای کارشناس یکی  برای مشتری .
              2-  تعدادی کارشناس تعریف شده است که با کلیک بر روی هر کدام وارد پنل کارشناس خواهیم شد.
              3- شروع مکالمه توسط مشتری با کلیک بر روی chatbox پایین صفحه (سمت راست پایین).
              4- شروع کردن مکالمه توسط کارشناس. 
              5- ادامه،خاتمه یا انتقال مکالمه توسط کارشناس.

              نصب کتابخانه‌های زیر:
              //client
              Install-Package angularjs 
              Install-Package angular-strap 
              Install-Package Microsoft.AspNet.SignalR.JS 
              install-package AngularJs.SignalR.Hub 
              Install-Package jQuery.TimeAgo
              Install-Package FontAwesome
              Install-Package toastr
              Install-Package Twitter.Bootstrap.RTL 
              bower install angular-smilies  
              
              //server
              Install-Package Newtonsoft.Json
              Install-Package Microsoft.AspNet.SignalR 
              Install-Package EntityFramework

              گام‌های برنامه:
              1-ایجاد جداول 
              جدول Message: هر پیام دارای فرستنده و گیرنده‌ای، به همراه زمان ارسال میباشد.
              جدول Session: شامل لیستی از پیام‌ها به همراه ارجاعی به خود (استفاده هنگام انتقال مکالمه )
               public partial class Message
                  {
                      public int Id { get; set; }
                      public string Sender { get; set; }
                      public string Receiver { get; set; }
                      public string Body { get; set; }
                      public DateTimeOffset? CreationTime { get; set; }
                      public int? SessionId { get; set; }
                      public virtual Session Session { get; set; }
                  }
                  public partial class Session
                  {
                      public Session()
                      {
                         Messages = new List<Message>();
                         Sessions = new List<Session>();
                      }
                      public int Id { get; set; }
                      public string AgentName { get; set; }
                      public string CustomerName { get; set; }
                      public DateTime CreatedDateTime { get; set; }
                      public int? ParentId { get; set; }
                      public virtual Session Parent { get; set; }
                      public virtual ICollection<Message> Messages { get; set; }
                      public virtual ICollection<Session> Sessions { get; set; }
                  }

              2- ایجاد ویو مدلهای زیر
                  public class UserInformation
                  {
                      public string ConnectionId { get; set; }
                      public bool IsOnline { get; set; }
                      public string UserName { get; set; }
                  }
                  public class ChatSessionVm
                  {
                      public string Key { get; set; }
                      public List<string> Value { get; set; }
                  }
                  public class AgentViewModel
                  {
                      public int Id { get; set; }
                      public string CustomerName { get; set; }
                      public int Lenght { get; set; }
                      public DateTimeOffset? Date { get; set; }
                  }

              3- ایجاد Hub در سرور
               [HubName("chatHub")]
                  public class ChatHub : Microsoft.AspNet.SignalR.Hub
                  {
                  }

              4- فراخوانی chathub توسط کلاینت: برای آشنایی با سرویس  hub کلیک نمایید.

              listeners متدهای سمت کلاینت
              methods آرایه ای از متدهای سمت سرور

               $scope.myHub = new hub("chatHub", {
                listeners: {},
                methods: []
              })
              در صورت موفقیت آمیز بودن اتصال به هاب، متد init سمت سرور فراخوانی میشود و وضعیت آنلاین بودن کارشناسان برای کلاینت مشخص میشود.
               $scope.myHub.promise.done(function () {
                   $scope.myHub.init();
                   $scope.myHub.promise.done(function () { });
                });
               public void Init()
                      {
                          _chatSessions = _chatSessions ?? (_chatSessions = new List<ChatSessionVm>());
                          _agents = _agents ?? (_agents = new ConcurrentDictionary<string, UserInformation>());
                          Clients.Caller.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                      }

              5-وضعیت کارشناسان :
              در صورت آنلاین بودن کارشناسان: ارسال اولین پیام و تقاضای شروع مکالمه
              در صورت آفلاین بودن کارشناسان: ارسال تیکت(بزودی)
              اگر برای اولین بار  پیامی را ارسال میکنید، برای شما session ایی ایجاد نشده است. در اینصورت مکان تقاضای مشتری از سایت http://ipinfo.io دریافت شده و به سرور ارسال می‌گردد و  متد logvist سرور، تقاضای شروع مکالمه مشتری را به اطلاع  تمام کارشناسان میرساند و وضعیت chatbox را تغییر میدهد.
              اگر session برای مشتری تعریف شده باشد، مکالمه مشتری با کارشناس مربوطه انجام میگردد.
               $scope.requestChat = function (msg) {
                              if (!defaultCustomerUserName) {
                                  //گرفتن کاربر لاگین شده
                                  //ما از آرایه تصادفی استفاده میکنیم
                                  var nameDefaultArray = [
                                      'حسین', 'حسن', 'علی', 'عباس', 'زهرا', 'سمیه'
                                  ];
                                  defaultCustomerUserName=nameDefaultArray[Math.floor(Math.random() * nameDefaultArray.length)];
                              }
                              var userName = defaultCustomerUserName;
                              if (!$scope.chatId) {
                                  $scope.chatId = sessionStorage.getItem(chatKey);
                                  $http.get("http://ipinfo.io")
                                    .success(function (response) {
                                        $scope.myHub.logVisit(response.city, response.country, msg, userName);
                                    }).error(function (e, status, headers, config) {
                                        $scope.myHub.logVisit("Tehran", "Ir", msg, userName)
                                    });
                                  $scope.myHub.requestChat(msg);
                                  $scope.chatTitle = $scope.options.waitingForOperator;
                                  $scope.pendingRequestChat = true;
                              } else {
                                  $scope.myHub.clientSendMessage(msg, userName);
                              };
                              $scope.message = "";
                          };

              6-مشاهده تقاضای مکالمه کاربران  توسط کارشناسان
              کارشناسان در صورت تمایل، شروع به مکالمه با کاربر مینمایند و مکالمه آغاز میگردد.با شروع مکالمه توسط کارشناس، متد acceptRequestChat  سرور فراخوانی میشود.
               پیام‌های مناسب برای کارشناس مربوطه، برای مشتری و تمام کارشناسان (به تمام کارشناسان اطلاع داده می‌شود که مشتری با چه کارشناسی در حال مکالمه میباشد) ارسال میگردد و مقادیر مربوطه در دیتابیس ذخیره میگردد.
              public void AcceptRequestChat(string customerConnectionId, string body, string userName)
                      {
                          var agent = FindAgent(Context.ConnectionId);
                          var session = _chatSessions.FirstOrDefault(item => item.Key.Equals(agent.Key));
                          if (session == null)
                          {
                              _chatSessions.Add(new ChatSessionVm
                              {
                                  Key = agent.Key,
                                  Value = new List<string> { customerConnectionId }
                              });
                          }
                          else
                          {
                              session.Value.Add(customerConnectionId);
                          }
                          Clients.Client(Context.ConnectionId).agentChat(customerConnectionId, body, userName);
                          Clients.Client(customerConnectionId).clientChat(customerConnectionId, agent.Value.UserName);
                          foreach (var item in _agents.Where(item => item.Value.IsOnline))
                          {
                              Clients.Client(item.Value.ConnectionId).refreshChatWith(agent.Value.UserName, customerConnectionId);
                          }
                     var session = _db.Sessions.Add(new Session
                          {
                              AgentName = agent.Key,
                              CustomerName = userName,
                              CreatedDateTime = DateTime.Now
                          });
                          _db.SaveChanges();
              
                          var message = new Message
                          {
                              CreationTime = DateTime.Now,
                              Sender = agent.Key,
                              Receiver = userName,
                              body=body,
                              Session = session
                          };
                          _db.Messages.Add(message);
                          _db.SaveChanges();
                      }
              7-خاتمه مکالمه توسط کارشناس یا مشتری امکان پذیر میباشد:
              متد closeChat  سرور فراخوانی میگردد. پیام مناسبی به مشتری و تمام کارشناسان ارسال میگردد.
              public void CloseChat(string id)
                      {
                          var findAgent = FindAgent(Context.ConnectionId);
                          var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
                          if (session == null) return;
                          Clients.Client(id).clientAddMessage(findAgent.Key, "مکالمه شما با کارشناس مربوطه به اتمام رسیده است");
              
                          foreach (var agent in _agents)
                          {
                              Clients.Client(agent.Value.ConnectionId).refreshLeaveChat(agent.Value.UserName, id);
                          }
                          _chatSessions.Remove(session);
                      }

              8-انتقال مکالمه مشتری به کارشناسی دیگر
              مکالمه از کارشناس فعلی گرفته شده و به کارشناس جدید داده می‌شود؛ به همراه ارسال پیام‌های مناسب به طرف‌های مربوطه
                 public void EngageVisitor(string newAgentId, string cumtomerId, string customerName,string clientSessionId)
                      {
                          #region remove session of current agent
                          var currentAgent = FindAgent(Context.ConnectionId);
                          var currentSession = _chatSessions.FirstOrDefault(item => item.Value.Contains(cumtomerId));
                          if (currentSession != null)
                          {
                              _chatSessions.Remove(currentSession);
                          }
                          #endregion
              
                          #region add  session to new agent
                          var newAgent = FindAgent(newAgentId);
                          var newSession = _chatSessions.FirstOrDefault(item => item.Key.Equals(newAgent.Key));
                          if (newSession == null)
                          {
                              _chatSessions.Add(new ChatSessionVm
                              {
                                  Key = newAgent.Key,
                                  Value = new List<string> { cumtomerId }
                              });
                          }
                          else
                          {
                              newSession.Value.Add(cumtomerId);
                          }
                          #endregion
              
                          Clients.Client(currentAgent.Value.ConnectionId).addMessage(cumtomerId, newAgent.Key,
                              "ادامه مکالمه به کارشناس  " + newAgent.Key + "مقابل  منتقل شد");
                          Clients.Client(newAgentId).addMessage(cumtomerId, currentAgent.Key,
                              "لطفا مکالمه را ادامه دهید.با تشکر");
              
                          Clients.Client(cumtomerId).clientAddMessage(newAgent.Value.UserName,
                              "مکالمه شما با کارشناس زیر برقرار گردید" + newAgent.Key);
              
                          var session = _db.Sessions.FirstOrDefault
                              (item => item.AgentName.Equals(currentAgent.Value.UserName)
                               && item.CustomerName.Equals(customerName));
                          if (session != null)
                          {
                              var sessionId = session.Id;
                              var messages = _db.Messages.Where(item => item.Session.Id.Equals(sessionId));
                              var result = JsonConvert.SerializeObject(messages, new Formatting(), _settings);
                              Clients.Client(newAgentId).visitorSwitchConversation
                                  (Context.ConnectionId, customerName, result, clientSessionId);
                          }
                          foreach (var item in _agents.Where(item => item.Value.IsOnline))
                          {
                              Clients.Client(item.Value.ConnectionId).refreshChatWith(newAgent.Value.UserName, cumtomerId);
                          }
                          _db.Sessions.Add(new Session
                          {
                              AgentName = newAgent.Key,
                              CustomerName = customerName,
                              CreatedDateTime = DateTime.Now,
                              Parent = _db.Sessions.Where(item => item.AgentName.Equals(currentAgent.Key)
                                    && item.CustomerName.Equals(customerName)).OrderByDescending(item => item.Id).FirstOrDefault()
                          });
                          _db.SaveChanges();
                      }
              از آنجاییکه اسم متدها کاملا گویا میباشد، به نظر نیازی به توضیح بیشتری ندارند.
              فایل کامل  app.js 
              var app = angular.module("app", ["SignalR", 'ngRoute', 'ngAnimate', 'ngSanitize', 'mgcrea.ngStrap', 'angular-smilies']); 
              
              app.config(["$routeProvider", "$provide", "$httpProvider", "$locationProvider",
                      function ($routeProvider, $provide, $httpProvider, $locationProvider) {
                          $routeProvider.
                             when('/', { templateUrl: 'app/views/home.html', controller: "HomeCtrl" }).
                             when('/agent', { templateUrl: 'app/views/agent.html', controller: "ChatCtrl" })
                              .otherwise({
                                  redirectTo: "/"
                              });;
                      }]);
              app.controller("HomeCtrl", ["$scope", function ($scope) {
                  $scope.title = "home";
              }])
              app.controller("ChatCtrl", ["$scope", "Hub", "$location", "$http", "$rootScope",
                  function ($scope, hub, $location, $http, $rootScope) {
                      if (!$scope.myHub) {
                          var chatKey = "angular-signalr";
                          var defaultCustomerUserName = null;
                          function getid(id) {
                              var find = false;
                              var position = null;
                              angular.forEach($scope.chatConversation, function (index, i) {
                                  if (index.id === id && !find) {
                                      find = true;
                                      position = i;
                                      return;
                                  }
                              });
                              return position;
                          }
                          function apply() {
                              $scope.$apply();
                          }
                          $scope.boxheader = function () {
                              var height = 0;
                              $("#chat-box").slideToggle('slow', function () {
                                  if ($("#chat-box-header").css("bottom") === "0px") {
                                      height = $("#chat-box").height() + 20;
                                  } else {
                                      height = 0;
                                  }
                                  $("#chat-box-header").css("bottom", height);
                              });
                          };
                          var init = function () {
                              $scope.agent = {
                                  id: "", name: "", isOnline: false
                              };
                              $rootScope.msg = "";
                              $scope.alarmStatus = false;
                              $scope.options = {
                                  offlineTitle: "آفلاین",
                                  onlineTitle: "آنلاین",
                                  waitingForOperator: "لطفا منتظر بمانید تا به اپراتور وصل شوید",
                                  emailSent: "ایمیل ارسال گردید",
                                  emailFailed: "متاسفانه ایمیل ارسال نگردید",
                                  logOut: "خروج",
                                  setting: "تنظیمات",
                                  conversion: "آرشیو",
                                  edit: "ویرایش",
                                  alarm: "قطع/وصل کردن صدا",
                                  complete: "تکمیل",
                                  pending: "منتظر ماندن",
                                  reject: "عدم پذیرش",
                                  lock: "آنلاین شدن",
                                  unlock: "آفلاین شدن",
                                  alarmOn: "روشن",
                                  alarmOff: "خاموش",
                                  upload: "آپلود"
                              };
                              $scope.chatConversation = [];
                              $scope.chatSessions = [];
                              $scope.customerVisit = [];
              
                              $scope.agentClientMsgs = [];
                              $scope.clientAgentMsg = [];
                          }();
              //تعریف هاب به همراه متدهای آن
                          $scope.myHub = new hub("chatHub", {
                              listeners: {
                                  "clientChat": function (id, agentName) {
                                      $scope.clientAgentMsg.push({ name: agentName, msg: "با سلام در خدمت میباشم" });
                                      $scope.chatTitle = "کارشناس: " + agentName;
                                      $scope.pendingRequestChat = false;
                                      sessionStorage.setItem(chatKey, id);
                                  }, "agentChat": function (id, firstComment, customerName) {
                                      var date = new Date();
                                      var position = getid(id);
                                      if (position > 0) {
                                          $scope.chatSessions[position].length = $scope.chatConversation[position].length + 1;
                                          $scope.chatSessions[position].date = date.toISOString();
                                          return;
                                      }
                                      else {
                                          $scope.chatConversation.push({
                                              id: id,
                                              sessions: [{
                                                  name: customerName,
                                                  msg: firstComment,
                                                  date: date
                                              }],
                                              agentName: $scope.agent.name,
                                              customerName: customerName,
                                              dateStartChat: date.getHours() + ":" + date.getMinutes(),
                                          });
                                          $scope.chatSessions.push({
                                              id: id,
                                              length: 1,
                                              userName: customerName,
                                              date: date.toISOString()
                                          });
                                      }
                                      sessionStorage.setItem(chatKey, id);
                                      apply();
                                  }, 
              //برروز رسانی لیست برای کارشناسان
              "refreshChatWith": function (agentName, customerConnectionId) {
                                      angular.forEach($scope.customerVisit, function (index, i) {
                                          if (index.connectionId === customerConnectionId) {
                                              $scope.customerVisit[i].chatWith = agentName;
                                          }
                                      });
                                      apply();
                                  },
              //برروز رسانی لیست برای کارشناسان
               "refreshLeaveChat": function (agentName, customerConnectionId) {
                                      angular.forEach($scope.customerVisit, function (index, i) {
                                          if (index.connectionId === customerConnectionId) {
                                              $scope.customerVisit[i].chatWith =agentName + "---" + "  به مکالمه خاتمه داده است ";
                                          }
                                      });
                                      apply();
                                  }
              //وضعیت آنلاین بودن کارشناسان
                                  , "onlineStatus": function (state) {
                                      if (state) {
                                          $scope.chatTitle = $scope.options.onlineTitle;
                                          $scope.hasOnline = true;
                                          $scope.hasOffline = false;
                                      } else {
                                          $scope.chatTitle = $scope.options.offlineTitle;
                                          $scope.hasOffline = true;
                                          $scope.hasOnline = false;
                                      }
                                      $scope.$apply()
                                  }, "loginResult": function (status, id, name) {
                                      if (status) {
                                          $scope.agent.id = id;
                                          $scope.agent.name = name;
                                          $scope.agent.isOnline = true;
                                          $scope.userIsLogin = $scope.agent;
                                          $scope.$apply(function () {
                                              $location.path("/agent");
                                          });
                                      } else {
                                          $scope.agent = null;
                                          toastr.error("کارشناسی با این مشخصات وجود ندارد");
                                          return;
                                      }
                                  }, "newVisit": function (userName, city, country, chatWith, connectionId, firstComment) {
                                      var exist = false;
                                      angular.forEach($scope.customerVisit, function (index) {
                                          if (index.connectionId === connectionId) {
                                              exist = true;
                                              return;
                                          }
                                      });
                                      if (!exist) {
                                          var date = new Date();
                                          $scope.customerVisit.unshift({
                                              userName: userName,
                                              date: date,
                                              city: city,
                                              country: country,
                                              chatWith: chatWith,
                                              connectionId: connectionId,
                                              firstComment: firstComment
                                          });
                                          if ($scope.alarmStatus) {
                                              var snd = new Audio("/App/assets/sounds/Sedna.ogg");
                                              snd.play();
                                          }
                                          toastr.success("تقاضای جدید دریافت گردید");
                                          apply();
                                      }
                                  }, "addMessage": function (id, from, value) {
                                      if ($scope.alarmStatus) {
                                          var snd = new Audio("/App/assets/sounds/newmsg.mp3");
                                          snd.play();
                                      }
                                      $scope.agentUserMsgs = [];
                                      var date = new Date();
                                      var position = getid(id);
                                      if ($scope.chatConversation.length > 0 && position != null) {
                                          $scope.chatConversation[position].sessions.push({ name: from, msg: value, date: date });
                                      }
                                      var item = $scope.chatConversation[position];
                                      if (item) {
                                          angular.forEach(item.sessions, function (index) {
                                              $scope.agentUserMsgs.push({ name: index.name, msg: index.msg, date: date });
                                          });
                                          $scope.chatSessions[position].length = $scope.chatSessions[position].length + 1;
                                      }
                                      apply();
                                  }, "clientAddMessage": function (id, from) {
                                      if ($scope.alarmStatus) {
                                          var snd = new Audio("/App/assets/sounds/newmsg.mp3");
                                          snd.play();
                                      }
                                      $scope.clientAgentMsg.push({ name: id, msg: from });
                                      apply();
                                  }, "visitorSwitchConversation": function (id, customerName, sessions, sessionId) {
                                      sessions = JSON.parse(sessions);
                                      var date = new Date();
                                      var sessionList = [];
                                      angular.forEach(sessions, function (index) {
                                          sessionList.push({
                                              name: index.sender,
                                              msg: index.body,
                                              date: index.creationTime
                                          });
                                      });
                                      $scope.chatConversation.push({
                                          id: sessionId,
                                          sessions: sessionList,
                                          customerName: customerName,
                                          dateStartChat: date.getHours() + ":" + date.getMinutes(),
                                          agentName: $scope.agent.name
                                      });
                                      $scope.chatSessions.push({
                                          id: sessionId,
                                          length: sessions.length,
                                          date: date
                                      });
                                  }, "receiveTicket": function (items) {
                                      angular.forEach(JSON.parse(items), function (index) {
                                          $scope.ticketList = [];
                                          $scope.ticketList.push(index);
                                      });
                                  }, 
              //آرشیو گفته گوهای کارشناس
              "receiveHistory": function (items) {
                                      $scope.agentHistory = [];
                                      angular.forEach(JSON.parse(items), function (index) {
                                          $scope.agentHistory.push(index);
                                      });
                                      apply();
                                  }, 
              //جزییات آرشیو گفتگوها
              "detailsHistory": function (items) {
                                      $scope.historyMsg = [];
                                      angular.forEach(JSON.parse(items), function (index) {
                                          $scope.historyMsg.push({ name: index.sender, msg: index.body, date: index.creationTime });
                                      });
                                      $("#detailsAgentHistory").modal();
                                      apply();
                                  }, 
              //لیست کارشناسان آنلاین
              "agentList": function (items) {
                                      $scope.agentList = [];
                                      angular.forEach(items, function (index) {
                                          if ($scope.agent.name != index.Key) {
                                              $scope.agentList.push({ name: index.Key, id: index.Value.ConnectionId });
                                          }
                                      });
                                      $("#agentList").modal();
                                      apply();
                                  }
                              },
                              methods: ["agentConnect", "sendTicket", "requestChat", "clientSendMessage", "closeChat", "init", "logVisit",
                                  "agentChangeStatus", "engageVisitor", "agentSendMessage", "transfer", "leaveChat", "acceptRequestChat",
                                  "leaveChat", "detailsSessoinMessage", "showAgentList", "getAgentHistoryChat"
                              ], errorHandler: function (error) {
                                  console.error(error);
                              }
                          });
                          $scope.myHub.promise.done(function () {
                              $scope.myHub.init();
                              $scope.myHub.promise.done(function () { });
                          });
              
                          $scope.LeaveChat = function () {
                              $scope.myHub.LeaveChat();
                          };
                          $scope.loginAgent = function (userName) {
                              // username :security user username from agent role
                              if (userName == "hossein" || userName == "ali") {
                                  $scope.myHub.promise.done(function () {
                                      $scope.myHub.agentConnect(userName).then(function (result) {
                                          $scope.agent.name = userName;
                                          $scope.agent.isOnline = true;
                                      });
                                  });
                              }
                          };
                          $scope.requestChat = function (msg) {
                              if (!defaultCustomerUserName) {
                                  //گرفتن کاربر لاگین شده
                                  //ما از آرایه تصادفی استفاده میکنیم
                                  var nameDefaultArray = [
                                      'حسین', 'حسن', 'علی', 'عباس', 'زهرا', 'سمیه'
                                  ];
                                  defaultCustomerUserName=nameDefaultArray[Math.floor(Math.random() * nameDefaultArray.length)];
                              }
                              var userName = defaultCustomerUserName;
                              if (!$scope.chatId) {
                                  $scope.chatId = sessionStorage.getItem(chatKey);
                                  $http.get("http://ipinfo.io")
                                    .success(function (response) {
                                        $scope.myHub.logVisit(response.city, response.country, msg, userName);
                                    }).error(function (e, status, headers, config) {
                                        $scope.myHub.logVisit("Tehran", "Ir", msg, userName)
                                    });
                                  $scope.myHub.requestChat(msg);
                                  $scope.chatTitle = $scope.options.waitingForOperator;
                                  $scope.pendingRequestChat = true;
                              } else {
                                  $scope.myHub.clientSendMessage(msg, userName);
                              };
                              $scope.message = "";
                          };
                          $scope.acceptRequestChat = function (customerConnectionId, firstComment, customerName) {
                              $scope.myHub.acceptRequestChat(customerConnectionId, firstComment, customerName);
                          };
                          $scope.changeAgentStatus = function () {
                              $scope.agent.isOnline = !$scope.agent.isOnline;
                              $scope.myHub.agentChangeStatus($scope.agent.isOnline);
                          };
                          $scope.detailsChat = function (chatId, userName) {
                              $scope.agentUserMsgs = [];
                              angular.forEach($scope.chatConversation, function (index) {
                                  if (index.id === chatId) {
                                      $scope.dateStartChat = index.dateStartChat;
                                      angular.forEach(index.sessions, function (value) {
                                          $scope.agentUserMsgs.push({ name: value.name, msg: value.msg, date: value.date });
                                      });
                                  };
                              });
                              $scope.agentChatWithUser = chatId;
                              $scope.customerName = userName;
                              $("#agentUserChat").modal();
                          };
                          $scope.ticket = {
                              submit: function () {
                                  var name = $scope.ticket.name;
                                  var email = $scope.ticket.email;
                                  var comment = $scope.ticket.comment;
                                  $scope.myHub.sendTicket(name, email, comment);
                              }
                          };
                          $scope.showHistory = function () {
                              $scope.myHub.getAgentHistoryChat($scope.agent.name);
                          };
                          $scope.detailsChatHistory = function (id) {
                              $scope.myHub.detailsSessoinMessage(id, $scope.agent.id);
                          };
                          $scope.agentMsgToUser = function (msg) {
                              var chatId = $scope.agentChatWithUser;
                              var customerName = $scope.customerName;
                              if (!customerName) {
                                  angular.forEach($scope.customerVisit, function (index) {
                                      if (index.connectionId == chatId) {
                                          customerName = index.userName;
                                      }
                                  });
                              }
                              if (chatId !== "" && msg !== "") {
                                  $scope.myHub.agentSendMessage(chatId, msg, customerName);
                              }
                              //not bind to scope.msg! not correctly work
                              $scope.msg = "";
                              $("#post-msg").val("");
                          };
                          $scope.closeChat = function (chatId) {
                              var item = $scope.chatConversation[getid(chatId)];
                              $scope.myHub.closeChat(chatId);
                          };
                          $scope.engageVisitor = function (newAgentId) {
                              var customerId = $scope.customerId;
                              var customerName = $scope.customerName;
                              var clientSessionId = $scope.clientSessionId;
                              $scope.myHub.engageVisitor(newAgentId, customerId, customerName, clientSessionId);
                              $("[data-dismiss=modal]").trigger({ type: "click" });
                          };
                          $scope.selectVisitor = function (customerId, customerName, clientSessionId) {
                              $scope.customerId = customerId;
                              $scope.customerName = customerName;
                              $scope.clientSessionId = clientSessionId;
                              $scope.myHub.showAgentList();
                          };
                          $scope.setClass = function (item) {
                              if (item === "من")
                                  return "question";
                              else
                                  return "response";
                          };
                          $scope.setdirectionClass = function (item) {
                              if (item === $scope.agent.name)
                                  return { "float": "left" };
                              else
                                  return { "float": "right" };
                          };
                          $scope.setArrowClass = function (item) {
                              if (item === $scope.agent.name)
                                  return "left-arrow";
                              else
                                  return "right-arrow";
                          };
                          $scope.setAlarm = function () {
                          $scope.alarmStatus = !$scope.alarmStatus;
                          };
                      }
                  }]);
              app.directive("showtab", function () {
                  return {
                      link: function (scope, element, attrs) {
                          element.click(function (e) {
                              e.preventDefault();
                              $(element).addClass("active");
                              $(element).tab("show");
                          });
                      }
                  };
              });
              //زمان ارسال پیام
              app.directive("timeAgo", function ($q) {
                  return {
                      restrict: "AE",
                      scope: false,
                      link: function (scope, element, attrs) {
                          jQuery.timeago.settings.strings =
                          {
                              prefixAgo: null,
                              prefixFromNow: null,
                              suffixAgo: "پیش",
                              suffixFromNow: "از حالا",
                              seconds: "کمتر از یک دقیقه",
                              minute: "در حدود یک دقیقه",
                              minutes: "%d دقیقه",
                              hour: "حدود یگ ساعت",
                              hours: "حدود %d ساعت ",
                              day: "یک روز",
                              days: "%d روز",
                              month: "حدود یک ماه",
                              months: "%d ماه",
                              year: "حدود یک سال",
                              years: "%d سال",
                              wordSeparator: " ",
                              numbers: []
                          }
                          var parsedDate = $q.defer();
                          parsedDate.promise.then(function () {
                              jQuery(element).timeago();
                          });
                          attrs.$observe("title", function (newValue) {
                              parsedDate.resolve(newValue);
                          });
                      }
                  };
              });
              فایل chathub.cs
              برای آشنایی بیشتر مقاله نگاهی به SignalR Hubs   مفید خواهد بود.
                 [HubName("chatHub")]
                  public class ChatHub : Microsoft.AspNet.SignalR.Hub
                  {
                      private readonly ApplicationDbContext _db = new ApplicationDbContext();
                      private static ConcurrentDictionary<string, UserInformation> _agents;
                      private static List<ChatSessionVm> _chatSessions;
                      private readonly JsonSerializerSettings _settings = new JsonSerializerSettings
                      {
                          ContractResolver = new CamelCasePropertyNamesContractResolver(),
                          ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                      };
                      public void Init()
                      {
                          _chatSessions = _chatSessions ?? (_chatSessions = new List<ChatSessionVm>());
                          _agents = _agents ?? (_agents = new ConcurrentDictionary<string, UserInformation>());
                          Clients.Caller.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                      }
                      public void AgentConnect(string userName)
                      {
                          //ما برای ساده کردن مقایسه ساده ای انجام دادیم فقط کاربر حسین یا علی میتواند کارشناس باشد
                          if (userName == "hossein" || userName == "ali")
                          {
                              var agent = new UserInformation();
                              if (_agents.Any(item => item.Key == userName))
                              {
                                  agent = _agents[userName];
                                  agent.ConnectionId = Context.ConnectionId;
                              }
                              else
                              {
                                  agent.ConnectionId = Context.ConnectionId;
                                  agent.UserName = userName;
                                  agent.IsOnline = true;
                                  _agents.TryAdd(userName, agent);
              
                              }
                              Clients.Caller.loginResult(true, agent.ConnectionId, agent.UserName);
                              Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                          }
                          else
                          {
                              Clients.Caller.loginResult(false, null, null);
                          }
                      }
                      public void AgentChangeStatus(bool status)
                      {
                          var agent = _agents.FirstOrDefault(x => x.Value.ConnectionId == Context.ConnectionId).Value;
                          if (agent == null) return;
                          agent.IsOnline = status;
                          Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                      }
                      public void LogVisit(string city, string country, string firstComment, string userName)
                      {
                          foreach (var agent in _agents)
                          {
                              Clients.Client(agent.Value.ConnectionId).newVisit(userName, city, country, null, Context.ConnectionId, firstComment);
                          }
                      }
                      public void AcceptRequestChat(string customerConnectionId, string body, string userName)
                      {
                          var agent = FindAgent(Context.ConnectionId);
                          var session = _chatSessions.FirstOrDefault(item => item.Key.Equals(agent.Key));
                          if (session == null)
                          {
                              _chatSessions.Add(new ChatSessionVm
                              {
                                  Key = agent.Key,
                                  Value = new List<string> { customerConnectionId }
                              });
                          }
                          else
                          {
                              session.Value.Add(customerConnectionId);
                          }
                          Clients.Client(Context.ConnectionId).agentChat(customerConnectionId, body, userName);
                          Clients.Client(customerConnectionId).clientChat(customerConnectionId, agent.Value.UserName);
                          foreach (var item in _agents.Where(item => item.Value.IsOnline))
                          {
                              Clients.Client(item.Value.ConnectionId).refreshChatWith(agent.Value.UserName, customerConnectionId);
                          }
                          _db.Sessions.Add(new Session
                          {
                              AgentName = agent.Key,
                              CustomerName = userName,
                              CreatedDateTime = DateTime.Now
                          });
                          _db.SaveChanges();
              
                          var message = new Message
                          {
                              CreationTime = DateTime.Now,
                              Sender = agent.Key,
                              Receiver = userName,
                              Body = body,
                              //ConnectionId = _agents.FirstOrDefault(item => item.Value.UserName == userName).Key,
                              Session = _db.Sessions.OrderByDescending(item => item.Id)
                              .FirstOrDefault(item => item.AgentName.Equals(agent.Key) && item.CustomerName.Equals(userName))
                          };
                          _db.Messages.Add(message);
                          _db.SaveChanges();
                      }
                      public void GetAgentHistoryChat(string userName)
                      {
                          var dic = new Dictionary<int, int>();
                          var lenght = 0;
                          var chats = _db.Sessions.OrderBy(item => item.Id).Include(item => item.Parent)
                              .Where(item => item.AgentName.Equals(userName)).ToList();
              
                          foreach (var session in chats)
                          {
                              Result(session, ref lenght);
                              dic.Add(session.Id, lenght);
                              lenght = 0;
                          }
                          if (!chats.Any()) return;
              
                          var historyResult = chats.Select(item => new AgentViewModel
                          {
                              Id = item.Id,
                              CustomerName = item.CustomerName,
                              Date = item.CreatedDateTime,
                              Lenght = dic.Any(di => di.Key.Equals(item.Id)) ? dic.FirstOrDefault(di => di.Key.Equals(item.Id)).Value : 0,
                          }).OrderByDescending(item => item.Id).ToList();
                          Clients.Caller.receiveHistory(JsonConvert.SerializeObject(historyResult, new Formatting(), _settings));
                      }
                      public void DetailsSessoinMessage(int sessionId, string agentId)
                      {
                          var session = _db.Sessions.FirstOrDefault(item => item.Id.Equals(sessionId));
                          if (session == null) return;
                          var list = new List<Message>();
                          GetAllMessages(session, list);
                          var result = JsonConvert.SerializeObject(list.OrderBy(item => item.Id), new Formatting(), _settings);
                          Clients.Client(Context.ConnectionId).detailsHistory(result);
                      }
                      public void ClientSendMessage(string body, string userName)
                      {
                          var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(Context.ConnectionId));
                          if (session == null || session.Key == null) return;
                          var agentId = _agents.FirstOrDefault(item => item.Key.Equals(session.Key)).Value.ConnectionId;
                          Clients.Caller.clientAddMessage("من", body);
                          Clients.Client(agentId).addMessage(Context.ConnectionId, userName, body);
                          var message = new Message
                          {
                              Sender = FindAgent(agentId).Key,
                              Receiver = userName,
                              Body = body,
                              CreationTime = DateTime.Now,
                              Session = FindSession(userName, FindAgent(agentId).Key)
                          };
                          _db.Messages.Add(message);
                          _db.SaveChanges();
                      }
                      public void AgentSendMessage(string id, string body, string userName)
                      {
                          var agent = FindAgent(Context.ConnectionId);
                          Clients.Caller.addMessage(id, agent.Value.UserName, body);
                          Clients.Client(id).clientAddMessage(agent.Value.UserName, body);
                          var message = new Message
                          {
                              Sender = agent.Key,
                              Receiver = userName,
                              Body = body,
                              Session = FindSession(agent.Key, userName),
                              CreationTime = DateTime.Now
                          };
                          _db.Messages.Add(message);
                          _db.SaveChanges();
                      }
                      public void CloseChat(string id)
                      {
                          var findAgent = FindAgent(Context.ConnectionId);
                          var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
                          if (session == null) return;
                          Clients.Client(id).clientAddMessage(findAgent.Key, "مکالمه شما با کارشناس مربوطه به اتمام رسیده است");
              
                          foreach (var agent in _agents)
                          {
                              Clients.Client(agent.Value.ConnectionId).refreshLeaveChat(agent.Value.UserName, id);
                          }
                          _chatSessions.Remove(session);
                      }
                      public void RequestChat(string message)
                      {
                          Clients.Caller.clientAddMessage("من", message);
                      }
                      public void EngageVisitor(string newAgentId, string cumtomerId, string customerName,string clientSessionId)
                      {
                          #region remove session of current agent
                          var currentAgent = FindAgent(Context.ConnectionId);
                          var currentSession = _chatSessions.FirstOrDefault(item => item.Value.Contains(cumtomerId));
                          if (currentSession != null)
                          {
                              _chatSessions.Remove(currentSession);
                          }
                          #endregion
              
                          #region add  session to new agent
                          var newAgent = FindAgent(newAgentId);
                          var newSession = _chatSessions.FirstOrDefault(item => item.Key.Equals(newAgent.Key));
                          if (newSession == null)
                          {
                              _chatSessions.Add(new ChatSessionVm
                              {
                                  Key = newAgent.Key,
                                  Value = new List<string> { cumtomerId }
                              });
                          }
                          else
                          {
                              newSession.Value.Add(cumtomerId);
                          }
                          #endregion
              
                          Clients.Client(currentAgent.Value.ConnectionId).addMessage(cumtomerId, newAgent.Key,
                              "ادامه مکالمه به کارشناس  " + newAgent.Key + "مقابل  منتقل شد");
                          Clients.Client(newAgentId).addMessage(cumtomerId, currentAgent.Key,
                              "لطفا مکالمه را ادامه دهید.با تشکر");
              
                          Clients.Client(cumtomerId).clientAddMessage(newAgent.Value.UserName,
                              "مکالمه شما با کارشناس زیر برقرار گردید" + newAgent.Key);
              
                          var session = _db.Sessions.FirstOrDefault
                              (item => item.AgentName.Equals(currentAgent.Value.UserName)
                               && item.CustomerName.Equals(customerName));
                          if (session != null)
                          {
                              var sessionId = session.Id;
                              var messages = _db.Messages.Where(item => item.Session.Id.Equals(sessionId));
                              var result = JsonConvert.SerializeObject(messages, new Formatting(), _settings);
                              Clients.Client(newAgentId).visitorSwitchConversation
                                  (Context.ConnectionId, customerName, result, clientSessionId);
                          }
                          foreach (var item in _agents.Where(item => item.Value.IsOnline))
                          {
                              Clients.Client(item.Value.ConnectionId).refreshChatWith(newAgent.Value.UserName, cumtomerId);
                          }
                          _db.Sessions.Add(new Session
                          {
                              AgentName = newAgent.Key,
                              CustomerName = customerName,
                              CreatedDateTime = DateTime.Now,
                              Parent = _db.Sessions.Where(item => item.AgentName.Equals(currentAgent.Key)
                                    && item.CustomerName.Equals(customerName)).OrderByDescending(item => item.Id).FirstOrDefault()
                          });
                          _db.SaveChanges();
                      }
                      public void ShowAgentList()
                      {
                          Clients.Caller.agentList(_agents.ToList());
                      }
                      public override Task OnDisconnected(bool stopCalled)
                      {
                          var id = Context.ConnectionId;
                          var isAgent = _agents != null && _agents.Any(item => item.Value.ConnectionId.Equals(id));
                          if (isAgent)
                          {
                              UserInformation agent;
                              var currentAgentConnectionId = FindAgent(id).Key;
                              if (currentAgentConnectionId == null)
                                  return base.OnDisconnected(stopCalled);
                              if (_chatSessions.Any())
                              {
                                  var sessions = _chatSessions.FirstOrDefault(item => item.Key.Equals(currentAgentConnectionId));
                                  //اطلاع دادن به تمام کاربرانی که در حال مکالمه با کارشناس هستند
                                  if (sessions != null)
                                  {
                                      var result = sessions.Value.ToList();
                                      for (var i = 0; i < result.Count(); i++)
                                      {
                                          var localId = result[i];
                                          Clients.Client(localId).clientAddMessage(currentAgentConnectionId, "ارتباط شما با مشاور مورد نظر قطع شده است");
                                      }
                                  }
                              }
                              _agents.TryRemove(currentAgentConnectionId, out agent);
                              Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                              Clients.Client(id).loginResult(false, null, null);
                          }
                          else
                          {
                              if (_chatSessions == null ||
                                  !_chatSessions.Any(item => item.Value.Contains(id)
                                      && _agents == null))
                                  return base.OnDisconnected(stopCalled);
              
                              var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
                              if (session == null)
                                  return base.OnDisconnected(stopCalled);
              
                              var agentName = session.Key;
                              var agent = _agents.FirstOrDefault(item => item.Key.Equals(agentName));
                              if (agent.Key != null)
                              {
                                  Clients.Client(agent.Value.ConnectionId).addMessage(id, "کاربر", "اتصال با کاربر قطع شده است");
                              }
                          }
                          return base.OnDisconnected(stopCalled);
                      }
              
              
                      private KeyValuePair<string, UserInformation> FindAgent(string connectionId)
                      {
                          return _agents.FirstOrDefault(item => item.Value.ConnectionId.Equals(connectionId));
                      }
                      private Session FindSession(string key, string userName)
                      {
                          return _db.Sessions.Where(item => item.AgentName.Equals(key) && item.CustomerName.Equals(userName))
                              .OrderByDescending(item => item.Id).FirstOrDefault();
                      }
                      private static void Result(Session parent, ref int lenght)
                      {
                          while (true)
                          {
                              if (parent == null)
                                  return;
                              lenght += parent.Messages.Count();
                              parent = parent.Parent;
                          }
                      }
                      private static List<Message> GetAllMessages(Session node, List<Message> list)
                      {
                          if (node == null) return null;
                          list.AddRange(node.Messages);
                          if (node.Parent != null)
                          {
                              GetAllMessages(node.Parent, list);
                          }
                          return null;
                      }
                  }
              فایل agent.html
              <div>
                  <div>
                      <h2>
                          خوش آمدید
                          <span ng-bind="agent.name">
                          </span>
                          <a ng-click="changeAgentStatus()">
                              <i ng-if="changeStatus==null"
                                 data-placement="bottom"
                                 data-trigger="hover "
                                 bs-tooltip="options.lock"></i>
                              <i ng-if="changeStatus==true"
                                 data-placement="bottom"
                                 data-trigger="hover"
                                 bs-tooltip="options.unlock"></i>
                          </a>
                      </h2>
                      <div style="float: left">
                          <a ng-click="setAlarm()">
                              <i ng-show="alarmStatus"
                                 data-placement="bottom"
                                 data-trigger="hover "
                                 bs-tooltip="options.alarmOn"></i>
              
                              <i ng-show="!alarmStatus"
                                 data-placement="bottom"
                                 data-trigger="hover "
                                 bs-tooltip="options.alarmOff"></i>
                          </a>
                          <!--<a data-placement="bottom"
                             data-trigger="hover "
                             bs-tooltip="options.conversion" ng-click="showHistory()"><i></i></a>-->
              
                          <a data-placement="bottom"
                             data-trigger="hover "
                             bs-tooltip="options.edit"><i></i><span></span></a>
              
                          <a data-placement="bottom"
                             data-trigger="hover "
                             bs-tooltip="options.setting"><i></i></a>
              
                          <a data-placement="bottom"
                             data-trigger="hover "
                             bs-tooltip="options.signOut" ng-click="LeaveChat()"><i></i><span></span></a>
                      </div>
                  </div>
                  <div>
                      <div>
                          <div id="chat-content">
                              <div>
                                  <ul>
                                      <li>
                                          <a showtab href="#online-list">آنلاین</a>
                                      </li>
                                      <li>
                                          <a ng-click="showHistory()" showtab href="#conversation">آرشیو گفتگوها</a>
                                      </li>
                                  </ul>
                                  <div>
                                      <div id="online-list">
                                          <div>
                                              <h2>
                                                  <i></i><span></span>
                                                  <span>نمایش آنلاین مراجعه ها</span>
                                              </h2>
                                          </div>
                                          <div>
                                              <div id="agent-chat">
                                                  <div id="real-time-visits">
                                                      <table id="current-visits">
                                                          <thead>
                                                              <tr>
                                                                  <th>نام کاربر</th>
                                                                  <th>زمان اولین تقاضا</th>
                                                                  <th>منطقه</th>
                                                                  <th>پاسخ</th>
                                                              </tr>
                                                          </thead>
                                                          <tbody>
                                                              <tr id="{{item.connectionId}}" ng-animate="animate" ng-repeat="item in customerVisit ">
                                                                  <td ng-bind="item.userName"></td>
                                                                  <td>
                                                                      <span time-ago title="{{item.date}}"></span>
                                                                  </td>
                                                                  <td>
                                                                      <span ng-bind="item.country"></span> /<span ng-bind="item.city"> </span>
                                                                  </td>
                                                                  <td>
                                                                      <a style="cursor: pointer" ng-if="item.chatWith== null"
                                                                         ng-click="acceptRequestChat(item.connectionId,item.firstComment,item.userName)">
                                                                          شروع مکالمه
                                                                      </a>
                                                                      <span ng-if="item.chatWith ">
                                                                          وضعیت:
                                                                          <span>در حال مکالمه با</span>
                                                                          <span ng-bind="item.chatWith"></span>
                                                                          <a ng-show="item.chatWith==agent.name"
                                                                            
                                                                             ng-click="selectVisitor(item.connectionId,item.userName,item.connectionId)">
                                                                              انتقال مکالمه
                                                                          </a>
                                                                      </span>
                                                                      <ul ng-repeat="session in chatSessions track by $index" style="padding:0px;">
                                                                          <li ng-if="session.id==item.connectionId" id="{{session.id}}">
                                                                              <div>
                                                                                  <p>
                                                                                      تاریخ شروع مکالمه:
                                                                                      <span time-ago title="{{session.date}}"></span>
                                                                                  </p>
                                                                                  <p>
                                                                                      تعداد پیام ها:
                                                                                      <span ng-bind="session.length"></span>
                                                                                  </p>
                                                                              </div>
                                                                              <p>
                                                                                  <a ng-click="detailsChat(session.id,session.userName)">جزییات </a>
                                                                                  <a ng-click="closeChat(session.id)">
                                                                                      خاتمه عملیات
                                                                                  </a>
                                                                              </p>
                                                                          </li>
                                                                      </ul>
                                                                  </td>
                                                              </tr>
                                                          </tbody>
                                                      </table>
                                                  </div>
                                              </div>
                                          </div>
                                      </div>
                                      <div id="conversation">
                                          <div>
                                              <h2>
                                                  <i></i><span></span>
                                                  <span>آرشیو گفتگوهای </span>
                                                  {{agent.name}}
                                              </h2>
                                          </div>
                                          <div>
                                              <div>
                                                  <table id="current-visits">
                                                      <thead>
                                                          <tr>
                                                              <th>شناسه مشتری</th>
                                                              <th>نام مشتری</th>
                                                              <th>تعداد محاوره ها</th>
                                                              <th>تاریخ</th>
                                                              <th>جزئیات</th>
                                                          </tr>
                                                      </thead>
                                                      <tbody>
                                                          <tr ng-repeat="item in agentHistory track by $index">
                                                              <td ng-bind="item.id"></td>
                                                              <td ng-bind="item.customerName"></td>
                                                              <th ng-bind="item.lenght"></th>
                                                              <td><span time-ago title="{{item.date}}"></span></td>
                                                              <th>
                                                                  <ang-click="detailsChatHistory(item.id)" >مشاهده جزییات گفتگو</a>
                                                              </th>
                                                          </tr>
                                                      </tbody>
                                                  </table>
                                              </div>
                                          </div>
                                      </div>
                                  </div>
                              </div>
                          </div>
                      </div>
                  </div>
                  <div id="detailsAgentHistory" tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true">
                      <div>
                          <div>
                              <div>
                                  <div>
                                      <button type="button" data-dismiss="modal" aria-hidden="true">×</button>
                                  </div>
                                  <h2>
                                      <span></span>تاریخچه گفتگو
                                  </h2>
                              </div>
                              <div>
                                  <div style="display: block">
                                      <ul ng-repeat="item in historyMsg">
                                          <li>
                                              <span ng-bind="item.name" ng-style="setdirectionClass(item.name)">
                                              </span>
                                              <span ng-style="setdirectionClass(item.name)">
                                                  <span ng-class="setArrowClass(item.name)"></span>
                                                  <span time-ago title="{{item.date}}"></span>
                                                  <span>
                                                      <p ng-bind-html="item.msg | smilies"></p>
                                                  </span>
                                              </span>
                                          </li>
                                      </ul>
                                  </div>
              
                              </div>
                          </div>
                      </div>
                  </div>
                  <div id="agentList" tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true">
                      <div>
                          <div>
                              <div>
                                  <div>
                                      <button type="button" data-dismiss="modal" aria-hidden="true">×</button>
                                  </div>
                                  <h2>
                                      <span></span>لیست تمام کارشناسان
                                  </h2>
                              </div>
                              <div>
                                  <div style="display: block;">
                                      <div ng-show="agentList.length==0">
                                          کارشناس آنلاینی وجود ندارد
                                      </div>
                                      <ul ng-repeat="item in agentList">
                                          <li>
                                              <span>
                                                  <a ng-click="engageVisitor(item.id)">{{item.name}}</a>
                                              </span>
                                          </li>
                                      </ul>
                                  </div>
                              </div>
                          </div>
                      </div>
                  </div>
                  <div id="agentUserChat"  tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true">
                      <div>
                          <div>
                              <div>
                                  <div>
                                      <button type="button" data-dismiss="modal" aria-hidden="true">×</button>
                                  </div>
                                  <h2>
                                      <span></span>گفتگو
                                  </h2>
                              </div>
                              <div>
                                  <div>
                                      <div>
                                          <div style="display: block;">
                                              <label>شروع چت در </label>:
                                              <span ng-bind="dateStartChat"></span>
              
                                              <ul>
                                                  <li ng-repeat="item in agentUserMsgs">
                                                      <span ng-bind="item.name" ng-style="setdirectionClass(item.name)">
              
                                                      </span>
                                                      <span ng-style="setdirectionClass(item.name)">
                                                          <span ng-class="setArrowClass(item.name)"></span>
                                                          <span time-ago title="{{item.date}}"></span>
              
                                                          <span>
                                                              <p ng-bind-html="item.msg | smilies"></p>
                                                          </span>
                                                      </span>
                                                  </li>
                                              </ul>
                                              <div>
                                                  <div>
                                                      <textarea id="post-msg" ng-model="msg" placeholder="متن خود را وارد نمایید" style="overflow: hidden; word-wrap: break-word; resize: horizontal; height: 80px; max-width: 100%"></textarea>
                                                      <span smilies-selector="msg" smilies-placement="right" smilies-title="Smilies"></span>
                                                  </div>
                                                  <div style="text-align: center; margin-top: 5px">
                                                      <button ng-click="agentMsgToUser(msg)">ارسال</button>
                                                  </div>
                                              </div>
                                          </div>
                                      </div>
                                  </div>
                              </div>
                          </div>
                      </div>
                  </div>
              </div>
              فایل index.cshtml
              <html ng-app="app">
              <head>
                  <meta charset="utf-8" />
                  <meta name="viewport" content="width=device-width, initial-scale=1.0">
                  <title>Live Support</title>
              
                  <link href="~/Content/bootstrap-rtl.css" rel="stylesheet" />
                  <link href="~/Scripts/smilies/angular-smilies-embed.css" rel="stylesheet" />
                  <link href="~/Content/font-awesome.css" rel="stylesheet" />
              
                  <link href="~/Content/toastr.css" rel="stylesheet" />
                  <link href="~/Content/liveSupport.css" rel="stylesheet" />
              
                  <script src="~/Scripts/jquery-1.10.2.js"></script>
                  <script src="~/Scripts/toastr.js"></script>
                  <script src="~/Scripts/jquery.timeago.js"></script>
              
                  <script src="~/Scripts/angular.js"></script>
                  <script src="~/Scripts/angular-animate.js"></script>
                  <script src="~/Scripts/angular-sanitize.js"></script>
                  <script src="~/Scripts/angular-route.js"></script>
              
                  <script src="~/Scripts/angular-strap.js"></script>
                  <script src="~/Scripts/angular-strap.tpl.js"></script>
              
                  <script src="~/Scripts/smilies/angular-smilies.js"></script>
              
                  <script src="~/Scripts/jquery.signalR-2.2.0.js"></script>
                  <script src="~/Scripts/angular-signalr-hub.js"></script>
              
                  <script src="~/app/app.js"></script>
                  @Scripts.Render("~/bundles/bootstrap")
              
              </head>
              <body ng-controller="ChatCtrl">
                  <div ng-view>
                  </div>
              
                  <div id="chat-box-header" ng-click="boxheader()">
                      {{chatTitle}}
                  </div>
                  <div id="chat-box">
                      <div ng-show="hasOnline">
                          <div id="style-1" style="min-height:100px;">
                              <div ng-repeat="item in clientAgentMsg  track by $index">
                                  <span ng-class="setClass(item.name)">
                                      {{item.name}}
                                  </span>
                                  <br />
                                  <p ng-bind-html="item.msg | smilies"></p>
                              </div>
                          </div>
                          <div>
                              <label>پیام</label>
                              <div style="text-align: left; clear: both">
                                  <a data-placement="top"
                                     data-trigger="hover "
                                     bs-tooltip="options.alarm" ng-click="alarm()"><i></i></a>
                                  <a data-placement="top"
                                     data-trigger="hover "
                                     bs-tooltip="options.signOut" href="signOut()"><i></i><span></span></a>
                                  <a data-placement="top"
                                     data-trigger="hover "
                                     bs-tooltip="options.upload" href="fileupload()">
                                      <span><i></i></span>
                                  </a>
                              </div>
                              <div>
                                  <textarea style="height: 150px; max-height: 160px;" ng-model="message" placeholder=" متن خود را وارد نمایید"></textarea>
                                  <span smilies-selector="message" smilies-placement="right" smilies-title="Smilies"></span>
                              </div>
                          </div>
                          <div style="text-align: center">
                              <button type="button" ng-disabled="pendingRequestChat" ng-click="requestChat(message)">ارسال </button>
                          </div>
                      </div>
                      <div ng-show="hasOffline">
                          <div>
                              <form name="Ticket" id="form1">
                                  <fieldset>
                                      <div>
                                          <label>نام</label>
                                          <input name="email"
                                                 ng-model="ticket.name"
                                                >
                                      </div>
                                      <div>
                                          <label>ایمیل</label>
                                          <input name="email"
                                                 ng-model="ticket.email"
                                                >
                                      </div>
                                      <div>
                                          <label>پیام</label>
                                      </div>
                                      <div>
                                          <textarea ng-model="ticket.comment" placeholder="متن خود را وارد نمایید"></textarea>
                                          <span smilies-selector="ticket.comment" smilies-placement="right" smilies-title="Smilies"></span>
                                      </div>
                                  </fieldset>
                                  <div style="text-align: center">
                                      <button type="button"
                                              ng-click="ticket.submit(ticket)">
                                          ارسال
                                      </button>
                                  </div>
                              </form>
                          </div>
                      </div>
                  </div>
                 
              </body>
              
              </html>
              LiveSupport.zip 

              نکات تکمیلی :
              نگاشت  Hub‌ها به برنامه در مسیر ("signalr /") در فایل  ConfigureAuth.Cs 
                app.MapSignalR();

              نظرات مطالب
              واژه‌های کلیدی جدید and، or و not در C# 9.0
              معادل‌های string.IsNullOrEmpty در C# 9.0 جهت اطلاع و آشنایی با Syntax جدید؛ اگر جائی آن‌ها را دیدید!
              using System;
              
              namespace CS9Features
              {
                  public static class ModernizingACodebase
                  {
                      public static void PropertyPatternToReplaceIsNullorEmpty1()
                      {
                          string hello = null;
              
                          // Old approach
                          if (!string.IsNullOrEmpty(hello))
                          {
                              Console.WriteLine($"{hello} has {hello.Length} letters.");
                          }
              
                          // New approach, with a property pattern
                          if (hello is { Length: > 0 })
                          {
                              Console.WriteLine($"{hello} has {hello.Length} letters.");
                          }
                      }
              
                      public static void PropertyPatternToReplaceIsNullorEmpty2()
                      {
                          // For arrays
                          var greetings = new string[2];
                          greetings[0] = "Hello world";
                          greetings = null;
              
                          // Old approach
                          if (greetings != null && !string.IsNullOrEmpty(greetings[0]))
                          {
                              Console.WriteLine($"{greetings[0]} has {greetings[0].Length} letters.");
                          }
              
                          // New approach
                          if (greetings?[0] is { Length: > 0 } hi)
                          {
                              Console.WriteLine($"{hi} has {hi.Length} letters.");
                          }
                      }
                  }
              }
              برای مطالعه‌ی بیشتر