نظرات مطالب
ObservableCollection در Entity Framework
سلام آقای جعفری. ممنون از مطلب مفیدتون. شاید سوالم خیلی ربطی به مطلب شما نداشته باشه ولی میخواستم ازتون بپرسم که ما وقتی توی پروژه WPF از Entity Framework Code First استفاده میکنیم باید اینترفیس INotifyPropertyChanged رو برای هر Entity پیاده سازی کنیم یا نه؟! 
نظرات مطالب
امکان ساخت قالب برای پروژه‌های NET Core.
با توجه به آپدیت شدن پروژه DNTIdentity-Master با آپدیت جدید dotnetcore3، برای تهیه یک template جدید از پروژه فوق با توجه به دستور فوق ابتدا ویرایش لازم در فایل template.json صورت گرفت و سپس فایل register_template.bat اجرا گردید.سپس در پوشه‌ای جدید دستور dotnet new... اجرا گردید. ولی پوشه‌های هم نام با template پایه ایجاد می‌گردد؟
مطالب
به روز رسانی تمام فیلدهای رشته‌ای تمام جداول بانک اطلاعاتی توسط Entity framework 6.x
یکی از مراحلی که پس از ارتقاء یک سایت به HTTPS باید صورت گیرد، به روز رسانی آدرس‌های قدیمی درج شده‌ی در صفحات مختلف، از HTTP به HTTPS است؛ وگرنه با خطای «قسمتی از صفحه امن نیست» توسط مرورگر مواجه خواهیم شد:


روش‌های زیادی برای مدیریت این مساله وجود دارند؛ مانند استفاده از ماژول‌های URL Rewrite برای بازنویسی آدرس‌های نهایی صفحه‌ی در حال رندر و یا ... به روز رسانی مستقیم بانک اطلاعاتی، یافتن تمام فیلدهای رشته‌ای ممکن در تمام جداول موجود و سپس اعمال تغییرات.


یافتن لیست تمام جداول قابل مدیریت توسط Entity framework

در ابتدا می‌خواهیم لیست پویای تمام جداول مدیریت شده‌ی توسط EF را پیدا کنیم. از این جهت که نمی‌خواهیم به ازای هر کدام یک کوئری جداگانه بنویسیم.
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EFReplaceAll.Models;

namespace EFReplaceAll.Config
{
    public class DbSetInfo
    {
        public IQueryable<object> DbSet { set; get; }
        public Type DbSetType { set; get; }
    }

    public class MyContext : DbContext
    {
        public DbSet<Product> Products { set; get; }
        public DbSet<Category> Categories { set; get; }
        public DbSet<User> Users { set; get; }

        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }

        public IList<DbSetInfo> GetAllDbSets()
        {
            return this.GetType()
                .GetProperties()
                .Where(p => p.PropertyType.IsGenericType &&
                            p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>))
                .Select(p => new DbSetInfo
                {
                    DbSet = (IQueryable<object>)p.GetValue(this, null),
                    DbSetType = p.PropertyType.GetGenericArguments().First()
                })
                .ToList();
        }
    }
}
در اینجا متد GetAllDbSets، به صورت پویا لیست DbSetها را به همراه نوع جنریک آن‌ها، بازگشت می‌دهد. با استفاده از این لیست می‌توان رکوردهای تمام جداول را واکشی و سپس تغییر داد.


یافتن فیلدهای رشته‌ای رکوردهای تمام جداول و سپس به روز رسانی آن‌ها

می‌خواهیم متدی را طراحی کنیم که در آن لیستی از یافتن‌ها و جایگزینی‌ها قابل تعیین باشد. به همین جهت مدل زیر را تعریف می‌کنیم:
using System;

namespace EFReplaceAll.Utils
{
    public class ReplaceOp
    {
        public string ToFind { set; get; }
        public string ToReplace { set; get; }
        public StringComparison Comparison { set; get; }
    }
}
در اینجا خاصیت Comparison امکان جستجو و جایگزینی غیرحساس به حروف کوچک و بزرگ را نیز میسر می‌کند.

سپس متدی که کار یافتن تمام فیلدهای رشته‌ای و سپس جایگزین کردن آن‌ها را انجام می‌دهد به صورت زیر خواهد بود:
using System.Collections.Generic;
using System.Linq;
using EFReplaceAll.Config;

