مطالب
امکان تعریف توابع خاص بانک‌های اطلاعاتی در EF Core
یکی از اهداف کار با ORMها، رسیدن به کدی قابل ترجمه و استفاده‌ی توسط تمام بانک‌های اطلاعاتی ممکن است و یکی از الزامات رسیدن به این هدف، صرفنظر کردن از قابلیت‌های بومی بانک‌های اطلاعاتی است که در سایر بانک‌های اطلاعاتی دیگر معادلی ندارند. برای مثال SQL Server به همراه توابع توکاری مانند datediff و datepart برای کار با زمان و تاریخ است؛ اما این توابع را به صورت مستقیم نمی‌توان در ORMها استفاده کرد. چون به محض استفاده‌ی از آن‌ها، کد تهیه شده دیگر قابلیت انتقال به سایر بانک‌های اطلاعاتی را نخواهد داشت. اما ... اگر این هدف را نداشته باشیم، چطور؟ آیا می‌توان یک تابع DateDiff سفارشی را برای EF Core تهیه نمود و از تمام قابلیت‌های بومی آن در کوئری‌های LINQ استفاده کرد؟ بله! یک چنین قابلیتی تحت عنوان DbFunctions در EF Core پشتیبانی می‌شود که روش تهیه‌ی آن‌ها را در این مطلب بررسی خواهیم کرد.


معرفی موجودیت Person

در مثال این مطلب قصد داریم، معادل توابع بومی مخصوص SQL Server را که امکان کار با DateTime را مهیا می‌کنند، در EF Core تعریف کنیم. به همین جهت نیاز به موجودیتی داریم که دارای خاصیتی از این نوع باشد:
using System;

namespace EFCoreDbFunctionsSample.Entities
{
    public class Person
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public DateTime AddDate { get; set; }
    }
}


گزارشگیری بر اساس تعداد روز گذشته‌ی از ثبت نام

اکنون فرض کنید می‌خواهیم گزارشی را از تمام کاربرانی که در طی 10 روز قبل ثبت نام کرده‌اند، تهیه کنیم. اگر کوئری زیر را برای این منظور تهیه کنیم:
var usersInfo = context.People.Where(person => (DateTime.Now - person.AddDate).Days <= 10).ToList();
با استثنای زیر متوقف خواهیم شد:
'The LINQ expression 'DbSet<Person>.Where(p => (DateTime.Now - p.AddDate).Days <= 10)'
could not be translated. Either rewrite the query in a form that can be translated,
or switch to client evaluation explicitly by inserting a call to either
AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().
See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
عنوان می‌کند که یک چنین کوئری LINQ ای قابلیت ترجمه‌ی به SQL را ندارد. اما ... نکته‌ی مهم اینجا است که خود SQL Server یک چنین توانمندی را به صورت توکار دارا است:
SELECT [p].[Id], [p].[AddDate], [p].[Name]
FROM [People] AS [p]
WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= 10
برای انجام کوئری مدنظر فقط کافی است از تابع DATEDIFF توکار آن با پارامتر Day، استفاده کنیم تا لیست تمام کاربران ثبت نام کرده‌ی در طی 10 روز قبل را بازگشت دهد. اکنون سؤال اینجا است که آیا می‌توان چنین تابعی را به EF Core معرفی کرد؟


روش تعریف تابع DATEDIFF سفارشی در EF Core

برای تعریف متد DateDiff مخصوص EF Core، ابتدا باید یک کلاس static را تعریف کرد و سپس تنها امضای این متد را، معادل امضای تابع توکار SQL Server تعریف کرد. این متد نیازی نیست تا پیاده سازی را داشته باشد. به همین جهت بدنه‌ی آن‌را صرفا با یک throw new InvalidOperationException مقدار دهی می‌کنیم. هدف از این متد، استفاده‌ی از آن در LINQ Expressions است و قرار نیست به صورت مستقیمی بکار گرفته شود:
namespace EFCoreDbFunctionsSample.DataLayer
{
    public enum SqlDateDiff
    {
        Year,
        Quarter,
        Month,
        DayOfYear,
        Day,
        Week,
        Hour,
        Minute,
        Second,
        MilliSecond,
        MicroSecond,
        NanoSecond
    }

    public static class SqlDbFunctionsExtensions
    {
        public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end)
            => throw new InvalidOperationException($"{nameof(SqlDateDiff)} method cannot be called from the client side.");
        public static readonly MethodInfo SqlDateDiffMethodInfo = typeof(SqlDbFunctionsExtensions)
            .GetRuntimeMethod(
                nameof(SqlDbFunctionsExtensions.SqlDateDiff),
                new[] { typeof(SqlDateDiff), typeof(DateTime), typeof(DateTime) }
            );
    }
}
در اینجا علاوه بر تعریف امضای متد DateDiff که در اینجا SqlDateDiff نام گرفته‌است، فیلد SqlDateDiffMethodInfo را نیز مشاهده می‌کنید. در حین تعریف و معرفی DbFunctions سفارشی به EF Core، متدهایی که اینکار را انجام می‌دهند، پارامترهای ورودی از نوع MethodInfo دارند. به همین جهت یک چنین تعریفی انجام شده‌است.


روش معرفی تابع DATEDIFF سفارشی به EF Core

پس از تعریف امضای متد معادل DateDiff، اکنون نوبت به معرفی آن به EF Core است:
namespace EFCoreDbFunctionsSample.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.HasDbFunction(SqlDbFunctionsExtensions.SqlDateDiffMethodInfo)
                .HasTranslation(args =>
                {
                    var parameters = args.ToArray();
                    var param0 = ((SqlConstantExpression)parameters[0]).Value.ToString();
                    return SqlFunctionExpression.Create("DATEDIFF",
                        new[]
                        {
                            new SqlFragmentExpression(param0), // It should be written as DateDiff(day, ...) and not DateDiff(N'day', ...) .
                            parameters[1],
                            parameters[2]
                        },
                        SqlDbFunctionsExtensions.SqlDateDiffMethodInfo.ReturnType,
                        typeMapping: null);
                });
        }
    }
}
کار تعریف DbFunctions سفارشی توسط متد HasDbFunction صورت می‌گیرد. پارامتر این متد، همان MethodInfo معادل امضای تابع توکار مدنظر است.
سپس توسط متد HasTranslation، مشخص می‌کنیم که این متد به چه نحوی قرار است به یک عبارت SQL ترجمه شود. پارامتر args ای که در اینجا در اختیار ما قرار می‌گیرد، دقیقا همان پارامترهای متد public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) هستند که در این مثال خاص، شامل سه پارامتر می‌شوند. پارامترهای دوم و سوم آن‌را به همان نحوی که دریافت می‌کنیم، به SqlFunctionExpression.Create ارسال خواهیم کرد. اما پارامتر اول را از نوع enum تعریف کرده‌ایم و همچنین قرار نیست به صورت 'N'day و رشته‌ای به سمت بانک اطلاعاتی ارسال شود، بلکه باید به همان نحو اصلی آن (یعنی day)، در کوئری نهایی درج گردد، به همین جهت ابتدا Value آن‌را استخراج کرده و سپس توسط SqlFragmentExpression عنوان می‌کنیم آن‌را باید به همین نحو درج کرد.
پارامتر اول متد SqlFunctionExpression.Create، باید دقیقا معادل نام متد توکار مدنظر باشد. پارامتر دوم آن، لیست پارامترهای این تابع است. پارامتر سوم آن، نوع خروجی این تابع است که از طریق MethodInfo معادل، قابل استخراج است.


استفاده‌ی از DbFunction سفارشی جدید در برنامه

پس از این تعاریف و معرفی‌ها، اکنون می‌توان متد سفارشی SqlDateDiff تهیه شده را به صورت مستقیمی در کوئری‌های LINQ استفاده کرد تا قابلیت ترجمه‌ی به SQL را پیدا کنند:
var sinceDays = 10;
users = context.People.Where(person =>
      SqlDbFunctionsExtensions.SqlDateDiff(SqlDateDiff.Day, person.AddDate, DateTime.Now) <= sinceDays).ToList();
/*
SELECT [p].[Id], [p].[AddDate], [p].[Name]
FROM [People] AS [p]
WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= @__sinceDays_0
*/


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: EFCoreDbFunctionsSample.zip
این کدها به همراه چند تابع سفارشی دیگر نیز هستند.
مطالب
مدیریت تغییرات در سیستم های مبتنی بر WCF
تشریح مسئله : در صورتی که بعد از انتشار برنامه؛ در نسخه بعدی مدل سمت سرور تغییر کرده باشد و امکان بروز رسانی مدل‌های سمت کلاینت وجود نداشته باشد برای حل این مسئله بهترین روش کدام است.
نکته : برای فهم بهتر مطالب آشنایی اولیه با مفاهیم WCF الزامی است.
ابتدا مدل زیر را در نظر بگیرید:
   [DataContract]
    public class Book 
    {
        [DataMember]
        public int Code { get; set; }

        [DataMember]
        public string Name { get; set; }             
    }
