نظرات مطالب
پشتیبانی از انقیاد پویا در سی‌شارپ
یک مثال از ExpandoObject
public static class ExpandoXml
{
    public static dynamic AsExpando(this XDocument document)
    {
        return CreateExpando(document.Root);
    }

    private static dynamic CreateExpando(XElement element)
    {
        var result = new ExpandoObject() as IDictionary<string, object>;

        if (element.Elements().Any(e => e.HasElements))
        {
            var list = new List<ExpandoObject>();
            result.Add(element.Name.ToString(), list);
            foreach (var childElement in element.Elements())
            {
                list.Add(CreateExpando(childElement));
            }
        }
        else
        {
            foreach (var leafElement in element.Elements())
            {
                result.Add(leafElement.Name.ToString(), leafElement.Value);
            }
        }

        return result;
    }
}

طریقه استفاده
class Program
{
    static void Main(string[] args)
    {
        var doc1 = XDocument.Load("Employees.xml");
        foreach (var element in doc1.Element("Employees").Elements("Employee"))
        {
            Console.WriteLine(element.Element("FirstName").Value);
        }

        var doc2 = XDocument.Load("Employees.xml").AsExpando();
        foreach (var employee in doc2.Employees)
        {
            Console.WriteLine(employee.FirstName);
        }
    }
}
همانطور که در مقاله ذکر شد، برای حالت پیشرفته‌تر آن می‌توان با استفاده از DynamicObject یک DynamicXml را تهیه کرد.
مطالب
روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت
اگر در کدهای خود قطعه کد ذیل را دارید:
using(var client = new HttpClient())
{
   // do something with http client
}
استفاده‌ی از using در اینجا، نه‌تنها غیرضروری و اشتباه است، بلکه سبب از کار افتادن زود هنگام برنامه‌ی شما با صدور استثنای ذیل خواهد شد:
 Unable to connect to the remote server
System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.


HttpClient خود را Dispose نکنید

کلاس HttpClient اینترفیس IDisposable را پیاده سازی می‌کند. بنابراین روش استفاده‌ی اصولی آن باید به صورت ذیل و با پیاده سازی خودکار رهاسازی منابع مرتبط با آن باشد:
using (var client = new HttpClient())
{
       var result = await client.GetAsync("http://example.com/");
}
اما در این حال فرض کنید به همین روش تعدادی درخواست را ارسال کرده‌اید:
for (int i = 0; i < 10; i++)
{
      using (var client = new HttpClient())
      {
            var result = await client.GetAsync("http://example.com/");
            Console.WriteLine(result.StatusCode);
      }
}
مشکل این روش، در ایجاد سوکت‌های متعددی است که حتی پس از بسته شدن برنامه نیز باز، باقی خواهند ماند:
  TCP    192.168.1.6:13996      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13997      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13998      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13999      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14000      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14001      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14002      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14003      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14004      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14005      93.184.216.34:http     TIME_WAIT
این یک نمونه‌ی خروجی برنامه‌ی فوق، توسط دستور netstat «پس از بسته شدن کامل برنامه» است.

بنابراین اگر برنامه‌ی شما تعداد زیادی کاربر دارد و یا تعداد زیادی درخواست را به روش فوق ارسال می‌کند، سیستم عامل به حد اشباع ایجاد سوکت‌های جدید خواهد رسید.
این مشکل نیز ارتباطی به طراحی این کلاس و یا زبان #C و حتی استفاده‌ی از using نیز ندارد. این رفتار، رفتار معمول سیستم عامل، با سوکت‌های ایجاد شده‌است. TIME_WAIT ایی را که در اینجا ملاحظه می‌کنید، به معنای بسته شدن اتصال از طرف برنامه‌ی ما است؛ اما سیستم عامل هنوز منتظر نتیجه‌ی نهایی، از طرف دیگر اتصال است که آیا قرار است بسته‌ی TCP ایی را دریافت کند یا خیر و یا شاید در بین راه تاخیری وجود داشته‌است. برای نمونه ویندوز به مدت 240 ثانیه یک اتصال را در این حالت حفظ خواهد کرد، که مقدار آن نیز در اینجا تنظیم می‌شود:
 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay]

بنابراین روش توصیه شده‌ی کار با HttpClient، داشتن یک وهله‌ی سراسری از آن در برنامه و عدم Dispose آن است. HttpClient نیز thread-safe طراحی شده‌است و دسترسی به یک شیء سراسری آن در برنامه‌های چند ریسمانی مشکلی را ایجاد نمی‌کند. همچنین Dispose آن نیز غیرضروری است و پس از پایان برنامه به صورت خودکار توسط سیستم عامل انجام خواهد شد.


تمام اجزای HttpClient به صورت Thread-safe طراحی نشده‌اند

تا اینجا به این نتیجه رسیدیم که روش صحیح کار کردن با HttpClient، نیاز به داشتن یک وهله‌ی Singleton از آن‌را در سراسر برنامه دارد و Dispose صریح آن، بجز اشباع سوکت‌های سیستم عامل و ناپایدار کردن تمام برنامه‌هایی که از آن سرویس می‌گیرند، حاصلی را به همراه نخواهد داشت. در این بین مطابق مستندات HttpClient، استفاده‌ی از متدهای ذیل این کلاس thread-safe هستند:
CancelPendingRequests
DeleteAsync
GetAsync
GetByteArrayAsync
GetStreamAsync
GetStringAsync
PostAsync
PutAsync
SendAsync
اما تغییر این خواص در کلاس HttpClient به هیچ عنوان thread-safe نبوده و در برنامه‌های چند ریسمانی و چند کاربری، مشکل ساز می‌شوند:
BaseAddress
DefaultRequestHeaders
MaxResponseContentBufferSize
Timeout
بنابراین در طراحی کلاس مدیریت کننده‌ی HttpClient برنامه‌ی خود نیاز است به ازای هر BaseAddress‌، یک HttpClient خاص آن‌را ایجاد کرد و HttpClientهای سراسری نمی‌توانند BaseAddress‌های خود را نیز به اشتراک گذاشته و تغییری را در آن ایجاد کنند.