namespace EFReplaceAll.Utils
{
    public static class UpdateDbContextContents
    {
        public static void ReplaceAllStringsAcrossTables(IList<ReplaceOp> replaceOps)
        {
            int dbSetsCount;
            using (var uow = new MyContext())
            {
                dbSetsCount = uow.GetAllDbSets().Count;
            }

            for (var i = 0; i < dbSetsCount; i++)
            {
                using (var uow = new MyContext()) // using a new context each time to free resources quickly.
                {
                    var dbSetResult = uow.GetAllDbSets()[i];
                    var stringProperties = dbSetResult.DbSetType.GetProperties()
                        .Where(p => p.PropertyType == typeof(string))
                        .ToList();
                    var dbSetEntities = dbSetResult.DbSet;
                    var haveChanges = false;
                    foreach (var entity in dbSetEntities)
                    {
                        foreach (var stringProperty in stringProperties)
                        {
                            var oldPropertyValue = stringProperty.GetValue(entity, null) as string;
                            if (string.IsNullOrWhiteSpace(oldPropertyValue))
                            {
                                continue;
                            }

                            var newPropertyValue = oldPropertyValue;
                            foreach (var replaceOp in replaceOps)
                            {
                                newPropertyValue = newPropertyValue.ReplaceString(replaceOp.ToFind, replaceOp.ToReplace, replaceOp.Comparison);
                            }
                            if (oldPropertyValue != newPropertyValue)
                            {
                                stringProperty.SetValue(entity, newPropertyValue, null);
                                haveChanges = true;
                            }
                        }
                    }

                    if (haveChanges)
                    {
                        uow.SaveChanges();
                    }
                }
            }
        }

    }
}
توضیحات:
- در اینجا using (var uow = new MyContext()) را زیاد مشاهده می‌کنید. علت اینجا است که اگر تنها با یک Context کار کنیم، EF تمام تغییرات و تمام رکوردهای وارد شده‌ی به آن‌را کش می‌کند و مصرف حافظه‌ی برنامه با توجه به خواندن تمام رکوردهای بانک اطلاعاتی توسط آن، ممکن است به چند گیگابایت برسد. به همین جهت از Contextهایی با طول عمر کوتاه استفاده شده‌است تا میزان مصرف RAM این متد سبب کرش برنامه نشود.
- در ابتدای کار توسط متد GetAllDbSets که به Context اضافه کردیم، تعداد DbSetهای موجود را پیدا می‌کنیم تا بتوان بر روی آن‌ها حلقه‌ای را تشکیل داد و به ازای هر کدام یک (()using (var uow = new MyContext را تشکیل داد.
- سپس با استفاده از نوع DbSet که توسط خاصیت dbSetResult.DbSetType در دسترس است، خواص رشته‌ای ممکن این DbSet یافت می‌شوند.
- در ادامه dbSetResult.DbSet یک Data Reader را به صورت پویا بر روی DbSet جاری باز کرده و تمام رکوردهای این DbSet را یک به یک بازگشت می‌دهد.
- در اینجا با استفاده از Reflection، از رکورد جاری، مقادیر خواص رشته‌ای آن دریافت شده و سپس کار جستجو و جایگزینی انجام می‌شود.
- در آخر هم فراخوانی uow.SaveChanges کار ثبت تغییرات صورت گرفته را انجام می‌دهد.


متدی برای جایگزینی غیرحساس به حروف بزرگ و کوچک

متد استاندارد Replace رشته‌ها، حساس به حروف بزرگ و کوچک است. یک نمونه‌ی عمومی‌تر را که در آن بتوان StringComparison.OrdinalIgnoreCase را تعیین کرد، در ذیل مشاهده می‌کنید که از آن در متد ReplaceAllStringsAcrossTables فوق استفاده شده‌است:
using System;
using System.Text;

namespace EFReplaceAll.Utils
{
    public static class StringExtensions
    {
        public static string ReplaceString(this string src, string oldValue, string newValue, StringComparison comparison)
        {
            if (string.IsNullOrWhiteSpace(src))
            {
                return src;
            }

            if (string.Compare(oldValue, newValue, comparison) == 0)
            {
                return src;
            }

            var sb = new StringBuilder();

            var previousIndex = 0;
            var index = src.IndexOf(oldValue, comparison);

            while (index != -1)
            {
                sb.Append(src.Substring(previousIndex, index - previousIndex));
                sb.Append(newValue);
                index += oldValue.Length;

                previousIndex = index;
                index = src.IndexOf(oldValue, index, comparison);
            }

            sb.Append(src.Substring(previousIndex));

            return sb.ToString();
        }
    }
}
و در آخر یک مثال از استفاده‌ی این متد تهیه شده، جهت به روز رسانی لینک‌های HTTP به HTTPS در تمام جداول برنامه به صورت زیر است:
            UpdateDbContextContents.ReplaceAllStringsAcrossTables(
                new[]
                {
                    new ReplaceOp
                    {
                        ToFind = "https://www.dntips.ir",
                        ToReplace = "https://www.dntips.ir",
                        Comparison = StringComparison.OrdinalIgnoreCase
                    },
                    new ReplaceOp
                    {
                        ToFind = "https://www.dntips.ir",
                        ToReplace = "https://www.dntips.ir",
                        Comparison = StringComparison.OrdinalIgnoreCase
                    }
                });

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFReplaceAll.zip
نظرات مطالب
ساخت یک اپلیکیشن ساده ToDo با ASP.NET Identity
سلام
اگر پروژه ای داشته باشیم با ساختار زیر
MyProject
MyProject.Service
MyProject.Model
MyProject.Data (Entity framework code first)
اگر بخوایم تو لایه MyProject از asp .net identity  استفاده کنیم، به چه نحوی باید این کار رو انجام داد که ساختار دیتابیس که در لایه دیتا دچار مشکل نشه و جداول مورد نیاز asp .net identity به دیتا بیس اضافه بشه؟
نظرات مطالب
ایجاد ایندکس منحصربفرد بر روی چند فیلد با هم در EF Code first
با سلام.
میخواستم بدونم برای ایندکس منحصر به فرد هیچ validation توکاری درEF وجود داره؟ که برای مثال وقتی دو تا رکورد تکراری رو به dbset اضافه میکنیم قبل از savechange عمل validation رو انجام بده، یا اینکه باید به صورت دستی validate کنیم.و اگه آره بهترین روش از نظر بهینه بودن برای این کار چی میتونه باشه؟ValidateEntity یا Ivalidation object؟ یا...؟
با تشکر از وقتی که میگذارید.
مطالب
Blazor 5x - قسمت 14 - کار با فرم‌ها - بخش 2 - تعریف فرم‌ها و اعتبارسنجی آن‌ها
در ادامه قصد داریم از سرویس زیر که در قسمت قبل تکمیل شد، در یک برنامه‌ی Blazor Server استفاده کنیم:
namespace BlazorServer.Services
{
    public interface IHotelRoomService
    {
        Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO);

        Task<int> DeleteHotelRoomAsync(int roomId);

        IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync();

        Task<HotelRoomDTO> GetHotelRoomAsync(int roomId);

        Task<HotelRoomDTO> IsRoomUniqueAsync(string name);

        Task<HotelRoomDTO> UpdateHotelRoomAsync(int roomId, HotelRoomDTO hotelRoomDTO);
    }
}


تعریف کامپوننت‌های ابتدایی نمایش لیست اتاق‌ها و ثبت و ویرایش آن‌ها


در ابتدا کامپوننت‌های خالی نمایش لیست اتاق‌ها و همچنین فرم خالی ثبت و ویرایش آن‌ها را به همراه مسیریابی‌های مرتبط، ایجاد می‌کنیم. به همین جهت ابتدا داخل پوشه‌ی Pages، پوشه‌ی جدید HotelRoom را ایجاد کرده و فایل جدید HotelRoomList.razor را با محتوای ابتدایی زیر، به آن اضافه می‌کنیم.
@page "/hotel-room"

<div class="row mt-4">
    <div class="col-8">
        <h4 class="card-title text-info">Hotel Rooms</h4>
    </div>
    <div class="col-3 offset-1">
        <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink>
    </div>
</div>

@code {

}
این کامپوننت در مسیر hotel-room/ قابل دسترسی خواهد بود. بر این اساس، به کامپوننت Shared\NavMenu.razor مراجعه کرده و مدخل منوی آن‌را تعریف می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="hotel-room">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Hotel Rooms
    </NavLink>
</li>

تا اینجا صفحه‌ی ابتدایی نمایش لیست اتاق‌ها، به همراه یک دکمه‌ی افزودن اتاق جدید نیز هست. به همین جهت فایل جدید Pages\HotelRoom\HotelRoomUpsert.razor را به همراه مسیریابی hotel-room/create/ برای تعریف کامپوننت ابتدایی ثبت و ویرایش اطلاعات اتاق‌ها، اضافه می‌کنیم:
@page "/hotel-room/create"

<h3>HotelRoomUpsert</h3>

@code {

}
- واژه‌ی Upsert در مورد فرمی بکاربرده می‌شود که هم برای ثبت اطلاعات و هم برای ویرایش اطلاعات از آن استفاده می‌شود.
- NavLink تعریف شده‌ی در کامپوننت نمایش لیست اتاق‌ها، به مسیریابی کامپوننت فوق اشاره می‌کند.


ایجاد فرم ثبت یک اتاق جدید

برای ثبت یک اتاق جدید نیاز است به مدل UI آن که همان HotelRoomDTO تعریف شده‌ی در قسمت قبل است، دسترسی داشت. به همین جهت در پروژه‌ی BlazorServer.App، ارجاعی را به پروژه‌ی BlazorServer.Models.csproj اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <ProjectReference Include="..\BlazorServer.Models\BlazorServer.Models.csproj" />
  </ItemGroup>
</Project>
سپس جهت سراسری اعلام کردن فضای نام آن، یک سطر زیر را به انتهای فایل BlazorServer.App\_Imports.razor اضافه می‌کنیم:
@using BlazorServer.Models
اکنون می‌توانیم کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor را به صورت زیر تکمیل کنیم:
@page "/hotel-room/create"

<div class="row mt-2 mb-5">
    <h3 class="card-title text-info mb-3 ml-3">@Title Hotel Room</h3>
    <div class="col-md-12">
        <div class="card">
            <div class="card-body">
                <EditForm Model="HotelRoomModel">
                    <div class="form-group">
                        <label>Name</label>
                        <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText>
                    </div>
                </EditForm>
            </div>
        </div>
    </div>
</div>

@code
{
    private HotelRoomDTO HotelRoomModel = new HotelRoomDTO();
    private string Title = "Create";
}
توضیحات:
- در برنامه‌های Blazor، کامپوننت ویژه‌ی EditForm را بجای تگ استاندارد form، مورد استفاده قرار می‌دهیم.
- این کامپوننت، مدل فرم را از فیلد HotelRoomModel که در قسمت کدها تعریف کردیم، دریافت می‌کند. کار آن تامین اطلاعات فیلدهای فرم است.
- سپس در EditForm تعریف شده، بجای المان استاندارد input، از کامپوننت InputText برای دریافت اطلاعات متنی استفاده می‌شود. با bind-value@ در قسمت چهارم این سری بیشتر آشنا شدیم و کار آن two-way data binding است. در اینجا هر اطلاعاتی که وارد می‌شود، سبب به روز رسانی خودکار مقدار خاصیت HotelRoomModel.Name می‌شود و برعکس.

یک نکته: در قسمت قبل، مدل UI را از نوع رکورد C# 9.0 و init only تعریف کردیم. رکوردها، با EditForm و two-way databinding آن سازگاری ندارند (bind-value@ در اینجا) و بیشتر برای کنترلرهای برنامه‌های Web API که یکبار قرار است کار وهله سازی آن‌ها در زمان دریافت اطلاعات از کاربر صورت گیرد، مناسب هستند و نه با فرم‌های پویای Blazor. به همین جهت به پروژه‌ی BlazorServer.Models مراجعه کرده و نوع آن‌ها را به کلاس و init‌ها را به set معمولی تغییر می‌دهیم تا در فرم‌های Blazor هم قابل استفاده شوند.

تا اینجا کامپوننت ثبت اطلاعات یک اتاق جدید، چنین شکلی را پیدا کرده‌است:



تکمیل سایر فیلدهای فرم ورود اطلاعات اتاق

پس از تعریف فیلد ورود اطلاعات نام اتاق، سایر فیلدهای متناظر با HotelRoomDTO را نیز به صورت زیر به EditForm تعریف شده اضافه می‌کنیم که در اینجا از InputNumber برای دریافت اطلاعات عددی و از InputTextArea، برای دریافت اطلاعات متنی چندسطری استفاده شده‌است:
<EditForm Model="HotelRoomModel">
    <div class="form-group">
        <label>Name</label>
        <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText>
    </div>
    <div class="form-group">
        <label>Occupancy</label>
        <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber>
    </div>
    <div class="form-group">
        <label>Rate</label>
        <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber>
    </div>
    <div class="form-group">
        <label>Sq ft.</label>
        <InputText @bind-Value="HotelRoomModel.SqFt" class="form-control"></InputText>
    </div>
    <div class="form-group">
        <label>Details</label>
        <InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea>
    </div>
    <div class="form-group">
        <button class="btn btn-primary">@Title Room</button>
        <NavLink href="hotel-room" class="btn btn-secondary">Back to Index</NavLink>
    </div>
</EditForm>
با این خروجی:



تعریف اعتبارسنجی‌های فیلدهای یک فرم Blazor

در حین تعریف یک فرم، برای واکنش نشان دادن به دکمه‌ی submit، می‌توان رویداد OnSubmit را به کامپوننت EditForm اضافه کرد که سبب فراخوانی متدی در قسمت کدهای کامپوننت جاری خواهد شد؛ مانند فراخوانی متد HandleHotelRoomUpsert در مثال زیر:
<EditForm Model="HotelRoomModel" OnSubmit="HandleHotelRoomUpsert">
</EditForm>

@code
{
    private HotelRoomDTO HotelRoomModel = new HotelRoomDTO();

    private async Task HandleHotelRoomUpsert()
    {

    }
}
هرچند HotelRoomDTO تعریف شده به همراه تعریف اعتبارسنجی‌هایی مانند Required است، اما اگر بر روی دکمه‌ی submit کلیک کنیم، متد HandleHotelRoomUpsert فراخوانی می‌شود. یعنی روال رویدادگردان OnSubmit، صرفنظر از وضعیت اعتبارسنجی مدل فرم، همواره با submit فرم، اجرا می‌شود.
اگر این مورد، مدنظر نیست، می‌توان بجای OnSubmit، از رویداد OnValidSubmit استفاده کرد. در این حالت اگر اعتبارسنجی مدل فرم با شکست مواجه شود، دیگر متد HandleHotelRoomUpsert فراخوانی نخواهد شد. همچنین در این حالت می‌توان خطاهای اعتبارسنجی را نیز در فرم نمایش داد:
<EditForm Model="HotelRoomModel" OnValidSubmit="HandleHotelRoomUpsert">
    <DataAnnotationsValidator />
    @*<ValidationSummary />*@
    <div class="form-group">
        <label>Name</label>
        <InputText @bind-Value="HotelRoomModel.Name" class="form-control"></InputText>
        <ValidationMessage For="()=>HotelRoomModel.Name"></ValidationMessage>
    </div>
    <div class="form-group">
        <label>Occupancy</label>
        <InputNumber @bind-Value="HotelRoomModel.Occupancy" class="form-control"></InputNumber>
        <ValidationMessage For="()=>HotelRoomModel.Occupancy"></ValidationMessage>
    </div>
    <div class="form-group">
        <label>Rate</label>
        <InputNumber @bind-Value="HotelRoomModel.RegularRate" class="form-control"></InputNumber>
        <ValidationMessage For="()=>HotelRoomModel.RegularRate"></ValidationMessage>
    </div>
- در اینجا قسمت‌های تغییر کرده را مشاهده می‌کنید که به همراه درج DataAnnotationsValidator و ValidationMessage‌ها است.
- کامپوننت DataAnnotationsValidator، اعتبارسنجی مبتنی بر data annotations را مانند [Required]، در دامنه‌ی دید یک EditForm فعال می‌کند.
- اگر خواستیم تمام خطاهای اعتبارسنجی را به صورت خلاصه‌ای در بالای فرم نمایش دهیم، می‌توان از کامپوننت ValidationSummary استفاده کرد.
- و یا اگر خواستیم خطاها را به صورت اختصاصی‌تری ذیل هر تکست‌باکس نمایش دهیم، می‌توان از کامپوننت ValidationMessage کمک گرفت. خاصیت For آن از نوع <Expression<System.Func تعریف شده‌است که اجازه‌ی تعریف strongly typed نام خاصیت در حال اعتبارسنجی را به صورتی که مشاهده می‌کنید، میسر می‌کند.



ثبت اولین اتاق هتل

در ادامه می‌خواهیم روال رویدادگردان HandleHotelRoomUpsert را مدیریت کنیم. به همین جهت نیاز به کار با سرویس IHotelRoomService ابتدای بحث خواهد بود. بنابراین در ابتدا به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضای نام سرویس‌های برنامه را اضافه می‌کنیم:
@using BlazorServer.Services
اکنون امکان تزریق IHotelRoomService را که در قسمت قبل پیاده سازی و به سیستم تزریق وابستگی‌های برنامه معرفی کردیم، پیدا می‌کنیم:
@page "/hotel-room/create"

@inject IHotelRoomService HotelRoomService
@inject NavigationManager NavigationManager


@code
{
    private HotelRoomDTO HotelRoomModel = new HotelRoomDTO();
    private string Title = "Create";

    private async Task HandleHotelRoomUpsert()
    {
        var roomDetailsByName = await HotelRoomService.IsRoomUniqueAsync(HotelRoomModel.Name);
        if (roomDetailsByName != null)
        {
            //there is a duplicate room. show an error msg.
            return;
        }

        var createdResult = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
        NavigationManager.NavigateTo("hotel-room");
    }
}
در اینجا در ابتدا، سرویس IHotelRoomService به کامپوننت جاری تزریق شده و سپس از متدهای IsRoomUniqueAsync و CreateHotelRoomAsync آن، جهت بررسی منحصربفرد بودن نام اتاق و ثبت نهایی اطلاعات مدل برنامه که به فرم جاری به صورت دو طرفه‌ای متصل است، استفاده کرده‌ایم. در نهایت پس از ثبت اطلاعات، کاربر به صفحه‌ی نمایش لیست اتاق‌ها، توسط سرویس توکار NavigationManager، هدایت می‌شود.

اگر پیشتر با ASP.NET Web Forms کار کرده باشید (اولین روش توسعه‌ی برنامه‌های وب در دنیای دات نت)، مدل برنامه نویسی Blazor Server، بسیار شبیه به کار با وب فرم‌ها است؛ البته بر اساس آخرین تغییرات دنیای دانت نت مانند برنامه نویسی async، کار با سرویس‌ها، تزریق وابستگی‌های توکار و غیره.


نمایش لیست اتاق‌های ثبت شده


تا اینجا موفق شدیم اطلاعات یک مدل اعتبارسنجی شده را در بانک اطلاعاتی ثبت کنیم. مرحله‌ی بعد، نمایش لیست اطلاعات ثبت شده‌ی در بانک اطلاعاتی است. بنابراین به کامپوننت HotelRoomList.razor مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
@page "/hotel-room"

@inject IHotelRoomService HotelRoomService

<div class="row mt-4">
    <div class="col-8">
        <h4 class="card-title text-info">Hotel Rooms</h4>
    </div>
    <div class="col-3 offset-1">
        <NavLink href="hotel-room/create" class="btn btn-info">Add New Room</NavLink>
    </div>
</div>

<div class="row mt-4">
    <div class="col-12">
        <table class="table table-bordered table-hover">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Occupancy</th>
                    <th>Rate</th>
                    <th>
                        Sqft
                    </th>
                    <th>

                    </th>
                </tr>
            </thead>
            <tbody>
                @if (HotelRooms.Any())
                {
                    foreach (var room in HotelRooms)
                    {
                        <tr>
                            <td>@room.Name</td>
                            <td>@room.Occupancy</td>
                            <td>@room.RegularRate.ToString("c")</td>
                            <td>@room.SqFt</td>
                            <td></td>
                        </tr>
                    }
                }
                else
                {
                    <tr>
                        <td colspan="5">No records found</td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</div>

@code
{
    private List<HotelRoomDTO> HotelRooms = new List<HotelRoomDTO>();

    protected override async Task OnInitializedAsync()
    {
        await foreach(var room in HotelRoomService.GetAllHotelRoomsAsync())
        {
            HotelRooms.Add(room);
        }
    }
}
توضیحات:
- متد GetAllHotelRoomsAsync، لیست اتاق‌های ثبت شده را بازگشت می‌دهد. البته خروجی آن از نوع <IAsyncEnumerable<HotelRoomDTO است که از زمان C# 8.0 ارائه شد و روش کار با آن اندکی متفاوت است. IAsyncEnumerable‌ها را باید توسط await foreach پردازش کرد.
- همانطور که در مطلب بررسی چرخه‌ی حیات کامپوننت‌ها نیز عنوان شد، متدهای رویدادگران OnInitialized و نمونه‌ی async آن برای دریافت اطلاعات از سرویس‌ها طراحی شده‌اند که در اینجا نمونه‌ای از آن‌را مشاهده می‌کنید.
- پس از تشکیل لیست اتاق‌ها، حلقه‌ی foreach (var room in HotelRooms) تعریف شده، ردیف‌های آن‌را در UI نمایش می‌دهد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-14.zip
نظرات مطالب
استفاده از افزونه‌ی jsTree در ASP.NET MVC
- هیچ الزامی ندارد که ساختار serialization مورد نیاز jstree، با ساختار جدول بانک اطلاعاتی شما یکی باشد.
- جدولی را که طراحی کردید، صرفا با JsTreeOperationData تطابق دارد.
- این جدول اصول شیء‌گرایی مدل‌های خود ارجاع دهنده را لحاظ نکرده‌است و صرفا یک ساختار ساده‌ی دریافت اطلاعات از کاربر هست و نه بیشتر.
- اگر قرار است با این نوع جداول و کلاس‌های غیر شیءگرا کار کنید، نیاز است SQL خام بنویسید و از مفاهیم CTE استفاده کنید.

نتیجه گیری؟
مدل خودتان را با مدلی که در مقاله‌ی مدل‌های خود ارجاع دهنده عنوان شده، تطبیق دهید تا بتوانید از قابلیت‌های شیء‌گرای EF استفاده کنید.
نظرات مطالب
مشکلات نصب به روز رسانی‌های اخیر
سلام من با مشکل زیر در سیلورلایت روبه رو شدم =>
Error: Unhandled Error in Silverlight Application Code: 1001    Category: RuntimeError       Message: AG_E_UNKNOWN_ERROR
بعد از ایجاد پروژه و موقع اجرا (هیچ کدی هم ننوشتم)
به نظر شما مشکل از کجاست؟
مطالب دوره‌ها
تهیه کوئری بر روی ایندکس‌های Full Text Search
در دو قسمت قبل ابتدا سیستم FTS را نصب و فعال کردیم و سپس تعدادی رکورد را ثبت کرده، کاتالوگ‌های FTS، ایندکس‌ها و Stop words متناظری را ایجاد کردیم. در این قسمت قصد داریم از این اطلاعات ویژه، استفاده کرده و کوئری بگیریم. مواردی که بررسی خواهند شد اصطلاحا Predicates نام داشته و شامل توابع مخصوصی مانند Contains و Freetext می‌شوند.


با استفاده از Contains predicate چه اطلاعاتی را می‌توان جستجو کرد؟

متد Contains مخصوص FTS، قابلیت یافتن کلمات و عبارات، تطابق کامل با عبارت در حال جستجو و یا حتی جستجوهای فازی را دارد. همچنین حالات مختلف صرفی یا inflectional یک کلمه را نیز می‌تواند جستجو کند (مانند jump، jumps و jumped). البته این مورد وابسته است به زبانی که در حین ایجاد ایندکس مشخص می‌شود. امکان یافتن کلماتی نزدیک و مشابه به کلماتی دیگر نیز پیش بینی شده‌است. پیشوندها و پسوندها را نیز می‌توان جستجو کرد. امکان تعیین وزن و اهمیت کلمات در حال جستجو وجود دارند (برای مثال در این جستجوی خاص، کلمه‌ی ویژه اهمیت بیشتری نسبت به بقیه دارد). متد Contains امکان جستجوی Synonyms را نیز دارد. برای مثال یافتن رکوردهایی که معنایی مشابه need دارند اما دقیقا حاوی کلمه‌ی need نیستند.


بررسی ریز جزئیات توانمندی‌های Contains predicate

1) جستجوی کلمات ساده
 -- Simple term
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'data');
در این کوئری که بر روی جدول Documents قسمت قبل انجام می‌شود، به دنبال عین واژه‌ی در حال جستجو هستیم.
باید دقت داشت که این نوع کوئری‌ها، حساس به حروف کوچک و بزرگ نیستند.
همچنین عبارت وارد شده از نوع یونیکد است. به همین جهت برای جلوگیری از تغییر encoding رشته وارد شده (و تفسیر آن بر اساس Collation بانک اطلاعاتی)، یک N به ابتدای عبارت افزوده شده‌است.