حالا یک سرویس برای دریافت و ارسال اطلاعات این مدل به کلاینت می‌نویسیم.
  [ServiceContract]
    public interface ISampleService
    {
        [OperationContract]
        IEnumerable<Book> GetAll();

        [OperationContract]
        void Save( Book book );
    }
و سرویسی که Contract بالا رو پیاده سازی کند.
public class SampleService : ISampleService
    {
        public List<Book> ListOfBook 
        {
            get; 
            private set; 
        }

        public SampleService()
        {
            ListOfBook = new List<Book>();
        }
        public IEnumerable<Book> GetAll()
        {
            ListOfBook.AddRange( new Book[] 
            {
                new Book(){Code=1 , Name="Book1"},
                new Book(){Code=2 , Name="Book2"},
            } );
            return ListOfBook;
        }

        public void Save( Book book )
        {
            ListOfBook.Add( book );
        }
    }
 متد GetAll برای ارسال اطلاعات به کلاینت و متد Save نیز برای دریافت اطلاعات از کلاینت.
حالا یک پروژه Console Application بسازید و از روش AddServiceReference سرویس مورد نظر را به Client اضافه کنید. برنامه را تست کنید. بدون هیچ مشکلی کار می‌کند.
حالا اگر در نسخه بعدی سیستم مجبور شویم به مدل Book یک خاصیت دیگر به نام Author را نیز اضافه کنیم و امکان Update کردن سرویس در سمت کلاینت وجود نداشته باشد چه اتفاقی خواهد افتاد.
به صورت زیر:
 [DataContract]
    public class Book 
    {
        [DataMember]
        public int Code { get; set; }
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public string Author { get; set; }
    }
به طور پیش فرض اگر در DataContract‌های سمت سرور و کلاینت اختلاف وجود داشته باشد این موارد نادیده گرفته می‌شوند. یعنی همیشه مقدار خاصیت Author برابر null خواهد بود.
نکته : برای Value Type‌ها مقادیر پیش فرض و برای Reference Type‌ها مقدار Null.
اگر برای DataMemberAttribute خاصیت IsRequired را برابر true کنیم از این پس برای هر درخواستی که مقدار Author  آن مقدار نداشته باشد یک Protocol Exception  پرتاب می‌شود. به صورت زیر:
[DataMember( IsRequired = true )]
public string Author { get; set; }
اما این همیشه راه حل مناسبی نیست.
روش دیگر این است که Desrialize کردن مدل را تغییر دهیم. بدین معنی که هر گاه مقدار Author برابر Null بود یک مقدار پیش فرض برای آن در نظر بگیریم. این کار با نوشتن یک متد و قراردادن OnDeserializingAttribute به راحتی امکان پذیر است. کلاس Book به صورت زیر تغییر می‌کند.
  [DataContract]
    public class Book
    {
        [DataMember]
        public int Code { get; set; }
        [DataMember]
        public string Name { get; set; }

        [DataMember( IsRequired = true )]
        public string Author { get; set; }

        [OnDeserializing]
        private void OnDeserializing( StreamingContext context )
        {
            if ( string.IsNullOrEmpty( Author ) )
            {
                Author = "Masoud Pakdel";
            }
        }
    }
حال اگر از سمت کلاینت کلاس Book دریافت شود که مقدار خاصیت Author آن برابر Null باشد توسط متد OnDeserializing مقدار پیش فرض به آن اعمال می‌شود.مثل تصویر زیر:

روش بعدی استفاده از اینترفیس IExtensibleDataObject  است. بعد از اینکه کلاس Book این اینترفیس را پیاده سازی کرد مشکل Versioning Round Trip حل می‌شود. به این صورت که سرویس یا کلاینتی که نسخه قدیمی را می‌شناسد اگر نسخه جدید را دریافت کند خصوصیاتی را که نمی‌شناسد مثل Author در خاصیت ExtensionData ذخیره می‌شود و هنگامی که کلاس Book برای سرویس یا کلاینتی که نسخه جدید را می‌شناسد DataContractSerializer اطلاعات مورد نظر را از خصوصیت ExtensionData بیرون می‌کشد و کلاس Book جدید را باز سازی می‌کند. بررسی کلاس ExtensionData توسط خود DataContractSreializer انجام می‌شود و نیاز به هیچ گونه ای کد نویسی ندارد.

[DataContract]
    public class Book : IExtensibleDataObject
    {
        [DataMember]
        public int Code { get; set; }
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public string Author { get; set; }
    
        public virtual ExtensionDataObject ExtensionData
        {
            get { return _extensionData; }
            set
            {
                _extensionData = value;
            }
        }
        private ExtensionDataObject _extensionData;
    }
اگر کد متد GetAll سمت سرور را به صورت زیر تغییر دهیم که خاصیت Author هم مقدار داشته باشد با استفاده از خاصیت ExtensionData کلاینت هم از این مقدار مطلع خواهد شد.
public IEnumerable<Book> GetAll()
        {
            ListOfBook.AddRange( new Book[] 
            {
                new Book(){Code=1 , Name="Book1", Author="Masoud Pakdel"},
                new Book(){Code=2 , Name="Book2" },
            } );
            return ListOfBook;
        }
کلاینت هم به صورت زیر :

همان طور که می‌بینید این نسخه از کلاینت هیچ گونه اطلاعی از وجود یک خاصیت به نام Author ندارد ولی از طریق ExtensionData متوجه می‌شود یک خاصیت به نام Author به مدل سمت سرور اضافه شده است.

اما در صورتی که قصد داشته باشیم که یک سرویس خاص از همان نسخه قدیمی کلاس Book استفاده کند و نیاز به نسخه جدید آن نداشته باشد می‌توانیم این کار را از طریق مقدار دهی True به خاصیت IgnoreExtensionDataObject  در ServiceBehaviorAttribute انجام داد. بدین شکل

 [ServiceBehavior( IgnoreExtensionDataObject = true )]
  public class SampleService : ISampleService
از این پس سرویس بالا از همان مدل Book بدون خاصیت Author استفاده می‌کند.

منابع :
مطالب
ایده‌ی ثبت خودکار سرویس‌ها، به همراه تنظیمات؛ بدون نوشتن هیچ کدی در ConfigureServices با روش Installer
خودکارسازی، در قسمت‌های مختلف یک پروژه می‌تواند انجام شود. نمونه‌های مختلف این خودکارسازی‌ها که اکثرا توسط رفلکشن انجام می‌شوند شامل نگاشت خودکار Dto به Entity و بالعکس (توسط AutoMapper)، ثبت خودکار تمام Entityها در DbContext بدون نیاز به ثبت تک تک آن‌ها به صورت  public DbSet<Person> People { get; set; }  (که در این روش خودکار، اسم جداول می‌تواند به صورت جمع ثبت شود)، ثبت خودکار EntityTypeConfigurationها، ثبت خودکار کلیه‌ی کلاس‌های Profile برای کانفیگ AutoMapper و رجیستر خودکار DI سرویس‌ها، تا نیازی به نوشتن کدهای تکراری و مشابه   ;()<services.AddTransient<IUserService, UserService نداشته باشیم. 
 برای مشاهده‌ی عملی پیاده‌سازی این نمونه‌ها می‌توانید به پروژه‌ی ASP.NET Core WebAPI مراجعه کنید. در این مقاله می‌خواهیم همین سناریو را برای ثبت سرویس‌هایمان در متد ConfigureServices انجام دهیم، تا نیازی به نوشتن هیچ کدی برای آن‌ها نداشته باشیم. 

