ارسال پارامترهای اختیاری به دستور Sql و مشکل با دستور is null
Select [VoucherRows].[Description] AS [Description],[VoucherRows].[Creditor] AS [Creditor],[VoucherRows].[Debtor] AS [Debtor],[Vouchers].[Number] AS [Number],[Vouchers].[SubmitDate] AS [SubmitDate] From [VoucherRows] AS [VoucherRows],[Vouchers] AS [Vouchers] Where [VoucherRows].[VoucherId] IS Null OR [Vouchers].[Id] = [VoucherRows].[VoucherId] And @p1 IS Null OR [Vouchers].[Number] >= @p1 And @p2 IS Null OR [Vouchers].[Number] <= @p2 And @p3 IS Null OR [Vouchers].[Description] Like @p3 Order By [Vouchers].[SubmitDate] DESC,[Vouchers].[Number] ASC
//The specified argument value for the function is not valid. [ Argument # = 1,Name of function(if known) = isnull ]
//new group $("#NewGroup").on('click', function () { var catName = 'new';// $("#appendNodeText").val(); $.ajax({ dataType: "json", type: "POST", url: "@Url.Action(MVC.Categories.NewCategory())", data: { name: catName } }).done(function (data) { alert(data); }); });
1.30 Visual Studio Code منتشر شد
Welcome to the November 2018 release of Visual Studio Code. There are a number of significant updates in this version that we hope you will like, some of the key highlights include:
- Multiline search improvements - Easily create multiline search patterns without using regex.
- Custom title bar on Linux - The custom title and menu bar is now the default on Linux.
- References view - Find All References view includes history of recent searches.
- Snippet comment variables - Snippet variables insert correct comment per language.
- JS/TS callback display - Now you can see the context of anonymous callbacks.
- JSDoc Markdown highlighting - Including syntax highlighting for Markdown code blocks in JSDoc.
- Simplified debug configuration - Better defaults and Quick Pick UI for initial launch configuration.
- Run tasks on folder open - Configure tasks to run when you first open a project folder.
- Choose extension version - Install earlier versions of Marketplace extensions.
public class LocalizedRequiredAttribute : RequiredAttribute, IClientValidatable { private readonly string _resourceKey; public string SourceName { get; set; } = LocalizationSourceNames.Default; public LocalizedRequiredAttribute(string resourceKey) { _resourceKey = resourceKey; } public override string FormatErrorMessage(string name) { return LocalizationHelper.GetString(SourceName, _resourceKey); } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { yield return new ModelClientValidationRequiredRule(FormatErrorMessage(null)); } }
- استفاده از Postman برای آزمایش یک برنامهی Web API
- استفاده از strest برای آزمایش یک برنامهی Web API
روش سومی هم برای انجام اینکار وجود دارد که به صورت توکار از زمان ارائهی ASP.NET Core 2.1 به همراه TestServer آزمایشی آن میسر شد. این روش در نگارش 3.1، با تغییر روش تعریف فایل program.cs، جهت سازگاری آن با آزمونهای یکپارچگی/آزمایش کل سیستم، بهبود یافتهاست که خلاصهای از آن را در این مطلب بررسی میکنیم.
آزمونهای یکپارچگی در ASP.NET Core
آزمونهای یکپارچگی، برخلاف آزمونهای واحد که عموما از اشیاء تقلیدی استفاده میکنند، دقیقا بر روی همان سیستمی که قرار است به کاربر نهایی ارائه شود، اجرا میشوند. به همین جهت تنظیمات اولیهی آنها کمی بیشتر است و همچنین زمان اجرای آنها نیز به علت وابستگی به بانک اطلاعاتی واقعی، فایل سیستم، شبکه و غیره، نسبت به آزمونهای واحد بیشتر است.
برای ایجاد آزمونهای یکپارچگی در برنامههای ASP.NET Core، حداقل سه مرحله باید طی شوند:
الف) ایجاد یک class library که ارجاعی را به پروژهی اصلی دارد. این پروژه حاوی آزمایشهای ما خواهد بود.
ب) راه اندازی یک هاست وب آزمایشی برای ارسال درخواستها به آن و دریافت پاسخهای نهایی.
ج) استفاده از یک test runner (انواع و اقسام فریم ورکهای unit testing) برای اجرای آزمایشها
ایجاد یک پروژهی کتابخانه برای هاست و اجرای آزمایشهای یکپارچگی
فرض کنید میخواهیم برای همان پروژهی ایجاد JWTها، آزمایش یکپارچگی بنویسیم. پس از ایجاد یک پروژهی کتابخانهی جدید که قرار است هاست آزمایشهای ما شود، نیاز است محتوای فایل csproj آنرا به صورت زیر تغییر داد:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <NoWarn>RCS1090</NoWarn> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\ASPNETCore2JwtAuthentication.WebApp\ASPNETCore2JwtAuthentication.WebApp.csproj" /> </ItemGroup> <ItemGroup> <None Include="..\ASPNETCore2JwtAuthentication.WebApp\appsettings.json" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> <ItemGroup> <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c329}" /> </ItemGroup> <ItemGroup> <PackageReference Include="fluentassertions" Version="5.10.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" /> <PackageReference Include="MSTest.TestFramework" Version="2.1.2" /> </ItemGroup> </Project>
1) TargetFramework آن باید به netcoreapp تنظیم شود.
2) باید ارجاع مستقیمی به کل پروژهی نهایی WebApp در آن وجود داشته باشد. چون در ادامه میخواهیم فایل Program.cs آنرا برای راه اندازی یک هاست وب آزمایشی، فراخوانی کنیم.
3) بستهی نیوگتی که کار راه اندازی هاست وب آزمایشی را انجام میدهد، Microsoft.AspNetCore.Mvc.Testing نام دارد. این بسته، کار کپی فایلهای پروژهی اصلی و همچنین تنظیم مسیر پروژه را به این مسیر جدید نیز انجام میدهد.
4) روش افزودن بستههای MSTest را مشاهده میکنید.
5) همچنین جهت سادهتر شدن بررسی نتایج آزمونهای انجام شده میتوان از fluentassertions نیز استفاده کرد.
راه اندازی هاست وب آزمایشی جهت انجام آزمونهای واحد
پس از انجام تنظیمات ابتدایی پروژهی آزمون یکپارچگی، نیاز است یک WebApplicationFactory سفارشی را ایجاد کرد:
using ASPNETCore2JwtAuthentication.WebApp; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; namespace ASPNETCore2JwtAuthentication.IntegrationTests { public class CustomWebApplicationFactory : WebApplicationFactory<Program> { protected override IWebHostBuilder CreateWebHostBuilder() { var builder = base.CreateWebHostBuilder(); builder.ConfigureLogging(logging => { //TODO: ... }); return builder; } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureTestServices(services => { // Don't run `IHostedService`s when running as a test services.RemoveAll(typeof(IHostedService)); }); } } }
در ادامه روش سفارشی سازی WebApplicationFactory را مشاهده میکنید. برای مثال اگر خواستید سرویسها و تنظیمات پیشفرض برنامهی اصلی را تغییر دهید میتوانید متد CreateWebHostBuilder را بازنویسی کنید و یا اگر خواستید سرویس جدیدی را اضافه و یا حذف کنید، میتوان متد ConfigureWebHost را بازنویسی کرد.
استفاده از WebApplicationFactory سفارشی، جهت ایجاد یک HttpClient
هدف اصلی از ایجاد CustomWebApplicationFactory نه فقط راه اندازی یک هاست وب سفارشی است، بلکه توسط متد CreateClient آن میتوان به یک HttpClient دسترسی یافت که قابلیت ارسال اطلاعات را به برنامهی وبی که در پشت صحنه راه اندازی میشود، دارا است. کار CustomWebApplicationFactory شبیه به راه اندازی dotnet run در پشت صحنهاست. در اینجا دیگر نیازی نیست تا اینکار را به صورت دستی انجام داد. به همین جهت چون برنامهی وب اصلی به نحو متداولی در پشت صحنه اجرا میشود، عموما راه اندازی آن که شامل تنظیمات اولیه و یا حتی ایجاد بانک اطلاعاتی است، کمی کند است و اگر قرار باشد هربار اینکار صورت گیرد، به آزمونهای بسیار کندی خواهیم رسید. به همین جهت میتوان یک کلاس singleton را برای مدیریت تک وهلهی نهایی HttpClient آن به صورت زیر ایجاد کرد:
using System; using System.Threading; using System.Net.Http; namespace ASPNETCore2JwtAuthentication.IntegrationTests { public static class TestsHttpClient { private static readonly Lazy<HttpClient> _serviceProviderBuilder = new Lazy<HttpClient>(getHttpClient, LazyThreadSafetyMode.ExecutionAndPublication); /// <summary> /// A lazy loaded thread-safe singleton /// </summary> public static HttpClient Instance { get; } = _serviceProviderBuilder.Value; private static HttpClient getHttpClient() { var services = new CustomWebApplicationFactory(); return services.CreateClient(); //NOTE: This action is very time consuming, so it should be defined as a singleton. } } }
نوشتن اولین آزمون یکپارچگی
پس از تنظیم هاست وب آزمایشی و ایجاد یک HttpClient از پیش تنظیم شده که به آن اشاره میکند، اکنون میتوان اولین آزمون یکپارچگی را به صورت زیر نوشت:
using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ASPNETCore2JwtAuthentication.IntegrationTests { [TestClass] public class JwtTests { [TestMethod] public async Task TestLoginWorks() { // Arrange var client = TestsHttpClient.Instance; // Act var token = await doLoginAsync(client); // Assert token.Should().NotBeNull(); token.AccessToken.Should().NotBeNullOrEmpty(); token.RefreshToken.Should().NotBeNullOrEmpty(); } [TestMethod] public async Task TestCallProtectedApiWorks() { // Arrange var client = TestsHttpClient.Instance; // Act var token = await doLoginAsync(client); // Assert token.Should().NotBeNull(); token.AccessToken.Should().NotBeNullOrEmpty(); token.RefreshToken.Should().NotBeNullOrEmpty(); // Act const string protectedApiUrl = "/api/MyProtectedApi"; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); var response = await client.GetAsync(protectedApiUrl); response.EnsureSuccessStatusCode(); // Assert var responseString = await response.Content.ReadAsStringAsync(); responseString.Should().NotBeNullOrEmpty(); var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var apiResponse = JsonSerializer.Deserialize<MyProtectedApiResponse>(responseString, options); apiResponse.Title.Should().NotBeNullOrEmpty(); apiResponse.Title.Should().Be("Hello from My Protected Controller! [Authorize]"); } private static async Task<Token> doLoginAsync(HttpClient client) { const string loginUrl = "/api/account/login"; var user = new { Username = "Vahid", Password = "1234" }; var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, loginUrl) { Content = new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json") }); response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); responseString.Should().NotBeNullOrEmpty(); return JsonSerializer.Deserialize<Token>(responseString); } } }
- در هر آزمونی نیاز است در ابتدا به TestsHttpClient.Instance، که همان HttpClient ساخته شدهی توسط CustomWebApplicationFactory است، دسترسی یافت و همانطور که عنوان شد، دسترسی به وهلهای از HttpClient که به هاست وب آزمایشی برنامهی اصلی اشاره میکند، عموما بسیار زمانبراست و برای مثال در دو آزمایش نوشته شدهی در اینجا اگر قرا باشد هربار اینکار از صفر انجام شود، زمان به اتمام رسیدن این آزمایشها بسیار طولانی خواهد شد. به همین جهت طول عمر TestsHttpClient را singleton تعریف کردیم تا فقط یکبار کار برپایی وب سرور آزمایشی در پشت صحنه انجام شود.
- سپس مابقی کار، همان روش استاندارد کار با HttpClient است. در ابتدا درخواستی را به سمت سرور آزمایشی که در پشت صحنه در حال اجرا است، ارسال میکنیم. چون HttpClient دریافتی توسط CustomWebApplicationFactory تنظیم شدهاست، دیگر نیازی به ذکر آدرس پایهی وب سایت مانند https://localhost:5001 نیست و آدرسهای ذکر شدهی در اینجا، نسبی هستند. سپس محتوای Response دریافتی از سرور را جهت تکمیل آزمایشات، بررسی خواهیم کرد.
یک نکته: اگر OpenAPI را در برنامههای Web API فعال کنید، میتوان با استفاده از ابزارهای تولید کد، کدهای مرتبط با HttpClient را نیز به صورت خودکار تولید و سپس از آنها در اینجا استفاده کرد.
اجرای آزمونهای یکپارچگی نوشته شده
چون ظاهر این آزمونها با آزمونهای واحد MSTest یا هر فریم ورک مشابه دیگری یکسان است، میتوان از امکانات IDEها برای اجرای آنها استفاده کرد و یا حتی میتوان دستور dotnet test را نیز در ریشهی این پروژهی جدید برای اجرای تمام آزمونهای نوشته شده، اجرا کرد:
کدهای کامل این مطلب را در اینجا میتوانید مشاهده کنید.
public class BaseEntity : IBaseEntity { [JsonIgnore] int Id { get; set; } [JsonIgnore] string? Audit { get; set; } }
public class AuditSourceValues { [JsonProperty("hn")] public string? HostName { get; set; } [JsonProperty("mn")] public string? MachineName { get; set; } [JsonProperty("rip")] public string? RemoteIpAddress { get; set; } [JsonProperty("lip")] public string? LocalIpAddress { get; set; } [JsonProperty("ua")] public string? UserAgent { get; set; } [JsonProperty("an")] public string? ApplicationName { get; set; } [JsonProperty("av")] public string? ApplicationVersion { get; set; } [JsonProperty("cn")] public string? ClientName { get; set; } [JsonProperty("cv")] public string? ClientVersion { get; set; } [JsonProperty("o")] public string? Other { get; set; } }
public class EntityAudit<TEntity> { [JsonProperty("type")] [JsonConverter(typeof(StringEnumConverter))] public EntityEventType EventType { get; set; } [JsonProperty("user", NullValueHandling = NullValueHandling.Include)] public int? ActorUserId { get; set; } [JsonProperty("at")] public DateTime ActDateTime { get; set; } [JsonProperty("sources")] public AuditSourceValues? AuditSourceValues { get; set; } [JsonProperty("newValues", NullValueHandling = NullValueHandling.Include)] public TEntity NewEntity { get; set; } = default!; public string? SerializeJson() { return JsonSerializer.Serialize(this, options: new JsonSerializerOptions { WriteIndented = false, IgnoreNullValues = true }); } }
دقت کنید که این کلاس به صورت جنریک تعریف شده است تا اگر بعدا بخواهیم آن را Deserialize کنیم و مثلا از آن API بسازیم، یا استفادهی خاصی را از آن داشته باشیم، بهراحتی به Entity مد نظر تبدیل شود. در این مقاله فقط به ذخیرهی آن پرداخته میشود و استفاده از این فیلد که به راحتی و با کمک DbFunctionها در Entity Framework قابل انجام است به خواننده واگذار میشود.
public enum EntityEventType { Create = 0, Update = 1, Delete = 2 }
public interface IAuditSourcesProvider { AuditSourceValues GetAuditSourceValues(); }
public class AuditSourcesProvider : IAuditSourcesProvider { protected readonly IHttpContextAccessor HttpContextAccessor; public AuditSourcesProvider(IHttpContextAccessor httpContextAccessor) { HttpContextAccessor = httpContextAccessor; } public virtual AuditSourceValues GetAuditSourceValues() { var httpContext = HttpContextAccessor.HttpContext; return new AuditSourceValues { HostName = GetHostName(httpContext), MachineName = GetComputerName(httpContext), LocalIpAddress = GetLocalIpAddress(httpContext), RemoteIpAddress = GetRemoteIpAddress(httpContext), UserAgent = GetUserAgent(httpContext), ApplicationName = GetApplicationName(httpContext), ClientName = GetClientName(httpContext), ClientVersion = GetClientVersion(httpContext), ApplicationVersion = GetApplicationVersion(httpContext), Other = GetOther(httpContext) }; } protected virtual string? GetUserAgent(HttpContext httpContext) { return httpContext.Request?.Headers["User-Agent"].ToString(); } protected virtual string? GetRemoteIpAddress(HttpContext httpContext) { return httpContext.Connection?.RemoteIpAddress?.ToString(); } protected virtual string? GetLocalIpAddress(HttpContext httpContext) { return httpContext.Connection?.LocalIpAddress?.ToString(); } protected virtual string GetHostName(HttpContext httpContext) { return httpContext.Request.Host.ToString(); } protected virtual string GetComputerName(HttpContext httpContext) { return Environment.MachineName; } protected virtual string? GetApplicationName(HttpContext httpContext) { return Assembly.GetEntryAssembly()?.GetName().Name; } protected virtual string? GetApplicationVersion(HttpContext httpContext) { return Assembly.GetEntryAssembly()?.GetName().Version.ToString(); } protected virtual string? GetClientVersion(HttpContext httpContext) { return httpContext.Request?.Headers["client-version"]; } protected virtual string? GetClientName(HttpContext httpContext) { return httpContext.Request?.Headers["client-name"]; } protected virtual string? GetOther(HttpContext httpContext) { return null; } }
حالا برای تامین اطلاعات کلاس EntityAudit کار مشابهی میکنیم. ابتدا اینترفیس IEntityAuditProvider را به صورت زیر تعریف میکنیم:
public interface IEntityAuditProvider { string? GetAuditValues(EntityEventType eventType, object? entity, string? previousJsonAudit = null); }
و سپس کلاس EntityAuditProvider را ایجاد میکنیم:
public class EntityAuditProvider : IEntityAuditProvider { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuditSourcesProvider _auditSourcesProvider; #region Constructor Injections public EntityAuditProvider(IHttpContextAccessor httpContextAccessor, IAuditSourcesProvider auditSourcesProvider) { _httpContextAccessor = httpContextAccessor; _auditSourcesProvider = auditSourcesProvider; } #endregion public virtual string? GetAuditValues(EntityEventType eventType, object? newEntity, string? previousJsonAudit = null) { var httpContext = _httpContextAccessor.HttpContext; int? userId; var user = httpContext.User; if (!user.Identity.IsAuthenticated) userId = null; else userId = user.Claims.Where(x => x.Type == "UserID").Select(x => x.Value).First().ToInt(); var auditSourceValues = _auditSourcesProvider.GetAuditSourceValues(); var auditJArray = new JArray(); // Update & Delete if (eventType == EntityEventType.Update || eventType == EntityEventType.Delete) { auditJArray = JArray.Parse(previousJsonAudit!); } // Delete => No NewValues if (eventType == EntityEventType.Delete) { newEntity = null; } JObject newAuditJObject = JObject.FromObject(new EntityAudit<object?> { EventType = eventType, ActorUserId = userId, ActDateTime = DateTime.Now, AuditSourceValues = auditSourceValues, NewEntity = newEntity }, new JsonSerializer { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None }); auditJArray.Add(newAuditJObject); return auditJArray.SerializeToJson(true); } }
public class AuditSaveChangesInterceptor : SaveChangesInterceptor { private readonly IEntityAuditProvider _entityAuditProvider; #region Constructor Injections public AuditSaveChangesInterceptor(IEntityAuditProvider entityAuditProvider) { _entityAuditProvider = entityAuditProvider; } #endregion public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { ApplyAudits(eventData.Context.ChangeTracker); return base.SavingChanges(eventData, result); } public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = new CancellationToken()) { ApplyAudits(eventData.Context.ChangeTracker); return base.SavingChangesAsync(eventData, result, cancellationToken); } private void ApplyAudits(ChangeTracker changeTracker) { ApplyCreateAudits(changeTracker); ApplyUpdateAudits(changeTracker); ApplyDeleteAudits(changeTracker); } private void ApplyCreateAudits(ChangeTracker changeTracker) { var addedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Added); foreach (var addedEntry in addedEntries) { if (addedEntry.Entity is IBaseEntity entity) { entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Create, entity); } } } private void ApplyUpdateAudits(ChangeTracker changeTracker) { var modifiedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Modified); foreach (var modifiedEntry in modifiedEntries) { if (modifiedEntry.Entity is IBaseEntity entity) { var eventType = entity.IsArchived ? EntityEventType.Delete : EntityEventType.Update; // Maybe Soft Delete entity.Audit = _entityAuditProvider.GetAuditValues(eventType, entity, entity.Audit); } } } private void ApplyDeleteAudits(ChangeTracker changeTracker) { var deletedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Deleted); foreach (var modifiedEntry in deletedEntries) { if (modifiedEntry.Entity is IBaseEntity entity) { entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Delete, entity, entity.Audit); } } } }
و سپس آن را به سیستم معرفی میکنیم:
services.AddDbContext<ATADbContext>((serviceProvider, options) => { options .UseSqlServer(...) // Interceptors var entityAuditProvider = serviceProvider.GetRequiredService<IEntityAuditProvider>(); options.AddInterceptors(new AuditSaveChangesInterceptor(entityAuditProvider)); });
نمونهی کامل فیلد Audit که در JsonFormatter قرار داده شده است، بعد از ایجاد شدن و یکبار آپدیت و سپس حذف نرم رکورد:
[ { "type":"Create", "user":1, "at":"2020-11-24T23:05:54.2692711+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":{ "Name":"Farshad" } }, { "type":"Update", "user":1, "at":"2020-11-24T23:06:20.0838188+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":{ "Name":"Edited Farshad" } }, { "type":"Delete", "user":null, "at":"2020-11-24T23:06:28.601837+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":null } ]
ولی روش گفته شده در این مقاله، همین عملیات را به صورت کاملتری و فقط بر روی یک ستون همان جدول انجام میدهد که باعث ذخیرهی دیتای کمتر، یکپارچگی بهتر و دسترسیپذیری و راحتی استفاده از آن میشود.
تا اینجا در قسمت سوم، نحوهی قراردادن یک کامپوننت را در کامپوننتی دیگر، توسط مقدار دهی خاصیت directives مزین کنندهی Component بررسی کردیم. همینقدر که یک کامپوننت دارای selector باشد، قابلیت قرارگرفتن در یک کامپوننت دیگر را دارد. اما چگونه باید بین این کامپوننتها ارتباط برقرار کرد؟
تهیه کامپوننت نمایش ستارهای امتیازهای محصولات
مثال نمایش لیست محصولات سری جاری، دارای ستون «5Star Rating» است. در این قسمت میخواهیم بجای نمایش عددی این امتیازها، کامپوننتی را طراحی کنیم که نماش ستارهای آنها را سبب شود. این کامپوننت باید بتواند یک مقدار ورودی، یا همان عدد امتیاز محصول را از کامپوننت دربرگیرندهی آن دریافت کند. همچنین میخواهیم اگر کاربر بر روی این ستارهها کلیک کرد، کامپوننت در برگیرنده را نیز مطلع سازیم.
در این مثال در فایل product-list.component.html چنین سطری تعریف شدهاست:
<td>{{ product.starRating }}</td>
با توجه به اینکه کامپوننت نمایش ستارهای امتیازها، قابلیت استفادهی مجدد را دارد و الزامی ندارد که حتما در لیست محصولات، بکار گرفته شود، بهتر است محل تعریف آنرا به خارج از پوشهی products فعلی منتقل کنیم. برای مثال میتوان پوشهی app\shared را برای آن و تمامی کامپوننتهای با قابلیت استفادهی مجدد ایجاد کرد.
برای شروع، فایل جدید App\shared\star.component.ts را اضافه کنید؛ با کدهای کامل ذیل:
import { Component, OnChanges, Input, Output, EventEmitter } from 'angular2/core'; @Component({ selector: 'ai-star', templateUrl: 'app/shared/star.component.html', styleUrls: ['app/shared/star.component.css'] }) export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>(); ngOnChanges(): void { this.starWidth = this.rating * 86 / 5; } onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); } }
سپس مسیر template و مسیر فایل css ویژهی آن، در تزئین کنندهی Component مشخص شدهاند. محتوای کامل این دو فایل را در ذیل مشاهده میکنید:
الف) محتوای فایل App\shared\star.component.html
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'> <div style="width: 86px"> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> </div> </div>
.crop { overflow: hidden; } div { cursor: pointer; }
معرفی مقدماتی life cycle hooks در قسمت قبل صورت گرفت. در اینجا چون نیاز است به ازای هر بار رندر شدن این کامپوننت، عرض آن متفاوت باشد، بنابراین نیاز است راهی را پیدا کنیم تا بتوان مقدار خاصیت starWidth را متغیر کرد. به همین منظور از hook مخصوص این تغییرات یا همان OnChanges استفاده میشود. بنابراین باید کلاس این کامپوننت، اینترفیس OnChanges را پیاده سازی کند. پس از آن، importهای لازم جهت تعریف OnChanges به ابتدای فایل اضافه شده و همچنین متد ngOnChanges نیز جهت تکمیل کار پیاده سازی اینترفیس OnChanges، به کلاس جاری اضافه میشود.
کار متد ngOnChanges، تبدیل عدد امتیاز یک محصول، به عرض div نمایش ستارهها است.
مکانیزم کار رخداد ngOnChanges و دریافت اطلاعات از والد
متد ngOnChanges، تنها به خواص ویژهای به نام «input properties» واکنش نشان میدهد. اگر یک کامپوننت تو در توی قرار گرفتهی در یک کامپوننت دیگر، بخواهد اطلاعاتی را از والد خود دریافت کند، باید خاصیتی را در معرض دید آن دربرگیرنده قرار دهد. این کار توسط decorator ویژهای به نام ()Input@ انجام میشود.
به همین جهت است که پیش از خاصیت rating در کلاس StarComponent، شاهد درج مزین کنندهی ویژهی ()Input@ هستیم:
export class StarComponent implements OnChanges { @Input() rating: number;
پس از آن، کامپوننت دربرگیرنده یا والد، این خاصیت ورودی ویژه را از طریق روش property binding متداول، مقدار دهی میکند:
[rating]='product.starRating'
بدیهی است در اینجا چون خاصیت starWidth از نوع ورودی تعریف نشدهاست، قابلیت property binging فوق را در کامپوننت والد، ندارد.
اکنون به ازای هر بار نمایش این کامپوننت فرزند، خاصیت rating ورودی آن مقدار دهی شده و مقدار آن در رخداد ngOnChanges قابل دسترسی و استفاده خواهد بود. اینجا است که میتوان از این مقدار تغییر یافته، جهت ترجمهی آن به عرض div نمایش ستارهها، استفاده کرد.
ارسال دادهها از کامپوننت فرزند به کامپوننت والد
تا اینجا با استفاده از «خواص ورودی» امکان دسترسی به مقادیر ارسالی از طرف والد را در کامپوننت فرزند، پیدا کردیم. عکس آن نیز امکان پذیر است؛ اما توسط رخدادها.
کامپوننت فرزند، با استفاده از decorator ویژهی دیگری به نام ()Output@ امکان ارسال رخدادها را به کامپوننت والد پیدا میکند:
export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
در مثال جاری اگر کاربر بر روی div ستارههای نمایش داده شده کلیک کند، اتصال به آن از طریق event binging متداول انجام میشود (متد جدید onClick به رخداد click متصل شدهاست):
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'>
onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); }
تا اینجا مرحلهی تنظیمات رخدادها در کامپوننت فرزند صورت گرفت. ابتدا خاصیتی از نوع Output تعریف شد. سپس در کدهای قالب این کامپوننت جدید، متد onClick به رخداد click متصل گردید و سپس در کدهای مدیریت کنندهی این متد، متد ratingClicked.emit جهت ارسال اطلاعات نهایی به والد، فراخوانی گردید.
اکنون در کامپوننت والد، باید این مراحل برای دریافت اطلاعات از کامپوننت فرزند خود، طی شوند:
الف) ابتدا نام خاصیت مزین شدهی با Output، به عنوان مقصد event binding مشخص میشود و سپس متدی در کلاس کامپوننت والد، به آن متصل میگردد:
(ratingClicked)='onRatingClicked($event)'
ب) در ادامه، تعریف این متد جدید متصل شده را به کلاس ProductListComponent اضافه میکنیم:
onRatingClicked(message: string): void { this.pageTitle = 'Product List: ' + message; }
به این ترتیب با کلیک بر روی div هر کامپوننت نمایش ستارهای امتیازها، خاصیت pageTitle درج شدهی در صفحه تغییر میکند.
استفاده از کامپوننت نمایش ستارهای امتیازها
نکات کلی افزودن این کامپوننت جدید، تفاوتی با مطالب عنوان شدهی در قسمت سوم، در حین بررسی مراحل افزودن دایرکتیو نمایش لیست محصولات، به کامپوننت ریشهی سایت ندارد و یکی هستند.
برای افزودن و استفاده از این کامپوننت جدید، ابتدا قالب product-list.component.html را گشوده و سپس سطر نمایش عددی امتیاز یک محصول را به نحو ذیل تغییر میدهیم:
<td> <ai-star [rating]='product.starRating' (ratingClicked)='onRatingClicked($event)'> </ai-star> </td>
سپس باید به کلاس کامپوننت لیست محصولات (کامپوننت در برگیرنده) اعلام کرد که این کامپوننت جدید را باید از کجا پیدا کند. برای این منظور فایل product-list.component.ts را گشوده و خاصیت directives این کامپوننت را مقدار دهی میکنیم:
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; import { StarComponent } from '../shared/star.component'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe], directives: [StarComponent] })
نمونهای از اجرای برنامه را در تصویر ذیل مشاهده میکنید:
در اینجا ستون امتیازهای محصولات با کامپوننت نمایش ستارهای این امتیازها جایگزین شدهاست و همچنین با کلیک بر روی یکی از آنها، عنوان panel جاری تغییر کردهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part6.zip
خلاصهی بحث
در اینجا نحوهی طراحی API عمومی یک کامپوننت را بررسی کردیم. تا زمانیکه خواص کلاس یک کامپوننت به نحو متداولی تعریف میشوند، میدان دید آنها محدود است به قالب تعریف شدهی متناظر با آنها. اگر نیاز است خاصیتی خارج از این قالب و به صورت عمومی در کامپوننت دربرگیرندهی دیگری در دسترس قرار گیرد، آنرا با مزین کنندهی ()Input@ مشخص میکنیم و اگر قرار است این کامپوننت فرزند، اطلاعاتی را به کامپوننت والد ارسال کند، اینکار را توسط رخدادها و با تعریف ویژگی ()Output@ و EventEmitter انجام میدهد. نوع آرگومان جنریک EventEmitter، تعیین کنندهی نوع اطلاعاتی است که قرار است به کامپوننت دربرگیرنده ارسال شوند.
پس از تعریف کامپوننت فرزند، برای تعریف آن در کامپوننت والد، از نام selector آن به عنوان یک المان جدید HTML استفاده میشود و سپس با استفاده از property binding، اطلاعات لازم، به خاصیت از نوع ()Input@ کامپوننت فرزند ارسال میگردد. از event binding برای دریافت رخدادها از کامپوننت فرزند استفاده میشود. در اینجا هر رخدادی که توسط مزین کنندهی ()Output@ تعریف شده باشد، میتواند به عنوان مقصد event binding تعریف شود و اگر نیاز است به رخدادهای property binding از والد به فرزند، گوش فرا داد، میتوان اینترفیس OnChanges را در کلاس کامپوننت فرزند پیاده سازی کرد.
معرفی موجودیت Person
در مثال این مطلب قصد داریم، معادل توابع بومی مخصوص SQL Server را که امکان کار با DateTime را مهیا میکنند، در EF Core تعریف کنیم. به همین جهت نیاز به موجودیتی داریم که دارای خاصیتی از این نوع باشد:
using System; namespace EFCoreDbFunctionsSample.Entities { public class Person { public int Id { get; set; } public string Name { get; set; } public DateTime AddDate { get; set; } } }
گزارشگیری بر اساس تعداد روز گذشتهی از ثبت نام
اکنون فرض کنید میخواهیم گزارشی را از تمام کاربرانی که در طی 10 روز قبل ثبت نام کردهاند، تهیه کنیم. اگر کوئری زیر را برای این منظور تهیه کنیم:
var usersInfo = context.People.Where(person => (DateTime.Now - person.AddDate).Days <= 10).ToList();
'The LINQ expression 'DbSet<Person>.Where(p => (DateTime.Now - p.AddDate).Days <= 10)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
SELECT [p].[Id], [p].[AddDate], [p].[Name] FROM [People] AS [p] WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= 10
روش تعریف تابع DATEDIFF سفارشی در EF Core
برای تعریف متد DateDiff مخصوص EF Core، ابتدا باید یک کلاس static را تعریف کرد و سپس تنها امضای این متد را، معادل امضای تابع توکار SQL Server تعریف کرد. این متد نیازی نیست تا پیاده سازی را داشته باشد. به همین جهت بدنهی آنرا صرفا با یک throw new InvalidOperationException مقدار دهی میکنیم. هدف از این متد، استفادهی از آن در LINQ Expressions است و قرار نیست به صورت مستقیمی بکار گرفته شود:
namespace EFCoreDbFunctionsSample.DataLayer { public enum SqlDateDiff { Year, Quarter, Month, DayOfYear, Day, Week, Hour, Minute, Second, MilliSecond, MicroSecond, NanoSecond } public static class SqlDbFunctionsExtensions { public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) => throw new InvalidOperationException($"{nameof(SqlDateDiff)} method cannot be called from the client side."); public static readonly MethodInfo SqlDateDiffMethodInfo = typeof(SqlDbFunctionsExtensions) .GetRuntimeMethod( nameof(SqlDbFunctionsExtensions.SqlDateDiff), new[] { typeof(SqlDateDiff), typeof(DateTime), typeof(DateTime) } ); } }
روش معرفی تابع DATEDIFF سفارشی به EF Core
پس از تعریف امضای متد معادل DateDiff، اکنون نوبت به معرفی آن به EF Core است:
namespace EFCoreDbFunctionsSample.DataLayer { public class ApplicationDbContext : DbContext { // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasDbFunction(SqlDbFunctionsExtensions.SqlDateDiffMethodInfo) .HasTranslation(args => { var parameters = args.ToArray(); var param0 = ((SqlConstantExpression)parameters[0]).Value.ToString(); return SqlFunctionExpression.Create("DATEDIFF", new[] { new SqlFragmentExpression(param0), // It should be written as DateDiff(day, ...) and not DateDiff(N'day', ...) . parameters[1], parameters[2] }, SqlDbFunctionsExtensions.SqlDateDiffMethodInfo.ReturnType, typeMapping: null); }); } } }
سپس توسط متد HasTranslation، مشخص میکنیم که این متد به چه نحوی قرار است به یک عبارت SQL ترجمه شود. پارامتر args ای که در اینجا در اختیار ما قرار میگیرد، دقیقا همان پارامترهای متد public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) هستند که در این مثال خاص، شامل سه پارامتر میشوند. پارامترهای دوم و سوم آنرا به همان نحوی که دریافت میکنیم، به SqlFunctionExpression.Create ارسال خواهیم کرد. اما پارامتر اول را از نوع enum تعریف کردهایم و همچنین قرار نیست به صورت 'N'day و رشتهای به سمت بانک اطلاعاتی ارسال شود، بلکه باید به همان نحو اصلی آن (یعنی day)، در کوئری نهایی درج گردد، به همین جهت ابتدا Value آنرا استخراج کرده و سپس توسط SqlFragmentExpression عنوان میکنیم آنرا باید به همین نحو درج کرد.
پارامتر اول متد SqlFunctionExpression.Create، باید دقیقا معادل نام متد توکار مدنظر باشد. پارامتر دوم آن، لیست پارامترهای این تابع است. پارامتر سوم آن، نوع خروجی این تابع است که از طریق MethodInfo معادل، قابل استخراج است.
استفادهی از DbFunction سفارشی جدید در برنامه
پس از این تعاریف و معرفیها، اکنون میتوان متد سفارشی SqlDateDiff تهیه شده را به صورت مستقیمی در کوئریهای LINQ استفاده کرد تا قابلیت ترجمهی به SQL را پیدا کنند:
var sinceDays = 10; users = context.People.Where(person => SqlDbFunctionsExtensions.SqlDateDiff(SqlDateDiff.Day, person.AddDate, DateTime.Now) <= sinceDays).ToList(); /* SELECT [p].[Id], [p].[AddDate], [p].[Name] FROM [People] AS [p] WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= @__sinceDays_0 */
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: EFCoreDbFunctionsSample.zip
این کدها به همراه چند تابع سفارشی دیگر نیز هستند.
public class XamAppExceptionHandler : BitExceptionHandler { public override void OnExceptionReceived(Exception exp, IDictionary<string, string> properties = null) { #if DEBUG System.Diagnostics.Debugger.Break(); #endif base.OnExceptionReceived(exp, properties); } }
BitExceptionHandler.Current = new XamAppExceptionHandler();
حال برای لاگ کردن این خطاها، میتوانید از Microsoft's AppCenter استفاده کنید. استفاده از امکانات App Center رایگان بوده و برای استفادهی در ایران محدودیتی ندارد. ابتدا در سایت مربوطه ثبت نام کنید و سپس سه بار Add New app را بزنید و به نامهای XamApp_Windows، XamApp_iOS و XamApp_Android سه برنامه را بسازید و برای Android و iOS گزینهی Xamarin را انتخاب و برای ویندوز نیز UWP را انتخاب کنید.
سپس پکیجهای Microsoft.AppCenter.Crashes و Microsoft.AppCenter.Analytics را بر روی XamApp نصب نموده و کد زیر را در سه فایل AppDelegate.cs/MainActivity.cs/App.xaml.cs برای iOS/Android/Windows کپی کنید:
AppCenter.Start("copy-your-guid-key-for-iOS-Android-Windows-here", typeof(Crashes), typeof(Analytics));
برای هر یک از برنامههای Android/iOS/Windows یک Guid متفاوت دارید که در قسمت Getting Started در سایت App Center میتوانید آنها را مشاهده کنید. هر بار که این کد را کپی میکنید، مقدار Guid درست را بگذارید.
برای گزارش کردن خطاهای برنامه، کافی است کد زیر را به XamAppExceptionHandler.cs که در ابتدای این قسمت در موردش صحبت کرده بودیم اضافه کنید:
Crashes.TrackError(exp, properties);
حال اگر برنامه را اجرا و شروع به تست کنید و یا آن را در اختیار تسترها و مشتریها بگذارید، نه تنها گزارش تمامی خطاها و کرشها را خواهید داشت که حتی آمار استفاده کنندههای برنامه (شامل کشور و مشخصات دستگاه و ...) را نیز خواهید داشت.
حال در یک کد ده خطی، اگر در خط پنجم خطایی رخ دهد، اگر چه باعث بسته شدن برنامه نمیشود و لاگ نیز میشود، ولی در مواقعی خیلی خاص، شاید بخواهید در صورت رخ دادن خطا، چند خط کد بعدی کماکان اجرا شوند. در این حالت شما Try/Catch مینویسید که برای عبور کردن از خطا از آن استفاده کردهاید. در این صورت، ترجیحا آن را به شکل زیر بنویسید:
// code 1... try { // code 2... } catch (Exception ex) { BitExceptionHandler.Current.OnExceptionReceived(ex, new Dictionary<string, string> { { "SomeData", "2" } }); } // code 3...
در این کد مثال، فرض کنیم که برخی اوقات در code 2 خطایی رخ میدهد که برای ما مهم نیست و میخواهیم حتی در صورت رخ دادن خطا، code 3 اجرا شود. توصیه میکنیم در این موارد که در برنامه خیلی هم نباید متداول باشد، لااقل خطا را با کمک کد BitExceptionHandler.Current.OnExceptionReceived لاگ کنید و همچنین با داشتن یک Dictionary میتوانید حتی دیتای بیشتری را نیز به AppCenter فرستاده و در پرتال مربوطه مشاهده کنید.
به صورت کلی بهتر است از این نوع Try/Catchها پرهیز کنید و حتی اگر جایی Catch ای نوشتید، در نهایت دوباره خطا را throw کنید.
try { // some codes... } catch { // Do something related to exception... // for example, show some alerts to the user. throw; // You don't need to call BitExceptionHandler.Current.OnExceptionReceived... }
در مثال فوق، قصد داریم وقتی خطایی رخ داد، پیامی را به کاربر اطلاع دهیم. در این صورت، پس از نمایش پیام مربوطه، مجددا خطا را throw کنید. در این صورت، نیازی به فراخوانی BitExceptionHandler.Current.OnExceptionReceieved نیز نیست.
البته AppCenter در زمینه پابلیش کردن برنامه و همچنین Push Notification و ... نیز دارای امکاناتی هست که به موضوع این قسمت ارتباطی ندارند.