مطالب
استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First
یکی از روش‌های تهیه‌ی برنامه‌های چند مستاجری، ایجاد بانک‌های اطلاعاتی مستقلی به ازای هر مشتری است؛ یا نمونه‌ی دیگر آن، برنامه‌هایی هستند که اطلاعات هر سال را در یک بانک اطلاعاتی جداگانه نگه‌داری می‌کنند. در ادامه قصد داریم، نحوه‌ی کار با این بانک‌های اطلاعاتی را به صورت همزمان، توسط EF Code first و در حالت استفاده از الگوی واحد کار و تزریق وابستگی‌ها، به همراه فعال سازی خودکار مباحث migrations و به روز رسانی ساختار تمام بانک‌های اطلاعاتی مورد استفاده، بررسی کنیم.


مشخص سازی رشته‌های متفاوت اتصالی

فرض کنید برنامه‌ی جاری شما قرار است از دو بانک اطلاعاتی مشخص استفاده کند که تعاریف رشته‌های اتصالی آن‌ها در وب کانفیگ به صورت ذیل مشخص شده‌اند:
  <connectionStrings>
    <clear />
    <add name="Sample07Context" connectionString="Data Source=(local);Initial Catalog=TestDbIoC;Integrated Security = true" providerName="System.Data.SqlClient" />
    <add name="Database2012" connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true" providerName="System.Data.SqlClient" />
  </connectionStrings>
البته، ذکر این مورد کاملا اختیاری است و می‌توان رشته‌های اتصالی را به صورت پویا نیز در زمان اجرا مشخص و مقدار دهی کرد.


تغییر Context برنامه جهت پذیرش رشته‌های اتصالی پویای قابل تغییر در زمان اجرا

اکنون که قرار است کاربران در حین ورود به برنامه، بانک اطلاعاتی مدنظر خود را انتخاب کنند و یا سیستم قرار است به ازای کاربری خاص، رشته‌ی اتصالی خاص او را به Context ارسال کند، نیاز است Context برنامه را به صورت ذیل تغییر دهیم:
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DomainClasses;
 
namespace EF_Sample07.DataLayer.Context
{
    public class Sample07Context : DbContext, IUnitOfWork
    {
        public DbSet<Category> Categories { set; get; }
        public DbSet<Product> Products { set; get; }
 
        /// <summary>
        /// It looks for a connection string named Sample07Context in the web.config file.
        /// </summary>
        public Sample07Context()
            : base("Sample07Context")
        {
        }
 
        /// <summary>
        /// To change the connection string at runtime. See the SmObjectFactory class for more info.
        /// </summary>
        public Sample07Context(string connectionString)
            : base(connectionString)
        {
            //Note: defaultConnectionFactory in the web.config file should be set.
        }
 
 
        public void SetConnectionString(string connectionString)
        {
            this.Database.Connection.ConnectionString = connectionString;
        }
    }
}
در اینجا دو متد سازنده را مشاهده می‌کنید. سازنده‌ی پیش فرض، از رشته‌ای اتصالی با نامی مساوی Sample07Context استفاده می‌کند و سازنده‌ی دوم، امکان پذیرش یک رشته‌ی اتصالی پویا را دارد. مقدار پارامتر ورودی آن می‌تواند نام رشته‌ی اتصالی و یا حتی مقدار کامل رشته‌ی اتصالی باشد. حالت پذیرش نام رشته‌ی اتصالی زمانی مفید است که همانند مثال ابتدای بحث، این نام‌ها را پیشتر در فایل کانفیگ برنامه ثبت کرده باشید و حالت پذیرش مقدار کامل رشته‌ی اتصالی، جهت مقدار دهی پویای آن بدون نیاز به ثبت اطلاعاتی در فایل کانفیگ برنامه مفید است.

یک متد دیگر هم در اینجا در انتهای کلاس به نام SetConnectionString تعریف شده‌است. از این متد در حین ورود کاربر به سایت می‌توان استفاده کرد. برای مثال حداقل دو نوع طراحی را می‌توان درنظر گرفت:
الف) کاربر با برنامه‌ای کار می‌کند که به ازای سال‌های مختلف، بانک‌های اطلاعاتی مختلفی دارد و در ابتدای ورود، یک drop down انتخاب سال کاری برای او درنظر گرفته شده‌است (علاوه بر سایر ورودی‌های استانداردی مانند نام کاربری و کلمه‌ی عبور). در این حالت بهتر است متد SetConnectionString نام رشته‌ی اتصالی را بر اساس سال انتخابی، در حین لاگین دریافت کند که اطلاعات آن در فایل کانفیگ سایت پیشتر مشخص شده‌است.
ب) کاربر یا مشتری پس از ورود به سایت، نیاز است صرفا از بانک اطلاعاتی خاص خودش استفاده کند. بنابراین اطلاعات تعریف کاربران و مشتری‌ها در یک بانک اطلاعاتی مجزا قرار دارند و پس از لاگین، نیاز است رشته‌ی اتصالی او به صورت پویا از بانک اطلاعاتی خوانده شده و سپس توسط متد SetConnectionString تنظیم گردد.


مدیریت سشن‌های رشته‌ی اتصالی جاری

پس از اینکه کاربر، در حین ورود مشخص کرد که از چه بانک اطلاعاتی قرار است استفاده کند و یا اینکه برنامه بر اساس اطلاعات ثبت شده‌ی او تصمیم‌گیری کرد که باید از کدام رشته‌ی اتصالی استفاده کند، نگهداری این رشته‌ی اتصالی نیاز به سشن دارد تا به ازای هر کاربر متصل به سایت منحصربفرد باشد. در مورد مدیریت سشن‌ها در برنامه‌های وب، از نکات مطرح شده‌ی در مطلب «مدیریت سشن‌ها در برنامه‌های وب به کمک تزریق وابستگی‌ها» استفاده خواهیم کرد:
using System;
using System.Threading;
using System.Web;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap;
using StructureMap.Web;
using StructureMap.Web.Pipeline;
 
namespace EF_Sample07.IoCConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        public static void HttpContextDisposeAndClearAll()
        {
            HttpContextLifecycle.DisposeAndClearAll();
        }
 
        private static Container defaultContainer()
        {
            return new Container(ioc =>
            {
                // session manager setup
                ioc.For<ISessionProvider>().Use<DefaultWebSessionProvider>();
                ioc.For<HttpSessionStateBase>()
                   .Use(ctx => new HttpSessionStateWrapper(HttpContext.Current.Session));
 
                ioc.For<IUnitOfWork>()
                   .HybridHttpOrThreadLocalScoped()
                   .Use<Sample07Context>()
                    // Remove these 2 lines if you want to use a connection string named Sample07Context, defined in the web.config file.
                   .Ctor<string>("connectionString")
                   .Is(ctx => getCurrentConnectionString(ctx));
 
                ioc.For<ICategoryService>().Use<EfCategoryService>();
                ioc.For<IProductService>().Use<EfProductService>();
 
                ioc.For<ICategoryService>().Use<EfCategoryService>();
                ioc.For<IProductService>().Use<EfProductService>();
 
                ioc.Policies.SetAllProperties(properties =>
                {
                    properties.OfType<IUnitOfWork>();
                    properties.OfType<ICategoryService>();
                    properties.OfType<IProductService>();
                    properties.OfType<ISessionProvider>();
                });
            });
        }
 
        private static string getCurrentConnectionString(IContext ctx)
        {
            if (HttpContext.Current != null)
            {
                // this is a web application
                var sessionProvider = ctx.GetInstance<ISessionProvider>();
                var connectionString = sessionProvider.Get<string>("CurrentConnectionString");
                if (string.IsNullOrWhiteSpace(connectionString))
                {
                    // It's a default connectionString.
                    connectionString = "Database2012";
                    // this session value should be set during the login phase
                    sessionProvider.Store("CurrentConnectionStringName", connectionString);
                }
 
                return connectionString;
            }
            else
            {
                // this is a desktop application, so you can store this value in a global static variable.
                return "Database2012";
            }
        }
    }
}
در اینجا نحوه‌ی پویا سازی تامین رشته‌ی اتصالی را مشاهده می‌کنید. در مورد اینترفیس ISessionProvider و کلاس پایه HttpSessionStateBase پیشتر در مطلب «مدیریت سشن‌ها در برنامه‌های وب به کمک تزریق وابستگی‌ها» بحث شد.
نکته‌ی مهم این تنظیمات، قسمت مقدار دهی سازنده‌ی کلاس Context برنامه به صورت پویا توسط IoC Container جاری است. در اینجا هر زمانیکه قرار است وهله‌ای از Sample07Context ساخته شود، از سازنده‌ی دوم آن که دارای پارامتری به نام connectionString است، استفاده خواهد شد. همچنین مقدار آن به صورت پویا از متد getCurrentConnectionString که در انتهای کلاس تعریف شده‌است، دریافت می‌گردد.
در این متد ابتدا مقدار HttpContext.Current بررسی شده‌است. این مقدار اگر نال باشد، یعنی برنامه‌ی جاری یک برنامه‌ی دسکتاپ است و مدیریت رشته‌ی اتصالی جاری آن‌را توسط یک خاصیت Static یا Singleton تعریف شده‌ی در برنامه نیز می‌توان تامین کرد. از این جهت که در هر زمان، تنها یک کاربر در App Domain جاری برنامه‌ی دسکتاپ می‌تواند وجود داشته باشد و Singleton یا Static تعریف شدن اطلاعات رشته‌ی اتصالی، مشکلی را ایجاد نمی‌کند. اما در برنامه‌های وب، چندین کاربر در یک App Domain به سیستم وارد می‌شوند. به همین جهت است که مشاهده می‌کنید در اینجا از تامین کننده‌ی سشن، برای نگهداری اطلاعات رشته‌ی اتصالی جاری کمک گرفته شده‌است.