استفاده‌ی سراسری و مجدد از HttpClient، تغییرات DNS را متوجه نمی‌شود

با طراحی یک کلاس مدیریت کننده‌ی سراسری HttpClient با طول عمر Singelton، به یک مشکل دیگر نیز برخواهیم خورد: چون در اینجا از اتصالات، استفاده‌ی مجدد می‌شوند، دیگر تغییرات DNS را لحاظ نخواهند کرد.
برای حل این مشکل، در زمان ایجاد یک HttpClient سراسری، به ازای یک BaseAddress مشخص، باید از ServicePointManager کوئری گرفته و زمان اجاره‌ی اتصال آن‌را دقیقا مشخص کنیم:
var sp = ServicePointManager.FindServicePoint(new Uri("http://thisisasample.com"));
sp.ConnectionLeaseTimeout = 60*1000; //In milliseconds
با این‌کار هرچند هنوز هم از اتصالات استفاده‌ی مجدد می‌شود، اما این استفاده‌ی مجدد، نامحدود نبوده و مدت معینی را پیدا می‌کند.


طراحی یک کلاس، برای مدیریت سراسری وهله‌های HttpClient‌

تا اینجا به صورت خلاصه به نکات ذیل رسیدیم:
- HttpClient باید به صورت یک وهله‌ی سراسری Singleton مورد استفاده قرار گیرد. هر وهله سازی مجدد آن 35ms زمان می‌برد.
- Dispose یک HttpClient غیرضروری است.
- HttpClient تقریبا thread safe طراحی شده‌است؛ اما تعدادی از خواص آن مانند BaseAddress‌  اینگونه نیستند.
- برای رفع مشکل اتصالات چسبنده (اتصالاتی که هیچگاه پایان نمی‌یابند)، نیاز است timeout آن‌را تنظیم کرد.

بنابراین بهتر است این نکات را در یک کلاس به صورت ذیل کپسوله کنیم:
using System;
using System.Collections.Generic;
using System.Net.Http;

namespace HttpClientTips
{
    public interface IHttpClientFactory : IDisposable
    {
        HttpClient GetOrCreate(
            Uri baseAddress,
            IDictionary<string, string> defaultRequestHeaders = null,
            TimeSpan? timeout = null,
            long? maxResponseContentBufferSize = null,
            HttpMessageHandler handler = null);
    }
}

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;

namespace HttpClientTips
{
    /// <summary>
    /// Lifetime of this class should be set to `Singleton`.
    /// </summary>
    public class HttpClientFactory : IHttpClientFactory
    {
        // 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the HttpClient more than
        // once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
        // threads but only one of the objects succeeds in creating the HttpClient.
        private readonly ConcurrentDictionary<Uri, Lazy<HttpClient>> _httpClients =
                         new ConcurrentDictionary<Uri, Lazy<HttpClient>>();
        private const int ConnectionLeaseTimeout = 60 * 1000; // 1 minute

        public HttpClientFactory()
        {
            // Default is 2 minutes: https://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.dnsrefreshtimeout(v=vs.110).aspx
            ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
            // Increases the concurrent outbound connections
            ServicePointManager.DefaultConnectionLimit = 1024;
        }

        public HttpClient GetOrCreate(
           Uri baseAddress,
           IDictionary<string, string> defaultRequestHeaders = null,
           TimeSpan? timeout = null,
           long? maxResponseContentBufferSize = null,
           HttpMessageHandler handler = null)
        {
            return _httpClients.GetOrAdd(baseAddress,
                             uri => new Lazy<HttpClient>(() =>
                             {
                                 // Reusing a single HttpClient instance across a multi-threaded application means
                                 // you can't change the values of the stateful properties (which are not thread safe),
                                 // like BaseAddress, DefaultRequestHeaders, MaxResponseContentBufferSize and Timeout.
                                 // So you can only use them if they are constant across your application and need their own instance if being varied.
                                 var client = handler == null ? new HttpClient { BaseAddress = baseAddress } :
                                               new HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress };
                                 setRequestTimeout(timeout, client);
                                 setMaxResponseBufferSize(maxResponseContentBufferSize, client);
                                 setDefaultHeaders(defaultRequestHeaders, client);
                                 setConnectionLeaseTimeout(baseAddress, client);
                                 return client;
                             },
                             LazyThreadSafetyMode.ExecutionAndPublication)).Value;
        }

        public void Dispose()
        {
            foreach (var httpClient in _httpClients.Values)
            {
                httpClient.Value.Dispose();
            }
        }

        private static void setConnectionLeaseTimeout(Uri baseAddress, HttpClient client)
        {
            // This ensures connections are used efficiently but not indefinitely.
            client.DefaultRequestHeaders.ConnectionClose = false; // keeps the connection open -> more efficient use of the client
            ServicePointManager.FindServicePoint(baseAddress).ConnectionLeaseTimeout = ConnectionLeaseTimeout; // ensures connections are not used indefinitely.
        }

        private static void setDefaultHeaders(IDictionary<string, string> defaultRequestHeaders, HttpClient client)
        {
            if (defaultRequestHeaders == null)
            {
                return;
            }
            foreach (var item in defaultRequestHeaders)
            {
                client.DefaultRequestHeaders.Add(item.Key, item.Value);
            }
        }

        private static void setMaxResponseBufferSize(long? maxResponseContentBufferSize, HttpClient client)
        {
            if (maxResponseContentBufferSize.HasValue)
            {
                client.MaxResponseContentBufferSize = maxResponseContentBufferSize.Value;
            }
        }

