مطالب
ذخیره سازی تنظیمات برنامه‌های ASP.NET Core در بانک اطلاعاتی به کمک Entity Framework Core
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config » با مقدمات کار با فایل‌های تنظیمات برنامه و تامین کننده‌های مختلف آن‌ها آشنا شدیم. در این مطلب قصد داریم یک نمونه‌ی سفارشی تامین کننده‌های تنظیمات برنامه را بر اساس دریافت و ذخیره سازی اطلاعات در بانک اطلاعاتی، تهیه کنیم.


ساختار موجودیت تنظیمات برنامه

تنظیمات برنامه با هر قالبی که تهیه شوند، دست آخر به صورت یک <Dictionary<string,string در برنامه پردازش شده و قابل دسترسی می‌شوند. بنابراین موجودیت معادل این Dictionary را به صورت زیر تعریف می‌کنیم:
namespace DbConfig.Web.DomainClasses
{
    public class ConfigurationValue
    {
        public int Id { get; set; }
        public string Key { get; set; }
        public string Value { get; set; }
    }
}


ساختار Context برنامه و مقدار دهی اولیه‌ی آن

پس از تعریف موجودیت تنظیمات برنامه، آن‌را به صورت زیر به Context برنامه معرفی می‌کنیم:
    public class MyAppContext : DbContext, IUnitOfWork
    {
        public MyAppContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<ConfigurationValue> Configurations { set; get; }
همچنین، برای مقدار دهی مقادیر اولیه‌ی تنظیمات برنامه نیز اینبار می‌توان به کمک متد HasData، به صورت زیر عمل کرد:
        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<ConfigurationValue>(entity =>
            {
                entity.Property(e => e.Key).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Key).IsUnique();
                entity.Property(e => e.Value).IsRequired();
                entity.HasData(new ConfigurationValue
                {
                    Id = 1,
                    Key = "key-1",
                    Value = "value_from_ef_1"
                });
                entity.HasData(new ConfigurationValue
                {
                    Id = 2,
                    Key = "key-2",
                    Value = "value_from_ef_2"
                });
            });
        }

ایجاد یک IConfigurationSource سفارشی مبتنی بر بانک اطلاعاتی

انواع و اقسام تامین کننده‌های تنظیمات برنامه در پروژه‌های ASP.NET Core، در حقیقت یک پیاده سازی سفارشی از اینترفیس IConfigurationSource هستند. به همین جهت در ادامه یک نمونه‌ی مبتنی بر EF Core آن را تهیه می‌کنیم:
    public class EFConfigurationSource : IConfigurationSource
    {
        private readonly IServiceProvider _serviceProvider;

        public EFConfigurationSource(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new EFConfigurationProvider(_serviceProvider);
        }
    }
در اینجا چون می‌خواهیم به IUnitOfWork دسترسی پیدا کنیم، IServiceProvider را به سازنده‌ی این تامین کننده تزریق کرده‌ایم. کار اصلی ساخت آن نیز در متد Build، با ارائه‌ی یک IConfigurationProvider سفارشی انجام می‌شود. اینجا است که اطلاعات را از بانک اطلاعاتی خوانده و در اختیار سیستم تنظیمات برنامه قرار می‌دهیم:
    public class EFConfigurationProvider : ConfigurationProvider
    {
        private readonly IServiceProvider _serviceProvider;

        public EFConfigurationProvider(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
            ensureDatabaseIsCreated();
        }

        public override void Load()
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
                this.Data?.Clear();
                this.Data = uow.Set<ConfigurationValue>()
                               .AsNoTracking()
                               .ToList()
                               .ToDictionary(c => c.Key, c => c.Value);
            }
        }

        private void ensureDatabaseIsCreated()
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
                uow.Migrate();
            }
        }
    }
در ConfigurationProvider فوق، متد Load، در آغاز برنامه فراخوانی شده و در اینجا فرصت داریم تا خاصیت this.Data آن‌را که از نوع <Dictionary<string,string است، مقدار دهی کنیم. بنابراین از serviceProvider تزریق شده‌ی در سازنده‌ی کلاس استفاده کرده و به وهله‌ای از IUnitOfWork دسترسی پیدا می‌کنیم. سپس بر این اساس تمام رکوردهای جدول متناظر با ConfigurationValue را دریافت و توسط متد ToDictionary، تبدیل به ساختار مدنظر خاصیت this.Data می‌کنیم.
در اینجا فراخوانی متد ensureDatabaseIsCreated را نیز مشاهده می‌کنید. کلاس EFConfigurationProvider در آغاز برنامه و پیش از هر عمل دیگری وهله سازی شده و سپس متد Load آن فراخوانی می‌شود. به همین جهت نیاز است یا پیشتر، بانک اطلاعاتی را توسط دستورات Migration ایجاد کرده باشید و یا متد ensureDatabaseIsCreated، اطلاعات Migration موجود را به بانک اطلاعاتی برنامه اعمال می‌کند.


معرفی EFConfigurationSource به برنامه

جهت معرفی ساده‌تر EFConfigurationSource تهیه شده، ابتدا یک متد الحاقی را بر اساس آن تهیه می‌کنیم:
    public static class EFExtensions
    {
        public static IConfigurationBuilder AddEFConfig(this IConfigurationBuilder builder,
            IServiceProvider serviceProvider)
        {
            return builder.Add(new EFConfigurationSource(serviceProvider));
        }
    }
سپس می‌توان این متد AddEFConfig را به صورت زیر به تنظیمات برنامه در کلاس Startup اضافه و معرفی کرد:
namespace DbConfig.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, MyAppContext>();
            services.AddScoped<IConfigurationValuesService, ConfigurationValuesService>();

            var connectionString = Configuration.GetConnectionString("SqlServerConnection")
                     .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data"));
            services.AddDbContext<MyAppContext>(options =>
                    {
                        options.UseSqlServer(
                            connectionString,
                            dbOptions =>
                                {
                                    var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                                    dbOptions.CommandTimeout(minutes);
                                    dbOptions.EnableRetryOnFailure();
                                });
                    });

            var serviceProvider = services.BuildServiceProvider();
            var configuration = new ConfigurationBuilder()
                                       .AddConfiguration(Configuration) // Adds all of the existing configurations
                                       .AddEFConfig(serviceProvider)
                                       .Build();
            services.AddSingleton<IConfigurationRoot>(sp => configuration); // Replace
            services.AddSingleton<IConfiguration>(sp => configuration); // Replace
در اینجا ابتدا نیاز است یک ConfigurationBuilder جدید را ایجاد کنیم تا بتوان AddEFConfig را بر روی آن فراخوانی کرد. در این بین، خود برنامه نیز تعدادی تامین کننده‌ی تنظیمات پیش‌فرض را نیز دارد که قصد نداریم سبب پاک شدن آن‌ها شویم. به همین جهت آن‌ها را توسط متد AddConfiguration، افزوده‌ایم. پس از تعریف این ConfigurationBuilder جدید، نیاز است آن‌را جایگزین IConfiguration و IConfigurationRoot پیش‌فرض برنامه کنیم که روش آن‌را در دو متد services.AddSingleton ملاحظه می‌کنید.
همچنین روش دسترسی به serviceProvider مورد نیاز AddEFConfig، توسط متد services.BuildServiceProvider نیز در کدهای فوق مشخص است. به همین جهت مجبور شدیم این تعریف را در اینجا قرار دهیم و گرنه می‌شد از کلاس Program و یا حتی سازنده‌ی کلاس Startup نیز استفاده کرد. مشکل این دو مکان عدم دسترسی به سرویس IUnitOfWork و سایر تنظیمات برنامه است.


آزمایش برنامه

اگر به قسمت «ساختار Context برنامه و مقدار دهی اولیه‌ی آن» مطلب جاری دقت کرده باشید، دو کلید پیش‌فرض در اینجا ثبت شده‌اند. به همین جهت در ادامه با تزریق سرویس IConfiguration به سازنده‌ی یک کنترلر، سعی در خواندن مقادیر آن‌ها خواهیم کرد:
namespace DbConfig.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IConfiguration _configuration;

        public HomeController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public IActionResult Index()
        {
            return Json(
                new
                {
                    key1 = _configuration["key-1"],
                    key2 = _configuration["key-2"]
                });
        }
با این خروجی:



به روز رسانی بانک اطلاعاتی برنامه و بارگذاری مجدد اطلاعات IConfiguration