2) جستجوی عبارات
 -- Simple term - phrase
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'"data warehouse"');
اگر نیاز به یافتن عین عبارتی که از چند کلمه تشکیل شده‌است می‌باشد، نیاز است آن‌را با "" محصور کرد.

3) استفاده از عملگرهای منطقی مانند OR و AND
 -- Simple terms with logical OR
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'data OR index');
در این کوئری نحوه‌ی استفاده از عملگر منطقی OR را مشاهده می‌کنید.
و یا نحوه‌ی بکارگیری AND NOT در کوئری ذیل مشخص شده‌است:
 -- Simple terms with logical AND NOT
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'data AND NOT mining');
در این کوئری به دنبال رکوردهایی هستیم که docexcerpt آن‌ها دارای کلمه‌ی data بوده، اما شامل mining نمی‌شوند.
به علاوه با استفاده از پرانتزها می‌توان تقدم و تاخر عملگرهای منطقی را بهتر مشخص کرد:
 -- Simple terms with mny logical operators, order defined with parentheses
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'data OR (fact AND warehouse)');

4) جستجوی پیشوندها
 -- Prefix
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'"add*"');
در کوئری فوق به دنبال رکوردهایی هستیم که docexcerpt آن‌ها با کلمه‌ی add شروع می‌شوند. در این حالت نیز استفاده از "" اجباری است. اگر از "" استفاده نشود، FTS به دنبال تطابق عینی با عبارت وارد شده خواهد گشت.