        private static void setRequestTimeout(TimeSpan? timeout, HttpClient client)
        {
            if (timeout.HasValue)
            {
                client.Timeout = timeout.Value;
            }
        }
    }
}
در اینجا به ازای هر baseAddress جدید، یک HttpClient خاص آن ایجاد می‌شود تا در کل برنامه مورد استفاده‌ی مجدد قرار گیرد. برای مدیریت thread-safe ایجاد HttpClientها نیز از نکته‌ی مطلب «الگویی برای مدیریت دسترسی همزمان به ConcurrentDictionary» استفاده شده‌است. همچنین نکات تنظیم ConnectionLeaseTimeout و سایر خواص غیر thread-safe کلاس HttpClient نیز در اینجا لحاظ شده‌اند.

پس از تدارک این کلاس، نحوه‌ی معرفی آن به سیستم باید به صورت Singleton باشد. برای مثال اگر از ASP.NET Core استفاده می‌کنید، آن‌را به صورت ذیل ثبت کنید:
namespace HttpClientTips.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IHttpClientFactory, HttpClientFactory>();
            services.AddMvc();
        }

اکنون، یک نمونه، نحوه‌ی استفاده‌ی از اینترفیس IHttpClientFactory تزریقی به صورت ذیل می‌باشد:
namespace HttpClientTips.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IHttpClientFactory _httpClientFactory;
        public HomeController(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        public async Task<IActionResult> Index()
        {
            var host = new Uri("http://localhost:5000");
            var httpClient = _httpClientFactory.GetOrCreate(host);
            var responseMessage = await httpClient.GetAsync("home/about").ConfigureAwait(false);
            var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
            return Content(responseContent);
        }
سرویس IHttpClientFactory یک HttpClient را به ازای host درخواستی ایجاد کرده و در طول عمر برنامه از آن استفاده‌ی مجدد می‌کند. به همین جهت دیگر مشکل اشباع سوکت‌ها در این سیستم رخ نخواهند داد.


برای مطالعه‌ی بیشتر

You're using HttpClient wrong and it is destabilizing your software
Disposable, Finalizers, and HttpClient
Using HttpClient as it was intended (because you’re not)
Singleton HttpClient? Beware of this serious behaviour and how to fix it
Beware of the .NET HttpClient
Effectively Using HttpClient
مطالب
Blazor 5x - قسمت 16 - کار با فرم‌ها - بخش 4 - تهیه سرویس‌های آپلود تصاویر
در ادامه می‌خواهیم برای هر اتاق ثبت شده، تعدادی تصویر مرتبط را نیز به سرور آپلود کرده و مشخصات آن‌ها را در بانک اطلاعاتی ثبت کنیم. به همین جهت در این قسمت سرویس ثبت اطلاعات تصاویر در بانک اطلاعاتی و سرویس آپلود فایل‌ها را تهیه می‌کنیم.


تعریف موجودیت و DbSet تصاویر یک اتاق هتل

برای اینکه بتوان اطلاعات تصاویر آپلودی را در بانک اطلاعاتی ثبت کرد، نیاز است یک رابطه‌ی یک به چند را بین یک اتاق و تصاویر مرتبط با آن برقرار کرد. به همین جهت ابتدا به پروژه‌ی BlazorServer.Entities.csproj مراجعه کرده و موجودیت ثبت اطلاعات تصاویر را تعریف می‌کنیم:
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class HotelRoomImage
    {
        public int Id { get; set; }

        public string RoomImageUrl { get; set; }

        [ForeignKey("RoomId")]
        public virtual HotelRoom HotelRoom { get; set; }
        public int RoomId { get; set; }
    }
}
که در اینجا باید سر دیگر این رابطه‌ی one-to-many، در جدول HotelRoom نیز تعریف شود:
namespace BlazorServer.Entities
{
    public class HotelRoom
    {
        // ...
        public virtual ICollection<HotelRoomImage> HotelRoomImages { get; set; }
    }
}
در آخر باید این موجودیت جدید را به Context برنامه معرفی کرد. برای اینکار به پروژه‌ی BlazorServer.DataAccess مراجعه کرده و DbSet متناظری را تعریف می‌کنیم:
namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<HotelRoomImage> HotelRoomImages { get; set; }

        // ...
    }
}
پس از این تغییرات، نیاز است یکبار دیگر عملیات Migrations را اجرا کرد، تا ساختار متناظر بانک اطلاعاتی این تغییرات ایجاد شود. بنابراین توسط خط فرمان به پوشه‌ی پروژه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم. در اینجا نگارش 5.0.3 باید معادل نگارشی از EF-Core باشد که از آن در حال استفاده‌اید:
dotnet tool update --global dotnet-ef --version 5.0.3
dotnet build
dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext
dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
در مورد این دستورات در قسمت 13 بیشتر بحث شده‌است.


تعریف مدل UI متناظر با هر تصویر

همانطور که در قسمت 13 نیز عنوان شد، در حین کار با رابط کاربری برنامه، با موجودیت‌های بانک اطلاعاتی، به صورت مستقیم کار نخواهیم کرد و بر اساس نیازهای برنامه، یکسری کلاس DTO را تعریف می‌کنیم. بنابراین به پروژه‌ی BlazorServer.Models مراجعه کرده و DTO متناظر با HotelRoomImage را به صورت زیر اضافه می‌کنیم:
namespace BlazorServer.Models
{
    public class HotelRoomImageDTO
    {
        public int Id { get; set; }

        public int RoomId { get; set; }

        public string RoomImageUrl { get; set; }
    }
}
و همچنین جهت سهولت تبدیل اطلاعات بین موجودیت تعریف شده و DTO ی آن، نگاشت AutoMapper دو طرفه‌ای را در پروژه‌ی BlazorServer.Models.Mappings برقرار می‌کنیم:
using AutoMapper;
using BlazorServer.Entities;

namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            // ...
            CreateMap<HotelRoomImageDTO, HotelRoomImage>().ReverseMap(); // two-way mapping
        }
    }
}

تعریف سرویس کار با HotelRoomImage