فرض کنید توسط سرویسی، اطلاعات جدول ConfigurationValue را تغییر داده‌اید. نکته‌ی مهم اینجا است که اینکار سبب فراخوانی مجدد متد Load کلاس EFConfigurationProvider نخواهد شد و عملا این تغییرات در سراسر برنامه توسط تزریق اینترفیس IConfiguration قابل دسترسی نخواهند بود (مگر اینکه برنامه مجددا ری‌استارت شود). نکته‌ی به روز رسانی این اطلاعات به صورت زیر است:
    public class ConfigurationValuesService : IConfigurationValuesService
    {
        private readonly IConfiguration _configuration;

        public ConfigurationValuesService(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        private void reloadEFConfigurationProvider()
        {
            ((IConfigurationRoot)_configuration).Reload();
        }
در جائیکه نیاز است پس از به روز رسانی بانک اطلاعاتی، تنظیمات برنامه را نیز بارگذاری مجدد کنید، ابتدا اینترفیس IConfiguration را به سازنده‌ی آن تزریق کرده و سپس به نحو فوق، متد Reload را فراخوانی کنید. اینکار سبب می‌شود تا یکبار دیگری متد Load کلاس EFConfigurationProvider نیز فراخوانی شود که باعث بارگذاری مجدد تنظیمات برنامه خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreDbConfig.zip
مطالب
نکاتی در مورد استفاده از توابع تجمعی در Entity framework
استفاده از Aggregate functions یا توابع تجمعی چه در زمان SQL نویسی مستقیم و یا در حالت استفاده از LINQ to Entities نیاز به ملاحظات خاصی دارد که عدم رعایت آن‌ها سبب کرش برنامه در زمان موعد خواهد شد. در ادامه تعدادی از این موارد را مرور خواهیم کرد.

ابتدا مدل‌های برنامه را در نظر بگیرید که از یک صورتحساب، به همراه ریز قیمت‌های آیتم‌های مرتبط با آن تشکیل شده است:
    public class Bill
    {
        public int Id { set; get; }
        public string Name { set; get; }

        public virtual ICollection<Transaction> Transactions { set; get; }
    }

    public class Transaction
    {
        public int Id { set; get; }
        public DateTime AddDate { set; get; }
        public int Amount { set; get; }

        [ForeignKey("BillId")]
        public virtual Bill Bill { set; get; }
        public int BillId { set; get; }
    }
در ادامه این کلاس‌ها را در معرض دید EF Code first قرار می‌دهیم:
    public class MyContext : DbContext
    {
        public DbSet<Bill> Bills { get; set; }
        public DbSet<Transaction> Transactions { get; set; }
    }
همچنین تعدادی رکورد اولیه را نیز جهت انجام آزمایشات به بانک اطلاعاتی متناظر، اضافه خواهیم کرد:
    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            var bill1 = new Bill { Name = "bill-1" };
            context.Bills.Add(bill1);

            for (int i = 0; i < 11; i++)
            {
                context.Transactions.Add(new Transaction
                {
                    AddDate = DateTime.Now.AddDays(-i),
                    Amount = 1000000000 + i,
                    Bill = bill1
                });
            }
            base.Seed(context);
        }
    }
در اینجا به عمد از ارقام بزرگ استفاده شده است تا نمایانگر عملکرد یک سیستم واقعی در طول زمان باشد.


اولین مثال: یک جمع ساده

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            using (var context = new MyContext())
            {
                var sum = context.Transactions.Sum(x => x.Amount);
                Console.WriteLine(sum);
            }
        }
    }
ساده‌ترین نیازی را که در اینجا می‌توان مدنظر داشت، جمع کل تراکنش‌‌های سیستم است. به نظر شما خروجی کوئری فوق چیست؟
خروجی SQL کوئری فوق به نحو زیر است:
SELECT 
         [GroupBy1].[A1] AS [C1]
         FROM ( SELECT 
                    SUM([Extent1].[Amount]) AS [A1]
                    FROM [dbo].[Transactions] AS [Extent1]
                    )  AS [GroupBy1]
و خروجی واقعی آن استثنای زیر می‌باشد:
 Arithmetic overflow error converting expression to data type int.
بله. محاسبه ممکن نیست؛ چون جمع حاصل از بازه اعداد صحیح خارج شده است.

راه حل:
نیاز است جمع را بر روی Int64 بجای Int32 انجام دهیم:
var sum2 = context.Transactions.Sum(x => (Int64)x.Amount);

SELECT 
      [GroupBy1].[A1] AS [C1]
         FROM ( SELECT 
                    SUM( CAST( [Extent1].[Amount] AS bigint)) AS [A1]
                    FROM [dbo].[Transactions] AS [Extent1]
               )  AS [GroupBy1]                  


مثال دوم: سیستم باید بتواند با نبود رکوردها نیز صحیح کار کند
برای نمونه کوئری زیر را بر روی بازه‌ا‌ی که سیستم عملکرد نداشته است، در نظر بگیرید:
var date = DateTime.Now.AddDays(10);
var sum3 = context.Transactions
                  .Where(x => x.AddDate > date)  
                  .Sum(x => (Int64)x.Amount);               
یک چنین خروجی SQL ایی دارد:
SELECT 
     [GroupBy1].[A1] AS [C1]
        FROM ( SELECT 
                    SUM( CAST( [Extent1].[Amount] AS bigint)) AS [A1]
                    FROM [dbo].[Transactions] AS [Extent1]
                    WHERE [Extent1].[AddDate] > @p__linq__0
              )  AS [GroupBy1]
اما در سمت کدهای ما با خطای زیر متوقف می‌شود:
The cast to value type 'Int64' failed because the materialized value is null.
Either the result type's generic parameter or the query must use a nullable type.
راه حل: استفاده از نوع‌های nullable در اینجا ضروری است:
var date = DateTime.Now.AddDays(10);
var sum3 = context.Transactions
                  .Where(x => x.AddDate > date)
                  .Sum(x => (Int64?)x.Amount) ?? 0;
به این ترتیب، خروجی صفر را بدون مشکل، دریافت خواهیم کرد.

مثال سوم: حالت‌های خاص استفاده از خواص راهبری
کوئری زیر را در نظر بگیرید:
 var sum4 = context.Bills.First().Transactions.Sum(x => (Int64?)x.Amount) ?? 0;
در اینجا قصد داریم جمع تراکنش‌های صورتحساب اول را بدست بیاوریم که از طریق استفاده از خاصیت راهبری Transactions کلاس Bill، به نحو فوق میسر شده است. به نظر شما خروجی SQL آن به چه صورتی است؟
SELECT 
     [Extent1].[Id] AS [Id], 
     [Extent1].[AddDate] AS [AddDate], 
     [Extent1].[Amount] AS [Amount], 
     [Extent1].[BillId] AS [BillId]
   FROM [dbo].[Transactions] AS [Extent1]
   WHERE [Extent1].[BillId] = @EntityKeyValue1
بله! در اینجا خبری از Sum نیست. ابتدا کل اطلاعات دریافت شده و سپس جمع و منهای نهایی در سمت کلاینت بر روی آن‌ها انجام می‌شود؛ که بسیار ناکارآمد است. (قرار است این مورد ویژه، در نگارش‌های بعدی بهبود یابد)
راه حل کنونی:
var entry = context.Bills.First();
var sum5 = context.Entry(entry).Collection(x => x.Transactions).Query().Sum(x => (Int64?)x.Amount) ?? 0;
در اینجا باید از روش خاصی که مشاهده می‌کنید جهت کار با خواص راهبری استفاده کرد و نکته اصلی آن استفاده از متد Query است. حاصل کوئری LINQ فوق اینبار SQL مطلوب زیر است که سمت سرور عملیات جمع را انجام می‌دهد و نه سمت کلاینت:
SELECT 
    [GroupBy1].[A1] AS [C1]
     FROM ( SELECT 
               SUM( CAST( [Extent1].[Amount] AS bigint)) AS [A1]
                   FROM [dbo].[Transactions] AS [Extent1]
                    WHERE [Extent1].[BillId] = @EntityKeyValue1
            )  AS [GroupBy1]                  


نکاتی که در اینجا ذکر شدند در مورد تمام توابع تجمعی مانند Sum، Count، Max و Min و غیره صادق هستند و باید به آن‌ها نیز دقت داشت.
نظرات مطالب
سفارشی کردن ASP.NET Identity در MVC 5

سلام...ممنون از پاسختون.

با توجه به راهنمایی شما در قسمت context در لایه data layer بدین صورت درج کردم

public class Context : DbContext
    {
        public DbSet<IdentityUserClaim> AspNetUserClaims { set; get; }
        public DbSet<IdentityRole> AspNetRoles { set; get; }
        public DbSet<IdentityUserLogin> AspNetUserLogins { set; get; }
        public DbSet<IdentityUserRole> AspNetUserRoles { set; get; }
        public DbSet<IdentityUser> AspNetUsers { set; get; }
        //--------------------------------------
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id).ToTable("AspNetRoles");
            modelBuilder.Entity<IdentityUser>().ToTable("AspNetUsers");
            modelBuilder.Entity<IdentityUserLogin>().HasKey(l => new { l.UserId, l.LoginProvider, l.ProviderKey }).ToTable("AspNetUserLogins");
            modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId }).ToTable("AspNetUserRoles");
            modelBuilder.Entity<IdentityUserClaim>().ToTable("AspNetUserClaims");
        }
    }