ثبت سرویس‌های مختلف، به همراه تنظیمات آن‌ها (مانند Authentication، Swagger، DbContext، ApiVersioning و ...) در استارتاپ می‌تواند به چندین صورت انجام شود.
روش اول اینکه به صورت دستی تمام کدهای مربوط به رجیستر کردن سرویس‌ها و تنظیمات آن‌ها، در متد ConfigureServices نوشته شود که خیلی جالب نیست و موجب شلوغ شدن سریع این متد می‌شود. نمونه‌ی این شیوه را برای ثبت سرویس مربوط به DbContext می‌بینیم:
public void ConfigureServices(IServiceCollection services) {
      // DbContext Service
      services.AddDbContext<AppDbContext>(options =>
            {
                options
             .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {     sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });

      // register other services ....

}


راه دوم روش استفاده از متدهای الحاقی است؛ طوریکه برای هر سرویس، یک متد الحاقی را تعریف کنیم و از آن، در این متد استفاده کنیم که حجم کدها را تا حد زیادی کم می‌کند. برای مثال ثبت سرویس بالا را می‌توانیم در کلاس دیگری با نام DbContextServiceCollectionExtensions.cs ثبت کنیم:
public static class DbContextServiceCollectionExtensions
    {
        public static void AddDbContext(this IServiceCollection services)
        {
              services.AddDbContext<AppDbContext>(options =>
            {
                options
                    .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {
                    sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });
        }
    }
و سپس در متد ConfigureServices می‌توان آن را به صورت زیر استفاده کرد:
public void ConfigureServices(IServiceCollection services) {
// Add DbContext
 services.AddDbContext();

//.... Register other services
}

ولی اگه پروژه‌ی ما متوسط به بالا باشد، کم‌کم تعداد سرویس‌های ما زیاد می‌شود (برای مثال چند نمونه از سرویس‌های رایج مورد استفاده، شامل سرویس‌های لاگ خطاها مثل Elmah و سرویس HttpClientFactory و AutoRegisterDi (توضیح در ادامه مقاله) و AutoMapper و Cache و EFSecondLevelCache و Hangfire و ....) می‌بینیم که تعداد این سرویس‌ها هم زیاد است و حتی به صورت اکستنشن هم به مرور زمان باعث شلوغ شدن استارتاپ می‌شوند. ضمن اینکه یک کار تکراری است که باید هر بار انجام شود.

راه سوم ثبت سرویس، استفاده از یک اینترفیس به نام IServiceInstaller و استفاده از آن در کلاس‌های مختلف مربوط به ثبت سرویس و بعد خواندن خودکار این تنظیمات با یک خط کد ساده‌ی رفلکشن است که در ادامه می‌بینیم: 
اینترفیس IServiceInstaller را تعریف می‌کنیم: 
public interface IServiceInstaller
    {
        void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly);
    }
توضیح: پارامتر appSettings کلاسی شامل کلیه‌ی مقادیر فایل appsettings.json است. شما می‌توانید بجای آن از IConfiguration استفاده کنید و مقدار آن را در Startup پاس دهید. پارامتر آخر برای حالتی است که این فایل‌ها را در لایه‌ی دیگری به غیر از لایه‌ی اصلی Api (مثل لایه‌ی WebFamewrk) پیاده سازی می‌کنید.
سپس کلاس‌های ثبت سرویس‌هایمان را با ارث بری از این اینترفیس می‌سازیم. برای نمونه رجیستر DbContext را با ایجاد کلاسی به نام DbContextInstaller انجام می‌دهیم:
public class DbContextInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            services.AddDbContext<AppDbContext>(options =>
            {
                options
               .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder =>
                {
                    sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds
                    sqlServerOptionsBuilder.EnableRetryOnFailure();
                    sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
                })
                    //Tips
                    .ConfigureWarnings(warning => warning.Throw(RelationalEventId
                        .QueryPossibleExceptionWithAggregateOperatorWarning));

                // Activate EF Second Level Cache
                options.AddInterceptors(new SecondLevelCacheInterceptor());
            });
        }
    }

حالا برای ثبت این کلاس و کلاس‌های مشابه Installer، می‌آییم یک متد الحاقی را برای متد ConfigureServices می‌نویسیم که در آن از رفلکشن استفاده می‌کنیم: 
public static class ServiceInstallerExtensions
    {
        public static void InstallServicesInAssemblies(this IServiceCollection services, AppSettings appSettings)
        {
            var startupProjectAssembly = Assembly.GetCallingAssembly();
            var assemblies = new[] { startupProjectAssembly, Assembly.GetExecutingAssembly() };
            var installers = assemblies.SelectMany(a => a.GetExportedTypes())
                .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(IServiceInstaller).IsAssignableFrom(c))
                .Select(Activator.CreateInstance).Cast<IServiceInstaller>().ToList();
            installers.ForEach(i => i.InstallServices(services, appSettings, startupProjectAssembly));
        }
    }

در نهایت متد ConfigureServices ما به صورت زیر خواهد بود (بعد از اضافه کردن تمام سرویس‌ها!):
public void ConfigureServices(IServiceCollection services)
        {
            //* HttpContextAccessor
            // services.AddHttpContextAccessor();

            //* Controllers
            services.AddControllers(options => { options.Filters.Add(new AuthorizeFilter()); })
                .AddNewtonsoftJson();

            //* Installers
            services.InstallServicesInAssemblies(_appSettings);
        }
کار تمام شد. حالا تمام سرویس‌های ما با ایجاد کلاس مرتبط و implement شدن از اینترفیس IServiceInstaller، به طور خودکار در استارتاپ و متد ConfigureServies ثبت خواهند شد.

فقط یک نکته آخر اینکه برای رجیستر خودکار DI سرویس‌ها (و ننوشتن کدهایی مانند   ()<services.AddTransient<IUserService, UserService برای رجیستر هر سرویس) می‌توانیم از Autofac استفاده کنیم (در پروژه‌ی بالا آمده است) و یا از پکیج AutoRegisterDi استفاده کنیم (متعلق به Jon P Smith) که از خود Container داخلی Core استفاده می‌کند و از Autofac سبکتر است. کلاسی می‌سازیم به نام RegisterServicesUsingAutoRegisterDiInstaller: 
public class RegisterServicesUsingAutoRegisterDiInstaller : IServiceInstaller
    {
        public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly)
        {
            var dataAssembly = typeof(SomeRepository).Assembly;
            var serviceAssembly = typeof(SomeService).Assembly;
            var webFrameworkAssembly = Assembly.GetExecutingAssembly();
            var startupAssembly = startupProjectAssembly;
            var assembliesToScan = new[] { dataAssembly, serviceAssembly, webFrameworkAssembly, startupAssembly };

            #region Generic Type Dependencies
            services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
            #endregion

            #region Scoped Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(IScopedDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Scoped);
            #endregion

            #region Singleton Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Singleton);
            #endregion

            #region Transient Dependency Interface
            services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan)
                .Where(c => c.GetInterfaces().Contains(typeof(ITransientDependency)))
                .AsPublicImplementedInterfaces(); // Default is Transient
            #endregion

            #region Register DIs By Name
            services.RegisterAssemblyPublicNonGenericClasses(dataAssembly)
                .Where(c => c.Name.EndsWith("Repository")
                            && !c.GetInterfaces().Contains(typeof(ITransientDependency))
                            && !c.GetInterfaces().Contains(typeof(IScopedDependency))
                            && !c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces(ServiceLifetime.Scoped);

            services.RegisterAssemblyPublicNonGenericClasses(serviceAssembly)
                .Where(c => c.Name.EndsWith("Service")
                            && !c.GetInterfaces().Contains(typeof(ITransientDependency))
                            && !c.GetInterfaces().Contains(typeof(IScopedDependency))
                            && !c.GetInterfaces().Contains(typeof(ISingletonDependency)))
                .AsPublicImplementedInterfaces();
            #endregion
        }
    }
 (رجیستر در اینجا با اولویت اینترفیس‌های ITransiantDependency، IScopedDependency، ISingletonDependency و سپس اتمام نام سرویس با کلمه‌های Repository و Service انجام می‌شود که شما می‌توانید با منطق و نیاز خودتان آن‌ها را تغییر دهید)
نظرات مطالب
طراحی جدول فایل‌های پیوستی پایگاه داده
با تشکر.
به غیر از روش دوم و  حالت آخر در تمام مواردی که فرمودید ، نیاز است ابتدا به عنوان مثال مقاله درج شود و سپس بتوان مشخصات فایل را در جدول مورد نظر ثبت کرد ؛ یک حالت دیگر هم در صورتی که نیاز به Privacy نباشد ، میتوان در نظر گرفت که احتمال میدم در سایت جاری هم به این شکل استفاده میشود ، آن هم داشتن یک جدول فارغ از جداول دیگر  و تنها ارتباطی با جدول کاربران داشته باشد که مدلی به شکل زیر دارد : 
 public class Attachment
    {

        #region Properties
        /// <summary>
        /// sets or gets identifier for attachment
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// sets or gets name for attachment
        /// </summary>
        public virtual string FileName { get; set; }
        /// <summary>
        /// sets or gets type of attachment
        /// </summary>
        public virtual string ContentType { get; set; }
        /// <summary>
        /// sets or gets size of attachment
        /// </summary>
        public virtual long Size { get; set; }
        /// <summary>
        /// sets or gets Extention of attachment
        /// </summary>
        public virtual string Extensions { get; set; }
        /// <summary>
        /// sets or gets Creation Date
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets counts of download this file
        /// </summary>
        public virtual long DownloadsCount { get; set; }

        public virtual DateTime ModifiedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets identifier of attachment's owner
        /// </summary>
        public virtual long OwnerId { get; set; }
        /// <summary>
        /// sets or gets identifier of attachment's owner
        /// </summary>
        public virtual User Owner { get; set; }
        #endregion
    }
در این حالت یکپارچه کردن ارسال فایل با ویرایشگر متن هم امکان پذیر خواهد بود و همچنین بازهم نیاز نیست فایلها چند باره ارسال شوند و به تفکیک هم میشود فایل‌های کاربران را در اختیارشان گذاشت.
مطالب
نوشتن آزمون‌های واحد به کمک کتابخانه‌ی Moq - قسمت اول - معرفی
گاهی از اوقات، برای نوشتن آزمون‌های واحد، ایزوله سازی قسمتی که می‌خواهیم آن‌را بررسی کنیم، از سایر قسمت‌های سیستم مشکل می‌شود. برای مثال اگر در کلاسی کار اتصال به بانک اطلاعاتی صورت می‌گیرد و قصد داریم برای آن آزمون واحد بنویسیم، اما قرار نیست که الزاما با بانک اطلاعاتی کار کنیم، در این حالت نیاز به یک نمونه‌ی تقلیدی یا Mock از بانک اطلاعاتی را خواهیم داشت، تا کار دسترسی به بانک اطلاعاتی را شبیه سازی کند. در این سری با استفاده از کتابخانه‌ی بسیار معروف Moq (ماک‌یو تلفظ می‌شود؛ گاهی از اوقات هم ماک)، کار ایزوله سازی کلاس‌ها را انجام خواهیم داد، تا بتوانیم آن‌ها را مستقل از هم آزمایش کنیم.