در اینجا نیز همانند سرویسی که برای انجام عملیات تجاری مرتبط با یک اتاق هتل، در قسمت 13 پیاده سازی کردیم، سرویس دیگری را در پروژه‌ی BlazorServer.Services برای کار با تصاویر اتاق‌ها تهیه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IHotelRoomImageService
    {
        Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO);

        Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId);

        Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId);

        Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId);
    }
}
برای نمونه بر اساس اطلاعات مدل UI برنامه، نیاز است بتوانیم اطلاعات یک تصویر را ثبت و یا حذف کنیم و یا لیست تصاویر یک اتاق را از بانک اطلاعاتی دریافت کنیم؛ با این پیاده سازی:
namespace BlazorServer.Services
{
    public class HotelRoomImageService : IHotelRoomImageService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public HotelRoomImageService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO)
        {
            var image = _mapper.Map<HotelRoomImage>(imageDTO);
            await _dbContext.HotelRoomImages.AddAsync(image);
            return await _dbContext.SaveChangesAsync();
        }

        public async Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId)
        {
            var image = await _dbContext.HotelRoomImages.FindAsync(imageId);
            _dbContext.HotelRoomImages.Remove(image);
            return await _dbContext.SaveChangesAsync();
        }

        public async Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId)
        {
            var imageList = await _dbContext.HotelRoomImages.Where(x => x.RoomId == roomId).ToListAsync();
            _dbContext.HotelRoomImages.RemoveRange(imageList);
            return await _dbContext.SaveChangesAsync();
        }

        public Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId)
        {
            return _dbContext.HotelRoomImages
                            .Where(x => x.RoomId == roomId)
                            .ProjectTo<HotelRoomImageDTO>(_mapperConfiguration)
                            .ToListAsync();
        }
    }
}
پس از این تعاریف، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و این سرویس را به سیستم تزریق وابستگی‌های برنامه معرفی می‌کنیم:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IHotelRoomImageService, HotelRoomImageService>();
            // ...


تهیه سرویسی برای آپلود فایل‌های یک برنامه‌ی Blazor Server به سرور

جهت ساده سازی کار آپلود، در برنامه‌های Blazor Server، سرویس جدید FileUploadService را به پروژه‌ی BlazorServer.Services اضافه می‌کنیم:
using Microsoft.AspNetCore.Components.Forms;
using System.Threading.Tasks;

namespace BlazorServer.Services
{
    public interface IFileUploadService
    {
        void DeleteFile(string fileName, string webRootPath, string uploadFolder);
        Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder);
    }
}
کار آن حذف یک فایل، بر اساس مسیر آن است و همچنین دریافت یک IBrowserFile از کاربر و ذخیره سازی اطلاعات آن در سرور؛ با این پیاده سازی:
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.IO;
using System.Threading.Tasks;

namespace BlazorServer.Services
{
    public class FileUploadService : IFileUploadService
    {
        private const int MaxBufferSize = 0x10000;

        public void DeleteFile(string fileName, string webRootPath, string uploadFolder)
        {
            var path = Path.Combine(webRootPath, uploadFolder, fileName);
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }

        public async Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder)
        {
            createUploadDir(webRootPath, uploadFolder);
            var (fileName, imageFilePath) = getOutputFileInfo(inputFile, webRootPath, uploadFolder);

            using (var outputFileStream = new FileStream(
                        imageFilePath, FileMode.Create, FileAccess.Write,
                        FileShare.None, MaxBufferSize, useAsync: true))
            {
                using var inputStream = inputFile.OpenReadStream();
                await inputStream.CopyToAsync(outputFileStream);
            }

            return $"{uploadFolder}/{fileName}";
        }

        private static (string FileName, string FilePath) getOutputFileInfo(
                    IBrowserFile inputFile, string webRootPath, string uploadFolder)
        {
            var fileName = Path.GetFileName(inputFile.Name);
            var imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName);
            if (File.Exists(imageFilePath))
            {
                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
                var fileExtension = Path.GetExtension(fileName);
                fileName = $"{fileNameWithoutExtension}-{Guid.NewGuid()}{fileExtension}";
                imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName);
            }
            return (fileName, imageFilePath);
        }

        private static void createUploadDir(string webRootPath, string uploadFolder)
        {
            var folderDirectory = Path.Combine(webRootPath, uploadFolder);
            if (!Directory.Exists(folderDirectory))
            {
                Directory.CreateDirectory(folderDirectory);
            }
        }
    }
}
اگر در ASP.NET Core، اطلاعات فایل ارسالی به سرور، توسط IFormFile به اکشن متدهای کنترلرها ارسال می‌شود، در برنامه‌های Blazor Server اینکار توسط IBrowserFile صورت می‌گیرد. کلیات کار با آن، بسیار شبیه به IFormFile است و اگر به مطلب «بررسی روش آپلود فایل‌ها در ASP.NET Core» مراجعه کنید، تفاوت آنچنانی را مشاهده نخواهید کرد. تنها تفاوت پیاده سازی که در اینجا وجود دارد، نیاز به استفاده‌ی از متد ()inputFile.OpenReadStream جهت دسترسی به محتوای فایل آپلودی، برای ذخیره‌ی آن در سمت سرور است؛ وگرنه مابقی کدهای آپلود آن، با ASP.NET Core یکی است.
همچنین برای دسترسی به IBrowserFile در یک سرویس، نیاز است وابستگی زیر را نیز به پروژه‌ی سرویس‌ها اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.3" />
  </ItemGroup>
</Project>
پس از آن، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و این سرویس را به سیستم تزریق وابستگی‌های برنامه معرفی می‌کنیم:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IFileUploadService, FileUploadService>();
            // ...
در قسمت بعد، از این سرویس‌ها جهت مدیریت آپلود تصاویر استفاده خواهیم کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-16.zip
مطالب
انتخاب پویای فیلد ها در LINQ

