Ubah app.config default saat runtime

130

Saya memiliki masalah berikut:
Kami memiliki aplikasi yang memuat modul (tambahan). Modul-modul ini mungkin memerlukan entri di app.config (mis. Konfigurasi WCF). Karena modul dimuat secara dinamis, saya tidak ingin memiliki entri ini di file app.config aplikasi saya.
Yang ingin saya lakukan adalah sebagai berikut:

  • Buat app.config baru dalam memori yang menggabungkan bagian konfigurasi dari modul
  • Beri tahu aplikasi saya untuk menggunakan app.config baru itu

Catatan: Saya tidak ingin menimpa app.config default!

Ini harus bekerja secara transparan, sehingga misalnya ConfigurationManager.AppSettingsmenggunakan file baru itu.

Selama evaluasi masalah ini, saya datang dengan solusi yang sama seperti yang disediakan di sini: Muat ulang app.config dengan nunit .
Sayangnya, sepertinya tidak melakukan apa-apa, karena saya masih mendapatkan data dari app.config yang normal.

Saya menggunakan kode ini untuk mengujinya:

Console.WriteLine(ConfigurationManager.AppSettings["SettingA"]);
Console.WriteLine(Settings.Default.Setting);

var combinedConfig = string.Format(CONFIG2, CONFIG);
var tempFileName = Path.GetTempFileName();
using (var writer = new StreamWriter(tempFileName))
{
    writer.Write(combinedConfig);
}

using(AppConfig.Change(tempFileName))
{
    Console.WriteLine(ConfigurationManager.AppSettings["SettingA"]);
    Console.WriteLine(Settings.Default.Setting);
}

Ini mencetak nilai yang sama dua kali lipat, meskipun combinedConfigberisi nilai-nilai selain app.config yang normal.

Daniel Hilgarth
sumber
Hosting modul secara terpisah AppDomaindengan file konfigurasi yang sesuai bukan merupakan pilihan?
João Angelo
Tidak juga, karena itu akan menghasilkan banyak panggilan Cross-AppDomain, karena aplikasi berinteraksi cukup banyak dengan modul.
Daniel Hilgarth
Bagaimana dengan aplikasi restart ketika modul baru perlu dimuat?
João Angelo
Ini tidak bekerja bersama dengan persyaratan bisnis. Selain itu, saya tidak dapat menimpa app.config, karena pengguna tidak memiliki hak untuk melakukannya.
Daniel Hilgarth
Anda akan memuat ulang untuk memuat App.config yang berbeda, bukan yang ada di file program. PeretasanReload app.config with nunit dapat berfungsi, tidak yakin, jika digunakan pada entri aplikasi sebelum konfigurasi apa pun dimuat.
João Angelo

Jawaban:

280

Retasan dalam pertanyaan terkait berfungsi jika digunakan sebelum sistem konfigurasi digunakan pertama kali. Setelah itu, tidak berfungsi lagi.
Alasannya:
Ada kelas ClientConfigPathsyang cache jalur. Jadi, bahkan setelah mengubah lintasan dengan SetData, itu tidak dibaca kembali, karena sudah ada nilai-nilai yang di-cache. Solusinya adalah menghapus ini juga:

using System;
using System.Configuration;
using System.Linq;
using System.Reflection;

public abstract class AppConfig : IDisposable
{
    public static AppConfig Change(string path)
    {
        return new ChangeAppConfig(path);
    }

    public abstract void Dispose();

    private class ChangeAppConfig : AppConfig
    {
        private readonly string oldConfig =
            AppDomain.CurrentDomain.GetData("APP_CONFIG_FILE").ToString();

        private bool disposedValue;

        public ChangeAppConfig(string path)
        {
            AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", path);
            ResetConfigMechanism();
        }

        public override void Dispose()
        {
            if (!disposedValue)
            {
                AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", oldConfig);
                ResetConfigMechanism();


                disposedValue = true;
            }
            GC.SuppressFinalize(this);
        }