کلید این سشن نیز در این مثال مساوی CurrentConnectionStringName تعریف شده‌است. بنابراین در حین لاگین موفقیت آمیز کاربر، دو مرحله‌ی زیر باید طی شوند:
 sessionProvider.Store("CurrentConnectionString", "Sample07Context");
uow.SetConnectionString(WebConfigurationManager.ConnectionStrings[_sessionProvider.Get<string>("CurrentConnectionString")].ConnectionString);
ابتدا باید سشن CurrentConnectionStringName به بانک اطلاعاتی انتخابی کاربر تنظیم شود. برای نمونه در این مثال خاص، از نام رشته‌ی اتصالی مشخص شده‌ی در وب کانفیگ برنامه (مثال ابتدای بحث) به نام Sample07Context استفاده شده‌است.
سپس از متد SetConnectionString برای خواندن مقدار نام مشخص شده در سشن CurrentConnectionStringName کمک گرفته‌ایم. هرچند سازنده‌ی کلاس Context برنامه، هر دو حالت استفاده از نام رشته‌ی اتصالی و یا مقدار کامل رشته‌ی اتصالی را پشتیبانی می‌کند، اما خاصیت this.Database.Connection.ConnectionString تنها رشته‌ی کامل اتصالی را می‌پذیرد (بکار رفته در متد SetConnectionString).

تا اینجا کار پویا سازی انتخاب و استفاده از رشته‌ی اتصالی برنامه به پایان می‌رسد. هر زمانیکه قرار است Context برنامه توسط IoC Container نمونه سازی شود، به متد getCurrentConnectionString رجوع کرده و مقدار رشته‌ی اتصالی را از سشن تنظیم شده‌‌ای به نام CurrentConnectionStringName دریافت می‌کند. سپس از مقدار آن جهت مقدار دهی سازنده‌ی دوم کلاس Context استفاده خواهد کرد.


مدیریت migrations خودکار برنامه در حالت استفاده از چندین بانک اطلاعاتی

یکی از مشکلات کار با برنامه‌های چند دیتابیسی، به روز رسانی ساختار تمام بانک‌های اطلاعاتی مورد استفاده، پس از تغییری در ساختار مدل‌های برنامه است. از این جهت که اگر تمام بانک‌های اطلاعاتی به روز نشوند، کوئری‌های جدید برنامه که از خواص و فیلدهای جدید استفاده می‌کنند، دیگر کار نخواهند کرد. پویا سازی اعمال این تغییرات را می‌توان به صورت ذیل انجام داد:
using System;
using System.Data.Entity;
using System.Web;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.IoCConfig;
 
namespace EF_Sample07.WebFormsAppSample
{
    public class Global : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            initDatabases();
        }
 
        private static void initDatabases()
        {
            // defined in web.config
            string[] connectionStringNames =
            {
                "Sample07Context",
                "Database2012"
            };
 
            foreach (var connectionStringName in connectionStringNames)
            {
                Database.SetInitializer(
                    new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>(connectionStringName));
 
                using (var ctx = new Sample07Context(connectionStringName))
                {
                    ctx.Database.Initialize(force: true);
                }
            }
        }
 
        void Application_EndRequest(object sender, EventArgs e)
        {
            SmObjectFactory.HttpContextDisposeAndClearAll();
        } 
    }
}
نکته‌ی مهمی که در اینجا بکار گرفته شده‌است، مشخص سازی صریح سازنده‌ی شیء MigrateDatabaseToLatestVersion است. به صورت معمول در اکثر برنامه‌های تک دیتابیسی، نیازی به مشخص سازی پارامتر سازنده‌ی این کلاس نیست و در این حالت از سازنده‌ی بدون پارامتر کلاس Context برنامه استفاده خواهد شد. اما اگر سازنده‌ی آن‌را مشخص کنیم، به صورت خودکار از متد سازنده‌ای در کلاس Context استفاده می‌کند که پارامتر رشته‌ی اتصالی را به صورت پویا می‌پذیرد.
در این مثال خاص، متد initDatabases در حین آغاز برنامه فراخوانی شده‌است. منظور این است که اینکار در طول عمر برنامه تنها کافی است یکبار انجام شود و پس از آن است که EF Code first می‌تواند از رشته‌های اتصالی متفاوتی که به آن ارسال می‌شود، بدون مشکل استفاده کند. زیرا اطلاعات نگاشت کلاس‌های مدل برنامه به جداول بانک اطلاعاتی به این ترتیب است که کش می‌شوند و یا بر اساس کلاس Configuration به صورت خودکار به بانک اطلاعاتی اعمال می‌گردند.


کدهای کامل این مثال را که در حقیقت نمونه‌ی بهبود یافته‌ی مطلب «EF Code First #12» است، از اینجا می‌توانید دریافت کنید:
UoW-Sample
مطالب
استفاده از چندین Context در EF 6 Code first
در نگارش قبلی EF Code first به ازای یک پروژه تنها یک سیستم Migration قابل تعریف بود و این سیستم مهاجرت، تنها با یک DbContext کار می‌کرد. در نگارش ششم این کتابخانه، سیستم مهاجرت Code first آن از چندین DbContext، به ازای یک پروژه که به یک بانک اطلاعاتی اشاره می‌کنند، پشتیبانی می‌کند. مزیت اینکار اندکی بهبود در نگهداری تنها کلاس DbContext تعریف شده است. برای مثال می‌توان یک کلاس DbContext مخصوص قسمت ثبت نام را ایجاد کرد. یک کلاس DbContext مخصوص کلیه جداول مرتبط با مقالات را و همینطور الی آخر. نهایتا تمام این Contextها سبب ایجاد یک بانک اطلاعاتی واحد خواهند شد.
اگر در یک پروژه EF Code first چندین Context وجود داشته باشد و دستور enable-migrations را بدون پارامتری فراخوانی کنیم، پیغام خطای More than one context type was found in the assmbly xyz را دریافت خواهیم کرد.
الف) اما در EF 6 می‌توان با بکار بردن سوئیچ جدید ContextTypeName، به ازای هر Context، مهاجرت مرتبط با آن‌را تنظیم نمود:
 enable-migrations -ContextTypeName dbContextName1 -MigrationDirectory DataContexts\Name1Migrations
همچنین در اینجا نیز می‌توان با استفاده از سوئیچ MigrationDirectory، فایل‌های تولید شده را در پوشه‌های مجزایی ذخیره کرد.

ب) در مرحله بعد، نیاز به فراخوانی دستور add-migration است:
 add-migration -ConfigurationTypeName FullNameSpaceCtx1.Configuration "InitialCreate"
با اجرای دستور enable-migrations یک کلاس Configuration جهت DbContext مشخص شده، ایجاد می‌شود. سپس آدرس کامل این کلاس را به همراه ذکر دقیق فضای نام آن در اختیار دستور add-migration قرار می‌دهیم.
ذکر کامل فضای نام، از این جهت مهم است که کلاس Configuration به ازای Contextهای مختلف ایجاد شده، یک نام را خواهد داشت؛ اما در فضاهای نام متفاوتی قرار می‌گیرد.
با اجرای دستور add-migration، کدهای سی شارپ مورد نیاز جهت اعمال تغییرات بر روی ساختار بانک اطلاعاتی تولید می‌شوند. در مرحله بعد، این کدها تبدیل به دستورات SQL متناظری شده و بر روی بانک اطلاعاتی اجرا خواهند شد.
بدیهی است اگر دو Context در برنامه تعریف کرده باشید، دوبار باید دستور enable-migrations و دوبار دستور add-migration را با پارامترهای اشاره کننده به Conetxtهای مدنظر اجرا کرد.

ج) سپس برای اعمال این تغییرات، باید دستور update-database را اجرا کرد.
 update-database  -ConfigurationTypeName FullNameSpaceCtx1.Configuration
اینبار دستور update-database نیز بر اساس نام کامل کلاس Configuration مدنظر باید اجرا گردد و به ازای هر Context موجود، یکبار نیاز است اجرا گردد.
نهایتا اگر به بانک اطلاعاتی مراجعه کنید، تمام جداول و تعاریف را یکجا در همان بانک اطلاعاتی می‌توانید مشاهده نمائید.


داشتن چندین Context در برنامه و مدیریت تراکنش‌ها

در EF، هر DbContext معرف یک واحد کار است. یعنی تراکنش‌ها و چندین عمل متوالی مرتبط انجام شده، درون یک DbContext معنا پیدا می‌کنند. متد SaveChanges نیز بر همین اساس است که کلیه اعمال ردیابی شده در طی یک واحد کار را در طی یک تراکنش به بانک اطلاعاتی اعمال می‌کند. همچنین مباحثی مانند lazy loading نیز در طی یک Context مفهوم دارند. به علاوه دیگر امکان join نویسی بین دو Context وجود نخواهد داشت. باید اطلاعات را از یکی واکشی و سپس این اطلاعات درون حافظه‌ای را به دیگری ارسال کنید.

