var state = Battery.State; // Charging, Full, Discharging, ...
var state = Battery.State; // Charging, Full, Discharging, ...
"ApplicationName": "Dependency Injection Demo", "GreetingMessage": "Welcome to Dependency Injection Demo", "AllowedHosts": "*", "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } },
برای سادگی کار، با بخش Logging
کاری نداریم . اکنون فایل AppConfig.cs را به برنامه اضافه میکنیم:
namespace AspNetCoreDependencyInjection.Configs { public class AppConfig { public string ApplicationName { get; set; } public string GreetingMessage { get; set; } public string AllowedHosts { get; set; } } }
برای دسترسی بهتر میتوانیم سازندهی کلاس Startup را تغییر دهیم:
public IWebHostEnvironment Environment { get; } public IConfiguration Configuration { get; } public IServiceCollection Services { get; set; } public Startup(IWebHostEnvironment environment) { var builder = new ConfigurationBuilder() .SetBasePath(environment.ContentRootPath) .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables(); this.Environment = environment; this.Configuration = builder.Build(); }
var appSettingsFile = environment.IsProduction() ? "appsettings.json" : "appsettings_dev.json"; var builder = new ConfigurationBuilder() .SetBasePath(environment.ContentRootPath) .AddJsonFile( appSettingsFile , optional: true) .AddEnvironmentVariables();
services.AddSingleton(services => new AppConfig { ApplicationName = this.Configuration["ApplicationName"], GreetingMessage = this.Configuration["GreetingMessage"], AllowedHosts = this.Configuration["AllowedHosts"] });
در کد بالا در هنگام اجرای برنامه، یک نمونه از کلاس AppConfig را با طول حیات Singleton ثبت کردیم و Property های این شیء را به وسیلهی ایندکس Configuration[“FieldName”]، تک تک پر کردیم.
حالا میتوانیم سرویس AppConfig را
در هر کلاسی از برنامهی خودمان تزریق و از آن استفاده کنیم. برای مثل در اینجا یک
کنترلر به نام AppSettingsController ساختم و کلاس فوق را به آن تزریق کردم:
public class AppSettingsController : Controller { private readonly AppConfig _appConfig; public AppSettingsController(AppConfig appConfig) { _appConfig = appConfig; } // codes here … }
"UserOptionConfig": { "UsersAvatarsFolder": "avatars", "UserDefaultPhoto": "icon-user-default.png", "UserAvatarImageOptions": { "MaxWidth": 150, "MaxHeight": 150 } }, "LiteDbConfig": { "ConnectionString": "Filename=\\Data\\DependencyInjectionDemo.db;Connection=direct;Password=@123456;" }
برای LiteDbConfig
مانند AppConfig عمل میکنیم، ولی در هنگام ثبت آن، به روش زیر عمل میکنیم. تنها تفاوتی
که وجود دارد، نحوهی دستیابی به فیلدهای درونی فایل JSON به وسیلهی شیء Configuration
است:
services.AddSingleton(services => new LiteDbConfig { ConnectionString = this.Configuration["LiteDbConfig:ConnectionString"], });
اکنون برای استفادهی از مدخل UserOptionConfig،
کلاسهای زیر را میسازیم:
namespace AspNetCoreDependencyInjection.Configs { public class UserOptionConfig { public string UsersAvatarsFolder { get; set; } public string UserDefaultPhoto { get; set; } public UserAvatarImageOptions UserAvatarImageOptions { get; set; } } public class UserAvatarImageOptions { public int MaxHeight { get; set; } public int MaxWidth { get; set; } } }
جداسازی بخشهای مختلف تنظیمات پیکربندی باعث میشود تا بتوانیم دو اصل اساسی از طراحی نرم افزار را رعایت کنیم :
در اینجا نیاز به استفاده از پکیج Microsoft.Extensions.Options.ConfigurationExtensions را داریم که به صورت درونی در ASP.NET Core تعبیه شده است.
برای ثبت این تنظیمات درون DI Container، از نمونهی جنریک متد Configure در IServiceCollection به صورت زیر استفاده میکنیم:
services.Configure<UserOptionConfig>(this.Configuration.GetSection("UserOptionConfig"));
متد GetSection بر اساس نام بخش تنظیمات، خود آن تنظیم و تمامی تنظیمات درونی آن را به صورت یک IConfigurationSection بر میگرداند و متد Configure<TOption> یک IConfiguration را گرفته و به صورت خودکار به TOption اتصال میدهد و سپس این شیء را درون DI Container به عنوان یک IConfigurationOptions<TOption> و با طول حیات Singleton ثبت میکند.
برای دسترس به UserOptionConfig درون کلاس مورد نظر ما، اینترفیس <IOptionMonitor<TOption را به سازندهی کلاس مورد نظر تزریق میکنیم. کد زیر را که نسخهی تغییر یافتهی کلاس AppSettingsController است را مشاهده کنید:private readonly LiteDbConfig _liteDbConfig; private readonly AppConfig _appConfig; private readonly UserOptionConfig _userOptionConfig; public AppSettingsController(AppConfig appConfig , LiteDbConfig liteDbConfig , IOptionsMonitor<UserOptionConfig> userOptionConfig) { _appConfig = appConfig; _liteDbConfig = liteDbConfig; _userOptionConfig = userOptionConfig.CurrentValue; }
نکته ای که وجود دارد، کلاسهای تعریف شده برای استفادهی از این الگو باید شرایط زیر را داشته باشند ( مثل کلاس UserOptionConfig ) :
هر دو روش بالا که یکی به
صورت عادی تنظیمات را ثبت میکند و دیگری با استفاده از Option Pattern بخشهای مختلف را ثبت میکند،
مناسب هستند. البته گاهی اوقات فایلهای تنظیمات پروژهی شما در لایههای زیرین (یا درونیتر اگر از onion architecture استفاده میکنید) قرار دارند و شما نمیخواهید
در آن لایهها و لایههای درونیتر، وابستگی به پکیجهای ASP.NET Core ایجاد کنید. در این حالت با در
نظر گرفتن دو اصل ISP و Separation of Concerns ،
به ازای هر بخش مختلف از تنظیمات، فایلهای تنظیمات را در لایههای زیرین/درونی
تعریف کرده، بعد در لایههای بالاتر/بیرونیتر آنها را به درون سرویسها یا کلاسهای مورد نیاز، تزریق کنید. البته مثل همین مثال، ثبت این سرویسها درون برنامهی ASP.NET Core که
معمولا بالاترین/بیرونیترین لایه از پروژهی ما هست، انجام میشود.
using System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Data.Objects; using System.Diagnostics; using System.Linq; using System.Web; using System.Web.Caching; namespace EfSecondLevelCaching.Core { public static class EfHttpRuntimeCacheProvider { #region Methods (6) // Public Methods (2) public static IList<TEntity> ToCacheableList<TEntity>( this IQueryable<TEntity> query, int durationMinutes = 15, CacheItemPriority priority = CacheItemPriority.Normal) { return query.Cacheable(x => x.ToList(), durationMinutes, priority); } /// <summary> /// Returns the result of the query; if possible from the cache, otherwise /// the query is materialized and the result cached before being returned. /// The cache entry has a one minute sliding expiration with normal priority. /// </summary> public static TResult Cacheable<TEntity, TResult>( this IQueryable<TEntity> query, Func<IQueryable<TEntity>, TResult> materializer, int durationMinutes = 15, CacheItemPriority priority = CacheItemPriority.Normal) { // Gets a cache key for a query. var queryCacheKey = query.GetCacheKey(); // The name of the cache key used to clear the cache. All cached items depend on this key. var rootCacheKey = typeof(TEntity).FullName; // Try to get the query result from the cache. printAllCachedKeys(); var result = HttpRuntime.Cache.Get(queryCacheKey); if (result != null) { debugWriteLine("Fetching object '{0}__{1}' from the cache.", rootCacheKey, queryCacheKey); return (TResult)result; } // Materialize the query. result = materializer(query); // Adding new data. debugWriteLine("Adding new data: queryKey={0}, dependencyKey={1}", queryCacheKey, rootCacheKey); storeRootCacheKey(rootCacheKey); HttpRuntime.Cache.Insert( key: queryCacheKey, value: result, dependencies: new CacheDependency(null, new[] { rootCacheKey }), absoluteExpiration: DateTime.Now.AddMinutes(durationMinutes), slidingExpiration: Cache.NoSlidingExpiration, priority: priority, onRemoveCallback: null); return (TResult)result; } /// <summary> /// Call this method in `public override int SaveChanges()` of your DbContext class /// to Invalidate Second Level Cache automatically. /// </summary> public static void InvalidateSecondLevelCache(this DbContext ctx) { var changedEntityNames = ctx.ChangeTracker .Entries() .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified || x.State == EntityState.Deleted) .Select(x => ObjectContext.GetObjectType(x.Entity.GetType()).FullName) .Distinct() .ToList(); if (!changedEntityNames.Any()) return; printAllCachedKeys(); foreach (var item in changedEntityNames) { item.removeEntityCache(); } printAllCachedKeys(); } // Private Methods (4) private static void debugWriteLine(string format, params object[] args) { if (!Debugger.IsAttached) return; Debug.WriteLine(format, args); } private static void printAllCachedKeys() { if (!Debugger.IsAttached) return; debugWriteLine("Available cached keys list:"); int count = 0; var enumerator = HttpRuntime.Cache.GetEnumerator(); while (enumerator.MoveNext()) { if (enumerator.Key.ToString().StartsWith("__")) continue; // such as __System.Web.WebPages.Deployment debugWriteLine("queryKey: {0}", enumerator.Key.ToString()); count++; } debugWriteLine("count: {0}", count); } private static void removeEntityCache(this string rootCacheKey) { if (string.IsNullOrWhiteSpace(rootCacheKey)) return; debugWriteLine("Removing items with dependencyKey={0}", rootCacheKey); // Removes all cached items depend on this key. HttpRuntime.Cache.Remove(rootCacheKey); } private static void storeRootCacheKey(string rootCacheKey) { // The cacheKeys of a cacheDependency that are not already in cache ARE NOT inserted into the cache // on the Insert of the item in which the dependency is used. if (HttpRuntime.Cache.Get(rootCacheKey) != null) return; HttpRuntime.Cache.Add( rootCacheKey, rootCacheKey, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, null); } #endregion Methods } }
var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault());
using System.Data.Entity; using EfSecondLevelCaching.Core; using EfSecondLevelCaching.Test.Models; namespace EfSecondLevelCaching.Test.DataLayer { public class ProductContext : DbContext { public DbSet<Product> Products { get; set; } public override int SaveChanges() { this.InvalidateSecondLevelCache(); return base.SaveChanges(); } } }
using System.Data.Entity; using System.Linq; using EfSecondLevelCaching.Core; using EfSecondLevelCaching.Test.DataLayer; using EfSecondLevelCaching.Test.Models; using System; namespace EfSecondLevelCaching { public static class TestUsages { public static void RunQueries() { using (ProductContext context = new ProductContext()) { var isActive = true; var name = "Product1"; // reading from db var list1 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from cache var list2 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from cache var list3 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from db var list4 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == "Product2") .ToCacheableList(); } // removes products cache using (ProductContext context = new ProductContext()) { var p = new Product() { IsActive = false, ProductName = "P4", ProductNumber = "004" }; context.Products.Add(p); context.SaveChanges(); } using (ProductContext context = new ProductContext()) { var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault()); var data2 = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault()); context.SaveChanges(); } } } }
Adding new data: queryKey=72AF5DA1BA9B91E24DCCF83E88AD1C5F, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache. Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache. Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Adding new data: queryKey=11A2C33F9AD7821A0A31003BFF1DF886, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F queryKey: 11A2C33F9AD7821A0A31003BFF1DF886 queryKey: EfSecondLevelCaching.Test.Models.Product count: 3 Removing items with dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: count: 0 Available cached keys list: count: 0 Adding new data: queryKey=02E6FE403B461E45C5508684156C1D10, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: 02E6FE403B461E45C5508684156C1D10 queryKey: EfSecondLevelCaching.Test.Models.Product count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__02E6FE403B461E45C5508684156C1D10' from the cache.
data-val-remote="کلمه عبور وارد شده را راحت می‌توان حدس زد!" data-val-remote-additionalfields="*.Password1" data-val-remote-type="POST" data-val-remote-url="/register/checkpassword"
export class EmailValidatorDirective implements Validator {
export class RemoteValidatorDirective implements AsyncValidator {
validate(c: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;
>ng g d CustomValidators/RemoteValidator -m custom-validators.module
import { Directive, Input } from "@angular/core"; import { AsyncValidator, AbstractControl, NG_ASYNC_VALIDATORS } from "@angular/forms"; import { Http, RequestOptions, Response, Headers } from "@angular/http"; import "rxjs/add/operator/map"; import "rxjs/add/operator/distinctUntilChanged"; import "rxjs/add/operator/takeUntil"; import "rxjs/add/operator/take"; import { Observable } from "rxjs/Observable"; import { Subject } from "rxjs/Subject"; @Directive({ selector: "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]", providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: RemoteValidatorDirective, multi: true } ] }) export class RemoteValidatorDirective implements AsyncValidator { @Input("remote-url") remoteUrl: string; @Input("remote-field") remoteField: string; @Input("remote-additional-fields") remoteAdditionalFields: string; constructor(private http: Http) {} validate(control: AbstractControl): Observable<{ [key: string]: any }> { if (!this.remoteUrl || this.remoteUrl === undefined) { return Observable.throw("`remoteUrl` is undefined."); } if (!this.remoteField || this.remoteField === undefined) { return Observable.throw("`remoteField` is undefined."); } const dataObject = {}; if ( this.remoteAdditionalFields && this.remoteAdditionalFields !== undefined ) { const otherFields = this.remoteAdditionalFields.split(","); otherFields.forEach(field => { const name = field.trim(); const otherControl = control.root.get(name); if (otherControl) { dataObject[name] = otherControl.value; } }); } // This is used to signal the streams to terminate. const changed$ = new Subject<any>(); changed$.next(); // This will signal the previous stream (if any) to terminate. const debounceTime = 400; return new Observable((obs: any) => { control.valueChanges .takeUntil(changed$) .take(1) .debounceTime(debounceTime) .distinctUntilChanged() .flatMap(term => { dataObject[this.remoteField] = term; return this.doRemoteValidation(dataObject); }) .subscribe( (result: IRemoteValidationResult) => { if (result.result) { obs.next(null); } else { obs.next({ remoteValidation: { remoteValidationMessage: result.message } }); } obs.complete(); }, error => { obs.next(null); obs.complete(); } ); }); } private doRemoteValidation(data: any): Observable<IRemoteValidationResult> { const headers = new Headers({ "Content-Type": "application/json" }); // for ASP.NET MVC const options = new RequestOptions({ headers: headers }); return this.http .post(this.remoteUrl, JSON.stringify(data), options) .map(this.extractData) .do(result => console.log("remoteValidation result: ", result)) .catch(this.handleError); } private extractData(res: Response): IRemoteValidationResult { const body = <IRemoteValidationResult>res.json(); return body || (<IRemoteValidationResult>{}); } private handleError(error: Response): Observable<any> { console.error("observable error: ", error); return Observable.throw(error.statusText); } } export interface IRemoteValidationResult { result: boolean; message: string; }
@Directive({ selector: "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]", providers: [ { provide: NG_ASYNC_VALIDATORS, useExisting: RemoteValidatorDirective, multi: true } ] })
export class RemoteValidatorDirective implements AsyncValidator { @Input("remote-url") remoteUrl: string; @Input("remote-field") remoteField: string; @Input("remote-additional-fields") remoteAdditionalFields: string;
<input #username="ngModel" required maxlength="8" minlength="4" type="text" appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName" remote-additional-fields="email,password" class="form-control" name="username" [(ngModel)]="model.username">
export class UserRegisterComponent implements OnInit { remoteUsernameValidationUrl = "api/Employee/CheckUser";
[remote-field]="'FirstName'"
export interface IRemoteValidationResult { result: boolean; message: string; }
namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class EmployeeController : Controller { [HttpPost("[action]")] [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult CheckUser([FromBody] Employee model) { var remoteValidationResult = new { result = true, message = $"{model.FirstName} is fine!" }; if (model.FirstName?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false) { remoteValidationResult = new { result = false, message = "username:`Vahid` is already taken." }; } return Json(remoteValidationResult); } } }
<div class="form-group" [class.has-error]="username.invalid && username.touched"> <label class="control-label">User Name</label> <input #username="ngModel" required maxlength="8" minlength="4" type="text" appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName" remote-additional-fields="email,password" class="form-control" name="username" [(ngModel)]="model.username"> <div *ngIf="username.pending" class="alert alert-warning"> Checking server, Please wait ... </div> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-danger" *ngIf="username.errors.remoteValidation"> {{username.errors.remoteValidation.remoteValidationMessage}} </div> </div> </div>
obs.next({ remoteValidation: { remoteValidationMessage: result.message } });
// This is used to signal the streams to terminate. const changed$ = new Subject<any>(); changed$.next(); // This will signal the previous stream (if any) to terminate. const debounceTime = 400; return new Observable((obs: any) => { control.valueChanges .takeUntil(changed$) .take(1) .debounceTime(debounceTime) .distinctUntilChanged()
هم چنین در B*Db چهار روش برای دست یابی با دادهها (پیاده سازی عملیات CRUD) وجود دارد از قبیل:
pm> install-Package BirghtStarDb
PM> Install-Package BirghtStarDbLibs
[Entity] public interface IBook { public int Code { get; set; } public string Title { get; set; } }
[Entity] public interface IBook { [Identifier] public string MyId { get; } public int Code { get; set; } public string Title { get; set; } }
استفاده از اینترفیس برای ساخت مدل انعطاف پذیری بالایی را در اختیار ما قرار میدهد که بعدا مفصل بحث خواهد شد. برای عملیات درج داده میتوان به صورت زیر عمل کنید:
MyEntityContext context = new MyEntityContext("type=embedded;storesdirectory=c:\brightstar;storename=test"); var book = context.Books.Create(); book.Code = 1; book.Title = "Test"; context.Books.Add(book); context.SaveChanges();
C:\Users\Vahid>dotnet new --list Templates Short Name Language Tags -------------------------------------------- ------------------- ------------ ---------------------- Blazor Server App blazorserver [C#] Web/Blazor Blazor WebAssembly App blazorwasm [C#] Web/Blazor/WebAssembly