ابزارهای زیادی جهت تست کردن API های برنامههای وب موجود است. یکی از معروفترین آنها Fiddler است که ابزاری مستقل جهت دیباگ تحت پروتکل HTTP میباشد. یکی دیگر از این نرم افزارها Swashbuckle است که از Nuget قابل دریافت است و صفحهای را به برنامه اضافه میکند که به اختصار API های برنامه را نمایش داده و امکان اجرای آنها را فراهم میکند.
اما سادهترین راه جهت کردن تست کردن API های یک برنامه، استفاده از PowerShell است که عمل ایجاد درخواستهای HTTP را به راحتی از طریق Command line فراهم میکند و به شما اجازه میدهد بدون وارد شدن به جزئیات، بر روی نتیجه API مورد نظر تمرکز کنید. PowerShell ابتدا فقط برای محیط ویندوز طراحی شده بود ولی در حال حاضر قابلیت اجرای تحت Linux و Mac را نیز دارد.
در این بخش به شما نشان خواهم داد که چگونه متدهای یک Controller را با استفاده از PowerShell تست نمایید.
بدین منظور ابتدا کلاسی را به نام Reservation با مشخصات زیر، در یک پروژه ASP.NET Core Web Application ایجاد نمایید.public class Reservation { public int ReservationId { get; set; } public string ClientName { get; set; } public string Location { get; set; } }
public interface IRepository { IEnumerable<Reservation> Reservations { get; } Reservation this[int id] { get; } Reservation AddReservation(Reservation reservation); Reservation UpdateReservation(Reservation reservation); void DeleteReservation(int id); }
public class MemoryRepository : IRepository { private Dictionary<int, Reservation> items; public MemoryRepository() { items = new Dictionary<int, Reservation>(); new List<Reservation> { new Reservation { ClientName = "Alice", Location = "Board Room" }, new Reservation { ClientName = "Bob", Location = "Lecture Hall" }, new Reservation { ClientName = "Joe", Location = "Meeting Room 1" } }.ForEach(r => AddReservation(r)); } public Reservation this[int id] => items.ContainsKey(id) ? items[id] : null; public IEnumerable<Reservation> Reservations => items.Values; public Reservation AddReservation(Reservation reservation) { if (reservation.ReservationId == 0) { int key = items.Count; while (items.ContainsKey(key)) { key++; }; reservation.ReservationId = key; } items[reservation.ReservationId] = reservation; return reservation; } public void DeleteReservation(int id) => items.Remove(id); public Reservation UpdateReservation(Reservation reservation) => AddReservation(reservation); }
سپس کنترلری را به نام ReservationController به پروژه اضافه کنید که شامل متدهای زیر باشد:
[Route("api/[controller]")] public class ReservationController : Controller { private IRepository repository; public ReservationController(IRepository repo) => repository = repo; [HttpGet] public IEnumerable<Reservation> Get() => repository.Reservations; [HttpGet("{id}")] public Reservation Get(int id) => repository[id]; [HttpPost] public Reservation Post([FromBody] Reservation res) => repository.AddReservation(new Reservation { ClientName = res.ClientName, Location = res.Location }); [HttpPut] public Reservation Put([FromBody] Reservation res) => repository.UpdateReservation(res); [HttpPatch("{id}")] public StatusCodeResult Patch(int id, [FromBody] JsonPatchDocument<Reservation> patch) { Reservation res = Get(id); if (res != null) { patch.ApplyTo(res); return Ok(); } return NotFound(); } [HttpDelete("{id}")] public void Delete(int id) => repository.DeleteReservation(id); }
تست کردن درخواست از نوع GET
حالا دستور زیر را در پنجره PowerShell وارد کنید:Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
location | clientName | reservationId |
Board Room | Alice | 0 |
Lecture Hall | Bob | 1 |
Meeting Room 1 | Joe | 2 |
حال جهت نمایش فقط یک رکورد از اطلاعات فوق، دستور زیر را وارد نمایید:
Invoke-RestMethod http://localhost:7000/api/reservation/1 -Method GET
location | clientName | reservationId |
Lecture Hall | Bob | 1 |
تست درخواست از نوع Post
تمامی متدهای ارائه شده توسط یک API را میتوان به کمک PowerShell تست کرد. اگرچه در این حالت فرمت بعضی از درخواستهای ارسالی ممکن است کمی به هم ریخته به نظر برسد. در اینجا درخواستی جهت اضافه کردن یک رکورد را به لیست Reservation ارسال میکنیم و نتیجه را در پاسخ ارسال شده از سمت سرور خواهیم دید:
Invoke-RestMethod http://localhost:7000/api/reservation -Method POST -Body (@{clientName="Anne"; location="Meeting Room 4"} | ConvertTo-Json) -ContentType "application/json"
location | clientName | reservationId |
Meeting Room 4 | Anne | 1 |
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
location | clientName | reservationId |
Board Room | Alice | 0 |
Lecture Hall | Bob | 1 |
Meeting Room 1 | Joe | 2 |
Meeting Room 4 | Anne | 3 |
تست درخواست از نوع PUT
درخواست از نوع PUT جهت جایگزینی دادهای موجود در لیست، با دادهی جدید است. بدین منظور کد داخلی شیء مورد نظر (در اینجا ReservationId) باید در URL گنجانده شود و مقادیر فیلدهای کلاس، در متن درخواست. مثالی جهت درخواست اصلاح داده موجود از طریق فرمان PowerShell :
Invoke-RestMethod http://localhost:7000/api/reservation -Method PUT -Body (@{reservationId="1"; clientName="Bob"; location="Media Room"} | ConvertTo-Json) -ContentType "application/json"
location | clientName | reservationId |
Media Room | Bob | 1 |
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
location | clientName | reservationId |
Board Room | Alice | 0 |
Media Room | Bob | 1 |
Meeting Room 1 | Joe | 2 |
Meeting Room 4 | Anne | 3 |
استفاده از دستور PATCH
این دستور جهت تغییر اطلاعات یک رکورد به کار میرود، ولی استفاده از آن در غالب برنامههای پیاده سازی شده نادیده گرفته میشود که البته چنانچه کاربران برنامههای مذکور به تمامی فیلدهای یک رکورد دسترسی داشته باشند، معقولانه به نظر میرسد. اما در یک برنامه بزرگ و پیچیده ممکن است به دلایلی از جمله مسایل امنیتی، کاربران اجازه دسترسی به تمامی فیلدهای یک رکورد را نداشته باشند و در این حالت نمیتوان از دستور PUT جهت ارسال درخواست استفاده کرد. دستورهای مبتنی بر PATCH گزینشی بوده و میتوان فقط فیلدهای خاصی را که مورد نظر میباشند، با آن تغییر داد.
ASP.NET Core MVC از استاندارد Json PATCH پشتیبانی میکند و این کار اجازه میدهد تا بتوان تغییرات را بطور یکنواختی انجام داد. جهت مشاهده جزئیات این دستور میتوانید به این لینک مراجعه کنید. اما برای مثال فرض کنید میخواهید چنین درخواستی را که به فرمت Json است از طریق HTTP PATCH به سمت سرور ارسال کنید:
[ { "op": "replace", "path": "clientName", "value": "Bob"}, { "op": "replace", "path": "location", "value": "Lecture Hall"} ]
دربسیاری از مواقع فقط از دستور replace درخواست PATCH استفاده میشود و کد داخلی رکورد مورد نظر، جهت تغییر در Url درخواست ارسالی، فرستاده خواهد شد. ASP.NET Core MVC به طور اتوماتیک این دستور را پردازش کرده و آنرا به صورت آبجکتی از نوع <JsonPatchDocument<T به اکشن کنترلر قید شده، پاس خواهد داد که در اینجا نوع T، از نوع کلاس مورد نظر ما میباشد. فرمت دستور ارسالی از طریق Powershell به شکل زیر خواهد بود:
Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method PATCH -Body (@ { op="replace"; path="clientName"; value="Bob"},@{ op="replace"; path="location"; value="Lecture Hall"} | ConvertTo-Json) -ContentType "application/json"
جهت مشاهده تغییر ایجاد شده، دستور زیر را مجددا اجرا نمایید:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
location | clientName | reservationId |
Board Room | Alice | 0 |
Media Room | Bob | 1 |
Lecture Hall | Bob | 2 |
Meeting Room 4 | Anne | 3 |
استفاده از دستور Delete
استفاده از دستور Delete هم به شکل زیر خواهد بود:
Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method DELETE
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
هرچند ارتقاء به HttpClient الزامی نیست و کدهای پیشین، هنوز هم به خوبی کار میکنند؛ اما طراحی جدید آن شامل ویژگیهای توکاری است که به سختی توسط HTTP Module پیشین قابل پیاده سازی هستند.
به روز رسانی وابستگیهای پروژه
پیش از هرکاری نیاز است وابستگیهای پروژه را به روز رسانی کرد و یکی از روشهای سادهی یافتن شماره نگارشهای جدید بستههای تعریف شدهی در فایل package.json برنامه، استفاده از بستهی npm-check-updates است:
npm install npm-check-updates -g ncu
در اینجا شماره نگارشهای جدید مشخص شدهاند و همچنین روش سریع ارتقاء به آنها نیز ذکر شدهاست. فقط کافی است دستورات ذیل را صادر کنیم تا این به روز رسانیها توسط ncu انجام شوند:
ncu -a npm update
تغییرات مورد نیاز جهت معرفی ماژول HttpClient
این ماژول جدید از طریق اینترفیس HttpClientModule ارائه میشود. بنابراین اولین تغییر در جهت ارتقاء به نگارش 4.3، اصلاح importهای لازم است:
از:
import { HttpModule } from '@angular/http';
import { HttpClientModule } from '@angular/common/http';
پس از آن، این HttpClientModule را به لیست imports ماژول اصلی برنامه اضافه میکنیم؛ تا در کل برنامه قابل دسترسی شود:
@NgModule({ imports: [ // ... HttpClientModule, // ... ], declarations: [ ... ], providers: [ ... ], exports: [ ... ] }) export class AppModule { }
تغییرات مورد نیاز در سازندهها و تزریق وابستگیها
پس از تغییرات فوق، دیگر دسترسی به HttpModule پیشین را نداریم. بنابراین نیاز است هر جائی را که سرویس Http به سازندهی کلاسی تزریق شدهاست، یافته و به صورت ذیل تغییر دهیم:
از:
constructor(private http: Http) { }
import { HttpClient } from '@angular/common/http'; // ... constructor(private http: HttpClient) { }
تغییرات مورد نیاز در کدهای سرویسها جهت کار با HTTP Verbs
یکی از اهداف HTTP Client جدید، سادگی کار با اطلاعات دریافتی از سرور است. برای مثال در HTTP Module پیشین، روش دریافت اطلاعات از سرور به صورت ذیل است:
public get(): Observable<MyType> => { return this.http.get(url) .map((response: Response) => <MyType>response.json()); }
در HTTP Client جدید دیگر نیازی نیست تا متد ()json. فراخوانی شود. در اینجا به صورت پیشفرض نوع بازگشتی از سرور JSON فرض میشود. همچنین اکنون متدهای get/put/post و امثال آن برخلاف HTTP Client قبلی، جنریک هستند. یعنی در همینجا میتوان نوع بازگشتی را هم مشخص کرد. به این ترتیب، قطعه کد قدیمی فوق، به کد سادهی ذیل تبدیل میشود که در آن خبری از map و همچنین یک cast اضافی نیست:
get<T>(url: string): Observable<T> { return this.http.get<T>(url); }
post<T>(url: string, body: string): Observable<T> { return this.http.post<T>(url, body); }
نکته 1: در اینجا اگر خروجی از سرور، نوع دیگری را داشت، نیاز است responseType را به صورت صریحی به شکل ذیل مشخص کرد:
getData() { this.http.get(this.url, { responseType: 'text' }).subscribe(res => { this.data = res; }); }
نکته 2: ممکن است اطلاعات بازگشتی از سمت سرور، داخل یک فیلد محصور شده باشند:
{ "results": [ "Item 1", "Item 2", ] }
this.http.get('/api/items').subscribe(data => { this.results = data['results']; });
نکاتی را که باید حین کار با یک RxJS Observable-based API در نظر داشت
این API جدید نیز همانند قبل مبتنی بر RxJS Observables است. بنابراین نکات ذیل در مورد آن نیز صادق است:
- اگر متد subscribe بر روی این observables فراخوانی نشود، اتفاقی رخ نخواهد داد.
- اگر چندین بار مشترک این observables شویم، چندین درخواست HTTP صادر میشوند.
- این نوع خاص از observables، تنها یک مقدار را بازگشت میدهند. اگر درخواست HTTP موفقیت آمیز باشد، این observables یک نتیجه را بازگشت داده و سپس خاتمه پیدا میکنند.
- این observables اگر در حین درخواست HTTP با خطایی مواجه شوند، سبب صدور استثنایی میشوند.
تغییرات مورد نیاز در کدهای سرویسها جهت کار با HTTP Headers
در اینجا برای تعریف headers میتوان به صورت ذیل عمل کرد:
import { HttpHeaders } from "@angular/common/http"; const headers = new HttpHeaders({ "Content-Type": "application/json" });
const headers = new HttpHeaders().set("Accept", "application/json").set('Content-Type', 'application/json');
سپس آنرا به عنوان پارامتر سوم، به متدهای http ارسال میکنیم. یک مثال:
updateAppProduct(id: number, item: AppProduct): Observable<AppProduct> { const header = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .put<AppProduct>( `${this.baseUrl}/UpdateProduct/${id}`, JSON.stringify(item), { headers: header } ) .map(response => response || {}); }
تعریف پارامتر options اینبار به صورت یک شیء دارای چندین خاصیت درآمدهاست. به همین جهت است که در اینجا یک {} را نیز مشاهده میکنید:
(method) HttpClient.post(url: string, body: any, options?: { headers?: HttpHeaders; observe?: "body"; params?: HttpParams; reportProgress?: boolean; responseType?: "json"; withCredentials?: boolean; }): Observable<Object>
یک نکته: شیء HttpHeaders به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
const headers = new HttpHeaders(); headers = headers.set('Content-Type', 'application/json'); headers = headers.set('Accept', 'application/json');
const headers = new HttpHeaders() .set('Content-Type', 'application/json') .set('Accept', 'application/json') ;
امکان تعریف HttpParams
اگر به شیء options در تعریف فوق دقت کنید، دارای خاصیت اختیاری params نیز هست. از آن میتوان جهت تعریف کوئری استرینگها استفاده کرد. برای مثال درخواست ذیل:
http .post('/api/items/add', body, { params: new HttpParams().set('id', '3'), }) .subscribe();
/api/items/add?id=3
یک نکته: شیء HttpParams به صورت immutable طراحی شدهاست. یعنی اگر آنرا به صورت ذیل فراخوانی کنیم:
const params = new HttpParams(); params.set('orderBy', '"$key"') params.set('limitToFirst', "1");
const params = new HttpParams() .set('orderBy', '"$key"') .set('limitToFirst', "1");
const params = new HttpParams({fromString: 'orderBy="$key"&limitToFirst=1'});
تغییرات مورد نیاز در کدهای سرویسها جهت مدیریت خطاها
در اینجا اینبار خطای بازگشتی، از نوع ویژهی HttpErrorResponse است که شامل اطلاعات شماره کد و متن خطای حاصل میباشد:
import { HttpClient, HttpHeaders, HttpErrorResponse } from "@angular/common/http"; postData() { this.http.post(this.url, this.payload).subscribe( res => { console.log(res); }, (err: HttpErrorResponse) => { console.log(err.error); console.log(err.name); console.log(err.message); console.log(err.status); if (err.error instanceof Error) { console.log("Client-side error occured."); } else { console.log("Server-side error occured."); } } ); }
امکان سعی مجدد در اتصال توسط HTTP Client
ممکن است در اولین سعی در اتصال به سرور، خطایی رخ دهد و یا سرور در دسترس نباشد. در اینجا توسط متد retry میتوان درخواست سعی مجدد در اتصال را صادر کرد.
برای این منظور ابتدا عملگر retry مربوط به RxJS را import میکنیم:
import 'rxjs/add/operator/retry';
http .get<ItemsResponse>('/api/items') .retry(3) .subscribe(...);
امکان درخواست کل Response بجای Body
اگر به امضای پارامتر اختیاری options دقت کنید، خاصیت observe آن به صورت پیش فرض به body تنظیم شدهاست. به این معنا که تنها body یک response را تبدیل به یک شیء JSON میکند:
(method) HttpClient.post(url: string, body: any, options?: { headers?: HttpHeaders; observe?: "body"; params?: HttpParams; reportProgress?: boolean; responseType?: "json"; withCredentials?: boolean; }): Observable<Object>
http .get<MyJsonData>('/data.json', {observe: 'response'}) .subscribe(resp => { console.log(resp.headers.get('X-Custom-Header')); console.log(resp.body.someField); });
یک نکتهی تکمیلی: کدهای سری کار با فرمها در Angular را اگر به HttpClient ارتقاء دهیم، خلاصهی تغییرات آنها به این صورت خواهند بود.
فعال سازی Subgrid
در اینجا مواردی را که باید جهت فعال سازی subgrid به تعاریف اولیهی jqGrid اضافه کرد، مشاهده میکنید:
$('#list').jqGrid({ caption: "آزمایش یازدهم", // ... jsonReader: { // ... subgrid: { root: "Rows", repeatitems: true, cell: "RowCells" } }, // ... subGrid: true, subGridModel: [{ name: ['شرکت', 'آدرس', 'کد پستی', 'شهر', 'کشور', 'تلفن', 'وب سایت'], width: [100, 100, 100, 100, 100, 100, 100], align: ['center', 'center', 'center', 'center', 'center', 'center', 'center'], params: ['@(StronglyTyped.PropertyName<Product>(x=>x.Name))'] }], subGridOptions:{ reloadOnExpand : false //load only once }, subGridUrl: '@Url.Action("GetGetSupplierData", "Home")' });
با تنظیم subGrid: true نمایش ستون + داری که در تصویر فوق مشخص است، انجام میشود.
subGridModel بیانگر ساختار اطلاعاتی است که قرار است نمایش داده شوند.
آرایه name، نام سر ستونها را مشخص میکند.
آرایه width، عرض ستونهای زیرگرید را مقدار دهی خواهد کرد.
آرایه align محل و سمت قرارگیری هر یک از مقادیر سلولها را تعیین میکند.
آرایه params اختیاری است. زمانیکه کاربر بر روی یک + ستون subgrid، برای باز شدن این زیرگرید کلیک میکند، صرفا Id ردیف به سرور ارسال میشود. اگر در این بین میخواهید، خاصیت خاصی از گرید اصلی نیز به سرور ارسال شود، آرایه params را مقدار دهی کنید. برای نمونه در اینجا Name ردیف انتخاب شده نیز به ارسال ارسال خواهد شد (برگه شبکه شکل فوق).
subGridOptions یک سری تنظیمات اضافه را به همراه دارد. اگر میخواهید اطلاعات زیرگرید فقط یکبار بارگذاری شود و با هربار کلیک کاربر از سرور دریافت نگردد، خاصیت reloadOnExpand آنرا false کنید.
subGridUrlآدرسی که تامین کننده اطلاعات JSON زیرگرید میباشد.
در این حالت، کدهای سمت سرور بازگشت اطلاعات زیر گرید به شکل زیر میباشد:
public ActionResult GetGetSupplierData(int id, string name) { var list = ProductDataSource.LatestProducts; var products = list.Where(x => x.Id == id).ToList(); if (!products.Any()) return Json(null, JsonRequestBehavior.AllowGet); var productsData = new JqGridData { Rows = (products.Select(product => new JqGridRowData { Id = product.Id, RowCells = new List<string> { product.Supplier.CompanyName, product.Supplier.Address, product.Supplier.PostalCode, product.Supplier.City, product.Supplier.Country, product.Supplier.Phone, product.Supplier.HomePage } })).ToList() }; return Json(productsData, JsonRequestBehavior.AllowGet); }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid11.zip
JSON.NET نسخه 7 منتشر شد
CREATE TABLE Users ( id INT NOT NULL AUTO_INCREMENT, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, gender ENUM('Male', 'Female') NOT NULL, PRIMARY KEY (id) ); CREATE TABLE BlogPosts ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, user_id INT NOT NULL, KEY user_id (user_id), CONSTRAINT `blogposts_ibfk_1` FOREIGN KEY (user_id) REFERENCES Users (id), PRIMARY KEY (id) )
SELECT U.id, CONCAT_WS(' ', U.first_name, U.last_name) AS FullName, BP.title FROM Users AS U JOIN BlogPosts AS BP ON U.id = BP.user_id;
تبدیل یک ساختار Relational به JSON
میتوانیم خروجی موردنظر را در قالب JSON نیز کوئری بگیریم:
SELECT JSON_OBJECT('id', U.id, 'user', CONCAT_WS(' ', U.first_name, U.last_name), 'title', BP.title) FROM Users AS U JOIN BlogPosts AS BP ON U.id = BP.user_id;
به عنوان مثال میتوانیم لیست کاربران را به همراه بلاگ پستهایشان، اینگونه کوئری بگیریم:
SELECT JSON_OBJECT('user', CONCAT_WS(' ', U.first_name, U.last_name), 'blog_posts', JSON_ARRAYAGG(JSON_OBJECT('id', BP.id, 'title', BP.title))) AS JSON FROM Users AS U JOIN BlogPosts AS BP ON U.id = BP.user_id GROUP BY U.id;
خروجی کوئری فوق اینچنین خواهد بود:
تبدیل یک ساختار JSON به Relational
همچنین میتوانیم یک ساختار JSON را به صورت Relational تبدیل کنیم. اینکار توسط تابع JSON_TABLE قابل انجام است. کاری که این تابع انجام میدهد، ایجاد یک جدول موقت و کپی کردن دیتای موردنظر درون آن است. فرض کنید ساختار JSON زیر را به اینصورت درون دیتابیس ذخیره کردهایم:
{ "id": "1", "new": false, "sku": "asdf123", "tag": ["fashion", "men", "jacket", "full sleeve"], "name": "Lorem ipsum jacket", "image": [ "/assets/img/product/fashion/1.jpg", "/assets/img/product/fashion/3.jpg", "/assets/img/product/fashion/6.jpg", "/assets/img/product/fashion/8.jpg", "/assets/img/product/fashion/9.jpg" ], "price": 12.45, "rating": 4, "category": ["fashion", "men"], "discount": 10, "offerEnd": "October 5, 2020 12:11:00", "saleCount": 54, "description": { "fullDescription": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?", "shortDescription": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur." } }
اکنون میخواهیم چنین خروجیای داشته باشیم:
کوئری موردنیاز برای تهیه خروجی فوق اینچنین خواهد بود:
SELECT newTable.* FROM experiments.productMetadata, JSON_TABLE( data, "$" COLUMNS ( id INT PATH "$.id", new CHAR(5) PATH "$.new", sku CHAR(20) PATH "$.sku", price FLOAT PATH "$.price", rating FLOAT PATH "$.rating", name CHAR(255) PATH "$.name", discount FLOAT PATH "$.discount", offerEnd TEXT PATH "$.offerEnd", saleCount INT PATH "$.saleCount", description TEXT PATH "$.description.shortDescription" ) ) AS newTable;
تابع JSON_TABLE دو ورودی نیاز خواهد داشت؛ ورودی اول ستون JSONی است که میخوایم از آن کوئری بگیریم. ورودی دوم با تعیین path شروع خواهد شد. از آنجائیکه محتوای داخل ستون data به صورت آبجکت ذخیره شدهاست، از $ استفاده کردهایم که به معنای داکیومنت جاری است. سپس توسط کلمه کلیدی COLUMNS ساختار جدول موقتمان را تعریف خواهیم کرد. این ساختار به صورت یک آرگومان به COLUMNS ارسال خواهد شد و شبیه به ساختار CREATE TABLE است؛ با این تفاوت که بعد از تعریف نوع دادهای هر ستون باید مسیر رسیدن به مقدار موردنظر را نیز تعیین کنیم که در واقع همان سینتکس pathی است که در مثالهای قبل نیز بررسی کردیم. به عنوان مثال برای رسیدن به مقدار پراپرتی name، مسیر را به صورت name.$ نوشتهایم. این path از آرایه نیز پشتیبانی میکند؛ مثلاً برای دسترسی به عنصر اول آرایه tag کافی است اینگونه عمل کنیم:
tag CHAR(20) PATH "$.tag[0]"
همچنین تابع JSON_TABLE، از ساختارهای تودرتو نیز پشتیبانی میکند. به عنوان مثال برای داشتن مقادیر tag, category, image در خروجی میتوانیم از کلمه کلیدی NESTED استفاده کنیم:
SELECT newTable.* FROM experiments.productMetadata, JSON_TABLE( data, "$" COLUMNS ( id INT PATH "$.id", new CHAR(5) PATH "$.new", sku CHAR(20) PATH "$.sku", price FLOAT PATH "$.price", rating FLOAT PATH "$.rating", NESTED PATH '$.tag[*]' COLUMNS (tag TEXT PATH '$'), name CHAR(255) PATH "$.name", discount FLOAT PATH "$.discount", offerEnd TEXT PATH "$.offerEnd", saleCount INT PATH "$.saleCount", description TEXT PATH "$.description.shortDescription", NESTED PATH '$.image[*]' COLUMNS (image TEXT PATH '$'), NESTED PATH '$.category[*]' COLUMNS (category TEXT PATH '$') ) ) AS newTable;
درون COLUMNS میتوانیم یک name FOR ORDINALITY نیز تعیین کنیم. این فیلد دقیقاً مشابه AUTO INCREMENT در ساختار CREATE TABLE میباشد. به این معنا که به ازای هر آیتم فیلد nested، یک واحد اضافه خواهد شد. از آن میتوانیم به عنوان rowId برای آیتم آرایه استفاده کنیم:
NESTED PATH '$.category[*]' COLUMNS (categoryRowId FOR ORDINALITY, category TEXT PATH '$')
همچنین از ON EMPTY برای پراپرتیهایی که در ساختار JSON وجود ندارند نیز میتوانیم استفاده کنیم. به عنوان مثال در کوئری زیر گفتهایم در صورت عدم وجود price، یک مقدار پیشفرض باید نمایش داده شود و همچنین در صورت عدم وجود name، یک خطا در خروجی نمایش داده شود:
name CHAR(255) PATH "$.name" ERROR ON EMPTY, price FLOAT PATH "$.price" DEFAULT "0" ON EMPTY,
همچنین میتوانیم مقدار NULL را در صورت عدم وجود name ست کنیم:
name CHAR(255) PATH "$.name" NULL ON EMPTY,
public class EFController : Controller { // // GET: /EF/ public ActionResult AjaxConnected([DataSourceRequest] DataSourceRequest request ) { using (var dbef=new dbTestEntities()) { IQueryable<Person> persons = dbef.People; DataSourceResult result = persons.ToDataSourceResult(request); return Json(result.Data,JsonRequestBehavior.AllowGet); } } }
@{ ViewBag.Title = "AjaxConnected"; } <h2>AjaxConnected</h2> @(Html.Kendo().Grid<TelerikMvcApp2.Models.Person>( ) .Name("Grid") .DataSource(builder => builder .Ajax() .Read(operationBuilder => operationBuilder.Action("AjaxConnected", "EF")) ) .Columns(factory => { factory.Bound(person => person.personId); factory.Bound(person => person.Name); factory.Bound(person => person.LastName); }) .Pageable() .Sortable())
[{"personId":1,"Name":"Amin","LastName":"Saadati"}, {"personId":2,"Name":"Fariba","LastName":"Ghochani "},{"personId":4,"Name":"Milad","LastName":"Rahman i"},{"personId":5,"Name":"rima","LastName":"rad"}, {"personId":6,"Name":"ali","LastName":"kiva"},{"pe rsonId":7,"Name":"sahel","LastName":"abasi"},{"per sonId":8,"Name":"medi","LastName":"ghaem"},{"perso nId":9,"Name":"mino","LastName":"kafash"},{"person Id":10,"Name":"behzad","LastName":"tizro"},{"perso nId":11,"Name":"toti","LastName":"saadati"},{"pers onId":12,"Name":"parinaz","LastName":"karami"},{"p ersonId":13,"Name":"sadegh","LastName":"hojati"},{ "personId":14,"Name":"milad","LastName":"ebadipor" },{"personId":15,"Name":"farid","LastName":"riazi" },{"personId":16,"Name":"said","LastName":"abdoli" },{"personId":17,"Name":"behzad","LastName":"ariaf ar"},{"personId":18,"Name":"jamshid","LastName":"k otahi"}]
جهت بهینه سازی روش ارائه شده در مقاله "بارگذاری یک یوزرکنترل با استفاده از جیکوئری" ، میتوان مبحث فشرده سازی را نیز به آن افزود.
برای این منظور نیاز است تا بتوان response حاصل را کاملا کنترل کرد و این مورد از طریق یک http module به خوبی قابل انجام است. مبحث http compression و پیاده سازی آنرا احتمالا بارها در سایتهای مختلف نیز دیدهاید:
using System;
using System.IO;
using System.IO.Compression;
using System.Globalization;
using System.Web;
public class JsonCompressionModule : IHttpModule
{
public JsonCompressionModule()
{
}
public void Dispose()
{
}
public void Init(HttpApplication app)
{
app.PreRequestHandlerExecute += new EventHandler(Compress);
}
private void Compress(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
HttpRequest request = app.Request;
HttpResponse response = app.Response;
if (request.ContentType.ToLower(CultureInfo.InvariantCulture).StartsWith("application/json"))
{
if (!((request.Browser.IsBrowser("IE")) && (request.Browser.MajorVersion <= 6)))
{
string acceptEncoding = request.Headers["Accept-Encoding"];
if (!string.IsNullOrEmpty(acceptEncoding))
{
acceptEncoding = acceptEncoding.ToLower(CultureInfo.InvariantCulture);
if (acceptEncoding.Contains("gzip"))
{
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
response.AddHeader("Content-encoding", "gzip");
}
else if (acceptEncoding.Contains("deflate"))
{
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
response.AddHeader("Content-encoding", "deflate");
}
}
}
}
}
}
if ( !request.Url.PathAndQuery.ToLower().Contains( ".asmx" ) )
return;
جهت اعمال این ماژول به برنامه ASP.Net خود، کافی است سطر زیر را به قسمت httpModules وب کانفیگ افزود:
<httpModules>
<add name="JsonCompressionModule" type="JsonCompressionModule"/>
</httpModules>
متاسفانه افزونهی فایرباگ فایرفاکس اندازهی نهایی response را نمایش میدهد و در گزارش آن حتی خبری از Content-encoding اضافه شده نیز نخواهد بود. بنابراین برای بررسی این روش مناسب نیست.
ابزار دیگری که اساسا برای این نوع آزمایشات طراحی شده است، برنامه معروف فیدلر میباشد (که توسط مدیر پروژه تیم IE برنامه نویسی شده است).
برای استفاده از فیدلر جهت دیباگ درخواستهای local باید یک نکتهی کوچک را رعایت کرد:
همانطور که در URL فوق مشاهده میکنید یک نقطه پس از localhost اضافه شده است تا خروجی محلی مربوطه قابل بررسی شود.
مطابق تصویر فوق، هم content-encoding اضافه شده مشخص است و هم حجم پاسخ دریافتی از 40 کیلوبایت (بر اساس یک تست معمولی روی صفحهای مشخص) به نزدیک یک کیلوبایت و اندی کاهش یافته است.
دریافت افزونهی jsTree
برای دریافت افزونهی jsTree میتوان به مخزن کد آن در Github مراجعه کرد و همچنین مستندات آنرا در سایت jstree.com قابل مطالعه هستند.
تنظیمات مقدماتی jsTree
در این مطلب فرض شدهاست که فایل jstree.min.js، در پوشهی Scripts و فایلهای CSS آن در پوشهی Content\themes\default کپی شدهاند.
به این ترتیب layout برنامه چنین شکلی را خواهد یافت:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link href="~/Content/Site.css" rel="stylesheet" /> <link href="~/Content/themes/default/style.min.css" rel="stylesheet" /> <script src="~/Scripts/jquery.min.js"></script> <script src="~/Scripts/jstree.min.js"></script> </head> <body dir="rtl"> @RenderBody() @RenderSection("scripts", required: false) </body> </html>
نمایش راست به چپ اطلاعات
در کدهای این افزونه به تگ body و ویژگی dir آن برای تشخیص راست به چپ بودن محیط دقت میشود. به همین جهت این تعریف را در layout فوق ملاحظه میکنید. برای مثال اگر به فایل jstree.contextmenu.js (موجود در مجموعه سورسهای این افزونه) مراجعه کنید، یک چنین تعریفی قابل مشاهده است:
right_to_left = $("body").css("direction") === "rtl";
تهیه ساختاری جهت ارائهی خروجی JSON
با توجه به اینکه قصد داریم به صورت پویا با این افزونه کار کنیم، نیاز است بتوانیم ساختار سلسله مراتبی مدنظر را با فرمت JSON ارائه دهیم. در ادامه کلاسهایی که معادل فرمت JSON قابل قبول توسط این افزونه را تولید میکنند، ملاحظه میکنید:
using System.Collections.Generic; namespace MvcJSTree.Models { public class JsTreeNode { public string id { set; get; } // نام این خواص باید با مستندات هماهنگ باشد public string text { set; get; } public string icon { set; get; } public JsTreeNodeState state { set; get; } public List<JsTreeNode> children { set; get; } public JsTreeNodeLiAttributes li_attr { set; get; } public JsTreeNodeAAttributes a_attr { set; get; } public JsTreeNode() { state = new JsTreeNodeState(); children = new List<JsTreeNode>(); li_attr = new JsTreeNodeLiAttributes(); a_attr = new JsTreeNodeAAttributes(); } } public class JsTreeNodeAAttributes { // به هر تعداد و نام اختیاری میتوان خاصیت تعریف کرد public string href { set; get; } } public class JsTreeNodeLiAttributes { // به هر تعداد و نام اختیاری میتوان خاصیت تعریف کرد public string data { set; get; } } public class JsTreeNodeState { public bool opened { set; get; } public bool disabled { set; get; } public bool selected { set; get; } public JsTreeNodeState() { opened = true; } } }
- هر چند اسامی مانند a_attr، مطابق اصول نامگذاری دات نت نیستند، ولی این نامها را تغییر ندهید. زیرا این افزونه دقیقا به همین نامها و با همین املاء نیاز دارد.
- id، میتواند دقیقا معادل id یک رکورد در بانک اطلاعاتی باشد. Text عنوان گرهای (node) است که نمایش داده میشود. icon در اینجا مسیر یک فایل png است جهت نمایش در کنار عنوان هر گره. توسط state میتوان مشخص کرد که زیر شاخهی جاری به صورت باز نمایش داده شود یا بسته. به کمک خاصیت children میتوان زیر شاخهها را تا هر سطح و تعدادی که نیاز است تعریف نمود.
- خاصیتهای li_attr و a_attr کاملا دلخواه هستند. برای مثال در اینجا دو خاصیت href و data را در کلاسهای مرتبط با آنها مشاهده میکنید. میتوانید در اینجا به هر تعداد ویژگی سفارشی دیگری که جهت تعریف یک گره نیاز است، خاصیت اضافه کنید.
سادهترین مثالی که از ساختار فوق میتواند استفاده کند، اکشن متد زیر است:
[HttpPost] public ActionResult GetTreeJson() { var nodesList = new List<JsTreeNode>(); var rootNode = new JsTreeNode { id = "dir", text = "Root 1", icon = Url.Content("~/Content/images/tree_icon.png"), a_attr = { href = "http://www.bing.com" } }; nodesList.Add(rootNode); nodesList.Add(new JsTreeNode { id = "test1", text = "Root 2", icon = Url.Content("~/Content/images/tree_icon.png"), a_attr = { href = "http://www.bing.com" } }); return Json(nodesList, JsonRequestBehavior.AllowGet); }
بنابراین ساختارهای خود ارجاع دهنده را به خوبی میتوان با این افزونه وفق داد.
فعال سازی اولیه سمت کلاینت افزونه jsTree
برای استفادهی پویای از این افزونه در سمت کلاینت، فقط نیاز به یک DIV خالی است:
<div id="jstree"> </div>
$('#jstree').jstree({ "core": { "multiple": false, "check_callback": true, 'data': { 'url': '@getTreeJsonUrl', "type": "POST", "dataType": "json", "contentType": "application/json; charset=utf8", 'data': function (node) { return { 'id': node.id }; } }, 'themes': { 'variant': 'small', 'stripes': true } }, "types": { "default": { "icon": '@Url.Content("~/Content/images/bookmark_book_open.png")' }, }, "plugins": ["contextmenu", "dnd", "state", "types", "wholerow", "sort", "unique"], "contextmenu": { "items": function (o, cb) { var items = $.jstree.defaults.contextmenu.items(); items["create"].label = "ایجاد زیر شاخه"; items["rename"].label = "تغییر نام"; items["remove"].label = "حذف"; var cpp = items["ccp"]; cpp.label = "ویرایش"; var subMenu = cpp["submenu"]; subMenu["copy"].label = "کپی"; subMenu["paste"].label = "پیست"; subMenu["cut"].label = "برش"; return items; } } });
- multiple : false به این معنا است که نمیخواهیم کاربر بتواند چندین گره را با نگه داشتن دکمهی کنترل انتخاب کند.
- check_callback : true کدهای مرتبط با منوی کلیک سمت راست ماوس را فعال میکند.
- در قسمت data کار تبادل اطلاعات با سرور جهت دریافت فرمت JSON ایی که به آن اشاره شد، انجام میشود. متغیر getTreeJsonUrl یک چنین شکلی را میتواند داشته باشد:
@{ ViewBag.Title = "Demo"; var getTreeJsonUrl = Url.Action(actionName: "GetTreeJson", controllerName: "Home"); }
- در قسمت types که مرتبط است با افزونهای به همین نام، آیکن پیش فرض یک نود جدید ایجاد شده را مشخص کردهایم.
- گزینهی plugins، لیست افزونههای اختیاری این افزونه را مشخص میکند. برای مثال contextmenu منوی کلیک سمت راست ماوس را فعال میکند، dnd همان کشیدن و رها کردن گرهها است در زیر شاخههای مختلف. افزونهی state، انتخاب جاری کاربر را در سمت کلاینت ذخیره و در مراجعهی بعدی او بازیابی میکند. با ذکر افزونهی wholerow سبب میشویم که انتخاب یک گره، معادل انتخاب یک ردیف کامل از صفحه باشد. افزونهی sort کار مرتب سازی خودکار اعضای یک زیر شاخه را انجام میدهد. افزونهی unique سبب میشود تا در یک زیر شاخه نتوان دو عنوان یکسان را تعریف کرد.
- در قسمت contextmenu نحوهی بومی سازی گزینههای منوی کلیک راست ماوس را مشاهده میکنید. در حالت پیش فرض، عناوینی مانند create، rename و امثال آن نمایش داده میشوند که به نحو فوق میتوان آنرا تغییر داد.
با همین حد تنظیم، این افزونه کار نمایش سلسله مراتبی اطلاعات JSON ایی دریافت شده از سرور را انجام میدهد.
ذخیره سازی گرههای جدید و تغییرات سلسله مراتب پویای تعریف شده در سمت سرور
همانطور که عنوان شد، اگر افزونهی اختیاری contextmenu را فعال کنیم، امکان افزودن، ویرایش و حذف گرهها و زیر شاخهها را خواهیم یافت. برای انتقال این تغییرات به سمت سرور، باید به نحو ذیل عمل کرد:
$('#jstree').jstree({ // تمام تنظیمات مانند قبل }).on('delete_node.jstree', function (e, data) { }) .on('create_node.jstree', function (e, data) { }) .on('rename_node.jstree', function (e, data) { }) .on('move_node.jstree', function (e, data) { }) .on('copy_node.jstree', function (e, data) { }) .on('changed.jstree', function (e, data) { }) .on('dblclick.jstree', function (e) { }) .on('select_node.jstree', function (e, data) { });
در تمام این حالات، جایی که data در اختیار ما است، میتوان یک چنین ساختار جاوا اسکریپتی را برای ارسال به سرور طراحی کرد:
function postJsTreeOperation(operation, data, onDone, onFail) { $.post('@doJsTreeOperationUrl', { 'operation': operation, 'id': data.node.id, 'parentId': data.node.parent, 'position': data.position, 'text': data.node.text, 'originalId': data.original ? data.original.id : data.node.original.id, 'href': data.node.a_attr.href }) .done(function (result) { onDone(result); }) .fail(function (result) { alert('failed.....'); onFail(result); }); }
.on('create_node.jstree', function (e, data) { postJsTreeOperation('CreateNode', data, function (result) { data.instance.set_id(data.node, result.id); }, function (result) { data.instance.refresh(); }); })
و معادل سمت سرور دریافت کنندهی این اطلاعات، اکشن متد ذیل میتواند باشد:
[HttpPost] public ActionResult DoJsTreeOperation(JsTreeOperationData data) { switch (data.Operation) { case JsTreeOperation.CopyNode: case JsTreeOperation.CreateNode: //todo: save data var rnd = new Random(); // آی دی رکورد پس از ثبت در بانک اطلاعاتی دریافت و بازگشت داده شود return Json(new { id = rnd.Next() }, JsonRequestBehavior.AllowGet); case JsTreeOperation.DeleteNode: //todo: save data return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); case JsTreeOperation.MoveNode: //todo: save data return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); case JsTreeOperation.RenameNode: //todo: save data return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); default: throw new InvalidOperationException(string.Format("{0} is not supported.", data.Operation)); } }
namespace MvcJSTree.Models { public enum JsTreeOperation { DeleteNode, CreateNode, RenameNode, MoveNode, CopyNode } public class JsTreeOperationData { public JsTreeOperation Operation { set; get; } public string Id { set; get; } public string ParentId { set; get; } public string OriginalId { set; get; } public string Text { set; get; } public string Position { set; get; } public string Href { set; get; } } }
در اینجا Href را نیز مشاهده میکنید. همانطور که عنوان شد، اعضای JsTreeNodeAAttributes اختیاری هستند. بنابراین اگر این اعضاء را تغییر دادید، باید خواص JsTreeOperationData و همچنین اعضای شیء تعریف شده در postJsTreeOperation را نیز تغییر دهید تا با هم تطابق پیدا کنند.
چند نکتهی تکمیلی
اگر میخواهید که با دوبار کلیک بر روی یک گره، کاربر به href آن هدایت شود، میتوان از کد ذیل استفاده کرد:
var selectedData; // ... .on('dblclick.jstree', function (e) { var href = selectedData.node.a_attr.href; alert('selected node: ' + selectedData.node.text + ', href:' + href); // auto redirect if (href) { window.location = href; } // activate edit mode //var inst = $.jstree.reference(selectedData.node); //inst.edit(selectedData.node); }) .on('select_node.jstree', function (e, data) { //alert('selected node: ' + data.node.text); selectedData = data; });
حتی اگر خواستید که با دوبار کلیک بر روی یک گره، گزینهی ویرایش آن فعال شود، کدهای آن را به صورت کامنت مشاهده میکنید.
مثال کامل این بحث را از اینجا میتوانید دریافت کنید:
MvcJSTree.zip
System.Text.Json source generator چیست؟
پایهی تمام اعمال serialization و deserialization در دات نت، استفاده از Reflection است که در زمینهی ارائهی برنامههایی با کارآیی بالا و با مصرف حافظهی پایین، بهینه عمل نمیکند. راهحل جایگزین استفاده از Reflection که در زمان اجرای برنامه رخ میدهد، به همراه دات نت 5 ارائه شد و source generators نام دارد. Source generators امکان تولید فایلهای #C را در زمان کامپایل برنامه میسر میکنند که نسبت به راهحل Reflection که در زمان اجرای برنامه فعال میشود، کارآیی بسیار بیشتری را ارائه میکنند. برای مثال به همراه دات نت 6، علاوه بر روش پیشفرض مبتنی بر Reflection ارائه شدهی توسط System.Text.Json، راه حل جدید امکان استفادهی از source generators توکار آن نیز پیش بینی شدهاست. کار اصلی آن، انجام تمام مراحلی است که پیشتر توسط Reflection در زمان اجرای برنامه صورت میگرفت، اینبار در زمان کامپایل برنامه و ارائهی آن به صورت از پیش آماده شده و مهیا.
مزایای این روش شامل موارد زیر است:
- بالا رفتن سرعت برنامه
- کاهش زمان آغاز اولیهی برنامه
- کاهش میزان حافظهی مورد نیاز برنامه
- عدم نیاز به استفادهی از System.Reflection و System.Reflection.Emit
- ارائهی Trim-compatible serialization که سبب کاهش اندازهی نهایی برنامه میشود. برای مثال در برنامههای Blazor میتوان با فعالسازی Trimming، کدهای استفاده نشده را از فایلهای بایناری نهایی حذف کرد. استفاده از source generators، با این روش سازگاری کاملی دارد.
مثالی از نحوهی کار با JSON در دات نت 6، توسط source generators آن
فرض کنید قصد داریم اعمال serialization و deserialization از نوع JSON را بر روی نمونههای کلاس زیر انجام دهیم:
namespace Test { internal class Person { public string FirstName { get; set; } public string LastName { get; set; } } }
using System.Text.Json.Serialization; namespace Test { [JsonSerializable(typeof(Person))] internal partial class MyJsonContext : JsonSerializerContext { } }
پس از آن فقط کافی است MyJsonContext را به عنوان پارامتر متدهای جدید Serialize و یا Deserialize، به صورت زیر ارسال کنیم تا از آن استفاده شود:
Person person = new() { FirstName = "Jane", LastName = "Doe" }; byte[] utf8Json = JsonSerializer.SerializeToUtf8Bytes(person, MyJsonContext.Default.Person); person = JsonSerializer.Deserialize(utf8Json, MyJsonContext.Default.Person);
متدهای جدید این API مبتنی بر source generators را در ادامه ملاحظه میکنید:
namespace System.Text.Json { public static class JsonSerializer { public static object? Deserialize(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerContext context) => ...; public static object? Deserialize(ReadOnlySpan<char> json, Type returnType, JsonSerializerContext context) => ...; public static object? Deserialize(string json, Type returnType, JsonSerializerContext context) => ...; public static object? Deserialize(ref Utf8JsonReader reader, Type returnType, JsonSerializerContext context) => ...; public static ValueTask<object?> DeserializeAsync(Stream utf8Json, Type returnType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static TValue? Deserialize<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static TValue? Deserialize<TValue>(string json, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static TValue? Deserialize<TValue>(ReadOnlySpan<char> json, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static TValue? Deserialize<TValue>(ref Utf8JsonReader reader, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static string Serialize(object? value, Type inputType, JsonSerializerContext context) => ...; public static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerContext context) { } public static Task SerializeAsync(Stream utf8Json, object? value, Type inputType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task SerializeAsync<TValue>(Stream utf8Json, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static byte[] SerializeToUtf8Bytes(object? value, Type inputType, JsonSerializerContext context) => ...; public static byte[] SerializeToUtf8Bytes<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...; public static void Serialize<TValue>(Utf8JsonWriter writer, TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { } public static string Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...; } }
برای معرفی تنظیمات serialization و deserialization، برای مثال تهیهی خروجیهای CamelCase، میتوان از ویژگی JsonSourceGenerationOptions به صورت زیر استفاده کرد:
using System.Text.Json.Serialization; namespace Test { [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(Person))] internal partial class MyJsonContext : JsonSerializerContext { } }
string json = JsonSerializer.Serialize(person, MyJsonContext.Default.Person); Person person = JsonSerializer.Deserialize(json, MyJsonContext.Default.Person);
روش استفاده از JSON Source generators در برنامههای ASP.NET Core
در این نوع برنامهها، JsonSerializerContextها را میتوان توسط متد AddContext به صورت زیر به تنظیمات JSON برنامه معرفی کرد:
services.AddControllers().AddJsonOptions(options => options.AddContext<MyJsonContext>());
روش استفاده از JSON Source generators در برنامههای Blazor
البته در اینجا بیشتر منظور امکان استفادهی از آنها توسط HttpClient است که به صورت زیر توسط متد GetFromJsonAsync واقع در فضای نام System.Net.Http.Json، میسر شدهاست:
[JsonSerializable(typeof(WeatherForecast[]))] internal partial class MyJsonContext : JsonSerializerContext { } @code { private WeatherForecast[] forecasts; private static JsonSerializerOptions Options = new(JsonSerializerDefaults.Web); private static MyJsonContext Context = new MyJsonContext(Options); protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync("sample-data/weather.json", Context.WeatherForecastArray); } }
namespace System.Net.Http.Json { public static partial class HttpClientJsonExtensions { public static Task<object?> GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<object?> GetFromJsonAsync(this HttpClient client, System.Uri? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; } public static partial class HttpContentJsonExtensions { public static Task<object?> ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...; public static Task<T?> ReadFromJsonAsync<T>(this HttpContent content, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...; } }