مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت سوم

در این قسمت به پیاده سازی و توضیح مدل‌های انجمن خواهیم پرداخت. قبل از شروع پیشنهاد می‌کنم مقالات قبلی را مطالعه کنید.
همکاران این قسمت:
سلمان معروفی 
سید مجتبی حسینی 
پیشنیاز این قسمت:
مقالات SQL Antipattern 

سعی کردیم چندین پروژه‌ی سورس باز را هم بررسی کنیم و در نهایت کاملترین و بهترین روش را پیاده سازی کنیم. NForum ، MyBB ، MVCForum ، بخش CMS مربوط به SmartStore و ساختار دیتابیس StackOverFlow ازجمله‌ی آنها هستند.

ساختار انجمن‌ها اغلب به شکل سلسله مراتبی می‌باشد و این مورد در دسته بندی آنها خیلی مفید خواهد بود. صرف اینکه بتوان برای این مورد یک مدل خود ارجاع در نظر گرفت کاری خاصی ندارد. ولی مشکل از آنجا شروع می‌شود که بخواهیم برای انجمن هایمان مدیرانی هم تعیین کنیم یا فقط تا عمق مشخصی را واکشی کنیم و خیلی چالش برانگیزتر از اینها، اگر لازم باشد دسترسی‌های مدیران یک انجمن قابلیت اعمال بر زیرشاخه‌ها را داشته باشد و در مقابل زیرشاخه‌ها هم بتوانند از این ارث بری ممانعت کنند و از این نوع چالش‌های شیرین دیگر.
مدل انجمن
  /// <summary>
    /// Represents the Forum
    /// </summary>
    public class Forum
    {

        #region Properties
        /// <summary>
        /// gets or sets Id that Identify Forum
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets Forum's title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets Description of forum
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// gets or sets value indicating Custom Slug
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets order for display forum
        /// </summary>
        public virtual long DisplayOrder { get; set; }
        /// <summary>
        /// Indicating This Forum is Active or Not
        /// </summary>
        public virtual bool IsActive { get; set; }
        /// <summary>
        /// Indicating This Forum is Close or Not
        /// </summary>
        public virtual bool IsClose { get; set; }
        /// <summary>
        /// Indicating This Forum is Private or Not
        /// </summary>
        public virtual bool IsPrivate { get; set; }
        /// <summary>
        /// sets or gets password for login to Private forums
        /// </summary>
        public virtual string PasswordHash { get; set; }
        /// <summary>
        /// sets or gets depth of forum in tree structure of forums
        /// </summary>
        public virtual int Depth { get; set; }
        /// <summary>
        /// sets or gets Count of  posts That they are Approved
        /// </summary>
        public virtual long ApprovedPostsCount { get; set; }
        /// <summary>
        /// sets or gets Count of  topics That they are Approved
        /// </summary>
        public virtual long ApprovedTopicsCount { get; set; }
        /// <summary>
        /// Gets or sets the id of last topic
        /// </summary>
        public virtual long LastTopicId { get; set; }
        /// <summary>
        /// gets or sets date of creation of last topic
        /// </summary>
        public virtual DateTime? LastTopicCreatedOn { get; set; }
        /// <summary>
        /// gets or sets title of last topic
        /// </summary>
        public virtual string LastTopicTitle { get; set; }
        /// <summary>
        /// gets or sets creator of last topic
        /// </summary>
        public virtual string LastTopicCreator { get; set; }
        /// <summary>
        /// gets or sets id of creator that create last topic
        /// </summary>
        public virtual long LastTopicCreatorId { get; set; }
        /// <summary>
        /// Indicate in this Forum Moderate Topics Before Display 
        /// </summary>
        public virtual bool ModerateTopics { get; set; }
        /// <summary>
        /// Indicate in this Forum Moderate Posts Before Dipslay
        /// </summary>
        public virtual bool ModeratePosts { get; set; }
        /// <summary>
        /// gets or sets Count of posts that they are UnApproved
        /// </summary>
        public virtual long UnApprovedPostsCount { get; set; }
        /// <summary>
        /// gets or sets Count of topics that they are UnApproved
        /// </summary>
        public virtual long UnApprovedTopicsCount { get; set; }
        /// <summary>
        /// gets or sets Rowversion
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        /// <summary>
        /// gets or sets icon name with size 200*200 px for snippet 
        /// </summary>
        public virtual string SocialSnippetIconName { get; set; }
        /// <summary>
        /// gets or sets title for snippet
        /// </summary>
        public virtual string SocialSnippetTitle { get; set; }
        /// <summary>
        /// gets or sets description for snippet
        /// </summary>
        public virtual string SocialSnippetDescription { get; set; }
        /// <summary>
        /// gets or sets path for tree structure antipattern (1/3/4/23)
        /// </summary>
        public virtual string Path { get; set; }
        /// <summary>
        /// Indicate this forum inherit moderators from parent forum
        /// </summary>
        public virtual bool IsModeratorsInherited { get; set; }
        /// <summary>
        /// gets or set datetime that Last Post is Created In this forum. used for ForumTracking 
        /// </summary>
        public virtual DateTime? LastPostCreatedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets identifier forum's parent
        /// </summary>
        public virtual long? ParentId { get; set; }
        /// <summary>
        /// sets or gets forum's parent
        /// </summary>
        public virtual Forum Parent { get; set; }
        /// <summary>
        /// sets or gets sub forums of forum
        /// </summary>
        public virtual ICollection<Forum> Children { get; set; }
        /// <summary>
        /// set or get topics of forum
        /// </summary>
        public virtual ICollection<ForumTopic> Topics { get; set; }
        /// <summary>
        /// get or set moderators of this forum
        /// </summary>
        public virtual ICollection<ForumModerator> Moderators { get; set; }
        /// <summary>
        /// get or set Subscriptions List 
        /// </summary>
        public virtual ICollection<User> Subscribers { get; set; }
        /// <summary>
        /// get or set Announcements Collection of this Forum
        /// </summary>
        public virtual ICollection<ForumAnnouncement> Announcements { get; set; }
        /// <summary>
        /// get or set Trackers List Of this Forum
        /// </summary>
        public virtual ICollection<ForumTracker> Trackers  { get; set; }
        /// <summary>
        /// get or set Posts List that Posted in this forum for increase Performance for get Posts Count 
        /// </summary>
        public virtual ICollection<ForumPost> Posts  { get; set; }
        /// <summary>
        /// get or set 
        /// </summary>
        public virtual ICollection<ForumTopicTracker> TopicTrackers  { get; set; }
        #endregion
مدل بالا نشان دهنده‌ی ساختار انجمن‌های ما می‌باشد. خصوصیت هایی که نیاز به توضیح دارند به شکل زیر می‌باشند:
  1. IsActive : مشخص کننده‌ی این است که در این انجمن امکان ارسال تاپیک و پست وجود دارد و در صورت false بودن این خصوصیت، بر تمام زیر انجمن‌ها هم اعمال خواهد شد و برای زمانی مفید است که میخواهیم برای مدتی به هر دلیل خاصی امکان ارسال تاپیک و پست را برای انجمن خاصی، ندهیم. 
  2. IsColsed : خصوصت اولی که مطرح شد اگر مقدار false بگیرد، همچنان کاربران می‌توانند تایپک‌ها و پست‌های قبلی را مشاهده و مطالعه کنند. ولی با مقدار دهی این خصوصیت با مقدار false، امکان کلیه‌ی فعالیت‌ها و مشاهده‌ای را از محتوای این انجمن و زیر انجمن‌های آن نخواهیم داشت.
  3. IsPrivate : برای مواقعی که لازم است برای انجمن خاصی کلمه‌ی عبور در نظر بگیریم تا افراد خاص که کلمه‌ی عبور آن را دارند بتوانند در آن انجمن فعالیت کنند، در نظر گرفته شده است.
  4. ApprovedPostsCount , UnApprovedPostsCount,ApprovedTopicsCount,UnApprovedTopicsCount : برای بالا بردن کارآیی سیستم به مانند مدل‌های قبل در نظر گرفته شده‌اند.
  5. LastTopicId, LastTopicTitle , LastTopicCreator , LastTopicCreatorId , LastTopicCreatedOn: همچنین برای افزایش کارآیی سیستم و نمایش به عنوان قسمتی از مشخصات قابل مشاهده از هر انجمن، در نظر گرفته شده‌اند.
  6. Depth : برای نشان دادن عمق گره در درخت استفاده می‌شود که هنگام درج انجمن، این مورد از نتیجه‌ی جمع عمق پدر انتخاب شده و یک، به دست خواهد آمد. این مورد هنگام واکشی برای مثال 4 سطح اول برای نمایش آنها در صفحه‌ی اول انجمن به صورت سلسله مراتبی خیلی مفید خواهد بود.
  7. Path : برای استفاده از SQL Antipattern شمارش مسیر در نظر گرفته شده است. این مورد جزء Best Practice‌‌ها می‌تواند باشد. چون هم با استفاده از ساختار خود ارجاع، درخت خود را داریم و با این Antipattern کوئری‌های مربوط به درخت خیلی راحت خواهد بود.
  8. IsModeratorsInherited : اگر لازم است مدیران انجمن، پدر را به عنوان مدیر خود قبول کنند، این خصوصیت مقدار true خواهد گرفت.
  9. Subscribers : هر انجمنی می‌تواند یکسری مشترک نیز داشته باشد (به منظور اطلاع رسانی با درج یک تاپیک جدید در خود انجمن یا زیر انجمن‌های آن) .
  10. Posts : به منظور افزایش کارایی هنگام محاسبه تعداد پست‌های ارسالی در یک انجمن  ، در نظر گرفته شده است.
  11. TopicTrackers : در مقاله بعد توضیح داده خواهد شد.
  12. LastPostCreatedOn : به منظور استفاده از آن برای سیستم Tracking انجمن‌ها استفاده خواهد شد . 
ساختار درختی آن هم قابل مشاهده بوده  و نیاز به توضیح خاضی ندارد. در هر انجمن ما، یکسری تاپیک مطرح خواهد شد و برای این منظور لیستی از ForumTopic را در این کلاس معرفی کرده‌ایم. 
علاوه بر اینها انجمن‌های ما مدیرانی هم خواهند داشت که برای این منظور نیز لیستی از ForumModerator را در مدل بالا تعریف کرده‌ایم. همچنین تصمیم گرفتیم امکانی را برای سیستم انجمن در نظر بگیرم تا اگر لازم بود یک سری اعلان در بالای انجمن‌ها نشان داده شوند و در بخش مدیریت بتوان این امکان را هندل کرد؛ که لیستی  است از ForumAnnouncement‌های مدل بالا.

مدل مدیران انجمن
 /// <summary>
    /// Represents The Moderator For Forum
    /// </summary>
    public class ForumModerator
    {
        #region NavigationProperties
        /// <summary>
        /// gets or sets Forum
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets identifier of forum
        /// </summary>
        public virtual long ForumId { get; set; }
        /// <summary>
        /// gets or sets user that moderate forum
        /// </summary>
        public virtual User Moderator { get; set; }
        /// <summary>
        /// gets or sets id of user that moderate forum
        /// </summary>
        public virtual long ModeratorId { get; set; }
        /// <summary>
        /// gets or sets permission of user that moderate forum
        /// </summary>
        public virtual ForumModeratorPermissions Permissions { get; set; }
        /// <summary>
        /// indicate moderator's permissions in this forum apply with
        /// </summary>
        public virtual bool ApplyChildren { get; set; }
        #endregion
    }

    [Flags]
    public enum  ForumModeratorPermissions
    {
        CanEditPosts=1,
        CanDeletePosts=2,
        CanManageTopics=4,
        CanOpenCloseTopics=8,
       ...
    }
این مدل نشان می‌دهد که کاربر x به عنوان مدیر انجمن y می‌باشد و یکسری دسترسی‌ها را نیز در این انجمن خواهد داشت.
  1. ApplyChildren : برای اعمال دسترسی‌های مدیریتی کاربر x به زیر انجمن‌های انجمن y البته اگر خصوصیت IsModeratorsInherited زیر انجمن‌های مورد نظر با مقدار true مقدار دهی شده باشد.
  2. Permissions : از نوع ForumModeratorPermissions و نگهدارنده دسترسی‌های کاربر x به عنوان مدیر انجمن y، می‌باشد.
  3. نکته : برای این مدل آی دی در نظر گرفته نشده است و از کلید مرکب متشکل از ForumId و ModeratorId استفاده خواهیم کرد. 
مدل اعلان‌های انجمن
    /// <summary>
    /// Represents the Announcement that shown Top Of Forums
    /// </summary>
    public class ForumAnnouncement
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ForumAnnouncement"/>
        /// </summary>
        public ForumAnnouncement()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
           CreatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Identifier 
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets DateTime That this Announcement Will be Shown
        /// </summary>
        public virtual DateTime StartOn { get; set; }
        /// <summary>
        /// gets or sets DateTime That this Announcement Will be Finished 
        /// </summary>
        public virtual DateTime? ExpireOn { get; set; }
        /// <summary>
        /// gets or sets Content of this Announcement
        /// </summary>
        public virtual string Message { get; set; }
        /// <summary>
        /// Indicate this Announcement Will be shown on Children Forums
        /// </summary>
        public virtual bool ApplyChildren { get; set; }
        /// <summary>
        /// gets or sets datetime that this record created
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Forum that associated With this Announcement
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets Identifier of Forum that associated With this Announcement
        /// </summary>
        public virtual long ForumId { get; set; }
        #endregion
    }
