نظرات مطالب
C# 8.0 - Ranges & Indices
بهبودهای indexes و ranges در دات نت 6

تا پیش از دات نت 6، امکان استفاده‌ی از مفاهیم این مطلب به همراه LINQ میسر نبود؛ اما در دات نت 6 می‌توان آن‌ها را به IEnumerable‌ها نیز اعمال کرد. برای مثال بجای
list.Skip(10).Take(5)
می‌توان نوشت:
list.Take(10..15)
و یا دریافت آخرین 10 عضو:
list.Take(^10..)
و یا دریافت تنها یک عضو از انتها:
list.ElementAt(^1)
مطالب
بررسی نکات دریافت فایل‌های حجیم توسط HttpClient
HttpClient به عنوان جایگزینی برای HttpWebRequest API قدیمی، به همراه NET 4.5. ارائه شد و هدف آن یکپارچه کردن پیاده سازی‌های متفاوت موجود به همراه ارائه را‌ه‌حلی چندسکویی است که از WPF/UWP ، ASP.NET تا NET Core. و iOS/Android را نیز پشتیبانی می‌کند. تمام قابلیت‌های جدید پروتکل HTTP مانند HTTP/2 نیز از این پس تنها به همراه این API ارائه می‌شوند.
در مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» با روش استفاده‌ی تک وهله‌ای آن آشنا شدیم. در این مطلب نکات ویژه‌ی دریافت فایل‌های حجیم آن‌را بررسی خواهیم کرد. بدون توجه به این نکات، یا OutOfMemoryException را دریافت خواهید کرد و یا پیش از پایان کار، با خطای Timeout این پروسه به پایان خواهد رسید.


مشکل اول: نیاز به تغییر Timeout پیش فرض

فرض کنید می‌خواهیم فایل حجیمی را با تنظیمات پیش‌فرض HttpClient دریافت کنیم:
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace HttpClientTips.LargeFiles
{
    class Program
    {
        private static readonly HttpClient _client = new HttpClient();

        static async Task Main(string[] args)
        {
            var bytes = await DownloadLargeFileAsync();
        }

        public static async Task<byte[]> DownloadLargeFileAsync()
        {
            Console.WriteLine("Downloading a 4K content - too much bytes.");
            var response = await _client.GetAsync("http://downloads.4ksamples.com/downloads/sample-Elysium.2013.2160p.mkv");
            var bytes = await response.Content.ReadAsByteArrayAsync();
            return bytes;
        }
    }
}
در این حالت باتوجه به اینکه Timeout پیش‌فرض HttpClient به 100 ثانیه تنظیم شده‌است، اگر سرعت دریافت بالایی را نداشته باشید و نتوانید این فایل را پیش از 2 دقیقه دریافت کنید، برنامه با استثنای TaskCancelledException متوقف خواهد شد.
بنابراین اولین تغییر مورد نیاز، تنظیم صریح Timeout آن است:
private static readonly HttpClient _client = new HttpClient
{
    Timeout = Timeout.InfiniteTimeSpan
};


مشکل دوم: دریافت استثنای OutOfMemoryExceptions

روش دریافت پیش‌فرض اطلاعات توسط HttpClient، نگهداری و بافر تمام آن‌ها در حافظه‌ی سیستم است. این روش برای اطلاعات کم حجم، مشکلی را به همراه نخواهد داشت. بنابراین در حین دریافت فایل‌های چندگیگابایتی با آن، حتما با استثنای OutOfMemoryException مواجه خواهیم شد.
namespace HttpClientTips.LargeFiles
{
    class Program
    {
        private static readonly HttpClient _client = new HttpClient
        {
            Timeout = Timeout.InfiniteTimeSpan
        };

        static async Task Main(string[] args)
        {
            await DownloadLargeFileAsync();
        }

        public static async Task DownloadLargeFileAsync()
        {
            Console.WriteLine("Downloading a 4K content. too much bytes.");
            var response = await _client.GetAsync("http://downloads.4ksamples.com/downloads/sample-Elysium.2013.2160p.mkv");
            using (var streamToReadFrom = await response.Content.ReadAsStreamAsync())
            {
                string fileToWriteTo = Path.GetTempFileName();
                Console.WriteLine($"Save path: {fileToWriteTo}");
                using (var streamToWriteTo = File.Open(fileToWriteTo, FileMode.Create))
                {
                    await streamToReadFrom.CopyToAsync(streamToWriteTo);
                }
            }
        }
    }
}
در این حالت برای رفع مشکل، از متد ReadAsStreamAsync آن استفاده می‌کنیم. به این ترتیب بجای یک آرایه‌ی بزرگ از بایت‌ها، با استریمی از آن‌ها سر و کار داشته و به این صورت مشکل مواجه شدن با کمبود حافظه برطرف می‌شود.
مشکل: در این حالت اگر برنامه را اجرا کنید، تا پایان کار متد DownloadLargeFileAsync، حجم فایل دریافتی تغییری نخواهد کرد. یعنی هنوز هم کل فایل در حافظه بافر می‌شود و سپس استریم آن در اختیار FileStream نهایی برای نوشتن قرار خواهد گرفت.
علت این‌جا است که متد client.GetAsync تا زمانیکه کل Response ارسالی از طرف سرور خوانده نشود (headers + content)، عملیات را سد کرده و منتظر می‌ماند. بنابراین با این تغییرات عملا به نتیجه‌ی دلخواه نرسیده‌ایم.


دریافت اطلاعات Header و سپس استریم کردن Content

چون متد client.GetAsync تا دریافت کامل headers + content متوقف می‌ماند، می‌توان به آن اعلام کرد تنها هدر را به صورت کامل دریافت کن و سپس باقیمانده‌ی عملیات دریافت بدنه‌ی Response را به صورت Stream در اختیار ادامه‌ی برنامه قرار بده. برای اینکار نیاز است پارامتر HttpCompletionOption را تکمیل کرد:
var response = await _client.GetAsync(
                "http://downloads.4ksamples.com/downloads/sample-Elysium.2013.2160p.mkv",
                HttpCompletionOption.ResponseHeadersRead);
پارامتر HttpCompletionOption.ResponseHeadersRead به متد GetAsync اعلام می‌کند که پس از خواندن هدر Response، ادامه‌ی عملیات را در اختیار سطرهای بعدی کد قرار بده و عملیات را تا پایان خواندن کامل Response در حافظه، متوقف نکن.


مشکل سوم: برنامه در دریافت سومین فایل از یک سرور هنگ می‌کند.

تعداد اتصالات همزمانی را که می‌توان توسط HttpClient به یک سرور گشود، محدود هستند. برای مثال این عدد در Full .NET Framework مساوی 2 است. بنابراین اگر اتصال سوم موازی را شروع کنیم، چون Timeout را به بی‌نهایت تنظیم کرده‌ایم، این قسمت از برنامه هیچگاه تکمیل نخواهد شد.
روش تنظیم تعداد اتصالات مجاز به یک سرور:
- در Full .NET Framework با تنظیم خاصیت ServicePointManager.DefaultConnectionLimit است که به 2 تنظیم شده‌است.
- این مورد در NET Core. توسط پارامتر HttpClientHandler و خاصیت MaxConnectionsPerServer آن تنظیم می‌شود:
private static readonly HttpClientHandler _handler = new HttpClientHandler
{
    MaxConnectionsPerServer = int.MaxValue, // default for .NET Core
    UseDefaultCredentials = true
};
private static readonly HttpClient _client = new HttpClient(_handler)
{
    Timeout = Timeout.InfiniteTimeSpan
};
البته مقدار پیش‌فرض آن int.MaxValue است که نسبت به حالت Full .NET Framework عدد بسیار بزرگتری است.
نظرات مطالب
استفاده از shim و stub برای mock کردن در آزمون واحد
من نویسنده خوبی نیستم و شاید بهتر باشه که در اینباره نظر ندهم. به هر روی چند نکته به نظرم آمد باشد که مورد توجه شما واقع شود:
  • مقدمه را هنوز کامل نکردی. مقدمه خواننده را در جای پرتی از ماجرا رها میکند. اگر چهار خط آخر مقدمه را دوباره بخوانید متوجه میشوید که اگر تمام کاری که برای داشتن آزمون واحد باید انجام شود همین سه مورد باشد دیگر هرگز کسی به Fakes نیاز پیدا نمیکند، پس باید در ادامه میگفتید که این حالت مطلوب است ولی همیشه عملی نیست.
  • شروع و پایان مثالها مشخص نبود. مثالها بدون عنوان بودند. در شروع مثال باید مقدمه ای از مثال را مطرح میکردی و بعد مراحل مثال را توضیح میدادی.
  • در مثال اول باید بر بیشتر بر روی DataAccessLayer تاکید میکردی و صریح مشخص میکردی که عدم توانایی برنامه نویس در تغییر این کلاس و یا معماری سیستم گزینه IoC را کنار میگذارد و به این ترتیب مثال شما سودمندی Shim را بهتر نشان میداد.
  • در مثال دوم، کد CardToStub را ارائه نکردی، اگر،طبق آنچه انتظار میرود، وابستگی که در CardToStub وجود دارد به اینترفیس ICartSaver است در این صورت اساساً مثال شما هیچ دلیل و انگیزشی برای Stub فراهم نمیکند. باید باز هم ذهنیت خواننده را شکل میدادی و او را متوجه این موضوع میکردی که در پیاده سازی دیگری که برنامه نویس قدرت اعمال تغییر در آن ندارد وابستگی سخت وجود دارد و به این دلیل Stub میتواند مفید واقع شود.
