However, it is clear that Microsoft’s future direction is in the Core space . Yes, the last update to the full .NET framework did include improvements for ASP.NET and WebForms, but clearly the future innovation and hard work will be in the new core frameworks like .NET core, ASP.NET Core, Entity Framework core, and whatever other cores come along in the future.
WCF
- مقایسهای کوتاه بین WCF و ASMX
- نحوه استفاده از TransactionFlow در WCF
- مدیریت Instance در WCF
- WCF Method Overloading
- مدیریت تغییرات در سیستمهای مبتنی بر WCF
- آشنایی با KnownTypeAttribute در WCF
- Data Contracts and Circular References
- استفاده از Lambda Expression در پروژههای مبتنی بر WCF
- فراخوانی سرویسهای WCF به صورت Async
- مقایسه بین Proxy و ChannelFactory در WCF
- بررسی متدهای یک طرفه در WCF
- Message Header سفارشی در WCF
- MTOM در WCF
- اعتبارسنجی سرویسهای WCF
- تغییر فضای نام کلاس poco استفاده شده در WCF و از کار افتادن برنامهی مشتری بدون دریافت پیام خطا
- پیاده سازی ServiceHostFactory سفارشی در WCF
- پیاده سازی InstanceProvider برای سرویسهای WCF
- Long Polling در WCF
- Routing Service در WCF
- فیلترها در WCF Routing Service
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 1
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 2
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 3
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 4
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 5
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 6
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 7
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 8
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 9
- ایجاد سرویس چندلایهی WCF با Entity Framework در قالب پروژه - 10
DocerMaster : OS : CentOS7 IP: 192.168.64.3 DockerWorker: OS: CentOS7 IP: 192.168.64.4
sudo yum install docker
$ sudo service docker start $ sudo systemctl start docker.service
firewall-cmd --permanent --add-port=2376/tcp firewall-cmd --permanent --add-port=2377/tcp firewall-cmd --permanent --add-port=7946/tcp firewall-cmd --permanent --add-port=7946/udp firewall-cmd --permanent --add-port=4789/udp firewall-cmd --permanent --add-port=80/tcp firewall-cmd --reload
systemctl restart docker
~]# firewall-cmd --permanent --add-port=2376/tcp ~]# firewall-cmd --permanent --add-port=7946/tcp ~]# firewall-cmd --permanent --add-port=7946/udp ~]# firewall-cmd --permanent --add-port=4789/udp ~]# firewall-cmd --permanent --add-port=80/tcp ~]# firewall-cmd --reload ~]# systemctl restart docker
sudo docker swarm init –advertise-addr 192.168.64.3
همانطور که مشاهده میکنید، پس از راه اندازی، اعلانی مبنی بر اینکه این نود به عنوان Manager شناخته شده و اینکه برای اضافه کردن یک نود Worker چه دستوری را باید اجرا کرد، نمایش داده شدهاست.
اکنون کافیاست این خط کد را در نود Worker کپی کنیم:
بعد از موفقیت آمیز بودن اجرای آن، میتوانید در کامپیوتر Master، با دستور زیر تمام نودها را مشاهده کنید:
$ sudo docker node ls
همانطور که مشاهده میکنید، دو نود وجود دارد که یکی به عنوان Leader شناخته میشود. هر زمانی که نیاز بود، میشود به راحتی یک Worker دیگر را اضافه کرد.
برای راه اندازی یک کانتینر، swarm از CLI کاملی برخوردار هست؛ اما مایلم اینجا از یک ابزار خوب، برای مدیریت Swarm استفاده کنم. Portainer به عنوان یه ابزار عالی برای مدیریت Imageها و Containerهای داکر محسوب میشود که کاملا swarm را پشتیبانی میکند.
برای راه اندازی portainer کافی است کد زیر را در سیستم Master اجرا کنید:
$ docker volume create portainer_data $ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer
البته به دلیل عدم دسترسی به داکر هاب از کشور ایران، عملا امکان pull کردن این image، مستقیما از داکر هاب و بدون وی پی ان وجود ندارد.
بعد از موفقیت آمیز بودن راه اندازی portainer میتوانید از طریق آدرس http://192.168.64.3:9000 به آن دسترسی داشته باشید. در اولین ورود، پسورد ادمین را تنظیم میکنید و بعد از وارد شدن، صفحهای مطابق شکل زیر را خواهید دید:
اگر بر روی منوی swarm کلیک کنید، همهی نودها را مشاهده خواهید کرد و در صورتیکه بر روی Containers کلیک کنید، همهی Container هایی را که بر روی این سرور وجود دارند، خواهید دید. مهمترین قسمت، بخش Service هاست که مشخصات Container هایی که روی swarm توزیع شدن را نشان میدهد و همینطور تعداد Container هایی از این image که Scale شدند. همینطور که میبینید فعلا فقط همین Portainer در حال اجراست.
اجازه دهید یک مثال کاربردیتر بزنیم و یک سرویس را ایجاد کنیم.
من بر روی کامپیوتر شخصیام و نه سرورها، با دستور زیر یک پروژهی MVC را با دات نت Core ایجاد میکنم:
dotnet new mvc
و سپس دستور dotnet publish را اجرا میکنم و به پوشهای که محتویات پابلیش شده در آن قرار دارند رفته و یک فایل بدون پسوند را به نام dockerfile ایجاد میکنم و متن زیر را در آن مینویسم:
همینطور که میبینید من از image مخصوص اجرای دات نت Core در این container استفاده میکنم. پوشهی کانتینر را تنظیم میکنم و همهی فایلهایی که در پوشهی جاری سیستم خودم وجود دارند را به پوشهی جاری کانتینر منتقل میکنم و سپس دستور دات نت را با پارامتر اسم dll پروژهام اجرا میکنم. این کل محتویات فایل داکر من هست.
ترمینال را در همین پوشهی publish باز میکنم و دستور زیر را اجرا میکنم:
docker build –t swarmtest:dev .
docker save swarmtest:dev –o swarmtest.tar
طبق شکل زیر یک فایل tar که حاوی image برنامه من هست، ایجاد شد:
حالا با دستور زیر این فایل رو به سرور Master منتقل میکنم:
scp –r swarmtest.tar root@192.168.64.3:/srv/images
همانطور که میبینید، فایل tar به پوشهای که قبلا در سرور ایجاد کردم، منتقل شد.
حالا به سرور و پوشهای که فایل tar آنجا قرار دارد رفته و با دستور زیر این image را بر روی سیستم load میکنم:
sudo docker load –i swarmtest.tar
همانطور که در تصویر میبینید، بعد از load شدن، image مورد نظرمان به داکر اضافه شدهاست.
حالا برای اجرا کردن این سرویس بر روی swarm، آدرس portainer را باز میکنیم و به قسمت services میرویم و بر روی دکمهی add service کلیک میکنیم:
در قسمت نام، نام سرویس و در قسمت imageConfiguration از منوی imageها، ایمیجی را که ایجاد کردیم، انتخاب میکنیم. در قسمت Replicas تعداد instanceهای container ای را که میخواهیم از روی image ایجاد شوند، مشخص میکنیم. (این قسمت را بر روی هر وضعیتی میتوانیم قرار دهیم و زیاد و کم کنیم) و در قسمت port mapping، پورت درون Container و پورتی را که میخواهیم بر روی هاست به نمایش درآید، وارد میکنیم.
همانطور که میبینید من به راحتی میتوانم تعداد Containerها را Scale کنم و نگرانیای بابت load balancing و اینکه کدام container بر روی کدوم سرور ایجاد میشود، نخواهم داشت.
برای نمایش برنامه کافی است پورتی را که برای هاست وارد کردیم، با آی پی Master وارد کنیم:
EF Code First #12
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddNodeServices(); }
حال میتوانید به وهلهای از اینترفیس INodeServices، از طریق تزریق وابستگیها دسترسی داشته باشید. اینترفیس INodeServices یک Api میباشد و مشخص میکند که کدام قطعه از کد NET. میتواند کد جاوااسکریپتی را که در محیط Node اجرا میشود، فراخوانی کند. همچنین میتوانید از خاصیت FromServices برای دریافت وهلهای از اینترفیس INodeServices در اکشن خود استفاده نمایید.
public async Task<IActionResult> Add([FromServices] INodeServices nodeServices) { var num1 = 10; var num2 = 20; var result = await nodeServices.InvokeAsync<int>("AddModule.js", num1, num2); ViewData["ResultFromNode"] = $"Result of {num1} + {num2} is {result}"; return View(); }
سپس کد جاوااسکریپتی متناظر با تابعی را که توسط متد InvokeAsync فراخوانی میشود، به صورت زیر مینویسیم:
module.exports = function(callback, num1, num2) { var result = num1 + num2; callback(null, result); };
حال بیاییم مثالی دیگر را مرور کنیم. میخواهیم از صفحه وب درخواستی، عکسی را تهیه کنیم. بدین منظور از کتابخانه url-to-image استفاده میکنیم. برای نصب آن دستور npm install --save url-to-image را در خط فرمان تایپ میکنیم.
بعد از اتمام نصب این بسته، متدی را برای دریافت اطلاعات ارسالی این کتابخانه تدارک میبینیم.
[HttpPost] public async Task<IActionResult> GenerateUrlPreview([FromServices] INodeServices nodeServices) { var url = Request.Form["Url"].ToString(); var fileName = System.IO.Path.ChangeExtension(DateTime.UtcNow.Ticks.ToString(), "jpeg"); var file = await nodeServices.InvokeAsync<string>("UrlPreviewModule.js", url, System.IO.Path.Combine("PreviewImages", fileName)); return Content($"/Home/Download?img={fileName}"); } public IActionResult Download() { var image = Request.Query["img"].ToString(); var fileName = System.IO.Path.Combine("PreviewImages", image); var isExists = System.IO.File.Exists(fileName); if (isExists) { Response.Headers.Add($"Content-Disposition", "attachment; filename=\"" + image + "\""); var bytes = System.IO.File.ReadAllBytes(fileName); return File(bytes, "image/jpeg"); } else { return NotFound(); } }
سپس متد UrlPreviewModule.js را به صورت زیر مینویسیم:
var urlToImage = require('url-to-image'); module.exports = function (callback, url, imageName) { urlToImage(url, imageName).then(function () { callback(null, imageName); }).catch(function (err) { callback(err, imageName); }); };
سرویسهای Node به توسعه دهندگان ASP.NET Core امکان استفاده از اکوسیستم NPM را که دارای قابلیتهای فراوانی میباشد، میدهد.
خلاصه مهاجرت داده پروفایل ها
- کلاس جدیدی بسازید که دارای خواصی برای ذخیره اطلاعات پروفایل است.
- کلاس جدیدی بسازید که از 'ProfileBase' ارث بری میکند و متدهای لازم برای دریافت پروفایل کاربران را پیاده سازی میکند.
- استفاده از تامین کنندههای پیش فرض را، در فایل web.config فعال کنید. و کلاسی که در مرحله 2 ساختید را بعنوان کلاس پیش فرض برای خواندن اطلاعات پروفایل معرفی کنید.
شروع به کار
پوشه جدیدی با نام 'Models' بسازید تا اطلاعات پروفایل را در آن قرار دهیم.
بعنوان یک مثال، بگذارید تا تاریخ تولد کاربر، شهر سکونت، قد و وزن او را در پروفایلش ذخیره کنیم. قد و وزن بصورت یک کلاس سفارشی (custom class) بنام 'PersonalStats' ذخیره میشوند. برای ذخیره و بازیابی پروفایل ها، به کلاسی احتیاج داریم که 'ProfileBase' را ارث بری میکند. پس کلاس جدیدی با نام 'AppProfile' بسازید.
public class ProfileInfo { public ProfileInfo() { UserStats = new PersonalStats(); } public DateTime? DateOfBirth { get; set; } public PersonalStats UserStats { get; set; } public string City { get; set; } } public class PersonalStats { public int? Weight { get; set; } public int? Height { get; set; } } public class AppProfile : ProfileBase { public ProfileInfo ProfileInfo { get { return (ProfileInfo)GetPropertyValue("ProfileInfo"); } } public static AppProfile GetProfile() { return (AppProfile)HttpContext.Current.Profile; } public static AppProfile GetProfile(string userName) { return (AppProfile)Create(userName); } }
پروفایل را در فایل web.config خود فعال کنید. نام کلاسی را که در مرحله قبل ساختید، بعنوان کلاس پیش فرض برای ذخیره و بازیابی پروفایلها معرفی کنید.
<profile defaultProvider="DefaultProfileProvider" enabled="true" inherits="UniversalProviders_ProfileMigrations.Models.AppProfile"> <providers> ..... </providers> </profile>
برای دریافت اطلاعات پروفایل از کاربر، فرم وب جدیدی در پوشه Account بسازید و آنرا 'AddProfileData.aspx' نامگذاری کنید.
<h2> Add Profile Data for <%# User.Identity.Name %></h2> <asp:Label Text="" ID="Result" runat="server" /> <div> Date of Birth: <asp:TextBox runat="server" ID="DateOfBirth"/> </div> <div> Weight: <asp:TextBox runat="server" ID="Weight"/> </div> <div> Height: <asp:TextBox runat="server" ID="Height"/> </div> <div> City: <asp:TextBox runat="server" ID="City"/> </div> <div> <asp:Button Text="Add Profile" ID="Add" OnClick="Add_Click" runat="server" /> </div>
کد زیر را هم به فایل code-behind اضافه کنید.
protected void Add_Click(object sender, EventArgs e) { AppProfile profile = AppProfile.GetProfile(User.Identity.Name); profile.ProfileInfo.DateOfBirth = DateTime.Parse(DateOfBirth.Text); profile.ProfileInfo.UserStats.Weight = Int32.Parse(Weight.Text); profile.ProfileInfo.UserStats.Height = Int32.Parse(Height.Text); profile.ProfileInfo.City = City.Text; profile.Save(); }
دقت کنید که فضای نامی که کلاس AppProfile در آن قرار دارد را وارد کرده باشید.
اپلیکیشن را اجرا کنید و کاربر جدیدی با نام 'olduser' بسازید. به صفحه جدید 'AddProfileData' بروید و اطلاعات پروفایل کاربر را وارد کنید.
با استفاده از پنجره Server Explorer میتوانید تایید کنید که اطلاعات پروفایل با فرمت xml در جدول 'Profiles' ذخیره میشوند.
مهاجرت الگوی دیتابیس
اسکریپت مورد نیاز را از آدرس https://raw.github.com/suhasj/UniversalProviders-Identity-Migrations/master/Migration.txt دریافت کرده و آن را اجرا کنید. اگر اتصال خود به دیتابیس را تازه کنید خواهید دید که جداول جدیدی اضافه شده اند. میتوانید دادههای این جداول را بررسی کنید تا ببینید چگونه اطلاعات منتقل شده اند.
مهاجرت اپلیکیشن برای استفاده از ASP.NET Identity
- Microsoft.AspNet.Identity.EntityFramework
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Facebook
- Microsoft.Owin.Security.Google
- Microsoft.Owin.Security.MicrosoftAccount
- Microsoft.Owin.Security.Twitter
using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Collections.Generic; using System.Linq; using System.Web; using UniversalProviders_ProfileMigrations.Models; namespace UniversalProviders_Identity_Migrations { public class User : IdentityUser { public User() { CreateDate = DateTime.UtcNow; IsApproved = false; LastLoginDate = DateTime.UtcNow; LastActivityDate = DateTime.UtcNow; LastPasswordChangedDate = DateTime.UtcNow; Profile = new ProfileInfo(); } public System.Guid ApplicationId { get; set; } public bool IsAnonymous { get; set; } public System.DateTime? LastActivityDate { get; set; } public string Email { get; set; } public string PasswordQuestion { get; set; } public string PasswordAnswer { get; set; } public bool IsApproved { get; set; } public bool IsLockedOut { get; set; } public System.DateTime? CreateDate { get; set; } public System.DateTime? LastLoginDate { get; set; } public System.DateTime? LastPasswordChangedDate { get; set; } public System.DateTime? LastLockoutDate { get; set; } public int FailedPasswordAttemptCount { get; set; } public System.DateTime? FailedPasswordAttemptWindowStart { get; set; } public int FailedPasswordAnswerAttemptCount { get; set; } public System.DateTime? FailedPasswordAnswerAttemptWindowStart { get; set; } public string Comment { get; set; } public ProfileInfo Profile { get; set; } } }
انتقال داده پروفایلها به جداول جدید
آخرین نسخه پکیج Entity Framework را نصب کنید. همچنین یک رفرنس به اپلیکیشن وب پروژه بدهید (کلیک راست روی پروژه و گزینه 'Add Reference').
کد زیر را در کلاس Program.cs وارد کنید. این قطعه کد پروفایل تک تک کاربران را میخواند و در قالب 'ProfileInfo' آنها را serialize میکند و در دیتابیس ذخیره میکند.
public class Program { var dbContext = new ApplicationDbContext(); foreach (var profile in dbContext.Profiles) { var stringId = profile.UserId.ToString(); var user = dbContext.Users.Where(x => x.Id == stringId).FirstOrDefault(); Console.WriteLine("Adding Profile for user:" + user.UserName); var serializer = new XmlSerializer(typeof(ProfileInfo)); var stringReader = new StringReader(profile.PropertyValueStrings); var profileData = serializer.Deserialize(stringReader) as ProfileInfo; if (profileData == null) { Console.WriteLine("Profile data deserialization error for user:" + user.UserName); } else { user.Profile = profileData; } } dbContext.SaveChanges(); }
برخی از مدلهای استفاده شده در پوشه 'IdentityModels' تعریف شده اند که در پروژه اپلیکیشن وبمان قرار دارند، بنابراین افزودن فضاهای نام مورد نیاز فراموش نشود.
کد بالا روی دیتابیسی که در پوشه App_Data وجود دارد کار میکند، این دیتابیس در مراحل قبلی در اپلیکیشن وب پروژه ایجاد شد. برای اینکه این دیتابیس را رفرنس کنیم باید رشته اتصال فایل app.config اپلیکیشن کنسول را بروز رسانی کنید. از همان رشته اتصال web.config در اپلیکیشن وب پروژه استفاده کنید. همچنین آدرس فیزیکی کامل را در خاصیت 'AttachDbFilename' وارد کنید.
یک Command Prompt باز کنید و به پوشه bin اپلیکیشن کنسول بالا بروید. فایل اجرایی را اجرا کنید و نتیجه را مانند تصویر زیر بررسی کنید.
در پنجره Server Explorer جدول 'AspNetUsers' را باز کنید. حال ستونهای این جدول باید خواص کلاس مدل را منعکس کنند.
کارایی سیستم را تایید کنید
روشهای مختلف اطلاع رسانی به سیستم ردیابی تغییرات
متد DbSet.Add کار اطلاع رسانی تبدیل وهلههای ثبت شده را به کوئریهای Insert رکوردهای جدید، انجام میدهد:
using (var db = new BloggingContext()) { var blog = new Blog { Url = "http://sample.com" }; db.Blogs.Add(blog); db.SaveChanges(); }
سیستم ردیابی اطلاعات، اگر تغییراتی را در خواص اشیاء تحت نظر خود مشاهده کند، سبب تولید کوئریهای Update میگردد. یک چنین اشیایی تحت نظر Context هستند:
الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شدهاند.
ب) اشیایی که در طول عمر Context به آن اضافه شدهاند (حالت قبل).
using (var db = new BloggingContext()) { var blog = db.Blogs.First(); blog.Url = "http://sample.com/blog"; db.SaveChanges(); }
و متد DbSet.Remove کار اطلاع رسانی تبدیل وهلههای حذف شده را به کوئریهای Delete معادل، انجام میدهد:
using (var db = new BloggingContext()) { var blog = db.Blogs.First(); db.Blogs.Remove(blog); db.SaveChanges(); }
به علاوه امکان ترکیب متدهای Add، Remove و همچنین به روز رسانی اشیاء در طی یک Context و با فراخوانی یک SaveChanges در انتهای کار نیز وجود دارد. از این جهت که یک Context، الگوی واحد کار را پیاده سازی میکند و بیانگر یک تراکنش است. در این حالت ترکیبی، یا کل تراکنش با موفقیت به پایان میرسد و یا در صورت بروز مشکلی، هیچکدام از تغییرات درخواستی، اعمال نخواهند شد.
عملیات ردیابی، بر روی هر نوع Projections صورت نمیگیرد
اگر توسط LINQ Projections، نتیجهی نهایی کوئری را تغییر دادید، فقط در زمانی سیستم ردیابی بر روی آن فعال خواهد بود که projection نهایی حاوی اصل موجودیت مدنظر باشد. برای مثال در کوئری ذیل چون در Projection صورت گرفتهی در متد Select، هنوز در خاصیت Blog، به اصل موجودیت Blog اشاره میشود، نتیجهی این کوئری نیز تحت نظر سیستم ردیابی خواهد بود:
using (var context = new BloggingContext()) { var blog = context.Blogs .Select(b => new { Blog = b, Posts = b.Posts.Count() }); }
using (var context = new BloggingContext()) { var blog = context.Blogs .Select(b => new { Id = b.BlogId, Url = b.Url }); }
لغو سیستم ردیابی تغییرات، در زمانیکه به آن نیازی نیست
سیستم ردیابی تغییرات بر اساس مفاهیم AOP و تولید پروکسیهای آن کار میکند. این پروکسیها، اشیایی شفاف هستند که اشیاء شما را احاطه میکنند و هر تغییری را که اعمال میکنید، ابتدا از این غشاء رد شده و در سیستم ردیابی EF ثبت میشوند. سپس به وهلهی اصلی شیء موجود اعمال خواهند شد.
بدیهی است تولید این پروکسیها، دارای سربار است و اگر هدف شما صرفا کوئری گرفتن از اطلاعات، جهت نمایش آنها است، نیازی به تولید خودکار این پروکسیها را ندارید و این مساله سبب کاهش مصرف حافظهی برنامه و بالا رفتن سرعت آن میشود.
در قسمت قبل عنوان شد که «یک چنین اشیایی تحت نظر Context هستند: الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شدهاند.»
اگر میخواهید این حالت پیش فرض را لغو کنید، از متد AsNoTracking استفاده نمائید:
using (var context = new BloggingContext()) { var blogs = context.Blogs.AsNoTracking().ToList(); }
اگر میخواهید متد AsNoTracking را به صورت خودکار به تمام کوئریهای یک context خاص اعمال کنید، روش کار و تنظیم آن به صورت زیر است:
using (var context = new BloggingContext()) { context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
نکات به روز رسانی ارجاعات موجودیتها
دو حالت زیر را درنظر بگیرید که در اولی، blog از بانک اطلاعاتی واکشی شدهاست و post به صورت مستقیم وهله سازی شدهاست:
using (var context = new BloggingContext()) { var blog = context.Blogs.First(); var post = new Post { Title = "Intro to EF Core" }; blog.Posts.Add(post); context.SaveChanges(); }
using (var context = new BloggingContext()) { var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" }; var post = context.Posts.First(); blog.Posts.Add(post); context.SaveChanges(); }
در حالت دوم، ابتدا blog در بانک اطلاعاتی ثبت میشود (چون برخلاف حالت اول، تحت نظر context نیست) و سپس این post (که تحت نظر context است) به مجموعه مطالب آن اضافه میشود (بلاگ جدیدی اضافه شده و ارجاع مطلب موجودی به آن اضافه میشود).
وارد کردن یک موجودیت به سیستم ردیابی اطلاعات
در مثال قبل مشاهده کردیم که اگر موجودیتی تحت نظر context نباشد (برای مثال توسط یک کوئری به context وارد نشده باشد)، در حین ذخیره سازی ارجاعات، با آن به صورت یک وهلهی جدید رفتار شده و حتما در بانک اطلاعاتی به صورت یک رکورد جدید ذخیره میشود؛ حتی اگر Id آنرا دستی تنظیم کرده باشید که ندید گرفته خواهد شد.
اگر Id و سایر اطلاعات شیءایی را دارید، نیازی نیست تا حتما توسط یک کوئری ابتدا آنرا از بانک اطلاعاتی دریافت و سپس به صورت خودکار وارد سیستم ردیابی کنید؛ متد Attach نیز یک چنین کاری را انجام میدهد:
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Blog.Attach(blog); context.SaveChanges();
علاوه بر متد Attach، متد AttachRange نیز برای افزودن لیستی از موجودیتها در حالت EntityState.Unchanged، پیش بینی شدهاست.
روش دیگر انجام اینکار به صورت ذیل است:
در اینجا ابتدا یک وهلهی جدید از Blog ایجاد شدهاست و سپس توسط متد Entry به Context وارد شده و همچنین حالت آن به صورت صریح، به تغییر یافته، مشخص گردیدهاست:
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Entry(blog).State = EntityState.Modified ; context.SaveChanges();
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Update(blog); context.SaveChanges();
به علاوه متد UpdateRange نیز برای افزودن لیستی از موجودیتها در حالت EntityState.Modified، پیش بینی شدهاست.
یک نکته: متدهای Attach و Update، هم بر روی یک DbSet و هم بر روی Context، قابل اجرا هستند. اگر بر روی Context اجرا شدند، نوع موجودیت دریافتی به نوع DbSet متناظر به صورت خودکار نگاشت شده و استفاده میشود (context.Set<T>().Attach(entity)). یعنی در حقیقت بین این دو حالت تفاوتی نیست و امکان فراخوانی این متدها بر روی Context، صرفا جهت سهولت کار درنظر گرفته شدهاست.
تفاوت رفتار context.Entry در EF Core با EF 6.x
متد context.Entry در EF 6.x هم وجود دارد. اما در EF core سبب تغییر وضعیت گراف متصل به یک شیء نمیشود و ضعیت روابط آنرا به روز رسانی نمیکند (برخلاف EF 6.x). اگر در EF Core نیاز به یک چنین به روز رسانی گراف مانندی را داشتید، باید از متد جدید context.ChangeTracker.TrackGraph به نحو ذیل استفاده نمائید:
context.ChangeTracker.TrackGraph(blog, e => e.Entry.State = EntityState.Added);
کوئری گرفتن از سیستم ردیابی اطلاعات
این سناریوها را درنظر بگیرید:
- میخواهم سیستمی شبیه به تریگرهای اس کیوال سرور را با EF داشته باشم.
- میخواهم اطلاعات تمام رکوردهای ثبت شده، حذف شده و به روز رسانی شده را لاگ کنم.
- میخواهم پس از ثبت رکوردی در هر جای برنامه، شبیه به مباحث SQL Server Service Broker و SqlDependency بلافاصله مطلع شده و توسط SignalR اطلاع رسانی کنم.
و در حالت کلی میخواهم پیش و یا پس از ثبت اطلاعات، بتوانم به تغییرات صورت گرفته دسترسی داشته باشم و عملیاتی را بر روی آنها انجام دهم. تمام این موارد و سناریوها را با کوئری گرفتن از سیستم ردیابی اطلاعات EF میتوان پیاده سازی کرد.
برای نمونه در مطلب قبل و قسمت «طراحی یک کلاس پایه، بدون تنظیمات ارث بری روابط»، یک کلاس پایه را که مقادیر پیش فرض خود را از SQL Server دریافت میکند، طراحی کردیم. در اینجا میخواهیم با استفاده از سیستم ردیابی EF، طراحی این کلاس پایه را عمومی کرده و سازگار با تمام بانکهای اطلاعاتی موجود کنیم.
جهت یادآوری، کلاس پایه موجودیتها، یک چنین شکلی را داشته:
public class BaseEntity { public int Id { set; get; } public DateTime? DateAdded { set; get; } public DateTime? DateUpdated { set; get; } }
public class Person : BaseEntity { public string FirstName { get; set; } public string LastName { get; set; } }
public class ApplicationDbContext : DbContext { // same as before public override int SaveChanges() { this.ChangeTracker.DetectChanges(); var modifiedEntries = this.ChangeTracker .Entries<BaseEntity>() .Where(x => x.State == EntityState.Modified); foreach (var modifiedEntry in modifiedEntries) { modifiedEntry.Entity.DateUpdated = DateTime.UtcNow; } var addedEntries = this.ChangeTracker .Entries<BaseEntity>() .Where(x => x.State == EntityState.Added); foreach (var addedEntry in addedEntries) { addedEntry.Entity.DateAdded = DateTime.UtcNow; } return base.SaveChanges(); } }
در اینجا کار با کوئری گرفتن از خاصیت ChangeTracker شروع میشود. سپس باید مشخص کنیم چه نوع موجودیتهایی را مدنظر داریم. چون تمام موجودیتهای ما از کلاس پایهی BaseEntity مشتق میشوند، بنابراین کوئری گرفتن بر روی این نوع، به معنای دسترسی به تمام موجودیتهای برنامه نیز هست. سپس در اینجا اگر حالتی EntityState.Modified بود، فقط مقدار خاصیت DateUpdated را به صورت خودکار مقدار دهی میکنیم و اگر حالتی EntityState.Added بود، تنها مقدار خاصیت DateAdded را به روز رسانی خواهیم کرد.
در یک چنین حالتی دیگر نیازی نیست تا مقادیر این خواص را در حین ثبت اطلاعات برنامه به صورت دستی مشخص کنیم.
یک نکته: اگر به ابتدای متد بازنویسی شده دقت کنید، فراخوانی متد this.ChangeTracker.DetectChanges در آن انجام شدهاست. علت اینجا است که این فراخوانی به صورت خودکار توسط متد base.SaveChanges انجام میشود، اما چون این مرحله را تا انتهای متد بازنویسی شده، به تاخیر انداختهایم، نیاز است خودمان به صورت دستی سبب محاسبهی مجدد تغییرات صورت گرفته شویم.
نکتهای در مورد بهبود کیفیت کدهای متد SaveChanges: استفادهی Change Tracker به این صورت با بازنویسی متد SaveChanges بسیار مرسوم است. اما پس از مدتی به متد SaveChanges ایی خواهید رسید که کنترل آن از دست خارج میشود. به همین جهت برای EF 6.x پروژههایی مانند EFHooks طراحی شدهاند تا کپسوله سازی بهتری را بتوان ارائه داد. انتقال کدهای آن به EF Core کار مشکلی نیست و اصل آن، بازنویسی HookedDbContext آن است که نحوهی مدیریت شکیلتر کوئری گرفتن از ChangeTracker را بیان میکند.
خواص سایهای یا Shadow properties
EF Core به همراه مفهوم کاملا جدیدی است به نام خواص سایهای. این نوع خواص در سمت کدهای ما و در کلاسهای موجودیتهای برنامه وجود خارجی نداشته، اما در سمت جداول بانک اطلاعاتی وجود دارند و اکنون امکان کوئری گرفتن و کار کردن با آنها در EF Core میسر شدهاست.
برای تعریف آنها، بجای افزودن خاصیتی به کلاسهای برنامه، کار از متد OnModelCreating به نحو ذیل شروع میشود:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>().Property<DateTime>("DateAdded");
سپس برای کار کردن و کوئری گرفتن از آن میتوان از متد جدید EF.Property، به نحو ذیل استفاده کرد:
var blogs = context.Blogs.OrderBy(b => EF.Property<DateTime>(b, "DateAdded"));
context.Entry(myBlog).Property("DateAdded").CurrentValue = DateTime.Now;
foreach (var addedEntry in addedEntries) { addedEntry.Property("DateAdded").CurrentValue = DateTime.UtcNow; }
در قسمت قبل که لیست اتاقهای دریافتی از Web API را نمایش دادیم، هرکدام از آنها، به همراه یک دکمهی Book هم هستند (تصویر فوق) که هدف از آن، فراهم آوردن امکان رزرو کردن آن اتاق، توسط کاربران سایت است. این قسمت را میتوان به عنوان تمرینی جهت یادآوری مراحل مختلف تهیهی یک Web API و قسمتهای سمت کلاینت آن، تکمیل کرد.
تهیه موجودیت و مدل متناظر با صفحهی ثبت رزرو یک اتاق
تا اینجا در برنامهی سمت کلاینت، زمانیکه بر روی دکمهی Go صفحهی اول کلیک میکنیم، تاریخ شروع رزرو و تعداد روز مدنظر، به صفحهی مشاهدهی لیست اتاقها ارسال میشود. اکنون میخواهیم در این لیست اتاقهای نمایش داده شده، اگر بر روی لینک Book اتاقی کلیک شد، به صفحهی اختصاصی رزرو آن اتاق هدایت شویم (مانند تصویر فوق). به همین جهت نیاز است موجودیت متناظر با اطلاعاتی را که قرار است از کاربر دریافت کنیم، به صورت زیر به پروژهی BlazorServer.Entities اضافه کنیم:
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace BlazorServer.Entities { public class RoomOrderDetail { public int Id { get; set; } [Required] public string UserId { get; set; } [Required] public string StripeSessionId { get; set; } public DateTime CheckInDate { get; set; } public DateTime CheckOutDate { get; set; } public DateTime ActualCheckInDate { get; set; } public DateTime ActualCheckOutDate { get; set; } public long TotalCost { get; set; } public int RoomId { get; set; } public bool IsPaymentSuccessful { get; set; } [Required] public string Name { get; set; } [Required] public string Email { get; set; } public string Phone { get; set; } [ForeignKey("RoomId")] public HotelRoom HotelRoom { get; set; } public string Status { get; set; } } }
namespace BlazorServer.Common { public static class BookingStatus { public const string Pending = "Pending"; public const string Booked = "Booked"; public const string CheckedIn = "CheckedIn"; public const string CheckedOutCompleted = "CheckedOut"; public const string NoShow = "NoShow"; public const string Cancelled = "Cancelled"; } }
namespace BlazorServer.DataAccess { public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; } // ... } }
dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddRoomOrderDetails --context ApplicationDbContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
پس از تعریف یک موجودیت، یک DTO متناظر با آنرا که جهت مدلسازی UI از آن استفاده خواهیم کرد، در پروژهی BlazorServer.Models ایجاد میکنیم:
using System; using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public class RoomOrderDetailsDTO { public int Id { get; set; } [Required] public string UserId { get; set; } [Required] public string StripeSessionId { get; set; } [Required] public DateTime CheckInDate { get; set; } [Required] public DateTime CheckOutDate { get; set; } public DateTime ActualCheckInDate { get; set; } public DateTime ActualCheckOutDate { get; set; } [Required] public long TotalCost { get; set; } [Required] public int RoomId { get; set; } public bool IsPaymentSuccessful { get; set; } [Required] public string Name { get; set; } [Required] public string Email { get; set; } public string Phone { get; set; } public HotelRoomDTO HotelRoomDTO { get; set; } public string Status { get; set; } } }
namespace BlazorServer.Models.Mappings { public class MappingProfile : Profile { public MappingProfile() { // ... CreateMap<RoomOrderDetail, RoomOrderDetailsDTO>().ReverseMap(); // two-way mapping } } }
ایجاد سرویسی برای کار با جدول RoomOrderDetails
در برنامهی سمت کلاینت برای کار با بانک اطلاعاتی، دیگر نمیتوان از سرویسهای سمت سرور به صورت مستقیم استفاده کرد. به همین جهت آنها را از طریق یک Web API endpoint، در معرض دید استفاده کننده قرار میدهیم. اما پیش از اینکار، سرویس سمت سرور Web API باید بتواند با سرویس دسترسی به اطلاعات جدول RoomOrderDetails، کار کند. بنابراین در ادامه این سرویس را تهیه میکنیم:
namespace BlazorServer.Services { public interface IRoomOrderDetailsService { Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details); Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync(); Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId); Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate); Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id); Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status); } }
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { private readonly ApplicationDbContext _dbContext; private readonly IMapper _mapper; private readonly IConfigurationProvider _mapperConfiguration; public RoomOrderDetailsService(ApplicationDbContext dbContext, IMapper mapper) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapperConfiguration = mapper.ConfigurationProvider; } public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details) { var roomOrder = _mapper.Map<RoomOrderDetail>(details); roomOrder.Status = BookingStatus.Pending; var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder); await _dbContext.SaveChangesAsync(); return _mapper.Map<RoomOrderDetailsDTO>(result.Entity); } public Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync() { return _dbContext.RoomOrderDetails .Include(roomOrderDetail => roomOrderDetail.HotelRoom) .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration) .ToListAsync(); } public async Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId) { var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails .Include(u => u.HotelRoom) .ThenInclude(x => x.HotelRoomImages) .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration) .FirstOrDefaultAsync(u => u.Id == roomOrderId); roomOrderDetailsDTO.HotelRoomDTO.TotalDays = roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days; return roomOrderDetailsDTO; } public Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate) { return _dbContext.RoomOrderDetails .AnyAsync( roomOrderDetail => roomOrderDetail.RoomId == RoomId && roomOrderDetail.IsPaymentSuccessful && ( (checkInDate < roomOrderDetail.CheckOutDate && checkInDate > roomOrderDetail.CheckInDate) || (checkOutDate > roomOrderDetail.CheckInDate && checkInDate < roomOrderDetail.CheckInDate) ) ); } public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id) { throw new NotImplementedException(); } public Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status) { throw new NotImplementedException(); } } }
- از متد CreateAsync برای تبدیل مدل فرم ثبت اطلاعات، به یک رکورد جدول RoomOrderDetails، استفاده میکنیم.
- متد GetAllRoomOrderDetailsAsync، لیست تمام سفارشهای ثبت شده را بازگشت میدهد.
- متد GetRoomOrderDetailAsync بر اساس شماره اتاقی که دریافت میکند، لیست سفارشات آن اتاق خاص را بازگشت میدهد. این لیست به علت استفاده از Includeهای تعریف شده، به همراه مشخصات اتاق و همچنین تصاویر مرتبط با آن اتاق نیز هست.
- متد IsRoomBookedAsync بر اساس شماره اتاق و بازهی زمانی درخواستی توسط یک کاربر مشخص میکند که آیا اتاق خالی شدهاست یا خیر؟
پس از تعریف این سرویس، به کلاس آغازین پروژهی Web API مراجعه کرده و آنرا به سیستم تزریق وابستگیها، معرفی میکنیم:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddScoped<IRoomOrderDetailsService, RoomOrderDetailsService>(); // ...
تشکیل سرویس ابتدایی کار با RoomOrderDetails در پروژهی WASM
در ادامه، تعاریف خالی سرویس سمت کلاینت کار با RoomOrderDetails را به پروژهی WASM اضافه میکنیم. تکمیل این سرویس را به قسمت بعدی واگذار خواهیم کرد:
namespace BlazorWasm.Client.Services { public interface IClientRoomOrderDetailsService { Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details); Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details); } }
namespace BlazorWasm.Client.Services { public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService { private readonly HttpClient _httpClient; public ClientRoomOrderDetailsService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details) { throw new NotImplementedException(); } public Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details) { throw new NotImplementedException(); } } }
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>(); // ... } } }
تعریف مدل فرم ثبت اطلاعات سفارش
پس از تدارک مقدمات فوق، اکنون میتوانیم کار تکمیل فرم ثبت اطلاعات سفارش را شروع کنیم. به همین جهت مدل مخصوص آنرا در برنامهی سمت کلاینت به صورت زیر تشکیل میدهیم:
using BlazorServer.Models; namespace BlazorWasm.Client.Models.ViewModels { public class HotelRoomBookingVM { public RoomOrderDetailsDTO OrderDetails { get; set; } } }
تعریف کامپوننت جدید RoomDetails و مقدار دهی اولیهی مدل آن
در ادامه فایل جدید BlazorWasm.Client\Pages\HotelRooms\RoomDetails.razor را ایجاد کرده و به صورت زیر مقدار دهی اولیه میکنیم:
@page "/hotel/room-details/{Id:int}" @inject IJSRuntime JsRuntime @inject ILocalStorageService LocalStorage @inject IClientHotelRoomService HotelRoomService @if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null) { <div class="spinner"></div> } else { } @code { [Parameter] public int? Id { get; set; } HotelRoomBookingVM HotelBooking = new HotelRoomBookingVM(); int NoOfNights = 1; protected override async Task OnInitializedAsync() { try { HotelBooking.OrderDetails = new RoomOrderDetailsDTO(); if (Id != null) { if (await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking) != null) { var roomInitialInfo = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking); HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync( Id.Value, roomInitialInfo.StartDate, roomInitialInfo.EndDate); NoOfNights = roomInitialInfo.NoOfNights; HotelBooking.OrderDetails.CheckInDate = roomInitialInfo.StartDate; HotelBooking.OrderDetails.CheckOutDate = roomInitialInfo.EndDate; HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = roomInitialInfo.NoOfNights; HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount = roomInitialInfo.NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate; } else { HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync( Id.Value, DateTime.Now, DateTime.Now.AddDays(1)); NoOfNights = 1; HotelBooking.OrderDetails.CheckInDate = DateTime.Now; HotelBooking.OrderDetails.CheckOutDate = DateTime.Now.AddDays(1); HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = 1; HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount = HotelBooking.OrderDetails.HotelRoomDTO.RegularRate; } } } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } }
- سپس سرویس توکار IJSRuntime به کامپوننت تزریق شدهاست تا توسط آن و Toastr، بتوان خطاهایی را به کاربر نمایش داد.
- از سرویس ILocalStorageService برای دسترسی به اطلاعات شروع به رزرو شخص و تعداد روز مدنظر او استفاده میکنیم که در قسمت قبل آنرا مقدار دهی کردیم.
- همچنین از سرویس IClientHotelRoomService که آنرا نیز در قسمت قبل افزودیم، برای فراخوانی متد GetHotelRoomDetailsAsync آن استفاده کردهایم.
در روال آغازین OnInitializedAsync، اگر Id تنظیم شده بود، یعنی کاربر به درستی وارد این صفحه شدهاست. سپس بررسی میکنیم که آیا اطلاعاتی از درخواست ابتدایی او در Local Storage مرورگر وجود دارد یا خیر؟ اگر این اطلاعات وجود داشته باشد، بر اساس آن، بازهی تاریخی دقیقی را میتوان تشکیل داد و اگر خیر، این بازه را از امروز، به مدت 1 روز درنظر میگیریم.
پس از پایان کار متد OnInitializedAsync، چون اجزای HotelBooking مقدار دهی کامل شدهاند، نمایش loading ابتدای کامپوننت، متوقف شده و قسمت else شرط نوشته شده اجرا میشود؛ یعنی اصل UI فرم نمایان خواهد شد.
در قسمت قبل، متد GetHotelRoomDetailsAsync را تکمیل نکردیم؛ چون به آن نیازی نداشتیم و فقط قصد داشتیم تا لیست تمام اتاقها را نمایش دهیم. اما در اینجا برای تکمیل کدهای آغازین کامپوننت RoomDetails، متد دریافت اطلاعات یک اتاق را نیز تکمیل میکنیم تا توسط آن بتوان در این کامپوننت نیز جزئیات اتاق انتخابی را نمایش داد:
namespace BlazorWasm.Client.Services { public class ClientHotelRoomService : IClientHotelRoomService { private readonly HttpClient _httpClient; public ClientHotelRoomService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate) { // How to url-encode query-string parameters properly var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/hotelroom/{roomId}")) .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}") .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}") .Uri; return _httpClient.GetFromJsonAsync<HotelRoomDTO>(uri); } public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate) { // ... } } }
اتصال مدل کامپوننت RoomDetails به فرم ثبت سفارش آن
تا اینجا مدل فرم را مقدار دهی اولیه کردیم. اکنون میتوانیم قسمت else شرط نوشته شده را تکمیل کرده و در قسمتی از آن، مشخصات اتاق جاری را نمایش دهیم و در قسمتی دیگر، فرم ثبت سفارش را تکمیل کنیم.
الف) نمایش مشخصات اتاق جاری
در کامپوننت جاری با استفاده از خواص مقدار دهی اولیه شدهی شیء HotelBooking.OrderDetails.HotelRoomDTO، میتوان جزئیات اتاق انتخابی را نمایش داد که نمونهای از آنرا در قسمت قبل هم مشاهده کردید:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null) { <div class="spinner"></div> } else { <div class="mt-4 mx-4 px-0 px-md-5 mx-md-5"> <div class="row p-2 my-3 " style="border-radius:20px; "> <div class="col-12 col-lg-7 p-4" style="border: 1px solid gray"> <div class="row px-2 text-success border-bottom"> <div class="col-8 py-1"><p style="font-size:x-large;margin:0px;">Selected Room</p></div> <div class="col-4 p-0"><a href="hotel/rooms" class="btn btn-secondary btn-block">Back to Room's</a></div> </div> <div class="row"> <div class="col-6"> <div id="" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel"> <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel"> <ol class="carousel-indicators"> <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li> <li data-target="#carouselExampleIndicators" data-slide-to="1"></li> </ol> <div class="carousel-inner"> <div class="carousel-item active"> <img class="d-block w-100" src="images/slide1.jpg" alt="First slide"> </div> </div> <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Previous</span> </a> <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Next</span> </a> </div> </div> </div> <div class="col-6"> <span class="float-right pt-4"> <span class="float-right">Occupancy : @HotelBooking.OrderDetails.HotelRoomDTO.Occupancy adults </span><br /> <span class="float-right pt-1">Size : @HotelBooking.OrderDetails.HotelRoomDTO.SqFt sqft</span><br /> <h4 class="text-warning font-weight-bold pt-5"> <span style="border-bottom:1px solid #ff6a00"> @HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount.ToString("#,#.00#;(#,#.00#)") </span> </h4> <span class="float-right">Cost for @HotelBooking.OrderDetails.HotelRoomDTO.TotalDays nights</span> </span> </div> </div> <div class="row p-2"> <div class="col-12"> <p class="card-title text-warning" style="font-size:xx-large">@HotelBooking.OrderDetails.HotelRoomDTO.Name</p> <p class="card-text" style="font-size:large"> @((MarkupString)@HotelBooking.OrderDetails.HotelRoomDTO.Details) </p> </div> </div> </div> }
قسمت دوم UI کامپوننت جاری، نمایش فرم زیر است که اجزای مختلف آن به فیلد HotelBooking متصل شدهاند:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null) { <div class="spinner"></div> } else { // ... <div class="col-12 col-lg-5 p-4 2 mt-4 mt-md-0" style="border: 1px solid gray;"> <EditForm Model="HotelBooking" class="container" OnValidSubmit="HandleCheckout"> <div class="row px-2 text-success border-bottom"><div class="col-7 py-1"><p style="font-size:x-large;margin:0px;">Enter Details</p></div></div> <div class="form-group pt-2"> <label class="text-warning">Name</label> <InputText @bind-Value="HotelBooking.OrderDetails.Name" type="text" class="form-control" /> </div> <div class="form-group pt-2"> <label class="text-warning">Phone</label> <InputText @bind-Value="HotelBooking.OrderDetails.Phone" type="text" class="form-control" /> </div> <div class="form-group"> <label class="text-warning">Email</label> <InputText @bind-Value="HotelBooking.OrderDetails.Email" type="text" class="form-control" /> </div> <div class="form-group"> <label class="text-warning">Check in Date</label> <InputDate @bind-Value="HotelBooking.OrderDetails.CheckInDate" type="date" disabled class="form-control" /> </div> <div class="form-group"> <label class="text-warning">Check Out Date</label> <InputDate @bind-Value="HotelBooking.OrderDetails.CheckOutDate" type="date" disabled class="form-control" /> </div> <div class="form-group"> <label class="text-warning">No. of nights</label> <select class="form-control" value="@NoOfNights" @onchange="HandleNoOfNightsChange"> @for (var i = 1; i <= 10; i++) { if (i == NoOfNights) { <option value="@i" selected="selected">@i</option> } else { <option value="@i">@i</option> } } </select> </div> <div class="form-group"> <button type="submit" class="btn btn-success form-control">Checkout Now</button> </div> </EditForm> </div> </div> </div> }
@code { // ... private async Task HandleNoOfNightsChange(ChangeEventArgs e) { NoOfNights = Convert.ToInt32(e.Value.ToString()); HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync( Id.Value, HotelBooking.OrderDetails.CheckInDate, HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights)); HotelBooking.OrderDetails.CheckOutDate = HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights); HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = NoOfNights; HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount = NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate; } private async Task HandleCheckout() { if (!await HandleValidation()) { return; } } private async Task<bool> HandleValidation() { if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Name)) { await JsRuntime.ToastrError("Name cannot be empty"); return false; } if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Phone)) { await JsRuntime.ToastrError("Phone cannot be empty"); return false; } if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Email)) { await JsRuntime.ToastrError("Email cannot be empty"); return false; } return true; } }
- همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کردهایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده میکنید، کردهایم. این روش دستی را نیز میتوان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آنرا توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمیکند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کردهایم.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-29.zip
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
ثبت یک برنامهی جدید در گوگل
اگر بخواهیم از گوگل به عنوان یک IDP ثالث در IdentityServer استفاده کنیم، نیاز است در ابتدا برنامهی IDP خود را به آن معرفی و در آنجا ثبت کنیم. برای این منظور مراحل زیر را طی خواهیم کرد:
1- مراجعه به developer console گوگل و ایجاد یک پروژهی جدید
https://console.developers.google.com
در صفحهی باز شده، بر روی دکمهی select project در صفحه و یا لینک select a project در نوار ابزار آن کلیک کنید. در اینجا دکمهی new project و یا create را مشاهده خواهید کرد. هر دوی این مفاهیم به صفحهی زیر ختم میشوند:
در اینجا نامی دلخواه را وارد کرده و بر روی دکمهی create کلیک کنید.
2- فعالسازی API بر روی این پروژهی جدید
در ادامه بر روی لینک Enable APIs And Services کلیک کنید و سپس google+ api را جستجو نمائید.
پس از ظاهر شدن آن، این گزینه را انتخاب و در صفحهی بعدی، آنرا با کلیک بر روی دکمهی enable، فعال کنید.
3- ایجاد credentials
در اینجا بر روی دکمهی create credentials کلیک کرده و در صفحهی بعدی، این سه گزینه را با مقادیر مشخص شده، تکمیل کنید:
• Which API are you using? – Google+ API • Where will you be calling the API from? – Web server (e.g. node.js, Tomcat) • What data will you be accessing? – User data
• نام: همان مقدار پیشفرض آن
• Authorized JavaScript origins: آنرا خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آنرا با آدرس زیر مقدار دهی خواهیم کرد.
https://localhost:6001/signin-google
سپس در ذیل این صفحه بر روی دکمهی «Create OAuth 2.0 Client ID» کلیک کنید تا به صفحهی «Set up the OAuth 2.0 consent screen» بعدی هدایت شوید. در اینجا دو گزینهی آنرا به صورت زیر تکمیل کنید:
- Email address: همان آدرس ایمیل واقعی شما است.
- Product name shown to users: یک نام دلخواه است. نام برنامهی خود را برای نمونه ImageGallery وارد کنید.
برای ادامه بر روی دکمهی Continue کلیک نمائید.
4- دریافت credentials
در پایان این گردش کاری، به صفحهی نهایی «Download credentials» میرسیم. در اینجا بر روی دکمهی download کلیک کنید تا ClientId و ClientSecret خود را توسط فایلی به نام client_id.json دریافت نمائید.
سپس بر روی دکمهی Done در ذیل صفحه کلیک کنید تا این پروسه خاتمه یابد.
تنظیم برنامهی IDP برای استفادهی از محتویات فایل client_id.json
پس از پایان عملیات ایجاد یک برنامهی جدید در گوگل و فعالسازی Google+ API در آن، یک فایل client_id.json را دریافت میکنیم که اطلاعات آن باید به صورت زیر به فایل آغازین برنامهی IDP اضافه شود:
الف) تکمیل فایل src\IDP\DNT.IDP\appsettings.json
{ "Authentication": { "Google": { "ClientId": "xxxx", "ClientSecret": "xxxx" } } }
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
- authenticationScheme تنظیم شده باید یک عبارت منحصربفرد باشد.
- همچنین SignInScheme یک چنین مقداری را در اصل دارد:
public const string ExternalCookieAuthenticationScheme = "idsrv.external";
آزمایش اعتبارسنجی کاربران توسط اکانت گوگل آنها
اکنون که تنظیمات اکانت گوگل به پایان رسید و همچنین به برنامه نیز معرفی شد، برنامهها را اجرا کنید. مشاهده خواهید کرد که امکان لاگین توسط اکانت گوگل نیز به صورت خودکار به صفحهی لاگین IDP ما اضافه شدهاست:
در اینجا با کلیک بر روی دکمهی گوگل، به صفحهی لاگین آن که به همراه نام برنامهی ما است و انتخاب اکانتی از آن هدایت میشویم:
پس از آن، از طرف گوگل به صورت خودکار به IDP (همان آدرسی که در فیلد Authorized redirect URIs وارد کردیم)، هدایت شده و callback رخداده، ما را به سمت صفحهی ثبت اطلاعات کاربر جدید هدایت میکند. این تنظیمات را در قسمت قبل ایجاد کردیم:
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class ExternalController : Controller { public async Task<IActionResult> Callback() { var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user = AutoProvisionUser(provider, providerUserId, claims); var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl }); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" , new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId }); return Redirect(continueWithUrl); }
در اینجا نحوهی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحهی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده میکنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد و به برنامه با این هویت جدید وارد میشود.
اتصال کاربر وارد شدهی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است
تا اینجا اگر کاربری از طریق یک IDP خارجی به برنامه وارد شود، او را به صفحهی ثبت نام کاربر هدایت کرده و پس از دریافت اطلاعات او، اکانت خارجی او را به اکانتی جدید که در IDP خود ایجاد میکنیم، متصل خواهیم کرد. به همین جهت بار دومی که این کاربر به همین ترتیب وارد سایت میشود، دیگر صفحهی ثبت نام و تکمیل اطلاعات را مشاهده نمیکند. اما ممکن است کاربری که برای اولین بار از طریق یک IDP خارجی به سایت ما وارد شدهاست، هم اکنون دارای یک اکانت دیگری در سطح IDP ما باشد؛ در اینجا فقط اتصالی بین این دو صورت نگرفتهاست. بنابراین در این حالت بجای ایجاد یک اکانت جدید، بهتر است از همین اکانت موجود استفاده کرد و صرفا اتصال UserLogins او را تکمیل نمود.
به همین جهت ابتدا نیاز است لیست Claims بازگشتی از گوگل را بررسی کنیم:
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); foreach (var claim in claims) { _logger.LogInformation($"External provider[{provider}] info-> claim:{claim.Type}, value:{claim.Value}"); }
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, value:Vahid N. External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname, value:Vahid External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname, value:N. External provider[Google] info-> claim:urn:google:profile, value:https://plus.google.com/105013528531611201860 External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, value:my.name@gmail.com
[HttpGet] public async Task<IActionResult> Callback() { // ... var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user wasn't found by provider, but maybe one exists with the same email address? if (provider == "Google") { // email claim from Google var email = claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); if (email != null) { var userByEmail = await _usersService.GetUserByEmailAsync(email.Value); if (userByEmail != null) { // add Google as a provider for this user await _usersService.AddUserLoginAsync(userByEmail.SubjectId, provider, providerUserId); // redirect to ExternalLoginCallback var continueWithUrlAfterAddingUserLogin = Url.Action("Callback", new {returnUrl = returnUrl}); return Redirect(continueWithUrlAfterAddingUserLogin); } } } var returnUrlAfterRegistration = Url.Action("Callback", new {returnUrl = returnUrl}); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration", new {returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId}); return Redirect(continueWithUrl); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.