مدل بالا نشان دهنده‌ی اعلان‌هایی است که می‌توان با امکان تنظیم زمان آغاز و اتمام، آنها را در صفحات انجمن‌ها نمایش داد. این امکان هم از لحاظ مدیریتی می‌تواند مفید باشد و هم اگر لازم شد، تبلیغی انجام شود. برای اعمال رابطه‌ی یک به چند، یک خصوصیت از نوع Forum با همراه ForumId را در مدل بالا تعریف کرده‌ایم.
  1. ApplyChildren : برای مشخص کردین نمایش این اعلان در زیر انجمن‌های انجمن مورد نظر
  2. ExpireOn : به این دلیل نال پذیر در نظر گرفته شده است که اگر لازم بود، در زمان مشخصی به پایان نرسد و با null مقدار دهی شود.
کلاس پایه AuditBaseEntity 
 /// <summary>
    /// Represents a base class for AuditLog
    /// </summary>
    public abstract class AuditBaseEntity
    {
        #region Properties
        /// <summary>
        /// sets or gets identifier
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets datetime that is created
        /// </summary>
        public  virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime? LastModifiedOn { get; set; }
        /// <summary>
        /// gets or sets reason of Last Update for increase performance
        /// </summary>
        public virtual string LastModifyReason { get; set; }
        /// <summary>
        /// gets or sets displayName of Last Modifier  for increase performance
        /// </summary>
        public virtual string LastModifier{ get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// gets or sets rowversion for synchronization problem
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        /// <summary>
        /// gets or sets count of this content's Updates
        /// </summary>
        public virtual int ModifyCount { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets creator of this record
        /// </summary>
        public virtual User Creator { get; set; }
        /// <summary>
        /// gets or sets creator's Id of this record
        /// </summary>
        public virtual long CreatorId { get; set; }
        #endregion
    }
این کلاس به منظور کپسوله کردن یکسری فیلد تکراری برای مدل‌هایی که نیاز است آخرین تغییر دهنده و زمان آن را ذخیره کنند، در نظر گرفته شده است و هدف از آن هیچ گونه اعمال ارث بری TPH یا TPT هم نیست.
ModifyLocked : برای زمانی مفید است که مدیریت امکان ویرایش یک مطلب را به صورت دستی غیرفعال میکند.
مدل تاپیک ها
  /// <summary>
    /// Represents the Topic in the Forums
    /// </summary>
    public class ForumTopic 
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ForumTopic"/>
        /// </summary>
        public ForumTopic()
        {
            CreatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets identifier
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets datetime that is created
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets Title Of this topic
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets name of tags that assosiated with 
        /// this content fo increase performance
        /// </summary>
        public virtual string TagNames { get; set; }
        /// <summary>
        /// indicate this topic is Sticky and will be shown top of forum
        /// </summary>
        public virtual bool IsSticky { get; set; }
        /// <summary>
        /// indicate this topic is closed
        /// </summary>
        public virtual bool IsClosed { get; set; }
        /// <summary>
        /// gets or sets identifier of  last post in this topic
        /// </summary>
        public virtual long LastPostId { get; set; }
        /// <summary>
        /// gets or sets identifier of Last user that post in this topic
        /// </summary>
        public virtual long LastPosterId { get; set; }
        /// <summary>
        /// gets or sets title of last Post in this topic
        /// </summary>
        public virtual string LastPostTitle { get; set; }
        /// <summary>
        /// gets or sets displayName of user that create lastpost in this topic
        /// </summary>
        public virtual string LastPoster { get; set; }
        /// <summary>
        /// gets or sets datetime that last post posted in this topic
        /// </summary>
        public virtual DateTime? LastPostCreatedOn { get; set; }
        /// <summary>
        /// indicate this topic is approved
        /// </summary>
        public virtual bool IsApproved { get; set; }
        /// <summary>
        /// indicate this topic is type of Announcements and shown in Annoucements sections
        /// </summary>
        public virtual bool IsAnnouncement { get; set; }
        /// <summary>
        /// gets or sets viewed count 
        /// </summary>
        public virtual long ViewCount { get; set; }
        /// <summary>
        /// gets or sets count of posts that they are approved
        /// </summary>
        public virtual int ApprovedPostsCount { get; set; }
        /// <summary>
        /// gets or sets count of posts that they are Unapproved
        /// </summary>
        public virtual int UnApprovedPostsCount { get; set; }
        /// <summary>
        /// gets or sets specifications of this topic's rating
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets datetime that this topic closed
        /// </summary>
        public virtual DateTime? ClosedOn { get; set; }
        /// <summary>
        /// gets or sets reason that this topic colsed
        /// </summary>
        public virtual string ClosedReason { get; set; }
        /// <summary>
        /// gets or sets count of reports
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// indicate the posts of this topic should be Moderate Before Dipslay
        /// </summary>
        public virtual bool ModeratePosts { get; set; }
        /// <summary>
        /// gets or sets Level of this topic
        /// </summary>
        public virtual ForumTopicLevel Level { get; set; }
        /// <summary>
        /// gets or sets type of this topic
        /// </summary>
        public virtual ForumTopicType Type { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Collection of tags that associated with this topic
        /// </summary>
        public virtual ICollection<Tag> Tags { get; set; }
        /// <summary>
        /// gets or sets forum
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets identifier of Forum
        /// </summary>
        public virtual long ForumId { get; set; }
        /// <summary>
        /// gets or sets Posts Of this topic
        /// </summary>
        public virtual ICollection<ForumPost> Posts { get; set; }
        /// <summary>
        /// get or set Subscriptions List 
        /// </summary>
        public virtual ICollection<User> Subscribers { get; set; }
        /// <summary>
        /// get or set Trackkers list of this Topic
        /// </summary>
        public virtual ICollection<ForumTopicTracker> Trackers { get; set; }
        /// <summary>
        /// gets or sets creator of this record
        /// </summary>
        public virtual User Creator { get; set; }
        /// <summary>
        /// gets or sets creator's Id of this record
        /// </summary>
        public virtual long CreatorId { get; set; }
        #endregion
    }

 public enum ForumTopicType
    {
        Non,
        Tutorial,
        Conversation,
        Question,
        News,
        Article
    }

    public enum ForumTopicLevel
    {
        Professional,
        Intermediate,
        Beginner
    }
مدل بالا مشخص کننده‌ی تاپیک‌های انجمن می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
  1. LastPostId , LastPosterId, LastPoster  , LastPostTitle  ,LastPostCreatedOn: برای افزایش کارآیی سیستم در نظر گرفته شده‌اند.
  2. ModeratePosts : اگر لازم است پست‌های یک تاپیک خاص، قبل از نمایش مدیریت شوند، با true مقدار دهی خواهد شد.
  3. Tags : لیستی از برچسب‌ها که برای اعمال رابطه‌ی چند به چند با مدل برچسب‌های معرفی شده‌ی در مقاله اول، در نظر گرفته شده است.
  4. Posts : در هر تاپیکی یک سری پست به عنوان جواب‌های آن ارسال خواهد شد.
  5. Subscribers  : به مانند انجمن‌ها، تاپیک‌های ما هم می‌توانند یک سری مشترک داشته باشند، تا از تغییرات این تاپیک مطلع شوند .
  6. Trackers : مربوط به سیستم Tracking تاپیک میباشد. و در مقاله بعد توضیح داده خواهد شد.

 حتما لازم خواهد بود تاریخچه‌ی تغییرات برای  پست‌های ارسالی ذخیره شوند؛ در مقاله‌ی بعدی به این موضوع هم خواهیم پرداخت.
نتیجه این قسمت

مطالب
پیاده سازی عملیات CRUD با استفاده از پروتکل OData
OData  یکی از بهترین روش‌های پیاده سازی RESTful Apis میباشد. Open Data Protocol یا به اصطلاح OData یک data access protocol برای وب میباشد که اجازه‌ی تغییر دادن و نوشتن کوئری درون CRUD مربوطه را میدهد (create - read - update - delete). Asp.Net WebApi از ورژن 3 و 4 این پروتکل بطور کامل پشتیبانی می‌نماید.
در این آموزش ما از WebApi 2.2 , OData V4, Ef 6 استفاده کرده‌ایم.
با استفاده از ویژوال استودیو یک پروژه‌ی Asp.Net را از نوع Empty به نام ProductService میسازیم.

هم چنین در قسمت Add folders and core references تیک گزینه‌ی Web Api را نیز فعال مینماییم.


حال احتیاج به نصب پکیج OData با استفاده از nuget package manager داریم. کافیست دستور زیر را در package manager console وارد نماییم.

Install-Package Microsoft.AspNet.Odata

این دستور آخرین ورژن Odata package را از nuget دانلود مینماید.

بعد از نصب شدن OData نیاز به اضافه کردن یک Model داریم. کلاسی را به نام Product در پوشه‌ی Models میسازیم.

کلاس Product.cs حاوی فیلد‌های زیر است.

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }
    }
}

