ليس appsettings.json وحده ── الممارسة العمليّة لإدارة الإعدادات في تطبيقات Windows للأعمال (الإعدادات حسب البيئة، المعلومات السرّيّة، وجهة الكتابة)
· آخر تحديث: · غو كومورا · CSharp, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, إدارة الإعدادات, تطوير Windows, الاستشارات التقنية
«أريد تغيير سلسلة الاتّصال حسب كلّ بيئة» أو «أريد حفظ إعدادات العرض لكلّ مستخدم» أو «وضعتُ مفتاح API داخل appsettings.json بالخطأ» ── في إدارة الإعدادات لتطبيقات Windows للأعمال، يصل الجميع إلى مرحلة وضع ملفّ appsettings.json واحد والقراءة منه عبر IConfiguration، لكن ما بعد ذلك ── الإعدادات حسب البيئة، وأماكن حفظ الإعدادات القابلة للكتابة، والتعامل مع المعلومات السرّيّة، وتغيير الإعدادات أثناء التشغيل ── مجال كثيراً ما يُترَك دون تنظيم ويدخل حيّز التشغيل الفعليّ كما هو.
في هذا المقال، سنرتّب – بالترتيب الذي يكثر فيه التردّد في العمل الفعليّ – الأساس الذي تقوم عليه منظومة الإعدادات في .NET وهو تراكب IConfiguration مع مزوّدي الإعدادات، ثمّ الإعدادات حسب البيئة، وطريقة الاستلام الآمنة من ناحية النوع (type-safe) عبر نمط IOptions، وأماكن حفظ الإعدادات القابلة للكتابة، والتعامل مع المعلومات السرّيّة، والحدود الواقعيّة لتغيير الإعدادات أثناء التشغيل، وصولاً إلى خريطة الانتقال من app.config/Settings.settings.
1. الخلاصة أوّلاً
يبدأ قرار إدارة الإعدادات بتحديد «مكان الحفظ» لكلّ «نوع من الإعدادات». لنبدأ بتلخيص الصورة العامّة في جدول قرار.
| نوع الإعداد | مثال محدّد | المكان الأوّل المرشَّح للحفظ | السبب |
|---|---|---|---|
| القيم الافتراضيّة للتطبيق | المستوى الافتراضيّ للسجلّ (log level)، المعطيات الافتراضيّة لواجهة المستخدم | appsettings.json |
قيم مشتركة لا تعتمد على البيئة، تُضمَّن في نتاج البناء |
| الإعدادات حسب البيئة | سلسلة الاتّصال أو نقطة نهاية API التي تختلف بين بيئة الاختبار والبيئة الفعليّة | appsettings.{Environment}.json + متغيّرات البيئة |
يمكن الاستفادة من ترتيب الاستبدال الافتراضيّ كما هو |
| الإعدادات الخاصّة بكلّ مستخدم | آخر مجلّد تمّ فتحه، موضع النافذة، إعدادات العرض الشخصيّة | ملفّ خاصّ تحت %LOCALAPPDATA% (أو %APPDATA%) |
يلزم مجال كتابة لكلّ مستخدم على حدة |
| الإعدادات الخاصّة بكلّ جهاز (مشتركة بين كلّ المستخدمين) | رقم منفذ COM الخاصّ بالجهاز، عنوان خادم التراخيص | ملفّ خاصّ تحت %ProgramData% |
يضبطه المدير مرّة واحدة لكلّ جهاز، ويُشارَك بين كلّ المستخدمين |
| المعلومات السرّيّة | كلمة مرور سلسلة الاتّصال، مفتاح API، الرمز المميّز (token) | ملفّ محميّ بـ DPAPI (في الإنتاج)، user-secrets (في التطوير فقط) | لا توضع نصّاً صريحاً؛ لا يُستخدَم آليّة التطوير في الإنتاج |
| القيم التي تتغيّر أثناء التشغيل | أعلام الميزات (feature flags)، التغيير الديناميكيّ لمستوى السجلّ | appsettings.json (reloadOnChange) + IOptionsMonitor |
يُستخدَم فقط للقيم التي يُراد تطبيقها دون إعادة تشغيل |
بناءً على هذا الجدول، نكتب الخلاصة أوّلاً:
- تتّبع أولويّة الإعدادات قاعدة واحدة: «المزوّد المُضاف لاحقاً يفوز». بالإعداد الافتراضيّ، تُقرَأ الإعدادات بالترتيب:
appsettings.json←appsettings.{Environment}.json← user secrets (في بيئة التطوير فقط) ← متغيّرات البيئة ← معطيات سطر الأوامر، وإن وُجد نفس المفتاح، تُستبدَل القيمة بالقيمة التي قُرئت لاحقاً. مجرّد تذكّر هذا الترتيب يفسّر معظم حالات «كتبتُه في json لكنّه لا ينعكس».1 - باستخدام
Host.CreateApplicationBuilder، يُهيَّأ هذا التدرّج افتراضيّاً دون الحاجة لبنائه بنفسك. حتّى في تطبيقات سطح المكتب مثل Windows Forms/WPF، يمكن الاستفادة من نفس القيم الافتراضيّة عبر استخدام Generic Host.23 - يتمّ تبديل البيئة عبر
DOTNET_ENVIRONMENT(أوASPNETCORE_ENVIRONMENT). في سلسلةWebApplication، تكون الأولويّة لـDOTNET_ENVIRONMENT، وإن لم يُضبَط أيّ منهما فالقيمة الافتراضيّة هيProduction. في تطبيقات سطح المكتب أو خدمات Windows، يلزم تصميم «من الذي يُمرِّر هذا المتغيّر إلى العمليّة، وكيف» مسبقاً.4 - لا تُستقبَل الإعدادات عبر DTO، بل كنوع عبر عائلة
IOptions<T>. إن كفى استلامها مرّة واحدة عند بدء التشغيل استخدمIOptions<T>، وإن أردتَ تطبيق التغييرات دون إعادة تشغيل استخدمIOptionsMonitor<T>. استخدام كليهما دون فهم الفرق بينهما يسبّب حادثين معاً: «تتغيّر قيمة لا يُراد لها أن تتغيّر» و«لا تتغيّر قيمة يُراد لها أن تتغيّر».5 - اجعل خلل الإعدادات يُسقِط التطبيق عند بدء التشغيل، لا عند وقت التنفيذ. بدمج
ValidateDataAnnotations()معValidateOnStart()، يصبح خطأ الإعدادات «يُكتشَف باستثناء فور بدء التشغيل»، ما يمنع حادثة «اكتشاف NullReferenceException في منتصف ليل الإنتاج».6 - لا توضع المعلومات السرّيّة نصّاً صريحاً في
appsettings.json. استخدم user-secrets في التطوير، وDPAPI (ProtectedData) أو مدير بيانات الاعتماد في الإنتاج. تنصّ الوثائق الرسميّة صراحةً على أنّ user-secrets غير مشفَّرة وأنّها آليّة مخصَّصة للتطوير فقط.78
2. أساس منظومة الإعدادات في .NET ── IConfiguration ومزوّدو الإعدادات
تتألّف منظومة الإعدادات في .NET من آليّة تُحمِّل عدّة مزوّدي إعدادات (configuration providers) بشكل متراكب خلف واجهة مخزن مفتاح-قيمة واحدة تُسمّى IConfiguration. وميزة هذه الآليّة هي إمكانيّة توحيد مصادر مختلفة الطبيعة – JSON، ومتغيّرات البيئة، ومعطيات سطر الأوامر، وINI، وXML، ومجموعات في الذاكرة – خلف نفس واجهة IConfiguration.9
يتراكم المزوّدون وفق قاعدة بسيطة: «الذي يُضاف لاحقاً يفوز». إن وُجد نفس المفتاح في عدّة مزوّدين، تكون القيمة الفعّالة هي قيمة المزوّد الذي أُضيف أخيراً.1
عند استخدام Host.CreateApplicationBuilder(args)، يُبنى المزوّدون بالترتيب التالي افتراضيّاً (كلّما كبر الرقم زادت الأولويّة، أي يفوز الأخير).2
appsettings.jsonappsettings.{Environment}.json- Secret Manager (في بيئة التطوير فقط)
- متغيّرات البيئة
- معطيات سطر الأوامر
هذا الترتيب الافتراضيّ مشترك سواء كان التطبيق أداة سطر أوامر (console) أو Worker Service أو تطبيق Windows Forms. المثال التالي أدنى إعداد ممكن.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// بشكل افتراضيّ، تُقرَأ appsettings.json / appsettings.{Environment}.json / متغيّرات البيئة /
// معطيات سطر الأوامر بترتيب الأولويّة المذكور أعلاه
string? connectionString = builder.Configuration.GetConnectionString("Main");
using IHost host = builder.Build();
await host.RunAsync();
حتّى في تطبيق سطح مكتب مثل Windows Forms، يمكن استخدام نفس HostApplicationBuilder مباشرةً بمجرّد إضافة حزمة Microsoft.Extensions.Hosting. وكما كتبنا في مدوّنتنا في «ما هو .NET Generic Host»، فإنّ Generic Host هو أساس يتكفّل بالإعدادات وDI والـ logging معاً، ولا داعٍ للتخلّي عن فوائد منظومة الإعدادات لمجرّد كون التطبيق سطح مكتب. للاطّلاع على طريقة بناء ملموسة في تطبيق يتضمّن معالجة مقيمة (resident)، راجع أيضاً «استخدام Generic Host + BackgroundService في تطبيق سطح مكتب».
هناك نقطة تستحقّ الانتباه في التعامل مع متغيّرات البيئة. تُقرَأ إعدادات المضيف (host) (كجذر المحتوى واسم البيئة) من متغيّرات البيئة ذات البادئة DOTNET_، لكنّ هذه لا تُستخدَم في إعدادات التطبيق (القيم العامّة المقروءة عبر IConfiguration). أمّا متغيّرات البيئة المُراد قراءتها كإعدادات تطبيق، فهي مُدمَجة في الترتيب الافتراضيّ عبر AddEnvironmentVariables() دون بادئة.10 وإن أردتَ إضافة بادئة خاصّة بك، أضِفها صراحةً بالشكل builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_").
using Microsoft.Extensions.Configuration;
// يُضاف بعد المزوّدين الافتراضيّين، لذا تكون له الأولويّة الأعلى
builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_");
3. الإعدادات حسب البيئة ── appsettings.{Environment}.json
إن أردتَ تغيير سلسلة الاتّصال أو نقطة النهاية حسب كلّ بيئة، استخدم ملفّات خاصّة بكلّ بيئة مثل appsettings.Development.json / appsettings.Staging.json / appsettings.Production.json. والمهمّ هنا أنّ هذا الملفّ يُعامَل كملفّ فروقات (diff) «يُعيد الكتابة فوق» appsettings.json الأساسيّ. لا حاجة لإعادة كتابة كلّ البنود، بل يكفي كتابة القيم التي تختلف حسب كلّ بيئة فقط.1
يُحدَّد اسم البيئة عبر متغيّر البيئة DOTNET_ENVIRONMENT أو ASPNETCORE_ENVIRONMENT. عند استخدام WebApplication، تكون الأولويّة لقيمة DOTNET_ENVIRONMENT على ASPNETCORE_ENVIRONMENT، وإن لم يُضبَط أيّ منهما فالقيمة الافتراضيّة هي Production. في Windows لا يُفرَّق بين الأحرف الكبيرة والصغيرة في اسم متغيّر البيئة، لكن يُفرَّق بينها في Linux، لذا إن كان الحاويات (containers) ضمن الاعتبار، فمن الأسلم توحيد طريقة الكتابة.4
والمشكلة هنا أنّ «كيفيّة تمرير متغيّر البيئة» نفسها تصبح مهمّة قائمة بذاتها في تطبيقات سطح المكتب أو خدمات Windows. فآليّات وقت التطوير مثل launchSettings.json في ASP.NET Core لا تُستخدَم إلّا في التطوير المحلّيّ، لذا يلزم تجهيز وسيلة منفصلة لتبديل البيئة في التشغيل الفعليّ. الطرق الثلاث الشائعة للتمرير هي التالية.
- ضبطه كمتغيّر بيئة على مستوى الجهاز. إن جعلتَه دائماً بالشكل
setx DOTNET_ENVIRONMENT Production /M، يُورَّث إلى كلّ العمليّات التي تعمل على ذلك الجهاز. لكن هذا لا يناسب الحالة التي يُراد فيها تشغيل تطبيقات لبيئات متعدّدة على نفس الجهاز. - تمرير متغيّر البيئة إلى عمليّة تشغيل خدمة Windows. عند تشغيله كخدمة مقيمة، يبدأ التشغيل في سياق مختلف عن جلسة المستخدم التفاعليّ، لذا لا تُورَّث متغيّرات بيئة المستخدم. للاطّلاع على تفاصيل حساب التشغيل وفصل الجلسات، راجع «كيفيّة بناء خدمات Windows وتشغيلها». الحلّ الواقعيّ هو استخدام متغيّر بيئة على مستوى الجهاز، أو جعل ملفّ تشغيل الخدمة عبارة عن دفعة (batch) غلاف رقيقة تُنفِّذ
SETقبل تشغيل الملفّ الأساسيّ. - عند التشغيل عبر Task Scheduler، تمريره كمعطى سطر أوامر أضمن. لا توجد في واجهة «العمليّة» (Action) الخاصّة بـ Task Scheduler وسيلة لضبط متغيّر بيئة مباشرةً، لذا يقلّ عدد الحوادث بتمريره كمعطى سطر أوامر مثل
--environment Productionوقراءته عبرAddCommandLine(args). لخّصنا خصائص حساب التشغيل ونوع تسجيل الدخول الخاصّة بـ Task Scheduler في «لماذا لا تُنفَّذ مهامّ Task Scheduler، أو تنتهي برمز 0x1».
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
var options = new HostApplicationBuilderSettings
{
Args = args,
// لمسارات التشغيل التي لا يُمرَّر إليها متغيّر البيئة (كـ Task Scheduler مثلاً)،
// يُسمَح أيضاً بتبديل البيئة عبر معطيات سطر الأوامر
};
HostApplicationBuilder builder = Host.CreateApplicationBuilder(options);
Console.WriteLine($"البيئة الحاليّة: {builder.Environment.EnvironmentName}");
4. نمط الخيارات (Options Pattern) ── استقبال الإعدادات كنوع
القراءة عبر مفتاح نصّي مثل IConfiguration["Key:SubKey"] لها نقطة ضعف: لا يمكن اكتشاف الخطأ الإملائيّ، ويصعب التتبّع كلّما زاد التداخل عمقاً. في العمل الفعليّ، الأساس هو استخدام نمط الخيارات (options pattern) الذي يربط الإعدادات بصنف POCO ويستقبلها كنوع.
public sealed class ExternalApiOptions
{
public const string SectionName = "ExternalApi";
public required string BaseUrl { get; set; }
public required string ApiKey { get; set; }
public int TimeoutSeconds { get; set; } = 30;
}
يُكتَب التسجيل والربط كالتالي.
using Microsoft.Extensions.DependencyInjection;
builder.Services
.AddOptions<ExternalApiOptions>()
.Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName));
توجد ثلاث واجهات لاستقبال الإعدادات، ولكلّ منها طبيعة مختلفة.5
| الواجهة | عمر التسجيل (lifetime) | تطبيق تغيير الإعدادات | الاستخدام الرئيسيّ |
|---|---|---|---|
IOptions<T> |
singleton | لا يُطبَّق (يُحسَب مرّة واحدة فقط عند بدء التشغيل) | إعدادات يُفترَض أنّها لا تتغيّر. الأبسط |
IOptionsSnapshot<T> |
scope | يُعاد الحساب كلّما أُعيد بناء الـ scope | سياقات ذات scope واضح، كنطاق طلب الويب (request scope) |
IOptionsMonitor<T> |
singleton | يمكن الحصول على أحدث قيمة في أيّ وقت مع إشعار تغيير (OnChange) |
سياقات يُراد فيها كشف التغيير فوراً، كالخدمات المقيمة |
في تطبيقات لا تملك نطاق طلب HTTP، كتطبيقات سطح المكتب أو خدمات Windows، فإنّ استخدام IOptionsSnapshot<T> يتصرّف عمليّاً مثل IOptions<T> تماماً ما لم تُنشئ الـ scope بنفسك. إن أردتَ كشف التغيير، فاختيار IOptionsMonitor<T> مباشرةً يقلّل التردّد.
تُستَلَم قيم الإعدادات (BaseUrl وApiKey والمهلة الزمنيّة) من IOptionsMonitor في كلّ استدعاء، لكنّ HttpClient نفسه يُستلَم من IHttpClientFactory ويُعاد استخدامه. فإنشاء HttpClient بـ new والتخلّص منه (Dispose) في كلّ استدعاء يعني تدمير مجمّع المقابس والاتّصالات الداخليّ وإعادة بنائه في كلّ مرّة، ما يؤدّي في الاستطلاع (polling) عالي التواتر أو معالجة الدفعات إلى استنزاف المنافذ العابرة (ephemeral ports)؛ لذا القاعدة المتّبعة هي ترك إعادة استخدام الاتّصال لـ IHttpClientFactory حتّى لو تغيّرت القيم.
using Microsoft.Extensions.Options;
public sealed class ExternalApiClient(
IHttpClientFactory httpClientFactory,
IOptionsMonitor<ExternalApiOptions> optionsMonitor)
{
public async Task<string> FetchAsync(CancellationToken cancellationToken)
{
// يُؤخَذ أحدث قيمة في كلّ استدعاء. إن كان ملفّ الإعدادات قد تحدَّث فسينعكس ذلك
ExternalApiOptions current = optionsMonitor.CurrentValue;
// يُستلَم HttpClient نفسه عبر الـ factory، ويُعاد استخدام مجمّع الاتّصالات الداخليّ
HttpClient client = httpClientFactory.CreateClient(nameof(ExternalApiClient));
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(new Uri(current.BaseUrl), "status"));
request.Headers.Add("X-Api-Key", current.ApiKey);
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(current.TimeoutSeconds));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
using HttpResponseMessage response = await client.SendAsync(request, linkedCts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
}
في جهة الاستدعاء، سجِّل عميلاً مُسمَّى (named client) بالشكل builder.Services.AddHttpClient(nameof(ExternalApiClient));. القيم التي قد تتغيّر بتغيير الإعدادات مثل BaseUrl وApiKey تُحمَّل على HttpRequestMessage في كلّ طلب، بينما تُترَك تكلفة إنشاء نسخة HttpClient والتخلّص منها للـ factory ── وهذا هو توزيع الأدوار.
إن ظهر خطأ الإعدادات كاستثناء وقت التنفيذ، يطول التحقيق. بدمج التحقّق عبر DataAnnotations مع ValidateOnStart()، يمكن جعل التصميم بحيث لا يستطيع التطبيق ذو الإعدادات المعطوبة أن يبدأ التشغيل أصلاً.6
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
public sealed class ExternalApiOptions
{
public const string SectionName = "ExternalApi";
[Required, Url]
public required string BaseUrl { get; set; }
[Required, MinLength(16)]
public required string ApiKey { get; set; }
[Range(1, 300)]
public int TimeoutSeconds { get; set; } = 30;
}
builder.Services
.AddOptions<ExternalApiOptions>()
.Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart(); // يُنفَّذ التحقّق عند بدء تشغيل المضيف (StartAsync/RunAsync)، قبل بدء خدمات المضيف
إن لم تُضِف ValidateOnStart()، يتأخّر التحقّق إلى لحظة أوّل وصول فعليّ لتلك الخيارات. والتحقّق نفسه لا يعمل مباشرةً بعد Build()، بل عند بدء تشغيل المضيف (StartAsync/RunAsync)، لذا احذر أنّ الشيفرة الاختباريّة التي تكتفي بـ Build() دون استدعاء RunAsync() لن يُنفَّذ فيها التحقّق بعد. لمنع الحادثة المتقطّعة من نوع «بدأ التشغيل لكنّه ينهار فور فتح الشاشة التي تستخدم ذلك الإعداد»، أضِف ValidateOnStart() كقاعدة أساسيّة في إعدادات تطبيقات الأعمال.6
5. أين توضع الإعدادات القابلة للكتابة
appsettings.json هو مكان لوضع «قيم افتراضيّة للقراءة فقط»، وليس مكاناً لحفظ الإعدادات التي يعدّلها التطبيق نفسه. معظم تطبيقات الأعمال تُثبَّت تحت Program Files، ولا يملك المستخدم القياسيّ صلاحيّة الكتابة هناك، ما يؤدّي إلى حادثة إمّا استثناء وقت التشغيل أو اختلاف المحتوى الظاهر لكلّ مستخدم بسبب افتراض نظام الملفّات (file system virtualization) في Windows.
الإعدادات التي تحتاج إلى كتابة تُقسَّم حسب طبيعتها كالتالي. الأسلم هو الاعتماد على المجلّدات الخاصّة (special folders) التي يمكن الحصول عليها عبر Environment.GetFolderPath.11
| مكان الحفظ | طريقة الحصول عليه | الاستخدام |
|---|---|---|
%LOCALAPPDATA%\اسم الشركة\اسم التطبيق |
Environment.SpecialFolder.LocalApplicationData |
الافتراضيّ للإعدادات والبيانات الخاصّة بكلّ مستخدم |
%APPDATA%\اسم الشركة\اسم التطبيق (Roaming) |
Environment.SpecialFolder.ApplicationData |
فقط للإعدادات المُراد أن تتبع المستخدم في بيئة الملفّ الشخصيّ المتنقّل (roaming profile) |
%ProgramData%\اسم الشركة\اسم التطبيق |
Environment.SpecialFolder.CommonApplicationData |
إعدادات على مستوى الجهاز مشتركة بين كلّ المستخدمين. يلزم تصميم ACL |
using System;
using System.IO;
using System.Text.Json;
public sealed class UserSettingsStore
{
private readonly string _filePath;
public UserSettingsStore()
{
string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string dir = Path.Combine(root, "KomuraSoft", "MyApp");
Directory.CreateDirectory(dir);
_filePath = Path.Combine(dir, "user-settings.json");
}
public UserSettings Load()
{
if (!File.Exists(_filePath))
{
return new UserSettings();
}
string json = File.ReadAllText(_filePath);
return JsonSerializer.Deserialize<UserSettings>(json) ?? new UserSettings();
}
public void Save(UserSettings settings)
{
string json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
// يُفتَرض أنّ الكتابة المتعدّدة داخل نفس العمليّة تُسلسَل من جهة الاستدعاء
File.WriteAllText(_filePath, json);
}
}
public sealed class UserSettings
{
public string? LastOpenedFolder { get; set; }
public int WindowWidth { get; set; } = 1024;
public int WindowHeight { get; set; } = 768;
}
إن خُلطت الإعدادات الخاصّة بالمستخدم مع الإعدادات الخاصّة بالجهاز في ملفّ واحد، تحدث حادثة من نوع «إعدادات المستخدم أ تسبّب مشكلة للمستخدم ب» في بيئة يستخدم فيها عدّة مستخدمين نفس الجهاز. للاطّلاع على التمييز بين إعدادات المستخدم وإعدادات الجهاز، وفهم بنية ملفّات تعريف مستخدمي Windows نفسها، راجع «آليّة ملفّات تعريف مستخدمي Windows»، وللاطّلاع على جدول قرار شامل لاختيار مكان الحفظ، راجع أيضاً «كيفيّة اختيار مكان حفظ بيانات تطبيقات Windows».
6. المعلومات السرّيّة ── سلسلة الاتّصال ومفتاح API
كتابة كلمة مرور سلسلة الاتّصال أو مفتاح API مباشرةً في appsettings.json تبقى خطراً من الناحية العمليّة حتّى لو أُدرِج الملفّ في .gitignore. فقد يتسرّب النصّ الصريح عبر التوزيع في مجلّد مشترك، أو إرسال الملفّ ضمن الدعم الفنّيّ، أو بقاء ملفّ إعدادات قديم «متحجّر» في نسخة احتياطيّة.
في وقت التطوير، استخدم Secret Manager (dotnet user-secrets). لكنّه آليّة لتجربة التطوير، وغير مشفَّر. تُحفَظ القيم كنصّ JSON صريح في %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json، والوثائق الرسميّة نفسها تنصّ صراحةً على «عدم معاملته كمخزن موثوق، وأنّه مخصَّص للتطوير فقط». استخدامه بوضع معلومات سرّيّة للإنتاج داخل user-secrets وتوزيعها هو استخدام خاطئ.7
dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "قيمة للتطوير"
في التشغيل الفعليّ، استخدم DPAPI (Data Protection API) التي يوفّرها Windows للتشفير المرتبط ببيانات اعتماد المستخدم أو الجهاز. الغلاف (wrapper) هو Protect/Unprotect من System.Security.Cryptography.ProtectedData، وباستخدام DataProtectionScope.CurrentUser، لا يمكن فكّ التشفير إلّا لمن سجّل الدخول بنفس حساب المستخدم وعلى نفس الجهاز. DPAPI ميزة خاصّة بـ Windows فقط، وتُرمى في المنصّات الأخرى PlatformNotSupportedException، فضع ذلك في الاعتبار عند التصميم.8
using System.Security.Cryptography;
using System.Text;
public static class SecretProtector
{
// خلط إنتروبيا (entropy) تدلّ على الغرض يقلّل احتمال الخلط مع بيانات محميّة لأغراض أخرى
private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("MyApp.ExternalApi.ApiKey");
public static string Protect(string plainText)
{
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] protectedBytes = ProtectedData.Protect(plainBytes, Entropy, DataProtectionScope.CurrentUser);
return Convert.ToBase64String(protectedBytes);
}
public static string Unprotect(string protectedBase64)
{
byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
byte[] plainBytes = ProtectedData.Unprotect(protectedBytes, Entropy, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(plainBytes);
}
}
التفاصيل على مستوى التنفيذ – أين تُحفَظ القيمة المحميّة بـ DPAPI، وأيّ نطاق (scope) يُختار بين CurrentUser وLocalMachine، والمطبّات في تطبيقات الأعمال التي يستخدمها عدّة مستخدمين على نفس الجهاز – مشروحة بتفصيل أكبر في «حفظ المعلومات السرّيّة في تطبيقات Windows - تجنّب الإعدادات النصّيّة الصريحة عبر DPAPI»، فراجعها عند التنفيذ.
الخطّ الفاصل بين ما يجوز تركه نصّاً صريحاً وما لا يجوز بسيط: يُحكَم عليه بـ«هل يتطلّب تسرّب هذه القيمة إعادة إصدار كلمة المرور أو مفتاح API، أو التحقيق في وصول غير مصرَّح به، أو الإبلاغ لجهة إشرافيّة؟». اسم مضيف سلسلة الاتّصال أو رقم المنفذ غالباً ما يكون ضرره ضئيلاً عند التسرّب، أمّا كلمة المرور أو رمز المصادقة المُضمَّن فيها فيبقى دائماً موضع حماية. بتصميم سلسلة الاتّصال مبنيّة من «معلومات الخادم» و«بيانات الاعتماد» بشكل منفصل، وجعل الأخيرة فقط موضوعاً لحماية DPAPI، يمكن الفصل على مستوى الشيفرة بين الجزء الذي يكون ضرره ضئيلاً إن بقي نصّاً صريحاً والجزء الواجب حمايته.
7. تغيير الإعدادات أثناء التشغيل ── واقع reloadOnChange
الاستدعاء الافتراضيّ لـ AddJsonFile (الذي ينفّذه Host.CreateApplicationBuilder داخليّاً) مضبوط على reloadOnChange: true، فيُكشَف تغيّر ملفّي appsettings.json / appsettings.{Environment}.json وتُعاد قراءتهما تلقائيّاً. آليّة التنفيذ هي أنّ PhysicalFileProvider يستخدم داخليّاً FileSystemWatcher لمراقبة التغيّرات.12
لهذه الآليّة عدّة حدود من الناحية العمليّة.
- لا يُطبَّق التغيير إلّا على القيم المقروءة عبر
IOptionsMonitor<T>(وIOptionsSnapshot<T>). يحتفظIOptions<T>بالقيمة عند بدء التشغيل، لذا تبقى القيمة القديمة حتّى بعد تعديل الملفّ. معظم الاستفسارات من نوع «عدّلتُ ملفّ الإعدادات مباشرةً لكنّه لا ينعكس» سببها الخلط بين هاتين الواجهتين. - قد لا يُرسِل
FileSystemWatcherإشعار التغيير بشكل مضمون في أنظمة ملفّات مثل حاويات Docker أو المشاركات الشبكيّة (network share). في مثل هذه البيئات، يُحوَّل إلى مراقبة استقصائيّة (polling) بفاصل 4 ثوانٍ (لا يمكن تغيير الفاصل) بضبط متغيّر البيئةDOTNET_USE_POLLING_FILE_WATCHERعلىtrue.13 لخّصنا الخصائص العامّة لـFileSystemWatcherنفسه – كالتفويت وطفح المخزن المؤقّت وتكرار الأحداث – في «الدليل العمليّ لـ FileSystemWatcher». - قد يُطلَق إشعار تغيير ملفّ الإعدادات عدّة مرّات لتغيير واحد فقط في الملفّ. إن كان التطبيق يُنفِّذ معالجة ثقيلة عند كشف التغيير، يلزم اعتبارات مثل تجميع (debounce) الإطلاقات المتتالية القصيرة، أو مقارنة hash محتوى الملفّ لمعالجة التغيّر الفعليّ فقط.
لا حاجة للسعي دائماً إلى «تطبيق تغيير الإعدادات دون إعادة تشغيل». يجوز تطبيق القيم الخفيفة مثل مستوى السجلّ أو أعلام الميزات فوراً عبر IOptionsMonitor، أمّا القيم التي قد تتعارض مع الموارد العاملة فور تغييرها – مثل سلسلة اتّصال قاعدة البيانات أو حجم thread pool – فمن الأسلم التصريح صراحةً بأنّها «تُطبَّق بإعادة التشغيل» كمواصفة. الأهمّ من استخدام reloadOnChange من عدمه هو تصميم يُبلِّغ المسؤول عن التشغيل بوضوح: «هذا الإعداد يسري فور الحفظ» أو «هذا الإعداد يتطلّب إعادة التشغيل».
8. خريطة الانتقال من app.config / Settings.settings
عند ترحيل تطبيق من زمن .NET Framework إلى .NET، يلزم أيضاً إعادة بناء آليّة الإعدادات. لنُنظّم علاقات التطابق.14
| .NET Framework | .NET | ملاحظات |
|---|---|---|
<appSettings> في App.config / Web.config |
appsettings.json + IConfiguration |
يمكن التعبير عن البنية الهرميّة بشكل طبيعيّ عبر تداخل JSON |
ConfigurationManager.AppSettings["Key"] |
builder.Configuration["Key"] أو IOptions<T> |
من الوصول النصّيّ إلى الوصول المُنمَّط (typed) |
ConfigurationManager.ConnectionStrings |
builder.Configuration.GetConnectionString("Name") |
يُحافَظ على اتّفاقيّة قسم ConnectionStrings |
Settings.settings (نطاق المستخدم) |
ملفّ JSON خاصّ تحت %LOCALAPPDATA% |
لا توجد آليّة توليد تلقائيّ مثل ApplicationSettingsBase؛ يجب التسلسل والحفظ بنفسك |
Settings.settings (نطاق التطبيق) |
appsettings.json |
يُعامَل كقيمة افتراضيّة للقراءة فقط |
تشفير <connectionStrings> (عبر aspnet_regiis وما شابه) |
DPAPI (ProtectedData) |
تتغيّر آليّة التشفير نفسها؛ يلزم إعادة البناء عند الترحيل |
هناك مطبّان شائعان يُقَع فيهما أثناء الترحيل.
الأوّل هو أنّ إضافة حزمة NuGet باسم System.Configuration.ConfigurationManager تجعل شيفرة قراءة App.config تعمل كما هي. هذا وسيلة فعّالة كخطوة أولى في الترحيل، لكن ينبغي عدم تركها هكذا، بل إدراج الانتقال إلى appsettings.json ضمن الخطّة. فحتّى المكتبات المحيطة مثل مزوّدي الـ logging تنتقل بالكامل تقريباً نحو افتراض appsettings.json، فيتضاءل مبرِّر إبقاء الآليّة القديمة لمجرّد قراءة App.config.14
الثاني هو إعدادات نطاق المستخدم في Settings.settings. كان في .NET Framework آليّة تحفظ تلقائيّاً إعدادات كلّ مستخدم بمجرّد استدعاء Properties.Settings.Default.Save()، لكن لا توجد في .NET آليّة توليد تلقائيّ مكافئة قياسيّاً. عند الترحيل، يلزم تجهيز صنف حفظ خاصّ كما وُضِّح في الفصل 5.
جانب آخر من الترحيل ── التوافق مع المكتبات التابعة، ووجود تكامل COM من عدمه، وإعادة النظر في طريقة التوزيع، وغيرها من البنود الواجب التحقّق منها خارج نطاق إدارة الإعدادات، مُلخَّصة من زاوية الجرد الشامل في «قائمة تحقّق ما قبل الانتقال من .NET Framework إلى .NET»، فنوصي بمراجعتها في المرحلة المبكّرة من مشروع الترحيل.
الخلاصة
تتألّف إدارة الإعدادات في .NET من تركيب ثلاث آليّات: تراكب المزوّدين عبر IConfiguration، والاستلام الآمن من ناحية النوع عبر عائلة IOptions، وتبديل البيئة عبر متغيّرات البيئة. هذا الجزء يُستخدَم بنفس الطريقة في معظم التطبيقات، لكنّ الهمّ الخاصّ بتطبيقات Windows للأعمال يتركّز فيما بعد ذلك ── أين تُوضَع الإعدادات القابلة للكتابة، وكيف تُحمى المعلومات السرّيّة، وإلى أيّ حدّ يُسمَح بتغيير الإعدادات أثناء التشغيل، وكيف تُطوى أصول جيل app.config.
خطوة إلى الأمام من حالة «كلّ شيء مكتوب في appsettings.json»، بفصل مكان الحفظ وطريقة التعامل حسب نوع كلّ إعداد. نأمل أن يكون جدول القرار في هذا المقال نقطة انطلاق لذلك التنظيم. جرد إدارة الإعدادات لتطبيق قائم، أو استشارة اتّجاه الانتقال من app.config، غالباً ما لا يظهر حلّها الأمثل إلّا بالنظر إلى ملفّات الإعدادات الفعليّة وبيئة النشر، فلا تتردَّد بالتواصل معنا عند التردّد.
مقالات ذات صلة
- ما هو .NET Generic Host
- استخدام Generic Host + BackgroundService في تطبيق سطح مكتب
- لماذا لا تُنفَّذ مهامّ Task Scheduler، أو تنتهي برمز 0x1
- كيفيّة بناء خدمات Windows وتشغيلها
- كيفيّة اختيار مكان حفظ بيانات تطبيقات Windows
- آليّة ملفّات تعريف مستخدمي Windows
- حفظ المعلومات السرّيّة في تطبيقات Windows - تجنّب الإعدادات النصّيّة الصريحة عبر DPAPI
- الدليل العمليّ لـ FileSystemWatcher
- قائمة تحقّق ما قبل الانتقال من .NET Framework إلى .NET
مجالات الاستشارة ذات الصلة
تتعامل شركة Komura Soft LLC مع تصميم إدارة الإعدادات لتطبيقات Windows للأعمال، وتصميم تشغيل الإعدادات حسب البيئة، والاستشارة التقنيّة حول اتّجاه الانتقال من أصول app.config القائمة.
المراجع
</content>
-
Microsoft Learn، Configuration in .NET - Alternative hosting approach. حول أولويّة مزوّدي الإعدادات التي يبنيها
Host.CreateApplicationBuilderافتراضيّاً (معطيات سطر الأوامر ← متغيّرات البيئة ← user secrets في بيئة التطوير ← appsettings.{Environment}.json ← appsettings.json). ↩ ↩2 ↩3 -
Microsoft Learn، .NET Generic Host - Host builder settings. حول الترتيب الافتراضيّ لإعدادات المضيف (host configuration) التي يقرؤها
Host.CreateApplicationBuilder(متغيّرات البيئة ذات البادئة DOTNET_، ومعطيات سطر الأوامر) وإعدادات التطبيق (appsettings.json، وappsettings.{Environment}.json، وSecret Manager، ومتغيّرات البيئة، ومعطيات سطر الأوامر). ↩ ↩2 -
Microsoft Learn، Use the .NET Generic Host in a Windows Forms app. حول خطوات دمج Generic Host في تطبيق Windows Forms والاستفادة من DI والإعدادات والـ logging. ↩
-
Microsoft Learn، ASP.NET Core runtime environments - Environment variables that determine the runtime environment. حول العلاقة بين DOTNET_ENVIRONMENT وASPNETCORE_ENVIRONMENT، وأولويّة DOTNET_ENVIRONMENT عند استخدام WebApplication، والقيمة الافتراضيّة Production عند عدم الضبط، وأنّ Windows لا يُفرِّق بين حالة الأحرف في اسم متغيّر البيئة بينما Linux يُفرِّق. ↩ ↩2
-
Microsoft Learn، Options pattern in .NET - Options interfaces. حول الفروق بين
IOptions<TOptions>وIOptionsSnapshot<TOptions>وIOptionsMonitor<TOptions>من ناحية عمر التسجيل وتوقيت تطبيق تغيير الإعدادات والميزات المدعومة. ↩ ↩2 -
Microsoft Learn، Options pattern in .NET - Options validation. حول طريقة ضبط التحقّق عبر DataAnnotations بواسطة
ValidateDataAnnotations()، والتحقّق عند بدء التشغيل بواسطةValidateOnStart()(أوAddOptionsWithValidateOnStart). ↩ ↩2 ↩3 -
Microsoft Learn، Safe storage of app secrets in development in ASP.NET Core - Use the Secret Manager tool. حول أنّ Secret Manager يحفظ المعلومات السرّيّة كنصّ صريح في
%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.jsonدون تشفير، وأنّه مخصَّص للتطوير فقط ولا ينبغي معاملته كمخزن موثوق. ↩ ↩2 -
Microsoft Learn، ProtectedData Class. حول تابعَي
ProtectedData.Protect/Unprotectاللذين يُغلِّفان DPAPI (Data Protection API)، والفرق بين نطاقَيDataProtectionScope.CurrentUser/LocalMachine، وكونها ميزة خاصّة بـ Windows فقط تُرمى في المنصّات الأخرى كاستثناءPlatformNotSupportedException. ↩ ↩2 -
Microsoft Learn، Configuration providers in .NET. حول آليّة مزوّدي الإعدادات التي توحّد مصادر إعدادات مختلفة الطبيعة – JSON، ومتغيّرات البيئة، وسطر الأوامر، وINI، وXML – خلف
IConfiguration. ↩ -
Microsoft Learn، Configuration providers in .NET - Environment variable configuration provider. حول أنّ الإعدادات الافتراضيّة تقرأ متغيّرات البيئة ذات البادئة DOTNET_ ومعطيات سطر الأوامر ضمن إعدادات المضيف وإعدادات التطبيق، لكنّها لا تُستخدَم في إعدادات المستخدم، وطريقة إضافة بادئة مخصَّصة. ↩
-
Microsoft Learn، Environment.GetFolderPath Method. حول طريقة الحصول على مسارات المجلّدات الخاصّة عبر تعداد
Environment.SpecialFolderوتابعGetFolderPath. ↩ -
Microsoft Learn، Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. حول معطى
reloadOnChangeالخاصّ بـAddJsonFile، وآليّة استخدامPhysicalFileProviderداخليّاً لـFileSystemWatcherلمراقبة تغيّر ملفّ الإعدادات. ↩ -
Microsoft Learn، Options pattern in .NET - IOptionsMonitor. حول اقتصار إشعار تغيير
IOptionsMonitorعلى مزوّدي الإعدادات المبنيّين على نظام الملفّات، وإمكانيّة التحوّل إلى مراقبة استقصائيّة بفاصل 4 ثوانٍ عبر متغيّر البيئةDOTNET_USE_POLLING_FILE_WATCHERعندما لا يكون إشعار التغيير مضموناً في حاويات Docker أو المشاركات الشبكيّة. ↩ -
Microsoft Learn، Modernize after upgrading to .NET from .NET Framework - App.config. حول خطوات الانتقال من App.config إلى appsettings.json، والحفاظ على التوافق عبر حزمة NuGet باسم
System.Configuration.ConfigurationManager، واستخدام حزمةMicrosoft.Extensions.Configuration.Json. ↩ ↩2
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
لا تُحِط HttpClient بـ using ── الممارسة العمليّة للاتّصال عبر HTTP في تطبيقات C# للأعمال (أنماط الإنشاء، وضبط المهلة، وإعادة المحاولة)
إنشاء HttpClient داخل using في كلّ مرّة يؤدّي إلى استنزاف المقابس (sockets)، وجعله static يجعله لا يتابع تغيّر DNS ── نشرح آليّة هاتين ال...
تحديد سبب «البطء» باستخدام PerfView وdotnet-trace ── مدخل عمليّ لتحقيق أداء .NET
عندما يصبح تطبيق الأعمال «بطيئاً» أو «يستهلك المعالج بالكامل»، ما الأداة التي تستخدمها وما الذي تنظر إليه؟ ننظِّم توزيع الأدوار بين PerfV...
الطباعة وإخراج PDF في تطبيقات أعمال Windows ── كيفيّة الاختيار بين System.Drawing.Printing وWPF ومكتبات التقارير
نرتّب عبر جدول قرار حسب المتطلّبات طباعة WinForms باستخدام PrintDocument، وطباعة FlowDocument/FixedDocument في WPF، وخيارات إخراج PDF. ون...
عندما يُعامَل تطبيق Windows الذي طوّرته شركتك بوصفه فيروساً ── التعامل مع الكشف الخاطئ في Microsoft Defender والتعايش مع أثره على الأداء
نرتّب الإجراء الرسميّ عند كشف Microsoft Defender تطبيق Windows الذي طوّرته شركتك خطأً. نشرح آليّة برامج مكافحة الفيروسات الحديثة، والإبلا...
السكون والإسبات وModern Standby والتطبيقات طويلة التشغيل ── منع «التوقّف في منتصف الليل» بالتصميم
نرتّب أسباب وصول تطبيقات Windows طويلة التشغيل إلى حالة «توقّفت عندما نظرت صباحاً»، انطلاقاً من الفرق بين سكون S3 والإسبات وModern Standb...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
لأن إدارة الإعدادات وتصميم ملفّات الإعدادات يدخلان ضمن نطاق الاستشارات العمليّة لتطوير تطبيقات Windows.
الاستشارات التقنية ومراجعة التصميم
لأن تحديد اتّجاه الانتقال من app.config القائم يندرج ضمن استشارة تقنيّة تتضمّن مراجعة تصميم.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل يجوز وضع appsettings.json في نفس مجلّد الـ exe؟
- لا مشكلة إن كانت قيماً افتراضيّة للقراءة فقط. لكن إن كان ذلك المجلّد يُثبَّت تحت Program Files، فلا يمكن تصميم التطبيق بحيث يعدّل appsettings.json بنفسه. فالمستخدم القياسيّ لا يملك صلاحيّة الكتابة تحت Program Files، ما يؤدّي إمّا إلى استثناء وقت التشغيل، أو إلى اختلاف المحتوى الظاهر لكلّ مستخدم بسبب ميزة الافتراض (virtualization) في Windows. افصل الإعدادات التي تحتاج إلى كتابة في ملفّ منفصل تحت %LOCALAPPDATA% أو %ProgramData%.
- أيّهما ينبغي أن يكون له الأولويّة، متغيّرات البيئة أم appsettings.json؟
- الأساس هو عدم تغيير ترتيب الأولويّة الافتراضيّ، واستخدام الآليّة كما هي حيث يُعاد الكتابة فوق القيم بالترتيب: appsettings.json ← appsettings.{Environment}.json ← متغيّرات البيئة ← معطيات سطر الأوامر. ومعيار الحكم عند التردّد هو «طبيعة القيمة». إن جعلتَ القيم الافتراضيّة التي يجوز تضمينها في نتاج البناء (build) توضَع في appsettings.json، والقيم التي تختلف حسب وجهة النشر أو الحاوية (container) توضَع في متغيّرات البيئة، فلن يبقى مجال للتردّد لاحقاً.
- كيف ينبغي تقسيم أصناف (classes) الإعدادات؟
- الأساس هو تقسيم أصناف الخيارات (options) حسب وحدة الوظيفة أو المسؤوليّة. فمثلاً ConnectionOptions لسلسلة الاتّصال، وExternalApiOptions لتكامل واجهة API الخارجيّة، بحيث لا تُحشَر إعدادات غير مترابطة في صنف واحد. وعند التقسيم، تنفصل بشكل طبيعيّ أيضاً وحدات التحقّق (validation) وإعادة القراءة عبر IOptionsSnapshot/IOptionsMonitor، ويكتمل التحقّق عبر DataAnnotations لكلّ صنف على حدة.
- هل ينبغي الانتقال من ملفّات INI أو الـ Registry؟
- إن كنتَ تعيد بناء إدارة الإعدادات من الصفر، فالموصى به هو التوحيد على appsettings.json + IConfiguration. ويوفّر .NET أيضاً مزوّد إعدادات (configuration provider) لملفّات INI، لذا يمكن قراءة ملفّات INI القائمة كما هي مع الانتقال تدريجيّاً. أمّا الـ Registry فليس ضمن النطاق القياسيّ لمزوّدي الإعدادات في .NET، لذا إن وُجدت أصول قائمة تعتمد على الـ Registry، فالحلّ الواقعيّ هو كتابة شيفرة جسر (bridge) للقراءة فقط، ثمّ التحوّل تدريجيّاً نحو appsettings.json.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة