اشتراک‌ها
ساخت اولین پروژه WebAPI با MVC 6
Previous versions of ASP.NET included the Web API framework for creating web APIs. In ASP.NET 5, this functionality has been merged into the MVC 6 framework. Unifying the two frameworks makes it simpler to build apps that include both UI (HTML) and APIs, because now they share the same code base and pipeline. 
ساخت اولین پروژه WebAPI با MVC 6
اشتراک‌ها
امکانات جدید صفحات Razor

Razor Pages is a new feature of ASP.NET Core MVC that makes coding page-focused scenarios easier and more productive.

Razor Pages requires ASP.NET Core 2.0.0 or later. Tooling support for Razor Pages ships in Visual Studio 2017 Update 3 or later. 

امکانات جدید صفحات Razor
نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی
- Identity server یک محصول کاملا مجزای از ASP.NET Core Identity است و توسط تیم دیگری خارج از مایکروسافت توسعه داده می‌شود و این دو ارتباطی به هم ندارند.
- هرچند امکان استفاده‌ی از ASP.NET Core Identity برای مدیریت کاربران Identity server هم وجود دارد.
- کاری که در این سری انجام شده، ابتدا در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایل‌های Quick Start UI را به پروژه‌ی IDP اضافه کردیم (یعنی دقیقا از کدهای اصلی تیم Identity server استفاده شده). بعد در این قسمت، سایر کدهای اصلی بسته‌ی EF Core آن‌را (^ و ^) به پروژه اضافه و سفارشی سازی کرده و در قسمت‌های دیگر، این کدها را تکمیل کرده‌ایم. بنابراین در این سری از کدهای استاندارد خود  Identity server استفاده شده و سپس توسعه‌ی بیشتری پیدا کرده‌اند. برای نمونه کلاس‌های موجودیت‌های مثال این سری، از اینجا تامین شده‌اند.
مطالب
Minimal API's در دات نت 6 - قسمت پنجم - پیاده سازی الگوی CQRS
تا قسمت قبل موفق شدیم فایل Program.cs برنامه‌ی Minimal API's را خلوت کنیم و همچنین زیرساختی را برای توسعه‌ی مبتنی بر ویژگی‌ها، ارائه دهیم. اما ... هنوز endpoints ما چنین شکلی را دارند:
        endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) =>
        {
            var authors = await ctx.Authors.ToListAsync();
            return authors;
        });

        endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) =>
        {
            var author = new Author();
            author.FirstName = authorDto.FirstName;
            author.LastName = authorDto.LastName;
            author.Bio = authorDto.Bio;
            author.DateOfBirth = authorDto.DateOfBirth;

            ctx.Authors.Add(author);
            await ctx.SaveChangesAsync();

            return author;
        });
 و یک چنین رویه‌ای جهت کار مستقیم با DbContext در اکشن متدهای MVC هیچگاه توصیه نمی‌شود. برای مثال به طور معمول، عملیاتی که در بدنه‌ی Lambda expressions فوق انجام شده، عموما به Repositories و Services محول شده و در نهایت از سرویس‌ها، در اکشن متدها استفاده می‌شود. در معماری جاری که در پیش گرفته‌ایم، دو لایه‌ی Repositories و Services حذف شده‌اند و دیگر خبری از آن‌ها نیست. در اینجا کار سرویس‌ها و مخازن، به هندلرهای معماری CQRS واگذار خواهند شد. هر هندلر نیز متکی به خود است و مستقل از سایر هندلرها طراحی می‌شود و این‌ها صرفا بر اساس نیازهای ویژگی جاری توسعه خواهند یافت و دقیقا در همان پوشه‌ی ویژگی مورد بررسی نیز قرار می‌گیرند؛ و نه پراکنده در لایه‌ای و یا پروژه‌ای دیگر. به این ترتیب درک یک ویژگی متکی به خود برنامه، ساده‌تر شده و در طول زمان، نگهداری و توسعه‌ی آن نیز ساده‌تر خواهد شد. مشکل داشتن سرویس‌هایی بزرگ که در معماری‌های متداول وجود دارند، استفاده‌ی از متدهای آن‌ها در چندین اکشن متد و چندین کنترلر مختلف است و اگر یکی از متدهای این سرویس بزرگ ما تغییر کند، بر روی چندین کنترلر تاثیر می‌گذارد که ممکن است سبب از کار افتادگی بعضی از آن‌ها شود؛ اما در اینجا هرکاری که انجام می‌شود و هر هندلری که توسعه می‌یابد، فقط مختص به یک کار و یک ویژگی مشخص است.


