مطالب
اضافه کردن OData به پروژه‌های ASP.NET Core 3.1 با اضافه کردن فقط 20 کلمه به کد!
به مناسبت ارائه‌ی نسخه 7.4 از Microsoft.AspNetCore.OData که دیروز صورت پذیرفت، تصمیم گرفتم آموزش استفاده از OData را در پروژه‌های ASP.NET Core 3.1 به بالا که دارای endpoint routing هستند (روش توصیه شده)، تهیه کنم تا در آن، پروژه کمترین تغییر ممکن را برای اضافه شدن OData داشته باشد و ببینیم که استفاده از آن در نسخه‌های جدید، به چه میزان آسان شده است.

ابتدا با dotnet --version و یا dotnet --info و یا هر روش دیگری، از نصب بودن dot net core 3.1 sdk مطمئن می‌شویم. سپس دستور
dotnet new webapi -o SampleApi
را می‌زنیم.

در ویژوال استودیو، این دستور معادل ساخت پروژه‌ای جدید از نوع ASP.NET Core Web Application است که در دیالوگ بعدی، از بین گزینه‌های Empty، Api و Web Application و ... ما گزینه‌ی Api را انتخاب می‌کنیم.
این یک پروژه‌ی Web Api و با استفاده از endpoint routing است. در این پروژه یک WeatherForecast وجود دارد که نقش مدل را ایفا می‌کند و یک WeatherForecastController که در Get خود، تعدادی از WeatherForecastها را ساخته و باز می‌گرداند. این پروژه فاقد دیتابیس است.

حال چه کنیم که این پروژه OData enabled شود؟
1- دو Package زیر را نصب می‌کنیم:
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="7.4.0" />

2- در Startup.cs ابتدا
using Microsoft.AspNet.OData.Extensions;
را در بالای فایل اضافه کرده، و کدهای ConfigureServices را به شکل زیر تغییر می‌دهیم:
services.AddControllers().AddNewtonsoftJson(); // Add .AddNewtonsoftJson() to services.AddControllers();
services.AddOData(); // add this new line
همچنین کد app.UseEndpoints را به صورت زیر تغییر می‌دهیم:
app.UseEndpoints(endpoints => // Existing code
{   
    endpoints.MapControllers(); // Existing code
    endpoints.EnableDependencyInjection(); // Add this new line
    endpoints.Select().Expand().Filter().OrderBy().Count().MaxTop(20); // Add this new line
});

سپس در WeatherForecastController و متد Get، کد را به شکل زیر تغییر دهید:
// replace:
[HttpGet] // Existing code  
// with:
[HttpGet, EnableQuery] // new code
برنامه را اجرا کنید و آدرس زیر را بزنید:
وجود orderby=TemperatureC باعث می‌شود که WeatherForcastها، مرتب شده بر اساس درجه سانتیگرادی که دارند، برای کلاینت ارسال شوند.

همانطور که مشاهده کردید، به ساده‌ترین شکل ممکن، OData به پروژه اضافه شد!

به صورت کلی OData امکان فیلتر کردن رکوردهای بازگشتی، Projection، مرتب سازی، Paging، گروه بندی، Aggregation و ... را دارد که در ادامه چند مثالی را با هم می‌بینیم. 
فقط با توجه به اینکه مثال پیش فرض ASP.NET Core، یعنی WeatherForecast کمی گنگ و غیر متداول است، از آن میگذریم و با فرض لیستی از مشتریان با ساختار زیر پیش می‌رویم:
public class Customer
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public Gender Gender { get; set; }

    public AddressInfo Address { get; set; }
}

public class AddressInfo
{
    public int StreetNo { get; set; }

    public string PostalCode { get; set; }
}

public enum Gender
{
    Man, Woman, Other
}  
برای بازگردادن مشتریان خانم، داریم: (آموزش فیلتر بر روی enum)
?$filter=Gender eq 'Woman'
برای مرتب سازی مشتریان خانم بر اساس شماره خیابان آدرس آنها: (آموزش مرتب سازی بر روی Nested Properties و ترکیب orderby و filter)
?$filter=Gender eq 'Woman'&$orderby=Address/StreetNo
برای بازگرداندن مشتریانی که در اسم آنها کلمه‌ی ali وجود دارد:
?$filter=contains(FirstName,'Ali')
برای مشاهده لیست امکانات کوئری گیری و مثال‌های بیشتر می‌توانید به این لینک مراجعه کنید.

امکان کوئری گیری به صورت مفصل وجود دارد و می‌توانید مواردی چون Any / All را نیز در فیلترهای خود بگنجانید.
به علاوه اگر به جای یک آرایه یا لیست، یک IQueryable از EntityFrameworkCore گرفته و ... بازگردانید، OData کوئری ارسالی را روی IQueryable مربوطه اعمال و paging و orderby و ... تماما روی دیتابیس انجام و فقط دیتای لازم و مورد نیاز از دیتابیس خوانده شده و به کلاینت باز می‌گردد که این مهم در بهبود عملکرد برنامه بسیار موثر است.