        private static void ResetConfigMechanism()
        {
            typeof(ConfigurationManager)
                .GetField("s_initState", BindingFlags.NonPublic | 
                                         BindingFlags.Static)
                .SetValue(null, 0);

            typeof(ConfigurationManager)
                .GetField("s_configSystem", BindingFlags.NonPublic | 
                                            BindingFlags.Static)
                .SetValue(null, null);

            typeof(ConfigurationManager)
                .Assembly.GetTypes()
                .Where(x => x.FullName == 
                            "System.Configuration.ClientConfigPaths")
                .First()
                .GetField("s_current", BindingFlags.NonPublic | 
                                       BindingFlags.Static)
                .SetValue(null, null);
        }
    }
}

Penggunaannya seperti ini:

// the default app.config is used.
using(AppConfig.Change(tempFileName))
{
    // the app.config in tempFileName is used
}
// the default app.config is used.

Jika Anda ingin mengubah app.config yang digunakan untuk seluruh runtime aplikasi Anda, cukup masukkan AppConfig.Change(tempFileName)tanpa menggunakan suatu tempat di awal aplikasi Anda.

Daniel Hilgarth
sumber
4
Ini benar-benar luar biasa. Terima kasih banyak telah memposting ini.
user981225
3
@Daniel Itu luar biasa - Saya mengerjakannya menjadi metode exentension untuk ApplicationSettingsBase, sehingga saya bisa memanggil Settings.Default.RedirectAppConfig (path). Saya akan memberi Anda +2 jika saya bisa!
JMarsch
2
@ PhilWhittington: Itu yang saya katakan, ya.
Daniel Hilgarth
2
keluar dari minat, adakah alasan untuk menekan finalizer apakah tidak ada finalizer dinyatakan?
Gusdor
3
Selain itu, menggunakan refleksi untuk mengakses bidang pribadi mungkin berfungsi sekarang, tetapi itu bisa menggunakan peringatan bahwa itu tidak didukung dan dapat merusak versi NET Framework.
10

Anda dapat mencoba menggunakan Configuration dan Add ConfigurationSection saat runtime

Configuration applicationConfiguration = ConfigurationManager.OpenMappedExeConfiguration(
                        new ExeConfigurationFileMap(){ExeConfigFilename = path_to_your_config,
                        ConfigurationUserLevel.None
                        );

applicationConfiguration.Sections.Add("section",new YourSection())
applicationConfiguration.Save(ConfigurationSaveMode.Full,true);

EDIT: Ini adalah solusi berdasarkan refleksi (meskipun tidak terlalu bagus)

Buat kelas yang berasal dari IInternalConfigSystem

public class ConfigeSystem: IInternalConfigSystem
{
    public NameValueCollection Settings = new NameValueCollection();
    #region Implementation of IInternalConfigSystem

    public object GetSection(string configKey)
    {
        return Settings;
    }

    public void RefreshConfig(string sectionName)
    {
        //throw new NotImplementedException();
    }

    public bool SupportsUserConfig { get; private set; }

    #endregion
}

lalu melalui refleksi set ke bidang pribadi di ConfigurationManager

        ConfigeSystem configSystem = new ConfigeSystem();
        configSystem.Settings.Add("s1","S");

        Type type = typeof(ConfigurationManager);
        FieldInfo info = type.GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static);
        info.SetValue(null, configSystem);

        bool res = ConfigurationManager.AppSettings["s1"] == "S"; // return true
Stecya
sumber
Saya tidak melihat bagaimana ini membantu saya. Ini akan menambahkan bagian ke file yang ditentukan oleh file_path. Ini tidak akan membuat bagian tersedia untuk pengguna ConfigurationManager.GetSection, karena GetSectionmenggunakan app.config default.
Daniel Hilgarth
Anda dapat menambahkan Bagian ke app.config yang ada. Baru saja mencoba ini - bekerja untuk saya
Stecya
Kutipan dari pertanyaan saya: "Catatan: Saya tidak ingin menimpa app.config default!"
Daniel Hilgarth
5
Apa yang salah? Sederhana: Pengguna tidak berhak menimpanya, karena program diinstal di% ProgramFiles% dan pengguna bukan administrator.
Daniel Hilgarth
2
@Stecya: Terima kasih atas usaha Anda. Tapi tolong lihat jawaban saya untuk solusi nyata untuk masalah ini.
Daniel Hilgarth
5