پراپرتی Id، کلید این entity است و کلاینت میتواند کوئری را بر روی entity، به وسیله‌ی key بزند. برای مثال برای گرفتن Product با Id برابر 2، باید این url را ارسال نمود "(2)Products/"

پرواضح است که Id در Database به عنوان Primary key در نظر گرفته شده است.

حال احتیاج به نصب Entity Framework داریم که با ارسال دستور زیر از طریق nuget نصب خواهد شد

Install-Package EntityFramework

بعد از نصب کردن ef نیاز به اضافه کردن connection string در web config داریم.

<connectionStrings>
    <add name="ProductsContext" connectionString="Data Source=.; 
        Initial Catalog=ProductsContext; Integrated Security=True;MultipleActiveResultSets=True;"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

الان میتوانیم کلاس ProductsContext را درون پوشه‌ی Models ایجاد نماییم. محتویات آن را به صورت زیر وارد مینماییم

using System.Data.Entity;
namespace ProductService.Models
{
    public class ProductsContext : DbContext
    {
        public ProductsContext() 
                : base("name=ProductsContext")
        {
        }
        public DbSet<Product> Products { get; set; }
    }
}

درون Constructor کلاس ProductsContext، داریم name=ProductsContext که باید برابر name درون connection string باشد.

حال نیاز به کانفیگ OData داریم. درون پوشه‌ی App_Start و کلاس WebApiConfig.cs محتویات زیر را جایگزین متد register نمایید:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());
    }
}