یک نکته
می‌توان یک DbSet را در چندین ‍Context تعریف کرد. یعنی اگر بحث join نویسی مطرح است، با تکرار تعریف DbSetها اینکار قابل انجام است اما این مساله اساس جداسازی Contextها را نیز زیر سؤال می‌برد.
 

داشتن چندین Context در برنامه و مدیریت رشته‌های اتصالی

در EF Code first روش‌های مختلفی برای تعریف رشته اتصالی به بانک اطلاعاتی وجود دارند. اگر تغییر خاصی در کلاس مشتق شده از DbContext ایجاد نکنیم، نام کلید رشته اتصالی تعریف شده در فایل کانفیگ باید به نام کامل کلاس Context برنامه اشاره کند. اما با داشتن چندین Context به ازای یک دیتابیس می‌توان از روش ذیل استفاده کرد:

  public class Ctx1 : DbContext
    {
        public Ctx1()
            : base("DefaultConnection")
        {
            //Database.Log = sql => Debug.Write(sql);
        }
    }

  public class Ctx2 : DbContext
    {
        public Ctx2()
            : base("DefaultConnection")
        {
            //Database.Log = sql => Debug.Write(sql);
        }
    }

  <connectionStrings>
    <add name="DefaultConnection" connectionString="…." providerName="System.Data.SqlClient" />
  </connectionStrings>
در اینجا در سازنده کلاس‌های Context تعریف شده، نام کلید رشته اتصالی موجود در فایل کانفیگ برنامه ذکر شده است. به این ترتیب این دو Context به یک بانک اطلاعاتی اشاره خواهند کرد.


چه زمانی بهتر است از چندین Context در برنامه استفاده کرد؟
عموما در طراحی‌های سازمانی SQL Server، تمام جداول از schema مدیریتی به نام dbo استفاده نمی‌کنند. جداول فروش از schema خاص خود و جداول کاربران از schema دیگری استفاده خواهند کرد. با استفاده از چندین Context می‌توان به ازای هر کدام از schemaهای متفاوت موجود، «یک ناحیه ایزوله» را ایجاد و مدیریت کرد.
  public class Ctx2 : DbContext
    {
        public Ctx2()
            : base("DefaultConnection")
        {
            //Database.Log = sql => Debug.Write(sql);
        }

         protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.HasDefaultSchema("sales");
            base.OnModelCreating(modelBuilder);
        }
    }
در این حالت در EF 6 می‌توان DefaultSchema کلی یک Context را در متد بازنویسی شده OnModelCreating به نحو فوق تعریف و مدیریت کرد. در این مثال به صورت خودکار کلیه DbSetهای Ctx2 از schema ایی به نام sales استفاده می‌کنند.
مطالب
ارتقاء به Entity framework 6 و استفاده از بانک‌های اطلاعاتی غیر از SQL Server
برای ارتقاء برنامه‌های قدیمی به EF 6 (که با دات نت 4 به بعد سازگار است) دو حالت استفاده از نیوگت را در حین افزودن ارجاعات لازم به کتابخانه‌های مرتبط با EF باید مدنظر داشت:
الف) از نیوگت استفاده کرده‌اید
در این حالت فقط کافی است کنسول پاورشل نیوگت را در VS.NET گشوده و دستور update-package را صادر کنید. (1) به صورت خودکار آخرین نگارش EF دریافت شده و (2) همچنین فایل کانفیگ برنامه برای افزودن و به روز رسانی تعاریف مرتبط با نگارش 6 به روز گردیده و (3) همچنین اسمبلی اضافی و قدیمی System.Data.Entity.dll نیز حذف خواهد شد.

ب) اگر از نیوگت استفاده نکرده‌اید
ابتدا یک فایل متنی ساده را به نام packages.config با محتوای ذیل به پروژه خود اضافه کنید.
 <?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="EntityFramework" version="5.0.0" targetFramework="net40" />
</packages>
سپس بر روی نام Solution در VS.NET کلیک راست کرده و گزینه فعال سازی Restore بسته‌های نیوگت را فعال کنید (انتخاب گزینه Enable NuGet Package Restore). در ادامه یکبار برنامه را Build کنید تا پوشه packages به صورت خودکار از اینترنت دریافت و بازسازی شود. اکنون دستور update-package را در کنسول پاورشل نیوگت صادر کنید. همان مراحل قسمت الف تکرار خواهند شد.

لازم به ذکر است، اگر پروژه شما از چندین زیر پروژه تشکیل شده است که هر کدام نیز ارجاعی را به اسمبلی EF دارند، باید فایل packages.config فوق را به این زیر پروژه‌ها نیز اضافه کنید. دستور update-package، زیر پروژه‌ها را نیز اسکن کرده و تمام ارجاعات لازم را به صورت خودکار به روز می‌کند. همچنین اسمبلی‌های قدیمی اضافی را نیز حذف خواهد کرد. به این ترتیب با تداخل نگارش‌های قدیمی و جدید EF مواجه نخواهید شد.


مشکلاتی که ممکن است با آن‌ها مواجه شوید:

الف) برنامه کامپایل نمی‌شود
تنها تغییری که جهت کامپایل برنامه باید انجام دهید، استفاده از فضاهای نام جدید بجای فضاهای قدیمی موجود در اسمبلی منسوخ و حذف شده System.Data.Entity.dll است. خود VS.NET قابلیت یافتن فضاهای نام مرتبط را دارد و یا اگر از Resharper نیز استفاده می‌کنید، این قابلیت بهبود یافته است. در کل مثلا System.Data.EntityState شده است System.Data.Entity.EntityState و امثال آن که به روز رسانی آن‌ها نکته خاصی ندارد .


ب) پروایدر بانک اطلاعاتی مورد استفاده یافت نمی‌شود
به صورت پیش فرض فقط پروایدر SQL Server به همراه بسته EF 6 است. حتی پروایدر SQL Server CE نیز با آن ارائه نمی‌شود. اگر از SQL Server CE استفاده کرده‌اید، باید دستور ذیل را نیز پس از نصب EF 6 صادر کنید:
 PM> Install-Package EntityFramework.SqlServerCompact
تا با خطای ذیل مواجه نشوید:
 No Entity Framework provider found for the ADO.NET provider with invariant name 'System.Data.SqlServerCe.4.0'.
Make sure the provider is registered in the 'entityFramework' section of the application config file.
See http://go.microsoft.com/fwlink/?LinkId=260882 for more information.
استفاده از نیوگت به روشی که عنوان شد، فایل کانفیگ برنامه شما را جهت افزودن تعاریف پروایدرهای لازم، به روز می‌کند و این مورد در EF 6 الزامی است (حتما باید تعریف پروایدر در فایل کانفیگ موجود باشد).


ج) خطای عدم وجود کلید خارجی جدول Migration را دریافت می‌کنید
 The foreign key constraint does not exist. [ PK_dbo.__MigrationHistory ]
تا EF 5 نام کلید اصلی جدول MigrationHistory به صورت PK___MigrationHistory می‌باشد.
 ALTER TABLE [__MigrationHistory] ADD CONSTRAINT [PK___MigrationHistory] PRIMARY KEY ([MigrationId]);
در EF 6 این نام شده است PK_dbo.__MigrationHistory
برای حل این مشکل تنها کافی است دستورات ذیل را یکبار بر روی بانک اطلاعاتی خود صادر کنید تا نام مورد نظر به عنوان کلید اصلی جدول migration اضافه شود؛ در غیراینصورت اصلا برنامه اجرا نخواهد شد:
 ALTER TABLE [__MigrationHistory] drop CONSTRAINT [PK___MigrationHistory];
ALTER TABLE [__MigrationHistory] ADD CONSTRAINT [PK_dbo.__MigrationHistory] PRIMARY KEY (MigrationId);
البته یکبار برنامه را اجرا کنید. اگر خطای نبود کلید اصلی یاد شده صادر شد، آنگاه دو دستور فوق را اجرا نمائید.
مطالب
نحوه کار با ftp - بخش اول
امروز می‌خوام نحوه کار با FTP بصورت ساده برای کاربران و برنامه نویسان مبتدی رو آموزش بدم.

برای استفاده از FTP نیاز به یک اکانت FTP در سایت مورد نظر بهمراه دسترسی به پوشه ای مشخص می‌باشد.
برای مثال ما یک اکانت FTP در سایت dotnettip.info داریم که به پوشه upload دسترسی داره.

ابتدا در فایل Web.config و در بین تگ های  appSettings مقادیر زیر را برای دسترسی به اکانت و نام کاربری و رمز عبور ذخیره می‌کنیم.
<add key="FtpAddress" value="ftp://ftp.dotnetips.info" />
<add key="FtpUser" value="uploadcenter" />
<add key="FtpPass" value="123123" />
<add key="FolderPath" value="~/Upload/" />
*نکته : برای امنیت بیشتر و دسترسی به اطلاعات اکانت می‌شود از روش‌های دیگری نیز استفاده کرد.

در ادامه یک کلاس در App_code  پروژه خود با نام FTPHelper ایجاد می‌کنیم و کد زیر را در آن قرار می‌دهیم:
تکه کد بالا برای ست کردن مقادیر نام کاربری و رمز عبور و آدرس FTP در کلاس مذکور که بصورت پیش‌فرض از web.config پر می‌شود ایجاد و بکار خواهد رفت.
using System.Net;
using System.IO;
using System.Configuration; 