البته این رو به حساب اینکه من یک خواننده بسیار مبتدی هستم گفتم شاید مقاله برای دیگران بیشتر از من قابل فهم است. ولی در کل مقاله خوبی بود و برای من کابردی بود.
مطالب
راهکار لاگ متناسب با Cloud و On-Premise
Application Insights راهکار ارائه شده توسط Microsoft است که در سه بخش به ما کمک می‌کند تا سیستم لاگ مؤثر و کارآمدی داشته باشیم:

۱- متدهای پایه Log که به صورت دستی فراخوانی می‌شوند، مانند TrackEvent برای ثبت یک رویداد بیزینسی که این متدها فراتر از متدهای معمول loggerهای متداول هستند.

۲- به صورت خودکار، Application Insights خطاهای سیستم را لاگ نموده و همچنین در زمان کار کردن با Http Client، دیتابیس و سایر Dependencyها، میزان طول کشیدن آنها را به همراه آدرس Request یا متن Sql Command و سایر اطلاعات مفید را نیز ذخیره می‌کند که این خود منجر به جمع شدن دیتایی بسیار ارزشمند در سیستم می‌شود.
البته اگر یک Dependency به صورت خودکار شناسایی نشود، مانند Redis، شما می‌توانید خودتان با متد TrackDependency اطلاعات آن‌را به AppInsights بدهید.

۳- داشبورد App Insights در Azure این امکان را می‌دهد که به سریع‌ترین شکل ممکن در لاگ‌ها جستجو نمود و برای مثال تمامی کارهای انجام شده توسط یک کاربر خاص را به صورت یک‌جا مشاهده و بررسی کرد.
فرضا اگر کاربر درخواست گرفتن خروجی Excel از لیست محصولات را داشته و این ۱ ثانیه طول کشیده، چقدر آن در انتظار دیتابیس بوده و ...
به علاوه از Power BI نیز می‌توانید برای بیرون کشیدن نکات مهم استفاده کنید.

البته شاید App Insights برای کسانی که Azure Account نداشته باشند، مناسب به نظر نرسد، ولی اگر راهکاری برای ذخیره سازی On-Premise اطلاعات لاگ شده وجود داشته باشد چه؟ مثلا اطلاعات آن را در Elastic موجود در سرورهای شرکت، داخل ایران ذخیره نمود، بدون الزام به این‌که حتی آن سرور دسترسی به اینترنت داشته باشد.

بله، این امکان وجود دارد و با کمک  Microsoft Diagnostics EventFlow می‌توان اطلاعات App Insights را در هر جایی از جمله Elastic ذخیره نمود و بدین طریق از عمده مزایای App Insights بدون داشتن Azure Account بهره مند شد.

برای این منظور به شکل زیر عمل کنید: (آموزش برای ASP.NET Core 3.1 بوده، ولی برای سایر پروژه‌ها نیز قابل استفاده است)

۱- ابتدا Application Insights را به پروژه خود اضافه کنید.بدین منظور لازم است Packageهای Microsoft.Extensions.Logging.ApplicationInsights و Microsoft.ApplicationInsights.AspNetCore را نصب کنید. 

