بررسی ساختار یک قالب Angular Material
قالب، مجموعهای از رنگها است که به کامپوننتهای Angular Material اعمال میشود. هر قالب از چندین جعبهرنگ یا palette تشکیل میشود:
- primary palette: به صورت گستردهای در تمام کامپوننتها مورد استفادهاست.
- accent palette: به المانهای تعاملی انتساب داده میشود.
- warn palette: برای نمایش خطاها و اخطارها بکار میرود.
- foreground palette: برای متون و آیکنها استفاده میشود.
- background palette: برای پسزمینهی المانها بکار میرود.
روش انتخاب این جعبه رنگها نیز به صورت زیر است:
<mat-card> Main Theme: <button mat-raised-button color="primary"> Primary </button> <button mat-raised-button color="accent"> Accent </button> <button mat-raised-button color="warn"> Warning </button> </mat-card>
همانطور که در قسمت اول این سری نیز بررسی کردیم، بستهی Angular Material به همراه چندین قالب از پیش طراحی شدهاست (قالبهای از پیش آمادهی متریال را در پوشهی node_modules\@angular\material\prebuilt-themes میتوانید مشاهده کنید) و در حین اجرای برنامه تنها یکی از آنها که در فایل styles.css ذکر شدهاست، مورد استفاده قرار میگیرد.
اگر نیاز به سفارشی سازی بیشتری وجود داشته باشد، میتوان قالبهای ویژهی خود را نیز طراحی کرد. این قالب جدید باید mat-core() sass mixin را import کند که حاوی تمام شیوهنامههای مشترک بین کامپوننتها است. این مورد باید تنها یکبار به کل برنامه الحاق شود تا حجم آنرا بیش از اندازه زیاد نکند. سپس این قالب سفارشی، جعبه رنگهای خاص خودش را معرفی میکند. در ادامه این جعبه رنگها توسط توابع mat-light-theme و یا mat-dark-theme ترکیب شده و مورد استفاده قرار میگیرند. سپس این قالب را include خواهیم کرد. به این ترتیب یک قالب سفارشی Angular Material، چنین طرحی را دارد:
@import '~@angular/material/theming'; @include mat-core(); $candy-app-primary: mat-palette($mat-indigo); $candy-app-accent: mat-palette($mat-pink, A200, A100, A400); $candy-app-warn: mat-palette($mat-red); $candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn); @include angular-material-theme($candy-app-theme);
ایجاد یک قالب سفارشی جدید Angular Material
برای ایجاد یک قالب سفارشی نیاز است از فایلهای sass استفاده کرد. بنابراین بهترین روش ایجاد برنامههای Angular Material در ابتدای کار، ذکر صریح نوع style مورد استفاده به sass است:
ng new MyProjectName --style=sass
"styles": [ "node_modules/material-design-icons/iconfont/material-icons.css", "src/styles.css", "src/custom.theme.scss" ],
پس از افزودن و تنظیم فایل custom.theme.scss، به فایل styles.css مراجعه کرده و قالب فعلی را به صورت comment در میآوریم:
/* @import "~@angular/material/prebuilt-themes/indigo-pink.css"; */ body { margin: 0; }
@import '~@angular/material/theming'; @include mat-core(); $my-app-primary: mat-palette($mat-blue-grey); $my-app-accent: mat-palette($mat-pink, 500, 900, A100); $my-app-warn: mat-palette($mat-deep-orange); $my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn); @include angular-material-theme($my-app-theme); .alternate-theme { $alternate-primary: mat-palette($mat-light-blue); $alternate-accent: mat-palette($mat-yellow, 400); $alternate-theme: mat-light-theme($alternate-primary, $alternate-accent); @include angular-material-theme($alternate-theme); }
در اینجا روش تعریف یک قالب دوم (alternate-theme) را نیز مشاهده میکنید. علت تعریف قالب دوم در همین فایل جاری، کاهش حجم نهایی برنامه است. از این جهت که اگر alternate-themeها را در فایلهای scss دیگری قرار دهیم، مجبور به import تعاریف اولیهی قالبهای Angular Material در هرکدام به صورت جداگانهای خواهیم بود که حجم قابل ملاحظهای را به خود اختصاص میدهند. به همین جهت قالبهای دیگر را نیز در همینجا به صورت کلاسهای ثانویه تعریف خواهیم کرد.
در این حالت روش استفادهی از این قالب ثانویه به صورت زیر میباشد:
<mat-card class="alternate-theme"> Alternate Theme: <button mat-raised-button color="primary"> Primary </button> <button mat-raised-button color="accent"> Accent </button> <button mat-raised-button color="warn"> Warning </button> </mat-card>
پس از افزودن فایل src\custom.theme.scss به برنامه، اگر آنرا اجرا کنیم به خروجی زیر خواهیم رسید:
افزودن امکان انتخاب پویای قالبها به برنامه
قصد داریم به منوی برنامه که اکنون گزینهی new contact را به همراه دارد، گزینهی toggle theme را هم جهت تغییر پویای قالب اصلی برنامه اضافه کنیم. به همین جهت فایل toolbar.component.html را گشوده و به صورت زیر تغییر میدهیم:
<mat-menu #menu="matMenu"> <button mat-menu-item (click)="openAddContactDialog()">New Contact</button> <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button> </mat-menu>
بنابراین جهت تبادل اطلاعات بین toolbar و sidenav از یک رخداد استفاده خواهیم کرد. برای این منظور فایل toolbar.component.ts را گشوده و این رخداد را به آن اضافه میکنیم:
export class ToolbarComponent implements OnInit { @Output() toggleTheme = new EventEmitter<void>();
<app-toolbar (toggleTheme)="toggleTheme()" (toggleSidenav)="sidenav.toggle()"></app-toolbar>
export class SidenavComponent { isAlternateTheme = false; toggleTheme() { this.isAlternateTheme = !this.isAlternateTheme; } }
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill [class.alternate-theme]="isAlternateTheme">
افزودن پشتیبانی از راست به چپ به قالب برنامه
اگر به mat-sidenav-container ویژگی dir=rtl را اضافه کنیم، قالب برنامه راست به چپ خواهد شد. در ادامه میخواهیم شبیه به حالت تغییر پویای قالب سایت، گزینهای را به منوی برنامه جهت تغییر جهت برنامه نیز اضافه کنیم. برای این منظور به قالب toolbar.component.html مراجعه کرده و گزینهی Toggle dir را به آن اضافه میکنیم:
<mat-menu #menu="matMenu"> <button mat-menu-item (click)="openAddContactDialog()">New Contact</button> <button mat-menu-item (click)="toggleTheme.emit()">Toggle theme</button> <button mat-menu-item (click)="toggleDir.emit()">Toggle dir</button> </mat-menu>
export class ToolbarComponent implements OnInit { @Output() toggleDir = new EventEmitter<void>();
<app-toolbar (toggleDir)="toggleDir()" (toggleTheme)="toggleTheme()" (toggleSidenav)="sidenav.toggle()"></app-toolbar>
export class SidenavComponent implements OnInit, OnDestroy { dir = "ltr"; toggleDir() { this.dir = this.dir === "ltr" ? "rtl" : "ltr"; } }
<mat-sidenav-container fxLayout="row" class="app-sidenav-container" fxFill [dir]="dir" [class.alternate-theme]="isAlternateTheme">
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-06.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
در این قسمت مدلهای مربوط به بخش انجمن را تکمیل کرده و همچنین سیستم نظرسنجی را نیز بررسی خواهیم کرد.
همکاران این قسمت:
سلمان معروفی
سید مجبتی حسینی
مدل پستهای انجمن
/// <summary> /// Represents The Post of Forum /// </summary> public class ForumPost : AuditBaseEntity { #region Ctor /// <summary> /// create one instance of <see cref="ForumPost"/> /// </summary> public ForumPost() { CreatedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets body of this post /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets Count of this post's reports /// </summary> public virtual int ReportsCount { get; set; } /// <summary> /// gets or sets information of User-Agent /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets rating values /// <remarks>is a complex type</remarks> /// </summary> public virtual Rating Rating { get; set; } /// <summary> /// gets or sets author's ip address /// </summary> public virtual string CreatorIp { get; set; } /// <summary> /// gets or sets status of this post /// </summary> public virtual ForumPostStatus Status { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets ParentPost of this post /// </summary> public virtual ForumPost Reply { get; set; } /// <summary> /// gets or sets ParentPost's Id of this post /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets /// </summary> public virtual ICollection<ForumPost> Children { get; set; } /// <summary> /// gets or sets Topic That Associated with this Post /// </summary> public virtual ForumTopic Topic { get; set; } /// <summary> /// gets or sets Id of Topic That Associated with this Post /// </summary> public virtual long TopicId { get; set; } /// <summary> /// get or sets Histories of this Post's Updates /// </summary> public virtual ICollection<ForumPostHistory> Histories { get; set; } /// <summary> /// gets or sets Forum that this post created in it . used for retrive posts count /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets id of Forum that this post created in it . used for retrive posts count /// </summary> public virtual long ForumId { get; set; } #endregion } public enum ForumPostStatus { /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */ [Display(Name = "تأیید شده")] Approved = 0, [Display(Name = "در انتظار بررسی")] Pending = 1, [Display(Name = "جفنگ")] Spam = 2, [Display(Name = "زباله دان")] Trash = -1 }
مدل بالا مشخص کنندهی پستهایی که در پاسخ به تاپیکها ارسال میشوند، میباشد. ساختار درختی آن به منظور امکان پاسخ به پستها در نظر گرفته شده است. در هر تاپیک چندین پست ارسال میشود که اولین پست ارسال شده، همان محتوای اصلی تاپیک میباشد. بدین منظور خصوصیت Topic را در مدل بالا تعریف کردهایم. برای این پستهای ارسالی امکان امتیاز دهی و اخطار دادن نیز خواهیم داشت که به ترتیب خصوصیات Rating و ReportsCount (بحث شده در مقالات قبل) را در مدل بالا تعریف کردهایم. خصوصیت Status به منظور اعمال مدیریتی در نظر گرفته شده است که از نوع ForumPostStatus میباشد و در بالا تعریف آن نیز آمده است.
نکته : خصوصیتی از نوع مدل Forum نیز در مدل بالا تعریف شده است. هدف از آن افزایش سرعت ویرایش خصوصیات ApprovedPostsCount و UnApprovedPostsCount موجود در مدل Forum میباشد. در واقع هنگام درج پست جدید یا حذف پستی و یا ... ، لازم است خصوصیات مذکور به روز شوند.
علاوه بر این موارد ، لازم است تاریخچهی تغییرات پستهای ارسالی را هم نگهداری کرد تا در صورت نیاز به آنها استناد کنیم. از طرفی پستهای ارسالی را میتوان چندین بار ویرایش کرد. به همین دلیل خصوصیت Histories را که لیستی از مدل ForumPostHistory میباشد، در مدل بالا تعریف کردهایم.
مدل تاریخچهی تغییرات پست
/// <summary> /// Represents History Of Post's Updates /// </summary> public class ForumPostHistory { #region Ctor /// <summary> /// create one instance of <see cref="ForumPostHistory"/> /// </summary> public ForumPostHistory() { Id = SequentialGuidGenerator.NewSequentialGuid(); CreatedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets Identifier of this history /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets Reason of update /// </summary> public virtual string Reason { get; set; } /// <summary> /// gets or sets DateTime that this record added /// </summary> public virtual DateTime CreatedOn { get; set; } /// <summary> /// gets or sets body of this post /// </summary> public virtual string Body { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Post /// </summary> public virtual ForumPost Post { get; set; } /// <summary> /// gets or sets Id Of Post /// </summary> public virtual long PostId { get; set; } /// <summary> /// gets or sets User that modified this Record /// </summary> public virtual User Modifier { get; set; } /// <summary> /// gets or sets if of User that modified this Record /// </summary> public virtual long ModifierId { get; set; } #endregion }
اگر خصوصیت ModifyLocked مربوط به مدل ForumPost که آن را از کلاس پایه AuditBaseEntity به ارث برده است، دارای مقدار true باشد، این امکان وجود خواهد داشت تا بتوان پست مورد نظر را ویرایش کرده و اطلاعات قبلی، در قالب یک رکورد در جدول حاصل از مدل بالا ثبت شوند.
- Reason : دلیل این ویرایش به عمل آماده
- Body : محتوای پست یا تاپیک
- Modifier : کاربر انجام دهندهی این ویرایش
- CreatedOn : زمانی که این ویرایش انجام شده است
مدل ردیابی انجمن ها
public class ForumTracker { #region Ctor /// <summary> /// create one instance of <see cref="ForumTracker"/> /// </summary> public ForumTracker() { LastMarkedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets DateTime Of Las Visit by User /// </summary> public virtual DateTime LastMarkedOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Forum that Tracked /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets Id of Forum tath Tracked /// </summary> public virtual long ForumId { get; set; } /// <summary> /// gets or sets User that tracked The forum /// </summary> public virtual User Tracker { get; set; } /// <summary> /// gets or sets Id Of User that Tracked the forum /// </summary> public virtual long TrackerId { get; set; } #endregion } public class ForumTopicTracker { #region Ctor /// <summary> /// create one instance of <see cref="ForumTopicTracker"/> /// </summary> public ForumTopicTracker() { LastVisitedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets DateTime Of Las Visit by User /// </summary> public virtual DateTime LastVisitedOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets topc that Tracked /// </summary> public virtual ForumTopic Topic { get; set; } /// <summary> /// gets or sets Id of topic that Tracked /// </summary> public virtual long TopicId { get; set; } /// <summary> /// gets or sets User that tracked The topic /// </summary> public virtual User Tracker { get; set; } /// <summary> /// gets or sets Id Of User that Tracked the topic /// </summary> public virtual long TrackerId { get; set; } /// <summary> /// gets or sets Forum /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets Identifier of Forum . used for delete /// </summary> public virtual long ForumId { get; set; } #endregion }
از مدل ForumTopicTracker هم برای مشخص کردن اینکه کاربر کدام تاپیک را و در چه تاریخی آخرین بار مشاهده کرده است، کمک میگیریم. برای این منظور از خصوصیت LastVisitedOn استفاده میشود.
البته نیاز است هنگام واکشی انجمنها و تاپیکها، یکسری بررسیهایی را بر اساس این جداول انجام داد که تشریح این بررسیها را قصد دارم هنگام پیاده سازی سیستم انجام دهم.
این قسمت از کار کمی پیچیده است و برای خودم نیز چالش داشت. سعی کردم انجمنهای سورس باز PHP را بررسی کنم تا در نهایت به تحلیل بالا دست یافتم. مدلهای ارائه شده انجمن تا این قسمت، نیازهای مورد نظر ما را برآورده خواهند کرد.
مدل سیستم نظرسنجی
public class Poll : BaseContent { #region Ctor /// <summary> /// create one instance of <see cref="Poll"/> /// </summary> public Poll() { Rating = new Rating(); PublishedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or set Date that this Poll will Expire /// </summary> public virtual DateTime? ExpireOn { get; set; } /// <summary> ///indicating this poll allow to select multi item /// </summary> public virtual bool IsMultiSelect { get; set; } /// <summary> /// gets or sets Count of this poll's votes /// </summary> public virtual long VotesCount { get; set; } /// <summary> /// indicate this Poll is approved by admin if Poll.Moderate==true /// </summary> public virtual bool IsApproved { get; set; } #endregion #region NavigationProperties /// <summary> /// get or set comments of this poll /// </summary> public virtual ICollection<PollComment> Comments { get; set; } /// <summary> /// get or set Options Of Poll For selection /// </summary> public virtual ICollection<PollOption> Options { get; set; } /// <summary> /// get or set Users List That vote for this poll /// </summary> public virtual ICollection<User> Voters { get; set; } #endregion }
- ExpireOn : زمان اتمام فرصت رای دهی که اگر نال باشد در آن صورت زمان انقضا نخواهد داشت.
- IsMultiSelect : اگر انتخاب چندگزینهای مجاز باشد، این خصوصیت، با مقدار true مقدار دهی میشود.
- VotesCount : به منظور افزایش کارآیی در نظر گرفته شده است و تعداد کل رایهای داده شدهی به نظرسنجی را در بر میگیرد.
- Voters : برای جلوگیری از رای دهی چند بارهی کاربر به یک نظرسنجی، یک ارتباط چند به چند بین کاربر و نظرسنجی برقرار کردهایم. هر کاربر به چند نظر سنجی میتواند پاسخ دهد و به هر نظرسنجی توسط چندین کاربر رای داده میشود.
- PollOptions : هر نظر سنجی تعدادی گزینهی انتخابی هم خواهد داشت که برای همین منظور و اعمال ارتباط یک به چند بین نظرسنجی و گزینههای انتخابی، لیستی از PollOption را در مدل بالا تعریف کردهایم.
مدل گزینههای نظرسنجی
public class PollOption { #region Properties /// <summary> /// gets or sets identifier of this polloption /// </summary> public virtual long Id { get; set; } /// <summary> /// gets or sets Title of this polloption /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets count of votes /// </summary> public virtual long VotesCount { get; set; } /// <summary> /// gets or sets Description of this Option for more details /// </summary> public virtual string Description { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets the poll that assosiated with this Polloption /// </summary> public virtual Poll Poll { get; set; } /// <summary> /// gets or sets the id of poll that assosiated with this Polloption /// </summary> public virtual long PollId { get; set; } #endregion }
- Title: عنوان گزینهی مورد نظر
- Description: توضیح بیشتر برای گزینهی مورد نظر
- VotesCount: تعداد باری که یک گزینه در نظر سنجی انتخاب شده است.
مدل نظرات سیستم نظرسنجی
public class PollComment : BaseComment { #region Ctor public PollComment() { CreatedOn = DateTime.Now; Rating = new Rating(); } #endregion #region NavigationProperties /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual PollComment Reply { get; set; } /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual ICollection<PollComment> Children { get; set; } /// <summary> /// gets or sets poll that this comment sent to it /// </summary> public virtual Poll Poll { get; set; } /// <summary> /// gets or sets poll'Id that this comment sent to it /// </summary> public virtual long PollId { get; set; } #endregion }
در مقالهی بعد به بررسی سیستم پیام رسانی و همچنین بخشی از سیستم تحت عنوان Collections (امکان ساخت گروههای شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسیهای مختلف) خواهیم پرداخت.
نتیجه تا این قسمت
DotNetAuth
PM> Install-Package DNTFrameworkCore -Version 1.0.0
مثال اول: یک موجودیت ساده بدون نیاز به مباحث ردیابی تغییرات
public class MeasurementUnit : Entity<int>, IAggregateRoot { public const int MaxTitleLength = 50; public const int MaxSymbolLength = 50; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Symbol { get; set; } public byte[] RowVersion { get; set; } }
کلاس جنریک Entity، در برگیرنده یکسری اعضای مشترک بین سایر موجودیتهای سیستم از جمله Id و TrackingState (به منظور سناریوهای Master-Detail)، میباشد.
نکته: در این زیرساخت برای پیاده سازی CrudService برای یک موجودیت خاص، نیاز است تا واسط IAggregateRoot را نیز پیاده سازی کرده باشد. برای پیاده سازی واسط مذکور نیاز است تا خصوصیت RowVersion را به منظور مدیریت Optimistic مباحث همزمانی، به کلاس بالا اضافه کنیم. این موضوع برای موجودیتهای وابسته به یک Aggregate ضروری نیست، چرا که آنها با AggregateRoot ذخیره خواهند شد و تراکنش جدایی برای ثبت، ویرایش و یا حذف آنها وجود ندارد.
مثال دوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت و آخرین ویرایش
public class Blog : TrackableEntity<long>, IAggregateRoot { public const int MaxTitleLength = 50; public const int MaxUrlLength = 50; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Url { get; set; } public byte[] RowVersion { get; set; } }
کلاس جنریک TrackableEntity علاوه بر خصوصیات Id و TrackingState، یکسری خصوصیت دیگر از جمله زمان ثبت، زمان آخرین ویرایش، شناسه کاربر ثبت کننده، شناسه آخرین کاربر ویرایش کننده، اطلاعات مرورگرهای آنها و ... را نیز دارا میباشد. این خصوصیات به صورت خودکار توسط زیرساخت مقداردهی خواهند شد.
مثال سوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت، آخرین ویرایش و حذف نرم
public class Blog : FullTrackableEntity<long>, IAggregateRoot { public const int MaxTitleLength = 50; public const int MaxUrlLength = 50; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Url { get; set; } public byte[] RowVersion { get; set; } }
کلاس جنریک FullTrackableEntity علاوه بر خصوصیات ذکر شده در مثال دوم، یکسری خصوصیت دیگر از جمله IsDeleted، شناسه کاربر حذف کننده، زمان حذف و ... را نیز دارا میباشد. همچنین مباحث فیلتر خودکار رکوردهای حذف شده، به صورت خودکار توسط زیرساخت انجام میگیرد که امکان غیرفعال کردن آن در شرایط مورد نیاز نیز وجود دارد.
مثال چهارم: یک موجودیت با پشتیبانی از چند مستاجری
public class Blog : Entity<long>, IAggregateRoot, ITenantEntity { public const int MaxTitleLength = 50; public const int MaxUrlLength = 50; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Url { get; set; } public byte[] RowVersion { get; set; } public long TenantId { get; set; } }
با پیاده سازی واسط ITenantEntity، به صورت خودکار خصوصیت TenantId آن با توجه به اطلاعات مستاجر جاری سیستم مقداردهی خواهد شد و همچنین فیلتر خودکار بر روی رکوردهای مستاجرهای مختلف، توسط زیرساخت انجام میشود که این مکانیزم هم قابلیت غیرفعال شدن در شرایط خاص را دارد.
مثال پنجم: یک موجودیت به همراه تعدادی موجودیت جزئی (سناریوهای Master-Detail)
public class Invoice : TrackableEntity<long>, IAggregateRoot { public InvoiceStatus Status { get; set; } public decimal TotalNet { get; set; } public decimal Total { get; set; } public decimal PayableTotal { get; set; } public decimal Debit { get; set; } public decimal Credit { get; set; } public decimal Gratuity { get; set; } public byte[] RowVersion { get; set; } public ICollection<InvoiceItem> Items { get; set; } } public class InvoiceItem : TrackableEntity { public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal Price { get; set; } public decimal UnitPriceDiscount { get; set; } public long ItemId { get; set; } public Item Item { get; set; } public long InvoiceId { get; set; } public Invoice Invoice { get; set; } }
همانطور که مشخص میباشد، موجودیت وابسته یا همان Detail، نیاز به پیاده سازی IAggregateRoot را نخواهد داشت. همانطور که اشاره شد، تراکنش مجزایی برای این موجودیتها نخواهیم داشت و درون تراکنش AggregateRoot، عملیات CRUD آنها انجام خواهد شد و برای انجام عملیات ویرایش، به همراه Root متناظر با خود، واکشی خواهند شد. این موضوع یکی از نقاط قوت زیرساخت محسوب میشود که در مقالات آینده و در قسمت طراحی سرویسهای متناظر با موجودیتهای سیستم، با جزئیات بیشتری بررسی خواهد شد.
مثال ششم: یک موجودیت با امکان شماره گذاری خودکار
public class Task : TrackableEntity, IAggregateRoot, INumberedEntity { public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 1024; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Number { get; set; } public string Description { get; set; } public TaskState State { get; set; } = TaskState.Todo; public byte[] RowVersion { get; set; } }
همانطور که در مطلب «طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید» ملاحظه کردید، نیاز است تا موجودیت مورد نظر، پیاده ساز واسط INumberedEntity نیز باشد. این واسط دارای خصوصیت رشتهای Number میباشد و همچنین زیرساخت به صورت خودکار در زمان ثبت، این خصوصیت را برای موجودیتهایی از این نوع، با رعایت مباحث همزمانی مقداردهی میکند.
مثال هفتم: یک موجودیت با امکان ذخیره سازی اطلاعات اضافی در قالب فیلد JSON
public class Task : TrackableEntity, IAggregateRoot, INumberedEntity, IExtendableEntity { public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 1024; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Number { get; set; } public string Description { get; set; } public TaskState State { get; set; } = TaskState.Todo; public byte[] RowVersion { get; set; } public string ExtensionJson { get; set; } }
با پیاده سازی واسط IExtendableEntity، یکسری متد الحاقی برروی اشیاء موجودیت مورد نظر فعال خواهند شد که امکان مقداردهی یا خواندن این اطلاعات اضافی را خواهید داشت. به عنوان مثال:
var task = new Task(); task.SetExtensionValue("Name","Value"); var value = task.ReadExtensionValue("Name"); //or any complex object as string json
با دو متد الحاقی استفاده شده در بالا، امکان مقداردهی، تغییر و خواندن مقدار خصوصیتهای اضافی را خواهیم داشت که نیاز است موجودیت مورد نظر در دل خود نگهداری کند ولی ارزش و اهمیت زیادی در Domain ندارند.
مثال هشتم: طراحی یک نوع شمارشی (Enum)
public class OrderStatus : Enumeration { public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant()); public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant()); public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant()); public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant()); public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant()); public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant()); protected OrderStatus() { } public OrderStatus(int id, string name) : base(id, name) { } }
برای سناریوهایی که صرفا قصد انتخاب یک یا چند (حالت enum flags) مورد از بین یک لیست مشخص و سپس ذخیره سازی آنها را دارید، استفاده از نوع داده enum کفایت میکند؛ ولی اگر قصد استفاده از آنها برای flow control را دارید، در این صورت به طراحی شکنندهای خواهید رسید که پر شده است از if/else هایی که مقادیر مختلف enum مورد نظر را بررسی میکنند. با استفاده از کلاس Enumeration امکان مدل کردن انوع شمارشی که مرتبط هستند با منطق تجاری سیستم را با راه حل شیء گرا خواهید داشت. در این صورت رفتارهای متناظر با هریک از فیلدهای یک نوع شمارشی میتواند به عنوان رفتاری در دل خود کپسوله شده باشد و اینبار داده و رفتار کنار هم خواهند بود.
نکته: برای مطالعه بیشتر میتوانید به مطالب ^ و ^ مراجعه کنید.
در نهایت میتوانید برای سناریوهای خاص خودتان از سایر واسط های موجود در زیرساخت، نیز به شکل زیر استفاده کنید:
نیاز به حذف نرم بدون نگهداری اطلاعات ردیابی تغییرات
public interface ISoftDeleteEntity { bool IsDeleted { get; set; } }
.با پیاده سازی واسط بالا این امکان را خواهید داشت که صرفا از مکانیزم حذف نرم استفاده کنید؛ بدون نیاز به نگهداری سایر اطلاعات
نیاز به مقداردهی خودکار زمان ثبت یک موجودیت خاص
این امر با پیاده سازی واسط زیر امکان پذیر خواهد بود.
public interface IHasCreationDateTime { DateTimeOffset CreationDateTime { get; set; } }
با توجه به اعمال اصل ISP در مباحث مطرح شده در مطلب جاری، بنا به نیاز خود از این واسطها و کلاسهای پایه پیاده ساز آنها میتوانید استفاده کنید.
public class Rectangle { public double Width; public double Height; public double Area() { return Width*Height; } public double Perimeter() { return 2*(Width + Height); } }
تذکر: ممکن است این سوال پیش بیاید که خوب ما کلاس را نوشته ایم و خودمان میدانیم چه مقادیری برای فیلدهای آن مناسب است. اما مسئله اینجاست که اولاً ممکن است کلاس تهیه شده توسط برنامه نویس دیگری مورد استفاده قرار گیرد. یا حتی پس از مدتی فراموش کنیم چه مقادیری برای کلاسی که مدتی قبل تهیه کردیم مناسب است. و از همه مهمتر این است که کلاسها و اشیاء به عنوان ابزاری برای حل مسائل هستند و ممکن است مقادیری که به فیلدها اختصاص مییابد در زمان نوشتن برنامه مشخص نباشد و در زمان اجرای برنامه توسط کاربر یا کدهای بخشهای دیگر برنامه تعیین گردد.
به طور کلی هر چه کنترل و نظارت بیشتری بر روی مقادیر انتسابی به اشیاء داشته باشیم برنامه بهتر کار میکند و کمتر دچار خطاهای مهلک و بدتر از آن خطاهای منطقی میگردد. بنابراین باید ساز و کار این نظارت را در کلاس تعریف نماییم.
برای کلاسها یک نوع عضو دیگر هم میتوان تعریف کرد که دارای این ساز و کار نظارتی است. این عضو Property نام دارد و یک مکانیسم انعطاف پذیر برای خواندن، نوشتن یا حتی محاسبه مقدار یک فیلد خصوصی فراهم مینماید.
تا اینجا باید به این نتیجه رسیده باشید که تعریف یک متغیر با سطح دسترسی عمومی در کلاس روش پسندیده و قابل توصیه ای نیست. بنابراین متغیرها را در سطح کلاس به صورت خصوصی تعریف میکنیم و از طریق تعریف Property امکان استفاده از آنها در بیرون کلاس را ایجاد میکنیم.
حال به چگونگی تعریف Propertyها دقت کنید.
public class Rectangle { private double _width = 0; private double _height = 0; public double Width { get { return _width; } set { if (value > 0) { _width = value; } } } public double Height { get { return _height; } set { if (value > 0) { _height = value; } } } public double Area() { return _width * _height; } public double Perimeter() { return 2*(_width + _height); } }
دو قسمت اضافه شده دیگر تعریف دو Property مورد نظر است. یکی عرض و دیگری ارتفاع. خط اول تعریف پروپرتی تفاوتی با تعریف فیلد عمومی ندارد. اما همان طور که میبینید هر فیلد دارای یک بدنه است که با {} مشخص میشود. در این بدنه ساز و کار نظارتی تعریف میشود.
نحوه دسترسی به پروپرتیها مشابه فیلدهای عمومی است. اما پروپرتیها در حقیقت متدهای ویژه ای به نام اکسسور (Accessor) هستند که از طرفی سادگی استفاده از متغیرها را به ارمغان میآورند و از طرف دیگر دربردارنده امنیت و انعطاف پذیری متدها هستند. یعنی در عین حال که روشی عمومی برای داد و ستد مقادیر ارایه میدهند، کد پیاده سازی یا وارسی اطلاعات را مخفی نموده و استفاده کننده کلاس را با آن درگیر نمیکنند. قطعه کد زیر چگونگی استفاده از پروپرتی را نشان میدهد.
Rectangle rectangle = new Rectangle(); rectangle.Width = 10; Console.WriteLine(rectangle.Width);
تذکر: به دو اکسسور get و set مانند دو متد معمولی نگاه کنید از این نظر که میتوانید در بدنه آنها اعمال دلخواه دیگری بجز ذخیره و بازیابی اطلاعات پروپرتی را نیز انجام دهید.
چند نکته:
- اکسسور get هنگام بازگشت یا خواندن مقدار پروپرتی اجرا میشود و اکسسور set زمان انتساب یک مقدار جدید به پروپرتی فراخوانی میشود. جالب آنکه در صورت لزوم این دو اکسسور میتوانند دارای سطوح دسترسی متفاوتی باشند.
- داخل اکسسور set کلمه کلیدی value مقدار منتسب شده را در اختیار قرار میدهد تا در صورت لزوم بتوان بر روی آن پردازش لازم را انجام داد.
- یک پروپرتی میتواند فاقد اکسسور set باشد که در این صورت یک پروپرتی فقط خواندنی ایجاد میگردد. همچنین میتواند فقط شامل اکسسور set باشد که در این صورت فقط امکان انتساب مقادر به آن وجود دارد و امکان دریافت یا خواندن مقدار آن میسر نیست. چنین پروپرتی ای فقط نوشتنی خواهد بود.
- در بدنه اکسسور set الزامی به انتساب مقدار منتسب توسط کد مشتری نیست. در صورت صلاحدید میتوانید به جای آن هر مقدار دیگری را در نظر بگیرید یا عملیات مورد نظر خود را انجام دهید.
- در بدنه اکسسور get هم هر مقداری را میتوانید بازگشت دهید. یعنی الزامی وجود ندارد حتماً مقدار فیلد خصوصی متناظر با پروپرتی را بازگشت دهید. حتی الزامی به تعریف فیلد خصوصی برای هر پروپرتی ندارید. به طور مثال ممکن است مقدار بازگشتی اکسسور get حاصل محاسبه و ... باشد.
اکنون مثال دیگری را در نظر بگیرید. فرض کنید در یک پروژه فروشگاهی در حال تهیه کلاسی برای مدیریت محصولات هستید. قصد داریم یک پروپرتی ایجاد کنیم تا نام محصول را نگهداری کند و در حال حاضر هیچ محدودیتی برای نام یک محصول در نظر نداریم. کد زیر را ببینید.
public class Product { private string _name; public string Name { get { return _name; } set { _name = value; } } }
فرض کنید پس از مدتی متوجه شدید اگر نام بسیار طولانی ای برای محصول درج شود ظاهر برنامه شما دچار مشکل میشود. پس باید بر روی این مورد نظارت داشته باشید. دیدیم که برای رسیدن به این هدف باید فیلد عمومی را فراموش و به جای آن پروپرتی تعریف کنیم. اما مسئله اینست که تبدیل یک فیلد عمومی به پروپرتی میتواند سبب بروز ناسازگاریهایی در بخشهای دیگر برنامه که از این کلاس و آن فیلد استفاده میکنند شود. پس بهتر آن است که از ابتدا پروپرتی تعریف کنیم هر چند نیازی به عملیات نظارتی خاصی نداریم. در این حالت اگر نیاز به پردازش بیشتر پیدا شد به راحتی میتوانیم کد مورد نظر را در اکسسورهای موجود اضافه کنیم بدون آنکه نیازی به تغییر بخشهای دیگر باشد.
و یک خبر خوب! از سی شارپ ۳ به بعد ویژگی جدیدی در اختیار ما قرار گرفته است که میتوان پروپرتیهایی مانند مثال بالا را که نیازی به عملیات نظارتی ندارند، سادهتر و خواناتر تعریف نمود. این ویژگی جدید پروپرتی اتوماتیک یا Auto-Implemented Property نام دارد. مانند نمونه زیر.
public class Product { public string Name { get; set; } }
البته استفاده از پروپرتی برتری دیگری هم دارد. و آن کنترل سطح دسترسی اکسسورها است. مثال زیر را ببینید.
public class Student { public DateTime Birthdate { get; set; } public double Age { get; private set; } }
تذکر: در هنگام تعریف یک فیلد میتوان از کلمه کلیدی readonly استفاده کرد تا یک فیلد فقط خواندنی ایجاد گردد. اما در اینصورت فیلد تعریف شده حتی داخل کلاس هم فقط خواندنی است و فقط در هنگام تعریف یا در متد سازنده کلاس امکان مقدار دهی به آن وجود دارد. در بخشهای بعدی مفهوم سازنده کلاس مورد بررسی خواهد گرفت.
در مقالهی قبل توانستیم یک سری
از مدلهای مربوط به وبلاگ را آماده کنیم. در ادامه به تکمیل آن و همچین
آغاز تهیهی مدلهای مربوط به اخبار و پیغام خصوصی میپردازیم.
همکاران این قسمت:
سلمان معروفی
مدل گزارش دهی
/// <summary> /// Repersents a Report template for every cms section /// </summary> public class Report { #region Ctor /// <summary> /// Create one instance for <see cref="Report"/> /// </summary> public Report() { ReportedOn = DateTime.Now; Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// gets or sets identifier for Report /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets reason of report /// </summary> public virtual string Reason { get; set; } /// <summary> /// gets or sets section that is reported /// </summary> public virtual ReportSection Section { get; set; } /// <summary> /// gets or sets sectionid that is reported /// </summary> public virtual long SectionId { get; set; } /// <summary> /// gets or sets type of report /// </summary> public virtual ReportType Type{ get; set; } /// <summary> /// gets or sets report's datetime /// </summary> public virtual DateTime ReportedOn { get; set; } /// <summary> /// indicate this report is read by admin /// </summary> public virtual bool IsRead { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets id of user that is reporter /// </summary> public virtual long ReporterId { get; set; } /// <summary> /// gets or sets id of user that is reporter /// </summary> public virtual User Reporter { get; set; } #endregion } /// <summary> /// Represents Report Section /// </summary> public enum ReportSection { News, Poll, Announcement, ForumTopic, BlogComment, BlogPost, NewsComment, PollComment, AnnouncementComment, ForumPost, User, ... } /// <summary> /// Represents Type of Report /// </summary> public enum ReportType { Spam, Abuse, Advertising, ... }
قصد داریم در این سیستم به کاربران خاصی دسترسی گزارش دادن در بخشهای مختلف را بدهیم. این دسترسیها در بخش تنظیمات سیستم قابل تغییر خواهند بود (برای مثال براساس امتیاز ، براساس تعداد پست و ... ) . این امکان میتواند برای مدیریت سیستم مفید باشد.
برای سیستم گزارش دهی به مانند سیستم امتیاز دهی عمل خواهیم کرد. در کلاس Report، خصوصیت ReportSection از نوع دادهی شمارشی میباشد که در بالا تعریف آن نیز آماده است و مشخص کنندهی بخشهایی میباشد که لازم است امکان گزارش دهی داشته باشند. خصوصیت Type هم که از نوع شمارشی ReportType میباشد، مشخص کنندهی نوع گزارشی است که داده شده است.
علاوه بر نوع گزارش، میتوان دلیل گزارش را هم ذخیره کرد که برای این منظور خصوصیت Reason در نظر گرفته شدهاست. خصوصیت IsRead هم برای مدیریت این گزارشات در پنل مدیریت در نظر گرفته شده است. اگر در مقالهی قبل دقت کرده باشید، متوجه وجود خصوصیتی به نام ReportsCount در کلاس BaseContent و BaseComment خواهید شد که برای نشان دادن تعداد گزارشهایی است که برای آن مطلب یا نظر داده شده است، استفاده میشود.
کلاس پایه فایلهای ضمیمه
/// <summary> /// Represents a base class for every attachment /// </summary> public abstract class BaseAttachment { #region Ctor public BaseAttachment() { Id = SequentialGuidGenerator.NewSequentialGuid(); AttachedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// sets or gets identifier for attachment /// </summary> public virtual Guid Id { get; set; } /// <summary> /// sets or gets name for attachment /// </summary> public virtual string FileName { get; set; } /// <summary> /// sets or gets type of attachment /// </summary> public virtual string ContentType { get; set; } /// <summary> /// sets or gets size of attachment /// </summary> public virtual long Size { get; set; } /// <summary> /// sets or gets Extention of attachment /// </summary> public virtual string Extension { get; set; } /// <summary> /// sets or gets bytes of data /// </summary> //public byte[] Data { get; set; } /// <summary> /// sets or gets Creation Date /// </summary> public virtual DateTime AttachedOn { get; set; } /// <summary> /// gets or sets counts of download this file /// </summary> public virtual long DownloadsCount { get; set; } /// <summary> /// gets or sets datetime that is modified /// </summary> public virtual DateTime ModifiedOn { get; set; } /// <summary> /// gets or sets section that this file attached there /// </summary> public virtual AttachmentSection Section { get; set; } /// <summary> /// gets or sets information of user agent /// </summary> public virtual string Agent { get; set; } #endregion #region NavigationProperties /// <summary> /// sets or gets identifier of attachment's owner /// </summary> public virtual long OwnerId { get; set; } /// <summary> /// sets or gets identifier of attachment's owner /// </summary> public virtual User Owner { get; set; } #endregion } public enum AttachmentSection { News, Announcement, ForumTopic, Conversation, BlogComment, NewsComment, PollComment, AnnouncementComment, ForumPost, BlogPost, Group, ... }
کلاس بالا اکثر خصوصیات لازم برای مدل Attachment ما را در خود دارد. قصد داریم از ارث بری TPH برای مدیریت فایلهای ضمیمه استفاده کنیم. در سیستم بستهی ما، تنها کاربران احراز هویت شده میتوانند فایل ضمیمه کنند و برای همین منظور OwnerId را که همان ارسال کنندهی فایل میباشد، به صورت Nullable در نظر نگرفتهایم.
یک سری از مشخصات که نیاز به توضیح اضافی ندارند، ولی خصوصیت AttachmentSection که از نوع شمارشی AttachmentSection است، برای دسترسی راحت کاربر به فایلهای ارسالی خود در پنل کاربری در نظر گرفته شده است. برای بخشهای (وبلاگ - اخبار - نظرسنجیها - آگهیها - انجمن) که نیاز به Privacy خاصی نیست و احراز هویت کفایت میکند، مدل زیر را در نظر گرفته ایم:
مدل فایلهای ضمیمه عمومی
/// <summary> /// Repersent the attachment for file /// </summary> public class Attachment : BaseAttachment { }
/// <summary> /// Represents one news item /// </summary> public class NewsItem : BaseContent { #region Ctor /// <summary> /// create one instance of <see cref="NewsItem"/> /// </summary> public NewsItem() { Rating = new Rating(); PublishedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// indicating that this news show on sidebar /// </summary> public virtual bool ShowOnSideBar { get; set; } /// <summary> /// indicate this NewsItem is approved by admin if NewsItem.Moderate==true /// </summary> public virtual bool IsApproved { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets newsitem's Reviews /// </summary> public ICollection<NewsComment> Comments { get; set; } #endregion }
کلاس بالا نشان دهندهی اشتراکهای ما خواهند بود. این مدل ما هم از کلاس پایهی BaseContent بحث شده در مقالهی قبل، ارث بری کرده و علاوه بر آن دو خصوصیت دیگر تحت عنوان IsApproved برای اعمال مدیریتی در نظر گرفته شده است (اگر در بخش تنظیمات سیستم اخبار، مدیریت تصمیم گرفته باشد تا اخبار جدید به اشتراک گذاشته شده با تأیید مدیریتی منتشر شوند) و خصوصیت ShowOnSideBar هم به عنوان یک تنظیم مدیریتی برای خبر خاصی در نظر گرفته شده که لازم است به صورت sticky در سایدبار نمایش داده شود.
برای اخبار نیز امکان ارسال نظر خواهیم داشت که برای این منظور لیستی از مدل زیر (NewsComment) در مدل بالا تعریف شده است .
مدل نظرات اخبار
public class NewsComment : BaseComment { #region Ctor public NewsComment() { Rating = new Rating(); CreatedOn = DateTime.Now; } #endregion #region NavigationProperties /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual NewsComment Reply { get; set; } /// <summary> /// gets or sets body of blog NewsItem's comment /// </summary> public virtual ICollection<NewsComment> Children { get; set; } /// <summary> /// gets or sets NewsItem that this comment sent to it /// </summary> public virtual NewsItem NewsItem { get; set; } /// <summary> /// gets or sets NewsItem'Id that this comment sent to it /// </summary> public virtual long NewsItemId { get; set; } #endregion }
/// <summary> /// Indicate one conversation /// </summary> public class Conversation { #region Ctor /// <summary> /// create one instance of <see cref="Conversation"/> /// </summary> public Conversation() { Id = SequentialGuidGenerator.NewSequentialGuid(); SentOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier of record /// </summary> public virtual Guid Id { get; set; } /// <summary> /// represents this conversaion is seen /// </summary> public virtual bool IsRead { get; set; } /// <summary> /// gets or sets subject of this conversation /// </summary> public virtual string Subject { get; set; } /// <summary> /// gets or sets Date that this record added /// </summary> public virtual DateTime SentOn { get; set; } /// <summary> /// indicate this record deleted by sender /// </summary> public virtual bool DeletedBySender { get; set; } /// <summary> /// indicate this record deleted by receiver /// </summary> public virtual bool DeletedByReceiver { get; set; } /// <summary> /// gets or sets Messagescount that Unread by sender of this conversation /// </summary> public virtual int UnReadSenderMessagesCount { get; set; } /// <summary> /// gets or sets Messagescount that Unread by receiver of this conversation /// </summary> public virtual int UnReadReceiverMessagesCount { get; set; } /// <summary> /// gets or sets Messagescount of this conversation for increase performance /// </summary> public virtual int MessagesCount { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets if of user that start this conversation /// </summary> public virtual long SenderId { get; set; } /// <summary> /// gets or sets user that start this conversation /// </summary> public virtual User Sender { get; set; } /// <summary> /// gets or sets id of user that is recipient /// </summary> public virtual long ReceiverId { get; set; } /// <summary> /// gets or sets user that is recipient /// </summary> public virtual User Receiver { get; set; } /// <summary> /// get or set Messages of this conversation /// </summary> public virtual ICollection<ConversationReply> Messages { get; set; } /// <summary> /// get or set Attachments that attached in this conversation /// </summary> public virtual ICollection<ConversationAttachment> Attachments { get; set; } #endregion
مدل بالا نشان دهندهی گفتگوی بین دو کاربر میباشد. هر گفتگو امکان دارد با موضوع خاصی ایجاد شود و مسلما یک کاربر بهعنوان دریافت کننده و کاربر دیگری بعنوان ارسال کننده خواهد بود. برای این منظور خصوصیات Receiver و Sender که از نوع User هستند را در این کلاس در نظر گرفتهایم.
خصوصیات DeletedBySender و DeletedByReceiver هم برای این در نظر گفته شدهاند که اگر یک طرف این گفتگو خواهان حذف آن باشد، برای آن کاربر حذف نرم انجام دهیم و فعلا برای کاربر مقابل قابل دسترسی باشد.
UnReadSenderMessagesCount و UnReadReceiverMessagesCount هم برای بالا بردن کارآیی سیستم در نظر گفته شدهاند و در واقع تعداد پیغامهای خوانده نشده در یک گفتگو به صورت متمایز برای هر دو طرف، ذخیره میشود. هر گفتگو شامل یکسری پیغام رد و بدل شده خواهد بود که بدین منظور لیستی از ConversationReplyها را در مدل بالا تعریف کردهایم.
در هر گفتگو یکسری فایل هم ممکن است ضمیمه شود ، برای این منظور هم یک لیستی از کلاس ConversationAttachment در مدل گفتگو تعریف شده است که در ادامه پیاده سازی کلاس ConversationAttachment را هم خواهیم دید.
مدل ConversationReply به شکل زیر میباشد:
/// <summary> /// Represents One Reply to Conversation /// </summary> public class ConversationReply { #region Ctor /// <summary> /// create one instance of <see cref="ConversationReply"/> /// </summary> public ConversationReply() { Id = SequentialGuidGenerator.NewSequentialGuid(); SentOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier of record /// </summary> public virtual Guid Id { get; set; } /// <summary> /// represents this conversaionReply is seen /// </summary> public virtual bool IsRead { get; set; } /// <summary> /// gets or sets body of this conversationReply /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets Date that this record added /// </summary> public virtual DateTime SentOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Parent's Id Of this ConversationReply /// </summary> public virtual Guid? ParentId { get; set; } /// <summary> /// gets or sets Parent Of this ConversationReply /// </summary> public virtual ConversationReply Parent { get; set; } /// <summary> /// get or set Children Of this ConversationReply /// </summary> public virtual ICollection<ConversationReply> Children { get; set; } /// <summary> /// gets or sets if of user that start this conversationReply /// </summary> public virtual long SenderId { get; set; } /// <summary> /// gets or sets user that start this conversationReply /// </summary> public virtual User Sender { get; set; } /// <summary> /// gets or sets Conversation that this message sent in it /// </summary> public virtual Conversation Conversation{ get; set; } /// <summary> /// gets or sets Id of Conversation that this message sent in it /// </summary> public virtual Guid ConversationId { get; set; } #endregion }
مدل بالا نشان دهندهی پیغامهای داده شده در یک گفتگو با موضوعی خاص میباشد. ساختار درختی آن هم برای ایجاد امکان جواب دهی برای پیغامها در نظر گرفته شده است (الزامی نیست). هر پیغام در یک گفتگو ارسال شده و یک ارسال کننده نیز دارد که برای این منظور به ترتیب دو خصوصیت Conversation از نوع کلاس Conversation و Sender از نوع User در نظر گرفتهایم.
با توجه به وجود Privacy در گفتگو نیاز است تا مدل فایل ضمیمه بخش گفتگوها به شکل زیر باشد:
/// <summary> /// Represents the attachment That attached in Conversation /// </summary> public class ConversationAttachment : BaseAttachment { #region NavigationProperties public virtual Conversation Conversation { get; set; } public virtual Guid? ConversationId { get; set; } #endregion }
همانطور که کمی بالاتر بحث شد، قصد اعمال ارث بری TPH را برای مدیریت فایلهای ضمیمه داریم. برای این منظور مدل بالا نیز از کلاس BaseAttachment ارث بری کرده و دو خصوصیت اضافه هم برای اعمال ارتباط یک به چند با گفتگو خواهد داشت. توجه کنید که ConversationId به صورت Nullable تعریف شدهاست.
نتیجه این قسمت
روش اول: استفادهی دستی از اعتبارسنج کتابخانهی Fluent Validation
روشهای زیادی برای استفادهی از قواعد تعریف شدهی توسط کتابخانهی Fluent Validation وجود دارند. اولین روش، فراخوانی دستی اعتبارسنج، در مکانهای مورد نیاز است. برای اینکار در ابتدا نیاز است با اجرای دستور «dotnet add package FluentValidation.AspNetCore»، این کتابخانه را در پروژهی وب خود نیز نصب کنیم تا بتوانیم از کلاسها و متدهای آن استفاده نمائیم. پس از آن، روش دستی کار با کلاس RegisterModelValidator که در قسمت قبل آنرا تعریف کردیم، به صورت زیر است:
using FluentValidationSample.Models; using Microsoft.AspNetCore.Mvc; namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { public IActionResult Index() { return View(); } [HttpPost] public IActionResult RegisterValidateManually(RegisterModel model) { var validator = new RegisterModelValidator(); var validationResult = validator.Validate(model); if (!validationResult.IsValid) { return BadRequest(validationResult.Errors[0].ErrorMessage); } // TODO: Save the model return Ok(); } } }
یک نکته: متد الحاقی AddToModelState که در فضای نام FluentValidation.AspNetCore قرار دارد، امکان تبدیل نتیجهی اعتبارسنجی حاصل را به ModelState استاندارد ASP.NET Core نیز میسر میکند:
public IActionResult RegisterValidateManually(RegisterModel model) { var validator = new RegisterModelValidator(); var validationResult = validator.Validate(model); if (!validationResult.IsValid) { validationResult.AddToModelState(ModelState, null); return BadRequest(ModelState); } // TODO: Save the model return Ok(); }
روش دوم: تزریق اعتبارسنج تعریف شده در سازندهی کنترلر
بجای وهله سازی دستی RegisterModelValidator و ایجاد وابستگی مستقیمی به آن، میتوان از روش تزریق وابستگیهای آن نیز استفاده کرد. در این حالت اعتبارسنج RegisterModelValidator با طول عمر Transient به سیستم تزریق وابستگیها معرفی شده:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<IValidator<RegisterModel>, RegisterModelValidator>(); services.AddControllersWithViews(); }
namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { private readonly IValidator<RegisterModel> _registerModelValidator; public HomeController(IValidator<RegisterModel> registerModelValidator) { _registerModelValidator = registerModelValidator; } [HttpPost] public IActionResult RegisterValidatorInjection(RegisterModel model) { var validationResult = _registerModelValidator.Validate(model); if (!validationResult.IsValid) { return BadRequest(validationResult.Errors[0].ErrorMessage); } // TODO: Save the model return Ok(); } } }
روش سوم: خودکار سازی اجرای یک تک اعتبارسنج تعریف شده
اگر متد الحاقی AddFluentValidation را به صورت زیر به سیستم معرفی کنیم:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<IValidator<RegisterModel>, RegisterModelValidator>(); services.AddControllersWithViews().AddFluentValidation(); }
namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { [HttpPost] public IActionResult RegisterValidatorAutomatically(RegisterModel model) { if (!ModelState.IsValid) { // re-render the view when validation failed. return View(model); } // TODO: Save the model return Ok(); } } }
نکته 1: تنظیمات فوق برایASP.NET Web Pages و PageModels نیز یکی است. فقط با این تفاوت که اعتبارسنجها را فقط میتوان به مدلهایی که به صورت خواص یک page model تعریف شدهاند، اعمال کرد و نه به کل page model.
نکته 2: اگر کنترلر شما به ویژگی [ApiController] مزین شده باشد:
namespace FluentValidationSample.Web.Controllers { [Route("[controller]")] [ApiController] public class HomeController : Controller { [HttpPost] public IActionResult RegisterValidatorAutomatically(RegisterModel model) { // TODO: Save the model return Ok(); } } }
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "|84df05e2-41e0d4841bb61293.", "errors": { "FirstName": [ "'First Name' must not be empty." ] } }
public void ConfigureServices(IServiceCollection services) { // ... // override modelstate services.Configure<ApiBehaviorOptions>(options => { options.InvalidModelStateResponseFactory = context => { var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p => p.ErrorMessage)).ToList(); return new BadRequestObjectResult(new { Code = "00009", Message = "Validation errors", Errors = errors }); }; }); }
روش چهارم: خودکار سازی ثبت و اجرای تمام اعتبارسنجهای تعریف شده
و در آخر بجای معرفی دستی تک تک اعتبارسنجهای تعریف شده به سیستم تزریق وابستگیها، میتوان تمام آنها را با فراخوانی متد RegisterValidatorsFromAssemblyContaining، به صورت خودکار از یک اسمبلی خاص استخراج نمود و با طول عمر Transient، به سیستم معرفی کرد. در این حالت متد ConfigureServices به صورت زیر خلاصه میشود:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews().AddFluentValidation( fv => fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>() ); }
سازگاری اجرای خودکار FluentValidation با اعتبارسنجهای استاندارد ASP.NET Core
به صورت پیشفرض، زمانیکه FluentValidation اجرا میشود، اگر اعتبارسنج دیگری نیز در سیستم تعریف شده باشد، اجرا خواهد شد. به این معنا که برای مثال میتوان FluentValidation و DataAnnotations attributes و IValidatableObjectها را با هم ترکیب کرد.
اگر میخواهید این قابلیت را غیرفعال کنید و فقط سبب اجرای خودکار FluentValidationها شوید، نیاز است تنظیم زیر را انجام دهید:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews().AddFluentValidation( fv => { fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>(); fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false; } ); }
CQRS
* نکته : Naming Convention مورد استفاده برای Commandها به صورت دستوری است و کار Command در نام آن مشخص است؛ مثال : RegisterUser, SendForgottenPasswordEmail, PlaceOrder
3- این جداسازی باعث تمرکز بیشتر شما بر روی قسمتهای مختلف برنامه میشود؛ بخشهایی که وضعیت سیستم را تغییر میدهند از بخشهایی که صرفا دادههایی را خوانده و نمایش میدهند، بطور کامل جدا شدهاند و بهراحتی قابلیت تغییر هرکدام از این بخشها را خواهید داشت.
Events
Eventها میتوانند چندین Consumer داشته باشند؛ بنابراین میتوانیم یک EventHandler را برای UserRegistered بنویسیم که Email ارسال کند و EventHandler دیگری ایجاد کنیم که Notification ای را برای کاربر بفرستد.
Event Sourcing
مزیت Event Sourcing این است که State برنامه را در زمانهای مختلفی نگه داشتهایم و میتوانیم وضعیت سیستم را در تاریخی مشخص، پیدا کنیم و در صورت بهوجود آمدن مشکلی در سیستم، وضعیت آن را تا قبل از به مشکل خوردن، بررسی کنیم.
بعنوان مثال مبلغ یک حساب بانکی را در نظر بگیرید. یکی از راههای بهروز نگه داشتن این مبلغ بعد از هر تراکنش، در نظر گرفتن یک فیلد برای مبلغ و انجام عمل Update بعد از هر تراکنش بطور مستقیم برروی آن است. در این روش بهدلیل آپدیت کردن مستقیم این فیلد داخل دیتابیس، ما وضعیت قبلی (مبلغ قبلی) را از دست خواهیم داد و برای رسیدن به مبلغ قبلی مجبور به زدن چندین کوئری دیتابیسی و دریافت تراکنشهای قبلی و ... برای رسیدن به وضعیت قبلی سیستم هستیم.
روش دیگری وجود دارد که بجای بهروزرسانی مداوم state جاری، تمام Event هایی که در آن تراکنشی داخل سیستم رخ داده و این تراکنش State برنامه را تحت تاثیر خود قرار دادهاست، داخل یک دیتابیس اضافه نماییم. در این صورت بدلیل داشتن تمام رویدادهای اتفاق افتادهی در برنامه، میتوان وضعیت جاری سیستم را شبیه سازی و متوجه شد.
* در این سری آموزشی از دیتابیس Event Store برای پیاده سازی Event Sourcing استفاده خواهیم کرد.
در مقالهی بعدی، امکانات فریمورک MediatR را بررسی خواهیم کرد.
معرفی پروژه Orchard
برای پروژه Orchad سه هدف تعیین شده است :
در حال حاضر Orchard بیشتر به عنوان یک سکو (platform) برای ساخت وب سایتهای ایجاد محتوی استفاده میشود آنچه در Orchard حائز اهمیت است ذکر این نکته است که این سیستم به طور کامل با استفاده از ابزارهای متن باز نوشته شده است. Orchard از ASP.NET MVC 3.0 به همراه View engine جدید و فوق العاده آن یعنی Razor بهره میبرد. همچنین این پروژه وابستگی زیادی به دیگر ابزارهای متن باز نظیر NHibernate برای دسترسی به دادهها و همچنین Autofac برای dependency injection دارد شایان ذکر است که مجوز استفاده از Orchard تحت لیسانس BSD است.
طبق اعلام وب سایت رسمی این پروژه در عرض حدود یک سالی که از ارائه این CMS میگذرد بیش از یک میلیون بار دانلود و بیش از 300 ماژول و تم برای آن ساخته شده است که در گالری آن در دسترس میباشد. Orchard به صورت ریلیزهای جزئی ارائه میشود و جدیدترن نسخه آن در هنگام نوشتن این متن 1.5.1 میباشد.
تعداد زیادی سیستمهای مدیریت محتوای تجاری و یا متن باز در طول این سالها با استفاده از دات نت ارائه شده اند. (DotNetNuke (DNN بدون تردید یک از معروفترین و قدرتمندترین آنها است. این CMS در ابتدا با VB.NET نوشته شد و این رویه تا مدتها ادامه داشت تا اینکه در نسخه اخیر به #C تغییر کرد. اگرچه DNN و همچنین پروژه متن باز دیگری به نام Umbraco هر دو محبوب هستند اما با استفاده از WebFormها پیاده سازی شده اند( البته Umbraco در نسخه 5 قصد داشت که از ASP.NET MVC استفاده کند اما علی رغم در دسترس قرار گرفتن این نسخه ظاهرا تیم Umbraco برای تمرکز بیشتر روی نسخه وب فرمی, تصمیم ندارند این پروژه را ادامه دهند.) امروزه وب فرمها همانند گذشته محبوب نیستند به همین دلیل رغبت کمتری برای استفاده از این CMSها نسبت به قبل وجود دارد. با توجه به شواهد موجود بسیاری از برنامه نویسان دات نتی به سمت ASP.NET MVC مهاجرت کرده اند به همین دلیل سیستم Orchard بر مبنای این تکنولوژی نسبتا جدید دات نت پیاده شده است. با استفاده از Orchard میتوان یک وب سایت با عملکرد بسیار بالا بدون نوشتن حتی یک خط کد ایجاد نمود. اما مانند هر سیستم مدیریت محتوی دیگری اگر بخواهیم به آن قابلیت هایی را اضافه کنیم که به صورت پیش فرض در آن نیست باید با ساختار آن به خوبی آشنا شویم و همچنین بر ابزارهای مورد نیاز این کار نیز احاطه داشته باشیم. برای دریافت اطلاعات بیشتر میتوانید به وب سایت رسمی این پروژه در اینجا مراجعه کنید