public class FtpHelper
{
public FtpHelper()
{
       //Default Value Set From Application 
         _hostname = ConfigurationManager.AppSettings.GetValues("FtpAddress")[0]; 
        _username = ConfigurationManager.AppSettings.GetValues("FtpUser")[0]; 
        _password = ConfigurationManager.AppSettings.GetValues("FtpPass")[0];  
}
    
    #region "Properties"
    private string _hostname;
    /// <summary>
    /// Hostname
    /// </summary>
    /// <value></value>
    /// <remarks>Hostname can be in either the full URL format
    /// ftp://ftp.myhost.com or just ftp.myhost.com
    /// </remarks>
    public string Hostname
    {
        get
        {
            if (_hostname.StartsWith("ftp://"))
            {
                return _hostname;
            }
            else
            {
                return "ftp://" + _hostname;
            }
        }
        set
        {
            _hostname = value;
        }
    }
    private string _username;
    /// <summary>
    /// Username property
    /// </summary>
    /// <value></value>
    /// <remarks>Can be left blank, in which case 'anonymous' is returned</remarks>
    public string Username
    {
        get
        {
            return (_username == "" ? "anonymous" : _username);
        }
        set
        {
            _username = value;
        }
    }
    private string _password;
    public string Password
    {
        get
        {
            return _password;
        }
        set
        {
            _password = value;
        }
    }


    #endregion
}


سپس فضای نام‌های زیر را در کلاس خود قرار می‌دهیم.
using System.Net;
using System.IO;
 
حالا برای بارگذاری فایل می‌توانیم از یک تابع بصورت shared استفاده کنیم که بتوان با دادن آدرس فایل بصورت فیزیکی به تابع و مشخص کردن پوشه مورد نظر آنرا در هاست مقصد (FTP) بارگذاری کرد.توجه داشته باشیذ که تابع فوق نیازی به قرار گرفتن در کلاس بالا (FtpHelper) ندارد.یعنی می‌توان آنرا در هرجای برنامه پیاده سازی نمود.
public static bool Upload(string fileUrl)
        {
            if (File.Exists(fileUrl))
            {
                FtpHelper ftpClient = new FtpHelper();
                string ftpUrl = ftpClient.Hostname + System.IO.Path.GetFileName(fileUrl);


                FtpWebRequest ftp = (FtpWebRequest)FtpWebRequest.Create(ftpUrl);
                ftp.Credentials = new NetworkCredential(ftpClient.Username, ftpClient.Password);

                ftp.KeepAlive = true;
                ftp.UseBinary = true;
                ftp.Timeout = 3600000;
                ftp.KeepAlive = true;
                ftp.Method = WebRequestMethods.Ftp.UploadFile;

                const int bufferLength = 102400;
                byte[] buffer = new byte[bufferLength];
                int readBytes = 0;

                //open file for reading
                using (FileStream fs = File.OpenRead(fileUrl))
                {
                    try
                    {
                        //open request to send
                        using (Stream rs = ftp.GetRequestStream())
                        {
                            do
                            {
                                readBytes = fs.Read(buffer, 0, bufferLength);
                                fs.Write(buffer, 0, readBytes);
                            } while (!(readBytes < bufferLength));
                            rs.Close();
                        }

                    }
                    catch (Exception)
                    {
                     //Optional Alert for Exeption To Application Layer
                       //throw (new ApplicationException("بارگذاری فایل با خطا  رو به رو شد"));
 
                    }
                    finally
                    {
                        //ensure file closed
                        //fs.Close();
                    }

                }

                ftp = null;
                return true;
            }
            return false;



        }


تکه کد بالا فایل مورد نظر را در صورت وجود به صورت تکه‌های 100 کیلوبایتی بر روی ftp بارگذاری می‌کند، که می‌توانید مقدار آنرا نیز تغییر دهید.
اینکار باعث افزایش سرعت بارگذاری در فایل‌های با حجم بالا برای بارگذاری می‌شود.

در بخش‌های بعدی نحوه ایجاد پوشه ، حذف فایل ، حذف پوشه و دانلود فایل از روی FTP را بررسی خواهیم کرد.
نظرات مطالب
فایل‌های chm و مشکل فارسی - قسمت دوم
سلام
فکر کنم یک نگاهی هم به نرم افزار MGTEK Help Producer کنید بد نباشد.
از قابلیت این نرم افزار این است که به Word اضافه می شود فارسی را پشتیبانی می کند و نیز انواع فرمت خروجی دارد از جمله Chm
مطالب
شروع به کار با DNTFrameworkCore - قسمت 7 - ارتقاء به نسخه ‭4.5.x
بعد از انتشار قسمت 6 به عنوان آخرین قسمت مرتبط با تفکر مبتنی‌بر CRUD‏ ‎(‎CRUD-based thinking)‎ قصد دارم پشتیبانی از طراحی Application Layer مبتنی‌بر CQRS را نیز به این زیرساخت اضافه کنم.
در این مطلب تغییرات حاصل از طراحی مجدد و بازسازی انجام شده در نسخه جدید را مرور خواهیم کرد.

تغییرات کتابخانه DNTFrameworkCore

1- واسط‌های مورد استفاده جهت ردیابی موجودیت‌ها :
public interface ICreationTracking
{
    DateTime CreatedDateTime { get; set; }
}

public interface IModificationTracking
{
    DateTime? ModifiedDateTime { get; set; }
}
علاوه بر تغییر نام و نوع داده خصوصیت‌های تاریخ ایجاد و ویرایش، سایر خصوصیات به صورت خواص سایه‌ای در کتابخانه DNTFrameworkCore.EFCore مدیریت خواهند شد. 
2. با اضافه شدن واسط IHasRowIntegrity برای پشتیبانی از امکان تشخیص اصالت ردیف‌های یک بانک اطلاعاتی با استفاده از EF Core، خصوصیت RowVersion به Version تغییر نام پیدا کرد.
public interface IHasRowIntegrity
{
    string Hash { get; set; }
}

public interface IHasRowVersion
{
    byte[] Version { get; set; }
}
3- ارث‌بری از کلاس AggregateRoot در سناریوهای CRUD و در زمان استفاده از CrudService هیچ ضرورتی ندارد و صرفا برای پشتیبانی از طراحی مبتنی‌بر DDD کاربرد خواهد داشت. اگر قصد طراحی یک Rich Domain Model را دارید و رویکرد DDD را دنبال می‌کنید، با استفاده از کلاس پایه AggregateRoot امکان مدیریت DomainEventهای مرتبط با یک Aggregate را خواهید داشت. 
public abstract class AggregateRoot<TKey> : Entity<TKey>, IAggregateRoot
    where TKey : IEquatable<TKey>
{
    private readonly List<IDomainEvent> _events = new List<IDomainEvent>();
    public IReadOnlyCollection<IDomainEvent> Events => _events.AsReadOnly();

    protected virtual void AddDomainEvent(IDomainEvent newEvent)
    {
        _events.Add(newEvent);
    }

    public virtual void ClearEvents()
    {
        _events.Clear();
    }
}
4- امکان Publish رخ‌دادهای مرتبط با یک AggregateRoot به IEventBus اضافه شده است:
public static class EventBusExtensions
{
    public static Task TriggerAsync(this IEventBus bus, IEnumerable<IDomainEvent> events)
    {
        var tasks = events.Select(async domainEvent => await bus.TriggerAsync(domainEvent));
        return Task.WhenAll(tasks);
    }

    public static async Task PublishAsync(this IEventBus bus, IAggregateRoot aggregateRoot)
    {
        await bus.TriggerAsync(aggregateRoot.Events);
        aggregateRoot.ClearEvents();
    }
}
5- واسط IDbSeed به IDbSetup تغییر نام پیدا کرده است.

6- اضافه شدن یک سرویس برای ذخیره‌سازی اطلاعات به صورت Key/Value در بانک اطلاعاتی:
public interface IKeyValueService : IApplicationService
{
    Task SetValueAsync(string key, string value);
    Task<Maybe<string>> LoadValueAsync(string key);
    Task<bool> IsTamperedAsync(string key);
}

public class KeyValue : Entity, IModificationTracking, ICreationTracking, IHasRowIntegrity
{
    public string Key { get; set; }
    [Encrypted] public string Value { get; set; }
    public string Hash { get; set; }
    public DateTime CreatedDateTime { get; set; }
    public DateTime? ModifiedDateTime { get; set; }
}
7- AuthorizationProvider حذف شده و جمع آوری دسترسی‌های سیستم به عهده خود استفاده کننده از این زیرساخت می‌باشد.

8- اضافه شدن امکان Exception Mapping و همچنین سفارشی سازی پیغام‌های خطای عمومی:
    public class ExceptionOptions
    {
        public List<ExceptionMapItem> Mappings { get; } = new List<ExceptionMapItem>();

        [Required] public string DbException { get; set; }
        [Required] public string DbConcurrencyException { get; set; }
        [Required] public string InternalServerIssue { get; set; }

        public bool TryFindMapping(DbException dbException, out ExceptionMapItem mapping)
        {
            mapping = null;

            var words = new HashSet<string>(Regex.Split(dbException.ToStringFormat(), @"\W"));

            var mappingItem = Mappings.FirstOrDefault(a => a.Keywords.IsProperSubsetOf(words));
            if (mappingItem == null)
            {
                return false;
            }

            mapping = mappingItem;

            return true;
        }
    }