Mocking چیست؟

فرض کنید برنامه‌ای را داریم که از تعدادی کلاس تشکیل شده‌است. در این بین می‌خواهیم تعدادی از آن‌ها را به صورت ایزوله‌ی از کل سیستم آزمایش کنیم. البته باید درنظر داشت که این کلاس‌ها در حین اجرای واقعی برنامه، از تعدادی وابستگی خاص در همان سیستم استفاده می‌کنند. برای مثال کلاسی در این بین برای بررسی میزان اعتبار مالی یک کاربر، نیاز دارد تا با یک وب سرویس خارجی کار کند. اما چون می‌خواهیم این کلاس را به صورت ایزوله‌ی از کل سیستم آزمایش کنیم، اینبار بجای استفاده‌ی از وابستگی واقعی این کلاس، آن وابستگی را با یک نمونه‌ی تقلیدی یا Mock object در اینجا، جایگزین می‌کنیم.
بنابراین Mocking به معنای جایگزین کردن یک وابستگی واقعی سیستم که در زمان اجرای آن مورد استفاده قرار می‌گیرد، با نمونه‌ی تقلیدی مختص زمان آزمایش برنامه، جهت بالابردن سهولت نوشتن آزمون‌های واحد است.


دلایل و مزایای استفاده‌ی از Mocking

- یکی از مهم‌ترین دلایل استفاده‌ی از Mocking، کاهش پیچیدگی تنظیمات اولیه‌ی نوشتن آزمون‌های واحد است. برای مثال اگر در برنامه‌ی خود از تزریق وابستگی‌ها استفاده می‌کنید و کلاسی دارای چندین وابستگی تزریق شده‌ی به آن است، برای آزمایش این کلاس نیاز به تدارک تمام این وابستگی‌ها را خواهید داشت تا بتوان این کلاس را وهله سازی کرد و همچنین برنامه را نیز کامپایل نمود. اما در این بین ممکن است آزمایش متدی در همان کلاس، الزاما از تمام وابستگی‌های تزریق شده‌ی در یک کلاس استفاده نکند. در این حالت، Mocking می‌تواند تنظیمات پیچیده‌ی وهله سازی این کلاس را به حداقل برساند.
- Mocking می‌تواند سبب افزایش سرعت اجرای آزمون‌های واحد نیز شود. برای مثال با تقلید سرویس‌های خارجی مورد استفاده‌ی در برنامه (هر عملی که از مرزهای سیستم رد شود مانند کار با شبکه، بانک اطلاعاتی، فایل سیستم و غیره)، می‌توان میزان I/O و همچنین زمان صرف شده‌ی به آن‌را به حداقل رساند.
- از mock objects می‌توان برای رهایی از مشکلات کار با مقادیر غیرمشخص استفاده کرد. برای مثال اگر در کدهای خود از DateTime.Now استفاده می‌کنید یا اعداد اتفاقی و امثال آن، هربار که آزمون‌های واحد را اجرا می‌کنیم، خروجی متفاوتی را دریافت کرده و بسیاری از آزمون‌های نوشته شده با مشکل مواجه می‌شوند. به کمک mocking می‌توان بجای این مقادیر غیرمشخص، یک مقدار ثابت و مشخص را بازگشت دهد.
- چون به سادگی می‌توان mock objects را تهیه کرد، می‌توان کار توسعه و آزمایش برنامه را پیش از به پایان رسیدن پیاده سازی اصلی سرویس‌های مدنظر، همینقدر که اینترفیس آن سرویس مشخص باشد، شروع کرد که می‌تواند برای کارهای تیمی بسیار مفید باشد.
- اگر وابستگی مورد استفاده ناپایدار و یا غیرقابل پیش بینی است، می‌توان توسط mocking به یک نمونه‌ی قابل پیش بینی و پایدار مخصوص آزمون‌های برنامه رسید.
- اگر وابستگی خارجی مورد استفاده به ازای هر بار استفاده، هزینه‌ای را شارژ می‌کند، می‌توان توسط mocking، هزینه‌ی آزمون‌های برنامه را کاهش داد.


Unit test چیست؟

بدیهی است در کنار آزمایش ایزوله‌ی قسمت‌های مختلف برنامه توسط mocking، باید کل برنامه را جهت بررسی دستیابی به نتایج واقعی نیز آزمایش کرد که به این نوع آزمون‌ها، آزمون یکپارچگی (Integration Tests)، API Tests ،UI Tests و غیره می‌گویند که در کنار Unit tests ما حضور خواهند داشت. بنابراین اکنون این سؤال مطرح می‌شود که یک Unit چیست؟
در برنامه‌ای که از چندین کلاس تشکیل می‌شود، به یک کلاس، یک Unit گفته می‌شود. همچنین اگر در این سیستم، دو یا چند کلاس با هم کار می‌کنند (کلاسی که از چندین وابستگی استفاده می‌کند)، این‌ها با هم نیز یک Unit را تشکیل دهند. بنابراین تعریف Unit بستگی به نحوه‌ی درک عملکرد یک سیستم و تعامل اجزای آن با هم دارد.


واژه‌های متناظر با Mock objects

در حین مطالعه‌ی منابع مرتبط با آزمون‌های واحد ممکن است با این واژه‌های تقریبا مشابه مواجه شوید: fakes ،stubs ،dummies و mocks. اما تفاوت آن‌ها در چیست؟
- Fakes در حقیقت یک نمونه پیاده سازی واقعی، اما غیرمناسب محیط واقعی و اصلی پروژه‌است. برای نمونه EF Core به همراه یک نمونه in-memory database هم هست که دقیقا با مفهوم Fakes تطابق دارد.
- از Dummies صرفا جهت تهیه‌ی پارامترهای مورد نیاز برای اجرای یک آزمایش استفاده می‌شوند. این پارامترها، هیچگاه در آزمایش‌های انجام شده مورد استفاده قرار نمی‌گیرند.
- از Stubs برای ارائه‌ی پاسخ‌هایی مشخص به فراخوان‌ها استفاده می‌شود. برای مثال یک متد یا خاصیت، دقیقا چه چیزی را باید بازگشت دهند.
- از Mocks برای بررسی تعامل اجزای مختلف در حال آزمایش استفاده می‌شود. آیا متدی یا خاصیتی مورد استفاده قرار گرفته‌است یا خیر؟

باید درنظر داشت که زمانیکه یک شیء Mock را توسط کتابخانه‌ی Moq تهیه می‌کنیم، هر سه مفهوم stubs ،dummies و mocks را با هم به همراه دارد. به همین جهت در این سری زمانیکه به یک mock object اشاره می‌شود، هر سه مفهوم مدنظر هستند.

واژه‌ی دیگری که ممکن است در این گروه زیاد مشاهده شود، «Test double» نام دارد که ترکیب هر 4 مورد fakes ،stubs ،dummies و mocks می‌باشد. در کل هر زمانیکه یک شیء مورد استفاده‌ی در زمان اجرای برنامه را جهت آزمایش ساده‌تر آن جایگزین می‌کنید، یک Test double را ایجاد کرده‌اید.


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

در این سری قصد داریم یک برنامه‌ی وام دهی را آزمایش کنیم که قسمت‌های مختلف آن دارای وابستگی‌های خاصی می‌باشند. ساختار این برنامه را در ادامه مشاهده می‌کنید:


موجودیت‌های برنامه‌ی وام دهی
namespace Loans.Entities
{
    public class Applicant
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public int Age { set; get; }

        public string Address { set; get; }

        public decimal Salary { set; get; }
    }
}

namespace Loans.Entities
{
    public class LoanProduct
    {
        public int Id { set; get; }

        public string ProductName { set; get; }

        public decimal InterestRate { set; get; }
    }
}

namespace Loans.Entities
{
    public class LoanApplication
    {
        public int Id { set; get; }

        public LoanProduct Product { set; get; }

        public LoanAmount Amount { set; get; }

        public Applicant Applicant { set; get; }

        public bool IsAccepted { set; get; }
    }

    public class LoanAmount
    {
        public string CurrencyCode { get; set; }

        public decimal Principal { get; set; }
    }
}

مدل‌های برنامه‌ی وام دهی

namespace Loans.Models
{
    public class IdentityVerificationStatus
    {
        public bool Passed { get; set; }
    }
}

سرویس‌های برنامه‌ی وام دهی

using Loans.Models;

namespace Loans.Services.Contracts
{
    public interface IIdentityVerifier
    {
        void Initialize();

        bool Validate(string applicantName, int applicantAge, string applicantAddress);