نکته مهم این است که در این روش، امنیت برنامه به خطر نمیافتد؛ زیرا اگر شما بر اساس منطق برنامه، یک Where را سمت سرور اعمال کنید، Client فقط میتواند روی دیتایی که حق دارد ببیند Paging و OrderBy و... اعمال کند، نه اینکه دیتای بیشتری را از سرور دریافت کند.
همچنین در این روش، استفاده از Swagger، Routing و ... تماما به روشی است که همین الان آن را بلدید، در کنار رعایت best practiceهایی چون بازگرداندن Dto به جای Entity و ... نیز کاملا امکان پذیر است.
نظرات مطالب
EF Code First #10
- شما می‌تونید با استفاده از fluent api کنترل کاملی بر روی نام‌های خودکار تولیدی داشته باشید. یک سری پیش فرض در ابتدای امر هست؛ اما تمام این‌ها با fluent api قابل بازنویسی است.
- اینکه چه نامی در بانک اطلاعاتی تولید شده در EF Code first اهمیتی ندارد. شما با اشیاء سروکار دارید. قرار نیست مستقیما از فیلدی کوئری بگیرید یا قرار نیست مستقیما SQL خام بنویسید. زمانیکه از LINQ استفاده می‌کنید تمام ترجمه‌ها خودکار است صرفنظر از اینکه نام‌ها در سمت دیتابیس الان چه چیزی هست.
- تمام navigation property‌‌ها به جداول مورد نظر مراجعه می‌کنند. lazy loading به معنای عدم بارگذاری اطلاعات اشیاء مرتبط در بار اول فراخوانی شیء پایه است و تنها بارگذاری اطلاعات اشیاء وابسته در زمان نیاز. دقیقا در زمانیکه خاصیتی از آن شیء مرتبط فراخوانی شود و نه قبل از آن.
- زمانیکه primary key یک جدول رو دارید بهتر است از متد Find استفاده کنید بجای کوئری LINQ فوق. به این ترتیب از سطح اول کش برخوردار خواهید شد (تعداد کمتر رفت و برگشت به بانک اطلاعاتی).
- شما بدون مشکل می‌تونید مستقیما از خواص اشیاء مرتبط استفاده کنید و اگر می‌خواهید lazy loading را متوقف کنید (خصوصا برای نمایش اطلاعات در یک گرید) فقط کافی است از متد Include یاد شده استفاده کنید.

مطالب
آشنایی با NHibernate - قسمت دوم

آزمون واحد کلاس نگاشت تهیه شده

در مورد آشنایی با آزمون‌های واحد لطفا به برچسب مربوطه در سمت راست سایت مراجعه بفرمائید. همچنین در مورد اینکه چرا به این نوع API کلمه Fluent اطلاق می‌شود، می‌توان به تعریف آن جهت مطالعه بیشتر مراجعه نمود.

در این قسمت قصد داریم برای بررسی وضعیت کلاس نگاشت تهیه شده یک آزمون واحد تهیه کنیم. برای این منظور ارجاعی را به اسمبلی nunit.framework.dll به پروژه UnitTests که در ابتدای کار به solution جاری در VS.Net افزوده بودیم، اضافه نمائید (همچنین ارجاع‌هایی به اسمبلی‌های پروژه NHSample1 ، FluentNHibernate ، System.Data.SQLite ، NHibernate.ByteCode.Castle و Nhibernate نیز نیاز هستند). تمام اسمبلی‌های این فریم ورک‌ها از پروژه FluentNHibernate قابل استخراج هستند.

سپس سه کلاس زیر را به پروژه آزمون واحد اضافه خواهیم کرد.
کلاس TestModel : (جهت مشخص سازی محل دریافت اطلاعات نگاشت)

using FluentNHibernate;
using NHSample1.Domain;

namespace UnitTests
{
public class TestModel : PersistenceModel
{
public TestModel()
{
AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
}
}
}

کلاس FixtureBase : (جهت ایجاد سشن NHibernate در ابتدای آزمون واحد و سپس پاکسازی اشیاء در پایان کار)

using NUnit.Framework;
using NHibernate;
using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;

namespace UnitTests
{
public class FixtureBase
{
protected SessionSource SessionSource { get; set; }
protected ISession Session { get; private set; }

[SetUp]
public void SetupContext()
{
var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.InMemory);

SessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
new TestModel());

Session = SessionSource.CreateSession();
SessionSource.BuildSchema(Session);
}

[TearDown]
public void TearDownContext()
{
Session.Close();
Session.Dispose();
}
}
}

و کلاس CustomerMapping_Fixture.cs : (جهت بررسی صحت نگاشت تهیه شده با کمک دو کلاس قبل)

using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;

namespace UnitTests
{
[TestFixture]
public class CustomerMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_customer()
{
new PersistenceSpecification<Customer>(Session)
.CheckProperty(c => c.Id, 1001)
.CheckProperty(c => c.FirstName, "Vahid")
.CheckProperty(c => c.LastName, "Nasiri")
.CheckProperty(c => c.AddressLine1, "Addr1")
.CheckProperty(c => c.AddressLine2, "Addr2")
.CheckProperty(c => c.PostalCode, "1234")
.CheckProperty(c => c.City, "Tehran")
.CheckProperty(c => c.CountryCode, "IR")
.VerifyTheMappings();
}
}
}