5) جستجوهای Proximity

Proximity در اینجا به معنای یافتن واژه‌هایی هستند که نزدیک (از لحاظ تعداد فاصله بر حسب کلمات) به واژه‌ای دیگر می‌باشند.
 -- Simple proximity
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'NEAR(problem, data)');
برای این منظور از واژه‌ی NEAR استفاده می‌شود؛ به همراه ذکر دو واژه‌ای که به دنبال آن‌ها هستیم. معنای کوئری فوق این است: رکوردهایی را پیدا کن که در آن در یک جایی از خلاصه سند، کلمه‌ی problem وجود دارد و در جایی دیگر از آن خلاصه‌ی سند، کلمه‌ی data.
همچنین می‌توان مشخص کرد که این نزدیک بودن دقیقا به چه معنایی است:
 -- Proximity with max distance 5 words
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'NEAR((problem, data),5)');

-- Proximity with max distance 1 word
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'NEAR((problem, data),1)');
در این کوئری‌ها اعداد 1 و 5، بیانگر فاصله‌ی بین دو کلمه‌‌ای هستند (فاصله بر اساس تعداد کلمه) که قرار است در نتایج جستجو حضور داشته باشند. مقدار پیش فرض آن Max است؛ یعنی در هر جایی از سند.
همچنین می‌توان مشخص کرد که ترتیب جستجو باید دقیقا بر اساس نحوه‌ی تعریف این کلمات در کوئری باشد:
 -- Proximity with max distance and order
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'NEAR((problem, data),5, TRUE)');
GO
پارامتر آخر یا flag، به صورت پیش فرض false است. به این معنا که ترتیب این دو کلمه در جستجو اهمیتی ندارند.

