gRPC + ASP.NET Core as a Migration Path for WCFs in .NET Core
Feature | WCF | ASP.Net Core + gRPC |
Platforms | Windows | Windows, Linux, MacOS |
Protocols | LRPC/Named Pipes/HTTP/TCP/MSMQ | Binary (GRPC) + HTTP2 (TCP/Named Pipes/LRPC) .AddProtocol(“ncacn_ip_tcp”, “8080”) .AddProtocol(“ncacn_np”, @”\pipe\MyService”) .AddProtocol(“ncalrpc”, “MyService”) By removing the ASP.NET Core stack and just using .NET Core |
Injected Aspects | Behaviors | ASP.NET Core DI Middleware/ gRPC interceptors |
Distributed Transactions | *Yes – [TransactionFlow], transactionscopes, and supported bindings | *No |
Transport Security | SSL/TLS | SSL/TLS |
Message Security | Certificates/credentials | Certificates/credentials https://docs.microsoft.com/en-us/aspnet/core/grpc/authn-and-authz?view=aspnetcore-3.0 |
Windows Authentication | Kerberos/NTLM | AAD Sync/ASFS + ASP.NET Core middleware |
Proxies/Contracts | Service Contracts/Data Contracts | Protocol Buffers |
Proxy-less Communication | WCF Channel Factory | † Protobuf-Net.GRPC |
ثبت برنامهی خود در گوگل و انجام تنظیمات آن
اولین کاری که برای استفاده از نگارش سوم Google Analytics API باید صورت گیرد، ثبت برنامهی خود در Google Developer Console است. برای این منظور ابتدا به آدرس ذیل وارد شوید:
سپس بر روی دکمهی Create project کلیک کنید. نام دلخواهی را وارد کرده و در ادامه بر روی دکمهی Create کلیک نمائید تا پروفایل این پروژه ایجاد شود.
تنها نکتهی مهم این قسمت، بخاطر سپردن نام پروژه است. زیرا از آن جهت اتصال به API گوگل استفاده خواهد شد.
پس از ایجاد پروژه، به صفحهی آن وارد شوید و از منوی سمت چپ صفحه، گزینهی Credentials را انتخاب کنید. در ادامه در صفحهی باز شده، بر روی دکمهی Create new client id کلیک نمائید.
در صفحهی باز شده، گزینهی Service account را انتخاب کنید. اگر سایر گزینهها را انتخاب نمائید، کاربری که قرار است از API استفاده کند، باید بتواند توسط مرورگر نصب شدهی بر روی کامپیوتر اجرا کنندهی برنامه، یکبار به گوگل لاگین نماید که این مورد مطلوب برنامههای وب و همچنین سرویسها نیست.
در اینجا ابتدا یک فایل مجوز p12 را به صورت خودکار دریافت خواهید کرد و همچنین پس از ایجاد client id، نیاز است، ایمیل آنرا جایی یادداشت نمائید:
از این ایمیل و همچنین فایل p12 ارائه شده، جهت لاگین به سرور استفاده خواهد شد.
همچنین نیاز است تا به برگهی APIs پروژهی ایجاد شده رجوع کرد و گزینهی Analytics API آنرا فعال نمود:
تا اینجا کار ثبت و فعال سازی برنامهی خود در گوگل به پایان میرسد.
دادن دسترسی به Client ID ثبت شده در برنامهی Google Analytics
پس از اینکه Client ID سرویس خود را ثبت کردید، نیاز است به اکانت Google Analytics خود وارد شوید. سپس در منوی آن، گزینهی Admin را پیدا کرده و به آن قسمت، وارد شوید:
در ادامه به گزینهی User management آن وارد شده و به ایمیل Client ID ایجاد شده در قسمت قبل، دسترسی خواندن و آنالیز را اعطاء کنید:
در صورت عدم رعایت این مساله، کلاینت API، قادر به دسترسی به Google Analytics نخواهد بود.
استفاده از نگارش سوم Google Analytics API در دات نت
قسمت مهم کار، تنظیمات فوق است که در صورت عدم رعایت آنها، شاید نصف روزی را مشغول به دیباگ برنامه شوید. در ادامه نیاز است پیشنیازهای دسترسی به نگارش سوم Google Analytics API را نصب کنیم. برای این منظور، سه بستهی نیوگت ذیل را توسط کنسول پاورشل نیوگت، به برنامه اضافه کنید:
PM> Install-Package Google.Apis PM> Install-Package Google.Apis.auth PM> Install-Package Google.Apis.Analytics.v3
using System; using System.Linq; using System.Security.Cryptography.X509Certificates; using Google.Apis.Analytics.v3; using Google.Apis.Analytics.v3.Data; using Google.Apis.Auth.OAuth2; using Google.Apis.Services; namespace GoogleAnalyticsAPIv3Tests { public class AnalyticsQueryParameters { public DateTime Start { set; get; } public DateTime End { set; get; } public string Dimensions { set; get; } public string Filters { set; get; } public string Metrics { set; get; } } public class AnalyticsAuthentication { public Uri SiteUrl { set; get; } public string ApplicationName { set; get; } public string ServiceAccountEmail { set; get; } public string KeyFilePath { set; get; } public string KeyFilePassword { set; get; } public AnalyticsAuthentication() { KeyFilePassword = "notasecret"; } } public class GoogleAnalyticsApiV3 { public AnalyticsAuthentication Authentication { set; get; } public AnalyticsQueryParameters QueryParameters { set; get; } public GaData GetData() { var service = createAnalyticsService(); var profile = getProfile(service); var query = service.Data.Ga.Get("ga:" + profile.Id, QueryParameters.Start.ToString("yyyy-MM-dd"), QueryParameters.End.ToString("yyyy-MM-dd"), QueryParameters.Metrics); query.Dimensions = QueryParameters.Dimensions; query.Filters = QueryParameters.Filters; query.SamplingLevel = DataResource.GaResource.GetRequest.SamplingLevelEnum.HIGHERPRECISION; return query.Execute(); } private AnalyticsService createAnalyticsService() { var certificate = new X509Certificate2(Authentication.KeyFilePath, Authentication.KeyFilePassword, X509KeyStorageFlags.Exportable); var credential = new ServiceAccountCredential( new ServiceAccountCredential.Initializer(Authentication.ServiceAccountEmail) { Scopes = new[] { AnalyticsService.Scope.AnalyticsReadonly } }.FromCertificate(certificate)); return new AnalyticsService(new BaseClientService.Initializer { HttpClientInitializer = credential, ApplicationName = Authentication.ApplicationName }); } private Profile getProfile(AnalyticsService service) { var accountListRequest = service.Management.Accounts.List(); var accountList = accountListRequest.Execute(); var site = Authentication.SiteUrl.Host.ToLowerInvariant(); var account = accountList.Items.FirstOrDefault(x => x.Name.ToLowerInvariant().Contains(site)); var webPropertyListRequest = service.Management.Webproperties.List(account.Id); var webPropertyList = webPropertyListRequest.Execute(); var sitePropertyList = webPropertyList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site)); var profileListRequest = service.Management.Profiles.List(account.Id, sitePropertyList.Id); var profileList = profileListRequest.Execute(); return profileList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site)); } } }
مثالی از نحوه استفاده از کلاس GoogleAnalyticsApiV3
در ادامه یک برنامهی کنسول را ملاحظه میکنید که از کلاس GoogleAnalyticsApiV3 استفاده میکند:
using System; using System.Collections.Generic; using System.Linq; namespace GoogleAnalyticsAPIv3Tests { class Program { static void Main(string[] args) { var statistics = new GoogleAnalyticsApiV3 { Authentication = new AnalyticsAuthentication { ApplicationName = "My Project", KeyFilePath = "811e1d9976cd516b55-privatekey.p12", ServiceAccountEmail = "10152bng4j3mq@developer.gserviceaccount.com", SiteUrl = new Uri("https://www.dntips.ir/") }, QueryParameters = new AnalyticsQueryParameters { Start = DateTime.Now.AddDays(-7), End = DateTime.Now, Dimensions = "ga:date", Filters = null, Metrics = "ga:users,ga:sessions,ga:pageviews" } }.GetData(); foreach (var result in statistics.TotalsForAllResults) { Console.WriteLine(result.Key + " -> total:" + result.Value); } Console.WriteLine(); foreach (var row in statistics.ColumnHeaders) { Console.Write(row.Name + "\t"); } Console.WriteLine(); foreach (var row in statistics.Rows) { var rowItems = (List<string>)row; Console.WriteLine(rowItems.Aggregate((s1, s2) => s1 + "\t" + s2)); } Console.ReadLine(); } } }
چند نکته
ApplicationName همان نام پروژهای است که ابتدای کار، در گوگل ایجاد کردیم.
KeyFilePath مسیر فایل مجوز p12 ایی است که گوگل در حین ایجاد اکانت سرویس، در اختیار ما قرار میدهد.
ServiceAccountEmail آدرس ایمیل اکانت سرویس است که در قسمت ادمین Google Analytics به آن دسترسی دادیم.
SiteUrl آدرس سایت شما است که هم اکنون در Google Analytics دارای یک اکانت و پروفایل ثبت شدهاست.
توسط AnalyticsQueryParameters میتوان نحوهی کوئری گرفتن از Google Analytics را مشخص کرد. تاریخ شروع و پایان گزارش گیری در آن مشخص هستند. در مورد پارامترهایی مانند Dimensions و Metrics بهتر است به مرجع کامل آن در گوگل مراجعه نمائید:
Dimensions & Metrics Reference
برای نمونه در مثال فوق، تعداد کاربران، سشنهای آن و همچنین تعداد بار مشاهدهی صفحات، گزارشگیری میشود.
برای مطالعه بیشتر
Using Google APIs in Windows Store Apps
How To Use Google Analytics From C# With OAuth
Google Analytic’s API v3 with C#
.NET Library for Accessing and Querying Google Analytics V3 via Service Account
Google OAuth2 C#
"securitySchemes": { "basicAuth": { "type":"http", "description":"Input your username and password to access this API", "scheme":"basic" } } … "security":[ {"basicAuth":[]} ]
- خاصیت security کار اعمال Scheme تعریف شده را به کل API یا صرفا قسمتهای خاصی از آن، انجام میدهد.
در ادامه مثالی را بررسی خواهیم کرد که مبتنی بر basic authentication کار میکند و در این حالت به ازای هر درخواست به API، نیاز است یک نام کاربری و کلمهی عبور نیز ارسال شوند. البته روش توصیه شده، کار با JWT و یا OpenID Connect است؛ اما جهت تکمیل سادهتر این قسمت، بدون نیاز به برپایی مقدماتی پیچیده، کار با basic authentication را بررسی میکنیم و اصول کلی آن از دیدگاه مستندات OpenAPI Specification تفاوتی نمیکند.
افزودن Basic Authentication به API برنامه
برای پیاده سازی Basic Authentication نیاز به یک AuthenticationHandler سفارشی داریم:
using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; namespace OpenAPISwaggerDoc.Web.Authentication { public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { public BasicAuthenticationHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { if (!Request.Headers.ContainsKey("Authorization")) { return Task.FromResult(AuthenticateResult.Fail("Missing Authorization header")); } try { var authenticationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter); var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':'); var username = credentials[0]; var password = credentials[1]; if (username == "DNT" && password == "123") { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } return Task.FromResult(AuthenticateResult.Fail("Invalid username or password")); } catch { return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header")); } } } }
پس از دریافت مقدار هدر Authorization، ابتدا مقدار آنرا از base64 به حالت معمولی تبدیل کرده و سپس بر اساس حرف ":"، دو قسمت را از آن جداسازی میکنیم. قسمت اول را به عنوان نام کاربری و قسمت دوم را به عنوان کلمهی عبور پردازش خواهیم کرد. در این مثال جهت سادگی، این دو باید مساوی DNT و 123 باشند. اگر اینچنین بود، یک AuthenticationTicket دارای Claim ای حاوی نام کاربری را ایجاد کرده و آنرا به عنوان حاصل موفقیت آمیز بودن عملیات بازگشت میدهیم.
مرحلهی بعد، استفاده و معرفی این BasicAuthenticationHandler تهیه شده به برنامه است:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(defaultScheme: "Basic") .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);
همچنین نیاز است میانافزار اعتبارسنجی را نیز با فراخوانی متد app.UseAuthentication، به برنامه اضافه کرد که باید پیش از فراخوانی app.UseMvc صورت گیرد تا به آن اعمال شود:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseStaticFiles(); app.UseAuthentication(); app.UseMvc(); } } }
همچنین برای اینکه تمام اکشن متدهای موجود را نیز محافظت کنیم، میتوان فیلتر Authorize را به صورت سراسری اعمال کرد:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddMvc(setupAction => { setupAction.Filters.Add(new AuthorizeFilter()); // ...
تکمیل مستندات API جهت انعکاس تنظیمات محافظت از اکشن متدهای آن
پس از تنظیم محافظت دسترسی به اکشن متدهای برنامه، اکنون نوبت به مستند کردن آن است و همانطور که در ابتدای بحث نیز عنوان شد، برای این منظور نیاز به تعریف خواص securitySchemes و security در OpenAPI Specification است:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(setupAction => { // ... setupAction.AddSecurityDefinition("basicAuth", new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, Scheme = "basic", Description = "Input your username and password to access this API" }); });
پس از این تنظیم اگر برنامه را اجرا کنیم، یک دکمهی authorize اضافه شدهاست:
با کلیک بر روی آن، صفحهی ورود نام کاربری و کلمهی عبور ظاهر میشود:
اگر آنرا تکمیل کرده و سپس برای مثال لیست نویسندگان را درخواست کنیم (با کلیک بر روی دکمهی try it out آن و سپس کلیک بر روی دکمهی execute ذیل آن)، تنها خروجی 401 یا unauthorized را دریافت میکنیم:
- بنابراین برای تکمیل آن، مطابق نکات قسمت چهارم، ابتدا باید status code مساوی 401 را به صورت سراسری، مستند کنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(setupAction => { setupAction.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status401Unauthorized));
- همچنین هرچند با کلیک بر روی دکمهی Authorize در Swagger UI و ورود نام کاربری و کلمهی عبور توسط آن، در همانجا پیام Authorized را دریافت کردیم، اما اطلاعات آن به ازای هر درخواست، به سمت سرور ارسال نمیشود. به همین جهت در حین درخواست لیست نویسندگان، پیام unauthorized را دریافت کردیم. برای رفع این مشکل نیاز است به OpenAPI Spec اعلام کنیم که تعامل با API، نیاز به Authentication دارد:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(setupAction => { // ... setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "basicAuth" } }, new List<string>() } }); });
پس از این تنظیمات، Swagger UI با افزودن یک آیکن قفل به مداخل APIهای محافظت شده، به صورت زیر تغییر میکند:
در این حالت اگر بر روی آیکن قفل کلیک کنیم، همان صفحهی دیالوگ ورود نام کاربری و کلمهی عبوری که پیشتر با کلیک بر روی دکمهی Authorize ظاهر شد، نمایش داده میشود. با تکمیل آن و کلیک مجدد بر روی آیکن قفل، جهت گشوده شدن پنل API و سپس کلیک بر روی try it out آن، برای مثال میتوان به API محافظت شدهی دریافت لیست نویسندگان، بدون مشکلی، دسترسی یافت:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-06.zip
معرفی رسمی C# 7.1
Features not supported in SQL Server Compact 4.0
علتش این است که مثلا LINQ to SQL دیگر با SQL CE 4 پشتیبانی نمیشود چون خود LINQ to SQL دیگر توسط MS توسعه جدی پیدا نمیکند و به EF سوئیچ شده و EF هم پروایدر رسمی برای SQL CE 4 داره.
در NET Core 2.0. از یک اصطلاحا «compatibility shim» مخصوص استفاده میشود که امکان افزودن ارجاعات به full framework libraryها را بدون نیاز به تغییر target framework برنامه میسر میکند. یعنی در اینجا میتوان یک کتابخانهی قدیمی دات نتی را در برنامههای مبتنی بر NET Core. بر روی لینوکس نیز اجرا کرد و در این حالت نیازی به تبدیل اجباری این کتابخانه به نسخهی NET Core. آن نیست.
NET Core 2.0. پیاده سازی کنندهی NET Standard 2.0. است
NET Standard. در حقیقت یک قرار داد است که سکوهای کاری مختلف دات نتی مانند Full .NET Framework ، Xamarin ، Mono ، UWP و غیره میتوانند آنرا پیاده سازی کنند. یک نمونهی دیگر این پیاده سازیها نیز NET Core. است. برای مثال دات نت 4.6.1، استاندارد و قرار داد شمارهی 2 دات نت را پیاده سازی میکند. به همین صورت NET Core 2.0. نیز پیاده سازی کنندهی این استاندارد شماره 2 است.
با تغییرات اخیر، اکنون NuGet میتواند کتابخانههای مبتنی بر NET Standard 2. را در برنامههای مبتنی بر سکوهای کاری که آنرا پیاده سازی میکنند، بدون مشکل اضافه کند. برای مثال میتوان اسمبلیهای دات نت 4.6.1 را به برنامههای ASP.NET Core 2.0 اضافه کرد (کاری که در نگارش 1x آن به صورت مستقیم میسر نیست) و یا میتوان اسمبلیهای کامپایل شدهی برای دات نت استاندارد 2 را به برنامههای مبتنی بر دات نت 4.6.1 اضافه کرد.
آیا واقعا کتابخانههای قدیمی دات نتی توسط برنامههای NET Core 2.0. در لینوکس نیز اجرا خواهند شد؟
دات نت استاندارد، بیش از یک قرار داد چیزی نیست و پیاده سازی کنندگان آن میتوانند سطح بیشتری را نسبت به این قرار داد نیز لحاظ کنند. برای مثال دات نت 4.6.1 شامل سطح API بیشتری از دات نت استاندارد 2 است.
به همین جهت باید درنظر داشت که امکان اضافه کردن یک بستهی نیوگت از یک کتابخانهی نوشته شدهی برای دات نت کامل در برنامههای دات نت Core به معنای تضمینی برای کار کردن آن در زمان اجرا نخواهد بود. از این جهت که دات نت کامل، به همراه قسمتهایی است که در NET Standard. وجود خارجی ندارند. بنابراین اگر کتابخانهی استفاده شده صرفا این API مشترک را هدف قرار دادهاست، هم قابلیت اتصال و هم قابلیت اجرا را خواهد داشت؛ اما اگر برای مثال کسی بستهی NServiceBus را به پروژهی ASP.NET Core 2.0 اضافه کند، بدون مشکل کامپایل خواهد شد. اما از آنجائیکه این کتابخانه از MSMQ استفاده میکند که خارج از میدان دید این استاندارد است، در زمان اجرا با شکست مواجه خواهد شد.
«compatibility shim» در NET Standard 2.0. چگونه کار میکند؟
در NET Core.، پیاده سازی Object در System.Runtime قرار دارد و کد تولید شدهی توسط آن یک چنین ارجاعی را [System.Runtime]System.Object تولید میکند. اما در دات نت کلاسیک، System.Object در mscorlib قرار دارد. به همین جهت زمانیکه سعی کنید اسمبلیهای دات نت کلاسیک را در NET Core 1.x. استفاده کنید، پیام یافتن نشدن نوعها را دریافت خواهید کرد. اما در NET Core 2.0. یک پیاده سازی صوری (facade) از mscorlib وجود دارد که کار آن هدایت نوع درخواستی، به نوع واقعی پیاده سازی شدهی در NET Core. است.
در این تصویر استفادهی از یک کتابخانهی ثالث را مشاهده میکنید که ارجاعی را به [mscorlib]Microsoft.Win32.RegistryKey دارد (مبتنی بر دات نت کلاسیک است). همچنین یک mscorlib مشخص شدهی به صورت facade را نیز مشاهده میکنید. کار آن هدایت درخواست نوع واقع شدهی در mscorlib، به نوع موجود [Microsoft.Win32.Registry] Microsoft.Win32.RegistryKey است و تنها زمانی کار خواهد کرد که Microsoft.Win32.RegistryKey.dll وجود خارجی داشته باشد. به این معنا که رجیستری، یک مفهوم ویندوزی است و این کتابخانه بر روی ویندوز بدون مشکل کار میکند. اما تحت لینوکس، این قسمت خاص با پیام PlatformNotSupportedException خاتمه خواهد یافت. اما اگر قسمتهایی از این کتابخانه را استفاده کنید که در تمام سکوهای کاری وجود داشته باشند، بدون مشکل قادر به استفادهی از آن خواهید بود.
یک مثال: استفاده از کتابخانهی رمزنگاری اطلاعات Inferno
آخرین نگارش کتابخانهی رمزنگاری اطلاعات Inferno مربوط به NET 4.5.2. است. مراحل ذیل را پس از نصب SDK جدید NET Core 2.0. در خط فرمان طی میکنیم:
الف) ایجاد پوشهی UseNET452InNetCore2 و سپس ایجاد یک پروژهی کنسول جدید
dotnet new console
ب) افزودن بستهی نیوگت Inferno به پروژه
dotnet add package Inferno
log : Installing Inferno 1.4.0. warn : Package 'Inferno 1.4.0' was restored using '.NETFramework,Version=v4.6.1' instead of the project target framework '.NETCoreApp,Version=v2.0'. This package may not be fully compatible with your project. info : Package 'Inferno' is compatible with all the specified frameworks in project 'D:\UseNET452InNetCore2\UseNET452InNetCore2.csproj'. info : PackageReference for package 'Inferno' version '1.4.0' added to file 'D:\UseNET452InNetCore2\UseNET452InNetCore2.csproj'.
ج) استفاده از کتابخانهی Inferno جهت تولید یک عدد تصادفی thread safe
using System; using SecurityDriven.Inferno; namespace UseNET452InNetCore2 { class Program { static CryptoRandom random = new CryptoRandom(); static void Main(string[] args) { Console.WriteLine($"rnd: {random.NextLong()}"); } } }
د) اجرای برنامه
در ادامه اگر دستور dotnet run را صادر کنیم، ابتدا اخطاری را صادر میکند که این بسته ممکن است دارای قسمتهایی باشد که با NET core 2.0. سازگار نیست و سپس خروجی نهایی را بدون مشکل اجرا کرده و نمایش میدهد.
>dotnet run warning NU1701: This package may not be fully compatible with your project. rnd: 8167886599578111106
JazSharp is a unit testing framework for .NET that works very similar to Jasmine:
-
Tests are defined using Describe and It
-
Method calls can be replaced with spies allowing you to truly isolate the code you are testing
-
Full set of Assets (known as Expects) including ToHaveBeenCalledWith, ToEqual and ToContain which perform recursive comparisons