مطالب
شروع کار با GraphQL در ASP.NET Core
در این مقاله هدف این است که GraphQL را در ASP.NET Core راه اندازی کنیم. از یک کتابخانه ثالث برای آسان‌تر کردن یکپارچگی استفاده می‌کنیم و همچنین با جزئیات، توضیح خواهیم داد که چگونه می‌توان از element‌های مربوط به GraphQL مثل (Type ،Query و Schema) برای کامل کردن فرآیند یکپارچگی در ASP.NET Core استفاده کنیم.

GraphQL  و تفاوت‌های آن با REST
GraphQl یک query language می‌باشد که query‌ها را با استفاده از type system‌‌‌هایی که ما برای داده‌ها تعریف می‌کنیم، اجرا می‌کند. GraphQL  به هیچ زبان یا پایگاه داده مشخصی گره نخورده است. 

  •  GraphQL نیازمند رفت و برگشت‌های کمتری به server، به منظور بازیابی داده‌ها برای template یا view است. همراه با REST ما مجبور هستیم که چندیدن endpoint مثلا  (... api/students, api/courses, api/instructors ) را برای گرفتن همه داده‌های که برای template یا view نیاز داریم ملاقات کنیم؛ ولی این شرایط در GraphQL برقرار نیست. با GraphQL ما تنها یک query را ایجاد می‌کنیم که چندین تابع (resolver) را در سمت سرور فراخوانی می‌کند و همه داده‌ها را از منابع مختلفی، در یک درخواست برگشت می‌دهد. 

  • همراه با REST، همانطور که Application ما رشد می‌کند، تعداد endpoint‌ها هم زیاد می‌شوند که این نیازمند زمان بیشتری برای نگهداری می‌باشد. اما با GraphQL ما تنها یک endpoint  داریم؛ همین! 
  • با استفاده از GraphQL، ما هرگز به مشکل گرفتن داده‌هایی کم یا زیاد از منبع روبرو نخواهیم شد. به این خاطر است که ما query‌ها را با فیلد‌هایی که چیز‌هایی را که نیاز داریم، نشان می‌دهند، تعریف می‌کنیم. در این صورت ما همیشه چیز‌هایی را که درخواست داده‌ایم، دریافت می‌کنیم.

بنابراین اگر یک query شبیه زیر را ارسال کنیم :
query OwnersQuery {
  owners {
    name
    account {
      type
    }
  } 
}
صد درصد مطمئن هستیم که خروجی زیر برگشت داده می‌شود:
{
  "data": {
    "owners": [
     {
      "name": "John Doe",
      "accounts": [
        {
          "type": "Cash"
        },
        {
          "type": "Savings"
        }
      ]
     }
    ]
  }
}

همراه با REST  این شرایط برقرار نمی‌باشد. بعضی از مواقع ما چیزی بیشتر یا کمتر از آنچه که نیاز داریم دریافت می‌کنیم؛ که این بستگی به اینکه چگونه action‌ها در یک endpoint مشخص، پیاده سازی شده‌اند، دارد.  

شروع کار 
یک پروژه جدید را با استفاده از دستور زیر ایجاد می‌کنیم: 
dotnet new api -n ASPCoreGraphQL
سپس ساختار زیر را ایجاد می‌کنیم :


پوشه Contracts شامل واسط‌های مورد نیاز برای repository logic می‌باشد:

namespace ASPCoreGraphQL.Contracts
{
    public interface IOwnerRepository
    {
    }
}
namespace ASPCoreGraphQL.Contracts
{
    public interface IAccountRepository
    {
    }
}


در پوشه Models، کلاس‌های مدل را نگه داری می‌کنیم؛ به همراه یک کلاس context و کلاس‌های configuration:
public class Owner
{
    [Key]
    public Guid Id { get; set; }
    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; }
    public string Address { get; set; }
 
    public ICollection<Account> Accounts { get; set; }
}

public class Account
{
    [Key]
    public Guid Id { get; set; }
    [Required(ErrorMessage = "Type is required")]
    public TypeOfAccount Type { get; set; }
    public string Description { get; set; }
 
    [ForeignKey("OwnerId")]
    public Guid OwnerId { get; set; }
    public Owner Owner { get; set; }
}

public enum TypeOfAccount
{
    Cash,
    Savings,
    Expense,
    Income
}
    public class ApplicationContext : DbContext
    {
        public ApplicationContext(DbContextOptions options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {

        }

        public DbSet<Owner> Owners { get; set; }
        public DbSet<Account> Accounts { get; set; }
    }

در پوشه Repository، کلاس‌های مرتبط با منطق بازیابی داده‌ها را داریم:
public class OwnerRepository : IOwnerRepository
{
    private readonly ApplicationContext _context;
 
    public OwnerRepository(ApplicationContext context)
    {
        _context = context;
    }
}
public class AccountRepository : IAccountRepository
{
    private readonly ApplicationContext _context;
 
    public AccountRepository(ApplicationContext context)
    {
        _context = context;
    }
}
repository logic، یک راه اندازی ابتدایی بدون هیچ لایه اضافه‌تری است.

کلاس context و کلاس‌های repository، در فایل Startup.cs ثبت می‌شوند:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationContext>(opt =>
         opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); 
 
    services.AddScoped<IOwnerRepository, OwnerRepository>();
    services.AddScoped<IAccountRepository, AccountRepository>();
 
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
        .AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
}


Integration of GraphQL in ASP.NET Core 
برای کار کردن با GraphQL در ابتدا نیاز است که کتابخانه GraphQL را نصب کنیم. به همین منظور در ترمینال مربوط به VS Code، دستور زیر را وارد می‌کنیم: 
dotnet add package GraphQL
و همچنین کتابخانه‌ی زیر که به ما کمک می‌کند تا GraphQL.NET را به عنوان یک وابستگی دریافت کنیم:
dotnet add package GraphQL.Server.Transports.AspNetCore
و در نهایت نصب کتابخانه زیر که کمک می‌کند query‌های GraphQL را به سرور ارسال کنیم:
dotnet add package GraphQL.Server.Ui.Playground

(Creating GraphQL Specific Objects (Type, Query, Schema

کار را با ایجاد کردن یک پوشه جدید به نام GraphQL و سپس در آن ایجاد یک پوشه دیگر به نام GraphQLSchema، شروع می‌کنیم. در پوشه GraphQLSchema، یک کلاس را به نام AppSchema، ایجاد می‌کنیم.
کلاس AppSchema باید از کلاس Schema ارث بری کند تا در فضای نام GraphQL.Types قرار گیرد. در سازنده این کلاس، IDependencyResolver را تزریق می‌کنیم که قرار است به ما کمک کند تا اشیاء Query ،Mutation یا Subscription را resolve کنیم:
public class AppSchema : Schema
{
    public AppSchema(IDependencyResolver resolver)
        :base(resolver)
    {
 
    }
}
چیزی که مهم است بدانیم این است که خصوصیات schema، مثل Query ،Mutation و Subscription، واسط IObjectGraphType را پیاده سازی می‌کنند تا اشیائی را که قرار است resolve کنیم، همین type را پیاده سازی کرده باشند. در ضمن GraphQL API نمی‌تواند مدل را به عنوان نتیجه به صورت مستقیم بازگشت دهد.
فعلا این کلاس را در همین حالت می‌گذاریم و سپس یک پوشه را به نام GraphQLTypes در پوشه GraphQL ایجاد می‌کنیم. در پوشه GraphQLTypes یک کلاس را به نام OwnerType ایجاد می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object.");
        Field(x => x.Name).Description("Name property from the owner object.");
        Field(x => x.Address).Description("Address property from the owner object.");
    }
}
از کلاس OwnerType به عنوان یک جایگزین برای مدل Owner درون یک GraphQL API استفاده می‌کنیم. این کلاس از نوع جنریک ObjectGraphType ارث بری می‌کند. با متد Field، فیلد‌هایی را که بیانگر خصوصیات مدل Owner می‌باشند، مشخص می‌کنیم.
در ابتدا واسط IOwnerRepository و کلاس  OwnerRepository را به حالت زیر ویرایش می‌کنیم:
public interface IOwnerRepository
{
    IEnumerable<Owner> GetAll();
}


public class OwnerRepository : IOwnerRepository
{
    private readonly ApplicationContext _context;
 
    public OwnerRepository(ApplicationContext context)
    {
        _context = context;
    }
 
    public IEnumerable<Owner> GetAll() => _context.Owners.ToList();
}

در ادامه یک پوشه دیگر را به نام GraphQLQueries در پوشه‌ی GraphQL ایجاد و سپس در آن یک کلاس را به نام AppQuery ایجاد و آن را به حالت زیر ویرایش می‌کنیم:
public class AppQuery : ObjectGraphType
{
    public AppQuery(IOwnerRepository repository)
    {
        Field<ListGraphType<OwnerType>>(
           "owners",
           resolve: context => repository.GetAll()
       );
    }
}

توضیحات AppQuery
همانطور که می‌بینیم این کلاس از ObjectGraphType ارث بری می‌کند. در سازنده کلاس، IOwnerRepository را تزریق می‌کنیم و یک فیلد را به منظور برگشت دادن نتیجه برای یک Query مشخص، ایجاد می‌کنیم. در این کلاس، از نوع جنریک متد Field، استفاده کرده‌ایم که تعدادی type را به عنوان یک پارامتر جنریک پذیرش می‌کند که این بیانگر GraphQL.NET برای type ‌های معمول در NET. می‌باشد. علاوه بر ListGraphType،  نوع‌هایی مثل IntGraphType  و StringGraphType و ... وجود دارند (لیست کامل).
پارامتر owners نام فیلد می‌باشد (query مربوط به کلاینت باید با این نام مطابقت داشته باشد) و پارامتر دوم نتیجه می‌باشد.
بعد از انجام این مقدمات، اکنون کلاس AppSchema  را باز می‌کنیم و به حالت زیر آن را ویرایش می‌کنیم:
public class AppSchema : Schema
{
    public AppSchema(IDependencyResolver resolver)
        :base(resolver)
    {
        Query = resolver.Resolve<AppQuery>();
    }
}

