<a href="/">Test</a>
چند روش استفاده از ng-class در anuglarjs بررسی میکند
String syntax
<div ng-class="textType">Look! I'm Words!</div>
<div ng-class="[styleOne, styleTwo]">Look! I'm Words!</div>
<div ng-class="{ 'text-success': awesome, 'text-large': giant }">
ng-class="$even ? 'even-row' : 'odd-row'">
و .......
- Solution Explorer: touch pad gesture scroll is too sensitive.
- C++/CLI: std::move causes std::unique_ptr parameter to be destructed before function call.
- live share shows "failed to sign in" .
- VS symbol search fails to find symbols.
- Undo randomly stops working.
- VS "Go to defintion" functionality frequently does not work.
- Linux CMake Intellisense HeaderCache not downloading headers for 15.8.0 Preview 4.0.
- When opening a project that requires elevated permissions, Visual Studio does not automatically open the selected project after restarting elevated.
- vcpkgsrv.exe crashes.
- New Visual Studio Code keymap not applying?.
آماده سازی مقدمات پروژهی آزمون واحد
در ادامهی مثال این سری، پروژهی جدید NotifyPropertyChangedGenerator.Tests را از نوع class library با تنظیمات فایل csproj. زیر ایجاد میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.2.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> <PackageReference Include="MSTest.TestFramework" Version="2.2.10" /> <PackageReference Include="MSTest.TestAdapter" Version="2.2.10" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj" /> </ItemGroup> </Project>
ایجاد یک کلاس کمکی برای اجرای Source Generators در پروژههای آزمون واحد
در اینجا میخواهیم همان کاری را که کامپایلر سیشارپ در پشت صحنه انجام میدهد، شبیه سازی کنیم تا بتوانیم یک تولید کنندهی کد را به مراحل کامپایل کد، معرفی و سپس آنرا اجرا کنیم:
internal static class SourceGeneratorTestsExtensions { public static (GeneratorDriver Driver, Compilation OutputCompilation, ImmutableArray<Diagnostic> Diagnostics) RunGenerators(this string source, params ISourceGenerator[] generators) { var references = AppDomain.CurrentDomain.GetAssemblies() .Where(assembly => !assembly.IsDynamic) .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)) .Cast<MetadataReference>(); var inputCompilation = CSharpCompilation.Create("compilation", new[] { CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest)) }, references, new CSharpCompilationOptions(OutputKind.ConsoleApplication)); GeneratorDriver driver = CSharpGeneratorDriver.Create(generators); driver = driver.RunGeneratorsAndUpdateCompilation( inputCompilation, out var outputCompilation, out var diagnostics); return (driver, outputCompilation, diagnostics); } }
نوشتن اولین آزمون واحد مخصوص یک تولید کنندهی کد
پس از تهیهی متدی که میتواند یک قطعه کد و تعدادی Source Generator را به کامپایلر سیشارپ، جهت پردازش معرفی کند، یک نمونه نحوهی استفادهی از آن جهت نوشتن آزمونهای واحد کاملا مستقل و متکی به خود، به صورت زیر است:
using Microsoft.VisualStudio.TestTools.UnitTesting; using PropertyChangedGenerator = NotifyPropertyChangedGenerator.NotifyPropertyChangedGenerator; namespace NotifyPropertyChangedGenerator.Tests; [TestClass] public class GeneratorTest { [TestMethod] public void SimpleGeneratorTest() { var userSource = @" using System; using System.ComponentModel; namespace NotifyPropertyChangedDemo { public class Test : INotifyPropertyChanged { private int regularField; private int IndexBackingField; } } "; var (driver, outputCompilation, diagnostics) = userSource.RunGenerators(new PropertyChangedGenerator()); var newFile = outputCompilation.SyntaxTrees .Single(x => Path.GetFileName(x.FilePath).EndsWith(".Test.cs")); Assert.IsNotNull(newFile); Assert.IsTrue(newFile.FilePath.EndsWith("Test.Notify.Test.cs")); var generatedSource = newFile.GetText().ToString(); Assert.IsTrue(generatedSource.Contains("namespace NotifyPropertyChangedDemo")); // We can now assert things about the resulting compilation: Assert.IsTrue(diagnostics.IsEmpty); // there were no diagnostics created by the generators // we have two syntax trees, the original 'user' provided one, and the one added by the generator Assert.IsTrue(outputCompilation.SyntaxTrees.Count() == 2); // verify the compilation with the added source has no diagnostics Assert.IsTrue(outputCompilation.GetDiagnostics().IsEmpty); } }
- سپس این قطعه کد و نمونهای از تولید کنندهی کد را به کامپایلر ارسال و اجرا کردهایم.
- اکنون بر اساس خروجی کامپایلر برای مثال میتوان به فایل تولید شده و SyntaxTrees آن دسترسی پیدا کرد و یا با کمک متد GetText، به کل محتوای این فایل تولید شده دسترسی یافت و برای مثال آنرا با مقداری که انتظار داریم مقایسه کرد تا به این ترتیب بتوان از صحت عملکرد تولید کنندهی کد، اطمینان حاصل نمود.
- همانطور که عنوان شد، اکنون قرار دادن break-point در قسمتهای مختلف آزمون واحد تهیه شده بسیار سادهاست و به این ترتیب میتوان یک چنین پروژههایی را در تمام IDEها دیباگ کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: SourceGeneratorTests-part5.zip
انتقال WebAssembly به سرور یا WASI
Bringing WebAssembly to the .NET Mainstream - Steve Sanderson, Microsoft
Many developers still consider WebAssembly to be a leading-edge, niche technology tied to low-level systems programming languages. However, C# and .NET (open-source, cross-platform technologies used by nearly one-third of all professional developers [1]) have run on WebAssembly since 2017. Blazor WebAssembly brought .NET into the browser on open standards, and is now one of the fastest-growing parts of .NET across enterprises, startups, and hobbyists. Next, with WASI we could let you run .NET in even more places, introducing cloud-native tools and techniques to a wider segment of the global developer community. This is a technical talk showing how we bring .NET to WebAssembly. Steve will demonstrate how it runs both interpreted and AOT-compiled, how an IDE debugger can attach, performance tradeoffs, and how a move from Emscripten to WASI SDK lets it run in Wasmtime/Wasmer or higher-level runtimes like wasmCloud. Secondly, you'll hear lessons learned from Blazor as an open-source project - challenges and misconceptions faced bringing WebAssembly beyond early adopters. [1] StackOverflow survey 2021
پیشنیازهای کار با EF Core Migrations
در قسمت قبل در حین بررسی «برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است.
بررسی ابزارهای خط فرمان EF Core و تشکیل ساختار بانک اطلاعاتی بر اساس کلاسهای برنامه
پس از تکمیل پیشنیازهای کار با EF Core، از طریق خط فرمان به پوشهی جاری پروژه وارد شده و دستور dotnet ef را صادر کنید.
یک نکته: در ویندوز اگر در پوشهای، کلید shift را نگه دارید و در آن پوشه کلیک راست کنید، در منوی باز شده، گزینهی جدیدی را به نام Open command window here مشاهده خواهید کرد که میتواند به سرعت خط فرمان را از پوشهی جاری شروع کند.
خروجی صدور فرمان dotnet ef را در ذیل مشاهده میکنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef _/\__ ---==/ \\ ___ ___ |. \|\ | __|| __| | ) \\\ | _| | _| \_/ | //|\\ |___||_| / \\\/\\ Entity Framework .NET Core CLI Commands 1.0.0-preview2-21431 Usage: dotnet ef [options] [command] Options: -h|--help Show help information -v|--verbose Enable verbose output --version Show version information --assembly <ASSEMBLY> The assembly file to load. --startup-assembly <ASSEMBLY> The assembly file containing the startup class. --data-dir <DIR> The folder used as the data directory (defaults to current working directory). --project-dir <DIR> The folder used as the project directory (defaults to current working directory). --content-root-path <DIR> The folder used as the content root path for the application (defaults to application base directory). --root-namespace <NAMESPACE> The root namespace of the target project (defaults to the project assembly name). Commands: database Commands to manage your database dbcontext Commands to manage your DbContext types migrations Commands to manage your migrations Use "dotnet ef [command] --help" for more information about a command.
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase
نام دلخواه InitialDatabase را در انتهای نام فایل 13950526050417_InitialDatabase مشاهده میکنید.
اگر قصد حذف این مرحله را داشته باشیم، میتوان دستور dotnet ef migrations remove را مجددا صادر کرد.
فایل 13950526050417_InitialDatabase به همراه کلاسی است که در آن دو متد Up و Down قابل مشاهده هستند. متد Up نحوهی ایجاد جدول جدیدی را از کلاس Person بیان میکند و متد Down نحوهی Drop این جدول را پیاده سازی کردهاست.
فایل ApplicationDbContextModelSnapshot.cs دارای کلاسی است که خلاصهای از تعاریف موجودیتهای ذکر شدهی در DB Context برنامه را به همراه دارد و تفسیر آنها را از دیدگاه EF در اینجا میتوان مشاهده کرد.
پس از مرحلهی افزودن migrations، نوبت به اعمال آن به بانک اطلاعاتی است. تا اینجا EF تنها متدهای Up و Down مربوط به ساخت و حذف ساختار جداول را ایجاد کردهاست. اما هنوز آنها را به بانک اطلاعاتی برنامه اعمال نکردهاست. برای اینکار در پوشهی جاری دستور ذیل را صادر کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update Applying migration '13950526050417_InitialDatabase'. Done.
اکنون اگر به لیست بانکهای اطلاعاتی مراجعه کنیم، بانک اطلاعاتی جدید TestDbCore2016 را به همراه جدول متناظر کلاس Person میتوان مشاهده کرد:
در اینجا جدول دیگری به نام __EFMigrationsHistory نیز قابل مشاهدهاست که کار آن ذخیره سازی وضعیت فعلی Migrations در بانک اطلاعاتی، جهت مقایسههای آتی است. این جدول صرفا توسط ابزارهای EF استفاده میشود و نباید به صورت مستقیم تغییری در آن ایجاد کنید.
مقدار دهی اولیهی جداول بانکهای اطلاعاتی در EF Core
در همین حالت اگر کنترلر TestDB مطرح شدهی در انتهای بحث قسمت قبل را اجرا کنیم، به این استثناء خواهیم رسید:
این تصویر بدین معنا است که کار Migrations موفقیت آمیز بودهاست و اینبار امکان اتصال و کار با بانک اطلاعاتی وجود دارد، اما این جدول حاوی اطلاعات اولیهای برای نمایش نیست.
در نگارش قبلی EF Code First، امکانات Migrations به همراه یک متد Seed نیز بود که توسط آن کار مقدار دهی اولیهی جداول را میتوان انجام داد (زمانیکه جدولی ایجاد میشود، در همان هنگام، چند رکورد خاص نیز به آن اضافه شوند. برای مثال به جدول کاربران، رکورد اولین کاربر یا همان Admin اضافه شود). این متد در EF Core 1.0 وجود ندارد.
برای این منظور کلاس جدیدی را به نام ApplicationDbContextSeedData به همان پوشهی جدید Migrations اضافه کنید؛ با این محتوا:
using System.Collections.Generic; using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.Extensions.DependencyInjection; namespace Core1RtmEmptyTest.Migrations { public static class ApplicationDbContextSeedData { public static void SeedData(this IServiceScopeFactory scopeFactory) { using (var serviceScope = scopeFactory.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>(); if (!context.Persons.Any()) { var persons = new List<Person> { new Person { FirstName = "Admin", LastName = "User" } }; context.AddRange(persons); context.SaveChanges(); } } } } }
public void Configure(IServiceScopeFactory scopeFactory) { scopeFactory.SeedData();
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
- برای پیاده سازی الگوی واحد کار، اولین قدم، مشخص سازی طول عمر Db Context برنامه است. برای اینکه تنها یک Context در طول یک درخواست وهله سازی شود، نیاز است به نحو صریحی طول عمر آنرا به حالت Scoped تنظیم کرد. متد AddDbContext دارای پارامتری است که این طول عمر را دریافت میکند. بنابراین در اینجا ServiceLifetime.Scoped ذکر شدهاست. همچنین در این مثال از نمونهای که IConfigurationRoot به سازندهی کلاس ApplicationDbContext تزریق شده (نکتهی انتهای بحث قسمت قبل)، استفاده شدهاست. به همین جهت تنظیمات options آنرا ملاحظه نمیکنید.
- مرحلهی بعد نحوهی دسترسی به این سرویس ثبت شده در یک کلاس static دارای متدی الحاقی است. در اینجا دیگر دسترسی مستقیمی به تزریق وابستگیها نداریم و باید کار را با IServiceScopeFactory شروع کنیم. در اینجا میتوانیم به صورت دستی یک Scope را ایجاد کرده، سپس توسط ServiceProvider آن، به سرویس ApplicationDbContext دسترسی پیدا کنیم و در ادامه از آن به نحو متداولی استفاده نمائیم. IServiceScopeFactory جزو سرویسهای توکار ASP.NET Core است و در صورت ذکر آن به عنوان پارامتر جدیدی در متد Configure، به صورت خودکار وهله سازی شده و در اختیار ما قرار میگیرد.
- نکتهی مهمی که در اینجا بکار رفتهاست، ایجاد Scope و dispose خودکار آن توسط عبارت using است. باید دقت داشت که ایجاد Scope و تخریب آن به صورت خودکار در ابتدا و انتهای درخواستها توسط ASP.NET Core انجام میشود. اما چون شروع کار ما از متد Configure است، در اینجا خارج از Scope قرار داریم و باید مدیریت ایجاد و تخریب آنرا به صورت دستی انجام دهیم که نمونهای از آنرا در متد SeedData کلاس ApplicationDbContextSeedData ملاحظه میکنید. در اینجا Scope ایی ایجاد شدهاست. سپس دادههای اولیهی مدنظر به بانک اطلاعاتی اضافه گردیده و در آخر این Scope تخریب شدهاست.
- اگر کار ایجاد و تخریب scope، به نحوی که مشخص شدهاست انجام نگیرد، طول عمر درخواستی خارج از Scope، همواره Singleton خواهد بود. چون خارج از طول عمر درخواست جاری قرار داریم و هنوز کار به سرویس دهی درخواستها نرسیدهاست. بنابراین مدیریت Scopeها هنوز شروع نشدهاست و باید به صورت دستی انجام شود.
در این حالت اگر برنامه را اجرا کنیم، این خروجی قابل مشاهده است:
که به معنای کار کردن متد SeedData و ثبت اطلاعات اولیهای در بانک اطلاعاتی است.
اعمال تغییرات به مدلهای برنامه و به روز رسانی ساختار بانک اطلاعاتی
فرض کنید به کلاس Person قسمت قبل، خاصیت Age را هم اضافه کردهایم:
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } } }
An unhandled exception occurred while processing the request. SqlException: Invalid column name 'Age'.
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add v2 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
با اجرا این دستورات، فایل جدید 13950526073248_v2 به پوشهی Migrations اضافه میشود. این فایل حاوی نحوهی به روز رسانی بانک اطلاعاتی، بر اساس خاصیت جدید Age است. سپس با اجرای دستور dotnet ef database update، کار به روز رسانی بانک اطلاعاتی بر اساس مرحلهی v2 انجام میشود.
بنابراین هر بار که تغییری را در مدلهای خود ایجاد میکنید، یکبار باید کلاس مهاجرت آنرا ایجاد کنید و سپس آنرا به بانک اطلاعاتی اعمال نمائید.
تهیه اسکریپت تغییرات بجای اعمال تغییرات توسط ابزارهای EF
شاید علاقمند باشید که پیش از اعمال تغییرات به بانک اطلاعاتی، یک اسکریپت SQL از آن تهیه کنید (جهت مطالعه و یا اعمال دستی آن توسط خودتان). برای اینکار میتوانید دستور ذیل را در پوشهی جاری پروژه اجرا کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations script -o v2.sql
تغییرات ساختار جدول __EFMigrationsHistory در EF Core 1.0
در EF 6.x، ساختار اطلاعات جدول نگهداری تاریخچهی تغییرات، بسیار پیچیده بود و شامل رشتهای gzip شدهی حاوی یک snapshot از کل ساختار دیتابیس در هر مرحلهی migration بود. در این نگارش، این snapshot حذف شدهاست و بجای آن فایل ApplicationDbContextModelSnapshot.cs را مشاهده میکنید (تنها یک snapshot به ازای کل context برنامه). همچنین در اینجا کاملا مشخص است که چه مراحلی به بانک اطلاعاتی اعمال شدهاند و دیگر خبری از رشتهی gzip شدهی قبلی نیست (تصویر فوق).
در شکل زیر ساختار قبلی این جدول را در EF 6.x مشاهده میکنید. در EF 6.x حتی فضای نام کلاسهای موجودیتهای برنامه هم مهم هستند و در صورت تغییر، مشکل ایجاد میشود:
مهاجرت خودکار از EF Core حذف شدهاست
در EF 6.x در کنار کلاس Db Context یک کلاس Configuration هم وجود داشت که برای مثال امکان چنین تعریفی در آن میسر هست:
public Configuration() { AutomaticMigrationsEnabled = true; }
با حذف مهاجرت خودکار:
- دیگر نیازی نیست تا model snapshots در بانک اطلاعاتی ذخیره شوند (همان ساده شدن ساختار جدول ذخیره سازی تاریخچهی مهاجرتهای فوق).
- در حالت افزودن یک مرحلهی مهاجرت، دیگر نیازی به کوئری گرفتن از بانک اطلاعاتی نخواهد بود (سرعت بیشتر).
- میتوان چندین مرحلهی مهاجرت را افزود بدون اینکه الزاما مجبور به اعمال آنها به بانک اطلاعاتی باشیم.
- کاهش کدهای مدیریت ساختار بانک اطلاعاتی.
- تیمها برای یکی کردن تغییرات خود مشکلی نخواهند داشت چون دیگر snapshot مدلها در جدول __EFMigrationsHistory ذخیره نمیشود.
بنابراین در EF Core میتوان مهاجرت v1 را اضافه کرد. سپس تغییراتی را در کدها اعمال کرد. در ادامه مهاجرت v2 را تولید کرد و در آخر کار اعمال یکجای اینها را به بانک اطلاعاتی انجام داد.
هرچند در اینجا اگر میخواهید مرحلهی اجرای دستور dotnet ef database update را حذف کنید، میتوانید از کدهای ذیل نیز استفاده نمائید:
using Core1RtmEmptyTest.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Core1RtmEmptyTest.Migrations { public static class DbInitialization { public static void Initialize(this IServiceScopeFactory scopeFactory) { using (var serviceScope = scopeFactory.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>(); // Applies any pending migrations for the context to the database. // Will create the database if it does not already exist. context.Database.Migrate(); } } } }
کار متد Migrate، ایجاد بانک اطلاعاتی در صورت عدم وجود و سپس اعمال تمام مراحل migration ایی است که در جدول __EFMigrationsHistory ثبت نشدهاند (دقیقا همان کار دستور dotnet ef database update را انجام میدهد).
تفاوت متد Database.EnsureCreated با متد Database.Migrate
اگر به متدهای context.Database دقت کنید، یکی از آنها EnsureCreated نام دارد. این متد نیز سبب تولید بانک اطلاعاتی بر اساس ساختار Context برنامه میشود. اما هدف آن صرفا استفادهی از آن در آزمونهای واحد سریع است. از این جهت که جدول __EFMigrationsHistory را تولید نمیکند (برخلاف متد Migrate). بنابراین بجز آزمونهای واحد، در جای دیگری از آن استفاده نکنید چون به دلیل عدم تولید جدول __EFMigrationsHistory توسط آن، قابلیت استفادهی از بانک اطلاعاتی تولید شدهی توسط آن با امکانات migrations وجود ندارد. در پایان آزمون واحد نیز میتوان از متد EnsureDeleted برای حذف این بانک اطلاعاتی موقتی استفاده کرد.
در قسمت بعد، مطالب تکمیلی مهاجرتها را بررسی خواهیم کرد. برای مثال چگونه میتوان کلاسهای موجودیتها را به اسمبلیهای دیگری منتقل کرد.
These are the customer-reported issues addressed in this version:
Visual Studio crashes when you open a solution with a test project.
Visual Studio Freezes in Debug with Chrome.
Failure to install HelpViewer.
UI delay while typing R code.
C# 7.0 Regression in Tuples.
Xamarin - Dynamic Type Platform not supported exception.
Xamarin – Dynamic object is not supported.
Xamarin - Xamarin.iOS: ArgumentNullException for instruction parameter in Mono.Linker's MarkException() method.
ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب آن را MVC + Web API انتخاب کنید.
ابتدا به فایل WebApiConfig.cs در پوشه App_Start مراجعه کنید و مسیر پیش فرض را حذف کنید. برای مسیریابی سرویسها از قابلیت جدید Attribute Routing استفاده خواهیم کرد. فایل مذکور باید مانند لیست زیر باشد.
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services // Web API routes config.MapHttpAttributeRoutes(); } }
سپس در پوشه Models کلاس جدیدی بنام VideoStream ایجاد کنید. این کلاس مسئول نوشتن داده فایلهای ویدیویی در OutputStream خواهد بود. کد کامل این کلاس را در لیست زیر مشاهده میکنید.
public class VideoStream { private readonly string _filename; private long _contentLength; public long FileLength { get { return _contentLength; } } public VideoStream(string videoPath) { _filename = videoPath; using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read)) { _contentLength = video.Length; } } public async void WriteToStream(Stream outputStream, HttpContent content, TransportContext context) { try { var buffer = new byte[65536]; using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read)) { var length = (int)video.Length; var bytesRead = 1; while (length > 0 && bytesRead > 0) { bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length)); await outputStream.WriteAsync(buffer, 0, bytesRead); length -= bytesRead; } } } catch (HttpException) { return; } finally { outputStream.Close(); } } }
شرح کلاس VideoStream
این کلاس ابتدا دو فیلد خصوصی تعریف میکند. یکی filename_ که فقط-خواندنی است و نام فایل ویدیو درخواستی را نگهداری میکند. و دیگری contentLength_ که سایز فایل ویدیو درخواستی را نگهداری میکند.
یک خاصیت عمومی بنام FileLength نیز تعریف شده که مقدار خاصیت contentLength_ را بر میگرداند.
متد سازنده این کلاس پارامتری از نوع رشته بنام videoPath را میپذیرد که مسیر کامل فایل ویدیوی مورد نظر است. در این متد، متغیرهای filename_ و contentLength_ مقدار دهی میشوند. نکتهی قابل توجه در این متد استفاده از پارامتر FileShare.Read است که باعث میشود فایل مورد نظر هنگام باز شدن قفل نشود و برای پروسههای دیگر قابل دسترسی باشد.
در آخر متد WriteToStream را داریم که مسئول نوشتن داده فایلها به OutputStream است. اول از همه دقت کنید که این متد از کلمه کلیدی async استفاده میکند بنابراین بصورت asynchronous اجرا خواهد شد. در بدنه این متد متغیری بنام buffer داریم که یک آرایه بایت با سایز 64KB را تعریف میکند. به بیان دیگر اطلاعات فایلها را در پکیجهای 64 کیلوبایتی برای کلاینت ارسال خواهیم کرد. در ادامه فایل مورد نظر را باز میکنیم (مجددا با استفاده از FileShare.Read) و شروع به خواندن اطلاعات آن میکنیم. هر 64 کیلوبایت خوانده شده بصورت async در جریان خروجی نوشته میشود و تا هنگامی که به آخر فایل نرسیده ایم این روند ادامه پیدا میکند.
while (length > 0 && bytesRead > 0) { bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length)); await outputStream.WriteAsync(buffer, 0, bytesRead); length -= bytesRead; }
حال که کلاس VideoStream را در اختیار داریم میتوانیم پروژه را تکمیل کنیم. در پوشه کنترلرها کلاسی بنام VideoControllerبسازید. کد کامل این کلاس را در لیست زیر مشاهده میکنید.
public class VideoController : ApiController { [Route("api/video/{ext}/{fileName}")] public HttpResponseMessage Get(string ext, string fileName) { string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext)); if (File.Exists(videoPath)) { FileInfo fi = new FileInfo(videoPath); var video = new VideoStream(videoPath); var response = Request.CreateResponse(); response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream, new MediaTypeHeaderValue("video/" + ext)); response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fi.Name.Replace(" ", "")); response.Content.Headers.Add("Content-Length", video.FileLength.ToString()); return response; } else { return Request.CreateResponse(HttpStatusCode.NotFound); } } }
شرح کلاس VideoController
همانطور که میبینید مسیر دستیابی به این کنترلر با استفاده از قابلیت Attribute Routing تعریف شده است.
[Route("api/video/{ext}/{fileName}")]
api/video/mp4/sample
متد Get این کنترلر دو پارامتر با نامهای ext و fileName را میپذیرد که همان فرمت و نام فایل هستند. سپس با استفاده از کلاس HostingEnvironment سعی میکنیم مسیر کامل فایل درخواست شده را بدست آوریم.
string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext));
var video = new VideoStream(videoPath);
var response = Request.CreateResponse(); response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream, new MediaTypeHeaderValue("video/" + ext));
کلاس PushStreamContent در فضای نام System.Net.Http وجود دارد. همانطور که میبینید امضای Action پاس داده شده، با امضای متد WriteToStream در کلاس VideoStream مطابقت دارد.
در آخر دو Header به پاسخ ارسالی اضافه میکنیم تا نوع داده ارسالی و سایز آن را مشخص کنیم.
response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fileName); response.Content.Headers.Add("Content-Length", video.FileLength.ToString());
if(File.Exists(videoPath)) { // removed for bravity } else { return Request.CreateResponse(HttpStatusCode.NotFound); }
public class HomeController : Controller { // GET: Home public ActionResult Index() { return View(); } }
<div> <div> <video width="480" height="270" controls="controls" preload="auto"> <source src="/api/video/mp4/sample" type="video/mp4" /> Your browser does not support the video tag. </video> </div> </div>
اگر پروژه را اجرا کنید میبینید که ویدیو مورد نظر آماده پخش است. برای اینکه ببینید چطور دادههای ویدیو در قالب پکیجهای 64 کیلو بایتی دریافت میشوند از ابزار مرورگرتان استفاده کنید. مثلا در گوگل کروم F12 را بزنید و به قسمت Network بروید. صفحه را یکبار مجددا بارگذاری کنید تا ارتباطات شبکه مانیتور شود. اگر به المنت sample دقت کنید میبینید که با شروع پخش ویدیو پکیجهای اطلاعات یکی پس از دیگری دریافت میشوند و اطلاعات ریز آن را میتوانید مشاهده کنید.
پروژه نمونه به این مقاله ضمیمه شده است. قابلیت Package Restore فعال شده و برای صرفه جویی در حجم فایل، تمام پکیجها و محتویات پوشه bin حذف شده اند. برای تست بیشتر میتوانید فایل sample.mp4 را با فایلی حجیمتر جایگزین کنید تا نحوه دریافت اطلاعات را با روشی که در بالا بدان اشاره شد مشاهده کنید.
AsyncVideoStreaming.rar
تنظیمات مقدماتی GitHub
در ابتدا نیاز است یک مخزن کد خالی را در GitHub ایجاد کنید. برای این منظور به برگهی Repositories در اکانت GitHub خود مراجعه کرده و بر روی دکمهی New کلیک کنید:
سپس در صفحهی بعدی، نام پروژه را به همراه توضیحاتی وارد نمائید و بر روی دکمهی Create repository کلیک کنید. در اینجا سایر گزینهها را انتخاب نکنید. نیازی به انتخاب گزینهی READ ME و یا انتخاب مجوز و غیره نیست. تمام این کارها را در سمت پروژهی اصلی میتوان انجام داد و یا VS.NET فایلهای ignore را به صورت خودکار ایجاد میکند. در اینجا صرفا هدف، ایجاد یک مخزن کد خالی است.
از اطلاعات صفحهی بعدی، تنها به آدرس مخصوص GitHub آن نیاز داریم. از این آدرس در VS.NET برای ارسال اطلاعات به سرور استفاده خواهیم کرد:
تنظیمات VS.NET برای ارسال پروژه به مخزن GitHub
پس از ایجاد یک مخزن کد خالی در GitHub، اکنون میتوانیم پروژهی خود را به آن ارسال کنیم. برای این منظور از منوی File، گزینهی Add to source control را انتخاب کنید و در صفحهی باز شده، گزینهی Git را انتخاب نمائید:
سپس در کنار برگهی Solution Explorer، برگهی Team Explorer را انتخاب کنید. در اینجا بر روی دکمهی Home در نوار ابزار آن کلیک کرده و سپس بر روی دکمهی Unsynced commits کلیک نمائید.
در ادامه در صفحهی باز شده، همان آدرس مخصوص مخزن کد جدید را در GitHub وارد کرده و بر روی دکمهی Publish کلیک کنید:
در اینجا بلافاصله صفحهی لاگینی ظاهر میشود که باید در آن مشخصات اکانت GitHub خود را وارد نمائید:
به این ترتیب عملیات Publish اولیه انجام شده و تصویر ذیل نمایان خواهد شد:
در اینجا بر روی دکمهی Sync کلیک کنید. به این ترتیب مخزن کد GitHub به پروژهی جاری متصل خواهد شد:
سپس نیاز است فایلهای موجود را به مخزن کد GitHub ارسال کرد. بنابراین پس از مشاهدهی پیام موفقیت آمیز بودن عملیات همگام سازی، بر روی دکمهی Home در نوار ابزار کلیک کرده و اینبار گزینهی Changes را انتخاب کنید:
در اینجا پیام اولین ارسال را وارد کرده و سپس بر روی دکمهی Commit کلیک کنید:
پس از مشاهدهی پیام موفقیت آمیز بودن commit محلی، نیاز است تا آنرا با سرور نیز هماهنگ کرد. به همین جهت در اینجا بر روی لینک Sync کلیک کرده و در صفحهی بعدی بر روی دکمهی Sync کلیک کنید:
اندکی صبر کنید تا فایلها به سرور ارسال شوند. اکنون اگر به GitHub مراجعه کنید، فایلهای ارسالی قابل مشاهده هستند:
اعمال تغییرات بر روی پروژهی محلی و ارسال به سرور
در ادامه میخواهیم دو فایل README.md و LICENSE.md را به پروژه اضافه کنیم. پس از افزودن آنها، یا هر تغییر دیگری در پروژه، اینبار برای ارسال تغییرات به سرور، تنها کافی است به برگهی Team explorer مراجعه کرده و ابتدا بر روی دکمهی Home کلیک کرد تا منوی انتخاب گزینههای آن ظاهر شود. در اینجا تنها کافی است گزینهی Changes را انتخاب و دقیقا همان مراحل عنوان شدهی پیشین را تکرار کرد. ابتدا ورود پیام Commit و سپس Commit. در ادامه Sync محلی و سپس Sync با سرور.
روش دوم - بوم میکروسرویس (microservice canvas)
یکی دیگر از روشهای مناسب برای مستند سازی یک سرویس و ساختار درونی آن، استفاده از روش بوم میکروسرویس میباشد. بوم میکروسرویس نیز تا حدودی شبیه به کارتهای CRC و همچنین روش microservice design card که پیشتر بررسی گردید، میباشد؛ با این تفاوت که اطلاعات بیشتری را نگهداری مینماید.
• Description – ارائه یک توضیح مختصر در مورد سرویس
• Capabilities – معرفی بخشهایی از منطق کسب و کار که در این سرویس پیاده سازی شدهاست.
• Service APIs – معرفی عملیات یا operations (شامل commands و queries) که در این سرویس پیاده سازی شدهاند و همچنین معرفی وقایع یا همان domain event هایی که توسط سرویس منتشر میشوند.
• Quality attributes – معرفی non-functional requirementsهای سرویس
• Observability (قابلیتهای مشاهده و بررسی سرویس) – معرفی health check endpoints و key metrics و ... .
وابستگیهای یک سرویس (A service’s dependencies) که لزوما استفاده بیرونی ندارد و بیشتر منبعی برای خود اعضای تیم خواهد بود، در بخشی تحت عنوان dependencies مشخص میشوند که خود شامل دو قسمت به شرح زیر میباشد:
• Subscribes – اشتراک در کانال پیامهایی که شامل وقایع سایر سرویسها میباشند.
موارد مربوط به پیاده سازی یک سرویس (A service’s implementation)
تولید بوم میکروسرویس