امکان تغییر رشته‌ی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

تغییر پویای رشته‌ی اتصالی به بانک اطلاعاتی در نگارش‌های پیشین EF، مشکل بودند که نمونه‌هایی از آن را پیشتر در مطالب زیر مشاهده کرده‌اید:
- «تنظیم رشته اتصالی Entity Framework به بانک اطلاعاتی به وسیله کد»
- «استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First»

اما EF Core نه تنها این مشکل را پوشش را داده‌است، بلکه امکان تزریق وابستگی‌ها و استفاده‌ی از سرویس‌های مختلف را نیز در این حین، پیش بینی کرده‌است که در ادامه جزئیات آن‌را مرور می‌کنیم.


نیاز به تغییر رشته‌ی اتصالی به بانک اطلاعاتی در زمان اجرا

دلایل نیاز به امکان تغییر رشته‌ی اتصالی در زمان اجرا شامل موارد زیر هستند:
- در برنامه‌هایی کمی پیچیده‌تر و سابقه دار، ممکن است عملیات تجاری یکسال را در بانک اطلاعاتی سال 98 و دیگری را در بانک اطلاعاتی سال 99 ثبت کنید. در این حالت کاربران باید بتوانند در زمان اجرا به هر بانک اطلاعاتی که پیشتر با آن کار کرده‌اند، متصل شده و از آن استفاده کنند.
- یکی از روش‌های پیاده سازی برنامه‌های چند مستاجری، داشتن یک بانک اطلاعاتی مجزا، به ازای هر مستاجر است. در این حالت نیز تک برنامه‌ی ما باید بتواند بر اساس Id مشتری، بانک اطلاعاتی متناظری را در زمان اجرا انتخاب کند.
- نیاز به داشتن چندین context در برنامه و کار با بانک‌های اطلاعاتی متفاوت در زمان اجرا؛ مانند کار با SQL Server، اوراکل و یا SQLite


روش تغییر رشته‌ی اتصالی به بانک اطلاعاتی در EF Core در زمان اجرای برنامه

اگر به روش ثبت متداول سرویس DbContext برنامه و پروایدر آن دقت کنیم:
services.AddDbContext<ApplicationDbContext>(options =>
      options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")
));
یک action delegate قابل مشاهده‌است. کار این اکشن، تنظیم پروایدر و تمام نیازهای یک رشته‌ی اتصالی به بانک اطلاعاتی، جهت شروع به کار با Context برنامه است. نکته‌ی مهمی که در اینجا وجود دارد، فراخوانی هرباره‌ی این action، به ازای هر اتصال تشکیل شده‌است. یعنی کدهای داخل این action delegate کش نمی‌شوند و همین مساله امکان تغییر پویای آن‌ها را میسر می‌کند.

یک نکته: چون این اطلاعات کش نمی‌شوند، اگر رشته‌ی اتصالی شما ثابت است (و نیازی به تغییر آن در زمان اجرای برنامه نیست)، محل تامین آن‌را به پیش از سطر services.AddDbContext انتقال دهید و فقط نتیجه‌ی محاسبه شده‌ی نهایی را استفاده کنید تا کارآیی برنامه افزایش یابد؛ در غیراینصورت فراخوانی Configuration.GetConnectionString مدام تکرار خواهد شد.


دریافت یک قالب قابل تغییر از تنظیمات برنامه و تغییر آن با هدرهای درخواست رسیده‌ی به آن

فرض کنید قالب رشته‌ی اتصالی برنامه در فایل appsettings.json به صورت زیر است:
"ConnectionStrings": {
    "ConnectionTemplate": "Data Source=.;Initial Catalog={db_Name};Integrated Security=True",
}
و db_Name آن قرار است برای مثال از یک query string، سشن، کوکی و یا فیلد خاصی در هدر HTTP رسیده تامین شود. برای مثال سال مالی انتخابی و یا شماره مستاجر انتخابی به صورت یک فیلد خاص HTTP به سمت برنامه ارسال می‌شوند.
بنابراین اکنون نیاز است به ازای هر درخواست رسیده بتوان به سرویس IHttpContextAccessor و شیء HttpContext.Request جاری دسترسی یافت و سپس از هدرهای رسیده، برای مثال هدر ویژه‌ی tenantId و یا year را پردازش کرد؛ اما در تعریف services.AddDbContext فوق چگونه می‌توان اینکار را انجام داد؟
خوشبختانه متد services.AddDbContext، دارای یک overload دیگر نیز هست که امکان دسترسی به تمام سرویس‌های جاری سیستم را میسر می‌کند:
services.AddDbContext<ApplicationDbContext>((serviceProvider, dbContextBuilder) =>
{
   var connectionStringTemplate = Configuration.GetConnectionString("ConnectionTemplate");
   var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
   var dbName = httpContextAccessor.HttpContext.Request.Headers["tenantId"].First();
   var connectionString = connectionStringTemplate.Replace("{db_Name}", dbName);
   dbContextBuilder.UseSqlServer(connectionString);
});
همانطور که مشاهده می‌کنید، overload دوم متد services.AddDbContext، امکان ارسال serviceProvider را نیز به این action delegate دارد. پس از آن می‌توان توسط متد GetRequiredService آن به هر سرویس مدنظری که در سیستم ثبت شده‌است، دسترسی یافت و برای مثال در اینجا فیلد هدر سفارشی tenantId را از آن استخراج نمود و در قالب رشته‌ی اتصالی به بانک اطلاعاتی، در زمان اجرا به صورت پویایی جایگزین کرد.
همچنین در صورت نیاز می‌توان UseSqlServer آن‌را نیز در این action delegate به هر پروایدر دیگری در زمان اجرا تغییر داد و از این لحاظ محدودیتی وجود ندارد.