ایجاد Command و هندلر مخصوص ایجاد یک نویسنده‌ی جدید


در الگوی CQRS، یک دستور، کاری را بر روی بانک اطلاعاتی انجام می‌دهد. برای مثال در اینجا قرار است نویسنده‌ای را ثبت کند. در ادامه می‌خواهیم بدنه‌ی endpoints.MapPost فوق را با الگوی CQRS انطباق دهیم. به همین جهت به یک Command نیاز داریم:
using MediatR;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Api.Features.Authors;

public class CreateAuthorCommand : IRequest<Author>
{
    public AuthorDto AuthorDto { get; set; } = default!;
}
اینترفیس IRequest کتابخانه‌ی MediatR که در انتهای قسمت قبل به پروژه اضافه شد، چنین امضایی را دارد:
public interface IRequest<out TResponse> : IBaseRequest
یعنی <IRequest<Author به این معنا است که قرار است «خروجی» این عملیات، یک Author باشد و CreateAuthorCommand می‌تواند شامل تمام خواصی باشد که در جهت برآورده کردن این دستور مورد نیاز هستند؛ برای مثال کل اطلاعات شیء AuthorDto در اینجا.

سپس نیاز به یک هندلر است تا دستور رسیده را پردازش کند:
namespace MinimalBlog.Api.Features.Authors;

public class CreateAuthorCommandHandler : IRequestHandler<CreateAuthorCommand, Author>
{
    private readonly MinimalBlogDbContext _context;
    private readonly IMapper _mapper;

    public CreateAuthorCommandHandler(MinimalBlogDbContext context, IMapper mapper)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    public async Task<Author> Handle(CreateAuthorCommand request, CancellationToken cancellationToken)
    {
        if (request == null)
        {
            throw new ArgumentNullException(nameof(request));
        }

        var toAdd = _mapper.Map<Author>(request.AuthorDto);
        _context.Authors.Add(toAdd);
        await _context.SaveChangesAsync(cancellationToken);
        return toAdd;
    }
}
اینترفیس IRequestHandler چنین امضایی را دارد:
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
که اولین آرگومان جنریک آن، همان Command ای است که قرار است پردازش کند و خروجی آن، اطلاعاتی است که قرار است بازگشت دهد. یعنی متد Handle فوق، قرار است عملیات endpoints.MapPost را پیاده سازی کند و در اینجا با استفاده از AutoMapper، انتساب‌های آن حذف و ساده شده‌اند و مابقی آن، با بدنه‌ی lambda expression مربوط به endpoints.MapPost، یکی است. این هندلر، معادل یک یا چند متد از متدهای یک سرویس بزرگ است که در اینجا به صورت اختصاصی جهت پردازش فرمانی در کنار هم قرار می‌گیرند و متکی به خود هستند.

پس از این تغییرات، بدنه‌ی lambda expression مربوط به endpoints.MapPost به صورت زیر تغییر کرده و ساده می‌شود:
endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto) =>
{
     var command = new CreateAuthorCommand { AuthorDto = authorDto };
     var author = await mediator.Send(command);
     return author;
});
در اینجا تزریق وابستگی IMediator را مشاهده می‌کنید. با فراخوانی متد Send آن، شیء‌ای به هندلر متناظری ارسال شده، پردازش می‌شود و در نهایت شیءای را بازگشت خواهد داد. برای مثال در اینجا شیء Dto یک نویسنده به هندلر CreateAuthorCommandHandler ارسال و تبدیل به شیءای از نوع Author مربوط به دومین برنامه شده، سپس در بانک اطلاعاتی ذخیره می‌شود و در نهایت این نویسنده که اکنون به همراه یک Id نیز هست، بازگشت داده می‌شود. بنابراین هر هندلر یک object in و یک object out دارد که به عنوان آرگومان‌های جنریک IRequestHandler تعریف می‌شوند.



نکته 1: await داخل بدنه‌ی lambda expression مربوط به endpoints را فراموش نکنید. تمام متدهای IMediator از نوع aysnc هستند؛ هرچند روش نامگذاری SendAsync را رعایت نکرده‌اند و اگر این await فراموش شود، مشاهده خواهید کرد که برنامه در حین فراخوانی endpoints در مرورگر، در حالت هنگ و صبر کردن نامحدود قرار می‌گیرد، بدون اینکه کاری را انجام دهد و یا حتی استثنایی را صادر کند.


نکته 2: در پیاده سازی هندلر، استفاده از cancellationToken را نیز مشاهده می‌کنید. تقریبا تمام متدهای async مربوط به EF-Core به همراه پارامتری جهت دریافت cancellationToken هم هستند. اگر کاربری قصد لغو یک درخواست طولانی را داشته باشد و بر روی دکمه‌ی stop مرورگر کلیک کند و یا حتی صفحه را چندین بار ریفرش کند، این به معنای abort درخواست(های) رسیده‌است. وجود این cancellationTokenها، بار سرور را کاهش داده و عملیات در حال اجرای سمت سرور را در یک چنین حالت‌هایی متوقف می‌کند.
البته هندلری که در اینجا تعریف شده، این cancellationToken را باید از mediator دریافت کند که در کدهای endpoint فوق، چنین نیست. برای رفع این مشکل باید به صورت زیر عمل کرد:
endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) =>
        {
            var request = new GetAllAuthorsQuery();
            var authors = await mediator.Send(request, ct);
            return authors;
        });
این مورد را می‌توان به صورت یک best practice، به تمام endpoints اضافه کرد.


نکته 3: هندلرها عموما چیزی را بازگشت نمی‌دهند؛ صرف نظر از هندلر فوق که نیاز بوده تا Id شیء ذخیره شده را بازگشت دهد، عموما به همراه هیچ خروجی نیستند. به همین جهت در حین تعریف آن‌ها فقط کافی است در آرگومان‌های جنریک آن‌ها، نوع خروجی را ذکر نکنیم:
public class Handler : IRequestHandler<Command>
در یک چنین حالتی، امضای IRequestHandler به صورت خودکار به همراه خروجی از نوع Unit خواهد بود:
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, Unit> where TRequest : IRequest<Unit>
که این Unit معادل Void در کتابخانه‌ی mediator است و به نحو زیر در هندلرها مدیریت می‌شود:
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken)
{
   // ...
   return Unit.Value;
}
در یک چنین حالتی، تعریف یک Command نیز بر اساس اینترفیس IRequest انجام می‌شود:
public class Command : IRequest


ایجاد Query و هندلر مخصوص بازگشت لیست نویسنده‌‌ها

در الگوی CQRS، یک کوئری قرار است اطلاعاتی را بازگشت دهد و ... وضعیت بانک اطلاعاتی را تغییر نمی‌دهد. بنابراین در اینجا یک IRequest که قرار است لیستی از نویسندگان را بازگشت دهد، تعریف می‌کنیم. بدنه‌ی آن هم می‌تواند خالی باشد و یا به همراه خواصی مانند اطلاعات صفحه بندی و یا مرتب سازی گزارشگیری رسیده‌ی از درخواست:
using MediatR;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Api.Features.Authors;

public class GetAllAuthorsQuery : IRequest<List<Author>>
{
}
سپس نیاز به یک هندلر است تا درخواست رسیده را پردازش کند. این هندلر، کوئری فوق را دریافت کرده و لیست کاربران را بازگشت می‌دهد:
namespace MinimalBlog.Api.Features.Authors;

public class GetAllAuthorsHandler : IRequestHandler<GetAllAuthorsQuery, List<Author>>
{
    private readonly MinimalBlogDbContext _context;