LINQ یک DLS  بر مبنای .NET  می باشد که برای پرس و جو در منابع داده ای مانند پایگاه‌های داده ، فایل‌های XML و یا لیستی از اشیاء درون حافظه کاربرد دارد.

یکی از بزرگترین مزیت‌های آن Syntax  آسان و خوانا آن می‌باشد.

LINQ  از 2 نوع نمادگذاری پشتیبانی می‌کند:

  • Inline LINQ یا query expressions : 
var result = 
    from product in dbContext.Products
    where product.Category.Name == "Toys"
    where product.Price >= 2.50
    select product.Name;
  • Fluent Syntax : 
var result = dbContext.Products
    .Where(p => p.Category.Name == "Toys" && p.Price >= 250)
    .Select(p => p.Name);

در پرس و چو‌های بالا فیلد‌های مورد نیاز در قسمت Select در زمان Compile شناخته شده هستند . اما گاهی ممکن است فیلد‌های مورد نیاز در زمان اجرا مشخص شوند.

به عنوان مثال یک گزارش ساز پویا که کاربر مشخص می‌کند چه ستون هایی در خروجی نمایش داده شوند یا یک جستجوی پیشرفته که ستون‌های خروجی به اختیار کاربر در زمان اجرا مشخص می‌شوند. 

این مدل را در نظر داشته باشید :

    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Field1 { get; set; }
        public string Field2 { get; set; }
        public string Field3 { get; set; }


        public static IEnumerable<Student> GetStudentSource()
        {
            for (int i = 0; i < 10; i++)
            {
                yield return new Student
                                 {
                                     Id = i,
                                     Name = "Name " + i,
                                     Field1 = "Field1 " + i,
                                     Field2 = "Field2 " + i,
                                     Field3 = "Field3 " + i
                                 };
            }
        }
    }

ستون‌های کلاس Student  را در رابط کاربری برنامه جهت انتخاب به کاربر نمایش می‌دهیم. سپس کاربر یک یا چند ستون را انتخاب می‌کند که قسمت Select  کوئری برنامه باید  بر اساس فیلد‌های مورد نظر کاربر مشخص شود.

یکی از روش هایی که می‌توان از آن بهره برد استفاده از کتاب خانه Dynamic LINQ معرفی شده در اینجا می باشد.

این کتابخانه جهت سهولت در نصب به کمک NuGet در این آدرس قرار دارد.

فرض بر این است که فیلد‌های انتخاب شده توسط کاربر با "," از یکدیگر جدا شده اند. 

    public class Program
    {
        private static void Main(string[] args)
        {
            System.Console.WriteLine("Specify the desired fields : ");
            string fields = System.Console.ReadLine();
            IEnumerable<Student> students = Student.GetStudentSource();
            IQueryable output = students.AsQueryable().Select(string.Format("new({0})", fields));
            foreach (object item in output)
            {
                System.Console.WriteLine(item);
            }
          
            System.Console.ReadKey();
        }
  
    }

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

این روش مزایا و معایب خودش را دارد ، به عنوان مثال خروجی یک لیست از شیء Student  نیست یا این Select  فقط برای روی یک شیء IQueryable  قابل انجام است.

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

    public  class SelectBuilder <T>
    {
        public static Func<T, T> CreateNewStatement(string fields)
        {
            // input parameter "o"
            var xParameter = Expression.Parameter(typeof(T), "o");


            // new statement "new T()"
            var xNew = Expression.New(typeof(T));

            // create initializers
            var bindings = fields.Split(',').Select(o => o.Trim())
                .Select(o =>
                {

                    // property "Field1"
                    var property = typeof(T).GetProperty(o);

                    // original value "o.Field1"
                    var xOriginal = Expression.Property(xParameter, property);

                    // set value "Field1 = o.Field1"
                    return Expression.Bind(property, xOriginal);
                }
            ).ToList();

            // initialization "new T { Field1 = o.Field1, Field2 = o.Field2 }"
            var xInit = Expression.MemberInit(xNew, bindings);

            // expression "o => new T { Field1 = o.Field1, Field2 = o.Field2 }"
            var lambda = Expression.Lambda<Func<T, T>>(xInit, xParameter);

            // compile to Func<T, T>
            return lambda.Compile();
        }
    }
برای استفاده از متد CreateNewStatement باید اینگونه عمل کرد :  
       IEnumerable<Student> result = students.Select(SelectBuilder<Student>.CreateNewStatement("Field1, Field2")).ToList();

            foreach (Student student in result)
            {
                System.Console.WriteLine(student.Field1);
            }
خروجی یک لیست از Student  می باشد.
 نحوه‌ی کارکرد CreateNewStatement :

ابتدا فیلد‌های انتخابی کاربر که با "," جدا شده اند به ورودی پاس داده می‌شود سپس یک statement  خالی ایجاد می‌شود

o=>new Student()
فیلد‌های ورودی از یکدیگر تفکیک می‌شوند و به کمک Reflection پراپرتی معادل فیلد رشته ای در کلاس Student پیدا می‌شود :  
var property = typeof(T).GetProperty(o);
سپس عبارت Select و تولید شیء جدید بر اساس فیلد‌های ورودی تولید می‌شود و برای استفاده Compile  به Func می‌شود. در نهایت Func  تولید شده به Select پاس داده می‌شود و لیستی از Student  بر مبنای فیلد‌های انتخابی تولید می‌شود. 

دریافت مثال : DynamicSelect.zip 
مطالب
استفاده از LINQ جهت جستجوی فایل‌ها

یکی دیگر از کاربردهای anonymous types ، امکان استفاده از قابلیت‌های LINQ برای جستجوی فایل‌ها و پوشه‌ها است.
مثال:

using System;
using System.Linq;
using System.IO;

namespace LINQtoDir
{
class Program
{
static void Main(string[] args)
{
var query = from f in new DirectoryInfo(@"C:\Documents and Settings\vahid\My Documents\My Pictures")
.GetFiles("*.*", SearchOption.AllDirectories)
where f.Extension.ToLower() == ".png" || f.Extension.ToLower() == ".jpg"
orderby f.LastAccessTime
select new
{
DateLastModified = f.LastWriteTime,
Extension = f.Extension,
Size = f.Length,
FileName = f.Name
};

foreach (var file in query)
Console.WriteLine(file.FileName);

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

در این مثال توسط کوئری نوشته شده، تمامی تصاویر jpg و یا png موجود در پوشه my pictures یافت شده و سپس بر اساس LastAccessTime مرتب می‌شوند. در آخر با استفاده از anonymous types ، یک شیء IEnumerable از خواص مورد نظر فایل‌های یافت شده، بازگشت داده می‌شود. اکنون هر استفاده‌ی دلخواهی را می‌توان از این شیء انجام داد.

مطالب
روش دیگر نوشتن Model binderهای سفارشی در ASP.NET Core 7x با معرفی IParseable
ASP.NET MVC از روش بکارگیری binding providerها برای تدارک زیرساخت model binding استفاده می‌کند که در این روش، داده‌های پارامترهای یک action method از طریق هدرها، کوئری استرینگ‌ها، بدنه‌ی درخواست و غیره تهیه می‌شوند. در حالت پیش‌فرض اگر این پارامترها از نوع‌های ساده‌ای مانند اعداد و یا DateTime تشکیل شده باشند و یا به همراه یک TypeConverter باشند که امکان تبدیل این رشته را به آن نوع خاص بدهد، به صورت خودکار bind خواهند شد و هر نوع دیگری، به صورت یک نوع پیچیده درنظر گرفته می‌شود. نوع پیچیده یعنی bind برای مثال اطلاعات بدنه‌ی درخواست به تک تک خواص آن نوع. برای نمونه در کنترلر زیر:
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries =
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy",
        "Hot", "Sweltering", "Scorching",
    };

    // /WeatherForecast/GetForecast2?from=1&to=3
    [HttpGet("[action]")]
    public IEnumerable<WeatherForecast> GetForecast2(Duration days)
    {
        return Enumerable.Range(days.From, days.To)
                         .Select(index => new WeatherForecast
                                          {
                                              Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                                              TemperatureC = Random.Shared.Next(-20, 55),
                                              Summary = Summaries[Random.Shared.Next(Summaries.Length)],
                                          })
                         .ToArray();
    }
}
که از دو مدل زیر استفاده می‌کند:
public class Duration
{
    public int From { get; set; }
    public int To { get; set; }
}

public class WeatherForecast
{
    public DateOnly Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string? Summary { get; set; }
}
می‌توان خواص پارامتر days را از طریق کوئری استرینگ‌های HttpGet، برای مثال با ارائه‌ی آدرس WeatherForecast/GetForecast2?from=1&to=3 به صورت خودکار تامین کرد. زمانیکه اطلاعات رسیده چنین شکلی را داشته باشند، کار پردازش و bind آن‌ها در حالت HttpGet، خودکار است.


روش دیگر پردازش اطلاعات رشته‌ای رسیده و تشکیل یک Model Binder سفارشی در ASP.NET Core 7x

اکنون فرض کنید بجای آدرس WeatherForecast/GetForecast2?from=1&to=3 که اطلاعات را از طریق کوئری استرینگ مشخص و استانداردی دریافت می‌کند، می‌خواهیم اطلاعات آن‌را از طریق یک قالب سفارشی و غیراستاندارد مانند WeatherForecast/GetForecast3/1-3 دریافت کنیم:
// /WeatherForecast/GetForecast3/1-3
[HttpGet("[action]/{days}")]
public IEnumerable<WeatherForecast> GetForecast3(Days days)
    {
        return Enumerable.Range(days.From, days.To)
                         .Select(index => new WeatherForecast
                                          {
                                              Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                                              TemperatureC = Random.Shared.Next(-20, 55),
                                              Summary = Summaries[Random.Shared.Next(Summaries.Length)],
                                          })
                         .ToArray();
    }
یکی از راه‌های انجام اینکار، نوشتن model binderهای سفارشی مخصوص است و یا اکنون در ASP.NET Core 7x می‌توان با پیاده سازی اینترفیس IParsable به صورت خودکار و با روشی دیگر به این مقصود رسید:
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace NET7Mvc.Models;

public class Days : IParsable<Days>
{
    public Days(int from, int to)
    {
        From = from;
        To = to;
    }

    public int From { get; }
    public int To { get; }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider,
                                [MaybeNullWhen(false)] out Days result)
    {
        var items = value?.Split('-', StringSplitOptions.RemoveEmptyEntries);
        if (items is { Length: 2 })
        {
            if (int.TryParse(items[0], NumberStyles.None, provider, out var from)
                && int.TryParse(items[1], NumberStyles.None, provider, out var to))
            {
                result = new Days(from, to);
                return true;
            }
        }

        result = default;
        return false;
    }

    public static Days Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
            throw new ArgumentException("Could not parse the given value.", nameof(value));
        }

        return result;
    }
}
- برای پیاده سازی این اینترفیس باید دو متد TryParse و Parse آن‌را به صورت فوق پیاده سازی کرد و توسط آن، روش تبدیل رشته‌ی دریافتی از کاربر را به شیء مدنظر، مشخص کرد.
- همینقدر که مدلی IParsable را پیاده سازی کرده باشد، از امکانات آن به صورت خودکار استفاده خواهد شد و نیازی به معرفی و یا تنظیمات خاص دیگری ندارد.
- البته این قابلیت جدید نیست و پشتیبانی از IParsable، پیشتر در Minimal API دات نت 6 ارائه شده بود؛ اما در دات نت 7 توسط ASP.NET Core MVC نیز قابل استفاده شده‌است.
مطالب دوره‌ها
استفاده از Factories برای حذف Service locators در برنامه‌های WinForms
یک برنامه‌ی WinForms را درنظر بگیرید که از دو فرم تشکیل شده است.
فرم اول کار نمایش فرم 2 را به عهده دارد.
فرم دوم کار ارسال ایمیل را انجام می‌دهد. این ایمیل نیز از طریق سرویس ذیل فراهم می‌شود:
namespace WinFormsIoc.Services
{
    public interface IEmailsService
    {
        void SendEmail(string from, string to, string title, string message);
    }
}

