var row = new string[3]; row[0] = "Liverpool"; row[0] = "15";
public class Performance { public string TeamName { get; set; } public int Score { get; set; } }
var row = new string[3]; row[0] = "Liverpool"; row[0] = "15";
public class Performance { public string TeamName { get; set; } public int Score { get; set; } }
import 'rxjs/add/operator/map';
import { map } from 'rxjs/operators';
import { map, catchError, tap } from 'rxjs/operators';
import { Observable } from 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw'; Observable.throw('error');
import { ErrorObservable } from 'rxjs/observable/ErrorObservable'; ErrorObservable.create('error');
import "rxjs/add/observable/timer"; const timerSource$ = Observable.timer(initialDelay);
import { timer } from 'rxjs/observable/timer'; const timerSource$ = timer(initialDelay);
import { from} from 'rxjs/observable/from'; import { map, scan, filter } from 'rxjs/operators'; const source$ = range(1,10); const sumOfSquaredOddNumbers$ = source$.pipe( filter(n => n % 2 !== 0), map(n => n * n), scan((acc,s) => acc + s, 0) ); sumOfSquaredOddNumbers$.subscribe(v => console.log(v)); /*** Output ****/ 1 10 35 84 165
import { of } from 'rxjs/observable/of'; import { from } from 'rxjs/observable/from'; import { range } from 'rxjs/observable/range'; const source$ = of(1,2,3); const rangeSource$ = range(0,5);
import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/filter'; names = allUserData. map(user => user.name). filter(name => name);
import { Observable } from 'rxjs/Observable'; import { map, filter } from 'rxjs/operators'; names = allUserData.pipe( map(user => user.name), filter(name => name), );
version: '3' networks: shared-network: services: nodeapp1: image: nodeapp1 build: context: nodeapp1 dockerfile: dockerfile ports: - "8181:80" networks: - shared-network nodeapp2: image: nodeapp2 build: context: nodeapp2 dockerfile: dockerfile ports: - "8182:80" networks: - shared-network mongo: image: mongo ports: - "27017:27017" networks: - shared-network
docker-compose build
docker-compose up
docker-compose down
docker-compose stop
docker-compose start
docker-compose run --build
FROM node COPY . /var/www WORKDIR /var/www RUN npm i EXPOSE 80 ENTRYPOINT node index
const express = require('express'); const fetch = require('node-fetch'); const app = express(); app.get('/', async (req, res) => { let response = await fetch("http://nodeapp2/"); text = await response.text(); res.send(text); }) app.listen(80, () => console.log(`listening on port 80!`))
version: '3' services: nodeapp1: environment: - PRODUCTION: 'true' nodeapp2: environment: - PRODUCTION: 'true'
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up
%USERPROFILE%/.omnisharp
{ "RoslynExtensionsOptions": { "LocationPaths": [ "C:\\lib\\roslynator" ] } }
Write-Host "Download, unzip and enable Roslynator for Visual Studio Code" $name = "josefpihrt.Roslynator2017" $url = "https://marketplace.visualstudio.com/items?itemName=$name" $currentDir = $PSScriptRoot $file = "$currentDir\Roslynator.zip" $pattern = "<script class=`"vss-extension`" defer=`"defer`" type=`"application\/json`">(.*?)<\/script>" $regex = [regex]"(?m)$pattern" Write-Host "Grab the home page of the $name." $dom = (New-Object Net.WebClient).DownloadString($url); if($dom -and $dom -match $pattern) { $matches = $regex.Match($dom) $jsonText = $matches[0].Groups[1] $json = ConvertFrom-Json $jsonText $version = $Json.versions[0].version # Parse the json in the page for the latest version number $parts = $name.Split(".") $publisher = $parts[0] $package = $parts[1] # Assemble the url for the vsix package $packageUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/$publisher/vsextensions/$package/$version/vspackage" Write-Host "Download the vsix package: $packageUrl" (New-Object Net.WebClient).DownloadFile($packageUrl, $file) Write-Host "Using $currentDir as the current dir." Write-Host "Unzip $file." $shellApp = new-object -com shell.application $zipFile = $shellApp.namespace($file) $destination = $shellApp.namespace($currentDir) $destination.Copyhere($zipFile.items(), 0x14)# overwrite and be silent Write-Host "Delete VS specific files. Otherwise they will interfere with the MEF services inside OmniSharp." Remove-Item "$currentDir\Roslynator.VisualStudio.Core.dll","$currentDir\Roslynator.VisualStudio.dll", "$currentDir\Roslynator.VisualStudio.pkgdef" $omnisharpJsonFilePath = "$env:USERPROFILE\.omnisharp\omnisharp.json"; Write-Host "Create $omnisharpJsonFilePath file." $omnisharpJson = @" {{ "RoslynExtensionsOptions": {{ "LocationPaths": [ "{0}" ] }} }} "@ -f $currentDir -Replace "\\","\\" $omnisharpJson | Out-File "$omnisharpJsonFilePath" -Confirm Write-Host "Done!" } else { Write-Host "Failed to find the packageUrl!" }
PS D:\Prog\1396\RoslynatorVSCode> .\update.ps1 Download, unzip and enable Roslynator for Visual Studio Code Grab the home page of the josefpihrt.Roslynator2017. Download the vsix package: https://marketplace.visualstudio.com/_apis/public/gallery/publishers/josefpihrt/vsextensions/Roslynator2017/1.7.0/vspackage Using D:\Prog\1396\RoslynatorVSCode as the current dir. Unzip D:\Prog\1396\RoslynatorVSCode\Roslynator.zip. Delete VS specific files. Otherwise they will interfere with the MEF services inside OmniSharp. Create C:\Users\Vahid\.omnisharp\omnisharp.json file. Confirm Are you sure you want to perform this action? Performing the operation "Output to File" on target "C:\Users\Vahid\.omnisharp\omnisharp.json". [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): Done!
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>netcoreapp2.0</TargetFramework><LangVersion>latest</LangVersion></PropertyGroup><ItemGroup><PackageReference Include="System.Memory" Version="4.4.0-preview1-25305-02" /></ItemGroup></Project>
static class NameValidatorUsingString { public static bool IsValidName(string name) { if (name.Length < 3) return false; if (char.IsLetter(name[0])) return true; return false; } }
string fullName = "User 1"; string firstName = fullName.Substring(0, 4); NameValidatorUsingString.IsValidName(firstName);
char[] anotherFullName = { 'A', 'B' };
NameValidatorUsingString.IsValidName(anotherFullName);
NameValidatorUsingString.IsValidName(new string(anotherFullName));
static class NameValidatorUsingSpan { public static bool IsValidName(ReadOnlySpan<char> name) { if (name.Length < 3) return false; if (char.IsLetter(name[0])) return true; return false; } }
ReadOnlySpan<char> fullName = "User 1".AsSpan(); ReadOnlySpan<char> firstName = fullName.Slice(0, 4); NameValidatorUsingSpan.IsValidName(firstName);
char[] anotherFullName = { 'A', 'B' }; NameValidatorUsingSpan.IsValidName(anotherFullName);
var arr = new byte[10]; Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>
Span<byte> slicedBytes = bytes.Slice(start: 5, length: 2); slicedBytes[0] = 42; slicedBytes[1] = 43; slicedBytes[2] = 44; // Throws IndexOutOfRangeException bytes[2] = 45; // OK
var array = new byte[100]; Span<byte> interiorRef1 = array.AsSpan().Slice(start: 20);
Span<byte> interiorRef2 = new Span<byte>(array: array, start: 20, length: array.Length - 20);
class Impossible { Span<byte> field; }
static async Task<int> ChecksumReadAsync(Memory<byte> buffer, Stream stream) { int bytesRead = await stream.ReadAsync(buffer); return Checksum(buffer.Span.Slice(0, bytesRead)); // Or buffer.Slice(0, bytesRead).Span }
public class Customer { public string Name { get; set; } } public class Order { public Customer Customer { get; set; } }
public class Customer { public string Name { get; set; } public ICollection<Order> Orders { get; set; } } public class Order { public Customer Customer { get; set; } }
همان طور که با مثالهای آورده شده قابل مشاهده است، تصمیم به استفاده از ارتباط یک طرفه یا دوطرفه، تصمیمی صرفا فنی نیست و به شرایط قواعد کسب و کار نیز ارتباط مییابد.
اگر از ORM در طراحی لایه دسترسی داده استفاده شود، معمولا ارتباطهای دوطرفه در نگاه اول مشکل ساز به نظر نمیرسند، یا حتی لازم خواهند بود (برای استفاده از امکانات mapping بر اساس قرارداد). اما ارتباطات دو طرفه معمولا امکان دسترسی به اقلام اطلاعاتی را بدون در نظر گرفتن قواعد کسب و کاری لازم، بوجود میآورند. یکی از اشکالات اصلی طراحی با ارتباط دو طرفه ارتباط بیش از اندازه کلاسها با یکدیگر است. در این صورت ممکن است تغییر در یک کلاس، به کلاسهای دیگری نفوذ کند که این مورد خود یک الگوی کد بد بو است.
عموما با توجه به هزینهای که ارتباط دو طرفه ایجاد میکند، بهتر است استفاده از آن، تا آخرین لحظه نیاز به تعویق بیفتد و اولویت طراحی با ارتباط یک طرفه باشد.
public static int Add(in int number1, in int number2) { number1 = 5; // Cannot assign to variable 'in int' because it is a readonly variable return number1 + number2; }
<ItemGroup><PackageReference Include="BenchmarkDotNet" Version="0.10.12" /></ItemGroup>
using BenchmarkDotNet.Attributes; namespace CS72Tests { public struct Input { public decimal Number1; public decimal Number2; } [MemoryDiagnoser] public class InBenchmarking { const int loops = 50000000; Input inputInstance = new Input(); [Benchmark(Baseline = true)] public decimal RunNormalLoop_Pass_By_Value() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = Run(inputInstance); } return result; } [Benchmark] public decimal RunInLoop_Pass_By_Reference() { decimal result = 0M; for (int i = 0; i < loops; i++) { result = RunIn(in inputInstance); } return result; } public decimal Run(Input input) { return input.Number1; } public decimal RunIn(in Input input) { return input.Number1; } } }
static void Main(string[] args) { var summary = BenchmarkRunner.Run<InBenchmarking>();
Method | Mean | Error | StdDev | Scaled | Allocated | ---------------------------- |----------:|---------:|---------:|-------:|----------:| RunNormalLoop_Pass_By_Value | 280.04 ms | 2.219 ms | 1.733 ms | 1.00 | 0 B | RunInLoop_Pass_By_Reference | 91.75 ms | 1.733 ms | 1.780 ms | 0.33 | 0 B |
public static class Factorial { public static int Calculate(in this int num) { int result = 1; for (int i = num; i > 1; i--) result *= i; return result; } }
int num = 3; Console.WriteLine($"(in num) -> {Factorial.Calculate(in num)}"); Console.WriteLine($"(num) -> {Factorial.Calculate(num)}"); Console.WriteLine($"num. -> {num.Calculate()}");
(in num) -> 6 (num) -> 6 num. -> 6
public class CX { public void A(Input a) { Console.WriteLine("int a"); } public void A(in Input a) { Console.WriteLine("in int a"); } }
public class Y { public void Test() { var inputInstance = new Input(); var cx = new CX(); cx.A(inputInstance); // The call is ambiguous between the following methods or properties: 'CX.A(Input)' and 'CX.A(in Input)' } }
public IEnumerable<int> B(in int a) // Iterators cannot have ref or out parameters { Console.WriteLine("in int a"); yield return 1; }
public async Task C(in int a) // Async methods cannot have ref or out parameters { await Task.Delay(1000); }
using System; namespace CS72Tests { struct MyStruct { public int MyValue { get; set; } public void UpdateMyValue(int value) { MyValue = value; } } public static class TestInStructs { public static void Run() { var myStruct = new MyStruct(); myStruct.UpdateMyValue(1); UpdateMyValue(myStruct); Console.WriteLine(myStruct.MyValue); } static void UpdateMyValue(in MyStruct myStruct) { myStruct.UpdateMyValue(5); } } }
static void UpdateMyValue(in MyStruct myStruct) { myStruct.MyValue = 5; // Cannot assign to a member of variable 'in MyStruct' because it is a readonly variable myStruct.UpdateMyValue(5); }
import { SpecialCollection } from "../../special"; import { LoginComponent } from "../login"; import { TextUtils } from ".../../utils/text"; import { Router } from "../../../core/router";
import { Data } from '../data';
import { Data } from '../../../data';
import { BrowserStorageService } from "./../../core/browser-storage.service";
{ "compilerOptions": { "baseUrl": "src", "paths": { "@app/*": [ "app/*" ], "@app/core/*": [ "app/core/*" ], "@app/shared/*": [ "app/shared/*" ], "@env/*": [ "environments/*" ] } } }
import { BrowserStorageService } from "@app/core/browser-storage.service";
import { BrowserStorageService } from "./../../core/browser-storage.service";
import { Component } from '@angular/core';
"paths": { "@angular/*": ["node_modules/@angular/*"] },
resolve: { extensions: ['*', '.js', '.ts'], modules: [ rootDir, path.join(rootDir, 'node_modules') ], alias: { '@app': 'src/app' } },
import { BrowserStorageService } from "./../../core/browser-storage.service";
import { BrowserStorageService } from "@app/core/browser-storage.service";
import { BrowserStorageService } from "@app/core";
export * from "./browser-storage.service"; export * from "./app-config.service"; export * from "./seo-service";
sudo apt-get update -y sudo apt-get upgrade -y
sudo java -version
sudo add-apt-repository -y ppa:webupd8team/java
gpg: keyring `/tmp/tmpkjrm4mnm/secring.gpg' created gpg: keyring `/tmp/tmpkjrm4mnm/pubring.gpg' created gpg: requesting key EEA14886 from hkp server keyserver.ubuntu.com gpg: /tmp/tmpkjrm4mnm/trustdb.gpg: trustdb created gpg: key EEA14886: public key "Launchpad VLC" imported gpg: no ultimately trusted keys found gpg: Total number processed: 1 gpg: imported: 1 (RSA: 1) OK
sudo apt-get update
sudo apt-get install oracle-java8-installer -y
sudo java -version
java version "1.8.0_66" Java(TM) SE Runtime Environment (build 1.8.0_66-b17) Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)
پس از دریافت Apache Kafka، از طریق یک terminal جدید وارد پوشه Downloads شوید. سپس فایل مورد نظر را از حالت فشرده خارج کنید و وارد پوشه اصلی آن شوید. برای این کار از دستورات زیر استفاده کنید:
tar -xzf kafka_2.11-1.0.0.tgz cd kafka_2.11-1.0.0
سپس با استفاده از دستور "ls " لیست تمامی فایلها و فولدرهای موجود در فولدر kafka_2.11-1.0.0 و را نمایش دهید:
در لیست نمایش داده شده دو فولد اصلی bin و config وجود دارند که به ترتیب فایلهای اجرایی و کانفیگهای پیشفرض و مورد نیاز، در آنها قرار دارند.
اجرای Apache ZooKeeper:
همانطور که در بخش قبل توضیح داده شد، Apache Kafka هیچ Stateی را در خود ذخیره و مدیریت نمیکند و اصطلاحا Stateless میباشد و مدیریت تمامی این Stateها را بر عهده Apache ZooKeeper قرار میدهد. بنابراین قبل از اینکه بخواهیم Kafka را اجرا کنیم، ابتدا باید Apache ZooKeeper را اجرا کنیم. برای اجرای Apache ZooKeeper در terminalی که قبلا باز کردهاید، دستور زیر را اجرا کنید:
bin/zookeeper-server-start.sh config/zookeeper.properties
با اجرای فایل zookeeper-server-start.sh و ارسال کانفیگ پیشفرضش در فولدر config با نام zookeeper.properties، قسمت مدیریت کننده Stateهای Kafka اجرا میشود.
اجرای Apache Kafka:
پس از اجرای Apache ZooKeeper، باید یک terminal جدید را باز کنید و با استفاده از دستور زیر kafka-server را با استفاده از کانفیگ پیشفرضش اجرا کنید:
bin/kafka-server-start.sh config/server.properties
به همین راحتی Kafka Server اجرا شده و آمادهی استفاده است. برای ادامه و نمایش دادن سایر قابلیتهای Kafka باید یک Topic جدید را ایجاد کنیم.
ایجاد Topic:
همانطور که میدانید تمامی پیامهای شما در Partitionهای Topic ذخیره میشوند؛ پس قبل از اینکه بخواهیم توسط یک Producer پیامی را ارسال کنیم یا اینکه بخواهیم توسط Consumer پیامی را دریافت کنیم، ابتدا باید Topic مربوطه را ایجاد کنیم. بهترین و عمومیترین مثال برای نمایش قابلیتهای Publish-Subscribe در Kafka، مثال چت بین کاربران است که در آن یک کاربر به عنوان Producer عمل میکند و پیامهایی را برای سایر کاربران که نقش Consumer را دارند ارسال میکند. kafka-topics
.sh
در Kafka ابزاریست برای مدیریت Topic ها که با استفاده از آن میتوانید Topicهای مورد نیاز را تعریف، ویرایش و یا حذف کنید.
یک terminal جدید را آغاز و با استفاده از دستور زیر یک Topic را با نام userChat ایجاد کنید:
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic userChat
با این دستور یک Topic در Kafka با نام userChat ایجاد میشود و مشخصات آن (منظور stateهای مرتبط با آن است)، در zookeeper نیز ثبت میشود.
توضیحات Optionها و آرگومانهایی که در دستور ایجاد Topic استفاده شدند به شرح زیر است:
create-- :
مشخص کننده این است که شما میخواهید یک Topic جدید را ایجاد کنید.
zookeeper localhost:2181--:
مشخص کننده آدرس سرور zookeeper است (سرور zookeeper بصورت پیشفرض از پورت 2181 استفاده میکند).
replication-factor 1--:
مشخص کننده این است که تنها یک کپی از این Topic در سرور ایجاد شود. البته در این مثال به دلیل اینکه تنها یک سرور از Kafka را اجرا کردهایم در صورتیکه این مقدار را بیش از عدد 1 در نظر بگیریم، با خطا مواجه میشویم.
partitions 1--
تعداد Partitionهای این Topic را مشخص میکند. مقدار Partition در حالتیکه حتی یک سرور را نیز اجرا کردهایم، میتواند بیش از 1 باشد؛ اما در این حالت Primary Broker برای تمام Partitionهای همین سرور در نظر گرفته میشود.
topic userChat--
نام Topic را مشخص میکند.
پس از اینکه Topic مورد نظر ایجاد شد، با استفاده از دستور زیر میتوانیم لیست Topicهای ایجاد شده را مشاهده کنیم:
bin/kafka-topics.sh --list --zookeeper localhost:2181
توضیحات Optionها و آرگومانهایی که در دستور نمایش لیست Topicها استفاده شدند به شرح زیر است:
list--:
شما میخواهید لیست topicها را مشاهده کنید.
zookeeper localhost:2181--:
همانند دستور ایجاد، مشخص کننده سرور zookeeper میباشد.
تا اینجا برای ارسال و دریافت پیام در بین Applicationهای مختلف همه چیز آمادهاست. البته همانطور که در بخشهای بعدی این سری مقالات میبینیم، در عمل Producerها و Consumerها زیرسیستمهایی هستند که خود ما با استفاده از هر زبان برنامه نویسی پیاده سازی میکنیم. اما در این قسمت برای نمایش و تکمیل مثالمان، از ابزارهایی که خود Kafka در اختیار ما قرار داده استفاده میکنیم.
اجرای یک Producer و ارسال پیام به Kafka:
kafka-console-producer.shابزاریست که با استفاده از آن میتوانید به راحتی یک Producer را ایجاد کنید. در terminalی که قبلا برای مدیریت Topicها باز کردهاید با استفاده از دستور زیر، Producer را اجرا میکنیم:
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic userChat
توضیحات Optionها و آرگومانهایی که در دستور ایجاد Producer استفاده شدند به شرح زیر است:
broker-list localhost:9092--:
نشان دهنده لیست Brokerها میباشد و در صورتیکه تعداد آنها بیش از یک باشد، میتوان آنها را با "," از هم جدا کرد (پورت پیشفرض Kafka server برای دریافت دستورات، 9092 میباشد).
topic userChat--:
Topicی از Kafka server را مشخص میکند که این Producer میخواهد پیامهای خود را برایش ارسال کند.
پس از اجرای دستور فوق، با تایپ کردن و زدن کلید Enter، پیام مورد نظر به Broker موردنظر یعنی localhost:9092 ارسال و در تنها Partition تاپیک userChat ذخیره میشود.
اجرای یک Consumer و دریافت پیامهای موجود در Topic مورد نظر:
kafka-console-consumer.sh ابزاریست که خود Kafka در اختیار ما قرار داده است و با استفاده از آن میتوانیم به راحتی یک Consumer برای userChat ،Topic ایجاد کنیم.
در یک terminal جدید با استفاده از دستور زیر یک Consumer را ایجاد کنید که userChat ،Topic را Subscribe میکند:
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic userChat --from-beginning
توضیحات Optionها و آرگومانهایی که در دستور ایجاد یک Consumer استفاده شدند به شرح زیر است:
bootstrap-server localhost:9092-- :
مشخص کننده Kafka serverی است که میخواهیم از طریق آن به تمامی اعضای Cluster دسترسی داشته باشیم. در صورتیکه بیش از یک سرور را بخواهیم وارد کنیم، باید آنها را با "," از هم جدا کنیم.
topic userChat --:
مشخص کننده Topicی است که این Consumer میخواهد روی آن Subscribe کند.
from-beginning--:
مشخص کننده این است که Consumer ایجاد شده میخواهد تمامی پیامهای موجود در userChat ، Topic را از اولین offset تا آخرین offset دریافت کند.
پس از اجرای کد فوق مشاهده میکنید که پیامهایی که قبلا Producer در تاپیک userChat ثبت کردهاست، برای این Consumer ارسال میشوند.
از اینجا به بعد هر لحظه که در terminal ارسال کننده یا Producer پیامی تایپ کنید و کلید Enter را بزنید، بلافاصله پیام مورد نظر برای Consumer ارسال میشود و در terminal آن نمایش داده میشود. حتی میتوانیم چندین Consumer را در terminal های مختلفی اجرا کنیم. در این صورت با ارسال هر پیام از طرف Producer، تمامی Consumerها آن را نمایش میدهند.
با استفاده از سادگی راه اندازی و قابلیتهای بسیار زیادی که Apache Kafka در مدیریت جریان دادهای بین سیستمهایمان به ما میدهد، میتوانیم به سادگی در سیستمهایمان قابلیتهای مقیاس پذیری افقی، تحمل در برابر خطا، در دسترس بودن، کارآیی بالا و سادگی مدیریت ارتباطات بخشهای مختلف را اضافه کنیم. در بخشهای بعدی به نحوه ایجاد یک Cluster و اینکه چگونه میتوان از این بستر در Net. استفاده کرد، میپردازیم.
// Behavior 1 object obj = null; bool objValueEqual = obj.Equals(null); // Behavior 2 object obj = null; Type objType = obj.GetType(); // Behavior 3 string str = (string)null; bool strType = str is string; // Behavior 4 int num = 5; Nullable<int> nullableNum = 5; bool typeEqual = num.GetType() == nullableNum.GetType(); // Behavior 5 Type inType = typeof(int); Type nullableIntType = typeof(Nullable<int>); bool typeEqual = inType == nullableIntType;
static void Main(string[] args) { Console.WriteLine(Method(null)); Console.ReadLine(); } private static string Method(object obj) { return "Object parameter"; } private static string Method(string str) { return "String parameter"; }
var rounded = Math.Round(1.5); // 2 var rounded = Math.Round(2.5); // 2 var rounded = Math.Round(2.5, MidpointRounding.ToEven); // 2 var rounded = Math.Round(2.5, MidpointRounding.AwayFromZero); // 3 var value = 1.4f; var rounded = Math.Round(value + 0.1f); // 1
public static class Config { public static bool ThrowException { get; set; } = true; } public class FailingClass { static FailingClass() { if (Config.ThrowException) { throw new InvalidOperationException(); } } }
try { var failedInstance = new FailingClass(); } catch (TypeInitializationException) { } Config.ThrowException = false; var instance = new FailingClass();
public class BaseClass { { public BaseClass() { VirtualMethod(1); } public virtual int VirtualMethod(int dividend) { return dividend / 1; } } public class DerivedClass : BaseClass { int divisor; public DerivedClass() { divisor = 1; } public override int VirtualMethod(int dividend) { return base.VirtualMethod(dividend / divisor); } }
class Program { static void Main(string[] args) { var instance = new DerivedClass(); var result = instance.Method(); result = ((BaseClass)instance).Method(); Console.WriteLine(instance + " -> " + result); // Derived Class ... -> Method in BaseClass Console.ReadLine(); } } public class BaseClass { public virtual string Method() { return "Method in BaseClass"; } } public class DerivedClass : BaseClass { public override string ToString() { return "Derived Class ... "; } public new string Method() { return "Method in DerivedClass"; } }
class Program { static void Main(string[] args) { var instance = new DerivedClass(); var result = instance.Method(); // -> Method in DerivedClass result = ((IInterface)instance).Method(); // -> Method belonging to IInterface Console.WriteLine(result); Console.ReadLine(); } } public interface IInterface { string Method(); } public class DerivedClass : IInterface { public string Method() { return "Method in DerivedClass"; } string IInterface.Method() { return "Method belonging to IInterface"; } }
private IEnumerable<int> GetEnumerable(StringBuilder log) { using (var test = new Test(log)) { return Enumerable.Range(1, 5); } }
var log = new StringBuilder(); foreach (var number in GetEnumerable(log)) { log.AppendLine($"{number}"); }
Created Disposed 1 2 3 4 5
using (var test = new Test(log)) { foreach (var i in Enumerable.Range(1,5)) { yield return i; } }
Created 1 2 3 4 5 Disposed
{ "compilerOptions": { "experimentalDecorators": true, ... }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, "preserveWhiteSpace": false, ... } }
export interface PonyModel { name: string; }
import { PonyModel } from "./pony"; @Component({ selector: "app-detect-common-errors-test", templateUrl: "./detect-common-errors-test.component.html", styleUrls: ["./detect-common-errors-test.component.css"] }) export class DetectCommonErrorsTestComponent implements OnInit { ponyModel: PonyModel = { name: "Pony1" };
<p>Hello {{ponyModel.age}}
\detect-common-errors-test.component.html(5,4): : Property 'age' does not exist on type 'PonyModel'.
هدف، ارائه راهحلی برای نمایش جدولی اطلاعات، جستجو، مرتب سازی و صفحه بندی و همچنین انجام عملیات ثبت، ویرایش و حذف بر روی آنها به صورت Ajaxای در بخش back office نرم افزار میباشد.
پیش نیازها:
ایده کار به این شکل میباشد که برای نمایش اطلاعات به صورت جدولی با قابلیتهای مذکور، لازم است یک اکشن Index برای نمایش اولیه و صفحه اول اطلاعات صفحه بندی شده و اکشن متدی به نام List برای پاسخ به درخواستهای صفحه بندی، مرتب سازی، تغییر تعداد آیتمها در هر صفحه و همچنین جستجو، داشته باشیم که این اکشن متد List، بعد از واکشی اطلاعات مورد نظر از منبع داده، آنها را به همراه اطلاعاتی که در کوئری استرینگ درخواست جاری وجود دارد در قالب یک PartialView به کلاینت ارسال کند.
ایجاد مدلهای پایه
همانطور که در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» مطرح شد، برای پیاده سازی متدهای GetPagedList در ApplicationServiceها از الگوی Request/Response استفاده میکنیم. برای این منظور واسط و کلاسهای زیر را خواهیم داشت:
واسط IPagedQueryModel
public interface IPagedQueryModel { int Page { get; set; } int PageSize { get; set; } /// <summary> /// Expression of Sorting. /// </summary> /// <example> /// Examples: /// "Name_ASC" /// </example> string SortExpression { get; set; } }
این واسط قراردادی میباشد برای نوع و نام پارامترهایی که توسط کلاینت به سرور ارسال میشود. پراپرتی SortExpression آن، نام و ترتیب مرتب سازی را مشخص میکند؛ برای این منظور FieldName_ASC و FieldName_DESC به ترتیب برای حالات مرتب سازی صعودی و نزولی براساس FieldName مقدار دهی خواهد شد.
برای جلوگیری از تکرار این خصوصیات در مدلهای کوئری مربوط به موجودیتها، میتوان کلاس پایهای به شکل زیر در نظر گرفت که پیاده ساز واسط بالا میباشد:
public class PagedQueryModel : IPagedQueryModel, IShouldNormalize { public int Page { get; set; } public int PageSize { get; set; } /// <summary> /// Expression of Sorting. /// </summary> /// <example> /// Examples: /// "Name_ASC" /// </example> public string SortExpression { get; set; } public virtual void Normalize() { if (Page < 1) Page = 1; if (PageSize < 1) PageSize = 10; if (SortExpression.IsEmpty()) SortExpression = "Id_DESC"; } }
مدل بالا علاوه بر پیاده سازی واسط IPagedQueryModel، پیاده ساز واسط IShouldNormalize نیز میباشد؛ دلیل وجود چنین واسطی در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» توضیح داده شده است:
پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.
کلاس PagedQueryResult
public class PagedQueryResult<TModel> { public PagedQueryResult() { Items = new List<TModel>(); } public IEnumerable<TModel> Items { get; set; } public long TotalCount { get; set; } }
دلیل وجود کلاس بالا در مقاله «طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور» توضیح داده شده است:
عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتمهای تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیفهای صفحهی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی میتواند باشد، بهتر است آنرا جنریک تعریف کنیم.
کلاس PagedListModel
همانطور که در اول بحث توضیح داده شد، لازم است اطلاعاتی را که کلاینت از طریق کوئری استرینگ برای صفحه بندی و ... ارسال کرده بود نیز به PartialView ارسال کنیم. این قسمت کار ایده اصلی این روش را در بر میگیرد؛ اگر نخواهیم اطلاعات کوئری استرینگ دریافتی از کلاینت را دوباره به PartialView ارسال کنیم، مجبور خواهیم بود تمام کارهای مربوط به تشخیص آیکن مرتب سازی ستونهای جدول، ریست کردن المنتهای مربوط به صفحه بندی و مرتب سازی را در در زمان انجام جستجو و یکسری کارهای از این قبل را در سمت کلاینت مدیریت کنیم که هدف مقاله جاری پیاده سازی این روش نمیباشد.
public class PagedListModel<TModel> { public IPagedQueryModel Query { get; set; } public PagedQueryResult<TModel> Result { get; set; } }
پراپرتی Query در برگیرنده پارامتر ورودی اکشن متد List میباشد که پراپرتیهای آن با مقادیر موجود در کوئری استرینگ درخواست جاری مقدار دهی شدهاند؛ البته بدون وجود کلاس بالا نیز به کمک ViewBag میشود این اطلاعات ترکیبی را به ویو ارسال کرد که پیشنهاد نمیشود.
متد GetPagedListAsync موجود در CrudApplicationService
public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel> : ApplicationService, ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel> where TEntity : Entity, new() where TCreateModel : class where TEditModel : class, IModel where TModel : class, IModel where TDeleteModel : class, IModel where TPagedQueryModel : PagedQueryModel, new() where TDynamicQueryModel : DynamicQueryModel { #region Properties protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking(); public IUnitOfWork UnitOfWork { get; set; } public IMapper Mapper { get; set; } protected IDbSet<TEntity> EntitySet => UnitOfWork.Set<TEntity>(); #endregion #region ICrudApplicationService Members #region Methods public virtual async Task<PagedQueryResult<TModel>> GetPagedListAsync(TPagedQueryModel model) { Guard.ArgumentNotNull(model, nameof(model)); var query = ApplyFiltering(model); var totalCount = await query.LongCountAsync().ConfigureAwait(false); var result = query.ProjectTo<TModel>(Mapper.ConfigurationProvider); result = result.ApplySorting(model); result = result.ApplyPaging(model); return new PagedQueryResult<TModel> { Items = await result.ToListAsync().ConfigureAwait(false), TotalCount = totalCount }; } #endregion #endregion #region Protected Methods /// <summary> /// Apply Filtering To GetPagedList and GetPagedListAsync /// </summary> /// <param name="model"></param> /// <returns></returns> protected virtual IQueryable<TEntity> ApplyFiltering(TPagedQueryModel model) { Guard.ArgumentNotNull(model, nameof(model)); return UnTrackedEntitySet; } #endregion }
در بدنه این متد، ابتدا عملیات جستجو توسط متد ApplyFiltering انجام میشود. این متد به صورت پیش فرض هیچ شرطی را بر روی کوئری ارسالی به منبع داده اعمال نمیکند؛ مگر اینکه توسط زیر کلاسها بازنویسی شود و فیلترهای مورد نیاز اعمال شوند. سپس تعداد کل آیتمهای فیلتر شده محاسبه شده و بعد از عملیات Projection، مرتب سازی و صفحه بندی انجام میگیرد. برای مباحث مرتب سازی و صفحه بندی از دو متد زیر کمک گرفته شدهاست:
public static class QueryableExtensions { public static IQueryable<TModel> ApplySorting<TModel>(this IQueryable<TModel> query, IPagedQueryModel request) { Guard.ArgumentNotNull(request, nameof(request)); Guard.ArgumentNotNull(query, nameof(query)); return query.OrderBy(request.SortExpression.Replace('_', ' ')); } public static IQueryable<TModel> ApplyPaging<TModel>(this IQueryable<TModel> query, IPagedQueryModel request) { Guard.ArgumentNotNull(request, nameof(request)); Guard.ArgumentNotNull(query, nameof(query)); return request != null ? query.Page((request.Page - 1) * request.PageSize, request.PageSize) : query; } }
به منظور مرتب سازی از کتابخانه System.Liq.Dynamic کمک گرفته شدهاست.
نکته: مشخص است که این روش، وابستگی به وجود متد GetPagedListAsync ندارد و صرفا برای تشریح ارتباط مطالبی که قبلا منتشر شده بود، مطرح شد.
پیاده سازی اکشن متدهای Index و List
public partial class RolesController : BaseController { #region Fields private readonly IRoleService _service; private readonly ILookupService _lookupService; #endregion #region Constractor public RolesController(IRoleService service, ILookupService lookupService) { Guard.ArgumentNotNull(service, nameof(service)); Guard.ArgumentNotNull(lookupService, nameof(lookupService)); _service = service; _lookupService = lookupService; } #endregion #region Index / List [HttpGet] public virtual async Task<ActionResult> Index() { var query = new RolePagedQueryModel(); var result = await _service.GetPagedListAsync(query).ConfigureAwait(false); var pagedList = new PagedListModel<RoleModel> { Query = query, Result = result }; var model = new RoleIndexViewModel { PagedListModel = pagedList, Permissions = _lookupService.GetPermissions() }; return View(model); } [HttpGet, AjaxOnly, NoOutputCache] public virtual async Task<ActionResult> List(RolePagedQueryModel query) { var result = await _service.GetPagedListAsync(query).ConfigureAwait(false); var model = new PagedListModel<RoleModel> { Query = query, Result = result }; return PartialView(MVC.Administration.Roles.Views._List, model); } #endregion }
به عنوان مثال در بالا کنترلر مربوط به گروههای کاربری را مشاهده میکنید. به دلیل اینکه علاوه بر مباحث صفحه بندی و مرتب سازی، امکان جستجو بر اساس نام و دسترسیهای گروه کاربری را نیز نیاز داریم، لازم است مدل زیر را ایجاد کنیم:
public class RolePagedQueryModel : PagedQueryModel { public string Name { get; set; } public string Permission { get; set; } }
در این مورد خاص لازم است لیست دسترسیهای موجود درسیستم به صورت لیستی برای انتخاب در فرم جستجو مهیا باشد. فرم جستجو در ویو مربوط به اکشن Index قرار میگیرد و قرار نیست به همراه پارشال ویو List_ در هر درخواستی از سرور دریافت شود. لذا لازم است مدلی برای ویو Index در نظر بگیریم که به شکل زیر میباشد:
public class RoleIndexViewModel { public RoleIndexViewModel() { Permissions = new List<LookupItem>(); } public IReadOnlyList<LookupItem> Permissions { get; set; } public PagedListModel<RoleModel> PagedListModel { get; set; } }
پراپرتی PagedListModel در برگیرنده اطلاعات مربوط به نمایش اولیه جدول اطلاعات میباشد و پراپرتی Permissions لیست دسترسیهای موجود درسیستم را به ویو منتقل خواهد کرد. اگر ویو ایندکس شما به داده اضافه ای نیاز ندارد، از ایجاد مدل بالا صرف نظر کنید.
ویو Index.cshtml
@model RoleIndexViewModel @{ ViewBag.Title = L("Administration.Views.Role.Index.Title"); ViewBag.ActiveMenu = AdministrationMenuNames.RoleManagement; } <div class="row"><div class="col-md-12"><div id="filterPanel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="filterPanel"><div class="panel panel-default margin-bottom-5"><div class="panel-body"> @using (Ajax.BeginForm(MVC.Administration.Roles.List(), new AjaxOptions { UpdateTargetId = "RolesList", HttpMethod = "GET" }, new { id = "filterForm", data_submit_on_reset = "true" })) {<div class="row"><div class="col-md-3"><input type="text" name="Name" class="form-control" value="" placeholder="@L("Administration.Role.Fields.Name")" /></div><div class="col-md-3"> @Html.DropDownList("Permission", Model.Permissions.ToSelectListItems(), L("Administration.Views.Role.FilterBy.Permission"),new {@class="form-control"})</div><div class="col-md-3"><button type="submit" role="button" class="btn btn-info"> @L("Commands.Filter")</button><button type="reset" role="button" class="btn btn-default"><i class="fa fa-close"></i> @L("Commands.Reset")</button></div></div> }</div></div></div></div></div><div class="row"><div class="col-md-12" id="RolesList"> @{Html.RenderPartial(MVC.Administration.Roles.Views._List, Model.PagedListModel);}</div></div>
فرم جستجو باید دارای ویژگی data_submit_on_reset با مقدار "true" باشد. به منظور پاکسازی فرم جستجو و ارسال درخواست جستجو با فرمی خالی از داده، برای بازگشت به حالت اولیه از تکه کد زیر استفاده خواهد شد:
$(document).on("reset", "form[data-submit-on-reset]", function () { var form = this; setTimeout(function () { $(form).submit(); }); });
در ادامه پارشال ویو List_ با داده ارسالی به ویو Index، رندر شده و کار نمایش اولیه اطلاعات به صورت جدولی به اتمام میرسد.
پارشال ویو List.cshtml_
@model PagedListModel<RoleModel> @{ Layout = null; var rowNumber = (Model.Query.Page - 1) * Model.Query.PageSize + 1; var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary()))); }<div class="panel panel-default margin-bottom-5"><table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList"><thead><tr><th style="width: 5%;"> #</th><th class="col-md-3 sortable"> @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))</th><th class="col-md-3 sortable"> @Html.SortableColumn("DisplayName", L("Administration.Role.Fields.DisplayName"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))</th><th class="col-md-3 sortable"> @Html.SortableColumn("IsDefault", L("Administration.Role.Fields.IsDefault"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))</th><th style="width: 5%;"></th></tr></thead><tbody> @foreach (var role in Model.Result.Items) {<tr><td>@(rowNumber++.ToPersianNumbers())</td><td>@role.Name</td><td>@role.DisplayName</td><td class="text-center">@Html.DisplayFor(a => role.IsDefault)</td><td class="text-center operations"><div class="btn-group"><span class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span><ul class="dropdown-menu dropdown-menu-left"><li><a href="#" role="button" data-ajax="true" data-ajax-method="GET" data-ajax-update="#main-modal div.modal-content" data-ajax-url="@Url.Action(MVC.Administration.Roles.Edit(role.Id))" data-toggle="modal" data-target="#main-modal"><i class="fa fa-pencil"></i> @L("Commands.Edit")</a></li><li><a href="#" role="button" id="delete-@role.Id" data-delete-url="@Url.Action(MVC.Administration.Roles.Delete())" data-delete-model='{"Id":"@role.Id","RowVersion":"@Convert.ToBase64String(role.RowVersion)"}'><i class="fa fa-trash"></i> @L("Commands.Delete")</a></li></ul></div></td></tr> }</tbody></table></div><div class="row"><div class="col-md-8"> @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary))) @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm") @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))</div></div>
به ترتیب فایل بالا را بررسی میکنیم:
var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));
refreshUrl برای ارسال درخواست به اکشن متد List در نظر گرفته شدهاست که در کوئری استرینگ مربوط به خود، اطلاعاتی (مرتب سازی، شماره صفحه، اطلاعات جستجو و همچنین تعداد آیتمهای موجود در هر صفحه) را دارد که حالت فعلی گرید را میتوانیم دوباره از سرور درخواست کنیم.
<table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">
دو ویژگی data-ajax-refresh-url و data-ajax-refresh-update برای جدولی که لازم است عملیات CRUD را پشتیبانی کند، لازم میباشد. در قسمت دوم به استفاده از این دو ویژگی در هنگام عملیات ثبت، ویرایش و حذف خواهیم پرداخت.
<th class="col-md-3 sortable"> @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))</th>
ستونی که امکان مرتب سازی را دارد باید th آن، کلاس sortable را داشته باشد. همچنین باید از هلپری که پیاده سازی آن را در ادامه خواهیم دید، استفاده کنیم. این هلپر، نام فیلد، عنوان ستون، مدل Query و همچین یک urlFactory را در قالب یک Func<RouteValueDictionary,string> دریافت میکند.
پیاده سازی هلپر SortableColumn
public static MvcHtmlString SortableColumn(this HtmlHelper html, string columnName, string columnDisplayName, IPagedQueryModel queryModel, string updateTargetId, Func<RouteValueDictionary, string> urlFactory) { var dictionary = queryModel.ToDictionary(); var routeValueDictionary = new RouteValueDictionary(dictionary) { ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName) ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC") ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC") ? string.Empty : $"{columnName}_DESC" }; var url = urlFactory(routeValueDictionary); var aTag = new TagBuilder("a"); aTag.Attributes.Add("href", "#"); aTag.Attributes.Add("data-ajax", "true"); aTag.Attributes.Add("data-ajax-method", "GET"); aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}"); aTag.Attributes.Add("data-ajax-url", url); aTag.InnerHtml = columnDisplayName; var iconCssClass = !queryModel.SortExpression.StartsWith(columnName) ? "fa-sort" : queryModel.SortExpression.EndsWith("DESC") ? "fa-sort-down" : "fa-sort-up"; var iTag = new TagBuilder("i"); iTag.AddCssClass($"fa {iconCssClass}"); return new MvcHtmlString($"{aTag}\n{iTag}"); }
ابتدا مدل Query با متد الحاقی زیر تبدیل به دیکشنری میشود. این کار از این جهت مهم است که پراپرتیهای لیست موجود در مدل Query، لازم است به فرم خاصی به سرور ارسال شوند که در تکه کد زیر مشخص میباشد.
public static IDictionary<string, object> ToDictionary(this object source) { return source.ToDictionary<object>(); } public static IDictionary<string, T> ToDictionary<T>(this object source) { if (source == null) throw new ArgumentNullException(nameof(source)); var dictionary = new Dictionary<string, T>(); foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source)) { AddPropertyToDictionary(property, source, dictionary); } return dictionary; } private static void AddPropertyToDictionary<T>(PropertyDescriptor property, object source, IDictionary<string, T> dictionary) { var value = property.GetValue(source); var items = value as IEnumerable; if (items != null && !(items is string)) { var i = 0; foreach (var item in items) { dictionary.Add($"{property.Name}[{i++}]", (T)item); } } else if (value is T) { dictionary.Add(property.Name, (T)value); } }
در متد بالا، از TypeDescriptor که یکی دیگر از ابزارهای دسترسی به متا دیتای انوع دادهای است، استفاده شده و خروجی نهایی آن یک دیکشنری با کلیدهایی با اسامی پراپرتیهای وهله ورودی میباشد.
در ادامه پیاده سازی هلپر SortableColumn، از دیکشنری حاصل، یک وهله از RouteValueDictionary ساخته میشود. در زمان رندر شدن PartialView لازم است مشخص شود که برای دفعه بعدی که بر روی این ستون کلیک میشود، باید چه مقداری با پارامتر SortExpression موجود در کوئری استرینگ ارسال شود. از این جهت برای پشتیبانی ستون، از حالتهای مرتب سازی صعودی، نزولی و برگشت به حالت اولیه بدون مرتب سازی، کد زیر را خواهیم داشت:
var routeValueDictionary = new RouteValueDictionary(dictionary) { ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName) ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC") ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC") ? string.Empty : $"{columnName}_DESC" };
در ادامه urlFactory با routeValueDictionary حاصل، Invoke میشود تا url نهایی برای مرتب سازیهای بعدی را از طریق یک لینک تزئین شده با data اتریبیوتهای Unobtrusive Ajax در th مربوطه قرار دهیم.
برای مباحث صفحه بندی، بارگزاری مجدد و تغییر تعداد آیتمها در هر صفحه، از سه هلپر زیر کمک خواهیم گرفت:
<div class="row"><div class="col-md-8"> @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary))) @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm") @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))</div></div>
پیاده سازی هلپر Pager
public static MvcHtmlString Pager<TModel>(this HtmlHelper html, PagedListModel<TModel> model, string updateTargetId, Func<RouteValueDictionary, string> urlFactory) { return html.PagedListPager( new StaticPagedList<TModel>(model.Result.Items, model.Query.Page, model.Query.PageSize, (int)model.Result.TotalCount), page => { var dictionary = model.Query.ToDictionary(); var routeValueDictionary = new RouteValueDictionary(dictionary) { ["Page"] = page }; return urlFactory(routeValueDictionary); }, PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing( new PagedListRenderOptions { DisplayLinkToFirstPage = PagedListDisplayMode.Always, DisplayLinkToLastPage = PagedListDisplayMode.Always, DisplayLinkToPreviousPage = PagedListDisplayMode.Always, DisplayLinkToNextPage = PagedListDisplayMode.Always, MaximumPageNumbersToDisplay = 6, DisplayItemSliceAndTotal = true, DisplayEllipsesWhenNotShowingAllPageNumbers = true, ItemSliceAndTotalFormat = $"تعداد کل: {model.Result.TotalCount.ToPersianNumbers()}", FunctionToDisplayEachPageNumber = page => page.ToPersianNumbers(), }, new AjaxOptions { AllowCache = false, HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = updateTargetId })); }
در متد بالا از کتابخانه PagedList.Mvc استفاده شدهاست. یکی از overloadهای متد PagedListPager آن، یک پارامتر از نوع Func<int, string> به نام generatePageUrl را دریافت میکند که امکان شخصی سازی فرآیند تولید لینک به صفحات بعدی و قبلی را به ما میدهد. ما نیز از این امکان برای افزودن اطلاعات موجود در مدل Query، به کوئری استرینگ لینکهای تولیدی استفاده کردیم و صرفا برای لینکهای ایجادی لازم بود مقادیر پارامتر Page موجود در کوئری استرینگ تغییر کند که در کد بالا مشخص میباشد.
پیاده سازی هلپر PageSize
public static MvcHtmlString PageSize(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, object htmlAttributes = null, string filterFormId = null, params int[] numbers) { if (numbers.Length == 0) numbers = new[] { 10, 20, 30, 50, 100 }; var dictionary = queryModel.ToDictionary(); var routeValueDictionary = new RouteValueDictionary(dictionary) { [nameof(IPagedQueryModel.Page)] = 1 }; routeValueDictionary.Remove(nameof(IPagedQueryModel.PageSize)); var url = urlFactory(routeValueDictionary); var formTag = new TagBuilder("form"); formTag.Attributes.Add("action", url); formTag.Attributes.Add("method", "GET"); formTag.Attributes.Add("data-ajax", "true"); formTag.Attributes.Add("data-ajax-method", "GET"); formTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}"); formTag.Attributes.Add("data-ajax-url", url); if (htmlAttributes != null) formTag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); formTag.AddCssClass("form-inline inline"); var items = numbers.Select(number => new SelectListItem { Value = number.ToString(), Text = number.ToString().ToPersianNumbers(), Selected = queryModel.PageSize == number }); formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString(); if (filterFormId.IsEmpty()) return new MvcHtmlString($"{formTag}"); // ReSharper disable once MustUseReturnValue var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>"; return new MvcHtmlString($"{formTag}\n{scriptBlock}"); }
ایده کار به این صورت است که یک المنت select، درون یک المنت form قرار میگیرد و در زمان change آن، فرم مربوطه submit میشود.
formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();
در زمان تغییر تعداد نمایشی آیتمها در هر صفحه، لازم است حالت فعلی گرید حفظ شود و صرفا پارامتر Page ریست شود.
نکته مهم:در این طراحی اگر فرم جستجویی دارید، در زمان جستجو هیچیک از پارامترهای مربوط به صفحه بندی و مرتب سازی به سرور ارسال نخواهند شد (در واقع ریست میشوند) و کافیست یک درخواست GET معمولی با ارسال محتویات فرم به سرور صورت گیرد؛ ولی لازم است PageSize تنظیم شده، در زمان اعمال فیلتر نیز به سرور ارسال شود. از این جهت اسکریپتی برای ایجاد یک input مخفی در فرم جستجو نیز هنگام رندر شدن PartialView در صفحه تزریق میشود.
var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";
پیاده سازی هلپر Refresh
public static MvcHtmlString Refresh(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, string label = null) { var dictionary = queryModel.ToDictionary(); var routeValueDictionary = new RouteValueDictionary(dictionary); var url = urlFactory(routeValueDictionary); var aTag = new TagBuilder("a"); aTag.Attributes.Add("href", "#"); aTag.Attributes.Add("role", "button"); aTag.Attributes.Add("data-ajax", "true"); aTag.Attributes.Add("data-ajax-method", "GET"); aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}"); aTag.Attributes.Add("data-ajax-url", url); aTag.AddCssClass("btn btn-default"); var iTag = new TagBuilder("i"); iTag.AddCssClass("fa fa-refresh"); aTag.InnerHtml = $"{iTag} {label}"; return new MvcHtmlString(aTag.ToString()); }
متد بالا نیز به مانند refreshUrl که پیشتر مطرح شد، برای بارگزاری مجدد حالت فعلی گرید استفاده میشود و از این جهت است که مقادیر مربوط به کلیدهای routeValueDictionary را تغییر ندادهایم.
روش دیگر برای مدیریت این چنین کارهایی، استفاده از یک المنت form و قرادادن کل گرید به همراه یک سری input مخفی معادل با پارامترهای دریافتی اکشن متد List و مقدار دهی آنها در زمان کلیک بر روی دکمههای صفحه بندی، بارگزاری مجدد، دکمه اعمال فیلتر و لیست آبشاری تنظیم تعداد آیتمها، درون آن نیز میتواند کار ساز باشد؛ اما در زمان پیاده سازی خواهید دید که پیاده سازی آن خیلی سرراست، به مانند پیاده سازی موجود در مطلب جاری نخواهد بود.
در قسمت دوم، به پیاده سازی عملیات ثبت، ویرایش و حذف برپایه مودالهای بوت استرپ و افزونه Unobtrusive Ajax خواهیم پرداخت.
کدهای کامل قسمت جاری، بعد از انتشار قسمت دوم، در مخزن گیت هاب شخصی قرار خواهد گرفت.
[Route("api/[controller]")] public class TypeaheadController : Controller { [HttpGet("[action]")] public async Task<IActionResult> SearchCountries(string term) { await Task.Delay(1000); // simulating a slow operation var items = new[] { "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and/or Barbuda" }; var results = string.IsNullOrWhiteSpace(term) ? items : items.Where(item => item.StartsWith(term, StringComparison.OrdinalIgnoreCase)); return Json(results.ToArray()); } }
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { ErrorObservable } from "rxjs/observable/ErrorObservable"; import { catchError, map } from "rxjs/operators"; @Injectable() export class SearchService { constructor(private http: HttpClient) { } searchCountries(term: string): Observable<string[]> { return this.http .get(`/api/Typeahead/SearchCountries?term=${encodeURIComponent(term)}`) .pipe( map(response => response || {}), catchError((error: HttpErrorResponse) => ErrorObservable.create(error)) ); } }
<label>Country: </label><input type="text" (input)="onSearch1Change($event.target.value)" /><ul class="list-group"><li class="list-group-item" *ngFor="let country of countries1"> {{country}}</li></ul>
onSearch1Change(value: string) { }
constructor(private searchService: SearchService) { }
export class AutocompleteSampleComponent implements OnInit { countries1: string[] = []; private model1Changed: Subject<string> = new Subject<string>(); private dueTime = 300; constructor(private searchService: SearchService) { } ngOnInit() { this.model1Changed .pipe( debounceTime(this.dueTime), distinctUntilChanged(), flatMap(inputValue => { console.log("debounced input value1", inputValue); return this.searchService.searchCountries(inputValue); }) ) .subscribe(countries => { this.countries1 = countries; }); } onSearch1Change(value: string) { this.model1Changed.next(value); } }
// A1: Request for `ABC` // A2: Response for `ABC` // B1: Request for `ABCX` // B2: Response for `ABCX` --A1----------A2--> ------B1--B2------>
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddNodeServices(); }
public async Task<IActionResult> Add([FromServices] INodeServices nodeServices) { var num1 = 10; var num2 = 20; var result = await nodeServices.InvokeAsync<int>("AddModule.js", num1, num2); ViewData["ResultFromNode"] = $"Result of {num1} + {num2} is {result}"; return View(); }
module.exports = function(callback, num1, num2) { var result = num1 + num2; callback(null, result); };
حال بیاییم مثالی دیگر را مرور کنیم. میخواهیم از صفحه وب درخواستی، عکسی را تهیه کنیم. بدین منظور از کتابخانه url-to-image استفاده میکنیم. برای نصب آن دستور npm install --save url-to-image را در خط فرمان تایپ میکنیم.
بعد از اتمام نصب این بسته، متدی را برای دریافت اطلاعات ارسالی این کتابخانه تدارک میبینیم.
[HttpPost] public async Task<IActionResult> GenerateUrlPreview([FromServices] INodeServices nodeServices) { var url = Request.Form["Url"].ToString(); var fileName = System.IO.Path.ChangeExtension(DateTime.UtcNow.Ticks.ToString(), "jpeg"); var file = await nodeServices.InvokeAsync<string>("UrlPreviewModule.js", url, System.IO.Path.Combine("PreviewImages", fileName)); return Content($"/Home/Download?img={fileName}"); } public IActionResult Download() { var image = Request.Query["img"].ToString(); var fileName = System.IO.Path.Combine("PreviewImages", image); var isExists = System.IO.File.Exists(fileName); if (isExists) { Response.Headers.Add($"Content-Disposition", "attachment; filename=\"" + image + "\""); var bytes = System.IO.File.ReadAllBytes(fileName); return File(bytes, "image/jpeg"); } else { return NotFound(); } }
سپس متد UrlPreviewModule.js را به صورت زیر مینویسیم:
var urlToImage = require('url-to-image'); module.exports = function (callback, url, imageName) { urlToImage(url, imageName).then(function () { callback(null, imageName); }).catch(function (err) { callback(err, imageName); }); };
سرویسهای Node به توسعه دهندگان ASP.NET Core امکان استفاده از اکوسیستم NPM را که دارای قابلیتهای فراوانی میباشد، میدهد.
<system.webServer><handlers><add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" /></handlers><aspNetCore processPath="dotnet" arguments=".\MyApp.dll" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true" /></system.webServer>
Right click on the folder -> Properties -> Security tab -> Click at Edit button -> Enter `IIS AppPool\DefaultAppPool` user (IIS AppPool\<app_pool_name>) -> Click at Check names -> OK -> Then give it `read & execute` or other permissions.
"Console": { "LogLevel": { "Default": "Information" } }
Application ‘<IIS path>’ with physical root ‘<Application path>’ failed to start process with commandline ‘”dotnet” .\MyApp.dll’, ErrorCode = ‘0x80004005 : 80008083.
HTTP Error 500.19 - Internal Server Error The requested page cannot be accessed because the related configuration data for the page is invalid.
The Module DLL C:\WINDOWS\system32\inetsrv\aspnetcore.dll failed to load. The data is the error.
ابزارهای زیادی جهت تست کردن 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); }
[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); }
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 |
تمامی متدهای ارائه شده توسط یک 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 جهت جایگزینی دادهای موجود در لیست، با دادهی جدید است. بدین منظور کد داخلی شیء مورد نظر (در اینجا 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
Now listening on: http://localhost:5000
Now listening on: https://localhost:5001 Now listening on: http://localhost:5000
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection();
services.AddHttpsRedirection(options => options.HttpsPort = 5002);
services.AddHsts(options => { options.MaxAge = TimeSpan.FromDays(100); options.IncludeSubDomains = true; options.Preload = true; });
ASP.NET Core ------------ Successfully installed the ASP.NET Core HTTPS Development Certificate.
dotnet install tool dotnet-dev-certs -g --version 2.1.0-preview1-final dotnet dev-certs https --trust
{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:4929", "sslPort": 44313 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_HTTPS_PORT": "44313" } }, "TestWebApp": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:5001;http://localhost:5000" } } } }
ng g c Loginlayout
ng g c homelayout
<div style="width: 100%;height: 250px;background-color: aquamarine"><h1>Header</h1></div><router-outlet></router-outlet><div style="width: 100%;height: 250px;background-color: brown"><h1>Foother</h1></div>
export const routes: Routes = [ { path: 'Loginlayout', component: LoginlayoutComponent , children: [ { path: 'Login', component: LoginComponent, pathMatch: 'full'} ] }, { path: 'Homelayout', component: HomelayoutComponent, children: [ { path: 'About', component: AbouComponent, pathMatch: 'full'}, { path: 'Home', component: HomeComponent, pathMatch: 'full'} ] } ];
path: '', component: HomelayoutComponent, children: [ { path: '', component:HomeComponent, pathMatch: 'full'} ]
http://localhost:51095/Home/Index
ngrok http [port] -host-header="localhost:[port]"
http://127.0.0.1:4040