    public GetAllAuthorsHandler(MinimalBlogDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public Task<List<Author>> Handle(GetAllAuthorsQuery request, CancellationToken cancellationToken)
    {
        return _context.Authors.ToListAsync(cancellationToken);
    }
}
پس از این تغییرات، بدنه‌ی lambda expression مربوط به endpoints.MapGet به صورت زیر تغییر کرده و ساده می‌شود:
endpoints.MapGet("/api/authors", async (IMediator mediator) =>
{
   var request = new GetAllAuthorsQuery();
   var authors = await mediator.Send(request);
   return authors;
});
مزیت استفاده‌ی از الگوی CQRS، تنها به حذف لایه‌ی سرویس و رسیدن به ویژگی‌هایی مستقل و متکی به خود، منحصر نیست. با استفاده از این الگو می‌توان مقیاس پذیری برنامه را نیز افزایش داد. برای مثال یک بانک اطلاعاتی بهینه سازی شده را صرفا برای کوئری‌ها، درنظر گرفت و بانک اطلاعاتی دیگری را تنها برای اعمال Write که Commands بر روی آن اجرا می‌شوند و در اینجا تنها نیاز به همگام سازی اطلاعات بانک اطلاعاتی Write، با بانک اطلاعاتی Read است که در بسیاری از اوقات پرکارتر از بانک‌های اطلاعاتی دیگر است:


و یا حتی معماری CQRS با معماری Event store نیز قابل ترکیب است:


در اینجا بجای استفاده از بانک اطلاعاتی Write، از یک Event store استفاده می‌شود. کار event store، دریافت رویدادهای write است و سپس باز پخش آن‌ها به بانک اطلاعاتی Read؛ تا کار همگام سازی به این نحو صورت گیرد.


روشی برای نظم دادن به نحوه‌ی تعریف کلاس‌های الگوی CQRS

تا اینجا برای مثال کلاسCreateAuthorCommand  را در یک فایل مجزا و سپس هندلر آن‌را به نام CreateAuthorCommandHandler در یک فایل دیگر تعریف کردیم. می‌توان جهت بالابردن خوانایی برنامه، کاهش رفت و برگشت‌ها برای یافتن کلاس‌های مرتبط و همچنین سهولت یافتن هندلرهای مرتبط با هر متد mediator.Send، از روش زیر نیز استفاده کرد:
public static class CreateAuthor
{
    public class Command : IRequest<AuthorGetDto>
    {
        // ...
    }

    public class Handler : IRequestHandler<Command, AuthorGetDto>
    {
       // ...
    }
}
در اینجا از nested classes استفاده شده‌است. ابتدا نام اصلی Command و یا کوئری ذکر می‌شود؛ که نام کلاس دربرگیرنده‌ی اصلی را تشکیل می‌دهد. سپس دو کلاس بعدی فقط Command و Handler نام می‌گیرند و نه هیچ نام دیگری. به این ترتیب به یکسری نام یک دست در کل پروژه خواهیم رسید. زمانیکه قرار است mediator.Send فراخوانی شود، اینبار چنین شکلی را پیدا می‌کند که مزیت آن، سهولت یافتن هندلر مرتبط، فقط با پیگیری کلاس اصلی CreateAuthor است:
var command = new CreateAuthor.Command { AuthorDto = authorDto };
var author = await mediator.Send(command, ct);

در مورد کوئری‌ها هم می‌توان به قالب مشابهی رسید که در اینجا هم کوئری و هندلر آن، ذیل نام اصلی مدنظر قرار می‌گیرند:
public static class GetAllAuthors
{
    public class Query : IRequest<List<AuthorGetDto>>
    {
       //...
    }