یک نکته: البته برنامه نباید هر tenantId ای را پردازش کند و این خودش می‌تواند تبدیل به یک نقیصه‌ی امنیتی شود. به همین جهت برای مثال می‌توان tenantId را در یک JWT قرار داد و در حین تعیین اعتبار آن و کاربر جاری، این مقدار را نیز بررسی کرد.
  • #
    ‫۳ سال و ۳ ماه قبل، دوشنبه ۲۰ اردیبهشت ۱۴۰۰، ساعت ۰۵:۱۱
    سلام و با تشکر؛ من در حال توسعه یک سیستم حسابداری هستم که کاربر در فرم login سال مالی انتخاب میکنه. حالا من میخوام کاربر بعد از انتخاب سال مالی و پر کردن سشن مربوطه کانکش اون سال مالی انتخاب بشه. از کد شما استفاده کردم به صورت زیر:
                services.AddDbContext<MarketDbContext>((serviceProvider, dbContextBuilder) =>
                {
                    var connectionStringTemplate = Configuration.GetConnectionString("Connection");
                    var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
                    var dbName = httpContextAccessor.HttpContext.Session.GetString("databasename");
                    var connectionString = connectionStringTemplate.Replace("{db_Name}", dbName);
                    dbContextBuilder.UseSqlServer(connectionString);
                });
    و در اکشن لوگین هم کد زیر قرار دادم جهت ساخت سشن :
    HttpContext.Session.SetString("databasename", "DB1399");
    ولی اصلا به اجرای کد بالا نرسیده از کد زیر خطای null بودن میگیره :
                    var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
    ممنون میشم راهنمایی کنید
    • #
      ‫۳ سال و ۳ ماه قبل، دوشنبه ۲۰ اردیبهشت ۱۴۰۰، ساعت ۰۵:۲۳
      IHttpContextAccessor (با فرض ثبت سرویس آن) فقط در طول یک درخواست قابل استفاده و دسترسی است. ممکن است در ابتدای شروع برنامه که کار ساخت بانک اطلاعاتی یا اعمال Migration صورت می‌گیرد، این کدها را فراخوانی کنید. در آن لحظه HttpContext ای در دسترس نیست؛ چون هنوز کار به راه اندازی کنترلرها و رسیدن درخواستی نرسیده‌است (در حالت کلی در زمان Startup، اعمال صورت گرفته، خارج از HttpContext است). بنابراین در ابتدای برنامه، نال بودن آن‌را بررسی کنید. اگر نال بود، از Configuration برای شروع کار استفاده کنید. در مابقی حالات چون در طول درخواست‌ها استفاده می‌شود، مشکلی نخواهد داشت.
      namespace TestBackend
      {
        public class Startup
        {
          // ...
      
          public void ConfigureServices(IServiceCollection services)
          {
            // ...
      
            services.AddHttpContextAccessor();
            // + services.AddSession() && app.UseSession()
            services.AddDbContext<TestContext>((serviceProvider, options) =>
            {        
              options.UseSqlServer(GetConnectionString(serviceProvider));
            });
      
            // ...
          }
      
           // ...
      
          private string GetConnectionString(IServiceProvider serviceProvider)
          {
            var connectionStringTemplate = Configuration.GetConnectionString("ConnectionTemplate");
      
            try
            {
               var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); // This needs services.AddHttpContextAccessor();
               var dbName = httpContextAccessor.HttpContext.Session.GetString("databasename"); // This needs services.AddSession(); && app.UseSession();
               return connectionStringTemplate.Replace("{db_Name}", dbName);
            }
            catch(Exception ex) 
            {
                 var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(Startup));
                 logger.LogError("GetConnectionString error", ex, "Failed to get connection string.");
            }
      
            return connectionStringTemplate.Replace("{db_Name}", "---Default-DB-Name-Here---");
          }
        }
      }
  • #
    ‫۱ سال و ۵ ماه قبل، دوشنبه ۲۲ اسفند ۱۴۰۱، ساعت ۱۴:۰۲
    روش دیگری برای تغییر رشته‌های اتصالی در زمان اجرا

    به همراه مثال‌های رسمی EF-Core 7x، مثال LazyConnectionStringSample.cs به این مورد پرداخته؛ برای اینکار یک DbConnectionInterceptor سفارشی را طراحی کرده که در متد ConnectionOpeningAsync بازنویسی شده‌ی آن، امکان تغییر رشته‌ی اتصالی جاری به صورت پویا وجود دارد:
        public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
        {
            public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
                DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
                CancellationToken cancellationToken = new())
            {
                if (string.IsNullOrEmpty(connection.ConnectionString))
                {
                    connection.ConnectionString =  "new data ..." ;
                }
    
                return result;
            }
        }