نگاهی به افزونهی SQL Complete
دوراهی انتخاب NHibernate و Entityframework
- کد نهایی تمیزتر. چون کلاسها را خودتان طراحی میکنید و توسط ابزارها به صورت خودکار تولید نمیشوند، کنترل بیشتر و نهایتا کیفیت بالاتری دارند.
- ساده است. درگیر نگهداری edmx modelها نخواهید بود. به روز رسانی بانک اطلاعاتی آن هم میتواند کاملا خودکار شود.
2 - دیتاست که کلا کارآیی بالایی نداره. اما ... نهایتا مطمئن هستم خروجی EF (به همراه تمام best practices لحاظ شده در آن) سرعت بالاتری از کلاسهای دست ساز sql helper موجود در وب دارد. برای مثال سطح اول کش آن خیلی از کوئریها را مجددا به بانک اطلاعاتی ارسال نمیکند. قابلیت اجرای به تعویق افتاده کوئریهای لینک امکان تهیه کوئریهای بسیار پیچیده را در یک رفت و برگشت مهیا میکند. کاری که با sql helperهای معمولی نیازی به چندبار رفت و برگشت دارد. قابلیتهای lazy loading آن میتواند مصرف حافظه و بار سرور را درصورت استفاده صحیح کاهش دهد. کوئریهای آن strongly typed و پارامتری هستند (تحت نظر کامپایلر + امنیت + سرعت (کوئریهای پارامتری مانند رویههای ذخیره شده کش میشوند)). به صورت پیش فرض از تراکنشها استفاده میکند و ... خیلی از الگوهای مفید دیگر که چندین سال باید وقت صرف کنید تا نمونه آنها را پیاده سازی کنید. یعنی کار اصولی با بانک اطلاعاتی صرفا یک select ساده نیست که بر اساس آن کارآیی و یا بهتر بودن روشی را مشخص کنید.
// -- FilteredInclude_EFCore5 var list = dbContext.Blogs .AsNoTracking() .Include(p => p.Posts.Where(p => p.Title.Contains("test title"))) .ToList(); return Json(list);
System.InvalidOperationException: 'Lambda expression used inside Include is not valid.'
// -- NonFilteredInclude var list = dbContext.Blogs .AsNoTracking() .Include(e => e.Posts) .ToList(); list.ForEach(p => p.Posts = p.Posts.Where(p => p.Title.Contains("test title")).ToList());
// -- Projection_Manually var list = dbContext.Blogs .AsNoTracking() .Select(p => new { p.Id, p.Name, Posts = p.Posts.Where(p => p.Title.Contains("test title")).ToList() }).ToList();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Description], [t].[Title] FROM [Blogs] AS [b] LEFT JOIN ( SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title] FROM [Posts] AS [p] WHERE CHARINDEX(N'test title', [p].[Title]) > 0 ) AS [t] ON [b].[Id] = [t].[BlogId] ORDER BY [b].[Id], [t].[Id]
- در صورت نیاز به ویرایش (عدم استفاده از AsNoTracking) بدلیل استفاده از anonymous بجای Blog، هیچ شیء Blog ایی در ChangeTracker ثبت نخواهد شد، ولی اشیا Post در ChangeTracker ثبت میشوند. در نتیجه تنها 1 شیء در ChangeTracker اضافه خواهد شد.
- کد نویسی را کثیف میکند؛ مخصوصا اگر نیاز به شرط گذاری بر روی چندین Navigation Collection تو در تو را داشته باشید.
// -- IncludeFilter_EFCorePlus var list = dbContext.Blogs .AsNoTracking() .IncludeFilter(e => e.Posts.Where(p => p.Title.Contains("test tile"))) .ToList();
-- EF+ Query Future: 1 of 2 SELECT [b].[Id], [b].[Name] FROM [Blogs] AS [b] ; -- EF+ Query Future: 2 of 2 SELECT [t].[Id], [t].[BlogId], [t].[Description], [t].[Title] FROM [Blogs] AS [b] INNER JOIN ( SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title] FROM [Posts] AS [p] WHERE CHARINDEX(N'test title', [p].[Title]) > 0 ) AS [t] ON [b].[Id] = [t].[BlogId] ;
- همانطور که میبینید این دستور، 2 کوئری را اجرا میکند. سرعت آن از روش قبلی کمی کندتر است و memory allocation بیشتری را انجام میدهد.
- در صورت عدم استفاده از AsNoTracking، اشیاء Blog را نیز ثبت میکند؛ درنتیجه تعداد 101 آبجکت (100 Blog و 1 Post) به ChangeTracker اضافه خواهند شد.
- کد نویسی تمیزتر و راحتتری در سمت سی شارپ دارد.
- این روش در EF6 نیز قابل استفاده است.
// -- FilteredInclude_EFCore5 var list = dbContext.Blogs .AsNoTracking() .Include(p => p.Posts.Where(p => p.Title.Contains("test title"))) .ToList();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Description], [t].[Title] FROM [Blogs] AS [b] LEFT JOIN ( SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title] FROM [Posts] AS [p] WHERE CHARINDEX(N'test title', [p].[Title]) > 0 ) AS [t] ON [b].[Id] = [t].[BlogId] ORDER BY [b].[Id], [t].[Id]
- این روش بسیار بهینه است و از روش قبلی (دوم) کمی سریعتر بوده و memory allocation کمتری (نزدیک به روش اول) دارد.
- در صورت عدم استفاده از AsNoTracking، مانند قبلی عمل میکند؛ درنتیجه تعداد 101 آبجکت به ChangeTracker اضافه خواهند شد.
- کد نویسی تمیزتر و راحتتری در سمت سی شارپ دارد.
آشنایی اولیه با gRPC
syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
Google.Protobuf grpc Grpc.Tools
<ItemGroup> <Protobuf Include="helloworld.proto" /> </ItemGroup>
using System; using System.Collections.Generic; using System.Threading.Tasks; using Helloworld; using Grpc.Core; namespace ServerGrpc { class GreeterImpl : Greeter.GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { System.Console.WriteLine("request made!"); return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } } class Program { const int Port = 50051; public static void Main(string[] args) { Server server = new Server { Services = { Greeter.BindService(new GreeterImpl()) }, Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) } }; server.Start(); Console.WriteLine("Greeter server listening on port " + Port); Console.WriteLine("Press any key to stop the server..."); Console.ReadKey(); server.ShutdownAsync().Wait(); } } }
obj/Debug/netcoreapp2.2(یا نسخهی دیگری که استفاده میکنید)
protoc helloworld.proto --go_out=plugins=grpc:.
package main import ( "fmt" "golang.org/x/net/context" "google.golang.org/grpc" "gosample/proto" ) func main() { initial() } func initial(){ conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure()) defer conn.Close() client := helloworld.NewGreeterClient(conn) data, _ := client.SayHello(context.Background(), &helloworld.HelloRequest{Name : "Ali"}) fmt.Println(data) }
ماهیت این پایگاه داده وب سرویسی مبتنی بر REST است و فرمت اطلاعاتی که از سرور دریافت میشود، JSON است.
گام اول: باید آخرین نسخه RavenDB را دریافت کنید. همان طور که مشاهده میکنید، ویرایشهای مختلف کتابخانه هایی که برای نسخه Client و همچنین Server طراحی شده است، دراین فایل قرار گرفته است.
برای راه اندازی Server باید فایل Start را اجرا کنید، چند ثانیه بعد محیط مدیریتی آن را در مرورگر خود مشاهده میکنید. در بالای صفحه روی لینک Databases کلیک کنید و در صفحه باز شده گزینه New Database را انتخاب کنید. با دادن یک نام دلخواه حالا شما یک پایگاه داده ایجاد کرده اید. تا همین جا دست نگه دارید و اجازه دهید با این محیط دوست داشتنی و قابلیتهای آن بعدا آشنا شویم.
در گام دوم به Visual Studio میرویم و نحوه ارتباط با پایگاه داده و استفاده از دستورات آن را فرا میگیریم.
گام دوم:
با یک پروژه Test شروع میکنیم که در هر گام تکمیل میشود و میتوانید پروژه کامل را در پایان این پست دانلود کنید.
برای استفاده از کتابخانههای مورد نیاز دو راه وجود دارد:
- استفاده از NuGet : با استفاده از دستور زیر Package مورد نیاز به پروژه شما افزوده میشود.
PM> Install-Package RavenDB -Version 1.0.919
- اضافه کردن کتابخانهها به صورت دستی : کتابخانههای مورد نیاز شما در همان فایلی که دانلود شده بود و در پوشه Client قرار دارند.
کتابخانه هایی را که NuGet به پروژه من اضافه کرد، در تصویر زیر مشاهده میکنید :
با Newtonsoft.Json در اولین بخش بحث آشنا شدید. NLog هم یک کتابخانه قوی و مستقل برای مدیریت Log است که این پایگاه داده از آن بهره برده است.
" دلیل اینکه از پروژه تست استفاده کردم ؛ تمرکز روی کدها و مشاهده تاثیر آنها ، مستقل از UI و لایههای دیگر نرم افزار است. بدیهی است که استفاده از آنها در هر پروژه امکان پذیر است. "
برای شروع نیاز به آدرس Server و نام پایگاه داده داریم که میتوانید در App.config به عنوان تنظیمات نرم افزار شما ذخیره شود و هنگام اجرای نرم افزار مقدار آنها را خوانده و در متغییرهای readonly ذخیره شوند.
<appSettings> <add key="ServerName" value="http://SorousH-HP:8080/"/> <add key="DatabaseName" value="TestDatabase" /> </appSettings>
هنگامی که صفحه Management Studio در مرورگر باز است، میتوانید از نوار آدرس مرورگر خود آدرس سرور را به دست آورید.
[TestClass] public class BeginnerTest { private readonly string serverName; private readonly string databaseName; public BeginnerTest() { serverName = ConfigurationManager.AppSettings["ServerName"]; databaseName = ConfigurationManager.AppSettings["DatabaseName"]; } }
برای برقراری ارتباط با پایگاه داده نیاز به یک شئ از جنس DocumentStore و جهت انجام عملیات مختلف ( ذخیره، حذف و ... ) نیاز به یک شئ از جنس IDocumentSession است. کد زیر، نحوه کار با آنها را به شما نشان میدهد :
[TestClass] public class BeginnerTest { private readonly string serverName; private readonly string databaseName; private DocumentStore documentStore; private IDocumentSession session; public BeginnerTest() { serverName = ConfigurationManager.AppSettings["ServerName"]; databaseName = ConfigurationManager.AppSettings["DatabaseName"]; } [TestInitialize] public void TestStart() { documentStore = new DocumentStore { Url = serverName }; documentStore.Initialize(); session = documentStore.OpenSession(databaseName); } [TestCleanup] public void TestEnd() { session.SaveChanges(); documentStore.Dispose(); session.Dispose(); } }
در طراحی این پایگاه داده از اگوی Unit Of Work استفاده شده است. به این معنی که تمام تغییرات در حافظه ذخیره میشوند و به محض اجرای دستور ;()session.SaveChanges ارتباط برقرار شده و تمام تغییرات ذخیره خواهند شد.
هنگام شروع ( تابع : TestStart ) متغییر session مقدار دهی میشود و در پایان کار ( تابع : TestEnd ) تغییرات ذخیره شده و منابعی که توسط این دو شئ در حافظه استفاده شده است، رها میشود.
البته بر مبنای طراحی شما، دستور ;()session.SaveChanges میتواند پس از انجام هر عملیات اجرا شود.
class User { public int Id { get; set; } public string Name { get; set; } public string Address { get; set; } public int Zip { get; set; }اهی }
[TestMethod] public void Insert() { var user = new User { Id = 1, Name = "John Doe", Address = "no-address", Zip = 65826 }; session.Store(user); }
لحظهی لذت بخشی است...
یکی از روشهای خواندن اطلاعات هم به صورت زیر است:
[TestMethod] public void Select() { var user = session.Load<User>(1); }
تا این جا، سادهترین مثالهای ممکن را مشاهده کردید و حتما در بحث بعد مثالهای جالبتر و دقیقتری را بررسی میکنیم و همچنین نگاهی به جزئیات طراحی و قراردادهای از پیش تعیین شده میاندازیم.
- نسخه بدون کتابخانههای موردنیاز ( 2 مگابایت ) : RavenDBTest_Small.zip
- نسخه کامل ( 15 مگابایت ) : RavenDBTest.zip
RavenDB؛ تجربه متفاوت از پایگاه داده
LINQPad برای تست کردن سریع عبارات LINQ فوق العاده است. با استفاده از آن بدون نیاز به VS.NET خیلی سریع و در عرض چند ثانیه میتونید عبارات LINQ خودتون رو نوشته و تست کنید. این LINQ میتونه LINQ to Objects باشه یا LINQ to SQL یا LINQ to Entities و غیره.
خلاصه چیزی شبیه به management studio مخصوص SQL Server را تصور کنید که اینبار بجای SQL نویسی، LINQ مینویسید، حاصل را نمایش میدهد؛ علاوه بر آن خروجی SQL تولیدی و حتی IL نهایی را هم نمایش میدهد که برای دیباگ بسیار مفید است.
به همراه آن یک سری مثال هم وجود دارد که جهت فراگیری LINQ یا حتی استفاده از آنها به عنوان مرجع بینظیر است.
افزودن فیلد آپلود تصاویر، به فرم ثبت اطلاعات یک اتاق
در ادامه به کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor که تا این قسمت آنرا تکمیل کردهایم مراجعه کرده و فیلد جدید InputFile را ذیل قسمت ثبت توضیحات، اضافه میکنیم:
<div class="form-group"> <InputFile OnChange="HandleImageUpload" multiple></InputFile> </div> @code { private async Task HandleImageUpload(InputFileChangeEventArgs args) { } }
- در این کامپوننت، رویداد 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>(); } }
تکمیل متد رویدادگردان 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 }); } } }
@using Microsoft.AspNetCore.Hosting @using System.Linq @using System.IO
- متد ()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; }
<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); } } }
HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
محل فراخوانی 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."); // ... }
یک نکته: در انتهای بحث خواهیم دید که اینکار غیرضروری است و با وجود رابطهی 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); } // ... }
بنابراین به فایل 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); } } }
افزودن تصاویر جدید، در حین ویرایش یک رکورد
پس از نمایش لیست تصاویر منتسب به یک اتاق در حال ویرایش، اکنون میخواهیم در همین حالت اگر کاربر تصویر جدیدی را انتخاب کرد، این تصویر را نیز به لیست تصاویر ثبت شدهی در بانک اطلاعاتی اضافه کنیم. برای اینکار نیز به متد 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."); //... }
برای رفع این مشکل میتوان در متد 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);
مقدار دهی RoomId یک تصویر، در اینجا غیرضروری است؛ چون RoomId و Room، به عنوان کلید خارجی این رابطه تعریف شدهاند که در اینجا Room یک تصویر، دقیقا همین اتاق در حال ثبت است و EF Core در حین ثبت نهایی، آنرا به صورت خودکار در تمام تصاویر مرتبط نیز مقدار دهی میکند.
یعنی نیازی به چندین بار رفت و برگشت تعریف شدهی در متد AddHotelRoomImageAsync نیست و اساسا نیازی به آن نیست؛ نه برای ثبت و نه برای ویرایش اطلاعات!
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-17.zip