    public class Handler : IRequestHandler<Query, List<AuthorGetDto>>
    {
       //...
    }
}
و اگر کدهای نهایی این سری را که از قسمت اول قابل دریافت است بررسی کنید، از همین ساختار یکدست، برای تعاریف دستورات و کوئری‌ها استفاده شده‌است.
مطالب
امکان بررسی سلامت برنامه در ASP.NET Core 2.2
ASP.NET Core 2.2 به همراه تعدادی قابلیت جدید است که یکی از آن‌ها بررسی سلامت برنامه یا Health Check نام دارد. در بسیاری از اوقات ممکن است از سرویس‌های ping و یا درخواست مشاهده‌ی صفحات وب سایت در بازه‌های زمانی مشخصی، جهت اطمینان حاصل کردن از برپایی و سلامت آن استفاده کنید. اما این سرویس‌ها الزاما وضعیت سلامت برنامه را نمی‌توانند به خوبی گزارش کنند. به همین جهت امکان ارائه‌ی گزارش‌های دقیق‌تری توسط ویژگی Health Check به ASP.NET Core اضافه شده‌است.

پیاده سازی ویژگی Health Check بدون استفاده از قابلیت‌های ASP.NET Core 2.2

اگر بخواهیم در بررسی سلامت برنامه، وضعیت بانک اطلاعاتی آن‌را گزارش دهیم، می‌توان یک چنین اکشن متدی را طراحی کرد که در آن اتصالی به بانک اطلاعاتی باز شده و اگر در حین فراخوانی مسیر working/، استثنائی رخ داد، با بازگشت status code مساوی 503، عدم سلامت برنامه اعلام شود؛ کاری که سرویس‌های ping متداول نمی‌توانند آن‌را با این دقت انجام دهند:
[Route("working")]
public ActionResult Working()
{
    using (var connection = new SqlConnection(_connectionString))
    {
        try
        {
            connection.Open();
        }
        catch (SqlException)
        {
            return new HttpStatusCodeResult(503, "Generic error");
        }
    }
   return new EmptyResult();
}

بازنویسی قطعه کد فوق با ویژگی جدید Health Check در ASP.NET Core 2.2

اکنون اگر بخواهیم قطعه کد فوق را با کمک ویژگی‌های جدید ASP.NET Core 2.2 بازنویسی کنیم، روش کار به صورت زیر خواهد بود:
namespace MvcHealthCheckTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHealthChecks()
                    .AddCheck("sql", () =>
                        {
                            using (var connection = new SqlConnection(Configuration["connectionString"]))
                            {
                                try
                                {
                                    connection.Open();
                                }
                                catch (SqlException)
                                {
                                    return HealthCheckResult.Unhealthy();
                                }
                            }
                            return HealthCheckResult.Healthy();
                        });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseHealthChecks("/working");
- ابتدا توسط متد services.AddHealthChecks، سرویس بررسی سلامت برنامه، ثبت و معرفی می‌شود.
- سپس توسط متد app.UseHealthChecks، بدون اینکه نیاز باشد کنترلر و اکشن متد جدیدی را جهت بازگشت وضعیت سلامت برنامه، تعریف کنیم، مسیر working/ قابل دسترسی خواهد شد.
تا اینجا اگر این مسیر را به سرویس بررسی uptime برنامه‌ی خود معرفی کنید، صرفا وضعیت قابل دسترسی بودن مسیر working/ را دریافت خواهید کرد. اگر نیاز به گزارش دقیق‌تری وجود داشت، می‌توان به کمک متد AddCheck، یک منطق سفارشی را نیز به آن افزود؛ همانند بررسی امکان اتصال به بانک اطلاعاتی، به روشی که ملاحظه می‌کنید. در اینجا اگر منطق مدنظر با موفقیت اجرا شد، HealthCheckResult.Healthy بازگشت داده می‌شود و یا HealthCheckResult.Unhealthy در صورت عدم موفقیت. هر کدام از این متدها می‌توانند توضیحات و یا اطلاعات بیشتری را نیز توسط پارامترهای خود ارائه دهند.


امکان تهیه سرویس‌های سفارشی بررسی سلامت برنامه

در مثال قبل، منطق بررسی سلامت برنامه را همانجا داخل متد ConfigureServices، به کمک متد services.AddHealthChecks().AddCheck معرفی کردیم. امکان انتقال این کدها به سرویس‌های سفارشی، با پیاده سازی اینترفیس IHealthCheck نیز وجود دارد:
    public class SqlServerHealthCheck : IHealthCheck
    {
        private readonly IConfiguration _configuration;