توضیحات:
اکنون به عنوان یک برنامه نویس متعهد نیاز است تا کار صورت گرفته در قسمت قبل را آزمایش کنیم.
کار بررسی صحت نگاشت تعریف شده در قسمت قبل توسط کلاس استاندارد PersistenceSpecification فریم ورک FluentNHibernate انجام خواهد شد (در کلاس CustomerMapping_Fixture). این کلاس برای انجام عملیات آزمون واحد نیاز به کلاس پایه دیگری به نام FixtureBase دارد که در آن کار ایجاد سشن NHibernate (در قسمت استاندارد SetUp آزمون واحد) و سپس آزاد سازی آن را در هنگام خاتمه کار ، انجام می‌دهد (در قسمت TearDown آزمون واحد).
این ویژگی‌ها که در مباحث آزمون واحد نیز به آن‌ها اشاره شده است، سبب اجرای متدهایی پیش از اجرا و بررسی هر آزمون واحد و سپس آزاد سازی خودکار منابع خواهند شد.
برای ایجاد یک سشن NHibernate نیاز است تا نوع دیتابیس و همچنین رشته اتصالی به آن (کانکشن استرینگ) مشخص شوند. فریم ورک Fluent NHibernate با ایجاد کلاس‌های کمکی برای این امر، به شدت سبب ساده‌ سازی انجام آن شده است. در این مثال، نوع دیتابیس به SQLite و در حالت دیتابیس در حافظه (in memory)، تنظیم شده است (برای انجام امور آزمون واحد با سرعت بالا).
جهت اجرای هر دستوری در NHibernate نیاز به یک سشن می‌باشد. برای تعریف شیء سشن، نه تنها نیاز به مشخص سازی نوع و حالت دیتابیس مورد استفاده داریم، بلکه نیاز است تا وهله‌ای از کلاس استاندارد PersistanceModel را نیز جهت مشخص سازی کلاس نگاشت مورد استفاده مشخص نمائیم. برای این منظور کلاس TestModel فوق تعریف شده است تا این نگاشت را از اسمبلی مربوطه بخواند و مورد استفاده قرار دهد (بر پایی اولیه این مراحل شاید در ابتدای امر کمی زمانبر باشد اما در نهایت یک پروسه استاندارد است). توسط این کلاس به سیستم اعلام خواهیم کرد که اطلاعات نگاشت را باید از کدام کلاس دریافت کند.
تا اینجای کار شیء SessionSource را با معرفی نوع دیتابیس و همچنین محل دریافت اطلاعات نگاشت اشیاء معرفی کردیم. در دو سطر بعدی متد SetupContext کلاس FixtureBase ، ابتدا یک سشن را از این منبع سشن تهیه می‌کنیم. شیء منبع سشن در این فریم ورک در حقیقت یک factory object است (الگوهای طراحی برنامه نویسی شیءگرا) که امکان دسترسی به انواع و اقسام دیتابیس‌ها را فراهم می‌سازد. برای مثال اگر روزی نیاز بود از دیتابیس اس کیوال سرور استفاده شود، می‌توان از کلاس MsSqlConfiguration بجای SQLiteConfiguration استفاده کرد و همینطور الی آخر.
در ادامه توسط شیء SessionSource کار ساخت database schema را نیز به صورت پویا انجام خواهیم داد. بله، همانطور که متوجه شده‌اید، کار ساخت database schema نیز به صورت پویا توسط فریم ورک NHibernate با توجه به اطلاعات کلاس‌های نگاشت، صورت خواهد گرفت.

این مراحل، نحوه ایجاد و بر پایی یک آزمایشگاه آزمون واحد فریم ورک Fluent NHibernate را مشخص ساخته و در پروژه‌های شما می‌توانند به کرات مورد استفاده قرار گیرند.

در ادامه اگر آزمون واحد را اجرا نمائیم (متد can_correctly_map_customer در کلاس CustomerMapping_Fixture)، نتیجه باید شبیه به شکل زیر باشد:



توسط متد CheckProperty کلاس PersistenceSpecification ، امکان بررسی نگاشت تهیه شده میسر است. اولین پارامتر آن، یک lambda expression خاصیت مورد نظر جهت بررسی است و دومین آرگومان آن، مقداری است که در حین آزمون به خاصیت تعریف شده انتساب داده می‌شود.

نکته:
شاید سؤال بپرسید که در تابع can_correctly_map_customer عملا چه اتفاقاتی رخ داده است؟ برای بررسی آن در متد SetupContext کلاس FixtureBase ، اولین سطر آن‌را به صورت زیر تغییر دهید تا عبارات SQL نهایی تولید شده را نیز بتوانیم در حین عملیات تست مشاهده نمائیم:

var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.ShowSql().InMemory);




مطابق متد تست فوق، عبارات تولید شده به شرح زیر هستند:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 1001
NHibernate: SELECT customer0_.Id as Id0_0_, customer0_.FirstName as FirstName0_0_, customer0_.LastName as LastName0_0_, customer0_.AddressLine1 as AddressL4_0_0_, customer0_.AddressLine2 as AddressL5_0_0_, customer0_.PostalCode as PostalCode0_0_, customer0_.City as City0_0_, customer0_.CountryCode as CountryC8_0_0_ FROM "Customer" customer0_ WHERE customer0_.Id=@p0;@p0 = 1001

نکته جالب این عبارات، استفاده از کوئری‌های پارامتری است به صورت پیش فرض که در نهایت سبب بالا رفتن امنیت بیشتر برنامه (یکی از راه‌های جلوگیری از تزریق اس کیوال در ADO.Net که در نهایت توسط تمامی این فریم ورک‌ها در پشت صحنه مورد استفاده قرار خواهند گرفت) و همچنین سبب بکار افتادن سیستم‌های کش دیتابیس‌های پیشرفته مانند اس کیوال سرور می‌شوند (execution plan کوئری‌های پارامتری در اس کیوال سرور جهت بالا رفتن کارآیی سیستم کش می‌شوند و اهمیتی هم ندارد که حتما رویه ذخیره شده باشند یا خیر).

ادامه دارد ...

اشتراک‌ها
سوالات مصاحبه ای ASP.NET Core
What is .NET Core?
What is ASP.NET Core?
Can ASP.NET Core work with the .NET framework?
What is Kestrel?
Dependency Injection in ASP.NET Core?
The configuration in ASP.NET Core?
Talk about Logging in ASP.NET Core?
Explain Middleware in ASP.NET Core?
Explain startup process in ASP.NET Core?
سوالات مصاحبه ای ASP.NET Core
مطالب
تزریق مستقیم وابستگی‌ها در Action Method ها و Handler Method های ASP.NET Core
همانطور که میدانید  ASP.NET Core از تزریق وابستگی‌ها ( Dependency Injection ) پشتیبانی میکند، که یک نوع Design Pattern است، و در اصل یک تکنیک است برای دستیابی و پیاده سازی اصل Inversion Of Control یا همان IoC بین کلاس‌ها و وابستگی‌های آن کلاس‌ها. متد ConfigureServices در کلاس Startup وظیفه‌ای دارد برای ثبت و تعریف سرویس‌های مورد استفاده‌ی در برنامه از جمله Platform Features مانند EF Core و معماری MVC. این متد در اصل DI Container ما است و میتوانیم از آن برای loosely coupled کردن برنامه مان از آن استفاده کنیم. در این متد 3 روش برای ثبت و تعریف سرویس‌های ما وجود دارد، که این 3 روش در اصل طول عمر ( حیات ) یک سرویس را مشخص میکنند. در پایین نام این سه LifeTime را برای شما لیست میکنم:
  1. Transient
  2. Scoped
  3. Singleton