        void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid);

        void Validate(string applicantName, int applicantAge, string applicantAddress,
            ref IdentityVerificationStatus status);
    }
}

namespace Loans.Services.Contracts
{
    public interface ICreditScorer
    {
        int Score { get; }

        void CalculateScore(string applicantName, string applicantAddress);
    }
}

using System;
using Loans.Entities;
using Loans.Services.Contracts;

namespace Loans.Services
{
    public class LoanApplicationProcessor
    {
        private const decimal MinimumSalary = 1_500_000_0;
        private const int MinimumAge = 18;
        private const int MinimumCreditScore = 100_000;

        private readonly IIdentityVerifier _identityVerifier;
        private readonly ICreditScorer _creditScorer;

        public LoanApplicationProcessor(
            IIdentityVerifier identityVerifier,
            ICreditScorer creditScorer)
        {
            _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier));
            _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer));
        }

        public bool Process(LoanApplication application)
        {
            application.IsAccepted = false;

            if (application.Applicant.Salary < MinimumSalary)
            {
                return application.IsAccepted;
            }

            if (application.Applicant.Age < MinimumAge)
            {
                return application.IsAccepted;
            }

            _identityVerifier.Initialize();

            var isValidIdentity = _identityVerifier.Validate(
                application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);

            if (!isValidIdentity)
            {
                return application.IsAccepted;
            }

            _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address);
            if (_creditScorer.Score < MinimumCreditScore)
            {
                return application.IsAccepted;
            }

            application.IsAccepted = true;
            return application.IsAccepted;
        }
    }
}

using System;
using Loans.Models;
using Loans.Services.Contracts;

namespace Loans.Services
{
    public class IdentityVerifierServiceGateway : IIdentityVerifier
    {
        public DateTime LastCheckTime { get; private set; }

        public void Initialize()
        {
            // Initialize connection to external service
        }

        public bool Validate(string applicantName, int applicantAge, string applicantAddress)
        {
            Connect();
            var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
            LastCheckTime = DateTime.Now;
            Disconnect();

            return isValidIdentity;
        }

        private void Connect()
        {
            // Open connection to external service
        }

        private bool CallService(string applicantName, int applicantAge, string applicantAddress)
        {
            // Make call to external service, interpret the response, and return result

            return false; // Simulate result for demo purposes
        }

        private void Disconnect()
        {
            // Close connection to external service
        }

        public void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid)
        {
            throw new NotImplementedException();
        }

        public void Validate(string applicantName, int applicantAge, string applicantAddress,
            ref IdentityVerificationStatus status)
        {
            throw new NotImplementedException();
        }
    }
}
توضیحات:
هدف از این برنامه، درخواست یک وام جدید است. Application در اینجا به معنای درخواست یا فرم جدید است و Applicant نیز شخصی است که این درخواست را داده‌است.
در اینجا بیشتر تمرکز ما بر روی کلاس LoanApplicationProcessor است که دارای دو وابستگی تزریق شده‌ی به آن نیز می‌باشد:
        public LoanApplicationProcessor(
            IIdentityVerifier identityVerifier,
            ICreditScorer creditScorer)
        {
            _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier));
            _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer));
        }
از این وابستگی‌ها برای تصدیق هویت درخواست کننده و همچنین بررسی میزان اعتبار او استفاده می‌شود.
تمام این منطق نیز در متد Process آن قابل مشاهده‌است که هدف اصلی آن، بررسی قابل پذیرش بودن درخواست یک وام جدید است.


نوشتن اولین تست، برای برنامه‌ی وام دهی

در اولین تصویر این قسمت، پروژه‌ی class library دومی را نیز به نام Loans.Tests مشاهده می‌کنید. فایل csproj آن به صورت زیر برای کار با MSTest تنظیم شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Loans\Loans.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
    <PackageReference Include="MSTest.TestAdapter" Version="2.0.0" />
    <PackageReference Include="MSTest.TestFramework" Version="2.0.0" />    
  </ItemGroup>
</Project>
که در آن ارجاعی به پروژه‌ی Loans.csproj و همچنین وابستگی‌های MSTest، تنظیم شده‌اند.

اکنون اولین آزمون واحد ما در کلاس جدید LoanApplicationProcessorShould چنین شکلی را پیدا می‌کند:
using Loans.Entities;
using Loans.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {
        [TestMethod]
        public void DeclineLowSalary()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};
            var processor = new LoanApplicationProcessor(null, null);
            processor.Process(application);

            Assert.IsFalse(application.IsAccepted);
        }
    }
}
در حین کار با MSTest، کلاس آزمون واحد باید به ویژگی TestClass و متدهای public void آن به ویژگی TestMethod مزین شوند تا توسط این فریم‌ورک آزمون واحد شناسایی شده و مورد آزمایش قرار گیرند.
در این آزمایش، شخص درخواست کننده، حقوق کمی دارد و می‌خواهیم بررسی کنیم که آیا LoanApplicationProcessor می‌تواند آن‌را بر اساس مقدار MinimumSalary، رد کند یا خیر؟
public class LoanApplicationProcessor
{
    private const decimal MinimumSalary = 1_500_000_0;

در حین وهله سازی LoanApplicationProcessor، دو وابستگی آن به null تنظیم شده‌اند؛ چون می‌دانیم که بررسی MinimumSalary پیش از سایر بررسی‌ها صورت می‌گیرد و اساسا در این آزمایش، نیازی به این وابستگی‌ها نداریم.
اما اگر سعی در اجرای این آزمایش کنیم (برای مثال با اجرای دستور dotnet test در خط فرمان)، آزمایش اجرا نشده و با استثنای زیر مواجه می‌شویم:
Test method Loans.Tests.LoanApplicationProcessorShould.DeclineLowSalary threw exception:
System.ArgumentNullException: Value cannot be null.
Parameter name: identityVerifier
چون در سازنده‌ی کلاس LoanApplicationProcessor، در صورت نال بودن وابستگی‌های دریافتی، یک استثناء صادر می‌شود. بنابراین ذکر آن‌ها الزامی است:
        public LoanApplicationProcessor(
            IIdentityVerifier identityVerifier,
            ICreditScorer creditScorer)
        {
            _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier));
            _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer));
        }


نصب کتابخانه‌ی Moq جهت برآورده کردن وابستگی‌های کلاس LoanApplicationProcessor

در این آزمایش چون وجود وابستگی‌های در سازنده‌ی کلاس، برای ما اهمیتی ندارند و همچنین ذکر آن‌ها نیز الزامی است، می‌خواهیم توسط کتابخانه‌ی Moq، دو نمونه‌ی تقلیدی از آن‌ها را تهیه کرده (همان dummies که پیشتر معرفی شدند) و جهت برآورده کردن بررسی صورت گرفته‌ی در سازنده‌ی کلاس LoanApplicationProcessor، آن‌ها را ارائه کنیم.
کتابخانه‌ی بسیار معروف Moq، با پروژه‌های مبتنی بر NETFramework 4.5. و همچنین NETStandard 2.0. به بعد سازگار است و برای نصب آن، می‌توان یکی از دو دستور زیر را صادر کرد:
> dotnet add package Moq
> Install-Package Moq

اما چرا کتابخانه‌ی Moq؟
کتابخانه‌ی Moq این اهداف را دنبال می‌کند: ساده‌است، به شدت کاربردی‌است و همچنین strongly typed است. این کتابخانه سورس باز بوده و تعداد بار دانلود بسته‌ی نیوگت آن میلیونی است.


پس از نصب آن، اولین آزمایشی را که نوشتیم، به صورت زیر اصلاح می‌کنیم:
using Loans.Entities;
using Loans.Services;
using Loans.Services.Contracts;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {
        [TestMethod]
        public void DeclineLowSalary()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};

            var mockIdentityVerifier = new Mock<IIdentityVerifier>();
            var mockCreditScorer = new Mock<ICreditScorer>();

            var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object);
            processor.Process(application);

            Assert.IsFalse(application.IsAccepted);
        }
    }
}
در اینجا بجای ارسال null به سازنده‌ی کلاس LoanApplicationProcessor، جهت برآورده کردن مقدار پیش‌فرض پارامترهای آن و کامپایل شدن برنامه، نمونه‌های تقلیدی دو وابستگی مورد نیاز آن‌را تهیه و به آن ارسال کرده‌ایم.
کار با ذکر new Mock شروع شده و آرگومان جنریک آن‌را از نوع وابستگی‌هایی که نیاز داریم، مقدار دهی می‌کنیم. سپس خاصیت Object آن، امکان دسترسی به این شیء تقلید شده را میسر می‌کند.
اکنون اگر مجددا این آزمون واحد را اجرا کنیم، مشاهده خواهیم کرد که بجای صدور استثناء، با موفقیت به پایان رسیده‌است:



گاهی از اوقات جایگزین کردن یک وابستگی null با نمونه‌ی Mock آن کافی نیست

