کتابخانه ToProgress
کتابخانه layzr.js
نحوه استفاده از MVC در ASP.NET Core
صورت مساله
قصد داریم اطلاعاتی را با فرمت JSON، از یک API خارجی، توسط HttpClient دریافت و سپس آنرا به یک DTO فرضی، به نام GitHubRepositoryDto نگاشت کنیم.
راه حل 1
در این روش از وهله سازی مستقیم HttpClient به همراه استفادهی از یک عبارت using کمک گرفته شدهاست. همچنین چون عملیات async است، نتیجهی آنرا به کمک خاصیت Result دریافت کردهایم که پس از آن، کل اطلاعات دریافتی را به صورت یک رشته، در اختیار خواهیم داشت:
public class GitHubClient { public IReadOnlyCollection<GitHubRepositoryDto> GetRepositories() { using (var httpClient = new HttpClient{BaseAddress = new Uri(GitHubConstants.ApiBaseUrl)}) { var result = httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).Result; return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result); } } }
- استفاده از خاصیت Result، هیچگاه ایدهی خوبی نبوده است و یک عملیات async را تبدیل به عملیاتی Blocking میکند که حتی میتواند سبب بروز dead-lock نیز شود.
- HttpClient نباید Dispose شود. علت آنرا در مطلب «روش استفادهی صحیح از HttpClient در برنامههای دات نت» مفصل بررسی کردهایم.
- دریافت کل response یک API به صورت یک رشتهی بزرگ، یک Large object heap را بهوجود میآورد که باز هم ایدهی خوبی نیست.
راه حل 2
اگر خاصیت Result راه حل 1 را حذف کنیم، به راه حل 2 خواهیم رسید:
public class GitHubClient : IGitHubClient { public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories() { using (var httpClient = new HttpClient { BaseAddress = new Uri(GitHubConstants.ApiBaseUrl) }) { var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath); return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result); } } }
- اینبار از دسترسی asynchronous واقعی استفاده شدهاست.
معایب:
- ایجاد و تخریب یک HttpClient جدید به ازای هر فراخوانی.
- دریافت و ذخیره سازی کل response به صورت یک رشته.
راه حل 3
در این نگارش، HttpClient از طریق وهله سازی در سازندهی کلاس دریافت شده و به این ترتیب امکان استفادهی مجدد را پیدا میکند:
public class GitHubClient : IGitHubClient { private readonly HttpClient _httpClient; public GitHubClient() { _httpClient = new HttpClient { BaseAddress = new Uri(GitHubConstants.ApiBaseUrl) }; } public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories() { var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false); return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result); } }
services.AddSingleton<GitHubClient>();
- دسترسی asynchronous واقعی به API مدنظر.
- استفادهی مجدد از HttpClient
معایب:
- دریافت و ذخیره سازی کل response به صورت یک رشته.
- چون طول عمر GitHubClient از نوع Singleton است و برای همیشه از یک وهلهی سراسری استفاده میکند، از تغییرات DNS آگاه نخواهد شد.
راه حل 4
تا اینجا همانطور که ملاحظه کردید، به سادگی میتوان HttpClient را به نحو نادرستی مورد استفاده قرار داد. ایجاد مجدد آن به علت عدم رها شدن بلافاصلهی سوکتهای لایهی زرین آن توسط سیستم عامل، مشکل حادی را به نام sockets exhaustion پدید میآورد. به همین جهت، این کلاس باید یکبار نمونه سازی شده و در طول عمر برنامه از همین تک وهلهی آن استفاده شود. یک روش اینکار تعریف آن به صورت اشیاء singleton و یا static است. مشکلی که این روش به همراه دارد، عدم باخبر شدن آن از تغییرات DNS است. برای رفع این مسایل، از NET Core 2.1. به بعد، خود مایکروسافت با ارائهی یک IHttpClientFactory، روش استانداری را برای مدیریت وهلههای HttpClient ارائه کردهاست:
public class GitHubClient : IGitHubClient { private readonly IHttpClientFactory _httpClientFactory; public GitHubClient(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); } public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories() { var httpClient = _httpClientFactory.CreateClient("GitHub"); var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false); return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result); } }
services.AddHttpClient("GitHub", x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); }); services.AddSingleton<GitHubClient>();
مزیتها:
- استفادهی از یک IHttpClientFactory توکار
معایب:
- استفادهی یک از کلاینت نامدار، بجای یک کلاینت مشخص شدهی بر اساس نوع آن.
- دریافت و ذخیره سازی کل response به صورت یک رشته.
روش ثبت services.AddHttpClient را که در اینجا ملاحظه میکنید، یک روش ثبت نامدار است و بر اساس نام رشتهای GitHub کار میکند. همین نام در متد GetRepositories به صورت httpClientFactory.CreateClient("GitHub") برای دسترسی به یک HttpClient جدید استفاده شدهاست.
راه حل 5
در اینجا از یک کلاینت نوعدار، بجای یک کلاینت نامدار، استفاده شدهاست:
public class GitHubClient : IGitHubClient { private readonly HttpClient _httpClient; public GitHubClient(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories() { var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false); return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result); } }
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
مزایا:
- استفاده از IHttpClientFactory
- استفاده از یک کلاینت نوعدار، بجای یک نمونهی نامدار
معایب:
- اینبار تمام استفاده کنندگان از IGitHubClient ما باید دارای طول عمر transient باشند (خصوصیت کلاینتهای نوعدار است)؛ برخلاف راه حلهای پیشین که میتوانستند singleton تعریف شوند (یا امکان فراخوانی IGitHubClient از سرویسهای singleton نیز وجود داشت).
- دریافت و ذخیره سازی کل response به صورت یک رشته.
راه حل 6
اگر در جائی نیاز به استفاده و تزریق یک کلاینت نوعدار، در یک سرویس با طول عمر singleton را داشتید، روش آن به صورت زیر است:
public class GitHubClientFactory { private readonly IServiceProvider _serviceProvider; public GitHubClientFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public GitHubClient Create() { return _serviceProvider.GetRequiredService<GitHubClient>(); } }
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); }); services.AddSingleton<GitHubClientFactory>();
مزایا:
- استفاده از IHttpClientFactory
- استفاده از یک کلاینت نوعدار
- استفاده کنندهی از GitHubClientFactory، میتوانند طول عمر singleton نیز داشته باشد
معایب:
- دریافت و ذخیره سازی کل response به صورت یک رشته.
راه حل 7
از اینجا به بعد، هدف ما بهینه سازی عملیات است و رفع مشکل کار با یک رشتهی بزرگ. برای این منظور بجای متد GetStringAsync، از متد SendAsync که امکان streaming را فراهم میکند، استفاده خواهیم کرد. به این ترتیب، بجای ارسال یک رشتهی بزرگ به متد Deserialize، امکان دسترسی به استریم response را توسط آن میسر کردهایم.
public class GitHubClient : IGitHubClient { private readonly HttpClient _httpClient; private readonly JsonSerializer _jsonSerializer; public GitHubClient(HttpClient httpClient, JsonSerializer jsonSerializer) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); } public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories() { var request = CreateRequest(); var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead); using (var responseStream = await result.Content.ReadAsStreamAsync()) { using (var streamReader = new StreamReader(responseStream)) using (var jsonTextReader = new JsonTextReader(streamReader)) { return _jsonSerializer.Deserialize<List<GitHubRepositoryDto>>(jsonTextReader); } } } private static HttpRequestMessage CreateRequest() { return new HttpRequestMessage(HttpMethod.Get, GitHubConstants.RepositoriesPath); } }
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); }); services.AddSingleton<GitHubClientFactory>(); services.AddSingleton<JsonSerializer>();
- کار با IHttpClientFactory
- استفاده از یک کلاینت نوعدار
- کار با استریم response
معایب:
- استفاده از ResponseContentRead
راه حل 8
در این روش بجای سطر ذیل در راه حل 7
var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead);
var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
مزایا:
- کار با IHttpClientFactory
- استفاده از یک کلاینت نوعدار
- کار با استریم response
- استفاده از ResponseHeadersRead
معایب:
- شاید بتوان از کتابخانهی دیگری برای json deserialization استفاده کرد؟
Build Real App in Angular 10 and ASP.Net Web API
These are 2 of the hottest frameworks right now for the ‘back-end’ (Microsoft’s ASP.NET Core) and the ‘front-end’ (Google’s Angular) and are well worth spending the time to learn.
This course starts from scratch, you neither need to know Angular 1 nor Angular 2
We will start from nothing and incrementally build this property dealing application front-end using Angular 10.
And then we will connect our front-end with the Web-API until we have a fully functional Web Application that we will publish to Firebase and then on IIS.
Domain Model یا Business Layer
پیاده سازی را از منطق تجاری یا Business Logic آغاز میکنیم. در روش کد نویسی Smart UI، منطق تجاری در Code Behind قرار میگرفت اما در روش لایه بندی، منطق تجاری و روابط بین دادهها در Domain Model طراحی و پیاده سازی میشوند. در مطالب بعدی راجع به Domain Model و الگوهای پیاده سازی آن بیشتر صحبت خواهم کرد اما بصورت خلاصه این لایه یک مدل مفهومی از سیستم میباشد که شامل تمامی موجودیتها و روابط بین آنهاست.
الگوی Domain Model جهت سازماندهی پیچیدگیهای موجود در منطق تجاری و روابط بین موجودیتها طراحی شده است.
شکل زیر مدلی را نشان میدهد که میخواهیم آن را پیاده سازی نماییم. کلاس Product موجودیتی برای ارائه محصولات یک فروشگاه میباشد. کلاس Price جهت تشخیص قیمت محصول، میزان سود و تخفیف محصول و همچنین استراتژیهای تخفیف با توجه به منطق تجاری سیستم میباشد. در این استراتژی همکاران تجاری از مشتریان عادی تفکیک شده اند.
Domain Model را در پروژه SoCPatterns.Layered.Model پیاده سازی میکنیم. بنابراین به این پروژه یک Interface به نام IDiscountStrategy را با کد زیر اضافه نمایید:
public interface IDiscountStrategy { decimal ApplyExtraDiscountsTo(decimal originalSalePrice); }
علت این نوع نامگذاری Interface فوق، انطباق آن با الگوی Strategy Design Pattern میباشد که در مطالب بعدی در مورد این الگو بیشتر صحبت خواهم کرد. استفاده از این الگو نیز به این دلیل بود که این الگو مختص الگوریتم هایی است که در زمان اجرا قابل انتخاب و تغییر خواهند بود.
توجه داشته باشید که معمولا نام Design Pattern انتخاب شده برای پیاده سازی کلاس را بصورت پسوند در انتهای نام کلاس ذکر میکنند تا با یک نگاه، برنامه نویس بتواند الگوی مورد نظر را تشخیص دهد و مجبور به بررسی کد نباشد. البته به دلیل تشابه برخی از الگوها، امکان تشخیص الگو، در پاره ای از موارد وجود ندارد و یا به سختی امکان پذیر است.
الگوی Strategy یک الگوریتم را قادر میسازد تا در داخل یک کلاس کپسوله شود و در زمان اجرا به منظور تغییر رفتار شی، بین رفتارهای مختلف سوئیچ شود.
حال باید دو کلاس به منظور پیاده سازی روال تخفیف ایجاد کنیم. ابتدا کلاسی با نام TradeDiscountStrategy را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:
public class TradeDiscountStrategy : IDiscountStrategy { public decimal ApplyExtraDiscountsTo(decimal originalSalePrice) { return originalSalePrice * 0.95M; } }
سپس با توجه به الگوی Null Object کلاسی با نام NullDiscountStrategy را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:
public class NullDiscountStrategy : IDiscountStrategy { public decimal ApplyExtraDiscountsTo(decimal originalSalePrice) { return originalSalePrice; } }
از الگوی Null Object زمانی استفاده میشود که نمیخواهید و یا در برخی مواقع نمیتوانید یک نمونه (Instance) معتبر را برای یک کلاس ایجاد نمایید و همچنین مایل نیستید که مقدار Null را برای یک نمونه از کلاس برگردانید. در مباحث بعدی با جزئیات بیشتری در مورد الگوها صحبت خواهم کرد.
با توجه به استراتژیهای تخفیف کلاس Price را ایجاد کنید. کلاسی با نام Price را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:
public class Price { private IDiscountStrategy _discountStrategy = new NullDiscountStrategy(); private decimal _rrp; private decimal _sellingPrice; public Price(decimal rrp, decimal sellingPrice) { _rrp = rrp; _sellingPrice = sellingPrice; } public void SetDiscountStrategyTo(IDiscountStrategy discountStrategy) { _discountStrategy = discountStrategy; } public decimal SellingPrice { get { return _discountStrategy.ApplyExtraDiscountsTo(_sellingPrice); } } public decimal Rrp { get { return _rrp; } } public decimal Discount { get { if (Rrp > SellingPrice) return (Rrp - SellingPrice); else return 0; } } public decimal Savings { get{ if (Rrp > SellingPrice) return 1 - (SellingPrice / Rrp); else return 0; } } }
کلاس Price از نوعی Dependency Injection به نام Setter Injection در متد SetDiscountStrategyTo استفاده نموده است که استراتژی تخفیف را برای کالا مشخص مینماید. نوع دیگری از Dependency Injection با نام Constructor Injection وجود دارد که در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.
جهت تکمیل لایه Model، کلاس Product را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:
public class Product { public int Id {get; set;} public string Name { get; set; } public Price Price { get; set; } }
موجودیتهای تجاری ایجاد شدند اما باید روشی اتخاذ نمایید تا لایه Model نسبت به منبع داده ای بصورت مستقل عمل نماید. به سرویسی نیاز دارید که به کلاینتها اجازه بدهد تا با لایه مدل در اتباط باشند و محصولات مورد نظر خود را با توجه به تخفیف اعمال شده برای رابط کاربری برگردانند. برای اینکه کلاینتها قادر باشند تا نوع تخفیف را مشخص نمایند، باید یک نوع شمارشی ایجاد کنید که به عنوان پارامتر ورودی متد سرویس استفاده شود. بنابراین نوع شمارشی CustomerType را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:
public enum CustomerType { Standard = 0, Trade = 1 }
برای اینکه تشخیص دهیم کدام یک از استراتژیهای تخفیف باید بر روی قیمت محصول اعمال گردد، نیاز داریم کلاسی را ایجاد کنیم تا با توجه به CustomerType تخفیف مورد نظر را اعمال نماید. کلاسی با نام DiscountFactory را با کد زیر ایجاد نمایید:
public static class DiscountFactory { public static IDiscountStrategy GetDiscountStrategyFor (CustomerType customerType) { switch (customerType) { case CustomerType.Trade: return new TradeDiscountStrategy(); default: return new NullDiscountStrategy(); } } }
در طراحی کلاس فوق از الگوی Factory استفاده شده است. این الگو یک کلاس را قادر میسازد تا با توجه به شرایط، یک شی معتبر را از یک کلاس ایجاد نماید. همانند الگوهای قبلی، در مورد این الگو نیز در مباحث بعدی بیشتر صحبت خواهم کرد.
لایهی سرویس با برقراری ارتباط با منبع داده ای، دادههای مورد نیاز خود را بر میگرداند. برای این منظور از الگوی Repository استفاده میکنیم. از آنجایی که لایه Model باید مستقل از منبع داده ای عمل کند و نیازی به شناسایی نوع منبع داده ای ندارد، جهت پیاده سازی الگوی Repository از Interface استفاده میشود. یک Interface به نام IProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:
public interface IProductRepository { IList<Product> FindAll(); }
الگوی Repository به عنوان یک مجموعهی در حافظه (In-Memory Collection) یا انباره ای از موجودیتهای تجاری عمل میکند که نسبت به زیر بنای ساختاری منبع داده ای کاملا مستقل میباشد.
کلاس سرویس باید بتواند استراتژی تخفیف را بر روی مجموعه ای از محصولات اعمال نماید. برای این منظور باید یک Collection سفارشی ایجاد نماییم. اما من ترجیح میدهم از Extension Methods برای اعمال تخفیف بر روی محصولات استفاده کنم. بنابراین کلاسی به نام ProductListExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:
public static class ProductListExtensionMethods { public static void Apply(this IList<Product> products, IDiscountStrategy discountStrategy) { foreach (Product p in products) { p.Price.SetDiscountStrategyTo(discountStrategy); } } }
الگوی Separated Interface تضمین میکند که کلاینت از پیاده سازی واقعی کاملا نامطلع میباشد و میتواند برنامه نویس را به سمت Abstraction و Dependency Inversion به جای پیاده سازی واقعی سوق دهد.
حال باید کلاس Service را ایجاد کنیم تا از طریق این کلاس، کلاینت با لایه Model در ارتباط باشد. کلاسی به نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Model اضافه کنید:
public class ProductService { private IProductRepository _productRepository; public ProductService(IProductRepository productRepository) { _productRepository = productRepository; } public IList<Product> GetAllProductsFor(CustomerType customerType) { IDiscountStrategy discountStrategy = DiscountFactory.GetDiscountStrategyFor(customerType); IList<Product> products = _productRepository.FindAll(); products.Apply(discountStrategy); return products; } }
در اینجا کدنویسی منطق تجاری در Domain Model به پایان رسیده است. همانطور که گفته شد، لایهی Business یا همان Domain Model به هیچ منبع داده ای خاصی وابسته نیست و به جای پیاده سازی کدهای منبع داده ای، از Interfaceها به منظور برقراری ارتباط با پایگاه داده استفاده شده است. پیاده سازی کدهای منبع داده ای را به لایهی Repository واگذار نمودیم که در بخشهای بعدی نحوه پیاده سازی آن را مشاهده خواهید کرد. این امر موجب میشود تا لایه Model درگیر پیچیدگیها و کد نویسیهای منبع داده ای نشود و بتواند به صورت مستقل و فارغ از بخشهای مختلف برنامه تست شود. لایه بعدی که میخواهیم کد نویسی آن را آغاز کنیم، لایهی Service میباشد.
در کد نویسیهای فوق از الگوهای طراحی (Design Patterns) متعددی استفاده شده است که به صورت مختصر در مورد آنها صحبت کردم. اصلا جای نگرانی نیست، چون در مباحث بعدی به صورت مفصل در مورد آنها صحبت خواهم کرد. در ضمن، ممکن است روال یادگیری و آموزش بسیار نامفهوم باشد که برای فهم بیشتر موضوع، باید کدها را بصورت کامل تست نموده و مثالهایی را پیاده سازی نمایید.
The project has not been restored or restore failed -run 'dotnet restore'
The project does not list one of 'win10-x64, win81-x64, win7-x64' in the 'runtimes'
"runtimes": { "win10-x64": {}, "win81-x64": {}, "win8-x64": {} }
- مقدمه ای بر RavenDB – قسمت سوم | www.dotnetdev.info
- 108 Mono Icons: Huge Set of Minimal Icons | Tutorial9 | www.tutorial9.net
- 500 Metro Style WP7 Icons - Fear and Loathing | weblogs.asp.net
- Metro style for ILSpy | jcooney.net
- SharpVectorGraphics (aka SVG#) | sourceforge.net
- Svg2Xaml | svg2xaml.codeplex.com
- MonoGame 2.0 | cocoa-mono.org
- آموزش رایگان و تعاملی مباحث مقدماتی دات نت | mottishaked.com
- بسته آیکونهای متناسب با سبک مترو | somerandomdude.com
- ساخت برنامههای تجاری با JS و HTML در ویندوز 8 بیشباهت به یک شوخی نیست! | neverindoubtnet.blogspot.com
- شیونامهی «مترو» مخصوص برنامههای WPF | www.theleagueofpaul.com