Libraries and Schema Registration 
 در کلاس Startup نیاز است کتابخانه‌های نصب شده و هم چنین  کلاس schema ایجاد شده را ثبت کنیم. این کار را با ویرایش کردن متد ConfigureServices  و Configure  انجام میدهیم:
public void ConfigureServices(IServiceCollection services)
{
    ...
 
    services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
    services.AddScoped<AppSchema>();
 
    services.AddGraphQL(o => { o.ExposeExceptions = false; })
        .AddGraphTypes(ServiceLifetime.Scoped);

   ...
}
در متد ConfigureServices، در ابتدا DependencyResolver و سپس کلاس schema را ثبت می‌کنیم. علاوه بر آن، GraphQL را با متد AddGraphQL ثبت می‌کنیم. سپس همه تایپ‌های GraphQL را با متد AddGraphTypes ثبت می‌کنیم. بدون این متد (AddGraphTypes)، ما مجبور بودیم که همه type ‌ها و query ‌ها را به صورت دستی در APIامان ثبت کنیم.
در نهایت در متد schema  Configure را به خط لوله درخواست‌ها (request’s pipeline) اضافه می‌کنیم و هم چنین ابزار Playground UI:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   ...
    app.UseGraphQL<AppSchema>();
    app.UseGraphQLPlayground(options: new GraphQLPlaygroundOptions());
    app.UseMvc();
}

برای آزمایش GraphQL API امان، از ابزار GraphQL.UI.Playground استفاده می‌کنیم. در ابتدا پروژه را با دستور زیر اجرا می‌کنیم:
dotnet run
و سپس در مرورگر به این آدرس می‌رویم: 
https://localhost:5001/ui/playground
اکنون یک query را با نام owners ارسال می‌کنیم (این نام باید با نام query که در AppQuery در نظر گرفتیم، مطابقت داشته باشد). سپس نتیجه لازم را دریافت می‌کنیم. همانطور که مشاهده می‌کنیم، همه چیز بر اساس انتظاری که می‌رفت کار می‌کند. 



در قسمت بعد درابطه با  کوئری‌های پیشرفته، Error Handling و Data Loader در GraphQL  صحبت خواهیم کرد. 

کد‌های مربوط به این قسمت را از اینجا دریافت کنید .ASPCoreGraphQL.zip
نظرات مطالب
رمزنگاری خودکار فیلدها توسط Entity Framework Core
یک مثال تکمیلی: روش ذخیره‌ی آرایه‌ای از رشته‌ها
public class MyEntity
{
   public Guid Id {get;set;}
   public string[] Strings { get; set; } 
}
توسط تبدیلگر زیر:
modelBuilder.Entity<MyEntity>()
   .Id(e=> e.Id)
   .Property(e => e.Strings)
   .HasConversion(
         v => string.Join(',', v),
         v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
مطالب
الگوی وضعیت State Pattern
الگوی وضعیت، یکی از الگوهای رفتاری Gang Of Four است و بسیار شبیه به الگوی Strategy  می‌باشد؛ ولی با کپسوله سازی بیشتر. در الگوی استراتژی تغییر وضعیت از بیرون کلاس اعمال می‌د ولی در الگوی وضعیت، بر اساس تغییر وضعیت درونی خودش صورت می‌گیرد.
یکی از استفاده‌های این الگو برای مثال در پلیرهاست که وضعیت پخش را چون Play,Pause و ... در خود دارند. در اینجا هم از این مثال استفاده می‌کنیم:
ابتدا یک اینترفیس برای وضعیت خود بسازید که آرگومان ورودی متد آن را در مرحله بعد تعریف میکنیم:
public interface IState
{
    void PressPlay(MP3PlayerContext context);
}
سپس نوبت ایجاد کلاس اصلی یا همان دستگاه پخش که به آن Context می‌گوییم می‌رسد تا تغییر وضعیت الگو را به آن بسپاریم:
public class MP3PlayerContext
{
    public MP3PlayerContext()
    {
        this.CurrentState = new StandbyState();
    }
    
    public MP3PlayerContext(IState state)
    {
       this.CurrentState = state; 
    }
    
    public IState CurrentState { get; set; }
    
    public void Play()
    {
        this.CurrentState.PressPlay(this);
    }
}
سپس کلاس‌های مختلف خود را بر اساس اینترفیس بالا می‌سازیم:
public class StandbyState : IState
{
    public void PressPlay(MP3PlayerContext context)
    {
        context.CurrentState = new PlayingState();
    }
}

public class PlayingState : IState
{
    public void PressPlay(MP3PlayerContext context)
    {
        context.CurrentState = new StandbyState();
    }
}
در کدهای بالا، کلاس‌های Playing و StandBy در واقع شبیه سازی از عمل کلید پخش هستند که با هر بار فشردن آن، پخش به طور موقت توقف کرده و یا پخش خود را از سر می‌گیرد. کلاس Context نیز باید در ابتدا به طور پیش فرض با یکی از این مقادیر پر شود و برای دکمه پخش مشخص است که کلاس PlayingState می‌باشد.
بدین ترتیب در اولین اجرای متد Play در کلاس Context، کلاس PlayingState اجرا می‌شود و وضعیت، به StandbyState تغییر می‌کند و هر بار که مجددا متد Play اجرا گردد، تعویض بین این دو کلاس صورت می‌گیرد.
مطالب
ارسال خطاهای رخ‌داده‌ی در برنامه‌های سمت کلاینت Blazor WASM، به تلگرام
هر زمانیکه در سمت کلاینت، استثناء یا خطایی رخ می‌دهد، کاربر با نوار زرد رنگی در پایین صفحه، از آن مطلع می‌شود؛ اما برنامه نویس چطور؟! به همین جهت در این مطلب قصد داریم تمام خطاهای رخ داده‌ی در برنامه‌ی سمت کلاینت را لاگ کرده و به سرور تلگرام ارسال کنیم. مزیت کار کردن با تلگرام، دسترسی به سروری است که تقریبا همواره در دسترس است و برخلاف بانک اطلاعاتی برنامه که ممکن است در لحظه‌ی بروز خطا، خودش سبب ساز اصلی باشد و قادر به ثبت اطلاعات خطاهای رسیده‌ی از سمت کلاینت نباشد، چنین مشکلی را با تلگرام نداریم (مانند همان جمله‌ی معروف: «بک‌آپ سروری که روی همان سرور گرفته می‌شود، بک آپ نام ندارد!»). همچنین بررسی و حذف گزارش‌های رسیده‌ی به آن نیز بسیار ساده‌است و می‌توان این گزارش‌ها را مستقل از سرور برنامه و از طریق وسایل مختلفی مانند گوشی‌های همراه، تبلت‌ها و غیره نیز بررسی کرد.




نحوه‌ی نمایش خطاها در برنامه‌های Blazor

در حین توسعه‌ی برنامه‌های Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر می‌شود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهده‌ی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ داده‌است اکتفا کرده و گزینه‌ی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد می‌دهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت می‌گیرد:
<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
که شیوه نامه‌های پیش‌فرض آن در فایل wwwroot/css/app.css قرار دارند. در حالت عادی المان blazor-error-ui به همراه یک display: none است که از نمایش آن جلوگیری می‌کند. اما در زمان بروز خطایی، فریم‌ورک آن‌را به صورت display: block نمایش می‌دهد.


نحوه‌ی مدیریت استثناءها در برنامه‌های Blazor

توصیه شده‌است که کار مدیریت استثناءها باید توسط توسعه دهنده صورت گیرد و بهتر است جزئیات آن‌ها و یا stack-trace آن‌ها را به کاربر نمایش نداد؛ تا مبادا اطلاعات حساسی فاش شوند و یا کاربر مهاجم بتواند توسط آن‌ها اطلاعات ارزشمندی را از نحوه‌ی عملکرد برنامه بدست آورد.
برخلاف برنامه‌های ASP.NET Core که دارای یک middleware pipeline هستند و برای مثال توسط آن‌ها می‌توان مدیریت سراسری خطاهای رخ‌داده را انجام داد، چنین ویژگی در برنامه‌های Blazor وجود ندارد؛ چون در اینجا مرورگر است که هاست برنامه بوده و processing pipeline آن‌را تشکیل می‌دهد.
اما ... اگر استثنائی مدیریت نشده در یک برنامه‌ی Blazor رخ‌دهد، این استثناء در ابتدا توسط یک ILogger، لاگ شده و سپس در کنسول مرورگر نمایش داده می‌شود. در اینجا Console Logging Provider، تامین کننده‌ی پیش‌فرض سیستم ثبت وقایع برنامه‌های Blazor است. به همین جهت استثناءهای مدیریت نشده‌ی برنامه را می‌توان در کنسول توسعه دهندگان مرورگر نیز مشاهده کرد. برای مثال اگر سطح لاگ ارائه شده LogLevel.Error باشد، به صورت خودکار به معادل console.error ترجمه می‌شود.
بنابراین اگر در برنامه‌ی Blazor جاری یک ILoggerProvider سفارشی را تهیه و آن‌را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، می‌توان از تمام وقایع سیستم (هر قسمتی از آن که از ILogger استفاده می‌کند)، منجمله تمام خطاهای رخ‌داده (و مدیریت نشده) مطلع شد و برای مثال آن‌ها را به سمت Web API برنامه، جهت ثبت در بانک اطلاعاتی و یا نمایش در برنامه‌ی تلگرام، ارسال کرد و این دقیقا همان کاری است که قصد داریم در ادامه انجام دهیم.


نوشتن یک ILoggerProvider سفارشی جهت ارسال رخ‌دادها برنامه‌ی سمت کلاینت، به یک Web API

برای ارسال تمام وقایع برنامه‌ی کلاینت به سمت سرور، نیاز است یک ILoggerProvider سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class ClientLoggerProvider : ILoggerProvider
    {
        private readonly HttpClient _httpClient;
        private readonly WebApiLoggerOptions _options;
        private readonly NavigationManager _navigationManager;

        public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
        {
            if (serviceProvider is null)
            {
                throw new ArgumentNullException(nameof(serviceProvider));
            }

            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            _httpClient = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<HttpClient>();
            _options = options.Value;
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new WebApiLogger(_httpClient, _options, _navigationManager);
        }

        public void Dispose()
        {
        }
    }
}
توضیحات:
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، روش آن به صورت زیر است:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public static class ClientLoggerProviderExtensions
    {
        public static ILoggingBuilder AddWebApiLogger(this ILoggingBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.Services.AddSingleton<ILoggerProvider, ClientLoggerProvider>();
            return builder;
        }
    }
}
باید کلاسی را داشته باشیم مانند ClientLoggerProvider که یک ILoggerProvider را پیاده سازی می‌کند و نحوه‌ی ثبت آن نیز باید حتما Singleton باشد. مزیت معرفی ILoggerProvider به این نحو، امکان دسترسی به سرویس‌های برنامه در سازنده‌ی کلاس ClientLoggerProvider است و در این حالت دیگر نیاز به نوشتن new ClientLoggerProvider نبوده و خود سیستم تزریق وابستگی‌ها، سازنده‌های ClientLoggerProvider را تامین می‌کند.
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده می‌کنید:
public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
با استفاده از IServiceProvider می‌توان به HttpClient برنامه دسترسی یافت. از این جهت که چون HttpClient به صورت پیش‌فرض با طول عمر Scoped به سیستم معرفی شده، امکان تزریق مستقیم آن به سازنده‌ی یک ILoggerProvider از نوع Singleton وجود ندارد. به همین جهت از IServiceProvider برای تامین آن استفاده خواهیم کرد. مابقی موارد مانند IOptions که تنظیمات این لاگر را فراهم می‌کند و یا NavigationManager استاندارد برنامه که امکان دسترسی به Url جاری را میسر می‌کند، به صورت پیش‌فرض دارای طول عمر Singleton هستند و می‌توان آن‌ها را بدون مشکل، به سازنده‌ی لاگر سفارشی، تزریق کرد.
مهم‌ترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت می‌دهد:
public ILogger CreateLogger(string categoryName)
{
   return new WebApiLogger(_httpClient, _options, _navigationManager);
}
بنابراین در ادامه نیاز است، یک ILogger سفارشی را نیز پیاده سازی کنیم:
using System;
using System.Net.Http;
using System.Net.Http.Json;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLogger : ILogger
    {
        private readonly WebApiLoggerOptions _options;
        private readonly HttpClient _httpClient;
        private readonly NavigationManager _navigationManager;

        public WebApiLogger(HttpClient httpClient, WebApiLoggerOptions options, NavigationManager navigationManager)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public IDisposable BeginScope<TState>(TState state) => default;

        public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter is null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            try
            {
                ClientLog log = new()
                {
                    LogLevel = logLevel,
                    EventId = eventId,
                    Message = formatter(state, exception),
                    Exception = exception?.Message,
                    StackTrace = exception?.StackTrace,
                    Url = _navigationManager.Uri
                };
                _httpClient.PostAsJsonAsync(_options.LoggerEndpointUrl, log);
            }
            catch
            {
                // don't throw exceptions from the logger
            }
        }
    }
}
نحوه‌ی عملکرد این ILogger سفارشی بسیار ساده‌است:
- متد IsEnabled آن مشخص می‌کند که چه سطحی از رخ‌دادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت می‌کند:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLoggerOptions
    {
        public string LoggerEndpointUrl { set; get; }

        public LogLevel LogLevel { get; set; } = LogLevel.Information;
    }
}
در این تنظیمات مشخص می‌کنیم که Url مربوط به اکشن متد Web API ما که قرار است اطلاعات به سمت آن ارسال شوند، چیست؟ همچنین حداقل سطح لاگ مدنظر را نیز باید مشخص کنیم. اطلاعات آن توسط فایل Client\wwwroot\appsettings.json با این محتوای فرضی قابل تنظیم است:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "WebApiLogger": {
    "LogLevel": "Warning",
    "LoggerEndpointUrl": "/api/logs"
  }
}
و همچنین باید کلاس WebApiLoggerOptions را به نحو زیر در کلاس Program برنامه به سیستم تزریق وابستگی‌ها، معرفی کرد تا <IOptions<WebApiLoggerOptions قابلیت تزریق به سازنده‌ی تامین کننده‌ی لاگر را پیدا کند:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            // …
        }
    }
}
- متد لاگ این لاگر سفارشی، پیام نهایی قابل ارسال به سمت Web API را تشکیل داده و توسط متد httpClient.PostAsJsonAsync آن‌را ارسال می‌کند. به همین جهت ساختار لاگ مدنظر را در فایل Shared\ClientLog.cs به صورت زیر تعریف کرده‌ایم که بین برنامه‌ی کلاینت و سرور، مشترک است:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Shared
{
    public class ClientLog
    {
        public LogLevel LogLevel { get; set; }

        public EventId EventId { get; set; }

        public string Message { get; set; }

        public string Exception { get; set; }

        public string StackTrace { get; set; }

        public string Url { get; set; }
    }
}
این اطلاعاتی است که کلاینت به ازای رخ‌دادی خاص، جمع آوری کرده و به سمت سرور ارسال می‌کند.

