تعریف تزریق وابستگی تنظیمات برنامه
در مطلب «تزریق وابستگیها فراتر از کلاسها در برنامههای Angular» با روش تزریق ثوابت برنامه آشنا شدیم. در این مثال، برنامهی کلاینت بر روی پورت 4200 اجرا میشود و برنامهی سمت سرور وب، بر روی پورت 5000. به همین جهت نیاز است این آدرس پایه سمت سرور را در تمام قسمتهای برنامه که با سرور کار میکنند، در دسترس داشته باشیم و روش مناسب برای پیاده سازی آن همان قسمت «تزریق تنظیمات برنامه توسط تامین کنندهی مقادیر» مطلب یاد شدهاست. به همین جهت فایل جدید src\app\core\services\app.config.ts را در پوشهی core\services برنامه ایجاد میکنیم:
import { InjectionToken } from "@angular/core"; export let APP_CONFIG = new InjectionToken<string>("app.config"); export interface IAppConfig { apiEndpoint: string; loginPath: string; logoutPath: string; refreshTokenPath: string; accessTokenObjectKey: string; refreshTokenObjectKey: string; } export const AppConfig: IAppConfig = { apiEndpoint: "http://localhost:5000/api", loginPath: "account/login", logoutPath: "account/logout", refreshTokenPath: "account/RefreshToken", accessTokenObjectKey: "access_token", refreshTokenObjectKey: "refresh_token" };
سپس تنظیمات ابتدایی تزریق وابستگیهای IAppConfig را در فایل src\app\core\core.module.ts به صورت ذیل انجام میدهیم:
import { AppConfig, APP_CONFIG } from "./app.config"; @NgModule({ providers: [ { provide: APP_CONFIG, useValue: AppConfig } ] }) export class CoreModule {}
طراحی سرویس Auth
پس از لاگین باید بتوان به اطلاعات اطلاعات کاربر وارد شدهی به سیستم، در تمام قسمتهای برنامه دسترسی پیدا کرد. به همین جهت نیاز است این اطلاعات را در یک سرویس سراسری singleton قرار داد تا همواره یک وهلهی از آن در کل برنامه قابل استفاده باشد. مرسوم است این سرویس را AuthService بنامند. بنابراین محل قرارگیری این سرویس سراسری در پوشهی Core\services و محل تعریف آن در قسمت providers آن خواهد بود. به همین جهت ابتدا ساختار این سرویس را با دستور ذیل ایجاد میکنیم:
ng g s Core/services/Auth
create src/app/Core/services/auth.service.ts (110 bytes)
import { AuthService } from "./services/auth.service"; @NgModule({ providers: [ // global singleton services of the whole app will be listed here. BrowserStorageService, AuthService, { provide: APP_CONFIG, useValue: AppConfig } ] }) export class CoreModule {}
در ادامه به تکمیل AuthService خواهیم پرداخت و قسمتهای مختلف آنرا مرور میکنیم.
اطلاع رسانی به کامپوننت Header در مورد وضعیت لاگین
در مطلب «صدور رخدادها از سرویسها به کامپوننتها در برنامههای Angular» با نحوهی کار با BehaviorSubject آشنا شدیم. در اینجا میخواهیم توسط آن، پس از لاگین موفق، وضعیت لاگین را به کامپوننت هدر صادر کنیم، تا لینک لاگین را مخفی کرده و لینک خروج از سیستم را نمایش دهد:
import { BehaviorSubject } from "rxjs/BehaviorSubject"; @Injectable() export class AuthService { private authStatusSource = new BehaviorSubject<boolean>(false); authStatus$ = this.authStatusSource.asObservable(); constructor() { this.updateStatusOnPageRefresh(); } private updateStatusOnPageRefresh(): void { this.authStatusSource.next(this.isLoggedIn()); }
در اینجا در سازندهی کلاس، بر اساس خروجی متد وضعیت لاگین شخص، برای اولین بار، متد next این BehaviorSubject فراخوانی میشود. علت قرار دادن این متد در سازندهی کلاس سرویس، عکس العمل نشان دادن به refresh کامل صفحه، توسط کاربر است و یا عکس العمل نشان دادن به وضعیت بهخاطر سپاری کلمهی عبور، در اولین بار مشاهدهی سایت و برنامه. در این حالت متد isLoggedIn، کش مرورگر را بررسی کرده و با واکشی توکنها و اعتبارسنجی آنها، گزارش وضعیت لاگین را ارائه میدهد. پس از آن، خروجی آن (true/false) به مشترکین اطلاع رسانی میشود.
در ادامه، متد next این BehaviorSubject را در متدهای login و logout نیز فراخوانی خواهیم کرد.
تدارک ذخیره سازی توکنها در کش مرورگر
از طرف سرور، دو نوع توکن access_token و refresh_token را دریافت میکنیم. به همین جهت یک enum را جهت مشخص سازی آنها تعریف خواهیم کرد:
export enum AuthTokenType { AccessToken, RefreshToken }
import { BrowserStorageService } from "./browser-storage.service"; export enum AuthTokenType { AccessToken, RefreshToken } @Injectable() export class AuthService { private rememberMeToken = "rememberMe_token"; constructor(private browserStorageService: BrowserStorageService) { } rememberMe(): boolean { return this.browserStorageService.getLocal(this.rememberMeToken) === true; } getRawAuthToken(tokenType: AuthTokenType): string { if (this.rememberMe()) { return this.browserStorageService.getLocal(AuthTokenType[tokenType]); } else { return this.browserStorageService.getSession(AuthTokenType[tokenType]); } } deleteAuthTokens() { if (this.rememberMe()) { this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.AccessToken]); this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.RefreshToken]); } else { this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.AccessToken]); this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.RefreshToken]); } this.browserStorageService.removeLocal(this.rememberMeToken); } private setLoginSession(response: any): void { this.setToken(AuthTokenType.AccessToken, response[this.appConfig.accessTokenObjectKey]); this.setToken(AuthTokenType.RefreshToken, response[this.appConfig.refreshTokenObjectKey]); } private setToken(tokenType: AuthTokenType, tokenValue: string): void { if (this.rememberMe()) { this.browserStorageService.setLocal(AuthTokenType[tokenType], tokenValue); } else { this.browserStorageService.setSession(AuthTokenType[tokenType], tokenValue); } } }
- متد rememberMe مشخص میکند که آیا وضعیت بهخاطر سپاری کلمهی عبور توسط کاربر انتخاب شدهاست یا خیر؟ این مقدار را نیز در local storage ماندگار ذخیره میکنیم تا در صورت بستن مرورگر و مراجعهی مجدد به آن، در دسترس باشد و به صورت خودکار پاک نشود.
- متد setToken، بر اساس وضعیت rememberMe، مقادیر توکنهای دریافتی از سرور را در local storage و یا session storage ذخیره میکند.
- متد getRawAuthToken بر اساس یکی از مقادیر enum ارسالی به آن، مقدار خام access_token و یا refresh_token ذخیره شده را بازگشت میدهد.
- متد deleteAuthTokens جهت حذف تمام توکنهای ذخیره شدهی توسط برنامه استفاده خواهد شد. نمونهی کاربرد آن در متد logout است.
- متد setLoginSession پس از لاگین موفق فراخوانی میشود. کار آن ذخیره سازی توکنهای دریافتی از سرور است. فرض آن نیز بر این است که خروجی json از طرف سرور، توکنها را با کلیدهایی دقیقا مساوی access_token و refresh_token بازگشت میدهد:
{"access_token":"...","refresh_token":"..."}
تکمیل متد ورود به سیستم
در صفحهی لاگین، کاربر نام کاربری، کلمهی عبور و همچنین گزینهی «بهخاطر سپاری ورود» را باید تکمیل کند. به همین جهت اینترفیسی را برای این کار به نام Credentials در محل src\app\core\models\credentials.ts ایجاد میکنیم:
export interface Credentials { username: string; password: string; rememberMe: boolean; }
@Injectable() export class AuthService { constructor( @Inject(APP_CONFIG) private appConfig: IAppConfig, private http: HttpClient, private browserStorageService: BrowserStorageService ) { this.updateStatusOnPageRefresh(); } login(credentials: Credentials): Observable<boolean> { const headers = new HttpHeaders({ "Content-Type": "application/json" }); return this.http .post(`${this.appConfig.apiEndpoint}/${this.appConfig.loginPath}`, credentials, { headers: headers }) .map((response: any) => { this.browserStorageService.setLocal(this.rememberMeToken, credentials.rememberMe); if (!response) { this.authStatusSource.next(false); return false; } this.setLoginSession(response); this.authStatusSource.next(true); return true; }) .catch((error: HttpErrorResponse) => Observable.throw(error)); } }
در اینجا نیاز است اطلاعات شیء Credentials را به مسیر http://localhost:5000/api/account/login ارسال کنیم. به همین جهت نیاز به سرویس IAppConfig تزریق شدهی در سازندهی کلاس وجود دارد تا با دسترسی به this.appConfig.apiEndpoint، مسیر تنظیم شدهی در فایل src\app\core\services\app.config.ts را دریافت کنیم.
پس از لاگین موفق:
- ابتدا وضعیت rememberMe انتخاب شدهی توسط کاربر را در local storage مرورگر جهت مراجعات آتی ذخیره میکنیم.
- سپس متد setLoginSession، توکنهای دریافتی از شیء response را بر اساس وضعیت rememberMe در local storage ماندگار و یا session storage فرار، ذخیره میکند.
- در آخر با فراخوانی متد next مربوط به authStatusSource با پارامتر true، به تمام کامپوننتهای مشترک به این سرویس اعلام میکنیم که وضعیت لاگین موفق بودهاست و اکنون میتوانید نسبت به آن عکس العمل نشان دهید.
تکمیل متد خروج از سیستم
کار خروج، با فراخوانی متد logout صورت میگیرد:
@Injectable() export class AuthService { constructor( @Inject(APP_CONFIG) private appConfig: IAppConfig, private http: HttpClient, private router: Router ) { this.updateStatusOnPageRefresh(); } logout(navigateToHome: boolean): void { this.http .get(`${this.appConfig.apiEndpoint}/${this.appConfig.logoutPath}`) .finally(() => { this.deleteAuthTokens(); this.unscheduleRefreshToken(); this.authStatusSource.next(false); if (navigateToHome) { this.router.navigate(["/"]); } }) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { console.log("logout", result); }); } }
اعتبارسنجی وضعیت لاگین و توکنهای ذخیره شدهی در مرورگر
برای اعتبارسنجی access token دریافتی از طرف سرور، نیاز به بستهی jwt-decode است. به همین جهت دستور ذیل را در خط فرمان صادر کنید تا بستهی آن به پروژه اضافه شود:
> npm install jwt-decode --save
import * as jwt_decode from "jwt-decode";
getDecodedAccessToken(): any { return jwt_decode(this.getRawAuthToken(AuthTokenType.AccessToken)); }
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
getDisplayName(): string { return this.getDecodedAccessToken().DisplayName; }
getAccessTokenExpirationDateUtc(): Date { const decoded = this.getDecodedAccessToken(); if (decoded.exp === undefined) { return null; } const date = new Date(0); // The 0 sets the date to the epoch date.setUTCSeconds(decoded.exp); return date; }
isAccessTokenTokenExpired(): boolean { const expirationDateUtc = this.getAccessTokenExpirationDateUtc(); if (!expirationDateUtc) { return true; } return !(expirationDateUtc.valueOf() > new Date().valueOf()); }
isLoggedIn(): boolean { const accessToken = this.getRawAuthToken(AuthTokenType.AccessToken); const refreshToken = this.getRawAuthToken(AuthTokenType.RefreshToken); const hasTokens = !this.isEmptyString(accessToken) && !this.isEmptyString(refreshToken); return hasTokens && !this.isAccessTokenTokenExpired(); } private isEmptyString(value: string): boolean { return !value || 0 === value.length; }
در قسمت بعد، از این سرویس اعتبارسنجی تکمیل شده جهت ورود به سیستم و تکمیل کامپوننت header استفاده خواهیم کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
[Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching", }; // /WeatherForecast/GetForecast2?from=1&to=3 [HttpGet("[action]")] public IEnumerable<WeatherForecast> GetForecast2(Duration days) { return Enumerable.Range(days.From, days.To) .Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)], }) .ToArray(); } }
public class Duration { public int From { get; set; } public int To { get; set; } } public class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string? Summary { get; set; } }
روش دیگر پردازش اطلاعات رشتهای رسیده و تشکیل یک Model Binder سفارشی در ASP.NET Core 7x
اکنون فرض کنید بجای آدرس WeatherForecast/GetForecast2?from=1&to=3 که اطلاعات را از طریق کوئری استرینگ مشخص و استانداردی دریافت میکند، میخواهیم اطلاعات آنرا از طریق یک قالب سفارشی و غیراستاندارد مانند WeatherForecast/GetForecast3/1-3 دریافت کنیم:
// /WeatherForecast/GetForecast3/1-3 [HttpGet("[action]/{days}")] public IEnumerable<WeatherForecast> GetForecast3(Days days) { return Enumerable.Range(days.From, days.To) .Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)], }) .ToArray(); }
using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace NET7Mvc.Models; public class Days : IParsable<Days> { public Days(int from, int to) { From = from; To = to; } public int From { get; } public int To { get; } public static bool TryParse([NotNullWhen(true)] string? value, IFormatProvider? provider, [MaybeNullWhen(false)] out Days result) { var items = value?.Split('-', StringSplitOptions.RemoveEmptyEntries); if (items is { Length: 2 }) { if (int.TryParse(items[0], NumberStyles.None, provider, out var from) && int.TryParse(items[1], NumberStyles.None, provider, out var to)) { result = new Days(from, to); return true; } } result = default; return false; } public static Days Parse(string value, IFormatProvider? provider) { if (!TryParse(value, provider, out var result)) { throw new ArgumentException("Could not parse the given value.", nameof(value)); } return result; } }
- همینقدر که مدلی IParsable را پیاده سازی کرده باشد، از امکانات آن به صورت خودکار استفاده خواهد شد و نیازی به معرفی و یا تنظیمات خاص دیگری ندارد.
- البته این قابلیت جدید نیست و پشتیبانی از IParsable، پیشتر در Minimal API دات نت 6 ارائه شده بود؛ اما در دات نت 7 توسط ASP.NET Core MVC نیز قابل استفاده شدهاست.
C# 8.0 - Nullable Reference Types
پس از نصب SDK جدید، نحوهی فعالسازی این قابلیت در فایل csproj، به صورت زیر درآمدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> </Project>
این موارد در Preview 7 جدید هستند:
1) امکان اضافه کردن قید جدید notnull در حین تعریف نوعهای جنریک
interface IDoStuff<TIn, TOut> where TIn : notnull where TOut : notnull { TOut DoStuff(TIn input); }
2) اضافه شدن یک سری ویژگی توکار جدید برای «پیش» بررسی کار با نوعهای ارجاعی نال نپذیر
ویژگی AllowNull به فراخوانها امکان ارسال نال را حتی اگر مجاز نباشد، میدهد. این ویژگی جدید در فضای نام System.Diagnostics.CodeAnalysis تعریف شدهاست. برعکس آن ویژگی DisallowNull، سبب خواهد شد تا فراخوانها حتی در صورت مجاز بودن نیز نتوانند نال ارسال کنند.
using System; using System.Diagnostics.CodeAnalysis; namespace ConsoleApp { public class MyClass { private string _innerValue = string.Empty; [AllowNull] public string MyValue { get { return _innerValue; } set { _innerValue = value ?? string.Empty; } } }
دو ویژگی یاد شده را میتوان بر روی پارامترهای متدها، پارامترهایی از نوع in و ref، فیلدها، خواص و ایندکسرها اعمال کرد.
3) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی کار با نوعهای ارجاعی نال نپذیر
دو ویژگی جدید MaybeNull و NotNull کار پس بررسی نال پذیری را انجام میدهند:
public class MyArray { // Result is the default of T if no match is found [return: MaybeNull] public static T Find<T>(T[] array, Func<T, bool> match) { //... } // Never gives back a null when called public static void Resize<T>([NotNull] ref T[]? array, int newSize) { //... } }
در متد Resize، پارامتر array، میتواند نال دریافت کند، چون نالپذیر تعریف شدهاست؛ اما چون به ویژگی NotNull مزین است، حاصل تغییرات بر روی آن (خروجی از متد، از طریق پارامتری از نوع ref) نمیتواند نال باشد.
دو ویژگی یاد شده را میتوان بر روی خروجی متدها، پارامترهایی از نوع out و ref، فیلدها، خواص و ایندکسرها اعمال کرد.
4) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی «شرطی» کار با نوعهای ارجاعی نال نپذیر
در مثالهای زیر کاربردهای دو ویژگی شرطی جدید NotNullWhen و MaybeNullWhen را مشاهده میکنید:
public class MyString { // True when 'value' is null public static bool IsNullOrEmpty([NotNullWhen(false)] string? value) { //... } }
public class MyVersion { // If it parses successfully, the Version will not be null. public static bool TryParse(string? input, [NotNullWhen(true)] out Version? version) { //... } }
public class MyQueue<T> { // 'result' could be null if we couldn't Dequeue it. public bool TryDequeue([MaybeNullWhen(false)] out T result) { //... } }
5) اضافه شدن یک سری ویژگی توکار جدید برای شرط گذاشتن بین ورودی و خروجی، در حین کار با نوعهای ارجاعی نال نپذیر
در متد زیر، هم خروجی و هم ورودی آن میتوانند نال باشند. اما میخواهیم اگر path نال نباشد، اطمینان حاصل کنیم که استفاده کننده میداند، خروجی این متد، حتما نال نخواهد بود:
class MyPath { [return: NotNullIfNotNull("path")] public static string? GetFileName(string? path) { //... } }
6) اضافه شدن یک سری ویژگی توکار جدید برای بررسی سیلان برنامه، در حین کار با نوعهای ارجاعی نال نپذیر
در اینجا نحوهی استفاده از دو ویژگی جدید DoesNotReturn و DoesNotReturnIf را مشاهده میکنید:
internal static class ThrowHelper { [DoesNotReturn] public static void ThrowArgumentNullException(ExceptionArgument arg) { //... } }
public static class MyAssertionLibrary { public static void MyAssert([DoesNotReturnIf(false)] bool condition) { //... } }
از زمانیکه دات نت فریم ورک ارائه شده (حدودا 8 سال یا بیشتر اگر بتای آنرا هم به حساب بیاوریم به سال 2000 بر میگردد)، نگارشهای متفاوتی تا به امروز در اختیار عموم قرار گرفته اند.
جدول زیر این موارد را تا این تاریخ لیست کرده و شماره نگارش دقیق آنها را نیز بر میشمارد:
.NET version | Actual version |
3.5 SP1 | 3.5.30729.1 |
3.5 | 3.5.21022.8 |
3.0 SP2 | 3.0.4506.2152 |
3.0 SP1 | 3.0.4506.648 |
3.0 | 3.0.4506.30 |
2.0 SP2 | 2.0.50727.3053 |
2.0 SP1 | 2.0.50727.1433 |
2.0 | 2.0.50727.42 |
1.1 SP1 | 1.1.4322.2032 |
1.1 SP1 (in 32 bit version of Windows 2003) | 1.1.4322.2300 |
1.1 | 1.1.4322.573 |
1.0 SP3 | 1.0.3705.6018 |
1.0 SP2 | 1.0.3705.288 |
1.0 SP1 | 1.0.3705.209 |
1.0 | 1.0.3705.0 |
برای بدست آوردن شماره نگارشهای نصب شده بر روی یک کامپیوتر متاسفانه راه سادهای وجود ندارد. امکاناتی هم که خود دات نت فریم ورک به صورت ذاتی ارائه میدهد به صورت زیر است:
class NetVersion
{
public static string Version
{
get
{
return Environment.Version + "\n" +
Environment.OSVersion;
}
}
}
که خروجی آن فقط آخرین نگارش CLR را شامل میشود.
برای مثال روی ویندوز اکس پی سرویس پک 3 با دات نت فریم ورک سه و نیم، سرویس پک یک خواهیم داشت:
2.0.50727.3053
Microsoft Windows NT 5.1.2600 Service Pack 3
خوبی این روش هم این است که اگر در یک هاست اینترنتی قصد داشتید شماره نگارش دات نت فریم ورک سرور را بررسی کنید، بدون مشکل پاسخ خواهد داد. برای مثال اگر به دات نت فریم ورک 2 سرویس پک 2 رسیدید، یعنی دات نت فریم ورک سه و نیم، سرویس پک یک حتما روی سرور نصب است، چون این دو با هم ارائه شدهاند و به صورت مجزا ارائه نشدهاند.
برای بدست آوردن لیست دات نت فریم ورکهای مختلف باید به رجیستری ویندوز مراجعه کرد. مسیرهای:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\5.0\User Agent\Post Platform
این روش یا حتی لیست کردن فولدرهای نصب شد در مسیر C:\WINDOWS\Microsoft.NET\Framework نیز شماره نگارش کامل را ارائه نمیدهند. تنها راه باقیمانده مراجعه به فایل mscorlib.dll هر پوشه و بررسی نگارش آن است:
اینکار را با برنامه نویسی به صورت زیر میتوان انجام داد:
public static string MscorlibVersion
{
get
{
//using System.Diagnostics;
FileVersionInfo myFileVersionInfo = FileVersionInfo.GetVersionInfo(
Environment.GetEnvironmentVariable("windir") +
@"\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll");
return myFileVersionInfo.ProductVersion;
}
}
Build Real App in Angular 10 and ASP.Net Web API
These are 2 of the hottest frameworks right now for the ‘back-end’ (Microsoft’s ASP.NET Core) and the ‘front-end’ (Google’s Angular) and are well worth spending the time to learn.
This course starts from scratch, you neither need to know Angular 1 nor Angular 2
We will start from nothing and incrementally build this property dealing application front-end using Angular 10.
And then we will connect our front-end with the Web-API until we have a fully functional Web Application that we will publish to Firebase and then on IIS.