پیاده سازی ویژگی Health Check بدون استفاده از قابلیتهای ASP.NET Core 2.2
اگر بخواهیم در بررسی سلامت برنامه، وضعیت بانک اطلاعاتی آنرا گزارش دهیم، میتوان یک چنین اکشن متدی را طراحی کرد که در آن اتصالی به بانک اطلاعاتی باز شده و اگر در حین فراخوانی مسیر working/، استثنائی رخ داد، با بازگشت status code مساوی 503، عدم سلامت برنامه اعلام شود؛ کاری که سرویسهای ping متداول نمیتوانند آنرا با این دقت انجام دهند:
[Route("working")] public ActionResult Working() { using (var connection = new SqlConnection(_connectionString)) { try { connection.Open(); } catch (SqlException) { return new HttpStatusCodeResult(503, "Generic error"); } } return new EmptyResult(); }
بازنویسی قطعه کد فوق با ویژگی جدید Health Check در ASP.NET Core 2.2
اکنون اگر بخواهیم قطعه کد فوق را با کمک ویژگیهای جدید ASP.NET Core 2.2 بازنویسی کنیم، روش کار به صورت زیر خواهد بود:
namespace MvcHealthCheckTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks() .AddCheck("sql", () => { using (var connection = new SqlConnection(Configuration["connectionString"])) { try { connection.Open(); } catch (SqlException) { return HealthCheckResult.Unhealthy(); } } return HealthCheckResult.Healthy(); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseHealthChecks("/working");
- سپس توسط متد app.UseHealthChecks، بدون اینکه نیاز باشد کنترلر و اکشن متد جدیدی را جهت بازگشت وضعیت سلامت برنامه، تعریف کنیم، مسیر working/ قابل دسترسی خواهد شد.
تا اینجا اگر این مسیر را به سرویس بررسی uptime برنامهی خود معرفی کنید، صرفا وضعیت قابل دسترسی بودن مسیر working/ را دریافت خواهید کرد. اگر نیاز به گزارش دقیقتری وجود داشت، میتوان به کمک متد AddCheck، یک منطق سفارشی را نیز به آن افزود؛ همانند بررسی امکان اتصال به بانک اطلاعاتی، به روشی که ملاحظه میکنید. در اینجا اگر منطق مدنظر با موفقیت اجرا شد، HealthCheckResult.Healthy بازگشت داده میشود و یا HealthCheckResult.Unhealthy در صورت عدم موفقیت. هر کدام از این متدها میتوانند توضیحات و یا اطلاعات بیشتری را نیز توسط پارامترهای خود ارائه دهند.
امکان تهیه سرویسهای سفارشی بررسی سلامت برنامه
در مثال قبل، منطق بررسی سلامت برنامه را همانجا داخل متد ConfigureServices، به کمک متد services.AddHealthChecks().AddCheck معرفی کردیم. امکان انتقال این کدها به سرویسهای سفارشی، با پیاده سازی اینترفیس IHealthCheck نیز وجود دارد:
public class SqlServerHealthCheck : IHealthCheck { private readonly IConfiguration _configuration; public SqlServerHealthCheck(IConfiguration configuration) { _configuration = configuration; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = new SqlConnection(_configuration["connectionString"])) { try { connection.Open(); } catch (SqlException) { return Task.FromResult(HealthCheckResult.Unhealthy()); } } return Task.FromResult(HealthCheckResult.Healthy()); } }
namespace MvcHealthCheckTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHealthChecks() .AddCheck<SqlServerHealthCheck>("sql");
سفارشی سازی خروجی بررسی سلامت برنامهها
تا اینجا از متدهای کلی Unhealthy و Healthy برای بازگشت وضعیت سلامت برنامه استفاده کردیم؛ خروجیهای بهتری را نیز میتوان ارائه داد:
public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = new SqlConnection(_configuration["connectionString"])) { try { connection.Open(); } catch (SqlException) { return Task.FromResult(new HealthCheckResult( status: context.Registration.FailureStatus, description: "It is dead!")); } } return Task.FromResult(HealthCheckResult.Healthy("Healthy as a horse")); }
روش دیگر سفارشی سازی خروجی آن، استفاده از پارامتر دوم متد app.UseHealthChecks است:
namespace MvcHealthCheckTest { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseHealthChecks("/working", new HealthCheckOptions { ResponseWriter = async (context, report) => { var result = JsonConvert.SerializeObject(new { status = report.Status.ToString(), errors = report.Entries.Select(e => new { key = e.Key, value = Enum.GetName(typeof(HealthStatus), e.Value.Status) }) }); context.Response.ContentType = MediaTypeNames.Application.Json; await context.Response.WriteAsync(result); } });
معرفی کتابخانهای از IHealthCheckهای سفارشی
از مخزن کد AspNetCore.Diagnostics.HealthChecks میتوانید IHealthCheckهای سفارشی مخصوص SQL Server، MySQL و غیره را نیز دریافت و استفاده کنید.
من با VS 2013 کار میکنم، در قسمت "افزودن فایل tsconfig.json به پروژه" در لیست ظاهر شده، گزینهی TypeScript JSON Configuration File برای من نمایش داده نمیشود مشکل کجاست؟
localForage is a fast and simple storage library for JavaScript.localForage uses localStorage in browsers with no IndexedDB or WebSQL.
localforage.setItem('key', 'value', function (err) { // if err is non-null, we got an error localforage.getItem('key', function (err, value) { // if err is non-null, we got an error. otherwise, value is the value }); });
:Framework Support
AngularJS
Angular 4 and up
Backbone
Ember
Vue
TypeScript:
import localForage from "localforage";
In this post I’ll quickly explain the minimum you need to know in order to publish an Angular component to npm. By the end of the post you’ll know how your module to:
- Be platform independent (i.e. run in Web Workers, Universal).
- Should be bundled and distributed.
- Work with the Angular’s Ahead-of-Time compiler.
- Play well with TypeScript and allow autocompletion and compile-time type checking.
angular-translate دایرکتیو و فیلتر هایی را به صورت کامپوننت عرضه کرده است که شما میتوانید به وسیلهی آنها پروژه خود را به زبانهای گوناگون localize کنید. آنچه که در تصویر فوق مشاهده میکنید در حقیقت ساختار ماژول angular-translate را نمایش میدهد. به این صورت که در اولین لایه دایرکتیو translate قرار گرفته است.
سطح بعدی شاید کمی جذابتر به نظر برسد. هر دو بخش دایرکتیو و فیلتر از سرویس تزریق شده translate$ استفاده میکنند. این بدین معنی است که در هنگام تغییر یک زبان به زبان دیگر به وسیله دایرکتیوهایی که در view نوشته شده است، در حقیقت شما این دایرکتیوها را به سطح فیلتر انتقال دادهاید و پس از آن، فیلتر آن را به لایه سرویس برده و سرویس کار اصلی را انجام میدهد.
در سطح بعدی لایه Interpolator قرار گرفته است که وظیفه تغییر DOMها را بر عهده دارد. این فرآیند متغیرهایی را که در ریسورس قرار دادهایم، درایه به درایه در view و در مکانهای هم نام قرار میدهد. این فرآیند به وسیله راهکارهای گوناگونی از قبیل Message Format و یا Angular Core Interpolation Service امکان پذیر است.
جعبه خاکستری رنگ بخش missing translation handler را نشان میدهد. این handlerها زمانی فراخوانی میگردند که translator به دنبال یک key میگردد که تعریف نشده باشد. angular-translate خود شامل یک logging service میباشد که به صورت extension باید آن را به پروژه خود اضافه نمایید که در بخشهای بعدی در مورد آن مفصلتر بحث میکنیم.
بخش asynchronous loader شما را قادر میسازد که دادههای زبانهای متفاوت را به صورت غیر هم زمان بارگذاری نمایید. angular-translate از ماژولهای asynchronous loader پشتیبانی میکند. روش معرفی شده در سایت مرجع در مورد بارگذاری غیر همزمان urlLoader و staticFilesLoader هستند که در بخشهای بعدی به آن خواهیم پرداخت.
angular-translate برای ذخیره سازی دادههای بازگذاری شده و نگه داشتن زبان سایت در صورت reload شدن صفحه راهکارهای متفاوتی را ارائه نموده است. این قابلیت در ابتدا چک میکند که ریسورسهای زبان درون یک local storage قرار گرفتهاند یا خیر. این عملیات طی یک فرآیند جستجوی key-value صورت میگیرد و در نهایت فایلهای ریسورس زبان مورد نظر که درون کوکی یا localStorage ذخیره شدهاند بارگذاری میشوند. angular-translate به صورت توکار دارای دو راهکار localStorage و cookieStorage برای ذخیره سازی ریسورسها میباشد.
در بخش بعدی در مورد نحوهی ذخیره سازی دادهها به دو روش cookieStorage و localStorage بیشتر صحبت خواهیم کرد.
A set of files that comprise a standalone app, a library, or a set of end-to-end (e2e) tests.
ng --version
ng new angular-apps --create-application=false
angular-apps/README.md angular-apps/angular.json angular-apps/package.json angular-apps/tsconfig.json angular-apps/tslint.json angular-apps/.editorconfig angular-apps/.gitignore
cd angular-apps
ng generate application admin
ng generate library ngx-qrcode
ng generate service auth --project=admin
projects/admin/src/app/auth.service.spec.ts projects/admin/src/app/auth.service.ts
ng serve admin
ng new
What name would you like to use for the new workspace and initial project?
به روز رسانیهای خوشبینانهی UI
پیاده سازی اعمال CRUD توسط Axios در قسمت قبل، به همراه یک مشکل مهم است: اعمال کار با شبکه و سرور، زمانبر هستند و مدتی طول میکشد تا پاسخ عملیات از سمت سرور دریافت شود. در این بین اگر خطایی رخ دهد، مابقی کدهای نوشته شدهی در متدهایی مانند Update و Delete، اجرا نمیشوند. به این حالت «به روز رسانی بدبینانهی UI» گفته میشود. در حالت خوشبینانه، فرض بر این است که در اکثر موارد، فراخوانی سرور با موفقیت به پایان میرسد. در یک چنین حالتی، ابتدا UI به روز رسانی میشود و سپس فراخوانیهای سمت سرور صورت میگیرند. اگر این فراخوانی با شکست مواجه شد، مجددا UI را به حالت قبلی آن باز میگردانیم:
handleDelete = async post => { const posts = this.state.posts.filter(item => item.id !== post.id); this.setState({ posts }); await axios.delete(`${apiEndpoint}/${post.id}`); };
اما اگر در این بین خطایی رخ داد، چه باید کرد؟ باید آخرین تغییر انجام شده را به حالت اول باز گرداند. انجام یک چنین کاری در React سادهاست. چون ما state را به صورت مستقیم ویرایش نمیکنیم، همیشه میتوان ارجاعی را به state قبلی، ذخیره و سپس در صورت نیاز آنرا بازیابی کرد:
handleDelete = async post => { const originalPosts = this.state.posts; const posts = this.state.posts.filter(item => item.id !== post.id); this.setState({ posts }); // Optimistic Update try { await axios.delete(`${apiEndpoint}/${post.id}`); } catch (ex) { alert("An error occurred when deleting a post!"); this.setState({ posts: originalPosts }); // Undo changes } };
مدیریت خطاهای رخ دادهی در حین فراخوانی سرور
تا اینجا مشاهده کردیم که یک روش مدیریت خطاها در کدهای Axios، قرار دادن آنها در یک قطعه کد try/catch است. در اینجا نیز باید بتوان بین خطاهای پیش بینی شده و نشده، تفاوت قائل شد.
- خطاهای پیش بینی شده: برای مثال اگر درخواست حذف رکوردی را دادیم که در بانک اطلاعاتی موجود نیست، انتظار داریم سرور، خطای 404 یا return NotFound را بازگشت دهد و یا 400 که معادل bad request است و در حالت ارسال دادههایی غیرمعتبر، رخ میدهد. در این موارد بهتر است خطاهایی خاص را به کاربران نمایش داد؛ برای مثال رکورد درخواستی وجود ندارد یا پیشتر حذف شدهاست.
- خطاهای پیش بینی نشده: این نوع خطاها نباید و یا قرار نیست در شرایط عادی رخ دهند. برای مثال اگر شبکه در دسترس نیست، امکان ارتباط با سرور نیز میسر نخواهد بود و یا حتی ممکن است خطایی در کدهای سمت سرور، سبب بروز خطایی شده باشد. این نوع خطاها ابتدا باید لاگ شوند تا با بررسیهای آتی آنها، بتوان مشکلات پیش بینی نشده را بهتر برطرف کرد. همچنین در یک چنین مواردی، باید یک پیام خطای خیلی عمومی را به کاربر نمایش داد؛ برای مثال «یک خطای پیش بینی نشده رخ دادهاست.».
برای مدیریت این دو حالت باید به جزئیات شیء ex، در بدنهی catch، دقت کرد که دارای دو خاصیت request و response است. اگر ex.response تنظیم شده بود، یعنی دریافت خروجی از سرور موفقیت آمیز بودهاست. اگر سرور در دسترس نباشد و یا برنامهی سمت سرور کرش کرده باشد، ex.response نال خواهد بود. اگر ex.request نال نبود، یعنی ارسال درخواست به سمت سرور با موفقیت انجام شدهاست. برای مثال جهت بررسی خطای مورد انتظار 404، میتوان در قسمت catch(ex) به صورت زیر عمل کرد:
try { await axios.delete(`${apiEndpoint}/${post.id}`); } catch (ex) { if (ex.response && ex.response.status === 404) { alert("This post has already been deleted!"); } else { console.log("Error", ex); alert("An unexpected error occurred when deleting a post!"); } this.setState({ posts: originalPosts }); // Undo changes }
عموما خطاهای پیشبینی شده را لاگ نمیکنیم؛ چون ممکن است کاربر، یک صفحه را در چندین برگه باز کرده باشد و در یکی، رکوردی را حذف کند. در این حال، این رکورد هنوز در برگههای دیگر موجود است و اگر مجددا درخواست حذف آنرا صادر کند، مشکل خاصی از دیدگاه برنامه رخ ندادهاست و نیازی به پیگیریهای آتی را ندارد. یعنی صرفا یک client error است.
مدیریت سراسری خطاهای رخ دادهی در حین فراخوانی سرور
برای مدیریت خطاها، نیاز است یک چنین try/catchهایی را در تمام قسمتهای برنامه که با سرور کار میکنند، قرار دهیم. برای کاهش این کدهای تکراری، از interceptors کتابخانهی Axios استفاده میشود. در این کتابخانه میتوان در جاهائیکه درخواستی به سمت سرور ارسال میشود و یا پاسخی از سمت سرور دریافت میشود، قطعه کدهایی سراسری را قرار داد و بر روی درخواست و یا پاسخ، تغییراتی را اعمال کرد و یا حتی اطلاعات مربوطه را لاگ کرد؛ به این نوع قطعه کدها، interceptor گفته میشود و برای تعریف آنها میتوان از axios.interceptors.request و یا axios.interceptors.response، خارج از کلاس جاری استفاده کرد. برای مثال بر روی شیء axios.interceptors.response، میتوان متد use را فراخوانی کرد که دو پارامتر را که هر کدام یک callback function هستند، میپذیرد. اولی در صورت موفقیت آمیز بودن response فراخوانی میشود و دومی در صورت شکست آن. اگر نیازی به هر کدام نبود، میتوان آنرا به null مقدار دهی کرد. اگر مدیریت قسمت شکست علمیات مدنظر است، نیاز خواهد بود در پایان این callback function، یک Rejected Promise را بازگشت داد تا ادامهی برنامه، به درستی مدیریت شود. در این حالت اگر خطایی رخ دهد، ابتدا این interceptor فراخوانی میشود و سپس کنترل به بدنهی catch منتقل خواهد شد:
import "./App.css"; import axios from "axios"; import React, { Component } from "react"; axios.interceptors.response.use(null, error => { console.log("interceptor called."); return Promise.reject(error); }); const apiEndpoint = "https://localhost:5001/api/posts"; class App extends Component {
axios.interceptors.response.use(null, error => { const expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log("Error", error); alert("An unexpected error occurred when deleting a post!"); } return Promise.reject(error); });
یک نکته: استفاده از try/catchها فقط برای بازگشت UI به حالت قبلی و یا نمایش خطایی خاص به کاربر توصیه میشوند. اگر از روش «به روز رسانیهای خوشبینانهی UI» استفاده نمیکنید و همچنین خطاهای ویژهای بجز خطای عمومی لاگ شدهی در interceptor فوق مدنظر شما نیست، نیازی هم به try/catch نخواهد بود و پس از بروز خطا، قسمتهای بعدی کد اجرا نمیشوند؛ اما خطای عمومی فوق نمایش داده خواهد شد.
ایجاد یک HTTP Service با قابلیت استفادهی مجدد
تا اینجا تعریف interceptor را پیش از کلاس کامپوننت جاری قرار دادهایم که هم سبب شلوغی این ماژول شدهاست و هم در صورت نیاز به آن در سایر برنامهها، باید همین قطعه کد را مجددا در آنها کپی کرد. به همین جهت پوشهی جدید src\services را ایجاد کرده و سپس فایل src\services\httpService.js را در آن با محتوای زیر ایجاد میکنیم:
import axios from "axios"; axios.interceptors.response.use(null, error => { const expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log("Error", error); alert("An unexpected error occurred when deleting a post!"); } return Promise.reject(error); }); export default { get: axios.get, post: axios.post, put: axios.put, delete: axios.delete };
سپس به app.js مراجعه کرده و این ماژول را با یک نام دلخواه import میکنیم:
import http from "./services/httpService";
ایجاد یک ماژول Config
بهبود دیگری را که میتوانیم اعمال کنیم، انتقال const apiEndpoint تعریف شده، به یک ماژول مجزا است؛ تا اگر نیاز به استفادهی از آن در قسمتهای دیگری نیز وجود داشت، به سادگی بتوان آنرا مدیریت کرد. به همین جهت فایل جدید src\config.json را با محتوای زیر ایجاد میکنیم:
{ "apiEndpoint" : "https://localhost:5001/api/posts" }
import config from "./config.json";
نمایش بهتر خطاها به کاربر توسط کتابخانهی react-toastify
بجای alert توکار مرورگرها، میتوان یک صفحهی دیالوگ زیباتر را برای نمایش خطاها درنظر گرفت. به همین جهت ابتدا کتابخانهی react-toastify را نصب میکنیم:
> npm i react-toastify --save
import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";
render() { return ( <React.Fragment> <ToastContainer />
import { toast } from "react-toastify"; // ... axios.interceptors.response.use(null, error => { // ... if (!expectedError) { // ... toast.error("An unexpected error occurrred."); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-backend-part-03.zip و sample-22-frontend-part-03.zip