object text1 = "test"; object text2 = "test"; object num1 = 1; object num2 = 1; Console.WriteLine("text1 == text2 : " + (text1 == text2)); Console.WriteLine("num1 == num2 : " + (num1 == num2));
به نظر شما چه چیزی در خروجی نمایش داده میشود؟
خطی که text2 تعریف شده است را تغییر میدهیم :
object text2 = "test".ToLower();
اما چرا در کد اولی اینگونه نبود؟
بنابراین text1 و text2 در کد اولی واقعا یکی هستند و یک نمونه برای آنها ایجاد شده است.
البته چند نکته در اینجا هست :
اگر text1 و text2 به صورت string تعریف شوند، نتیجه text1==text2 در هر دو حالت فوق برابر true است. چون عملگر == در کلاس string یکبار دیگر overload شده است:
public sealed class String : ... { ... public static bool operator ==(string a, string b) { return string.Equals(a, b); } ... }
این که کدام یک از overloadها اجرا شوند (کلاس پایه، کلاس اصلی، ...) به نوع دو متغییر اطراف == بستگی دارد. مثلا در کد زیر :
string text1 = "test"; string text2 = "test".ToLower(); Console.WriteLine("text1 == text2 (string) : " + (text1 == text2)); Console.WriteLine("text1 == text2 (object) : " + ((object)text1 == (object)text2));
اولین نتیجه true و دومی false است. چون در اولی عملگر == تعریف شده در کلاس string مورد استفاده قرار میگیرد اما در دومی عملگر == تعریف شده در کلاس object.
اگر دقت نشود این رفتار مشکلزا میشود. مثلا حالتی را در نظر بگیرید که text1 ورودی کاربر است و text2 از بانک اطلاعاتی خوانده شده است و با اینکه مقادیر یکسان دارند نتیجه == آنها false است. اگر تعریف عملگرها در کلاس object به صورت virtual بود و در کلاسهای دیگر override میشد، این تغییر نوعها تاثیری نداشت. اما عملگرها به صورت static تعریف میشوند و امکان override شدن ندارند. به همین خاطر کلاس object متدی به اسم Equals در اختیار گذاشته که کلاسها آنرا override میکنند و معمولا از این متد برای سنجش برابری دو کلاس استفاده میشود :
object text1 = "test"; object text2 = "test".ToLower(); Console.WriteLine("text1 Equals text2 : " + text1.Equals(text2)); Console.WriteLine("text1 Equals text2 : " + object.Equals(text1, text2));
یک مورد استفاده آن هنگام lock روی رشتههاست. برای مثال در کد زیر DeviceId یک رشته است که از بانک اطلاعاتی خوانده میشود و باعث میشود که چند job همزمان به یک دستگاه وصل نشوند :
lock (job.DeviceId) { job.Execute(); }
میتوان lock را اینگونه اصلاح کرد :
lock (string.Intern(job.DeviceId)) { job.Execute(); }
ادامه آشنایی با NUnit
حالتهای مختلف Assert :
NUnit framework حالتهای مختلفی از دستور Assert را پشتیبانی میکند که در ادامه با آنها آشنا خواهیم شد.
کلاس Assertion :
این کلاس دارای متدهای زیر است:
public static void Assert(bool condition)
public static void Assert(string message, bool condition)
public static void AssertEquals(string message, object expected, object actual)
public static void AssertEquals(string message, float expected, float actual, float delta)
public static void AssertEquals(string message, double expected, double actual, double delta)
public static void AssertEquals(string message, int expected, int actual)
public static void AssertEquals(int expected, int actual)
public static void AssertEquals(object expected, object actual)
public static void AssertEquals(float expected, float actual, float delta)
public static void AssertEquals(double expected, double actual, double delta)
public static void AssertNotNull(string message, object anObject)
public static void AssertNotNull(object anObject)
public static void AssertNull(string message, object anObject)
public static void AssertNull(object anObject)
public static void AssertSame(string message, object expected, object actual)
public static void AssertSame(object expected, object actual)
public static void Fail(string message)
public static void Fail()
نکته:
در یک متد آزمایش واحد شما مجازید به هرتعدادی که لازم است از متدهای Assertion استفاده نمائید. در این حالت اگر تنها یکی از متدهای assertion با شکست روبرو شود، کل متد آزمایش واحد شما مردود گزارش شده و همچنین عبارات بعدی Assertion بررسی نخواهند شد. بنابراین توصیه میشود به ازای هر متد آزمایش واحد، تنها از یک Assertion استفاده نمائید.
مهم!
کلاس Assertion منسوخ شده است و توصیه میشود بجای آن از کلاس Assert استفاده گردد.
آشنایی با کلاس Assert :
این کلاس از متدهای زیر تشکیل شده است:
الف) بررسی حالتهای تساوی
Assert.AreEqual( object expected, object actual );
Assert.AreNotEqual( object expected, object actual );
برای مشاهده انواع و اقسام overload های آنها میتوانید به راهنمای NUnit که پس از نصب، در پوشه doc آن قرار میگیرد مراجعه نمائید.
همچنین دو متد زیر و انواع overload های آنها جهت برسی اختصاصی حالت تساوی دو شیء بکار میروند:
Assert.AreSame( object expected, object actual );
Assert.AreNotSame( object expected, object actual );
Assert.Contains( object anObject, IList collection );
Assert.IsTrue( bool condition );
Assert.IsFalse( bool condition);
Assert.IsNull( object anObject );
Assert.IsNotNull( object anObject );
Assert.IsNaN( double aDouble );
Assert.IsEmpty( string aString );
Assert.IsEmpty( ICollection collection );
Assert.IsNotEmpty( string aString );
Assert.IsNotEmpty( ICollection collection );
ج) بررسی حالتهای مقایسهای
Assert.Greater( double arg1, double arg2 );
Assert.GreaterOrEqual( int arg1, int arg2 );
Assert.Less( int arg1, int arg2 );
Assert.LessOrEqual( int arg1, int arg2 );
از ذکر انواع و اقسام overload های این توابع جهت طولانی نشدن مطلب پرهیز شد.
د) بررسی نوع اشیاء
Assert.IsInstanceOfType( Type expected, object actual );
Assert.IsNotInstanceOfType( Type expected, object actual );
Assert.IsAssignableFrom( Type expected, object actual );
Assert.IsNotAssignableFrom( Type expected, object actual );
ه) متدهای کمکی
Assert.Fail();
Assert.Ignore();
طراحی متدی که بررسی کند آیا یک رشته مورد نظر حاوی عبارتی خاص میباشد یا خیر:
public void AssertStringContains( string expected, string actual,
string message )
{
if ( actual.IndexOf( expected ) < 0 )
Assert.Fail( message );
}
StringAssert.Contains( string expected, string actual );
StringAssert.StartsWith( string expected, string actual );
StringAssert.EndsWith( string expected, string actual );
StringAssert.AreEqualIgnoringCase( string expected, string actual );
StringAssert.IsMatch( string expected, string actual );
ز) بررسی فایلها
FileAssert.AreEqual( Stream expected, Stream actual );
FileAssert.AreEqual( FileInfo expected, FileInfo actual );
FileAssert.AreEqual( string expected, string actual );
FileAssert.AreNotEqual( Stream expected, Stream actual );
FileAssert.AreNotEqual( FileInfo expected, FileInfo actual );
FileAssert.AreNotEqual( string expected, string actual );
ح) بررسی collections
CollectionAssert.AllItemsAreInstancesOfType( IEnumerable collection, Type expectedType );
CollectionAssert.AllItemsAreNotNull( IEnumerable collection );
CollectionAssert.AllItemsAreUnique( IEnumerable collection );
CollectionAssert.AreEqual( IEnumerable expected, IEnumerable actual );
CollectionAssert.AreEquivalent( IEnumerable expected, IEnumerable actual);
CollectionAssert.AreNotEqual( IEnumerable expected, IEnumerable actual );
CollectionAssert.AreNotEquivalent( IEnumerable expected,IEnumerable actual );
CollectionAssert.Contains( IEnumerable expected, object actual );
CollectionAssert.DoesNotContain( IEnumerable expected, object actual );
CollectionAssert.IsSubsetOf( IEnumerable subset, IEnumerable superset );
CollectionAssert.IsNotSubsetOf( IEnumerable subset, IEnumerable superset);
CollectionAssert.IsEmpty( IEnumerable collection );
CollectionAssert.IsNotEmpty( IEnumerable collection );
نکته: در تمامی overload های این توابع، آرگومان message نیز وجود دارد. از این آرگومان زمانیکه آزمایش با شکست مواجه شد، جهت ارائه اطلاعات بیشتری استفاده میگردد.
ادامه دارد...
CoffeeScript #13
بخشهای بد
در ادامهی قسمت قبل، به مواردی که توسط CoffeeScript اصلاح شدهاند، میپردازیم.
Reserved words
کلمات کلیدی خاصی در جاوااسکریپت وجود دارد مانند class، enum و const که برای نسخههای بعدی جاوااسکریپت در آینده رزرو شدهاند. استفاده از این کلمات در برنامههای جاوااسکریپت میتواند نتایج غیرقابل پیش بینی داشته باشد. برخی از مرورگرهای به خوبی از عهدهی این کار برمیآیند و بعضی دیگر به طور کامل جلوی استفاده از اینها را گرفتهاند. CoffeeScript بعد از تشخیص استفاده از یک کلمهی کلیدی، با یک راه کار خاص، از این موضوع میگریزد.
به عنوان مثال، فرض کنید میخواهیم از کلمه کلیدی class به عنوان یک خصوصیت در یک شیء استفاده کنیم:
myObj = { delete: "I am a keyword!" } myObj.class = ->
var myObj; myObj = { "delete": "I am a keyword!" }; myObj["class"] = function() {};
Equality comparisons
مقایسه برابری ضعف دیگری است که در جاوااسکریپت باعث ایجاد رفتاری گیج کننده و اغلب باعث ایجاد اشکالاتی در کد نوشته شده میشود. به مثال زیر توجه کنید:
"" == "0"// false 0 == ""// true 0 == "0"// true false == "false"// false false == "0"// true false == undefined// false false == null// false null == undefined// true " \t\r\n" == 0// true
راه حل این کار استفاده از عملگر برابری سختگیرانه است، که از 3 مساوی تشکیل شده است: === عملگر برابر سخت گیرانه دقیقا مانند عملگر برابری عادی عمل میکند و تنها نوع دادهها را بررسی میکند که با هم برابر باشند.
توصیه میشود که همیشه از عملگر برابری سختگیرانه استفاده کنید و هرجا لازم بود قبل مقایسه عمل تبدیل نوع دادهها را انجام دهید.
CoffeeScript این مشکل را به صورت کامل حل کرده است؛ یعنی هر جایی که عمل مقایسه == انجام شود به === تبدیل میشود. شما باید به صورت صریح نوع دادهها را قبل از مقایسه تبدیل کرده باشید.
نکته: در مقایسهها رشته خالی ""، null ،undefined و عدد 0 همگی false برمی گردانند.
alert "Empty Array" unless [].length alert "Empty String" unless "" alert "Number 0" unless 0
if (![].length) { alert("Empty Array"); } if (!"") { alert("Empty String"); } if (!0) { alert("Number 0"); }
alert "This is not called" unless ""?
if ("" == null) { alert("This is not called"); }
Function definition
خیلی جالب است که در جاوااسکریپت میتوانید تابعی را بعد از اینکه فراخوانی کردید، تعریف کنید. به عنوان مثال، کد زیر به صورت کامل اجرا میشود:
wem(); function wem() {alert("hi");}
if (true) { function declaration() { return "first"; } } else { function declaration() { return "second"; } } declaration();
، تابع ()declaration مقدار "first" را برگشت خواهد داد و در دیگر مرورگرها مانند Chrome، مقدار "second" برگشت داده خواهد شد. در حالیکه به نظر میرسد که قسمت else هیچگاه اجرا نخواهد شد.در صورتیکه علاقمند به کسب اطلاعات بیشتری دربارهی نحوه تعریف توابع، هستید باید راهنمای آقای Juriy Zaytsev را مطالعه کنید. به صورت خلاصه، رفتار نسبتا مبهم مرورگرها میتواند منجر به ایجاد مشکلاتی در مسیر نوشتن یک پروژه شوند.
همه چیز در CoffeeScript در نظر گرفته شده است و بهترین روش برای حل این مشکل، حذف کلمه function و به جای آن استفاده از عبارت (expression) تابع است.
Number property lookups
نقصی که در پارسر جاوااسکریپت در مواجه با نماد نقطه (dot notation) بر روی اعداد وجود دارد، تفسیر آن به ممیز شناور، بجای مراجعه به ویژگیهای آن است. برای مثال کد جاوااسکریپت زیر باعث ایجاد خطای نحوی میشود:5.toString();
(5).toString(); 5..toString();
فعالسازی Windows Authentication در IIS
پس از publish برنامه و رعایت مواردی که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS» بحث شد، باید به قسمت Authentication برنامهی مدنظر، در کنسول مدیریتی IIS رجوع کرد:
و سپس Windows Authentication را با کلیک راست بر روی آن و انتخاب گزینهی Enable، فعال نمود:
این تنظیم دقیقا معادل افزودن تنظیمات ذیل به فایل web.config برنامه است:
<system.webServer> <security> <authentication> <anonymousAuthentication enabled="true" /> <windowsAuthentication enabled="true" /> </authentication> </security> </system.webServer>
فعالسازی Windows Authentication در IIS Express
اگر برای آزمایش میخواهید از IIS Express به همراه ویژوال استودیو استفاده کنید، نیاز است فایلی را به نام Properties\launchSettings.json با محتوای ذیل در ریشهی پروژهی خود ایجاد کنید (و یا تغییر دهید):
{ "iisSettings": { "windowsAuthentication": true, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:3381/", "sslPort": 0 } } }
تغییر مهم فایل web.config برنامه جهت هدایت اطلاعات ویندوز به آن
اگر پروژهی شما فایل web.config ندارد، باید آنرا اضافه کنید؛ با حداقل محتوای ذیل:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <handlers> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/> </handlers> <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true"/> </system.webServer> </configuration>
تنظیمات برنامهی ASP.NET Core جهت فعالسازی Windows Authentication
پس از فعالسازی windowsAuthentication در IIS و همچنین تنظیم forwardWindowsAuthToken به true در فایل web.config برنامه، اکنون جهت استفادهی از windowsAuthentication دو روش وجود دارد:
الف) تنظیمات مخصوص برنامههای Self host
اگر برنامهی وب شما قرار است به صورت self host ارائه شود (بدون استفاده از IIS)، تنها کافی است در تنظیمات ابتدای برنامه در فایل Program.cs، استفادهی از میانافزار HttpSys را ذکر کنید:
namespace ASPNETCore2WindowsAuthentication { public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup<Startup>() .UseHttpSys(options => // Just for local tests without IIS, Or self-hosted scenarios on Windows ... { options.Authentication.Schemes = AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM; options.Authentication.AllowAnonymous = true; //options.UrlPrefixes.Add("http://+:80/"); }) .Build(); host.Run(); } } }
ب) تنظیمات مخصوص برنامههایی که قرار است در IIS هاست شوند
در اینحالت تنها کافی است UseIISIntegration در تنظیمات ابتدایی برنامه ذکر شود و همانطور که عنوان شد، نیازی به UseHttpSys در این حالت نیست:
namespace ASPNETCore2WindowsAuthentication { public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); }) .UseStartup<Startup>() .Build(); host.Run(); } } }
فعالسازی میانافزار اعتبارسنجی ASP.NET Core جهت یکپارچه شدن با Windows Authentication
در پایان تنظیمات فعالسازی Windows Authentication نیاز است به فایل Startup.cs برنامه مراجعه کرد و یکبار AddAuthentication را به همراه تنظیم ChallengeScheme آن به IISDefaults افزود:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.Configure<IISOptions>(options => { // Sets the HttpContext.User // Note: Windows Authentication must also be enabled in IIS for this to work. options.AutomaticAuthentication = true; options.ForwardClientCertificate = true; }); services.AddAuthentication(options => { // for both windows and anonymous authentication options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme; }); }
آزمایش برنامه با تدارک یک کنترلر محافظت شده
در اینجا قصد داریم اطلاعات ذیل را توسط تعدادی اکشن متد، نمایش دهیم:
private string authInfo() { var claims = new StringBuilder(); if (User.Identity is ClaimsIdentity claimsIdentity) { claims.Append("Your claims: \n"); foreach (var claim in claimsIdentity.Claims) { claims.Append(claim.Type + ", "); claims.Append(claim.Value + "\n"); } } return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}"; }
namespace ASPNETCore2WindowsAuthentication.Controllers { public class HomeController : Controller { public IActionResult Index() { return View(); } [Authorize] public IActionResult Windows() { return Content(authInfo()); } private string authInfo() { var claims = new StringBuilder(); if (User.Identity is ClaimsIdentity claimsIdentity) { claims.Append("Your claims: \n"); foreach (var claim in claimsIdentity.Claims) { claims.Append(claim.Type + ", "); claims.Append(claim.Value + "\n"); } } return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}"; } [AllowAnonymous] public IActionResult Anonymous() { return Content(authInfo()); } [Authorize(Roles = "Domain Admins")] public IActionResult ForAdmins() { return Content(authInfo()); } [Authorize(Roles = "Domain Users")] public IActionResult ForUsers() { return Content(authInfo()); } } }
dotnet publish
اکنون اگر برنامه را در مرورگر مشاهده کنیم، یک چنین خروجی قابل دریافت است:
در اینجا نام کاربر وارد شدهی به ویندوز و همچنین لیست تمام Claims او مشاهده میشوند. مسیر Home/Windows نیز توسط ویژگی Authorize محافظت شدهاست.
برای محدود کردن دسترسی کاربران به اکشن متدها، توسط گروههای دومین و اکتیودایرکتوری، میتوان به نحو ذیل عمل کرد:
[Authorize(Roles = @"<domain>\<group>")] //or [Authorize(Roles = @"<domain>\<group1>,<domain>\<group2>")]
services.AddAuthorization(options => { options.AddPolicy("RequireWindowsGroupMembership", policy => { policy.RequireAuthenticatedUser(); policy.RequireRole(@"<domain>\<group>")); } });
[Authorize(Policy = "RequireWindowsGroupMembership")]
[HttpGet("[action]")] public IActionResult SomeValue() { if (!User.IsInRole(@"Domain\Group")) return StatusCode(403); return Ok("Some Value"); }
افزودن Claims سفارشی به Claims پیشفرض کاربر سیستم
همانطور که در شکل فوق ملاحظه میکنید، یک سری Claims حاصل از Windows Authentication در اینجا به شیء User اضافه شدهاند؛ بدون اینکه برنامه، صفحهی لاگینی داشته باشد و همینقدر که کاربر به ویندوز وارد شدهاست، میتواند از برنامه استفاده کند.
اگر نیاز باشد تا Claims خاصی به لیست Claims کاربر جاری اضافه شود، میتوان از پیاده سازی یک IClaimsTransformation سفارشی استفاده کرد:
public class ApplicationClaimsTransformation : IClaimsTransformation { private readonly ILogger<ApplicationClaimsTransformation> _logger; public ApplicationClaimsTransformation(ILogger<ApplicationClaimsTransformation> logger) { _logger = logger; } public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) { if (!(principal.Identity is ClaimsIdentity identity)) { return Task.FromResult(principal); } var claims = addExistingUserClaims(identity); identity.AddClaims(claims); return Task.FromResult(principal); } private IEnumerable<Claim> addExistingUserClaims(IIdentity identity) { var claims = new List<Claim>(); var user = @"VahidPC\Vahid"; if (identity.Name != user) { _logger.LogError($"Couldn't find {identity.Name}."); return claims; } claims.Add(new Claim(ClaimTypes.GivenName, user)); return claims; } }
services.AddScoped<IClaimsTransformation, ApplicationClaimsTransformation>(); services.AddAuthentication(options => { // for both windows and anonymous authentication options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme; });
به این ترتیب میتوان لیست Claims ثبت شدهی یک کاربر را در یک بانک اطلاعاتی استخراج و به لیست Claims فعلی آن افزود و دسترسیهای بیشتری را به او اعطاء کرد (فراتر از دسترسیهای پیشفرض سیستم عامل).
برای دسترسی به مقادیر این Claims نیز میتوان به صورت ذیل عمل کرد:
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); var userName = User.FindFirstValue(ClaimTypes.Name); var userName = User.FindFirstValue(ClaimTypes.GivenName);
کدهای کامل این برنامه را از اینجا میتوانید دریافت کنید: ASPNETCore2WindowsAuthentication.zip
در بعضی از شرایط پیش رفته، ممکن است نمونه سازی از یک Implementation Type، نیاز به دخالت مستقیم ما را داشته باشد. Implementation Factory کنترل بیشتری بر چگونگی استفادهی از Implementation Typeها را به ما ارائه میدهد. در هنگام ثبت سرویسی که Implementation Factory را در اختیار ما قرار میدهد، ما یک Delegate را برای فراخوانی سرویس استفاده میکنیم. این Delegate مسئول ساخت یک نمونه از Service Type است. برای مثال وقتی از الگوهای builder یا factory برای ساخت یک شیء استفاده میکنید، شاید نیاز باشد که Implementation Factory را به صورت دستی پیاده سازی کنید. اولین قدم این است که کدتان را در صورت امکان چنان refactor کنید تا DI Container بتواند آن را به صورت خودکار بسازد؛ ولی اینکار همیشه ممکن نیست. برای مثال بعضی از برنامه نویسان ترجیح میدهند یک Config را مستقیما از IOptionMonitor بگیرند و بعد در هر جائیکه خواستند، بجای تزریق IOptionMonitor به سرویس، مستقیما از همان سرویس ثبت شده استفاده کنند:
services.AddSingleton<ILiteDbConfig>(sp =>sp.GetRequiredService<IOptionsMonitor<LiteDbConfig>>().CurrentValue);
یک کاربرد بالقوهی دیگر برای Implementation Factory ، استفاده از Composite Pattern است. هر چند Microsoft DI Container به صورت پیش فرض از Composite Pattern پشتیبانی نمیکند، ولی ما میتوانیم آنرا پیاده سازی کنیم. فرض کنید که قبلا به ازای انجام کاری، به کاربر یک ایمیل را میفرستادیم؛ ولی حالا مالک محصول میآید و میگوید که علاوه بر ایمیل، باید پیامک هم بفرستید و ما یا این سرویس پیامک را از قبل داریم و یا باید آن را بسازیم که فرض میکنیم از قبل آن را داریم. برای این کار ما یک اینترفیس کلیتر به نام INotificationService میسازیم که دو سرویس IEmailNotificationService و ISmsNotificaitonService از آن ارث بری میکنند:
public interface INotificationService { void SendMessage(string msg, int userId); }
public class CompositeNotificationService : INotificationService { private readonly IEnumerable<INotificationService> _notificationServices; public CompositeNotificationService(IEnumerable<INotificationService> notificationServices) { _notificationServices = notificationServices; } public void SendMessage(string msg, int userId) { foreach (var notificationServicei in _notificationServices) { notificationServicei.SendMessage(msg, userId); } } }
services.AddScoped<IEmailNotificationService, EmailNotificationService>(); services.AddScoped<ISMSNotificationService, SMSNotificationService>(); services.AddSingleton<INotificationService>(sp => new CompositeNotificationService( new INotificationService[] { sp.GetRequiredService<IEmailNotificationService>() , sp.GetRequiredService<ISMSNotificationService>() } ));
وهله سازی سفارشی
در مثال بعدی نشان میدهیم که چطور میتوانیم از Implementation Factory برای برگرداندن پیادهسازی سرویسهایی که Service Provider امکان ساخت
خودکار آنها را ندارد، استفاده کنیم. فرض کنید یک کلاس Account داریم که از IAccount ارث بری میکند
و برای ساخت آن باید از متدهای IAccountBuilder که فرآیند ساخت را انجام میدهند، استفاده کنیم. بنابراین امکان ساخت مستقیم یک
شیء از IAccount وجود ندارد. در این حالت بدین صورت عمل میکنیم:
services.AddTransient<IAccountBuilder, AccountBuilder>(); services.AddScoped<IAccount>(sp => { var builder = sp.GetService<IAccountBuilder>(); builder.WithAccountType(AccountType.Guest); return builder.Build(); });
ثبت یک نمونه برای چندین اینترفیس
ممکن است بنا به دلایلی مجبور باشیم یک implementation Type را برای چند سرویس (اینترفیس) به ثبت برسانیم. در این حالت
نمونهی شیء ساخته شده، توسط هر کدام از اینترفیسها قابل استفاده است. فرض کنید یک سرویس Greeting
داریم که پیش از این فقط اینترفیس IHomeGreetingService را پیاده سازی میکرد؛ ولی بنا به دلایلی تصمیم گرفتیم که سرویسی
جامعتر را با نیازمندیهای دیگری نیز تعریف کنیم و GreetingService آن را پیاده سازی کند:
public class GreetingService : IHomeGreetingService , IGreetingService
{ // code here }
احتمالا اولین چیزی که به ذهنمان میرسد این است:
services.TryAddSingleton<IHomeGreetingService, GreetingService>(); services.TryAddSingleton<IGreetingService, GreetingService>();
مشکل روش بالا این است که دو نمونه از GreetingService ساخته میشود و درون حافظه باقی میماند و در حقیقت برای هر اینترفیس، یک نوع جداگانه از GreetingService ثبت میشود؛ در حالیکه ما میخواهیم هر دو اینترفیس فقط از یک نمونه از شیء GreetingService استفاده کنند. دو راه حل برای این مشکل وجود دارد:
var greetingService = new GreetingService(Environment); services.TryAddSingleton<IHomeGreetingService>(greetingService); services.TryAddSingleton<IGreetingService>(greetingService);
در اینجا سازندهی کلاس GreetingService فقط به environment نیاز داشت که یکی از سرویسهای پایهی فریم ورک هست و در این مرحله در دسترس است. به صورت کلی مشکل روش بالا این است که ما مسئول نمونه سازی از سرویس GreetingService هستیم! اگر GreetingService برای ساخته شدن به سرویسها یا ورودی هایی نیاز داشته باشد که فقط در زمان اجرا در دسترس باشند، ما امکان نمونه سازی آنها را نداریم؛ علاوه بر این نمیتوان از روشهای بالای برای حالتهای Scoped یا Transient استفاده کرد.
روش بعدی همان روش استفاده
از Implementation Factory است که در ادامه آن را میبینید:
services.TryAddSingleton<GreetingService>(); services.TryAddSingleton<IHomeGreetingService>(sp => sp.GetRequiredService<GreetingService>()); services.TryAddSingleton<IGreetingService>(sp => sp.GetRequiredService<GreetingService>());
در این روش خود DI Container مسئول نمونه سازی از GreetingService است. علاوه بر این میتوان با استفاده از روش فوق از طول حیاتهای Scoped و Transient هم استفاده کرد؛ در حالیکه در روش قبلی این کار امکان پذیر نبود.
Open Generics Service
گاهی از اوقات میخواهید
سرویسهایی را ثبت کنید که از اینترفیسی جنریک ارث بری میکنند. هر نوع جنریک در
زمان اجرا، نوع مخصوص به خود را واکشی میکند. ثبت کردن دستی این سرویسها میتواند خسته کننده باشد. برای همین مایکروسافت در DI Container خود قابلیت ثبت و واکشی سرویسهای جنریک را نیز در
اختیار ما گذاشتهاست. بیایید نگاهی به سرویس ILogger<T> بیندازیم. این یک سرویس درونی فریمورک است و میتواند به ازای هر نوع، کارهای
مربوط به ثبت log را
انجام بدهد و در پروژهها معمولا از این اینترفیس برای ثبت لاگها در سطح
کنترلر و سرویسها استفاده میشود:
public interface ILogger<out TCategoryName> : ILogger { }
در حالت عادی اگر سرویسی
مشابه سرویس فوق را داشته باشیم، برای ثبت کردن هر سرویس با نوع جنریک اختصاصی آن،
مجبوریم به صورت دستی آن را درون DI Container ثبت کنیم؛ مثلا باید به این صورت عمل کنیم:
services.TryAddScoped<ILogger<HomeController>,Logger<HomeController>>();
services.TryAddScoped(typeof(ILogger<>) , typeof(Logger<>));
دسته بندی سرویسها درون متدهای مختلف و پاکسازی متد ConfigurationService
تا اینجای کار ما سرویسهای مختلفی را به روشهای مختلفی ثبت کردهایم. حتی در همین آموزش ساده، تعداد زیاد سرویسهای ثبت شده، باعث شلوغی و در هم ریختگی کدهای ما میشوند که خوانایی و در ادامه اشکال زدایی و توسعهی کدها را برای ما سختتر میکنند. سادهترین کار برای دسته بندی کدها، استفاده از متدهای private محلی یا استفاده از متدهای توسعهای(الحاقی) است که در اینجا مثالی از استفاده از متدهای توسعهای را آوردهام:namespace AspNetCoreDependencyInjection.Extensions { public static class DICRegisterationExetnsion { /// <summary> /// مثال ثبت برای اپن جنریت /// </summary> /// <param name="services"></param> public static void OpenGenericRegisterationExample(this IServiceCollection services) { services.TryAddScoped<ILogger<HomeController>, Logger<HomeController>>(); services.TryAddScoped(typeof(ILogger<>), typeof(Logger<>)); } /// <summary> /// ثبت تنظیمات به روشهای مختلف /// </summary> public static void RegisterConfiguration(this IServiceCollection services, IConfiguration configuration) { services.AddSingleton(services => new AppConfig { ApplicationName = configuration["ApplicationName"], GreetingMessage = configuration["GreetingMessage"], AllowedHosts = configuration["AllowedHosts"] }); services.AddSingleton(services => new AccountTypeBalanceConfig( new List<(AccountType, long)> { (AccountType.Guest , Convert.ToInt64 (configuration["AccountInitialBalance.Guest"]) ) , (AccountType.Silver , Convert.ToInt64 (configuration["AccountInitialBalance.Silver"]) ) , (AccountType.Gold , Convert.ToInt64 (configuration["AccountInitialBalance.Gold"]) ) , (AccountType.Platinum , Convert.ToInt64 (configuration["AccountInitialBalance.Platinum"]) ) , (AccountType.Titanium , Convert.ToInt64 (configuration["AccountInitialBalance.Titanium"]) ) , }) ); services.AddSingleton(services => new LiteDbConfig { ConnectionString = configuration["LiteDbConfig:ConnectionString"], }); services.Configure<UserOptionConfig>(configuration.GetSection("UserOptionConfig")); } } }
حالا در کلاس ConfigureServices ، درون متدStartup ، به این صورت از این متدهای توسعهای استفاده میکنیم:
services.RegisterConfiguration(this.Configuration); services.OpenGenericRegisterationExample();
میتوانید کد منبع این آموزش را در اینجا ببینید.
در مقاله قبلی از بیلدر برای فیلترگذاری و مرتب سازی نتایج استفاده کردیم و در این مقاله هم برای به روزرسانی و ایندکس گذاری استفاده میکنیم.
به روزرسانی
کد زیر، تاریخ آخرین ورود کتب به انبار را به روزرسانی میکند؛ بدین صورت که اگر تاریخ آخرین به روزرسانی انبار کمتر یا مساوی ده روز است، آنرا برابر 5 روز قبل قرار دهد.
var update = Builders<Book>.Update.Set("LastStock", new DateTime().AddDays((-5))); var filter = Builders<Book>.Filter.Lte("LastStock", new DateTime().AddDays(-10)); collection.UpdateManyAsync(filter, update);
در بسیاری از موارد نیاز است که به روزرسانی یک فیلد، بسته به مقدار فعلیاش صورت بگیرد. برای مثال در کد زیر ما قصد داریم کتبی را که قیمت آنها بالای دویست هزار ریال است، سی هزار ریال از قیمت آنها کم کنیم:
var update = Builders<Book>.Update.Inc("Price", -30000); var filter = Builders<Book>.Filter.Gt("Price", 200000); collection.UpdateMany(filter, update);
حذف
جهت حذف یک سند از سیستم با توجه به مواردی که تا به الان داشتیم باید این حدس را زده باشید که برای حذف، باید از متد Delete استفاده کنیم و یک فیلتر را باید به عنوان شرط گذاری به عنوان پارامتر آن، در نظر بگیریم. کد زیر کتابهایی را که به زبان انگلیسی هستند، از کالکشن حذف میکند:
var filter = Builders<Book>.Filter.Eq("Language.Name", "English"); collection.DeleteMany(filter);
Language.Name
var client = new MongoClient(); var db = client.GetDatabase("publisher"); db.DropCollection("books");
var client = new MongoClient(); client.DropDatabase("publisher");
ایندکس گذاری Indexing
ایندکسها باعث میشوند که دسترسی سریعتری را به دادهها داشته باشیم و فیلدهایی را که در پرس و جوها مورد استفاده قرار میگیرند، سریعتر پیدا کنیم. به عنوان مثال فیلد Year فیلدی است که به احتمال زیاد در پرس و جوهایمان مرتبا استفاده خواهیم کرد تا به لیست کتب آن سال دسترسی سریعتری داشته باشیم. برای ایندکس گذاری هم از کلاس Builders استفاده میکنیم:
var index = Builders<Book>.IndexKeys.Ascending("Year"); collection.Indexes.CreateOneAsync(index);
ایندکسها ترکیبی Compound Index
حتی میتوانید این ایندکس گذاری را بر روی دو فیلد، یعنی به حالت ترکیبی هم انجام دهید:
var index = Builders<Book>.IndexKeys.Ascending("Year").Descending("LastStock"); collection.Indexes.CreateOneAsync(index);
مونگو برای جست و جو اسناد در سیستم خود، از ساختار Btree استفاده میکند و گرهها یا همان Nodeها در واقع به ترتیب خاصی در این درخت باینری قرار میگیرند.
اگر شما تنها به دنبال یک آیتم باشید، مرتب سازی تاثیری بر روی این جست و جو نخواهد داشت؛ ولی اگر چندین آیتم یا گره را بخواهید برگردانید، مرتب سازی باعث میشود که این آیتمها نزدیک یکدیگر باشند و سریعتر واکشی شوند. ولی اینکه این مرتب سازی صعودی یا نزولی باشد تاثیری بر روند آن ندارد. چرا که در هر دو حالت آیتمها نزدیک به یکدیگر هستند.
ولی اگر ایندکس شما ترکیبی باشد، این مرتب سازی تاثیر خودش را نشان خواهد داد. ابتدا درخت را بر اساس سال به صورت صعودی مرتب میکند و بعدا زیرمجموعه هر سال را بر اساس تاریخ آخرین به روزرسانی انبار به صورت نزولی مرتب میکند.
این نوع ایندکسها بر روی آرایههای یک فیلد به صورت خودکار ایجاد میگردند و لازم نیست تا خودتان دستی آنها را ایجاد کنید. موقعیکه فیلدی در کلاستان از نوع یک آرایه دارید، مقادیر این آرایه در درخت نیز لیست میشوند و به طور خودکار توسط این نوع اندیکس، ایندکس گذاری میشوند.
ایندکس جغرافیایی GeoSpatial Index
برای دادههای جرافیای میتوان این نوع ایندکس را پیاده سازی کرد. موقعیکه شما با مختصات جغرافیایی کار میکنید، این ایندکسها میتوانند به شما در بازیابی سریعتر مکانها و مختصات کمک کنند. از آنجا که مطالب این نوع ایندکسها تخصصیتر است، آن را به زمان دیگری موکول میکنیم.
var index2 = Builders<Book>.IndexKeys.Geo2D("Location"); var index3 = Builders<Book>.IndexKeys.Geo2DSphere("Location"); var index4 = Builders<Book>.IndexKeys.GeoHaystack("Location");
ایندکس متنی Text Index
این نوع ایندکسها بر روی فیلدهای نوع رشته String قرار داده میشوند تا جست و جوی عبارتها بین آنها سادهتر گردد. زمانیکه ما در مورد جست و جو در بین مطالب یک سایت صحبت میکنیم، سریعا عبارات Full Text Search یا Elastic و لوسین را به یاد میآوریم. در مونگو هم برای این نوع جست و جوها ایندکس Text را داریم که از این پس بتوانیم جست و جوی سریعتری بر روی آن داشته باشیم.
var index5 = Builders<Book>.IndexKeys.Text("Content");
قابلیتهای ایندکس گذاری این نوع دیتابیس حتی به مواردی چون Sharding ، SparseIndex ,TTL Indexes و ... هم میرسد که از حوصله مقاله فعلی خارج است و باید به زمان دیگری و در یک مقاله تخصصیتر آنها را بررسی کرد.
در قسمت بعدی، ذخیره و بازیابی فایلها در سطح دیتابیس را مورد بررسی قرار میدهیم.
ایجاد یک پروژهی جدید Blazor WASM
برای پیاده سازی و اجرای مثالهای این قسمت، نیاز به یک پروژهی جدید Blazor WASM را داریم که میتوان آنرا با اجرای دستور dotnet new blazorwasm --hosted در یک پوشهی خالی، ایجاد کرد.
یک نکته: دستور فوق به همراه یک سری پارامتر اختیاری مانند hosted-- نیز هست. برای مشاهدهی لیست آنها دستور dotnet new blazorwasm --help را صادر کنید. برای مثال ذکر پارامتر hosted-- سبب میشود تا یک ASP.NET Core host نیز برای Blazor WebAssembly app ایجاد شده تولید شود.
حالت hosted-- آن یک چنین ساختاری را دارد که از سه پروژه و پوشهی Client ،Server و Shared تشکیل میشود:
در اینجا یک پروژهی خالی WASM ایجاد شده که برخلاف حالت معمولی dotnet new blazorwasm که در قسمت قبل آنرا بررسی کردیم، دیگر از فایل استاتیک wwwroot\sample-data\weather.json در آن خبری نیست. بجای آن، یک پروژهی استاندارد ASP.NET Core Web API را در پوشهی جدید Server ایجاد کرده که کار ارائهی اطلاعات این سرویس آب و هوا را انجام میدهد و برنامهی WASM ایجاد شده، این اطلاعات را توسط HTTP Client خود، از سرور Web API دریافت میکند.
بنابراین اگر مدل برنامهای که قصد دارید تهیه کنید، ترکیبی از یک Web API و WASM است، روش hosted--، آغاز آنرا بسیار ساده میکند.
نکته: روش اجرای این نوع برنامهها با اجرای دستور dotnet run در داخل پوشهی Server پروژه، انجام میشود. با اینکار هم سرور ASP.NET Core آغاز میشود و هم برنامهی WASM توسط آن ارائه میگردد. در این حالت اگر آدرس https://localhost:5001 را در مرورگر باز کنیم، هم قسمتهای بدون نیاز به سرور پروژهی WASM قابل دسترسی است (مانند کار با شمارشگر آن) و هم قسمت دریافت اطلاعات از سرور آن، در منوی Fetch Data.
شروع به کار با Razor
پس از ایجاد یک پروژهی جدید WASM، به فایل Client\Pages\Index.razor آن مراجعه کرده و محتوای پیشفرض آنرا بجز سطر اول زیر، حذف میکنیم:
@page "/"
در فایلهای razor. میتوان ترکیبی از کدهای #C و HTML را نوشت. برای مثال:
@page "/" <p>Hello, @name</p> @code { string name = "Vahid N."; }
یک نکته: با توجه به اینکه تغییرات زیادی را در فایل جاری اعمال خواهیم کرد، بهتر است برنامه را با دستور dotnet watch run اجرا کرد، تا این تغییرات را تحت نظر قرار داده و آنها را به صورت خودکار کامپایل کند. به این صورت دیگر نیازی نخواهد بود به ازای هر تغییر، یکبار دستور dotnet run اجرا شود.
در زمان درج متغیرهای #C در بین کدهای HTML توسط razor، استفاده از تمام متدهای الحاقی زبان #C نیز مجاز هستند؛ مانند:
<p>Hello, @name.ToUpper()</p>
یا حتی میتوان یک متد جدید را مانند CustomToUpper در قطعه کد razor، تعریف کرد و از آن به صورت زیر استفاده نمود:
@page "/" <p>Hello, @name.ToUpper()</p> <p>Hello, @CustomToUpper(name)</p> @code { string name = "Vahid N."; string CustomToUpper(string value) => value.ToUpper(); }
<p>Let's add 2 + 2 : @2 + 2 </p>
<p>Let's add 2 + 2 : @(2 + 2) </p>
<button @onclick="@(()=>Console.WriteLine("Test"))">Click me</button>
در اینجا اگر از Console.WriteLine("Test")@ استفاده میشد، به معنای انتساب یک رشتهی محاسبه شده به رویداد onclick بود که مجاز نیست.
روش دیگر انجام اینکار به صورت زیر است:
@page "/" <button @onclick="@WriteLog">Click me 2</button> @code { void WriteLog() { Console.WriteLine("Test"); } }
@page "/" <button @onclick="@(()=>WriteLogWithParam("Test 3"))">Click me 3</button> @code { void WriteLogWithParam(string value) { Console.WriteLine(value); } }
یک نکته: اگر به اشتباه بجای WriteLogWithParam، همان WriteLog قبلی را بنویسیم، کامپایلر (در حال اجرای توسط دستور dotnet watch run) خطای زیر را نمایش میدهد؛ پیش از اینکه برنامه در مرورگر اجرا شود:
BlazorRazorSample\Client\Pages\Index.razor(12,25): error CS1501: No overload for method 'WriteLog' takes 1 arguments
امکان تعریف کلاسها در فایلهای razor.
در فایلهای razor.، محدود به تعریف یک سری متدها و متغیرهای ساده نیستیم. در اینجا امکان تعریف کلاسها نیز وجود دارد و همچنین میتوان از کلاسهای خارجی (کلاسهایی که خارج از فایل razor جاری تعریف شدهاند) نیز استفاده کرد.
@page "/" <p>Hello, @StringUtils.MyCustomToUpper(name)</p> @code { public class StringUtils { public static string MyCustomToUpper(string value) => value.ToUpper(); } }
البته این کلاس را تنها میتوان داخل همین کامپوننت استفاده کرد. برای اینکه بتوان از امکانات این کلاس، در سایر کامپوننتها نیز استفاده کرد، میتوان آنرا در پروژهی Shared قرار داد. اگر به تصویر ابتدای مطلب جاری دقت کنید، سه پروژه ایجاد شدهاست:
الف) پروژهی کلاینت: که همان WASM است.
ب) پروژهی سرور: که یک پروژهی ASP.NET Core Web API ارائه کنندهی سرویس و API آب و هوا است و همچنین هاست کنندهی WASM ما.
ج) پروژهی Shared: کدهای این پروژه، بین هر دو پروژه به اشتراک گذاشته میشوند و برای مثال محل مناسبی است برای تعریف DTO ها. برای نمونه WeatherForecast.cs قرار گرفتهی در آن، DTO یا data transfer object سرویس API برنامه است که قرار است به کلاینت بازگشت داده شود. به این ترتیب دیگر نیازی نخواهد بود تا این تعاریف را در پروژههای سرور و کلاینت تکرار کنیم و میتوان کدهای اینگونه را به اشتراک گذاشت.
کاربرد دیگر آن تعریف کلاسهای کمکی است؛ مانند StringUtils فوق. به همین به پروژهی Shared مراجعه کرده و کلاس StringUtils را به صورت زیر در آن تعریف میکنیم (و یا حتی میتوان این قطعه کد را داخل یک پوشهی جدید، در همان پروژهی WASM نیز قرار داد):
namespace BlazorRazorSample.Shared { public class StringUtils { public static string MyNewCustomToUpper(string value) => value.ToUpper(); } }
پس از آن روش استفادهی از این کلاس کمکی خارجی اشتراکی به صورت زیر است:
@page "/" @using BlazorRazorSample.Shared <p>Hello, @StringUtils.MyNewCustomToUpper(name)</p>
یک نکته: میتوان به فایل Client\_Imports.razor مراجعه و مدخل زیر را به انتهای آن اضافه کرد:
@using BlazorRazorSample.Shared
کار با حلقهها در فایلهای razor.
همانطور که عنوان شد، یکی از کاربردهای پروژهی Shared، امکان به اشتراک گذاشتن مدلها، در برنامههای کلاینت و سرور است. برای مثال یک پوشهی جدید Models را در این پروژه ایجاد کرده و کلاس MovieDto را به صورت زیر در آن تعریف میکنیم:
using System; namespace BlazorRazorSample.Shared.Models { public class MovieDto { public string Title { set; get; } public DateTime ReleaseDate { set; get; } } }
@using BlazorRazorSample.Shared.Models
@page "/" <div> <h3>Movies</h3> @foreach(var movie in movies) { <p>Title: <b>@movie.Title</b></p> <p>ReleaseDate: @movie.ReleaseDate.ToString("dd MMM yyyy")</p> } </div> @code { List<MovieDto> movies = new List<MovieDto> { new MovieDto { Title = "Movie 1", ReleaseDate = DateTime.Now.AddYears(-1) }, new MovieDto { Title = "Movie 2", ReleaseDate = DateTime.Now.AddYears(-2) }, new MovieDto { Title = "Movie 3", ReleaseDate = DateTime.Now.AddYears(-3) } }; }
یک نکته: در حین تعریف فیلدهای code@، امکان استفادهی از var وجود ندارد؛ مگر اینکه از آن بخواهیم در داخل بدنهی یک متد استفاده کنیم.
و یا نمونهی دیگری از حلقههای #C مانند for را میتوان به صورت زیر تعریف کرد:
@for(var i = 0; i < movies.Count; i++) { <div style="background-color: @(i % 2 == 0 ? "blue" : "red")"> <p>Title: <b>@movies[i].Title</b></p> <p>ReleaseDate: @movies[i].ReleaseDate.ToString("dd MMM yyyy")</p> </div> }
نمایش شرطی عبارات در فایلهای razor.
اگر به مثال توکار Client\Pages\FetchData.razor مراجعه کنیم (مربوط به حالت host-- که در ابتدای مطلب عنوان شد)، کدهای زیر قابل مشاهده هستند:
@page "/fetchdata" @using BlazorRazorSample.Shared @inject HttpClient Http <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } }
برای رفع این مشکل، ابتدا یک if@ مشاهده میشود، تا نال بودن forecasts را بررسی کند:
@if (forecasts == null) { <p><em>Loading...</em></p> }
روش نمایش عبارات HTML در فایلهای razor.
فرض کنید عنوان اول فیلم مثال جاری، به همراه یک تگ HTML هم هست:
new MovieDto { Title = "<i>Movie 1</i>", ReleaseDate = DateTime.Now.AddYears(-1) },
<p>Title: <b>@((MarkupString)movie.Title)</b></p>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-03.zip
برای اجرای آن وارد پوشهی Server شده و دستور dotnet run را اجرا کنید.
هرچند ارتقاء به HttpClient الزامی نیست و کدهای پیشین، هنوز هم به خوبی کار میکنند؛ اما طراحی جدید آن شامل ویژگیهای توکاری است که به سختی توسط HTTP Module پیشین قابل پیاده سازی هستند.
به روز رسانی وابستگیهای پروژه
پیش از هرکاری نیاز است وابستگیهای پروژه را به روز رسانی کرد و یکی از روشهای سادهی یافتن شماره نگارشهای جدید بستههای تعریف شدهی در فایل package.json برنامه، استفاده از بستهی npm-check-updates است:
npm install npm-check-updates -g ncu
در اینجا شماره نگارشهای جدید مشخص شدهاند و همچنین روش سریع ارتقاء به آنها نیز ذکر شدهاست. فقط کافی است دستورات ذیل را صادر کنیم تا این به روز رسانیها توسط ncu انجام شوند:
ncu -a npm update
تغییرات مورد نیاز جهت معرفی ماژول HttpClient
این ماژول جدید از طریق اینترفیس HttpClientModule ارائه میشود. بنابراین اولین تغییر در جهت ارتقاء به نگارش 4.3، اصلاح importهای لازم است:
از:
import { HttpModule } from '@angular/http';
import { HttpClientModule } from '@angular/common/http';
پس از آن، این HttpClientModule را به لیست imports ماژول اصلی برنامه اضافه میکنیم؛ تا در کل برنامه قابل دسترسی شود:
@NgModule({ imports: [ // ... HttpClientModule, // ... ], declarations: [ ... ], providers: [ ... ], exports: [ ... ] }) export class AppModule { }
تغییرات مورد نیاز در سازندهها و تزریق وابستگیها
پس از تغییرات فوق، دیگر دسترسی به HttpModule پیشین را نداریم. بنابراین نیاز است هر جائی را که سرویس Http به سازندهی کلاسی تزریق شدهاست، یافته و به صورت ذیل تغییر دهیم:
از:
constructor(private http: Http) { }
import { HttpClient } from '@angular/common/http'; // ... constructor(private http: HttpClient) { }
تغییرات مورد نیاز در کدهای سرویسها جهت کار با HTTP Verbs
یکی از اهداف HTTP Client جدید، سادگی کار با اطلاعات دریافتی از سرور است. برای مثال در HTTP Module پیشین، روش دریافت اطلاعات از سرور به صورت ذیل است:
public get(): Observable<MyType> => { return this.http.get(url) .map((response: Response) => <MyType>response.json()); }
در HTTP Client جدید دیگر نیازی نیست تا متد ()json. فراخوانی شود. در اینجا به صورت پیشفرض نوع بازگشتی از سرور JSON فرض میشود. همچنین اکنون متدهای get/put/post و امثال آن برخلاف HTTP Client قبلی، جنریک هستند. یعنی در همینجا میتوان نوع بازگشتی را هم مشخص کرد. به این ترتیب، قطعه کد قدیمی فوق، به کد سادهی ذیل تبدیل میشود که در آن خبری از map و همچنین یک cast اضافی نیست:
get<T>(url: string): Observable<T> { return this.http.get<T>(url); }
post<T>(url: string, body: string): Observable<T> { return this.http.post<T>(url, body); }
نکته 1: در اینجا اگر خروجی از سرور، نوع دیگری را داشت، نیاز است responseType را به صورت صریحی به شکل ذیل مشخص کرد:
getData() { this.http.get(this.url, { responseType: 'text' }).subscribe(res => { this.data = res; }); }
نکته 2: ممکن است اطلاعات بازگشتی از سمت سرور، داخل یک فیلد محصور شده باشند:
{ "results": [ "Item 1", "Item 2", ] }
this.http.get('/api/items').subscribe(data => { this.results = data['results']; });
نکاتی را که باید حین کار با یک RxJS Observable-based API در نظر داشت
این API جدید نیز همانند قبل مبتنی بر RxJS Observables است. بنابراین نکات ذیل در مورد آن نیز صادق است:
- اگر متد subscribe بر روی این observables فراخوانی نشود، اتفاقی رخ نخواهد داد.
- اگر چندین بار مشترک این observables شویم، چندین درخواست HTTP صادر میشوند.
- این نوع خاص از observables، تنها یک مقدار را بازگشت میدهند. اگر درخواست HTTP موفقیت آمیز باشد، این observables یک نتیجه را بازگشت داده و سپس خاتمه پیدا میکنند.
- این observables اگر در حین درخواست HTTP با خطایی مواجه شوند، سبب صدور استثنایی میشوند.
تغییرات مورد نیاز در کدهای سرویسها جهت کار با HTTP Headers
در اینجا برای تعریف headers میتوان به صورت ذیل عمل کرد:
import { HttpHeaders } from "@angular/common/http"; const headers = new HttpHeaders({ "Content-Type": "application/json" });
const headers = new HttpHeaders().set("Accept", "application/json").set('Content-Type', 'application/json');
سپس آنرا به عنوان پارامتر سوم، به متدهای http ارسال میکنیم. یک مثال:
updateAppProduct(id: number, item: AppProduct): Observable<AppProduct> { const header = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .put<AppProduct>( `${this.baseUrl}/UpdateProduct/${id}`, JSON.stringify(item), { headers: header } ) .map(response => response || {}); }
تعریف پارامتر options اینبار به صورت یک شیء دارای چندین خاصیت درآمدهاست. به همین جهت است که در اینجا یک {} را نیز مشاهده میکنید:
(method) HttpClient.post(url: string, body: any, options?: { headers?: HttpHeaders; observe?: "body"; params?: HttpParams; reportProgress?: boolean; responseType?: "json"; withCredentials?: boolean; }): Observable<Object>
یک نکته: شیء HttpHeaders به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
const headers = new HttpHeaders(); headers = headers.set('Content-Type', 'application/json'); headers = headers.set('Accept', 'application/json');
const headers = new HttpHeaders() .set('Content-Type', 'application/json') .set('Accept', 'application/json') ;
امکان تعریف HttpParams
اگر به شیء options در تعریف فوق دقت کنید، دارای خاصیت اختیاری params نیز هست. از آن میتوان جهت تعریف کوئری استرینگها استفاده کرد. برای مثال درخواست ذیل:
http .post('/api/items/add', body, { params: new HttpParams().set('id', '3'), }) .subscribe();
/api/items/add?id=3
یک نکته: شیء HttpParams به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
const params = new HttpParams(); params.set('orderBy', '"$key"') params.set('limitToFirst', "1");
const params = new HttpParams() .set('orderBy', '"$key"') .set('limitToFirst', "1");
const params = new HttpParams({fromString: 'orderBy="$key"&limitToFirst=1'});
تغییرات مورد نیاز در کدهای سرویسها جهت مدیریت خطاها
در اینجا اینبار خطای بازگشتی، از نوع ویژهی HttpErrorResponse است که شامل اطلاعات شماره کد و متن خطای حاصل میباشد:
import { HttpClient, HttpHeaders, HttpErrorResponse } from "@angular/common/http"; postData() { this.http.post(this.url, this.payload).subscribe( res => { console.log(res); }, (err: HttpErrorResponse) => { console.log(err.error); console.log(err.name); console.log(err.message); console.log(err.status); if (err.error instanceof Error) { console.log("Client-side error occured."); } else { console.log("Server-side error occured."); } } ); }
امکان سعی مجدد در اتصال توسط HTTP Client
ممکن است در اولین سعی در اتصال به سرور، خطایی رخ دهد و یا سرور در دسترس نباشد. در اینجا توسط متد retry میتوان درخواست سعی مجدد در اتصال را صادر کرد.
برای این منظور ابتدا عملگر retry مربوط به RxJS را import میکنیم:
import 'rxjs/add/operator/retry';
http .get<ItemsResponse>('/api/items') .retry(3) .subscribe(...);
امکان درخواست کل Response بجای Body
اگر به امضای پارامتر اختیاری options دقت کنید، خاصیت observe آن به صورت پیش فرض به body تنظیم شدهاست. به این معنا که تنها body یک response را تبدیل به یک شیء JSON میکند:
(method) HttpClient.post(url: string, body: any, options?: { headers?: HttpHeaders; observe?: "body"; params?: HttpParams; reportProgress?: boolean; responseType?: "json"; withCredentials?: boolean; }): Observable<Object>
http .get<MyJsonData>('/data.json', {observe: 'response'}) .subscribe(resp => { console.log(resp.headers.get('X-Custom-Header')); console.log(resp.body.someField); });
یک نکتهی تکمیلی: کدهای سری کار با فرمها در Angular را اگر به HttpClient ارتقاء دهیم، خلاصهی تغییرات آنها به این صورت خواهند بود.