این کد دو فرآیند زیر را انجام میدهد

1) ساخت Entity Data Model (EDM)

2) اضافه کردن route

EDM یک مدل انتزاعی از data است. EDM برای تولید سند metadata استفاده میشود. کلاس ODataModelBuilder برای ساخت EDM با استفاده از default naming convention میباشد که باعث کاهش کد‌ها میشود. ضمنا کلاس MapODataServiceRoute برای ساخت OData v4 route میباشد. همانگونه که اطلاع دارید، تعریف route برای مدیریت کردن WebApi و چگونگی مسیریابی درخواست‌های http میباشد.

اگر application شما احتیاج به چند OData endpoint داشته باشد، میتوانید برای هر کدام route‌های جدا و همچنین نام یکتایی را برای routeName و routePrefix آن در نظر بگیرید.


اضافه کردن OData Controller

یک Controller، کلاسی برای مدیریت کردن درخواست‌های http میباشد. شما باید Controllerهای مجزایی را برای هر entity set در OData service خود بسازید. در این مقاله Controller مربوط به موجودیت Product را میسازیم.

در Solution Explorer با کلیک راست بر روی پوشه‌ی Controller، کلاسی به نام ProducsController را میسازیم. دقت کنید نام آن حتما باید به Controller ختم شود.

در OData V3 میتوانیم Controller را با استفاده از Scaffolding بسازیم؛ ولی در V4 این ویژگی وجود ندارد!

محتویات زیر را در این کنترلر اضافه مینماییم:

using ProductService.Models;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.OData;
namespace ProductService.Controllers
{
    public class ProductsController : ODataController
    {
        ProductsContext db = new ProductsContext();
        private bool ProductExists(int key)
        {
            return db.Products.Any(p => p.Id == key);
        } 
        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

این مرحله‌ی ابتدایی از پیاده سازی کنترلر میباشد و در قسمت بعد به پیاده سازی CRUD مربوط به آن میپردازیم.


Querying The Entity Set

این 2 متد را به کنترلر خود اضافه مینماییم

[EnableQuery]
public IQueryable<Product> Get()
{
    return db.Products;
}
[EnableQuery]
public SingleResult<Product> Get([FromODataUri] int key)
{
    IQueryable<Product> result = db.Products.Where(p => p.Id == key);
    return SingleResult.Create(result);
}

ویژگی EnableQuery به معنای امکان Query زدن از سمت کلاینت به آن میباشد. FromODataUri نیز برای امکان پاس دادن پارامتر از طریق Uri است.

متد Get بدون پارامتر، قادر به برگرداندن تمامی Product‌ها میباشد و متد Get با پارامتر، قادر به برگرداندن آن Product خاص با استفاده از unique Id است.

در صورت داشتن EnableQuery با استفاده از Query Option هایی مثل filter$ و sort$ و غیره از سمت کلاینت قادر به تغییر دادن کوئری‌های خود هستیم.


Adding and Entity to Entity Set

برای اجازه دادن به کلاینت، جهت اضافه کردن یک Product به دیتابیس، متد Post زیر را اضافه مینماییم

public async Task<IHttpActionResult> Post(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Created(product);
}


Updation an Entity

OData از دو روش متفاوت برای Update کردن یک موجودیت استفاده مینماید.

1) Patch : امکان partial update برای موجودیت مربوطه را فراهم میسازد.

2) Put : موجودیت جدید را به صورت کامل جایگزین مینماید.

مشکل روش Put این است که کلاینت مجبور به ارسال تمامی فیلد‌های مربوطه میباشد. حتی آن هایی که اساسا تغییری نکرده‌اند. بنابراین روش Patch ترجیح داده میشود.

در هر صورت ما به پیاده سازی هر دو روش می‌پردازیم:

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var entity = await db.Products.FindAsync(key);
    if (entity == null)
    {
        return NotFound();
    }
    product.Patch(entity);
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(entity);
}
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    if (key != update.Id)
    {
        return BadRequest();
    }
    db.Entry(update).State = EntityState.Modified;
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(update);
}

در قسمت Patch کنترلر از <Delta<T استفاده میکند که typeی است برای track کردن تغییرات در مدل مربوطه.


Deleting an Entity

برای حذف هر موجودیت نیز کافیست متد زیر را به کنترلر خود اضافه نمایید:

public async Task<IHttpActionResult> Delete([FromODataUri] int key)
{
    var product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return StatusCode(HttpStatusCode.NoContent);
}

