بازگردانی پایگاه داده بدون فایل لاگ
USE [master] GO -- Method 1: I use this method EXEC sp_attach_single_file_db @dbname='TestDb', @physname=N'C:\Program Files\Microsoft SQL Server\MSSQL10.MSSQLSERVER\MSSQL\DATA\TestDb.mdf' GO
The Intersection of Microservices, Domain-Driven Design and Entity Framework Core
Domain-Driven Design (DDD) provides much of the strategic design guidance that we can use to determine the boundaries around and interactions between Microservices in our solutions. DDD also follows up with tactical design patterns for your business logic. In this session we'll take a look at some of these patterns and how EF Core naturally, or with some additional configuration, persists the data that your microservices depend on.
We are proud to present the first beta release of Kendo UI for Angular 2. It’s been designed specifically for Angular 2. Written in Typescript, built as native Query-free components and distributed as NPM packages, Kendo UI for Angular 2 makes integrating UI components into ng2 a piece of cake for developers. In this beta release, you’ll find the business application essential building blocks — form elements, grid and data visualization components.
یکی از مهمترین بخش هایی که یک Farm Admin شیرپوینت باید به آن توجه کند ، تهیه پشتیبانهای مناسب از شیرپوینت است . این امر در باب نگهداری از Farm بسیار مهم بوده و در صورتی که شما یک Farm Administrator هستید و به این امر توجه لازم را ندارید ، پیشنهاد میکنم این پست را مطالعه کنید .
واژه مناسب بودن Backup ، شامل مسایلی چون محدوده و دامنه ای است که از آن پشتیبان تهیه میشود (یک Site Collection یا Application یا تمام Farm و ... ) ونیز قابل استفاده بودن آن فایل پشتیان (چرا که گرفتن یک پشتیبان به این معنی نیست شما میتوانید صد درصد آن را Restore نمایید پس باید حتما آزمایش شود ) . همچنین شامل این نکته نیز میشود که با Restore کردن این پشتیبان چه مقدار از اطلاعات شما از بین رفته و قابل بازگشت نمیباشد (منظور بازه زمانی است که شما به صورت دوره ای پشتیبان گیری میکنید )
با این مقدمه به جزییات بیشتری میپردازم :
از امروز قابلیتهای اجتماعی گوگل ریدر با گوگل پلاس یکی شده و به همین جهت یک سری از امکانات قدیمی آن حذف گردیدهاند؛ مانند به اشتراک گذاری و لایک زدن و غیره و تمام اینها با دکمهی به علاوه یک گوگل پلاس جایگزین شدهاند. اینبار میشود علاقمندیها را از گوگل ریدر به حلقههای گوگل پلاس هدایت کرد. همهی اینها خوب؛ اما سیستم به اشتراک گذاریهای روزانهی من رو به هم ریخته این کارها! قبلا از حاصل اشتراکها در گوگل ریدر، یک فید تهیه میشد که الان دیگر وجود خارجی ندارد. هیچکدام از حلقههای گوگل پلاس هم فید ندارند. به این ترتیب این محصول، تبدیل به یک فیدخوان معمولی شده است. مثل 100ها برنامهی مشابه دیگر.
اما ... در ادامه ببینیم که چطور میتوان از گوگل ریدر جدید، راهی به خارج باز کرد!
الف) زمانیکه در محیط گوگل ریدر، بر روی دکمه به علاوه یک گوگل پلاس قرار گرفته در ذیل یک مطلب کلیک میکنید، میتوان نام حلقهای از حلقههای گوگل پلاس را انتخاب کرد تا مطلب ما به آن ارسال شود. یکی از این حلقهها، Public نام دارد. بنابراین اینبار اگر قصد به اشتراک گذاری عمومی مطلبی را داشتید مطابق شکل زیر، حلقه Public را وارد نمائید و جالب اینجا است که این حلقه فقط در گوگل ریدر قابل انتخاب است؛ نه در خود گوگل پلاس.
ب) اکنون به اکانت گوگل پلاس خود مراجعه کنید. در قسمت پروفایل، به لینک بالای صفحه دقت نمائید. چیزی است شبیه به:
https://plus.google.com/u/0/userId/posts
این userId برای ادامه کار مهم است.
ج) از این userId برای فراخوانی لینک زیر استفاده خواهیم کرد (userId آن باید جایگزین شود):
https://plus.google.com/_/stream/getactivities/?&sp=[1,2,"userId",null,null,null,null,"social.google.com",[]]
خروجی آن طبق معمول متداول گوگل، فرمت JSON است و میتوان از آن لیست اشتراکهای عمومی را استخراج کرد. برای نمونه کلاس GooglePlusJsonParser تهیه شده زیر میتواند این عملیات را انجام دهد:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
//needs a ref. to System.Web.Extensions asm.
using System.Web.Script.Serialization;
namespace GooglePlus
{
[DebuggerDisplay("{DateTime}-{Title}-{Comment}-{Url}")]
public class GooglePlusItem
{
public DateTime DateTime { set; get; }
public string Url { set; get; }
public string Title { set; get; }
public string Comment { set; get; }
}
public static class Utils
{
public static DateTime ConvertFromUnixTimestamp(this long data)
{
return new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(data);
}
public static string ToSafeString(this object data)
{
return data == null ? string.Empty : data.ToString();
}
}
public class GooglePlusJsonParser
{
public static IList<GooglePlusItem> GetMyPublicItemsList(string userId)
{
var url = string.Format("https://plus.google.com/_/stream/getactivities/?&sp=[1,2,\"{0}\",null,null,null,null,\"social.google.com\",[]]", userId);
var jsonData = new WebClient().DownloadString(url);
return ParsePublicData(jsonData);
}
public static IList<GooglePlusItem> ParsePublicData(string jsonData)
{
jsonData = removeRedundantData(jsonData);
var posts = parseJson(jsonData);
return createItemsList(posts);
}
private static IList<GooglePlusItem> createItemsList(object[] posts)
{
var result = new List<GooglePlusItem>();
foreach (object[] post in posts)
{
var items = (object[])((object[])post[66])[0];
result.Add(new GooglePlusItem
{
DateTime = (((Int64)post[5]) / 1000).ConvertFromUnixTimestamp(),
Title = items[3].ToSafeString(),
Url = items[1].ToSafeString(),
Comment = post[4].ToSafeString()
});
}
return result;
}
private static object[] parseJson(string jsonData)
{
var jsonObject = new JavaScriptSerializer().Deserialize<object[]>(jsonData);
var posts = (object[])(((object[])jsonObject[1])[0]);
return posts;
}
private static string removeRedundantData(string jsonData)
{
return jsonData.Replace(")]}'", string.Empty)
.Replace("[,", "[\"\",")
.Replace(",,", ",\"\",")
.Replace(",,", ",\"\",");
}
}
}
نحوه استفاده از آن هم میتواند فراخوانی متد GooglePlusJsonParser.GetMyPublicItemsList به همراه userId یاده شده باشد.
ایده اصلی از:
Getting the Google+ Feed for any profile in JSON
آپلود فایل توسط فرمهای پویای jqGrid
function doUpload(response, postdata, rowId) { var result = $.parseJSON(response.responseText); if (result.id < 1) return [false, "عملیات ثبت موفقیت آمیز نبود", result.id]; var fileElementId = 'xThumbnail'; if (rowId) { fileElementId = rowId + "_" + fileElementId; } var val = $("#" + fileElementId).val(); if (val == '' || val === undefined) { return [false, "لطفا فایلی را انتخاب کنید", result.id]; } alert(fileElementId); $('#grid1').block({ message: '<h4>در حال ارسال فایل به سرور</h4>' }); $.ajaxFileUpload({ url: "@Url.Action("UploadFile", "ProductTypes", new { area = "Admin" })", secureuri: false, fileElementId: fileElementId, dataType: 'json', data: { id: result.id }, complete: function () { $('#grid1').unblock(); }, success: function (data, status) { $("#grid").trigger("reloadGrid"); }, error: function (data, status, e) { alert(e); } }); return [true, "با تشکر!", result.id]; }
تهیه سرویس اطلاعات پویای برنامه
سرویس Web API ارائه شدهی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشتهای آنها به سمت کلاینت باز میگرداند و ساختار موجودیتهای آنها به صورت زیر است:
موجودیت کاربر که یک رابطهی one-to-many را با UserNotes دارد:
using System; using System.Collections.Generic; namespace MaterialAspNetCoreBackend.DomainClasses { public class User { public User() { UserNotes = new HashSet<UserNote>(); } public int Id { set; get; } public DateTimeOffset BirthDate { set; get; } public string Name { set; get; } public string Avatar { set; get; } public string Bio { set; get; } public ICollection<UserNote> UserNotes { set; get; } } }
using System; namespace MaterialAspNetCoreBackend.DomainClasses { public class UserNote { public int Id { set; get; } public DateTimeOffset Date { set; get; } public string Title { set; get; } public User User { set; get; } public int UserId { set; get; } } }
public interface IUsersService { Task<List<User>> GetAllUsersIncludeNotesAsync(); Task<User> GetUserIncludeNotesAsync(int id); }
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class UsersController : Controller { private readonly IUsersService _usersService; public UsersController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpGet] public async Task<IActionResult> Get() { return Ok(await _usersService.GetAllUsersIncludeNotesAsync()); } [HttpGet("{id:int}")] public async Task<IActionResult> Get(int id) { return Ok(await _usersService.GetUserIncludeNotesAsync(id)); } } }
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
https://localhost:5001/api/users
و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت میدهد.
تنظیم محل تولید خروجی Angular CLI
ساختار پوشه بندی پروژهی جاری به صورت زیر است:
همانطور که ملاحظه میکنید، کلاینت Angular در یک پوشهاست و برنامهی سمت سرور ASP.NET Core در پوشهای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشهی wwwroot پروژهی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش میکنیم:
"build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
ng build --no-delete-output-path --watch
dotnet watch run
بنابراین دو صفحهی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشهی MaterialAngularClient) و در دومی dotnet watch run را در پوشهی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا میشوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژهها ایجاد کردید، صرفا همان قسمت کامپایل میشود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.
تعریف معادلهای کلاسهای موجودیتهای سمت سرور، در برنامهی Angular
در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادلهای کلاسهای موجودیتهای سمت سرور خود را به صورت اینترفیسهایی تایپاسکریپتی تعریف کنیم:
ng g i contact-manager/models/user ng g i contact-manager/models/user-note
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote { id: number; title: string; date: Date; userId: number; }
import { UserNote } from "./user-note"; export interface User { id: number; birthDate: Date; name: string; avatar: string; bio: string; userNotes: UserNote[]; }
ایجاد سرویس Angular دریافت اطلاعات از سرور
ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد میکنیم:
ng g s contact-manager/services/user --no-spec
import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root" }) export class UserService { constructor() { } }
کدهای تکمیل شدهی UserService را در ذیل مشاهده میکنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { User } from "../models/user"; @Injectable({ providedIn: "root" }) export class UserService { constructor(private http: HttpClient) { } getAllUsersIncludeNotes(): Observable<User[]> { return this.http .get<User[]>("/api/users").pipe( map(response => response || []), catchError((error: HttpErrorResponse) => throwError(error)) ); } getUserIncludeNotes(id: number): Observable<User> { return this.http .get<User>(`/api/users/${id}`).pipe( map(response => response || {} as User), catchError((error: HttpErrorResponse) => throwError(error)) ); } }
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشتهای آنها از سرور واکشی میکند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشتهای او از سرور دریافت میکند.
بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material
بستهی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوهی «افزودن آیکنهای متریال به برنامه» را بررسی کردیم که در آنجا آیکنهای مرتبط، از فایلهای قلم، دریافت و نمایش داده میشوند. این کامپوننت، علاوه بر قلم آیکنها، از فایلهای svg حاوی آیکنها نیز پشتیبانی میکند که یک نمونه از این فایلها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شدهاست (چون برنامهی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <svg viewBox="0 0 128 128" height="100%" width="100%" pointer-events="none" display="block" id="user1" >
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماسها را با صورت زیر تکمیل میکنیم:
import { Component } from "@angular/core"; import { MatIconRegistry } from "@angular/material"; import { DomSanitizer } from "@angular/platform-browser"; @Component() export class ContactManagerAppComponent { constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) { iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg")); } }
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon( "unicorn", this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg") );
<mat-icon svgIcon="unicorn"></mat-icon>
یک نکته: پوشهی node_modules\material-design-icons به همراه تعداد قابل ملاحظهای فایل svg نیز هست.
نمایش لیست کاربران در sidenav
در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق میکنیم:
import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component() export class SidenavComponent implements OnInit { users: User[] = []; constructor(private userService: UserService) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => this.users = data); } }
اکنون میخواهیم از این اطلاعات جهت نمایش پویای آنها در sidenav استفاده کنیم. در قسمت قبل، جای آنها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
<mat-list> <mat-list-item>Item 1</mat-list-item> <mat-list-item>Item 2</mat-list-item> <mat-list-item>Item 3</mat-list-item> </mat-list>
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine href="#"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آنها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.
اتصال کاربران به صفحهی نمایش جزئیات آنها
در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شدهاست. در ادامه میخواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine [routerLink]="['/contactmanager', user.id]"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: ":id", component: MainContentComponent }, { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ];
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کردهایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل میکنیم:
import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component({ selector: "app-main-content", templateUrl: "./main-content.component.html", styleUrls: ["./main-content.component.css"] }) export class MainContentComponent implements OnInit { user: User; constructor(private route: ActivatedRoute, private userService: UserService) { } ngOnInit() { this.route.params.subscribe(params => { this.user = null; const id = params["id"]; if (!id) { return; } this.userService.getUserIncludeNotes(id) .subscribe(data => this.user = data); }); } }
اکنون میتوان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user"> <mat-spinner></mat-spinner> </div> <div *ngIf="user"> <mat-card> <mat-card-header> <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon> <mat-card-title> <h2>{{ user.name }}</h2> </mat-card-title> <mat-card-subtitle> Birthday {{ user.birthDate | date:'d LLLL' }} </mat-card-subtitle> </mat-card-header> <mat-card-content> <mat-tab-group> <mat-tab label="Bio"> <p> {{user.bio}} </p> </mat-tab> <!-- <mat-tab label="Notes"></mat-tab> --> </mat-tab-group> </mat-card-content> </mat-card> </div>
همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کردهایم (اگر دقت کنید، هر کامپوننت آن سه برگهی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره میتوان با کامپوننتهای این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا میکنیم که مناسب کار ما باشد. سپس سورس آنرا از همانجا کپی و در برنامه قرار میدهیم و در آخر آنرا بر اساس اطلاعات خود سفارشی سازی میکنیم.
نمایش جزئیات اولین کاربر در حین بارگذاری اولیهی برنامه
تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماسها ظاهر میشود و همانطور باقی میماند، با اینکه هنوز موردی انتخاب نشدهاست. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظهی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش میدهیم:
import { Router } from "@angular/router"; @Component() export class SidenavComponent implements OnInit, OnDestroy { users: User[] = []; constructor(private userService: UserService, private router: Router) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => { this.users = data; if (data && data.length > 0 && !this.router.navigated) { this.router.navigate(["/contactmanager", data[0].id]); } }); } }
البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id']; if (!id) id = 1;
بستن خودکار sidenav در حالت نمایش موبایل
اگر اندازهی صفحهی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتمهای آنرا انتخاب کنیم، هرچند آنها نمایش داده میشوند، اما زیر این sidenav مخفی باقی خواهند ماند:
بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینههای آن، این قسمت بسته شده و ناحیهی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شدهاست:
<mat-sidenav #sidenav
import { MatSidenav } from "@angular/material"; @Component() export class SidenavComponent implements OnInit, OnDestroy { @ViewChild(MatSidenav) sidenav: MatSidenav;
ngOnInit() { this.router.events.subscribe(() => { if (this.isScreenSmall) { this.sidenav.close(); } }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
namespace EntitySample1.DomainClasses { public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public virtual PersonInfo PersonInfo { get; set; } public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; } public virtual ICollection<Address> Addresses { get; set; } } }
namespace EntitySample1.DomainClasses { public class PersonInfo { public int Id { get; set; } public string Note { get; set; } public string Major { get; set; } } }
namespace EntitySample1.DomainClasses { public enum PhoneType { Home, Mobile, Work } public class PhoneNumber { public int Id { get; set; } public string Number { get; set; } public PhoneType PhoneType { get; set; } public virtual Person Person { get; set; } } }
namespace EntitySample1.DomainClasses { public class Address { public int Id { get; set; } public string City { get; set; } public string Street { get; set; } public virtual ICollection<Person> Persons { get; set; } } }
namespace EntitySample1.DataLayer { public class PhoneBookDbContext : DbContext { public DbSet<Person> Persons { get; set; } public DbSet<PhoneNumber> PhoneNumbers { get; set; } public DbSet<Address> Addresses { get; set; } } }
استفاده از ردیابی تغییر عکس فوری
ردیابی تغییر عکس فوری، وابسته به این است که EF بفهمد، چه زمانی تغییرات رخ داده است. رفتار پیش فرض DbContext API ، این هست که به صورت خودکار بازرسی لازم را در نتیجهی رخدادهای DbContext انجام دهد. DetectChanges تنها اطلاعات مدیریت حالت context، که وظیفهی انعکاس تغییرات صورت گرفته به پایگاه داده را دارد، به روز نمیکند، بلکه اصلاح رابطه(ralationship) ترکیبی از خواص راهبری مرجع ، مجموعه ای و کلیدهای خارجی را انجام میدهد. این خیلی مهم خواهد بود که درک روشنی داشته باشیم از این که چگونه و چه زمانی تغییرات تشخیص داده میشوند،چه چیزی باید از آن انتظار داشته باشیم و چگونه کنترلش کنیم.
چه زمانی تشخیص خودکار تغییرات اجرا میشود؟
متد DetectChanges کلاس ObjectContext، از EF نسخهی 4 به عنوان بخشی از الگوی ردیابی تغییر عکس فوری اشیای POCO ،در دسترس بوده است. تفاوتی که در مورد DataContext.ChangeTracker.DetectChanges( در حقیقت ObjectContext.DetectChanges فراخوانی میشود) وجود دارد این است که، رویدادهای خیلی بیشتری وجود دارند که به صورت خودکار DetectChanges را فراخوانی میکنند.
لیستی از متدهایی که باعث انجام عمل تشخیص تغییرات (DetectChanges)، میشوند را در ادامه مشاهده میکنید:
• DbSet.Add
• DbSet.Find
• DbSet.Remove
• DbSet.Local
• DbSet.SaveChanges
• فراخوانی Linq Query از DbSet
• DbSet.Attach
• DbContext.GetValidationErrors
• DbContext.Entry
• DbChangeTracker.Entries
کنترل زمان فراخوانی DetectChanges
بیشترین زمانی که EF احتیاج به فهمیدن تغییرات دارد، در زمان SaveChanges است، اما حالتهای زیاد دیگری نیز هست. برای مثال، اگر ما از ردیاب تغییرات، درخواست وضعیت فعلی یک شی را بکنیم،EF احتیاج به اسکن کردن و بررسی تغییرات رخ داده را دارد. همچنین وضعیتی را در نظر بگیرید که شما از پایگاه داده یک شماره تلفن را واکشی میکنید و سپس آن را به مجموعه شماره تلفنهای یک شخص جدید اضافه میکنید.آن شماره تلفن اکنون تغییر کرده است، چرا که انتساب آن به یک شخص جدید،خاصیت PersonId آن را تغییر داده است. ولی EF برای اینکه بفهمد تغییر رخ داده است(یا حتی نداده است) ، احتیاج به اسکن کردن همهی اشیا Person دارد.
بیشتر عملیاتی که بر روی DbContext API انجام میدهید، موجب فراخوانی DetectChanges میشود. در بیشتر موارد DetectChanges به اندازه کافی سریع هست تا باعث ایجاد مشکل کارایی نشود. با این حال ممکن است ، شما تعداد خیلی زیادی اشیا در حافظه داشته باشید، و یا تعداد زیادی عملیات در DbContext ، در مدت خیلی کوتاهی انجام دهید، رفتار تشخیص خودکار تغییرات ممکن است، باعث نگرانیهای کارایی شود. خوشبختانه گزینه ای برای خاموش کردن رفتار تشخیص خودکار تغییرات وجود دارد و هر زمانی که میدانید لازم است، میتوانید آن را به صورت دستی فراخوانی کنید.
EF بر مبنای این فرض ساخته شده است که شما ، در صورتی که در فراخوانی آخرین API، موجودیتی تغییر پیدا کرده است، قبل از فراخوانی API جدید، باید DetectChanges صدا زده شود. این شامل فراخوانی DetectChanges، قبل از اجرای هر query نیز میشود.اگر این عمل ناموفق یا نابجا انجام شود،ممکن است عواقب غیر منتظره ای در بر داشته باشد. DbContext انجام این وظیفه را بر عهده گرفته است و به همین دلیل به طور پیش فرض تشخیص تغییرات خودکار آن فعال است.
نکته: تشخیص اینکه چه زمانی احتیاج به فراخوانی DetectChanges است،آن طور که ساده و بدیهی به نظر میآید نیست. تیم EF شدیدا توصیه کرده اند که فقط، وقتی با مشکلات عدم کارایی روبرو شدید، تشخیص تغییرات را به حالت دستی در بیاورید.همچنین توصیه شده که در چنین مواقعی، تشخیص خودکار تغییرات را فقط برای قسمتی از کد که با کارایی پایین مواجه شدید خاموش کنید و پس از اینکه اجرای آن قسمت از کد تمام شد،دوباره آن را روشن کنید.
برای خاموش یا روشن کردن تشخیص خودکار تغییرات، باید متغیر بولین DbContext.Configuration.AutoDetectChangesEnabled را تنظیم کنید.
در مثال زیر، ما در متد ManualDetectChanges، تشخیص خودکار تغییرات را خاموش کرده ایم و تاثیرات آن را بررسی کرده ایم.
private static void ManualDetectChanges() { using (var context = new PhoneBookDbContext()) { context.Configuration.AutoDetectChangesEnabled = false; // turn off Auto Detect Changes var p1 = context.Persons.Single(p => p.FirstName == "joe"); p1.LastName = "Brown"; Console.WriteLine("Before DetectChanges: {0}", context.Entry(p1).State); context.ChangeTracker.DetectChanges(); // call detect changes manually Console.WriteLine("After DetectChanges: {0}", context.Entry(p1).State); } }
در کدهای بالا ابتدا تشخیص خودکار تغییرات را خاموش کرده ایم و سپس یک شخص با نام joe را از دیتابیس فراخواندیم و سپس نام خانوادگی آن را به Brown تغییر دادیم. سپس در خط بعد، وضعیت فعلی موجودیت p1 را از context جاری پرسیدیم. در خط بعدی، DetectChanges را به صورت دستی صدا زده ایم و دوباره همان پروسه را برای به دست آوردن وضیعت شی p1، انجام داده ایم. همان طور که میبینید ، برای به دست آوردن وضعیت فعلی شی مورد نظر از متد Entry متعلق به ChangeTracker API استفاده میکنیم، که در آینده مفصل در مورد آن بحث خواهد شد. اگر شما متد Main را با صدا زدن ManualDetectChanges ویرایش کنید ، خروجی زیر را مشاهده خواهید کرد:
Before DetectChanges: Unchanged After DetectChanges: Modified
همان طور که انتظار میرفت، به دلیل خاموش کردن تشخیص خودکار تغییرات، context قادر به تشخیص تغییرات صورت گرفته در شی p1 نیست، تا زمانی که متد DetectChanges را به صورت دستی صدا بزنیم. دلیل این که در دفعه اول، ما نتیجهی غلطی مشاهده میکنیم، این است که ما قانون را نقض کرده ایم و قبل از صدا زدن هر API ، متد DetectChanges را صدا نزده ایم. خوشبختانه چون ما در اینجا وضعیت یک شی را بررسی کردیم، با عوارض جانبی آن روبرو نشدیم.
نکته: به این نکته توجه داشته باشید که متد Entry به صورت خودکار، DetectChanges را فراخوانی میکند. برای اینکه دانسته بخواهیم این رفتار را غیر فعال کنیم، باید AutoDetectChangesEnabled را غیر فعال کنیم.
در مثال فوق ،خاموش کردن تشخیص خودکار تغییرات، برای ما مزیتی به همراه نداشت و حتی ممکن بود برای ما دردسر ساز شود. ولی حالتی را در نظر بگیرید که ما یک سری API را فراخوانی میکنیم ،بدون این که در این بین ،در حالت اشیا تغییری ایجاد کنیم.در نتیجه میتوانیم از فراخوانیهای بی جهت DetectChanges جلوگیری کنیم.
در متد AddMultiplePersons مثال بعدی، این کار را نشان داده ام:
private static void AddMultiplePerson() { using (var context = new PhoneBookDbContext()) { context.Configuration.AutoDetectChangesEnabled = false; context.Persons.Add(new Person { FirstName = "brad", LastName = "watson", BirthDate = new DateTime(1990, 6, 8) }); context.Persons.Add(new Person { FirstName = "david", LastName = "brown", BirthDate = new DateTime(1990, 6, 8) }); context.Persons.Add(new Person { FirstName = "will", LastName = "smith", BirthDate = new DateTime(1990, 6, 8) }); context.SaveChanges(); } }
استفاده از DetectChanges برای فراخوانی اصلاح رابطه
DetectChanges همچنین مسئولیت انجام اصلاح رابطه ، برای هر رابطه ای که تشخیص دهد تغییر کرده است را دارد.اگر شما بعضی از روابط را تغییر دادید و مایل بودید تا همهی خواص راهبری و خواص کلید خارجی را منطبق کنید، DetectChanges این کار را برای شما انجام میدهد. این قابلیت میتواند برای سناریوهای data-binding که در آن ممکن است در رابط کاربری(UI) یکی از خواص راهبری (یا حتی یک کلید خارجی) تغییر کند، و شما بخواهید که خواص دیگری این رابطه به روز شوند و تغییرات را نشان دهند، مفید واقع شود.
متد DetectRelationshipChanges در مثال زیر از DetectChanges برای انجام اصلاح رابطه استفاده میکند.
private static void DetectRelationshipChanges() { using (var context = new PhoneBookDbContext()) { var phone1 = context.PhoneNumbers.Single(x => x.Number == "09351234567"); var person1 = context.Persons.Single(x => x.FirstName == "will"); person1.PhoneNumbers.Add(phone1); Console.WriteLine("Before DetectChanges: {0}", phone1.Person.FirstName); context.ChangeTracker.DetectChanges(); // ralationships fix-up Console.WriteLine("After DetectChanges: {0}", phone1.Person.FirstName); } }
در اینجا ابتدا ما شماره تلفنی را از دیتابیس لود میکنیم. سپس شخص دیگری را نیز با نام will از دیتابیس میخوانیم. قصد داریم شماره تلفن خوانده شده را به این شخص نسبت دهیم و مجموعه شماره تلفنهای وی اضافه کنیم و ما این کار را با افزودن phone1 به مجموعه شماره تلفنهای person1 انجام داده ایم. چون ما از اشیای POCO استفاده کرده ایم،EF نمیفهمد که ما این تغییر را ایجاد کرده ایم و در نتیجه کلید خارجی PersonId شی phone1 را اصلاح نمیکند. ما میتوانیم تا زمانی صبر کنیم تا متدی مثل SaveChanges، متد DetectChanges را فراخوانی کند،ولی اگر بخواهیم این عمل در همان لحظه انجام شود، میتوانیم DetectChanges را دستی صدا بزنیم.
اگر ما متد Main را با اضافه کردن فرخوانی DetectRealtionShipsChanges تغییر بدهیم و آن را اجرا کنیم، نتیجه زیر را مشاهده میکنید:
Before DetectChanges: david After DetectChanges: will
تا قبل از فراخوانی تشخیص تغییرات(DetectChanegs)، هنوز phone1 منتسب به شخص قدیمی(david) بوده، ولی پس از فراخوانی DetectChanges ، اصلاح رابطه رخ داده و همه چیز با یکدیگر منطبق میشوند.
فعال سازی و کار با پروکسیهای ردیابی تغییر
اگر پروفایلر کارایی شما، فراخوانیهای بیش از اندازه DetectChnages را به عنوان یک مشکل شناسایی کند، و یا شما ترجیح میدهید که اصلاح رابطه به صورت بلادرنگ صورت گیرد ، ردیابی تغییر پروکسیهای پویا، به عنوان گزینه ای دیگر مطرح میشود.فقط با چند تغییر کوچک در کلاسهای POCO، EF قادر به ساخت پروکسیهای پویا خواهد بود.پروکسیهای ردیابی تغییر به EF اجازه ردیابی تغییرات در همان لحظه ای که ما تغییری در اشیای خود میدهیم را میدهند و همچنین امکان انجام اصلاح رابطه را در هر زمانی که تغییرات روابط را تشخیص دهد، دارد.
برای اینکه پروکسی ردیابی تغییر بتواند ساخته شود، باید قوانین زیر رعایت شود:
• کلاس باید public باشد و seald نباشد.
• همهی خواص(properties) باید virtual تعریف شوند.
• همهی خواص باید getter و setter با سطح دسترسی public داشته باشند.
• همهی خواص راهبری مجموعه ای باید نوعشان، از نوع ICollection<T> تعریف شوند.
کلاس Person مثال خود را به گونه ای بازنویسی کرده ایم که تمام قوانین فوق را پیاده سازی کرده باشد.
نکته: توجه داشته باشید که ما دیگر در داخل سازنده کلاس ،کدی نمینویسیم و منطقی که باعث نمونه سازی اولیه خواص راهبری میشدند، را پیاده سازی نمیکنیم. این پروکسی ردیاب تغییر، همهی خواص راهبری مجموعه ای را تحریف کرده و ار نوع مجموعه ای مخصوص خود(EntityCollection<T>) استفاده میکند. این نوع مجموعه ای، هر تغییری که در این مجموعه صورت میگیرد را زیر نظر گرفته و به ردیاب تغییر گزارش میدهد. اگر تلاش کنید تا نوع دیگری مانند List<T> که معمولا در سازنده کلاس از آن استفاده میکردیم را به آن انتساب دهیم، پروکسی، استثنایی را پرتاب میکند.
namespace EntitySample1.DomainClasses { public class Person { public virtual int Id { get; set; } public virtual string FirstName { get; set; } public virtual string LastName { get; set; } public virtual DateTime BirthDate { get; set; } public virtual PersonInfo PersonInfo { get; set; } public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; } public virtual ICollection<Address> Addresses { get; set; } } }
با این که احتیاجات رسیدن به پروکسیهای ردیابی تغییر خیلی ساده هستند، اما سادهتر از آن ها، فراموش کردن یکی از آن هاست.حتی از این هم سادهتر میشود که در آینده تغییری در آن کلاسها ایجاد کنید و ناخواسته یکی از آن قوانین را نقض کنید.به این خاطر، فکر خوبیست که یک آزمون واحد نیز اضافه کنیم تا مطمئن شویم که EF توانسته، پروکسی ردیابی تغییر را ایجاد کند یا نه.
در مثال زیر یک متد نوشته شده که این مورد را مورد آزمایش قرار میدهد. همچنین فراموش نکنید که فضای نام System.Data.Object.DataClasses را به usingهای خود اضافه کنید.
private static void TestForChangeTrackingProxy() { using (var context = new PhoneBookDbContext()) { var person = context.Persons.First(); var isProxy = person is IEntityWithChangeTracker; Console.WriteLine("person is a proxy: {0}", isProxy); } }
اکنون متد ManualDetectChanges را که کمی بالاتر بررسی کرده ایم را در نظر بگیرید و کد context.ChangeTracker.DetectChanges آن را حذف کنید و بار دیگر آن را فرا بخوانید و نتیجه را مشاهده کنید:
Before DetectChanges: Modified After DetectChanges: Modified
اکنون متد DetectRelationshipChanges را ویرایش کرده و برنامه را اجرا کنید:
Before DetectChanges: will After DetectChanges: will
نکته: زمانی که شما از پروکسیهای ردیابی تغییر استفاده میکنید،احتیاجی به غیرفعال کردن تشخیص خودکار تغییرات نیست. DetectChanges برای همه اشیایی که تغییرات را به صورت بلادرنگ گزارش میدهند،فرآیند تشخیص تغییرات را انجام نمیدهد. بنابراین فعال سازی پروکسیهای ردیابی تغییر،برای رسیدن به مزایای کارایی بالا در هنگام عدم استفاده از DetectChanges کافی است. در حقیقت زمانی که EF، یک پروکسی ردیابی پیدا میکند، از مقادیر خاصیت ها، عکس فوری نمیگیرد. همچنین DetectChanges این را نیز میداند که نباید تغییرات موجودیت هایی که عکسی از مقادیر اصلی آنها ندارد را اسکن کند.
تذکر: اگر شما موجودیت هایی داشته باشید که شامل انواع پیچیده(Complex Types) میشوند،EF هنوز هم از ردیابی تغییر عکس فوری، برای خواص موجود در نوع پیچیده استفاده میکند، و از این جهت لازم است کهEF، برای نمونهی نوع پیچیده، پروکسی ایجاد نمیکند.شما هنوز هم، تشخیص خودکار تغییرات خواصی که مستقیما درون آن موجودیت(Entity) تعریف شده اند را دارید، ولی تغییرات رخ داده درون نوع پیچیده، فقط از طریق DetectChanges قابل تشخیص است.
چگونگی اطمینان از اینکه نمونههای جدید ، پروکسیها را دریافت خواهند کرد
EF به صورت خودکار برای نتایج حاصل از کوئری هایی که شما اجرا میکنید، پروکسیها را ایجاد میکند. با این حال اگر شما فقط از سازندهی کلاس POCO خود برای ایجاد نمونهی جدید استفاده کنید،دیگر پروکسیها ایجاد نخواهند شد.بدین منظور برای دریافت پروکسی ها، شما باید از متد DbSet.Create برای دریافت نمونههای جدید آن موجودیت استفاده کنید.
نکته: اگر شما، پروکسیهای ردیابی تغییر را برای موجودیتی از مدلتان فعال کرده باشید،هنوز هم میتوانید،نمونههای فاقد پروکسی آن موجودیت را ایجاد و بیافزایید.خوشبختانه EF با موجودیتهای پروکسی و غیر پروکسی در همان مجموعه(set) کار میکند.شما باید آگاه باشید که ردیابی خودکار تغییرات و یا اصلاح رابطه، برای نمونه هایی که پروکسی هایی ردیابی تغییر نیستند، قابل استفاده نیستند.داشتن مخلوطی از نمونههای پروکسی و غیر پروکسی در همان مجموعه، میتواند گیج کننده باشد.بنابر این عموما توصیه میشود که برای ایجاد نمونههای جدید از DbSet.Create استفاده کنید، تا همهی موجودیتهای موجود در مجموعه، پروکسیهای ردیابی تغییر باشند.
متد CreateNewProxies را به برنامهی خود اضافه کرده و آن را اجرا کنید.
private static void CreateNewProxies() { using (var context = new PhoneBookDbContext()) { var phoneNumber = new PhoneNumber { Number = "987" }; var davidPersonProxy = context.Persons.Create(); davidPersonProxy.FirstName = "david"; davidPersonProxy.PhoneNumbers.Add(phoneNumber); Console.WriteLine(phoneNumber.Person.FirstName); } }
خروجی مثال فوق david خواهد بود.همان طور که میبینید با استفاده از context.Persons.Create، نمونهی ساخته شده، دیگر شی POCO نیست، بلکه davidPersonProxy، از جنس پروکسی ردیابی تغییر است و تغییرات آن به طور خودکار ردیابی شده و رابطه آن نیز به صورت خودکار اصلاح میشود.در اینجا نیز با افزودن phoneNumber به شماره تلفنهای davidPersonProxy، به طور خودکار رابطهی بین phoneNumber و davidPersonPeroxy بر قرار شده است. همان طور که میدانید این عملیات بدون استفاده از پروکسیهای ردیابی تغییرات امکان پذیر نیست و موجب بروز خطا میشود.
ایجاد نمونههای پروکسی برای انواع مشتق شده
اورلود جنریک دیگری برای DbSet.Create وجود دارد که برای نمونه سازی کلاسهای مشتق شده در مجموعه ما استفاده میشود .برای مثال، فراخوانی Create بر روی مجموعهی Persons ،نمونه ای از کلاس Person را بر میگرداند.ولی ممکن است کلاس هایی در مجموعهی Persons وجود داشته باشند، که از آن مشتق شده باشند، مانند Student. برای دریافت نمونهی پروکسی Student، از اورلود جنریک Create استفاده میکنیم.
var newStudent = context.Persons.Create<Student>();
تا به این جای کار باید متوجه شده باشید که ردیابی تغییرات، فرآیندی ساده و بدیهی نیست و مقداری سربار در کار است. در بعضی از بخشهای برنامه تان، احتمالا دادهها را به صورت فقط خواندنی در اختیار کاربران قرار میدهید و چون اطلاعات هیچ وقت تغییر نمیکنند، شما میخواهید که سربار ناشی از ردیابی تغییرات را حذف کنید.
خوشبختانه EF شامل متد AsNoTracking است که میتوان از آن برای اجرای کوئریهای بدون ردیابی استفاده کرد.یک کوئری بدون ردیابی، یک کوئری ساده هست که نتایج آن توسط context برای تشخیص تغییرات ردیابی نخواهد شد.
متد PrintPersonsWithoutChangeTracking را به برنامه اضافه کنید و آن را اجرا کنید:
private static void PrintPersonsWithoutChangeTracking() { using (var context = new PhoneBookDbContext()) { var persons = context.Persons.AsNoTracking().ToList(); foreach (var person in persons) { Console.WriteLine(person.FirstName); } } }
نکته: واکشی دادهها بدون ردیابی تغییرات،معمولا وقتی باعث افزایش قابل توجه کارایی میشود که بخواهیم تعداد خیلی زیادی داده را به صورت فقط خواندنی نمایش دهیم. اگر برنامهی شما داده ای را تغییر میدهد و میخواهد آن را ذخیره کند، باید از AsNoTracking استفاده نکنید.
AsNoTracking یک متد الحاقی است، که در <IQueryable<T تعریف شده است، در نتیجه شما میتوانید از آن، در کوئریهای LINQ نیز استفاده کنید. شما میتوانید از AsNoTracking، در انتهای DbSet ،در خط from کوئری استفاده کنید.
var query = from p in context.Persons.AsNoTracking() where p.FirstName == "joe" select p;
شما همچنین از AsNoTracking میتوانید برای تبدیل یک کوئری LINQ موجود، به یک کوئری فاقد ردیابی استفاده کنید. این نکته را به یاد داشته باشید که فقط AsNoTracking بر روی کوئری، فرانخوانده شده است، بلکه متغیر query را با نتیجهی حاصل از فراخوانی AsNoTracking بازنویسی(override) کرده است و این، از این جهت لازم است که AsNoTracking ،تغییری در کوئری ای که بر روی آن فراخوانده شده نمیدهد، بلکه یک کوئری جدید بر میگرداند.
var query = from p in context.Persons where p.FirstName == "joe" select p; query = query.AsNoTracking();
منبع: ترجمه ای آزاد از کتاب Programming Entity Framework: DbContext
ng new apollo-angular-project
ng add apollo-angular
const uri = 'https://localhost:5001/graphql';
npm install --save apollo-angular \ apollo-angular-link-http \ apollo-link \ apollo-client \ apollo-cache-inmemory \ graphql-tag \ graphql
{ "compilerOptions": { // ... "lib": [ "es2017", "dom", "esnext.asynciterable" ] } }
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from "@angular/common/http"; import { ApolloModule, APOLLO_OPTIONS } from "apollo-angular"; import { HttpLinkModule, HttpLink } from "apollo-angular-link-http"; import { InMemoryCache } from "apollo-cache-inmemory"; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, ApolloModule, HttpLinkModule ], providers: [ { provide: APOLLO_OPTIONS, useFactory: (httpLink: HttpLink) => { return { cache: new InMemoryCache(), link: httpLink.create({ uri: "https://localhost:5001/graphql" }) } }, deps: [ HttpLink ] }], bootstrap: [ AppComponent ] }) export class AppModule { }
export type OwnerInputType = { name: string; address: string; }
export type AccountType = { 'id': string; 'description': string; 'ownerId' : string; 'type': string; }
import { AccountType } from './accountType'; export type OwnerType = { 'id': string; 'name': string; 'address': string; 'accounts': AccountType[]; }
ng g s graphql
import { Injectable } from '@angular/core'; import { Apollo } from 'apollo-angular'; import gql from 'graphql-tag'; @Injectable({ providedIn: 'root' }) export class GraphqlService { constructor(private apollo: Apollo) { } }
public getOwners = () => { return this.apollo.query({ query: gql`query getOwners{ owners{ id, name, address, accounts{ id, description, type } } }` }); }
export class AppComponent implements OnInit { public owners: OwnerType[]; public loading = true; constructor(private graphQLService: GraphqlService) { } ngOnInit() { this.graphQLService.getOwners().subscribe(result => { this.owners = result.data["owners"] as OwnerType[]; this.loading = result.loading; }); } }
<div> <div *ngIf="!this.loading"> <table> <thead> <tr> <th> # </th> <th> نام و نام خانوادگی </th> <th> آدرس </th> </tr> </thead> <tbody> <ng-container *ngFor="let item of this.owners;let idx=index" [ngTemplateOutlet]="innertable" [ngTemplateOutletContext]="{item:item, index:idx}"></ng-container> </tbody> </table> </div> <div *ngIf="this.loading"> <p> در حال بارگذاری لیست ... </p> </div> </div> <ng-template #innertable let-item="item" let-idx="index"> <tr> <td>{{idx+1}}</td> <td>{{item.name}}</td> <td>{{item.address}}</td> </tr> <tr *ngIf="this.item.accounts && this.item.accounts.length > 0"> <td colspan="4"> <div> <p>Accounts</p> </div> <div> <table> <thead> <tr> <th>#</th> <th>نوع</th> <th>توضیحات</th> </tr> </thead> <tbody> <tr *ngFor="let innerItem of this.item.accounts;let innerIndex=index"> <td> {{innerIndex+1}} </td> <td> {{innerItem.type}} </td> <td> {{innerItem.description}} </td> </tr> </tbody> </table> </div> </td> </tr> </ng-template>
dotnet restore dotnet run
ng serve
public getOwners = () => { return this.apollo.watchQuery<any>({ query: gql`query getOwners{ owners{ id, name, address, accounts{ id, description, type } } }` }) }
export class AppComponent implements OnInit { loading: boolean; public owners: OwnerType[]; private querySubscription: Subscription; constructor(private graphQLService: GraphqlService) { } ngOnInit() { this.querySubscription = this.graphQLService.getOwners() .valueChanges .subscribe(result => { this.loading = result.loading; this.owners = result.data["owners"] as OwnerType[]; }); } ngOnDestroy() { this.querySubscription.unsubscribe(); } }
public getOwner = (id) => { return this.apollo.query({ query: gql`query getOwner($ownerID: ID!){ owner(ownerId: $ownerID){ id, name, address, accounts{ id, description, type } } }`, variables: { ownerID: id } }) }
public createOwner = (ownerToCreate: OwnerInputType) => { return this.apollo.mutate({ mutation: gql`mutation($owner: ownerInput!){ createOwner(owner: $owner){ id, name, address } }`, variables: { owner: ownerToCreate } }) }
public updateOwner = (ownerToUpdate: OwnerInputType, id: string) => { return this.apollo.mutate({ mutation: gql`mutation($owner: ownerInput!, $ownerId: ID!){ updateOwner(owner: $owner, ownerId: $ownerId){ id, name, address } }`, variables: { owner: ownerToUpdate, ownerId: id } }) }
public deleteOwner = (id: string) => { return this.apollo.mutate({ mutation: gql`mutation($ownerId: ID!){ deleteOwner(ownerId: $ownerId) }`, variables: { ownerId: id } }) }