        public SqlServerHealthCheck(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public Task<HealthCheckResult> CheckHealthAsync(
            HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var connection = new SqlConnection(_configuration["connectionString"]))
            {
                try
                {
                    connection.Open();
                }
                catch (SqlException)
                {
                    return Task.FromResult(HealthCheckResult.Unhealthy());
                }
            }
            return Task.FromResult(HealthCheckResult.Healthy());
        }
    }
در اینجا کدهای AddCheck را به متد CheckHealthAsync منتقل کردیم. پس از آن برای معرفی آن به سیستم می‌توان از روش زیر استفاده کرد:
namespace MvcHealthCheckTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHealthChecks()
                    .AddCheck<SqlServerHealthCheck>("sql");
متد AddCheck، کلاس SqlServerHealthCheck را به صورت یک سرویس جدید با طول عمر Transient به سیستم تزریق وابستگی‌های NET Core. معرفی می‌کند (یعنی با هربار درخواست مسیر working/، یک وهله‌ی جدید از این کلاس ساخته شده و استفاده می‌شود) که امکان تزریق در سازنده‌ی کلاس آن نیز وجود دارد.


سفارشی سازی خروجی بررسی سلامت برنامه‌ها

تا اینجا از متدهای کلی Unhealthy و Healthy برای بازگشت وضعیت سلامت برنامه استفاده کردیم؛ خروجی‌های بهتری را نیز می‌توان ارائه داد:
public Task<HealthCheckResult> CheckHealthAsync(
            HealthCheckContext context,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var connection = new SqlConnection(_configuration["connectionString"]))
            {
                try
                {
                    connection.Open();
                }
                catch (SqlException)
                {
                    return Task.FromResult(new HealthCheckResult(
                                                   status: context.Registration.FailureStatus,
                                                   description: "It is dead!"));
                }
            }
            return Task.FromResult(HealthCheckResult.Healthy("Healthy as a horse"));
        }
در نهایت نیاز است خروجی از نوع HealthCheckResult بازگشت داده شود. این خروجی را یا می‌توان توسط متدهای Healthy و Unhealthy با پارامترهای مخصوص آن‌ها ایجاد کرد و یا مانند این مثال، توسط وهله سازی مستقیم آن.
روش دیگر سفارشی سازی خروجی آن، استفاده از پارامتر دوم متد app.UseHealthChecks است:
namespace MvcHealthCheckTest
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseHealthChecks("/working", new HealthCheckOptions
            {
                ResponseWriter = async (context, report) =>
                {
                    var result = JsonConvert.SerializeObject(new
                    {
                        status = report.Status.ToString(),
                        errors = report.Entries.Select(e =>
                        new
                        {
                            key = e.Key,
                            value = Enum.GetName(typeof(HealthStatus), e.Value.Status)
                        })
                    });
                    context.Response.ContentType = MediaTypeNames.Application.Json;
                    await context.Response.WriteAsync(result);
                }
            });
در اینجا یک خروجی JSON، از ریز خطاهای گزارش شده، تهیه شده و توسط context.Response.WriteAsync به فراخوان ارائه می‌شود.


معرفی کتابخانه‌ای از IHealthCheckهای سفارشی

از مخزن کد AspNetCore.Diagnostics.HealthChecks می‌توانید IHealthCheckهای سفارشی مخصوص SQL Server، MySQL و غیره را نیز دریافت و استفاده کنید.
اشتراک‌ها
مشخصات یک کنترلر خوب ASP.NET Core API

When developing controllers in ASP.NET Core, there are certain practices that should be avoided to ensure maintainability, performance, and adherence to best practices. Here are 12 things we should avoid in our controllers. 

مشخصات یک کنترلر خوب ASP.NET Core API
اشتراک‌ها
مایکروسافت: NET Core. آینده‌ی NET. است

All future investment in .NET will be in .NET Core. This includes:  Runtime,   JIT , AOT,   GC,  BCL (Base Class Library),  C#, VB .NET , F#, ASP.NET,  Entity Framework ML.NET,  WinForms, WPF  and Xamarin.  

مایکروسافت: NET Core. آینده‌ی NET. است