ولی وقتی سایت اجرا می‌شود ایراد زیر نمایش داده می‌شود

بازخوردهای دوره
تزریق وابستگی‌های AutoMapper در لایه سرویس برنامه
من تنظیمات تزریق وابستگی مربوط به AutoMapper را در همان محل قرارگیری تزریق وابستگی Serviceها قرار داده ام و ObjectFactory به این شکل شد:
public static class WoObjectFactory
    {
        private static readonly Lazy<Container> ContainerBuilder =
           new Lazy<Container>(DefaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);

        public static IContainer Container
        {
            get { return ContainerBuilder.Value; }
        }

        private static Container DefaultContainer()
        {
            
            var _container = new Container(x =>
            {
                var platformSpecificRegistry = PlatformAdapter.Resolve<IPlatformSpecificMapperRegistry>();
                platformSpecificRegistry.Initialize();

                x.For<ConfigurationStore>().Singleton().Use<ConfigurationStore>().Ctor<IEnumerable<IObjectMapper>>().Is(MapperRegistry.Mappers);
                x.For<IConfigurationProvider>().Use(ctx => ctx.GetInstance<ConfigurationStore>());
                x.For<IConfiguration>().Use(ctx => ctx.GetInstance<ConfigurationStore>());
                x.For<ITypeMapFactory>().Use<TypeMapFactory>();
                x.For<IMappingEngine>().Singleton().Use<MappingEngine>().SelectConstructor(() => new MappingEngine(null));

                x.For<IUnitOfWork>().HybridHttpOrThreadLocalScoped().Use(() => new WirelessOrganizationContext());

                x.Scan(scan =>
                {
                    scan.AssemblyContainingType<IDeviceService>();
                    scan.TheCallingAssembly();
                    scan.WithDefaultConventions();
                });
                x.Scan(scanner =>
                {
                    scanner.AssembliesFromApplicationBaseDirectory();

                    scanner.ConnectImplementationsToTypesClosing(typeof(ITypeConverter<,>))
                           .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped());

                    scanner.ConnectImplementationsToTypesClosing(typeof(ValueResolver<,>))
                        .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped());
                });

                x.Scan(scan =>
                {
                    scan.TheCallingAssembly();
                    scan.WithDefaultConventions();
                    scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName);
                });

                

            });
            ConfigureAutoMapper(_container);
            return _container;
        }

        private static void ConfigureAutoMapper(IContainer container)
        {
            var configuration = container.TryGetInstance<IConfiguration>();
            if (configuration == null) return;
            //saying AutoMapper how to resolve services
            configuration.ConstructServicesUsing(container.GetInstance);
            foreach (var profile in container.GetAllInstances<Profile>())
            {
                configuration.AddProfile(profile);
            }
        }
    }
اما وقتی پروژه اجرا می‌شود اکسپشن زیر اتفاق می‌افتد، که البته بعد از اضافه کردن تنظیمات مربوط به تزریق وابستگی AutoMapper اتفاق افتاد، در این Value مربوط به ContainerBuilder  مقداری ندارد:

مطالب
مروری کوتاه بر کارکرد Ocelot

با پیشرفت بیشتر تکنولوژی وب در سال‌های اخیر و رشد کاربران فضای اینترنتی، خدمات و پیچیدگی‌های بیشتری به نرم افزارها اضافه شده و به همین دلیل استفاده از میکروسرویس‌ها بجای حالت قدیمی مونولوتیک (یک برنامه همه کاره) طرفداران بیشتری پیدا کرد‌ه‌است. در این حالت برنامه به قسمت‌های خرد و مجزایی تبدیل شده و هر پروژه ساختار و تکنولوژی مخصوص به خود را مدیریت میکند و در این بین با استفاده روش‌های متفاوتی به ایجاد ارتباط با یکدیگر میپردازند .  

مشکلی که در این حالت میتواند رخ دهد، زیاد شدن مسیرهای متفاوت برای اتصال به هر یک از سرویس‌ها و سخت‌تر شدن به روزرسانی این مسیرها می‌باشد. به همین دلیل در این بخش، نیاز به ابزاری میباشد تا بتوان از طریق آن، مسیردهی ساده‌ای را ایجاد کرد و در پشت صحنه  مسیردهی‌های متفاوتی را کنترل نمود. با ایجاد چنین ابزاری در واقع شما   API Gateway ایجاد نموده‌اید. یکی از معروفترین کتابخانه‌های این حوزه، Ocelot میباشد. کار با این ابزار بسیار ساده بوده و امکانات بسیار زیاد و قدرتمندی را فراهم مینماید.

برای اینکار ابتدا سه پروژه را می‌سازیم که موارد زیر را شامل می‌گردد:

پروژه اول نوع Api : با دریافت Id در اکشن‌متد مورد نظر، شیء user بازگردانده میشود:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string UserName { get; set; }


    public static List<User> GetUsers()
    {
        return new List<User>()
        {
            new()
            {
                Id = 1,
                FirstName = "علی",
                LastName = "یگانه مقدم",
                UserName = "yeganehaym"
            },
            new ()
            {
                Id = 2,
                FirstName = "وحید",
                LastName = "نصیری",
                UserName = "VahidN"
            },
        };
    }
}
[ApiController]
[Route("/api/[controller]/{id?}")]
public class UserController : ControllerBase
{

    [HttpGet]
    public User GetUser(int id)
    {
        var users = Users.User.GetUsers();
        var user = users.FirstOrDefault(x => x.Id == id);
        return user;
    }
}

 

پروژه دوم نوع Api : دریافت لیستی از محصولات:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
    public  int Quantity { get; set; }


    public static List<Product> GetProducts()
    {
        return new List<Product>()
        {
            new()
            {
                Id = 1,
                Name = "LCD",
                Price = 20000,
                Quantity = 10
            },
            new()
            {
                Id = 1,
                Name = "Mouse",
                Price = 320000,
                Quantity = 15
            },
            new()
            {
                Id = 1,
                Name = "Keyboard",
                Price = 50000,
                Quantity = 25
            },
        };
    }
}
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{

    [HttpGet]
    public List<Product> GetProducts()
    {
        return Product.GetProducts();
    }

}


پروژه سوم همان ApiGateway هست و همین‌که یک پروژه‌ی وب خالی باشد، کفایت میکند. در این پروژه   Ocelot  را نصب نموده  و سپس فایلی با نام  ocelot.json را با محتوای زیر به ریشه‌ی پروژه همانند فایل‌های appsettings.json اضافه میکنیم:

{
    "Routes":[
        
        {
        "DownstreamPathTemplate":"/api/User/{id}",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7279"
            }
        ],
        "UpstreamPathTemplate":"/GetUser/{id}",
        "UpstreamHttpMethod":[
            "GET"
        ]},
        {
        "DownstreamPathTemplate":"/api/Product",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7261"
            }
        ],
     
        "UpstreamPathTemplate":"/Products",
        "UpstreamHttpMethod":[
            "GET"
        ]
        }
    ]
    
   
}

این فایل‌ها شامل دو قسمتUpStream و DownStream میشوند. آپ‌استریم‌ها در واقع آدرسی است که شما قصد اتصال به آن‌را دارید و قسمت داون‌استریم، سرویس مقصدی است که ocelot باید درخواست شما را به سمت آن ارسال نماید. به‌عنوان مثل شما با ارسال درخواستی به آدرس Products ، در پشت صحنه به آدرس localhost:7261/api/product ارسال میگردد. بدین صورت سیستم نهایی تنها به یک دامنه و آدرس منسجم ارسال شده، ولی در پشت صحنه این آدرس‌ها ممکن است به تعداد زیادی سرویس در آدرس‌های متفاوتی ارسال گردند.

جهت راه اندازی نهایی، کد زیر را به فایل Program.cs اضافه میکنیم:

builder.Services.AddOcelot();
app.UseOcelot();


پس از اضافه کردن پیکربندی و middleware آن، کد زیر را نیز جهت شناسایی فایل ocelot به فایل Program.cs نیز اضافه مینماییم:

builder.Configuration.SetBasePath(builder.Environment.ContentRootPath)    
    .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);

همچنین در صورت تمایل میتوانید کد را به شکل زیر هم نوشته تا بتوانید تنظیمات متفاوتی را برای محیط اجرایی متفاوتی ایجاد نمایید:

