public class Attachment { #region Properties /// <summary> /// sets or gets identifier for attachment /// </summary> public virtual Guid Id { get; set; } /// <summary> /// sets or gets name for attachment /// </summary> public virtual string FileName { get; set; } /// <summary> /// sets or gets type of attachment /// </summary> public virtual string ContentType { get; set; } /// <summary> /// sets or gets size of attachment /// </summary> public virtual long Size { get; set; } /// <summary> /// sets or gets Extention of attachment /// </summary> public virtual string Extensions { get; set; } /// <summary> /// sets or gets Creation Date /// </summary> public virtual DateTime CreatedOn { get; set; } /// <summary> /// gets or sets counts of download this file /// </summary> public virtual long DownloadsCount { get; set; } public virtual DateTime ModifiedOn { get; set; } #endregion #region NavigationProperties /// <summary> /// sets or gets identifier of attachment's owner /// </summary> public virtual long OwnerId { get; set; } /// <summary> /// sets or gets identifier of attachment's owner /// </summary> public virtual User Owner { get; set; } #endregion }
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; namespace BlazorWasm.Client.Utils { public class JwtInfo { public IEnumerable<Claim> Claims { set; get; } public DateTime? ExpirationDateUtc { set; get; } public bool IsExpired { set; get; } public IEnumerable<string> Roles { set; get; } } /// <summary> /// From the Steve Sanderson’s Mission Control project: /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs /// </summary> public static class JwtParser { public static JwtInfo ParseClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = getBase64WithoutPadding(payload); foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes)) { if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array) { foreach (var itemValue in element.EnumerateArray()) { claims.Add(new Claim(keyValue.Key, itemValue.ToString())); } } else { claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString())); } } var roles = getRoles(claims); var expirationDateUtc = getDateUtc(claims, "exp"); var isExpired = getIsExpired(expirationDateUtc); return new JwtInfo { Claims = claims, Roles = roles, ExpirationDateUtc = expirationDateUtc, IsExpired = isExpired }; } private static IList<string> getRoles(IList<Claim> claims) => claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList(); private static byte[] getBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } private static bool getIsExpired(DateTime? expirationDateUtc) => !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow); private static DateTime? getDateUtc(IList<Claim> claims, string type) { var exp = claims.SingleOrDefault(claim => claim.Type == type); if (exp == null) { return null; } var expValue = getTimeValue(exp.Value); if (expValue == null) { return null; } var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); return dateTimeEpoch.AddSeconds(expValue.Value); } private static long? getTimeValue(string claimValue) { if (long.TryParse(claimValue, out long resultLong)) return resultLong; if (float.TryParse(claimValue, out float resultFloat)) return (long)resultFloat; if (double.TryParse(claimValue, out double resultDouble)) return (long)resultDouble; return null; } } }
public class Field { [JsonExtensionData] public Dictionary<string, object> Property { get; set; } } public class FieldType { public string Type { get; set; } }
var data = new { model = new { fields = new List<Field> { new Field { Property = new Dictionary<string, object> { {"Id", new FieldType { Type = "number" }}, {"Name", new FieldType { Type = "string" }} } } } } }; var dataJson = JsonConvert.SerializeObject(data, Formatting.Indented);
{ "model": { "fields": [ { "Id": { "Type": "number" }, "Name": { "Type": "string" } } ] } }
- اگر از ASP.NET MVC استفاده میکنید، نیاز است از آن کمک بگیرید. از این جهت که خاصیت JsonExtensionData سبب میشود تا نام ثابت خاصیت Property، از خروجی نهایی حذف شود و اعضای دیکشنری، جزئی از خاصیتهای موجود شوند.
- نکتهی «گرفتن خروجی CamelCase از JSON.NET» را هم باید مد نظر داشته باشید.
Failed to load resource: the server responded with a status of 500 (Internal Server Error)
[HttpPost] public ActionResult EditProductData(string oper, string title,string groupTitle,string shopTitle,int AvailableCount,int Priority, int? id) { if (oper == "add") { var product = new Product() { Title = title, }; db.Products.Add(product); db.SaveChanges(); return Content("true"); } else if (oper == "del") { var product = db.Products.Find(id); db.Products.Remove(product); return Content("true"); } else if (oper == "edit") { var product = db.Products.Find(id); product.Title = title; product.GroupId = int.Parse(groupTitle); product.ShopId = int.Parse(shopTitle); product.AvailableCount = AvailableCount; product.Priority = Priority; db.SaveChanges(); return Content("true"); } return Content("false"); }
namespace ReUsableQueries.Model { public class Student { public int Id { get; set; } public string Name { get; set; } public string LastName { get; set; } public int Age { get; set; } [ForeignKey("BornInCityId")] public virtual City BornInCity { get; set; } public int BornInCityId { get; set; } } public class City { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Student> Students { get; set; } } }
using System.Data.Entity; using ReUsableQueries.Model; namespace ReUsableQueries.DAL { public class MyContext : DbContext { public DbSet<City> Cities { get; set; } public DbSet<Student> Students { get; set; } } }
public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { var city1 = new City { Name = "city-1" }; var city2 = new City { Name = "city-2" }; context.Cities.Add(city1); context.Cities.Add(city2); var student1 = new Student() {Name = "Shaahin",LastName = "Kiassat",Age=22,BornInCity = city1}; var student2 = new Student() { Name = "Mehdi", LastName = "Farzad", Age = 31, BornInCity = city1 }; var student3 = new Student() { Name = "James", LastName = "Hetfield", Age = 49, BornInCity = city2 }; context.Students.Add(student1); context.Students.Add(student2); context.Students.Add(student3); base.Seed(context); } }
var context = new MyContext(); var query= context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where( x => x.Age == age);
var query= context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where( x => x.Age == age).OrderBy(x=>x.LastName).Skip(skip).Take(take);
var query = context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where ( x => x.Age == age).Where(x => x.BornInCityId == 1).OrderBy(x => x.Age);
namespace ReUsableQueries.Quries { public static class StudentQueryExtension { public static IQueryable<Student> FindStudentsByName(this IQueryable<Student> students,string name) { return students.Where(x => x.Name.Contains(name)); } public static IQueryable<Student> FindStudentsByLastName(this IQueryable<Student> students, string lastName) { return students.Where(x => x.LastName.Contains(lastName)); } public static IQueryable<Student> SkipAndTake(this IQueryable<Student> students, int skip , int take) { return students.Skip(skip).Take(take); } public static IQueryable<Student> OrderByAge(this IQueryable<Student> students) { return students.OrderBy(x=>x.Age); } } }
var query = context.Students.FindStudentsByName(name).FindStudentsByLastName(lastName).SkipAndTake(skip,take);
var query = context.Students.AsQueryable(); if (searchByName) { query= query.FindStudentsByName(name); } if (orderByAge) { query = query.OrderByAge(); } if (paging) { query = query.SkipAndTake(skip, take); } return query.ToList();
در قسمت قبل به معرفی postgresql پرداختیم; در این قسمت قصد ایجاد و راه اندازی یک api با استفاده از دیتابیس postgresql و استفاده از تکنولوژیهای آن را با استفاده از docker داریم.
ابتدا با استفاده از دستور زیر یک پروژهی جدید asp.net core را ایجاد کنید:
dotnet new webapi --minimal -o YourDirectoryPath:\YourFolderName
سپس فایل docker-compose.yaml را به روت پروژه اضافه کنید که شامل کانفیگهای زیر میباشد:
version: '3.1' services: db: image: postgres container_name: db restart: always environment: POSTGRES_PASSWORD: postgres POSTGRES_USERNAME: postgres POSTGRES_DB: BloggingDb ports: - "5432:5432" volumes: - postgres_data:/data/db adminer: image: adminer restart: always ports: - 8080:8080 pgadmin4: image: dpage/pgadmin4 restart: always environment: PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org PGADMIN_DEFAULT_PASSWORD: admin PGADMIN_CONFIG_SERVER_MODE: 'False' ports: - 5050:80 volumes: - pgadmin:/var/lib/pgadmin depends_on: - db volumes: postgres_data: pgadmin:
سپس با اجرای دستور زیر در روت پروژه، سرویسها را راه اندازی کنید:
docker compose up -d
معرفی سرویسهای استفاده شده در تنظیمات فایل بالا:
سرویس db :
نمونه ایمیج اصلی، volume، تنظیمات connection string در آن استفاده شده است.
سرویس adminer :
https://hub.docker.com/_/adminer /
Adminer - Database management in a single PHP file
یک برنامه تحت وب مدیریت پایگاه داده ساده میباشد که ویژگیها MySql را در کنار سرعت و امنیت ارائه میدهد و در آدرس http://localhost:8080 / اجرا خواهد شد.
سرویس pgadmin4 :
dpage/pgadmin4 - Docker Image | Docker Hub
در حال حاضر این برنامه محبوبترین برنامه مدیریت پایگاه داده میباشد که ویژگیهای پیشرفتهای را نیز پوشش میدهد و در آدرس http://localhost:5050 / اجرا خواهد شد.
اکنون نوبت نوشتن کدها میباشد.
- تنظیم connection string در فایل appsettings.json:
"ConnectionStrings": { "BloggingContext": "Username=postgres;Password=postgres;Server=localhost;Database=BloggingDb” }
- و همینطور پکیجهای زیر را به برنامه خود رفرنس دهید:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.Design
- مدلهای برنامه را در مسیر /Models ایجاد کنید:
namespace NpgsqlAPI.Models; public class Post { public int PostId { get; set; } public string Title { get; set; } = null!; public string Content { get; set; } = null!; public int BlogId { get; set; } public Blog Blog { get; set; } = null!; } namespace NpgsqlAPI.Models; public class Blog { public int BlogId { get; set; } public string? Url { get; set; } public List<Post>? Posts { get; set; } }
- سپس BloggingContext را در مسیر /Data ایجاد کنید:
using Microsoft.EntityFrameworkCore; using NpgsqlAPI.Models; namespace NpgsqlAPI.Data; public class BloggingContext : DbContext { public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { } public DbSet<Blog> Blogs => Set<Blog>(); public DbSet<Post
- سپس اینترفیس IBlogServices را در مسیر /Servicec/Blogs ایجاد کنید:
using NpgsqlAPI.Models; namespace NpgsqlAPI.Services.Blogs; public interface IBlogServices { Task<IEnumerable<Blog>> GetList(); Task<Blog?> Get(uint id); Task<uint> Add(Blog obj); Task AddRange(Blog[] obj); Task Update(Blog obj); Task UpdateRange(Blog[] obj); Task Remove(uint id); }
- و سپس پیاده سازی آن را در فایل BlogEFServices و در کنار اینترفیس آن قرار دهید:
using Microsoft.EntityFrameworkCore; using NpgsqlAPI.Data; using NpgsqlAPI.Models; namespace NpgsqlAPI.Services.Blogs; public sealed class BlogEFServices : IBlogServices { private readonly BloggingContext _context; public BlogEFServices(BloggingContext context) { _context = context; } public async Task<uint> Add(Blog obj) { await _context.Blogs.AddAsync(obj); return (uint)await SaveChangesAsync(); } public async Task AddRange(Blog[] obj) { await _context.Blogs.AddRangeAsync(obj); await SaveChangesAsync(); } public async Task<Blog?> Get(uint id) { return await _context.Blogs.FirstOrDefaultAsync(x=>x.BlogId == id); } public async Task<IEnumerable<Blog>> GetList() { return await _context.Blogs.ToListAsync(); } public async Task Remove(uint id) { var entity = await Get(id); _context.Blogs.Remove(entity!); await SaveChangesAsync(); } public async Task Update(Blog obj) { _context.Blogs.Update(obj); await SaveChangesAsync(); } public async Task UpdateRange(Blog[] obj) { _context.Blogs.UpdateRange(obj); await SaveChangesAsync(); } private async Task<int> SaveChangesAsync() { return await _context.SaveChangesAsync(); } }
- اکنون endpointهای api را در فایل program.cs ایجاد کنید:
using System.Data; using Microsoft.EntityFrameworkCore; using Npgsql; using NpgsqlAPI.Services.Blogs; using NpgsqlAPI.Data; using NpgsqlAPI.Models; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); string connectionString = builder.Configuration.GetConnectionString("BloggingContext")!; builder.Services.AddDbContext<BloggingContext>(options => options.UseNpgsql(connectionString)); builder.Services.AddTransient<IDbConnection>(_ => new NpgsqlConnection(connectionString)); // builder.Services.AddScoped<IBlogServices, BlogDapperServices>(); // builder.Services.AddScoped<IBlogServices, BlogEFRawQueryServices>(); builder.Services.AddScoped<IBlogServices, BlogEFServices>(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.MapGet("/blogs", async (IBlogServices service) => await service.GetList()) .WithName("GetBlogs") .WithOpenApi(); app.MapGet("/blogs/{id}", async (IBlogServices service, uint id) => await service.Get(id)) .WithName("GetBlog") .WithOpenApi(); app.MapPost("/blogs", async (IBlogServices service, Blog blog) => await service.Add(blog)) .WithName("AddBlog") .WithOpenApi(); app.MapDelete("/blogs/{id}", async (IBlogServices service, uint id) => await service.Remove(id)) .WithName("RemoveBlog") .WithOpenApi(); app.MapPut("/blogs", async (IBlogServices service, Blog blog) => await service.Update(blog)) .WithName("UpdateBlog") .WithOpenApi(); app.MapPut("/blogs/Bulk", async (IBlogServices service, Blog[] blogs) => await service.UpdateRange(blogs)) .WithName("UpdateBulkBlog") .WithOpenApi(); app.MapPost("/blogs/Bulk", async (IBlogServices service, Blog[] blogs) => await service.AddRange(blogs)) .WithName("AddBulkBlog") .WithOpenApi(); app.Run();
تمامی کدهای برنامه تا به اینجا نوشته شدهاند. اکنون migration را پس از اطمینان از اجرا بودن داکر اجرا کنید
dotnet ef migrations add Init dotnet ef database update
و برنامه را اجرا و تست کنید.
با استفاده از پارامترهای آبشاری میتوان شیءای را در اختیار تمام کامپوننتهای قرار گرفته شدهی در سلسله مراتب آنها قرار داد. برای مثال اگر در فایل Client\Shared\MainLayout.razor، جائیکه سایر کامپوننتها قرار است رندر شوند را توسط یک کامپوننت سطح بالا محصور کنیم:
<Alert> @Body </Alert>
بنابراین طراحی سادهی کامپوننت Alert ای (Client\Shared\Alert.razor) که تامین کنندهی یک پارامتر آبشاری سراسری است، به صورت زیر میتواند باشد:
<CascadingValue Value=this> @if(IsVisible) { <div class="alert @Css" role="alert"> @Message <button type="button" class="close" data-dismiss="alert" aria-label="Close" @onclick="HideAlert"> <span aria-hidden="true">×</span> </button> </div> } @ChildContent </CascadingValue> @code { [Parameter] public RenderFragment ChildContent { get; set; } private bool IsVisible; private string Message; private string Css = "alert-primary"; public void ShowAlert(string message, AlertType alertType) { IsVisible = true; Message = message; Css = alertType switch { AlertType.Success => "alert-success", AlertType.Info => "alert-primary", AlertType.Danger => "alert-danger", AlertType.Warning => "alert-warning", _ => "alert-primary" }; StateHasChanged(); } public void HideAlert() { IsVisible = false; } }
namespace BlazorWasmAlert.Client.Shared { public enum AlertType { Success, Info, Danger, Warning } }
الف) وجود یک CascadingValue که اینبار Value آن به خود کامپوننت اشاره میکند (Value=this). یعنی پارامتر آبشاری که در اختیار سایر کامپوننتهای محصور شدهی توسط آن ارسال میشود، دقیقا وهلهای از کامپوننت Alert است که توسط آن میتوان برای مثال، متد عمومی ShowAlert آنرا فراخوانی کرد:
<CascadingValue Value=this>
پس از درج این کامپوننت در فایل layout، روش استفادهی از آن برای مثال در کامپوننت Index به صورت زیر است:
@page "/" <h1>Hello, world!</h1> <button class="btn btn-primary" @onclick="ShowAlert">Show Alert!</button> @code { [CascadingParameter] public Alert Alert { get; set; } private void ShowAlert() { Alert.ShowAlert("This is a test!", AlertType.Info); } }
پ.ن.
در طراحی Blazor، از طراحی React الهام گرفته شدهاست و CascadingValue آن دقیقا معادل Context API جدید React است.
برای تعریف المانهای فرمها نیاز است ویژگیهای قابل توجهی را مانند placeholder ،required ،maxlength و غیره، تعریف کرد که در صورت زیاد بودن تعداد المانهای یک فرم، مدیریت تعریف این ویژگیها مشکل میشود. به همین جهت قابلیت ویژهای مخصوص اینکار به نام Attribute Splatting در Blazor درنظر گرفته شدهاست. برای توضیح آن، ابتدا کامپوننت والد Pages\LearnBlazor\AttributeSplatting.razor و کامپوننت فرزند Pages\LearnBlazor\LearnBlazorComponents\AttributeSplattingChild.razor را ایجاد میکنیم.
در کامپوننت فرزند یا همان AttributeSplattingChild، یک المان را به همراه تعدادی ویژگی تعریف شده مشاهده میکنید:
<div> <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4> <input id="roomName" placeholder="@Placeholder" required="@Required" maxlength="@MaxLength" class="form-control" /> </div> @code { [Parameter] public string Placeholder { get; set; } = "Initial Text"; [Parameter] public string Required { get; set; } = "required"; [Parameter] public string MaxLength { get; set; } = "10"; }
@page "/AttributeSplatting" <h1>Attribute Splatting</h1> <AttributeSplattingChild Placeholder="Enter the Room Name From Parent" MaxLength="5"> </AttributeSplattingChild>
مشکل! کامپوننت AttributeSplattingChild که فقط به همراه یک المان است، تا این لحظه نیاز به تعریف سه پارامتر جدید را جهت تامین ویژگیهای آن المان داشتهاست. اگر تعداد این المانها افزایش پیدا کرد، آیا راه بهتری برای مدیریت تعداد بالای ویژگیهای مورد نیاز وجود دارد؟
پاسخ: در یک چنین حالتی میتوان ویژگیهای هر المان را توسط پارامتری از نوع Dictionary مدیریت کرد؛ بجای تعریف تک تک آنها به صورت خواصی مجزا. به این قابلیت، Attribute Splatting میگویند.
در این حالت تمام کدهای AttributeSplattingChild.razor به صورت زیر خلاصه میشوند:
<div> <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4> <input id="roomName" @attributes="InputAttributes" class="form-control" /> </div> @code { [Parameter] public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object> { { "required" , "required"}, { "placeholder", "Initial Text"}, { "maxlength", 10} }; }
و همچنین در ادامه کامپوننت والد یا AttributeSplatting.razor نیز به صورت زیر تغییر میکند:
@page "/AttributeSplatting" <h1>Attribute Splatting</h1> <AttributeSplattingChild InputAttributes="InputAttributesFromParent"></AttributeSplattingChild> @code{ Dictionary<string, object> InputAttributesFromParent = new Dictionary<string, object> { { "required" , "required"}, { "placeholder", "Enter the Room Name From Parent"}, { "maxlength", 5} }; }
ساده سازی روش تعریف key/valueهای شیء دیکشنری Attribute Splatting
تا اینجا موفق شدیم تعداد قابل ملاحظهای از پارامترهای عمومی یک کامپوننت را تنها توسط یک شیء Dictionary مدیریت کنیم. همچنین همانطور که ملاحظه میکنید، هم Dictionary سمت کامپوننت فرزند و هم سمت کامپوننت والد، نیاز به مقدار دهی اولیهای را دارند. این مقدار دهی اولیه را میتوان به نحو دیگری نیز در حین استفادهی از قابلیت Attribute Splatting، انجام داد:
<div> <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4> <input id="roomName" @attributes="InputAttributes" placeholder="Initial Text" class="form-control" /> </div> @code { [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object>(); }
پس از این تغییر، کامپوننت والد هم به صورت زیر خلاصه میشود و دیگر نیازی به تعریف و مقدار دهی InputAttributes و یا تعریف مجزای یک دیکشنری را ندارد. در اینجا هر ویژگی که به المان نسبت داده شود، به عنوان Unmatched Values تفسیر شده و مورد استفاده قرار میگیرد.
@page "/AttributeSplatting" <h1>Attribute Splatting</h1> <AttributeSplattingChild placeholder="Placeholder default"></AttributeSplattingChild>
اگر به تصویر فوق دقت کنید، هرچند در کامپوننت والد مقدار placeholder، به متن دیگری تنظیم شده، اما متن تنظیم شدهی در کامپوننت فرزند، تقدم بیشتری پیدا کرده و نمایش داده شدهاست. علت اینجا است که محل قرارگیری آن در مثال فوق، در سمت راست دایرکتیو attributes@ است. اگر آنرا در سمت چپ attributes@ قرار دهیم، حق تقدم attributes@ بیشتر شده و مقدار تنظیم شدهی در سمت کامپوننت والد، بجای placeholder اولیهی تعریف شدهی در اینجا مورد استفاده قرار میگیرد:
<input id="roomName" placeholder="Initial Text" @attributes="InputAttributes" class="form-control" />
روش انتقال پارامترها به چندین زیر سطح
در قسمت قبل، ParentComponent.razor و ChildComponent.razor را تعریف و تکمیل کردیم. هدف از آنها، بررسی ویژگی Render Fragmentها بود. در ادامهی آن، یک زیر کامپوننت دیگر را نیز به نام Pages\LearnBlazor\LearnBlazorComponents\GrandChildComponent.razor اضافه میکنیم. هدف این است که کامپوننت Parent، کامپوننت Child را فراخوانی کند و کامپوننت Child، کامپوننت GrandChild را تا یک سلسله مراتب از کامپوننتها را تشکیل دهیم.
محتوای GrandChildComponent را هم بسیار ساده نگه میداریم، تا پارامتری رشتهای را دریافت کرده و نمایش دهد:
<div class="row"> <h4 class="text-primary pl-4 pt-2 col-12">Grand Child Component</h4> <br /> <p> There is a message - @MessageForGrandChild </p> </div> @code { [Parameter] public string MessageForGrandChild { get; set; } }
<div class="mt-2"> <GrandChildComponent MessageForGrandChild="@MessageForGrandChild"></GrandChildComponent> </div> @code { [Parameter] public string MessageForGrandChild { get; set; } // ... }
<ChildComponent MessageForGrandChild="This is a message from Grand Parent" Title="This is the second child component"> <p><b>@MessageText</b></p> </ChildComponent>
بنابراین اکنون این سؤال مطرح میشود که آیا میتوان پارامتری را در همان کامپوننت Parent تعریف کرد که توسط کامپوننت GrandChild قابل شناسایی و استفاده باشد، بدون اینکه کامپوننت Child را در این بین تغییر دهیم؟
پاسخ: بله. برای اینکار ویژگیهای CascadingValue و CascadingParameter در Blazor پیش بینی شدهاند.
در ابتدا، پارامتر MessageForGrandChild کامپوننت Child حذف کرده و سپس آنرا توسط کامپوننت توکار CascadingValue محصور میکنیم. در اینجا نیاز است مقدار انتقالی را نیز مشخص کنیم:
<CascadingValue Value="@MessageForGrandChild"> <ChildComponent Title="This is the second child component"> <p><b>@MessageText</b></p> </ChildComponent> </CascadingValue> @code { string MessageForGrandChild = "This is a message from Grand Parent";
<GrandChildComponent></GrandChildComponent>
[CascadingParameter] public string MessageForGrandChild { get; set; }
چند نکته:
- در اینجا نوع CascadingParameter تعریف شده، باید با نوع Value کامپوننت CascadingValue، در بالاترین سطح سلسله مراتب کامپوننتها، یکی باشد.
- نام CascadingParameter تعریف شده مهم نیست. فقط نوع آن مهم است.
- تمام کامپوننتهای موجود و پوشش داده شدهی در سلسله مراتب جاری، قابلیت تعریف CascadingParameter ای مانند مثال فوق را دارند و این تعریف، محدود به پایینترین سطح موجود نیست. برای مثال در اینجا در کامپوننت Child هم در صورت نیاز میتوان همین CascadingParameter را تعریف و استفاده کرد.
روش تعریف پارامترهای آبشاری نامدار
تا اینجا روش انتقال یک پارامتر را از بالاترین سطح، به پایینترین سطح سلسله مراتب کامپوننتهای تعریف شده، بررسی کردیم. اکنون شاید این سؤال مطرح شود که اگر خواستیم بیش از یک پارامتر را بین اجزای این سلسله، به اشتراک بگذاریم چه باید کرد؟
در این حالت میتوان پارامتر جدید را توسط یک کامپوننت CascadingValue تو در تو، به صورت زیر معرفی کرد؛ که اینبار نامدار نیز هست:
<CascadingValue Value="@MessageForGrandChild" Name="MessageFromGrandParent"> <CascadingValue Value="@Number" Name="GrandParentsNumber"> <ChildComponent Title="This is the second child component"> <p><b>@MessageText</b></p> </ChildComponent> </CascadingValue> </CascadingValue> @code { string MessageForGrandChild = "This is a message from Grand Parent"; int Number = 7;
پس از این تغییر، GrandChildComponent، این پارامترهای نامدار را از طریق ذکر صریح خاصیت Name ویژگی CascadingParameter، دریافت میکند:
<div class="row"> <h4 class="text-primary pl-4 pt-2 col-12">Grand Child Component</h4> <br /> There is a message: @Message <br /> GrandParentsNumber: @Number </div> @code { [CascadingParameter(Name = "MessageFromGrandParent")] public string Message { get; set; } [CascadingParameter(Name = "GrandParentsNumber")] public int Number { get; set; } }
یک نکته: چون نوع پارامترهای ارسالی یکی نیست، الزامی به ذکر نام آنها نبود. در این حالت بر اساس نوع پارامترهای آبشاری، عملیات اتصال مقادیر صورت میگیرد. اما اگر نوع هر دو را برای مثال رشتهای تعریف میکردیم، مقدار Number، بر روی مقدار MessageForGrandChild بازنویسی میشد. یعنی در UI، هر دو پارامتر هم نوع، یک مقدار را نمایش میدادند که در حقیقت مقدار پایینترین CascadingValue تعریف شدهاست. بنابراین ذکر نام پارامترهای آبشاری، روشیاست جهت تمایز قائل شدن بین پارامترهای هم نوع.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-09.zip
ابتدا بسته زیر را از طریق nuget نصب نمایید:
dotnet add package MongoDB.Driver
سپس مدلهای زیر را ایجاد نمایید:
public class BaseModel { public BaseModel() { CreationDate=DateTime.Now; } public string Id { get; set; } public DateTime CreationDate { get; set; } public bool IsRemoved { get; set; } public DateTime? ModificationDate { get; set; } }
این مدل شامل یک کلاس پایه برای id,CreationDate,ModificationDate,IsRemoved میباشد که بسیار شبیه مدلهایی است که عموما در EntityFramework تعریف میکنیم.
برای اینکه فیلد Id به صورت objectId ایجاد شود ولی به صورت رشتهای استفاده شود ابتدا ویژگی BsonId را در بالای آن تعریف کرده تا به عنوان شناسه یکتا سند شناخته شود و سپس با استفاده از ویژگی BsonRepresentation اعلام میکنیم که کار تبدیل به رشته و بلعکس آن به صورت خودکار در پشت صحنه صورت بگیرد:
public class BaseModel { [BsonId] [BsonRepresentation((BsonType.ObjectId))] public string Id { get; set; } }
البته این حالت برای زمانی مناسب است که ما
در استفاده از ویژگیها محدودیتی نداشته باشیم؛ ولی در بسیاری از نرم افزارها که از
معماریهای چند لایه مانند لایه پیازی استفاده میشود استفاده از این خصوصیتها یعنی اعمال کارکرد کتابخانه بالاتر بر روی لایههای زیرین که هسته نرم افزار
شناخته میشوند که صحیح نبوده و باید توسط لایههای بالاتر این تغییرات اعمال شوند که
میتواند از طریق کلاس این کار را انجام دهید. به ازای هر مدل که نیاز به تغییرات
دارد، یک حالت جدید تعریف شده و در ابتدای برنامه در فایل Program.cs یا قبل از دات نت 6 در Startup.cs صدا زده میشوند.
BsonClassMap.RegisterClassMap<BaseModel>(map => { map.SetIdMember(map.GetMemberMap(x=>x.Id)); map.GetMemberMap(x => x.Id) .SetSerializer(new StringSerializer(BsonType.ObjectId)); });
یک نکته بسیار مهم: کلاس و متد BsonClassMap . RegisterClassMap قادر به اعمال تغییرات بر روی خصوصیتهای کلاس والد نیستند و آن خصوصیات حتما باید در آن کلاسی که آن را کانفیگ میکنید، تعریف شده باشند؛ یعنی چنین چیزی که در کد زیر میبینید در زمان اجرا با یک خطا مواجه خواهد شد:
public class Employee : BaseModel { public string FirstName { get; set; } public string LastName { get; set; } } //================= BsonClassMap.RegisterClassMap<Employee >(map => { map.SetIdMember(map.GetMemberMap(x=>x.Id)); map.GetMemberMap(x => x.Id) .SetSerializer(new StringSerializer(BsonType.ObjectId)); });
روش استفاده از مونگو در asp.net core به صورت زیر بسیار متداول میباشد که در قسمتهای پیشین هم در این مورد نوشته بودیم:
MongoDbContext
public interface IMongoDbContext { IMongoCollection<TEntity> GetCollection<TEntity>(); } public class MongoDbContext : IMongoDbContext { private readonly IMongoClient _client; private readonly IMongoDatabase _database; public MongoDbContext(string databaseName,string connectionString) { var settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString)); _client = new MongoClient(settings); _database = _client.GetDatabase(databaseName); } public IMongoCollection<TEntity> GetCollection<TEntity>() { return _database.GetCollection<TEntity>(typeof(TEntity).Name.ToLower() + "s"); } }
سپس از طریق کد زیر IMongoDbContext را به سیستم تزریق وابستگیها معرفی میکنیم. الگوی استفاده شدهی در اینجا بر خلاف نسخههای sql که عموما به صورت AddScoped تعریف میشدند، در اینجا به صورت AddSingleton تعریف کردیم و نحوه پیاده سازی آن را نیز در طرف سمت راست به صورت صریح اعلام کردیم:
public static class MongoDbContextService { public static void AddMongoDbContext(this IServiceCollection services,string databaseName,string connectionString) { services.AddSingleton<IMongoDbContext>(serviceProvider => new MongoDbContext(databaseName, connectionString)); } } //=============== Program.cs builder.Services.AddMongoDbContext("bookstore", "mongodb://localhost:27017");
پیاده سازی SoftDelete در مونگو
در مونگو چیزی تحت عنوان Global Query Filter نداریم که تمام کوئری هایی که به سمت دیتابیس ارسال میشوند، توسط کانتکس اطلاح شوند؛ بدین جهت برای پیاده سازی این خصوصیت میتوان اینترفیسی با نام <IRepository<T را به شکل زیر طراحی نماییم:
public interface IRepository<T> where T : BaseModel { IMongoCollection<T> GetCollection(); IMongoQueryable<T> GetFilteredCollection(); } public class Repository<T> : IRepository<T> where T:BaseModel { private IMongoDbContext _mongoDbContext; public Repository(IMongoDbContext mongoDbContext) { _mongoDbContext = mongoDbContext; } public IMongoCollection<T> GetCollection() { return _mongoDbContext.GetCollection<T>(); } public IMongoQueryable<T> GetFilteredCollection() { var query= _mongoDbContext.GetCollection<T>().AsQueryable(); //================= Global Query Filters ==================== //Filter 1 query=query.Where(x => x.RemovedAt.HasValue == false); //============================================================== return query; } }
این کلاس یا اینترفیس شامل دو متد هستند که کلاس جنریک آنها باید از BaseModel ارث بری کرده باشد و اولین متد، تنها یک کالکشن بدون هیچگونه فیلتری است که میتواند نقش متد IgnoreQueryFilters را بازی کند و دیگری GetFilteredCollection است که در این متد ابتدا کالکشنی دریافت شده و سپس آن را به حالت کوئری تغییر داده و فیلترهای مورد نظر، مانند حذف منطقی را پیاده سازی میکنیم:
public interface IRepository<T> where T : BaseModel { IMongoCollection<T> GetCollection(); IMongoQueryable<T> GetFilteredCollection(); } public class Repository<T> : IRepository<T> where T:BaseModel { private IMongoDbContext _mongoDbContext; public Repository(IMongoDbContext mongoDbContext) { _mongoDbContext = mongoDbContext; } public IMongoCollection<T> GetCollection() { return _mongoDbContext.GetCollection<T>(); } public IMongoQueryable<T> GetFilteredCollection() { var query= _mongoDbContext.GetCollection<T>().AsQueryable(); //================= Global Query Filters ==================== //Filter 1 query=query.Where(x => x.RemovedAt.HasValue == false); //============================================================== return query; } }
اصلاح تاریخ ویرایش در مدل
در EF به لطف dbset و همچنین ChangeTracking امکان شناسایی حالتها وجود دارد و میتوانید در متدی مانند saveChanges مقدار تاریخ ویرایش را تنظیم نمود. برای مدلهای منگو چنین چیزی وجود ندارد و به همین دلیل چند روش زیر پیشنهاد میگردد:
یک. استفاده از اینترفیس INotifyPropertyChanged یا جهت حذف کدهای تکراری نیز از الگوی AOP بهره بگیرید.
دو. استفاده از یک <Repository<T همانند بالا که شامل متدهای داخلی Update و Delete هستند که در آنجا میتوانید این مقادیر را به صورت مستقیم تغییر دهید.
چند نکته کاربردی درباره Entity Framework
در متد Updateایی که نوشتید، قسمت Find حتما اتفاق میافته. چون Tracking خاموش هست (مطابق تنظیماتی که عنوان کردید)، بنابراین Find چیزی رو از کشی که وجود نداره نمیتونه دریافت کنه و میره سراغ دیتابیس. ماخذ :
The Find method on DbSet uses the primary key value to attempt to find an entity tracked by the context. If the entity is not found in the context then a query will be sent to the database to find the entity there. Null is returned if the entity is not found in the context or in the database.
قسمت Find متد Update شما در حالت detached اضافی است. یعنی اگر میدونید که این Id در دیتابیس وجود داره نیازی به Findاش نیست. فقط State اون رو تغییر بدید کار میکنه.
در حالت نه آنچنان Detached ! (دریافت یک لیست از Context ایی که ردیابی نداره)
با خاموش کردن Tracking حتما نیاز خواهید داشت تا متد context.ChangeTracker.DetectChanges رو هم پیش از ذخیره سازی یک لیست دریافت شده از بانک اطلاعاتی فراخوانی کنید. وگرنه چون این اطلاعات ردیابی نمیشوند، هر تغییری در آنها، وضعیت Unchanged رو خواهد داشت و نه Detached. بنابراین SaveChanges عمل نمیکنه؛ مگر اینکه DetectChanges فراخوانی بشه.
سؤال: این سربار که میگن چقدر هست؟ ارزشش رو داره که راسا خاموشش کنیم؟ یا بهتره فقط برای گزارشگیری این کار رو انجام بدیم؟
یک آزمایش:
using System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Diagnostics; using System.Linq; namespace EF_General.Models.Ex21 { public abstract class BaseEntity { public int Id { set; get; } } public class Factor : BaseEntity { public int TotalPrice { set; get; } } public class MyContext : DbContext { public DbSet<Factor> Factors { get; set; } public MyContext() { } public MyContext(bool withTracking) { if (withTracking) return; this.Configuration.ProxyCreationEnabled = false; this.Configuration.LazyLoadingEnabled = false; this.Configuration.AutoDetectChangesEnabled = false; } public void CustomUpdate<T>(T entity) where T : BaseEntity { if (entity == null) throw new ArgumentException("Cannot add a null entity."); var entry = this.Entry<T>(entity); if (entry.State != EntityState.Detached) return; /*var set = this.Set<T>(); // اینها اضافی است //متد فایند اگر اینجا باشه حتما به بانک اطلاعاتی رجوع میکنه در حالت منقطع از زمینه و در یک حلقه به روز رسانی کارآیی مطلوبی نخواهد داشت T attachedEntity = set.Find(entity.Id); if (attachedEntity != null) { var attachedEntry = this.Entry(attachedEntity); attachedEntry.CurrentValues.SetValues(entity); } else {*/ entry.State = EntityState.Modified; //} } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { if (!context.Factors.Any()) { for (int i = 0; i < 20; i++) { context.Factors.Add(new Factor { TotalPrice = i }); } } base.Seed(context); } } public class Performance { public TimeSpan ListDisabledTracking { set; get; } public TimeSpan ListNormal { set; get; } public TimeSpan DetachedEntityDisabledTracking { set; get; } public TimeSpan DetachedEntityNormal { set; get; } } public static class Test { public static void RunTests() { startDb(); var results = new List<Performance>(); var runs = 20; for (int i = 0; i < runs; i++) { Console.WriteLine("\nRun {0}", i + 1); var tsListDisabledTracking = PerformanceHelper.RunActionMeasurePerformance(() => updateListTotalPriceDisabledTracking()); var tsListNormal = PerformanceHelper.RunActionMeasurePerformance(() => updateListTotalPriceNormal()); var tsDetachedEntityDisabledTracking = PerformanceHelper.RunActionMeasurePerformance(() => updateDetachedEntityTotalPriceDisabledTracking()); var tsDetachedEntityNormal = PerformanceHelper.RunActionMeasurePerformance(() => updateDetachedEntityTotalPriceNormal()); results.Add(new Performance { ListDisabledTracking = tsListDisabledTracking, ListNormal = tsListNormal, DetachedEntityDisabledTracking = tsDetachedEntityDisabledTracking, DetachedEntityNormal = tsDetachedEntityNormal }); } var detachedEntityDisabledTrackingAvg = results.Average(x => x.DetachedEntityDisabledTracking.TotalMilliseconds); Console.WriteLine("detachedEntityDisabledTrackingAvg: {0} ms.", detachedEntityDisabledTrackingAvg); var detachedEntityNormalAvg = results.Average(x => x.DetachedEntityNormal.TotalMilliseconds); Console.WriteLine("detachedEntityNormalAvg: {0} ms.", detachedEntityNormalAvg); var listDisabledTrackingAvg = results.Average(x => x.ListDisabledTracking.TotalMilliseconds); Console.WriteLine("listDisabledTrackingAvg: {0} ms.", listDisabledTrackingAvg); var listNormalAvg = results.Average(x => x.ListNormal.TotalMilliseconds); Console.WriteLine("listNormalAvg: {0} ms.", listNormalAvg); } private static void updateDetachedEntityTotalPriceNormal() { using (var context = new MyContext(withTracking: true)) { var detachedEntity = new Factor { Id = 1, TotalPrice = 10 }; var attachedEntity = context.Factors.Find(detachedEntity.Id); if (attachedEntity != null) { attachedEntity.TotalPrice = 100; context.SaveChanges(); } } } private static void updateDetachedEntityTotalPriceDisabledTracking() { using (var context = new MyContext(withTracking: false)) { var detachedEntity = new Factor { Id = 2, TotalPrice = 10 }; detachedEntity.TotalPrice = 200; context.CustomUpdate(detachedEntity); // custom update with change tracking disabled. context.SaveChanges(); } } private static void updateListTotalPriceNormal() { using (var context = new MyContext(withTracking: true)) { foreach (var item in context.Factors) { item.TotalPrice += 10; // normal update with change tracking enabled. } context.SaveChanges(); } } private static void updateListTotalPriceDisabledTracking() { using (var context = new MyContext(withTracking: false)) { foreach (var item in context.Factors) { item.TotalPrice += 10; //نیازی به این دو سطر نیست //context.ChangeTracker.DetectChanges(); // هربار باید محاسبه صورت گیرد در غیراینصورت وضعیت تغییر نیافته گزارش میشود //context.CustomUpdate(item); // custom update with change tracking disabled. } context.ChangeTracker.DetectChanges(); // در غیراینصورت وضعیت تغییر نیافته گزارش میشود context.SaveChanges(); } } private static void startDb() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); // Forces initialization of database on model changes. using (var context = new MyContext()) { context.Database.Initialize(force: true); } } } public class PerformanceHelper { public static TimeSpan RunActionMeasurePerformance(Action action) { var stopwatch = new Stopwatch(); stopwatch.Start(); action(); stopwatch.Stop(); return stopwatch.Elapsed; } } }
detachedEntityDisabledTrackingAvg: 22.32089 ms. detachedEntityNormalAvg: 54.546815 ms. listDisabledTrackingAvg: 413.615445 ms. listNormalAvg: 393.194625 ms.
در حالت کار با لیستی از اشیاء دریافت شده از بانک اطلاعاتی، به روز رسانی حالت متصل به Context سریعتر است.