Solusi @aniel bekerja dengan baik. Solusi serupa dengan penjelasan lebih lanjut ada di sudut c-sharp. Untuk kelengkapan, saya ingin membagikan versi saya: dengan using, dan bendera bit disingkat.

using System;//AppDomain
using System.Linq;//Where
using System.Configuration;//app.config
using System.Reflection;//BindingFlags

    /// <summary>
    /// Use your own App.Config file instead of the default.
    /// </summary>
    /// <param name="NewAppConfigFullPathName"></param>
    public static void ChangeAppConfig(string NewAppConfigFullPathName)
    {
        AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", NewAppConfigFullPathName);
        ResetConfigMechanism();
        return;
    }

    /// <summary>
    /// Remove cached values from ClientConfigPaths.
    /// Call this after changing path to App.Config.
    /// </summary>
    private static void ResetConfigMechanism()
    {
        BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Static;
        typeof(ConfigurationManager)
            .GetField("s_initState", Flags)
            .SetValue(null, 0);

        typeof(ConfigurationManager)
            .GetField("s_configSystem", Flags)
            .SetValue(null, null);

        typeof(ConfigurationManager)
            .Assembly.GetTypes()
            .Where(x => x.FullName == "System.Configuration.ClientConfigPaths")
            .First()
            .GetField("s_current", Flags)
            .SetValue(null, null);
        return;
    }
Roland
sumber
4

Jika ada yang tertarik, inilah metode yang bisa digunakan pada Mono.

string configFilePath = ".../App";
System.Configuration.Configuration newConfiguration = ConfigurationManager.OpenExeConfiguration(configFilePath);
FieldInfo configSystemField = typeof(ConfigurationManager).GetField("configSystem", BindingFlags.NonPublic | BindingFlags.Static);
object configSystem = configSystemField.GetValue(null);
FieldInfo cfgField = configSystem.GetType().GetField("cfg", BindingFlags.Instance | BindingFlags.NonPublic);
cfgField.SetValue(configSystem, newConfiguration);
LiohAu
sumber
3

Solusi Daniel tampaknya berfungsi bahkan untuk majelis hilir yang pernah saya gunakan AppDomain.SetData sebelumnya, tetapi tidak mengetahui cara mengatur ulang flag konfigurasi internal

Dikonversi ke C ++ / CLI untuk mereka yang tertarik

/// <summary>
/// Remove cached values from ClientConfigPaths.
/// Call this after changing path to App.Config.
/// </summary>
void ResetConfigMechanism()
{
    BindingFlags Flags = BindingFlags::NonPublic | BindingFlags::Static;
    Type ^cfgType = ConfigurationManager::typeid;

    Int32 ^zero = gcnew Int32(0);
    cfgType->GetField("s_initState", Flags)
        ->SetValue(nullptr, zero);

    cfgType->GetField("s_configSystem", Flags)
        ->SetValue(nullptr, nullptr);

    for each(System::Type ^t in cfgType->Assembly->GetTypes())
    {
        if (t->FullName == "System.Configuration.ClientConfigPaths")
        {
            t->GetField("s_current", Flags)->SetValue(nullptr, nullptr);
        }
    }

    return;
}

/// <summary>
/// Use your own App.Config file instead of the default.
/// </summary>
/// <param name="NewAppConfigFullPathName"></param>
void ChangeAppConfig(String ^NewAppConfigFullPathName)
{
    AppDomain::CurrentDomain->SetData(L"APP_CONFIG_FILE", NewAppConfigFullPathName);
    ResetConfigMechanism();
    return;
}
Tagihan
sumber
1

Jika file konfigurasi Anda hanya ditulis dengan kunci / nilai di "pengaturan aplikasi", maka Anda dapat membaca file lain dengan kode seperti itu:

System.Configuration.ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
configFileMap.ExeConfigFilename = configFilePath;

System.Configuration.Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
AppSettingsSection section = (AppSettingsSection)configuration.GetSection("appSettings");

Kemudian Anda bisa membaca bagian. Pengaturan sebagai kumpulan KeyValueConfigurationElement.