builder.Configuration.SetBasePath(builder.Environment.ContentRootPath)    
    .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"ocelot.{builder.Environment.EnvironmentName}.json", optional: false, reloadOnChange: true);

هر سه برنامه را با هم اجرا نمایید و با استفاده از برنامه‌ی PostMan درخواستی را برای هر یک از موارد مورد نظر /Products و /GetUser/{1,2} به سمت پروژه ApiGateway ارسال نمایید.

Ocelot موارد دیگری از قبیل تنظیم Load Balancer بین سرویس ها، اتصال به سرویس‌های Service Discoveryچون Consul   یا  یوریکا  و کش کردن و ... را نیز فراهم می‌نماید.


عملیات کشینگ

جهت بحث کشینگ، ابتدا بسته زیر را اضافه نمایید:

Install-Package Ocelot.Cache.CacheManager

سپس پیکربندی ابتدایی را به شکل زیر تغییر دهید:

builder.Services.AddOcelot()
    .AddCacheManager(x => x.WithDictionaryHandle());

در ادامه در فایل Ocelot جیسون، برای هر بخشی که مدنظر شماست تا کشی را انجام دهد، کد زیر اضافه نمایید:

"FileCacheOptions":{
      "TtlSeconds":30,
       "Region":"custom"
}

TtlSeconds : مدت زمان کش به ثانیه

Region : یک عبارت رشته‌ای همانند یک عنوان یا نام که بعدا میتوانید از طریق api ‌ها به آن متصل شوید و عملیاتی چون خالی کردن کش را صادر نمایید.

حال برای بخش محصولات این تنظیمات ذکر میگردد:

{
    "Routes":[
        
        {
        "DownstreamPathTemplate":"/api/User/{id}",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7279"
            }
        ],
        "UpstreamPathTemplate":"/GetUser/{id}",
        "UpstreamHttpMethod":[
            "GET"
        ]
        },
        {
        "DownstreamPathTemplate":"/api/Product",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7261"
            }
        ],
     
        "UpstreamPathTemplate":"/Products",
        "UpstreamHttpMethod":[
            "GET"
        ],
            "FileCacheOptions":{
                "TtlSeconds":30,
                "Region":"custom"
            }
        }
    ]
    
   
}

 برای اینکه متوجه عملکرد آن شوید یک نقطه توقف را در اکشن دریافت محصول قرار دهید و سپس برنامه را در حالت دیباگ اجرا نمایید. در مرتبه اول باید نقطه توقف بتواند اجرای کد را به شما نمایش دهد ولی تا 30 ثانیه آینده هر چقدر از طریق Postman درخواستی را ارسال نمایید نقطه توقف اجرا نخواهد گردید، ولی نتیجه‌ی قبل برای شما ارسال خواهد شد.

این مورد را برای بخش کاربران هم انجام دهید و می‌بینید که برای هر userId و هر شکل  Url، یک پاسخ منحصر به فرد، دریافت و کش خواهد شد.


جلوگیری از درخواست‌های بیش از حد

یکی دیگر از ویژگی‌های Ocelot، جلوگیری از درخواست بیش از حد میباشد. به همین علت ابتدا کد زیر را به هر درخواستی که مدنظر شماست اضافه نمایید:

       "RateLimitOptions":{
                "ClientWhitelist":[
                ],
                "EnableRateLimiting":true,
                "Period":"5s",
                "PeriodTimespan":1,
                "Limit":1,
                "HttpStatusCode":429
            }


WhiteClients : برای مشخص کردن کلاینت‌هایی که نباید اعمال محدودیت روی آن‌ها صورت بگیرد.

EnableRateLimiting   : این مورد باعث فعالسازی آن میگردد.

Period: مدت زمانیکه حداکثر تعداد درخواست باید در آن بازه صورت بگیرد. به ترتیب برای ثانیه، دقیقه، ساعت و روز حروف s - m - h و d استفاده میگردد.

PeriodTimespan: بعد از محدود شدن، بعد از چه مدتی دوباره بتواند درخواستی را ارسال نماید. در اینجا بعد از محدودیت ارسال درخواست، بعد از یک ثانیه مجدد اجازه ارسال درخواست باز میگردد.

Limit: در بازه زمانی مشخص شده چند درخواست مورد قبول واقع میشود و بعد از آن دیگر اجازه ارسال درخواست را نخواهد داشت.

HttpStatusCode: در صورت فیلتر شدن درخواست‌های رسیده، چه کد وضعیتی باید برگردانده شود که عدد 429 به معنای Too Many Request میباشد.

با تنظیمات بالا هر کلاینت میتواند در 5 ثانیه، نهایتا یک درخواست را ارسال نماید و با ارسال بقیه درخواست‌ها، Ocelot بجای هدایت درخواست به سرویس مربوطه، کد وضعیت 429 را باز میگرداند و یک ثانیه بعد از گذشت 5 ثانیه میتواند مجددا درخواست خود را ارسال نماید.

در نهایت به یک فایل مشابه زیر می‌رسیم:

{
    "Routes":[
        
        {
        "DownstreamPathTemplate":"/api/User/{id}",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7279"
            }
        ],
        "UpstreamPathTemplate":"/GetUser/{id}",
        "UpstreamHttpMethod":[
            "GET"
        ],
        "FileCacheOptions":{
            "TtlSeconds":30,
            "Region":"custom"
        }
        },
        {
        "DownstreamPathTemplate":"/api/Product",
        "DownstreamScheme":"https",
        "DownstreamHostAndPorts":[
            {
                "Host":"localhost",
                "Port":"7261"
            }
        ],
     
        "UpstreamPathTemplate":"/Products",
        "UpstreamHttpMethod":[
            "GET"
        ],
            "RateLimitOptions":{
                "ClientWhitelist":[
                ],
                "EnableRateLimiting":true,
                "Period":"5s",
                "PeriodTimespan":1,
                "Limit":1,
                "HttpStatusCode":429
            }
        }
    ],
    "DangerousAcceptAnyServerCertificateValidator": true
    
   
}

برای تست آن با استفاد از PostMan مرتبا به آدرس Products/ درخواست ارسال نمایید. 

فایل پروژه : Ocelot.zip

مطالب
به روز رسانی ساده‌تر اجزاء ارتباطات در EF Code first به کمک GraphDiff
دو نوع حالت کلی کارکردن با EF وجود دارند: متصل و منقطع.
در حالت متصل مانند برنامه‌های متداول دسکتاپ، Context مورد استفاده در طول عمر صفحه‌ی جاری زنده نگه داشته می‌شود. در این حالت اگر شیءایی اضافه شود، حذف شود یا تغییر کند، توسط EF ردیابی شده و تنها با فراخوانی متد SaveChanges، تمام این تغییرات به صورت یکجا به بانک اطلاعاتی اعمال می‌شوند.
در حالت غیرمتصل مانند برنامه‌های وب، طول عمر Context در حد طول عمر یک درخواست است. پس از آن از بین خواهد رفت و دیگر فرصت ردیابی تغییرات سمت کاربر را نخواهد یافت. در این حالت به روز رسانی کلیه تغییرات انجام شده در خواص و همچنین ارتباطات اشیاء موجود، کاری مشکل و زمانبر خواهد بود.
برای حل این مشکل، کتابخانه‌ای به نام GraphDiff طراحی شده‌است که صرفا با فراخوانی متد UpdateGraph آن، به صورت خودکار، محاسبات تغییرات صورت گرفته در اشیاء منقطع و اعمال آن‌ها به بانک اطلاعاتی صورت خواهد گرفت. البته ذکر متد SaveChanges پس از آن نباید فراموش شود.


اصطلاحات بکار رفته در GraphDiff

برای کار با GraphDiff نیاز است با یک سری اصطلاح آشنا بود:

Aggregate root
گرافی است از اشیاء به هم وابسته که مرجع تغییرات داده‌ها به شمار می‌رود. برای مثال یک سفارش و آیتم‌های آن‌را درنظر بگیرید. بارگذاری آیتم‌های سفارش، بدون سفارش معنایی ندارند. بنابراین در اینجا سفارش aggregate root است.

AssociatedCollection/AssociatedEntity
حالت‌های Associated به GraphDiff اعلام می‌کنند که اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root نباید به روز رسانی شوند. در این حالت تنها ارجاعات به روز رسانی خواهند شد.
اگر خاصیت راهبری از نوع ICollection است، حالت AssociatedCollection و اگر صرفا یک شیء ساده است، از AssociatedEntity استفاده خواهد شد.

OwnedCollection/OwnedEntity
حالت‌های Owned به GraphDiff اعلام می‌کنند که جزئیات و همچنین ارجاعات اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root باید به روز رسانی شوند.


