Over the last decade, Android's open platform has created a thriving community of manufacturers and developers that reach a global audience with their devices and apps. This has expanded beyond phones to tablets, cars, watches, TVs and more—with more than 2.5 billion active devices around the world. As we continue to build Android for everyone in the community, our brand should be as inclusive and accessible as possible—and we think we can do better in a few ways.
This is a defense of the most prolific and dedicated public servant that has graced the world in my lifetime. One man has added hundreds of billions, if not trillions of dollars of value to the global economy. This man has worked tirelessly for the benefit of everyone around him. It is impossible to name a publicly traded company that has not somehow benefited from his contributions, and many have benefited to the tune of billions. In return for the countless billions of wealth that people made from the fruits of his labor, he was rewarded with poverty and ridicule. Now that the world is done taking from him, they are heading to the next step of vilifying him as incompetent.
تاریخچه
تا پیش از نگارش 1.5 تایپ اسکریپت، مفاهیم internal modules و external modules وجود داشتند. جهت نامگذاری بهتر و کاهش سردرگمی در استفادهی آنها، از نگارش 1.5 به بعد، ماژولهای داخلی به namespaces (فضاهای نام) تغییر نام یافتند و ماژولهای خارجی به نام «ماژول» خلاصه شدند.
همچنین از نگارش 1.5 به بعد، پشتیبانی کاملی از نحوهی تعریف «ماژولها در ES 6» نیز به عمل میآید. بنابراین مطالعهی آن نیز پیشنهاد میگردد.
مفهوم ماژولها
هدف اصلی از ماژولها، ارائهی روشی برای مدیریت و ساماندهی پروژههای بزرگ با تعداد فایلهای زیاد است. در اینجا فایلهای ارجاعی، در زمان اجرا، توسط runtime جاوا اسکریپت بارگذاری شده و سپس به امکانات آنها دسترسی خواهیم داشت. ماژولها به صورت توکار در Node.JS نیز پشتیبانی میشوند؛ البته با فرمت common.js که کامپایلر TypeScript نیز قادر به تولید آن است.
امکان کامپایل به روشهای قدیمیتر تعریف ماژولها در TypeScript
در مورد انواع روشهای قدیمیتر نحوهی تعریف ماژولهای در جاوا اسکریپت مانند common.js، AMD و امثال آنها، مطالعهی مطلب «ماژولها در ES 6» توصیه میشود. فقط نکتهای که در اینجا حائز اهمیت است، این است که چون TypeScript قادر است به ES 5 نیز کامپایل شود و در ES 5 روش جدید ES 6 جهت تعریف ماژولها وجود ندارد، امکان تبدیل و ترجمهی کدهای TypeScript به تمام نوعهای معروف و شناخته شدهی ماژولها مانند common.js توسط کامپایلر TypeScript به صورت خودکار وجود دارد. برای این منظور از سوئیچ module کامپایلر استفاده میشود.
نحوهی تعریف ماژولها در TypeScript
برای تبدیل یک فایل ts به یک ماژول، تنها کافی است موردی را از آن export کنیم. آیتمهای موجود در یک ماژول، تنها زمانی در سایر فایلها قابل استفاده خواهند بود که از آن export شده باشند:
در این مثال، یک اینترفیس، کلاس و متد export شدهاند. برای این منظور واژهی کلیدی export به پیش از هر کدام از آیتمهای مدنظر اضافه شدهاست.
روش دیگر انجام این تعاریف، حذف واژهی کلیدی export از تمام موارد تعریف شده و سپس خلاصه کردن آنها در یک سطر، توسط روش export statement است؛ به نحو ذیل:
مزیت این روش، مشخص بودن محل تعاریف خروجیها است؛ بدون اینکه نیازی باشد تا تمام فایلرا جهت یافتن exportها جستجو کرد.
همچنین در اینجا میتوان نام دیگری را نیز برای خروجیها درنظر گرفت. برای مثال بجای نام GetMagazineByTitle، با استفاده از as syntax، یک نام جدید معرفی شدهاست.
نحوهی استفادهی از ماژولها در TypeScript
برای استفادهی از امکانات خروجی مثال قبل، در یک ماژول دیگر، به نحو ذیل عمل میکنیم:
در اینجا پس از تعریف واژهی کلیدی import، لیست موارد مدنظر از خروجیهای فایل periodicals را داخل یک {} میتوان قید کرد. بنابراین نیازی نیست تا تمام خروجیهای یک ماژول را import کرد. همچنین در اینجا نیز با استفاده از as syntax میتوان نام جدیدی را برای موارد import شده تعیین کرد.
در انتها نیز مسیر نسبی فایل ts ماژول، بدون ذکر پسوند آن، پس از واژهی کلیدی from ذکر میشود.
اگر نیاز است تمام خروجیهای یک ماژول به صورت خودکار import شوند، میتوان از * استفاده کرد:
اینبار با توجه به as syntax استفاده شده، نحوهی دسترسی به خروجیهای ماژول مدنظر به صورت ذیل خواهد بود (ابتدا ذکر نام alias تعریف شده، به همراه یک دات):
خروجی پیش فرض یک ماژول
اگر تنها قرار است یک آیتم از ماژولی export شود، میتوان از مفهوم default export استفاده کرد:
در این مثال export default بر روی یک کلاس بدون نام تعریف شدهاست. تعریف نام کلاس در اینجا اختیاری است و ماژول import کنندهی آن نیازی به دانستن این نام ندارد؛ زیرا در این حالت import کننده میتواند نام دلخواهی را به این خروجی پیش فرض بدهد؛ مانند AnimatedMovie بدون نیاز به ذکر {}:
تا پیش از نگارش 1.5 تایپ اسکریپت، مفاهیم internal modules و external modules وجود داشتند. جهت نامگذاری بهتر و کاهش سردرگمی در استفادهی آنها، از نگارش 1.5 به بعد، ماژولهای داخلی به namespaces (فضاهای نام) تغییر نام یافتند و ماژولهای خارجی به نام «ماژول» خلاصه شدند.
همچنین از نگارش 1.5 به بعد، پشتیبانی کاملی از نحوهی تعریف «ماژولها در ES 6» نیز به عمل میآید. بنابراین مطالعهی آن نیز پیشنهاد میگردد.
مفهوم ماژولها
هدف اصلی از ماژولها، ارائهی روشی برای مدیریت و ساماندهی پروژههای بزرگ با تعداد فایلهای زیاد است. در اینجا فایلهای ارجاعی، در زمان اجرا، توسط runtime جاوا اسکریپت بارگذاری شده و سپس به امکانات آنها دسترسی خواهیم داشت. ماژولها به صورت توکار در Node.JS نیز پشتیبانی میشوند؛ البته با فرمت common.js که کامپایلر TypeScript نیز قادر به تولید آن است.
امکان کامپایل به روشهای قدیمیتر تعریف ماژولها در TypeScript
در مورد انواع روشهای قدیمیتر نحوهی تعریف ماژولهای در جاوا اسکریپت مانند common.js، AMD و امثال آنها، مطالعهی مطلب «ماژولها در ES 6» توصیه میشود. فقط نکتهای که در اینجا حائز اهمیت است، این است که چون TypeScript قادر است به ES 5 نیز کامپایل شود و در ES 5 روش جدید ES 6 جهت تعریف ماژولها وجود ندارد، امکان تبدیل و ترجمهی کدهای TypeScript به تمام نوعهای معروف و شناخته شدهی ماژولها مانند common.js توسط کامپایلر TypeScript به صورت خودکار وجود دارد. برای این منظور از سوئیچ module کامپایلر استفاده میشود.
نحوهی تعریف ماژولها در TypeScript
برای تبدیل یک فایل ts به یک ماژول، تنها کافی است موردی را از آن export کنیم. آیتمهای موجود در یک ماژول، تنها زمانی در سایر فایلها قابل استفاده خواهند بود که از آن export شده باشند:
// periodicals.ts export interface Periodical { issueNumber: number; } export class Magazine implements Periodical { issueNumber: number; } export function GetMagazineByIssueNumber(issue: number): Magazine { // retrieve and return a magazine }
روش دیگر انجام این تعاریف، حذف واژهی کلیدی export از تمام موارد تعریف شده و سپس خلاصه کردن آنها در یک سطر، توسط روش export statement است؛ به نحو ذیل:
// periodicals.ts interface Periodical { issueNumber: number; } class Magazine implements Periodical { issueNumber: number; } function GetMagazineByTitle(title: string): Magazine { // retrieve and return a magazine } export { Periodical, Magazine, GetMagazineByTitle as GetMag}
همچنین در اینجا میتوان نام دیگری را نیز برای خروجیها درنظر گرفت. برای مثال بجای نام GetMagazineByTitle، با استفاده از as syntax، یک نام جدید معرفی شدهاست.
نحوهی استفادهی از ماژولها در TypeScript
برای استفادهی از امکانات خروجی مثال قبل، در یک ماژول دیگر، به نحو ذیل عمل میکنیم:
// news.ts import { Magazine, GetMag as GetMagazine} from './periodicals'; let newsMag: Magazine = GetMagazine('Weekly News');
در انتها نیز مسیر نسبی فایل ts ماژول، بدون ذکر پسوند آن، پس از واژهی کلیدی from ذکر میشود.
اگر نیاز است تمام خروجیهای یک ماژول به صورت خودکار import شوند، میتوان از * استفاده کرد:
// kids.ts import * as mag from './periodicals';
let kidMag: mag.Magazine= mag.GetMag('Games and Stuff!');
خروجی پیش فرض یک ماژول
اگر تنها قرار است یک آیتم از ماژولی export شود، میتوان از مفهوم default export استفاده کرد:
// movie.ts export default class{ title: string; director: string; }
// kids.ts import AnimatedMovie from './movie’; let cartoon = new AnimatedMovie();
پیش نیاز این مطلب، قسمت قبل آن است. در قسمت قبل، یک کلاس جنریک را به نام BaseDto ایجاد کردیم که با ارث بری Dtoهای پروژه از این کلاس، علاوه بر متدهای ToEntity و FromEntity جهت ساده سازی عملیات نگاشت، Mappingهای لازم بین Dtoها و Entityهای مربوطه، توسط Reflection به صورت خودکار انجام میشد.
در این قسمت میخواهیم مکانیزم Mapping خودکار را کمی تغییر داده و قابلیت سفارشی سازی Mappingها را فراهم کنیم. سورس کامل مثال را میتوانید در این ریپازیتوری مشاهده کنید.
ابتدا یک اینترفیس را به نام IHaveCustomMapping به نحو زیر ایجاد میکنیم.
public interface IHaveCustomMapping { void CreateMappings(AutoMapper.Profile profile); }
به عنوان مثال کلاس زیر، Mapping لازم برای PostDto و Post را درون متد CreateMappings خود اعمال میکند.
public class PostDtoMapping : IHaveCustomMapping { public void CreateMappings(Profile profile) { profile.CreateMap<PostDto, Post>().ReverseMap(); } }
بدین منظور کلاسی را به نام CustomMappingProfile به نحو زیر تعریف میکنیم.
public class CustomMappingProfile : Profile { public CustomMappingProfile(IEnumerable<IHaveCustomMapping> haveCustomMappings) { foreach (var item in haveCustomMappings) item.CreateMappings(this); } }
- این کلاس از AutoMapper.Profile ارث بری کردهاست.
- درون سازندهی خود لیستی از اشیاء اینترفیس IHaveCustomMapping را دریافت کرده و بر روی آنها گردش میکند.
- و متد CreateMappings هرکدام را فراخوانی کرده و خودش (this : شی جاری) را (که از نوع Profile شده) به عنوان پارامتر ورودی پاس میدهد.
اکنون کلاس AutoMapperConfiguration قسمت قبل را به نحو زیر اصلاح میکنیم.
public static class AutoMapperConfiguration { public static void InitializeAutoMapper() { Mapper.Initialize(config => { config.AddCustomMappingProfile(); }); //Compile mapping after configuration to boost map speed Mapper.Configuration.CompileMappings(); } public static void AddCustomMappingProfile(this IMapperConfigurationExpression config) { config.AddCustomMappingProfile(Assembly.GetEntryAssembly()); } public static void AddCustomMappingProfile(this IMapperConfigurationExpression config, params Assembly[] assemblies) { var allTypes = assemblies.SelectMany(a => a.ExportedTypes); //Find all classes that implement IHaveCustomMapping inteface and create new instance of each var list = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.GetInterfaces().Contains(typeof(IHaveCustomMapping))) .Select(type => (IHaveCustomMapping)Activator.CreateInstance(type)); //Create a new automapper Profile for this list to create mapping then add to the config var profile = new CustomMappingProfile(list); config.AddProfile(profile); } }
- توضیحات متد های InitializeAutoMapper و AddCustomMappingProfile، مشابه مطلب قبل است و لازم به ذکر مجدد نیست.
- متد AddCustomMappingProfile آرایهای از اسمبلیها را دریافت و سپس تمامی نوعهای قابل دسترس آنها را (ExportedTypes) واکشی میکند.
- سپس توسط شرط Where، نوعهایی که کلاس بوده، abstract نیستند و از اینترفیس IHaveCustomMapping مشتق شدهاند فیلتر میشوند.
- سپس توسط متد Activator.CreateInstance، وهلهای از آنها ایجاد و به نوع IHaveCustomMapping تبدیل میشوند و نهایتا لیستی از اشیاء وهله سازی شده را باز میگرداند.
- سپس وهلهای از نوع CustomMappingProfile (که مسئول اعمال Mappingهای اشیاء دریافتی است و قبلا بررسی کردیم) ایجاد میکنیم و لیست مذکور را به سازنده آن پاس میدهیم.
- نهایتا profile ساخته شده (حاوی تمامی Mappingهای اعمال شده) را توسط متد config.AddProfile به AutoMapper معرفی میکنیم (در این لحظه تمامی Mappingهای تعریف شده داخل profile، به AutoMapper اعمال میشوند).
توسط این مکانیزم، هر کلاسی که اینترفیس IHaveCustomMapping را پیاده سازی کرده باشد، به صورت خودکار یافت شده و Mapping به آنها اعمال میشود. حال میتوان این مکانیزم را با BaseDto قسمت قبل ترکیب کرده و کلاس BaseDto را به نحو زیر اصلاح کنیم.
public abstract class BaseDto<TDto, TEntity, TKey> : IHaveCustomMapping where TEntity : BaseEntity<TKey> { [Display(Name = "ردیف")] public TKey Id { get; set; } /// <summary> /// Maps this dto to a new entity object. /// </summary> public TEntity ToEntity() { return Mapper.Map<TEntity>(CastToDerivedClass(this)); } /// <summary> /// Maps this dto to an exist entity object. /// </summary> public TEntity ToEntity(TEntity entity) { return Mapper.Map(CastToDerivedClass(this), entity); } /// <summary> /// Maps the specified entity to a new dto object. /// </summary> public static TDto FromEntity(TEntity model) { return Mapper.Map<TDto>(model); } protected TDto CastToDerivedClass(BaseDto<TDto, TEntity, TKey> baseInstance) { return Mapper.Map<TDto>(baseInstance); } //Get automapper Profile then create mapping and ignore unmapped properties public void CreateMappings(Profile profile) { var mappingExpression = profile.CreateMap<TDto, TEntity>(); var dtoType = typeof(TDto); var entityType = typeof(TEntity); //Ignore mapping to any property of source (like Post.Categroy) that dose not contains in destination (like PostDto) //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value foreach (var property in entityType.GetProperties()) { if (dtoType.GetProperty(property.Name) == null) mappingExpression.ForMember(property.Name, opt => opt.Ignore()); } //Pass mapping expressin to customize mapping in concrete class CustomMappings(mappingExpression.ReverseMap()); } //Concrete class can override this method to customize mapping public virtual void CustomMappings(IMappingExpression<TEntity, TDto> mapping) { } }
- کلاس جنریک BaseDto، متدCreateMappings اینترفیس IHaveCustomMapping را پیاده سازی میکند.
- درون این متد، Mapping بین دو نوع TDto و TEntity، توسط ()<profile.CreateMap<TDto, TEntity کانفیگ میشود.
- مانند مطلب قبل، خواصی را که نباید نگاشت شوند، توسط Reflection یافته و Ignore میکنیم.
- سپس Mapping برعکس را توسط ReverseMap اعمال کرده و به متد زیرین آن که virtual نیز است، پاس میدهیم.
متد CustomMappings ای که به صورت virtual تعریف شدهاست، این امکان را به ما میدهد که در کلاسهایی که از BaseDto ارث بری میکنند، در صورت لزوم آن را بازنویسی (override) کرده و سفارشی سازی دلخواهمان را بر روی Mapping دریافتی اعمال کنیم.
مثال: کلاس PostDto زیر از BaseDto ارث بری کرده و چون سفارشی سازیای لازم دارد، متد CustomMappings والد خود را override کرده است.
public class PostDto : BaseDto<PostDto, Post, long> { public string Title { get; set; } public string Text { get; set; } public int CategoryId { get; set; } public string CategoryName { get; set; } //=> Category.Name public string FullTitle { get; set; } //=> custom mapping for "Title (Category.Name)" public override void CustomMappings(IMappingExpression<Post, PostDto> mapping) { mapping.ForMember( dest => dest.FullTitle, config => config.MapFrom(src => $"{src.Title} ({src.Category.Name})")); } }
- این کلاس، خاصیتی به نام FullTitle دارد که معادلی (خاصیت همنامی) در کلاس Post برای آن وجود ندارد و قرار است مقدار ترکیبی حاصل از Title و Category.Name را نمایش دهد.
- به همین جهت متد CustomMappings را باز نویسی کرده، شیء mapping را دریافت و سفارشی سازی لازم را روی آن انجام دادهایم.
- توسط متد ForMember مشخص کردهایم که مقدار خاصیت FullTitle باید حاصلی از ترکیب Title و Category.Name به نحو مشخص شده باشد ( توسط متد MapFrom).
پس در این روش علاوه بر امکانات BaseDto و Mapping خودکار، امکان سفارشی سازی دلخواه را نیز خواهیم داشت.
برای کوئری گرفتن از دیتابیس نیز و تبدیل آنها به لیستی از Dtoها میتوان از متد ProjectTo بر روی IQueryable استفاده کرد و حتی شرط Where را بر روی کوئری Dtoها اعمال کرد مانند زیر:
List<PostDto> list = //ProjectTo method select only needed properties (of PostDto) not all properties //Also select only needed property of navigations (like Post.Category.Name) not all unlike Include //This ability called "Projection" await _applicationDbContext.Posts.ProjectTo<PostDto>() //We can also use Where on IQuerable<PostDto> .Where(p => p.Title.Contains("test") || p.CategoryName.Contains("test")) .ToListAsync();
- متد ProjectTo کوئری post را به IQueryable ای از postDto تبدیل میکند (این قابلیت Projection نامیده میشود).
- نگاشت خودکار خواص موجود در postDto توسط AutoMapper به صورت خودکار انجام میشود و فقط خواص لازم برای postDto واکشی میشوند (نه همه خواص در جدول post، که این به لحاظ کارآیی بهتر است).
- همچنین اگر خواصی را داخل Navigation Propertyها مانند CategoryName داشته باشیم، موقع کوئری گرفتن از دیتابیس، آنها نیز اعمال شده و فقط خواص لازم از Category واکشی میشوند (فقط خاصیت Name، بر خلاف Include که همه ستونها را واکشی میکند).
- همچنین میتوان بر روی خواص Dto شرط Where را قرار داد مانند p.CategoryName.Contains("test") و تماما به کوئری SQL معادل آن ترجمه و اجرا میشوند.
اشتراکها
آموزش ASP.NET Web API
اشتراکها
بررسی C# Object Notation
در زمان ارائهی ASP.NET Core 2.1، ویژگی جدیدی به نام [ApiController] ارائه شد که با استفاده از آن، یکسری اعمال توکار جهت سهولت کار با Web API توسط خود فریمورک انجام میشوند؛ برای مثال عدم نیاز به بررسی وضعیت ModelState و بررسی خودکار آن با علامتگذاری یک کنترلر به صورت ApiController. یکی دیگر از این ویژگیهای توکار، تبدیل خروجی تمام status codeهای بزرگتر و یا مساوی 400 یا همان Bad Request، به شیء جدید و استاندارد ProblemDetails است:
بازگشت یک چنین خروجی یکدست و استانداردی، استفادهی از آنرا توسط کلاینتها، ساده و قابل پیشبینی میکند. البته باید درنظر داشت که اگر در اینحالت، برنامه یک استثنای معمولی را سبب شود، ProblemDetails ای بازگشت داده نمیشود. اگر برنامه در حالت توسعه اجرا شود، با استفاده از میانافزار app.UseDeveloperExceptionPage، یک صفحهی نمایش جزئیات خطا ظاهر میشود و اگر برنامه در حالت تولید و ارائهی نهایی اجرا شود، یک صفحهی خالی (بدون داشتن response body) با status code مساوی 500 بازگشت داده میشود. این کمبود ویژه و امکانات سفارشی سازی بیشتر آن، به صورت توکار به ASP.NET Core 7x اضافه شدهاند و دیگر نیازی به استفاده از کتابخانههای ثالث دیگری برای انجام آن نیست.
ProblemDetails بر اساس RFC7807 طراحی شدهاست
RFC7807، قالب استانداردی را برای ارائهی خطاهای HTTP APIها تعریف میکند تا نیازی به وجود تعاریف متعددی در این زمینه نباشد و خروجی آن قابل پیشبینی و قابل بررسی توسط تمام کلاینتهای یک API باشد. کلاس ProblemDetails در ASP.NET Core نیز بر همین اساس طراحی شدهاست.
این RFC دو فرمت خروجی را بر اساس مقدار مشخص شدهی در هدر Content-Type بازگشت داده شده، مجاز میداند:
که با توجه به این هدر ارسالی، اگر از یک کلاینت از نوع HttpClient استفاده کنیم، میتوان بر اساس مقدار ویژهی «application/problem+json» تشخیص داد که خروجی API دریافتی، به همراه خطا است و نحوهی پردازش آن به صورت زیر خواهد بود:
در اینجا بدنهی اصلی شیء ProblemDetails بازگشت داده شده، میتواند به همراه اعضای زیر باشد:
- type: یک رشتهاست که به آدرس مستندات HTML ای مرتبط با خطای بازگشت داده شده، اشاره میکند.
- title: رشتهای است که خلاصهی خطای رخداده را بیان میکند.
- detail: رشتهای است که توضیحات بیشتری را در مورد خطای رخداده، بیان میکند.
- instance: رشتهای است که به آدرس محل بروز خطا اشاره میکند.
- status: عددی است که بیانگر HTTP status code بازگشتی از سمت سرور است.
البته اگر ویژگی ApiController بر روی کنترلرهای خود استفاده نمیکنید، میتوانید این خروجی را به صورت زیر هم با استفاده از return Problem، تولید کنید:
امکان افزودن اعضای سفارشی به شیء ProblemDetails
امکان بسط این خروجی، با افزودن اعضای سفارشی نیز پیشبینی شدهاست. یک نمونهی متداول و پرکاربرد آن، بازگشت خطاهای مرتبط با اعتبارسنجی اطلاعات رسیدهاست:
در اینجا عضو جدید errors را بنابر نیاز این مسالهی خاص، مشاهده میکنید که در صورت استفاده از ویژگی ApiController بر روی کنترلرهای Web API، به صورت خودکار توسط ASP.NET Core تولید میشود و نیازی به تنظیم خاصی و یا کدنویسی اضافهتری ندارد. کلاس مخصوص آن نیز ValidationProblemDetails است.
جهت افزودن اعضای سفارشی دیگری به شیء ProblemDetails میتوان به صورت زیر عمل کرد:
شیء ProblemDetails، به همراه خاصیت Extensions است که میتوان به آن یک <Dictionary<string, object را انتساب داد و نمونهای از آنرا در مثال فوق مشاهده میکنید. این مثال سبب میشود تا عضو جدیدی با کلید دلخواه invalidParams، به همراه لیستی از name و reasonها به خروجی نهایی اضافه شود. مقدار این کلید، از نوع object است؛ یعنی هر شیء دلخواهی را در اینجا میتوان تعریف و استفاده کرد.
معرفی سرویس جدید ProblemDetails در دات نت 7
در دات نت 7 میتوان سرویسهای جدید ProblemDetails را به نحو زیر به برنامه اضافه کرد:
پس از آن به 3 روش مختلف میتوان از امکانات این سرویسها استفاده کرد:
الف) با اضافه کردن میانافزار مدیریت خطاها
پس از آن، هر استثنای مدیریت نشدهای نیز به صورت یک ProblemDetails ظاهر میشود و دیگر همانند قبل، سبب نمایش یک صفحهی خالی نخواهد شد.
ب) با افزودن میانافزار StatusCodePages
در این حالت مواردی که استثناء شمرده نمیشوند مانند 404، در صورت بروز رسیدن به یک مسیریابی یافت نشده و یا 405، در صورت درخواست یک HTTP method غیرمعتبر نیز توسط یک ProblemDetails استاندارد مدیریت میشوند.
ج) با افزودن میانافزار صفحهی استثناءهای توسعه دهندهها
به این ترتیب در خروجی ProblemDetails، اطلاعات بیشتری از استثناء رخداده، مانند استکتریس آن ظاهر خواهد شد.
امکان بازگشت سادهتر یک ProblemDetails سفارشی در دات نت 7
برای سفارشی سازی خروجی ProblemDetails، علاوه بر راهحلی که پیشتر در این مطلب مطرح شد، میتوان در دات نت 7 از روش تکمیلی ذیل نیز استفاده کرد:
به این ترتیب در صورت لزوم میتوان یک عضو سفارشی سراسری را به تمام اشیاء ProblemDetails برنامه به صورت خودکار اضافه کرد و یا اگر میخواهیم این مورد را کمی اختصاصیتر کنیم، میتوان به صورت زیر عمل کرد:
الف) تعریف یک ErrorFeature سفارشی
در ASP.NET Core میتوان به شیء HttpContext.Features قابل تنظیم در هر اکشن متدی، اشیاء دلخواهی را مانند شیء سفارشی فوق، اضافه کرد و سپس در قسمت options.CustomizeProblemDetails تنظیماتی که ذکر شد، به دریافت و تنظیم آن، واکنش نشان داد.
ب) تنظیم مقدار ErrorFeature سفارشی در اکشن متدها
پس از تعریف شیءایی که قرار است به HttpContext.Features اضافه شود، اکنون روش تنظیم و مقدار دهی آنرا در یک اکشن متد، در مثال فوق مشاهده میکنید.
ج) واکنش نشان دادن به دریافت ErrorFeature سفارشی
پس از تنظیم HttpContext.Features در اکشن متدی، میتوان در options.CustomizeProblemDetails فوق، توسط متد ctx.HttpContext.Features.Get به آن شیء خاص تنظیم شده، در صورت وجود دسترسی یافت و سپس جزئیات بیشتری را از آن استخراج و مقادیر ctx.ProblemDetails جاری را که قرار است به کاربر بازگشت داده شوند، بازنویسی کرد و یا تغییر داد.
امکان تبدیل سادهتر اطلاعات استثناءهای سفارشی به یک ProblemDetails سفارشی در دات نت 7
بجای استفاده از تنظیمات services.AddProblemDetails جهت بازنویسی مقدار شیء ProblemDetails بازگشتی، میتوان جزئیات میانافزار app.UseExceptionHandler را نیز سفارشی سازی کرد و به بروز استثناءهای خاصی واکنش نشان داد. برای مثال فرض کنید یک استثنای سفارشی را به صورت زیر طراحی کردهاید:
و سپس در اکشن متدی، سبب بروز آن شدهاید:
اکنون میتوان در میانافزار مدیریت استثناءهای برنامه، نسبت به مدیریت این استثناء خاص، واکشن نشان داد و ProblemDetails متناظری را تولید و بازگشت داد:
در اینجا نحوهی کار با سرویس توکار IProblemDetailsService و سپس دسترسی به IExceptionHandlerFeature و استثنای صادر شده را مشاهده میکنید. پس از آن بر اساس نوع و اطلاعات این استثناء، میتوان یک ProblemDetails مخصوص را تولید و در خروجی ثبت کرد.
{ "type": "https://example.com/probs/out-of-credit", "title": "You do not have enough credit.", "detail": "Your current balance is 30, but that costs 50.", "instance": "/account/12345/msgs/abc", "status": 403, }
ProblemDetails بر اساس RFC7807 طراحی شدهاست
RFC7807، قالب استانداردی را برای ارائهی خطاهای HTTP APIها تعریف میکند تا نیازی به وجود تعاریف متعددی در این زمینه نباشد و خروجی آن قابل پیشبینی و قابل بررسی توسط تمام کلاینتهای یک API باشد. کلاس ProblemDetails در ASP.NET Core نیز بر همین اساس طراحی شدهاست.
این RFC دو فرمت خروجی را بر اساس مقدار مشخص شدهی در هدر Content-Type بازگشت داده شده، مجاز میداند:
- JSON: “application/problem+json” media type
- XML: “application/problem+xml” media type
که با توجه به این هدر ارسالی، اگر از یک کلاینت از نوع HttpClient استفاده کنیم، میتوان بر اساس مقدار ویژهی «application/problem+json» تشخیص داد که خروجی API دریافتی، به همراه خطا است و نحوهی پردازش آن به صورت زیر خواهد بود:
var mediaType = response.Content.Headers.ContentType?.MediaType; if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) { var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(null, ct) ?? new ProblemDetails(); // ... }
- type: یک رشتهاست که به آدرس مستندات HTML ای مرتبط با خطای بازگشت داده شده، اشاره میکند.
- title: رشتهای است که خلاصهی خطای رخداده را بیان میکند.
- detail: رشتهای است که توضیحات بیشتری را در مورد خطای رخداده، بیان میکند.
- instance: رشتهای است که به آدرس محل بروز خطا اشاره میکند.
- status: عددی است که بیانگر HTTP status code بازگشتی از سمت سرور است.
البته اگر ویژگی ApiController بر روی کنترلرهای خود استفاده نمیکنید، میتوانید این خروجی را به صورت زیر هم با استفاده از return Problem، تولید کنید:
[HttpPost("/sales/products/{sku}/availableForSale")] public async Task<IActionResult> AvailableForSale([FromRoute] string sku) { return Problem( "Product is already Available For Sale.", "/sales/products/1/availableForSale", 400, "Cannot set product as available.", "http://example.com/problems/already-available"); }
امکان بسط این خروجی، با افزودن اعضای سفارشی نیز پیشبینی شدهاست. یک نمونهی متداول و پرکاربرد آن، بازگشت خطاهای مرتبط با اعتبارسنجی اطلاعات رسیدهاست:
HTTP/1.1 400 Bad Request Content-Type: application/problem+json Content-Language: en { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "User": [ "The user name is not verified." ] } }
جهت افزودن اعضای سفارشی دیگری به شیء ProblemDetails میتوان به صورت زیر عمل کرد:
namespace WebApplication.Controllers { [ApiController] [Route("[controller]")] public class DemoController : ControllerBase { [HttpPost] public ActionResult Post() { var problemDetails = new ProblemDetails { Detail = "The request parameters failed to validate.", Instance = null, Status = 400, Title = "Validation Error", Type = "https://example.net/validation-error", }; problemDetails.Extensions.Add("invalidParams", new List<ValidationProblemDetailsParam>() { new("name", "Cannot be blank."), new("age", "Must be great or equals to 18.") }); return new ObjectResult(problemDetails) { StatusCode = 400 }; } } public class ValidationProblemDetailsParam { public ValidationProblemDetailsParam(string name, string reason) { Name = name; Reason = reason; } public string Name { get; set; } public string Reason { get; set; } } }
معرفی سرویس جدید ProblemDetails در دات نت 7
در دات نت 7 میتوان سرویسهای جدید ProblemDetails را به نحو زیر به برنامه اضافه کرد:
services.AddProblemDetails();
الف) با اضافه کردن میانافزار مدیریت خطاها
app.UseExceptionHandler();
ب) با افزودن میانافزار StatusCodePages
app.UseStatusCodePages();
ج) با افزودن میانافزار صفحهی استثناءهای توسعه دهندهها
app.UseDeveloperExceptionPage();
امکان بازگشت سادهتر یک ProblemDetails سفارشی در دات نت 7
برای سفارشی سازی خروجی ProblemDetails، علاوه بر راهحلی که پیشتر در این مطلب مطرح شد، میتوان در دات نت 7 از روش تکمیلی ذیل نیز استفاده کرد:
builder.Services.AddProblemDetails(options => options.CustomizeProblemDetails = ctx => ctx.ProblemDetails.Extensions.Add("MachineName", Environment.MachineName));
الف) تعریف یک ErrorFeature سفارشی
public class MyErrorFeature { public ErrorType Error { get; set; } } public enum ErrorType { ArgumentException }
ب) تنظیم مقدار ErrorFeature سفارشی در اکشن متدها
[HttpGet("{value}")] public IActionResult MyErrorTest(int value) { if (value <= 0) { var errorType = new MyErrorFeature { Error = ErrorType.ArgumentException }; HttpContext.Features.Set(errorType); return BadRequest(); } return Ok(value); }
ج) واکنش نشان دادن به دریافت ErrorFeature سفارشی
services.AddProblemDetails(options => options.CustomizeProblemDetails = ctx => { var MyErrorFeature = ctx.HttpContext.Features.Get<MyErrorFeature>(); if (MyErrorFeature is not null) { (string Title, string Detail, string Type) details = MyErrorFeature.Error switch { ErrorType.ArgumentException => ( nameof(ArgumentException), "This is an argument-exception.", "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1" ), _ => ( nameof(Exception), "default-exception", "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1" ) }; ctx.ProblemDetails.Title = details.Title; ctx.ProblemDetails.Detail = details.Detail; ctx.ProblemDetails.Type = details.Type; } } );
امکان تبدیل سادهتر اطلاعات استثناءهای سفارشی به یک ProblemDetails سفارشی در دات نت 7
بجای استفاده از تنظیمات services.AddProblemDetails جهت بازنویسی مقدار شیء ProblemDetails بازگشتی، میتوان جزئیات میانافزار app.UseExceptionHandler را نیز سفارشی سازی کرد و به بروز استثناءهای خاصی واکنش نشان داد. برای مثال فرض کنید یک استثنای سفارشی را به صورت زیر طراحی کردهاید:
public class MyCustomException : Exception { public MyCustomException( string message, HttpStatusCode statusCode = HttpStatusCode.BadRequest ) : base(message) { StatusCode = statusCode; } public HttpStatusCode StatusCode { get; } }
[HttpGet("{value}")] public IActionResult MyErrorTest(int value) { if (value <= 0) { throw new MyCustomException("The value should be positive!"); } return Ok(value); }
app.UseExceptionHandler(exceptionHandlerApp => { exceptionHandlerApp.Run(async context => { context.Response.ContentType = "application/problem+json"; if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService) { var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>(); var exceptionType = exceptionHandlerFeature?.Error; if (exceptionType is not null) { (string Title, string Detail, string Type, int StatusCode) details = exceptionType switch { MyCustomException MyCustomException => ( exceptionType.GetType().Name, exceptionType.Message, "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1", context.Response.StatusCode = (int)MyCustomException.StatusCode ), _ => ( exceptionType.GetType().Name, exceptionType.Message, "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1", context.Response.StatusCode = StatusCodes.Status500InternalServerError ) }; await problemDetailsService.WriteAsync(new ProblemDetailsContext { HttpContext = context, ProblemDetails = { Title = details.Title, Detail = details.Detail, Type = details.Type, Status = details.StatusCode } }); } } }); });
Many applications have a need to keep audit information on changes made to objects in the database. Traditionally, this would be done either through log events, stored procedures that implement the logging, or the use of archive/tombstone tables to store the old values before the modification (hopefully enforced through stored procedures). With all of these, there is always a chance that a developer could forget to do those things in a specific section of code, and that changes could be made through the application without logging the change correctly. With Entity Framework 4.1’s DbContext API, it is fairly easy to implement more robust audit logging in your application
اشتراکها