پیکربندی چندمحیطی در .NET MAUI: راهنمای dev، staging و production با appsettings و MSBuild

راهنمای عملی پیاده‌سازی پیکربندی چندمحیطی در .NET MAUI با استفاده از appsettings.json، MSBuild Configurations و IOptions برای جداسازی کامل تنظیمات dev، staging و production و مدیریت ایمن اسرار در CI/CD.

پیکربندی چندمحیطی .NET MAUI با appsettings و MSBuild
On this page
  1. چرا پیکربندی چندمحیطی اهمیت دارد
  2. افزودن appsettings.json به پروژه .NET MAUI
  3. تفکیک محیط با MSBuild و Build Configurations
  4. خواندن تنظیمات با IConfiguration و IOptions
  5. مدیریت اسرار در CI/CD

هر اپلیکیشن موبایل حرفه‌ای حداقل با سه محیط سروکار دارد: محیط توسعه که به سرور آزمایشی اشاره دارد، محیط staging که نسخه نزدیک به تولید را آزمون می‌کند، و محیط production که کاربران واقعی با آن روبه‌رو هستند. .NET MAUI ابزارهای داخلی لازم برای تفکیک این سه محیط را در اختیار شما می‌گذارد: ترکیبی از فایل‌های appsettings.json، شرط‌های MSBuild و الگوی IOptions<T> که با Generic Host نسخه‌های اخیر .NET یکپارچه شده است.

این راهنما از ابتدا تا انتها مراحل پیاده‌سازی یک لایه پیکربندی چندمحیطی را طی می‌کند؛ از اضافه‌کردن فایل‌های json تا تزریق تنظیمات type-safe در سرویس‌ها و در نهایت مدیریت اسرار حساس در خط لوله CI/CD.

چرا پیکربندی چندمحیطی اهمیت دارد

هاردکدکردن آدرس سرور تولید مستقیم در کد یا استفاده از یک فایل Constants.cs که قبل از هر انتشار دستی تغییر می‌کند، دو اشتباه رایجی هستند که خطرات جدی به همراه دارند. اگر توسعه‌دهنده‌ای فراموش کند مقدار را برگرداند، داده‌های واقعی کاربران در سرور تست می‌نشینند یا بالعکس، لاگ‌های سنگین debug به سرور production ارسال می‌شوند.

یک لایه پیکربندی مناسب سه مزیت عملی دارد:

  • قابل‌اعتماد بودن بیلد: هر بار که با یک Configuration خاص بیلد می‌کنید، تنظیمات صحیح به‌صورت خودکار کار می‌کنند.
  • سهولت تست: سرویس‌هایی که از IOptions<T> استفاده می‌کنند را می‌توان در Unit Testها با مقادیر مصنوعی جایگزین کرد.
  • امنیت: اسرار حساس از کد جدا می‌مانند و فقط در خط لوله CI/CD تزریق می‌شوند.

افزودن appsettings.json به پروژه .NET MAUI

.NET MAUI از Generic Host استفاده می‌کند، پس Microsoft.Extensions.Configuration از همان ابتدا در دسترس است. بسته‌ای که باید اضافه شود Microsoft.Extensions.Configuration.Json است که امکان خواندن فایل‌های json را فراهم می‌کند:

dotnet add package Microsoft.Extensions.Configuration.Json

سپس فایل‌های پیکربندی را در ریشه پروژه (کنار MauiProgram.cs) بسازید:

  • appsettings.json — تنظیمات پیش‌فرض مشترک بین همه محیط‌ها
  • appsettings.Debug.json — مقادیر خاص محیط توسعه
  • appsettings.Staging.json — مقادیر خاص محیط staging

فایل‌ها باید به‌عنوان EmbeddedResource علامت‌گذاری شوند تا درون باینری اپلیکیشن کامپایل شوند. اپ‌های اندروید و iOS نمی‌توانند مانند یک سرور ASP.NET Core از فایل‌سیستم بخوانند:

<ItemGroup>
  <!-- همیشه در همه بیلدها -->
  <EmbeddedResource Include="appsettings.json" />

  <!-- فقط وقتی فایل وجود داشته باشد -->
  <EmbeddedResource
    Include="appsettings.$(Configuration).json"
    Condition="Exists('appsettings.$(Configuration).json')" />
</ItemGroup>