تزریق وابستگی را در برنامه‌های ASP.NET Core میتوان به چند طریق انجام داد:
  1. Constructor Injection
  2. Property Injection
  3. Method Injection
  4. Action  & Handler Injection
که بیشترین نوع استفاده از طریق سازنده‌ی کلاس یا همان Constructor است ( نوع اول ).

در این مقاله میخواهیم مورد چهارم را مورد بررسی قرار دهیم، که برای تزریق مستقیم وابستگی به یک Handler Method یا Action Method استفاده میشود؛ بدون تزریق وابستگی به سازنده و ساخت فیلد خصوصی و readonly از Abstraction مورد نظر. مایکروسافت این نوع تزریق را به صورت یک Attribute در آورده است، که از کلاس Attribute هم ارث بری کرده است و در فضای نام
Microsoft.AspNetCore.Mvc
قرار گرفته است و نام آن FromServices است. برای بررسی اجمالی این کلاس میتوانید به لینک رو به رو مراجعه کنید. FromServicesAttribute Class  

خب نحوه استفاده از این Attribute را در ادامه یاد خواهیم گرفت. همانطور که در بالا گفته شد، با استفاده از این صفت دیگر نیازی به تزریق وابستگی در سازنده ندارید؛ البته از این صفت مواقعی استفاده میشود که برای مثال فقط در یک اکشن متد یا هندلر متد میخواهید استفاده کنید.
مثال واقعی:
فرض کنید که یک Controller دارید همراه با 15 عدد Action Method، یا یک Razor Page دارید که در PageModel آن چند Handler Method دارید، حالا نیاز به یک سرویسی دارید که در تمامی این Handler Method‌ها یا Action Method‌ها باید استفاده شود، مانند کلاس Context که برای EF Core است. حالا فرض کنید یکی از آن 15 کنترلرها نیاز به یک سرویس خاصی دارد، مثلا سرویس ارسال ایمیل، و در 14 تای دیگر این سرویس استفاده نمیشود، پس اینجا منطقی است که تزریق وابستگی به صورت مستقیم در خود Action Method انجام بگیرد. در مثال پایین میبینید که اینترفیس IEmailSender را در اکشن متد به صورت مستقیم تزریق کرده‌ایم، بدون دخالت سازنده کلاس. البته این Abstraction باید یک کلاس پیاده سازی کننده داشته باشد و در DI Container پروژه به ثبت رسیده باشد.
مثال شماره 1:
 public class AccountController : Controller
    {
        public IActionResult ConfirmEmail([FromServices] IEmailSender emailSender)
        {
           // emailSender.SendAsync(...);
        }
    }

مثال شماره 2:
public IActionResult About([FromServices] IDateTime dateTime)
{
    ViewData["Message"] = $"Current server time: {dateTime.Now}";

    return View();
}

مثال شماره 3:
[HttpGet]
 public ProductModel GetProduct([FromServices] IProductModelRequestService productModelRequest)
 {
     return productModelRequest.Value;
 }
از این صفت میتوانید در Handler Method‌های Razor Page‌ها هم استفاده کنید و هیچ محدودیتی در استفاده از آن‌ها در صفحات قدرتمند Razor وجود ندارد.
مطالب
تبادل داده ها بین لایه ها- قسمت دوم

قسمت اول : تبادل داده‌ها بین لایه ها- قسمت اول  

روش دوم: Uniform(Entity classes)

روش دیگر پاس دادن داده‌ها، روش uniform  است. در این روش کلاس‌های Entity، یک سری کلاس ساده به همراه یکسری Property ‌های Get و Set می‌باشند. این کلاس‌ها شامل هیچ منطق کاری نمی‌باشند. برای مثال کلاس CustomerEntity که دارای دو Property ، Customer Name  و Customer Code می‌باشد. شما می‌توانید تمام Entity ‌ها را به صورت یک پروژه‌ی مجزا ایجاد کرده و به تمام لایه‌ها رفرنس دهید. 


public class CustomerEntity
{
    protected string _CustomerName = "";
    protected string _CustomerCode = "";
    public string CustomerCode
    {
        get { return _CustomerCode; }
        set { _CustomerCode = value; }
    }
    public string CustomerName
    {
        get { return _CustomerName; }
        set { _CustomerName = value; }
    }
}

خوب، اجازه دهید تا از CustomerDal شروع کنیم. این کلاس یک Collection از CustomerEntity  را بر می‌گرداند و همچنین یک CustomerEntity را برای اضافه کردن به دیتابیس . توجه داشته باشید که لایه Data Access وظیفه دارد تا دیتای دریافتی از دیتابیس را به CustomerEntity تبدیل کند. 

public class CustomerDal
{
    public List<CustomerEntity> getCustomers()
    {
        // fetch customer records
        return new List<CustomerEntity>();
    }
    public bool Add(CustomerEntity obj)
    {
        // Insert in to DB
        return true;
    }
}

لایه Middle از CustomerEntity ارث بری می‌کند و یکسری operation را  به entity class اضافه خواهد کرد. داده‌ها در قالب Entity Class به لایه Data Access ارسال می‌شوند و در همین قالب نیز بازگشت داده می‌شوند. این مسئله در کد ذیل به روشنی مشاهده می‌شود. 

public class Customer : CustomerEntity
{
   
    public List<CustomerEntity> getCustomers()
    {
        CustomerDal obj = new CustomerDal();
        
        return obj.getCustomers();
    }
    public void Add()
    {
        CustomerDal obj = new CustomerDal();
        obj.Add(this);
    }
}

