تبدیلگر تاریخ شمسی برای AutoMapper
public class User { public int Id { set; get; } public string Name { set; get; } public DateTime RegistrationDate { set; get; } }
public class UserViewModel { public int Id { set; get; } public string Name { set; get; } public string RegistrationDate { set; get; } }
تبدیلگر سفارشی تاریخ میلادی به شمسی مخصوص AutoMapper
در ذیل یک تبدیلگر سفارشی مخصوص AutoMapper را با پیاده سازی اینترفیس ITypeConverter آن ملاحظه میکنید:
public class DateTimeToPersianDateTimeConverter : ITypeConverter<DateTime, string> { private readonly string _separator; private readonly bool _includeHourMinute; public DateTimeToPersianDateTimeConverter(string separator = "/", bool includeHourMinute = true) { _separator = separator; _includeHourMinute = includeHourMinute; } public string Convert(ResolutionContext context) { var objDateTime = context.SourceValue; return objDateTime == null ? string.Empty : toShamsiDateTime((DateTime)context.SourceValue); } private string toShamsiDateTime(DateTime info) { var year = info.Year; var month = info.Month; var day = info.Day; var persianCalendar = new PersianCalendar(); var pYear = persianCalendar.GetYear(new DateTime(year, month, day, new GregorianCalendar())); var pMonth = persianCalendar.GetMonth(new DateTime(year, month, day, new GregorianCalendar())); var pDay = persianCalendar.GetDayOfMonth(new DateTime(year, month, day, new GregorianCalendar())); return _includeHourMinute ? string.Format("{0}{1}{2}{1}{3} {4}:{5}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture), info.Hour.ToString("00"), info.Minute.ToString("00")) : string.Format("{0}{1}{2}{1}{3}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture)); } }
ثبت و معرفی تبدیلگرهای سفارشی AutoMapper
پس از تعریف یک تبدیلگر سفارشی AutoMapper، اکنون نیاز است آنرا به AutoMapper معرفی کنیم:
public class TestProfile1 : Profile { protected override void Configure() { // این تنظیم سراسری هست و به تمام خواص زمانی اعمال میشود this.CreateMap<DateTime, string>().ConvertUsing(new DateTimeToPersianDateTimeConverter()); this.CreateMap<User, UserViewModel>(); } public override string ProfileName { get { return this.GetType().Name; } } }
همانطور که مشاهده میکنید در اینجا دو نگاشت تعریف شدهاند. یکی برای تبدیل User به UserViewModel و دیگری، معرفی نحوهی نگاشت DateTime به string، توسط تبدیلگر سفارشی DateTimeToPersianDateTimeConverter است که به کمک متد الحاقی ConvertUsing صورت گرفتهاست.
باید دقت داشت که تنظیمات تبدیلگرهای سفارشی سراسری هستند و در کل برنامه و به تمام پروفایلها اعمال میشوند.
بررسی خروجی تبدیلگر سفارشی تاریخ
اکنون کار استفاده از تنظیمات AutoMapper با ثبت پروفایل تعریف شده آغاز میشود:
Mapper.Initialize(cfg => // In Application_Start() { cfg.AddProfile<TestProfile1>(); });
var dbUser1 = new User { Id = 1, Name = "Test", RegistrationDate = DateTime.Now.AddDays(-10) }; var uiUser = new UserViewModel(); Mapper.Map(source: dbUser1, destination: uiUser);
نوشتن تبدیلگرهای غیر سراسری
همانطور که عنوان شد، معرفی تبدیلگرها به AutoMapper سراسری است و در کل برنامه اعمال میشود. اگر نیاز است فقط برای یک مدل خاص و یک خاصیت خاص آن تبدیلگر نوشته شود، باید نگاشت مورد نظر را به صورت ذیل تعریف کرد:
this.CreateMap<User, UserViewModel>() .ForMember(userViewModel => userViewModel.RegistrationDate, opt => opt.ResolveUsing(src => { var dt = src.RegistrationDate; return dt.ToShortDateString(); }));
خصوصی سازی تبدیلگرها با تدارک موتورهای نگاشت اختصاصی
اگر میخواهید تنظیمات TestProfile1 به کل برنامه اعمال نشود، نیاز است یک MappingEngine جدید و مجزای از MappingEngine سراسری AutoMapper را ایجاد کرد:
var configurationStore = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers); configurationStore.AddProfile<TestProfile1>(); var mapper = new MappingEngine(configurationStore); mapper.Map(source: dbUser1, destination: uiUser);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
AM_Sample02.zip
For .NET Core 3.0, we’re shipping a brand new namespace called System.Text.Json with support for a reader/writer, a document object model (DOM), and a serializer. In this blog post, I’m telling you why we built it, how it works, and how you can try it.
آشنایی با Jaeger
برای پیاده سازی distributed tracing، میتوانیم از ابزار متن باز و محبوب Jaeger (با تلفظ یِگِر) که ابتدا توسط شرکت Uber منتشر شد، استفاده کنیم. نحوه کارکرد Jaeger بصورت زیر میباشد:
سادهترین روش برای راهاندازی Jager، استفاده از داکر ایمیج All in one که شامل ماژول های agent ، collector، query و ui است. پورت 6831 مربوط به agent و پورت 16686 مربوط به ui میباشد. برای جزئیات مربوط به ماژولهای مختلف از این لینک استفاده کنید.
docker run -d -p 6831:6831/udp -p 6832:6832/udp -p 14268:14268 -p 14250:14250 -p 16686:16686 -p 5778:5778 --name jaeger jaegertracing/all-in-one:latest
بعد از اجرای دستور بالا، اطلاعات مربوط به سرویسها و trace ها در ماژول Jager UI با آدرس http://localhost:16686 قابل مشاهده است.
جهت استفاده از Jaeger از پروژه تستی که شامل دو سرویس User و Gateway میباشد، استفاده میکنیم. در سرویس User، متد AddUser در صورت عدم وجود کاربر در دیتابیس، اطلاعات کاربر از گیتهاب را دریافت و در دیتابیس ذخیره میکند. سرویس Gateway از Ocelot برای مسیردهی درخواستها استفاده میکند. برای آشنایی با ocelot این پست را مطالعه نمایید.
public async Task<ApiResult<Models.User>> AddUserAsync(string username) { var result = new ApiResult<Models.User>(); var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Login == username); if (user is null) { try { var url = string.Format(_appConfig.Github.ProfileUrl, username); var apiResult = await _httpClient.GetStringAsync(url); var userDto = JsonSerializer.Deserialize<UserDto>(apiResult); user = _mapper.Map<Models.User>(userDto); await _applicationDbContext.Users.AddAsync(user); await _applicationDbContext.SaveChangesAsync(); result.Result = user; result.Message = "User successfully Created"; return result; } catch (Exception e) { result.Message = "User not found"; return result; } } result.Message = "User already exist"; result.Result = user; return result; }
برای ثبت Trace مربوط به درخواستها در Jaeger ، بعد از نصب پکیجهای Jaeger و OpenTracing.Contrib.NetCore در هر دو سرویس، در کانفیگ هریک از سرویسها مورد زیر را اضافه میکنیم:
"JaegerConfig": { "Host": "localhost", "Port": 6831, "IsEnabled": true, "SamplingRate": 0.5 }
و برای اضافه شدن tracer به برنامه از متد الحاقی زیر استفاده میکنیم:
public static class Extensions { public static void AddJaeger(this IServiceCollection services, IConfiguration configuration) { var config = configuration.GetSection("JaegerConfig").Get<JaegerConfig>(); if (!(config?.IsEnabled ?? false)) return; if (string.IsNullOrEmpty(config?.Host)) throw new Exception("invalid JaegerConfig"); services.AddSingleton<ITracer>(serviceProvider => { string serviceName = Assembly.GetEntryAssembly()?.GetName().Name; ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>(); var sampler = new ProbabilisticSampler(config.SamplingRate); var reporter = new RemoteReporter.Builder() .WithLoggerFactory(loggerFactory) .WithSender(new UdpSender(config.Host, config.Port, 0)) .WithFlushInterval(TimeSpan.FromSeconds(15)) .WithMaxQueueSize(300) .Build(); ITracer tracer = new Tracer.Builder(serviceName) .WithLoggerFactory(loggerFactory) .WithSampler(sampler) .WithReporter(reporter) .Build(); GlobalTracer.Register(tracer); return tracer; }); services.AddOpenTracing(); } }
برای ثبت traceها استراتژیهای متفاوتی وجود دارد. در اینجا از ProbabilisticSampler استفاده شدهاست که در سازندهی آن میتوان درصد ثبت Traceها را مقدار دهی کرد. در نهایت این متد الحاقی را در Startup اضافه میکنیم:
builder.Services.AddJaeger(builder.Configuration);
بعد از اجرای پروژه و فراخوانی https://localhost:6000/gateway/Users/Add ، سرویس Gateway، درخواست را به سرویس User ارسال میکند و این سرویسها در Jaeger UI قابل مشاهده هستند.
جهت مشاهده trace ها ، سرویس مورد نظر را انتخاب و روی Find Traces کلیک کنید. با کلیک روی Trace مورد نظر، جزئیات فعالیت هایی مثل فراخوانی سرویس و مراجعه به دیتابیس قابل مشاهده است.
برای اضافه کردن لاگ سفارشی به یک span، میتوان از اینترفیس ITracer استفاده کرد:
private readonly IUserService _userService; private readonly ITracer _tracer; public UsersController(IUserService userService, ITracer tracer) { _userService = userService; _tracer = tracer; }
[HttpPost] public async Task<ActionResult> AddUser(AddUserDto model) { var actionName = ControllerContext.ActionDescriptor.DisplayName; using var scope = _tracer.BuildSpan(actionName).StartActive(true); scope.Span.Log($"Add user log username: {model.Username}"); return Ok(await _userService.AddUserAsync(model.Username)); }
کدهای مربوط به این مطلب در اینجا قابل دسترسی است.
کدهای کامل کامپوننت Pages\LearnBlazor\Lifecycle.razor
@page "/lifecycle" @using System.Threading <div class="border"> <h3>Lifecycles Parent Component</h3> <div class="border"> <LifecycleChild CountValue="CurrentCount"></LifecycleChild> </div> <p>Current count: @CurrentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> <br /><br /> <button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount </div> @code { int CurrentCount = 0; int MaxCount = 5; private void IncrementCount() { CurrentCount++; Console.WriteLine("Parnet - IncrementCount is called"); } protected override void OnInitialized() { Console.WriteLine("Parnet - OnInitialized is called"); } protected override async Task OnInitializedAsync() { await Task.Delay(100); Console.WriteLine("Parnet - OnInitializedAsync is called"); } protected override void OnParametersSet() { Console.WriteLine("Parnet - OnParameterSet is called"); } protected override async Task OnParametersSetAsync() { await Task.Delay(100); Console.WriteLine("Parnet - OnParametersSetAsync is called"); } protected override void OnAfterRender(bool firstRender) { if (firstRender) { Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called"); CurrentCount = 111; } else { CurrentCount = 999; Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called"); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await Task.Delay(100); Console.WriteLine("Parnet - OnAfterRenderAsync is called"); } protected override bool ShouldRender() { Console.WriteLine("Parnet - ShouldRender is called"); return true; } void StartCountdown() { Console.WriteLine("Parnet - StartCountdown()"); var timer = new Timer(TimeCallBack, null, 1000, 1000); } void TimeCallBack(object state) { if (MaxCount > 0) { MaxCount--; Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)"); InvokeAsync(StateHasChanged); } } }
و کدهای کامل کامپوننت Pages\LearnBlazor\LearnBlazorComponents\LifecycleChild.razor
<h3 class="ml-3 mr-3">Lifecycles Child Componenet</h3> @code { [Parameter] public int CountValue { get; set; } protected override void OnInitialized() { Console.WriteLine(" Child - OnInitialized is called"); } protected override async Task OnInitializedAsync() { await Task.Delay(100); Console.WriteLine(" Child - OnInitializedAsync is called"); } protected override void OnParametersSet() { Console.WriteLine(" Child - OnParameterSet is called"); } protected override async Task OnParametersSetAsync() { await Task.Delay(100); Console.WriteLine(" Child - OnParametersSetAsync is called"); } protected override void OnAfterRender(bool firstRender) { if (firstRender) { Console.WriteLine(" Child - OnAfterRender(firstRender == true) is called"); } else { Console.WriteLine(" Child - OnAfterRender(firstRender == false) is called"); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await Task.Delay(100); Console.WriteLine(" Child - OnAfterRenderAsync is called"); } protected override bool ShouldRender() { Console.WriteLine(" Child - ShouldRender is called"); return true; } }
<li class="nav-item px-3"> <NavLink class="nav-link" href="lifecycle"> <span class="oi oi-list-rich" aria-hidden="true"></span> Lifecycles </NavLink> </li>
رویدادهای OnInitialized و OnInitializedAsync
@code { protected override void OnInitialized() { Console.WriteLine("Parnet - OnInitialized is called"); } protected override async Task OnInitializedAsync() { await Task.Delay(100); Console.WriteLine("Parnet - OnInitializedAsync is called"); }
در کامپوننت Lifecycle.razor، یک کامپوننت دیگر نیز به نام LifecycleChild.razor فراخوانی شدهاست. در این حالت ابتدا OnInitialized کامپوننت والد فراخوانی شدهاست و پس از آن بلافاصله فراخوانی OnInitialized کامپوننت فرزند را مشاهده میکنیم.
رویدادهای OnParametersSet و OnParametersSetAsync
این رویدادها یکبار در زمان بارگذاری اولیهی کامپوننت و بار دیگر هر زمانیکه کامپوننت فرزند، پارامتر جدیدی را از طریق کامپوننت والد دریافت میکند، فراخوانی میشوند. برای نمونه کامپوننت LifecycleChild، پارامتر CurrentCount را از والد خود دریافت میکند:
<LifecycleChild CountValue="CurrentCount"></LifecycleChild>
Parnet - IncrementCount is called Parnet - ShouldRender is called Child - OnParameterSet is called Child - ShouldRender is called Parnet - OnAfterRender(firstRender == false) is called Child - OnAfterRender(firstRender == false) is called Child - OnParametersSetAsync is called Child - ShouldRender is called Child - OnAfterRender(firstRender == false) is called Child - OnAfterRenderAsync is called Parnet - OnAfterRenderAsync is called Child - OnAfterRenderAsync is called
رویدادهای OnAfterRender و OnAfterRenderAsync
پس از هر بار رندر کامپوننت، این متدها فراخوانی میشوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آنها به پایان رسیدهاست. یکی از کاربردهای آن، آغاز کامپوننتهای جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.
یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمیشوند؛ چون در مرحلهی آخر رندر UI قرار دارند.
@code { protected override void OnAfterRender(bool firstRender) { if (firstRender) { Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called"); CurrentCount = 111; } else { CurrentCount = 999; Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called"); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await Task.Delay(100); Console.WriteLine("Parnet - OnAfterRenderAsync is called"); } }
سؤال: با توجه به مقدار دهیهای 111 و 999 صورت گرفتهی در متد OnAfterRender، در اولین بار نمایش کامپوننت، چه عددی به عنوان CurrentCount نمایش داده میشود؟
در اولین بار نمایش صفحه، لحظهای عدد 111 و سپس عدد 999 نمایش داده میشود. عدد 111 را در بار اول رندر و عدد 999 را در بار دوم رندر که پس از مقدار دهی پارامتر کامپوننت فرزند است، میتوان مشاهده کرد.
اما ... اگر پس از نمایش اولیهی صفحه، چندین بار بر روی دکمهی click me کلیک کنیم، همواره عدد 1000 مشاهده میشود. علت اینجا است که تغییرات مقادیر فیلدها در متد OnAfterRender، به UI اعمال نمیشوند؛ چون در این مرحله، رندر UI به پایان رسیدهاست. در اینجا فقط مقدار فیلد CurrentCount به 999 تغییر میکند و به همین صورت باقی میماند. دفعهی بعدی که بر روی دکمهی click me کلیک میکنیم، یک واحد به آن اضافه شده و اکنون است که کار رندر UI، مجددا شروع خواهد شد (در واکشن به یک رخداد و فراخوانی ضمنی StateHasChanged در پشت صحنه) و اینبار حاصل 999+1 را در UI مشاهده میکنیم و باز هم در پایان کار رندر، مجددا مقدار CurrentCount به 999 تغییر میکند که ... دیگر به UI منعکس نمیشود تا زمان کلیک بعدی و همینطور الی آخر.
رویدادهای StateHasChanged و ShouldRender
- اگر خروجی رویداد ShouldRender مساوی true باشد، اجازهی اعمال تغییرات به UI داده خواهد شد و برعکس. بنابراین اگر حالت UI تغییر کند و خروجی این متد false باشد، این تغییرات نمایش داده نخواهند شد.
- اگر رویداد StateHasChanged فراخوانی شود، به معنای درخواست رندر مجدد UI است. کاربرد آن در مکانهایی است که نیاز به اطلاع رسانی دستی تغییرات UI وجود دارد؛ درست پس از زمانیکه رندر UI به پایان رسیدهاست. برای آزمایش این مورد و فراخوانی دستی StateHasChanged، کدهای تایمر زیر تهیه شدهاند:
@page "/lifecycle" @using System.Threading button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount @code { int MaxCount = 5; void StartCountdown() { Console.WriteLine("Parnet - StartCountdown()"); var timer = new Timer(TimeCallBack, null, 1000, 1000); } void TimeCallBack(object state) { if (MaxCount > 0) { MaxCount--; Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)"); InvokeAsync(StateHasChanged); } } }
یک نکته: متدهای رویدادگردان در Blazor، میتوانند sync و یا async باشند؛ مانند متدهای OnClick و OnClickAsync زیر که هر دو پس از پایان متدها، سبب فراخوانی ضمنی StateHasChanged نیز میشوند (به این دلیل است که با کلیک بر روی دکمهای، UI هم به روز رسانی میشود). البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged میشوند؛ یکبار زمانیکه قسمت sync متد به پایان میرسد و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید:
<button @onclick="OnClick">Synchronous</button> <button @onclick="OnClickAsync">Asynchronous</button> @code{ void OnClick() { } // StateHasChanged is called after the method async Task OnClickAsync() { text = "click1"; // StateHasChanged is called here as the synchronous part of the method ends await Task.Delay(1000); await Task.Delay(2000); text = "click2"; } // StateHasChanged is called after the method }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-06.zip
معرفی موجودیت Person
در مثال این مطلب قصد داریم، معادل توابع بومی مخصوص SQL Server را که امکان کار با DateTime را مهیا میکنند، در EF Core تعریف کنیم. به همین جهت نیاز به موجودیتی داریم که دارای خاصیتی از این نوع باشد:
using System; namespace EFCoreDbFunctionsSample.Entities { public class Person { public int Id { get; set; } public string Name { get; set; } public DateTime AddDate { get; set; } } }
گزارشگیری بر اساس تعداد روز گذشتهی از ثبت نام
اکنون فرض کنید میخواهیم گزارشی را از تمام کاربرانی که در طی 10 روز قبل ثبت نام کردهاند، تهیه کنیم. اگر کوئری زیر را برای این منظور تهیه کنیم:
var usersInfo = context.People.Where(person => (DateTime.Now - person.AddDate).Days <= 10).ToList();
'The LINQ expression 'DbSet<Person>.Where(p => (DateTime.Now - p.AddDate).Days <= 10)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
SELECT [p].[Id], [p].[AddDate], [p].[Name] FROM [People] AS [p] WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= 10
روش تعریف تابع DATEDIFF سفارشی در EF Core
برای تعریف متد DateDiff مخصوص EF Core، ابتدا باید یک کلاس static را تعریف کرد و سپس تنها امضای این متد را، معادل امضای تابع توکار SQL Server تعریف کرد. این متد نیازی نیست تا پیاده سازی را داشته باشد. به همین جهت بدنهی آنرا صرفا با یک throw new InvalidOperationException مقدار دهی میکنیم. هدف از این متد، استفادهی از آن در LINQ Expressions است و قرار نیست به صورت مستقیمی بکار گرفته شود:
namespace EFCoreDbFunctionsSample.DataLayer { public enum SqlDateDiff { Year, Quarter, Month, DayOfYear, Day, Week, Hour, Minute, Second, MilliSecond, MicroSecond, NanoSecond } public static class SqlDbFunctionsExtensions { public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) => throw new InvalidOperationException($"{nameof(SqlDateDiff)} method cannot be called from the client side."); public static readonly MethodInfo SqlDateDiffMethodInfo = typeof(SqlDbFunctionsExtensions) .GetRuntimeMethod( nameof(SqlDbFunctionsExtensions.SqlDateDiff), new[] { typeof(SqlDateDiff), typeof(DateTime), typeof(DateTime) } ); } }
روش معرفی تابع DATEDIFF سفارشی به EF Core
پس از تعریف امضای متد معادل DateDiff، اکنون نوبت به معرفی آن به EF Core است:
namespace EFCoreDbFunctionsSample.DataLayer { public class ApplicationDbContext : DbContext { // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasDbFunction(SqlDbFunctionsExtensions.SqlDateDiffMethodInfo) .HasTranslation(args => { var parameters = args.ToArray(); var param0 = ((SqlConstantExpression)parameters[0]).Value.ToString(); return SqlFunctionExpression.Create("DATEDIFF", new[] { new SqlFragmentExpression(param0), // It should be written as DateDiff(day, ...) and not DateDiff(N'day', ...) . parameters[1], parameters[2] }, SqlDbFunctionsExtensions.SqlDateDiffMethodInfo.ReturnType, typeMapping: null); }); } } }
سپس توسط متد HasTranslation، مشخص میکنیم که این متد به چه نحوی قرار است به یک عبارت SQL ترجمه شود. پارامتر args ای که در اینجا در اختیار ما قرار میگیرد، دقیقا همان پارامترهای متد public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) هستند که در این مثال خاص، شامل سه پارامتر میشوند. پارامترهای دوم و سوم آنرا به همان نحوی که دریافت میکنیم، به SqlFunctionExpression.Create ارسال خواهیم کرد. اما پارامتر اول را از نوع enum تعریف کردهایم و همچنین قرار نیست به صورت 'N'day و رشتهای به سمت بانک اطلاعاتی ارسال شود، بلکه باید به همان نحو اصلی آن (یعنی day)، در کوئری نهایی درج گردد، به همین جهت ابتدا Value آنرا استخراج کرده و سپس توسط SqlFragmentExpression عنوان میکنیم آنرا باید به همین نحو درج کرد.
پارامتر اول متد SqlFunctionExpression.Create، باید دقیقا معادل نام متد توکار مدنظر باشد. پارامتر دوم آن، لیست پارامترهای این تابع است. پارامتر سوم آن، نوع خروجی این تابع است که از طریق MethodInfo معادل، قابل استخراج است.
استفادهی از DbFunction سفارشی جدید در برنامه
پس از این تعاریف و معرفیها، اکنون میتوان متد سفارشی SqlDateDiff تهیه شده را به صورت مستقیمی در کوئریهای LINQ استفاده کرد تا قابلیت ترجمهی به SQL را پیدا کنند:
var sinceDays = 10; users = context.People.Where(person => SqlDbFunctionsExtensions.SqlDateDiff(SqlDateDiff.Day, person.AddDate, DateTime.Now) <= sinceDays).ToList(); /* SELECT [p].[Id], [p].[AddDate], [p].[Name] FROM [People] AS [p] WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= @__sinceDays_0 */
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: EFCoreDbFunctionsSample.zip
این کدها به همراه چند تابع سفارشی دیگر نیز هستند.
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(User applicationUser) { // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType var userIdentity = await CreateIdentityAsync(applicationUser, DefaultAuthenticationTypes.ApplicationCookie); // Add custom user claims here userIdentity.AddClaim(new Claim("emailaddress", applicationUser.Email)); return userIdentity; }
public virtual async Task<ActionResult> Login(LoginViewModel viewModel, string returnUrl) { // ... more code var user = await this._userManager.FindByNameAsync(viewModel.UserName); await this._userManager.GenerateUserIdentityAsync(user); return this.View(viewModel); }
_.For<IPrincipal>().Use(() => HttpContext.Current.User);
var useremail = this._principal.GetClaimValue("emailaddress");
public class PostDto : BaseDto<PostDto, Post, long> { public string Title { get; set; } public string Text { get; set; } public int CategoryId { get; set; } public string CategoryName { get; set; } //=> Category.Name }
- کلاس PostDto خودش را به عنوان اولین پارامتر جنریک BaseDto معرفی میکند.
- به عنوان پارامتر دوم، باید کلاس Entity ایی که قرار است به آن نگاشت شود (Post) را معرفی کنیم.
- پارامتر سوم، نوع فیلد Id است که در اینجا خاصیت Id کلاسهای Post و PostDto ما، از نوع long است.
- نهایتا خواصی را که برای نگاشت لازم داریم، تعریف میکنیم مثل Title و...
- همچنین میتوانیم خواصی برای نگاشت با خواص Navigation Propertyهای Post هم تعریف کنیم؛ مانند CategoryName که به خاصیت Name از Category پست مربوطه اشاره میکند و AutoMapper به صورت هوشمندانه آنها را به هم نگاشت میکند.
public abstract class BaseDto<TDto, TEntity, TKey> where TDto : class, new() where TEntity : BaseEntity<TKey>, new() { [Display(Name = "ردیف")] public TKey Id { get; set; } public TEntity ToEntity() { return Mapper.Map<TEntity>(CastToDerivedClass(this)); } public TEntity ToEntity(TEntity entity) { return Mapper.Map(CastToDerivedClass(this), entity); } public static TDto FromEntity(TEntity model) { return Mapper.Map<TDto>(model); } protected TDto CastToDerivedClass(BaseDto<TDto, TEntity, TKey> baseInstance) { return Mapper.Map<TDto>(baseInstance); } }
- نوع TDto به کلاس Dto ما اشاره میکند؛ مثلا PostDto
- نوع TEntity به کلاس Entity ما اشاره میکند؛ مثلا Post
- نوع TKey به نوع خاصیت Id اشاره میکند.
- شرط لازم برای نوع TEntity این است که از <BaseEntity<TKey ارث بری کرده باشد (نوع پایهای که تمام Entityهای ما از آن ارث بری میکنند).
- متدهای کمکی ToEntity و FromEntity، کار نگاشت اشیاء را برای ما راحتتر میکنند.
public abstract class BaseEntity<TKey> { public TKey Id { get; set; } } public class Post : BaseEntity<long> { public string Title { get; set; } public string Text { get; set; } public int CatgeoryId { get; set; } public Category Category { get; set; } }
var postDto = new PostDto(); var post = postDto.ToEntity();
var post = // finded by id var updatePost = postDto.ToEntity(post);
var postDto = PostDto.FromEntity(post);
public static class AutoMapperConfiguration { public static void InitializeAutoMapper() { Mapper.Initialize(configuration => { configuration.ConfigureAutoMapperForDto(); }); //Compile mapping after configuration to boost map speed Mapper.Configuration.CompileMappings(); } public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config) { config.ConfigureAutoMapperForDto(Assembly.GetEntryAssembly()); } public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config, params Assembly[] assemblies) { var dtoTypes = GetDtoTypes(assemblies); var mappingTypes = dtoTypes .Select(type => { var arguments = type.BaseType.GetGenericArguments(); return new { DtoType = arguments[0], EntityType = arguments[1] }; }).ToList(); foreach (var mappingType in mappingTypes) config.CreateMappingAndIgnoreUnmappedProperties(mappingType.EntityType, mappingType.DtoType); } public static void CreateMappingAndIgnoreUnmappedProperties(this IMapperConfigurationExpression config, Type entityType, Type dtoType) { var mappingExpression = config.CreateMap(entityType, dtoType).ReverseMap(); //Ignore mapping to any property of source (like Post.Categroy) that dose not contains in destination (like PostDto) //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value foreach (var property in entityType.GetProperties()) { if (dtoType.GetProperty(property.Name) == null) mappingExpression.ForMember(property.Name, opt => opt.Ignore()); } } public static IEnumerable<Type> GetDtoTypes(params Assembly[] assemblies) { var allTypes = assemblies.SelectMany(a => a.ExportedTypes); var dtoTypes = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.BaseType != null && type.BaseType.IsGenericType && (type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,>) || type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,,>))); return dtoTypes; } }
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; AutoMapperConfiguration.InitializeAutoMapper(); }
public static IEnumerable<Type> GetDtoTypes(params Assembly[] assemblies) { var allTypes = assemblies.SelectMany(a => a.ExportedTypes); var dtoTypes = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.BaseType != null && type.BaseType.IsGenericType && (type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,>) || type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,,>))); return dtoTypes; }
- در خط اول ابتدا تمامی نوعهای قابل دسترس از بیرون (ExportedTypes) از assemblyهای دریافتی واکشی میشود.
- سپس توسط Where، نوعهایی که کلاس بوده، abstract نیستند و از BaseDto ارث بری کردهاند، فیلتر شده و بازگردانده میشوند.
public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config, params Assembly[] assemblies) { var dtoTypes = GetDtoTypes(assemblies); var mappingTypes = dtoTypes .Select(type => { var arguments = type.BaseType.GetGenericArguments(); return new { DtoType = arguments[0], EntityType = arguments[1] }; }).ToList(); foreach (var mappingType in mappingTypes) config.CreateMappingAndIgnoreUnmappedProperties(mappingType.EntityType, mappingType.DtoType); }
public static void CreateMappingAndIgnoreUnmappedProperties(this IMapperConfigurationExpression config, Type entityType, Type dtoType) { var mappingExpression = config.CreateMap(entityType, dtoType).ReverseMap(); //Ignore mapping to any property of entity (like Post.Categroy) that dose not contains in dto (like PostDto.CategoryName) //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value foreach (var property in entityType.GetProperties()) { if (dtoType.GetProperty(property.Name) == null) mappingExpression.ForMember(property.Name, opt => opt.Ignore()); } }
«حالت» یا state، شیءای است، حاوی اطلاعاتی که برنامه با آن سر و کار دارد. بنابراین مدیریت حالت، روشی است برای ردیابی و مدیریت دادههای مورد استفادهی در برنامه و تقریبا تمام برنامهها، به نحوی به آن نیاز دارند. هر کامپوننت در Blazor، دارای state خاص خودش است و این state از سایر کامپوننتها کاملا مستقل و ایزولهاست. این مورد با بزرگتر شدن برنامه و برقراری ارتباط بین کامپوننتها، مشکل ایجاد میکند. برای مثال اگر قرار است در منوی بالای سایت، تعداد محصولات موجود در سبد خرید یک شخص را نمایش دهیم، این تعداد، حاصل تعامل او با چندین کامپوننت مجزا خواهد بود که اینها الزاما در یک سلسه مراتب هم قرار نمیگیرند و به سادگی نمیتوان اطلاعات را به صورت آبشاری در بین آنها به اشتراک گذاشت. به همین جهت نیاز به روشی برای مدیریت حالت و به اشتراک گذاری آن در بین کامپوننتهای مختلف برنامه وجود دارد و خوشبختانه چون Blazor به همراه یک سیستم تزریق وابستگیهای توکار است، پیاده سازی یک چنین مدیریت کنندهای، سادهاست.
استفاده از الگوی Observer جهت مدیریت حالت برنامههای Blazor
زمانیکه همانند تصویر فوق با یک کامپوننت کار میکنیم، کاربر همواره کارش از تعامل با یک View آغاز میشود. این تعامل سبب صدور رخدادهایی میشود که این رخدادها، حالت و state کامپوننت را تغییر میدهند. تغییر حالت کامپوننت نیز بلافاصله سبب بهروز رسانی View میشود. در این مثال، حالت کامپوننت، داخل همان کامپوننت نگهداری میشود؛ مانند فیلدهایی که در قسمت code@ یک کامپوننت Blazor تعریف میکنیم و محدود به همان کامپوننت هستند.
با بزرگتر شدن برنامه، زمانی خواهد رسید که نیاز است حالت یک کامپوننت را با کامپوننتهای دیگر به اشتراک گذاشت. در این حالت باید این state را از داخل کامپوننت مدنظر استخراج کرد و در جائی دیگر قرار داد که عموما به آن state store گفته میشود:
در تصویر فوق، در بالای آن یک state store را داریم که محل نگهداری و ذخیره سازی حالت اشتراکی بین کامپوننتها است. سپس برای نمونه دو کامپوننت دیگر را داریم که رابطهی بین آنها، همان رابطهی مثلثی است که در تصویر اول این مطلب مشاهده کردیم. برای مثال در اثر تعامل کاربری با View کامپوننت 1، رخدادی صادر خواهد شد. مدیریت این رخداد، سبب تغییر state خواهد شد، اما اینبار این state دیگر داخل کامپوننت 1 قرار ندارد؛ بلکه داخل state store است و این store پس از آگاه شدن از تغییر وضعیت خود، دو کامپوننتی را که از آن تغدیه میکنند، جهت به روز رسانی Viewهایشان، مطلع میکند. همین چرخه در مورد کامپوننت 2 نیز برقرار است. اگر تعاملی با آن صورت گیرد، در نهایت اثر آن به هر دو کامپوننت متصل به state store اشتراکی، اطلاع رسانی میشود تا Viewهای هر دوی آنها به روز رسانی شوند. الگویی را که در اینجا مشاهده میکنید، در اصل یک الگوی Observer است:
در الگوی مشاهدهگر، یک Subject را داریم که تعداد زیادی Observer، مشترک آن هستند. در این مثال ما، Subject، همان State Store است و Observerها دقیقا همان کامپوننتهای مشترک به آن. Observerها به تغییرات Subject گوش فرا داده و بلافاصله بر اساس آن واکنش مناسبی را نشان میدهند.
پیاده سازی الگوی Observer جهت مدیریت حالت برنامههای Blazor
زمانیکه یک برنامهی متداول Blazor را توسط قالب پیشفرض آن ایجاد میکنیم، به همراه یک کامپوننت Counter است:
@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++; } }
بنابراین در قدم اول نیاز به یک State Store اشتراکی را داریم که بتوانیم توسط آن، مقدار جاری currentCount را ذخیره کرده و سپس تغییرات آنرا جهت به روز رسانی دو View (در کامپوننتهای Counter و NavMenu)، به مشترکین آن اطلاع رسانی کنیم. به همین جهت ابتدا پوشهی جدید Stores را در ریشهی پروژهی Blazor ایجاد میکنیم. نام این پوشه، از این جهت یک اسم جمع است که یک برنامه بنابر نیاز خودش میتواند چندین State Store را داشته باشد. سپس داخل این پوشه، پوشهی دیگری را به نام CounterStore، ایجاد میکنیم.
در اینجا در ابتدا شیء حالت مدنظر را ایجاد میکنیم که برای نمونه بر اساس نیاز برنامه و این مثال، از مقدار نهایی کلیک بر روی دکمهی شمارشگر تشکیل میشود:
namespace BlazorStateManagement.Stores.CounterStore { public class CounterState { public int Count { get; set; } } }
using System; namespace BlazorStateManagement.Stores.CounterStore { public interface ICounterStore { void DecrementCount(); void IncrementCount(); CounterState GetState(); void AddStateChangeListener(Action listener); void BroadcastStateChange(); void RemoveStateChangeListener(Action listener); } }
using System; namespace BlazorStateManagement.Stores.CounterStore { public class CounterStore : ICounterStore { private readonly CounterState _state = new(); private Action _listeners; public CounterState GetState() { return _state; } public void IncrementCount() { _state.Count++; BroadcastStateChange(); } public void DecrementCount() { _state.Count--; BroadcastStateChange(); } public void AddStateChangeListener(Action listener) { _listeners += listener; } public void RemoveStateChangeListener(Action listener) { _listeners -= listener; } public void BroadcastStateChange() { _listeners.Invoke(); } } }
- مخزن حالت پیاده سازی شدهی بر اساس الگوی مشاهدهگر، نیاز دارد تا بتواند لیست مشاهدهگرها را ثبت کند. به همین جهت به همراه متدهای AddStateChangeListener جهت ثبت یک مشاهدهگر جدید و RemoveStateChangeListener، جهت حذف مشاهدهگری از لیست موجود است.
- همچنین الگوی مشاهدهگر باید بتواند تغییرات صورت گرفتهی در حالتی را که نگهداری میکند (CounterState در اینجا)، به مشترکین خود اطلاع رسانی کند. اینکار را توسط متد BroadcastStateChange انجام میدهد. هر زمانیکه این متد فراخوانی شود، Actionهایی که به صورت پارامتر به متد AddStateChangeListener ارسال شدهاند، به صورت خودکار اجرا خواهند شد. این کار سبب میشود تا بتوان منطق خاصی را مانند به روز رسانی UI، در سمت کامپوننتهای مشترک به این مخزن، پیاده سازی کرد.
- در اینجا همچنین متدهایی برای افزایش و کاهش مقدار Count را نیز به همراه اطلاع رسانی به مشترکین، مشاهده میکنید.
پس از این تعریف نیاز است سرویس Store ایجاد شده را به برنامه معرفی کرد:
namespace BlazorStateManagement.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); //... builder.Services.AddScoped<ICounterStore, CounterStore>(); //... } } }
تغییر کامپوننتهای برنامه برای استفاده از سرویس ICounterStore
پس از معرفی سرویس ICounterStore به سیستم تزریق وابستگیهای برنامه، جهت سهولت استفادهی از آن، در ابتدا فضای نام آنرا به فایل سراسری Client\_Imports.razor اضافه میکنیم:
@using BlazorStateManagement.Stores.CounterStore
@page "/counter" @implements IDisposable @inject ICounterStore CounterStore <h1>Counter</h1> <p>Current count: @CounterStore.GetState().Count</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { protected override void OnInitialized() { base.OnInitialized(); CounterStore.AddStateChangeListener(UpdateView); } private void IncrementCount() { CounterStore.IncrementCount(); } private void UpdateView() { StateHasChanged(); } public void Dispose() { CounterStore.RemoveStateChangeListener(UpdateView); } }
- در اینجا در ابتدا سرویس ICounterStore، به کامپوننت تزریق شدهاست.
- سپس در متد رویدادگران آغازین OnInitialized، با استفاده از متد AddStateChangeListener، مشترک سرویس مخزن حالت شمارشگر شدهایم.
- همواره جهت پاکسازی کد و عدم اشتراک بیش از اندازهی به یک مخزن حالت، نیاز است در پایان کار یک کامپوننت، با پیاده سازی implements IDisposable@، کار حذف اشتراک را انجام دهیم. در غیراینصورت هربار که کامپوننت بارگذاری میشود، یک اشتراک جدید از این کامپوننت، به مخزن حالتی که طول عمر Singleton دارد، اضافه خواهد شد که نشانی از نشتی حافظهاست.
- دو قسمت دیگر را هم تغییر دادهایم. اینبار با استفاده از متد ()GetState، این Count اشتراکی را نمایش میدهیم و همچنین عمل به روز رسانی State را هم توسط متد IncrementCount انجام دادهایم.
در ادامه کامپوننت Client\Shared\NavMenu.razor را نیز جهت نمایش مقدار جاری Count، به صورت زیر به روز رسانی میکنیم:
@inject ICounterStore CounterStore <li class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> Counter: @CounterStore.GetState().Count </NavLink> </li> @code { protected override void OnInitialized() { base.OnInitialized(); CounterStore.AddStateChangeListener(() => StateHasChanged()); } // ... }
- در اینجا نیز در ابتدا سرویس ICounterStore، به کامپوننت تزریق شدهاست.
- سپس در متد رویدادگران آغازین OnInitialized، با استفاده از متد AddStateChangeListener، مشترک سرویس مخزن حالت شمارشگر شدهایم و هربار که متد BroadcastStateChange ای توسط یکی از کامپوننتهای متصل به مخزن حالت فراخوانی میشود (برای مثال در انتهای متد IncrementCount خود سرویس)، سبب اجرای Action آن که در اینجا StateHasChanged است، خواهد شد. فراخوانی StateHasChanged، کار اطلاع رسانی به UI، جهت رندر مجدد را انجام میدهد. به این ترتیب مقدار جدید Count توسط CounterStore.GetState().Count@ در منو نیز ظاهر خواهد شد:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorStateManagement.zip
Blazor 5x - قسمت 19 - کار با فرمها - بخش 7 - نکات ویژهی کار با EF-Core در برنامههای Blazor Server
طول عمر سرویسها، در برنامههای Blazor Server متفاوت هستند
هنگامیکه با یک ASP.NET Core Web API متداول کار میکنیم، درخواستهای HTTP رسیده، از میانافزارهای موجود رد شده و پردازش میشوند. اما هنگامیکه با Blazor Server کار میکنیم، به علت وجود یک اتصال دائم SignalR که عموما از نوع Web socket است، دیگر درخواست HTTP وجود ندارد. تمام رفت و برگشتهای برنامه به سرور و پاسخهای دریافتی، از طریق Web socket منتقل میشوند و نه درخواستها و پاسخهای متداول HTTP.
این روش پردازشی، اولین تاثیری را که بر روی رفتار یک برنامه میگذارد، تغییر طول عمر سرویسهای آن است. برای مثال در برنامههای Web API، طول عمر درخواستها، از نوع Scoped هستند و با شروع پردازش یک درخواست، سرویسهای مورد نیاز وهله سازی شده و در پایان درخواست، رها میشوند.
این مساله در حین کار با EF-Core نیز بسیار مهم است؛ از این جهت که در برنامههای Web API نیز EF-Core و DbContext آن، به صورت سرویسهایی با طول عمر Scoped تعریف میشوند. برای مثال زمانیکه یک چنین تعریفی را در برنامه داریم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
public static IServiceCollection AddDbContext<TContext>( [NotNullAttribute] this IServiceCollection serviceCollection, [CanBeNullAttribute] Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext;
اما زمانیکه مانند یک برنامهی مبتنی بر Blazor Server، دیگر HTTP Requests متداولی را نداریم، چطور؟ در این حالت زمانیکه یک اتصال SignalR برقرار شد، وهلهای از DbContext که در اختیار برنامهی Blazor Server قرار میگیرد، تا زمانیکه کاربر این اتصال را به نحوی قطع نکرده (مانند بستن کامل مرورگر و یا ریفرش صفحه)، ثابت باقی خواهد ماند. یعنی به ازای هر اتصال SignalR، طول عمر ServiceLifetime.Scoped پیشفرض تعریف شده، همانند یک وهلهی با طول عمر Singleton عمل میکند. در این حالت تمام صفحات و کامپوننتهای یک برنامهی Blazor Server، از یک تک وهلهی مشخص DbContext که در ابتدای کار دریافت کردهاند، کار میکنند و از آنجائیکه DbContext به صورت thread-safe کار نمیکند، این تک وهله مشکلات زیادی را ایجاد خواهد کرد که یک نمونه از آنرا در عمل، در پایان قسمت قبل مشاهده کردید:
«اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم و یا حتی اگر کاربر شروع کند به کلیک کردن سریع در قسمتهای مختلف برنامه، باز هم این خطا مشاهده میشود:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'. System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
هر درخواست Web API نیز بر روی یک ترد جداگانه اجرا میشود؛ اما چون ابتدا و انتهای درخواستها مشخص است، طول عمر Scoped، در ابتدای درخواست شروع شده و در پایان آن رها سازی میشود. به همین جهت استثنائی را که در اینجا مشاهده میکنید، در برنامههای Web API شاید هیچگاه مشاهده نشود.
معرفی DbContextFactory در EF Core 5x
همواره باید طول عمر DbContext را تا جای ممکن، کوتاه نگه داشت. مشکل فعلی ما، Singleton رفتار کردن DbContextها (داشتن طول عمر طولانی) در برنامههای Blazor Server هستند. یک چنین رفتاری را شاید در برنامههای دسکتاپ هم پیشتر مشاهده کرده باشید. برای مثال در برنامههای دسکتاپ WPF، تا زمانیکه یک فرم باز است، Context ایجاد شدهی در آن هم برقرار است و Dispose نمیشود. در یک چنین حالتهایی، عموما Context را در زمان نیاز، ایجاد کرده و پس از پایان آن کار کوتاه، Context را رها میکنند. به همین جهت نیاز به DbContext Factory ای وجود دارد که بتواند یک چنین پیاده سازیهایی را میسر کند و خوشبختانه از زمان EF Core 5x، یک چنین امکانی خصوصا برای برنامههای Blazor Server تحت عنوان DbContextFactory ارائه شدهاست که به عنوان راه حل استاندارد دسترسی به DbContext در اینگونه برنامهها مورد استفاده قرار میگیرد.
برای کار با DbContextFactory، اینبار در فایل BlazorServer.App\Startup.cs، بجای استفاده از services.AddDbContext، از متد AddDbContextFactory استفاده میشود:
public void ConfigureServices(IServiceCollection services) { var connectionString = Configuration.GetConnectionString("DefaultConnection"); //services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
روش اول کار با DbContextFactory در کامپوننتهای Blazor Server : وهله سازی از نو، به ازای هر متد
در این روش پس از ثبت AddDbContextFactory در فایل Startup برنامه مانند مثال فوق، ابتدا سرویس IDbContextFactory که به ApplicationDbContext اشاره میکند به ابتدای کامپوننت تزریق میشود:
@inject IDbContextFactory<ApplicationDbContext> DbFactory
private async Task DeleteImageAsync() { using var context = DbFactory.CreateDbContext(); var image = await context.HotelRoomImages.FindAsync(1); // ... }
روش دوم کار با DbContextFactory در کامپوننتهای Blazor Server : یکبار وهله سازی Context به ازای هر کامپوننت
در این روش میتوان طول عمر Context را معادل طول عمر کامپوننت تعریف کرد که مزیت استفادهی از Change tracking موجود در EF-Core را به همراه خواهد داشت. در این حالت کامپوننتهای Blazor Server، شبیه به فرمهای برنامههای دسکتاپ عمل میکنند:
@implements IDisposable @inject IDbContextFactory<ApplicationDbContext> DbFactory @code { private ApplicationDbContext Context; protected override async Task OnInitializedAsync() { Context = DbFactory.CreateDbContext(); await base.OnInitializedAsync(); } private async Task DeleteImageAsync() { var image = await Context.HotelRoomImages.FindAsync(1); // ... } public void Dispose() { Context.Dispose(); } }
- اما بجای اینکه به ازای هر متد، کار فراخوانی DbFactory.CreateDbContext صورت گیرد، یکبار در آغاز کار کامپوننت و در روال رویدادگردان OnInitializedAsync، کار وهله سازی Context کامپوننت انجام شده و از این تک Context در تمام متدهای کامپوننت استفاده خواهد شد.
- در این حالت کار Dispose خودکار این Context به متد Dispose نهایی کل کامپوننت واگذار شدهاست. برای اینکه این متد فراخوانی شود، نیاز است در ابتدای تعاریف کامپوننت، از دایرکتیو implements IDisposable@ استفاده کرد.
سؤال: اگر سرویسی از ApplicationDbContext تزریق شدهی در سازندهی خود استفاده میکند، چکار باید کرد؟
برای نمونه سرویسهای از پیش تعریف شدهی ASP.NET Core Identity، در سازندهی خود از ApplicationDbContext استفاده میکنند و نه از IDbContextFactory. در این حالت برای تامین ApplicationDbContextهای تزریق شده، فقط کافی است از روش زیر استفاده کنیم:
services.AddScoped<ApplicationDbContext>(serviceProvider => serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
سؤال: روش پیاده سازی سرویسهای یک برنامه Blazor Server به چه صورتی باید تغییر کند؟
تا اینجا روشهایی که برای استفاده از IDbContextFactory معرفی شدند (که روشهای رسمی و توصیه شدهی اینکار نیز هستند)، فرض را بر این گذاشتهاند که ما قرار است تمام منطق تجاری کار با بانک اطلاعاتی را داخل همان متدهای کامپوننتها انجام دهیم (این روش برنامه نویسی، بسیار مورد علاقهی مایکروسافت است و در تمام مثالهای رسمی آن به صورت ضمنی توصیه میشود!). اما اگر همانند مثالی که تاکنون در این سری بررسی کردیم، نخواهیم اینکار را انجام دهیم و علاقمند باشیم تا این منطق تجاری را به سرویسهای مجزایی، با مسئولیتهای مشخصی انتقال دهیم، روش استفادهی از IDbContextFactory چگونه خواهد بود؟
در این حالت از ترکیب روش دوم مطرح شدهی استفاده از IDbContextFactory که به همراه مزیت دسترسی کامل به Change Tracking توکار EF-Core و پیاده سازی الگوی واحد کار است و وهله سازی خودکار ApplicationDbContext که معرفی شد، استفاده خواهیم کرد؛ به این صورت:
الف) تمام سرویسهای EF-Core یک برنامهی Blazor Server باید اینترفیس IDisposable را پیاده سازی کنند.
این مورد برای سرویسهای پروژههای Web API، ضروری نیست؛ چون طول عمر Context آنها توسط خود IoC Container مدیریت میشود؛ اما در برنامههای Blazor Server، مطابق توضیحاتی که ارائه شد، خودمان باید این طول عمر را مدیریت کنیم.
بنابراین به پروژهی سرویسهای برنامه مراجعه کرده و هر سرویسی که ApplicationDbContext تزریق شدهای را در سازندهی خود میپذیرد، یافته و تعریف اینترفیس آنرا به صورت زیر تغییر میدهیم:
public interface IHotelRoomService : IDisposable { // ... } public interface IHotelRoomImageService : IDisposable { // ... }
public class HotelRoomService : IHotelRoomService { private bool _isDisposed; // ... public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { _dbContext.Dispose(); } } finally { _isDisposed = true; } } } }
ب) Dispose دستی تمام سرویسها، در کامپوننتهای مرتبط
در ادامه تمام کامپوننتهایی را که از سرویسهای فوق استفاده میکنند یافته و ابتدا دایرکتیو implements IDisposable@ را به ابتدای آنها اضافه میکنیم. سپس متد Dispose آنها را جهت فراخوانی متد Dispose سرویسهای فوق، تکمیل خواهیم کرد:
بنابراین ابتدا به فایل BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomUpsert.razor مراجعه کرده و تغییرات زیر را اعمال میکنیم:
@page "/hotel-room/create" @page "/hotel-room/edit/{Id:int}" @implements IDisposable // ... @code { // ... public void Dispose() { HotelRoomImageService.Dispose(); HotelRoomService.Dispose(); } }
@page "/hotel-room" @implements IDisposable // ... @code { // ... public void Dispose() { HotelRoomService.Dispose(); } }
مشکل! اینبار خطای dispose شدن context را دریافت میکنیم!
System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'ApplicationDbContext'.
مشکلی که در اینجا رخ داده این است که سرویسهایی را داریم با طول عمر به ظاهر Scoped که یکی از وابستگیهای آنها را به صورت دستی Dispose کردهایم. چون طول عمر Scoped در اینجا وجود ندارد و طول عمرها در اصل Singleton هستند، هربار که سرویس مدنظر مجددا درخواست شود، همان وهلهی ابتدایی که اکنون یکی از وابستگیهای آن Dispose شده، در اختیار برنامه قرار میگیرد.
پس از این تغییرات، اولین باری که برنامه را اجرا میکنیم، لیست اتاقها به خوبی نمایش داده میشوند و مشکلی نیست. بعد در همین حال و در همین صفحه، اگر بر روی دکمهی افزودن یک اتاق جدید کلیک کنیم، اتفاقی که رخ میدهد، فراخوانی متد Dispose کامپوننت لیست اتاقها است (بر روی آن یک break-point قرار دهید). بنابراین متد Dispose یک کامپوننت، با هدایت به یک مسیر دیگر، به صورت خودکار فراخوانی میشود. در این حالت Context برنامه Dispose شده و در کامپوننت ثبت یک اتاق جدید دیگر، در دسترس نخواهد بود؛ چون IHotelRoomService مورد استفاده مجددا وهله سازی نمیشود و از همان وهلهای که بار اول ایجاد شده، استفاده خواهد شد.
بنابراین سؤال اینجا است که چگونه میتوان سیستم تزریق وابستگیها را وادار کرد تا تمام سرویسهای تزریق شدهی به سازندههای سرویسهای HotelRoomService و HotelRoomImageService را مجددا وهله سازی کند و سعی نکند از همان وهلههای قبلی استفاده کند؟
پاسخ: یک روش این است که IHotelRoomImageService را خودمان به ازای هر کامپوننت به صورت دستی در روال رویدادگردان OnInitializedAsync وهله سازی کرده و DbFactory.CreateDbContext جدیدی را مستقیما به سازندهی آن ارسال کنیم. در این حالت مطمئن خواهیم شد که این وهله، جای دیگری به اشتراک گذاشته نمیشود:
@code { private IHotelRoomImageService HotelRoomImageService; protected override async Task OnInitializedAsync() { HotelRoomImageService = new HotelRoomImageService(DbFactory.CreateDbContext(), mapper); await base.OnInitializedAsync(); } private async Task DeleteImageAsync() { await HotelRoomImageService.DeleteAsync(1); // ... } public void Dispose() { HotelRoomImageService.Dispose(); } }
وادار کردن Blazor Server به وهله سازی مجدد سرویسهای کامپوننتها
بنابراین مشکل ما Singleton رفتار کردن سرویسها، در برنامههای Blazor است. برای مثال در برنامههای Blazor Server، تا زمانیکه اتصال SignalR برنامه برقرار است (مرورگر بسته نشده، برگهی جاری بسته نشده و یا کاربر صفحه را ریفرش نکرده)، هیچ سرویسی دوباره وهله سازی نمیشود.
برای رفع این مشکل، امکان Scoped رفتار کردن سرویسهای یک کامپوننت نیز در نظر گرفته شدهاند. برای نمونه کدهای کامپوننت HotelRoomList.razor را به صورت زیر تغییر میدهیم:
@page "/hotel-room" @*@implements IDisposable*@ @*@inject IHotelRoomService HotelRoomService*@ @inherits OwningComponentBase<IHotelRoomService>
چند نکته:
- فقط یکبار به ازای هر کامپوننت میتوان از دایرکتیو inherits استفاده کرد.
- زمانیکه طول عمر سرویسی را توسط OwningComponentBase مدیریت میکنیم، در حقیقت یک کلاس پایه را برای آن کامپوننت درنظر گرفتهایم که به همراه یک خاصیت عمومی ویژه، به نام Service و از نوع سرویس مدنظر ما است. در این حالت یا میتوان از خاصیت Service به صورت مستقیم استفاده کرد و یا میتوان به صورت زیر، همان کدهای قبلی را داشت و هربار که نیازی به HotelRoomService بود، آنرا به خاصیت عمومی Service هدایت کرد:
@code { private IHotelRoomService HotelRoomService => Service;
@page "/preferences" @using Microsoft.Extensions.DependencyInjection @inherits OwningComponentBase @code { private IHotelRoomService HotelRoomService { get; set; } private IHotelRoomImageService HotelRoomImageService { get; set; } protected override void OnInitialized() { HotelRoomService = ScopedServices.GetRequiredService<IHotelRoomService>(); HotelRoomImageService = ScopedServices.GetRequiredService<IHotelRoomImageService>(); } }
خلاصهی بحث جاری در مورد روش مدیریت DbContext برنامههای Blazor Server:
- بجای services.AddDbContext متداول، باید از AddDbContextFactory استفاده کرد:
services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddScoped<ApplicationDbContext>(serviceProvider => serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
- کامپوننتهای برنامه، سرویسهایی را که باید Scoped عمل کنند، دیگر نباید از طریق تزریق مستقیم آنها دریافت کنند؛ چون در این حالت همواره به همان وهلهای که در ابتدای کار ایجاد شده، میرسیم:
@inject IHotelRoomService HotelRoomService
@inherits OwningComponentBase<IHotelRoomService>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-19.zip