و روش استفاده از آن را در پروژه DNTFrameworkCore.TestAPI می‌توانید مشاهده کنید. برای معرفی نگاشت‌ها، می‌توان به شکل زیر در فایل appsetting.json عمل کرد:
"Exception": {
  "Mappings": [
    {
      "Message": "به دلیل وجود اطلاعات وابسته امکان حذف وجود ندارد",
      "Keywords": [
        "DELETE",
        "REFERENCE"
      ]
    },
    {
      "Message": "یک تسک با این عنوان قبلا در سیستم ثبت شده است",
      "MemberName": "Title",
      "Keywords": [
        "Task",
        "UIX_Task_NormalizedTitle"
      ]
    }
  ],
  "DbException": "امکان ذخیره‌سازی اطلاعات وجود ندارد؛ دوباره تلاش نمائید",
  "DbConcurrencyException": "اطلاعات توسط کاربری دیگر در شبکه تغییر کرده است",
  "InternalServerIssue": "متأسفانه مشکلی در فرآیند انجام درخواست شما پیش آمده است!"
}

8- اطلاعات مرتبط با مستأجر جاری در سناریوهای چند مستأجری از واسط IUserSession حذف شده و به واسط ITenantSession منتقل شده است. نوع داده خصوصیت UserId به String تغییر پیدا کرده و بر اساس نیاز می‌توان به شکل زیر از آن استفاده کرد:
_session.UserId
_session.UserId<long>()
_session.UserId<int>()
_session.UserId<Guid>()

علاوه بر آن خصوصیت ImpersonatorUserId که می‌تواند حاوی UserId کاربری باشد که در نقش کاربر دیگری در سناریوهای Impersonation وارد سیستم شده است؛ این مورد در سیستم Logging مبتنی‌بر فایل سیستم و بانک اطلاعاتی موجود در این زیرساخت، ثبت و نگهداری می‌شود.
9- لیست ClaimTypeهای مورد استفاده در این زیرساخت:
public static class UserClaimTypes
{
    public const string UserName = ClaimTypes.Name;
    public const string UserId = ClaimTypes.NameIdentifier;
    public const string SerialNumber = ClaimTypes.SerialNumber;
    public const string Role = ClaimTypes.Role;
    public const string DisplayName = nameof(DisplayName);
    public const string BranchId = nameof(BranchId);
    public const string BranchName = nameof(BranchName);
    public const string IsHeadOffice = nameof(IsHeadOffice);
    public const string TenantId = nameof(TenantId);
    public const string TenantName = nameof(TenantName);
    public const string IsHeadTenant = nameof(IsHeadTenant);
    public const string Permission = nameof(Permission);
    public const string PackedPermission = nameof(PackedPermission);
    public const string ImpersonatorUserId = nameof(ImpersonatorUserId);
    public const string ImpersonatorTenantId = nameof(ImpersonatorTenantId);
}
از خصوصیات Branch*‎ برای سناریوهای چند شعبه‎‌ای می‌توان استفاده کرد که در این صورت اگر یکی از شعب به عنوان دفتر مرکزی در نظر گرفته شود باید Claim‌ای با نام IsHeadOffice با مقدار true از زمان ورود به سیستم برای کاربران آن شعبه در نظر گرفته شود. 
خصوصیات Tenant*‎ برای سناریوهای چند مستأجری در نظر گرفته شده است که اگرطراحی مورد نظرتان به نحوی باشد که بخش مدیریت مستأجرهای سیستم در همان سیستم پیاده‌سازی شده باشد یا به تعبیری سیستم Host و Tenant یکی باشند، می‌توان Claim‌ای با نام IsHeadTenant با مقدار true در زمان ورود به سیستم برای کاربران Host (مستأجر اصلی) در نظر گرفته شود.
‌‌
10- مکانیزم Logging مبتنی‌بر فایل سیستم:
/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder)
{
    builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
    return builder;
}


/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure">Configure an instance of the <see cref="FileLoggerOptions" /> to set logging options</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action<FileLoggerOptions> configure)
{
    builder.AddFile();
    builder.Services.Configure(configure);

    return builder;
}
11- امکان TenantResolution برای شناسایی مستأجر جاری سیستم:
public interface ITenantResolutionStrategy
{
    string TenantId();
}

public interface ITenantStore
{
    Task<Tenant> FindTenantAsync(string tenantId);
}
از این واسط‌ها در میان افزار TenantResolutionMiddleware موجود در کتابخانه DNTFrameworkCore.Web.Tenancy استفاده شده است. و همچنین جهت دسترسی به اطلاعات مستأجر جاری سیستم می‌توان واسط زیر را تزریق و استفاده کرد:
public interface ITenantSession : IScopedDependency
{
    /// <summary>
    ///     Gets current TenantId or null.
    ///     This TenantId should be the TenantId of the <see cref="IUserSession.UserId" />.
    ///     It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in.
    /// </summary>
    string TenantId { get; }

    /// <summary>
    ///     Gets current TenantName or null.
    ///     This TenantName should be the TenantName of the <see cref="IUserSession.UserId" />.
    ///     It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in.
    /// </summary>
    string TenantName { get; }

    /// <summary>
    ///     Represents current tenant is head-tenant.
    /// </summary>
    bool IsHeadTenant { get; }

    /// <summary>
    ///     TenantId of the impersonator.
    ///     This is filled if a user with <see cref="IUserSession.ImpersonatorUserId" /> performing actions behalf of the
    ///     <see cref="IUserSession.UserId" />.
    /// </summary>
    string ImpersonatorTenantId { get; }
}
12- استفاده از SystemTime و IClock برای افزایش تست‌پذیری سناریوهای درگیر با DateTime:
public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.UtcNow;

    public static Func<DateTime, DateTime> Normalize = (dateTime) =>
        DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
}
public interface IClock : ITransientDependency
{
    DateTime Now { get; }
    DateTime Normalize(DateTime dateTime);
}

internal sealed class Clock : IClock
{
    public DateTime Now => SystemTime.Now();

    public DateTime Normalize(DateTime dateTime)
    {
        return SystemTime.Normalize(dateTime);
    }
}
13- تغییر واسط عمومی کلاس Result:
public class Result
{
    private static readonly Result _ok = new Result(false, string.Empty);
    private readonly List<ValidationFailure> _failures;

    protected Result(bool failed, string message) : this(failed, message,
        Enumerable.Empty<ValidationFailure>())
    {
        Failed = failed;
        Message = message;
    }

    protected Result(bool failed, string message, IEnumerable<ValidationFailure> failures)
    {
        Failed = failed;
        Message = message;
        _failures = failures.ToList();
    }

    public bool Failed { get; }
    public string Message { get; }
    public IEnumerable<ValidationFailure> Failures => _failures.AsReadOnly();

    [DebuggerStepThrough]
    public static Result Ok() => _ok;

    [DebuggerStepThrough]
    public static Result Fail(string message)
    {
        return new Result(true, message);
    }

    //...
}

روش معرفی سرویس‌های مرتبط با کتابخانه DNTFrameworkCore
services.AddFramework()
    .WithModelValidation()
    .WithFluentValidation()
    .WithMemoryCache()
    .WithSecurityService()
    .WithBackgroundTaskQueue()
    .WithRandomNumber();
متد WithFluentValidation یک متد الحاقی برای FrameworkBuilder می‌باشد که در کتابخانه DNTFrameworkCore.FluentValidation تعریف شده است.

تغییرات کتابخانه DNTFrameworkCore.EFCore

1- اگر از CrudService پایه موجود استفاده می‌کنید، محدودیت ارث‌بری از TrackableEntity از موجودیت اصلی برداشته شده است. همچنین همانطور که در نظرات مطالب قبلی در قالب نکته تکمیلی اشاره شد، متد  MapToEntity  به نحوی تغییر کرد که پاسخگوی اکثر نیازها باشد.
2- امکان تنظیم ModifiedProperties  برای موجودیت‌های وابسته در سناریوهایی با موجودیت‌های وابسته Master-Detail نیز مهیا شده است.
public abstract class TrackableEntity<TKey> : Entity<TKey>, ITrackable where TKey : IEquatable<TKey>
{
    [NotMapped] public TrackingState TrackingState { get; set; }
    [NotMapped] public ICollection<string> ModifiedProperties { get; set; }
}
3-  امکان ذخیره سازی تنظیمات برنامه‌های ASP.NET Core در یک بانک اطلاعاتی با استفاده از EF ، اضافه شده است که از همان موجودیت KeyValue برای نگهداری مقادیر استفاده می‌کند:
public static class ConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddEFCore(this IConfigurationBuilder builder,
        IServiceProvider provider)
    {
        return builder.Add(new EFConfigurationSource(provider));
    }
}
4- واسط IHookEngine حذف شده و سازنده کلاس پایه DbContextCore لیستی از IHook را به عنوان پارامتر می‌پذیرد:
protected DbContextCore(DbContextOptions options, IEnumerable<IHook> hooks) : base(options)
{
    _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks));
}
 همچنین امکان IgnoreHook برای غیرفعال کردن یک Hook خاص با استفاده از نام آن مهیا شده است:
public void IgnoreHook(string hookName)
{
    _ignoredHookList.Add(hookName);
}
امکان پیاده سازی Hook سفارشی را برای سناریوهای خاص هم با پیاده سازی واسط IHook و یا با ارث‌بری از کلاس‌های پایه موجود در زیرساخت، خواهید داشت. به عنوان مثال:
internal sealed class RowIntegrityHook : PostActionHook<IHasRowIntegrity>
{
    public override string Name => HookNames.RowIntegrity;
    public override int Order => int.MaxValue;
    public override EntityState HookState => EntityState.Unchanged;