لایه UI هم با تعریف یک Customer و فراخوانی operation ‌های مربوط به آن، داده‌ی مد نظر خود را در قالب CustomerEntity بازیابی خواهد کرد. اگر بخواهیم عمکرد روش uniform را خلاصه کنیم باید بگوییم، در این روش دیتای رد و بدل شده‌ی مابین کلیه لایه‌ها با یک ساختار استاندارد، یعنی Entity پاس داده می‌شوند.

مزایا و معایب روش uniform

مزایا

·Strongly typed به صورت  در تمامی لایه‌ها قابل دسترسی و استفاده می‌باشد. 

· به دلیل اینکه از ساختار عمومی Entity  استفاده می‌کند، بنابراین فقط یکبار نیاز به تبدیل داده‌ها وجود دارد. به این معنی که کافی است یک بار دیتای واکشی شده از دیتابیس را به یک ساختار Entity تبدیل کنید و در ادامه بدون هیچ تبدیل دیگری از این Entity استفاده کنید.

معایب

· تنها مشکلی که این روش دارد، مشکلی است به نام Double Loop . هنگامیکه شما در مورد کلاس‌های entity بحث می‌کنید، ساختار‌های دنیای واقعی را مدل می‌کنید. حال فرض کنید شما به دلیل یکسری مسایل فنی دیتابیس خود را Optimize  کرده اید. بنابراین ساختار دنیای واقعی با ساختاری که شما در نرم افزار مدل کرده‌اید متفاوت می‌باشد. بگذارید یک مثال بزنیم؛ فرض کنید که یک customer دارید، به همراه یکسری Address. همان طور که ذکر کردیم، به دلیل برخی مسایل فنی ( denormalized ) به صورت یک جدول در دیتا بیس ذخیره شده است. بنابراین سرعت واکشی اطلاعات بیشتر است. اما خوب اگر ما بخواهیم این ساختار را در دنیای واقعی بررسی کنیم، ممکن است با یک ساختار یک به چند مانند شکل ذیل برخورد کنیم. 

بنابراین مجبوریم یکسری کد جهت این تبدیل همانند کد ذیل بنویسیم.

foreach (DataRow o1 in oCustomers.Tables[0].Rows)
{
    obj.Add(new CustomerEntyityAddress()); // Fills customer
    foreach (DataRow o in oAddress.Tables[0].Rows)
    {
        obj[0].Add(new AddressEntity()); // Fills address
    }
}


مطالب
سری بررسی SQL Smell در EF Core - استفاده از مدل Entity Attribute Value - بخش دوم
در مطلب قبلی، مدل EAV را معرفی کردیم و گفتیم که این نوع پیاده‌سازی در واقع یک SQL Smell است؛ زیرا کوئری نویسی را سخت میکند و همچنین به دلیل عدم امکان تعریف constraints، کنترلی بر روی صحت دیتاهای وارده شد نخواهیم داشت. در نهایت با برنامه‌ای روبرو خواهیم شد که درک صحیحی از ماهیت دیتا ندارد. اما اگر در شرایطی مجبور به استفاده‌ی از این مدل هستید، بهتر است از فرمت JSON برای ذخیره‌سازی دیتای داینامیک استفاده کنید. بیشتر دیتابیس‌های رابطه‌ایی به صورت native از نوع داده‌ایی JSON پشتیبانی میکنند:  
CREATE TABLE EmployeeJsonAttributes (
  Id int NOT NULL AUTO_INCREMENT,
  EmployeeId int NOT NULL,
  Attributes json DEFAULT NULL,
  PRIMARY KEY (Id),
  FOREIGN KEY (EmployeeId) REFERENCES EmployeeEav (Id) ON DELETE CASCADE
)
همانطور که مشاهده می‌کنید در اینجا تایپ ستون Attributes، به JSON تنظیم شده است. بنابراین می‌توانیم از قابلیت‌های توکار دیتابیس (MySQL در مطلب جاری) برای ذخیره و بازیابی داده‌های JSON استفاده کنیم. در ادامه دو روش ذخیره JSON  را مشاهده میکنید: 
INSERT INTO EmployeeJsonAttributes VALUES (
101, 
  '{
  "name": "Jon",
    "lastName": "Doe",
    "dateOfBirth": "1989-01-01 10:10:10+05:30",
    "skills": [ "C#", "JS" ],
    "address":  {
  "country": "UK",
      "city": "London",
      "email": "jon.doe@example.com"
    }
  }'
)

INSERT INTO efcoresample.EmployeeJsonAttributes VALUES (
101, 
  JSON_OBJECT(
"name", "Jon", 
"lastName", "Doe",
"dateOfBirth", "1989-01-01 10:10:10+05:30",
"skills", JSON_ARRAY("C#", "JS"),
    "address", JSON_OBJECT(
  "country", "UK",
      "city", "London",
  "email", "jon.doe@example.com"
    )
  )
)

به عنوان مثال در ادامه میخواهیم کشور محل تولد یک کاربر خاص را نمایش دهیم. برای اینکار می‌توانیم از JSON_EXTRACT استفاده کنیم:
SELECT JSON_EXTRACT(Attributes, '$.address.country') as Country 
FROM EmployeeJsonAttributes
WHERE EmployeeId = 101;

-- Conutry
-- "UK"

همچنین می‌توانیم از عملگر column-path نیز به جای JSON_EXTRACT استفاده کنیم:
SELECT Attributes -> '$.address.country' as Country 
FROM EmployeeJsonAttributes
WHERE EmployeeId = 101;

-- Conutry
-- "UK"

بنابراین به راحتی می‌توانیم کوئری مطلب قبل را اینگونه بازنویسی کنیم:
SELECT EmployeeId, Attributes ->> '$.DateOfBirth' AS BirthDate FROM EmployeeJsonAttributes
WHERE Attributes ->> '$.DateOfBirth' > DATE_SUB(CURRENT_DATE(), INTERVAL 25 YEAR)
همانطور که مشاهده می‌کنید در کوئری فوق یک عملگر < دیگر نیز اضافه کرده‌ایم. هدف از آن حذف “” از خروجی نهایی می‌باشد. 