در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام می‌دهد، به صورت زیر خواهد بود:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            builder.Services.AddLogging(configure =>
            {
                configure.AddWebApiLogger();
            });

            await builder.Build().RunAsync();
        }
    }
}
تا اینجا اگر هر نوع استثنای مدیریت نشده‌ای در برنامه‌ی Blazor WASM رخ دهد، چون سطح لاگ آن بالاتر از Warning تنظیم شده‌ی در فایل Client\wwwroot\appsettings.json است:
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
به صورت خودکار به سمت کنترلر api/logs ارسال خواهد شد. بنابراین مرحله‌ی بعدی، تکمیل کنترلر یاد شده‌است.


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

پیش از اینکه کار تکمیل کنترلر api/logs را در برنامه‌ی Web API انجام دهیم، ابتدا در همان برنامه‌ی Web API، سرویسی را برای ارسال لاگ‌های رسیده به سمت تلگرام، تهیه می‌کنیم. علت اینکه این قسمت را به برنامه‌ی سمت سرور محول کرده‌ایم، شامل موارد زیر است:
- درست است که می‌توان کتابخانه‌های مرتبط با تلگرام را به برنامه‌ی سی‌شارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیم‌تر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آن‌را در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامه‌ی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامه‌ی سمت کلاینت به Web API، مزیت لاگ سمت سرور آن‌را مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر می‌کند و صرفا محدود به تلگرام نیست.

برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه می‌کنیم:
using System;
using System.Text;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;

namespace BlazorWasmTelegramLogger.Server.Services
{
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }

    public interface ITelegramBotService
    {
        Task SendLogAsync(ClientLog log);
    }

    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }

        public async Task SendLogAsync(ClientLog log)
        {
            var text = formatMessage(log);
            if (string.IsNullOrWhiteSpace(text))
            {
                return;
            }

            await _client.SendTextMessageAsync(_chatId, text, ParseMode.Markdown);
        }

        private static string formatMessage(ClientLog log)
        {
            if (string.IsNullOrWhiteSpace(log.Message))
            {
                return string.Empty;
            }

            var sb = new StringBuilder();
            sb.Append(toEmoji(log.LogLevel))
                .Append(" *")
                .AppendFormat("{0:hh:mm:ss}", DateTime.Now)
                .Append("* ")
                .AppendLine(log.Message);

            if (!string.IsNullOrWhiteSpace(log.Exception))
            {
                sb.AppendLine()
                    .Append('`')
                    .AppendLine(log.Exception)
                    .AppendLine(log.StackTrace)
                    .AppendLine("`")
                    .AppendLine();
            }

            sb.Append("*Url:* ").AppendLine(log.Url);
            return sb.ToString();
        }

        private static string toEmoji(LogLevel level) =>
            level switch
            {
                LogLevel.Trace => "⬜️",
                LogLevel.Debug => "🟦",
                LogLevel.Information => "⬛️️️",
                LogLevel.Warning => "🟧",
                LogLevel.Error => "🟥",
                LogLevel.Critical => "❌",
                LogLevel.None => "🔳",
                _ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
            };
    }
}
توضیحات:
- برای کار با API تلگرام، از کتابخانه‌ی معروف Telegram.Bot استفاده کرده‌ایم که به صورت زیر، وابستگی آن به برنامه‌ی Web API اضافه می‌شود:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Telegram.Bot" Version="15.7.1" />
  </ItemGroup>
</Project>
- این سرویس برای کار کردن، نیاز به تنظیمات زیر را دارد:
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }
- برای دریافت AccessToken، در برنامه‌ی تلگرام خود، بات مخصوصی را به نام https://t.me/botfather یافته و سپس آن‌را استارت کنید:


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

