الگوی طراحی Factory Method به همراه مثال
ساختار پوشهها و پروژههای قسمت Blazor Server
قسمت Blazor Server مدیریت هتل ما از 7 پروژه و پوشهی زیر تشکیل میشود:
- BlazorServer.App: پروژهی اصلی Blazor Server است که با اجرای دستور dotnet new blazorserver در پوشهی خالی آن آغاز میشود.
- BlazorServer.Common: پروژهای از نوع classlib، جهت قرارگیری کدهای مشترک بین پروژهها است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.DataAccess: پروژهای از نوع classlib، برای تعریف DbContext برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.Entities: پروژهای از نوع classlib، جهت تعریف کلاسهای متناظر با جداول بانک اطلاعاتی برنامه است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.Models: پروژهای از نوع classlib، برای تعریف کلاسهای data transfer objects برنامه (DTO's) است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.Models.Mappings: پروژهای از نوع classlib، برای تعریف نگاشتهای بین DTO's و مجودیتهای برنامه و برعکس است که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
- BlazorServer.Services: پروژهای از نوع classlib، جهت تعریف کدهایی که منطق تجاری تعامل با بانک اطلاعاتی را از طریق BlazorServer.DataAccess میسر میکند که با اجرای دستور dotnet new classlib در این پوشه آغاز میشود.
اصلاح پروژهی BlazorServer.App جهت استفاده از LibMan
قالب پیشفرض BlazorServer.App، به همراه پوشهی wwwroot\css است که در آن بوت استرپ و open-iconic به همراه فایل site.css قرار دارند. چون این پروژه به همراه هیچ نوع روشی برای مدیریت نگهداری بستههای سمت کلاینت خود نیست، دو پوشهی بوت استرپ و open-iconic آنرا حذف کرده و از روش مطرح شدهی در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» استفاده خواهیم کرد:
dotnet tool update -g Microsoft.Web.LibraryManager.Cli libman init libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic
بعد از نصب بستههای ذکر شده، ابتدا سطر زیر را از ابتدای فایل پیشفرض wwwroot\css\site.css حذف میکنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>BlazorServer.App</title> <base href="~/" /> <link href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet" /> <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" /> <link href="BlazorServer.App.styles.css" rel="stylesheet" /> </head>
برای bundling & minification این فایلها میتوان از «Bundler Minifier» استفاده کرد.
پروژهی موجودیتهای مدیریت هتل
فایل BlazorServer.Entities.csproj وابستگی خاصی را نداشته و به صورت زیر تعریف شدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> </Project>
using System; using System.ComponentModel.DataAnnotations; namespace BlazorServer.Entities { public class HotelRoom { [Key] public int Id { get; set; } [Required] public string Name { get; set; } [Required] public int Occupancy { get; set; } [Required] public decimal RegularRate { get; set; } public string Details { get; set; } public string SqFt { get; set; } public string CreatedBy { get; set; } public DateTime CreatedDate { get; set; } = DateTime.Now; public string UpdatedBy { get; set; } public DateTime UpdatedDate { get; set; } } }
پروژهی تعریف DbContext برنامهی مدیریت هتل
فایل BlazorServer.DataAccess.csproj به این صورت تعریف شدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> </Project>
- همچنین دو وابستگی مورد نیاز جهت کار با EntityFrameworkCore و اجرای مهاجرتها را نیز به آن افزودهایم.
پس از تامین این وابستگیها، اکنون میتوان DbContext ابتدایی برنامه را به صورت زیر تعریف کرد که کار آن، در معرض دید قرار دادن HotelRoom به صورت یک DbSet است:
using BlazorServer.Entities; using Microsoft.EntityFrameworkCore; namespace BlazorServer.DataAccess { public class ApplicationDbContext : DbContext { public DbSet<HotelRoom> HotelRooms { get; set; } public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } } }
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\BlazorServer.DataAccess\BlazorServer.DataAccess.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> </Project>
- سپس چون میخواهیم از تامین کنندهی بانک اطلاعاتی SQL Server نیز استفاده کنیم، وابستگیهای آنرا نیز افزودهایم.
با این تنظیمات، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و کار افزودن AddDbContext و UseSqlServer را انجام میدهیم تا DbContext برنامه از طریق تزریق وابستگیها قابل دسترسی شود و همچنین رشتهی اتصالی مشخص شده نیز به تامین کنندهی SQL Server ارسال شود:
namespace BlazorServer.App { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); // ...
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=HotelManagement;Trusted_Connection=True;MultipleActiveResultSets=true" } }
اجرای مهاجرتها و تشکیل ساختار بانک اطلاعاتی
پس از این تنظیمات، اکنون میتوانیم به پوشهی BlazorServer\BlazorServer.DataAccess مراجعه کرده و از طریق خط فرمان، دستورات زیر را صادر کنیم:
dotnet tool update --global dotnet-ef --version 5.0.3 dotnet build dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
- همیشه بهتر است پیش از اجرای عملیات Migration، یکبار dotnet build را اجرا کرد؛ تا اگر خطایی وجود دارد، بتوان جزئیات دقیق آنرا مشاهده کرد. چون عموما این جزئیات در حین اجرای دستورات بعدی، با پیام مختصر «عملیات شکست خورد»، نمایش داده نمیشوند.
- دستور سوم، کار تشکیل پوشهی BlazorServer\BlazorServer.DataAccess\Migrations و تولید خودکار دستورات تشکیل بانک اطلاعاتی را بر اساس ساختار DbContext برنامه انجام میدهد.
- دستور چهارم، بر اساس اطلاعات موجود در پوشهی BlazorServer\BlazorServer.DataAccess\Migrations، بانک اطلاعاتی واقعی را تولید میکند.
در این دستورات ذکر پروژهی آغازین برنامه جهت یافتن وابستگیهای پروژه ضروری است.
تکمیل پروژهی DTOهای برنامه
همواره توصیه شدهاست که موجودیتهای برنامه را مستقیما در معرض دید UI قرار ندهید. حداقل مشکلی را که در اینجا ممکن است مشاهده کنید، حملات از نوع mass assignment هستند. برای مثال قرار است از کاربر، کلمهی عبور جدید آنرا دریافت کنید، ولی چون اطلاعات دریافتی، به اصل موجودیت متناظر با بانک اطلاعاتی نگاشت میشود، کاربر میتواند فیلد IsAdmin را هم خودش مقدار دهی کند! و چون سیستم Binding بسیار پیشرفته عمل میکند، این ورودی را معتبر یافته و در اینجا علاوه بر به روز رسانی کلمهی عبور، خواص دیگری را هم که نباید به روز رسانی شوند، به روز رسانی میکند و یا در بسیاری از موارد نیاز است data annotations خاصی را برای فیلدها تعریف کرد که ربطی به موجودیت اصلی ندارند و یا نیاز است فیلدهایی را در UI قرار داد که باز هم تناظر یک به یکی با موجودیت اصلی ندارند (گاهی کمتر و گاهی بیشتر هستند و باید بر روی آنها محاسباتی صورت گیرد تا قابلیت ذخیره سازی در بانک اطلاعاتی را پیدا کنند). به همین جهت کار مدل سازی UI و یا بازگشت اطلاعات نهایی از سرویسها را توسط DTOها که یک سری کلاس سادهی C# 9.0 از نوع record هستند، انجام میدهیم:
using System; using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public record HotelRoomDTO { public int Id { get; init; } [Required(ErrorMessage = "Please enter the room's name")] public string Name { get; init; } [Required(ErrorMessage = "Please enter the occupancy")] public int Occupancy { get; init; } [Range(1, 3000, ErrorMessage = "Regular rate must be between 1 and 3000")] public decimal RegularRate { get; init; } public string Details { get; init; } public string SqFt { get; init; } } }
در اینجا فیلدهای UI برنامه را که در قسمت بعد تکمیل خواهیم کرد، مشاهده میکنید؛ به همراه یک سری data annotation برای تعریف اجباری و یا بازهی مورد قبول، به همراه پیامهای خطای مرتبط.
نگاشت DTOهای برنامه به موجودیتها و بر عکس
یا میتوان خواص DTO تعریف شده را یکی یکی به موجودیتی متناظر با آن انتساب داد و یا میتوان از AutoMapper برای اینکار استفاده کرد. به همین جهت به BlazorServer.Models.Mappings.csproj مراجعه کرده و تغییرات زیر را اعمال میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\BlazorServer.Entities\BlazorServer.Entities.csproj" /> <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" /> </ItemGroup> </Project>
- همچنین بستهی مخصوص AutoMapper را که به همراه امکانات تزریق وابستگیهای آن نیز هست، در اینجا افزودهایم.
پس از افزودن این ارجاعات، نگاشت دو طرفهی بین مدل و موجودیت تعریف شده را به صورت زیر تعریف میکنیم:
using AutoMapper; using BlazorServer.Entities; namespace BlazorServer.Models.Mappings { public class MappingProfile : Profile { public MappingProfile() { CreateMap<HotelRoomDTO, HotelRoom>().ReverseMap(); // two-way mapping } } }
namespace BlazorServer.App { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); // ...
تعریف سرویس مدیریت اتاقهای هتل
پس از راه اندازی برنامه و تعریف موجودیتها، DbContext و غیره، اکنون میتوانیم از آنها جهت ارائهی منطق مدیریتی برنامه استفاده کنیم:
using System.Collections.Generic; using System.Threading.Tasks; using BlazorServer.Models; namespace BlazorServer.Services { public interface IHotelRoomService { Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO); Task<int> DeleteHotelRoomAsync(int roomId); IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync(); Task<HotelRoomDTO> GetHotelRoomAsync(int roomId); Task<HotelRoomDTO> IsRoomUniqueAsync(string name); Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO); } }
که پیاده سازی ابتدایی آن به صورت زیر است:
using System; using System.Collections.Generic; using System.Threading.Tasks; using AutoMapper; using AutoMapper.QueryableExtensions; using BlazorServer.DataAccess; using BlazorServer.Entities; using BlazorServer.Models; using Microsoft.EntityFrameworkCore; namespace BlazorServer.Services { public class HotelRoomService : IHotelRoomService { private readonly ApplicationDbContext _dbContext; private readonly IMapper _mapper; private readonly IConfigurationProvider _mapperConfiguration; public HotelRoomService(ApplicationDbContext dbContext, IMapper mapper) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapperConfiguration = mapper.ConfigurationProvider; } public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO) { var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO); hotelRoom.CreatedDate = DateTime.Now; hotelRoom.CreatedBy = ""; var addedHotelRoom = await _dbContext.HotelRooms.AddAsync(hotelRoom); await _dbContext.SaveChangesAsync(); return _mapper.Map<HotelRoomDTO>(addedHotelRoom.Entity); } public async Task<int> DeleteHotelRoomAsync(int roomId) { var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId); if (roomDetails == null) { return 0; } _dbContext.HotelRooms.Remove(roomDetails); return await _dbContext.SaveChangesAsync(); } public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync() { return _dbContext.HotelRooms .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .AsAsyncEnumerable(); } public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId) { return _dbContext.HotelRooms .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .FirstOrDefaultAsync(x => x.Id == roomId); } public Task<HotelRoomDTO> IsRoomUniqueAsync(string name) { return _dbContext.HotelRooms .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .FirstOrDefaultAsync(x => x.Name == name); } public async Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO) { if (roomId != hotelRoomDTO.Id) { return null; } var roomDetails = await _dbContext.HotelRooms.FindAsync(roomId); var room = _mapper.Map(hotelRoomDTO, roomDetails); room.UpdatedBy = ""; room.UpdatedDate = DateTime.Now; var updatedRoom = _dbContext.HotelRooms.Update(room); await _dbContext.SaveChangesAsync(); return _mapper.Map<HotelRoomDTO>(updatedRoom.Entity); } } }
- در این کدها استفاده از متد ProjectTo را هم مشاهده میکنید. استفاده از این متد، بسیار بهینهتر از کار با متد Map درون حافظهای است. از این جهت که بر روی SQL نهایی ارسالی به سمت سرور تاثیرگذار است و تعداد فیلدهای بازگشت داده شده را بر اساس DTO تعیین شده، کاهش میدهد. درغیراینصورت باید تمام ستونهای جدول را بازگشت داد و سپس با استفاده از متد Map درون حافظهای، کار نگاشت نهایی را انجام داد که آنچنان بهینه نیست.
در آخر نیاز است این سرویس را نیز به سیستم تزریق وابستگیهای برنامه معرفی کنیم. به همین جهت در فایل BlazorServer\BlazorServer.App\Startup.cs، تغییر زیر را اعمال خواهیم کرد:
namespace BlazorServer.App { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddScoped<IHotelRoomService, HotelRoomService>(); // ...
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-13.zip
%userprofile%\.templateengine\dotnetcli
و یا قالبی را که در قسمت قبل، به سیستم dotnet new اضافه کردیم، مدخل تعریف آن، در فایل templatecache.json ذیل، ثبت شدهاست (short name آنرا در این فایل جستجو کنید):
%userprofile%\.templateengine\dotnetcli\v2.0.0-preview2-006497\templatecache.json
برای حذف قالب تعریف شده از سیستم dotnet new، تنها دستور ذیل وجود دارد که سبب حذف تعریف تمام قالبهای سفارشی جدید میشود:
dotnet new --debug:reinit
ساخت بستهی نیوگت از قالب سفارشی
- برای ساخت بستهی نیوگت، ابتدا یک پوشهی مجزا را خارج از پروژهی خود ایجاد کنید (تصویر فوق).
- سپس آخرین نگارش فایل nuget.exe را از آدرس https://dist.nuget.org/index.html دریافت کنید و به داخل این پوشه کپی نمائید.
- فایل pack.bat دارای این یک سطر است:
nuget pack .\nuget\Templates.nuspec
- در ادامه داخل پوشهی nuget، مطابق تصویر فوق، پوشهای به نام Content و فایل خالی Templates.nuspec را ایجاد کنید.
پوشهی Content در برگیرندهی قسمتی از پروژهاست که قرار است درون بستهی نیوگت قرارگیرد (یعنی تمام فایلهای پروژه به همراه پوشهی مخصوص template.config. باید به اینجا کپی شوند). برای مثال پوشههای Bin و Obj و یا اسکریپتهای جانبی را میشود در اینجا لحاظ نکرد.
- محتوای فایل Templates.nuspec یک چنین ساختاری را دارد:
<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <metadata> <id>DNT.Identity</id> <version>1.0.0</version> <description>Empty DNT.Identity project</description> <authors>VahidN (https://www.dntips.ir/)</authors> <language>en-US</language> <projectUrl>https://github.com/VahidN/DNTIdentity</projectUrl> <licenseUrl>https://github.com/VahidN/DNTIdentity/blob/master/LICENSE.md</licenseUrl> <packageTypes> <packageType name="Template" /> </packageTypes> </metadata> </package>
- اکنون فایل pack.bat را اجرا کنید. پس از آن فایلی مانند DNT.Identity.1.0.0.nupkg تولید خواهد شد که آنرا میتوان در سایت nuget.org مانند سایر بستههای نیوگت آپلود کرد و به اشتراک گذاشت.
یک نکته: میشد فایل nuget.exe و pack.bat را در کنار پوشهی Content و فایل Templates.nuspec هم قرار داد. در این حالت پس از اجرای دستور nuget pack، فایلهای exe و bat نیز داخل فایل نهایی تولیدی قرار میگرفتند. بنابراین بهتر است اینها را درون یک پوشه قرار نداد.
نحوهی نصب یک قالب جدید پروژههای NET Core. از طریق نیوگت
پس از آپلود فایل nupkg حاصل در سایت nuget.org، اکنون نحوهی نصب آن در سیستم به صورت ذیل است:
در حالت عمومی:
dotnet new --install [name]::[version]
dotnet new --install DNT.Identity::*
همانطور که در تصویر فوق نیز ملاحظه میکنید، این قالب جدید در کنار سایر قالبهای پیشفرض SDK مربوط به NET Core. قرار گرفتهاست.
اکنون کار با این قالب نصب شده، همانند قسمت «نحوهی ایجاد یک پروژهی جدید بر اساس قالب نصب شده» مطلب پیشین است:
dotnet new dntidentity -n MyNewProj
- Pre-alpha
این مرحله شامل تمام فعالیتهای انجام شده قبل از مرحله تست میباشد. در این دوره آنالیز نیازمندیها، طراحی نرم افزار، توسعه نرم افزار و حتی تست واحد باشد. در نرم افزارهای سورس باز چندین نسخه قبل از آلفا ممکن است عرضه شوند. - Alpha
این مرحله شامل همه فعالیتها از زمان شروع تست میباشد. البته منظور از تست، تست تیمی و تست خود نرم افزار میباشد. نرم افزارهای آلفا هنوز ممکن است خطا و اشکالاتی داشته باشند و ممکن است اطلاعات شما از بین رود. در این مرحله امکانات جدیدی مرتبا به نرم افزار اضافه میگردد. - Beta
نرم افزار بتا، همه قابلیتهای آن تکمیل شده و خطاهای زیادی برای کامل شدن نرم افزار وجود دارد. در این مرحله بیشتر به تست کاهش تاثیرات به کاربران و تست کارایی دقت میشود. نسخه بتا، اولین نسخهای خواهد بود که بیرون شرکت و یا سازمان در دسترس قرار میگیرد. برخی توسعه دهندگان به این مرحله preview، technical preview یا early access نیز میگویند. - Release candidate
در این مرحله نرم افزار، آماده عرضه به مصرف کنندگان است و نرم افزارهایی مثل سیستم عاملهای ویندوز در دسترس تولید کنندگان قرار گرفته تا با جدیدترین سخت افزار خود یکپارچه شوند. -
General availability (GA)
در این مرحله، عرضه عمومی نرم افزار و بازاریابی و فروش نرم افزار مد نظر است و علاوه بر این تست امنیتی و در نرم افزارهای خیلی بزرگ عرضه جهانی صورت میگیرد
- Major Version
وقتی افزایش مییابد که تغییرات قابل توجهی در نرم افزار ایجاد شود - Minor Version
وقتی افزایش یابد که ویژگی جزئی یا اصلاحات قابل توجهی به نرم افزار ایجاد شود. - Build Number
به ازای هر بار ساخته شدن پروژه افزایش مییابد. - Revision
وقتی افزایش مییابد که نواقص و باگهای کوچکی رفع شوند.
major.minor[.build[.reversion]]
major.minor[.maintenance[.build]]
به عنوان یک راه حل، مجموعهی سادهای از قوانین و الزامات که چگونگی طراحی شمارههای نسخه و افزایش آن را مشخص میکند، وجود دارد. برای کار کردن با این سیستم، شما ابتدا نیاز به اعلام API عمومی دارید. این خود ممکن است شامل مستندات و یا اجرای کد باشد.
علیرغم آن، مهم است که این API، روشن و دقیق باشد. هنگامیکه API عمومی خود را تعیین کردید، تغییرات برنامه شما بر روی نسخه API عمومی تاثیر خواهد داشت و آنرا افزایش خواهد داد. بر این اساس، این مدل نسخهبندی را در نظر بگیرید: X.Y.Z یعنی (Major.Minor.Patch).
رفع حفرههایی که بر روی API عمومی تاثیر نمیگذارند، مقدار Patch را افزایش میدهند، تغییرات جدیدی که سازگار با نسخه قبلی است، مقدار Minor را افزایش میدهند و تغییرات جدیدی که کاملا بدیع هستند و به نحوی با تغییرات قبلی سازگار نیستند مقدار Major را افزایش میدهند.
- نرمافزارهایی که از نسخه بندی معنایی استفاده میکنند، باید یک API عمومی داشته باشند. این API میتواند در خود کد یا و یا به طور صریح در مستندات باشد که باید دقیق و جامع باشد.
- یک شماره نسخه صحیح باید به شکل X.Y.Z باشد که در آن X،Y و Z اعداد صحیح غیر منفی هستند. X نسخهی Major میباشد، Y نسخهی Minor و Z نسخهی Patch میباشد. هر عنصر باید یک به یک و بصورت عددی افزایش پیدا کند. به عنوان مثال: 1.9.0 -> 1.10.0 -> 1.11.0
- هنگامی که به یک نسخهی Major یک واحد اضافه میشود، نسخهی Minor و Patch باید به حالت 0 (صفر) تنظیم مجدد گردد. هنگامی که به شماره نسخهی Minor یک واحد اضافه میشود، نسخهی Patch باید به حالت 0 (صفر) تنظیم مجدد شود. به عنوان مثال: 1.1.3 -> 2.0.0 و 2.1.7 -> 2.2.0
- هنگامیکه یک نسخه از یک کتابخانه منتشر میشود، محتوای کتابخانه مورد نظر نباید به هیچ وجه تغییری داشته باشد. هر گونه تغییر جدیدی باید در قالب یک نسخه جدید انتشار پیدا کند.
- نسخهی Major صفر (0.Y.Z) برای توسعهی اولیه است. هر چیزی ممکن است در هر زمان تغییر یابد. API عمومی را نباید پایدار در نظر گرفت.
- نسخه 1.0.0 در حقیقت API عمومی را تعریف میکند. چگونگی تغییر و افزایش هر یک از نسخهها بعد از انتشار این نسخه، وابسته به API عمومی و تغییرات آن میباشد.
- نسخه Patch یا (x.y.Z | x > 0) فقط در صورتی باید افزایش پیدا کند که تغییرات ایجاد شده در حد برطرف کردن حفرههای نرمافزار باشد. برطرف کردن حفرههای نرمافزار شامل اصلاح رفتارهای اشتباه در نرمافزار میباشد.
- نسخه Minor یا (x.Y.z | x > 0) فقط در صورتی افزایش پیدا خواهد کرد که تغییرات جدید و سازگار با نسخه قبلی ایجاد شود. همچنین این نسخه باید افزایش پیدا کند اگر بخشی از فعالیتها و یا رفتارهای قبلی نرمافزار به عنوان فعالیت منقرض شده اعلام شود. همچنین این نسخه میتواند افزایش پیدا کند اگر تغییرات مهم و حیاتی از طریق کد خصوصی ایجاد و اعمال گردد. تغییرات این نسخه میتواند شامل تغییرات نسخه Patch هم باشد. توجه به این نکته ضروری است که در صورت افزایش نسخه Minor، نسخه Patch باید به 0 (صفر) تغییر پیدا کند.
- نسخه Major یا (X.y.z | X > 0) در صورتی افزایش پیدا خواهد کرد که تغییرات جدید و ناهمخوان با نسخه فعلی در نرمافزار اعمال شود. تغییرات در این نسخه میتواند شامل تغییراتی در سطح نسخه Minor و Patch نیز باشد. باید به این نکته توجه شود که در صورت افزایش نسخه Major، نسخههای Minor و Patch باید به 0 (صفر) تغییر پیدا کنند.
- یک نسخه قبل از انتشار میتواند توسط یک خط تیره (dash)، بعد از نسخه Patch (یعنی در انتهای نسخه) که انواع با نقطه (dot) از هم جدا میشوند، نشان داده شود. نشانگر نسخه قبل از انتشار باید شامل حروف، اعداد و خط تیره باشد [0-9A-Za-z-]. باید به این نکته دفت داشت که نسخههای قبل از انتشار خود به تنهایی یک انتشار به حساب میآیند اما اولویت و اهمیت نسخههای عادی را ندارد. برای مثال: 1.0.0-alpha ، 1.0.0-alpha.1 ، 1.0.0-0.3.7 ، 1.0.0-x.7.z.92
- یک نسخه Build میتواند توسط یک علامت مثبت (+)، بعد از نسخه Patch یا نسخه قبل از انتشار (یعنی در انتهای نسخه) که انواع آن با نقطه (dot) از هم جدا میشوند، نشان داده شود. نشانگر نسخه Build باید شامل حروف، اعداد و خط تیره باشد [0-9A-Za-z-]. باید به این نکته دقت داشت که نسخههای Build خود به تنهایی یک انتشار به حساب میآیند و اولویت و اهمیت بیشتری نسبت به نسخههای عادی دارند. برای مثال: 1.0.0+build.1 ، 1.3.7+build.11.e0f985a
- اولویتبندی نسخهها باید توسط جداسازی بخشهای مختلف یک نسخه به اجزای تشکیل دهنده آن یعنی Minor، Major، Patch، نسخه قبل از انتشار و نسخه Build و ترتیب اولویت بندی آنها صورت گیرد. نسخههای Minor، Major و Patch باید بصورت عددی مقایسه شوند. مقایسه نسخههای قبل از انتشار و نسخه Build باید توسط بخشهای مختلف که توسط جداکنندهها (نقطههای جداکننده) تفکیک شده است، به این شکل سنجیده شود:
بخشهایی که فقط حاوی عدد هستند، بصورت عددی مقایسه میشوند و بخشهایی که حاری حروف و یا خط تیره هستند بصورت الفبایی مقایسه خواهند شد.
بخشهای عددی همواره اولویت پایینتری نسبت به بخشهای غیر عددی دارند. برای مثال:
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0-rc.1+build.1 < 1.0.0 < 1.0.0+0.3.7 < 1.3.7+build < 1.3.7+build.2.b8f12d7 < 1.3.7+build.11.e0f985a
منبع نسخه بندی معنایی : semver.org
public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddTransient<ICustomerService, DefaultCustomerService>(); // ... } }
public class SupportController { // DefaultCustomerService will be injected here: public SupportController(ICustomerService customerService) { // ... } }
برای سفارشی سازی مراحل وهله سازی اشیاء توسط IoC Container توکار برنامه و امکان دخالت در آن، قابلیتی تحت عنوان «factory registration» نیز پیش بینی شدهاست که در ادامه آنرا بررسی میکنیم.
Factory Registration چیست؟
اگر در اسمبلی Microsoft.Extensions.DependencyInjection.Abstractions و فضای نام Microsoft.Extensions.DependencyInjection آن به کلاس ServiceCollectionServiceExtensions که متدهای الحاقی مانند AddScoped را ارائه میکند، بیشتر دقت کنیم، تک تک این متدها امضاهای دیگری را نیز دارند:
namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionServiceExtensions { public static IServiceCollection AddScoped<TService>( this IServiceCollection services) where TService : class; public static IServiceCollection AddScoped( this IServiceCollection services, Type serviceType, Type implementationType); public static IServiceCollection AddScoped( this IServiceCollection services, Type serviceType, Func<IServiceProvider, object> implementationFactory); public static IServiceCollection AddScoped<TService, TImplementation>(this IServiceCollection services) public static IServiceCollection AddScoped( this IServiceCollection services, Type serviceType); public static IServiceCollection AddScoped<TService>( this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class; public static IServiceCollection AddScoped<TService, TImplementation>( this IServiceCollection services, Func<IServiceProvider, TImplementation> implementationFactory) // ... } }
مثال 1 : تزریق وابستگیها در حالتیکه کلاس سرویس مدنظر دارای تعدادی پارامتر ثابت است
IoC Container توکار برنامههای NET Core.، به صورت خودکار وابستگیهای تزریق شدهی در سازندههای سرویسهای مختلف را تا هر چند سطح ممکن، به صورت خودکار وهله سازی میکند؛ به شرطیکه این وابستگیهای تزریق شده نیز خودشان سرویس بوده باشند و در تنظیمات ابتدایی آن ثبت و معرفی شده باشند. به عبارتی زمانیکه با سیستم تزریق وابستگیها کار میکنیم، مهم نیست که نگران مقدار دهی پارامترهای سازندهی تزریق شدهی در سازندههای سرویسی خاص باشیم. اما ... برای نمونه سرویس زیر را که یک رشته را در سازندهی خود دریافت میکند درنظر بگیرید:
namespace CoreIocServices { public interface IParameterizedService { string GetConstructorParameter(); } public class ParameterizedService : IParameterizedService { private readonly string _connectionString; public ParameterizedService(string connectionString) { _connectionString = connectionString; } public string GetConstructorParameter() { return _connectionString; } } }
services.AddTransient<IParameterizedService, ParameterizedService>();
services.AddTransient<IParameterizedService>(serviceProvider => { return new ParameterizedService("some value ...."); });
services.AddTransient<IParameterizedService>(serviceProvider => new ParameterizedService("some value ...."));
در اینجا چون serviceProvider نیز در اختیار ما است، حتی میتوان این مقدار را از سرویسی دیگر دریافت کرد و سپس مورد استفاده قرار داد:
services.AddTransient<IParameterizedService>(serviceProvider => { var config = serviceProvider.GetRequiredService<ITestService>().GetConfigValue(); return new ParameterizedService(config); });
نمونهی دیگری از این دست، کار با IUrlHelper توکار ASP.NET Core است. این سرویس برای اینکه پاسخ درستی را ارائه دهد، نیاز به ActionContext جاری را دارد تا بتواند از طریق آن به تمام جزئیات اکشن متد یک کنترلر و درخواست رسیده دسترسی داشته باشد. در این حالت برای ساده سازی کار با آن، بهتر است تامین وابستگیهای لحظهای این سرویس را با سفارشی سازی نحوهی وهله سازی آن، انجام دهیم، تا اینکه این قطعه کد تکراری را در هر جائیکه به IUrlHelper نیاز است، تکرار کنیم:
services.AddScoped<IUrlHelper>(serviceProvider => { var actionContext = serviceProvider.GetRequiredService<IActionContextAccessor>().ActionContext; var urlHelperFactory = serviceProvider.GetRequiredService<IUrlHelperFactory>(); return urlHelperFactory.GetUrlHelper(actionContext); });
مثال 2: وهله سازی در صورت نیاز به وابستگیهای یک سرویس، به کمک Lazy loading
فرض کنید دو سرویس را در سازندهی سرویس دیگری تزریق کردهاید:
namespace Services { public class OrderHandler : IOrderHandler { private readonly IAccounting _accounting; private readonly ISales _sales; public OrderHandler(IAccounting accounting, ISales sales) {
روش انجام یک چنین کارهایی با استفاده از کلاس Lazy اضافه شدهی به NET 4x. قابل انجام است:
public class OrderHandlerLazy : IOrderHandler { public OrderHandlerLazy(Lazy<IAccounting> accounting, Lazy<ISales> sales) {
services.AddTransient<IOrderHandler, OrderHandlerLazy>(); services.AddTransient<IAccounting, Accounting>() .AddTransient(serviceProvider => new Lazy<IAccounting>(() => serviceProvider.GetRequiredService<IAccounting>())); services.AddTransient<ISales, Sales>() .AddTransient(serviceProvider => new Lazy<ISales>(() => serviceProvider.GetRequiredService<ISales>()));
- سپس سرویسهایی که قرار است به صورت Lazy نیز واکشی شوند، بار دیگر توسط روش factory registration با وهله سازی new Lazy از نوع سرویس مدنظر و فراهم آوردن پیاده سازی آن با استفاده از serviceProvider.GetRequiredService، مجددا معرفی خواهند شد.
پس از این تنظیمات، اگر سرویس IOrderHandler را از طریق سیستم تزریق وابستگیها درخواست کنید، وابستگیهای تزریق شدهی در سازندهی آن فقط زمانی و در محلی وهله سازی میشوند که از طریق خاصیت Value شیء Lazy آنها مورد استفاده قرار گرفته شده باشند.
مثال کامل IOrderHandler را از فایل پیوستی انتهای مطلب میتوانید دریافت. اگر آنرا اجرا کنید (برنامهی کنسول آنرا)، در خروجی آن، فقط اجرا شدن سازندهی سرویسی را مشاهده میکنید که مورد استفاده قرار گرفته و نه وابستگی دومی که تزریق شده، اما استفاده نشدهاست.
مثال 3: چگونه بجای اینترفیسها، یک وهله از کلاسی مشخص را از سیستم تزریق وابستگیها درخواست کنیم؟
فرض کنید سرویسی را به صورت زیر به سیستم تزریق وابستگیها معرفی کردهاید:
services.AddTransient<IMyDisposableService, MyDisposableService>();
public class AnotherController { public AnotherController(MyDisposableService customerService) { // ... } }
An unhandled exception occurred while processing the request. InvalidOperationException: Unable to resolve service for type ‘MyDisposableService’ while attempting to activate ‘AnotherController’. Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, bool isDefaultParameterRequired)
services.AddTransient<IMyDisposableService, MyDisposableService>(); services.AddTransient<MyDisposableService>(serviceProvider => serviceProvider.GetRequiredService<IMyDisposableService>() as MyDisposableService);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: CoreDependencyInjectionSamples-06.zip
امن سازی برنامههای 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 وارد کنید.
امضای چندگانه ( Multi-signature ) به عنوان تأیید اعتبار چند علامتی یا چند عاملی شناخته میشود، یک ویژگی امنیتی است که برای محافظت از داراییهای دیجیتالی مانند ارز دیجیتال، امضای دیجیتال و سایر اطلاعات حساس استفاده میشود. این فرآیندی است که شامل الزام چندین طرف برای مجوز معامله یا اقدام، به جای تنها یک فرد است. این یک لایه امنیتی اضافی ایجاد میکند و خطر کلاهبرداری، سرقت یا دسترسی غیرمجاز را کاهش میدهد.
public async Task<IActionResult> UploadFile(string id, [FromForm] IFormFile file)