در مثالی که بررسی کردیم، اشیاء mock، کار برآورده کردن نیازهای ابتدایی آزمایش را انجام داده و سبب اجرای موفقیت آمیز آن شدند؛ اما همیشه اینطور نیست:
using Loans.Entities;
using Loans.Services;
using Loans.Services.Contracts;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {        
        [TestMethod]
        public void Accept()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};

            var mockIdentityVerifier = new Mock<IIdentityVerifier>();
            var mockCreditScorer = new Mock<ICreditScorer>();

            var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object);
            processor.Process(application);

            Assert.IsTrue(application.IsAccepted);
        }
    }
}
تفاوت این آزمایش جدید با قبلی، در دو مورد است: مقدار Salary به MinimumSalary تنظیم شده‌است و در آخر Assert.IsTrue را داریم.
اگر این آزمایش را اجرا کنیم، با شکست مواجه خواهد شد. علت اینجا است که هرچند در حال استفاده‌ی از دو mock object به عنوان وابستگی‌های مورد نیاز هستیم، اما تنظیمات خاصی را بر روی آن‌ها انجام نداده‌ایم و به همین جهت خروجی مناسبی را در اختیار LoanApplicationProcessor قرار نمی‌دهند. برای مثال مرحله‌ی بعدی بررسی اعتبار شخص در کلاس LoanApplicationProcessor، فراخوانی سرویس identityVerifier و متد Validate آن است که خروجی آن بر اساس کدهای فعلی، همیشه false است:
_identityVerifier.Initialize();
var isValidIdentity = _identityVerifier.Validate(
    application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);
در قسمت بعدی، کار تنظیم اشیاء mock را انجام خواهیم داد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MoqSeries-01.zip
مطالب
استفاده از خواص راهبری در EF Code first جهت ساده سازی کوئر‌ی‌ها
گاهی از اوقات یافتن معادل LINQ کوئری‌های SQLایی که پیشتر به سادگی و بر اساس ممارست، در کسری از دقیقه نوشته می‌شدند، آنچنان ساده نیست. برای مثال فرض کنید یک سری پروژه وجود دارند که به ازای هر پروژه، تعدادی بازخورد ثبت شده است. هر بازخورد نیز دارای وضعیت‌هایی مانند «در حال انجام» و «انجام شد» است. می‌خواهیم کوئری LINQ سازگار با EF ایی را تهیه کنیم که تعداد موارد «در حال انجام» را نمایش دهد.
بر این اساس، کلاس‌های مدل دومین مساله به صورت زیر خواهند بود:
    public class Project
    {
        public int Id { set; get; }
        public string Name { set; get; }

        public virtual ICollection<ProjectIssue> ProjectIssues { set; get; }
    }

    public class ProjectIssue
    {
        public int Id { set; get; }
        public string Body { set; get; }

        [ForeignKey("ProjectStatusId")]
        public virtual ProjectIssueStatus ProjectIssueStatus { set; get; }
        public int ProjectStatusId { set; get; }

        [ForeignKey("ProjectId")]
        public virtual Project Project { set; get; }
        public int ProjectId { set; get; }
    }

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

        public virtual ICollection<ProjectIssue> ProjectIssues { set; get; }
    }
یک پروژه می‌تواند تعدادی Issue ثبت شده داشته باشد. هر Issue نیز دارای وضعیتی مشخص است.
اگر EF Code first را وادار به تهیه جداول و روابط معادل کلاس‌های فوق کنیم:
    public class MyContext : DbContext
    {
        public DbSet<ProjectIssueStatus> ProjectStatus { get; set; }
        public DbSet<ProjectIssue> ProjectIssues { get; set; }
        public DbSet<Project> Projects { get; set; }
    }

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

        protected override void Seed(MyContext context)
        {
            var project1 = new Project { Name = "پروژه جدید" };
            context.Projects.Add(project1);

            var stat1 = new ProjectIssueStatus { Name = "درحال انجام" };
            var stat2 = new ProjectIssueStatus { Name = "انجام شد" };
            context.ProjectStatus.Add(stat1);
            context.ProjectStatus.Add(stat2);

            var issue1 = new ProjectIssue
            {
                Body = "تغییر قلم گزارش",
                ProjectIssueStatus = stat1,
                Project = project1
            };
            var issue2 = new ProjectIssue
            {
                Body = "تغییر لوگوی گزارش",
                ProjectIssueStatus = stat1,
                Project = project1
            };
            context.ProjectIssues.Add(issue1);
            context.ProjectIssues.Add(issue2);

            base.Seed(context);
        }
    }
 به شکل زیر خواهیم رسید:


سابقا برای یافتن تعداد متناظر با هر IssueStatus خیلی سریع می‌شد چنین کوئری را نوشت:


اما اکنون معادل آن با EF Code first چیست؟
    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());

            using (var ctx = new MyContext())
            {
                var projectId = 1;
                var list = ctx.ProjectStatus.Select(x => new
                                                    {
                                                        Id = x.Id,
                                                        Name = x.Name,
                                                        Count = x.ProjectIssues.Count(p => p.ProjectId == projectId)
                                                    }).ToList();
                foreach (var item in list)
                    Console.WriteLine("{0}:{1}",item.Name, item.Count);                
            }
        }
    }
بله. همانطور که ملاحظه می‌کنید در اینجا به کوئری بسیار ساده و واضحی با کمک استفاده از navigation properties (خواص راهبری مانند ProjectIssues) تعریف شده رسیده‌ایم. خروجی SQL تولید شده توسط EF نیز به صورت زیر است:
SELECT [Project1].[Id] AS [Id],
       [Project1].[Name] AS [Name],
       [Project1].[C1] AS [C1]
FROM   (
           SELECT [Extent1].[Id] AS [Id],
                  [Extent1].[Name] AS [Name],
                  (
                      SELECT COUNT(1) AS [A1]
                      FROM   [dbo].[ProjectIssues] AS [Extent2]
                      WHERE  ([Extent1].[Id] = [Extent2].[ProjectStatusId])
                             AND ([Extent2].[ProjectId] = 1 /*@p__linq__0*/)
                  ) AS [C1]
           FROM   [dbo].[ProjectIssueStatus] AS [Extent1]
       ) AS [Project1]

مطالب
کنترل نوع‌های داده با استفاده از EF در SQL Server
ورود سیستم‌های ORM مانند EF تحولی عظیم در در مباحث کار و تغییرات بر روی داده‌ها یا Data Manipulation بود. به طور خلاصه اصلی‌ترین هدف یک ORM، ایجاد فرامین شیء گرا به جای فرامین رابطه‌ای است؛ ولی در این بین نکات دیگری هم مد نظر گرفته شده‌است که یکی از آن‌ها پشتیبانی از چندین دیتابیس هست تا توسعه گران از یک سیستم واحد جهت اتصال به همه‌ی دیتابیس‌ها استفاده کنند و نیازی به دانش اضافه و سیستم جداگانه‌ای برای هر دیتابیس نباشد؛ مانند ADO که در دات نت به چندین دسته نقسیم شده بود و هم اینکه در صورتی که تمایلی به تغییر دیتابیس در آینده داشتید، کدها برای توسعه باز باشند و نیازی به تغییر سیستم نباشد.
ولی اگر کمی بیشتر به دنیای واقعی وارد شویم گاهی اوقات نیاز است که تنها بر روی یک دیتابیس فعالیت کنیم و یک دیتابیس نیاز است تا حد ممکن بهینه طراحی شود تا کارآیی بانک در حال حاضر و به خصوص در آینده تا حدی تضمین شود.
من همیشه در مورد EF یک نظری داشتم و آن اینست که با اینکه یک ORM، یک هدف مهم را در نظر دارد و آن اینست که تا حد ممکن استانداردهایی را که بین تمامی دیتابیس‌ها مشترک است، رعایت کند، ولی باز قابل قبول است اگر بگوییم که کاربران EF انتظار داشته باشند تا اطلاعات بیشتری در مورد sql server در آن نهفته باشد. از یک سو هر دو محصول مایکروسافت هستند و از سوی دیگر مطمئنا توسعه گران محصولات دات نت بیش از هر چیزی به sql server نگاه ویژه‌تری دارند. پس مایکروسافت در کنار حفظ آن ویژگی‌های مشترک، باید به حفظ استانداردهای جدایی برای sql server هم باشد.