متغیر MSBuild به نام $(Configuration) در زمان بیلد مقدار آن configuration فعال (مانند Debug یا Release یا Staging) را دارد، پس این تنظیم به‌طور خودکار فایل مناسب را انتخاب می‌کند.

حالا در MauiProgram.cs پیکربندی را بارگذاری کنید:

using System.Reflection;
using Microsoft.Extensions.Configuration;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>();

        var assembly = Assembly.GetExecutingAssembly();
        var appName = assembly.GetName().Name!;

        var baseStream = assembly.GetManifestResourceStream(
            $"{appName}.appsettings.json");
        if (baseStream is not null)
            builder.Configuration.AddJsonStream(baseStream);

        var envStream = assembly.GetManifestResourceStream(
            $"{appName}.appsettings.{GetEnvironmentName()}.json");
        if (envStream is not null)
            builder.Configuration.AddJsonStream(envStream);

        builder.Services.Configure<ApiSettings>(
            builder.Configuration.GetSection("ApiSettings"));

        return builder.Build();
    }

    private static string GetEnvironmentName()
    {
#if DEBUG
        return "Debug";
#elif STAGING
        return "Staging";
#else
        return "Release";
#endif
    }
}

تفکیک محیط با MSBuild و Build Configurations

Visual Studio به‌صورت پیش‌فرض دو Configuration دارد: Debug و Release. برای اضافه‌کردن Staging، از منوی Build → Configuration Manager یک Configuration جدید بسازید و آن را از روی Release کپی کنید.

در فایل csproj می‌توانید برای این Configuration یک ثابت preprocessor تعریف کنید:

<PropertyGroup Condition="'$(Configuration)' == 'Staging'">
  <DefineConstants>$(DefineConstants);STAGING</DefineConstants>
</PropertyGroup>

این کار باعث می‌شود دستورالعمل‌های #if STAGING در کد C# کار کنند. همچنین می‌توانید از این مکانیزم برای تغییر Application ID یا نام اپلیکیشن استفاده کنید تا نسخه‌های مختلف هم‌زمان روی یک دستگاه نصب شوند:

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
  <ApplicationId>com.mycompany.myapp.dev</ApplicationId>
  <ApplicationTitle>MyApp (Dev)</ApplicationTitle>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Staging'">
  <ApplicationId>com.mycompany.myapp.staging</ApplicationId>
  <ApplicationTitle>MyApp (Staging)</ApplicationTitle>
</PropertyGroup>

داشتن Application ID متفاوت به این معناست که هر محیط کاملاً مستقل است و می‌توان نسخه‌های debug، staging و production را روی یک دستگاه همزمان داشت بدون اینکه یکدیگر را جایگزین کنند.

خواندن تنظیمات با IConfiguration و IOptions

خواندن مستقیم از IConfiguration با کلیدهای رشته‌ای مستعد خطاهای تایپی است. الگوی بهتر استفاده از IOptions<T> است که تنظیمات را به یک کلاس strongly-typed ترجمه می‌کند. ابتدا کلاس تنظیمات را تعریف کنید:

public class ApiSettings
{
    public string BaseUrl { get; set; } = string.Empty;
    public int TimeoutSeconds { get; set; } = 30;
}

آن را در DI ثبت کنید:

builder.Services.Configure<ApiSettings>(
    builder.Configuration.GetSection("ApiSettings"));

یک نمونه appsettings.json پایه:

{
  "ApiSettings": {
    "BaseUrl": "https://api.myapp.com",
    "TimeoutSeconds": 30
  }
}

و appsettings.Debug.json که فقط مقادیر متفاوت را override می‌کند:

{
  "ApiSettings": {
    "BaseUrl": "https://api-dev.myapp.com",
    "TimeoutSeconds": 60
  }
}

در سرویس‌ها IOptions<ApiSettings> را تزریق کنید:

public class ApiService
{
    private readonly ApiSettings _settings;

    public ApiService(IOptions<ApiSettings> options)
    {
        _settings = options.Value;
    }

    public string GetBaseUrl() => _settings.BaseUrl;
}

در Unit Testها می‌توانید تنظیمات را مستقیم ایجاد کنید بدون نیاز به فایل json:

var settings = Options.Create(new ApiSettings
{
    BaseUrl = "https://test.example.com",
    TimeoutSeconds = 5
});
var service = new ApiService(settings);

مدیریت اسرار در CI/CD

کلیدهای API، رمزهای رمزنگاری و سایر اطلاعات حساس هرگز نباید در فایل‌های json کامیت شوند. راه‌حل استاندارد استفاده از secret‌های CI/CD و ایجاد فایل پیکربندی در زمان بیلد است.

یک فایل template مانند appsettings.Release.template.json را در مخزن کد قرار دهید که به‌جای مقادیر واقعی از placeholder استفاده می‌کند:

{
  "ApiSettings": {
    "BaseUrl": "PROD_API_URL_PLACEHOLDER",
    "ApiKey": "PROD_API_KEY_PLACEHOLDER"
  }
}

در GitHub Actions، با استفاده از secret‌های مخزن، فایل واقعی را پیش از بیلد بسازید:

- name: Inject production config
  run: |
    cp appsettings.Release.template.json appsettings.Release.json
    sed -i 's|PROD_API_URL_PLACEHOLDER|${{ secrets.PROD_API_URL }}|g' appsettings.Release.json
    sed -i 's|PROD_API_KEY_PLACEHOLDER|${{ secrets.PROD_API_KEY }}|g' appsettings.Release.json

همچنین appsettings.Release.json را به .gitignore اضافه کنید تا به‌طور تصادفی کامیت نشود:

# .gitignore
appsettings.Release.json
appsettings.Staging.json

برای داده‌های حساسی که در زمان اجرا در اختیار کاربر قرار می‌گیرند، مانند توکن‌های احراز هویت، از SecureStorage استفاده کنید. مقدار را پس از اولین ورود موفق کاربر دریافت و ذخیره کنید تا از تعبیه‌شدن آن در باینری جلوگیری شود. برای آشنایی با الگوی JWT و SecureStorage می‌توانید مقاله احراز هویت JWT در .NET MAUI را مطالعه کنید.

سوالات متداول

آیا می‌توان appsettings.json را در زمان اجرا تغییر داد؟

خیر. فایل‌های EmbeddedResource بخشی از باینری کامپایل‌شده هستند و تغییر نمی‌کنند. اگر نیاز دارید تنظیمات پس از انتشار به‌روز شوند، باید از یک سرویس جداگانه مانند Firebase Remote Config یا یک endpoint اختصاصی استفاده کنید.

تفاوت IOptions، IOptionsSnapshot و IOptionsMonitor چیست؟

IOptions<T> تنها یک‌بار در شروع برنامه خوانده می‌شود (singleton). IOptionsSnapshot<T> در هر درخواست مجدداً خوانده می‌شود (scoped) که در ASP.NET Core کاربرد دارد. IOptionsMonitor<T> تغییرات فایل پیکربندی در زمان اجرا را رصد می‌کند. برای تنظیمات EmbeddedResource در MAUI، IOptions<T> همیشه کافی است چون فایل در اجرا تغییر نمی‌کند.

چطور نام دقیق منبع تعبیه‌شده را پیدا کنم؟

در Debug، دستور Assembly.GetExecutingAssembly().GetManifestResourceNames() را اجرا کرده و خروجی را log کنید. نام منابع از Namespace پیش‌فرض پروژه + مسیر فایل با نقطه به‌جای اسلش ساخته می‌شود. مثلاً فایل appsettings.json در ریشه پروژه‌ای با namespace به نام MyApp به صورت MyApp.appsettings.json ثبت می‌شود.

آیا تنظیمات تعبیه‌شده می‌توانند از باینری استخراج شوند؟

بله. هر کسی که به فایل باینری دسترسی دارد می‌تواند با ابزارهای decompile به محتوای EmbeddedResource دسترسی پیدا کند. به همین دلیل اسرار حساس (کلیدهای API، رمزهای رمزنگاری) نباید در appsettings.json تعبیه شوند. این اطلاعات باید در زمان اجرا از سرور دریافت یا از SecureStorage خوانده شوند.

آیا می‌توان از این روش روی اندروید و iOS هر دو استفاده کرد؟

بله. چون EmbeddedResource بخشی از کد .NET است و نه فایل‌سیستم پلتفرم، کد MauiProgram.cs برای اندروید، iOS، macOS و ویندوز یکسان کار می‌کند. تفاوتی بین پلتفرم‌ها در این لایه وجود ندارد.

Article changelog (1)
  • — Content revised