اشتراکها
اشتراکها
ساخت برنامه های تحت وب با Blazor
Today we released our first public preview of Blazor , a new experimental .NET web framework using C#/Razor and HTML that runs in the browser with WebAssembly . Blazor enables full stack web development with the stability, consistency, and productivity of .NET.
یک نکتهی تکمیلی: تولید URLهای strongly typed در برنامههای Blazor WASM
یکی از مشکلات کار با برنامههای Blazor WASM، نیاز به کار با
آدرسهای رشتهای مانند زیر است که تحت کنترل کامپایلر نبوده و همچنین با
تغییر مسیریابیهای برنامه، بههم خواهند ریخت:
کتابخانهی « ApiUrlsGenerator »، این مشکل را برطرف کرده و به صورت خودکار بر اساس Action Methods موجود، نمونهی strongly typed آنها را تولید میکند:
var secretUrl = "api/WeatherForecast/_secretUrl";
var secretUrl = ApiUrls.WeatherForecast.HttpGet.SecretUrl;
یک نکته: کاهش زمان build پروژههای Blazor در حالت توسعه
در پروژههای Blazor، خاصیت ویژهی BlazorLinkOnBuild به true تنظیم شدهاست که سبب میشود تا موارد استفاده نشدهی در برنامه، از خروجی نهایی حذف شوند. این مورد هرچند حجم نهایی را کاهش میدهد، اما در زمان توسعه نیز فعال است و سبب بالا رفتن زمان build برنامه میشود؛ گاهی از اوقات تا 10 برابر! برای غیرفعال کردن آن در زمان debug و توسعه، میتوان فایل csproj را به صورت زیر ویرایش کرد. این تنظیم در حین publish در حالت release، غیرفعال میشود:
<PropertyGroup> <BlazorLinkOnBuild Condition="'$(Configuration)'!='Release'">false</BlazorLinkOnBuild> </PropertyGroup>
یک نکتهی تکمیلی: روش اعتبارسنجی مقایسهی مقدار دو ورودی
در برنامههای Blazor بهتر است از ویژگی جدید [CompareProperty] بجای [Compare] استفاده شود که جزئی از بستهی Microsoft.AspNetCore.Components.DataAnnotations.Validation است. (این مورد در Blazor 5x جزئی از همان ویژگی Compare اصلی شده و دیگر به آن نیازی نیست )
public class EditEmployeeModel { public string Email { get; set; } [CompareProperty("Email", ErrorMessage = "Email and Confirm Email must match")] public string ConfirmEmail { get; set; } }
وبلاگها ، سایتها و مقالات ایرانی (داخل و خارج از ایران)
- ریستارت کردن سایت ASP.NET
- انتشار نسخه نهایی راهنمای امنیتی WCF
- صنعت حل کپچای هند و راهحلهای مقابله با اسپم با تکیه بر تحلیل محتوا
- بالابردن ضریب موفقیت پیاده سازی ITIL در سازمان
- تکست باکس قابل ویرایش بوسیله ASP.NET AJAX
- لیستی از نرم افزارهای ORMapper
- استفاده مشترک از یک یا چند User Control در چندین IIS Application
Visual Studio
ASP. Net
طراحی و توسعه وب
- تعیین اعتبار زیباتر فیلدهای یک فرم
- آشنایی با یک سری تگ مجهور HTML !
- در انتخاب رنگهای مناسب و هماهنگ مشکل دارید؟
- معرفی 10 فریم ورک جاوا اسکریپتی
اسکیوال سرور
عمومی دات نت
ویندوز
متفرقه
در اکثر برنامههای سازمانی، مثل برنامههای مدیریت آرشیو اسناد، همواره این نیاز جزو خواستههای کاربران بوده که بتوانند به صورت مستقیم و از طریق تنها یک کلیک، تصویر مورد نظر را اسکن کرده و به صورت خودکار در برنامه وارد کنند؛ یعنی بدون اینکه نیاز باشد با استفاده از یک برنامه دیگر ابتدا تصویر را اسکن کرده و سپس در فرم وب، فایل اسکن شده را Browse کنند.
این نیاز اساسا به معنی دسترسی به سخت افزار کاربر از طریق مرورگر میباشد که به دلایل متعددی امکان پذیر نیست! مشکلات امنیتی ایجاد شده در این راه بسیار جدی است. اما خوشبختانه راههایی برای رسیدن به این هدف وجود دارند:
1- ActiveX : که به صورت native فقط در IE پشتیبانی میشود (در نسخههای جدیدتر IE نیاز به بروز رسانی پلاگین ActiveX controls میباشد) و برای استفادهی از آن در مرورگرهای Firefox و Chrome هم باید از پلاگینهای جانبی روی هر مرورگر استفاده کرد که مثلا برای اجرای بر روی Firefox، باید افزونهی IE Tab را نصب کرد که تنها کارش این است که سایت را از طریق موتور IE در پنجرهی فایرفاکس اجرا کند! که البته این مورد مثل این میماند که سایت در IE باز شده باشد که ممکن است زیاد خوشایند نباشد؛ در غیر اینصورت باید پروژه را از اول بر مبنای اجرای بر روی IE طراحی و پیاده سازی کرد. در ضمن از مشکلات اجرای پلاگین نوشته شده توسط برنامه نویس در IE و نسخههای مختلف آن هم چشم پوشی میکنیم. یکی از مزیتهای این روش نسبت به بقیه این هست که دانلود و نصب پلاگین مورد نیاز به صورت خودکار و توسط مرورگر انجام میشود.
2- استفاده از یک کامپوننت جانبی : مثل کامپوننتهای LEADTOOLS که ابزارهای متنوع و SDKهای بسیار قدرتمندی را برای اینکار و کارهای دیگر، مانند کار با اسکن تصاویر مغزی، پردازش تصویر، بارکد خوانها، مدیریت اسناد و ... فراهم کرده است. قیمت این ابزار بسیار زیاد است و در برخی موارد امکانات فراهم آورده بسیار بیشتر است از خواستهی ما. این ابزار، هم در HTML & Javascript و هم در DotNet قابل استفاده است و مستندات نسبتا خوبی هم در این زمینه ارائه کرده است. همچنین کامپوننت Dynamsoft که باز هم غیر رایگان هست و قیمت بالایی نیز دارد.
اگر روال کار کامپوننتهای بالا را مورد بررسی قرار دهید (از طریق اجرای Demo ها، اینجا و اینجا) متوجه خواهید شد که هر دو نیاز به نصب یک سرویس یا App سمت کلاینت جهت اجرای دستورات خود دارند. پس میشود اینطور نتیجه گرفت که انجام اینکار بدون اینکه چیزی سمت کاربر نصب شود، ممکن نیست و در هر دو، لینک نصب فایل exe سرویس برای دانلود قرار داده شده است. بر این اساس به راه حل سومی خواهیم رسید که خودمان این سرویس را جهت تعامل با اسکنر سمت کاربر طراحی و پیاده سازی نماییم.
اما چطور ممکن است که با اجرای یک فایل exe سمت کاربر (با این فرض که کاربر در یک دامین مطمئن قرار دارد و میشود درخواست نصب سرویس را نمود) این امکان را برای کاربر فراهم نمود که با یک کلیک در مرورگر، اسکنر به صورت خودکار اسکن را آغاز کرده و سپس تصویر حاصل را به یکی از کنترلرهای ما در سمت سرور ارسال نماید؟
برای اینکار ما با دو صورت مساله مواجه هستیم؛ اول اینکه چطور تصویر را سمت کاربر اسکن کنیم و دوم اینکه چطور این تصویر را به سرور ارسال نماییم!
برای مسالهی اول از کتابخانه Windows Image Acquisition (WIA) استفاده خواهیم نمود که این کتابخانه به ما این امکان را میدهد تا با سخت افزارهایی که از TWAIN پشتیبانی میکنند، بتوانیم ارتباط برقرار نماییم.
این نیاز اساسا به معنی دسترسی به سخت افزار کاربر از طریق مرورگر میباشد که به دلایل متعددی امکان پذیر نیست! مشکلات امنیتی ایجاد شده در این راه بسیار جدی است. اما خوشبختانه راههایی برای رسیدن به این هدف وجود دارند:
1- ActiveX : که به صورت native فقط در IE پشتیبانی میشود (در نسخههای جدیدتر IE نیاز به بروز رسانی پلاگین ActiveX controls میباشد) و برای استفادهی از آن در مرورگرهای Firefox و Chrome هم باید از پلاگینهای جانبی روی هر مرورگر استفاده کرد که مثلا برای اجرای بر روی Firefox، باید افزونهی IE Tab را نصب کرد که تنها کارش این است که سایت را از طریق موتور IE در پنجرهی فایرفاکس اجرا کند! که البته این مورد مثل این میماند که سایت در IE باز شده باشد که ممکن است زیاد خوشایند نباشد؛ در غیر اینصورت باید پروژه را از اول بر مبنای اجرای بر روی IE طراحی و پیاده سازی کرد. در ضمن از مشکلات اجرای پلاگین نوشته شده توسط برنامه نویس در IE و نسخههای مختلف آن هم چشم پوشی میکنیم. یکی از مزیتهای این روش نسبت به بقیه این هست که دانلود و نصب پلاگین مورد نیاز به صورت خودکار و توسط مرورگر انجام میشود.
2- استفاده از یک کامپوننت جانبی : مثل کامپوننتهای LEADTOOLS که ابزارهای متنوع و SDKهای بسیار قدرتمندی را برای اینکار و کارهای دیگر، مانند کار با اسکن تصاویر مغزی، پردازش تصویر، بارکد خوانها، مدیریت اسناد و ... فراهم کرده است. قیمت این ابزار بسیار زیاد است و در برخی موارد امکانات فراهم آورده بسیار بیشتر است از خواستهی ما. این ابزار، هم در HTML & Javascript و هم در DotNet قابل استفاده است و مستندات نسبتا خوبی هم در این زمینه ارائه کرده است. همچنین کامپوننت Dynamsoft که باز هم غیر رایگان هست و قیمت بالایی نیز دارد.
اگر روال کار کامپوننتهای بالا را مورد بررسی قرار دهید (از طریق اجرای Demo ها، اینجا و اینجا) متوجه خواهید شد که هر دو نیاز به نصب یک سرویس یا App سمت کلاینت جهت اجرای دستورات خود دارند. پس میشود اینطور نتیجه گرفت که انجام اینکار بدون اینکه چیزی سمت کاربر نصب شود، ممکن نیست و در هر دو، لینک نصب فایل exe سرویس برای دانلود قرار داده شده است. بر این اساس به راه حل سومی خواهیم رسید که خودمان این سرویس را جهت تعامل با اسکنر سمت کاربر طراحی و پیاده سازی نماییم.
اما چطور ممکن است که با اجرای یک فایل exe سمت کاربر (با این فرض که کاربر در یک دامین مطمئن قرار دارد و میشود درخواست نصب سرویس را نمود) این امکان را برای کاربر فراهم نمود که با یک کلیک در مرورگر، اسکنر به صورت خودکار اسکن را آغاز کرده و سپس تصویر حاصل را به یکی از کنترلرهای ما در سمت سرور ارسال نماید؟
برای اینکار ما با دو صورت مساله مواجه هستیم؛ اول اینکه چطور تصویر را سمت کاربر اسکن کنیم و دوم اینکه چطور این تصویر را به سرور ارسال نماییم!
برای مسالهی اول از کتابخانه Windows Image Acquisition (WIA) استفاده خواهیم نمود که این کتابخانه به ما این امکان را میدهد تا با سخت افزارهایی که از TWAIN پشتیبانی میکنند، بتوانیم ارتباط برقرار نماییم.
برای مسالهی دوم هم نیاز به پیاده سازی یک WCF Service و اجرای آن (هاست کردن) در سمت کلاینت داریم. در واقع با صدا زدن متدهای این سرویس، از کتابخانهی بالا استفاده کرده و اسکن را انجام میدهیم.
ادامه دارد...
اشتراکها
آیندهی Microsoft
- Microsoft will change how it reports financially.
- Microsoft will become a “consumer-focused enterprise company.”
- Windows will converge into a single code base capable of running on any platform.
- Application development will converge into allowing developers to maintain a single code base for an app that will run on any device capable of running Windows.
- Release frequency will dramatically increase.
- The “modern” user interface is here to stay.
هر زمانیکه در سمت کلاینت، استثناء یا خطایی رخ میدهد، کاربر با نوار زرد رنگی در پایین صفحه، از آن مطلع میشود؛ اما برنامه نویس چطور؟! به همین جهت در این مطلب قصد داریم تمام خطاهای رخ دادهی در برنامهی سمت کلاینت را لاگ کرده و به سرور تلگرام ارسال کنیم. مزیت کار کردن با تلگرام، دسترسی به سروری است که تقریبا همواره در دسترس است و برخلاف بانک اطلاعاتی برنامه که ممکن است در لحظهی بروز خطا، خودش سبب ساز اصلی باشد و قادر به ثبت اطلاعات خطاهای رسیدهی از سمت کلاینت نباشد، چنین مشکلی را با تلگرام نداریم (مانند همان جملهی معروف: «بکآپ سروری که روی همان سرور گرفته میشود، بک آپ نام ندارد!»). همچنین بررسی و حذف گزارشهای رسیدهی به آن نیز بسیار سادهاست و میتوان این گزارشها را مستقل از سرور برنامه و از طریق وسایل مختلفی مانند گوشیهای همراه، تبلتها و غیره نیز بررسی کرد.
نحوهی نمایش خطاها در برنامههای Blazor
در حین توسعهی برنامههای Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر میشود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهدهی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ دادهاست اکتفا کرده و گزینهی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد میدهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت میگیرد:
که شیوه نامههای پیشفرض آن در فایل 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 سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
توضیحات:
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگیهای برنامه معرفی کنیم، روش آن به صورت زیر است:
باید کلاسی را داشته باشیم مانند ClientLoggerProvider که یک ILoggerProvider را پیاده سازی میکند و نحوهی ثبت آن نیز باید حتما Singleton باشد. مزیت معرفی ILoggerProvider به این نحو، امکان دسترسی به سرویسهای برنامه در سازندهی کلاس ClientLoggerProvider است و در این حالت دیگر نیاز به نوشتن new ClientLoggerProvider نبوده و خود سیستم تزریق وابستگیها، سازندههای ClientLoggerProvider را تامین میکند.
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده میکنید:
با استفاده از IServiceProvider میتوان به HttpClient برنامه دسترسی یافت. از این جهت که چون HttpClient به صورت پیشفرض با طول عمر Scoped به سیستم معرفی شده، امکان تزریق مستقیم آن به سازندهی یک ILoggerProvider از نوع Singleton وجود ندارد. به همین جهت از IServiceProvider برای تامین آن استفاده خواهیم کرد. مابقی موارد مانند IOptions که تنظیمات این لاگر را فراهم میکند و یا NavigationManager استاندارد برنامه که امکان دسترسی به Url جاری را میسر میکند، به صورت پیشفرض دارای طول عمر Singleton هستند و میتوان آنها را بدون مشکل، به سازندهی لاگر سفارشی، تزریق کرد.
مهمترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت میدهد:
بنابراین در ادامه نیاز است، یک ILogger سفارشی را نیز پیاده سازی کنیم:
نحوهی عملکرد این ILogger سفارشی بسیار سادهاست:
- متد IsEnabled آن مشخص میکند که چه سطحی از رخدادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت میکند:
در این تنظیمات مشخص میکنیم که Url مربوط به اکشن متد Web API ما که قرار است اطلاعات به سمت آن ارسال شوند، چیست؟ همچنین حداقل سطح لاگ مدنظر را نیز باید مشخص کنیم. اطلاعات آن توسط فایل Client\wwwroot\appsettings.json با این محتوای فرضی قابل تنظیم است:
و همچنین باید کلاس WebApiLoggerOptions را به نحو زیر در کلاس Program برنامه به سیستم تزریق وابستگیها، معرفی کرد تا <IOptions<WebApiLoggerOptions قابلیت تزریق به سازندهی تامین کنندهی لاگر را پیدا کند:
- متد لاگ این لاگر سفارشی، پیام نهایی قابل ارسال به سمت Web API را تشکیل داده و توسط متد httpClient.PostAsJsonAsync آنرا ارسال میکند. به همین جهت ساختار لاگ مدنظر را در فایل Shared\ClientLog.cs به صورت زیر تعریف کردهایم که بین برنامهی کلاینت و سرور، مشترک است:
این اطلاعاتی است که کلاینت به ازای رخدادی خاص، جمع آوری کرده و به سمت سرور ارسال میکند.
در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام میدهد، به صورت زیر خواهد بود:
تا اینجا اگر هر نوع استثنای مدیریت نشدهای در برنامهی Blazor WASM رخ دهد، چون سطح لاگ آن بالاتر از Warning تنظیم شدهی در فایل Client\wwwroot\appsettings.json است:
به صورت خودکار به سمت کنترلر api/logs ارسال خواهد شد. بنابراین مرحلهی بعدی، تکمیل کنترلر یاد شدهاست.
ایجاد سرویسی برای ارسال لاگهای برنامه به سمت تلگرام
پیش از اینکه کار تکمیل کنترلر api/logs را در برنامهی Web API انجام دهیم، ابتدا در همان برنامهی Web API، سرویسی را برای ارسال لاگهای رسیده به سمت تلگرام، تهیه میکنیم. علت اینکه این قسمت را به برنامهی سمت سرور محول کردهایم، شامل موارد زیر است:
- درست است که میتوان کتابخانههای مرتبط با تلگرام را به برنامهی سیشارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیمتر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آنرا در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامهی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامهی سمت کلاینت به Web API، مزیت لاگ سمت سرور آنرا مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر میکند و صرفا محدود به تلگرام نیست.
برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه میکنیم:
توضیحات:
- برای کار با API تلگرام، از کتابخانهی معروف Telegram.Bot استفاده کردهایم که به صورت زیر، وابستگی آن به برنامهی Web API اضافه میشود:
- این سرویس برای کار کردن، نیاز به تنظیمات زیر را دارد:
- برای دریافت 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 قرار میدهیم:
که نحوهی ثبت و معرفی آنها به سیستم تزریق وابستگیهای برنامهی Web API، به صورت زیر است:
سرویس ITelegramBotService را با طول عمر Singleton معرفی کردهایم. چون new TelegramBotClient ای که در سازندهی آن صورت میگیرد:
باید فقط یکبار در طول عمر برنامه انجام شود و از این پس، هر بار که متد client.SendTextMessageAsync_ آن فراخوانی میگردد، پیامی به سمت بات و سپس کانال اختصاصی ما ارسال میشود.
ایجاد کنترلر Logs، جهت دریافت لاگهای رسیدهی از سمت کلاینت
مرحلهی آخر کار بسیار سادهاست. سرویس تکمیل شدهی ITelegramBotService را به سازندهی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آنرا فراخوانی میکنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
آزمایش برنامه
برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار دادهایم:
اکنون اگر برنامه را اجرا کرده و سپس بر روی دکمهی شمارشگر کلیک کنیم، همان تصویر ابتدای مطلب را که حاصل از ارسال جزئیات این استثنای مدیریت نشده به سمت تلگرام است، مشاهده خواهیم کرد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmTelegramLogger.zip
نحوهی نمایش خطاها در برنامههای 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>
نحوهی مدیریت استثناءها در برنامههای 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 فوق، سه وابستگی تزریق شده را مشاهده میکنید:
public ClientLoggerProvider( IServiceProvider serviceProvider, IOptions<WebApiLoggerOptions> options, NavigationManager navigationManager)
مهمترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت میدهد:
public ILogger CreateLogger(string categoryName) { return new WebApiLogger(_httpClient, _options, _navigationManager); }
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 } } } }
- متد IsEnabled آن مشخص میکند که چه سطحی از رخدادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت میکند:
using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public class WebApiLoggerOptions { public string LoggerEndpointUrl { set; get; } public LogLevel LogLevel { get; set; } = LogLevel.Information; } }
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "WebApiLogger": { "LogLevel": "Warning", "LoggerEndpointUrl": "/api/logs" } }
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)); // … } } }
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(); } } }
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
ایجاد سرویسی برای ارسال لاگهای برنامه به سمت تلگرام
پیش از اینکه کار تکمیل کنترلر 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; } }
پس از شروع این بات، ابتدا دستور 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…" } }
namespace BlazorWasmTelegramLogger.Server { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.Configure<TelegramLoggingBotOptions>(options => Configuration.GetSection("TelegramLoggingBot").Bind(options)); services.AddSingleton<ITelegramBotService, TelegramBotService>(); // ... } // ... } }
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); }
ایجاد کنترلر 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