دریافت و نصب GraphDiff

برای نصب خودکار کتابخانه‌ی GraphDiff می‌توان از دستور نیوگت ذیل استفاده کرد:
 PM> Install-Package RefactorThis.GraphDiff


بررسی GraphDiff در طی یک مثال

مدل‌های برنامه آزمایشی، از سه کلاس ذیل که روابط many-to-many و one-to-many با یکدیگر دارند، تشکیل شده‌است:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace GraphDiffTests.Models
{
    public class BlogPost
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public virtual ICollection<Tag> Tags { set; get; } // many-to-many

        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int UserId { get; set; }

        public BlogPost()
        {
            Tags = new List<Tag>();
        }
    }

    public class Tag
    {
        public int Id { set; get; }

        [StringLength(maximumLength: 450), Required]
        public string Name { set; get; }

        public virtual ICollection<BlogPost> BlogPosts { set; get; } // many-to-many

        public Tag()
        {
            BlogPosts = new List<BlogPost>();
        }
    }

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; } // one-to-many
    }
}
- یک مطلب می‌تواند چندین برچسب داشته باشد و هر برچسب می‌تواند به چندین مطلب انتساب داده شود.
- هر کاربر می‌تواند چندین مطلب ارسال کند.

در این حالت، Context برنامه چنین شکلی را خواهد یافت:
using System;
using System.Data.Entity;
using GraphDiffTests.Models;

namespace GraphDiffTests.Config
{
    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<BlogPost> BlogPosts { get; set; }
        public DbSet<Tag> Tags { get; set; }


        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
}
به همراه تنظیمات به روز رسانی ساختار بانک اطلاعاتی به صورت خودکار:
using System.Data.Entity.Migrations;
using System.Linq;
using GraphDiffTests.Models;

namespace GraphDiffTests.Config
{
    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            if(context.Users.Any())
                return;

            var user1 = new User {Name = "User 1"};
            context.Users.Add(user1);

            var tag1 = new Tag { Name = "Tag1" };
            context.Tags.Add(tag1);

            var post1 = new BlogPost { Title = "Title...1", Content = "Content...1", User = user1};
            context.BlogPosts.Add(post1);

            post1.Tags.Add(tag1);

            base.Seed(context);
        }
    }
}
در متد Seed آن یک سری اطلاعات ابتدایی ثبت شده‌اند؛ یک کاربر، یک برچسب و یک مطلب.




در این تصاویر به Id هر کدام از رکوردها دقت کنید. از آن‌ها در ادامه استفاده خواهیم کرد.
در اینجا نمونه‌ای از نحوه‌ی استفاده از GraphDiff را جهت به روز رسانی یک Aggregate root ملاحظه می‌کنید:
            using (var context = new MyContext())
            {
                var user1 = new User { Id = 1, Name = "User 1_1_1" };
                var post1 = new BlogPost { Id = 1, Title = "Title...1_1", Content = "Body...1_1",
                    User = user1, UserId = user1.Id };
                var tags = new List<Tag>
                {
                    new Tag {Id = 1, Name = "Tag1_1"},
                    new Tag {Id=12, Name = "Tag2_1"},
                    new Tag {Name = "Tag3"},
                    new Tag {Name = "Tag4"},
                };
                tags.ForEach(tag => post1.Tags.Add(tag));

                context.UpdateGraph(post1, map => map
                    .OwnedEntity(p => p.User)
                    .OwnedCollection(p => p.Tags)
                    );

                context.SaveChanges();
            }
پارامتر اول UpdateGraph، گرافی از اشیاء است که قرار است به روز رسانی شوند.
پارامتر دوم آن، همان مباحث Owned و Associated بحث شده در ابتدای مطلب را مشخص می‌کنند. در اینجا چون می‌خواهیم هم برچسب‌ها و هم اطلاعات کاربر مطلب اول به روز شوند، نوع رابطه را Owned تعریف کرده‌ایم.
در حین کار با متد UpdateGraph، ذکر Idهای اشیاء منقطع از Context بسیار مهم هستند. اگر دستورات فوق را اجرا کنیم به خروجی ذیل خواهیم رسید:




- همانطور که مشخص است، چون id کاربر ذکر شده و همچنین این Id در post1 نیز درج گردیده است، صرفا نام او ویرایش گردیده است. اگر یکی از موارد ذکر شده رعایت نشوند، ابتدا کاربر جدیدی ثبت شده و سپس رابطه‌ی مطلب و کاربر به روز رسانی خواهد شد (userId آن به userId آخرین کاربر ثبت شده تنظیم می‌شود).
- در حین ثبت برچسب‌ها، چون Id=1 از پیش در بانک اطلاعاتی موجود بوده، تنها نام آن ویرایش شده‌است. در سایر موارد، برچسب‌های تعریف شده صرفا اضافه شده‌اند (چون Id مشخصی ندارند یا Id=12 در بانک اطلاعاتی وجود خارجی ندارد).
- چون Id مطلب مشخص شده‌است، فیلدهای عنوان و محتوای آن نیز به صورت خودکار ویرایش شده‌اند.

و ... تمام این کارها صرفا با فراخوانی متدهای UpdateGraph و سپس SaveChanges رخ داده‌است.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
GraphDiffTests.zip
مطالب
تولید هدرهای Content Security Policy توسط ASP.NET Core برای برنامه‌های Angular
پیشتر مطلب «افزودن هدرهای Content Security Policy به برنامه‌های ASP.NET» را در این سایت مطالعه کرده‌اید. در اینجا قصد داریم معادل آن‌را برای ASP.NET Core تهیه کرده و همچنین نکات مرتبط با برنامه‌های Angular را نیز در آن لحاظ کنیم.


تهیه میان افزار افزودن هدرهای Content Security Policy