- مرحله‌ی بعد تنظیم ChatId است. نحوه‌ی کار برنامه به این صورت است که پیام‌ها را به این بات سفارشی خود ارسال کرده و این بات، آن‌ها را به کانال اختصاصی ما هدایت می‌کند. بنابراین یک کانال جدید را ایجاد کنید. ترجیحا بهتر است این کانال خصوصی باشد. سپس کاربر test_2021_logs_bot@ (همان نام منحصربفرد بات که حتما باید با @ شروع شود) را به عنوان عضو جدید کانال خود اضافه کنید. در اینجا عنوان می‌کند که این کاربر چون بات است، باید دسترسی ادمین را داشته باشد که دقیقا این دسترسی را نیز باید برقرار کنید تا بتوان توسط این بات، پیامی را به کانال اختصاصی خود ارسال کرد.
بنابراین تا اینجا یک کانال خصوصی را ایجاد کرده‌ایم که بات جدید test_2021_logs_bot@ عضو با دسترسی ادمین آن است. اکنون باید Id این کانال را بیابیم. برای اینکار بات دیگری را به نام JsonDumpBot@ یافته و استارت کنید. سپس در کانال خود یک پیام آزمایشی جدید را ارسال کنید و در ادامه این پیام را به بات JsonDumpBot@ ارسال کنید (forward کنید). همان لحظه‌ای که کار ارسال پیام به این بات صورت گرفت، Id کانال خود را در پاسخ آن می‌توانید مشاهده کنید:


در این تصویر مقدار forward_from_chat:id همان ChatId تنظیمات برنامه‌ی شما است.

در آخر این اطلاعات را در فایل Server\appsettings.json قرار می‌دهیم:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "TelegramLoggingBot": {
    "AccessToken": "1826…",
    "ChatId": "-1001…" 
  }
}
که نحوه‌ی ثبت و معرفی آن‌ها به سیستم تزریق وابستگی‌های برنامه‌ی Web API، به صورت زیر است:
namespace BlazorWasmTelegramLogger.Server
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<TelegramLoggingBotOptions>(options =>
                            Configuration.GetSection("TelegramLoggingBot").Bind(options));
            services.AddSingleton<ITelegramBotService, TelegramBotService>();

            // ...
        }

        // ...
    }
}
سرویس ITelegramBotService را با طول عمر Singleton معرفی کرده‌ایم. چون new TelegramBotClient ای که در سازنده‌ی آن صورت می‌گیرد:
    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }
باید فقط یکبار در طول عمر برنامه انجام شود و از این پس، هر بار که متد client.SendTextMessageAsync_ آن فراخوانی می‌گردد، پیامی به سمت بات و سپس کانال اختصاصی ما ارسال می‌شود.


ایجاد کنترلر Logs، جهت دریافت لاگ‌های رسیده‌ی از سمت کلاینت

مرحله‌ی آخر کار بسیار ساده‌است. سرویس تکمیل شده‌ی ITelegramBotService را به سازنده‌ی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آن‌را فراخوانی می‌کنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
using System;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Server.Services;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class LogsController : ControllerBase
    {
        private readonly ILogger<LogsController> _logger;
        private readonly ITelegramBotService _telegramBotService;

        public LogsController(ILogger<LogsController> logger, ITelegramBotService telegramBotService)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _telegramBotService = telegramBotService;
        }

        [HttpPost]
        public async Task<IActionResult> PostLog(ClientLog log)
        {
            // TODO: Save the client's `log` in the database

            _logger.Log(log.LogLevel, log.EventId, log.Url + Environment.NewLine + log.Message);

            await _telegramBotService.SendLogAsync(log);

            return Ok();
        }
    }
}


آزمایش برنامه

برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار داده‌ایم:
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        throw new InvalidOperationException("This is an exception message from the client!");
    }
}
اکنون اگر برنامه را اجرا کرده و سپس بر روی دکمه‌ی شمارشگر کلیک کنیم، همان تصویر ابتدای مطلب را که حاصل از ارسال جزئیات این استثنای مدیریت نشده به سمت تلگرام است، مشاهده خواهیم کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmTelegramLogger.zip
مطالب
مقدمه‌ای بر تزریق وابستگی‌ها درASP.NET Core
ASP.NET Core با ذهنیت پشتیبانی و استفاده از تزریق وابستگی‌ها ایجاد شده‌است. اپلیکیشن‌های ASP.NET Core از سرویس‌های ذاتی فریم ورک که داخل متدهای کلاس Startup پروژه تزریق شده‌اند و همچنین سرویس‌های اپلیکیشن که تنظیمات خاص آنها در پروژه انجام گرفته است، استفاده می‌کنند. سرویس کانتینر پیش فرض ارائه شده توسط ASP.NET Core، مجموعه‌ای حداقلی از ویژگی‌ها را ارائه می‌کند و هدف آن جایگزینی با دیگر فریم ورک‌های تزریق وابستگی نمی‌باشد.

مشاهده یا دانلود کدهای مقاله


تزریق وابستگی چیست؟

تزریق وابستگی (DI) تکنیکی برای دستیابی به اتصال شل بین اشیاء و همکاران اشیاء و وابستگی‌های بین آنها می‌باشد. یک شیء برای انجام وظایف خود، بجای اینکه اشیاء همکار خود را به صورت مستقیم نمونه سازی کند، یا از ارجاعات استاتیک استفاده نماید، می‌تواند از اشیائی که برایش تامین شده‌است، استفاده کند. در اغلب موارد کلاس‌ها، وابستگی‌های خود را از طریق سازنده‌ی خود درخواست می‌کنند، که به آنها اجازه می‌دهد اصل وابستگی صریح را رعایت کنند (Explicit Dependencies Principle). این روش را «تزریق در سازنده» می‌نامند.
از آنجا که در طراحی کلاس‌ها با استفاده از DI، نمونه سازی مستقیم، توسط کلاس‌ها و به صورت Hard-coded انجام نمی‌گیرد، وابستگی بین اشیاء کم شده و پروژه‌ای با اتصالات شل به دست می‌آید. با این کار اصل وابستگی معکوس (Dependency Inversion Principle) رعایت می‌شود. بر اساس این اصل، ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین خود وابسته باشند؛ بلکه هر دو باید به کلاس‌هایی انتزاعی وابسته باشند. اشیاء بجای ارجاع به پیاده سازی‌های خاص کلاس‌های همکار خود، کلاس‌های انتزاعی، معمولاٌ اینترفیس آنها را درخواست می‌کنند و هنگام نمونه سازی از آنها (داخل متد سازنده) کلاس پیاده سازی شده برایشان تامین می‌شود. خارج کردن وابستگی‌‎های مستقیم از کلاس‌ها و تامین پیاده سازی‌های این اینترفیس‌ها به صورت پارامتر‌هایی برای کلاس‌ها، یک مثال از الگوی طراحی استراتژی (Strategy design pattern) می‌باشد.

در حالتیکه کلاس‌ها به تعداد زیادی کلاس وابستگی داشته باشند و برای اجرا شدن، نیاز به تامین وابستگی‌هایشان داشته باشند، بهتر است یک کلاس اختصاصی، برای نمونه سازی این کلاس‌ها با وابستگی‌های مورد نیاز آنها، در سیستم وجود داشته باشد. این کلاس نمونه ساز را کانتینرIoC، یا کانتینر DI یا به طور خلاصه کانتینر می‌نامند ( Inversion of Control (IoC) ). کانتینر در اصل یک کارخانه می‌باشد که وظیفه‌ی تامین نمونه‌هایی از کلاس‌هایی را که از آن درخواست می‌شود، انجام می‌دهد. اگر یک کلاس تعریف شده، وابستگی به کلاس‌های دیگر داشته باشد و کانتینر برای ارائه وابستگی‌های کلاس تعریف شده تنظیم شده باشد، هر موقع نیاز به یک نمونه از این کلاس وجود داشته باشد، به عنوان بخشی از کار نمونه سازی از کلاس مورد نظر، کلاس‌های وابسته‌ی آن نیز ایجاد می‌شوند (همه‌ی کارهای مربوط به نمونه سازی کلاس خاص و کلاس‌های وابسته به آن توسط کانتینر انجام می‌گیرد). به این ترتیب، می‌توان وابستگی‌های بسیار پیچیده و تو در توی موجود در سیستم را بدون نیاز به هیچگونه نمونه سازی hard-code شده، برای کلاس‌ها فراهم کرد. کانتینرها علاوه بر ایجاد اشیاء و وابستگی‌های موجود در آنها، معمولا طول عمر اشیاء در اپلیکیشن را نیز مدیریت می‌کنند.
ASP.NET Core یک کانتینر بسیار ساده را به نام اینترفیس IServiceProvider  ارائه داده است که به صورت پیش فرض از تزریق وابستگی در سازنده‌ی کلاس‌ها پشتیبانی می‌کند و همچنین ASP.NET برخی از سرویس‌های خود را از طریق DI در دسترس قرار داده است. کانتینرASP.NET، یک اشاره‌گر به کلاس‌هایی است که به عنوان سرویس عمل می‌کنند. در ادامه‌ی این مقاله، سرویس‌ها به کلاس‌هایی گفته می‌شود که به وسیله‌ی کانتینر ASP.NET Core مدیریت می‌شوند. شما می‌توانید سرویس ConfigureServices کانتینر را در داخل کلاس Startup پروژه خود پیکربندی کنید.


تزریق وابستگی از طریق متد سازنده‌ی کلاس

تزریق وابستگی از طریق متد سازنده، مستلزم آن است که سازنده‌ی کلاس مورد نظر عمومی باشد. در غیر این صورت، اپلیکیشن شما استثنای InvalidOperationException  را با پیام زیر نشان می‌دهد:
 A suitable constructor for type 'YourType' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.

تزریق از طریق متد سازنده مستلزم آن است که تنها یک سازنده‌ی مناسب وجود داشته باشد. البته Overload سازنده امکان پذیر است؛ ولی باید تنها یک متد سازنده وجود داشته باشد که آرگومان‌های آن توسط DI قابل ارائه باشند. اگر بیش از یکی وجود داشته باشد، سیستم استثنای InvalidOperationException را با پیام زیر نشان می‌دهد:
 Multiple constructors accepting all given argument types have been found in type 'YourType'. There should only be one applicable constructor.

