تبدیل عدد به حروف
به طور قطع توابع و کلاسهای تبدیل عدد به حروف، در جعبه ابزار توابع کمکی شما هم پیدا میشوند. روز قبل سعی کردم جهت آزمایش، عدد 3000,000,000,000,000 ریال را با کلاسی که دارم تست کنم و نتیجه overflow یا اصطلاحا ترکیدن سیستم بود! البته اگر مطالب این سایت را دنبال کرده باشید پیشتر در همین راستا مطلبی در مورد نحوهی صحیح بکارگیری توابع تجمعی SQL در این سایت منتشر شده است و جزو الزامات هر سیستمی است (تفاوتی هم نمیکند که به چه زبانی تهیه شده باشد). اگر آنرا رعایت نکردهاید، سیستم شما «روزی» دچار overflow خواهد شد.
در کل این کلاس تبدیل عدد به حروف را به صورت ذیل اصلاح کردم و همچنین دو زبانه است؛ چیزی که کمتر در پیاده سازیهای عمومی به آن توجه شده است:
using System.Collections.Generic;
using System.Linq;
namespace NumberToWordsLib
{
/// <summary>
/// Number to word languages
/// </summary>
public enum Language
{
/// <summary>
/// English Language
/// </summary>
English,
/// <summary>
/// Persian Language
/// </summary>
Persian
}
/// <summary>
/// Digit's groups
/// </summary>
public enum DigitGroup
{
/// <summary>
/// Ones group
/// </summary>
Ones,
/// <summary>
/// Teens group
/// </summary>
Teens,
/// <summary>
/// Tens group
/// </summary>
Tens,
/// <summary>
/// Hundreds group
/// </summary>
Hundreds,
/// <summary>
/// Thousands group
/// </summary>
Thousands
}
/// <summary>
/// Equivalent names of a group
/// </summary>
public class NumberWord
{
/// <summary>
/// Digit's group
/// </summary>
public DigitGroup Group { set; get; }
/// <summary>
/// Number to word language
/// </summary>
public Language Language { set; get; }
/// <summary>
/// Equivalent names
/// </summary>
public IList<string> Names { set; get; }
}
/// <summary>
/// Convert a number into words
/// </summary>
public static class HumanReadableInteger
{
#region Fields (4)
private static readonly IDictionary<Language, string> And = new Dictionary<Language, string>
{
{ Language.English, " " },
{ Language.Persian, " و " }
};
private static readonly IList<NumberWord> NumberWords = new List<NumberWord>
{
new NumberWord { Group= DigitGroup.Ones, Language= Language.English, Names=
new List<string> { string.Empty, "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }},
new NumberWord { Group= DigitGroup.Ones, Language= Language.Persian, Names=
new List<string> { string.Empty, "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" }},
new NumberWord { Group= DigitGroup.Teens, Language= Language.English, Names=
new List<string> { "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" }},
new NumberWord { Group= DigitGroup.Teens, Language= Language.Persian, Names=
new List<string> { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" }},
new NumberWord { Group= DigitGroup.Tens, Language= Language.English, Names=
new List<string> { "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }},
new NumberWord { Group= DigitGroup.Tens, Language= Language.Persian, Names=
new List<string> { "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" }},
new NumberWord { Group= DigitGroup.Hundreds, Language= Language.English, Names=
new List<string> {string.Empty, "One Hundred", "Two Hundred", "Three Hundred", "Four Hundred",
"Five Hundred", "Six Hundred", "Seven Hundred", "Eight Hundred", "Nine Hundred" }},
new NumberWord { Group= DigitGroup.Hundreds, Language= Language.Persian, Names=
new List<string> {string.Empty, "یکصد", "دویست", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد" , "نهصد" }},
new NumberWord { Group= DigitGroup.Thousands, Language= Language.English, Names=
new List<string> { string.Empty, " Thousand", " Million", " Billion"," Trillion", " Quadrillion", " Quintillion", " Sextillian",
" Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
" Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
" Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
" Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
new NumberWord { Group= DigitGroup.Thousands, Language= Language.Persian, Names=
new List<string> { string.Empty, " هزار", " میلیون", " میلیارد"," تریلیون", " Quadrillion", " Quintillion", " Sextillian",
" Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
" Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
" Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
" Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
};
private static readonly IDictionary<Language, string> Negative = new Dictionary<Language, string>
{
{ Language.English, "Negative " },
{ Language.Persian, "منهای " }
};
private static readonly IDictionary<Language, string> Zero = new Dictionary<Language, string>
{
{ Language.English, "Zero" },
{ Language.Persian, "صفر" }
};
#endregion Fields
#region Methods (7)
// Public Methods (5)
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this int number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this uint number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this byte number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this decimal number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this double number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this long number, Language language)
{
if (number == 0)
{
return Zero[language];
}
if (number < 0)
{
return Negative[language] + NumberToText(-number, language);
}
return wordify(number, language, string.Empty, 0);
}
// Private Methods (2)
private static string getName(int idx, Language language, DigitGroup group)
{
return NumberWords.Where(x => x.Group == group && x.Language == language).First().Names[idx];
}
private static string wordify(long number, Language language, string leftDigitsText, int thousands)
{
if (number == 0)
{
return leftDigitsText;
}
var wordValue = leftDigitsText;
if (wordValue.Length > 0)
{
wordValue += And[language];
}
if (number < 10)
{
wordValue += getName((int)number, language, DigitGroup.Ones);
}
else if (number < 20)
{
wordValue += getName((int)(number - 10), language, DigitGroup.Teens);
}
else if (number < 100)
{
wordValue += wordify(number % 10, language, getName((int)(number / 10 - 2), language, DigitGroup.Tens), 0);
}
else if (number < 1000)
{
wordValue += wordify(number % 100, language, getName((int)(number / 100), language, DigitGroup.Hundreds), 0);
}
else
{
wordValue += wordify(number % 1000, language, wordify(number / 1000, language, string.Empty, thousands + 1), 0);
}
if (number % 1000 == 0) return wordValue;
return wordValue + getName(thousands, language, DigitGroup.Thousands);
}
#endregion Methods
}
}
دریافت پروژه کامل به همراه Unit tests مرتبط
کتابچهی طراحی سیستم
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
تنظیم مجوز امضای توکنهای IDP
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddCustomUserStore() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients());
مرحلهی بعد، تغییر AddDeveloperSigningCredential به یک نمونهی واقعی است. استفادهی از روش فعلی آن چنین مشکلاتی را ایجاد میکند:
- اگر برنامهی IDP را در سرورهای مختلفی توزیع کنیم و این سرورها توسط یک Load balancer مدیریت شوند، هر درخواست رسیده، به سروری متفاوت هدایت خواهد شد. در این حالت هر برنامه نیز مجوز امضای توکن متفاوتی را پیدا میکند. برای مثال اگر یک توکن دسترسی توسط سرور A امضاء شود، اما در درخواست بعدی رسیده، توسط مجوز سرور B تعیین اعتبار شود، این اعتبارسنجی با شکست مواجه خواهد شد.
- حتی اگر از یک Load balancer استفاده نکنیم، به طور قطع Application pool برنامه در سرور، در زمانی خاص Recycle خواهد شد. این مورد DeveloperSigningCredential تنظیم شده را نیز ریست میکند. یعنی با ریاستارت شدن Application pool، کلیدهای مجوز امضای توکنها تغییر میکنند که در نهایت سبب شکست اعتبارسنجی توکنهای صادر شدهی توسط IDP میشوند.
بنابراین برای انتشار نهایی برنامه نمیتوان از DeveloperSigningCredential فعلی استفاده کرد و نیاز است یک signing certificate را تولید و تنظیم کنیم. برای این منظور از برنامهی makecert.exe مایکروسافت که جزئی از SDK ویندوز است، استفاده میکنیم. این فایل را از پوشهی src\IDP\DNT.IDP\MakeCert نیز میتوانید دریافت کنید.
سپس دستور زیر را با دسترسی admin اجرا کنید:
makecert.exe -r -pe -n "CN=DntIdpSigningCert" -b 01/01/2018 -e 01/01/2025 -eku 1.3.6.1.5.5.7.3.3 -sky signature -a sha256 -len 2048 -ss my -sr LocalMachine
پس از آن در قسمت run ویندوز، دستور mmc را وارد کرده و enter کنید. سپس از منوی File گزینهی Add remove span-in را انتخاب کنید. در اینجا certificate را add کنید. در صفحهی باز شده Computer Account و سپس Local Computer را انتخاب کنید و در نهایت OK. اکنون میتوانید این مجوز جدید را در قسمت «Personal/Certificates»، مشاهده کنید:
در اینجا Thumbprint این مجوز را در حافظه کپی کنید؛ از این جهت که در ادامه از آن استفاده خواهیم کرد.
چون این مجوز از نوع self signed است، در قسمت Trusted Root Certification Authorities قرار نگرفتهاست که باید این انتقال را انجام داد. در غیراینصورت میتوان توسط آن توکنهای صادر شده را امضاء کرد اما به عنوان یک توکن معتبر به نظر نخواهند رسید.
در ادامه این مجوز جدید را انتخاب کرده و بر روی آن کلیک راست کنید. سپس گزینهی All tasks -> export را انتخاب کنید. نکتهی مهمی را که در اینجا باید رعایت کنید، انتخاب گزینهی «yes, export the private key» است. کپی و paste این مجوز از اینجا به جایی دیگر، این private key را export نمیکند. در پایان این عملیات، یک فایل pfx را خواهید داشت.
- در آخر نیاز است این فایل pfx را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید. برای اینکار بر روی نود certificate آن کلیک راست کرده و گزینهی All tasks -> import را انتخاب کنید. سپس مسیر فایل pfx خود را داده و این مجوز را import نمائید.
پس از ایجاد مجوز امضای توکنها و انتقال آن به Trusted Root Certification Authorities، نحوهی معرفی آن به IDP به صورت زیر است:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddSigningCredential(loadCertificateFromStore()) .AddCustomUserStore() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()); } private X509Certificate2 loadCertificateFromStore() { var thumbPrint = Configuration["CertificateThumbPrint"]; using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine)) { store.Open(OpenFlags.ReadOnly); var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true); if (certCollection.Count == 0) { throw new Exception("The specified certificate wasn't found."); } return certCollection[0]; } } private X509Certificate2 loadCertificateFromFile() { // NOTE: // You should check out the identity of your application pool and make sure // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work. var certificate = new X509Certificate2( fileName: Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data", Configuration["X509Certificate:FileName"]), password: Configuration["X509Certificate:Password"], keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); return certificate; } } }
پس از این تغییرات، برنامه را اجرا کنید. سپس مسیر discovery document را طی کرده و آدرس jwks_uri آنرا در مرورگر باز کنید. در اینجا خاصیت kid نمایش داده شده با thumbPrint مجوز یکی است.
https://localhost:6001/.well-known/openid-configuration https://localhost:6001/.well-known/openid-configuration/jwks
انتقال سایر قسمتهای فایل Config برنامهی IDP به بانک اطلاعاتی
قسمت آخر آماده سازی برنامه برای انتشار آن، انتقال سایر دادههای فایل Config، مانند Resources و Clients برنامهی IDP، به بانک اطلاعاتی است. البته هیچ الزامی هم به انجام اینکار نیست. چون اگر تعداد برنامههای متفاوتی که در سازمان قرار است از IDP استفاده کنند، کم است، تعریف مستقیم آنها داخل فایل Config برنامهی IDP، مشکلی را ایجاد نمیکند و این تعداد رکورد الزاما نیازی به بانک اطلاعاتی ندارند. اما اگر بخواهیم امکان به روز رسانی این اطلاعات را بدون نیاز به کامپایل مجدد برنامهی IDP توسط یک صفحهی مدیریتی داشته باشیم، نیاز است آنها را به بانک اطلاعاتی منتقل کنیم. این مورد مزیت به اشتراک گذاری یک چنین اطلاعاتی را توسط Load balancers نیز میسر میکند.
البته باید درنظر داشت قسمت دیگر اطلاعات IdentityServer شامل refresh tokens و reference tokens هستند. تمام اینها اکنون در حافظه ذخیره میشوند که با ریاستارت شدن Application pool برنامه از بین خواهند رفت. بنابراین حداقل در این مورد استفادهی از بانک اطلاعاتی اجباری است.
خوشبختانه قسمت عمدهی این کار توسط خود تیم IdentityServer توسط بستهی IdentityServer4.EntityFramework انجام شدهاست که در اینجا از آن استفاده خواهیم کرد. البته در اینجا این بستهی نیوگت را مستقیما مورد استفاده قرار نمیدهیم. از این جهت که نیاز به 2 رشتهی اتصالی جداگانه و دو Context جداگانه را دارد که داخل خود این بسته تعریف شدهاست و ترجیح میدهیم که اطلاعات آنرا با ApplicationContext خود یکی کنیم.
برای این منظور آخرین سورس کد پایدار آنرا از این آدرس دریافت کنید:
https://github.com/IdentityServer/IdentityServer4.EntityFramework/releases
انتقال موجودیتها به پروژهی DNT.IDP.DomainClasses
در این بستهی دریافتی، در پوشهی src\IdentityServer4.EntityFramework\Entities آن، کلاسهای تعاریف موجودیتهای متناظر با منابع IdentityServer قرار دارند. بنابراین همین فایلها را از این پروژه استخراج کرده و به پروژهی DNT.IDP.DomainClasses در پوشهی جدید IdentityServer4Entities اضافه میکنیم.
البته در این حالت پروژهی DNT.IDP.DomainClasses نیاز به این وابستگیها را خواهد داشت:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="System.ComponentModel.Annotations" Version="4.3.0" /> <PackageReference Include="IdentityServer4" Version="2.2.0" /> </ItemGroup> </Project>
انتقال تنظیمات روابط بین موجودیتها، به پروژهی DNT.IDP.DataLayer
در فایل src\IdentityServer4.EntityFramework\Extensions\ModelBuilderExtensions.cs بستهی دریافتی، تعاریف تنظیمات این موجودیتها به همراه نحوهی برقراری ارتباطات بین آنها قرار دارد. بنابراین این اطلاعات را نیز از این فایل استخراج و به پروژهی DNT.IDP.DataLayer اضافه میکنیم. البته در اینجا از روش IEntityTypeConfiguration برای قرار هر کدام از تعاریف یک در کلاس مجزا استفاده کردهایم.
پس از این انتقال، به کلاس Context برنامه مراجعه کرده و توسط متد builder.ApplyConfiguration، این فایلهای IEntityTypeConfiguration را معرفی میکنیم.
تعاریف DbSetهای متناظر با موجودیتهای منتقل و تنظیم شده در پروژهی DNT.IDP.DataLayer
پس از انتقال موجودیتها و روابط بین آنها، دو فایل DbContext را در این بستهی دریافتی خواهید یافت:
الف) فایل src\IdentityServer4.EntityFramework\DbContexts\ConfigurationDbContext.cs
این فایل، موجودیتهای تنظیمات برنامه مانند Resources و Clients را در معرض دید EF Core قرار میدهد.
سپس فایل src\IdentityServer4.EntityFramework\Interfaces\IConfigurationDbContext.cs نیز جهت استفادهی از این DbContext در سرویسهای این بستهی دریافتی تعریف شدهاست.
ب) فایل src\IdentityServer4.EntityFramework\DbContexts\PersistedGrantDbContext.cs
این فایل، موجودیتهای ذخیره سازی اطلاعات مخصوص IDP را مانند refresh tokens و reference tokens، در معرض دید EF Core قرار میدهد.
همچنین فایل src\IdentityServer4.EntityFramework\Interfaces\IPersistedGrantDbContext.cs نیز جهت استفادهی از این DbContext در سرویسهای این بستهی دریافتی تعریف شدهاست.
ما در اینجا DbSetهای هر دوی این DbContextها را در ApplicationDbContext خود، خلاصه و ادغام میکنیم.
انتقال نگاشتهای AutoMapper بستهی دریافتی به پروژهی جدید DNT.IDP.Mappings
در پوشهی src\IdentityServer4.EntityFramework\Mappers، تعاریف نگاشتهای AutoMapper، برای تبدیلات بین موجودیتهای برنامه و IdentityServer4.Models انجام شدهاست. کل محتویات این پوشه را به یک پروژهی Class library جدید به نام DNT.IDP.Mappings منتقل و فضاهای نام آنرا نیز اصلاح میکنیم.
انتقال src\IdentityServer4.EntityFramework\Options به پروژهی DNT.IDP.Models
در پوشهی Options بستهی دریافتی سه فایل موجود هستند:
الف) Options\ConfigurationStoreOptions.cs
این فایل، به همراه تنظیمات نام جداول متناظر با ذخیره سازی اطلاعات کلاینتها است. نیازی به آن نداریم؛ چون زمانیکه موجودیتها و تنظیمات آنها را به صورت مستقیم در اختیار داریم، نیازی به فایل تنظیمات ثالثی برای انجام اینکار نیست.
ب) Options\OperationalStoreOptions.cs
این فایل، تنظیمات نام جداول مرتبط با ذخیره سازی توکنها را به همراه دارد. به این نام جداول نیز نیازی نداریم. اما این فایل به همراه سه تنظیم زیر جهت پاکسازی دورهای توکنهای قدیمی نیز هست:
namespace IdentityServer4.EntityFramework.Options { public class OperationalStoreOptions { public bool EnableTokenCleanup { get; set; } = false; public int TokenCleanupInterval { get; set; } = 3600; public int TokenCleanupBatchSize { get; set; } = 100; } }
public TokenCleanup( IServiceProvider serviceProvider, ILogger<TokenCleanup> logger, IOptions<OperationalStoreOptions> options)
کلاسی است به همراه خواص نام اسکیمای جداول که در دو کلاس تنظیمات قبلی بکار رفتهاست. نیازی به آن نداریم.
انتقال سرویسهای IdentityServer4.EntityFramework به پروژهی DNT.IDP.Services
بستهی دریافتی، شامل دو پوشهی src\IdentityServer4.EntityFramework\Services و src\IdentityServer4.EntityFramework\Stores است که سرویسهای آنرا تشکیل میدهند (جمعا 5 سرویس TokenCleanup، CorsPolicyService، ClientStore، PersistedGrantStore و ResourceStore). بنابراین این سرویسها را نیز مستقیما از این پوشهها به پروژهی DNT.IDP.Services کپی خواهیم کرد.
همانطور که عنوان شد دو فایل Interfaces\IConfigurationDbContext.cs و Interfaces\IPersistedGrantDbContext.cs برای دسترسی به دو DbContext این بستهی دریافتی در سرویسهای آن، تعریف شدهاند و چون ما در اینجا صرفا یک ApplicationDbContext را داریم که از طریق IUnitOfWork، در دسترس لایهی سرویس قرار میگیرد، ارجاعات به دو اینترفیس یاد شده را با IUnitOfWork تعویض خواهیم کرد تا مجددا قابل استفاده شوند.
انتقال متدهای الحاقی معرفی سرویسهای IdentityServer4.EntityFramework به پروژهی DNT.IDP
پس از انتقال قسمتهای مختلف IdentityServer4.EntityFramework به لایههای مختلف برنامهی جاری، اکنون نیاز است سرویسهای آنرا به برنامه معرفی کرد که در نهایت جایگزین متدهای فعلی درون حافظهای کلاس آغازین برنامهی IDP میشوند. خود این بسته در فایل زیر، به همراه متدهایی الحاقی است که این معرفی را انجام میدهند:
src\IdentityServer4.EntityFramework\Extensions\IdentityServerEntityFrameworkBuilderExtensions.cs
به همین جهت این فایل را به پروژهی وب DNT.IDP ، منتقل خواهیم کرد؛ همانجایی که در قسمت دهم AddCustomUserStore را تعریف کردیم.
این کلاس پس از انتقال، نیاز به تغییرات ذیل را دارد:
الف) چون یکسری از کلاسهای تنظیمات را حذف کردیم و نیازی به آنها نداریم، آنها را نیز به طور کامل از این فایل حذف میکنیم. تنها تنظیم مورد نیاز آن، OperationalStoreOptions است که اینبار آنرا از فایل appsettings.json دریافت میکنیم. بنابراین ذکر این مورد نیز در اینجا اضافی است.
ب) در آن، دو Context موجود در بستهی اصلی IdentityServer4.EntityFramework مورد استفاده قرار گرفتهاند. ما در اینجا آنها را نیز با تک Context برنامهی خود تعویض میکنیم.
ج) در آن سرویسی به نام TokenCleanupHost تعریف شدهاست. آنرا نیز به لایهی سرویسها منتقل میکنیم. همچنین در امضای سازندهی آن بجای تزریق مستقیم OperationalStoreOptions از <IOptions<OperationalStoreOptions استفاده خواهیم کرد.
نگارش نهایی و تمیز شدهی IdentityServerEntityFrameworkBuilderExtensions را در اینجا مشاهده میکنید.
افزودن متدهای الحاقی جدید به فایل آغازین برنامهی IDP
پس از انتقال IdentityServerEntityFrameworkBuilderExtensions به پروژهی DNT.IDP، اکنون نوبت به استفادهی از آن است:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddSigningCredential(loadCertificateFromStore()) .AddCustomUserStore() .AddConfigurationStore() .AddOperationalStore();
اجرای Migrations در پروژهی DNT.IDP.DataLayer
پس از پایان این نقل و انتقالات، اکنون نیاز است ساختار بانک اطلاعاتی برنامه را بر اساس موجودیتها و روابط جدید بین آنها، به روز رسانی کنیم. به همین جهت فایل add_migrations.cmd موجود در پوشهی src\IDP\DNT.IDP.DataLayer را اجرا میکنیم تا کلاسهای Migrations متناظر تولید شوند و سپس فایل update_db.cmd را اجرا میکنیم تا این تغییرات، به بانک اطلاعاتی برنامه نیز اعمال گردند.
انتقال اطلاعات فایل درون حافظهای Config، به بانک اطلاعاتی برنامه
تا اینجا اگر برنامه را اجرا کنیم، دیگر کار نمیکند. چون جداول کلاینتها و منابع آن خالی هستند. به همین جهت نیاز است اطلاعات فایل درون حافظهای Config را به بانک اطلاعاتی منتقل کنیم. برای این منظور سرویس ConfigSeedDataService را به برنامه اضافه کردهایم:
public interface IConfigSeedDataService { void EnsureSeedDataForContext( IEnumerable<IdentityServer4.Models.Client> clients, IEnumerable<IdentityServer4.Models.ApiResource> apiResources, IEnumerable<IdentityServer4.Models.IdentityResource> identityResources); }
برای استفادهی از آن، به کلاس آغازین برنامهی IDP مراجعه میکنیم:
namespace DNT.IDP { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... initializeDb(app); seedDb(app); // ... } private static void seedDb(IApplicationBuilder app) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var configSeedDataService = scope.ServiceProvider.GetService<IConfigSeedDataService>(); configSeedDataService.EnsureSeedDataForContext( Config.GetClients(), Config.GetApiResources(), Config.GetIdentityResources() ); } }
آزمایش برنامه
پس از اعمال این تغییرات، برنامهها را اجرا کنید. سپس به بانک اطلاعاتی آن مراجعه نمائید. در اینجا لیست جداول جدیدی را که اضافه شدهاند ملاحظه میکنید:
همچنین برای نمونه، در اینجا اطلاعات منابع منتقل شدهی به بانک اطلاعاتی، از فایل Config، قابل مشاهده هستند:
و یا قسمت ذخیره سازی خودکار توکنهای تولیدی توسط آن نیز به درستی کار میکند:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
ابزارهای پیش نیاز:
در اولین قدم، برنامهی متن باز Visual Studio Code را از اینجا دانلود و نصب کنید. برنامهی Visual Studio Code که در ادامهی فعالیتهای جدید متن باز مایکروسافت به بازار عرضه شده است، سریع، سبک و کاملا قابل توسعه و سفارشی سازی است و از اکثر زبانهای معروف پشتیبانی میکند.
در قدم بعدی، شما باید NET Core. را از اینجا (64 بیتی) دانلود و نصب کنید.
.NET Core چیست؟
NET Core. در واقع پیاده سازی بخشی از NET. اصلی است که به صورت متن باز در حال توسعه میباشد و بر روی لینوکس و مکینتاش هم قابل اجراست. موتور اجرای دات نت کامل CLR نام دارد و NET Core. نیز دارای موتور اجرایی CoreCLR است و شامل فریمورک CoreFX میباشد.
در حال حاضر شما میتوانید با استفاده از NET Core. برنامههای کنسولی و تحت وب با ASP.NET 5 بنویسید و احتمالا در آینده میتوان امیدوار بود که از ساختارهای پیچیدهتری مثل WPF نیز پشتیبانی کند.
پس از آنکه NET Core. را دانلود و نصب کردید، جهت شروع پروژه، یک پوشه را در یکی از درایوها ساخته (در این مثال E:\Projects\EF7-SQLite-NETCore) و Command prompt را در آنجا باز کنید. سپس دستورات زیر را به ترتیب اجرا کنید:
dotnet restore
dotnet run
- NuGet.Config (این فایل، تنظیمات مربوط به نیوگت را جهت کشف و دریافت وابستگیهای پروژه، شامل میشود)
- Program.cs (این فایل سی شارپ حاوی کد برنامه است)
- project.json (این فایل حاوی اطلاعات پلتفرم هدف و لیست وابستگیهای پروژه است)
دستور dotnet restore بر اساس لیست وابستگیها و پلتفرم هدف، وابستگیهای لازم را از مخزن نیوگت دریافت میکند. (در صورتی که در هنگام اجرای این دستور با خطای NullReferenceException مواجه شدید از دستور dnu restore استفاده کنید. این خطا در گیت هاب در حال بررسی است)
دستور dotnet run هم سورس برنامه را کامپایل و اجرا میکند. در صورتی که پیام Hello World را مشاهده کردید، یعنی برنامهی شما تحت NET Core. با موفقیت اجرا شده است.
توسعهی پروژه با Visual Studio Code
در ادامه، قصد داریم پروژهی HelloWorld را تحت Visual Studio Code باز کرده و تغییرات بعدی را در آنجا اعمال کنیم. پس از باز کردن Visual Studio Code از منوی File گزینهی Open Folder را انتخاب کنید و پوشهی حاوی پروژه (EF7-SQLite-NETCore) را انتخاب کنید. اکنون پروژهی شما تحت VS Code باز شده و قابل ویرایش است.
سپس از لیست فایلهای پروژه، فایل project.json را باز کرده و در بخش "dependencies" یک ردیف را برای EntityFramework.SQLite به صورت زیر اضافه کنید. به محض افزودن این خط در project.json و ذخیرهی آن، در صورتیکه قبلا این وابستگی دریافت نشده باشد، Visual Studio Code با نمایش یک هشدار در بالای برنامه به شما امکان دریافت اتوماتیک این وابستگی را میدهد. در نتیجه کافیست دکمهی Restore را زده و منتظر شوید تا وابستگی EntityFramework.SQLite از مخزن ناگت دانلود و برای پروژهی شما تنظیم شود.
"EntityFramework.SQLite": "7.0.0-rc1-final"
پس از کامل شدن این مرحله، در پروژههای بعدی تمام ارجاعات به وابستگیهای دریافت شده، از طریق مخزن موجود در سیستم خود شما، برطرف خواهد شد و نیاز به دانلود مجدد وابستگیها نیست.
اکنون همهی موارد، جهت توسعهی پروژه آماده است. ماوس خود را بر روی ریشهی پروژه در VS Code قرار داده و New Folder را انتخاب کنید و نام Models را برای آن تایپ کنید. این پوشه قرار است مدل کلاسهای پروژه را شامل شود. در اینجا ما یک مدل به نام Book داریم و نام کانتکست اصلی پروژه را هم LibraryContext گذاشتهایم.
بر روی پوشهی Models راست کلیک کرده و گزینهی New File را انتخاب کنید. سپس فایلهای Book.cs و LibraryContext.cs را ایجاد کرده و کدهای زیر را برای مدل و کانتکست، در درون این دو فایل قرار دهید.
Book.cs
namespace Models { public class Book { public int ID { get; set; } public string Title { get; set; } public string Author{get;set;} public int PublishYear { get; set; } } }
using Microsoft.Data.Entity; using Microsoft.Data.Sqlite; namespace Models { public class LibraryContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = "test.db" }; var connectionString = connectionStringBuilder.ToString(); var connection = new SqliteConnection(connectionString); optionsBuilder.UseSqlite(connection); } public DbSet<Book> Books { get; set; } } }
در قدم آخر هم کافیست که فایل Program.cs را تغییر دهید و مقادیری را در دیتابیس ذخیره و بازخوانی کنید.
using System; using Models; namespace ConsoleApplication { public class Program { public static void Main(string[] args) { Console.WriteLine("EF7 + Sqlite with the taste of .NET Core"); try { using (var context = new LibraryContext()) { context.Database.EnsureCreated(); var book1 = new Book() { Title = "Adaptive Code via C#: Agile coding with design patterns and SOLID principles ", Author = "Gary McLean Hall", PublishYear = 2014 }; var book2 = new Book() { Title = "CLR via C# (4th Edition)", Author = "Jefrey Ritcher", PublishYear = 2012 }; context.Books.Add(book1); context.Books.Add(book2); context.SaveChanges(); ReadData(context); } Console.WriteLine("Press any key to exit ..."); Console.ReadKey(); } catch (Exception ex) { Console.WriteLine($"An exception occured: {ex.Message}\n{ex.StackTrace}"); } } private static void ReadData(LibraryContext context) { Console.WriteLine("Books in database:"); foreach (var b in context.Books) { Console.WriteLine($"Book {b.ID}"); Console.WriteLine($"\tName: {b.Title}"); Console.WriteLine($"\tAuthor: {b.Author}"); Console.WriteLine($"\tPublish Year: {b.PublishYear}"); Console.WriteLine(); } } } }
جهت اجرای برنامه کافیست Command prompt را در آدرس پروژه باز کرده و دستور dotnet run را اجرا کنید. پروژهی شما کامپایل و اجرا میشود و خروجی مشابه زیر را مشاهده خواهید کرد. اگر برنامه را مجددا اجرا کنید، به جای دو کتاب اطلاعات چهار کتاب نمایش داده خواهد شد؛ چرا که در هر مرحله اطلاعات دو کتاب در دیتابیس درج میشود.
اگر به پوشهی bin که در پوشهی پروژه ایجاد شده است، نگاهی بیندازید، خبری از فایل باینری نیست. چرا که در لحظه، تولید و اجرا شده است. جهت build کردن پروژه و تولید فایل باینری کافیست دستور dotnet build را اجرا کنید، تا فایل باینری در پوشهی bin ایجاد شود.
جهت انتشار برنامه میتوانید دستور dotnet publish را اجرا کنید. این دستور نه تنها برنامه، که تمام وابستگیهای مورد نیاز آن را برای اجرای در یک پلتفرم خاص تولید میکند. برای مثال بعد از اجرای این دستور یک پوشهی win7-x64 حاوی 211 فایل در مجموع تولید شده است که تمامی وابستگیهای این پروژه را شامل میشود.
در واقع این پوشه تمام وابستگیهای مورد نیاز پروژه را همراه خود دارد و در نتیجه جهت اجرای این برنامه برخلاف برنامههای معمولی دات نت، دیگر نیازی به نصب هیچ وابستگی مجزایی نیست و حتی پروژههای نوشته شده تحت NET Core. را میتوانید در سیستمهای عاملهای دیگری مثل لینوکس و مکینتاش و یا Windows IoT بر روی سخت افزار Raspberry Pi 2 هم اجرا کنید.
جهت مطالعهی بیشتر:
تبلیغات چگونه ما رو دنبال می کنند
نقشهی راه فراگیری ES6
Power of Reflection Emit
How to create a class with properties at run time