پیش نیاز : آشنایی اولیه با مفاهیم WCF برای فهم بهتر مطالب
در ابتدا یک WCf Service Application ایجاد کنید و مدل زیر را بسازید:
[DataContract]
public abstract class Person
{
[DataMember]
public int Code { get; set; }
[DataMember]
public string Name { get; set; }
}
کلاس #1
[DataContract] public class Student : Person { [DataMember] public int StudentId { get; set; } }
[DataContract] public class Teacher : Person { public int TeacherId { get; set; } }
[ServiceContract] public interface IStudentService { [OperationContract] IEnumerable<Person> GetAll(); }
public class StudentService : IStudentService { public IEnumerable<Person> GetAll() { List<Person> listOfPerson = new List<Person>(); listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Masoud Pakdel" } ); listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Mostafa Asgari"} ); listOfPerson.Add( new Student() { Code = 1, StudentId = 123, Name = "Saeed Alizadeh"} );
listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Mahdi Rad"} ); listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Mohammad Heydari" } ); listOfPerson.Add( new Teacher() { Code = 1, TeacherId = 321, Name = "Saeed Khatami"} ); return listOfPerson; }
class Program { static void Main( string[] args ) { StudentService.StudentServiceClient client = new StudentService.StudentServiceClient(); client.GetAll().ToList().ForEach( _record => { Console.Write( "Name : {0}", _record.Name ); Console.WriteLine( "Code : {0}", _record.Code ); } ); Console.ReadLine(); } }
مشکل از اینجا ناشی میشود که هنگام عمل سریالایز ، WCF Runtime با توجه به وهله سازی از کلاس Person میدونه که باید کلاس Student یا Teacher رو سریالایز کنه ولی در هنگام عمل دی سریالایز، WCF Runtime این موضوع رو درک نمیکنه به همین دلیل یک Communication Exception پرتاب میکنه. برای حل این مشکل و برای اینکه WCF Deserialize Engine رو متوجه نوع وهله سازی کلاسهای مشتق شده از کلاس پایه کنیم باید از KnownTypeAttribute استفاده کنیم. فقط کافیست که این Attribute رو بالای کلاس Person به ازای تمام کلاسهای مشتق شده از اون قرار بدید.بدین صورت:
[DataContract] [KnownType( typeof( Student ) )] [KnownType( typeof(Teacher) )] public abstract class Person { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } }
با وجود KnownType دیگه WCF Deserialize Engine میدونه که باید از کدام DataContact برای عمل دی سریالاز نمونه ساخته شده از کلاس Person استفاده کنه. دانستن این مطلب هنگام پیاده سازی مفاهیم ارث بری در ORM ها زمانی که از WCF استفاده میکنیم ضروری است.
پیشنیازهای اجرای پروژهی DNT Identity
- ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
- همچنین بانک اطلاعاتی پایهی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل میشود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشتهی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شدهاست و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
- کاربر پیش فرض Admin سیستم و کلمهی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده میشوند.
- تنظیمات ایمیل پیش فرض برنامه به استفادهی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شدهاست. بنابراین تمام ایمیلهای برنامه را جهت آزمایش محلی میتوانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیلها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامهی Outlook میتوان مشاهده کرد.
سفارشی سازی کلید اصلی موجودیتهای ASP.NET Core Identity
ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری سادهی آن که از یک string به عنوان نوع کلید اصلی استفاده میکنند و سری دوم، حالت جنریک که در آن میتوان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیتهای تغییر یافته را در پوشهی src\ASPNETCoreIdentitySample.Entities\Identity نیز میتوانید مشاهده کنید و شامل موارد ذیل هستند:
جدول نقشهای سیستم
public class Role : IdentityRole<int, UserRole, RoleClaim>, IAuditableEntity { public Role() { } public Role(string name) : this() { Name = name; } public Role(string name, string description) : this(name) { Description = description; } public string Description { get; set; } }
در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.
جدول کاربران منتسب به نقشها
public class UserRole : IdentityUserRole<int>, IAuditableEntity { public virtual User User { get; set; } public virtual Role Role { get; set; } }
در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده میشود.
جدول جدید IdentityRoleClaim سیستم
public class RoleClaim : IdentityRoleClaim<int>, IAuditableEntity { public virtual Role Role { get; set; } }
جدول UserClaim سیستم
public class UserClaim : IdentityUserClaim<int>, IAuditableEntity { public virtual User User { get; set; } }
جداول توکن و لاگینهای کاربران
public class UserToken : IdentityUserToken<int>, IAuditableEntity { public virtual User User { get; set; } } public class UserLogin : IdentityUserLogin<int>, IAuditableEntity { public virtual User User { get; set; } }
جدول کاربران سیستم
public class User : IdentityUser<int, UserClaim, UserRole, UserLogin>, IAuditableEntity { public User() { UserUsedPasswords = new HashSet<UserUsedPassword>(); UserTokens = new HashSet<UserToken>(); } [StringLength(450)] public string FirstName { get; set; } [StringLength(450)] public string LastName { get; set; } [NotMapped] public string DisplayName { get { var displayName = $"{FirstName} {LastName}"; return string.IsNullOrWhiteSpace(displayName) ? UserName : displayName; } } [StringLength(450)] public string PhotoFileName { get; set; } public DateTimeOffset? BirthDate { get; set; } public DateTimeOffset? CreatedDateTime { get; set; } public DateTimeOffset? LastVisitDateTime { get; set; } public bool IsEmailPublic { get; set; } public string Location { set; get; } public bool IsActive { get; set; } = true; public virtual ICollection<UserUsedPassword> UserUsedPasswords { get; set; } public virtual ICollection<UserToken> UserTokens { get; set; } } public class UserUsedPassword : IAuditableEntity { public int Id { get; set; } public string HashedPassword { get; set; } public virtual User User { get; set; } public int UserId { get; set; } }
فیلد IsActive نیز از این جهت اضافه شدهاست تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.
تعریف Shadow properties ثبت تغییرات رکوردها
در #C ارثبری چندگانهی کلاسها ممنوع است؛ مگر اینکه از اینترفیسها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمیتوان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافهتری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده میکنید. این اینترفیس کار علامتگذاری کلاسهایی را انجام میدهد که قصد داریم به آنها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاسهایی از نوع IAuditableEntity اعمال میشود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیتهای علامتگذاری شده اضافه میکند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.
تاثیر افزودن Shadow properties را بر روی کلاس نقشهای سیستم، در تصویر فوق ملاحظه میکنید. خواصی که به صورت معمول در کلاسهای برنامه ظاهر نمیشوند و صرفا هدف بازبینی سیستم را برآورده میکنند و مدیریت آنها نیز در اینجا کاملا خودکار است.
سفارشی سازی DbContext برنامه
نحوهی سفارشی سازی DbContext برنامه را در پوشهی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه میکنید. پوشهی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازیهای لازم بر روی آن انجام شدهاست؛ شامل:
- تغییر نوع کلید اصلی موجودیتها به همراه معرفی موجودیتهای تغییر یافته:
public abstract class ApplicationDbContextBase : IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>, IUnitOfWork
- اعمال متد BeforeSaveTriggers به تمام نگارشهای مختلف SaveChanges
protected void BeforeSaveTriggers() { ValidateEntities(); SetShadowProperties(); this.ApplyCorrectYeKe(); }
- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شدهی در فایل appsettings.json خوانده شده و اعمال میشوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده میکنید. کار آن استفادهی از بانک اطلاعاتی درون حافظهای، جهت انجام آزمونهای واحد و یا استفادهی از LocalDb و یا نگارش کامل SQL Server میباشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.
پس از تعریف این DbContext پایهی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده میکنید. این کلاس Context ایی است که در برنامه از آن استفاده میشود و از کلاس پایه ApplicationDbContextBase مشتق شدهاست:
public class ApplicationDbContext : ApplicationDbContextBase
تنظیمات mapping آنها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوهی استفادهی از آن را بهخاطر داشته باشید:
protected override void OnModelCreating(ModelBuilder builder) { // it should be placed here, otherwise it will rewrite the following settings! base.OnModelCreating(builder); // Adds all of the ASP.NET Core Identity related mappings at once. builder.AddCustomIdentityMappings(SiteSettings.Value); // Custom application mappings // This should be placed here, at the end. builder.AddAuditableShadowProperties(); }
سپس متد AddCustomIdentityMappings ذکر شدهاست. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه میکند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شدهاند.
در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایهای که پیشتر در مورد آنها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشتهای سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.
در قسمت بعدی، نحوهی سفارشی سازی سرویسهای پایهی Identity را بررسی خواهیم کرد. بدون این سفارشی سازی و اطلاعات رسانی به سرویسهای پایه که از چه موجودیتهای جدید سفارشی سازی شدهایی در حال استفاده هستیم، کار Migrations انجام نخواهد شد.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
https://github.com/MehdiSaeedifar/IrisStore
همچنین نمونهی آنلاین آنرا میتوانید در فروشگاه آیریس مشاهده کنید.
در ادامه برخی از قابلیتهای این سیستم را مشاهده میکنید:
جست و جو با قابلیت دسته بندی نتایج
به هنگام جست و جو، لیستی از موارد پیشنهادی به صورت دسته بندی شده نمایش داده میشود.
جست و جوی پیشرفته کالاها
جست و جو بر اساس قیمت، گروه، کلمات کلیدی و مرتب سازی نتایج انجام میگیرد. همچنین نتایج جست و جو بدون رفرش شدن صفحه و به صورت AJAX ای به همراه تغییر URL صفحه صورت میگیرد.
نمایش نمودار تغییرات قیمت
امکان نمایش نمودار تغییرات قیمت کالا در بازهی زمانی نیز پیش بینی شده است.
ویرایش اطلاعات به صورت inline
امکان ویرایش قیمت و تاریخ به صورت inline وجود دارد.
مدیریت تصاویر کالا
در این قسمت امکان آپلود همزمان چندین فایل به همراه پیش نمایش آنها وجود دارد. همچنین امکان کشیدن و رها کردن برای تغییر ترتیب چیدمان عکسها نیز مهیا است.( تصویر اول به عنوان کاور کالا در نظر گرفته میشود.)
قابلیتهای دیگر:
- مدیریت تصاویر اسلایدشو و تغییر ترتیب آنها از طریق کشیدن و رها کردن (drag & drop)
- تعریف برگه و تغییر ترتیب نمایش آنها از طریق کشیدن و رها کردن
- امکان ارسال پست
- تعریف دسته بندی
- مدیریت کاربران
- تعریف تنظیمات سایت
- نمایش کالا و پستهای مشابه
کارهایی که باید انجام شود:
- پیاده سازی سبد خرید و خرید آنلاین
تصویر پنل مدیریت
تصویر صفحهی اصلی:
همچنین به راحتی میتوان با طراحی قالب جدیدی، از این سیستم برای کاری غیر از فروشگاه اینترنتی استفاده کرد؛ سایتهای زیر نمونههای آنلاین دیگری از این سیستم هستند:
- http://www.petrapars.ir
- http://www.ava-tarh.ir
در نهایت فهرستی از کتاب خانهها و فناوریهای استفاده شده و همچنین مقالات مرتبط با این پروژه را قرار دادهام.
کتابخانهها و فریم ورکهای سمت سرور:
فناوری یا کتابخانه | توضیحات | مقالات مرتبط |
Bootstrap 3.x | فریم ورک پایه ای css سایت | - Bootstrap 3 RTL Theme - Twitter Bootstrap -سازگارسازی کلاسهای اعتبارسنجی Twitter Bootstrap 3 با فرمهای ASP.NET MVC -ساخت قالبهای نمایشی و ادیتور دکمه سه وضعیتی سازگار با Twitter bootstrap در ASP.NET MVC -نمایش اخطارها و پیامهای بوت استرپ به کمک TempData در ASP.NET MVC |
AdminLTE | قالب مدیریت سایت | - نسخه راستچین شده AdminLTE 2.2.1 |
Animate.css | انیمیشنهای css3 سایت | |
Font Awesome | پک آیکونهای برداری | |
Awesome Bootstrap Checkbox | زیبا سازی چک باکس ها | |
فونت فارسی وزیر | قلم فارسی | |
لطفا برای طرح سؤالات و پیشنهادات خود و جهت مدیریت بهتر آنها، از قسمت اختصاصی این پروژه در سایت استفاده نمائید.
نحوه استفاده از ViewModel در ASP.NET MVC
·قسمتی از Application است که Domain Logic را پیاده سازی میکند.
·همچنین با عنوان Business Logic نیز شناخته میشود.
Public Class PersonModel { long Id{get;set;} string Name{get;set;} }
Public Class PersonModel { long Id{get;set;} string Name{get;set;} int SavePerson(person p); Person Finde(long personId); }
FluentValidation #2
NotNull | اطمینان از اینکه خاصیت مورد نظر Null نباشد |
NotEmpty | اطمینان از اینکه خاصیت مورد نظر Null یا رشته خالی نباشد (یا مقدار پیش فرض نباشد، مثلا 0 برای int) |
NotEqual | اطمینان از اینکه خاصیت مورد نظر برابر مقدار تعیین شده نباشد (یا برابر مقدار خاصیت دیگری نباشد) |
Equal | اطمینان از اینکه خاصیت مور نظر برابر مقدار تعیین شده باشد (یا برابر مقدار خاصیت دیگری باشد) |
Length | اطمینان از اینکه طول رشتهی خاصیت مورد نظر در محدوده خاصی باشد |
LessThan | اطمینان از اینکه مقدار خاصیت مورد نظر کوچکتر از مقدار تعیین شده باشد (یا کوچکتر از خاصیت دیگری) |
LessThanOrEqual | اطمینان از اینکه مقدار خاصیت مورد نظر کوچکتر یا مساوی مقدار تعیین شده باشد (یا کوچکتر مساوی مقدار خاصیت دیگری) |
GreaterThan | اطمینان از اینکه مقدار خاصیت مورد نظر بزرگتر از مقدار تعیین شده باشد (یا بزرگتر از مقدار خاصیت دیگری) |
GreaterThanOrEqual | اطمینان از اینکه مقدار خاصیت مورد نظر بزرگتر مساوی مقدار تعیین شده باشد (یا بزرگتر مساوی مقدار خاصیت دیگری) |
Matches | اطمینان از اینکه مقدار خاصیت مورد نظر با عبارت باقائده (Regular Expression) تنظیم شده مطابقت داشته باشد |
Must | اعتبارسنجی یک predicate با استفاده از Lambada Expressions. اگر عبارت Lambada مقدار true برگرداند اعتبارسنجی با موفقیت انجام شده و اگر false برگرداند، اعتبارسنجی با شکست مواجه شده است. |
اطمینان از اینکه مقدار خاصیت مورد نظر یک آدرس ایمیل معتبر باشد | |
CreditCard | اطمینان از اینکه مقدار خاصیت مورد نظر یک Credit Card باشد |
RuleFor(customer => customer.Surname).NotEqual(customer => customer.Forename);
RuleFor(customer => customer.Surname).NotNull().WithMessage("Please ensure that you have entered your Surname");
اعتبارسنجی تنها در مواقع خاص
RuleFor(customer => customer.CustomerDiscount).GreaterThan(0).When(customer => customer.IsPreferredCustomer);
When(customer => customer.IsPreferred, () => { RuleFor(customer => customer.CustomerDiscount).GreaterThan(0); RuleFor(customer => customer.CreditCardNumber).NotNull(); });
تعیین نحوه برخورد با اعتبارسنجیهای زنجیره ای
RuleFor(x => x.Surname).NotNull().NotEqual("foo");
RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEqual("foo");
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { // First set the cascade mode CascadeMode = CascadeMode.StopOnFirstFailure; // Rule definitions follow RuleFor(...) RuleFor(...) } }
راه اول ایجاد یک کلاس که از PropertyValidator مشتق میشود. برای توضیح نحوه استفاده از این راه، تصور کنید که میخواهیم یک اعتبارسنج سفارشی درست کنیم تا چک کند که یک لیست حتماً کمتر از 10 آیتم داخل خود داشته باشد. در این صورت کدی که بایستی نوشته شود به صورت زیر خواهد بود:
using System.Collections.Generic; using FluentValidation.Validators; public class ListMustContainFewerThanTenItemsValidator<T> : PropertyValidator { public ListMustContainFewerThanTenItemsValidator() : base("Property {PropertyName} contains more than 10 items!") { } protected override bool IsValid(PropertyValidatorContext context) { var list = context.PropertyValue as IList<T>; if(list != null && list.Count >= 10) { return false; } return true; } }
برای استفاده از این Validator سفارشی نیز میتوان از متد SetValidator به صورت زیر استفاده نمود:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).SetValidator(new ListMustContainFewerThanTenItemsValidator<Pet>()); } }
راه دیگر استفاده از آن تعریف یک Extension Method میباشد که در این صورت میتوان از آن به صورت زنجیره ای مانند دیگر Validatorها استفاده نمود:
public static class MyValidatorExtensions { public static IRuleBuilderOptions<T, IList<TElement>> MustContainFewerThanTenItems<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder) { return ruleBuilder.SetValidator(new ListMustContainFewerThanTenItemsValidator<TElement>()); } }
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).MustContainFewerThanTenItems(); } }
راه دوم استفاده از متد Custom میباشد. برای توضیح نحوه استفاه از این متد مثال قبل (چک کردن تعداد آیتمهای لیست) را به صورت زیر بازنویسی میکنیم:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { Custom(person => { return person.Pets.Count >= 10 ? new ValidationFailure("More than 10 pets is not allowed.") : null; }); } }
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).Must(HaveFewerThanTenPets).WithMessage("More than 9 pets is not allowed"); } private bool HaveFewerThanTenPets(IList<Pet> pets) { return pets.Count < 10; } }
پ.ن.
در این دو مقاله سعی شد تا ویژگیهای FluentValidation به صورت انتزاعی توضیح داده شود. در قسمت بعد نحوه استفاده از این کتابخانه در یک برنامه ASP.NET MVC نشان داده خواهد شد.
معادل IActionResult در Minimal API's
در Minimal API's دیگر خبری از IActionResultها نیست؛ اما بجای آن IResult را داریم. برای مثال فرض کنید میخواهیم بدنهی lambda expression دو endpoint ای را که تا این مرحله توسعه دادیم، تبدیل به دو متد مجزای private کنیم:
public class AuthorModule : IModule { public IEndpointRouteBuilder RegisterEndpoints(IEndpointRouteBuilder endpoints) { endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) => await GetAllAuthorsAsync(mediator, ct)); endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto, CancellationToken ct) => await CreateAuthorAsync(authorDto, mediator, ct)); return endpoints; } private static async Task<IResult> CreateAuthorAsync(AuthorDto authorDto, IMediator mediator, CancellationToken ct) { var command = new CreateAuthorCommand { AuthorDto = authorDto }; var author = await mediator.Send(command, ct); return Results.Ok(author); } private static async Task<IResult> GetAllAuthorsAsync(IMediator mediator, CancellationToken ct) { var request = new GetAllAuthorsQuery(); var authors = await mediator.Send(request, ct); return Results.Ok(authors); } }
Challenge, Forbid, SignIn, SignOut, Content, Text, Json, File, Bytes, Stream, Redirect, LocalRedirect, StatusCode NotFound, Unauthorized, BadRequest, Conflict, NoContent, Ok UnprocessableEntity, Problem, ValidationProblem, Created CreatedAtRoute, Accepted, AcceptedAtRoute
یک مثال: استفاده از متد Results.Problem جهت بازگشت پیام خطایی به کاربر:
try { return Results.Ok(await data.GetUsers()); } catch (Exception ex) { return Results.Problem(ex.Message); }
ساده سازی تعاریف هندلرهای endpoints در Minimal API's
تا اینجا هندلرهای یک endpoint را تبدیل به متدهایی مستقل کردیم و به صورت زیر فراخوانی شدند:
endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) => await GetAllAuthorsAsync(mediator, ct));
endpoints.MapGet("/api/authors", GetAllAuthorsAsync); endpoints.MapPost("/api/authors", CreateAuthorAsync);
غنی سازی اطلاعات Open API در Minimal API's
در اینجا چون با کنترلرها و اکشن متدها کار نمیکنیم، نمیتوانیم اطلاعات تکمیلی Open API را از طریق بکارگیری attributes مخصوص آنها اضافه کنیم. اولین تغییری که در Minimal API's جهت دریافت متادیتای endpoints قابل مشاهدهاست، چند سطر زیر است:
public static class ServiceCollectionExtensions { public static IServiceCollection AddApplicationServices(this IServiceCollection services, WebApplicationBuilder builder) { builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // ...
public class AuthorModule : IModule { public IEndpointRouteBuilder RegisterEndpoints(IEndpointRouteBuilder endpoints) { endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) => await GetAllAuthorsAsync(mediator, ct)) .WithName("GetAllAuthors") .WithDisplayName("Authors") .WithTags("Authors") .Produces(500); endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto, CancellationToken ct) => await CreateAuthorAsync(authorDto, mediator, ct)) .WithName("CreateAuthor") .WithDisplayName("Authors") .WithTags("Authors") .Produces(500); return endpoints; }
البته اگر تا اینجا برنامه را اجرا کنید، برای مثال نامهایی که تعریف شدهاند، در Swagger ظاهر نمیشوند. برای رفع این مشکل میتوان به صورت زیر عمل کرد:
builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = builder.Environment.ApplicationName, Version = "v1" }); options.TagActionsBy(ta => new List<string> { ta.ActionDescriptor.DisplayName! }); });
تغییر خروجی endpoints از مدل دومین، به یک Dto
در endpoints فوق، اطلاعات دریافتی از کاربر، یک dto است که توسط AutoMapper به مدل دومین، نگاشت میشود. اینکار خصوصا از دیدگاه امنیتی جهت رفع مشکلی به نام mass assignment و عدم مقدار دهی خودکار خواصی از مدل اصلی که نباید مقدار دهی شوند، بسیار مفید است. در حین بازگشت اطلاعات به کاربر نیز باید چنین رویهای درنظر گرفته شود. برای مثال مدل User میتواند به همراه آدرس ایمیل و کلمهی عبور هش شدهی او نیز باشد و نباید API ما این اطلاعات را بازگشت دهد. بازگشتی از آن باید بسیار کنترل شده و صرفا بر اساس نیاز مصرف کننده تنظیم شود. به همین جهت یک Dto مخصوص را نیز برای بازگشت اطلاعات از سرور اضافه میکنیم تا اطلاعات مشخصی را بازگشت دهد:
namespace MinimalBlog.Api.Features.Authors; public record AuthorGetDto { public int Id { get; init; } public string Name { get; init; } = default!; public string? Bio { get; init; } public DateTime DateOfBirth { get; init; } }
public class AuthorProfile : Profile { public AuthorProfile() { CreateMap<AuthorDto, Author>().ReverseMap(); CreateMap<Author, AuthorGetDto>() .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.FullName)); } }
الف) دستور و هندلر ایجاد نویسنده
public class CreateAuthorCommand : IRequest<AuthorGetDto>
- ابتدا نوع خروجی این هندلر نیز به AuthorGetDto تنظیم میشود:
public class CreateAuthorCommandHandler : IRequestHandler<CreateAuthorCommand, AuthorGetDto>
public async Task<AuthorGetDto> Handle(CreateAuthorCommand request, CancellationToken cancellationToken)
return _mapper.Map<AuthorGetDto>(toAdd);
ب) کوئری و هندلر بازگشت لیست نویسندهها
public class GetAllAuthorsQuery : IRequest<List<AuthorGetDto>>
public class GetAllAuthorsHandler : IRequestHandler<GetAllAuthorsQuery, List<AuthorGetDto>> { private readonly MinimalBlogDbContext _context; private readonly IMapper _mapper; public GetAllAuthorsHandler(MinimalBlogDbContext context, IMapper mapper) { _context = context ?? throw new ArgumentNullException(nameof(context)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } public async Task<List<AuthorGetDto>> Handle(GetAllAuthorsQuery request, CancellationToken cancellationToken) { var authors = await _context.Authors.ToListAsync(cancellationToken); return _mapper.Map<List<AuthorGetDto>>(authors); } }
endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) => await GetAllAuthorsAsync(mediator, ct)) .WithName("GetAllAuthors") .WithDisplayName("Authors") .WithTags("Authors") .Produces<List<AuthorGetDto>>() .Produces(500); endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto, CancellationToken ct) => await CreateAuthorAsync(authorDto, mediator, ct)) .WithName("CreateAuthor") .WithDisplayName("Authors") .WithTags("Authors") .Produces<AuthorGetDto>() .Produces(500);
پوشه بندی Features
تا اینجا تمام فایلهای متعلق به ویژگی Authors را در همان پوشه اصلی آن قرار دادهایم. در ادامه میتوان به ازای هر ویژگی خاص، 4 پوشهی Commands مخصوص Commands الگوی CQRS، پوشهی Models مخصوص تعریف DTO's، پوشهی Profiles مخصوص افزودن پروفایلهای AutoMapper و پوشهی Queries مخصوص تعریف کوئریهای الگوی CQRS را به نحوی که در تصویر فوق مشاهده میکنید، به پروژهی API اضافه کنیم.
پیاده سازی ویژگی Blogs
این پیاده سازی چون به همراه نکات جدیدی نیست و به همراه تعریف ماژول اصلی ویژگی، endpoints و الگوی CQRS ای است که تاکنون بحث شد، کدهای آن، به همراه کدهای پروژهی اصلی این پروژه که از قسمت اول قابل دریافت است، ارائه شدهاست.
تغییرات الگوریتمهای هش کردن اطلاعات
با حذف و تغییرنام کلاسهایی مانند SHA256Managed (و تمام کلاسهای Managed_) در NET Core.، معادل کدهایی مانند:
using (var sha256 = new SHA256Managed()) { // Crypto code here... }
public static string GetHash(string text) { using (var sha256 = SHA256.Create()) { var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(text)); return BitConverter.ToString(hashedBytes).Replace("-", "").ToLower(); } }
"dependencies": { "System.Security.Cryptography.Algorithms": "4.2.0" },
به علاوه اگر نیاز به محاسبهی هش حاصل از جمع چندین byte array را دارید، در اینجا میتوان از الگوریتمهای IncrementalHash به صورت ذیل استفاده کرد:
using (var md5 = IncrementalHash.CreateHash(HashAlgorithmName.MD5)) { md5.AppendData(byteArray1, 0, byteArray1.Length); md5.AppendData(byteArray2, 0, byteArray2.Length); var hash = md5.GetHashAndReset(); }
تولید اعداد تصادفی Thread safe در NET Core.
روشهای زیادی برای تولید اعداد تصادفی در برنامههای دات نت وجود دارند؛ اما مشکل اکثر آنها این است که thread safe نیستند و نباید از آنها در برنامههای چند ریسمانی (مانند برنامههای وب)، به نحو متداولی استفاده کرد. در این بین تنها کلاسی که thread safe است، کلاس RNGCryptoServiceProvider میباشد؛ آن هم با یک شرط:
private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider();
بنابراین اگر در کدهای خود چنین تعریفی را دارید:
var rand = new RNGCryptoServiceProvider();
در NET Core. این کلاس به طور کامل حذف شدهاست و معادل جدید آن کلاس RandomNumberGenerator است که به صورت ذیل قابل استفاده است (و در عمل تفاوتی بین کدهای آن با کدهای RNGCryptoServiceProvider نیست):
public interface IRandomNumberProvider { int Next(); int Next(int max); int Next(int min, int max); } public class RandomNumberProvider : IRandomNumberProvider { private readonly RandomNumberGenerator _rand = RandomNumberGenerator.Create(); public int Next() { var randb = new byte[4]; _rand.GetBytes(randb); var value = BitConverter.ToInt32(randb, 0); if (value < 0) value = -value; return value; } public int Next(int max) { var randb = new byte[4]; _rand.GetBytes(randb); var value = BitConverter.ToInt32(randb, 0); value = value % (max + 1); // % calculates remainder if (value < 0) value = -value; return value; } public int Next(int min, int max) { var value = Next(max - min) + min; return value; } }
public class Startup { public void ConfigureServices(IServiceCollection services) { services.TryAddSingleton<IRandomNumberProvider, RandomNumberProvider>();
نیاز به الگوریتمهای رمزنگاری متقارن قوی و معادل بهتر آنها در ASP.NET Core
ASP.NET Core به همراه یکسری API جدید است به نام data protection APIs که روشهایی را برای پیاده سازی بهتر الگوریتمهای هش کردن اطلاعات و رمزنگاری اطلاعات، ارائه میدهند و برای مثال ASP.NET Core Identity و یا حتی Anti forgery token آن، در پشت صحنه دقیقا از همین API برای انجام کارهای رمزنگاری اطلاعات استفاده میکنند.
برای مثال اگر بخواهید کتابخانهای را طراحی کرده و در آن از الگوریتم AES استفاده نمائید، نیاز است تنظیم اضافهتری را جهت دریافت کلید عملیات نیز اضافه کنید. اما با استفاده از data protection APIs نیازی به اینکار نیست و مدیریت ایجاد، نگهداری و انقضای این کلید به صورت خودکار توسط سیستم data protection انجام میشود. کلیدهای این سیستم موقتی هستند و طول عمری محدود دارند. بنابراین باتوجه به این موضوع، روش مناسبی هستند برای تولید توکنهای Anti forgery و یا تولید محتوای رمزنگاری شدهی کوکیها. بنابراین نباید از آن جهت ذخیره سازی اطلاعات ماندگار در بانکهای اطلاعاتی استفاده کرد.
فعال سازی این سیستم نیازی به تنظیمات اضافهتری در ASP.NET Core ندارد و جزو پیش فرضهای آن است. در کدهای ذیل، نمونهای از استفادهی از این سیستم را ملاحظه میکنید:
public interface IProtectionProvider { string Decrypt(string inputText); string Encrypt(string inputText); } namespace Providers { public class ProtectionProvider : IProtectionProvider { private readonly IDataProtector _dataProtector; public ProtectionProvider(IDataProtectionProvider dataProtectionProvider) { _dataProtector = dataProtectionProvider.CreateProtector(typeof(ProtectionProvider).FullName); } public string Decrypt(string inputText) { var inputBytes = Convert.FromBase64String(inputText); var bytes = _dataProtector.Unprotect(inputBytes); return Encoding.UTF8.GetString(bytes); } public string Encrypt(string inputText) { var inputBytes = Encoding.UTF8.GetBytes(inputText); var bytes = _dataProtector.Protect(inputBytes); return Convert.ToBase64String(bytes); } } }
public class Startup { public void ConfigureServices(IServiceCollection services) { services.TryAddSingleton<IProtectionProvider, ProtectionProvider>();
مستندات مفصل این API را در اینجا میتوانید مطالعه کنید.
معادل الگوریتم Rijndael در NET Core.
همانطور که عنوان شد، طول عمر کلیدهای data protection API محدود است و به همین جهت برای کارهایی چون تولید توکنها، رمزنگاری کوئری استرینگها و یا کوکیهای کوتاه مدت، بسیار مناسب است. اما اگر نیاز به ذخیره سازی طولانی مدت اطلاعات رمزنگاری شده وجود داشته باشد، یکی از الگوریتمهای مناسب اینکار، الگوریتم AES است.
الگوریتم Rijndael نگارش کامل دات نت، اینبار نام اصلی آن یا AES را در NET Core. پیدا کردهاست و نمونهای از نحوهی استفادهی از آن، جهت رمزنگاری و رمزگشایی اطلاعات، به صورت ذیل است:
public string Decrypt(string inputText, string key, string salt) { var inputBytes = Convert.FromBase64String(inputText); var pdb = new Rfc2898DeriveBytes(key, Encoding.UTF8.GetBytes(salt)); using (var ms = new MemoryStream()) { var alg = Aes.Create(); alg.Key = pdb.GetBytes(32); alg.IV = pdb.GetBytes(16); using (var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write)) { cs.Write(inputBytes, 0, inputBytes.Length); } return Encoding.UTF8.GetString(ms.ToArray()); } } public string Encrypt(string inputText, string key, string salt) { var inputBytes = Encoding.UTF8.GetBytes(inputText); var pdb = new Rfc2898DeriveBytes(key, Encoding.UTF8.GetBytes(salt)); using (var ms = new MemoryStream()) { var alg = Aes.Create(); alg.Key = pdb.GetBytes(32); alg.IV = pdb.GetBytes(16); using (var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write)) { cs.Write(inputBytes, 0, inputBytes.Length); } return Convert.ToBase64String(ms.ToArray()); } }
public class Personel { [Key] public int PersonelID { get; set; } [MaxLength(15)] public string Name { get; set; } [MaxLength(25)] public string Family { get; set; } [MaxLength(10)] public string CodeMelli { get; set; } }
public class PersonalContext : DbContext { public DbSet<Personel> Personel { get; set; } public override int SaveChanges() { return base.SaveChanges(); } }
Install-Package EntityFramework.BulkInsert-ef6
public ActionResult Insert() { int Counter = 1000; List<Personel> Lst = new List<Personel>(); // شبیه سازی خواندن رکوردها از فایل اکسل for (int i = 0; i < Counter; i++) { Lst.Add(new Personel { CodeMelli = "0000000000", Family = "Karimi", Name = "Mohammad" }); } PersonalContext db = new PersonalContext(); db.BulkInsert(Lst); db.SaveChanges(); return View(); }
ایجاد یک پروژهی Blazor WASM جدید
یک پوشهی جدید دلخواه را به نام BlazorWasmSQLite ایجاد کرده و با اجرای دستور dotnet new blazorwasm، یک پروژهی Blazor Web Assembly خالی جدید را در آن آغاز میکنیم. همانطور که از دستور نیز مشخص است، این پروژه از نوع hosted که به همراه Web API هم هست، نمیباشد.
افزودن Context و مدل EF-Core به برنامه
مدل برنامه به صورت زیر در پوشهی Models آن قرار میگیرد:
namespace BlazorWasmSQLite.Models; public class Car { public int Id { get; set; } public string Brand { get; set; } public int Price { get; set; } }
using Microsoft.EntityFrameworkCore; using BlazorWasmSQLite.Models; namespace BlazorWasmSQLite.Data; public class ClientSideDbContext : DbContext { public DbSet<Car> Cars { get; set; } = default!; public ClientSideDbContext(DbContextOptions<ClientSideDbContext> options) : base(options) { } }
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <ItemGroup> <!-- EF Core and Sqlite --> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" /> </ItemGroup> </Project>
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using BlazorWasmSQLite; using Microsoft.EntityFrameworkCore; using BlazorWasmSQLite.Data; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<HeadOutlet>("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); // Sets up EF Core with Sqlite builder.Services.AddDbContextFactory<ClientSideDbContext>(options => options .UseSqlite($"Filename=DemoData.db") .EnableSensitiveDataLogging()); await builder.Build().RunAsync();
ثبت تعدادی رکورد در بانک اطلاعاتی
در ادامه سعی میکنیم در فایل Index.razor، تعدادی رکورد را به بانک اطلاعاتی اضافه کنیم:
@page "/" @using Microsoft.Data.Sqlite @using Microsoft.EntityFrameworkCore @using BlazorWasmSQLite.Data @using BlazorWasmSQLite.Models <PageTitle>Index</PageTitle> <h1>Hello, world!</h1> Welcome to your new app. <SurveyPrompt Title="How is Blazor working for you?" /> @code { [Inject] private IDbContextFactory<ClientSideDbContext> _dbContextFactory { get; set; } = default!; protected override async Task OnInitializedAsync() { await using var db = await _dbContextFactory.CreateDbContextAsync(); await db.Database.EnsureCreatedAsync(); // create seed data if (!db.Cars.Any()) { var cars = new[] { new Car { Brand = "Audi", Price = 21000 }, new Car { Brand = "Volvo", Price = 11000 }, new Car { Brand = "Range Rover", Price = 135000 }, new Car { Brand = "Ford", Price = 8995 } }; await db.Cars.AddRangeAsync(cars); await db.SaveChangesAsync(); } await base.OnInitializedAsync(); } }
اولین سعی در اجرای برنامه
در ادامه سعی میکنیم تا برنامه را اجرا کنیم. با خطای زیر متوقف خواهیم شد:
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. System.TypeInitializationException: The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.DllNotFoundException: e_sqlite3 at SQLitePCL.SQLite3Provider_e_sqlite3.SQLitePCL.ISQLite3Provider.sqlite3_libversion_number()
رفع مشکل کار با SQLite با کامپایل ویژهی آن
برای دسترسی به کدهای native در Blazor WASM و مرورگر، باید آنها را توسط کامپایلر emcc به صورت زیر کامپایل کرد:
$ git clone https://github.com/cloudmeter/sqlite $ cd sqlite $ emcc sqlite3.c -shared -o e_sqlite3.o
مرحلهی بعد، معرفی این object file تولید شده به برنامه است. برای اینکار ابتدا باید dotnet workload install wasm-tools را نصب کرد (مهم). سپس به فایل csproj برنامه مراجعه کرده و فایل e_sqlite3.o را به آن معرفی میکنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <ItemGroup> <!-- EF Core and Sqlite --> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" /> <NativeFileReference Include="Data\e_sqlite3.o" /> </ItemGroup> </Project>
سعی در اجرای مجدد برنامه
پس از نصب wasm-tools و ذکر NativeFileReference فوق، اکنون اگر برنامه را اجرا کنیم، برنامه بدون مشکل اجرا خواهد شد:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmSQLite.zip