استفاده از JSON در EF Core 
متاسفانه در EF Core به صورت مستقیم نمی‌توانیم از JSON درون کلاس‌های سی‌شارپ استفاده کنیم (+ )، در نتیجه در سمت کلاس‌های سی‌شارپ باید از string استفاده کنیم و به نوعی به EF Core اطلاع دهیم که تایپ ستون موردنظرمان JSON است. در نتیجه خروجی نهایی درون دیتابیس، یک فیلد با تایپ JSON خواهد بود. برای اینکار به دو شیوه می‌توانیم تایپ ستون موردنظر را تعیین کنیم: 
// Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>(entity =>
    {
        entity.Property(e => e.Attributes).HasColumnType("json");
    });
}

// Data Annotations
[Column(TypeName = "json")]
public string Attributes { get; set; }

در نهایت برای تشکیل بانک اطلاعاتی، به مدلی با ساختار زیر نیاز خواهیم داشت:
public class EmployeeJsonAttribute
{
    public int Id { get; set; }
    public virtual EmployeeEav Employee { get; set; }
    public int EmployeeId { get; set; }
    [Column(TypeName = "json")]
    public string Attributes { get; set; }
}
در اینجا به جای تعریف ستون‌ها و مقادیر داینامیک‌شان از یک فیلد از نوع رشته‌ایی با نام Attributes استفاده شده است. از آنجائیکه نوع ستون در سمت دیتابیس به JSON تنظیم خواهد شد، در نتیجه هر نوع ساختار JSON معتبری را می‌توانیم درون آن ذخیره کنیم:
dbContext.EmployeeJsonAttributes.Add(new EmployeeJsonAttribute
{
    EmployeeId =  101,
    Attributes = JsonSerializer.Serialize(new
    {
        FirstName = "Sirwan",
        LastName = "Afifi",
        DateOfBirth = DateTime.Now.AddYears(-31)
    })
});

dbContext.SaveChanges();
همانطور که اشاره شده به دلیل عدم پشتیبانی از JSON در حال حاضر در EF Core امکان کوئری نویسی بر روی ستون JSON را نداریم. در همین حد که براساس فیلدهای دیگر جستجو را انجام داده و خروجی را Deserialize کنیم:
var employee = dbContext.EmployeeJsonAttributes.Find(201);
Console.WriteLine(JsonSerializer.Deserialize<Employee>(employee.Attributes).DateOfBirth);

برای نوشتن کوئری روی ستون JSON می‌توانید از Query Types  نیز استفاده کنید. 
مطالب
Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر
از زمان Blazor 5x، پشتیبانی توکار از آپلود فایل‌ها، به آن اضافه شده‌است و پیش از آن می‌بایستی از کامپوننت‌های ثالث استفاده می‌شد. در این قسمت نحوه‌ی استفاده از کامپوننت آپلود فایل‌های Blazor را بررسی می‌کنیم. همچنین یک نمونه مثال، از فرم‌های master-details را نیز با هم مرور خواهیم کرد.



افزودن فیلد آپلود تصاویر، به فرم ثبت اطلاعات یک اتاق

در ادامه به کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor که تا این قسمت آن‌را تکمیل کرده‌ایم مراجعه کرده و فیلد جدید InputFile را ذیل قسمت ثبت توضیحات، اضافه می‌کنیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
</div>

@code
{
    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {

    }
}
- ذکر ویژگی multiple در اینجا سبب می‌شود تا بتوان بیش از یک فایل را هربار انتخاب و آپلود کرد.
- در این کامپوننت، رویداد OnChange، پس از تغییر مجموعه‌ی فایل‌های اضافه شده‌ی به آن، فراخوانی می‌شود و آرگومانی از نوع InputFileChangeEventArgs را دریافت می‌کند.


افزودن لیست فایل‌های انتخابی به HotelRoomDTO

تا اینجا اگر به BlazorServer.Models\HotelRoomDTO.cs مراجعه کنیم (کلاسی که مدل UI فرم ثبت اطلاعات اتاق را فراهم می‌کند)، امکان افزودن لیست تصاویر انتخابی به آن وجود ندارد. به همین جهت در این کلاس، تغییر زیر را اعمال می‌کنیم:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class HotelRoomDTO
    {
        // ... 
        public virtual ICollection<HotelRoomImageDTO> HotelRoomImages { get; set; } = new List<HotelRoomImageDTO>();
    }
}
HotelRoomImageDTO را در قسمت قبل اضافه کردیم. متناظر با ICollection فوق، چنین خاصیتی در موجودیت HotelRoom که از نوع <ICollection<HotelRoomImage است نیز تعریف شده‌است تا بتوان به ازای هر اتاق، مشخصات تعدادی تصویر را در بانک اطلاعاتی ذخیره کرد.


تکمیل متد رویدادگردان HandleImageUpload

در ادامه، لیست فایل‌ها‌ی انتخاب شده‌ی توسط کاربر را دریافت کرده و آن‌ها را آپلود می‌کنیم:
@inject IHotelRoomService HotelRoomService
@inject NavigationManager NavigationManager
@inject IJSRuntime JsRuntime
@inject IFileUploadService FileUploadService
@inject IWebHostEnvironment WebHostEnvironment