namespace WinFormsIoc.Services
{
    public class EmailsService : IEmailsService
    {
        public void SendEmail(string from, string to, string title, string message)
        {
            //todo: ...
        }
    }
}
پیاده سازی متد SendEmail در اینجا مدنظر نیست. نکته‌ی مهم، مدیریت تامین و تزریق وابستگی‌های تعریف شده در سازنده‌ی آن است:
    public partial class Form2 : Form
    {
        private readonly IEmailsService _emailsService;
        public Form2(IEmailsService emailsService)
        {
            _emailsService = emailsService;
            InitializeComponent();
        }
احتمالا شاید عنوان کنید که در فرم اول، زمانیکه نیاز است فرم دوم نمایش داده شود، می‌نویسیم new Form2 و در پارامتر آن با استفاده از متد ObjectFactory.GetInstance سازنده‌ی آن‌را فراهم می‌کنیم:
 var form2 = new Form2(ObjectFactory.GetInstance<IEmailsService>());
form2.Show();
و یا اگر مدتی با IoC Containers کار کرده باشید، شاید پیشنهاد دهید که فقط بنویسید:
 var form2 = ObjectFactory.GetInstance<Form2>();
form2.Show();
و همین! به صورت خودکار اگر n پارامتر تزریق شده هم در سازنده‌ی فرم دوم وجود داشته باشند، بر اساس تنظیمات اولیه‌ی IoC Container مورد استفاده، نمونه سازی شده و برنامه بدون مشکل کار خواهد کرد.

مشکل! این دو راه حل هیچکدام به عنوان تزریق وابستگی‌ها شناخته نمی‌شوند و به الگوی Service locator معروف هستند. مشکل آن‌ها این است که کدهای ما در حال حاضر وابستگی مستقیمی به IoC container مورد استفاده پیدا کرده‌اند. در حالت اول ما خودمان دستی درخواست داده‌ایم که کدام وابستگی باید وهله سازی شود و در حالت دوم همانند حالت اول، کدهای ObjectFactory.GetInstance، مختص به یک IoC Container خاص است. نحوه‌ی صحیح کار با IoC Container‌ها باید به این نحو باشد که یکبار در آغاز برنامه تنظیم شوند و در ادامه سایر کلاس‌های برنامه طوری کار کنند که انگار IoC Container ایی وجود خارجی ندارد.


راه حل: ObjectFactory.GetInstance را کپسوله کنید.

using System.Windows.Forms;

namespace WinFormsIoc.IoC
{
    public interface IFormFactory
    {
        T Create<T>() where T : Form;
    }
}

using System.Windows.Forms;
using StructureMap;

namespace WinFormsIoc.IoC
{
    public class FormFactory : IFormFactory
    {
        public T Create<T>() where T : Form
        {
            return ObjectFactory.GetInstance<T>();
        }
    }
}
در اینجا یک اینترفیس را تعریف کرده‌ایم که متد ایجاد وهله‌ا‌ی از یک فرم را ارائه می‌دهد. پیاده سازی آن در برنامه‌‌ای که از StructureMap استفاده می‌کند، مطابق کلاس FormFactory است. اگر IoC Container دیگری باشد، فقط باید این پیاده سازی را تغییر دهید و نه کل برنامه را. اکنون برای استفاده از آن، IFormFactory را در سازنده‌ی کلاسی که نیاز دارد فرم‌های دیگر را نمایش دهد، تزریق می‌کنیم:
using System;
using System.Windows.Forms;
using WinFormsIoc.IoC;

namespace WinFormsIoc
{
    public partial class Form1 : Form
    {
        private readonly IFormFactory _formFactory;
        public Form1(IFormFactory formFactory)
        {
            _formFactory = formFactory;
            InitializeComponent();
        }

        private void btnShowForm2_Click(object sender, EventArgs e)
        {
            var form2 = _formFactory.Create<Form2>();
            form2.Show();
        }
    }
}
در کدهای فوق، فرم اول برنامه را ملاحظه می‌کنید که قرار است فرم دوم را نمایش دهد. IFormFactory در سازنده‌ی آن تزریق شده‌است. با فراخوانی متد Create آن، فرم دوم برنامه به همراه تمام وابستگی‌های تزریق شده‌ی در سازنده‌ی آن وهله سازی می‌شوند.
نکته‌ی مهم این کدها عدم وابستگی مستقیم آن به هیچ نوع IoC Container خاصی است. این فرم اصلا نمی‌داند که IoC Container ایی در برنامه وجود دارد یا خیر.


مشکل! با تغییر سازنده‌ی Form1 برنامه دیگر کامپایل نمی‌شود!
اگر فایل Program.cs را باز کنید، یک چنین سطری را دارد:
 Application.Run(new Form1());
چون سازنده‌ی فرم یک، اکنون پارامتر جدیدی پیدا کرده‌است، در اینجا می‌توان ObjectFactory.GetInstance را مستقیما بکار برد (در این حالت خاص که مرتبط است به کلاس آغازین برنامه، با توجه به اینکه وهله سازی آن مستقیما و خارج از کنترل ما انجام می‌شود، دیگر چاره‌ای نداریم و مجبور هستیم از الگوی Service locator استفاده کنیم).
 Application.Run(ObjectFactory.GetInstance<Form1>());

مثال کامل این بحث را از اینجا می‌توانید دریافت کنید
WinFormsIoc.zip
مطالب
استفاده ازExpressionها جهت ایجاد Strongly typed view در ASP.NET MVC
مدل زیر را در نظر بگیرید:
/// <summary>
    /// 
    /// </summary>
    public class CompanyModel
    {
        /// <summary>
        /// Table Identity
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// Company Name
        /// </summary>
        [DisplayName("نام شرکت")]
        public string CompanyName { get; set; }

        /// <summary>
        /// Company Abbreviation
        /// </summary>
        [DisplayName("نام اختصاری شرکت")]
        public string CompanyAbbr { get; set; }
    
    }
از View زیر جهت نمایش لیستی از شرکت‌ها متناظر با مدل جاری استفاده میشود:
@{
    const string viewTitle = "شرکت ها";
    ViewBag.Title = viewTitle;
    const string gridName = "companies-grid";
}
<div class="col-md-12">
    <div class="form-panel">
        <header>
            <div class="title">
                <i class="fa fa-book"></i>
                @viewTitle
            </div>
        </header>
        <div class="panel-body">
            <div id="@gridName">
            </div>
        </div>
    </div>
</div>
</div>
@section scripts
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#@gridName").kendoGrid({
                dataSource: {
                    type: "json",
                    transport: {
                        read: {
                            url: "@Html.Raw(Url.Action(MVC.Company.CompanyList()))",
                            type: "POST",
                            dataType: "json",
                            contentType: "application/json"
                        }
                    },
                    schema: {
                        data: "Data",
                        total: "Total",
                        errors: "Errors"
                        }
                    },
                    pageSize: 10,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true
                },
                pageable: {
                    refresh: true
                },
                sortable: {
                    mode: "multiple",
                    allowUnsort: true
                },
                editable: false,
                filterable: false,
                scrollable: false,
                columns: [ {
                    field: "CompanyName",
                    title: "نام شرکت",
                    sortable: true,
                }, {
                    field: "CompanyAbbr",
                    title: "مخفف نام شرکت",
                    sortable: true
                }]
            });
        });
    </script>
}


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

 /// <summary>
    /// 
    /// </summary>
    public static class PropertyExtensions
    {
        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static MemberInfo GetMember<T>(this Expression<Func<T, object>> expression)
        {
            var mbody = expression.Body as MemberExpression;

            if (mbody != null) return mbody.Member;
            //This will handle Nullable<T> properties.
            var ubody = expression.Body as UnaryExpression;
            if (ubody != null)
            {
                mbody = ubody.Operand as MemberExpression;
            }
            if (mbody == null)
            {
                throw new ArgumentException("Expression is not a MemberExpression", "expression");
            }
            return mbody.Member;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string PropertyName<T>(this Expression<Func<T, object>> expression)
        {
            return GetMember(expression).Name;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static string PropertyDisplay<T>(this Expression<Func<T, object>> expression)
        {
            var propertyMember = GetMember(expression);
            var displayAttributes = propertyMember.GetCustomAttributes(typeof(DisplayNameAttribute), true);
            return displayAttributes.Length == 1 ? ((DisplayNameAttribute)displayAttributes[0]).DisplayName : propertyMember.Name;
        }
    }



public static string PropertyName<T>(this Expression<Func<T, object>> expression)
جهت بدست آوردن نام متغییر هایمان استفاده مینماییم.


public static string PropertyDisplay<T>(this Expression<Func<T, object>> expression)
جهت بدست آوردن DisplayNameAttribute استفده میشود. درصورتیکه این DisplayNameAttribute یافت نشود نام متغییر بازگشت داده میشود.

بنابراین View مربوطه را اینگونه بازنویسی میکنیم:

@using Models
@{
    const string viewTitle = "شرکت ها";
    ViewBag.Title = viewTitle;
    const string gridName = "companies-grid";
}
<div class="col-md-12">
    <div class="form-panel">
        <header>
            <div class="title">
                <i class="fa fa-book"></i>
                @viewTitle
            </div>
        </header>
        <div class="panel-body">
            <div id="@gridName">
            </div>
        </div>
    </div>
</div>
</div>
@section scripts
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#@gridName").kendoGrid({
                dataSource: {
                    type: "json",
                    transport: {
                        read: {
                            url: "@Html.Raw(Url.Action(MVC.Company.CompanyList()))",
                            type: "POST",
                            dataType: "json",
                            contentType: "application/json"
                        }
                    },
                    schema: {
                        data: "Data",
                        total: "Total",
                        errors: "Errors"
                        }
                    },
                    pageSize: 10,
                    serverPaging: true,
                    serverFiltering: true,
                    serverSorting: true
                },
                pageable: {
                    refresh: true
                },
                sortable: {
                    mode: "multiple",
                    allowUnsort: true
                },
                editable: false,
                filterable: false,
                scrollable: false,
                columns: [ {
                    field: "@(PropertyExtensions.PropertyName<CompanyModel>(a => a.CompanyName))",
                    title: "@(PropertyExtensions.PropertyDisplay<CompanyModel>(a => a.CompanyName))",
                    sortable: true,
                }, {
                    field: "@(PropertyExtensions.PropertyName<CompanyModel>(a => a.CompanyAbbr))",
                    title: "@(PropertyExtensions.PropertyDisplay<CompanyModel>(a => a.CompanyAbbr))",
                    sortable: true
                }]
            });
        });
    </script>
}


نظرات مطالب
دریافت خروجی سایت
سلام بابت این کار خیلی از شما سپاسگذارم.

بنده فکر کنم همان 1 درصد مرورگر safari  باشم که سایت شما را با ipad مرور میکنم

از طرفی بدلیل اینکه مدیریت فایل PDF در دستگاههای مانند ipad خیلی خسته کننده است به شخصه از CHM بیشتر استفاده میکنم و کاربر پسندتر است

در هر صورت چه فایل CHM و چه فایل PDF باشد خیلی از شما متشکریم

به گفته دوستان داشتن حتی فایل txt برخی مطالب این سایت بسیار ارزشمندتر است