من چند رکورد تستی را به صورت زیر وارد کرده‌ام:

حال پروژه‌ی خود را run نموده و آدرس زیر را وارد نمایید:

http://localhost:YourPort/Products

پاسخ، مجموعه‌ای از entity‌های زیر خواهد بود:

{
  "@odata.context":"http://localhost:4516/$metadata#Products","value":[
    {
      "Id":1,"Name":"Ali","Price":2.00,"Category":"aaa"
    },{
      "Id":2,"Name":"Reza","Price":1.00,"Category":"bbb"
    },{
      "Id":3,"Name":"Ahmad","Price":0.00,"Category":"ccc"
    }
  ]
}

شما میتوانید از هر کدام از فیلتر‌های زیر برای کوئری زدن از کلاینت به سمت سرور استفاده نمایید. بطور مثال هر کدام از اینها پاسخ متفاوت و مربوط به خود را برگشت میدهد:

/Products(2)

Productی با آی دی 2 را بر میگرداند.

/Products?$filter=Id gt 1

محصولی را با آی دی بزرگتر از 1، بر میگرداند.

Products?$select=Name

روی محصولات select زده و فقط فیلد Name آن‌ها را بر میگرداند.

Products?$select=Name,Price

آرایه‌ای از objectهایی با پراپرتی Name و Price را بر میگرداند.

/Products?$top=3

فقط 3 رکورد اول را بر میگرداند.


همانطور که ملاحظه میفرمایید، استفاده از OData باعث کمتر شدن کد‌های سمت سرور و همچنین امکان کوئری زدن از سمت کلاینت به سمت سرور را مهیا می‌کند.

بعد از خواندن این مقاله ممکن است به این مساله فکر کنید که این کار باعث کاهش امنیت میشود. باید عرض کنم که امکانات زیادی برای محدود کردن کوئری‌ها، فراهم شده است و هیچ نگرانی از این بابت وجود ندارد. بطور مثال میتوانید تعیین کنید که از entity مربوطه فقط حداکثر 3 پراپرتی قابلیت کوئری زدن را دارند؛ یا اینکه حداکثر در هر کوئری، 10 رکورد قابلیت پاسخ دادن خواهد داشت.

پس بدین صورت میباشد که شما حداکثر امکانات ممکن را به سمت کلاینت میدهید و اختیار بدان واگذار شده که آیا از این امکانات حداکثری، استفاده نماید یا خیر.

امکانات این پروتکل منحصر به فرد است و در مقاله‌های بعدی به جزئیات بیشتر و دقیق‌تری خواهیم پرداخت.

اشتراک‌ها
ارتقاء JavaScript Editor در Visual Studio 2015
JavaScript is an important technology for development on many different platforms, including web, mobile app, and server programming. In Visual Studio 2013 we already support IntelliSense, Go to Definition, colorization, and formatting of JavaScript source, along with several other features. We’ve carried these forward into Visual Studio 2015 and we’ve improved the experience for JavaScript developers by focusing on three key areas:
  1. Improving the development experience when using popular JavaScript libraries
  2. Adding support for new JavaScript ECMAScript 2015 (also known as ES2015 and formerly ES6) language and web browser APIs
  3. Increasing your productivity in complex JavaScript code bases
ارتقاء JavaScript Editor در Visual Studio 2015
مطالب
بررسی میزان پوشش آزمون‌های واحد به کمک برنامه PartCover

همیشه در حین توسعه‌ی یک برنامه این سؤالات وجود دارند:
- چند درصد از برنامه تست شده است؟
- برای چه تعدادی از متدهای موجود آزمون واحد نوشته‌ایم؟
- آیا همین آزمون‌های واحد نوشته شده و موجود، کامل هستند و تمام عملکرد‌های متدهای مرتبط را پوشش می‌دهند؟

این سؤالات به صورت خلاصه مفهوم Code coverage را در بحث Unit testing ارائه می‌دهند: برای چه قسمت‌هایی از برنامه آزمون واحد ننوشته‌ایم و میزان پوشش برنامه توسط آزمون‌های واحد موجود تا چه حدی است؟
بررسی این سؤالات در یک پروژه‌ی کم حجم، ساده بوده و به صورت بازبینی بصری ممکن است. اما در یک پروژه‌ی بزرگ نیاز به ابزار دارد. به همین منظور تعدادی برنامه جهت بررسی code coverage مختص پروژه‌های دات نتی تابحال تولید شده‌اند که در ادامه لیست آن‌ها را مشاهده می‌کنید:
و ...

تمام این‌ها تجاری هستند. اما در این بین برنامه‌ی PartCover سورس باز و رایگان بوده و همچنین مختص به NUnit نیز تهیه شده است. این برنامه را از اینجا می‌توانید دریافت و نصب کنید. در ادامه نحوه‌ی تنظیم آن‌را بررسی خواهیم کرد:

الف) ایجاد یک پروژه آزمون واحد جدید
جهت توضیح بهتر سه سؤال مطرح شده در ابتدای این مطلب، بهتر است یک مثال ساده را در این زمینه مرور نمائیم: (پیشنیاز: (+))
یک Solution جدید در VS.NET آغاز شده و سپس دو پروژه جدید از نوع‌های کنسول و Class library به آن اضافه شده‌اند:



پروژه کنسول، برنامه اصلی است و در پروژه Class library ، آزمون‌های واحد برنامه را خواهیم نوشت.
کلاس اصلی برنامه کنسول به شرح زیر است:
namespace TestPartCover
{
public class Foo
{
public int DoFoo(int x, int y)
{
int z = 0;
if ((x > 0) && (y > 0))
{
z = x;
}
return z;
}

public int DoSum(int x)
{
return ++x;
}
}
}
و کلاس آزمون واحد آن در پروژه class library مثلا به صورت زیر خواهد بود:
using NUnit.Framework;

namespace TestPartCover.Tests
{
[TestFixture]
public class Tests
{
[Test]
public void TestDoFoo()
{
var result = new Foo().DoFoo(-1, 2);
Assert.That(result == 0);
}
}
}
که نتیجه‌ی بررسی آن توسط NUnit test runner به شکل زیر خواهد بود:



به نظر همه چیز خوب است! اما آیا واقعا این آزمون کافی است؟!

ب) در ادامه به کمک برنامه‌ی PartCover می‌خواهیم بررسی کنیم میزان پوشش آزمون‌های واحد نوشته شده تا چه حدی است؟

پس از نصب برنامه، فایل PartCover.Browser.exe را اجرا کرده و سپس از منوی فایل، گزینه‌ی Run Target را انتخاب کنید تا صفحه‌ی زیر ظاهر شود:



توضیحات:
در قسمت executable file آدرس فایل nunit-console.exe را وارد کنید. این برنامه چون در حال حاضر برای دات نت 2 کامپایل شده امکان بارگذاری dll های دات نت 4 را ندارد. به همین منظور فایل nunit-console.exe.config را باز کرده و تنظیمات زیر را به آن اعمال کنید (مهم!):
<configuration>
<startup>
<supportedRuntime version="v4.0.30319" />
</startup>

