محمد کمائی - Telegram : @komayi
محمد کمائی - Telegram : @komayi
خواندن ۵ دقیقه·۹ ماه پیش

ارور circular dependency در net core

وقتی یک سرویس کلاس به متدهای یک سرویس کلاس دیگه نیاز داره کلاس دوم رو در کلاس اول اینجکت میکنیم و از متدهاش استفاده می کنیم. معمولا هم از روش "constructor injection" استفاده میشود که مزیت نوشتن تنها یکبار در کلاس و استفاده در همه متدهای در سطح کلاس را دارد همچنین امکان mock کردن وابستگی و تست نویسی را دارد. وقتی به کانستراکتور سرویس اول سرویس دوم رو اینجکت میکنیم دات نت در زمان runtime وقتی میاد از سرویس 1 آبجکت بسازه قبلش باید از همه وابستگی هایی که در کانستراکتور سرویس اول وجود داره از جمله سرویس 2 آبجکت بسازه. تا اینجا مشکلی نیست ، ولی در سیستم های نرم افزاری بزرگ احتمال وابستگی سرویس کلاس 2 به سرویس کلاس 1 هم وجود دارد ، البته باید در طراحی سیستم دقت لازم شود تا این اتفاق نیفتد ولی اگر افتاد اینجوری میشه که : وقتی میاد از کلاس 1 آبجکت بسازه میبینه که کلاس 1 وابستگی به کلاس های دیگه داره پس باید از همه وابستگی های کلاس 1 (که کلاس 2 هم جزوش هست) آبجکت بسازه و وقتی میره از کلاس 2 آبجکت بسازه باز در کانستراکتور کلاس 2 میبینه که باید از کلاس 1 آبجکت بسازه پس میفته در یک لوپ تکراری بی نهایت و موقع استارت کردن پروژه این ارور رو میده:

System.AggregateException: 'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: BlazorHosted.Server.Services.IService1 Lifetime: Scoped ImplementationType: BlazorHosted.Server.Services.Service1': A circular dependency was detected for the service of type 'BlazorHosted.Server.Services.IService1'.

در فایل program.cs هم که به سرویس کالکشن دات نت کور دو سرویس مون رو اضافه کردیم تا در هر کلاسی نیاز به اینجکت باشه کار instantiate یا همون new کردن آبجکت رو انجام بده.

چه با "کلاس" چه با "اینترفیس" هم اینجکت کنیم این ارور رو میگیریم (اینجا با اینترفیس بوده) :

حتی به صورت غیر مستقیم هم این اتفاق میفته ، در تصویر زیر A و C براشون این اتفاق افتاده.

اگر در کانستراکتورها به صورت <>Lazy اینجکت کنیم هم این ارور رو میگیریم :

The lazily-initialized type does not have a public, parameterless constructor


علت اصلی به وجود آمدن این وابستگی ها طراحی اشتباه سیستم یا دیتابیس و یا وجود آنتی پترن در معماری نرم افزار است مثلا وجود لایه اضافی و اشتباه و آنتی پترن ریپازیتوری روی ef core که هم باعث پیچیدگی هم کاهش پرفرمنس وب اپ asp.net core میشه ، راه حل اصلی و اصولی اینه که با اصلاح طراحی و معماری این وابستگی ها اصلاح و حذف بشن (اینجکت DbContext در کلاس های سرویس و حذف کلاسهای ریپازیتوری) ولی اگر موارد استثنا موند و نمی شد کاریش کرد میشه از این راه ها استفاده کرد :


- یک راه موقت میتونه این باشه که یک کلاس جدید مثلا با نام ShareClass بسازیم و هر دو وابستگی رو در کلاسهای 1 و 2 حذف کنیم و متدهایی که در کلاس 2 بودن و کلاس 1 بهشون نیاز داره رو به ShareClass منتقل کنیم همینطور متدهایی که در کلاس 1 بودن و کلاس 2 بهشون نیاز داره رو به ShareClass منتقل کنیم و ShareClass رو به هر دو کلاس های 1 و 2 اینجکت کنیم ، ولی اتفاقی که میفته اینه که بعد یه مدت کلی متد که باید در جای درست خودشون باشن به کلاسهای Share منتقل میشن و دیگه توسعه و نگهداری و برنامه نویسی رو برامون سخت میکنه.