6) جستجوی بر روی بیش از یک فیلد
در قسمت قبل، FULLTEXT INDEX انتهای بحث را بر روی دو فیلد docexcerpt و doccontent تهیه کردیم. اگر نیاز باشد تا جستجوی انجام شده هر دو فیلد را شامل شود می‌توان به نحو ذیل عمل کرد:
 SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS((docexcerpt,doccontent), N'data');
در این حالت تنها کافی است دو فیلد را داخل یک پرانتز قرار داد.

یک نکته: اگر تعداد ستون‌های ایندکس شده زیاد است و نیاز داریم تا بر روی تمام آن‌ها FTS انجام شود، تنها کافی است پارامتر اول متد Contains را * وارد کنیم. * در اینجا به معنای تمام ستون‌هایی است که در حین تشکیل FULLTEXT INDEX ذکر شده‌اند.

7) جستجوهای صرفی یا inflectional
FTS بر اساس زبان انتخابی، در حین تشکیل ایندکس‌های خاص خودش، یک سری آنالیزهای دستوری را نیز بر روی واژه‌ها انجام می‌دهد. همچنین امکان تعریف زبان مورد استفاده در حین استفاده از متد Contains نیز وجود دارد.
 -- Inflectional forms

-- The next query does not return any rows
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'presentation');

