بر روی ماشینهای ویندوزی و ویندوزهای سرور، استفادهی از IIS به عنوان پروکسی درخواستها و ارسال آنها به Kestrel، روش توصیه شدهاست؛ از این جهت که حداقل قابلیتهایی مانند «port 80/443 forwarding»، مدیریت طول عمر برنامه، مدیریت مجوزهای SLL آن و خیلی از موارد دیگر توسط Kestrel پشتیبانی نمیشود.
معماری پردازش نگارشهای پیشین ASP.NET در IIS
در نگارشهای پیشین ASP.NET، همه چیز داخل پروسهای به نام w3wp.exe و یا IIS Worker Process پردازش میشود که در اصل چیزی نیست بجز همان IIS Application Pool. این AppPoolها، برنامههای ASP.NET شما را هاست میکنند و همچنین سبب وهله سازی و اجرای آنها نیز خواهند شد.
در اینجا درایور http.sys ویندوز، درخواستهای رسیده را دریافت کرده و سپس آنها را به سمت سایتهایی نگاشت شدهی به AppPoolهای مشخص، هدایت میکند.
معماری پردازش برنامههای ASP.NET Core در IIS
روش اجرای برنامههای ASP.NET Core با نگارشهای پیشین آنها کاملا متفاوت هستند؛ از این جهت که داخل پروسهی w3wp.exe اجرا نمیشوند. این برنامهها در یک پروسهی مجزای کنسول خارج از پروسهی w3wp.exe اجرا میشوند و حاوی وب سرور توکاری به نام کسترل (Kestrel) هستند.
این وب سرور، وب سروری است تماما دات نتی و به شدت برای پردازش تعداد بالای درخواستها بهینه سازی شدهاست؛ تا جایی که کارآیی آن در این یک مورد چند 10 برابر IIS است. هرچند این وب سرور فوق العاده سریع است، اما «تنها» یک وب سرور خام است و به همراه سرویسهای مدیریت وب، مانند IIS نیست.
در تصویر فوق مفهوم «پروکسی» بودن IIS را در حین پردازش برنامههای ASP.NET Core بهتر میتوان درک کرد. ابتدا درخواستهای رسیده به IIS میرسند و سپس IIS آنها را به طرف Kestrel هدایت میکند.
برنامههای ASP.NET Core، برنامههای کنسول متکی به خودی هستند که توسط دستور خط فرمان dotnet اجرا میشوند. این اجرا توسط ماژولی ویژه به نام AspNetCoreModule در IIS انجام میشود.
همانطور که در تصویر نیز مشخص است، AspNetCoreModule یک ماژول بومی IIS است و هنوز برای اجرا نیاز به IIS Application Pool دارد؛ با این تفاوت که در تنظیم AppPoolهای برنامههای ASP.NET Core، باید NET CLR Version. را به No managed code تنظیم کرد.
اینکار از این جهت صورت میگیرد که IIS در اینجا تنها نقش یک پروکسی هدایت درخواستها را به پروسهی برنامهی حاوی وب سرور Kestrel، دارد و کار آن وهله سازی NET Runtime. نیست. کار AspNetCoreModule این است که با اولین درخواست رسیدهی به برنامهی شما، آنرا بارگذاری کند. سپس درخواستهای رسیده را دریافت و به سمت برنامهی ASP.NET Core شما هدایت میکند (به این عملیات reverse proxy هم میگویند).
اگر دقت کرده باشید، برنامههای ASP.NET Core، هنوز دارای فایل web.config ایی با محتوای ذیل هستند:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <handlers> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/> </handlers> <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/> </system.webServer> </configuration>
یک نکته: در زمان publish برنامه، تنظیم و تبدیل مقادیر LAUNCHER_PATH و LAUNCHER_ARGS به معادلهای اصلی آنها صورت میگیرد (در ادامه مطلب بحث خواهد شد).
آیا واقعا هنوز نیازی به استفادهی از IIS وجود دارد؟
هرچند میتوان Kestrel را توسط یک IP و پورت مشخص، عمومی کرد و استفاده نمود، اما حداقل در ویندوز چنین توصیهای نمیشود و بهتر است از IIS به عنوان یک front end proxy استفاده کرد؛ به این دلایل:
- اگر میخواهید چندین برنامه را بر روی یک وب سرور که از طریق پورتهای 80 و 443 ارائه میشوند داشته باشید، نمیتوانید از Kestrel به صورت مستقیم استفاده کنید؛ زیرا از مفهوم host header routing که قابلیت ارائهی چندین برنامه را از طریق پورت 80 و توسط یک IP میسر میکند، پشتیبانی نمیکند. برای اینکار نیاز به IIS و یا در حقیقت درایور http.sys ویندوز است.
- IIS خدمات قابل توجهی را به برنامهی شما ارائه میکند. برای مثال با اولین درخواست رسیده، به صورت خودکار آنرا اجرا و بارگذاری میکند؛ به همراه تمام مدیریتهای پروسهای که در اختیار برنامههای ASP.NET در طی سالیان سال قرار داشتهاست. برای مثال اگر پروسهی برنامهی شما در اثر استثنایی کرش کرد، دوباره با درخواست بعدی رسیده، حتما برنامه را بارگذاری و آمادهی خدمات دهی مجدد میکند.
- در اینجا میتوان تنظیمات SSL را بر روی IIS انجام داد و سپس درخواستهای معمولی را به Kestrel ارسال کرد. به این ترتیب با یک مجوز میتوان چندین برنامهی Kestrel را مدیریت کرد.
- IISهای جدید به همراه ماژولهای بومی بسیار بهینه و کم مصرفی برای مواردی مانند gzip compression of static content, static file caching, Url Rewriting هستند که با فعال سازی آنها میتوان از این قابلیتها، در برنامههای ASP.NET Core نیز استفاده کرد.
نحوهی توزیع برنامههای ASP.NET Core به IIS
روش اول: استفاده از دستور خط فرمان dotnet publish
برای این منظور به ریشهی پروژهی خود وارد شده و دستور dotnet publish را با توجه به پارامترهای ذیل اجرا کنید:
dotnet publish --framework netcoreapp1.0 --output "c:\temp\mysite" --configuration Release
{ "publishOptions": { "include": [ "wwwroot", "Features", "appsettings.json", "web.config" ] }, "scripts": { "precompile": [ "dotnet bundle" ], "prepublish": [ //"bower install" ], "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] } }
پس از انتقال این فایلها به سرور، مابقی مراحل آن مانند قبل است. یک Application جدید تعریف شده و سپس ابتدا مسیر آن مشخص میشود و نکتهی اصلی، انتخاب AppPool ایی است که پیشتر شرح داده شد:
برنامههای ASP.NET Core باید به AppPool ایی تنظیم شوند که NET CLR Version. آنها No Managed Code است. همچنین بهتر است به ازای هر برنامهی جدید یک AppPool مجزا را ایجاد کنید تا کرش یک برنامه تاثیر منفی را بر روی برنامهی دیگری نگذارد.
روش دوم: استفاده از ابزار Publish خود ویژوال استودیو
اگر علاقمند هستید که روش خط فرمان فوق را توسط ابزار publish ویژوال استودیو انجام دهید، بر روی پروژه در solution explorer کلیک راست کرده و گزینهی publish را انتخاب کنید. در صفحهای که باز میشود، بر روی گزینهی custom کلیک کرده و نامی را وارد کنید. از این نام پروفایل، جهت ساده سازی مراحل publish، در دفعات آتی فراخوانی آن استفاده میشود.
در صفحهی بعدی اگر گزینهی file system را انتخاب کنید، دقیقا همان مراحل روش اول تکرار میشوند:
سپس میتوانید فریم ورک برنامه و نوع ارائه را مشخص کنید:
و در آخر کار، Publish به این پوشهی مشخص شده که به صورت پیش فرض در ذیل پوشهی bin برنامهاست، صورت میگیرد.
روش عیب یابی راه اندازی اولیهی برنامههای ASP.NET Core
در اولین سعی در اجرای برنامهی ASP.NET Core بر روی IIS به این خطا رسیدم:
در event viewer ویندوز چیزی ثبت نشده بود. اولین کاری را که در این موارد میتوان انجام داد به این صورت است. از طریق خط فرمان به پوشهی publish برنامه وارد شوید (همان پوشهای که توسط IIS عمومی شدهاست). سپس دستور dotnet prog.dll را صادر کنید. در اینجا prog.dll نام dll اصلی برنامه یا همان نام پروژه است:
همانطور که مشاهده میکنید، برنامه به دنبال پوشهی bower_components ایی میگردد که کار publish آن انجام نشدهاست (این پوشه در تنظیمات آغازین برنامه عمومی شدهاست و در لیست include قسمت publishOptions فایل project.json فراموش شدهاست).
روش دوم، فعال سازی stdoutLogEnabled موجود در فایل وب کانفیگ، به true است. در اینجا web.config نهایی تولیدی توسط عملیات publish را مشاهده میکنید که در آن پارامترهای processPath و arguments مقدار دهی شدهاند (همان قسمت postpublish فایل project.json). در اینجا مقدار stdoutLogEnabled به صورت پیش فرض false است. اگر true شود، همان خروجی تصویر فوق را در پوشهی logs خواهید یافت:
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <handlers> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" /> </handlers> <aspNetCore processPath="dotnet" arguments=".\Core1RtmEmptyTest.dll" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" /> </system.webServer> </configuration>
Warning: Could not create stdoutLogFile \\?\D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest\bin\Release\PublishOutput\logs\stdout_10064_201672893654.log, ErrorCode = -2147024893.
حداقلهای یک هاست ویندوزی که میخواهد برنامههای ASP.NET Core را ارائه دهد
پس از نصب IIS، نیاز است ASP.NET Core Module نیز نصب گردد. برای اینکار اگر بستهی NET Core Windows Server Hosting. را نصب کنید، کافی است:
https://go.microsoft.com/fwlink/?LinkId=817246
این بسته به همراه NET Core Runtime, .NET Core Library. و ASP.NET Core Module است. همچنین همانطور که عنوان شد، برنامههای ASP.NET Core باید به AppPool ایی تنظیم شوند که NET CLR Version. آنها No Managed Code است. اینها حداقلهای راه اندازی یک برنامهی ASP.NET Core بر روی سرورهای ویندوزی هستند.
هنوز فایل app_offline.htm نیز در اینجا معتبر است
یکی از خواص ASP.NET Core Module، پردازش فایل خاصی است به نام app_offline.htm. اگر این فایل را در ریشهی سایت قرار دهید، برنامه پردازش تمام درخواستهای رسیده را قطع خواهد کرد و سپس پروسهی برنامه خاتمه مییابد. هر زمانیکه این فایل حذف شد، مجددا با درخواست بعدی رسیده، برنامه آمادهی پاسخگویی میشود.
{ "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" } }
ابزارهای سراسری در NET Core 2.1.
dotnet new tool-manifest
{ "version": 1, "isRoot": true, "tools": {} }
dotnet tool install Cake.Tool
{ "version": 1, "isRoot": true, "tools": { "cake.tool": { "version": "0.34.1", "commands": [ "dotnet-cake" ] } } }
dotnet tool restore
معرفی Dotnet-Monitor
When running a dotnet application differences in diverse local and production environments can make collecting diagnostics artifacts (e.g., logs, traces, process dumps) challenging. dotnet-monitor
aims to simplify the process by exposing a consistent REST API regardless of where your application is run.
قبل از بررسی توابع، Script زیر را اجرا مینماییم، که شامل جدولی به نام Testو درج چند رکورد درون آن میباشد:
CREATE TABLE Test (ID INT, Product VARCHAR(100), Price INT, Color VARCHAR(100)) GO INSERT INTO Test SELECT 1, 'Toy', 100, 'Black' UNION ALL SELECT 2, 'Pen', 100, 'Black' UNION ALL SELECT 3, 'Pencil', 100, 'Blue' UNION ALL SELECT 4, 'Pencil', 100, 'Red' UNION ALL SELECT 5, 'Pencil', 200, 'Yellow' UNION ALL SELECT 6, 'Cup', 300, 'Orange' UNION ALL SELECT 7, 'Cup', 400, 'Brown' GO
ROW_NUMBER () OVER ([<partition_by_clause>] <order_by_clause>)
در ابتدا Query زیر را اجرا نمایید:
Select *, ROW_NUMBER() OVER ( ORDER BY Price DESC) AS RN from Test
- لازم به یادآوری است که استفاده از Order by در Syntax تابع Row_Number الزامی میباشد.
برای درک بیشتر Query زیر را اجرا نمایید:
Select *,ROW_NUMBER() OVER (PARTITION BY Product ORDER BY Price DESC) AS RN from Test
همانطور که در شکل مشاهده مینمایید، در ابتدا، جدول براساس فیلد Product، دسته بندی (Group by) شده است و سپس اعداد ترتیبی روی هر Group by بصورت جداگانه اعمال شده است.
تابع ()RANK
از تابع فوق در جهت رتبه بندی نمودن فیلدهای یک جدول استفاده میشود و Syntax آن بصورت زیر میباشد:
RANK () OVER ([<partition_by_clause>] <order_by_clause>)
ابتدا Query زیر را اجرا مینماییم:
Select *,RANK() over (ORDER BY Price ) AS RANK from Test
یادآوری: زمانی که دورن Order by ترتیب صعودی یا نزولی بودن را تعیین نکنیم، Order by بصورت پیش فرض صعودی میباشد.
همانطور که در شکل مشاهده مینمایید،رتبه بندی انجام شده به ترتیب نمیباشد، و برای مقادیر تکراری فیلد Price از Rank یکسانی استفاده شده است. نکته دیگر این که بین اعداد مشاهده شده در فیلد Rank نیز gap ایجاد میشود. به عبارت دیگر عمده تفاوت تابع Rank با تابع Row_Number همین مواردی است که بیان شده است.
در Syntax تابع Rank نیز کلمه Partition هم وجود دارد، که در جهت Group by فیلد یا فیلدهای خاصی استفاده میشود، و رتبه بندی نیز در این حالت روی Group by انجام میگردد.
برای درک بهتر Query زیر را اجرا نمایی:
Select *,RANK() over (Partition by Product ORDER BY Price Desc) AS RANK from Test
خروجی بصورت زیر خواهد بود:
همانطور که در شکل مشاهده مینمایید، رتبه بندی روی هر Group by بصورت جداگانه اعمال شده است.
تابع Dense_Rank
این تابع نیز همانند تابع Rank عمل میکند، با این تفاوت که هیچ gap ی بین اعداد آن رخ نمیدهد.
با جرای Query زیر خواهیم داشت:
Select *,dense_RANK() over (ORDER BY Price ) AS dense_RANK from Test
خروجی بصورت زیر خواهد بود:
همانطور که ملاحظه مینماییدهیچ gap ی بین اعداد Rank ایجاد نشده است.
و برای استفاده از Partition، درتابع Dense_Rank همانند تابعهای دیگر میباشد.
تابع NTILE:
این تابع نیز مانند توابع بالا در جهت رتبه بندی استفاده میشود، و بوسیله تابع فوق شما میتوانید رکوردهای جدول خود را به تعداد گروههای دلخواه تقسیم نمایید.و Syntaxآن بصورت زیر میباشد:
NTILE (integer_expression) OVER ([<partition_by_clause>] <order_by_clause>)
برای درک مطلب فوق مثالی میزنیم:
Select * ,NTILE(4) over ( ORDER BY Price desc) from Test
خروجی بصورت زیر خواهد بود:
در Syntax تابع فوق اشاره به Integer_Expressionشده است.که یک مقدار عددی دریافت میکند و بیانگر تعداد گروه بندی دلخواه میباشد.
حال سئوال اینجاست که رتبه بندی جدول به چه صورت انجام شده است:
همانطور که مشاهده مینمایید، جدول فوق شامل 7 رکورد میباشد،و ما در مثال خود،تمایل داشتیم که رکوردهای جدول به چهار گروه تقسیم و سپس رتبه بندی شوند، بنابراین 7 تقسیم بر 4 شده است و باقی مانده آن میشود 3
پس خواهیم داشت7=3+1*4
در ابتدا چهار گروه ایجاد میشودو در هر خانه یک رکورد قرار میگیرد
سپس 3 رکورد باقی میماند که از اولین گروه رو به پایین ، برای هر گروه فقط یک رکورد درج میشود، یعنی یک رکورد به گروه یک،یک رکورد به گروه 2 و هم چنین یک رکورد به گروه 3 بنابراین خواهیم داشت:
نکته مهم: اگر تعداد رکورد باقی مانده بعد از تقسیم بیش از یک عدد باشد، در زمان اختصاص دادن به گروه ها، به هر گروه از بالا به پایین فقط یک رکورد اختصاص داده میشود.
مثالی دیگر:
Select *,NTILE(3) over ( ORDER BY Price desc) AS NTILE from Test
خروجی:
در این حالت 7=1+2*3
امیدوارم مطلب فوق مفید واقع شده باشد.
تعریف موجودیت و DbSet تصاویر یک اتاق هتل
برای اینکه بتوان اطلاعات تصاویر آپلودی را در بانک اطلاعاتی ثبت کرد، نیاز است یک رابطهی یک به چند را بین یک اتاق و تصاویر مرتبط با آن برقرار کرد. به همین جهت ابتدا به پروژهی BlazorServer.Entities.csproj مراجعه کرده و موجودیت ثبت اطلاعات تصاویر را تعریف میکنیم:
using System.ComponentModel.DataAnnotations.Schema; namespace BlazorServer.Entities { public class HotelRoomImage { public int Id { get; set; } public string RoomImageUrl { get; set; } [ForeignKey("RoomId")] public virtual HotelRoom HotelRoom { get; set; } public int RoomId { get; set; } } }
namespace BlazorServer.Entities { public class HotelRoom { // ... public virtual ICollection<HotelRoomImage> HotelRoomImages { get; set; } } }
namespace BlazorServer.DataAccess { public class ApplicationDbContext : DbContext { public DbSet<HotelRoomImage> HotelRoomImages { get; set; } // ... } }
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
تعریف مدل UI متناظر با هر تصویر
همانطور که در قسمت 13 نیز عنوان شد، در حین کار با رابط کاربری برنامه، با موجودیتهای بانک اطلاعاتی، به صورت مستقیم کار نخواهیم کرد و بر اساس نیازهای برنامه، یکسری کلاس DTO را تعریف میکنیم. بنابراین به پروژهی BlazorServer.Models مراجعه کرده و DTO متناظر با HotelRoomImage را به صورت زیر اضافه میکنیم:
namespace BlazorServer.Models { public class HotelRoomImageDTO { public int Id { get; set; } public int RoomId { get; set; } public string RoomImageUrl { get; set; } } }
using AutoMapper; using BlazorServer.Entities; namespace BlazorServer.Models.Mappings { public class MappingProfile : Profile { public MappingProfile() { // ... CreateMap<HotelRoomImageDTO, HotelRoomImage>().ReverseMap(); // two-way mapping } } }
تعریف سرویس کار با HotelRoomImage
در اینجا نیز همانند سرویسی که برای انجام عملیات تجاری مرتبط با یک اتاق هتل، در قسمت 13 پیاده سازی کردیم، سرویس دیگری را در پروژهی BlazorServer.Services برای کار با تصاویر اتاقها تهیه میکنیم:
namespace BlazorServer.Services { public interface IHotelRoomImageService { Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO); Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId); Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId); Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId); } }
namespace BlazorServer.Services { public class HotelRoomImageService : IHotelRoomImageService { private readonly ApplicationDbContext _dbContext; private readonly IMapper _mapper; private readonly IConfigurationProvider _mapperConfiguration; public HotelRoomImageService(ApplicationDbContext dbContext, IMapper mapper) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapperConfiguration = mapper.ConfigurationProvider; } public async Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO) { var image = _mapper.Map<HotelRoomImage>(imageDTO); await _dbContext.HotelRoomImages.AddAsync(image); return await _dbContext.SaveChangesAsync(); } public async Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId) { var image = await _dbContext.HotelRoomImages.FindAsync(imageId); _dbContext.HotelRoomImages.Remove(image); return await _dbContext.SaveChangesAsync(); } public async Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId) { var imageList = await _dbContext.HotelRoomImages.Where(x => x.RoomId == roomId).ToListAsync(); _dbContext.HotelRoomImages.RemoveRange(imageList); return await _dbContext.SaveChangesAsync(); } public Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId) { return _dbContext.HotelRoomImages .Where(x => x.RoomId == roomId) .ProjectTo<HotelRoomImageDTO>(_mapperConfiguration) .ToListAsync(); } } }
namespace BlazorServer.App { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IHotelRoomImageService, HotelRoomImageService>(); // ...
تهیه سرویسی برای آپلود فایلهای یک برنامهی Blazor Server به سرور
جهت ساده سازی کار آپلود، در برنامههای Blazor Server، سرویس جدید FileUploadService را به پروژهی BlazorServer.Services اضافه میکنیم:
using Microsoft.AspNetCore.Components.Forms; using System.Threading.Tasks; namespace BlazorServer.Services { public interface IFileUploadService { void DeleteFile(string fileName, string webRootPath, string uploadFolder); Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder); } }
using Microsoft.AspNetCore.Components.Forms; using System; using System.IO; using System.Threading.Tasks; namespace BlazorServer.Services { public class FileUploadService : IFileUploadService { private const int MaxBufferSize = 0x10000; public void DeleteFile(string fileName, string webRootPath, string uploadFolder) { var path = Path.Combine(webRootPath, uploadFolder, fileName); if (File.Exists(path)) { File.Delete(path); } } public async Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder) { createUploadDir(webRootPath, uploadFolder); var (fileName, imageFilePath) = getOutputFileInfo(inputFile, webRootPath, uploadFolder); using (var outputFileStream = new FileStream( imageFilePath, FileMode.Create, FileAccess.Write, FileShare.None, MaxBufferSize, useAsync: true)) { using var inputStream = inputFile.OpenReadStream(); await inputStream.CopyToAsync(outputFileStream); } return $"{uploadFolder}/{fileName}"; } private static (string FileName, string FilePath) getOutputFileInfo( IBrowserFile inputFile, string webRootPath, string uploadFolder) { var fileName = Path.GetFileName(inputFile.Name); var imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName); if (File.Exists(imageFilePath)) { var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); var fileExtension = Path.GetExtension(fileName); fileName = $"{fileNameWithoutExtension}-{Guid.NewGuid()}{fileExtension}"; imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName); } return (fileName, imageFilePath); } private static void createUploadDir(string webRootPath, string uploadFolder) { var folderDirectory = Path.Combine(webRootPath, uploadFolder); if (!Directory.Exists(folderDirectory)) { Directory.CreateDirectory(folderDirectory); } } } }
همچنین برای دسترسی به IBrowserFile در یک سرویس، نیاز است وابستگی زیر را نیز به پروژهی سرویسها اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.3" /> </ItemGroup> </Project>
namespace BlazorServer.App { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IFileUploadService, FileUploadService>(); // ...
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-16.zip
برای مثال در برنامههای ASP.NET Core، یک چنین فرمی:
<form asp-controller="Manage" asp-action="ChangePassword" method="post"> <!-- Form details --> </form>
<form method="post" action="/Manage/ChangePassword"> <!-- Form details --> <input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkSldwD9CpLR...LongValueHere!" /> </form>
تولید خودکار کوکیهای Anti-forgery tokens برای برنامههای Angular
در سمت Angular، مطابق مستندات رسمی آن (^ و ^)، اگر کوکی تولید شدهی توسط برنامه، دارای نام مشخص «XSRF-TOKEN» باشد، کتابخانهی HTTP آن به صورت خودکار مقدار آنرا استخراج کرده و به درخواست بعدی ارسالی آن اضافه میکند. بنابراین در سمت ASP.NET Core تنها کافی است کوکی مخصوص فوق را تولید کرده و به Response اضافه کنیم. مابقی آن توسط Angular به صورت خودکار مدیریت میشود.
میتوان اینکار را مستقیما داخل متد Configure کلاس آغازین برنامه انجام داد و یا بهتر است جهت حجیم نشدن این فایل و مدیریت مجزای این مسئولیت، یک میانافزار مخصوص آنرا تهیه کرد:
using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace AngularTemplateDrivenFormsLab.Utils { public class AntiforgeryTokenMiddleware { private readonly RequestDelegate _next; private readonly IAntiforgery _antiforgery; public AntiforgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery) { _next = next; _antiforgery = antiforgery; } public Task Invoke(HttpContext context) { var path = context.Request.Path.Value; if (path != null && !path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { var tokens = _antiforgery.GetAndStoreTokens(context); context.Response.Cookies.Append( key: "XSRF-TOKEN", value: tokens.RequestToken, options: new CookieOptions { HttpOnly = false // Now JavaScript is able to read the cookie }); } return _next(context); } } public static class AntiforgeryTokenMiddlewareExtensions { public static IApplicationBuilder UseAntiforgeryToken(this IApplicationBuilder builder) { return builder.UseMiddleware<AntiforgeryTokenMiddleware>(); } } }
- در اینجا ابتدا سرویس IAntiforgery به سازندهی کلاس میان افزار تزریق شدهاست. به این ترتیب میتوان به سرویس توکار تولید توکنهای Antiforgery دسترسی یافت. سپس از این سرویس جهت دسترسی به متد GetAndStoreTokens آن برای دریافت محتوای رشتهای نهایی این توکن استفاده میشود.
- اکنون که به این توکن دسترسی پیدا کردهایم، تنها کافی است آنرا با کلید مخصوص XSRF-TOKEN که توسط Angular شناسایی میشود، به مجموعهی کوکیهای Response اضافه کنیم.
- علت تنظیم مقدار خاصیت HttpOnly به false، این است که کدهای جاوا اسکریپتی Angular بتوانند به مقدار این کوکی دسترسی پیدا کنند.
پس از تدارک این مقدمات، کافی است متد الحاقی کمکی UseAntiforgeryToken فوق را به نحو ذیل به متد Configure کلاس آغازین برنامه اضافه کنیم؛ تا کار نصب میان افزار AntiforgeryTokenMiddleware، تکمیل شود:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseAntiforgeryToken();
پردازش خودکار درخواستهای ارسالی از طرف Angular
تا اینجا برنامهی سمت سرور ما کوکیهای مخصوص Angular را با کلیدی که توسط آن شناسایی میشود، تولید کردهاست. در پاسخ، Angular این کوکی را در هدر مخصوصی به نام «X-XSRF-TOKEN» به سمت سرور ارسال میکند (ابتدای آن یک X اضافهتر دارد).
به همین جهت به متد ConfigureServices کلاس آغازین برنامه مراجعه کرده و این هدر مخصوص را معرفی میکنیم تا دقیقا مشخص گردد، این توکن از چه قسمتی باید جهت پردازش استخراج شود:
public void ConfigureServices(IServiceCollection services) { services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN"); services.AddMvc(); }
یک نکته: اگر میخواهید این کلیدهای هدر پیش فرض Angular را تغییر دهید، باید یک CookieXSRFStrategy سفارشی را برای آن تهیه کنید.
اعتبارسنجی خودکار Anti-forgery tokens در برنامههای ASP.NET Core
ارسال کوکی اطلاعات Anti-forgery tokens و سپس دریافت آن توسط برنامه، تنها یک قسمت از کار است. قسمت بعدی، بررسی معتبر بودن آنها در سمت سرور است. روش متداول انجام اینکار، افزودن ویژگی [ValidateAntiForgeryToken] به هر اکشن متد مزین به [HttpPost] است:
[HttpPost] [ValidateAntiForgeryToken] public IActionResult ChangePassword() { // ... return Json(…); }
public void ConfigureServices(IServiceCollection services) { services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN"); services.AddMvc(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); }); }
یک نکته: در این حالت بررسی سراسری، اگر در موارد خاصی نیاز به این اعتبارسنجی خودکار نبود، میتوان از ویژگی [IgnoreAntiforgeryToken] استفاده کرد.
آزمایش برنامه
برای آزمایش مواردی را که تا کنون بررسی کردیم، همان مثال «فرمهای مبتنی بر قالبها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» را بر اساس نکات متدهای ConfigureServices و Configure مطلب جاری تکمیل میکنیم. سپس برنامه را اجرا میکنیم:
همانطور که ملاحظه میکنید، در اولین بار درخواست برنامه، کوکی مخصوص Angular تولید شدهاست.
در ادامه اگر فرم را تکمیل کرده و ارسال کنیم، وجود هدر ارسالی از طرف Angular مشخص است و همچنین خروجی هم با موفقیت دریافت شدهاست:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-09.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
دریافت کتاب Pro ASP.NET Core MVC
NET Core 3.0 Preview 6. منتشر شد
Today, we are announcing .NET Core 3.0 Preview 6. It includes updates for compiling assemblies for improved startup, optimizing applications for size with linker and EventPipe improvements. We’ve also released new Docker images for Alpine on ARM64.