[MetadataType(typeof(CustomerMetadata))] public partial class Customer { class CustomerMetadata { } } public partial class Customer : IValidatableObject {
معرفی سرویس IActionDescriptorCollectionProvider در ASP.NET Core
فرض کنید میخواهیم لیست تمام کنترلرهای یک برنامهی ASP.NET Core را با ساختار ذیل تهیه کنیم که شامل نام کنترلر، نام اکشن متد و نام ناحیهی متناظر با آن (در صورت تنظیم) میباشد:
public class MvcActionViewModel { public string ControllerName { get; set; } public string ActionName { get; set; } public string AreaName { get; set; } }
public interface IMvcActionsDiscoveryService { ICollection<MvcActionViewModel> MvcActions { get; } } public class MvcActionsDiscoveryService : IMvcActionsDiscoveryService { public MvcActionsDiscoveryService(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) { var actionDescriptors = actionDescriptorCollectionProvider.ActionDescriptors.Items; foreach (var actionDescriptor in actionDescriptors) { var descriptor = actionDescriptor as ControllerActionDescriptor; if (descriptor == null) { continue; } var controllerTypeInfo = descriptor.ControllerTypeInfo; var actionMethodInfo = descriptor.MethodInfo; MvcActions.Add(new MvcActionViewModel { ControllerName = descriptor.ControllerName, ActionName = descriptor.ActionName, AreaName = controllerTypeInfo.GetCustomAttribute<AreaAttribute>()?.RouteValue }); } } public ICollection<MvcActionViewModel> MvcActions { get; } = new HashSet<MvcActionViewModel>(); }
- در کلاس آغازین برنامه نیازی به ثبت سرویس IActionDescriptorCollectionProvider نیست و اینکار پیشتر توسط خود ASP.NET Core انجام شدهاست.
- این provider حاوی لیست اطلاعات تمام اکشن متدهای ثبت شدهی توسط ASP.NET Core است. در اینجا تنها کافی است حلقهای را بر روی لیست آیتمهای آن تشکیل داده و سپس مقادیر ControllerName و یا ActionName را بدست بیاوریم.
- اگر نیاز به اطلاعات بیشتری از کنترلر و اکشن متد جاری در حال بررسی توسط حلقهی تهیه شده بود، میتوان از ControllerTypeInfo و MethodInfo آن استفاده کرد. این TypeInfoها با استفاده از Reflection، امکان دسترسی به اطلاعاتی مانند ویژگیهای اعمال شدهی به کنترلر یا اکشنی خاص را میسر میکنند. برای مثال در اینجا توسط اطلاعات نوع یک کنترلر در حال بررسی توانستهایم متد GetCustomAttribute را فراخوانی کرده و سپس بررسی کنیم که آیا دارای ویژگی جدید Area هست یا خیر؟ و اگر بله، مقدار RouteValue آن را که در حقیقت مقدار یا نام Area آن کنترلر است، بازگشت میدهیم.
نحوهی استفاده از سرویس IMvcActionsDiscoveryService تهیه شده
اگر دقت کرده باشید اطلاعات لیست MvcActions، در سازندهی این کلاس مقدار دهی شدهاند. علت اینجا است که اگر این کلاس را به صورت singleton ثبت کنیم، تنها یکبار در طول عمر برنامه و در همان آغاز کار، این لیست پر شده و سپس کش خواهد شد. بنابراین دسترسیهای بعدی به MvcActions، شامل فراخوانی سازندهی این کلاس نخواهند بود:
public static class MvcActionsDiscoveryServiceExtensions { public static IServiceCollection AddMvcActionsDiscoveryService(this IServiceCollection services) { services.AddSingleton<IMvcActionsDiscoveryService, MvcActionsDiscoveryService>(); return services; } }
public void ConfigureServices(IServiceCollection services) { services.AddMvcActionsDiscoveryService(); }
ماهیت این پایگاه داده وب سرویسی مبتنی بر REST است و فرمت اطلاعاتی که از سرور دریافت میشود، JSON است.
گام اول: باید آخرین نسخه RavenDB را دریافت کنید. همان طور که مشاهده میکنید، ویرایشهای مختلف کتابخانه هایی که برای نسخه Client و همچنین Server طراحی شده است، دراین فایل قرار گرفته است.
برای راه اندازی Server باید فایل Start را اجرا کنید، چند ثانیه بعد محیط مدیریتی آن را در مرورگر خود مشاهده میکنید. در بالای صفحه روی لینک Databases کلیک کنید و در صفحه باز شده گزینه New Database را انتخاب کنید. با دادن یک نام دلخواه حالا شما یک پایگاه داده ایجاد کرده اید. تا همین جا دست نگه دارید و اجازه دهید با این محیط دوست داشتنی و قابلیتهای آن بعدا آشنا شویم.
در گام دوم به Visual Studio میرویم و نحوه ارتباط با پایگاه داده و استفاده از دستورات آن را فرا میگیریم.
گام دوم:
با یک پروژه Test شروع میکنیم که در هر گام تکمیل میشود و میتوانید پروژه کامل را در پایان این پست دانلود کنید.
برای استفاده از کتابخانههای مورد نیاز دو راه وجود دارد:
- استفاده از NuGet : با استفاده از دستور زیر Package مورد نیاز به پروژه شما افزوده میشود.
PM> Install-Package RavenDB -Version 1.0.919
- اضافه کردن کتابخانهها به صورت دستی : کتابخانههای مورد نیاز شما در همان فایلی که دانلود شده بود و در پوشه Client قرار دارند.
کتابخانه هایی را که NuGet به پروژه من اضافه کرد، در تصویر زیر مشاهده میکنید :
با Newtonsoft.Json در اولین بخش بحث آشنا شدید. NLog هم یک کتابخانه قوی و مستقل برای مدیریت Log است که این پایگاه داده از آن بهره برده است.
" دلیل اینکه از پروژه تست استفاده کردم ؛ تمرکز روی کدها و مشاهده تاثیر آنها ، مستقل از UI و لایههای دیگر نرم افزار است. بدیهی است که استفاده از آنها در هر پروژه امکان پذیر است. "
برای شروع نیاز به آدرس Server و نام پایگاه داده داریم که میتوانید در App.config به عنوان تنظیمات نرم افزار شما ذخیره شود و هنگام اجرای نرم افزار مقدار آنها را خوانده و در متغییرهای readonly ذخیره شوند.
<appSettings> <add key="ServerName" value="http://SorousH-HP:8080/"/> <add key="DatabaseName" value="TestDatabase" /> </appSettings>
هنگامی که صفحه Management Studio در مرورگر باز است، میتوانید از نوار آدرس مرورگر خود آدرس سرور را به دست آورید.
[TestClass] public class BeginnerTest { private readonly string serverName; private readonly string databaseName; public BeginnerTest() { serverName = ConfigurationManager.AppSettings["ServerName"]; databaseName = ConfigurationManager.AppSettings["DatabaseName"]; } }
برای برقراری ارتباط با پایگاه داده نیاز به یک شئ از جنس DocumentStore و جهت انجام عملیات مختلف ( ذخیره، حذف و ... ) نیاز به یک شئ از جنس IDocumentSession است. کد زیر، نحوه کار با آنها را به شما نشان میدهد :
[TestClass] public class BeginnerTest { private readonly string serverName; private readonly string databaseName; private DocumentStore documentStore; private IDocumentSession session; public BeginnerTest() { serverName = ConfigurationManager.AppSettings["ServerName"]; databaseName = ConfigurationManager.AppSettings["DatabaseName"]; } [TestInitialize] public void TestStart() { documentStore = new DocumentStore { Url = serverName }; documentStore.Initialize(); session = documentStore.OpenSession(databaseName); } [TestCleanup] public void TestEnd() { session.SaveChanges(); documentStore.Dispose(); session.Dispose(); } }
در طراحی این پایگاه داده از اگوی Unit Of Work استفاده شده است. به این معنی که تمام تغییرات در حافظه ذخیره میشوند و به محض اجرای دستور ;()session.SaveChanges ارتباط برقرار شده و تمام تغییرات ذخیره خواهند شد.
هنگام شروع ( تابع : TestStart ) متغییر session مقدار دهی میشود و در پایان کار ( تابع : TestEnd ) تغییرات ذخیره شده و منابعی که توسط این دو شئ در حافظه استفاده شده است، رها میشود.
البته بر مبنای طراحی شما، دستور ;()session.SaveChanges میتواند پس از انجام هر عملیات اجرا شود.
class User { public int Id { get; set; } public string Name { get; set; } public string Address { get; set; } public int Zip { get; set; }اهی }
[TestMethod] public void Insert() { var user = new User { Id = 1, Name = "John Doe", Address = "no-address", Zip = 65826 }; session.Store(user); }
لحظهی لذت بخشی است...
یکی از روشهای خواندن اطلاعات هم به صورت زیر است:
[TestMethod] public void Select() { var user = session.Load<User>(1); }
تا این جا، سادهترین مثالهای ممکن را مشاهده کردید و حتما در بحث بعد مثالهای جالبتر و دقیقتری را بررسی میکنیم و همچنین نگاهی به جزئیات طراحی و قراردادهای از پیش تعیین شده میاندازیم.
- نسخه بدون کتابخانههای موردنیاز ( 2 مگابایت ) : RavenDBTest_Small.zip
- نسخه کامل ( 15 مگابایت ) : RavenDBTest.zip
- ایجاد یک پروژهی جدید ASP.NET Core در VS 2017
- تنظیمات یک برنامهی ASP.NET Core خالی برای اجرای یک برنامهی Angular CLI
- تنظیمات فایل آغازین یک برنامهی ASP.NET Core جهت ارائهی برنامههای Angular
- ایجاد ساختار اولیهی برنامهی Angular CLI در داخل پروژهی جاری: این مورد را تاکنون انجام دادهایم و تکمیل کردهایم. بنابراین تنها کاری که نیاز است انجام شود، cut و paste محتوای پوشهی angular-template-driven-forms-lab (پروژهی این سری) به ریشهی پروژهی ASP.NET Core است.
- تنظیم محل خروجی نهایی Angular CLI به پوشهی wwwroot
- روش اول و یا دوم اجرای برنامههای مبتنی بر ASP.NET Core و Angular CLI
البته سورس کامل تمام این تنظیمات را از انتهای بحث نیز میتوانید دریافت کنید.
ضمن اینکه هیچ نیازی هم به استفاده از VS 2017 نیست و هر دوی برنامهی Angular و ASP.NET Core را میتوان توسط VSCode به خوبی مدیریت و اجرا کرد.
ایجاد ساختار مقدماتی سرویس ارسال اطلاعات به سرور
در برنامههای Angular مرسوم است جهت کاهش مسئولیتهای یک کلاس و امکان استفادهی مجدد از کدها، منطق ارسال اطلاعات به سرور، به درون کلاس یک سرویس منتقل شود و سپس این سرویس به کلاسهای کامپوننتها، برای مثال یک فرم ثبت اطلاعات، برای ارسال و یا دریافت اطلاعات، تزریق گردد. به همین جهت، ابتدا ساختار ابتدایی این سرویس و تنظیمات مرتبط با آنرا انجام میدهیم.
ابتدا از طریق خط فرمان به پوشهی ریشهی برنامه وارد شده (جائیکه فایل Startup.cs قرار دارد) و سپس دستور ذیل را اجرا میکنیم:
>ng g s employee/FormPoster -m employee.module
installing service create src\app\employee\form-poster.service.spec.ts create src\app\employee\form-poster.service.ts update src\app\employee\employee.module.ts
ساختار ابتدایی این سرویس را نیز به نحو ذیل تغییر میدهیم:
import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; import { Employee } from './employee'; @Injectable() export class FormPosterService { constructor(private http:Http) { } postEmployeeForm(employee: Employee) { } }
چون این کلاس از ماژول توکار Http استفاده میکند، نیاز است این ماژول را نیز به قسمت imports فایل src\app\app.module.ts اضافه کنیم:
import { HttpModule } from "@angular/http"; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, EmployeeModule, AppRoutingModule ]
import { FormPosterService } from "../form-poster.service"; export class EmployeeRegisterComponent implements OnInit { constructor(private formPoster: FormPosterService) {} }
در ادامه برای آزمایش برنامه، به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی، دستورات:
>npm install >ng build --watch
>dotnet restore >dotnet watch run
به همین جهت برای آزمایش ابتدایی آن، آدرس http://localhost:5000 را در مرورگر باز کنید. برگهی developer tools مرورگر را نیز بررسی کنید تا خطایی در آن ظاهر نشده باشد. برای مثال اگر فراموش کرده باشید تا HttpModule را به app.module اضافه کنید، خطای no provider for HttpModule را مشاهده خواهید کرد.
مدیریت رخداد submit فرم در Angular
تا اینجا کار برپایی تنظیمات اولیهی کار با سرویس Http را انجام دادیم. مرحلهی بعد مدیریت رخداد submit فرم است. به همین جهت فایل src\app\employee\employee-register\employee-register.component.html را گشوده و سپس رخدادگردان submit را به فرم آن اضافه کنید:
<form #form="ngForm" (submit)="submitForm(form)" novalidate>
export class EmployeeRegisterComponent implements OnInit { submitForm(form: NgForm) { console.log(this.model); console.log(form.value); } }
در همین حال اگر بر روی دکمهی ok کلیک کنیم، چنین خروجی را در کنسول developer مروگر میتوان مشاهده کرد:
اولین مورد، محتوای this.model است و دومی محتوای form.value را گزارش کردهاست. همانطور که مشاهده میکنید، مقدار form.value بسیار شبیه است به وهلهای از مدلی که در سطح کلاس تعریف کردهایم و این مقدار همواره توسط Angular نگهداری و مدیریت میشود. بنابراین حتما الزامی نیست تا مدلی را جهت کار با فرمهای مبتنی بر قالبها به صورت جداگانهای تهیه کرد. توسط شیء form نیز میتوان به تمام اطلاعات فیلدها دسترسی یافت.
تکمیل سرویس ارسال اطلاعات به سرور
در ادامه میخواهیم اطلاعات مدل فرم را به سرور ارسال کنیم. برای این منظور سرویس FormPoster را به صورت ذیل تکمیل میکنیم:
import { Injectable } from "@angular/core"; import { Http, Response, Headers, RequestOptions } from "@angular/http"; import { Observable } from "rxjs/Observable"; import "rxjs/add/operator/do"; import "rxjs/add/operator/catch"; import "rxjs/add/observable/throw"; import "rxjs/add/operator/map"; import "rxjs/add/observable/of"; import { Employee } from "./employee"; @Injectable() export class FormPosterService { private baseUrl = "api/employee"; constructor(private http: Http) {} private extractData(res: Response) { const body = res.json(); return body.fields || {}; } private handleError(error: Response): Observable<any> { console.error("observable error: ", error); return Observable.throw(error.statusText); } postEmployeeForm(employee: Employee): Observable<Employee> { const body = JSON.stringify(employee); const headers = new Headers({ "Content-Type": "application/json" }); const options = new RequestOptions({ headers: headers }); return this.http .post(this.baseUrl, body, options) .map(this.extractData) .catch(this.handleError); } }
در متد postEmployeeForm، ابتدا توسط JSON.stringify محتوای شیء کارمند encode میشود. البته متد post اینکار را به صورت توکار نیز میتواند مدیریت کند. سپس ذکر هدر مناسب در اینجا الزامی است تا در سمت سرور بتوانیم اطلاعات دریافتی را به شیء متناظری نگاشت کنیم. در غیراینصورت model binder سمت سرور نمیداند که چه نوع فرمتی را دریافت کردهاست و چه نوع decoding را باید انجام دهد.
در قسمت map، کار بررسی اطلاعات دریافتی از سرور را انجام خواهیم داد و اگر در این بین خطایی وجود داشت، توسط متد handleError در کنسول developer مرورگر نمایش داده میشود.
خروجی متد postEmployeeForm یک Observable است. بنابراین تا زمانیکه یک subscriber نداشته باشد، اجرا نخواهد شد. به همین جهت به کلاس EmployeeRegisterComponent مراجعه کرده و متد submitForm را به نحو ذیل تکمیل میکنیم:
submitForm(form: NgForm) { console.log(this.model); console.log(form.value); // validate form this.validatePrimaryLanguage(this.model.primaryLanguage); if (this.hasPrimaryLanguageError) { return; } this.formPoster .postEmployeeForm(this.model) .subscribe( data => console.log("success: ", data), err => console.log("error: ", err) ); }
یک نکته: اگر علاقمند باشید تا ساختار واقعی شیء NgForm را مشاهده کنید، در ابتدای متد فوق، console.log(form.form) را فراخوانی کنید و سپس شیء حاصل را در کنسول developer مرورگر بررسی نمائید.
تکمیل Web API برنامهی ASP.NET Core جهت دریافت اطلاعات از کلاینتها
در ابتدای سرویس formPoster، یک چنین تعریفی را داریم:
export class FormPosterService { private baseUrl = "api/employee";
ابتدا مدل زیر را به پروژهی ASP.NET Core جاری، معادل نمونهی تایپاسکریپتی سمت کلاینت آن اضافه میکنیم. البته در اینجا یک Id نیز اضافه شدهاست:
namespace AngularTemplateDrivenFormsLab.Models { public class Employee { public int Id { set; get; } public string FirstName { get; set; } public string LastName { get; set; } public bool IsFullTime { get; set; } public string PaymentType { get; set; } public string PrimaryLanguage { get; set; } } }
سپس کنترلر جدید EmployeeController را با محتوای ذیل اضافه خواهیم کرد:
using Microsoft.AspNetCore.Mvc; using AngularTemplateDrivenFormsLab.Models; namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class EmployeeController : Controller { public IActionResult Post([FromBody] Employee model) { //todo: save model model.Id = 100; return Created("", new { fields = model }); } } }
در اینجا پس از ثبت فرضی مدل، Id آن به همراه اطلاعات مدل، به نحوی که ملاحظه میکنید، بازگشت داده شدهاست. این نوع خروجی، یک چنین JSON ایی را تولید میکند:
{"fields":{"id":100,"firstName":"Vahid","lastName":"N","isFullTime":true,"paymentType":"FullTime","primaryLanguage":"Persian"}}
private extractData(res: Response) { const body = res.json(); return body.fields || {}; }
نمایش success به همراه شیءایی که از سمت سرور دریافت شدهاست؛ که حاصل اجرای سطر ذیل در متد submitForm است:
data => console.log("success: ", data)
بارگذاری اطلاعات drop down از سرور
تا اینجا اطلاعات drop down نمایش داده شده از یک آرایهی مشخص سمت کلاینت تامین شدند. در ادامه قصد داریم تا آنها را از سرور دریافت کنیم. به همین جهت اکشن متد ذیل را به کنترلر سمت سرور برنامه اضافه کنید:
[HttpGet("/api/[controller]/[action]")] public IActionResult Languages() { string[] languages = { "Persian", "English", "Spanish", "Other" }; return Ok(languages); }
پس از آن در سمت کلاینت این تغییرات نیاز هستند:
ابتدا به سرویس FormPosterService دو متد ذیل را اضافه میکنیم که کار آنها دریافت و پردازش اطلاعات از api/employee/languages سمت سرور هستند:
private extractLanguages(res: Response) { const body = res.json(); return body || {}; } getLanguages(): Observable<any> { return this.http .get(`${this.baseUrl}/languages`) .map(this.extractLanguages) .catch(this.handleError); }
پس از آن دو تغییر ذیل را نیاز است به EmployeeRegisterComponent اعمال کنیم:
languages = []; ngOnInit() { this.formPoster .getLanguages() .subscribe( data => this.languages = data, err => console.log("get error: ", err) ); }
مشکل! ممکن است مدت زمانی طول بکشد تا این اطلاعات از سمت سرور دریافت شوند. در این حالت میتوان به شکل زیر در فایل employee-register.component.html فرم را تا زمان پر شدن دراپ داون آن مخفی کرد:
<h3 *ngIf="languages.length == 0">Loading...</h3> <div class="container" *ngIf="languages.length > 0">
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-05.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات:
>npm install >ng build --watch
>dotnet restore >dotnet watch run
در طراحی مدل دامین، بیشتر مواقع از نوعهای اولیه مانند int , string,… استفاده میکنیم و به عبارتی میتوانیم بگوییم در استفاده از این نوع داده وسواس داریم. قطعه کد زیر را در نظر بگیرید:
public class UserFactory { public User CreateUser(string email) { return new User(email); } }
اگر به خاطر داشته باشید، در قسمتهای قبلی در مورد مفهومی به نام Honesty صحبت کردیم. به طور ساده باید بتوانیم از روی امضای تابع، کاری را که تابع انجام میدهد و خروجی آن را ببینیم. این تابع Honest نیست؛ شرایطی که string میتواند درست نباشد، خالی باشد، طول غیر مجاز داشته باشد و ... را نمیتوانیم از امضای تابع حدس بزنیم.
جواب خیلی سادهاست؛ شما نیاز دارید تا یک Type اختصاصی را ایجاد کنید. برای مثال بجای استفاده از نوع string برای یک ایمیل، میتوانید یک کلاس را به عنوان Email ایجاد کنید که مشخصهای به نام Value دارد. این کار به روشهای مختلفی قابل انجام است؛ اما پیشنهاد من استفاده از این روش هست:
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace ValueOf { public class ValueOf<TValue, TThis> where TThis : ValueOf<TValue, TThis>, new() { private static readonly Func<TThis> Factory; /// <summary> /// WARNING - THIS FEATURE IS EXPERIMENTAL. I may change it to do /// validation in a different way. /// Right now, override this method, and throw any exceptions you need to. /// Access this.Value to check the value /// </summary> protected virtual void Validate() { } static ValueOf() { ConstructorInfo ctor = typeof(TThis) .GetTypeInfo() .DeclaredConstructors .First(); var argsExp = new Expression[0]; NewExpression newExp = Expression.New(ctor, argsExp); LambdaExpression lambda = Expression.Lambda(typeof(Func<TThis>), newExp); Factory = (Func<TThis>)lambda.Compile(); } public TValue Value { get; protected set; } public static TThis From(TValue item) { TThis x = Factory(); x.Value = item; x.Validate(); return x; } protected virtual bool Equals(ValueOf<TValue, TThis> other) { return EqualityComparer<TValue>.Default.Equals(Value, other.Value); } public override bool Equals(object obj) { if (obj is null) return false; if (ReferenceEquals(this, obj)) return true; return obj.GetType() == GetType() && Equals((ValueOf<TValue, TThis>)obj); } public override int GetHashCode() { return EqualityComparer<TValue>.Default.GetHashCode(Value); } public static bool operator ==(ValueOf<TValue, TThis> a, ValueOf<TValue, TThis> b) { if (a is null && b is null) return true; if (a is null || b is null) return false; return a.Equals(b); } public static bool operator !=(ValueOf<TValue, TThis> a, ValueOf<TValue, TThis> b) { return !(a == b); } public override string ToString() { return Value.ToString(); } } }
public class EmailAddress : ValueOf<string, EmailAddress> { }
EmailAddress emailAddress = EmailAddress.From("foo@bar.com");
public class Address : ValueOf<(string firstLine, string secondLine, Postcode postcode), Address> { }
روش معرفی شدهی در این مقاله، صرفا جهت آشنایی بیشتر شما و داشتن کدی تمیزتر از طریق مفاهیم برنامه نویسی تابعی خواهد بود. در دنیای واقعی، احتمالا مسائلی را برای ذخیره سازی این آبجکتها و یا کار با کتابخانههایی مانند Entity Framework خواهید داشت که به سادگی قابل حل است.
در صورتیکه مشکلی در پیاده سازی داشتید، میتوانید مشکل خود را زیر همین مطلب و یا بر روی gist آن کامنت کنید.
public class PostDto : BaseDto<PostDto, Post, long> { public string Title { get; set; } public string Text { get; set; } public int CategoryId { get; set; } public string CategoryName { get; set; } //=> Category.Name }
- کلاس PostDto خودش را به عنوان اولین پارامتر جنریک BaseDto معرفی میکند.
- به عنوان پارامتر دوم، باید کلاس Entity ایی که قرار است به آن نگاشت شود (Post) را معرفی کنیم.
- پارامتر سوم، نوع فیلد Id است که در اینجا خاصیت Id کلاسهای Post و PostDto ما، از نوع long است.
- نهایتا خواصی را که برای نگاشت لازم داریم، تعریف میکنیم مثل Title و...
- همچنین میتوانیم خواصی برای نگاشت با خواص Navigation Propertyهای Post هم تعریف کنیم؛ مانند CategoryName که به خاصیت Name از Category پست مربوطه اشاره میکند و AutoMapper به صورت هوشمندانه آنها را به هم نگاشت میکند.
public abstract class BaseDto<TDto, TEntity, TKey> where TDto : class, new() where TEntity : BaseEntity<TKey>, new() { [Display(Name = "ردیف")] public TKey Id { get; set; } public TEntity ToEntity() { return Mapper.Map<TEntity>(CastToDerivedClass(this)); } public TEntity ToEntity(TEntity entity) { return Mapper.Map(CastToDerivedClass(this), entity); } public static TDto FromEntity(TEntity model) { return Mapper.Map<TDto>(model); } protected TDto CastToDerivedClass(BaseDto<TDto, TEntity, TKey> baseInstance) { return Mapper.Map<TDto>(baseInstance); } }
- نوع TDto به کلاس Dto ما اشاره میکند؛ مثلا PostDto
- نوع TEntity به کلاس Entity ما اشاره میکند؛ مثلا Post
- نوع TKey به نوع خاصیت Id اشاره میکند.
- شرط لازم برای نوع TEntity این است که از <BaseEntity<TKey ارث بری کرده باشد (نوع پایهای که تمام Entityهای ما از آن ارث بری میکنند).
- متدهای کمکی ToEntity و FromEntity، کار نگاشت اشیاء را برای ما راحتتر میکنند.
public abstract class BaseEntity<TKey> { public TKey Id { get; set; } } public class Post : BaseEntity<long> { public string Title { get; set; } public string Text { get; set; } public int CatgeoryId { get; set; } public Category Category { get; set; } }
var postDto = new PostDto(); var post = postDto.ToEntity();
var post = // finded by id var updatePost = postDto.ToEntity(post);
var postDto = PostDto.FromEntity(post);
public static class AutoMapperConfiguration { public static void InitializeAutoMapper() { Mapper.Initialize(configuration => { configuration.ConfigureAutoMapperForDto(); }); //Compile mapping after configuration to boost map speed Mapper.Configuration.CompileMappings(); } public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config) { config.ConfigureAutoMapperForDto(Assembly.GetEntryAssembly()); } public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config, params Assembly[] assemblies) { var dtoTypes = GetDtoTypes(assemblies); var mappingTypes = dtoTypes .Select(type => { var arguments = type.BaseType.GetGenericArguments(); return new { DtoType = arguments[0], EntityType = arguments[1] }; }).ToList(); foreach (var mappingType in mappingTypes) config.CreateMappingAndIgnoreUnmappedProperties(mappingType.EntityType, mappingType.DtoType); } public static void CreateMappingAndIgnoreUnmappedProperties(this IMapperConfigurationExpression config, Type entityType, Type dtoType) { var mappingExpression = config.CreateMap(entityType, dtoType).ReverseMap(); //Ignore mapping to any property of source (like Post.Categroy) that dose not contains in destination (like PostDto) //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value foreach (var property in entityType.GetProperties()) { if (dtoType.GetProperty(property.Name) == null) mappingExpression.ForMember(property.Name, opt => opt.Ignore()); } } public static IEnumerable<Type> GetDtoTypes(params Assembly[] assemblies) { var allTypes = assemblies.SelectMany(a => a.ExportedTypes); var dtoTypes = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.BaseType != null && type.BaseType.IsGenericType && (type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,>) || type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,,>))); return dtoTypes; } }
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; AutoMapperConfiguration.InitializeAutoMapper(); }
public static IEnumerable<Type> GetDtoTypes(params Assembly[] assemblies) { var allTypes = assemblies.SelectMany(a => a.ExportedTypes); var dtoTypes = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.BaseType != null && type.BaseType.IsGenericType && (type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,>) || type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,,>))); return dtoTypes; }
- در خط اول ابتدا تمامی نوعهای قابل دسترس از بیرون (ExportedTypes) از assemblyهای دریافتی واکشی میشود.
- سپس توسط Where، نوعهایی که کلاس بوده، abstract نیستند و از BaseDto ارث بری کردهاند، فیلتر شده و بازگردانده میشوند.
public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config, params Assembly[] assemblies) { var dtoTypes = GetDtoTypes(assemblies); var mappingTypes = dtoTypes .Select(type => { var arguments = type.BaseType.GetGenericArguments(); return new { DtoType = arguments[0], EntityType = arguments[1] }; }).ToList(); foreach (var mappingType in mappingTypes) config.CreateMappingAndIgnoreUnmappedProperties(mappingType.EntityType, mappingType.DtoType); }
public static void CreateMappingAndIgnoreUnmappedProperties(this IMapperConfigurationExpression config, Type entityType, Type dtoType) { var mappingExpression = config.CreateMap(entityType, dtoType).ReverseMap(); //Ignore mapping to any property of entity (like Post.Categroy) that dose not contains in dto (like PostDto.CategoryName) //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value foreach (var property in entityType.GetProperties()) { if (dtoType.GetProperty(property.Name) == null) mappingExpression.ForMember(property.Name, opt => opt.Ignore()); } }
تغییر فضای نام کلاس poco استفاده شده در wcf و از کار افتادن برنامهی مشتری بدون دریافت پیام خطا
public class OutgoingJob { public int Id; public string JobId; public string Subject; public string Reciver; public byte[][] Attachments; }
public List<OutgoingJob> GetJobsList(int Count) { LogEvent("GetFaxsList Start..."); List<OutgoingJob> OutgoingJobs = new List<OutgoingJob>(); // business for fill list LogEvent("return job Count = " + OutgoingJobs.Count); return OutgoingJobs; }
[TestMethod] public void TestMethod1() { jobService ser = new jobService(); var listjob = ser.GetJobsList(5); Assert.AreNotEqual(0, listjob.Count); }
پیاده سازی UnitOfWork به وسیله MEF
متد AddFromAssembly نیاز به یک Assembly دارد تا بتونه تمام کلاس هایی رو که از کلاس EntityTypeConfiguration ارث برده اند رو پیدا کنه و اونها رو به صورت خودکار به ModelBuilder اضافه کنه. به همین دلیل من Assembly کلاس CategoryMap رو به اون پاس دادم. دقت کنید که اگر من n تا کلاس Map دیگه هم توی این ClassLibrary داشتم باز توسط همین دستور این کار به صورت خودکار انجام میشد. (پیشنهاد میکنم تست کنید)
نکته: این متد برگرفته شده از متد AddFromAssembly در NHibernate Session Configuration است.
البته بهتر است که یک کلاس پایه برای این کار بسازید و اون کلاس رو به AddFromAssembly پاس بدید.
در مورد سوال دوست عزیز مبنی بر اینکه متد Save رو در خود توابع Repository قرار دادم.
اگر به کدهای نوشته شده دقت کنید من اصلا مفهومی به نام Repository رو پیاده سازی نکردم. به این دلیل که خود DbContext ترکیبی از Repository Pattern , UnitOfWork است. متد SaveChange صدا زده شده همان متد SaveChange در DbContext است.
فرض کنید من یک کلاس Business دیگر به صورت زیر داشتم.
public class MyBusiness : BusinessBase<Entity.MyEntity> { [ImportingConstructor] public MyBusiness ( [Import( typeof( IUnitOfWork ) )] IUnitOfWork unitOfWork ) : base( unitOfWork ) { } public void Add(Entity.MyEntity entity) { UnitOfWork.Set<MyEntity>().Add(entity); } }
public class Category : BusinessBase<Entity.Category> { [ImportingConstructor] public Category( [Import( typeof( IUnitOfWork ) )] IUnitOfWork unitOfWork ) : base( unitOfWork ) { } public override void Add( Entity.Category entity ) { new MyBusiness().Add( new Entity.MyEntity() ); UnitOfWork.Set<Entity.Category>().Add( entity ); UnitOfWork.SaveChanges(); } }
ابتدا شیء user، در بالاترین سطح، دریافت شده و به صفحهای خاص از طریق ویژگیهای props ارسال میشود:
<Page user={user} />
<PageLayout user={user} />
<NavigationBar user={user} />
ایجاد شیء Context در برنامههای React
React Context، راه حلی است جهت به اشتراک گذاری دادهها، در بین انواع و اقسام کامپوننتهای یک برنامه، بدون اینکه نیازی باشد این اطلاعات را توسط props، از یک سطح، به سطحی دیگر، به صورت دستی انتقال داد. برای ایجاد یک نمونهی از آن، ابتدا پوشهی جدید src\contexts را افزوده و سپس فایل src\contexts\userContext.js را درون آن، با محتوای زیر ایجاد میکنیم:
import React from "react"; export const UserContext = React.createContext({ user: {} }); export const UserProvider = UserContext.Provider; export const UserConsumer = UserContext.Consumer;
تامین یک شیء Context در برنامه، در یک کامپوننت کلاسی و یا تابعی
تا اینجا یک شیء Context را به همراه اجزای export شدهی Provider و Consumer آن ایجاد کردیم. اکنون نوبت به پیاده سازی قسمت Provider آن است:
import "../../App.css"; import React, { Component } from "react"; import { UserProvider } from "../../contexts/userContext"; import Main from "./Main"; class App extends Component { state = { user: { name: "User 1" } }; componentDidMount() { // get user from the server or local storage and then set the currently logged in user to the this.state } render() { return ( <> <h1>App Class</h1> <UserProvider value={this.state.user}> <Main /> </UserProvider> </> ); } } export default App;
در ادامه قصد داریم اطلاعات این شیء user موجود در state را با تمام کامپوننتهایی که در درخت رندر کامپوننت جاری قرار میگیرند و با کامپوننت Main شروع میشوند، به اشتراک بگذاریم. این به اشتراک گذاری با import شیء UserProvider از ماژول contexts/userContext به نحوی که مشاهده میکنید، انجام میشود. شیء UserProvider، کار محصور سازی کامپوننت Main را انجام میدهد. سپس این Provider میتواند مقداری را توسط ویژگی value خود دریافت کند که برای مثال در اینجا شیء user است. اکنون این value تا n سطح بعدی که از کامپوننت Main مشتق میشوند نیز در دسترس خواهد بود.
یک نکته: متد React.createContext به همراه یک آرگومان defaultValue اختیاری است که در اختیار Consumerهای آن قرار داده میشود؛ اگر Provider متناظر با آن، در درخت کامپوننتهای برنامه، یافت نشود. یعنی تعریف Provider الزامی نیست. اگر نیاز است مقدار ثابتی را بین چندین کامپوننت به اشتراک بگذارید، فقط کافی است آنها را توسط React.createContext مقدار دهی اولیه کرده و ... استفاده کنید:
export const DefaultRouteContext = React.createContext({ path: '/welcome' });
خواندن شیء Context در کامپوننتی دیگر
اکنون که یک تامین کنندهی Context را ایجاد کردیم، برای خواندن اطلاعات آن در درخت کامپوننتهای محصور شدهی توسط UserProvider، میتوان به صورت زیر عمل کرد:
import React from "react"; import { UserConsumer } from "../../contexts/userContext"; export default function Main(props) { return ( <> <UserConsumer> {value => <div>User name: {value.name}.</div>} </UserConsumer> </> ); }
خروجی برنامه پس از این تغییرات به صورت زیر است:
ساده سازی دسترسی به UserConsumer توسط useContext Hook
نحوهی تعریف یک Provider و محصور سازی فرزندانی که باید از آن ارثبری کنند، در بین کامپوننتهای کلاسی و تابعی، یکی است. اما در کامپوننتهای تابعی حداقل میتوان نحوهی دسترسی به UserConsumer را به نحو زیر توسط useContext Hook ساده کرد:
import React, { useContext } from "react"; import { UserContext } from "../../contexts/userContext"; export default function Main() { const value = useContext(UserContext); return ( <> <div>User name: {value.name}.</div> </> ); }
مزیت دیگر این روش، ساده سازی کار با چندین شیء Context است. برای مثال اگر دو شیء Context را تعریف کرده باشید، خواندن دو مقدار از آنها، پیشتر چنین شکل تو در تویی را توسط دو Consumer پیدا میکرد:
function HeaderBar() { return ( <CurrentUser.Consumer> {user => <Notifications.Consumer> {notifications => <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> } } </CurrentUser.Consumer> ); }
function HeaderBar() { const user = useContext(CurrentUser); const notifications = useContext(Notifications); return ( <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> ); }
ارسال اطلاعات به کامپوننت Context Provider، از طریق کامپوننتهای فرزند
تا اینجا با استفاده از React Context، اطلاعات یک Provider را با فرزندان آن به اشتراک گذاشتیم؛ عکس این عمل نیز میسر است. برای اینکار، همانند تمام کامپوننتهای دیگری که برای ارسال اطلاعات به فراخوان خود از طریق رخدادها عمل میکنند، میتوان یک متد رویدادگردان را در کامپوننت والد، به استفاده کنندهی از Context ارسال کرد:
import "../../App.css"; import React, { Component } from "react"; import { UserProvider } from "../../contexts/userContext"; import Main from "./Main2"; class App extends Component { state = { user: { name: "User 1" } }; componentDidMount() { // get user from the server or local storage and then set the currently logged in user to the this.state } logout = () => { console.log("logout"); this.setState({ user: {} }); }; render() { const contextValue = { user: this.state.user, logoutUser: this.logout }; return ( <> <h1>App Class</h1> <UserProvider value={contextValue}> <Main /> </UserProvider> </> ); } } export default App;
اکنون تمام استفاده کنندههای از این UserProvider میتوانند با فراخوانی متد منتسب به logout، سبب پاک شدن اطلاعات کاربر موجود در state کامپوننت App، به روز رسانی state و در نتیجهی آن، رندر مجدد کامپوننت و ارائهی یک UserProvider جدید، با اطلاعاتی جدید به فرزندان آن شوند:
import React, { useContext } from "react"; import { UserContext } from "../../contexts/userContext"; export default function Main() { const { user, logoutUser } = useContext(UserContext); return ( <> <div>User name: {user.name}.</div> <button type="button" className="btn btn-primary" onClick={logoutUser}> Logout user </button> </> ); }
روش انجام اینکار بدون استفاده از useContext را نیز در ادامه مشاهده میکنید که در ابتدا نیاز به تعریف تابعی را دارد که همان خواص استخراجی را دریافت میکند. سپس باید بر اساس آنها، المانهای مدنظر نمایش نام کاربر و دکمهی خروج او را بازگشت داد:
import React from "react"; import { UserConsumer } from "../../contexts/userContext"; export default function Main(props) { return ( <> <UserConsumer> {({ user, logoutUser }) => ( <> <div>User name: {user.name}.</div> <button type="button" className="btn btn-primary" onClick={logoutUser} > Logout user </button> </> )} </UserConsumer> </> ); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-04.zip
private static void disposedContext() { using (var context = new MyContext()) { Debug.WriteLine("Posts count: " + context.BlogPosts.Count()); } } private static void nonDisposedContext() { var context = new MyContext(); Debug.WriteLine("Posts count: " + context.BlogPosts.Count()); }
سؤال: در یک برنامهی بزرگ چطور میتوان لیست Contextهای Dispose نشده را یافت؟
در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی میتوان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
using System.Data; using System.Data.Common; using System.Data.Entity.Infrastructure.Interception; namespace EFNonDisposedContext.Core { public class DatabaseInterceptor : IDbConnectionInterceptor { public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Closed); } public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Disposed); } public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Opened); } // the rest of the IDbConnectionInterceptor methods ... } }
مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شدهاست؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» میتوانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی میتوان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
public enum ConnectionStatus { None, Opened, Closed, Disposed } public class ConnectionInfo { public string ConnectionId { set; get; } public string StackTrace { set; get; } public ConnectionStatus Status { set; get; } public override string ToString() { return string.Format("{0}:{1} [{2}]",ConnectionId, Status, StackTrace); } }
StackTrace توسط نکتهی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کردهاند؟ » تهیه میشود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص میگردد.
در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آنها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشدهاست، مشخص میکنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
EFNonDisposedContext.zip