۲- در Program.cs بعد از
Host.CreateDefaultBuilder(args)
کد زیر را قرار دهید
.ConfigureLogging(loggingBuilder  =>
{
    loggingBuilder.ClearProviders();
    loggingBuilder.AddApplicationInsights(); 
})
این باعث می‌شود تا Loggerهای پیش فرض Console و Debug حذف شوند و البته اگر کتابخانه 3rd party ای از Microsoft.Extensions.Logging استفاده کرده باشد، اطلاعات لاگ آن به AppInsights نیز داده شود و در نهایت شما با کمک Microsoft Diagnostics EventFlow، آن اطلاعات را در Elastic و Console و سایر Outputها خواهید داشت.

۳- اگر جایی قصد لاگ کردن یک Event را دارید، یا در مثال استفاده از Redis میخواهید اطلاعات زمان طول کشیدن رفت و برگشت به Redis را لاگ کنید یا یک try/catch دارید که در catch آن خطا را مجدد throw نمی‌کنید، ولی قصد لاگ کردن exception را دارید، ابتدا TelemetryClient را inject نموده و از متدهای آن مانند TrackException استفاده کنید.
توجه داشته باشید که اگر از ILogger ارائه شده توسط MS.Ext.Logging استفاده کنید نیز کار خواهد کرد.

۴- پکیج Microsoft.Diagnostics.EventFlow.Inputs.ApplicationInsights را در پروژه نصب کنید و سپس از بین Output‌های معرفی شده نیز یکی را انتخاب و پکیج آن را نیز نصب کنید. شما می‌توانید دیتایی را که AppInsights به صورت خودکار جمع نموده را + دیتای ارائه شده توسط خودتان را به Elastic، Splunk و ... بفرستید.
ما در این مثال برای سادگی Std Out - Console Output را انتخاب می‌کنیم و پکیج Microsoft.Diagnostics.EventFlow.Outputs.StdOutput را نصب می‌کنیم.

۵- فایل eventFlowConfig.json را به پروژه اضافه کنید و موارد زیر را در آن قرار دهید:
 {
  "inputs": [
    {
      "type": "ApplicationInsights"
    }
  ],
  "outputs": [
    {
      "type": "StdOutput" // console output
    }
  ],
  "schemaVersion": "2016-08-11"
}  
در این فایل، inputs شامل ApplicationInsights بوده و البته موارد جالبی چون PerformanceCounters را نیز میتوانید در آن بگنجانید و outputs بسته به نیاز شما برابر با Elastic و ... است که در این صورت باید اطلاعات اتصال به Elastic شامل آدرس سرور و ... را نیز ارائه کنید. توجه داشته باشید که ما در حال استفاده از ApplicationInsights SDK هستیم، به عنوان کتابخانه Logging و در نهایت دیتا نه به ApplicationInsights در سرورهای Microsoft Azure، که به output یا outputهایی که در فایل eventFlowConfig.json می‌گوییم ارسال می‌شود.

۶- در Program.cs متد Main را به شکل زیر در بیاورید:
using (var pipeline = DiagnosticPipelineFactory.CreatePipeline("eventFlowConfig.json"))
{
    CreateHostBuilder(args, pipeline)
        .Build()
        .Run();
}  
و CreateHostBuilder نیز به شکل زیر باشد:
public static IHostBuilder CreateHostBuilder(string[] args, DiagnosticPipeline pipeline) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices(services => services.AddSingleton<ITelemetryProcessorFactory>(sp => new EventFlowTelemetryProcessorFactory(pipeline)))
        .ConfigureLogging(logginBuilder =>
        {
            logginBuilder.ClearProviders();
            loggingBuilder.AddApplicationInsights(); 
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });  

همه چیز آماده است. هم اکنون اگر Azure Account داشته باشید، می‌توانید با دادن instrumentationKey در appsetting.json از داشبورد فوق العاده ApplicationInsights استفاده کنید و اگر نه هم در سرورهای داخلی خودتان Splunk و ... را راه اندازی و در فایل eventFlowConfig.json، در قسمت outputs، اطلاعات آدرس آنها را بدهید و لاگ‌های مفصل و کاربردی ای که به صورت خودکار جمع آوری شده را به همراه اطلاعاتی که خودتان دستی ارائه کرده اید، یکجا تحویل بگیرید.

لینک پروژه در GitHub که حاوی مثال Elastic است.
اشتراک‌ها
.NET در سال 2015

شرح آنچه در سال 2015 پیرامون دات نت گذشت

.NET در سال 2015
اشتراک‌ها
پروژه Data.HashFunction

پیاده سازی هش‌های سریع مثل «مور مور هش» در دات نت

پروژه Data.HashFunction