کدهای کامل این میان افزار را در ادامه مشاهده می‌کنید:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace AngularTemplateDrivenFormsLab.Utils
{
    public class ContentSecurityPolicyMiddleware
    {
        private readonly RequestDelegate _next;

        public ContentSecurityPolicyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
            context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
            context.Response.Headers.Add("X-Content-Type-Options", "nosniff");

            string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
            context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
            return _next(context);
        }
    }

    public static class ContentSecurityPolicyMiddlewareExtensions
    {
        /// <summary>
        /// Make sure you add this code BEFORE app.UseStaticFiles();,
        /// otherwise the headers will not be applied to your static files.
        /// </summary>
        public static IApplicationBuilder UseContentSecurityPolicy(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ContentSecurityPolicyMiddleware>();
        }
    }
}
که نحوه‌ی استفاده از آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
public void Configure(IApplicationBuilder app)
{
   app.UseContentSecurityPolicy();

توضیحات تکمیلی

افزودن X-Frame-Options
 context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
از هدر X-FRAME-OPTIONS، جهت منع نمایش و رندر سایت جاری، در iframeهای سایت‌های دیگر استفاده می‌شود. ذکر مقدار SAMEORIGIN آن، به معنای مجاز تلقی کردن دومین جاری برنامه است.


افزودن X-Xss-Protection
 context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
تقریبا تمام مرورگرهای امروزی قابلیت تشخیص حملات XSS را توسط static analysis توکار خود دارند. این هدر، آنالیز اجباری XSS را فعال کرده و همچنین تنظیم حالت آن به block، نمایش و رندر قسمت مشکل‌دار را به طور کامل غیرفعال می‌کند.


افزودن X-Content-Type-Options
 context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
وجود این هدر سبب می‌شود تا مرورگر، حدس‌زدن نوع فایل‌ها، درخواست‌ها و محتوا را کنار گذاشته و صرفا به content-type ارسالی توسط سرور اکتفا کند. به این ترتیب برای مثال امکان لینک کردن یک فایل غیرجاوا اسکریپتی و اجرای آن به صورت کدهای جاوا اسکریپت، چون توسط تگ script ذکر شده‌است، غیرفعال می‌شود. در غیراینصورت مرورگر هرچیزی را که توسط تگ script به صفحه لینک شده باشد، صرف نظر از content-type واقعی آن، اجرا خواهد کرد.


افزودن Content-Security-Policy
string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
وجود این هدر، تزریق کدها و منابع را از دومین‌های دیگر غیرممکن می‌کند. برای مثال ذکر self در اینجا به معنای مجاز بودن الصاق و اجرای اسکریپت‌ها، شیوه‌نامه‌ها، تصاویر و اشیاء، صرفا از طریق دومین جاری برنامه است و هرگونه منبعی که از دومین‌های دیگر به برنامه تزریق شود، قابلیت اجرایی و یا نمایشی نخواهد داشت.

در اینجا ذکر unsafe-inline و unsafe-eval را مشاهده می‌کنید. برنامه‌های Angular به همراه شیوه‌نامه‌های inline و یا بکارگیری متد eval در مواردی خاص هستند. اگر این دو گزینه ذکر و فعال نشوند، در کنسول developer مرورگر، خطای بلاک شدن آن‌ها را مشاهده کرده و همچنین برنامه از کار خواهد افتاد.

یک نکته: با فعالسازی گزینه‌ی aot-- در حین ساخت برنامه، می‌توان unsafe-eval را نیز حذف کرد.


استفاده از فایل web.config برای تعریف SameSite Cookies

یکی از پیشنهادهای اخیر ارائه شده‌ی جهت مقابله‌ی با حملات CSRF و XSRF، قابلیتی است به نام  Same-Site Cookies. به این ترتیب مرورگر، کوکی سایت جاری را به همراه یک درخواست ارسال آن به سایت دیگر، پیوست نمی‌کند (کاری که هم اکنون با درخواست‌های Cross-Site صورت می‌گیرد). برای رفع این مشکل، با این پیشنهاد امنیتی جدید، تنها کافی است SameSite، به انتهای کوکی اضافه شود:
 Set-Cookie: sess=abc123; path=/; SameSite

نگارش‌های بعدی ASP.NET Core، ویژگی SameSite را نیز به عنوان CookieOptions لحاظ کرده‌اند. همچنین یک سری از کوکی‌های خودکار تولیدی توسط آن مانند کوکی‌های anti-forgery به صورت خودکار با این ویژگی تولید می‌شوند.
اما مدیریت این مورد برای اعمال سراسری آن، با کدنویسی میسر نیست (مگر اینکه مانند نگارش‌های بعدی ASP.NET Core پشتیبانی توکاری از آن صورت گیرد). به همین جهت می‌توان از ماژول URL rewrite مربوط به IIS برای افزودن ویژگی SameSite به تمام کوکی‌های تولید شده‌ی توسط سایت، کمک گرفت. برای این منظور تنها کافی است فایل web.config را ویرایش کرده و موارد ذیل را به آن اضافه کنید:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <outboundRules>
        <clear />
        <!-- https://scotthelme.co.uk/csrf-is-dead/ -->
        <rule name="Add SameSite" preCondition="No SameSite">
          <match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
          <action type="Rewrite" value="{R:0}; SameSite=lax" />
          <conditions></conditions>
        </rule>
        <preConditions>
          <preCondition name="No SameSite">
            <add input="{RESPONSE_Set_Cookie}" pattern="." />
            <add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=lax" negate="true" />
          </preCondition>
        </preConditions>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>


لاگ کردن منابع بلاک شده‌ی توسط مرورگر در سمت سرور

اگر به هدر Content-Security-Policy دقت کنید، گزینه‌ی آخر آن، ذکر اکشن متدی در سمت سرور است:
   "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
با تنظیم این مورد، می‌توان موارد بلاک شده را در سمت سرور لاگ کرد. اما این اطلاعات ارسالی به سمت سرور، فرمت خاصی را دارند:
{
  "csp-report": {
    "document-uri": "http://localhost:5000/untypedSha",
    "referrer": "",
    "violated-directive": "script-src",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; style-src 'self'; script-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'self'; report-uri /api/Home/CspReport",
    "disposition": "enforce",
    "blocked-uri": "eval",
    "line-number": 21,
    "column-number": 8,
    "source-file": "http://localhost:5000/scripts.bundle.js",
    "status-code": 200,
    "script-sample": ""
  }
}
به همین جهت ابتدا نیاز است توسط JsonProperty کتابخانه‌ی JSON.NET، معادل این خواص را تولید کرد:
    class CspPost
    {
        [JsonProperty("csp-report")]
        public CspReport CspReport { get; set; }
    }

    class CspReport
    {
        [JsonProperty("document-uri")]
        public string DocumentUri { get; set; }

        [JsonProperty("referrer")]
        public string Referrer { get; set; }

        [JsonProperty("violated-directive")]
        public string ViolatedDirective { get; set; }

        [JsonProperty("effective-directive")]
        public string EffectiveDirective { get; set; }

        [JsonProperty("original-policy")]
        public string OriginalPolicy { get; set; }

        [JsonProperty("disposition")]
        public string Disposition { get; set; }

        [JsonProperty("blocked-uri")]
        public string BlockedUri { get; set; }

        [JsonProperty("line-number")]
        public int LineNumber { get; set; }

        [JsonProperty("column-number")]
        public int ColumnNumber { get; set; }

        [JsonProperty("source-file")]
        public string SourceFile { get; set; }

        [JsonProperty("status-code")]
        public string StatusCode { get; set; }

        [JsonProperty("script-sample")]
        public string ScriptSample { get; set; }
    }
اکنون می‌توان بدنه‌ی درخواست را استخراج و سپس به این شیء ویژه نگاشت کرد:
namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class CspReportController : Controller
    {
        [HttpPost("[action]")]
        [IgnoreAntiforgeryToken]
        public async Task<IActionResult> Log()
        {
            CspPost cspPost;
            using (var bodyReader = new StreamReader(this.HttpContext.Request.Body))
            {
                var body = await bodyReader.ReadToEndAsync().ConfigureAwait(false);
                this.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
                cspPost = JsonConvert.DeserializeObject<CspPost>(body);
            }

            //TODO: log cspPost

            return Ok();
        }
    }
}
در اینجا نحوه‌ی استخراج Request.Body را به صورت خام را مشاهده می‌کنید. سپس توسط متد DeserializeObject کتابخانه‌ی JSON.NET، این رشته به شیء CspPost نگاشت شده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
ایجاد «خواص الحاقی»
حتما با متدهای الحاقی یا Extension methods آشنایی دارید؛ می‌توان به یک شیء، که حتی منبع آن در دسترس ما نیست، متدی را اضافه کرد. سؤال: در مورد خواص چطور؟ آیا می‌شود به وهله‌ای از یک شیء موجود از پیش طراحی شده، یک خاصیت جدید را اضافه کرد؟
احتمالا شاید عنوان کنید که با اشیاء dynamic می‌توان چنین کاری را انجام داد. اما سؤال در مورد اشیاء غیر dynamic است.
یا نمونه‌ی دیگر آن Attached Properties در برنامه‌های مبتنی بر Xaml هستند. می‌توان به یک شیء از پیش موجود Xaml، خاصیتی را افزود که البته پیاده سازی آن منحصر است به همان نوع برنامه‌ها.


راه حل پیشنهادی

یک Dictionary را ایجاد کنیم تا ارجاعی از اشیاء، به عنوان کلید، در آن ذخیره شده و سپس key/valueهایی به عنوان value هر شیء، در آن ذخیره شوند. این key/valueها همان خواص و مقادیر آن‌ها خواهند بود. هر چند این راه حل به خوبی کار می‌کند اما ... مشکل نشتی حافظه دارد.
شیء Dictionary یک ارجاع قوی را از اشیاء، درون خودش نگه داری می‌کند و تا زمانیکه در حافظه باقی است، سیستم GC مجوز رهاسازی منابع آن‌ها را نخواهد یافت؛ چون عموما این نوع Dictionaryها باید استاتیک تعریف شوند تا طول عمر آن‌ها با طول عمر برنامه یکی گردد. بنابراین اساسا اشیایی که به این نحو قرار است پردازش شوند، هیچگاه dispose نخواهند شد. راه حلی برای این مساله در دات نت 4 به صورت توکار به دات نت فریم ورک اضافه شده‌است؛ به نام ساختار داده‌ای ConditionalWeakTable.


معرفی ConditionalWeakTable

ConditionalWeakTable جزو ساختارهای داده‌ای کمتر شناخته شده‌ی دات نت است. این ساختار داده، اشاره‌گرهایی را به ارجاعات اشیاء، درون خود ذخیره می‌کند. بنابراین چون ارجاعاتی قوی را به اشیاء ایجاد نمی‌کند، مانع عملکرد GC نیز نشده و برنامه در دراز مدت دچار مشکل نشتی حافظه نخواهد شد. هدف اصلی آن ایجاد ارتباطی بین CLR و DLR است. توسط آن می‌توان به اشیاء دلخواه، خواصی را افزود. به علاوه طراحی آن به نحوی است که thread safe است و مباحث قفل گذاری بر روی اطلاعات، به صورت توکار در آن پیاده سازی شده‌است. کار DLR فراهم آوردن امکان پیاده سازی زبان‌های پویایی مانند Ruby و Python برفراز CLR است. در این نوع زبان‌ها می‌توان به وهله‌هایی از اشیاء موجود، خاصیت‌های جدیدی را متصل کرد.
به صورت خلاصه کار ConditionalWeakTable ایجاد نگاشتی است بین وهله‌هایی از اشیاء CLR (اشیایی غیرپویا) و خواصی که به آن‌ها می‌توان به صورت پویا انتساب داد. در کار GC اخلال ایجاد نمی‌کند و همچنین می‌توان به صورت همزمان از طریق تردهای مختلف، بدون مشکل با آن کار کرد.