@code
{
    // ...

    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {
        var files = args.GetMultipleFiles(maximumFileCount: 5);
        if (args.FileCount == 0 || files.Count == 0)
        {
            return;
        }

        var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
        if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
        {
            await JsRuntime.ToastrError("Please select .jpg/.jpeg/.png files only.");
            return;
        }

        foreach (var file in files)
        {
            var uploadedImageUrl = await FileUploadService.UploadFileAsync(file, WebHostEnvironment.WebRootPath, "Uploads");
            HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
        }
    }
}
- در اینجا نیاز به تزریق چند سرویس جدید هست؛ مانند IFileUploadService که در قسمت قبل تکمیل کردیم و سرویس توکار IWebHostEnvironment. به همین جهت به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضاهای نام متناظر زیر را اضافه می‌کنیم:
@using Microsoft.AspNetCore.Hosting
@using System.Linq
@using System.IO
برای مثال سرویس IWebHostEnvironment که از آن برای دسترسی به WebRootPath یا محل قرارگیری پوشه‌ی wwwroot استفاده می‌کنیم، در فضای نام Microsoft.AspNetCore.Hosting قرار دارد و یا متد Path.GetExtension در فضای نام System.IO و متد الحاقی Contains با دو پارامتر استفاده شده، در فضای نام System.Linq قرار دارند.
- متد ()args.GetMultipleFiles، امکان دسترسی به فایل‌های انتخابی توسط کاربر را میسر می‌کند که خروجی آن از نوع <IReadOnlyList<IBrowserFile است. در قسمت قبل، سرویس آپلود فایل‌هایی را که تکمیل کردیم، امکان آپلود یک IBrowserFile را به سرور میسر می‌کند. اگر متد ()GetMultipleFiles را بدون پارامتری فراخوانی کنیم، حداکثر 10 فایل را قبول می‌کند و اگر تعداد بیشتری انتخاب شده باشد، یک استثناء را صادر خواهد کرد.
- سپس بر اساس پسوند فایل‌های دریافتی، آن‌ها را صرفا به فایل‌های تصویری محدود کرده‌ایم.
- در آخر، لیست فایل‌های دریافتی را یکی یکی به سرور آپلود کرده و Url دسترسی به آن‌ها را به لیست HotelRoomImages اضافه می‌کنیم. فایل‌های آپلود شده در پوشه‌ی BlazorServer.App\wwwroot\Uploads قابل مشاهده هستند.


نمایش فایل‌های انتخاب شده‌ی توسط کاربر


در ادامه می‌خواهیم پس از آپلود فایل‌ها، آن‌ها را در ذیل کامپوننت InputFile نمایش دهیم. برای اینکار در ابتدا به فایل wwwroot\css\site.css مراجعه کرده و شیوه نامه‌ی نمایش تصاویر و عناوین آن‌ها را اضافه می‌کنیم:
.room-image {
  display: block;
  width: 100%;
  height: 150px;
  background-size: cover !important;
  border: 3px solid green;
  position: relative;
}

.room-image-title {
  position: absolute;
  top: 0;
  right: 0;
  background-color: green;
  color: white;
  padding: 0px 6px;
  display: inline-block;
}
سپس بر روی لیست HotelRoomModel.HotelRoomImages که در متد HandleImageUpload آن‌را تکمیل کردیم، حلقه‌ای را ایجاد کرده و تصاویر را بر اساس RoomImageUrl آن‌ها، نمایش می‌دهیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
    <div class="row">
    @if (HotelRoomModel.HotelRoomImages.Count > 0)
    {
        var serial = 1;
        foreach (var roomImage in HotelRoomModel.HotelRoomImages)
        {
            <div class="col-md-2 mt-3">
                <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; ">
                   <span class="room-image-title">@serial</span>
                </div>
                <button type="button" class="btn btn-outline-danger btn-block mt-4">Delete</button>
            </div>
            serial++;
        }
    }
    </div>
</div>

ذخیره سازی اطلاعات تصاویر آپلودی یک اتاق در بانک اطلاعاتی

تا اینجا موفق شدیم تصاویر انتخابی کاربر را آپلود کرده و همچنین لیست آن‌ها را نیز نمایش دهیم. در ادامه نیاز است تا این اطلاعات را در بانک اطلاعاتی ثبت کنیم. به همین جهت ابتدا سرویس IHotelRoomImageService را که در قسمت قبل تکمیل کردیم، به کامپوننت جاری تزریق می‌کنیم و سپس با استفاده از متد CreateHotelRoomImageAsync، رکوردهای تصویر متناظر با اتاق ثبت شده را اضافه می‌کنیم:
// ...
@inject IHotelRoomImageService HotelRoomImageService


@code
{
    // ...

    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages)
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
}
در حین آپلود فایل‌ها، فقط خاصیت RoomImageUrl را مقدار دهی کردیم:
HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
در اینجا RoomId هر imageDto را نیز بر اساس Id واقعی اتاق ثبت شده‌ی جاری، تکمیل کرده و سپس آن‌را به CreateHotelRoomImageAsync ارسال می‌کنیم.

محل فراخوانی AddHotelRoomImageAsync فوق، در متد HandleHotelRoomUpsert است که در قسمت‌های قبل تکمیل کردیم. در اینجا پس از ثبت اطلاعات اتاق در بانک اطلاعاتی است که به Id آن دسترسی پیدا می‌کنیم:
private async Task HandleHotelRoomUpsert()
    {
       // ...

       // Create Mode
       var createdRoomDto = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
       await AddHotelRoomImageAsync(createdRoomDto);
       await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully.");

       // ... 
    }
اکنون اگر اطلاعات اتاق جدیدی را تکمیل کرده و تصاویری را نیز به آن انتساب دهیم، با کلیک بر روی دکمه‌ی ثبت، ابتدا اطلاعات این اتاق در بانک اطلاعاتی ثبت شده و Id آن به‌دست می‌آید، سپس رکوردهای تصویر آن جداگانه ذخیره خواهند شد.

یک نکته: در انتهای بحث خواهیم دید که اینکار غیرضروری است و با وجود رابطه‌ی one-to-many تعریف شده‌ی توسط EF-Core، اگر لیست HotelRoomImages موجودیت اتاق تعریف شده و در حال ثبت نیز مقدار دهی شده باشد، به صورت خودکار جزئی از این رابطه و تنها در یک رفت و برگشت، ثبت می‌شود. یعنی همان متد CreateHotelRoomAsync، قابلیت ثبت خودکار اطلاعات خاصیت HotelRoomImages موجودیت اتاق را نیز دارا است.


نمایش تصاویر یک اتاق، در حالت ویرایش رکورد آن

