{ "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 } }); } } }); });
Scaffold-DbContext "connectionstring" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Entities -DataAnnotations
UseNetTopologySuite requires AddEntityFrameworkSqlServerNetTopologySuite to be called on the internal service provider used.
<?xml version="1.0" encoding="utf-8"?> <configuration> <!-- To customize the asp.net core module uncomment and edit the following section. For more info see https://go.microsoft.com/fwlink/?linkid=838655 --> <system.webServer> <handlers> <remove name="aspNetCore"/> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModulev2" resourceType="Unspecified"/> </handlers> <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout"> <handlerSettings> <handlerSetting name="experimentalEnableShadowCopy" value="true" /> <handlerSetting name="shadowCopyDirectory" value="../ShadowCopyDirectory/" /> <!-- Only enable handler logging if you encounter issues--> <!--<handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />--> <!--<handlerSetting name="debugLevel" value="FILE,TRACE" />--> </handlerSettings> </aspNetCore> </system.webServer> </configuration>
در قسمت بعد، قالب را هم از نوع empty انتخاب مینماییم.
در ادامه فایل project.json را باز کرده و در قسمت dependencies، تغییرات زیر را اعمال نمایید.
قبل از اینکه شما را از این همه وابستگی نگران کنم، باید عرض کنم فقط Microsoft.Owin , Microsoft.AspNetCore.Owin، پکیجهای اجباری هستند؛ باقی آنها برای نشان دادن انعطاف پذیری بالای این روش میباشند:
"dependencies": { "Microsoft.AspNet.OData": "5.9.1", "Microsoft.AspNet.SignalR": "2.2.1", "Microsoft.AspNet.WebApi.Client": "5.2.3", "Microsoft.AspNet.WebApi.Core": "5.2.3", "Microsoft.AspNet.WebApi.Owin": "5.2.3", "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Hosting": "1.0.0", "Microsoft.AspNetCore.Mvc": "1.0.0", "Microsoft.AspNetCore.Owin": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Net.Http": "2.2.29", "Microsoft.Owin": "3.0.1", "Microsoft.Owin.Diagnostics": "3.0.1", "Microsoft.Owin.FileSystems": "3.0.1", "Microsoft.Owin.StaticFiles": "3.0.1", "Newtonsoft.Json": "9.0.1" }, //etc...
بعد از ذخیره کردن این فایل، در پنجرهی Output خود شاهد دانلود شدن این پکیجها خواهید بود. در اینجا پکیجهای مربوط به Owin, Odata, SignalR را مشاهد میکنید. ضمن اینکه در کنار آن، AspNetCore.Mvc را نیز مشاهده میفرمایید. دلیل این کار این است که این دو نوع متفاوت قرار است در کنار هم کار کنند و هیچ مشکلی با دیگری ندارند.
در مسیر اصلی پروژهی خود کلاسی به نام OwinExtensions را با محتوای زیر بسازید:
namespace OwinCore { public static class OwinExtensions { public static IApplicationBuilder UseOwinApp( this IApplicationBuilder aspNetCoreApp, Action<IAppBuilder> configuration) { return aspNetCoreApp.UseOwin(setup => setup(next => { AppBuilder owinAppBuilder = new AppBuilder(); IApplicationLifetime aspNetCoreLifetime = (IApplicationLifetime)aspNetCoreApp.ApplicationServices.GetService(typeof(IApplicationLifetime)); AppProperties owinAppProperties = new AppProperties(owinAppBuilder.Properties); owinAppProperties.OnAppDisposing = aspNetCoreLifetime?.ApplicationStopping ?? CancellationToken.None; owinAppProperties.DefaultApp = next; configuration(owinAppBuilder); return owinAppBuilder.Build<Func<IDictionary<string, object>, Task>>(); })); } } }
یک Extension Method به نام UseOwinApp اضافه شده به IApplicationBuilder که مربوط به ASP.NET Core میباشد و درون آن نیز AppBuilder را که مربوط به Owin pipeline میباشد، نمونه سازی کردهایم که باعث میشود Owin pipeline بر روی ASP.NET Core pipeline سوار شود.
حال میخواهیم یک Middleware سفارشی را با استفاده از Owin نوشته و در Startup پروژه، آن را فراخوانی نماییم. کلاسی به نام AddSampleHeaderToResponseHeadersOwinMiddleware را با محتوای زیر تولید مینماییم:
namespace OwinCore { public class AddSampleHeaderToResponseHeadersOwinMiddleware : OwinMiddleware { public AddSampleHeaderToResponseHeadersOwinMiddleware(OwinMiddleware next) : base(next) { } public async override Task Invoke(IOwinContext context) { //throw new InvalidOperationException("ErrorTest"); context.Response.Headers.Add("Test", new[] { context.Request.Uri.ToString() }); await Next.Invoke(context); } } }
کلاسی است که از owinMiddleware ارث بری کرده و در متد override شدهی Invoke نیز با استفاده از IOwinContext، به پیاده سازی Middleware خود میپردازیم. Exception مربوطه را comment کرده (بعدا در مرحلهی تست از آن نیز استفاده مینماییم) و در خط بعدی در هدر response هر request، یک شیء را به نام Test و با مقدار Uri آن request، میسازیم.
خط بعدی هم اعلام میدارد که به Middleware بعدی برود.
در ادامه فایل Startup.cs را باز کرده و اینگونه متد Configure را تغییر دهید:
public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env) { aspNetCoreApp.UseOwinApp(owinApp => { if (env.IsDevelopment()) { owinApp.UseErrorPage(new ErrorPageOptions() { ShowCookies = true, ShowEnvironment = true, ShowExceptionDetails = true, ShowHeaders = true, ShowQuery = true, ShowSourceCode = true }); } owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>(); }); }
مشاهده میفرمایید با استفاده از UserOwinApp میتوانیم Middlewareهای Owinی خود را register نماییم و نکتهی قابل توجه این است که در کنار آن نیز میتوانیم از IHostingEnviroment مربوط به ASP.NET core استفاده نماییم. owinApp.UseErrorPage از Microsoft.Owin.Diagnostics گرفته شده است و در خط بعدی نیز Middleware شخصی خود را register کردهایم. پروژه را run کرده و در response این را مشاهد مینمایید.
اکنون اگر در Middleware سفارشی خود، آن Exception را از حالت comment در بیاوریم، در صورتیکه در حالت development باشیم، با این صفحه مواجه خواهیم شد:
Exception مربوطه را به حالت comment گذاشته و ادامه میدهیم.
برای اینکه نشان دهیم Owin و ASP.NET Core pipeline در کنار هم میتوانند کار کنند، یک Middleware را از نوع ASP.NET Core نوشته و آن را register مینماییم. کلاسی جدیدی را به نام AddSampleHeaderToResponseHeadersAspNetCoreMiddlware با محتوای زیر میسازیم:
namespace OwinCore { public class AddSampleHeaderToResponseHeadersAspNetCoreMiddlware { private readonly RequestDelegate Next; public AddSampleHeaderToResponseHeadersAspNetCoreMiddlware(RequestDelegate next) { Next = next; } public async Task Invoke(HttpContext context) { //throw new InvalidOperationException("ErrorTest"); context.Response.Headers.Add("Test2", new[] { "some text" }); await Next.Invoke(context); } } }
متد Configure در Startup.cs را نیز اینگونه تغییر میدهیم
public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env) { aspNetCoreApp.UseOwinApp(owinApp => { if (env.IsDevelopment()) { owinApp.UseErrorPage(new ErrorPageOptions() { ShowCookies = true, ShowEnvironment = true, ShowExceptionDetails = true, ShowHeaders = true, ShowQuery = true, ShowSourceCode = true }); } owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>(); }); aspNetCoreApp.UseMiddleware<AddSampleHeaderToResponseHeadersAspNetCoreMiddlware>(); }
اکنون AddSampleHeaderToResponseHeadersAspNetCoreMiddlware رجیستر شده است و بعد از run کردن پروژه و بررسی header response باید این را ببینیم
میبینید که به ترتیب اجرای Middlewareها، ابتدا Test مربوط به Owin و بعد آن Test2 مربوط به ASP.NET Core تولید شده است.
حال اجازه دهید Odata را با استفاده از Owin پیاده سازی نماییم. ابتدا کلاسی را به نام Product با محتوای زیر تولید نمایید:
namespace OwinCore { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } }
حال کلاسی را به نام ProductsController با محتوای زیر میسازیم:
namespace OwinCore { public class ProductsController : ODataController { [EnableQuery] public IQueryable<Product> Get() { return new List<Product> { new Product { Id = 1, Name = "Test" , Price = 10 } } .AsQueryable(); } } }
اگر مقالهی پیاده سازی Crud با استفاده از OData را مطالعه کرده باشید، قاعدتا با این کدها آشنا خواهید بود. ضمن اینکه پرواضح است که OData هیچ وابستگی به entity framework ندارد.
برای config آن نیز در Startup.cs پروژه و متد Configure، تغییرات زیر را اعمال مینماییم.
public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env) { //aspNetCoreApp.UseMvc(); aspNetCoreApp.UseOwinApp(owinApp => { if (env.IsDevelopment()) { owinApp.UseErrorPage(new ErrorPageOptions() { ShowCookies = true, ShowEnvironment = true, ShowExceptionDetails = true, ShowHeaders = true, ShowQuery = true, ShowSourceCode = true }); } // owinApp.UseFileServer(); as like as asp.net core static files middleware // owinApp.UseStaticFiles(); as like as asp.net core static files middleware // owinApp.UseWebApi(); asp.net web api / odata / web hooks HttpConfiguration webApiConfig = new HttpConfiguration(); ODataModelBuilder odataMetadataBuilder = new ODataConventionModelBuilder(); odataMetadataBuilder.EntitySet<Product>("Products"); webApiConfig.MapODataServiceRoute( routeName: "ODataRoute", routePrefix: "odata", model: odataMetadataBuilder.GetEdmModel()); owinApp.UseWebApi(webApiConfig); owinApp.MapSignalR(); //owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>(); }); //aspNetCoreApp.UseMiddleware<AddSampleHeaderToResponseHeadersAspNetCoreMiddlware>(); }
برای config مخصوص Odata، به HttpConfiguration نیاز داریم. بنابراین instanceی از آن گرفته و برای مسیریابی Odata از آن استفاده مینماییم.
با استفاده از پیاده سازی که از استاندارد Owin انجام دادیم، مشاهده کردید که Odata را همانند یک پروژهی معمولی asp.netی، config نمودیم. در خط بعدی هم SignalR را مشاهده مینمایید.
اکنون اگر آدرس زیر را در مرورگر خود وارد نمایید، پاسخ زیر را از Odata دریافت خواهید کرد:
http://localhost:YourPort/odata/Products
بعد از فرستادن request فوق، باید response زیر را دریافت نمایید:
{ "@odata.context":"http://localhost:4675/odata/$metadata#Products","value":[ { "Id":1,"Name":"Test","Price":10 } ] }
تعداد زیادی Owin Middleware موجود همانند Thinktecture IdentityServer, NWebSec, Nancy, Facebook OAuth , ... هم با همان آموزش راه اندازی بر روی Owin که دارند میتوانند در ASP.NET Core نیز استفاده شوند و زمانی که نسخهی ASP.NET Core اینها به آمادگی کامل رسید، با کمترین تغییری میتوان از آنها استفاده نمود.
Visual Studio 2022 17.4 منتشر شد
فیلدهای پویا در NHibernate
Microsoft's release notes highlights for Preview 3 include:
- Visual Studio now offers .NET Framework 4.7.2 development tools to supported platforms with 4.7.2 runtime included.
- We improved performance during project unload/reload and branch switching.
- With added support for Azure Functions, you now have a new target host in the Configure Continuous Delivery to Azure dialog.
- Git and TFS status now updates properly for external file changes in .NET Core projects.
- We added new productivity features, such as code cleanup, invert-if refactoring, Go to Enclosing Block, Multi-Caret support, and new keyboard profiles.
- C++ enhancements include Template IntelliSense, convert macro to constexpr lightbulbs, and experimental in-editor code analysis squiggles.
- You can now use cross-language debugging with Python 3.7.0rc1.
- Performance Profiling now offers the ability to pause/resume data collection and adds a new .NET Object Allocation Tracking tool.
- We included improvements for Android incremental builds in the Xamarinsupport for Xcode 9.4.