پیاده سازی خواص الحاقی به کمک ConditionalWeakTable

در اینجا نحوه‌ی استفاده از ConditionalWeakTable را جهت اتصال خواصی جدید به وهله‌های موجود اشیاء مشاهده می‌کنید:
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace ConditionalWeakTableSamples
{
    public static class AttachedProperties
    {
        public static ConditionalWeakTable<object,
            Dictionary<string, object>> ObjectCache = new ConditionalWeakTable<object,
                Dictionary<string, object>>();

        public static void SetValue<T>(this T obj, string name, object value) where T : class
        {
            var properties = ObjectCache.GetOrCreateValue(obj);

            if (properties.ContainsKey(name))
                properties[name] = value;
            else
                properties.Add(name, value);
        }

        public static T GetValue<T>(this object obj, string name)
        {
            Dictionary<string, object> properties;
            if (ObjectCache.TryGetValue(obj, out properties) && properties.ContainsKey(name))
                return (T)properties[name];
            return default(T);
        }

        public static object GetValue(this object obj, string name)
        {
            return obj.GetValue<object>(name);
        }
    }
}
ObjectCache تعریف شده از نوع استاتیک است؛ بنابراین در طول عمر برنامه زنده نگه داشته خواهد شد، اما اشیایی که به آن منتسب می‌شوند، خیر. هرچند به ظاهر در متد GetOrCreateValue، یک وهله از شیءایی موجود را دریافت می‌کند، اما در پشت صحنه صرفا IntPtr یا اشاره‌گری به این شیء را ذخیره سازی خواهد کرد. به این ترتیب در کار GC اخلالی صورت نخواهد گرفت و شیء مورد نظر، تا پایان کار برنامه به اجبار زنده نگه داشته نخواهد شد.


کاربرد اول

اگر با ASP.NET کار کرده باشید حتما با IPrincipal آشنایی دارید. خواصی مانند Identity یک کاربر در آن ذخیره می‌شوند.
سؤال: چگونه می‌توان یک خاصیت جدید به نام مثلا Disclaimer را به وهله‌ای از این شیء افزود:
    public static class ISecurityPrincipalExtension
    {
        public static bool Disclaimer(this IPrincipal principal)
        {
            return principal.GetValue<bool>("Disclaimer");
        }

        public static void SetDisclaimer(this IPrincipal principal, bool value)
        {
            principal.SetValue("Disclaimer", value);
        }
    }
در اینجا مثالی را از کاربرد کلاس AttachedProperties فوق مشاهده می‌کنید. توسط متد SetDisclaimer یک خاصیت جدید به نام Disclaimer به وهله‌ای از شیءایی از نوع  IPrincipal  قابل اتصال است. سپس توسط متد  Disclaimer قابل دستیابی خواهد بود.

اگر صرفا قرار است یک خاصیت به شیءایی متصل شود، روش ذیل نیز قابل استفاده می‌باشد (بجای استفاده از دیکشنری از یک کلاس جهت تعریف خاصیت اضافی جدید استفاده شده‌است):
using System.Runtime.CompilerServices;

namespace ConditionalWeakTableSamples
{
    public static class PropertyExtensions
    {
        private class ExtraPropertyHolder
        {
            public bool IsDirty { get; set; }
        }

        private static readonly ConditionalWeakTable<object, ExtraPropertyHolder> _isDirtyTable
                = new ConditionalWeakTable<object, ExtraPropertyHolder>();

        public static bool IsDirty(this object @this)
        {
            return _isDirtyTable.GetOrCreateValue(@this).IsDirty;
        }

        public static void SetIsDirty(this object @this, bool isDirty)
        {
            _isDirtyTable.GetOrCreateValue(@this).IsDirty = isDirty;
        }
    }
}


کاربرد دوم

ایجاد Id منحصربفرد برای اشیاء برنامه.
فرض کنید در حال نوشتن یک Entity framework profiler هستید. طراحی فعلی سیستم Interception آن به نحو زیر است:
public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
{
}
سؤال: اینجا رویداد بسته شدن یک اتصال را دریافت می‌کنیم؛ اما ... دقیقا کدام اتصال؟ رویداد Opened را هم داریم اما چگونه این اشیاء را به هم مرتبط کنیم؟ شیء DbConnection دارای Id نیست. متد GetHashCode هم الزامی ندارد که اصلا پیاده سازی شده باشد یا حتی یک Id منحصربفرد را تولید کند. این متد با تغییر مقادیر خواص یک شیء می‌تواند مقادیر متفاوتی را ارائه دهد. در اینجا می‌خواهیم به ازای ارجاعی از یک شیء، یک Id منحصربفرد داشته باشیم تا بتوانیم تشخیص دهیم که این اتصال بسته شده، دقیقا کدام اتصال باز شده‌است؟
راه حل: خوب ... یک خاصیت Id را به اشیاء موجود متصل کنید!
using System;
using System.Runtime.CompilerServices;

namespace ConditionalWeakTableSamples
{
    public static class UniqueIdExtensions
    {
        static readonly ConditionalWeakTable<object, string> _idTable = 
                                    new ConditionalWeakTable<object, string>();

        public static string GetUniqueId(this object obj)
        {
            return _idTable.GetValue(obj, o => Guid.NewGuid().ToString());
        }

        public static string GetUniqueId(this object obj, string key)
        {
            return _idTable.GetValue(obj, o => key);
        }
    }
}
در اینجا مثالی دیگر از پیاده سازی و استفاده از ConditionalWeakTable را ملاحظه می‌کنید. اگر در کش آن ارجاعی به شیء مورد نظر وجود داشته باشد، مقدار Guid آن بازگشت داده می‌شود؛ اگر خیر، یک Guid به ارجاعی از شیء، انتساب داده شده و سپس بازگشت داده می‌شود. به عبارتی به صورت پویا یک خاصیت UniqueId به وهله‌هایی از اشیاء اضافه می‌شوند. به این ترتیب به سادگی می‌توان آن‌ها را ردیابی کرد و تشخیص داد که اگر این Guid پیشتر جایی به اتصال باز شده‌ای منتسب شده‌است، در چه زمانی و در کجا بسته شده است یا اصلا ... خیر. جایی بسته نشده‌است.


برای مطالعه بیشتر
The Conditional Weak Table: Enabling Dynamic Object Properties
How to create mixin using C# 4.0
Disclaimer Page using MVC
Extension Properties Revised
Easy Modeling
Providing unique ID on managed object using ConditionalWeakTable
مطالب
INPC استاندارد با بهره گیری از صفت CallerMemberName
یکی از Attribute‌های بسیار کاربردی که در سی شارپ 5 اضافه شد CallerMemberNameAttribute بود. این صفت به یک متد اجازه میدهد که از فراخواننده‌ی خود مطلع شود. این صفت را می‌توان بر روی یک پارامتر انتخابی که مقدار پیش‌فرضی دارد اعمال نمود.

استفاده از این صفت هم بسیار ساده است:

private void A ( [CallerMemberName] string callerName = "") 
{
  Console.WriteLine("Caller is " + callerName);
}

private static void B()
{
        // let's call A
        A();
}
در کد فوق، متد A به راحتی می‌تواند بفهمد چه کسی آن را فراخوانی کرده است. از جمله کاربردهای این صفت در ردیابی و خطایابی است.

ولی یک استفاده‌ی بسیار کاربردی از این صفت، در پیاده سازی رابط INotifyPropertyChanged می‌باشد.

معمولا هنگام پیاده سازی INotifyPropertyChanged کدی شبیه به این را می‌نویسیم:

    public class PersonViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        private string name;
        public string Name
        {
            get { return name; }
            set
            {
                this.name = value;
                OnPropertyChanged("Name");
            }
        }
    }

یعنی در Setter معمولا نام ویژگی ای را که تغییر کرده است، به متد OnPropertyChanged می‌فرستیم تا اطلاع رسانی‌های لازم انجام پذیرد. تا اینجای کار همه چیز خوب و آرام است. اما به محضی که کد شما کمی طولانی شود و شما به دلایلی نیاز به Refactor کردن کد و احیانا تغییر نام ویژگی‌ها را پیدا کنید، آن موقع مسائل جدیدی بروز پیدا می‌کند.

برای مثال فرض کنید پس از نوشتن کلاس PersonViewModel تصمیم می‌گیرد نام ویژگی Name را به FirstName تغییر دهید؛ چرا که می‌خواهید اجزای نام یک شخص را به صورت مجزا نگهداری و پردازش کنید. پس احتمالا با زدن کلید F2 روی فیلد name آن را به firstName و ویژگی Name را به FirstName تغییر نام می‌دهید. همانند کد زیر:

private string firstName;
public string FirstName
{
            get { return firstName; }
            set
            {
                this.firstName = value;
                OnPropertyChanged("Name");
            }
}

برنامه را کامپایل کرده و در کمال تعجب می‌بینید که بخشی از برنامه درست رفتار نمی‌کند و تغییراتی که در نام کوچک شخص توسط کاربر ایجاد می‌شود به درستی بروزرسانی نمی‌شوند. علت ساده است: ما کد را به صورت اتوماتیک Refactor کرده ایم و گزینه‌ی Include String را در حین Refactor، در حالت پیشفرض غیرفعال رها کرده‌ایم. پس جای تعجبی ندارد که در هر جای کد که رشته‌ای به نام "Name" با ماهیت نام شخص داشته ایم، دست نخورده باقی مانده است. در واقع در کد تغییر یافته، هنگام تغییر FirstName، ما به سیستم گزارش می‌کنیم که ویژگی Name (که اصلا وجود ندارد) تغییر یافته است و این یعنی خطا.

حال احتمال بروز این خطا را در ViewModel هایی با ده‌ها ویژگی و ترکیب‌های مختلف در نظر بگیرید. پس کاملا محتمل است و برای خیلی از دوستان این اتفاق رخ داده است.

و اما راه حل چیست؟ به کارگیری صفت CallerMemberName

بهتر است که یک کلاس انتزاعی برای تمام ViewModel‌های خود داشته باشیم و پیاده سازی جدید INPC را در درون آن قرار دهیم تا براحتی VM‌های ما از آن مشتق شوند:

public abstract class ViewModelBase : INotifyPropertyChanged
{
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            OnPropertyChangedExplicit(propertyName);
        }

        protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection)
        {
            var memberExpression = (MemberExpression)projection.Body;
            OnPropertyChangedExplicit(memberExpression.Member.Name);
        }

        void OnPropertyChangedExplicit(string propertyName)
        {
            this.CheckPropertyName(propertyName);

            PropertyChangedEventHandler handler = this.PropertyChanged;

            if (handler != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            }
        }

        #region Check property name

        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        public void CheckPropertyName(string propertyName)
        {
            if (TypeDescriptor.GetProperties(this)[propertyName] == null)
                throw new Exception(String.Format("Could not find property \"{0}\"", propertyName));
        }

        #endregion // Check property name
}

در این کلاس، ما پارامتر propertyName را از متد OnPropertyChanged، توسط صفت CallerMemberName حاشیه نویسی کرده‌ایم. این کار باعث می‌شود در Setter‌های ویژگی‌ها، به راحتی بدون نوشتن نام ویژگی، عملیات اطلاع رسانی تغییرات را انجام دهیم. بدین صورت که کافیست متد OnPropertyChanged بدون هیچ آرگومانی در Setter فراخوانی شود و صفت CallerMemberName به صورت اتوماتیک نام ویژگی ای که فراخوانی از درون آن انجام شده است را درون پارامتر propertyName قرار می‌دهد.

پس کلاس PersonViewModel را به صورت زیر می‌توانیم اصلاح و تکمیل کنیم:

public class PersonViewModel : ViewModelBase
{
        private string firstName;
        public string FirstName
        {
            get { return firstName; }
            set
            {
                this.firstName = value;

                OnPropertyChanged();
                OnPropertyChanged(() => this.FullName);
            }
        }

        private string lastName;
        public string LastName
        {
            get { return lastName; }
            set
            {
                this.lastName = value;

                OnPropertyChanged();
                OnPropertyChanged(() => this.FullName);
            }
        }

        public string FullName
        {
            get { return string.Format("{0} {1}", FirstName, LastName); }
        }
}
همانطور که می‌بینید متد OnPropertyChanged بدون آرگومان فراخوانی میشود. اکنون اگر شما اقدام به Refactor کردن کد خود بکنید دیگر نگرانی از بابت تغییر نکردن رشته‌ها و کامنت‌ها نخواهید داشت و مطمئن هستید، نام ویژگی هر چیزی که باشد، به صورت خودکار به متد ارسال خواهد شد.

کلاس ViewModelBase یک پیاده سازی دیگر از OnPropetyChanged هم دارد که به شما اجازه می‌دهد با استفاده دستورات لامبدا، OnPropertyChanged را برای هر یک از اعضای دلخواه کلاس نیز فراخوانی کنید. همانطور که در مثال فوق می‌بینید، تغییرات نام خانوادگی در نام کامل شخص نیز اثرگذار است. در نتیجه به وسیله‌ی یک Func به راحتی بیان می‌کنیم که FullName هم تغییر کرده است و اطلاع رسانی برای آن نیز باید صورت پذیرد.

برای استفاده از صفت CallerMemberName باید دات نت هدف خود را 4.5 یا 4.6 قرار دهید.

ارجاع:
Raise INPC witout string name
نظرات مطالب
فعال سازی قسمت آپلود تصویر و فایل Kendo UI Editor
اگر thumb‌ها به درستی  نمایش داده نمی‌شود و فقط قسمتی از عکس رو مشاهده می‌کنید ، با استفاده از قطعه کد زیر این مشکل رفع خواهد شد :
1- تعریف کلاس به صورت زیر
public class ImageSize
    {
        public int Height
        {
            get;
            set;
        }

        public int Width
        {
            get;
            set;
        }
    }
2- تعریف کلاس ImageResizer همانند زیر :
public class ImageResizer
    {
        public ImageSize Resize(ImageSize originalSize, ImageSize targetSize)
        {
            var aspectRatio = (float)originalSize.Width / (float)originalSize.Height;
            var width = targetSize.Width;
            var height = targetSize.Height;

            if (originalSize.Width > targetSize.Width || originalSize.Height > targetSize.Height)
            {
                if (aspectRatio > 1)
                {
                    height = (int)(targetSize.Height / aspectRatio);
                }
                else
                {
                    width = (int)(targetSize.Width * aspectRatio);
                }
            }
            else
            {
                width = originalSize.Width;
                height = originalSize.Height;
            }

            return new ImageSize
            {
                Width = Math.Max(width, 1),
                Height = Math.Max(height, 1)
            };
        }
    }

3- تعریف کلاس ThumbnailCreator  همانند نمونه زیر :
 public class ThumbnailCreator
    {
        private static readonly IDictionary<string, ImageFormat> ImageFormats = new Dictionary<string, ImageFormat>{
            {"image/png", ImageFormat.Png},
            {"image/gif", ImageFormat.Gif},
            {"image/jpeg", ImageFormat.Jpeg}
        };

        private readonly ImageResizer resizer;

        public ThumbnailCreator()
        {
            this.resizer = new ImageResizer();
        }

        public byte[] Create(Stream source, ImageSize desiredSize, string contentType)
        {
            using (var image = Image.FromStream(source))
            {
                var originalSize = new ImageSize
                {
                    Height = image.Height,
                    Width = image.Width
                };

                var size = resizer.Resize(originalSize, desiredSize);

                using (var thumbnail = new Bitmap(size.Width, size.Height))
                {
                    ScaleImage(image, thumbnail);

                    using (var memoryStream = new MemoryStream())
                    {
                        thumbnail.Save(memoryStream, ImageFormats[contentType]);

                        return memoryStream.ToArray();
                    }
                }
            }
        }

        private void ScaleImage(Image source, Image destination)
        {
            using (var graphics = Graphics.FromImage(destination))
            {
                graphics.CompositingMode = CompositingMode.SourceCopy;
                graphics.CompositingQuality = CompositingQuality.HighQuality;
                graphics.SmoothingMode = SmoothingMode.AntiAlias;
                graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
                graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

                graphics.DrawImage(source, 0, 0, destination.Width, destination.Height);
            }
        }
    }

4- تعریف اکشن Thumbnail همانند زیر :
private FileContentResult CreateThumbnail(string physicalPath)
        {
            using (var fileStream = System.IO.File.OpenRead(physicalPath))
            {
                var desiredSize = new ImageSize
                {
                    Width = ThumbnailWidth,
                    Height = ThumbnailHeight
                };


                string contentType = MimeMapping.GetMimeMapping(physicalPath);
                return File(thumbnailCreator.Create(fileStream, desiredSize, contentType), contentType);
            }
        }

و در پایان اکشن GetThumbnail  را همانند زیر تغییر خواهیم داد :
 public virtual ActionResult GetThumbnail(string path)
        {
            path = GetSafeFileAndDirPath(path);
          //  return File(path, contentType); 
            return CreateThumbnail(path);
        }