تعدادی از برنامه نویسان در هنگام ایجاد Domain Model کم لطفی‌های زیادی را می‌کنند که یکی از آن‌ها عدم کنترل نوع داده‌های خود است. مثلا برای رشته‌ها هیچ محدودیتی را در نظر نمی‌گیرند. شاید در سمت کلاینت اینکار را انجام می‌دهند؛ ولی نکته‌ی مهم در طرف دیتابیس است که چگونه تعریف می‌شود. در این حالت (nvarchar(MAX در نظر گرفته میشود که به معنی اشاره به منطقه دوگیگابایتی از اطلاعات است. در نکات بعدی، قصد داریم این مرحله را یک گام به جلوتر پیش ببریم و آن هم ایجاد نوع داده‌های بهینه‌تر در Sql Server است.

نکته مهم: بدیهی است که تغییرات زیر، ORM شما را تنها به sql server مقید می‌کند که بعدها در صورت تغییر دیتابیس نیاز به حذف موارد زیر را خواهید داشت؛ در غیر اینصورت به مشکل عدم ایجاد دیتابیس برخواهید خورد.

اولین مورد مهم بحث تاریخ و زمان است؛ وقتی ما یک نوع داده را تنها DateTime در نظر بگیریم، در Sql Server هم همین نوع داده وجود دارد و انتخاب میشود. ولی اگر شما واقعا نیازی به این نوع داده نداشته باشید چطور؟ در حال حاضر من بر روی یک برنامه‌ی کارخانه کار میکنم که بخش کارمندان و گارگران آن سه داده زمانی زیر را شامل می‌شود:
        public DateTime BirthDate { get; set; }
        public DateTime HireDate { get; set; }
        public DateTime? LeaveDate { get; set; }

حال به جدول زیر نگاه کنید که هر نوع داده چه مقدار فضا را به خود اختصاص می‌دهد:
  SmallDateTime   4 بایت
  DateTime  8 بایت
  DateTime2  6 تا 8 بایت
  DateTimeOffset   8 تا 10 بایت
  Date  3 بایت
  Time  3 تا 5 بایت

از این جدول چه می‌فهمید؟ با یک نگاه می‌توان فهمید که ساختار بالای من باید 24 بایت گرفته باشد؛ برای ساختاری که هم تاریخ و هم زمان (ساعت) را پشتیبانی می‌کند. ولی با نگاه دقیق‌تر به نام پراپرتی‌ها این نکته روشن می‌شود که ما یک گپ Gap (فضای بیهوده) داریم چون زمان تولد، استخدام و ترک سازمان اصلا نیازی به ساعت ندارند و همان تاریخ کافی است. یعنی نوع Date با حجم کلی 9 بایت؛ که در نتیجه 15 بایت صرفه جویی در یک رکورد صورت خواهد گرفت.

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

          [Column(TypeName = "date")]
        public DateTime BirthDate { get; set; }

        [Column(TypeName = "date")]
        public DateTime HireDate { get; set; }

        [Column(TypeName = "date")]
        public DateTime? LeaveDate { get; set; }
خصوصیت Column از نسخه 4.5دات نت فریم ورک اضافه شده و در فضای نام System.ComponentModel.DataAnnotations.Schema قرار گرفته است.
نوع‌هایی که در بالا با سایز متغیر هستند، به نسبت دقتی که برای آن تعیین می‌کنید، سایز می‌گیرند. مثل (time(0 که 3 بایت از حافظه را می‌گیرد. در صورتی که time معرفی کنید، به جای اینکه از شیء DateTime استفاده کنید، از شی Timespan استفاده کنید، تا در پشت صحنه از نوع داده time استفاده کند. در این حالت حداکثر حافظه یعنی 5 بایت را برخواهد داشت و بهترین حالت ممکن این هست که نیاز خود را بسنجید و خودتان دقت آن را مشخص کنید. دو شکل زیر نحوه‌ی تعریف نوع زمان را مشخص می‌کنند. یکی حالت پیش فرض و دیگری انتخاب دقت:
     public class Testtypes
    {
           public TimeSpan CloseTime { get; set; }
            public TimeSpan CloseTime2 { get; set; }
    }
   public class TestConfig : EntityTypeConfiguration<Testtypes>
    {
        public TestConfig()
        {
            this.Property(x => x.CloseTime2).HasPrecision(3);
        }
    }
در تکه کد بالا همه از نوع time تعریف می‌شوند ولی در خصوصیت شماره یک نهایت استفاده از نوع تایم یعنی (time(7 مشخص می‌شود. ولی در خصوصیت بعدی چون در کانفیگ دقت آن را مشخص کرده‌ایم از نوع (time(3 تعریف می‌شود.

مورد دوم در مورد داده‌های اعشاری است:
بسیاری از برنامه نویسان تا آنجا که دیده‌ام از نوع float و single و double برای اعداد اعشاری استفاده می‌کنند ولی باید دید که در آن سمت دیتابیس، برای این نوع داده‌ها چه اتفاقی می‌افتد. نوع float در دات نت، با نوع single برابری میکند؛ هر دو یک نام جدا دارند، ولی در واقع یکی هستند. عموما برنامه نویسان به طور کلی بیشتر از همان single استفاده می‌کنند و برای انتساب برای این دو نوع هم حتما باید حرف f را بعد از عدد نوشت:
float flExample=23.2f;
باید توجه کنید که اگر مثلا float انتخاب کردید، تصور نکنید که همان float در دیتابیس خواهد بود. این دو متفاوت هستند تبدیلات به شکل زیر رخ می‌دهد:
//real
public float FloatData { get; set; }
//real
public Single SingleData { get; set; }
//float
public double DoubleData { get; set; }
  همه نوع‌های بالا اعداد اعشاری هستند که به صورت تقریبی و به صورت نماد اعشاری ذخیره می‌گردند و برای به دست آوردن مقدار ذخیره شده، هیچ تضمینی نیست همان عددی که وارد شده است بازگردانده شود. اگر تا به حال در برنامه هایتان به چنین مشکلی برنخوردید دلیلش اعداد اعشاری کوچک بوده است. ولی با بزرگتر شدن عدد، این تفاوت به خوبی دیده می‌شود. 
حالا اگر بخواهیم اعداد اعشاری را به طور دقیق ذخیره کنیم، مجبور به استفاده از نوع decimal هستیم. در دات نت آنچنان محدودیتی بر سر استفاده‌ی از آن نداریم. ولی در سمت سرور داده‌ها بهتر هست برای آن تدابیری اندیشیده شود. هر عدد دسیمال از دقت و مقیاس تشکیل می‌شود. دقت آن تعداد ارقامی است که در عدد وجود دارد و مقیاس آن تعداد ارقام اعشاری است. به عنوان مثال عدد زیر دقتش 7 و مقیاسش 3 است:
4235.254
در صورتی که عدد اعشاری را به دسیمال نسبت دهیم باید حرف m را بعد از عدد وارد کنیم:
decimal d1=4545.112m;
برای اعداد صحیح نیازی نیست.
برای تعیین نوع دسیمال از  fluent api استفاده می‌کنیم:
modelBuilder.Entity<Class>().Property(object => object.property).HasPrecision(7, 3);
کد زیر برای خصوصیت شماره یک، دقت 18 و مقیاس 2 را در نظر می‌گیرد و دومین خصوصیت طبق آنچه که برایش تعریف کرده ایم دقت 7 و مقیاس 3 است:
     public class Testtypes
    {
        public Decimal Decimal1 { get; set; }
        public Decimal Decimal2 { get; set; }

     }

 public class TestConfig : EntityTypeConfiguration<Testtypes>
    {
        public TestConfig()
        {
            this.Property(x => x.Decimal2).HasPrecision(7, 3);
        }
    }

مورد سوم مبحث رشته هاست
:
کدهای زیر را مطالعه فرمایید:
[StringLength(25)]
public string FirstName { get; set; }

[StringLength(30)]
[Column(TypeName = "varchar")]
public string EnProductTitle { get; set; }


public string ArticleContent { get; set; }
[Column(TypeName = "varchar(max)")]
public string ArticleContentEn { get; set; }
اولین رشته بالا (نام) را به محدوده‌ای از کاراکترها محدود کرده‌ایم. به طور پیش فرض تمامی رشته‌ها به صورت nvarchar در نظر گرفته می‌شوند. بدین ترتیب در رشته نام کوچک (nvarchar(25 در نظر گرفته خواهد شد. حال اگر بخواهیم فقط حروف انگلیسی پشتیبانی شوند، مثلا نام فنی کالا را بخواهید وارد کنید، بهتر هست که نوع آن به طرز صحیحی تعریف شود که در کد بالا با استفاده از خصوصیت Column نوع varchar را معرفی می‌کنم. بدین ترتیب تعریف نهایی نوع به شکل (varchar(30 خواهد بود. استفاده از fluentApi‌ها هم در این رابطه به شکل زیر است:
this.Property(e => e.EnProductTitle).HasColumnType("VARCHAR").HasMaxLength(30);
برای مواردی که محدوده‌ای تعریف نشود (nvarchar(MAX در نظر گرفته میشود مانند پراپرتی ArticleContent بالا. ولی اگر قصد دارید فقط حروف اسکی پشتیبانی گردند، مثلا متن انگلیسی مقاله را نیز نگه می‌دارید بهتر هست که نوع آن بهیته‌ترین حالت در نظر گرفته شود که برای پراپرتی ArticleContentEn نوع (varchar(MAX تعریف کرده‌ایم. 
همانطور که گفتیم پیش فرض رشته‌ها nvarchar است، در صورتی که دوست دارید این پیش فرض را تغییر دهید روش زیر را دنبال کنید:
modelBuilder.Properties<string>().Configure(c => c.HasColumnType("varchar"));


//=========== یا

modelBuilder.Properties<string>().Configure(c => c.IsUnicode(false));


جهت تکمیل بحث نیز هر کدام از متغیرهای عددی در سی شارپ معادل نوع‌های زیر در Sql Server هستند:
//tinyInt
public byte Age { get; set; }

//smallInt
public Int16 OldInt { get; set; }

//int
public int Int32 { get; set; }

//Bigint
public Int64 HighNumbers { get; set; }

نظرات مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت دوم
سلام ، مشکلی که من با Automapper دارم اینه 
 کلاسی به شکل زیر دارم 
 public class ProductType:BaseEntity
    {
        #region Field
        private string persianTitle;
        private string englishTitle;
        private IList<Product> products;
        #endregion
        #region Memebr
        public string PersianTitle
        {
            get
            {
                return (persianTitle);
            }
            set
            {
                persianTitle = Microsoft.Security.Application.Encoder.HtmlEncode(value);
            }
        }
        public string EnglishTitle
        {
            get
            {
                return (englishTitle);
            }
            set
            {
                englishTitle = Microsoft.Security.Application.Encoder.HtmlEncode(value);
            }
        }
        public virtual IList<Product> Products
        {
            get
            {
                return (products);
            }
            set
            {
                products = value;
            }
        }
        #endregion
    }
و خوب کلاسی با عنوان Product  هم موجوده  با کدی به این شکل سعی در آپدیت کردن این کلاس دارم
[HttpPost]
        public ActionResult Update(ProductTypeViewModel productTypeViewModel)
        {
            if (productTypeViewModel.ExaminId())
            {
                ProductType productType;
                productType = _productType.Find(x => x.Id == productTypeViewModel.Id);
                AutoMapper.Mapper.Map(productTypeViewModel, productType);
                _uow.SaveChanges();
            }
            return RedirectToAction("Index");
        }
متاسفانه خطا رخ میده ، خطایی مربوط به Context ظاهرا اینم پیام خطا 
issing type map configuration or unsupported mapping.

Mapping types:
ProductTypeViewModel -> ProductType_5334DF7BAFBE780DF5328E2D6DF2A0DC3350F23340BE2EE2FC506AE9EDEC38DA
MvcUserInterface.Areas.Management.Models.ProductTypeViewModel -> System.Data.Entity.DynamicProxies.ProductType_5334DF7BAFBE780DF5328E2D6DF2A0DC3350F23340BE2EE2FC506AE9EDEC38DA

Destination path:
ProductType_5334DF7BAFBE780DF5328E2D6DF2A0DC3350F23340BE2EE2FC506AE9EDEC38DA

Source value:
MvcUserInterface.Areas.Management.Models.ProductTypeViewModel
ممنون میشم اگه کسی تجربه ای داره کمکم کنه من برای پیاده سازی لایه‌های سرویس ، دامین و دیتا از روشی که آقای نصیری گفته استفاده کردم . ویرایش کلاس هایی که دارای عضوی به صورت لیست از کلاس دیگر هستند چگونه باید باشه ؟ برای map کردن این کلاس‌ها کار خاصی باید انجام بشه ؟
اشتراک‌ها
دریافت آرگومان‌های خط فرمان با Command Line Parser Library
class Options {
  [Option('r', "read", Required = true,
    HelpText = "Input files to be processed.")]
  public IEnumerable<string> InputFiles { get; set; }

  // Omitting long name, default --verbose
  [Option(
    HelpText = "Prints all messages to standard output.")]
  public bool Verbose { get; set; }

  [Option(Default = "中文",
    HelpText = "Content language.")]
  public string Language { get; set; }

  [Value(0, MetaName = "offset",
    HelpText = "File offset.")]
  public long? Offset { get; set;}  
}
دریافت آرگومان‌های خط فرمان با Command Line Parser Library
مطالب
معماری لایه بندی نرم افزار #3

Service Layer

نقش لایه‌ی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایه‌ی Facade نیز می‌شناسند. این لایه، داده‌ها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایه‌ی Presentation فراهم می‌کند. کلاس View Model یک Strongly Typed محسوب می‌شود که نماهای خاصی از داده‌ها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه می‌نماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.

الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interface‌ها و زیر سیستم‌های پیچیده ارائه می‌کند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.

کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductViewModel
{
    Public int ProductId {get; set;}
    public string Name { get; set; }
    public string Rrp { get; set; }
    public string SellingPrice { get; set; }
    public string Discount { get; set; }
    public string Savings { get; set; }
}

برای اینکه کلاینت با لایه‌ی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه می‌شود و پارامترهای مورد نیاز را فراهم می‌کند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

using SoCPatterns.Layered.Model;

namespace SoCPatterns.Layered.Service
{
    public class ProductListRequest
    {
        public CustomerType CustomerType { get; set; }
    }
}

در شی Response نیز بررسی می‌کنیم که درخواست به درستی انجام شده باشد، داده‌های مورد نیاز را برای کلاینت فراهم می‌کنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال می‌نماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductListResponse
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public IList<ProductViewModel> Products { get; set; }
}

به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما می‌توانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمی‌باشند. بنابراین بهترین انتخاب، استفاده از Extension Method‌ها می‌باشد که باید برای کلاس Product و در لایه‌ی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public static class ProductMapperExtensionMethods
{
    public static ProductViewModel ConvertToProductViewModel(this Model.Product product)
    {
        ProductViewModel productViewModel = new ProductViewModel();
        productViewModel.ProductId = product.Id;
        productViewModel.Name = product.Name;
        productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP);
        productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice);
        if (product.Price.Discount > 0)
            productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount);
        if (product.Price.Savings < 1 && product.Price.Savings > 0)
            productViewModel.Savings = product.Price.Savings.ToString(“#%”);
        return productViewModel;
    }
    public static IList<ProductViewModel> ConvertToProductListViewModel(
        this IList<Model.Product> products)
    {
        IList<ProductViewModel> productViewModels = new List<ProductViewModel>();
        foreach(Model.Product p in products)
        {
            productViewModels.Add(p.ConvertToProductViewModel());
        }
        return productViewModels;
    }
}

حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد می‌نماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductService
{
    private Model.ProductService _productService;
    public ProductService(Model.ProductService ProductService)
    {
        _productService = ProductService;
    }
    public ProductListResponse GetAllProductsFor(
        ProductListRequest productListRequest)
    {
        ProductListResponse productListResponse = new ProductListResponse();
        try
        {
            IList<Model.Product> productEntities =
                _productService.GetAllProductsFor(productListRequest.CustomerType);
            productListResponse.Products = productEntities.ConvertToProductListViewModel();
            productListResponse.Success = true;
        }
        catch (Exception ex)
        {
            // Log the exception…
            productListResponse.Success = false;
            // Return a friendly error message
            productListResponse.Message = ex.Message;
        }
        return productListResponse;
    }
}

کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال می‌کند. همچنین این لایه محل مناسبی برای Log کردن خطاها می‌باشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer می‌پردازیم.

Data Layer

برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:

برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده می‌کنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه می‌کنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.

از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.

اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SoCPatterns.Layered.Model;

namespace SoCPatterns.Layered.Repository
{
    public class ProductRepository : IProductRepository
    {
        public IList<Model.Product> FindAll()
        {
            var products = from p in new ShopDataContext().Products
                                select new Model.Product
                                {
                                    Id = p.ProductId,
                                    Name = p.ProductName,
                                    Price = new Model.Price(p.Rrp, p.SellingPrice)
                                };
            return products.ToList();
        }
    }
}

در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایه‌ی Data هم به پایان رسید و در ادامه به کدنویسی لایه‌ی Presentation و UI می‌پردازیم.

Presentation Layer

به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده می‌کنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:

using SoCPatterns.Layered.Service;

public interface IProductListView
{
    void Display(IList<ProductViewModel> Products);
    Model.CustomerType CustomerType { get; }
    string ErrorMessage { set; }
}

این Interface توسط Web Form‌های ASP.NET و یا Win Form‌ها باید پیاده سازی شوند. کار با Interface‌ها موجب می‌شود تا تست View‌ها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:

using SoCPatterns.Layered.Service;

namespace SoCPatterns.Layered.Presentation
{
    public class ProductListPresenter
    {
        private IProductListView _productListView;
        private Service.ProductService _productService;
        public ProductListPresenter(IProductListView ProductListView,
            Service.ProductService ProductService)
        {
            _productService = ProductService;
            _productListView = ProductListView;
        }
        public void Display()
        {
            ProductListRequest productListRequest = new ProductListRequest();
            productListRequest.CustomerType = _productListView.CustomerType;
            ProductListResponse productResponse =
                _productService.GetAllProductsFor(productListRequest);
            if (productResponse.Success)
            {
                _productListView.Display(productResponse.Products);
            }
            else
            {
                _productListView.ErrorMessage = productResponse.Message;
            }
        }
    }
}

کلاس Presenter وظیفه‌ی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایه‌ی Presentation به پایان رسیده است. از مزایای وجود لایه‌ی Presentation این است که تست نویسی مربوط به نمایش داده‌ها و تعامل بین کاربر و سیستم به سهولت انجام می‌شود بدون آنکه نگران دشواری Unit Test نویسی Web Form‌ها باشید. حال می‌توانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت.