{ "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 } }); } } }); });
Impersonation Process
Impersonation is when an admin user is logged in with the same privileges as a user, but without knowing their password or other credentials. I’ve used this in couple applications and it was invaluable for support cases and debugging user permissions.
[Authorize(Roles = "Admin")] // <-- Make sure only admins can access this public async Task<IActionResult> ImpersonateUser(String userId) { var currentUserId = User.GetUserId(); var impersonatedUser = await _userManager.FindByIdAsync(userId); var userPrincipal = await _signInManager.CreateUserPrincipalAsync(impersonatedUser); userPrincipal.Identities.First().AddClaim(new Claim("OriginalUserId", currentUserId)); userPrincipal.Identities.First().AddClaim(new Claim("IsImpersonating", "true")); // sign out the current user await _signInManager.SignOutAsync(); await HttpContext.Authentication.SignInAsync(cookieOptions.ApplicationCookieAuthenticationScheme, userPrincipal); return RedirectToAction("Index", "Home"); }
تزریق وابستگی چطور کار میکند؟
بهبود SEO در ASP.NET MVC
<system.webServer> <rewrite> <rules> <rule name="Enforce WWW" stopProcessing="true"> <match url=".*" /> <conditions> <add input="{CACHE_URL}" pattern="^(.+)://(?!www)(.*)" /> </conditions> <action type="Redirect" url="{C:1}://www.{C:2}" redirectType="Permanent" /> </rule> </rules> </rewrite> </system.webServer>
<system.webServer> <rewrite> <rules> <rule name="SEO - Lower case" stopProcessing="false"> <match url="(.*)" ignoreCase="false" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_METHOD}" pattern="GET" /> <add input="{R:1}" pattern="[A-Z]" ignoreCase="false" /> </conditions> <action type="Rewrite" url="_{ToLower:{R:1}}" /> </rule> </rules> </rewrite> </system.webServer>
خلاصه آن:
<!-- SEO rules (from: http://www.seomoz.org/blog/what-every-seo-should-know-about-iis#chaining) --> <!-- SEO | Section 1 | Whitelist --> <rule name="Whitelist - Resources" stopProcessing="true"> <match url="^(?:css/|scripts/|images/|install/|config/|umbraco/|umbraco_client/|base/|webresource\.axd|scriptresource\.axd|__browserLink|[^/]*/arterySignalR/.*)" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false" /> <action type="None" /> </rule> <!-- SEO | Section 2 | Rewrites (chaining) --> <rule name="SEO - Remove default.aspx" stopProcessing="false"> <match url="(.*?)/?default\.aspx$" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_METHOD}" pattern="GET" /> </conditions> <action type="Rewrite" url="_{R:1}" /> </rule> <rule name="SEO - Remove trailing slash" stopProcessing="false"> <match url="(.+)/$" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_METHOD}" pattern="GET" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> </conditions> <action type="Rewrite" url="_{R:1}" /> </rule> <rule name="SEO - Lower case" stopProcessing="false"> <match url="(.*)" ignoreCase="false" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_METHOD}" pattern="GET" /> <add input="{R:1}" pattern="[A-Z]" ignoreCase="false" /> </conditions> <action type="Rewrite" url="_{ToLower:{R:1}}" /> </rule> <!-- SEO | Section 3 | Redirecting --> <rule name="SEO - HTTP canonical redirect" stopProcessing="true"> <match url="^(_*)(.*)" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="true"> <add input="{HTTP_HOST}" pattern="^www\.(.*)" /> <add input="{HTTP_METHOD}" pattern="GET" /> <add input="{SERVER_PORT}" pattern="80" /> </conditions> <action type="Redirect" url="http://{C:1}/{R:2}" /> </rule> <rule name="SEO - HTTPS canonical redirect" stopProcessing="true"> <match url="^(_*)(.*)" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="true"> <add input="{HTTP_HOST}" pattern="^www\.(.*)" /> <add input="{HTTP_METHOD}" pattern="GET" /> <add input="{SERVER_PORT}" pattern="443" /> </conditions> <action type="Redirect" url="http://{C:1}/{R:2}" /> </rule> <rule name="SEO - Non-canonical redirect" stopProcessing="true"> <match url="^(_+)(.*)" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false"> <add input="{HTTP_METHOD}" pattern="GET" /> </conditions> <action type="Redirect" url="{R:2}" /> </rule> <!-- // SEO rules -->
The TimeSpan after which the authentication ticket stored inside the cookie expires. ExpireTimeSpan is added to the current time to create the expiration time for the ticket. The ExpiredTimeSpan value always goes into the encrypted AuthTicket verified by the server. It may also go into the Set-Cookie header, but only if IsPersistent is set. To set IsPersistent to true, configure the AuthenticationProperties passed to SignInAsync. The default value of ExpireTimeSpan is 14 days.
Select { [Measures].[Internet Sales Amount], [Measures].[Internet Tax Amount] } on columns, head( [Customer].[Customer Geography].[Country], 2 )on rows From [Adventure Works]
تابع Head، تعداد مشخص شده بر اساس پارامتر اول از آن محور را بر اساس نحوهی نمایش تنظیم شده در SSAS، واکشی میکند.
حال تصور کنید بخواهیم شرط زیر را بر روی کوئری بالا اجرا کنیم
( [Measures].[Internet Sales Amount] >= '2500000' )
به عبارت دیگر ما میخواهیم دو کشوری را انتخاب کنیم که میزان فروش اینترنتی آنها بالای 2500000 باشد.
کوئری مشابه زیر میباشد
Select { [Measures].[Internet Sales Amount], [Measures].[Internet Tax Amount] } on columns, head( [Customer].[Customer Geography].[Country], 2 )on rows From [Adventure Works] Where ( [Measures].[Internet Sales Amount] >= '2500000' )
البته خطای زیر را خواهیم داشت.
به یاد داشته باشیم در صورتیکه بخواهیم ایجاد محدودیت در نمایش دادهها را در یک محور داشته باشیم، باید از تابع Filter استفاده کنیم؛ به صورت زیر:
Select Filter( { [Measures].[Internet Sales Amount], [Measures].[Internet Tax Amount] } , [Measures].[Internet Sales Amount] >= 2644017.71 ) on columns, head( [Customer].[Customer Geography].[Country], 3 )on rows From [Adventure Works]
تابع Filter دو پارامتر می گیرد. پارامتر اول نام ردیف یا ستونی می باشد که روی آن می خواهیم عمل فیلتر را انجام دهیم. پارامتر دوم شرط فیلترینگ می باشد که می بایست مانند T/SQL دارای یک خروجی Boolean باشد
همچنان نتیجه درست نمیباشد ! چرا؟
اگر بخواهیم شرط روی Axis ردیف (کشور ها) اعمال گردد، باید عملیات فیلترینگ در این Axis انجام شود . بنابر این خروجی بدست آمده صحیح نمی باشد زیرا ما عملیات فیلترینگ را روی ستون ها انجام داده ایم.
کوئری زیر را اجرا نمایید
Select { [Measures].[Internet Sales Amount] ,[Measures].[Internet Tax Amount] } on columns, head( Filter( [Customer].[Customer Geography].[Country] , [Measures].[Internet Sales Amount] >= 2644017.71 ), 3) on rows From [Adventure Works]
البته توجه کنید که این کوئری، سه کشور اول که در شرط زیر قرار دارند را بر می گرداند و الزاما این سه کشور از تمام کشور های دیگر بیشتر نمی باشند.
در این حالت سه کشور که بالاتر از مقدار ذکر شده، فروش اینترنتی دارند، در خروجی قرار می گیرند . البته این سه کشور دارای بالاترین فروش نمی باشند بلکه به ترتیب اسم، از بالا گزینش انجام شده است و بعد از پیدا کردن سه کشور که در شرط قرار بگیرند، جستجو تمام شده است .
اگر بخواهیم سه کشوری را که بالاترین میزان فروش را دارند پیدا کنیم و شرط هم همواره اعمال گردد، کوئری زیر درست می باشد:
Select { [Measures].[Internet Sales Amount] ,[Measures].[Internet Tax Amount] } on columns, TopCount( Filter( [Customer].[Customer Geography].[Country] , [Measures].[Internet Sales Amount] >= 2644017.71 ), 3, [Measures].[Internet Sales Amount]) on rows From [Adventure Works]
در این حالت به جای تابع Head از تابع TopCount استفاده گردیده است .این تابع سه کشوری را که بیشترین فروش اینترنتی را داشته اند و این فروش بالاتر از مقدار ذکر شده در شرط می باشد را بر می گرداند .البته در اینجا تابع topcount دارای سه پارامتر می باشد و در پارامتر سوم اعلام میکند که تعداد بالای مجموعه براساس چه شاخصی باید به دست بیاید.
حال اگر بخواهیم سه ردیف انتهایی جدول را واکشی کنیم داریم:
Select { [Measures].[Internet Sales Amount], [Measures].[Internet Tax Amount] }on columns, tail([Customer].[Customer Geography].[Country], 3)on rows From [Adventure Works]
این تابع برعکس تابع Head کار میکند و N ردیف آ اخر مجموعه را بدست می آورد . البته در بالا فقط 3 ردیف انتهایی را
در خروجی آورده ایم و هیچ شرطی اعمال نگردیده است.
بازیابی منابع از اسمبلی
بهتر است فعلا از توزیعهای روزانهی EF Core 8 استفاده کنید یا تا انتشار نگارش 8.0.2 آن صبر کنید
If you are updating to EF Core 8, consider using the daily builds, or wait for the 31 fixes coming in 8.0.2 in Februrary 2024