Want to deepen your understanding of promises, async
and await
? The course will guide you there through 47 live examples and exercises. To get started, just click through to the first lesson: Why async, anyway?.
در این قسمت به پیاده سازی و توضیح مدلهای انجمن خواهیم پرداخت. قبل از شروع پیشنهاد میکنم مقالات قبلی را مطالعه کنید.
همکاران این قسمت:
سلمان معروفی
سید مجتبی حسینی
پیشنیاز این قسمت:
مقالات 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
- IsActive : مشخص کنندهی این است که در این انجمن امکان ارسال تاپیک و پست وجود دارد و در صورت false بودن این خصوصیت، بر تمام زیر انجمنها هم اعمال خواهد شد و برای زمانی مفید است که میخواهیم برای مدتی به هر دلیل خاصی امکان ارسال تاپیک و پست را برای انجمن خاصی، ندهیم.
- IsColsed : خصوصت اولی که مطرح شد اگر مقدار false بگیرد، همچنان کاربران میتوانند تایپکها و پستهای قبلی را مشاهده و مطالعه کنند. ولی با مقدار دهی این خصوصیت با مقدار false، امکان کلیهی فعالیتها و مشاهدهای را از محتوای این انجمن و زیر انجمنهای آن نخواهیم داشت.
- IsPrivate : برای مواقعی که لازم است برای انجمن خاصی کلمهی عبور در نظر بگیریم تا افراد خاص که کلمهی عبور آن را دارند بتوانند در آن انجمن فعالیت کنند، در نظر گرفته شده است.
- ApprovedPostsCount , UnApprovedPostsCount,ApprovedTopicsCount,UnApprovedTopicsCount : برای بالا بردن کارآیی سیستم به مانند مدلهای قبل در نظر گرفته شدهاند.
- LastTopicId, LastTopicTitle , LastTopicCreator , LastTopicCreatorId , LastTopicCreatedOn: همچنین برای افزایش کارآیی سیستم و نمایش به عنوان قسمتی از مشخصات قابل مشاهده از هر انجمن، در نظر گرفته شدهاند.
- Depth : برای نشان دادن عمق گره در درخت استفاده میشود که هنگام درج انجمن، این مورد از نتیجهی جمع عمق پدر انتخاب شده و یک، به دست خواهد آمد. این مورد هنگام واکشی برای مثال 4 سطح اول برای نمایش آنها در صفحهی اول انجمن به صورت سلسله مراتبی خیلی مفید خواهد بود.
- Path : برای استفاده از SQL Antipattern شمارش مسیر در نظر گرفته شده است. این مورد جزء Best Practiceها میتواند باشد. چون هم با استفاده از ساختار خود ارجاع، درخت خود را داریم و با این Antipattern کوئریهای مربوط به درخت خیلی راحت خواهد بود.
- IsModeratorsInherited : اگر لازم است مدیران انجمن، پدر را به عنوان مدیر خود قبول کنند، این خصوصیت مقدار true خواهد گرفت.
- Subscribers : هر انجمنی میتواند یکسری مشترک نیز داشته باشد (به منظور اطلاع رسانی با درج یک تاپیک جدید در خود انجمن یا زیر انجمنهای آن) .
- Posts : به منظور افزایش کارایی هنگام محاسبه تعداد پستهای ارسالی در یک انجمن ، در نظر گرفته شده است.
- TopicTrackers : در مقاله بعد توضیح داده خواهد شد.
- LastPostCreatedOn : به منظور استفاده از آن برای سیستم Tracking انجمنها استفاده خواهد شد .
/// <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, ... }
- ApplyChildren : برای اعمال دسترسیهای مدیریتی کاربر x به زیر انجمنهای انجمن y البته اگر خصوصیت IsModeratorsInherited زیر انجمنهای مورد نظر با مقدار true مقدار دهی شده باشد.
- Permissions : از نوع ForumModeratorPermissions و نگهدارنده دسترسیهای کاربر x به عنوان مدیر انجمن y، میباشد.
- نکته : برای این مدل آی دی در نظر گرفته نشده است و از کلید مرکب متشکل از 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 }
- ApplyChildren : برای مشخص کردین نمایش این اعلان در زیر انجمنهای انجمن مورد نظر
- ExpireOn : به این دلیل نال پذیر در نظر گرفته شده است که اگر لازم بود، در زمان مشخصی به پایان نرسد و با null مقدار دهی شود.
/// <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 }
/// <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 }
- LastPostId , LastPosterId, LastPoster , LastPostTitle ,LastPostCreatedOn: برای افزایش کارآیی سیستم در نظر گرفته شدهاند.
- ModeratePosts : اگر لازم است پستهای یک تاپیک خاص، قبل از نمایش مدیریت شوند، با true مقدار دهی خواهد شد.
- Tags : لیستی از برچسبها که برای اعمال رابطهی چند به چند با مدل برچسبهای معرفی شدهی در مقاله اول، در نظر گرفته شده است.
- Posts : در هر تاپیکی یک سری پست به عنوان جوابهای آن ارسال خواهد شد.
- Subscribers : به مانند انجمنها، تاپیکهای ما هم میتوانند یک سری مشترک داشته باشند، تا از تغییرات این تاپیک مطلع شوند .
- Trackers : مربوط به سیستم Tracking تاپیک میباشد. و در مقاله بعد توضیح داده خواهد شد.
حتما لازم خواهد بود تاریخچهی تغییرات برای پستهای ارسالی ذخیره شوند؛ در مقالهی بعدی به این موضوع هم خواهیم پرداخت.
هم چنین در قسمت 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 رکورد قابلیت پاسخ دادن خواهد داشت.
پس بدین صورت میباشد که شما حداکثر امکانات ممکن را به سمت کلاینت میدهید و اختیار بدان واگذار شده که آیا از این امکانات حداکثری، استفاده نماید یا خیر.
امکانات این پروتکل منحصر به فرد است و در مقالههای بعدی به جزئیات بیشتر و دقیقتری خواهیم پرداخت.
تبدیلگر زبان Go به #C
Converts source code developed using the Go programming language (see Go Language Specification) to the C# programming language (see C# Language Specification).
- Improving the development experience when using popular JavaScript libraries
- Adding support for new JavaScript ECMAScript 2015 (also known as ES2015 and formerly ES6) language and web browser APIs
- Increasing your productivity in complex JavaScript code bases
همیشه در حین توسعهی یک برنامه این سؤالات وجود دارند:
- چند درصد از برنامه تست شده است؟
- برای چه تعدادی از متدهای موجود آزمون واحد نوشتهایم؟
- آیا همین آزمونهای واحد نوشته شده و موجود، کامل هستند و تمام عملکردهای متدهای مرتبط را پوشش میدهند؟
این سؤالات به صورت خلاصه مفهوم 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;
}
}
}
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);
}
}
}
به نظر همه چیز خوب است! اما آیا واقعا این آزمون کافی است؟!
ب) در ادامه به کمک برنامهی 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
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
مراحل تهیه یک برنامه ثالث (برای مثال خارج از 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(); } } } }
سمت کلاینت استفاده از آن هیچ تفاوتی نمیکند و با جزئیات آن پیشتر آشنا شدهاید؛ برای مثال در کلاینت جیکوئری خاصیت connection.hub.url باید به مسیر جدید سرور هاب تنظیم گردد تا اتصالات به درستی برقرار شوند.
دریافت پروژه کامل مرتبط با این 4 قسمت (البته بدون فایلهای باینری آن، جهت کاهش حجم 32 مگابایتی)
SignalRSamples.zip
ایجاد یک برنامهی Minimal-API جدید در دات نت 8
پروژهای را که در اینجا پیگیری میکنیم، بر اساس قالب استاندارد تولید شدهی توسط دستور dotnet new webapi تکمیل میشود.
ایجاد یک صفحهی Blazor 8x به همراه مسیریابی و دریافت پارامتر
در ادامه قصد داریم که یک کامپوننت جدید را به نام SsrTest.razor در پوشهی جدید Components\Tests ایجاد کرده و برای آن مسیریابی از نوع page@ هم تعریف کنیم. یعنی نهفقط قصد داریم آنرا توسط RazorComponentResult رندر کنیم، بلکه میخواهیم اگر آدرس آنرا در مرورگر هم وارد کردیم، قابل دسترسی باشد.
به همین جهت یک پوشهی جدید را به نام Components در ریشهی پروژهی Web API جاری ایجاد میکنیم، با این محتوا:
برای ایده گرفتن از محتوای مورد نیاز، به «معرفی قالبهای جدید شروع پروژههای Blazor در دات نت 8» قسمت دوم این سری مراجعه کرده و برای مثال قالب سادهترین حالت ممکن را توسط دستور زیر تولید میکنیم (در یک پروژهی مجزا، خارج از پروژهی جاری):
dotnet new blazor --interactivity None
- فایل 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; } }
تغییرات مورد نیاز در فایل 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(); // ...
<script src="_framework/blazor.web.js"></script>
@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++; } }
// http://localhost:5227/server-interactive-component app.MapGet("/server-interactive-component", () => new RazorComponentResult<Counter>());
<!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>
سپس تعریف endpoint متناظر را به صورت زیر تغییر میدهیم:
// http://localhost:5227/server-interactive-component app.MapGet("/server-interactive-component", () => new RazorComponentResult<CounterInteractive>());
سؤال: آیا میتوان این خروجی 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>();
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.
کتابخانه 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();
}