سازندگان می‌توانند آرگومان‌هایی را از طریق DI دریافت کنند. برای این منظور آرگومان‌های این سازنده‌ها باید مقدار پیش فرضی را داشته باشند. به مثال زیر توجه نمایید:
// throws InvalidOperationException: Unable to resolve service for type 'System.String'...
public CharactersController(ICharacterRepository characterRepository, string title)
{
    _characterRepository = characterRepository;
    _title = title;
}

// runs without error
public CharactersController(ICharacterRepository characterRepository, string title = "Characters")
{
    _characterRepository = characterRepository;
    _title = title;
}


استفاده از سرویس ارائه شده توسط فریم ورک

متد ConfigureServices در کلاس Startup، مسئول تعریف سرویس‌هایی است که سیستم از آن استفاده می‌کند. از جمله‌ی این سرویس‌ها می‌توان به ویژگی‌های پلتفرم مانند EF Core و ASP.NET Core MVC اشاره کرد. IServiceCollection که به ConfigureServices ارائه می‌شود، سرویس‌های زیر را تعریف می‌کند (که البته بستگی به نوع پیکربندی هاست دارد):

  نوع سرویس    طول زندگی 
    Microsoft.AspNetCore.Hosting.IHostingEnvironment  
 Singleton 
    Microsoft.AspNetCore.Hosting.IApplicationLifetime     Singleton 
    Microsoft.AspNetCore.Hosting.IStartup     Singleton 
    Microsoft.AspNetCore.Hosting.Server.IServer     Singleton 
    Microsoft.Extensions.Options.IConfigureOptions     Transient 
    Microsoft.Extensions.ObjectPool.ObjectPoolProvider     Singleton 
    Microsoft.AspNetCore.Hosting.IStartupFilter     Transient 
    System.Diagnostics.DiagnosticListener     Singleton 
    System.Diagnostics.DiagnosticSource     Singleton 
    Microsoft.Extensions.Options.IOptions     Singleton 
    Microsoft.AspNetCore.Http.IHttpContextFactory     Transient 
    Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory     Transient 
    Microsoft.Extensions.Logging.ILogger     Singleton 
    Microsoft.Extensions.Logging.ILoggerFactory  
 Singleton 

در زیر نمونه ای از نحوه‌ی اضافه کردن سرویس‌های مختلف را به کانتینر، با استفاده از متدهای الحاقی مانند AddDbContext، AddIdentity و AddMvc، مشاهده می‌کنید:

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();
}
ویژگی‌ها و میان افزار‌های ارائه شده توسط ASP.NET، مانند MVC، از یک قرارداد، با استفاده از متد الحاقی AddServiceName برای ثبت تمام سرویس‌های مورد نیاز این ویژگی پیروی می‌کنند.


ثبت سرویس‌های اختصاصی

شما می‌توانید سرویس‌های اپلیکیشن خودتان را به ترتیبی که در تکه کد زیر مشاهده می‌کنید، ثبت نمایید. اولین نوع جنریک، نوعی است که از کانتینر درخواست خواهد شد و معمولا به شکل اینترفیس می‌باشد. نوع دوم، نوع پیاده سازی شده‌ای است که به وسیله‌ی کانتینر، نمونه سازی خواهد شد و کانتینر برای درخواست‌های از نوع اول، این نمونه از  تایپ را ارائه خواهد کرد:
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();

نکته:
هر متد الحاقی <services.Add<ServiceName، سرویس‌هایی را اضافه و پیکربندی می‌کند. به عنوان مثال services.AddMvc نیازمندی‌های سرویس MVC را اضافه می‌کند. توصیه می‌شود شما هم با افزودن متدهای الحاقی در فضای نام Microsoft.Extensions.DependencyInjection این قرارداد را رعایت نمائید. این کار باعث کپسوله شدن ثبت گروهی سرویس‌ها می‌شود.
متد AddTransient، برای نگاشت نوع‌های انتزاعی به سرویس‌های واقعی که نیاز به نمونه سازی به ازای هر درخواست دارند، استفاده می‌شود. در اصطلاح، طول عمر سرویس‌ها در اینجا مشخص می‌شوند. در ادامه گزینه‌های دیگری هم برای طول عمر سرویس‌ها تعریف خواهند شد. خیلی مهم است که برای هر یک از سرویس‌های ثبت شده، طول عمر مناسبی را انتخاب نمایید. آیا برای هر کلاس که سرویسی را درخواست می‌کند، باید یک نمونه‌ی جدید ساخته شود؟ آیا فقط یک نمونه در طول یک درخواست وب مورد استفاده قرار می‌گیرد؟ یا باید از یک نمونه‌ی واحد برای طول عمر کل اپلیکیشن استفاده شود؟
در مثال ارائه شده‌ی در این مقاله، یک کنترلر ساده به نام CharactersController وجود دارد که نام کاراکتری را نشان می‌دهد. متد Index، لیست کنونی کاراکترهایی را که در اپلیکیشن ذخیره شده‌اند، نشان می‌دهد. در صورتیکه این لیست خالی باشد، تعدادی به آن اضافه می‌کند. توجه داشته باشید، اگرچه این اپلیکیشن از Entity Framework Core و ClassDataContext برای داده‌های مانا استفاده می‌کند، هیچیکدام از آنها در کنترلر ظاهر نمی‌شوند. در عوض، مکانیزم دسترسی به داده‌های خاص، در پشت یک اینترفیس (ICharacterRepository) مخفی شده است (طبق الگوی طراحی ریپازیتوری). یک نمونه از ICharacterRepository از طریق سازنده درخواست می‌شود و به یک فیلد خصوصی اختصاص داده می‌شود، سپس برای دسترسی به کاراکتر‌ها در صورت لزوم استفاده می‌شود:
public class CharactersController : Controller
{
    private readonly ICharacterRepository _characterRepository;

    public CharactersController(ICharacterRepository characterRepository)
    {
        _characterRepository = characterRepository;
    }

    // GET: /characters/
    public IActionResult Index()
    {
        PopulateCharactersIfNoneExist();
        var characters = _characterRepository.ListAll();

        return View(characters);
    }

    private void PopulateCharactersIfNoneExist()
    {
        if (!_characterRepository.ListAll().Any())
        {
            _characterRepository.Add(new Character("Darth Maul"));
            _characterRepository.Add(new Character("Darth Vader"));
            _characterRepository.Add(new Character("Yoda"));
            _characterRepository.Add(new Character("Mace Windu"));
        }
    }
}

ICharacterRepository دو متد مورد نیاز کنترلر برای کار با نمونه‌های Character را تعریف می‌کند:
using System.Collections.Generic;
using DependencyInjectionSample.Models;

namespace DependencyInjectionSample.Interfaces
{
    public interface ICharacterRepository
    {
        IEnumerable<Character> ListAll();
        void Add(Character character);
    }
}
این اینترفیس با نوع واقعی CharacterRepository پیاده سازی شده است که در زمان اجرا استفاده می‌شود:

using System.Collections.Generic;
using System.Linq;
using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Models
{
    public class CharacterRepository : ICharacterRepository
    {
        private readonly ApplicationDbContext _dbContext;

        public CharacterRepository(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public IEnumerable<Character> ListAll()
        {
            return _dbContext.Characters.AsEnumerable();
        }

        public void Add(Character character)
        {
            _dbContext.Characters.Add(character);
            _dbContext.SaveChanges();
        }
    }
}
توجه داشته باشید که CharacterRepository یک ApplicationDbContext را در سازنده‌ی خود درخواست می‌کند. همانطور که مشاهده می‌شود هر وابستگی درخواست شده، به نوبه خود وابستگی‌های دیگری را درخواست می‌کند. تزریق وابستگی‌هایی به شکل زنجیره‌ای، همانند این مثال غیر معمول نیست. کانتینر مسئول resolve (نمونه سازی) همه‌ی وابستگی‌های موجود در گراف وابستگی و بازگرداندن سرویس کاملا resolve شده می‌باشد.

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

در مورد مثال مطرح شده، ICharacterRepository و به نوبه خود ApplicationDbContext باید با سرویس‌های خود در کانتینر ConfigureServices و کلاس Startup ثبت شوند. ApplicationDbContext با فراخوانی متد <AddDbContext<T پیکربندی می‌شود. کد زیر ثبت کردن نوع CharacterRepository را نشان می‌دهد:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseInMemoryDatabase()
    );

    // Add framework services.
    services.AddMvc();

    // Register application services.
    services.AddScoped<ICharacterRepository, CharacterRepository>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
    services.AddTransient<OperationService, OperationService>();
}
کانتکست انتیتی فریم ورک، با استفاده از متدهای کمکی که در تکه کد بالا نشان داده شده است، باید با طول عمر Scoped به کانتینر سرویس‌ها افزوده شود. این کار می‌تواند به صورت اتوماتیک انجام گیرد. همه‌ی ریپازیتوری‌هایی که از Entity Framework استفاده می‌کنند، باید از یک طول عمر مشابه استفاده کنند.

هشدار
خطر بزرگی را که باید در نظر گرفت، resolve کردن سرویس Scoped از طول عمر singleton می‌باشد. در صورت انجام این کار، احتمال دارد که سرویس‌ها وارد حالت نادرستی شوند.

سرویس‌هایی که وابستگی‌های دیگری هم دارند، باید آنها را در کانتینر ثبت کنند. اگر سازنده‌ی سرویس نیاز به یک primitive به عنوان ورودی داشته باشد، می‌توان با استفاده از الگوی گزینه‌ها و پیکربندی (options pattern and configuration)، ورودی‌های مناسبی را به سازنده‌ها منتقل کرد.


طول عمر سرویس‌ها و گزینه‌های ثبت