- یک راه حل میتونه استفاده از پترن mediator و پکیج mediatr باشه که اینطوری همه سرویس کلاسها نهایتا فقط یک وابستگی اونم به مدیت آر داشته باشن و مشکلی بابت وابستگی های رفت و برگشتی ندارن.


- یک راه حل دیگه میتونه این باشه که به صورت "پیشفرض" با همون روش "constructor injection" پیش رفت و "هر وقت نیاز به اینجکت های بازگشتی" مثل تصاویر کلاس های 1 و 2 در بالا شد از IServiceProvider خود دات نت استفاده کرد یعنی این اینترفیس رو در کلاسهای 1 و 2 اینجکت کرد و هر جا نیاز به استفاده از متدهای کلاس دیگر شد از متد GetRequiredService اش که به صورت Generic هست استفاده کرده و از سرویس پرووایدر دات نت یک نمونه از کلاس مورد نیاز رو گرفت و با زدن دات از متدهاش استفاده کرد به صورت زیر:

در این تصویر از روش جدید primary constructor در دات نت 8 برای اینجکت استفاده شده است.
در این تصویر از روش جدید primary constructor در دات نت 8 برای اینجکت استفاده شده است.

نکته : به جای اینجکت دونه دونه سرویس کلاس ها در program.cs میشه یه کار راحت تری انجام داد اونم اینکه اسم همه این کلاسها رو به Service ختم کنیم و با یه حلقه به سرویس پرووایدر دات نت اضافه کنیم به این صورت :

var appServices = typeof(Program).Assembly.GetTypes().Where(s => s.Name.EndsWith("Service") && s.IsInterface == false).ToList();
foreach (var appService in appServices)
builder.Services.Add(new ServiceDescriptor(appService, appService,
ServiceLifetime.Scoped));


- راه قبل برای تعداد وابستگی کم خوبه و مشکلی هم پیش نمیاد ولی اگه پروژه خیلی بزرگ هست با وابستگی های پیچیده و circular dependency های زیاد و اگر بخوایم تست بنویسیم ساخت یک نمونه از IServiceProvider برای mock سخت میشه چون باید تمام سرویس کلاسها رو موقع نمونه سازی بهش بدین ضمن اینکه از خطوط اول سرویس کلاس ها قابل تشخیص نیست که اون کلاس چه وابستگی هایی دارد ، در این مواقع این راه میتونه بهتر باشه : یک اکستنشن متد روی IServiceCollection بسازید به همراه کلاس LazilyResolved و تمام کلاس هایی که براشون مشکل circular dependency پیش میاد رو به صورت <>Lazy اینجکت کنید.

فایل IServiceCollectionExtension.cs :

سپس در Program.cs این خط را بعد از اینجکت کلاسها یا بعد اون foreach که بالاتر کدش رو نوشتم بزارید:

builder.Services.AddLazyResolution();

کلاسهای 3 و 4 که به هم وابستگی دارند را به صورت لیزی به هم اینجکت کنید:

و در هر کلاس دیگر که خواستین این کلاسها که مشکل circular dependency دارند را به صورت <>Lazy اینجکت کنید. موقع کال کردن متد مورد نظر هم Value. نیاز است.

دو روش آخر به صورت لیزی کار میکنن یعنی تا نیاز به آبجکت کلاس اینجکت شده در متدی نباشه اینجکت نمیشه و مشکل circular dependency رو حل میکنن.

سی شارپdependency injectionASPNET COREnet core
C# , .net core & angular & blazor , sql server
شاید از این پست‌ها خوشتان بیاید