عنوان:

‫روشی برای محاسبه‌ی تعداد کاربران آنلاین در ASP.NET Core


نویسنده: داود میرزایی
تاریخ: ۱۴۰۰/۰۶/۲۹ ۱۷:۰
آدرس: www.dntips.ir
در این مقاله قصد دارم روشی را برای محاسبه‌ی تعداد کاربران لاگین شده‌ی فعال در یک پروژه‌ی  Asp.net Core، توضیح دهم. در این روش، کاربرانی را آنلاین در نظر گرفته‌ایم که در 10 دقیقه‌ی گذشته، فعالیتی روی سامانه داشته‌اند. البته این زمان را می‌توانید تغییر دهید. برای اینکار ابتدا یک Middleware را به صورت زیر طراحی می‌کنیم :
public class OnlineUserMiddleWare
    {
        private readonly RequestDelegate _next;
        private readonly IMemoryCache _memoryCache;

        public OnlineUserMiddleWare(RequestDelegate next, IMemoryCache memoryCache)
        {
            _next = next;
            _memoryCache = memoryCache;
        }

        public async Task Invoke(HttpContext context)
        {
            if (!_memoryCache.TryGetValue("OnlineUsers", out Dictionary<string,DateTime> onlineUsers))
            {
                onlineUsers = new Dictionary<string, DateTime>();
                _memoryCache.Set("OnlineUsers", onlineUsers);
            }
            if (context.User.Identity.IsAuthenticated)
            {
                var name = context.User.Identity.Name;
                if (name != null)
                {
                    if (onlineUsers.ContainsKey(name))
                        onlineUsers[name] = DateTime.Now;
                    else
                        onlineUsers.Add(name, DateTime.Now);
                }

            }

           
                foreach (var online in onlineUsers)
                {
                    if (online.Value < DateTime.Now.AddMinutes(-10))
                        onlineUsers.Remove(online.Key);
                }
           
            await _next(context);
        }

        
    }
در اینجا به کمک IMemoryCache، یک کلید را به نام OnlineUsers در کش ایجاد کرده‌ایم. ابتدا بررسی می‌کنیم، اگر این کلید وجود نداشت، آن را ایجاد کند. نوع این کلید، یک  Dictionary است که نام کاربری و ساعت آخرین در خواست کاربران، در آن ذخیره می‌شود.
پس از اینکه مقدار OnlineUsers را از کش دریافت کردیم، چک می‌کنیم اگر کاربر لاگین شده‌است، نام کاربری را به همراه زمان جاری، در دیکشنری ذخیره کند و اگر کاربر درون دیکشنری موجود بود، فقط زمان آخرین درخواست را تغییر دهد (به‌روز کند). بنابراین تا به اینجا هر کسی در سامانه لاگین کند، نام کاربری او، به همراه زمان آخرین درخواست، در دیکشنری ذخیره می‌شود.
نهایتا باید بررسی کنیم، اگر کاربری 10 دقیقه از آخرین درخواستش گذشته باشد، از دیکشنری حذف شود. 
لازم است این Middleware به Startup اضافه شود: 
 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
        {
         ...
            app.UseMiddleware<OnlineUserMiddleWare>();
         ...
        }

برای نمایش تعداد کاربران آنلاین، کافی است در View یا مکان مورد نظر، مقدار دیکشنری را از memorycache فراخوانی کنیم: (حتی می‌توان لیست کاربران را هم نمایش داد؛ چون نام کاربری آنها در دیکشنری ذخیره شده‌است)
public class DashboardController 
    {
        private readonly IMemoryCache _memoryCache;
        public DashboardController(memoryCache)
        {
            _memoryCache = memoryCache;
        }
        public IActionResult Index()
        {
         
            _memoryCache.TryGetValue("OnlineUsers", out Dictionary<string, DateTime> onlineUsers);
            ViewBag.OnlineUsers = onlineUsers.Count;
            return View();
        }       
    }
اما یک نکته وجود دارد و اینکه در MiddleWare به ازای هر درخواست، کل دیکشنری را بررسی کرده‌ایم و کاربرانی را که 10 دقیقه از آخرین  فعالیت آنها گذشته است، حذف کرده‌ایم. این بررسی به ازای هر درخواست، کار مناسبی نیست؛ چرا که ممکن است در ترافیک بالا، مشکل ساز شود. لذا می‌توان سناریوهای مختلفی را برای آن در نظر گرفت:
 - مثلا فقط جائیکه می‌خواهیم آمار نمایش داده شود، این بررسی صورت گیرد. البته اگر این آمار در صفحه‌ای پرتکرار نمایش داده شود، این سناریو می‌تواند مفید باشد. ولی مثلا اگر قرار است آمار تنها برای مدیر نمایش داده شود و مدیر چندین روز این صفحه را بررسی نکند، ممکن است حجم این دیکشنری هر روز حجیم‌تر شود.
- می‌توان در صفحاتی که اطمینان داریم روزانه چندین بار فراخوانی می‌شوند، این فرآیند را بررسی کنیم؛ مثل صفحه لاگین . لذا قسمت بررسی در MiddleWare را به صورت زیر تغییر دادم :
           if(context.Request.Path.StartsWithSegments("/Identity/Account/Login"))
            {
                foreach (var online in onlineUsers)
                {
                    if (online.Value < DateTime.Now.AddMinutes(-10))
                        onlineUsers.Remove(online.Key);
                }
            }

نظرات

  • وحید نصیری در ۱۴۰۰/۰۶/۲۹ ۱۷:۷
    - زمانیکه از memory cache در ASP.NET Core استفاده می‌کنید، باید دقت داشت که افزودن آیتم‌ها به آن، thread-safe نیست و نیاز به یکسری ملاحظات خاص را دارد.
    - می‌توان با تنظیم MemoryCacheEntryOptions و AbsoluteExpiration آن، قسمت حذف دستی آیتم‌های کش را پیاده سازی نکرد.
    • داود میرزایی در ۱۴۰۰/۰۶/۲۹ ۱۷:۲۹
      متد invoke به صورت زیر اصلاح شد:
      public async Task Invoke(HttpContext context)
              {
                  if (!_memoryCache.TryGetValue("OnlineUsers", out Dictionary<string,DateTime> onlineUsers))
                  {
                      onlineUsers = new Dictionary<string, DateTime>();
                      _memoryCache.Set("OnlineUsers", onlineUsers , new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) });
                  }
                  if (context.User.Identity.IsAuthenticated)
                  {
                      var name = context.User.Identity.Name;
                      if (name != null)
                      {
                          if (onlineUsers.ContainsKey(name))
                              onlineUsers[name] = DateTime.Now;
                          else
                              onlineUsers.Add(name, DateTime.Now);
                      }
      
                  }
      
      
                  await _next(context);
              }
      در مورد بحث thread-safe  باید MemoryCashe را بازنویسی کرد و متد GetOrAdd را فراخوانی کرد 
  • مهدی کوهستانی در ۱۴۰۰/۰۸/۲۱ ۱۱:۳
    ممنون از پستی که گذاشتید. 
    فقط یک موردی هست، این سبک پیاده سازی شما فقط برای کاربران لاگین شده قابل استفاده است. در حالی که کاربرانی که از سایت ما دیدن می‌کنند ممکن است اکثرا لاگین نکرده باشند. 
    از طرفی پروتکل HTTP، از نوع state-less هست و نمیشه فهمید که درخواست‌ها از یک شخص مشخص دریافت شده اند و جز آمار نیاریم. البته میشه با cookie این مشکل را هم تا حدودی رفع کرد. 
    در حالت کلی فکر کنم استفاده از SignalR روش بهتری باشه