-- The next query returns a row
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'FORMSOF(INFLECTIONAL, presentation)');
GO
در این مثال در کوئری اول به دنبال عین واژه‌ی وارد شده هستیم که با توجه به تنظیمات قسمت قبل و داده‌های موجود، خروجی را به همراه ندارد.
اکنون اگر کوئری دوم را که از FORMSOF جهت تعیین روش INFLECTIONAL استفاده کرده است، اجرا کنیم، به یک رکورد خواهیم رسید که در آن جمع واژه‌ی presentation وجود دارد.


8) جستجو برای یافتن متشابهات

برای نمونه اگر SQL Server 2012 بر روی سیستم شما نصب باشد، محل نصب واژه‌نامه‌های Synonyms یا واژه‌هایی همانند از لحاظ معنایی را در مسیر زیر می‌توانید مشاهده کنید:
 C:\...\MSSQL11.MSSQLSERVER\MSSQL\FTData
این‌ها یک سری فایل XML هستند با ساختار ذیل:
<XML ID="Microsoft Search Thesaurus">
    <thesaurus xmlns="x-schema:tsSchema.xml">
<diacritics_sensitive>0</diacritics_sensitive>
        <expansion>
            <sub>Internet Explorer</sub>
            <sub>IE</sub>
            <sub>IE5</sub>
        </expansion>
        <replacement>
            <pat>NT5</pat>
            <pat>W2K</pat>
            <sub>Windows 2000</sub>
        </replacement>
        <expansion>
            <sub>run</sub>
            <sub>jog</sub>
        </expansion>
        <expansion>
            <sub>need</sub>
            <sub>necessity</sub>
        </expansion>
    </thesaurus>