سرویس‌های ASP.NET را می‌توان با طول عمرهای زیر پیکربندی کرد:
Transient: سرویس‌هایی با طول عمر Transient، در هر زمان که درخواست می‌شوند، مجددا ایجاد می‌شوند. این طول عمر برای سرویس‌های سبک و بدون حالت مناسب می‌باشند.
Scoped: سرویس‌هایی با طول عمر Scoped، تنها یکبار در طی هر درخواست ایجاد می‌شوند.
Singleton: سرویس‌هایی با طول عمر Singleton، برای اولین باری که درخواست می‌شوند (یا اگر در ConfigureServices نمونه‌ای را مشخص کرده باشید) ایجاد می‌شوند و درخواست‌های آتی برای این سرویس‌ها از همان نمونه‌ی ایجاد شده استفاده می‌کنند. اگر اپلیکیشن شما درخواست رفتار singleton را داشته باشد، پیشنهاد می‌شود که سرویس کانتینر را برای مدیریت طول عمر سرویس مورد نیاز پیکربندی کنید و خودتان الگوی طراحی singleton را پیاده سازی نکنید.

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

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

using System;

namespace DependencyInjectionSample.Interfaces
{
    public interface IOperation
    {
        Guid OperationId { get; }
    }

    public interface IOperationTransient : IOperation
    {
    }
    public interface IOperationScoped : IOperation
    {
    }
    public interface IOperationSingleton : IOperation
    {
    }
    public interface IOperationSingletonInstance : IOperation
    {
    }
}
ما این اینترفیس‌ها را با استفاده از یک کلاس واحد به نام Operation پیاده سازی کرده‌ایم. سازنده‌ی این کلاس، یک Guid به عنوان ورودی می‌گیرد؛ یا اگر Guid برایش تامین نشد، خودش یک Guid جدید را می‌سازد.
سپس در ConfigureServices، هر نوع با توجه به طول عمر مورد نظر، به کانتینر افزوده می‌شود:
services.AddScoped<ICharacterRepository, CharacterRepository>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddTransient<OperationService, OperationService>();
توجه داشته باشید که سرویس IOperationSingletonInstance، از یک نمونه‌ی خاص، با شناسه‌ی شناخته شده‌ی Guid.Empty استفاده می‌کند (این Guid فقط شامل اعداد صفر می‌باشد). بنابراین زمانیکه این تایپ مورد استفاده قرار می‌گیرد، کاملا واضح است. تمام این سرویس‌ها وابستگی‌های خود را به صورت پراپرتی نمایش می‌دهند. بنابراین می‌توان آنها را در View نمایش داد.

using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Services
{
    public class OperationService
    {
        public IOperationTransient TransientOperation { get; }
        public IOperationScoped ScopedOperation { get; }
        public IOperationSingleton SingletonOperation { get; }
        public IOperationSingletonInstance SingletonInstanceOperation { get; }

        public OperationService(IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationSingletonInstance instanceOperation)
        {
            TransientOperation = transientOperation;
            ScopedOperation = scopedOperation;
            SingletonOperation = singletonOperation;
            SingletonInstanceOperation = instanceOperation;
        }
    }
}
برای نشان دادن طول عمر اشیاء، در بین درخواست‌های جداگانه‌ی یک اپلیکیشن، مثال ذکر شده شامل کنترلر OperationsController می‌باشد که هر کدام از انواع IOperation و همچنین OperationService را درخواست می‌کند. سپس اکشن Index تمام مقادیر OperationId کنترل کننده و سرویس‌ها را نمایش می‌دهد:
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionSample.Controllers
{
    public class OperationsController : Controller
    {
        private readonly OperationService _operationService;
        private readonly IOperationTransient _transientOperation;
        private readonly IOperationScoped _scopedOperation;
        private readonly IOperationSingleton _singletonOperation;
        private readonly IOperationSingletonInstance _singletonInstanceOperation;

        public OperationsController(OperationService operationService,
            IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationSingletonInstance singletonInstanceOperation)
        {
            _operationService = operationService;
            _transientOperation = transientOperation;
            _scopedOperation = scopedOperation;
            _singletonOperation = singletonOperation;
            _singletonInstanceOperation = singletonInstanceOperation;
        }

        public IActionResult Index()
        {
            // viewbag contains controller-requested services
            ViewBag.Transient = _transientOperation;
            ViewBag.Scoped = _scopedOperation;
            ViewBag.Singleton = _singletonOperation;
            ViewBag.SingletonInstance = _singletonInstanceOperation;

            // operation service has its own requested services
            ViewBag.Service = _operationService;
            return View();
        }
    }
}

حالا دو درخواست جداگانه برای این کنترلر ساخته شده است:



به تفاوت‌های موجود در مقادیر OperationId در یک درخواست و بین درخواستها توجه کنید:
-  OperationId اشیاء Transient همیشه متفاوت می‌باشند. چون یک نمونه جدید برای هر کنترلر و هر سرویس ایجاد شده‌است.
- اشیاء Scoped در یک درخواست، یکسان هستند؛ اما در درخواست‌های مختلف متفاوت می‌باشند.
- اشیاء Singleton برای هر شی‌ء و هر درخواست (صرف نظر از اینکه یک نمونه در ConfigureServices ارائه شده است) یکسان می‌باشند.


درخواست سرویس

در ASP.NET سرویس‌های موجود در یک درخواست HttpContext از طریق مجموعه RequestServices قابل مشاهده می‌باشد.


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


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


طراحی سرویس‌ها برای تزریق وابستگی‌ها

شما باید سرویس‌های خود را طوری طراحی کنید که از تزریق وابستگی‌ها برای ارتباطات خود استفاده نمایند. این کار باعث کاهش استفاده از فراخوانی‌های متدهای استاتیک (متدهای استاتیک، حالت دار می‌باشند و استفاده‌ی زیاد از آنها باعث به وجود آمدن بوی بد کدی به نام static cling، می‌شود) و همچنین از بین رفتن نیاز به نمونه سازی مستقیم کلاس‌های وابسته داخل سرویس‌ها، می‌شود. هر موقع بخواهید بین new کردن یک کلاس، یا درخواست دادن آن از طریق تزریق وابستگی، یکی را انتخاب کنید، این اصطلاح را به یاد بیاورید،  New is Glue. با پیروی از اصول SOLID طراحی شیء گرا، به طور طبیعی کلاس‌های شما تمایل به کوچک بودن، کارا و قابل تست بودن را دارند.
اگر متوجه شدید که کلاس‌های شما تمایل دارند تا تعداد وابستگی‌های زیادی به آنها تزریق شود، چه باید بکنید؟ به طور کلی این مشکل نشانه‌ای است از نقض  Single Responsibility Principle یا SRP است و احتمالا کلاس‌های شما وظایف بیش از اندازه‌ای را دارند. در این گونه موارد تلاش کنید مقداری از وظایف کلاس را به یک کلاس جدید منتقل کنید. در نظر داشته باشید که کلاس‌های کنترلر باید به مسائل UI تمرکز کنند و قوانین کسب و کار و جزئیات دسترسی به داده‌ها باید در کلاس‌هایی جداگانه و مرتبط با خود قرار داشته باشند.
به طور خاص برای دسترسی به داده ، شما می‌توانید DbContext را به کنترلر‌های خود تزریق کنید (با فرض اینکه شما EF را به کانتینر سرویس ConfigureServices اضافه کرده‌اید). بعضی از توسعه دهندگان به جای تزریق مستقیم DbContext از یک اینترفیس ریپازیتوری استفاده می‌نمایند. می‌توانید با استفاده از یک اینترفیس برای کپسوله کردن منطق دسترسی به داده‌ها در یک مکان، تعداد تغییرات مورد نیاز را در صورت تغییر دیتابیس، به حداقل برسانید.


تخریب سرویس ها

سرویس کانتینر برای نوع‌های IDisposable که خودش ایجاد کرده‌است، متد Dispose را فراخوانی خواهد کرد. با این حال، اگر شما خودتان نمونه‌ای را به صورت دستی نمونه سازی و به کانتینر اضافه کرده باشید، سرویس کانتینر آنرا dispose نخواهد کرد.

مثال:
// Services implement IDisposable:
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public class Service3 : IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    // container will create the instance(s) of these types and will dispose them
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();

    // container did not create instance so it will NOT dispose it
    services.AddSingleton<Service3>(new Service3());
    services.AddSingleton(new Service3());
}

نکته:
در نسخه 1.0، کانتینر برای تمام اشیاء از نوع IDisposable از جمله اشیائی که خودش ایجاد نکرده بود، متد dispose را فراخوانی می‌کرد.


سرویس‌های کانتینر جانشین

کانتینر موجود در net core. به منظور تامین نیازهای اساسی فریم ورک ایجاد شده‌است و تعداد زیادی از اپلیکیشن‌ها از آن استفاده می‌کنند. با این حال، توسعه دهندگان می‌توانند کانتینرهای مورد نظر خود را جایگزین آن کنند. متد ConfigureServices به طور معمول مقدار void را بر می‌گرداند. اما با تغییر امضای آن به نوع بازگشتیIServiceProvider، می‌توان سرویس کانتینر متفاوتی را در اپلیکیشن پیکربندی کرد. سرویس‌های کانتینر IOC مختلفی برای NET. وجود دارند؛ در مثال زیر، Autofac استفاده شده است.
در ابتدا بسته‌های زیر را نصب کنید:
Autofac
Autofac.Extensions.DependencyInjection
سپس کانتینر را در ConfigureServices پیکربندی کنید و  IServiceProvider را به عنوان خروجی بازگردانید:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    // Add other framework services

    // Add Autofac
    var containerBuilder = new ContainerBuilder();
    containerBuilder.RegisterModule<DefaultModule>();
    containerBuilder.Populate(services);
    var container = containerBuilder.Build();
    return new AutofacServiceProvider(container);
}


توصیه ها

