مقدمه
ایجاد کلاس لاگ
public class DBLog { public int DBLogId { get; set; } public string? LogLevel { get; set; } public string? EventName { get; set; } public string? Message { get; set; } public string? StackTrace { get; set; } public DateTime CreatedDate { get; set; }=DateTime.Now; }
ایجاد دیتابیس لاگر
public class DBLogger:ILogger { private bool _isDisposed; private readonly ApplicationDbContext _dbContext; public DBLogger(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { var dblLogItem = new DBLog() { EventName = eventId.Name, LogLevel = logLevel.ToString(), Message = exception?.Message, StackTrace=exception?.StackTrace }; _dbContext.DBLogs.Add(dblLogItem); _dbContext.SaveChanges(); } public bool IsEnabled(LogLevel logLevel) { return true; } public IDisposable BeginScope<TState>(TState state) { return null; } }
ایجاد یک لاگ پروایدر سفارشی
حال باید یک لاگ پروایدر سفارشی را ایجاد کنیم تا بتوان یک نمونه از دیتابیس لاگر سفارشی بالا (DBLogger) را ایجاد کرد.
public class DbLoggerProvider:ILoggerProvider { private bool _isDisposed; private readonly ApplicationDbContext _dbContext; public DbLoggerProvider(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); } public ILogger CreateLogger(string categoryName) { return new DBLogger(_dbContext); } public void Dispose() { } }
همانطور که ملاحظه مینمایید، این لاگ پروایدر، از اینترفیس ILoggerProvider ارث بری کردهاست که دارای متد CreateLogger میباشد ئ این متد با شروع برنامه، یک نمونه از دیتابیس لاگر سفارشی ما را ایجاد میکند. در سازندهی این کلاس، DatabaseContext را مقدار دهی نمودهایم تا آنرا به کلاس DBLogger ارسال نماییم.
در انتها کافیست در کلاس Startup.cs این لاگ پروایدر سفارشی (DbLoggerProvider ) را صدا بزنیم.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) { . . . #region CustomLogProvider var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider; var appDbContext = serviceProvider.GetRequiredService<ApplicationDbContext>(); loggerFactory.AddProvider(new DbLoggerProvider(appDbContext)); #endregion . . .
مشکل!
public class DbLoggerProvider:ILoggerProvider { private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly IList<DBLog> _currentBatch = new List<DBLog>(); private readonly TimeSpan _interval = TimeSpan.FromSeconds(2); private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>()); private readonly Task _outputTask; private readonly ApplicationDbContext _dbContext; private bool _isDisposed; public DbLoggerProvider(ApplicationDbContext dbContext) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _outputTask = Task.Run(ProcessLogQueue); } public ILogger CreateLogger(string categoryName) { return new DBLogger(this,categoryName); } private async Task ProcessLogQueue() { while (!_cancellationTokenSource.IsCancellationRequested) { while (_messageQueue.TryTake(out var message)) { try { _currentBatch.Add(message); } catch { //cancellation token canceled or CompleteAdding called } } await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token); _currentBatch.Clear(); await Task.Delay(_interval, _cancellationTokenSource.Token); } } internal void AddLogItem(DBLog appLogItem) { if (!_messageQueue.IsAddingCompleted) { _messageQueue.Add(appLogItem, _cancellationTokenSource.Token); } } private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken) { try { if (!items.Any()) { return; } // We need a separate context for the logger to call its SaveChanges several times, // without using the current request's context and changing its internal state. foreach (var item in items) { var addedEntry = _dbContext.DbLogs.Add(item); } await _dbContext.SaveChangesAsync(cancellationToken); } catch { // don't throw exceptions from logger } } [SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception", Justification = "don't throw exceptions from logger")] private void Stop() { _cancellationTokenSource.Cancel(); _messageQueue.CompleteAdding(); try { _outputTask.Wait(_interval); } catch { // don't throw exceptions from logger } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { Stop(); _messageQueue.Dispose(); _cancellationTokenSource.Dispose(); _dbContext.Dispose(); } } finally { _isDisposed = true; } } } }
public class DBLogger:ILogger { private readonly LogLevel _minLevel; private readonly DbLoggerProvider _loggerProvider; private readonly string _categoryName; public DBLogger( DbLoggerProvider loggerProvider, string categoryName ) { _loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider)); _categoryName= categoryName; } public IDisposable BeginScope<TState>(TState state) { return new NoopDisposable(); } public bool IsEnabled(LogLevel logLevel) { return logLevel >= _minLevel; } public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } if (formatter == null) { throw new ArgumentNullException(nameof(formatter)); } var message = formatter(state, exception); if (exception != null) { message = $"{message}{Environment.NewLine}{exception}"; } if (string.IsNullOrEmpty(message)) { return; } var dblLogItem = new DBLog() { EventName = eventId.Name, LogLevel = logLevel.ToString(), Message = $"{_categoryName}{Environment.NewLine}{message}", StackTrace=exception?.StackTrace }; _loggerProvider.AddLogItem(dblLogItem); } private class NoopDisposable : IDisposable { public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { } } }
Today at .NET Conf 2019, we shared some exciting announcements for Xamarin and Visual Studio developers, including:
- XAML Hot Reload for Xamarin.Forms: Make changes to your XAML UI. See them reflected live on your emulator, simulator, or physical device.
- Xamarin Hot Restart: Test changes made to your app, including multi-file code edits, resources, and references, while using a much faster build and deploy cycle.
- iOS 13 and Android 10: Take advantage of the full power and performance of native platforms and APIs. Including iPadOS, dark mode, and foldable support.
اگر در یک صفحه، سه کامپوننت به این صورت وجود داشته باشند:
<ComponentA/> <ComponentB/> <ComponentC/>
پردازش اینها در Blazor SSR، ترتیبی نیست و موازی است (هر کدام، بر روی یک ترد مجزا پردازش میشوند). اما اگر کامپوننت B، داخل کامپوننت A باشد، اینها با هم و در طی یک ترد پردازش میشوند. هرچند این بحث پردازش موازی هم در صورت فراهم بودن منابع سیستمی و سختافزاری، تردهای آزاد در thread-pool و موارد دیگر، ممکن است رخدهد یا خیر. بنابراین گاهی از اوقات، در طول مدتی، خطایی را مشاهده نمیکنید؛ اما ... اگر به لاگهای سیستم در طول یک روز مراجعه کنید، وجود این خطاها کاملا مشخص است. بنابراین بهتر است بر اساس «شانس» کار نکنید. inject AppDbContext appDbContext@ به این معنا است که فقط یک نمونه از DbContext، در طول درخواست جاری (از این لحاظ، Blazor SSR با ASP.NET Core یکسان رفتار میکند) بین تمام اجزای در حال پردازش صفحهی در حال رندر، به اشتراک گذاشته شود. «ممکن است» در صورت عدم وجود پردازش موازی، هیچ خطایی را دریافت نکنید و یا ... به صورت «اتفاقی» و با مهیا بودن شرایط سیستمی و سختافزاری، خطای یاد شده را مشاهده کنید.
<CascadingValue Value="this" IsFixed="true"> main </CascadingValue>
The attribute names could not be inferred from bind attribute 'bind-value'. Bind attributes should be of the form 'bind' or 'bind-value' along with their corresponding optional parameters like 'bind-value:event', 'bind:format' etc.
به صورت خلاصه:
- اگر فقط از کامپوننتهای کلاسی استفاده میکنید، mobx-react@5 برای کار شما پاسخگو است.
- اگر از کامپوننتهای کلاسی و همچنین کامپوننتهای تابعی در برنامهی خود استفاده میکنید، mobx-react@6 به همراه mobx-react-lite نیز ارائه میشود و هر دو روش را با هم پوشش میدهد.
- اگر فقط از کامپوننتهای تابعی جدید استفاده میکنید، هوکهای کتابخانهی کوچک mobx-react-lite برای کار شما کافی است.
معرفی useLocalStore Hook و useObserver Hook
در مطالب قبلی، روش تعریف یک کلاس مخزن حالت MobX را توسط تزئین کنندههایی مانند observable، computed و action بررسی کردیم. همچنین دریافتیم که تعریف یک چنین تزئین کنندههایی، یا نیاز به استفادهی از تایپاسکریپت را دارد و یا باید پروژهی React را جهت تغییر کامپایلر Babel آن و فعالسازی decorators، مقداری ویرایش کرد. با useLocalStore Hook هرچند تمام روشهای قبلی هنوز هم پشتیبانی میشوند، اما دیگر نیاز به استفادهی از decorators نیست. useLocalStore تابعی است که یک شیء را باز میگرداند. هر خاصیتی از این شیء، به صورت خودکار observable درنظر گرفته میشود. تمام getters آن به عنوان computed properties تفسیر میشوند و تمام متدهای آن، action درنظر گرفته خواهند شد.
یک مثال:
import React from 'react' import { useLocalStore, useObserver } from 'mobx-react' // 6.x export const SmartTodo = () => { const todo = useLocalStore(() => ({ title: 'Click to toggle', done: false, toggle() { todo.done = !todo.done }, get emoji() { return todo.done ? '😜' : '🏃' }, })) return useObserver(() => ( <h3 onClick={todo.toggle}> {todo.title} {todo.emoji} </h3> )) }
- روش استفادهی از تابع useLocalStore، میتواند به صورت محلی (همانند اسم آن) مختص به یک کامپوننت باشد. یعنی میتوان بجای state استاندارد React که اجازهی تغییر مستقیم خواص آنرا نمیدهد، از MobX State محلی ارائه شدهی توسط useLocalStore استفاده کرد و یا میتوان useLocalStore را به صورت global نیز تعریف کرد که در ادامهی بحث به آن میپردازیم.
- در مثال فوق، طول عمر شیء ایجاد شدهی توسط useLocalStore، محلی و محدود به طول عمر کامپوننت تابعی تعریف شدهاست.
- در اینجا شیء بازگشت داده شدهی توسط useLocalStore، دارای دو خاصیت title و done است. این دو خاصیت بدون نیاز به هیچ تعریف خاصی، observable در نظر گرفته میشوند. Fi به علاوه خاصیت getter آن به نام emoji نیز به عنوان یک خاصیت محاسباتی MobX تفسیر شده و متد toggle آن به صورت یک action پردازش میشود. بنابراین در حین کار با MobX Hooks دیگر نیازی به تغییر ساختار پروژهی React، برای پشتیبانی از decorators نیست.
- در این مثال، return useObserver را نیز مشاهده میکنید. کار آن رندر مجدد کامپوننت، با تغییر یکی از خواص observable ردیابی شدهی توسط آن است.
امکان تعریف global state با کمک useLocalStore
نام useLocalStore از این جهت انتخاب شدهاست که مشخص کند مخزن حالت ایجاد شدهی توسط آن، درون یک کامپوننت به صورت محلی ایجاد میشود و سراسری نیست. اما این نکته به این معنا نیست که نمیتوان مخزن حالت ایجاد شدهی توسط آنرا در بین سلسه مراتب کامپوننتهای برنامه به اشتراک گذاشت. توسط تابع useLocalStore میتوان چندین مخزن حالت را ایجاد کرد و سپس توسط شیءای دیگر آنها را یکی کرده و در آخر به کمک Context API خود React آنرا در اختیار تمام کامپوننتهای برنامه قرار داد.
تا نگارش MobX 5x (و همچنین پس از آن)، توسط inject@ میتوان یک مخزن حالت را در اختیار یک کامپوننت قرار داد (مانند inject('myStore')). طراحی inject@ مربوط است به زمانیکه امکان دسترسی به Context پشت صحنهی React به صورت عمومی توسط Context API آن ارائه نشده بود. به همین جهت از این پس دیگر نیازی به استفادهی از آن نیست.
چگونه توسط MobX Hooks، یک مخزن حالت سراسری را ایجاد کنیم؟
برای ایجاد یک مخزن حالت سراسری با روش جدید MobX Hooks، مراحل زیر را میتوان طی کرد:
الف) ایجاد شیء store
ابتدا متدی را مانند createStore ایجاد میکنیم، به نحوی که یک شیء را بازگشت دهد. این شیء همانطور که عنوان شد، خواصش، getters و متدهای آن، توسط MobX ردیابی خواهند شد (مانند const todo = useLocalStore مثال فوق) و نیازی به اعمال MobX Decorators را ندارند.
export function createStore() { return { // ... } }
ب) برپایی Context
اینبار دیگر نه از شیء Provider خود MobX استفاده میکنیم و نه از تزئین کنندهی inject@ آن؛ بلکه از React Context استاندارد استفاده خواهیم کرد:
import React from 'react'; import { createStore } from './createStore'; import { useLocalStore } from 'mobx-react'; // 6.x or mobx-react-lite@1.4.0 const storeContext = React.createContext(null); export const StoreProvider = ({ children }) => { const store = useLocalStore(createStore); return <storeContext.Provider value={store}>{children}</storeContext.Provider>; } export const useStore = () => { const store = React.useContext(storeContext); if (!store) { throw new Error('useStore must be used within a StoreProvider.'); } return store }
- سپس توسط React.createContext، یک شیء Context استاندارد React را ایجاد میکنیم؛ به نام storeContext.
- تابع کمکی StoreProvider، جایگزین شیء Provider قبلی MobX میشود. یعنی کارش محصور کردن کامپوننت App برنامه است تا شیء store را در اختیار سلسه مراتب کامپوننتهای React قرار دهد. در اینجا children به همان کامپوننتهایی که قرار است توسط Context.Provider محصور شوند اشاره میکند.
- تابع کمکی useStore، جهت محصور کردن متد React.useContext، اضافه شدهاست. میتوانید useContext Hook را به صورت مستقیم در کامپوننتهای تابعی فراخوانی کنید و یا میتوانید از متد کمکی useStore بجای آن استفاده نمائید تا حجم کدهای تکراری برنامه کاهش یابد.
ج) استفادهی از StoreProvider تهیه شده
اکنون با استفاده از متد StoreProvider فوق که شیء Context.Provider استاندارد React را بازگشت میدهد، میتوان کامپوننتهای بالاترین کامپوننت سلسه مراتب کامپوننتهای برنامه را محصور کرد، تا تمام آنها بتوانند به store ذخیره شدهی در Provider، دسترسی پیدا کنند:
export default function App() { return ( <StoreProvider> <main> <Component1 /> <Component2 /> <Component3 /> </main> </StoreProvider> ); }
د) استفاده از store مهیا شده در کامپوننتهای تابعی برنامه
پس از تهیهی متدی کمکی useStore که در حقیقت همان useContext Hook است، میتوان به کمک آن در کامپوننتهای تابعی، به store و تمام امکانات آن دسترسی پیدا کرد:
const store = useStore();
سؤال: آیا هنوز هم میتوان یک مخزن پیچیدهی متشکل از چندین کلاس را تشکیل داد؟
پاسخ: بله. برای مثال ابتدا دو کلاس جدید CounterStore و ThemeStore را به نحو متداولی، با استفادهی از MobX decorators طراحی میکنیم (دقیقا مانند مثال قسمت قبل). سپس بجای ذکر نال، بجای پارامتر متد createContext، آنرا با یک شیء جدید مقدار دهی میکنیم که هر کدام از خواص آن، به یک وهله از مخازن حالت ایجاد شده اشاره میکند:
export const storesContext = React.createContext({ counterStore: new CounterStore(), themeStore: new ThemeStore(), }); export const useStores = () => React.useContext(storesContext);
const { counterStore } = useStores();
چند نکتهی تکمیلی
نکته 1: با اشیاء MobX از Object Destructuring استفاده نکنید!
اگر بر روی اشیاء MobX از Object Destructuring استفاده کنیم، خروجی آن تبدیل به متغیرهای سادهای خواهند شد که دیگر ردیابی نمیشوند.
برای مثال اگر counterStore مثال فوق به همراه خاصیت observable ای به نام activeUserName است، آنرا به صورت زیر تبدیل به متغیر activeUserName نکنید؛ چون دیگر reactive نخواهد بود:
const { counterStore: { activeUserName }, } = useStores();
const { counterStore } = useStores();
نکته 2: مدیریت side effects با MobX
در مورد اثرات جانبی و side effects در مطلب «قسمت 32 - React Hooks - بخش 3 - نکات ویژهی برقراری ارتباط با سرور» بیشتر بحث شد. اگر یک اثر جانبی مانند تنظیم document.title، به مقدار یک خاصیت observable وابسته بود، میتوان از متد autorun که تغییرات آنها را ردیابی میکند، درون useEffect Hook استاندارد، استفاده کرد:
import React from 'react' import { autorun } from 'mobx' function useDocumentTitle(store) { React.useEffect( () => autorun(() => { document.title = `${store.title} - ${store.sectionName}` }), [], // note empty dependencies ) }
نکته 4: روش فعالسازی MobX strict mode
اگر strict mode را در Mobx به روش زیر فعال کنیم:
import { configure } from "mobx"; configure({ enforceActions: true });
نکته 3: روش انجام اعمال async در MobX
فرض کنید یک عملیات async را در یک اکشن متد کلاس حالت MobX، به صورت زیر انجام دادهایم و نتیجهی آن به خاصیت weatherData آن کلاس که observable است، به صورت مستقیم انتساب داده شدهاست:
@action loadWeather = city => { fetch( `https://abnormal-weather-api.herokuapp.com/cities/search?city=${city}` ) .then(response => response.json()) .then(data => { this.weatherData = data; }); };
راه حل اول: تغییر خاصیت this.weatherData را به یک اکشن متد مجزا انتقال میدهیم:
@action setWeather = data => { this.weatherData = data; };
راه حل دوم: اگر نمیخواهیم یک اکشن متد جدید را تعریف کنیم، میتوان از متد کمکی runInAction در داخل یک callback استفاده کرد:
loadWeatherInline = city => { fetch(`http://jsonplaceholder.typicode.com/comments/${city}`) .then(response => response.json()) .then(data => { runInAction(() => (this.weatherData = data)); }); };
در مورد اعمال async/await چطور؟
در اینجا هم تفاوتی نمیکند. هر چیزی پس از await، شبیه به حالت متد then پردازش میشود. به همین جهت در اینجا نیز باید از یکی از دو راه حل ارائه شده، استفاده کرد:
loadWeatherAsync = async city => { const response = await fetch( `http://jsonplaceholder.typicode.com/comments/${city}` ); const data = await response.json(); runInAction(() => { this.weatherData = data; }); };
Oracle is pleased to announce that Oracle database support for Entity Framework Core 7 (EF7) is now available. Download Oracle EF7 for free from NuGet Gallery.