Ron
sumber
1
Seperti yang sudah saya katakan, saya ingin ConfigurationManager.GetSectionmembaca file baru yang saya buat. Solusi Anda tidak melakukan itu.
Daniel Hilgarth
@Aniel: mengapa? Anda dapat menentukan file apa saja di "configFilePath". Jadi, Anda hanya perlu tahu lokasi file yang baru dibuat. Apakah saya melewatkan sesuatu? Atau Anda benar-benar perlu menggunakan "ConfigurationManager.GetSection" dan tidak ada yang lain?
Ron
1
Ya, Anda melewatkan sesuatu: ConfigurationManager.GetSectionmenggunakan app.config default. Tidak peduli dengan file konfigurasi yang Anda buka OpenMappedExeConfiguration.
Daniel Hilgarth
1

Diskusi yang luar biasa, saya telah menambahkan lebih banyak komentar ke metode ResetConfigMechanism untuk memahami keajaiban di balik pernyataan / panggilan dalam metode tersebut. Juga ditambahkan path file cek

using System;//AppDomain
using System.Linq;//Where
using System.Configuration;//app.config
using System.Reflection;//BindingFlags
using System.Io;

/// <summary>
/// Use your own App.Config file instead of the default.
/// </summary>
/// <param name="NewAppConfigFullPathName"></param>
public static void ChangeAppConfig(string NewAppConfigFullPathName)
{
    if(File.Exists(NewAppConfigFullPathName)
    {
      AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", 
      NewAppConfigFullPathName);
      ResetConfigMechanism();
      return;
    }
}

/// <summary>
/// Remove cached values from ClientConfigPaths.
/// Call this after changing path to App.Config.
/// </summary>
private static void ResetConfigMechanism()
{
    BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Static;
      /* s_initState holds one of the four internal configuration state.
          0 - Not Started, 1 - Started, 2 - Usable, 3- Complete

         Setting to 0 indicates the configuration is not started, this will 
         hint the AppDomain to reaload the most recent config file set thru 
         .SetData call
         More [here][1]

      */
    typeof(ConfigurationManager)
        .GetField("s_initState", Flags)
        .SetValue(null, 0);


    /*s_configSystem holds the configuration section, this needs to be set 
        as null to enable reload*/
    typeof(ConfigurationManager)
        .GetField("s_configSystem", Flags)
        .SetValue(null, null);

      /*s_current holds the cached configuration file path, this needs to be 
         made null to fetch the latest file from the path provided 
        */
    typeof(ConfigurationManager)
        .Assembly.GetTypes()
        .Where(x => x.FullName == "System.Configuration.ClientConfigPaths")
        .First()
        .GetField("s_current", Flags)
        .SetValue(null, null);
    return;
}
Venkatesh Muniyandi
sumber
0

Daniel, jika mungkin coba gunakan mekanisme konfigurasi lainnya. Kami telah melalui rute ini di mana kami memiliki file konfigurasi statis / dinamis yang berbeda tergantung pada lingkungan / profil / grup dan itu menjadi sangat berantakan pada akhirnya.

Anda dapat mencoba semacam WebService Profil di mana Anda hanya menentukan satu URL Layanan Web dari klien dan tergantung pada detail Klien (Anda mungkin memiliki tingkat tingkat Grup / Pengguna), itu memuat semua konfigurasi yang dibutuhkan. Kami juga telah menggunakan MS Enterprise Library untuk sebagiannya.

itu adalah Anda tidak menggunakan konfigurasi dengan klien Anda dan Anda dapat mengelolanya secara terpisah dari klien Anda

Bek Raupov
sumber
3
Terima kasih atas jawaban anda. Namun, alasan keseluruhannya adalah untuk menghindari pengiriman file konfigurasi. Rincian konfigurasi untuk modul-modul dimuat dari database. Tetapi karena saya ingin memberi pengembang modul kenyamanan mekanisme konfigurasi .NET default, saya ingin menggabungkan konfigurasi modul tersebut ke dalam satu file konfigurasi saat runtime dan menjadikannya file konfigurasi default. Alasannya sederhana: Ada banyak perpustakaan yang dapat dikonfigurasi melalui app.config (mis. WCF, EntLib, EF, ...). Jika saya akan memperkenalkan mekanisme konfigurasi lain, konfigurasi akan (lanjutan)
Daniel Hilgarth