Async در ASP.NET
درک نحوه اجرای برنامههای غیرهمزمان
کتابخانه AjaxPageParser
دوره ساخت Minimal APIs در NET 7.
Learn Minimal APIs in .NET 7
Learn how to build Minimal APIs in .NET 7 with hands-on course. By the end of the course, you will be able to build well-constructed Minimal API Endpoints using C#, .NET7, and Swagger.
⭐️ Contents ⭐️
⌨️ (0:00:00) Introduction
⌨️ (0:01:30) Topics Covered
⌨️ (0:02:47) Why Minimal API?
⌨️ (0:06:07) Create Project
⌨️ (0:07:57) Comparing Files Minimal vs Standard
⌨️ (0:11:05) Program file changes
⌨️ (0:13:50) Clean Program class file
⌨️ (0:16:02) API Basics
⌨️ (0:16:44) What is API?
⌨️ (0:21:11) Request and response
⌨️ (0:25:59) Request Object
⌨️ (0:30:12) Response Object
⌨️ (0:35:36) httpverb
⌨️ (0:40:38) Create First Endpoint
⌨️ (0:43:43) Return Types
⌨️ (0:46:15) Route Parameters
⌨️ (0:48:29) Create Coupon Model and Coupon Store
⌨️ (0:51:38) Get All Endpoint
⌨️ (0:53:09) Get Individual Coupon
⌨️ (0:55:19) Create Coupon
⌨️ (0:59:53) Name Endpoints
⌨️ (1:03:17) Products and Accepts in Minimal API
⌨️ (1:06:58) Dependency Injection in Minimal API
⌨️ (1:10:25) Add DTOs
⌨️ (1:13:56) AutoMapper and Dependency Injection
⌨️ (1:18:32) Fluent Validators
⌨️ (1:24:07) Async Endpoints
⌨️ (1:26:11) API Response
⌨️ (1:32:57) Assignment - Put and Delete
⌨️ (1:33:49) Assignment Solution - Put and Delete Endpoints
کد نویسی تعاملی #C و #F با REPLs
معادل 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 ای است که تاکنون بحث شد، کدهای آن، به همراه کدهای پروژهی اصلی این پروژه که از قسمت اول قابل دریافت است، ارائه شدهاست.
export function error(){ alert('oops, an error'); }
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./Panel.razor.js"); await module.InvokeVoidAsync("error");
Pages/Panel.razor Pages/Panel.razor.js Pages/Panel.razor.css
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/RazorClassLibrary/componentName.razor.js");
export function showPrompt(message) { return prompt(message, 'Type anything here'); }
@page "/call-js-example-6" @implements IAsyncDisposable @inject IJSRuntime JS <h1>Call JS Example 6</h1> <p> <button @onclick="TriggerPrompt">Trigger browser window prompt</button> </p> <p> @result </p> @code { private IJSObjectReference? module; private string? result; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { module = await JS.InvokeAsync<IJSObjectReference>("import", "./scripts.js"); } } private async Task TriggerPrompt() { result = await Prompt("Provide some text"); } public async ValueTask<string?> Prompt(string message) => module is not null ? await module.InvokeAsync<string>("showPrompt", message) : null; async ValueTask IAsyncDisposable.DisposeAsync() { if (module is not null) { await module.DisposeAsync(); } } }