    protected override void Hook(IHasRowIntegrity entity, HookEntityMetadata metadata, IUnitOfWork uow)
    {
        metadata.Entry.Property(EFCore.Hash).CurrentValue = uow.EntityHash(entity);
    }
}
در بازطراحی انجام شده، دسترسی به وهله جاری DbContext هم از طریق واسط IUnitOfWork مهیا شده است.
5- متد EntityHash به واسط IUnitOfWork اضافه شده است که امکان محاسبه هش مرتبط با یک رکورد از یک موجودیت خاص را مهیا می‌کند؛ همچنین امکان تغییر الگوریتم و سفارشی سازی آن را به شکل زیر خواهید داشت:
//DbContextCore : IUnitOfWork

public string EntityHash<TEntity>(TEntity entity) where TEntity : class
{
    var row = Entry(entity).ToDictionary(p => p.Metadata.Name != EFCore.Hash &&
                                              !p.Metadata.ValueGenerated.HasFlag(ValueGenerated.OnUpdate) &&
                                              !p.Metadata.IsShadowProperty());
    return EntityHash<TEntity>(row);
}

protected virtual string EntityHash<TEntity>(Dictionary<string, object> row) where TEntity : class
{
    var json = JsonConvert.SerializeObject(row, Formatting.Indented);
    using (var hashAlgorithm = SHA256.Create())
    {
        var byteValue = Encoding.UTF8.GetBytes(json);
        var byteHash = hashAlgorithm.ComputeHash(byteValue);
        return Convert.ToBase64String(byteHash);
    }
}
همچنین از طریق متدهای الحاقی زیر که مرتبط با واسط IUnitOfWork می‌باشند، امکان دسترسی به رکوردهای دستکاری شده را خواهید داشت:
IsTamperedAsync
HasTamperedEntryAsync
TamperedEntryListAsync

 
6- همانطور که اشاره شد، خواص سایه‌ای مرتبط با سیستم ردیابی موجودیت‌ها نیز به شکل زیر تغییر نام پیدا کرده‌اند:
public const string CreatedDateTime = nameof(ICreationTracking.CreatedDateTime);
public const string CreatedByUserId = nameof(CreatedByUserId);
public const string CreatedByBrowserName = nameof(CreatedByBrowserName);
public const string CreatedByIP = nameof(CreatedByIP);

public const string ModifiedDateTime = nameof(IModificationTracking.ModifiedDateTime);
public const string ModifiedByUserId = nameof(ModifiedByUserId);
public const string ModifiedByBrowserName = nameof(ModifiedByBrowserName);
public const string ModifiedByIP = nameof(ModifiedByIP);
7- یک تبدیلگر سفارشی برای ذخیره سازی اشیا به صورت JSON اضافه شده است که برگرفته از کتابخانه Innofactor.EfCoreJsonValueConverter می‌باشد.
 8- دو متد الحاقی زیر برای نرمال‌سازی خصوصیات تاریخ از نوع DateTime و خصوصیات عددی از نوع Decimal به ModelBuilder اضافه شده‌اند:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{  
    modelBuilder.NormalizeDateTime();
    modelBuilder.NormalizeDecimalPrecision(precision: 20, scale: 6);
    
    base.OnModelCreating(modelBuilder);
}

9-  متد MigrateDbContext به این کتابخانه منتقل شده است:
MigrateDbContext<TContext>(this IHost host)
متد Seed واسط IDbSetup در صورت معرفی یک پیاده‌سازی از آن به سیستم تزریق وابستگی‌ها، در بدنه این متد فراخوانی خواهد شد.

روش معرفی سرویس‌های مرتبط با کتابخانه DNTFrameworkCore.EFCore
services.AddEFCore<ProjectDbContext>()
    .WithTrackingHook<long>()
    .WithDeletedEntityHook()
    .WithRowIntegrityHook()
    .WithNumberingHook(options =>
    {
        options.NumberedEntityMap[typeof(Task)] = new NumberedEntityOption
        {
            Prefix = "Task",
            FieldNames = new[] {nameof(Task.BranchId)}
        };
    });
همانطور که عنوان شد، محدودیت نوع خصوصیات CreatedByUserId و ModifiedByUserId برداشته شده است و از طریق متد WithTrackingHook قابل تنظیم می‎‌باشد.

تغییرات کتابخانه DNTFrameworkCore.Web.Tenancy


فعلا امکان شناسایی مستأجر جاری و دسترسی به اطلاعات آن از طریق واسط ITenantSession در دسترس می‌باشد؛ همچنین امکان تغییر و تعیین رشته اتصال به بانک اطلاعاتی هر مستأجر از طریق متد UseConnectionString واسط IUnitOfWork فراهم می‌باشد.
services.AddTenancy()
    .WithTenantSession()
    .WithStore<InMemoryTenantStore>()
    .WithResolutionStrategy<HostResolutionStrategy>();
app.UseTenancy();


سایر کتابخانه‌ها تغییرات خاصی نداشتند و صرفا نحوه معرفی سرویس‌های آنها ممکن است تغییر کند و یا وابستگی‌های آنها به آخرین نسخه موجود ارتقاء داده شده باشند که در پروژه DNTFrameworkCore.TestAPI اعمال شده‌اند.
لیست بسته‌های نیوگت نسخه ۴.۵.۳
PM> Install-Package DNTFrameworkCore
PM> Install-Package DNTFrameworkCore.EFCore
PM> Install-Package DNTFrameworkCore.EFCore.SqlServer
PM> Install-Package DNTFrameworkCore.Web
PM> Install-Package DNTFrameworkCore.FluentValidation
PM> Install-Package DNTFrameworkCore.Web.Tenancy
PM> Install-Package DNTFrameworkCore.Licensing
مطالب
بارگذاری UserControl در WPF به کمک الگوی MVVM
در نرم افزارهای تحت ویندوز روشها و سلیقه‌های متفاوتی برای چینش فرمها ، منو‌ها و دیگر اجزای برنامه وجود دارد. در یک نرم افزار اتوماسیون اداری که فرمهای ورود اطلاعات زیادی دارد فضای کافی برای نمایش همه‌ی فرم‌ها به کاربر نیست. یکی از روش هایی که می‌تواند به کار رود تقسیم قسمت‌های مختلف نرم افزار در View‌های جداگانه است. این کار استفاده‌ی مجدد از قسمت‌های مختلف و نگهداری کد را سهولت می‌بخشد. 

الگوی متداولی که در نرم افزار‌های WPF و Silverlight استفاده می‌شود الگوی MVVM است. (این الگو در جاوااسکریپت هم به سبب Statefull بودن استفاده می‌شود.) قبلا مطالب زیادی در این سایت جهت آموزش و توضیح این الگوی منتشر شده است.
فرض کنید نرم افزار از چند بخش تشکیل شده :

  • صفحه‌ی اصلی
  • منو
  • یک صفحه‌ی خوش آمدگویی
  • صفحه‌ی ورود و نمایش اطلاعات
می توان اجزا و تعریف هر یک از این قسمت‌ها را در یک UserControl قرار داد و در زمان مناسب آن را بارگذاری کرد. 
سوالی که مطرح است بارگذاری UserControl‌ها به کمک الگوی MVVM چگونه است ؟ 
کدهای XAML صفحه‌ی اصلی : 
<Window x:Class="TwoViews.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MVVM Light View Switching"
        d:DesignHeight="300"
        d:DesignWidth="300"
        DataContext="{Binding Main,
                              Source={StaticResource Locator}}"
        ResizeMode="NoResize"
        SizeToContent="WidthAndHeight"
        mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ContentControl Content="{Binding CurrentViewModel}" />

        <DockPanel Grid.Row="1" Margin="5">
            <Button Width="75"
                    Height="23"
                    Command="{Binding SecondViewCommand}"
                    Content="Second View"
                    DockPanel.Dock="Right" />
            <Button Width="75"
                    Height="23"
                    Command="{Binding FirstViewCommand}"
                    Content="First View"
                    DockPanel.Dock="Left" />
        </DockPanel>
    </Grid>
</Window>

