Your license for Duende IdentityServer only permits 5 number of clients. You have processed requests for 6. The clients used were: ["client6", "client2", "client5", "client4", "client3", "client1"].
معرفی System.Text.Json در NET Core 3.0.
System.Text.Json، در حال حاضر از مفهومی به نام type coercion/inference، پشتیبانی نمیکند. type coercion یعنی تبدیل یک مقدار، به مقداری دیگر که به صورت مستقیم قابل انتساب به یکدیگر نیستند. برای مثال اگر رشتهی "true" را درنظر بگیریم، قابلیت انتساب به یک خاصیت از نوع bool را ندارد. برای یک چنین مواردی در این API جدید، باید تبدیلگر نوشت.
یک مثال:
using System.Collections.Generic; using System.Text.Json; namespace JsonTests { public class Product { public int Id { get; set; } public string Name { get; set; } public bool IsInStock { get; set; } } class Program { static void Main(string[] args) { var products = JsonSerializer.Deserialize<List<Product>>("[{\"Id\":1026,\"Name\":\"P1\",\"IsInStock\":\"false\"}]"); } } }
An unhandled exception of type 'System.Text.Json.JsonException' occurred in System.Text.Json.dll Inner exceptions found, see $exception in variables window for more details. Innermost exception System.InvalidOperationException : Cannot get the value of a token type 'String' as a boolean.
public class BooleanConverter : JsonConverter<bool> { public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); if (value.Equals("true", StringComparison.OrdinalIgnoreCase) || value.Equals("yes", StringComparison.OrdinalIgnoreCase) || value.Equals("1", StringComparison.Ordinal)) { return true; } if (value.Equals("false", StringComparison.OrdinalIgnoreCase) || value.Equals("no", StringComparison.OrdinalIgnoreCase) || value.Equals("0", StringComparison.Ordinal)) { return false; } throw new NotSupportedException($"`{value}` can't be converted to `bool`."); } public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) { switch (value) { case true: writer.WriteStringValue("true"); break; case false: writer.WriteStringValue("false"); break; } } }
public abstract class JsonConverter<T> : JsonConverter { protected internal JsonConverter(); public override bool CanConvert(Type typeToConvert); public abstract T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options); public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options); }
در متد Read آن، مقدار رشتهای دریافت شدهی از منبع داده، در اختیار ما قرار میگیرد. سپس باید بر اساس این مقدار، مقدار متناظری را از نوع T که در اینجا bool است، بازگشت دهیم. برای مثال اگر یکی از مقادیر رشتهای true ،yes و 1 را دریافت کردیم، بجای آن true را بازگشت میدهیم.
اکنون برای استفادهی از آن خواهیم داشت:
var options = new JsonSerializerOptions(); options.Converters.Add(new BooleanConverter()); var products = JsonSerializer.Deserialize<List<Product>>( "[{\"Id\":1026,\"Name\":\"P1\",\"IsInStock\":\"false\"}]", options);
[JsonConverter(typeof(BooleanConverter))] public bool IsInStock { get; set; }
از این تبدیلگر برای حالت Serialize نیز میتوان استفاده کرد:
var options = new JsonSerializerOptions() {WriteIndented = true }; options.Converters.Add(new BooleanConverter()); var data = JsonSerializer.Serialize<List<Product>>(productList, options);
private static void seedDb(ApplicationDbContext context) { if (!context.Chapters.Any()) { var user1 = context.Users.Add(new User { Name = "Test User" }); context.Chapters.Add(new Chapter { Title = "Learn SQlite FTS5", Text = "This tutorial teaches you how to perform full-text search in SQLite using FTS5", User = user1.Entity }); context.Chapters.Add(new Chapter { Title = "Advanced SQlite Full-text Search", Text = "Show you some advanced techniques in SQLite full-text searching", User = user1.Entity }); context.Chapters.Add(new Chapter { Title = "SQLite Tutorial", Text = "Help you learn SQLite quickly and effectively", User = user1.Entity }); context.Chapters.Add(new Chapter { Title = "Handle markup in text", Text = "<p>Isn't this <font face=\"Comic Sans\">funny</font>?", User = user1.Entity }); context.Chapters.Add(new Chapter { Title = "آزمایش متن فارسی", Text = "برای نمونه تهیه شدهاست", User = user1.Entity }); context.Chapters.Add(new Chapter { Title = "Exclude test 1", Text = "in the years 2018-2019 something happened.", User = user1.Entity }); context.Chapters.Add(new Chapter { Title = "Exclude test 2", Text = "It was 2018 and then it was 2019", User = user1.Entity }); context.SaveChanges(); } }
ثبت اطلاعات فوق، چنین رکوردهایی را در جدول Chapters به وجود میآورد که شامل اطلاعات یونیکد، HTML ای و غیره است:
اجرای اولین کوئری بر روی جدول مجازی Chapters_FTS به صورت مستقیم
کوئریهای Full-text در SQLite، چنین شکل کلی را دارند و توسط تابع match انجام میشوند:
select * from Chapters_FTS where Chapters_FTS match "fts5"
همانطور که مشاهده میکنید در اینجا تنها دو ستونی که ایندکس شدهاند، در خروجی نهایی ظاهر میشوند؛ اما این جدول به همراه ستونهای مخفی توکار دیگری نیز هست:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "fts5"
- Rowid با توجه به تعریفی که در قسمت قبل انجام دادیم:
CREATE VIRTUAL TABLE "Chapters_FTS" USING fts5("Text", "Title", content="Chapters", content_rowid="Id")
- تمام جداول مجازی FTS، به همراه ستون مخفی rank نیز هستند که میزان نزدیک بودن خروجی حاصل را به کوئری درخواستی مشخص میکنند. این عدد توسط تابعی به نام bm25 تهیه میشود. اگر کوئری FTS به همراه قسمت where نباشد، مقدار rank همواره نال خواهد بود. اما اگر قسمت where به همراه match قید شود، مقدار rank، مقدار از پیش محاسبه شدهی تابع توکار bm25 است. به همین جهت کار با این مقدار از پیش محاسبه شده، سریعتر از فراخوانی مستقیم متد bm25 است. برای مثال دو کوئری زیر اساسا یکی هستند؛ اما دومی سریعتر است:
select * from Chapters_FTS where Chapters_FTS match "fts5" ORDER BY bm25(fts); select * from Chapters_FTS where Chapters_FTS match "fts5" ORDER BY rank;
یک نکته: کوئری FTS فوق بر روی هر دو ستون title و text اجرا میشود (و یا هر ستون موجود دیگری که پیشتر ایندکس شده باشد).
اجرای اولین کوئری بر روی جدول مجازی Chapters_FTS توسط EF Core
پس از آشنایی مقدماتی با کوئری نویسی FTS در SQLite، بر انجام یک چنین کوئری در EF Core میتوان به صورت زیر عمل کرد:
- ابتدا باید یک موجودیت بدون کلید را مطابق ستونهای مخفی و ایندکس شدهی بازگشتی تهیه کنیم:
namespace EFCoreSQLiteFTS.Entities { public class ChapterFTS { public int RowId { get; set; } public decimal? Rank { get; set; } public string Title { get; set; } public string Text { get; set; } } }
- سپس نیاز است این موجودیت بدون کلید را به EF معرفی کنیم:
namespace EFCoreSQLiteFTS.DataLayer { public class ApplicationDbContext : DbContext { //... protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<ChapterFTS>().HasNoKey().ToView(null); } //... } }
- و در آخر روش کوئری گرفتن از جدول مجازی FTS در EF Core به صورت زیر میباشد که توسط متد FromSqlRaw به صورت پارامتری (مقاوم در برابر حملات تزریق اسکیوال)، قابل انجام است:
const string ftsSql = "SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH {0}"; foreach (var chapter in context.Set<ChapterFTS>().FromSqlRaw(ftsSql, "fts5")) { Console.WriteLine($"Title: {chapter.Title}"); Console.WriteLine($"Text: {chapter.Text}"); }
بررسی قابلیتهای ویژهی کوئریهای FTS در SQLite
اکنون که با روش کلی کوئری گرفتن از جدول مجازی FTS آشنا شدیم، نکات ویژهی آنرا بررسی میکنیم و در اینجا بیشتر پارامتر ذکر شدهی پس از عملگر match تغییر خواهد کرد و مابقی قسمتهای آن ثابت و مانند قبل هستند.
بجای عملگر match میتوان از = نیز استفاده کرد
دو کوئری زیر دقیقا به یک معنا هستند:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "fts5"; SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS = "fts5";
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "fts5" ORDER by rank;
جستجوهایی به همراه واژههایی در کنار هم
از دیدگاه FTS، دو کوئری زیر که در قسمت match آنها، واژهها با فاصله در کنار هم قرار گرفتهاند، یکی هستند:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "learn SQLite" ORDER by rank; SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "learn + SQLite" ORDER by rank;
علت اینجا است که یک full-text search بر اساس ایندکس شدن واژهها تولید میشود و هر کدام از این واژهها به یک توکن نگاشت خواهند شد. به همین جهت است که در اینجا تفاوتی بین + و فاصله در عبارت جستجو شده وجود ندارد. در این حالت اگر در یکی از ستونهای ایندکس شده، واژهی learn و یا واژهی SQLite بکار رفته باشد، در خروجی نهایی لیست خواهد شد.
امکان جستجو بر اساس پیشوندها
میتوان با استفاده از *، تمام توکنهای ایندکس شده و شروع شدهی با واژهی مشخصی را جستجو کرد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "search*" ORDER by rank;
امکان استفاده از عملگرهای بولی NOT، AND و OR
اگر learn text را جستجو کنیم:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "learn text" ORDER by rank;
رکوردی با ID مساوی 1 بازگشت داده میشود. اما اگر نیاز باشد رکوردی بازگشت داده شود که حاوی learn باشد، اما text خیر، میتوان از عملگر NOT استفاده کرد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "learn NOT text" ORDER by rank;
که اینبار رکوردی با ID مساوی 3 را بازگشت دادهاست.
نکتهی مهم: عملگرهای بولی FTS مانند AND، OR، NOT و غیره باید با حروف بزرگ قید شوند.
در ادامه مثال دیگری از ترکیب عملگرهای بولی را مشاهده میکنید:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "search AND sqlite OR help" ORDER by rank;
که تقدم و تاخر این عملگرها را میتوان توسط پرانتزها به صورت صریحی نیز مشخص کرد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "search AND (sqlite OR help)" ORDER by rank;
امکان ذکر صریح ستونهای مدنظر در کوئری
همانطور که عنوان شد، حالت پیشفرض جستجوهای تمام متنی، جستجوی واژهی مدنظر در تمام ستونهای ایندکس شدهاست؛ اما شاید این مورد مدنظر شما نباشد. به همین منظور میتوان ابتدا نام ستون مدنظر را ذکر کرد و پس از آن یک : را قرار داد تا فقط جستجو بر روی آن ستون خاص صورت گیرد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "text:some AND title:sqlite" ORDER by rank;
امکان ترکیب نام ستونها به صورت {col2 col1 col3} نیز وجود دارد.
نکتهی مهم! در جستجوهای FTS در SQLite، ذکر - به معنای قید صریح نام یک ستون خاص است (و یا لیست ستونهایی به صورت {col2 col1 col3}-) که قرار نیست چیزی با آن(ها) انطباق داده شود (- شبیه به عملگر NOT عمل میکند؛ اینبار در مورد ستونها) و این مورد عموما تازهکاران را به اشتباه میاندازد. برای مثال در ابتدای بحث، دو رکورد را که دارای text ای مساوی عبارات زیر هستند، ثبت کردیم:
"in the years 2018-2019 something happened" "It was 2018 and then it was 2019"
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "2018-2019" ORDER by rank;
Execution finished with errors. Result: no such column: 2019
و یا میتوان عبارت جستجو شده را بین "" قرار داد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH '"2018-2019"' ORDER by rank;
و یا حتی میتوان '"2018 2019"' را نیز جستجو کرد که نتیجهی مشابهی را ارائه میدهد.
امکان جستجوی بر روی عبارات یونیکد
FTS5 و آخرین نگارش SQLite، به همراه tokenizer مخصوص یونیکد نیز هست و با اینگونه جستجوهای تمام متنی، مشکلی ندارد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "آزمایش" ORDER by rank;
توابع کمکی FTS در SQLite برای متمایز سازی عبارات یافت شدهی در متن
فرض کنید میخواهیم واژهی fts5 را جستجو کرده و همچنین در خروجی نهایی، هرجائیکه fts5 قرار دارد، آنرا به صورت bold نمایش دهیم. برای اینکار، تابع توکار highlight قابل استفادهاست. اما اگر در این بین خواستیم فقط قسمت کوتاهی از متن مورد نظر را که به جستجوی ما نزدیک است نمایش دهیم، میتوان از متد توکار snippet استفاده کرد:
SELECT rowid, highlight(Chapters_FTS, title, '<b>', '</b>') as title, snippet(Chapters_FTS, text, '<b>', '</b>', '...', 64) as text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "fts5" ORDER BY rank
نکتهی مهم: چون بر اساس نکات قسمت قبل، متنی که به Chapters_FTS ارسال میشود، نرمال سازی شدهاست، متدهای فوق کارآیی خودشان را از دست میدهند. برای مثال اگر در کوئری فوق، واژهی funny را که به یک رکورد HTML ای اشاره میکند، جستجو کنیم، خروجی زیر را دریافت خواهیم کرد:
خروجی نهایی، چون به جدول اصلی chapters متصل است، اصل متن را بازگشت میدهد، اما چون اطلاعاتی را که به Chapters_FTS ارسال کردهایم، فاقد تگهای HTML هستند، تا خروجی دقیقی حاصل شود، متدهای highlight و snippet دیگر قادر به علامتگذاری خروجی نهایی نبوده و اینکار را باید خودمان به صورت دستی در سمت کلاینت انجام دهیم.
PostgreSQL 17 منتشر شد
- Overhauled memory management for vacuuming, resulting in significantly lower memory usage and running time. More on this here.
- Incremental backup support.
- Faster B-tree index scans.
- MERGE enhancements, including view support.
- New functions to extract elements from UUIDs.
- WAL improvements – up to 2x write throughput on some workloads.
- Improvements to SQL/JSON support, including JSON_TABLE.
- Bulk loading improvements and perf improvements for COPY which gains the ON_ERROR ignore option to ignore errors.
- Identity columns on partitioned tables.
Here’s a summary of what’s new in this preview release:
Servers & middleware
Antiforgery middleware
API authoring
Antiforgery integration for minimal APIs
Native AOT
Request Delegate Generator supports interceptors feature
Full TrimMode is used for web projects compiled with trimming enabled
WebApplication.CreateEmptyBuilder
Blazor
Antiforgery integration
Server-side form handling improvements
Auto render mode
Register root-level cascading values
Improved integration of interactive components with server-side rendering
New EmptyContent parameter for Virtualize
Identity
New bearer token authentication handler
New API endpoints
Single page apps (SPA)
New Visual Studio templates
در قسمت قبل که لیست اتاقهای دریافتی از 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
به همراه هر درخواستی از سرور چه از طرف کلاینت و چه از طرف سرور، یک سری header نیز ارسال میشود. برای مثال مرورگر، نوع خود را به همراه یک سری از قابلیتهای مربوطه مانند الگوریتمهای فشرده سازی پشتیبانی شده به سرور ارسال میکند و در مقابل وب سرور هم یک سری هدر را مانند مدت زمان کش کردن اطلاعات دریافتی، نوع و نگارش سرور و امثال آن، به کلاینت ارسال خواهد کرد.
از دیدگاه امنیتی این اطلاعات اضافی هستند. برای مثال از تصویر زیر (که با استفاده از افزونهی فایرباگ تهیه شده) دقیقا میتوان وب سرور، نگارش آن و بسیاری از موارد دیگر را به سادگی تشخیص داد. در ادامه میخواهیم این هدرهای اضافی را حذف کنیم.
امکان تغییر و حذف هدرهای مربوط به response تنها با استفاده از امکانات IIS7 میسر است (در حالت integrated pipeline) و IIS6 چنین اجازهای را به ASP.Net نمیدهد.
برای این منظور یک پروژهی جدید، به نام SecurityMdl از نوع class library را ایجاد کرده و دو فایل SecurityMdl.cs و CRemoveHeader.cs را به آن اضافه خواهیم کرد:
//SecurityMdl.cs
using System;
using System.Web;
namespace SecurityMdl
{
public class SecurityMdl : IHttpModule
{
public void Init(HttpApplication app)
{
app.PreSendRequestHeaders += app_PreSendRequestHeaders;
}
static void app_PreSendRequestHeaders(object sender, EventArgs e)
{
CRemoveHeader.CheckPreSendRequestHeaders(sender);
}
public void Dispose() { }
}
}
در این Http module ، با تغییر اطلاعات دریافتی در روال رخداد گردان PreSendRequestHeaders میتوان به مقصود رسید:
//CRemoveHeader.cs
using System;
using System.Collections.Generic;
using System.Web;
namespace SecurityMdl
{
class CRemoveHeader
{
private static readonly List<string> _headersToRemoveCache
= new List<string>
{
"X-AspNet-Version",
"X-AspNetMvc-Version",
"Server"
};
public static void CheckPreSendRequestHeaders(Object sender)
{
//capture the current request
var currentResponse = ((HttpApplication)sender).Response;
//removing headers
//it only works with IIS 7.x's integrated pipeline
_headersToRemoveCache.ForEach(h => currentResponse.Headers.Remove(h));
//modify the "Server" Http Header
currentResponse.Headers.Set("Server", "Test");
}
}
}
و نهایتا برای استفاده از آن (علاوه بر افزودن ارجاعی به این ماژول جدید) چند سطر زیر را باید به وب کانفیگ برنامه اضافه کرد:
<system.webServer>
<modules>
<add name="SecurityMdl" type="SecurityMdl.SecurityMdl, SecurityMdl"/>
</modules>
با استفاده از این روش تمامی هدرهای مورد نظر بجز هدری به نام X-Powered-By حذف خواهند شد. برای حذف هدر مربوط به X-Powered-By که توسط خود IIS مدیریت میشود باید موارد زیر را به web.config برنامه اضافه کرد:
<system.webServer>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By"/>
</customHeaders>
</httpProtocol>
اکنون پس از این تغییرات حاصل کار و هدر نهایی دریافت شده از response یک برنامهی ASP.net به شکل زیر درخواهد آمد:
برای مطالعهی بیشتر
Remove the X-AspNet-Version header
Cloaking your ASP.NET MVC Web Application on IIS 7
IIS 7 - How to send a custom "Server" http header
Removing Unnecessary HTTP Headers in IIS and ASP.NET
Remove X-Powered-By: ASP.NET HTTP Response Header