تا اینجا فقط حالت ثبت یک رکورد جدید را پوشش دادیم. در این حالت اگر به لیست اتاق‌های ثبت شده مراجعه کرده و بر روی دکمه‌ی edit یکی از آن‌ها کلیک کنیم، به صفحه‌ی ویرایش رکورد منتقل خواهیم شد؛ اما این صفحه، فاقد اطلاعات تصاویر منتسب به آن رکورد است.
علت اینجا است که در حین ویرایش اطلاعات، در متد OnInitializedAsync، هرچند اطلاعات یک اتاق را از بانک اطلاعاتی دریافت کرده و آن‌را تبدیل به Dto آن می‌کنیم که سبب نمایش جزئیات هر خاصیت در فیلد متصل به آن در فرم جاری می‌شود:
    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
        }
        // ...
    }
اما چون یک رابطه‌ی one-to-many بین اتاق و تصاویر آن برقرار است، نیاز است این رابطه را از طریق eager-loading و فراخوانی متد Include، واکشی کنیم تا اینبار زمانیکه GetHotelRoomAsync فراخوانی می‌شود، به همراه اطلاعات navigation property لیست تصاویر اتاق (HotelRoomImages) نیز باشد.
بنابراین به فایل BlazorServer\BlazorServer.Services\HotelRoomService.cs مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        // ...
 
        public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync()
        {
            return _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .AsAsyncEnumerable();
        }

        public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId)
        {
            return _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
        }
    }
}
در اینجا تنها تغییری که صورت گرفته، استفاده از متد Include(x => x.HotelRoomImages) است؛ تا هنگامیکه اطلاعات یک اتاق را واکشی می‌کنیم، به صورت خودکار اطلاعات تصاویر مرتبط به آن نیز واکشی گردد و سپس توسط AutoMapper، به Dto آن انتساب داده شود (یعنی انتساب HotelRoomImages موجودیت اتاق، به همین خاصیت در DTO آن). این انتساب، سبب به روز رسانی خودکار UI نیز می‌شود. یعنی برای نمایش تصاویر مرتبط با یک اتاق، همان کدهای قبلی که پیشتر داشتیم، هنوز هم کار می‌کنند.


افزودن تصاویر جدید، در حین ویرایش یک رکورد

پس از نمایش لیست تصاویر منتسب به یک اتاق در حال ویرایش، اکنون می‌خواهیم در همین حالت اگر کاربر تصویر جدیدی را انتخاب کرد، این تصویر را نیز به لیست تصاویر ثبت شده‌ی در بانک اطلاعاتی اضافه کنیم. برای اینکار نیز به متد HandleHotelRoomUpsert مراجعه کرده و از متد AddHotelRoomImageAsync در قسمت به روز رسانی آن استفاده می‌کنیم:
private async Task HandleHotelRoomUpsert()
{
   //...

   // Update Mode
   var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel);
   await AddHotelRoomImageAsync(updatedRoomDto);
   await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully.");

   //...
}
مشکل! اگر از این روش استفاده کنیم، هربار به روز رسانی اطلاعات یک جدول، به همراه ثبت رکوردهای تکراری نمایش داده شده‌ی در حالت ویرایش هم خواهند بود. برای مثال فرض کنید سه تصویر را به یک اتاق انتساب داده‌اید. در حالت ویرایش، ابتدا این سه تصویر نمایش داده می‌شوند. بنابراین در لیست HotelRoomModel.HotelRoomImages وجود خواهند داشت. اکنون کاربر دو تصویر جدید دیگر را هم به این لیست اضافه می‌کند. در زمان ثبت، در متد AddHotelRoomImageAsync، بررسی نمی‌کنیم که این تصویر اضافه شده، جدید است یا خیر  و یا همان سه تصویر ابتدای کار نمایش فرم در حالت ویرایش هستند. به همین جهت رکوردها، تکراری ثبت می‌شوند.
برای رفع این مشکل می‌توان در متد AddHotelRoomImageAsync، جدید بودن یک تصویر را بر اساس RoomId آن بررسی کرد. اگر این RoomId مساوی صفر بود، یعنی تازه به لیست اضافه شده‌است و حاصل بارگذاری اولیه‌ی فرم ویرایش اطلاعات نیست:
    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages.Where(x => x.RoomId == 0))
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
در قسمت بعد، کدهای حذف اطلاعات اتاق‌ها و تصاویر مرتبط با هر کدام را نیز تکمیل خواهیم کرد.


یک نکته: متد AddHotelRoomImageAsync اضافی است!

چون از AutoMapper استفاده می‌کنیم، در ابتدای متد ثبت یک اتاق، کار نگاشت DTO، به موجودیت متناظر با آن انجام می‌شود:
public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO)
{
   var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
یعنی در اینجا چون خاصیت مجموعه‌ای HotelRoomImages موجود در HotelRoomDTO با نمونه‌ی مشابه آن در HotelRoom هم نام است، به صورت خودکار توسط AutoMapper به آن انتساب داده می‌شود و چون رابطه‌ی one-to-many در EF-Core تنظیم شده، همینقدر که hotelRoom حاصل، به همراه HotelRoomImages از پیش مقدار مقدار دهی شده‌است، به صورت خودکار آن‌ها را جزئی از اطلاعات همین اتاق ثبت می‌کند.
مقدار دهی RoomId یک تصویر، در اینجا غیرضروری است؛ چون RoomId و Room، به عنوان کلید خارجی این رابطه تعریف شده‌اند که در اینجا Room یک تصویر، دقیقا همین اتاق در حال ثبت است و EF Core در حین ثبت نهایی، آن‌را به صورت خودکار در تمام تصاویر مرتبط نیز مقدار دهی می‌کند.
یعنی نیازی به چندین بار رفت و برگشت تعریف شده‌ی در متد AddHotelRoomImageAsync نیست و اساسا نیازی به آن نیست؛ نه برای ثبت و نه برای ویرایش اطلاعات!


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-17.zip