// Angular 1 const module = angular.module('myModule', []); module.service('UserService', ['$http', function ($http) { this.getUsers = () => { return $http.get('http://api.mywebsite.com/users') .then(res => res.data) .catch(res => new Error(res.data.error)); } }]); /***************************************************************/ // Angular 2 import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; @Injectable() class UserService { constructor(private http: Http) {} getUsers(): Observable<User[]> { return this.http.get('http://api.mywebsite.com/users') .map((res: Response) => res.json()) .catch((res: Response) => Observable.throw(res.json().error); } }
static void Main(string[] args) { var configuration = new Raven.Database.Config.RavenConfiguration() { AccessControlAllowMethods = "All", AnonymousUserAccessMode = Raven.Database.Server.AnonymousUserAccessMode.All, DataDirectory = @"C:\Sam\labs\HttpServerData", Port = 8071, }; var database = new Raven.Database.DocumentDatabase(configuration); var server = new Raven.Database.Server.HttpServer(configuration, database); database.SpinBackgroundWorkers(); server.StartListening(); Console.WriteLine("RavenDB http server is running ..."); Console.ReadLine(); }
CloudStorageAccount storageAccount = CloudStorageAccount.FromConfigurationSetting(connectionString); // here is when later on you may add code for inititalizing CloudDrive chache CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); blobClient.GetContainerReference("drives").CreateIfNotExist(); CloudDrive cloudDrive = storageAccount.CreateCloudDrive( blobClient .GetContainerReference("drives") .GetPageBlobReference("ravendb4.vhd") .Uri.ToString() ); try { // create a 1GB Virtual Hard Drive cloudDrive.Create(1024); } catch (CloudDriveException /*ex*/ ) { // the most likely exception here is ERROR_BLOB_ALREADY_EXISTS // exception is also thrown if the drive already exists }
string driveLetter = cloudDrive.Mount(25, DriveMountOptions.Force);
LocalResource localCache = RoleEnvironment.GetLocalResource("RavenCache"); CloudDrive.InitializeCache(localCache.RootPath, localCache.MaximumSizeInMegabytes);
حال که پورت هم تنظیم شده است میتوانیم RavenDB را در Worker Role راه بیاندازیم:
var config = new RavenConfiguration { DataDirectory = driveLetter, AnonymousUserAccessMode = AnonymousUserAccessMode.All, HttpCompression = true, DefaultStorageTypeName = "munin", Port = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["Raven"].IPEndpoint.Port, PluginsDirectory = "plugins" }; try { documentDatabase = new DocumentDatabase(config); documentDatabase.SpinBackgroundWorkers(); httpServer = new HttpServer(config, documentDatabase); try { httpServer.StartListening(); } catch (Exception ex) { Trace.WriteLine("StartRaven Error: " + ex.ToString(), "Error"); if (httpServer != null) { httpServer.Dispose(); httpServer = null; } } } catch (Exception ex) { Trace.WriteLine("StartRaven Error: " + ex.ToString(), "Error"); if (documentDatabase != null) { documentDatabase.Dispose(); documentDatabase = null; } }
احراز هویت و اعتبارسنجی کاربران در برنامههای Angular - قسمت چهارم - به روز رسانی خودکار توکنها
ایجاد یک تایمر برای مدیریت دریافت و به روز رسانی توکن دسترسی
در مطلب «ایجاد تایمرها در برنامههای Angular» با روش کار با تایمرهای reactive آشنا شدیم. در اینجا قصد داریم از این امکانات جهت پیاده سازی به روز کنندهی خودکار access_token استفاده کنیم. در مطلب «احراز هویت و اعتبارسنجی کاربران در برنامههای Angular - قسمت دوم - سرویس اعتبارسنجی»، زمان انقضای توکن را به کمک کتابخانهی jwt-decode، از آن استخراج کردیم:
getAccessTokenExpirationDateUtc(): Date { const decoded = this.getDecodedAccessToken(); if (decoded.exp === undefined) { return null; } const date = new Date(0); // The 0 sets the date to the epoch date.setUTCSeconds(decoded.exp); return date; }
private refreshTokenSubscription: Subscription; scheduleRefreshToken() { if (!this.isLoggedIn()) { return; } this.unscheduleRefreshToken(); const expiresAtUtc = this.getAccessTokenExpirationDateUtc().valueOf(); const nowUtc = new Date().valueOf(); const initialDelay = Math.max(1, expiresAtUtc - nowUtc); console.log("Initial scheduleRefreshToken Delay(ms)", initialDelay); const timerSource$ = Observable.timer(initialDelay); this.refreshTokenSubscription = timerSource$.subscribe(() => { this.refreshToken(); }); } unscheduleRefreshToken() { if (this.refreshTokenSubscription) { this.refreshTokenSubscription.unsubscribe(); } }
- در ابتدا بررسی میکنیم که آیا کاربر لاگین کردهاست یا خیر؟ آیا اصلا دارای توکنی هست یا خیر؟ اگر خیر، نیازی به شروع این تایمر نیست.
- سپس تایمر قبلی را در صورت وجود، خاتمه میدهیم.
- در مرحلهی بعد، کار محاسبهی میزان زمان تاخیر شروع تایمر Observable.timer را انجام میدهیم. پیشتر زمان انقضای توکن موجود را استخراج کردهایم. اکنون این زمان را از زمان جاری سیستم برحسب UTC کسر میکنیم. مقدار حاصل، initialDelay این تایمر خواهد بود. یعنی این تایمر به مدت initialDelay صبر خواهد کرد و سپس تنها یکبار اجرا میشود. پس از اجرا، ابتدا متد refreshToken ذیل را فراخوانی میکند تا توکن جدیدی را دریافت کند.
در متد unscheduleRefreshToken کار لغو تایمر جاری در صورت وجود انجام میشود.
متد درخواست یک توکن جدید بر اساس refreshToken موجود نیز به صورت ذیل است:
refreshToken() { const headers = new HttpHeaders({ "Content-Type": "application/json" }); const model = { refreshToken: this.getRawAuthToken(AuthTokenType.RefreshToken) }; return this.http .post(`${this.appConfig.apiEndpoint}/${this.appConfig.refreshTokenPath}`, model, { headers: headers }) .finally(() => { this.scheduleRefreshToken(); }) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { console.log("RefreshToken Result", result); this.setLoginSession(result); }); }
در آخر چون این تایمر، خود متوقف شوندهاست (متد Observable.timer بدون پارامتر دوم آن فراخوانی شدهاست)، یکبار دیگر کار زمانبندی دریافت توکن جدید بعدی را در متد finally انجام میدهیم (متد scheduleRefreshToken را مجددا فراخوانی میکنیم).
تغییرات مورد نیاز در سرویس Auth جهت زمانبندی دریافت توکن دسترسی جدید
تا اینجا متدهای مورد نیاز شروع زمانبندی دریافت توکن جدید، خاتمهی زمانبندی و دریافت و به روز رسانی توکن جدید را پیاده سازی کردیم. محل قرارگیری و فراخوانی این متدها در سرویس Auth، به صورت ذیل هستند:
الف) در سازندهی کلاس:
constructor( @Inject(APP_CONFIG) private appConfig: IAppConfig, private browserStorageService: BrowserStorageService, private http: HttpClient, private router: Router ) { this.updateStatusOnPageRefresh(); this.scheduleRefreshToken(); }
ب) پس از لاگین موفقیت آمیز
در متد لاگین، پس از دریافت یک response موفقیت آمیز و تنظیم و ذخیره سازی توکنهای دریافتی، کار زمانبندی دریافت توکن دسترسی بعدی بر اساس refresh_token فعلی انجام میشود:
this.setLoginSession(response); this.scheduleRefreshToken();
ج) پس از خروج از سیستم
در متد logout، پس از حذف توکنهای کاربر از کش مرورگر، کار لغو تایمر زمانبندی دریافت توکن بعدی نیز صورت خواهد گرفت:
this.deleteAuthTokens(); this.unscheduleRefreshToken();
در این حالت اگر برنامه را اجرا کنید، یک چنین خروجی را که بیانگر دریافت خودکار توکنهای جدید است، پس از مدتی در کنسول developer مرورگر مشاهده خواهید کرد:
ابتدا متد scheduleRefreshToken اجرا شده و تاخیر آغازین تایمر محاسبه شدهاست. پس از مدتی متد refreshToken توسط تایمر فراخوانی شدهاست. در آخر مجددا متد scheduleRefreshToken جهت شروع یک زمانبندی جدید، اجرا شدهاست.
اعداد initialDelay محاسبه شدهای را هم که ملاحظه میکنید، نزدیک به همان 2 دقیقهی تنظیمات سمت سرور در فایل appsettings.json هستند:
"BearerTokens": { "Key": "This is my shared key, not so secret, secret!", "Issuer": "http://localhost/", "Audience": "Any", "AccessTokenExpirationMinutes": 2, "RefreshTokenExpirationMinutes": 60 }
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
Interceptor چیست؟
از زمان ارائهی NET 8 preview 6 SDK. به بعد، امکان رهگیری هر متدی از کدهای برنامه، به داتنت اضافه شدهاست؛ به همین جهت از واژهی Interceptor/رهگیر در اینجا استفاده میشود. خود تیم داتنت از این قابلیت در جهت بازنویسی پویای قسمتهایی از کدهای زیرساخت داتنت که از Reflection استفاده میکنند، با نگارشهای کامپایل شدهی مختص به برنامهی شما، کمک میگیرند. به این ترتیب سرعت و کارآیی برنامههای داتنت 8، بهبود قابل ملاحظهای را پیدا کردهاند. برای مثال ahead-of-time compilation (AOT) در داتنت 8 و ASP.NET Core 8x بر اساس این ویژگی پیاده سازی شدهاست. این ویژگی جدید، مکمل source generators است که در نگارشهای پیشین داتنت ارائه شده بود.
بررسی Interceptors با تهیهی یک مثال ساده
فرض کنید میخواهیم فراخوانی متد GetText زیر را رهگیری کرده و سپس آنرا با نمونهی دیگری جایگزین کنیم:
namespace CS8Tests; public class InterceptorsSample { public string GetText(string text) { return $"{text}, World!"; } }
namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] public sealed class InterceptsLocationAttribute : Attribute { public InterceptsLocationAttribute(string filePath, int line, int character) { } }
سپس فرض کنید فراخوانی متد GetText در فایل Program.cs برنامه به صورت زیر انجام شدهاست:
using CS8Tests; var example = new InterceptorsSample(); var text = example.GetText("Hello"); Console.WriteLine(text); //Hello, World!
در ادامه از این اطلاعات در رهگیر سفارشی زیر استفاده خواهیم کرد:
using System.Runtime.CompilerServices; namespace CS8Tests; public static class MyInterceptor { [InterceptsLocation("C:\\Path\\To\\CS8Tests\\Program.cs", 4, 20)] public static string InterceptorMethod(this InterceptorsSample example, string text) { return $"{text}, DNT!"; } }
اکنون اگر برنامه را اجرا کنیم ... با خطای زیر مواجه میشویم:
error CS9137: The 'interceptors' experimental feature is not enabled in this namespace. Add '<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CS8Tests</InterceptorsPreviewNamespaces>' to your project.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <!--<NoWarn>Test001</NoWarn>--> <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CS8Tests</InterceptorsPreviewNamespaces> </PropertyGroup> </Project>
Hello, DNT!
سؤال: آیا رهگیری انجام شده، در زمان کامپایل انجام میشود یا در زمان اجرا؟
برای این مورد میتوان به Low-Level C# code تولیدی مراجعه کرد. برای مشاهدهی یک چنین کدهایی میتوانید از منوی Tools->IL Viewer برنامهی Rider استفاده کرده و در برگهی ظاهر شده، گزینهی Low-Level C# آنرا انتخاب نمائید:
using CS8Tests; using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal class Program { private static void <Main>$(string[] args) { Console.WriteLine(new InterceptorsSample().InterceptorMethod("Hello")); } public Program() { base..ctor(); } }
سؤال: آیا این قابلیت واقعا کاربردی است؟!
اکنون شاید این سؤال مطرح شود که ... واقعا چه کسی قرار است مسیر کامل یک فایل، شماره سطر و شماره ستون فراخوانی متدی را به اینگونه در اختیار سیستم رهگیری قرار دهد؟! آیا واقعا این قابلیت، یک قابلیت کاربردی و مناسب است؟!
اینجا است که اهمیت source generators مشخص میشود. توسط source generators دسترسی کاملی به syntax trees وجود دارد و همچنین یکسری اطلاعات تکمیلی مانند FilePath و سپس CSharpSyntaxNodeها که دسترسی به دادههای متد ()GetLocation را دارند که مکان دقیق سطر و ستونهای فراخوانیها را مشخص میکند.
کاربردهای فعلی رهگیرها در دات نت 8
در دات نت 8، این موارد با استفاده از رهگیرها بهینه سازی شده و سرعت آنها افزایش یافتهاند:
- فراخوانیهایی که تمام اطلاعات آنها در زمان کامپایل فراهم است، مانند Regex.IsMatch(@"a+b+") که از یک الگوی ثابت و مشخص استفاده میکند، رهگیری شده و پیاده سازی آن با کدی استاتیک، جایگزین میشود.
- در ASP.NET Minimal API، استفاده از lambda expressions جهت ارائهی تعاریفی مانند:
app.MapGet("/products", handler: (int? page, int? pageLength, MyDb db) => { ... })
- بهبود کارآیی foreach loops جهت استفاده از ریاضیات برداری و SIMD در صورت امکان.
- بهبود کارآیی تزریق وابستگیها، زمانیکه به تعاریف مشخصی مانند ()<provider.Register<MyService ختم میشود.
- بجای استفاده از expression trees در زمان اجرای برنامه، اکنون میتوان کدهای SQL معادل را در زمان کامپایل برنامه تولید کرد.
- بهبود کارآیی Serializers، زمانیکه از یک نوع مشخص مانند ()<Serialize<MyType استفاده میشود و کامپایلر میتواند آنرا با کدهای زمان کامپایل، جایگزین کند.
محدودیتهای رهگیرها در داتنت 8
- رهگیرهای داتنت 8 فقط با متدها کار میکنند.
- مسیر ارائه شده حتما باید یک مسیر کامل و مشخص باشد. یعنی اگر این قطعه کد، به سیستم دیگری منتقل شود، کامپایل نخواهد شد و امکان ارائهی مسیرهای نسبی وجود ندارد.
- امضای متدها، حتما باید یکی باشد. یعنی نمیتوان یک رهگیر جنریک را تعریف کرد.
{ "name": "dntwebpack", "version": "1.0.0", "description": "a webpack tutorial", "main": "main.js", "scripts": { }, "author": "mehdi", "license": "MIT" }
<html> <!-- index.html --> <head> first part of webpack tut! </head> <body> <h1>webpack is awesome !</h1> <script src="bundle.js"></script> </body> </html>
//main.js //start of the journey with webpack console.log(`i'm bundled by webpack`);
فایل bundle.js را تولید میکنیم.
{ "name": "dntwebpack", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" ,"webpack":"webpack" }, "author": "mehdi", "license": "ISC", "devDependencies": { "webpack": "^1.13.1" } }
public abstract class myabstractclass { public abstract string dosomething( string input ); public double round( double number , int decimals ) { return math.round( number , decimals ); } }
روش اول:
در این روش ابتدا باید کلاسی نوشت تا کلاس abstract بالا رو پیاده سازی کنه:
public class mynewclass : myabstractclass { public override string dosomething( string input ) { return input; } }
[testclass] public class mytest { [testmethod] public void testround() { mynewclass mynewclass = new mynewclass(); var result = mynewclass.round( 5.55 , 1 ); assert.areequal( 5.6 , result ); } }
البته روش بالا خیلی مورد پسند من نیست.
در روش دوم که من خیلی بیشتر بهش علاقه دارم دیگه نیازی به استفاده از یک کلاس دوم برای پیاده سازی کلاس abstract نیست. بلکه در این روش از ابزار rhinomocks برای این کار استفاده میکنیم . استفاده از rhino mocks به چندین روش امکان پذیره که امروز 2 روش اونو براتون توضیح میدم:
در روش اول از mockrepository استفاده میکنیم و در روش دوم از روش aaa یا arrange-act-assert
استفاده از mockrepository :
ابتدا کدهای مربوطه رو مینویسم:
[testmethod] public void testwithmockrepository() { var mockrepository = new rhino.mocks.mockrepository(); var mock = mockrepository.partialmock<myabstractclass>(); using ( mockrepository.record() ) { expect.call( mock.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once(); } using ( mockrepository.playback() ) { assert.areequal( "hi..." , mock.dosomething( "salam" ) ); } }
در مورد expect.call باید بگم که از این کلاس برای شبیه سازی رفتار یک متد استفاده میشه (مثلا در مواقعی که یک متد برای انجام عملیات باید به دیتا بیس وصل شده و یک query را اجرا کنه) برای اینکه در تست، از این عملیات صرف نظر بشه از mockها استفاده کرده و رفتار متد رو به روش بالا شبیه سازی میکنیم. البته کار کردن با rhino mocks به صورت بالا دیگه از مد افتاده و جدیدا از روش aaa استفاده میشه که اونو در پایین توضیح میدم:
[testmethod] public void testwithaaa() { var mock = mockrepository.generatepartialmock<myabstractclass>(); mock.expect( x => x.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();//arange var result = mock.dosomething( "salam" );//act assert.areequal( "hi..." , result );//assert }
[testmethod] public void testwithaaa() { var mock = mockrepository.generatepartialmock<myabstractclass>(); mock.expect( x => x.dosomething( arg<string>.is.anything ) ).return( "hi..." ).repeat.once();//arange var result = mock.dosomething( "salam" );//act var result2 = mock.dosomething( "bye" );//act assert.areequal( "hi..." , result );//assert }
خطای آن هم واضح داره میگه که expected#1 هستش در حالی که actual#2 (تعداد دفعات حقیقی از دفعات مورد انتظار بیشتره)
توی پستهای بعدی (اگه وقت بشه) حتما در مورد rhino mocks بیشتر توضیح میدم
نوشتن یک Tag Helper سفارشی، برای رندر کردن لیستهای بوت استرپی
فرض کنید میخواهیم یک tag helper جدید را جهت رندر کردن لیست بوت استرپی ذیل تهیه کنیم:
<ul class="list-group"> <li class="list-group-item">Item 1</li> <li class="list-group-item">Item 2</li> <li class="list-group-item">Item 3</li> </ul>
{ "version": "1.0.0-*", "dependencies": { "NETStandard.Library": "1.6.0", "Microsoft.AspNetCore.Http.Extensions": "1.0.0", "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.1", "Microsoft.AspNetCore.Mvc.Core": "1.0.1", "Microsoft.AspNetCore.Mvc.ViewFeatures": "1.0.1", "Microsoft.AspNetCore.Razor.Runtime": "1.0.0" }, "frameworks": { "netstandard1.6": { "imports": "dnxcore50" } } }
بررسی آناتومی یک کلاس TagHelper
یک کلاس Tag Helper سفارشی، در حالت کلی میتواند شکل زیر را داشته باشد:
namespace Core1RtmEmptyTest.TagHelpers { [HtmlTargetElement("list-group")] public class ListGroupTagHelper : TagHelper { [HtmlAttributeName("asp-items")] public List<string> Items { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { } } }
توسط HtmlTargetElement نام نهایی تگ مرتبط با TagHelper سفارشی را تعریف و سفارشی سازی کردهایم. در این حالت این TagHelper جدید در Viewهای برنامه، توسط تگ ذیل شنایی میشود (بجای نام پیش فرض کلاس):
<list-group></list-group>
<list-group asp-items="Model.Items"></list-group>
برای اینکه نام این ویژگی را نیز بتوانیم سفارشی سازی کنیم، میتوان از ویژگی HtmlAttributeName استفاده کرد. در صورت عدم ذکر آن، از نام پیش فرض این خاصیت عمومی جهت تعریف ویژگیهای تگ نهایی استفاده میگردد.
عملیات نهایی افزودن تگهای HTML، به View برنامه، در متد Process انجام میشود. در اینجا توسط متدهایی مانند output.Content.AppendHtml میتوان خروجی دلخواهی را به صفحه اضافه کرد.
تکمیل کدهای Tag Helper سفارشی رندر کردن لیستهای بوت استرپی
پس از آشنایی با ساختار کلی یک کلاس TagHelper، اکنون میتوان کدهای آن را به نحو ذیل تکمیل کرد:
using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Core1RtmEmptyTest.TagHelpers { [HtmlTargetElement("list-group")] public class ListGroupTagHelper : TagHelper { [HtmlAttributeName("asp-items")] public List<string> Items { get; set; } protected HttpRequest Request => ViewContext.HttpContext.Request; [ViewContext, HtmlAttributeNotBound] public ViewContext ViewContext { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (output == null) { throw new ArgumentNullException(nameof(output)); } if (Items == null) { throw new InvalidOperationException($"{nameof(Items)} must be provided"); } output.TagName = "ul"; output.TagMode = TagMode.StartTagAndEndTag; output.Attributes.Add("class", "list-group"); foreach (var item in Items) { TagBuilder itemBuilder = new TagBuilder("li"); itemBuilder.AddCssClass("list-group-item"); itemBuilder.InnerHtml.Append(item); output.Content.AppendHtml(itemBuilder); } } } }
- چون میخواهیم تگ نهایی آن، list-group نام داشته باشد، آنرا توسط ویژگی HtmlTargetElement به صورت صریحی مشخص کردهایم.
- همچنین علاقمندیم تا ویژگی دریافت لیست آیتمها، نامی معادل asp-items داشته باشد. بنابراین آنرا نیز توسط ویژگی HtmlAttributeName، دقیقا مشخص کردهایم.
- در این کلاس، یک خاصیت اضافهی ViewContext را نیز مشاهده میکنید. ویژگی ViewContext اعمالی به آن، سبب خواهد شد تا اطلاعات درخواست جاری، به این خاصیت عمومی، به صورت خودکار تزریق شود. بنابراین اگر نیاز به اطلاعاتی مانند Request جاری دارید، شیء ViewContext.HttpContext.Request، این مقادیر را در اختیار شما قرار میدهد. به علاوه اگر دقت کرده باشید، این خاصیت با ویژگی HtmlAttributeNotBound مزین شدهاست. از این جهت که نمیخواهیم این خاصیت عمومی، در لیست ویژگیهای تگ نهایی TagHelper در حال تهیه، ظاهر شود.
- پس از آن کاری که انجام شده، تکمیل متد Process است. در اینجا توسط output.TagName مشخص میکنیم که TagHelper جاری، در بین تگهای ul قرار گیرد (مفهوم TagMode.StartTagAndEndTag ذکر شده) و همچنین این تگ محصور کننده دارای کلاس list-group بوت استرپ نیز خواهد بود.
- سپس بر روی لیست آیتمهای دریافت شده، یک حلقه را تشکیل داده و به کمک TagBuilder، تگهای li داخل ul برونی را تکمیل میکنیم. این TagBuilder در نهایت توسط متد output.Content.AppendHtml به View برنامه اضافه خواهد شد. در اینجا، متد Append هم وجود دارد. اگر از آن استفاده شود، خروجی نهایی HTML Encoded خواهد بود.
ثبت و استفادهی از TagHelper سفارشی تهیه شده
پس از تعریف TagHelper سفارشی فوق، ابتدا ارجاعی از اسمبلی آنرا به پروژهی جاری اضافه میکنیم و سپس به فایل ViewImports.cshtml_ برنامه مراجعه و یک سطر ذیل را به آن اضافه میکنیم:
@addTagHelper *,Core1RtmEmptyTest.TagHelpers
اکنون که این TagHelper در Viewهای برنامه قابل شناسایی است، روش افزودن آن، بر اساس همان سفارشی سازیهایی است که انجام دادیم:
Accord.NET #2
برای استفاده از Accord.NET میتوان به یکی از دو صورت زیر اقدام کرد :
- دریافت آخرین نسخهی متن باز و یا dllهای پروژهی Accord.NET از طریق گیت هاب
- نصب از طریق NuGet (با توجه به این که در چارچوب Accord.NET کتابخانههای متنوعی وجود دارند و در هر پروژه نیاز به نصب همگی آنها نیست، فضای نامهای مختلف در بستههای مختلف نیوگت قرار گرفتهاند و برای نصب هر کدام میتوانیم یکی از فرمانهای زیر را استفاده کنیم)
PM> Install-Package Accord.MachineLearning
PM> Install-Package Accord.Imaging
PM> Install-Package Accord.Neuro
نکته : روشهای یادگیری به دو دسته کلی با نظارت (Supervised learning) و بدون نظارت (Unsupervised learning) تقسیم بندی میشوند. در روش با نظارت، دادهها دارای برچسب یا label هستند و عملا نوع کلاسها مشخص هستند و اصطلاحا برای طبقه بندی (Classification) استفاده میشوند. در روش بدون نظارت، دادههایمان بدون برچسب هستند و فقط تعداد کلاس ها و نیز یک معیار تفکیک پذیری مشخص است و برای خوشه بندی (Clustering) استفاده میشوند.
عملکرد SVM یا ماشین بردار پشتیبان به صورت خلاصه به این صورت است که با در نظر گرفتن یک خط یا ابرصفحه جدا کننده فرضی، ماشین یا دسته بندی را ایجاد میکند که از نقاط ابتدایی کلاسهای مختلف که بردار پشتیبان یا SV نام دارند، بیشترین فاصله را دارند و در نهایت دادها را به دو کلاس مجزا تقسیم میکند.
در تصویر بالا مقداری خطا مشاهده میشود که با توجه با خطی بودن جداساز مجبور به پذیرش این خطا هستیم.
در نسخههای جدیدتر این الگوریتم یک Kernel ( از نوع خطی Linear ، چند جملهای Polynomial، گوسین Gaussian و یا ...) برای آن در نظر گرفته شد که عملا نگاشتی را بین خط (نه صرفا فقط خطی) را با آن ابرصفحه جداکننده برقرار کند. در نتیجه دسته بندی با خطای کمتری را خواهیم داشت. (اطلاعات بیشتر در + و همچنین مطالب دکتر سعید شیری درباره SVM در +)
یک مثال مفهومی : هدف اصلی در این مثال شبیه سازی تابع XNOR به Kernel SVM میباشد.
برای شروع کار از فضای نام MachineLearning استفاده میکنیم و بستهی نیوگت مربوطه را فرخوانی میکنیم. پس از اجرا، مشاهده میکنیم که فضای نامهای Accord.Math و Accord.Statistics نیز به پروژه اضافه میشود.
در ابتدا مقادیر ورودی و برچسبها را تعریف میکنیم
// ورودی double[][] inputs = { new double[] { 0, 0 }, // 0 xnor 0: 1 (label +1) new double[] { 0, 1 }, // 0 xnor 1: 0 (label -1) new double[] { 1, 0 }, // 1 xnor 0: 0 (label -1) new double[] { 1, 1 } // 1 xnor 1: 1 (label +1) }; // خروجی دسته بند ماشین بردار پشتیبان باید -1 یا +1 باشد int[] labels = { // 1, 0, 0, 1 1, -1, -1, 1 };
// ساخت کرنل IKernel kernel = createKernel(); // ساخت دسته بند به کمک کرنل انتخابی و تنظیم تعداد ویژگیها ورودیها به مقدار 2 KernelSupportVectorMachine machine = new KernelSupportVectorMachine(kernel, 2);
private static IKernel createKernel() { //var numPolyConstant = 1; //return new Linear(numPolyConstant); //var numDegree = 2; //var numPolyConstant = 1; //return new Polynomial(numDegree, numPolyConstant); //var numLaplacianSigma = 1000; //return new Laplacian(numLaplacianSigma); //var numSigAlpha = 7; //var numSigB = 6; //return new Sigmoid(numSigAlpha, numSigB); var numSigma = 0.1; return new Gaussian(numSigma); }
// معرفی دسته بندمان به الگوریتم یادگیری SMO SequentialMinimalOptimization teacher_smo = new SequentialMinimalOptimization(machine_svm, inputs, labels); // اجرای الگوریتم یادگیری double error = teacher_smo.Run(); Console.WriteLine(string.Format("error rate : {0}", error));
// بررسی یکی از ورودیها var sample = inputs[0]; int decision = System.Math.Sign(machine_svm.Compute(sample)); Console.WriteLine(string.Format("result for sample '0 xnor 0' is : {0}", decision));
دریافت کد
قبلا در همین وبسایت در مورد یکسانسازی حروف "ی" و "ک" مطلبی بیان شده است. تمرکز آن مطلب بر روی اعمال تغییرات، قبل از ذخیره در دیتابیس با استفاده از EF است. به عبارتی، متنهایی که توسط مدیر سایت یا هر کاربر دیگری برای ذخیره شدن در دیتابیس وارد شده است. اما در صورتی که کاربری در جستجوی خود از حروف "ی" و "ک" عربی استفاده کند چه میشود؟ در اینجا میخواهیم روشی را پیادهسازی کنیم که عمومی بوده و بتواند هر دوی این حالات را پوشش دهد.
مسئله این است که بایستی تمام ورودیهای کاربران سایت که از نوع رشته هستند چک و در صورت نیاز اصلاح شوند. بهترین روشی که به ذهن من میرسد این است که در فرایند Model Binding این عمل انجام بگیرد. با تعریف یک Model Binder برای نوع رشته میتوانیم عمل یکدستسازی را به صورت عمومی در تمام وبسایت اعمال کنیم.
در MVC یک Model Binder پیشفرض داریم به نام DefaultModelBinder . ما هم از همین کلاس استفاده میکنیم تا تمام کارها را برای ما انجام دهد. تنها بایستی در متد BindModel آن کد موردنظر خود را اضافه کنیم و سپس اجازه دهیم بقیه فرایند به شکل عادی ادامه پیدا کند.
کلاسی به نام StringModelBinder اضافه کرده و کلاس DefaultModelBinder را به عنوان کلاس پایه آن تعریف میکنیم. سپس متد BindModel آنرا override کرده، کد مربوط به یکدستسازی را اضافه میکنیم :
public class StringModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { object value = base.BindModel(controllerContext, bindingContext); if (value == null) { return value; } return value.ToString().Replace((char)1610, (char)1740).Replace((char)1603, (char)1705); } }
ابتدا مقدار به دست آمده توسط متد پیشفرض را میگیریم. سپس در صورت نیاز یکسانسازی را انجام میدهیم.
برای فعال کردن این Model Binder بایستی آنرا در رویداد Application_start برنامه، برای نوع رشته به ModelBinderهای برنامه اضافه کنیم :
ModelBinders.Binders.Add(typeof(string), new StringModelBinder());
روش دیگری که این روزها در اکثر فریمهای دات نتی مرسوم شده است، استفاده از Data Annotations جهت انتساب یک سری متادیتا به خاصیتهای تعریف شده کلاسها است. برای مثال ASP.NET MVC از این قابلیت زیاد استفاده میکند (در تولید پویای کد، یا اعتبار سنجیهای سمت سرور و کاربر).
به همین جهت برای سازگاری بیشتر PdfReport با مدلهای اینگونه فریم ورکها، اکثر ویژگیها و Data Annotations متداول را نیز میتوان در PdfReport بکار برد. همچنین تعدادی ویژگی سفارشی نیز تعریف شده است، که در ادامه به بررسی آنها خواهیم پرداخت.
آشنایی با مدلهای بکار رفته در مثال جاری:
using System.ComponentModel; namespace PdfReportSamples.Models { public enum JobTitle { [Description("Grunt")] Grunt, [Description("Programmer")] Programmer, [Description("Analyst Programmer")] AnalystProgrammer, [Description("Project Manager")] ProjectManager, [Description("Chief Information Officer")] ChiefInformationOfficer, } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using PdfReportSamples.Models; using PdfRpt.Aggregates.Numbers; using PdfRpt.ColumnsItemsTemplates; using PdfRpt.Core.Contracts; using PdfRpt.Core.Helper; using PdfRpt.DataAnnotations; namespace PdfReportSamples.DataAnnotations { public class Person { [IsVisible(false)] public int Id { get; set; } [DisplayName("User name")] //Note: If you don't specify the ColumnItemsTemplate, a new TextBlockField() will be used automatically. [ColumnItemsTemplate(typeof(TextBlockField))] public string Name { get; set; } [DisplayName("Job title")] public JobTitle JobTitle { set; get; } [DisplayName("Date of birth")] [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")] public DateTime DateOfBirth { get; set; } [DisplayName("Date of death")] [DisplayFormat(NullDisplayText = "-", DataFormatString = "{0:MM/dd/yyyy}")] public DateTime? DateOfDeath { get; set; } [DisplayFormat(DataFormatString = "{0:n0}")] [CustomAggregateFunction(typeof(Sum))] public int Salary { get; set; } [IsCalculatedField(true)] [DisplayName("Calculated Field")] [DisplayFormat(DataFormatString = "{0:n0}")] [AggregateFunction(AggregateFunction.Sum)] public string CalculatedField { get; set; } [CalculatedFieldFormula("CalculatedField")] public static Func<IList<CellData>, object> CalculatedFieldFormula = list => { if (list == null) return string.Empty; var salary = (int)list.GetValueOf<Person>(x => x.Salary); return salary * 0.8; };//Note: It's a static field, not a property. } }
- اگر قصد ندارید خاصیتی در این بین، در گزارشات ظاهر شود، از ویژگی IsVisible با مقدار false استفاده کنید.
- از ویژگی DisplayName جهت تعیین برچسبهای سرستونها استفاده خواهد شد.
- ذکر ویژگی ColumnItemsTemplate اختیاری است و اگر عنوان نشود به صورت خودکار از TextBlockField استفاده خواهد شد. اما اگر نیاز به استفاده از قالبهای ستونهای سفارشی و یا حتی قالبهای پیش فرض دیگری که متنی نیستند، وجود دارد، میتوانید از ویژگی ColumnItemsTemplate به همراه نوع کلاس مورد نظر استفاده نمائید. کلاسهای پیش فرض قالبهای ستونها در PdfReport در پوشه Lib\ColumnsItemsTemplates سورس آن قرار دارند.
- برای تعیین نحوه فرمت اطلاعات در اینجا میتوان از ویژگی DisplayFormat استفاده کرد. این ویژگی در اسمبلی System.ComponentModel.DataAnnotations.dll دات نت تعریف شده است؛ که در اینجا نمونهای از استفاده از آنرا برای تعیین نحوه نمایش تاریخ، ملاحظه میکنید. توسط این ویژگی حتی میتوان مشخص ساخت (توسط پارامتر NullDisplayText) که اگر اطلاعاتی null بود، بجای آن چه عبارتی نمایش داده شود.
- اگر علاقمند به اعمال تابعی تجمعی به ستونی خاص هستید، از ویژگی CustomAggregateFunction استفاده کنید. پارامتر آن نوع کلاس تابع مورد نظر است. یک سری تابع تجمعی پیش فرض در فضای نام PdfRpt.Aggregates.Numbers قرار دارند. البته امکان تهیه انواع سفارشی آنها نیز پیش بینی شده است که در قسمتهای بعد به آن خواهیم پرداخت.
- امکان تعریف خواص محاسباتی نیز پیش بینی شده است. برای این منظور دو کار را باید انجام داد:
الف) ویژگی IsCalculatedField را با مقدار true بر روی خاصیت مورد نظر اعمال کنید.
ب) هم نام خاصیت محاسباتی افزوده شده به کلاس جاری، ویژگی CalculatedFieldFormula را بر روی یک فیلد استاتیک عمومی در آن کلاس به نحوی که ملاحظه میکنید (مطابق امضای فیلد CalculatedFieldFormula فوق)، تعریف نمائید. (علت این است که نمیتوان توسط ویژگیها از delegates استفاده کرد و این محدودیت ذاتی وجود دارد)
در ادامه کدهای منبع داده فرضی مثال جاری ذکر شده است:
using System; using System.Collections.Generic; using PdfReportSamples.Models; namespace PdfReportSamples.DataAnnotations { public static class PersonnelDataSource { public static IList<Person> CreatePersonnelList() { return new List<Person> { new Person { Id = 1, Name = "Edward", DateOfBirth = new DateTime(1900, 1, 1), DateOfDeath = new DateTime(1990, 10, 15), JobTitle = JobTitle.ChiefInformationOfficer, Salary = 5000 }, new Person { Id = 2, Name = "Margaret", DateOfBirth = new DateTime(1950, 2, 9), DateOfDeath = null, JobTitle = JobTitle.AnalystProgrammer, Salary = 4000 }, new Person { Id = 3, Name = "Grant", DateOfBirth = new DateTime(1975, 6, 13), DateOfDeath = null, JobTitle = JobTitle.Programmer, Salary = 3500 } }; } } }
using System; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.DataAnnotations { public class DataAnnotationsPdfReport { public IPdfReportData CreatePdfReport() { return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.LeftToRight); doc.Orientation(PageOrientation.Portrait); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" }); }) .DefaultFonts(fonts => { fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("new rpt."); defaultHeader.RunDirection(PdfRunDirection.LeftToRight); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.ClassicTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.FitToContent); }) .MainTableDataSource(dataSource => { dataSource.StronglyTypedList(PersonnelDataSource.CreatePersonnelList()); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); }) .MainTableSummarySettings(summary => { summary.OverallSummarySettings("Total"); summary.PageSummarySettings("Page Summary"); summary.PreviousPageSummarySettings("Pervious Page Summary"); }) .MainTableAdHocColumnsConventions(adHocColumns => { adHocColumns.ShowRowNumberColumn(true); adHocColumns.RowNumberColumnCaption("#"); }) .Export(export => { export.ToExcel(); export.ToXml(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\DataAnnotationsSampleRpt.pdf")); } } }