dotnet new -i FeatherHttp.Templates::0.1.67-alpha.g69b43bed72 --nuget-source https://f.feedz.io/featherhttp/framework/nuget/index.json
Templates Short Name Language Tags ---------------------------------------------------------------------------------------------------------------------------------- FeatherHttp feather [C#] Web/ASP.NET/FeatherHttp
dotnet new feather --name todoAPI
همانطور که مشاهده میکنید پروژهی فوق تنها شامل دو فایل .csproj و Program.cs است. درون Program.cs و متد Main کار initialize کردن سرور HTTP صورت گرفته است. WebApplication.Create دقیقا همانند Host.CreateDefaultBuilder پروژههای ASP.NET Core عمل میکند؛ یعنی پیکربندی pipeline از قبیل اضافه کردن متغیرهای محیطی، خواندن از فایل JSON و ... را انجام میدهد اما با کد boilerplate کمتر. بنابراین خروجی WebApplication.Create یک ASP.NET Core Pipeline با قابلیت اضافه کردن تنظیمات دلخواه است. در ادامه جهت بررسی بیشتر Feather HTTP، یک مدل را به همراه یک سری دیتای In-memory به پروژه اضافه خواهیم کرد:
using System.Collections.Generic; using System.Text.Json.Serialization; using System.Linq; namespace todoAPI.Models { public class Todo { [JsonPropertyName("id")] public int Id { get; set; } [JsonPropertyName("title")] public string Title { get; set; } [JsonPropertyName("completed")] public bool Completed { get; set; } } public class TodoData { private readonly IList<Todo> _db = new List<Todo> { new Todo { Id = 1, Title = "Read book" }, new Todo { Id = 2, Title = "Watch an episode of Dark" }, new Todo { Id = 3, Title = "Publish a post on dotnettips" }, new Todo { Id = 4, Title = "Skype with my friend" }, }; public IList<Todo> GetAllToDoItmes() { return _db; } public void AddTodo(Todo item) { _db.Add(item); } public void ToggleTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); todo.Completed = !todo.Completed; } public void DeleteTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); _db.Remove(todo); } } }
در مثال فوق برای نگاشت نام خواص، از System.Text.Json توکار NET Core 3.0. استفاده شدهاست. در ادامه نیز از یک کلاس برای شبیهسازی CRUD یک Todo استفاده شدهاست. سپس برای داشتن اندپوینتهای موردنظر به ازای هر کدام از متدهای فوق درون متد Main، از app.Map... استفاده کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using todoAPI.Models; namespace todoAPI { class Program { private static readonly TodoData db = new TodoData(); static async Task Main(string[] args) { var app = WebApplication.Create(args); app.MapGet("/", GetTodos); app.MapPost("/api/todos", CreateTodo); app.MapPost("/api/todos/{id}", ToggleTodo); app.MapDelete("/api/todos/{id}", DeleteTodo); await app.RunAsync(); } static async Task GetTodos(HttpContext http) { var todos = db.GetAllToDoItmes(); await http.Response.WriteJsonAsync(todos); } static async Task CreateTodo(HttpContext http) { var todo = await http.Request.ReadJsonAsync<Todo>(); db.AddTodo(todo); http.Response.StatusCode = 204; } static async Task ToggleTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } db.ToggleTodo(id); http.Response.StatusCode = 204; } static async Task DeleteTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } db.DeleteTodo(id); http.Response.StatusCode = 204; } } }
هر کدام از اندپوینتهای فوق، یک ورودی HttpContext دریافت خواهند کرد. توسط این شیء میتوانیم به درخواست جاری و همچنین به پاسخ درخواست، دسترسی داشته باشیم.
استفاده از سیستم DI توکار NET Core.
همانطور که در ابتدای مطلب نیز عنوان شد، Feather HTTP یک wrapper بر روی APIهای موجود ASP.NET Core است، بنابراین میتوانیم از همان سرویس DI که درون پروژههای ASP.NET Core در اختیار داریم در اینجا نیز استفاده کنیم. در ادامه یک پوشهی جدید را به مثال قبل، با نام Controllers اضافه خواهیم کرد و درون آن یک فایل TodoController را با محتویات زیر ایجاد خواهیم کرد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using todoAPI.Models; using todoAPI.Services; namespace todoAPI.Controllers { public class TodoController { private readonly ITodoService _todoService; public TodoController(ITodoService todoService) { _todoService = todoService; } public async Task GetTodos(HttpContext http) { var todos = _todoService.GetAllToDoItmes(); await http.Response.WriteJsonAsync(todos); } public async Task CreateTodo(HttpContext http) { var todo = await http.Request.ReadJsonAsync<Todo>(); _todoService.AddTodo(todo); http.Response.StatusCode = 204; } public async Task ToggleTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } _todoService.ToggleTodo(id); http.Response.StatusCode = 204; } public async Task DeleteTodo(HttpContext http) { if (!http.Request.RouteValues.TryGet("id", out int id)) { http.Response.StatusCode = 400; return; } _todoService.DeleteTodo(id); http.Response.StatusCode = 204; } } }
کاری که انجام شده است، انتقال تمامی متدهای static به کلاس فوق و سپس جایگزین کردن کلمهی کلیدی static با public است. همچنین یه ارجاع به اینترفیس جدید با عنوان ITodoService اضافه شده است؛ درون پیادهسازی این اینترفیس همان متدهای کلاس TodoData را اضافه کردهایم:
using System.Collections.Generic; using todoAPI.Models; using System.Linq; namespace todoAPI.Services { public interface ITodoService { void AddTodo(Todo item); void DeleteTodo(int id); IList<Todo> GetAllToDoItmes(); void ToggleTodo(int id); } public class TodoService : ITodoService { private readonly IList<Todo> _db = new List<Todo> { new Todo { Id = 1, Title = "Read book" }, new Todo { Id = 2, Title = "Watch an episode of Dark" }, new Todo { Id = 3, Title = "Publish a post on dotnettips" }, new Todo { Id = 4, Title = "Skype with my friend" }, }; public IList<Todo> GetAllToDoItmes() { return _db; } public void AddTodo(Todo item) { _db.Add(item); } public void ToggleTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); todo.Completed = !todo.Completed; } public void DeleteTodo(int id) { var todo = _db.FirstOrDefault(x => x.Id == id); _db.Remove(todo); } } }
نکته: برای ایجاد اینترفیس از روی یک کلاس درون VS Code میتوانیم اینگونه عمل کنیم:
تغییرات فایل Program.cs
ابتدا باید using مربوط به DI را در ابتدای فایل اضافه کنیم:
using Microsoft.Extensions.DependencyInjection;
سپس توسط ServiceProvider یک وهله از کلاس موردنظر را ایجاد کردهایم و همچنین سرویسهای موردنظر را درون DI Container اضافه کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using todoAPI.Controllers; using todoAPI.Services; namespace todoAPI { class Program { static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddTransient<TodoController>(); builder.Services.AddTransient<ITodoService, TodoService>(); var serviceProvider = builder.Services.BuildServiceProvider(); var todoController = serviceProvider.GetService<TodoController>(); var app = WebApplication.Create(args); app.MapGet("/", todoController.GetTodos); app.MapPost("/api/todos", todoController.CreateTodo); app.MapPost("/api/todos/{id}", todoController.ToggleTodo); app.MapDelete("/api/todos/{id}", todoController.DeleteTodo); await app.RunAsync(); } } }
Convention Over Configuration
در کد قبلی به صورت دستی TodoController را توسط Service Location از DI درخواست کردهایم. اینکار را در ادامه میتوانیم به Feather HTTP سپرده تا کار وهلهسازی را براساس قواعد توکار برایمان انجام دهد:
using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using todoAPI.Services; namespace todoAPI { class Program { static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); builder.Services.AddControllers(); builder.Services.AddSingleton<ITodoService, TodoService>(); var serviceProvider = builder.Services.BuildServiceProvider(); var app = builder.Build(); app.MapControllers(); await app.RunAsync(); } } }
سپس در ادامه برای دسترسی به HTTP Context درون TodoController از IHttpContextAccessor استفاده کردهایم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using todoAPI.Models; using todoAPI.Services; namespace todoAPI.Controllers { public class TodoController { private readonly ITodoService _todoService; private readonly IHttpContextAccessor _accessor; public TodoController(ITodoService todoService, IHttpContextAccessor accessor) { _todoService = todoService; _accessor = accessor; } [HttpGet("/todos")] public async Task GetTodos() { var todos = _todoService.GetAllToDoItmes(); await _accessor.HttpContext.Response.WriteJsonAsync(todos); } [HttpPost("/todos")] public async Task CreateTodo() { var todo = await _accessor.HttpContext.Request.ReadJsonAsync<Todo>(); _todoService.AddTodo(todo); _accessor.HttpContext.Response.StatusCode = 204; } [HttpPost("/todos/{id}")] public async Task ToggleTodo(int id) { _todoService.ToggleTodo(id); _accessor.HttpContext.Response.StatusCode = 204; } [HttpDelete("/todos/{id}")] public async Task DeleteTodo(int id) { _todoService.DeleteTodo(id); _accessor.HttpContext.Response.StatusCode = 204; } } }
کدهای کامل مطلب را میتوانید از اینجا دریافت کنید.
متدهای async تقلبی
class MyService { public int CalculateXYZ() { // Tons of work to do in here! for (int i = 0; i != 10000000; ++i) ; return 42; } }
class MyService { public async Task<int> CalculateXYZAsync() { return await Task.Run(() => { // Tons of work to do in here! for (int i = 0; i != 10000000; ++i) ; return 42; }); } }
کاری که در اینجا انجام شده، استفادهی ناصحیح از Task.Run در حین طراحی یک متد و یک API است. عملیات انجام شده در آن واقعا غیرهمزمان نیست و در زمان انجام آن، باز هم ترد جدید اختصاص داده شده را تا پایان عملیات قفل میکند. اینجا است که باید بین CPU-bound operations و IO-bound operations تفاوت قائل شد. اگر Entity Framework 6 و یا کلاس WebClient و امثال آن، متدهایی Async را نیز ارائه دادهاند، اینها به معنای واقعی کلمه، غیرهمزمان هستند و در آنها کوچکترین CPU-bound operation ایی انجام نمیشود.
در حلقهای که در مثال فوق در حال پردازش است و یا تمام اعمال انجام شده توسط CPU، از مرزهای سیستم عبور نمیکنیم. نه قرار است فایلی را ذخیره کنیم، نه با اینترنت سر و کار داشته باشیم و یا مثلا اطلاعاتی را از وب سرویسی دریافت کنیم و نه هیچگونه IO-bound operation خاصی قرار است صورت گیرد.
زمانیکه برنامه نویسی قرار است با API شما کار کند و به امضای async Task میرسد، فرضش بر این است که در این متد واقعا یک کار غیرهمزمان در حال انجام است. بنابراین جهت بالابردن کارآیی برنامه، این نسخه را نسبت به نمونهی غیرهمزمان انتخاب میکند.
حال تصور کنید که استفاده کننده از این API یک برنامهی دسکتاپ نیست، بلکه یک برنامهی ASP.NET است. در اینجا Task.Run فراخوانی شده صرفا سبب خواهد شد عملیات مدنظر، بر روی یک ترد دیگر، نسبت به ترد اصلی اختصاص داده شده توسط ASP.NET برای فراخوانی و پردازش CalculateXYZAsync، صورت گیرد. این عملیات بهینه نیست. تمام پردازشهای درخواستهای ASP.NET در تردهای خاص خود انجام میشوند. وجود ترد دوم ایجاد شده توسط Task.Run در اینجا چه حاصلی را بجز سوئیچ بیجهت بین تردها و همچنین بالا بردن میزان کار Garbage collector دارد؟ در این حالت نه تنها سبب بالا بردن مقیاس پذیری سیستم نشدهایم، بلکه میزان کار Garbage collector و همچنین سوئیچ بین تردهای مختلف را در Thread pool برنامه به شدت افزایش دادهایم. همچنین یک چنین سیستمی برای تدارک تردهای بیشتر و مدیریت آنها، مصرف حافظهی بیشتری نیز خواهد داشت.
یک اصل مهم در طراحی کدهای Async
استفاده از Task.Run در پیاده سازی بدنه متدهای غیرهمزمان، یک code smell محسوب میشود.
چکار باید کرد؟
اگر در کدهای خود اعمال Async واقعی دارید که IO-bound هستند، از معادلهای Async طراحی شده برای کار با آنها، مانند متد SaveChangesAsync در EF، متد DownloadStringTaskAsync کلاس WebClient و یا متدهای جدید Async کلاس Stream برای خواندن و نوشتن اطلاعات استفاده کنید. در یک چنین حالتی ارائه متدهای async Task بسیار مفید بوده و در جهت بالابردن مقیاس پذیری سیستم بسیار مؤثر واقع خواهند شد.
اما اگر کدهای شما صرفا قرار است بر روی CPU اجرا شوند و تنها محاسباتی هستند، اجازه دهید مصرف کننده تصمیم بگیرد که آیا لازم است از Task.Run برای فراخوانی متد ارائه شده در کدهای خود استفاده کند یا خیر. اگر برنامهی دسکتاپ است، این فراخوانی مفید بوده و سبب آزاد شدن ترد UI میشود. اگر برنامهی وب است، به هیچ عنوان نیازی به Task.Run نبوده و فراخوانی متداول آن با توجه به اینکه درخواستهای برنامههای ASP.NET در تردهای مجزایی اجرا میشوند، کفایت میکند.
به صورت خلاصه
از Task.Run در پیاده سازی بدنه متدهای API خود استفاده نکنید.
از Task.Run در صورت نیاز (مثلا در برنامههای دسکتاپ) در حین فراخوانی و استفاده از متدهای API ارائه شده استفاده نمائید:
private async void MyButton_Click(object sender, EventArgs e) { await Task.Run(() => myService.CalculateXYZ()); }
بنابراین نوشتن یک چنین کدهایی در پیاده سازی یک API غیرهمزمان
await Task.Run(() => { for (int i = 0; i != 10000000; ++i) ; });
برای مطالعه بیشتر
Should I expose asynchronous wrappers for synchronous methods
// -- FilteredInclude_EFCore5 var list = dbContext.Blogs .AsNoTracking() .Include(p => p.Posts.Where(p => p.Title.Contains("test title"))) .ToList(); return Json(list);
System.InvalidOperationException: 'Lambda expression used inside Include is not valid.'
// -- NonFilteredInclude var list = dbContext.Blogs .AsNoTracking() .Include(e => e.Posts) .ToList(); list.ForEach(p => p.Posts = p.Posts.Where(p => p.Title.Contains("test title")).ToList());
// -- Projection_Manually var list = dbContext.Blogs .AsNoTracking() .Select(p => new { p.Id, p.Name, Posts = p.Posts.Where(p => p.Title.Contains("test title")).ToList() }).ToList();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Description], [t].[Title] FROM [Blogs] AS [b] LEFT JOIN ( SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title] FROM [Posts] AS [p] WHERE CHARINDEX(N'test title', [p].[Title]) > 0 ) AS [t] ON [b].[Id] = [t].[BlogId] ORDER BY [b].[Id], [t].[Id]
- در صورت نیاز به ویرایش (عدم استفاده از AsNoTracking) بدلیل استفاده از anonymous بجای Blog، هیچ شیء Blog ایی در ChangeTracker ثبت نخواهد شد، ولی اشیا Post در ChangeTracker ثبت میشوند. در نتیجه تنها 1 شیء در ChangeTracker اضافه خواهد شد.
- کد نویسی را کثیف میکند؛ مخصوصا اگر نیاز به شرط گذاری بر روی چندین Navigation Collection تو در تو را داشته باشید.
// -- IncludeFilter_EFCorePlus var list = dbContext.Blogs .AsNoTracking() .IncludeFilter(e => e.Posts.Where(p => p.Title.Contains("test tile"))) .ToList();
-- EF+ Query Future: 1 of 2 SELECT [b].[Id], [b].[Name] FROM [Blogs] AS [b] ; -- EF+ Query Future: 2 of 2 SELECT [t].[Id], [t].[BlogId], [t].[Description], [t].[Title] FROM [Blogs] AS [b] INNER JOIN ( SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title] FROM [Posts] AS [p] WHERE CHARINDEX(N'test title', [p].[Title]) > 0 ) AS [t] ON [b].[Id] = [t].[BlogId] ;
- همانطور که میبینید این دستور، 2 کوئری را اجرا میکند. سرعت آن از روش قبلی کمی کندتر است و memory allocation بیشتری را انجام میدهد.
- در صورت عدم استفاده از AsNoTracking، اشیاء Blog را نیز ثبت میکند؛ درنتیجه تعداد 101 آبجکت (100 Blog و 1 Post) به ChangeTracker اضافه خواهند شد.
- کد نویسی تمیزتر و راحتتری در سمت سی شارپ دارد.
- این روش در EF6 نیز قابل استفاده است.
// -- FilteredInclude_EFCore5 var list = dbContext.Blogs .AsNoTracking() .Include(p => p.Posts.Where(p => p.Title.Contains("test title"))) .ToList();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Description], [t].[Title] FROM [Blogs] AS [b] LEFT JOIN ( SELECT [p].[Id], [p].[BlogId], [p].[Description], [p].[Title] FROM [Posts] AS [p] WHERE CHARINDEX(N'test title', [p].[Title]) > 0 ) AS [t] ON [b].[Id] = [t].[BlogId] ORDER BY [b].[Id], [t].[Id]
- این روش بسیار بهینه است و از روش قبلی (دوم) کمی سریعتر بوده و memory allocation کمتری (نزدیک به روش اول) دارد.
- در صورت عدم استفاده از AsNoTracking، مانند قبلی عمل میکند؛ درنتیجه تعداد 101 آبجکت به ChangeTracker اضافه خواهند شد.
- کد نویسی تمیزتر و راحتتری در سمت سی شارپ دارد.