و همچنین
<runtime>
<loadFromRemoteSources enabled="true" />

در ادامه مقابل working directory‌ ، آدرس پوشه bin پروژه unit test را تنظیم کنید.
در این حالت working arguments به صورت زیر خواهند بود (در غیراینصورت باید مسیر کامل را وارد نمائید):
TestPartCover.Tests.dll /framework=4.0.30319 /noshadow

نام dll‌ وارد شده همان فایل class library تولیدی است. آرگومان بعدی مشخص می‌کند که قصد داریم یک پروژه‌ی دات نت 4 را توسط NUnit بررسی کنیم (اگر ذکر نشود پیش فرض آن دات نت 2 خواهد بود و نمی‌تواند اسمبلی‌های دات نت 4 را بارگذاری کند). منظور از noshadow این است که NUnit‌ مجاز به تولید shadow copies از اسمبلی‌های مورد آزمایش نیست. به این صورت برنامه‌ی PartCover می‌تواند بر اساس StackTrace نهایی، سورس متناظر با قسمت‌های مختلف را نمایش دهد.
اکنون نوبت به تنظیم Rules آن است که یک سری RegEx هستند؛ به عبارتی چه اسمبلی‌هایی آزمایش شوند و کدام‌ها خیر:
+[TestPartCover]*
-[nunit*]*
-[log4net*]*

همانطور که ملاحظه می‌کنید در اینجا از اسمبلی‌های NUnit و log4net صرفنظر شده است و تنها اسمبلی TestPartCover (همان برنامه کنسول، نه اسمبلی برنامه آزمون واحد) بررسی خواهد گردید.
اکنون بر روی دکمه Save در این صفحه کلیک کرده و فایل نهایی را ذخیره کنید (بعدا توسط دکمه Load در همین صفحه قابل بارگذاری خواهد بود). حاصل باید به صورت زیر باشد:
<PartCoverSettings>
<Target>D:\Prog\Libs\NUnit\bin\net-2.0\nunit-console.exe</Target>
<TargetWorkDir>D:\Prog\1390\TestPartCover\TestPartCover.Tests\bin\Debug</TargetWorkDir>
<TargetArgs>TestPartCover.Tests.dll /framework=4.0.30319 /noshadow</TargetArgs>
<Rule>+[TestPartCover]*</Rule>
<Rule>-[nunit*]*</Rule>
<Rule>-[log4net*]*</Rule>
</PartCoverSettings>

برای شروع به بررسی، بر روی دکمه Start کلیک نمائید. پس از مدتی، نتیجه به صورت زیر خواهد بود:



بله! آزمون واحد تهیه شده تنها 39 درصد اسمبلی TestPartCover را پوشش داده است. مواردی که با صفر درصد مشخص شده‌اند، یعنی فاقد آزمون واحد هستند و نکته مهم‌تر پوشش 91 درصدی متد DoFoo است. برای اینکه علت را مشاهده کنید از منوی View ، گزینه‌ی Coverage detail را انتخاب کنید تا تصویر زیر نمایان شود:



قسمت‌ نارنجی در اینجا به معنای عدم پوشش آن در متد TestDoFoo تهیه شده است. تنها قسمت‌های سبز را توانسته‌ایم پوشش دهیم و برای بررسی تمام شرط‌های این متد نیاز به آزمون‌های واحد بیشتری می‌باشد.

روش نهایی کار نیز به همین صورت است. ابتدا آزمون واحد تهیه می‌شود. سپس میزان پوشش آن بررسی شده و در ادامه سعی خواهیم کرد تا این درصد را افزایش دهیم.

مطالب دوره‌ها
نگاهی به گزینه‌های مختلف مهیای جهت میزبانی SignalR
حداقل چهار گزینه برای Hosting سرویس‌های Hub برنامه‌های مبتنی بر SignalR وجود دارند که تا به اینجا، مورد دوم آن بیشتر بررسی گردید:
1) OWIN
2) ASP.NET Hosting
3) Self Hosting
4) Cloud و ویندوز Azure

1) OWIN
اگر به اسمبلی‌های همراه با SignalR دقت کنید، یکی از آن‌ها Microsoft.AspNet.SignalR.Owin.dll نام دارد. OWIN مخفف Open web server interface for .NET است و کار آن ایجاد لایه‌ای بین وب سرورها و برنامه‌های وب می‌باشد. یکی از اهداف مهم آن ترغیب دنیای سورس باز به تهیه ماژول‌های مختلف قابل استفاده در وب سرورهای دات نتی است. نکته‌ی مهمی که در SignalR و کلیه میزبان‌های آن وجود دارد، بنا شدن تمامی آن‌ها برفراز OWIN می‌باشد.

2) ASP.NET Hosting
بدون شک، میزبانی ASP.NET از هاب‌های SignalR، مرسوم‌ترین روش استفاده از این فناوری می‌باشد. این نوع میزبانی نیز برفراز OWIN بنا شده است. نصب آن توسط اجرای دستور پاور شل ذیل در یک پروژه وب صورت می‌گیرد:
 PM> Install-Package Microsoft.AspNet.SignalR

3) خود میزبانی یا Self hosting
خود میزبانی نیز برفراز OWIN تهیه شده است و برای پیاده سازی آن نیاز است وابستگی‌های مرتبط با آن، از طریق NuGet به کمک فرامین پاور شل ذیل دریافت شوند:
 PM> Install-Package Microsoft.AspNet.SignalR.Owin
PM> Install-Package Microsoft.Owin.Hosting -Pre
PM> Install-Package Microsoft.Owin.Host.HttpListener -Pre
مواردی که با پارامتر pre مشخص شده‌اند، در زمان نگارش این مطلب هنوز در مرحله بتا قرار دارند اما برای دموی برنامه کفایت می‌کنند.
مراحل تهیه یک برنامه ثالث (برای مثال خارج از IIS یا یک وب سرور آزمایشی) به عنوان میزبان Hubs مورد نیاز به این شرح هستند:
الف) کلاس آغازین میزبان باید با پیاده سازی اینترفیسی به نام IAppBuilder تهیه شود.
ب) مسیریابی‌های مورد نیاز تعریف گردند.
ج) وب سرور HTTP یا HTTPS توکار برای سرویس دهی آغاز گردد.

باید توجه داشت که در این حالت برخلاف روش ASP.NET Hosting، سایر اسمبلی‌های برنامه جهت یافتن Hubهای تعریف شده، اسکن نمی‌شوند. همچنین هنگام کار با jQuery مباحث عنوان شده در مورد تنظیم دسترسی‌های Cross domain نیز باید در اینجا اعمال گردند. به علاوه اجرای وب سرور توکار آن به دلایل امنیتی، نیاز به دسترسی مدیریتی دارد.

برای پیاده سازی یک نمونه، به برنامه‌ای که تاکنون تهیه کرده‌ایم، یک پروژه کنسول دیگر را به نام ConsoleHost اضافه کنید. البته باید درنظر داشت در دنیای واقعی این نوع برنامه‌ها را عموما از نوع سرویس‌های ویندوز NT تهیه می‌کنند.
در ادامه سه فرمان پاور شل یاد شده را برای افزودن وابستگی‌های مورد نیاز فراخوانی نمائید. همچنین باید دقت داشت که این دستور بر روی پروژه جدید اضافه شده باید اجرا گردد.
using System;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.Owin.Hosting;
using Owin;

namespace SignalR02.ConsoleHost
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapHubs(new HubConfiguration { EnableCrossDomain = true });
        }
    }

    [HubName("chat")]
    public class ChatHub : Hub
    {
        public void SendMessage(string message)
        {
            var msg = string.Format("{0}:{1}", Context.ConnectionId, message);
            Clients.All.hello(msg);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (WebApplication.Start<Startup>("http://localhost:1073/"))
            {
                Console.WriteLine("Press a key to terminate the server...");
                Console.Read();
            }
        }
    }
}
سپس یک کلاس Startup را با امضایی که مشاهده می‌کنید تهیه نمائید. در اینجا مسیریابی و تنظیمات دسترسی از سایر دومین‌ها مشخص شده‌اند. در ادامه یک Hub نمونه، تعریف و نهایتا توسط WebApplication.Start، این وب سرور راه اندازی می‌شود. اکنون اگر برنامه را اجرا کرده و به مسیر http://localhost:1073/signalr/hubs مراجعه کنید، فایل پروکسی تعاریف متادیتای مرتبط با سرور قابل مشاهده خواهد بود.
سمت کلاینت استفاده از آن هیچ تفاوتی نمی‌کند و با جزئیات آن پیشتر آشنا شده‌اید؛ برای مثال در کلاینت جی‌کوئری خاصیت connection.hub.url باید به مسیر جدید سرور هاب تنظیم گردد تا اتصالات به درستی برقرار شوند.


دریافت پروژه کامل مرتبط با این 4 قسمت (البته بدون فایل‌های باینری آن، جهت کاهش حجم 32 مگابایتی)
  SignalRSamples.zip
مطالب
بررسی تغییرات Blazor 8x - قسمت چهاردهم - امکان استفاده از کامپوننت‌های Blazor در برنامه‌های ASP.NET Core 8x
ASP.NET Core 8x به همراه یک IResult جدید به‌نام RazorComponentResult است که توسط آن می‌توان در Endpoint‌های Minimal-API و همچنین اکشن متدهای MVC، از کامپوننت‌های Blazor، خروجی گرفت. این خروجی نه فقط static یا به عبارتی SSR، بلکه حتی می‌تواند تعاملی هم باشد. در این مطلب، جزئیات فعالسازی و استفاده از این IResult جدید را در یک برنامه‌ی Minimal-API بررسی می‌کنیم.


ایجاد یک برنامه‌ی Minimal-API جدید در دات نت 8

پروژه‌ای را که در اینجا پیگیری می‌کنیم، بر اساس قالب استاندارد تولید شده‌ی توسط دستور dotnet new webapi تکمیل می‌شود.


ایجاد یک صفحه‌ی Blazor 8x به همراه مسیریابی و دریافت پارامتر

در ادامه قصد داریم که یک کامپوننت جدید را به نام SsrTest.razor در پوشه‌ی جدید Components\Tests ایجاد کرده و برای آن مسیریابی از نوع page@ هم تعریف کنیم. یعنی نه‌فقط قصد داریم آن‌را توسط RazorComponentResult رندر کنیم، بلکه می‌خواهیم اگر آدرس آن‌را در مرورگر هم وارد کردیم، قابل دسترسی باشد.
به همین جهت یک پوشه‌ی جدید را به نام Components در ریشه‌ی پروژه‌ی Web API جاری ایجاد می‌کنیم، با این محتوا:
برای ایده گرفتن از محتوای مورد نیاز، به «معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8» قسمت دوم این سری مراجعه کرده و برای مثال قالب ساده‌ترین حالت ممکن را توسط دستور زیر تولید می‌کنیم (در یک پروژه‌ی مجزا، خارج از پروژه‌ی جاری):
dotnet new blazor --interactivity None
پس از اینکار، محتویات پوشه‌ی Components آن‌را مستقیما داخل پوشه‌ی پروژه‌ی Minimal-API جاری کپی می‌کنیم. یعنی در نهایت در این پروژه‌ی جدید Web API، به فایل‌های زیر می‌رسیم:
- فایل Imports.razor_ ساده شده برای سهولت کار با فضاهای نام در کامپوننت‌های Blazor (فضاهای نامی را که در آن وجود ندارند و مرتبط با پروژه‌ی دوم هستند، حذف می‌کنیم).
- فایل App.razor، برای تشکیل نقطه‌ی آغازین برنامه‌ی Blazor.
- فایل Routes.razor برای معرفی مسیریابی صفحات Blazor تعریف شده.
- پوشه‌ی Layout برای معرفی فایل MainLayout.razor که در Routes.razor استفاده شده‌است.

و ... یک فایل آزمایشی جدید به نام Components\Tests\SsrTest.razor با محتوای زیر:
@page "/ssr-page/{Data:int}"

<PageTitle>An SSR component</PageTitle>

<h1>An SSR component rendered by a Minimal-API!</h1>

<div>
    Data: @Data
</div>

@code {

    [Parameter]
    public int Data { get; set; }

}
این فایل، می‌تواند پارامتر Data را از طریق فراخوانی مستقیم آدرس فرضی http://localhost:5227/ssr-page/2 دریافت کند و یا ... از طریق خروجی جدید RazorComponentResult که توسط یک Endpoint سفارشی ارائه می‌شود:




تغییرات مورد نیاز در فایل Program.cs برنامه‌ی Web-API برای فعالسازی رندر سمت سرور Blazor

در ادامه کل تغییرات مورد نیاز جهت اجرای این برنامه را مشاهده می‌کنید:
var builder = WebApplication.CreateBuilder(args);

// ...

builder.Services.AddRazorComponents();

// ...

// http://localhost:5227/ssr-component?data=2
// or it can be called directly http://localhost:5227/ssr-page/2
app.MapGet("/ssr-component",
           (int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               return new RazorComponentResult<SsrTest>(parameters);
           });

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>();
app.Run();

// ...
توضیحات:
- همین اندازه تغییر در جهت فعالسازی رندر سمت سرور کامپوننت‌های Blazor در یک برنامه‌ی ASP.NET Core کفایت می‌کند. یعنی اضافه شدن:
AddRazorComponents ،UseAntiforgery و MapRazorComponents
- در اینجا نحوه‌ی ارسال پارامترها را به یک RazorComponentResult نیز مشاهده می‌کنید.
- در حالت فراخوانی از طریق مسیر endpoint (یعنی فراخوانی مسیر http://localhost:5227/ssr-component در مثال فوق)، خود کامپوننت فراخوانی شده، بدون layout تعریف شده‌ی در فایل App.razor، رندر می‌شود. علت اینجا است که layout برنامه به همراه کامپوننت Router و RouteView آن فعال می‌شود که این دو هم مختص به صفحات دارای مسیریابی Blazor هستند و برای رندر کامپوننت‌های خالص آن بکار گرفته نمی‌شوند. خروجی RazorComponentResult تنها یک static SSR خالص است؛ مگر اینکه فایل blazor.web.js را نیز بارگذاری کند.

یک نکته: اگر در حالت رندر توسط RazorComponentResult، علاقمند به استفاده‌ی از layout هستید، می‌توان از کامپوننت LayoutView داخل یک کامپوننت فرضی به صورت زیر استفاده کرد؛ اما این مورد هم شامل اطلاعات فایل App.razor نمی‌شود:
<LayoutView Layout="@typeof(MainLayout)">
    <PageTitle>Home</PageTitle>

    <h2>Welcome to your new app.</h2>
</LayoutView>


سؤال: آیا در این حالت کامپوننت‌های تعاملی هم کار می‌کنند؟

پاسخ: بله. فقط برای ایده گرفتن، یک نمونه پروژه‌ی تعاملی Blazor 8x را در ابتدا ایجاد کنید و قسمت‌های اضافی AddRazorComponents و MapRazorComponents آن‌را در اینجا کپی کنید؛ یعنی برای مثال جهت فعالسازی کامپوننت‌های تعاملی Blazor Server، به این دو تغییر زیر نیاز است:
// ...

builder.Services.AddRazorComponents()
       .AddInteractiveServerComponents();

// ...

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

// ...
همچنین باید دقت داشت که امکانات تعاملی، به دلیل وجود و دسترسی به یک سطر ذیل که در فایل Components\App.razor واقع شده، اجرایی می‌شوند:
<script src="_framework/blazor.web.js"></script>
و همانطور که عنوان شد، اگر از روش new RazorComponentResult استفاده می‌شود، باید این سطر را به صورت دستی اضافه‌کرد؛ چون به همراه رندر layout تعریف شده‌ی در فایل App.razor نیست. برای مثال فرض کنید کامپوننت معروف Counter را به صورت زیر داریم که حالت رندر آن به InteractiveServer تنظیم شده‌است:
@rendermode InteractiveServer

<h1>Counter</h1>

<p role="status">Current count: @_currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int _currentCount;

    private void IncrementCount()
    {
        _currentCount++;
    }

}
در این حالت پس از تعریف endpoint زیر، خروجی آن فقط یک صفحه‌ی استاتیک SSR خواهد بود و دکمه‌ی Click me آن کار نمی‌کند:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<Counter>());
علت اینجا است که اگر به سورس HTML رندر شده مراجعه کنیم، خبری از درج اسکریپت blazor.web.js در انتهای آن نیست. به همین جهت برای مثال فایل جدید CounterInteractive.razor را به صورت زیر اضافه می‌‌کنیم که ساختار آن شبیه به فایل App.razor است:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive server component</title>
    <base href="/"/>
</head>
<body>
   <h1>Interactive server component</h1>

   <Counter/>

  <script src="_framework/blazor.web.js"></script>
</body>
</html>
و هدف اصلی از آن، تشکیل یک قالب و درج اسکریپت blazor.web.js در انتهای آن است.
سپس تعریف endpoint متناظر را به صورت زیر تغییر می‌دهیم:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<CounterInteractive>());
اینبار به علت بارگذاری فایل blazor.web.js، امکانات تعاملی کامپوننت Counter فعال شده و قابل استفاده می‌شوند.


سؤال: آیا می‌توان این خروجی static SSR کامپوننت‌های بلیزر را در سرویس‌های یک برنامه ASP.NET Core هم دریافت کرد؟

منظور این است که آیا می‌توان از یک کامپوننت Blazor، به همراه تمام پیشرفت‌های Razor در آن که در Viewهای MVC قابل دسترسی نیستند، به‌شکل یک رشته‌ی خالص، خروجی گرفت و برای مثال از آن به‌عنوان قالب پویای محتوای ایمیل‌ها استفاده کرد؟
پاسخ: بله! زیر ساخت RazorComponentResult که از سرویس HtmlRenderer استفاده می‌کند، بدون نیاز به برپایی یک endpoint هم قابل دسترسی است:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace WebApi8x.Services;

public class BlazorStaticRendererService
{
    private readonly HtmlRenderer _htmlRenderer;

    public BlazorStaticRendererService(HtmlRenderer htmlRenderer) => _htmlRenderer = htmlRenderer;

    public Task<string> StaticRenderComponentAsync<T>() where T : IComponent
        => RenderComponentAsync<T>(ParameterView.Empty);

    public Task<string> StaticRenderComponentAsync<T>(Dictionary<string, object?> dictionary) where T : IComponent
        => RenderComponentAsync<T>(ParameterView.FromDictionary(dictionary));

    private Task<string> RenderComponentAsync<T>(ParameterView parameters) where T : IComponent =>
        _htmlRenderer.Dispatcher.InvokeAsync(async () =>
                                             {
                                                 var output = await _htmlRenderer.RenderComponentAsync<T>(parameters);
                                                 return output.ToHtmlString();
                                             });
}
برای کار با آن، ابتدا باید سرویس فوق را به صورت زیر ثبت و معرفی کرد:
builder.Services.AddScoped<HtmlRenderer>();
builder.Services.AddScoped<BlazorStaticRendererService>();
و سپس یک نمونه مثال فرضی نحوه‌ی تزریق و فراخوانی سرویس BlazorStaticRendererService به صورت زیر است که در آن روش ارسال پارامترها هم بررسی شده‌است:
app.MapGet("/static-renderer-service-test",
           async (BlazorStaticRendererService renderer, int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               var html = await renderer.StaticRenderComponentAsync<SsrTest>(parameters);
               return Results.Content(html, "text/html");
           });

کدهای کامل این مطلب را می‌توانید از اینجا دریافت کنید: WebApi8x.zip
اشتراک‌ها
معرفی قابلیت Hot Restart زامارین

توسط این قابلیت در برنامه‌های زاماین، تغییرات کد، رفرنس ها، فایل‌ها و... نیاز به کامپایل کامل و مجدد نداشته و تغییرات را سریع‌تر اعمال کرده و برنامه را ریستارت می‌کند.

Today at .NET Conf 2019, we announced Xamarin Hot Restart which enables you to test changes made to your app, including multi-file code edits, resources, and references, using a much faster build and deploy cycle.

XAML Hot Reload for Xamarin.Forms already provides fast iteration on XAML UIs by enabling you to see changes applied live in your running application. But what about other types of code and project edits? Xamarin Hot Restart will apply these types of changes to your application and quickly restart your app for rapid application development.  

معرفی قابلیت Hot Restart زامارین
اشتراک‌ها
کتابخانه Hangfire

An easy way to perform background job processing in your .NET and .NET Core applications. No Windows Service or separate process required. CPU and I/O intensive, long-running and short-running jobs are supported. Backed by Redis, SQL Server, SQL Azure and MSMQ. 

PM> Install-Package Hangfire

After installation, update your existing OWIN Startup file with the following lines of code. If you do not have this class in your project or don't know what is it, please read the Quick start guide to learn about how to install Hangfire.

public void Configuration(IAppBuilder app)
{
    GlobalConfiguration.Configuration.UseSqlServerStorage("<connection string or its name>");
    
    app.UseHangfireServer();
    app.UseHangfireDashboard();
}  
کتابخانه Hangfire