</XML>
در اینجا diacritics_sensitive به معنای حساسیت به لهجه است که به صورت پیش فرض برای تمام زبان‌ها خاموش است. سپس یک سری expansion و replacement را مشاهده می‌کنید.
فایل tsenu.xml به صورت پیش فرض برای زبان انگلیسی آمریکایی مورد استفاده قرار می‌گیرد. اگر محتویات آن‌را برای مثال با محتویات XML ایی فوق جایگزین کنید (در حین ذخیره باید دقت داشت که encoding فایل نیاز است Unicode باشد)، سپس باید SQL Server را از این تغییر نیز مطلع نمائیم:
 -- Load the US English file
EXEC sys.sp_fulltext_load_thesaurus_file 1033;
GO
 عدد 1033، عدد استاندارد زبان US EN است.
 البته اگر اینکار را انجام ندهیم، به صورت خودکار، اولین کوئری که از THESAURUS انگلیسی استفاده می‌کند، سبب بارگذاری آن خواهد شد.
 -- Synonyms

-- The next query does not return any rows
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'need');

-- The next query returns a row
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(docexcerpt, N'FORMSOF(THESAURUS, need)');
GO
در اولین مثال به دنبال عین واژه‌ی need در رکوردهای موجود هستیم که خروجی را بر نمی‌گرداند.
در ادامه اگر کوئری دوم را که از FORMSOF جهت تعیین روش THESAURUS استفاده کرده است، اجرا کنیم، به یک رکورد خواهیم رسید که در آن واژه‌ی necessity به کمک محتویات فایل tsenu.xml که پیشتر تهیه کردیم، بجای need وجود دارد.