2 دکمه در صفحه‌ی اصلی وجود دارد ، یکی از آنها وظیفه‌ی بارگذاری View اول و دیگری وظیفه‌ی بارگذاری View دوم را دارد ،  این دکمه‌ها نقش منو را در یک نرم افزار واقعی به عهده دارند. 
کدهای View-Model گره خورده (به کمک الگوی ViewModolLocator ) به View اصلی  : 
    /// This is our MainViewModel that is tied to the MainWindow via the 
    /// ViewModelLocator class.
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        /// <summary>
        /// Static instance of one of the ViewModels.
        /// </summary>
        private static readonly SecondViewModel SecondViewModel = new SecondViewModel();
        /// <summary>
        /// Static instance of one of the ViewModels.
        /// </summary>
        private static readonly FirstViewModel FirstViewModel = new FirstViewModel();
        /// <summary>
        /// The current view.
        /// </summary>
        private ViewModelBase _currentViewModel;
        /// <summary>
        /// Default constructor.  We set the initial view-model to 'FirstViewModel'.
        /// We also associate the commands with their execution actions.
        /// </summary>
        public MainViewModel()
        {
            CurrentViewModel = FirstViewModel;
            FirstViewCommand = new RelayCommand(ExecuteFirstViewCommand);
            SecondViewCommand = new RelayCommand(ExecuteSecondViewCommand);
        }
        /// <summary>
        /// The CurrentView property.  The setter is private since only this 
        /// class can change the view via a command.  If the View is changed,
        /// we need to raise a property changed event (via INPC).
        /// </summary>
        public ViewModelBase CurrentViewModel
        {
            get { return _currentViewModel; }
            set
            {
                if (_currentViewModel == value)
                    return;
                _currentViewModel = value;
                RaisePropertyChanged("CurrentViewModel");
            }
        }
        /// <summary>
        /// Simple property to hold the 'FirstViewCommand' - when executed
        /// it will change the current view to the 'FirstView'
        /// </summary>
        public ICommand FirstViewCommand { get; private set; }
        /// <summary>
        /// Simple property to hold the 'SecondViewCommand' - when executed
        /// it will change the current view to the 'SecondView'
        /// </summary>
        public ICommand SecondViewCommand { get; private set; }
        /// <summary>
        /// Set the CurrentViewModel to 'FirstViewModel'
        /// </summary>
        private void ExecuteFirstViewCommand()
        {
            CurrentViewModel = FirstViewModel;
        }
        /// <summary>
        /// Set the CurrentViewModel to 'SecondViewModel'
        /// </summary>
        private void ExecuteSecondViewCommand()
        {
            CurrentViewModel = SecondViewModel;
        }
    }

این ViewModel از کلاس پایه‌ی چارچوب MVVM Light مشتق شده است.  Command‌ها جهت Handle کردن کلیک دکمه‌ها هستند . نکته‌ی اصلی این ViewModel پراپرتی CurrentViewModel می‌باشد.  این پراپرتی به ویژگی Content کنترل ContentControl مقید (Bind) شده است. با کلیک شدن روی دکمه‌ها View مورد نظر به کاربر نمایش داده می‌شود.
WPF از کجا می‌داند کدام View را به ازای ViewModel خاص render کند ؟ 
در فایل App.xaml یک سری DataTemplate تعریف شده است : 
    <Application.Resources>
        <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
        <!--
            We define the data templates here so we can apply them across the
            entire application.
         
            The data template just says that if our data type is of a particular
            view-model type, then render the appropriate view.  The framework
            takes care of this dynamically.  Note that the DataContext for
            the underlying view is already set at this point, so the
            view (UserControl), doesn't need to have it's DataContext set
            directly.
        -->
        <DataTemplate DataType="{x:Type vm:SecondViewModel}">
            <views:SecondView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:FirstViewModel}">
            <views:FirstView />
        </DataTemplate>
    </Application.Resources>

به کمک این DataTemplate‌ها مشخص شده اگر نوع داده‌ی ما از یک نوع View-Model خاص می‌باشد View مناسب را به ازای آن Render کند. با تعریف DataTemplate‌ها در App.Xaml می‌توان از آنها در سطح نرم افزار استفاده کرد. می‌توان DataTemplate‌ها را جهت خلوت کردن App.xaml به Resource دیگری انتقال داد.
دریافت مثال : TwoViews.zip 

 
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت چهارم - تکمیل مستندات نوع‌های خروجی API
Swashbuckle.AspNetCore چگونه اطلاعات خود را فراهم می‌کند؟

در برنامه‌های ASP.NET Core، اطلاعات OpenAPI بر اساس سرویس توکاری به نام ApiExplorer تولید می‌شود که کار آن فراهم آوردن متادیتای مرتبط با یک برنامه‌ی وب است. برای مثال توسط این سرویس می‌توان به لیست کنترلرها، متدها و پارامترهای آن‌ها دسترسی یافت. Swashbuckle.AspNetCore به کمک ApiExplorer کار تولید OpenAPI Specification را انجام می‌دهد. برای فعالسازی این سرویس نیازی نیست کار خاصی انجام شود و زمانیکه ()services.AddMvc را فراخوانی می‌کنیم، ثبت و معرفی این سرویس نیز جزئی از آن است.


اهمیت تولید Response Types صحیح

در قسمت‌های قبل مشاهده کردیم که اگر متدی برای مثال در قسمتی از آن return NotFound یا 404 را داشته باشد، این نوع از خروجی، در OpenAPI Specification تولیدی لحاظ نمی‌شود و ناقص است و یا حتی ممکن است Response Type پیش‌فرض تولیدی که 200 است، ارتباطی به هیچکدام از نوع‌های خروجی یک اکشن متد نداشته باشد و نیاز به اصلاح آن است. این مورد برای تکمیل مستندات یک API ضروری است و استفاده کنندگان از یک API باید بدانند چون نوع خروجی‌هایی را ممکن است در شرایط مختلف، دریافت کنند.


روش تغییر و اصلاح Response Type پیش‌فرض OpenAPI Specification

اکشن متد GetBook کنترلر کتاب‌ها، دارای دو نوع return Ok و return NotFound است؛ اما OpenAPI Specification تولیدی پیش‌فرض، تنها حالت return Ok یا 200 را مستند می‌کند. برای تکمیل مستندات این اکشن متد، می‌توان به صورت زیر عمل کرد:
/// <summary>
/// Get a book by id for a specific author
/// </summary>
/// <param name="authorId">The id of the book author</param>
/// <param name="bookId">The id of the book</param>
/// <returns>An ActionResult of type Book</returns>
/// <response code="200">Returns the requested book</response>
[HttpGet("{bookId}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<Book>> GetBook(Guid authorId, Guid bookId)
در اینجا با استفاده از ویژگی ProducesResponseType، می‌توان StatusCodes بازگشتی از متد را به صورت صریحی مشخص کرد. وجود این متادیتاها سبب خواهد شد تاOpenAPI Specification تولیدی، به نحو صحیحی حالت 404 را نیز لحاظ کند.
در اینجا StatusCodes.Status400BadRequest را نیز مشاهده می‌کنید. هرچند حالت return BadRequest در کدهای این اکشن متد وجود خارجی ندارد، اما در صورت بروز مشکلی در فراخوانی و یا پردازش آن، به صورت خودکار توسط فریم‌ورک بازگشت داده می‌شود. بنابراین مستندسازی آن نیز ضروری است.

برای آزمایش آن، برنامه را اجرا کنید. در قسمت مستندات متد فوق، اکنون سه حالت 404، 400 و 200 قابل مشاهده هستند. برای نمونه بر روی دکمه‌ی try it out آن کلیک کرده و زمانیکه authorId و bookId را درخواست می‌کند، دو Guid اتفاقی و کاملا بی‌ربط را وارد کنید. همچنین Controls Accept header را نیز بر روی application/json قرار دهید. سپس بر روی دکمه‌ی execute در ذیل آن کلیک نمائید. یک چنین خروجی 404 کاملی را مشاهده خواهید کرد:


در این تصویر، response body بر اساس rfc 7807 تولید می‌شود و استاندارد گزارش مشکلات یک API است. این مورد اکنون به صورت یک اسکیمای جدید در انتهای مستندات تولیدی نیز قابل مشاهده‌است:



بهبود مستندات تشخیص نوع‌های مدل‌های خروجی اکشن متدها

مورد دیگری که در اینجا جالب توجه است، تشخیص نوع خروجی، در حالت return Ok است:


در اینجا اگر بر روی لینک Schema، بجای Example value پیش‌فرض کلیک کنیم، تصویر فوق حاصل می‌شود. تشخیص این نوع، به علت استفاده‌ی از ActionResult از نوع Book، به صورت زیر است که در ASP.NET Core 2.1 برای همین منظور (تکمیل مستندات Swagger) معرفی شده‌است:
public async Task<ActionResult<Book>> GetBook(Guid authorId, Guid bookId)
بنابراین از ASP.NET Core 2.1 به بعد، بهتر است در APIها خود از IActionResult استفاده نکنید و شروع به کار با <ActionResult<T نمائید تا بتوان مستندات بهتری را تولید کرد. اگر از IActionResult استفاده کنید، دیگر خبری از Example value و Schema تصویر فوق نخواهد بود و از روی متادیتای این اکشن متد نمی‌توان نوع خروجی آن‌را تشخیص داد. البته در این حالت می‌توان این مشکل را به صورت زیر نیز حل کرد؛ اما باز هم بهتر است از <ActionResult<T استفاده کنید:
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Book))]

یک نکته: در این تصویر، در قسمت توضیحات حالت 200، عبارت "Returns the requested book" مشاهده می‌شود. اما در حالت‌های دیگر response types تعریف شده، عبارات پیش‌فرض bad request و یا not found نمایش داده شده‌اند. نحوه‌ی بازنویسی این پیش‌فرض‌ها، با تکمیل مستندات XMLای اکشن متد و ذکر response code دلخواه، به صورت زیر است:
/// <response code="200">Returns the requested book</response>


استفاده از API Analyzers برای بهبود OpenAPI Specification تولیدی

اکنون این سؤال مطرح می‌شود که پس از این تغییرات، هنوز چه مواردی در OpenAPI Specification تولیدی ما وجود خارجی ندارند و بهتر است اضافه شوند. برای پاسخ به این سؤال، از زمان ارائه‌ی ASP.NET Core 2.2، بسته‌ی جدید Microsoft.AspNetCore.Mvc.Api.Analyzers نیز ارائه شده‌است که کار آن دقیقا بررسی همین نقایص و کمبودها است. بنابراین ابتدا آن‌را به فایل OpenAPISwaggerDoc.Web.csproj اضافه کرده و سپس دستور dotnet restore را صادر می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Api.Analyzers" Version="2.2.0" />
  </ItemGroup>
