متغیرهای سراسری در Postman
برای تعریف متغیرهای سراسری که در تمام برگههای Postman قابل دسترسی باشند، میتوان از متد pm.globals.set در قسمت Tests هر درخواست که پس از پایان درخواست جاری اجرا میشود، استفاده کرد. دراینجا فرصت خواهیم داشت تا مقدار دریافتی از سرور را در یک متغیر ذخیره کنیم. سپس میتوان از این متغیر، در حین ارسال درخواستی دیگر، استفاده کرد که نمونهای از آنرا در قسمت دوم، با تبدیل شیء response به یک شیء جاوا اسکریپتی و استخراج خاصیت uuid آن، مشاهده کردید:
let jsonResponse = pm.response.json(); pm.globals.set("uuid", jsonResponse.uuid);
که سبب باز شدن صفحهی دیالوگ زیر میشود که در آن میتوان کلید/مقدارهای جدیدی را به صورت دستی و بدون کدنویسی، تعریف و مقدار دهی کرد:
عملکرد | متد |
تعریف و مقدار دهی یک متغیر سراسری | pm.globals.set("varName", "VALUE"); |
دریافت مقدار یک متغیر سراسری | pm.globals.get("varName"); |
پاک کردن یک متغیر سراسری مشخص | pm.globals.unset("varName"); |
حذف تمام متغیرهای سراسری | pm.globals.clear(); |
یکی از کاربردهای مهم متغیرهای سراسری، دریافت توکنهای دسترسی پس از لاگین، در یک درخواست و استفادهی از این توکنها در درخواستهای دیگر میباشد.
روش استفادهی از متغیرهای تعریف شده
پس از تعریف این متغیرها، برای دسترسی به آنها میتوان از روش {{variableName}} در قسمتهای مختلف postman استفاده کرد:
Request URL: http://{{domain}}/users/{{userId}} Headers (key:value): X-{{myHeaderName}}:foo Request body: {"id": "{{userId}}", "name": "John Doe"}
متغیرهای محیطی در Postman
متغیرهای محیطی نیز بسیار شبیه به متغیرهای سراسری هستند، اما میدان دید آنها کمتر است. برای مثال فرض کنید که قصد دارید اطلاعات پایهی تنظیمات سرور و پورت آنها را در بین درخواستهای مختلف تغییر دهید؛ بدون اینکه بخواهید اصل درخواستها تغییری کنند؛ یا اینکه قسمت تعریف متغیرهای سراسری بیش از حد شلوغ شدهاست و قصد دارید آنها را گروه بندی کرده و مورد استفاده قرار دهید.
در ابتدای کار، هیچ محیط خاصی تعریف نشدهاست:
برای تعریف یک محیط جدید میتوان بر روی دکمهای با آیکن چشم، در بالای سمت راست صفحه و کلیک بر روی گزینهی Add آن، یک محیط جدید را ایجاد کرد:
در صفحهی باز شده ابتدا باید نامی را برای این محیط جدید انتخاب کرد و سپس میتوان key/valueهایی را مخصوص این محیط، تعریف نمود:
پس از تعریف متغیرهای جدید محیطی و مقادیر آنها، نحوهی استفادهی از این متغیرها دقیقا همانند روشی است که از متغیرهای سراسری استفاده کردیم و توسط روش {{variableName}} قابل دسترسی هستند.
برای ویرایش اطلاعات منتسب به یک محیط، ابتدا باید آنرا از dropdown محیطهای بالای صفحه انتخاب کرد. اکنون با کلیک بر روی دکمهای با آیکن چشم، در بالای سمت راست صفحه، لینک ویرایش این محیط انتخاب شده ظاهر میشود:
API کار با متغیرهای محیطی از طریق کد نویسی
عملکرد | متد |
تعریف و مقدار دهی یک متغیر محیطی | pm.environment.set("varName", "VALUE"); |
دریافت مقدار یک متغیر محیطی | pm.environment.get("varName"); |
پاک کردن یک متغیر محیطی مشخص | pm.environment.unset("varName"); |
حذف تمام متغیرهای محیطی | pm.environment.clear(); |
تفاوت میدان دید متغیرهای محیطی و متغیرهای سراسری
باید دقت داشت که هر دوی متغیرهای سراسری و محیطی، در تمام برگههای تعریف شده قابل دسترسی میباشند و از این لحاظ تفاوتی بین آنها نیست. اما فرض کنید یک متغیر سراسری را با نام port1 تعریف کردهاید و از آن برای ساخت آدرسی مانند https://localhost:{{port1}} استفاده کردهاید. همچنین دقیقا همین متغیر port1 را در محیط جدیدی به نام Env1 نیز تعریف کردهاید. اگر محیطی انتخاب نشده باشد، port1 به همان متغیر سراسری تعریف شده اشاره میکند.
اما اگر محیط انتخابی را به Env1 تغییر دهیم، اینبار port1، از طریق اطلاعات Env1 تامین شده و مقدار متغیر سراسری تعریف شده را بازنویسی (یا مخفی) میکند. بنابراین در حین کارکردن با محیطی مشخص، متغیرهای محیطی، بر متغیرهای سراسری مقدم هستند.
یک نکته: با نزدیک کردن اشارهگر ماوس، به یک متغیر تعریف شدهی در postman، میتوان میدان دید آنرا به سادگی مشاهده کرد و در این حالت دیگر جای حدس و گمانی باقی نمیماند.
عدم انتشار مقادیر اولیهی حساس، در حین گرفتن خروجیها
اگر به تصاویر فوق دقت کنید، حین تنظیم مقادیر متغیرها، ستون اول، initial value نام دارد و ستون دوم، current value. هنگام گرفتن خروجی از یک مجموعهی Postman، تنها این مقدار اولیه در خروجی وجود خواهد داشت و با دیگران به اشتراک گذاشته میشود. مقدار جاری همانی است که در حین ارسال درخواستها مورد استفاده قرار میگیرد. بنابراین تنها کاربرد initial value، در تهیهی خروجیها است که در انتهای قسمت سوم آنرا بررسی کردیم.
مشکل اینجا است که اگر از متدهای به روز رسانی مقادیر متغیرها استفاده کنیم، هر دو مقدار را تغییر میدهند که ممکن است علاقمند نباشید آنها را به اشتراک بگذارید. برای رفع این مشکل میتوان به منوی File->Settings آن مراجعه و گزینهی Automatically persist variable values را خاموش کرد:
با اینکار تغییر current value توسط متدهای API، سبب تغییر initial value که در exports ظاهر میشوند، نخواهد شد.
اضافه کردن سازندهی دوم به کلاس Context میتواند عملیات Migrations را با مشکل مواجه کند. بنابراین جهت ساده سازی آن، به فایل appsettings.json یک مدخل جدید را اضافه میکنیم:
{ "UseInMemoryDatabase": false }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var useInMemoryDatabase = _configuration["UseInMemoryDatabase"].Equals("true", StringComparison.OrdinalIgnoreCase); if (useInMemoryDatabase) { optionsBuilder.UseInMemoryDatabase(); } else { optionsBuilder.UseSqlServer( _configuration["ConnectionStrings:ApplicationDbContextConnection"] , serverDbContextOptionsBuilder => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; serverDbContextOptionsBuilder.CommandTimeout(minutes); }); } }
در این حالت در سمت کدهای آزمون واحد، مقدار کلید UseInMemoryDatabase را توسط یک InMemoryCollection تامین میکنیم و همچنین افزودن AddDbContext نیز معمولی خواهد بود:
var services = new ServiceCollection(); services.AddSingleton<IConfigurationRoot>(provider => { return new Microsoft.Extensions.Configuration.ConfigurationBuilder() .AddInMemoryCollection(new[] { new KeyValuePair<string,string>("UseInMemoryDatabase", "true"), }) .Build(); }); services.AddEntityFrameworkInMemoryDatabase().AddDbContext<SampleContext>(ServiceLifetime.Scoped);
UrlRewriter توسط Intelligencia.UrlRewriter
Label1.Text = GetProducts.Get() .Where(x => x.Id == int.Parse(Request.QueryString["PID"].ToString())) .Select(x => x.Name) .FirstOrDefault();
<a href='<%# "Details/" + Eval("Name")%>'><%# Eval("Name") %></a>
- اگر از JWE استفاده نمیکنید، بهتر است اطلاعات حساسی مانند شماره تلفن کاربر (و شاید در مواردی حتی آیدی کاربر) را در بدنه توکن قرار ندهیم چرا که قابل خوانده شدن است (که در این صورت استفاده از Guid برای آیدی کاربر می تواند کمی مفید باشد چرا که حداقل آیدی بقیه کاربران قابل پیش بینی نمیباشد).
- توکن JWT هیچ امنیتی در برابر خوانده شدن ندارد؛ ولی به لطف امضای (signature) آن، در برابر تغییر محتوا، ایمن است؛ چرا که در صورت تغییر محتوای آن، دیگر مقدار hash محتوا با امضای آن همخوانی نداشته و عملا از اعتبار ساقط میگردد.
var secretKey = Encoding.UTF8.GetBytes("LongerThan-16Char-SecretKey"); // must be 16 character or longer var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature); var encryptionkey = Encoding.UTF8.GetBytes("16CharEncryptKey"); //must be 16 character var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionkey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256); var claims = new List<Claim> { new Claim(ClaimTypes.Name, "UserName"), //user.UserName new Claim(ClaimTypes.NameIdentifier, "123"), //user.Id }; var descriptor = new SecurityTokenDescriptor { Issuer = _siteSetting.JwtSettings.Issuer, Audience = _siteSetting.JwtSettings.Audience, IssuedAt = DateTime.Now, NotBefore = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.NotBeforeMinutes), Expires = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.ExpirationMinutes), SigningCredentials = signingCredentials, EncryptingCredentials = encryptingCredentials, Subject = new ClaimsIdentity(claims) }; var tokenHandler = new JwtSecurityTokenHandler(); var securityToken = tokenHandler.CreateToken(descriptor); string encryptedJwt = tokenHandler.WriteToken(securityToken);
در ادامه لازم است در مرحله اعتبار سنجی و رمزگشایی توکن در سمت سرور، کلید و الگوریتم لازم را به آن معرفی کنیم تا middleware مربوطه بتواند توکن دریافتی را رمزگشایی و سپس اعتبار سنجی کند. بدین منظور در متد ConfigureServices کلاس Startup.cs خواهیم داشت:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { var secretkey = Encoding.UTF8.GetBytes("LongerThan-16Char-SecretKey"); var encryptionkey = Encoding.UTF8.GetBytes("16CharEncryptKey"); var validationParameters = new TokenValidationParameters { ClockSkew = TimeSpan.Zero, // default: 5 min RequireSignedTokens = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(secretkey), RequireExpirationTime = true, ValidateLifetime = true, ValidateAudience = true, //default : false ValidAudience = "MyWebsite", ValidateIssuer = true, //default : false ValidIssuer = "MyWebsite", TokenDecryptionKey = new SymmetricSecurityKey(encryptionkey) }; options.RequireHttpsMetadata = false; options.SaveToken = true; options.TokenValidationParameters = validationParameters; });
کد بالا مانند کد فعال سازی احراز هویت توسط JWT معمولی در ASP.NET Core است؛ با این تفاوت که:
ابتدا آرایه بایتی همان کلید رمزنگاری (encryptionkey) که قبلا توکن را با آن رمزنگاری کرده بودیم، گرفته شده و سپس توسط مقداردهی خاصیت TokenDecryptionKey کلاس TokenValidationParameters، معرفی شده است.
ولی شاید این سؤال برایتان پیش آید که چرا الگوریتم رمزنگاری مشخص نشده است؟ پس سرور از کجا میفهمد که این توکن بر اساس چه الگوریتمی رمزنگاری شده است؟
دلیل آن این است که به هنگام تولید توکن، اسم الگوریتم مربوطه، داخل بخش header توکن نوشته میشود. اگر تصویر قبل را مشاهده کنید مقدار header توکن به شرح زیر است.
{ "alg": "A128KW", "enc": "A128CBC-HS256", "typ": "JWT" }
پس سرور بر اساس این قسمت از توکن (header)، که هیچگاه رمزنگاری نمیشود، میفهمد که توسط چه الگوریتمی باید توکن را رمزگشایی کند که در اینجا A128CBC-HS256 (اختصار AES-128-CBC و HMAC-SHA256) است.
مثال کامل و قابل اجرای این مطلب را میتوانید از این ریپازیتوری دریافت کنید.
یک لامپ و سوئیچ برق را درنظر بگیرید. زمانیکه لامپ مشاهده میکند سوئیچ برق در حالت روشن قرار گرفتهاست، روشن خواهد شد و برعکس. در اینجا به سوئیچ، subject و به لامپ، observer گفته میشود. هر زمان که حالت سوئیچ تغییر میکند، از طریق یک callback، وضعیت خود را به observer اعلام خواهد کرد. علت استفاده از callbackها، ارائه راهحلهای عمومی است تا بتواند با انواع و اقسام اشیاء کار کند. به این ترتیب هر بار که شیء observer از نوع متفاوتی تعریف میشود (مثلا بجای لامپ یک خودرو قرار گیرد)، نیازی نخواهد بود تا subject را تغییر داد.
عموما به شیءایی که قرار است وضعیت را مشاهده یا رصد کند، Observer گفته میشود و به شیءایی که قرار است وضعیت آن رصد شود Observable یا Subject گفته میشود.
بد نیست بدانید این الگو یکی از کلیدیترین بخشهای معماری لایه بندی MVC نیز میباشد.
همچنین این نکته حائز اهمیت است که این الگو ممکن است باعث نشتی حافظه هم شود و به این مشکل Lapsed Listener Problem میگویند. یعنی یک listener وجود دارد که تاریخ آن منقضی شده، ولی هنوز در حافظه جا خوش کردهاست. این مشکل برای زبانهای شیءگرایی که با سیستمی مشابه GC پیاده سازی میشوند، رخ میدهد. برای جلوگیری از این حالت، برنامه نویس باید این مشکل را با رجیستر کردنها و عدم رجیستر یک شنوده، در مواقع لزوم حل کند. در غیر این صورت این شنونده بی جهت، یک ارتباط را زنده نگه داشته و حافظهی منبع را به هدر میدهد.
مثال: ما یک کلید داریم که سه کلاس RedLED،GreenLED و BlueLED قرار است آن را مشاهده و وضعیت کلید را رصد کنند.
برای پیاده سازی این الگو، ابتدا یک کلاس انتزاعی را با نام Observer که دارای متدی به نام Update است، ایجاد میکنیم. متغیر از نوع کلاس Observable را بعدا ایجاد میکنیم:
public abstract class Observer { protected Observable Observable; public abstract void Update(); }
public class Observable { private readonly List<Observer> _observers = new List<Observer>(); public void Attach(Observer observer) { _observers.Add(observer); } public void Dettach(Observer observer) { _observers.Remove(observer); } public void NotifyAllObservers() { foreach (var observer in _observers) { observer.Update(); } } }
حال کلاس Switch را با ارث بری از کلاس Observable مینویسیم:
public class Switch:Observable { private bool _state; public bool ChangeState { set { _state = value; NotifyAllObservers(); } get { return _state; } } }
برای هر سه چراغ، رنگی هم داریم:
public class RedLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Red LED is {((_on) ? "On" : "Off")}"); } }
public class GreenLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Green LED is {((_on) ? "On" : "Off")}"); } }
public class BlueLED:Observer { private bool _on = false; public override void Update() { _on = !_on; Console.WriteLine($"Blue LED is {((_on) ? "On" : "Off")}"); } }
var greenLed=new GreenLED(); var redLed=new RedLED(); var blueLed=new BlueLED(); var switchKey=new Switch(); switchKey.Attach(greenLed); switchKey.Attach(redLed); switchKey.Attach(blueLed); switchKey.ChangeState = true; switchKey.ChangeState = false;
Green LED is On Red LED is On Blue LED is On Green LED is Off Red LED is Off Blue LED is Off
حالتهای مختلف ذخیره سازی اطلاعات در مرورگر کاربر
Web Storage و یا Client-side storage در دو حالت کلی session storage و local storage قابل دسترسی است:
الف) session storage
در این حالت اطلاعات ذخیره شدهی در session storage، پس از بسته شدن مرورگر، به صورت خودکار حذف خواهند شد.
ب) local storage
اطلاعات ذخیره شدهی در local storage پس از بسته شدن مرورگر نیز باقی مانده و قابل دسترسی و بازیابی مجدد هستند. تاریخ انقضای آنها صرفا بر اساس خالی شدن دستی کش مرورگر توسط کاربر و یا حذف دستی اطلاعات آن توسط کدهای برنامه تعیین میشود.
هر دو حالت فوق به صورت ایزوله ارائه میشوند؛ با محدودیت حجم 10 مگابایت (جمع حجم نهایی هر دو حالت با هم، محدود به 10 مگابایت است). به این معنا که برنامههای هر دومین، تنها به محل ذخیره سازی خاص همان دومین دسترسی خواهند داشت.
همچنین API دسترسی به آنها synchronous است و کار کردن با آنها سادهاست.
البته Client-side storage به دو مورد فوق خلاصه نمیشود و شامل File Storage ،WebSQL ،IndexedDB و کوکیهای مرورگر نیز هست.
- File Storage هنوز مراحل آزمایشی خودش را طی میکند و مناسب برنامههای دنیای واقعی نیست.
- WebSQL قرار بود بر اساس بانک اطلاعاتی معروف SQLite ارائه شود؛ اما W3C در سال 2010 این استاندارد را منسوخ شده اعلام کرد و با IndexedDB جایگزین شد. دسترسی به آن async است و میتواند موضوع بحثی مجزا باشد.
- کوکیهای مرورگرها نیز یکی دیگر از روشهای ذخیره سازی اطلاعات در مرورگرها هستند و تنها به ذخیره سازی حداکثر 4096 بایت اطلاعات محدود هستند. کوکیها نیز همانند local storage پس از بسته شدن مرورگر باقی میمانند؛ اما برخلاف آن، دارای تاریخ انقضاء و همچنین قابلیت ارسال بین دومینها را نیز دارا میباشند. اگر تاریخ انقضای یک کوکی تعیین نشود، همانند session storage، در پایان کار مرورگر و بسته شدن آن، حذف خواهد شد.
تهیه یک سرویس Angular برای کار با Web Storage
جهت کپسوله سازی نحوهی کار با session storage و local storage میتوان سرویسی را برای اینکار تهیه کرد:
import { Injectable } from "@angular/core"; @Injectable() export class BrowserStorageService { getSession(key: string): any { const data = window.sessionStorage.getItem(key); return JSON.parse(data); } setSession(key: string, value: any): void { const data = value === undefined ? null : JSON.stringify(value); window.sessionStorage.setItem(key, data); } removeSession(key: string): void { window.sessionStorage.removeItem(key); } removeAllSessions(): void { for (const key in window.sessionStorage) { if (window.sessionStorage.hasOwnProperty(key)) { this.removeSession(key); } } } getLocal(key: string): any { const data = window.localStorage.getItem(key); return JSON.parse(data); } setLocal(key: string, value: any): void { const data = value === undefined ? null : JSON.stringify(value); window.localStorage.setItem(key, data); } removeLocal(key: string): void { window.localStorage.removeItem(key); } removeAllLocals(): void { for (const key in window.localStorage) { if (window.localStorage.hasOwnProperty(key)) { this.removeLocal(key); } } } }
در حالت setItem اطلاعاتی را که مرورگرها ذخیره میکنند باید رشتهای باشد. به همین جهت توسط متد JSON.stringify میتوان یک شیء را تبدیل به رشته کرد و ذخیره نمود و در حالت getItem توسط متد JSON.parse، میتوان این رشته را مجددا به همان شیء پیشین خود تبدیل کرد و بازگشت داد.
محل صحیح تعریف BrowserStorageService
همانطور که در مطلب «سازماندهی برنامههای Angular توسط ماژولها» بررسی شد، محل صحیح تعریف این سرویس سراسری مشترک در بین کامپوننتها و ماژولهای برنامه، در CoreModule و پوشهی src\app\core\browser-storage.service.ts است:
import { BrowserStorageService } from "./browser-storage.service"; import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterModule } from "@angular/router"; @NgModule({ imports: [CommonModule, RouterModule], exports: [], // components that are used in app.component.ts will be listed here. declarations: [], // components that are used in app.component.ts will be listed here. providers: [BrowserStorageService] // singleton services of the whole app will be listed here. }) export class CoreModule { };
و CoreModule نیز به AppModule اضافه میشود:
import { CoreModule } from "./core/core.module"; @NgModule({ imports: [ //... CoreModule, //... RouterModule.forRoot(appRoutes) ], //... }) export class AppModule { }
بنابراین یکی دیگر از روشهای به اشتراک گذاری اطلاعات در بین قسمتهای مختلف برنامه، ذخیره سازی آنها در session/local storage و سپس بازیابی آنها بر اساس کلیدهای مشخص آنها است.
مثالی از نحوهی کاربرد BrowserStorageService
برای آزمایش سرویس تهیه شده، از کامپوننت و قالب ذیل استفاده خواهیم کرد. در اینجا سرویس BrowserStorageService به سازندهی کلاس تزریق شدهاست و سپس دو حالت session storage و local storage مورد بررسی قرار گرفتهاند:
import { BrowserStorageService } from "./../../core/browser-storage.service"; import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-browser-storage-sample-test", templateUrl: "./browser-storage-sample-test.component.html", styleUrls: ["./browser-storage-sample-test.component.css"] }) export class BrowserStorageSampleTestComponent implements OnInit { fromSessionStorage = ""; fromLocalStorage = "" sessionStorageKey = "sessionStorageKey1"; localStorageKey = "localStorageKey1" constructor(private browserStorage: BrowserStorageService) { } ngOnInit() { } sessionStorageSetItem() { this.browserStorage.setSession(this.sessionStorageKey, "Val1"); } sessionStorageGetItem() { this.fromSessionStorage = this.browserStorage.getSession(this.sessionStorageKey); } localStorageSetItem() { this.browserStorage.setLocal(this.localStorageKey, { key1: "val1", key2: 2 }); } localStorageGetItem() { this.fromLocalStorage = JSON.stringify(this.browserStorage.getLocal(this.localStorageKey)); } }
<h1>Browser storage sample</h1> <div class="panel"> <button class="btn btn-primary" (click)="sessionStorageSetItem()" type="button">sessionStorage -> Set Item</button> <button class="btn btn-success" (click)="sessionStorageGetItem()" type="button">sessionStorage -> Get Item</button> <div class="alert alert-info" *ngIf="fromSessionStorage"> {{fromSessionStorage}} </div> </div> <div class="panel"> <button class="btn btn-warning" (click)="localStorageSetItem()" type="button">localStorage -> Set Item</button> <button class="btn btn-success" (click)="localStorageGetItem()" type="button">localStorage -> Get Item</button> <div class="alert alert-info" *ngIf="fromLocalStorage"> {{fromLocalStorage}} </div> </div>
در این حالت اگر برنامه را اجرا کنیم، یک چنین خروجی قابل مشاهده خواهد بود:
و اگر به برگهی Application کنسول ابزارهای توسعه دهندههای مرورگرها نیز مراجعه کنیم، این مقادیر ثبت شده را در دو حالت استفادهی از session storage و local storage، میتوان مشاهده کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
Blazor 5x - قسمت سوم - مبانی Razor
<text x="50" y="50">Some text</text>
"<text>" and "</text>" tags cannot contain attributes
<text> <!-- Here are your actual Text tags --> <text x="50" y="50">Some text</text> </text>
public class AccountType : ObjectGraphType<Account> { public AccountType() { Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the account object."); Field(x => x.Description).Description("Description property from the account object."); Field(x => x.OwnerId, type: typeof(IdGraphType)).Description("OwnerId property from the account object."); } }
public interface IAccountRepository { IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId); } public class AccountRepository : IAccountRepository { private readonly ApplicationContext _context; public AccountRepository(ApplicationContext context) { _context = context; } public IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId) => _context.Accounts .Where(a => a.OwnerId.Equals(ownerId)) .ToList(); }
public class OwnerType : ObjectGraphType<Owner> { public OwnerType(IAccountRepository repository) { Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object."); Field(x => x.Name).Description("Name property from the owner object."); Field(x => x.Address).Description("Address property from the owner object."); Field<ListGraphType<AccountType>>( "accounts", resolve: context => repository.GetAllAccountsPerOwner(context.Source.Id) ); } }
https://localhost:5001/ui/playground
{ owners{ id, name, address, accounts{ id, description, ownerId } } }
public class AccountTypeEnumType : EnumerationGraphType<TypeOfAccount> { public AccountTypeEnumType() { Name = "Type"; Description = "Enumeration for the account type object."; } }
public class AccountType : ObjectGraphType<Account> { public AccountType() { ... Field<AccountTypeEnumType>("Type", "Enumeration for the account type object."); } }
{ owners{ id, name, address, accounts{ id, description, type, ownerId } } }
public interface IAccountRepository { ... Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds); }
public class AccountRepository : IAccountRepository { ... public async Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds) { var accounts = await _context.Accounts.Where(a => ownerIds.Contains(a.OwnerId)).ToListAsync(); return accounts.ToLookup(x => x.OwnerId); } }
public class OwnerType : ObjectGraphType<Owner> { public OwnerType(IAccountRepository repository, IDataLoaderContextAccessor dataLoader) { ... Field<ListGraphType<AccountType>>( "accounts", resolve: context => { var loader = dataLoader.Context.GetOrAddCollectionBatchLoader<Guid, Account>("GetAccountsByOwnerIds", repository.GetAccountsByOwnerIds); return loader.LoadAsync(context.Source.Id); }); } }
services.AddGraphQL(o => { o.ExposeExceptions = false; }) .AddGraphTypes(ServiceLifetime.Scoped) .AddDataLoader();
public interface IOwnerRepository { ... Owner GetById(Guid id); } public class OwnerRepository : IOwnerRepository { ... Owner GetById(Guid id) => _context.Owners.SingleOrDefault(o => o.Id.Equals(id)); }
public class AppQuery : ObjectGraphType { public AppQuery(IOwnerRepository repository) { ... Field<OwnerType>( "owner", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }), resolve: context => { var id = context.GetArgument<Guid>("ownerId"); return repository.GetById(id); } ); } }
Field<OwnerType>( "owner", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }), resolve: context => { Guid id; if (!Guid.TryParse(context.GetArgument<string>("ownerId"), out id)) { context.Errors.Add(new ExecutionError("Wrong value for guid")); return null; } return repository.GetById(id); } );
{ owner(ownerId:"6f513773-be46-4001-8adc-2e7f17d52d83"){ id, name, address, accounts{ id, description, type, ownerId } }
string name = context.GetArgument<string>("name");
{ owner(ownerId:"53270061-3ba1-4aa6-b937-1f6bc57d04d2", name:"ANDY") { ... } }
{ first:owners{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } }, second:owners{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } } }
fragment SampleName on Type{ ... }
{ first:owners{ ...ownerFields }, second:owners{ ...ownerFields }, ... } fragment ownerFields on OwnerType{ ownerId:id, ownerName:name, ownerAddress:address, ownerAccounts:accounts { accountId:id, accountDescription:description, accountType:type } }
query OwnerQuery($ownerId:ID!) { owner(ownerId:$ownerId){ id, name, address, accounts{ id, description, type } } }
{ "ownerId":"6f513773-be46-4001-8adc-2e7f17d52d83" }
public class Customer { public int Id { get; set; } public string Name { get; set; } = null!; public CustomerType Type { get; set; } } public enum CustomerType { Individual, Institution, }
void ManyQueriesManyCalls() { using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<CustomerContext>(); var baseQuery = context.Customers.Select(customer => new { customer.Name, customer.Type, customer.Id, }); var total = baseQuery.Count(); var types = baseQuery.GroupBy(x => x.Type) .Select(x => x.Key).ToList(); var pageSize = 10; var pageIndex = 0; var results = baseQuery .OrderBy(x => x.Id) .Skip(pageSize * pageIndex) .Take(pageSize) .ToList(); Console.WriteLine($"Total:{total}, First Type: {types.First()}, First Item: {results.First().Name}"); }
SELECT COUNT(*) FROM [Customers] AS [c] SELECT [c].[Type] FROM [Customers] AS [c] GROUP BY [c].[Type] SELECT [c].[Name], [c].[Type], [c].[Id] FROM [Customers] AS [c] ORDER BY [c].[Id] OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
اگر بخواهیم این سه کوئری را یکبار به سمت بانک اطلاعاتی ارسال کنیم، میتوان از همان ترفند گروه بندی مطرح شدهی در این مثال برای ترکیب کوئریها استفاده کرد:
void ManyQueriesOnCall() { using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<CustomerContext>(); var baseQuery = context.Customers.Select(customer => new { customer.Name, customer.Type, customer.Id, }); var pageSize = 10; var pageIndex = 0; var allTogether = baseQuery .GroupBy(x => 1) .Select(bq => new { Total = baseQuery.Count(), Types = baseQuery.GroupBy(x => x.Type) .Select(x => x.Key) .ToList(), Results = baseQuery .OrderBy(x => x.Id) .Skip(pageSize * pageIndex) .Take(pageSize) .ToList(), }) .FirstOrDefault(); Console.WriteLine($"Total:{allTogether.Total}, First Type: {allTogether.Types.First()}, First Item: {allTogether.Results.First().Name}"); }
SELECT [t0].[Key], [t1].[Type], [t2].[Name], [t2].[Type], [t2].[Id] FROM ( SELECT TOP(1) [t].[Key] FROM ( SELECT 1 AS [Key] FROM [Customers] AS [c] ) AS [t] GROUP BY [t].[Key] ) AS [t0] OUTER APPLY ( SELECT [c0].[Type] FROM [Customers] AS [c0] GROUP BY [c0].[Type] ) AS [t1] OUTER APPLY ( SELECT [c1].[Name], [c1].[Type], [c1].[Id] FROM [Customers] AS [c1] ORDER BY [c1].[Id] OFFSET @__p_1 ROWS FETCH NEXT @__pageSize_2 ROWS ONLY ) AS [t2] ORDER BY [t0].[Key], [t1].[Type], [t2].[Id]
کدهای این مثال را از اینجا میتوانید دریافت کنید: EF7ManyQueriesOneCall.zip