هنگام کار با تزریق وابستگی‌ها، توصیه‌های ذیر را در نظر داشته باشید:
- DI برای اشیایی که دارای وابستگی پیچیده هستند، مناسب می‌باشد. کنترلرها، سرویس‌ها، آداپتورها و ریپازیتوری‌ها، نمونه‌هایی از این اشیاء هستند که می‌توانند به DI اضافه شوند.
- از ذخیره‌ی داده‌ها و پیکربندی مستقیم در DI اجتناب کنید. به عنوان مثال، معمولا سبد خرید کاربر نباید به سرویس کانتینر اضافه شود. پیکربندی باید از مدل گزینه‌ها استفاده کند. همچنین از اشیاء "data holder"، که فقط برای دسترسی دادن به اشیاء دیگر ایجاد شده‌اند، نیز اجتناب کنید. در صورت امکان بهتر است شیء واقعی مورد نیاز DI درخواست شود.
- از دسترسی استاتیک به سرویس‌ها اجتناب شود.
- از نمونه سازی مستقیم سرویس‌ها در کد برنامه خود اجتناب کنید.
- از دسترسی استاتیک به HttpContext اجتناب کنید.

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

مرجع: Introduction to Dependency Injection in ASP.NET Core
مطالب
پشتیبانی آنلاین سایت با 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();

مطالب
کامپوننت‌های جنریک در Blazor
طرح مشکل! نیاز به دریافت انواع و اقسام مقادیر یک جنس (مانند اعداد و یا تاریخ) در کامپوننت‌های Blazor

فرض کنید می‌خواهید عددی را در کامپوننتی، دریافت کنید. می‌توان اینکار را با تعریف یک پارامتر عمومی به صورت زیر انجام داد:
[Parameter] public int Value { get; set; }
و ... مشکل از همینجا شروع می‌شود! خوب، برای نوع‌های double ، decimal ، float ، long و غیره چه باید کرد؟ آیا باید به ازای هر کدام، یک پارامتر مخصوص را تعریف کنیم؟ و یا اگر خواستیم زمان و تاریخ را از کاربر دریافت کنیم، آیا باید او را مجبور به ارائه‌ی فقط DateTime کنیم و یا ... شاید بهتر باشد به او بگوییم اگر «رشته‌ای» را در اختیار ما قرار دهید، ما یکبار دیگر خودمان کار تبدیل آن‌را به نوع تاریخ انجام خواهیم داد!
برای حل این نوع مشکلات، می‌توان در Blazor پارامترها را جنریک تعریف کرد. برای نمونه اگر به طراحی یک سری کامپوننت‌های پایه‌ای Blazor دقت کنیم، امکان دریافت مستقیم انواع و اقسام اعداد و یا انواع و اقسام نوع‌های مرتبط با زمان را دارند؛ بدون اینکه این اطلاعات را به صورت رشته‌ای دریافت کنند و یا به ازای هر نوع عددی، یک پارامتر جداگانه را تعریف و یا اینکه استفاده کننده را محدود به ورود تنها یک نوع عددی و یا زمانی خاص کرده باشند.


نحوه‌ی جنریک تعریف کردن یک کامپوننت در Blazor

Blazor فایل‌های razor. را در حین پروسه‌ی build، به cs. تبدیل می‌کند و از آنجائیکه فایل‌های razor. الزامی به تعریف نام کلاس مرتبط را ندارند و این نام به صورت خودکار دقیقا از نام خود فایل، دریافت می‌شود، نیاز است راهی را یافت تا بتوان کلاس مرتبط را به صورت Generic تعریف کرد. برای این منظور، دایرکتیو ویژه‌ای به نام typeparam@ پیش‌ بینی شده‌است که توسط آن می‌توان یک یا چند نوع/پارامتر جنریک جدا شده‌ی توسط کاما را تعریف کنیم (تعریف شده‌ی در فایل فرضی MyGenericComponent.razor):
@typeparam T

@code {
    [Parameter] public T Value { get; set; }
}
در این مثال کامپوننتی را مشاهده می‌کنید که مقدار دریافتی خود را به صورت جنریک از نوع T دریافت می‌کند. این T باید توسط دایرکتیو typeparam@ دقیقا قید شود تا در حین ساخت خودکار کلاس معادل این فایل، مورد استفاده قرار گیرد.


نحوه‌ی جنریک تعریف کردن کامپوننت‌های دارای code-behind

اگر قسمت code@ فایل‌های razor. را به فایل‌های code-benind منتقل می‌کنید و داخل فایل razor.، کد #C نمی‌نویسید، روش جنریک تعریف کردن یک کامپوننت، به صورت زیر خواهد بود (برای مثال دو فایل MyGenericComponentWithCodeBehind.razor و MyGenericComponentWithCodeBehind.razor.cs تعریف شده‌اند):
using Microsoft.AspNetCore.Components;

namespace BlazorGenericComponents.Pages;

public partial class MyGenericComponentWithCodeBehind<T>
{
    [Parameter] public T Value { get; set; }
}
در اینجا ذکر دایرکتیو typeparam@ در فایل razor. متناظر هم باید صورت گیرد:
@typeparam T
یعنی هر دو کلاس نهایی حاصل از این فایل‌ها که به صورت partial تعریف شده‌اند (و در آخر یکی می‌شوند)، باید به صورت جنریک تعریف شوند.


روش مقید کردن پارامترهای جنریک در کامپوننت‌ها

از زمان دات نت 6 به بعد، امکان محدود کردن بازه‌ی نوع‌های قابل پذیرش T نیز میسر شده‌است:
@typeparam T where T : IMyInterface


روش مشخص کردن صریح نوع پارامترهای جنریک، در حین استفاده‌ی از آن‌ها

عموما نیازی به مشخص کردن نوع پارامترهای جنریک، در حین استفاده‌ی از آن‌ها نیست و کامپایلر بر اساس نوع مقدار ورودی، سعی خواهد کرد این نوع را به صورت خودکار تشخیص دهد؛ اما اگر به هر دلیلی چنین امری ممکن نبود و خطایی را دریافت کردید، می‌توان نوع پارامتر جنریک را به صورت زیر مشخص کرد:
<MyGenericComponent Value="10" T="int"/>
در اینجا نام پارامتر جنریک، ذکر شده و سپس نوعی به آن انتساب داده می‌شود.


روش پردازش پارامترهای جنریک در کامپوننت‌ها

تا اینجا امکان پذیرش نوع‌های جنریک را توسط استفاده کننده میسر کردیم؛ اما قسمت دیگر آن، نحوه‌ی کار با نوع T، درون یک کامپوننت است:
using Microsoft.AspNetCore.Components;

namespace BlazorGenericComponents.Pages;

public partial class MyGenericComponentWithCodeBehind<T>
{
    private string FormattedValue { set; get; }

    [Parameter] public T Value { get; set; }

    [Parameter] public string Format { get; set; }

    protected override void OnParametersSet()
    {
        FormattedValue = ConvertNumberToText();
    }

    private string ConvertNumberToText()
    {
        return Value switch
        {
            short shortValue => shortValue.ToString(Format),
            ushort ushortValue => ushortValue.ToString(Format),
            int intValue => intValue.ToString(Format),
            uint uintValue => uintValue.ToString(Format),
            byte byteValue => byteValue.ToString(Format),
            sbyte sbyteValue => sbyteValue.ToString(Format),
            decimal decimalValue => decimalValue.ToString(Format),
            double doubleValue => doubleValue.ToString(Format),
            float floatValue => floatValue.ToString(Format),
            long longValue => longValue.ToString(Format),
            ulong ulongValue => ulongValue.ToString(Format),
            _ => string.Empty
        };
    }
}
برای مثال فرض کنید کامپوننت جنریک ما قرار است انواع و اقسام اعداد را بپذیرد و سپس بر اساس Format مشخص شده، آن‌ها را نمایش دهد. در اینجا جهت واکنش نشان دادن به تغییرات Value، می‌توان از روال رخ‌داد گردان OnParametersSet استفاده کرد. سپس در متد ConvertNumberToText، بر اساس هر نوع میسر عددی، باید منطق ویژه‌ای را تهیه کرد که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.

و در آخر نمایش نهایی آن هم در فایل razor. متناظر، به این صورت خواهد بود:
@typeparam T

@FormattedValue
مطالب
آشنایی با الگوی طراحی Prototype
فرض کنید در حال پختن یک کیک هستید. ابتدا کیک را می‌پذید و سپس آن را تزیین می‌کنید. عملیات پختن کیک، فرآیند ثابتی است و تزیین کردن آن متفاوت. گاهی کیک را با کاکائو تزیین می‌کنید و گاهی با میوه و غیره. 
پیش از اینکه سناریو را بیش از این جلو ببریم، وارد بحث کد می‌شویم. طبق سناریوی فوق، فرض کنید کلاسی بنام Prototype دارید که این کلاس هم از کلاس انتزاعی APrototype ارث برده است. در ادامه یک شیء از این کلاس می‌سازید و مقادیر مختلف آن را تنظیم کرده و کار را ادامه می‌دهید. 
public abstract class APrototype : ICloneable     {
        public string Name { get; set; }
        public string Health { get; set; }
    }

    public class Prototype : APrototype
    {
        public override string ToString() { return string.Format("Player name: {0}, Health statuse: {1}", Name, Health); }
    }
در ادامه از این کلاس نمونه‌گیری می‌کنیم:
Prototype p1 = new Prototype { Name = "Vahid", Health = "OK" };
Console.WriteLine(p1.ToString());
حالا فرض کنید به یک آبجکت دیگر نیاز دارید، ولی این آبجکت عینا مشابه p1 است؛ لذا نمونه‌گیری، از ابتدا کار مناسبی نیست. برای اینکار کافیست کدها را بصورت زیر تغییر دهیم:
 public abstract class APrototype : ICloneable
    {
        public string Name { get; set; }
        public string Health { get; set; }
        public abstract object Clone();
    }

    public class Prototype : APrototype
    {
        public override object Clone() { return this.MemberwiseClone() as APrototype; }
        public override string ToString() { return string.Format("Player name: {0}, Health statuse: {1}", Name, Health); }
    }
در متد Clone از MemberwiseClone استفاده کرده‌ایم. خود Clone هم در داخل واسط ICloneable تعریف شده‌است و هدف از آن کپی نمودن آبجکت‌ها است. سپس کد فوق را بصورت زیر مورد استفاده قرار می‌دهیم:
Prototype p1 = new Prototype { Name = "Vahid", Health = "OK" };
Prototype p2 = p1.Clone() as Prototype;
Console.WriteLine(p1.ToString());
Console.WriteLine(p2.ToString());
با اجرای کد فوق مشاهده میشود p1 و p2 دقیقا عین هم کار می‌کنند. کل این فرآیند بیانگر الگوی Prototype می‌باشد. ولی تا اینجای کار درست است که الگو پیاده سازی شده است، ولی همچنین به نظر نقصی نیز در کد دیده می‌شود:
برای واضح نمودن نقص، یک کلاس بنام AdditionalDetails تعریف می‌کنیم. در واقع کد را بصورت زیر تغییر میدهیم:
 public abstract class APrototype : ICloneable
    {
        public string Name { get; set; }
        public string Health { get; set; }
        public AdditionalDetails Detail { get; set; }
        public abstract object Clone();
    }
    public class AdditionalDetails { public string Height { get; set; } }

    public class Prototype : APrototype
    {
        public override object Clone() { return this.MemberwiseClone() as APrototype; }
        public override string ToString() { return string.Format("Player name: {0}, Health statuse: {1}, Height: {2}", Name, Health, Detail.Height); }
    }
و از آن بصورت زیر استفاده می‌کنیم:
Prototype p1 = new Prototype { Name = "Vahid", Health = "OK", Detail = new AdditionalDetails { Height = "100" } };
Prototype p2 = p1.Clone() as Prototype;
p2.Detail.Height = "200";
Console.WriteLine(p1.ToString());
Console.WriteLine(p2.ToString());
خروجی که نمایش داده می‌شود در بخش Height هم برای p1 و هم برای p2 عدد 200 را نمایش می‌دهد که می‌تواند اشتباه باشد. چراکه p1 دارای Height برابر با 100 است و p2  دارای Height برابر با 200. به این اتفاق ShallowCopy گفته میشود که ناشی از استفاده از MemberwiseClone است که در مورد ارجاعات با آدرس رخ می‌دهد.  در این حالت بجای کپی نمودن مقدار، از کپی نمودن آدرس استفاده میشود (Ref Type چیست؟)
برای حل این مشکل باید DeepCopy انجام داد. لذا کد را بصورت زیر تغییر می‌دهیم:(DeepCopy و ShallowCopy چیست؟)
 public abstract class APrototype : ICloneable
    {
        public string Name { get; set; }
        public string Health { get; set; }
        //This is a ref type
        public AdditionalDetails Detail { get; set; }
        public abstract APrototype ShallowClone();
        public abstract object Clone();
    }

    public class AdditionalDetails { public string Height { get; set; } }

    public class Prototype : APrototype
    {
        public override object Clone()
        {
            Prototype cloned = MemberwiseClone() as Prototype;
            //We need to deep copy each ref types in order to prevent shallow copy
            cloned.Detail = new AdditionalDetails { Height = this.Detail.Height };
            return cloned;
        }
        //Shallow copy will copy ref type's address instead of their value, so any changes in cloned object or source object will take effect on both objects
        public override APrototype ShallowClone() { return this.MemberwiseClone() as APrototype; }
        public override string ToString() { return string.Format("Player name: {0}, Health statuse: {1}, Height: {2}", Name, Health, Detail.Height); }
    }
و سپس بصورت زیر از آن استفاده نمود:
 Prototype p1 = new Prototype { Name = "Vahid", Health = "OK", Detail = new AdditionalDetails { Height = "100" } };
            Prototype p2 = p1.Clone() as Prototype;
            p2.Detail.Height = "200";
            Console.WriteLine("<This is Deep Copy>");
            Console.WriteLine(p1.ToString());
            Console.WriteLine(p2.ToString());

            Prototype p3 = new Prototype { Name = "Vahid", Health = "OK", Detail = new AdditionalDetails { Height = "100" } };
            Prototype p4 = p3.ShallowClone() as Prototype;
            p4.Detail.Height = "200";
            Console.WriteLine("\n<This is Shallow Copy>");
            Console.WriteLine(p3.ToString());
            Console.WriteLine(p4.ToString());
لذا خروجی بصورت زیر را می‌توان مشاهده نمود:

البته در این سناریو ShallowCopy باعث اشتباه شدن نتایج می‌شود. شاید شما در دامنه‌ی نیازمندیهای خود، اتفاقا به ShallowCopy نیاز داشته باشید و DeepCopy مرتفع کننده‌ی نیاز شما نباشد. لذا کاربرد هر کدام از آنها وابستگی مستقیمی به دامنه‌ی نیازمندی‌های شما دارد.
نظرات مطالب
VS Code برای توسعه دهندگان ASP.NET Core - قسمت دوم - ایجاد و اجرای اولین برنامه
افرایش سرعت در نوشتن صفت (Attribute)‌های ویوو مدل ها
در اکثر پروژه‌ها، متن خطای اکثر ویوو مدل‌ها شبیه هم است و تفاوت خاصی ندارند؛ مثلا اتریبیوت Required، متن خطایش معمولا با این مضمون است: "لطفا فیلد ... را وارد نمایید." ما میتوانیم تمام این متن‌های خطاها را در جایی دیگر تعریف و در متن خطای اتریبیوت‌هایمان از آن استفاده کنیم که باعث میشود بعدا اگر خواستیم متن خطا‌ها را تغییر دهیم (مثلا در مورد اتریبیوت Required متن "لطفا فیلد ... را وارد نمایید" را با * عوض کنیم) در تمام پروژه این تغییر اعمال میشود و دیگر نیازی نیست تمامی متن خطاها را یکی یکی تغییر دهیم و نگهداری کد‌ها برای بعد راحت‌تر میشود.
برای شروع یک کلاس را برای متن خطاهای اتریبیوت‌هایمان تعریف میکنیم:
public static class AttributesErrorMessages
{
    public const string RequiredMessage = "لطفا {0} را وارد نمایید";
    public const string MinLengthMessage = "{0} نباید کمتر از {1} کاراکتر باشد";
    public const string MaxLengthMessage = "{0} نباید بیشتر از {1} کاراکتر باشد";
    public const string RegularExpressionMessage = "{0} را به درستی وارد نمایید";
    public const string StringLengthMessage = "{0} باید بین {2} کاراکتر و {1} کاراکتر باشد";
    public const string RemoteMessage = "با این {0} قبلا ثبت نام شده است";
}
حال برای تعریف هر ویوو مدل تنها کافی است آن را تعریف و در بالای آن، از اتریبیوت دلخواه استفاده و متن ارور آن را مطابق کلاس فوق وارد میکنیم، مثلا:
[StringLength(110, MinimumLength = 5, ErrorMessage = AttributesErrorMessages.StringLengthMessage)]
public string TestProp { get; set; }
حال میتوان کار را ساده‌تر نیز کرد و تمام اتریبیوت‌ها را به یک قطعه‌کد (Snippet) تبدیل کرد. برای این کار از طریق File>Preferences وارد منوی User Snippet میشویم و بعد زبان سی شارپ را انتخاب و بعد Snippet‌های خود را اضافه میکنیم:
{
   "Required":{
      "prefix":"required",
      "body":[
         "[Required(ErrorMessage = AttributesErrorMessages.RequiredMessage)]"
      ],
      "description":"Required attribute"
   },
   "Max Length":{
      "prefix":"maxlength",
      "body":[
         "[MaxLength(${1:number}, ErrorMessage = AttributesErrorMessages.MaxLengthMessage)]"
      ],
      "description":"Max length attribute"
   },
   "Min Length":{
      "prefix":"minlength",
      "body":[
         "[MinLength(${1:number}, ErrorMessage = AttributesErrorMessages.MinLengthMessage)]"
      ],
      "description":"Min length attribute"
   },
   "String Length":{
      "prefix":"stringlength",
      "body":[
         "[StringLength(${1:maximumNumber}, MinimumLength = ${2:minmumNumber}, ErrorMessage = AttributesErrorMessages.StringLengthMessage)]"
      ],
      "description":"String length attribute"
   },
   "Email Address":{
      "prefix":"emailaddress",
      "body":[
         "[EmailAddress(ErrorMessage = AttributesErrorMessages.RegularExpressionMessage)]"
      ],
      "description":"Email address attribute"
   },
   "Regular Expression":{
      "prefix":"regularexpression",
      "body":[
         "[RegularExpression(\"${1:patternString}\", ErrorMessage = AttributesErrorMessages.RegularExpressionMessage)]"
      ],
      "description":"Regular expression attribute"
   },
   "Remote Expression":{
      "prefix":"remote",
      "body":[
         "[Remote(\"${1:action}\", \"${2:controller}\", ErrorMessage = AttributesErrorMessages.RemoteMessage)]"
      ],
      "description":"Remote attribute"
   }
}
حال تنها کافی است اسم اتریبیوت مورد نظر را تایپ کنیم و آن Snippet را از پنجره Intellisense انتخاب کنیم.
نظرات مطالب
EF Code First #7
سلام
توی کلاس Context ما کدام موجودیت‌ها در Dbset میگزاریم؟ در این جا چرا شما فقط AlimentaryHabits وCustomer  رو توی Context گذاشتید؟
namespace EF_Sample35.DataLayer
{
 public class Sample35Context : DbContext
 {
 public DbSet<AlimentaryHabits> AlimentaryHabits { set; get; }
 public DbSet<Customer> Customers { set; get; }
 ...
 ...
 }
}