پس از نصب این ابزار جدید که به صورت افزونه‌ای برای کامپایلر #‍C کار می‌کند، بلافاصله اخطارهایی توسط کامپایلر ظاهر خواهند شد؛ مانند:
Controllers\BooksController.cs(40,17): warning API1000: Action method returns undeclared status code '404'.
Controllers\BooksController.cs(89,13): warning API1000: Action method returns undeclared status code '201'.
همانطور که ملاحظه می‌کنید، هنوز هم نیاز به تعریف تعدادی ProducesResponseType فراموش شده وجود دارد که آن‌ها را به صورت زیر اضافه خواهیم کرد:
/// <summary>
/// Get the books for a specific author
/// </summary>
/// <param name="authorId">The id of the book author</param>
/// <returns>An ActionResult of type IEnumerable of Book</returns>
[HttpGet()]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks(Guid authorId)
در ابتدا نوع‌های خروجی اکشن متد GetBooks را تکمیل می‌کنیم که آن نیز دارای return NotFound و همچنین return Ok است. به علاوه در اینجا ویژگی جدید ProducesDefaultResponseType را نیز ملاحظه می‌کنید که یک چنین خروجی را تولید می‌کند:


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


ساده سازی کدهای تکراری تعریف ProducesResponseTypeها

مواردی مانند StatusCodes.Status400BadRequest و یا 406 را در حالت عدم قبول درخواست (مثلا با انتخاب یک accept header اشتباه) و یا 500 را در صورت وجود استثنائی در سمت سرور، باید به تمام اکشن متدها نیز اضافه کرد؛ چون می‌توانند تحت شرایطی، نوع‌های خروجی معتبری باشند. برای خلاصه سازی این عملیات، یا می‌توان این ویژگی‌ها را بجای قراردادن آن‌ها در بالای تعریف امضای اکشن متدها، به بالای تعریف کلاس کنترلر انتقال داد. با اینکار ویژگی که به یک کنترلر اعمال شده باشد به تمام اکشن متدهای آن نیز اعمال خواهد شد و یا حتی برای عدم تعریف این ویژگی‌های تکراری به ازای هر کنترلر موجود، می‌توان آن‌ها را به صورت سراسری تعریف کرد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(setupAction =>
            {
                setupAction.Filters.Add(
                    new ProducesResponseTypeAttribute(StatusCodes.Status400BadRequest));
                setupAction.Filters.Add(
                    new ProducesResponseTypeAttribute(StatusCodes.Status406NotAcceptable));
                setupAction.Filters.Add(
                    new ProducesResponseTypeAttribute(StatusCodes.Status500InternalServerError));
                setupAction.Filters.Add(
                    new ProducesDefaultResponseTypeAttribute());

                setupAction.ReturnHttpNotAcceptable = true;
// ...
به این ترتیب یکسری از نوع‌های خروجی که ممکن است توسط خود فریم‌ورک به صورت خودکار بازگشت داده شوند، به صورت سراسری به تمام اکشن متدهای برنامه اعمال می‌شوند و دیگر نیازی به تعریف دستی آن‌ها نخواهد بود.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-04.zip

در قسمت بعد، روش‌های دیگری را برای تکمیل مستندات خروجی API بررسی می‌کنیم.
مطالب
مبانی TypeScript؛ فضاهای نام

همانطور که عنوان شد به ماژول‌های داخلی، فضاهای نام (namespace) گفته می‌شود. بنابراین از namespaceها نیز جهت مدیریت و ساماندهی پروژه‌های بزرگ با تعداد فایل‌های زیاد استفاده می‌شود. در واقع یک namespace حالت ساده‌تری از یک ماژول است؛ یعنی برای بارگذاری آن در مرورگر نیاز به loader خاصی نیست. بنابراین در پروژه‌هایی با مقیاس کوچکتر استفاده از namespaceها، انتخاب بهتری است. همچنین یکی از مزیت‌های مهم namespace جلوگیری از شلوغ شدن global scope است.  


نحوه‌ی تعریف namespace در TypeScript  

برای ایجاد یک namespace کافی است از کلمه کلیدی namespace استفاده کنیم: 

namespace Membership {
    export function AddMember(name: string) {
        // add a new member
    }
}

در کد فوق یک namespace را با نام Membership تعریف کرده‌ایم. درون این namespace می‌توانیم کدهای خود را بنویسیم. به عنوان مثال در کد فوق یک تابع را با نام AddMember درون آن تعریف کرده‌ایم و با استفاده از کلمه‌ی کلیدی export این تابع را در معرض دید مصرف کننده قرار داده‌ایم. لازم به ذکر است که یک namespace می‌تواند به صورت nested (تودرتو) نیز استفاده شود. اما باید توجه داشته باشید که برای داشتن چنین قابلیتی باید از export نیز قبل از کلمه‌ی کلیدی namespace استفاده شود: 

namespace Membership {
    export function AddMember(name: string) {
        // add a new member
    }
    
    export namespace Cards {
        export function IssueCard(memberNumber: number) {
            // issue new card
        }
    }
}

همانطور که مشاهده می‌کنید، درون یک nested namespace نیز می‌توانیم اشیاء را توسط کلمه‌ی کلیدی export در معرض دید مصرف کننده قرار دهیم.


نحوه‌ی استفاده از namespaceها در TypeScript 

برای استفاده از کدهای مثال قبل، در یک فایل دیگر می‌توانیم به صورت زیر یک ارجاع را به namespace مربوطه داشته باشیم:

/// <reference path="membership.ts" />

اکنون درون ادیتور، به صورت کامل به اشیاء export شده‌ی توسط فضای نام Membership دسترسی خواهید داشت. لازم به ذکر است به این نوع ارجاع، Triple-Slash References نیز گفته می‌شود. مزیت دیگر این روش این است که کامپایلر TypeScript ارجاعات را نیز کامپایل می‌کند.

برای استفاده از اشیاء مربوط به namespace ارجاع داده شده نیز باید از سینتکس نقطه استفاده کرد:

Membership.AddMember('Garrett');
Membership.Cards.IssueCard(500);

نکته‌ایی که در اینجا وجود دارد این است که کامپایلر TypeScript به صورت پیش‌فرض فایل‌های ارجاع داده شده را به فایل‌های جداگانه‌ایی کامپایل خواهد کرد و فرض را بر این خواهد گذاشت که هر کدام از فایل‌های کامپایل شده قرار است به ترتیب استفاده از آنها درون صفحه استفاده شوند. برای تولید یک فایل واحد می‌توانید از فلگ outFile استفاده کنید. با کمک این فلگ تمامی ارجاعات، بر اساس ترتیب استفاده از آنها، به یک فایل واحد تبدیل خواهند شد. جهت درک بهتر موضوع، یک فایل را با نام utilityFunctions.ts با محتویات زیر ایجاد کنید:

namespace Utility {
    export namespace Fees {
        export function CalculateLateFee(daysLate: number): number {
            return daysLate * .25;
        }
    }
    export function MaxBooksAllowed(age: number): number {
        if (age < 12) {
            return 3;
        } else {
            return 10;
        }
    }
    function privateFunc(): void {
        console.log('This is private....');
    }
}

در کد فوق یک namespace با نام Utility ایجاد کرده‌ایم سپس یک namespace دیگر و همچنین یک تابع را درون آن export کرده‌ایم. اکنون می‌خواهیم از این فضای نام درون یک فایل دیگر با نام app.ts استفاده کنیم. برای اینکار دورن فایل app.ts با استفاده از Triple-Slash یک ارجاع را به فضای نام عنوان شده اضافه کنید:

/// <reference path="utilityFunctions.ts" />

اکنون می‌توانیم از Intellisense مربوط به VS Code جهت دسترسی به اعضای فضای نام Utility استفاده کنیم:

لازم به ذکر است برای namespace مربوطه می‌توانیم alias نیز جهت دسترسی سریع‌تر، تعریف کنیم:

/// <reference path="utilityFunctions.ts" />
import util = Utility.Fees;
let fee = util.CalculateLateFee(10);
console.log(`Fee: ${fee}`);

توسط فرمان زیر نیز می‌توانیم فایل فوق را به راحتی کامپایل کنیم:

tsc --target ES5 app.ts

با فرمان فوق، فایل app.js به همراه تمامی فایل‌های ارجاع داده شده‌ی درون آن نیز کامپایل خواهند شد.

اکنون اگر بخواهیم توسط node فایل app.js را اجرا کنیم، با خطای ReferenceError: Utility is not defined مواجه خواهیم شد. دلیل آن نیز این است که node سعی در لود کردن فایل به صورت یک ماژول دارد و از آنجائیکه ما از namespace استفاده کرده‌ایم، فضای نام Utility توسط node قابل شناسنایی نمی‌باشد. اما درون یک صفحه‌ی HTML با قرار دادن فایل utilityFunctions.js و سپس app.js مشکلی نخواهیم داشت. برای حل این مورد در node کافی است از نکته‌ی یکی کردن فایل‌ها استفاده کنیم:

tsc --target ES5 app.ts --outFile out.js

در این‌حالت فایل‌ها به ترتیب استفاده، با هم ادغام شده و تبدیل به یک فایل با نام out.js خواهند شد.