9) جستجو بر روی خواص و متادیتای فایل‌ها
 -- Document properties
SELECT id, title, docexcerpt
FROM dbo.Documents
WHERE CONTAINS(PROPERTY(doccontent,N'Authors'), N'Test');
در اینجا نحوه‌ی جستجوی خواص فایل‌های docx ذخیره شده در قسمت قبل را مشاهده می‌کنید که شامل ذکر PROPERTY و ستون FTS مورد نظر است، به همراه نام خاصیت و عبارت جستجو.


کار با FREETEXT
 -- FREETEXT
SELECT *
FROM dbo.Documents
WHERE FREETEXT(docexcerpt, N'data presentation need');
FREETEXT عموما ردیف‌های بیشتری را نسبت به Contains بر می‌گرداند؛ چون جستجوی عمومی‌تری را انجام می‌دهد. در اینجا جستجو بر روی معنای عبارات انجام می‌شود و نه صرفا یافتن عباراتی دقیقا همانند عبارت در حال جستجو. در اینجا مباحث Synonyms و Inflectional ایی که پیشتر یاد شد، به صورت خودکار اعمال می‌شوند.
در کوئری فوق، کلیه رکوردهایی که با سه کلمه‌ی وارد شده (به صورت مجزا) به نحوی تطابق داشته باشند (تطابق کامل یا بر اساس تطابق‌های معنایی یا دستوری) باز گردانده خواهند شد.