เปลี่ยน app.config เริ่มต้นที่รันไทม์


130

ฉันมีปัญหาต่อไปนี้:
เรามีแอปพลิเคชันที่โหลดโมดูล (ส่วนเสริม) โมดูลเหล่านี้อาจต้องการรายการใน app.config (เช่นการกำหนดค่า WCF) เนื่องจากโมดูลถูกโหลดแบบไดนามิกฉันไม่ต้องการให้รายการเหล่านี้อยู่ในไฟล์ app.config ของแอปพลิเคชันของฉัน
สิ่งที่ฉันต้องการจะทำมีดังต่อไปนี้:

  • สร้าง app.config ใหม่ในหน่วยความจำที่รวมส่วนการกำหนดค่าจากโมดูล
  • บอกแอปพลิเคชันของฉันให้ใช้ app.config ใหม่นั้น

หมายเหตุ: ฉันไม่ต้องการเขียนทับ app.config เริ่มต้น!

ควรทำงานอย่างโปร่งใสตัวอย่างเช่นConfigurationManager.AppSettingsใช้ไฟล์ใหม่นั้น

ในระหว่างการประเมินของฉันของปัญหานี้ผมมาด้วยวิธีเดียวกับที่ให้ไว้ที่นี่: โหลด app.config กับ nunit
น่าเสียดายที่ดูเหมือนจะไม่ทำอะไรเลยเพราะฉันยังได้รับข้อมูลจาก app.config ปกติ

ฉันใช้รหัสนี้เพื่อทดสอบ:

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);
}

พิมพ์ค่าเดียวกันสองครั้งแม้ว่าจะcombinedConfigมีค่าอื่นนอกเหนือจาก app.config ปกติ


การโฮสต์โมดูลแยกต่างหากAppDomainกับไฟล์คอนฟิกูเรชันที่เหมาะสมไม่ใช่ตัวเลือก?
João Angelo

ไม่จริงเพราะนั่นจะส่งผลให้มีการโทรข้าม AppDomain จำนวนมากเนื่องจากแอปพลิเคชันโต้ตอบกับโมดูลค่อนข้างมาก
Daniel Hilgarth

วิธีการรีสตาร์ทแอปพลิเคชันเมื่อต้องโหลดโมดูลใหม่
João Angelo

สิ่งนี้ใช้ไม่ได้กับข้อกำหนดทางธุรกิจ นอกจากนี้ฉันไม่สามารถเขียนทับ app.config ได้เนื่องจากผู้ใช้ไม่มีสิทธิ์ทำเช่นนั้น
Daniel Hilgarth

คุณจะต้องโหลดซ้ำเพื่อโหลด App.config อื่นไม่ใช่ในไฟล์โปรแกรม การแฮ็กReload app.config with nunitอาจใช้งานได้ไม่แน่ใจว่าหากใช้กับรายการแอปพลิเคชันก่อนที่จะโหลดการกำหนดค่าใด ๆ
João Angelo

คำตอบ:


280

การแฮ็กในคำถามที่เชื่อมโยงจะทำงานได้หากมีการใช้งานก่อนที่ระบบกำหนดค่าจะถูกใช้ในครั้งแรก หลังจากนั้นก็ใช้งานไม่ได้อีกเลย
เหตุผล:
มีคลาสClientConfigPathsที่แคชเส้นทาง ดังนั้นแม้ว่าจะเปลี่ยนเส้นทางด้วยSetDataแล้วก็จะไม่อ่านซ้ำเนื่องจากมีค่าแคชอยู่แล้ว วิธีแก้ปัญหาคือการลบสิ่งเหล่านี้ด้วย:

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);
        }
    }
}

การใช้งานเป็นดังนี้:

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

หากคุณต้องการเปลี่ยน app.config ที่ใช้สำหรับรันไทม์ทั้งหมดของแอปพลิเคชันของคุณเพียงแค่ใส่AppConfig.Change(tempFileName)โดยไม่ต้องใช้ที่ใดที่หนึ่งที่จุดเริ่มต้นของแอปพลิเคชันของคุณ


4
นี่มันยอดเยี่ยมจริงๆ ขอบคุณมากสำหรับการโพสต์สิ่งนี้
user981225

3
@ Daniel นั่นยอดเยี่ยมมาก - ฉันใช้มันเป็นวิธี exentension สำหรับ ApplicationSettingsBase เพื่อที่ฉันจะได้เรียก Settings.Default.RedirectAppConfig (path) ฉันจะให้คุณ +2 ถ้าทำได้!
JMarsch

2
@PhilWhittington: นั่นคือสิ่งที่ฉันพูดใช่
Daniel Hilgarth

2
ไม่มีผลประโยชน์มีเหตุผลใดที่จะระงับโปรแกรม Finalizer หรือไม่มีการประกาศ Finalizer?
Gusdor

3
นอกจากนี้การใช้การสะท้อนกลับเพื่อเข้าถึงฟิลด์ส่วนตัวอาจใช้งานได้ในขณะนี้ แต่อาจใช้คำเตือนว่าไม่ได้รับการสนับสนุนและอาจทำลายในอนาคตของ. NET Framework

10

คุณสามารถลองใช้Configurationและ Add ConfigurationSectionบนรันไทม์

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

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

แก้ไข:นี่คือวิธีแก้ปัญหาโดยอาศัยการสะท้อน (ไม่ค่อยดีนัก)

สร้างคลาสที่ได้มาจาก 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
}

จากนั้นผ่านการสะท้อนให้ตั้งค่าเป็นฟิลด์ส่วนตัวใน 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

ฉันไม่เห็นว่าสิ่งนี้ช่วยฉันได้อย่างไร file_pathนี้จะเพิ่มส่วนไปยังแฟ้มที่ระบุโดย สิ่งนี้จะไม่ทำให้ผู้ใช้ส่วนนี้สามารถใช้ConfigurationManager.GetSectionงานได้เนื่องจากGetSectionใช้ app.config เริ่มต้น
Daniel Hilgarth

คุณสามารถเพิ่มส่วนใน app.config ที่มีอยู่ได้ เพิ่งลองสิ่งนี้ - ได้ผลสำหรับฉัน
Stecya

อ้างจากคำถามของฉัน: "หมายเหตุ: ฉันไม่ต้องการเขียนทับ app.config เริ่มต้น!"
Daniel Hilgarth

5
มีอะไรผิดปกติ ง่าย: ผู้ใช้ไม่มีสิทธิ์เขียนทับเนื่องจากโปรแกรมติดตั้งใน% ProgramFiles% และผู้ใช้ไม่ใช่ผู้ดูแลระบบ
Daniel Hilgarth

2
@Stecya: ขอบคุณสำหรับความพยายามของคุณ แต่โปรดดูคำตอบของฉันสำหรับวิธีแก้ปัญหาที่แท้จริง
Daniel Hilgarth

5

โซลูชัน @ Daniel ทำงานได้ดี วิธีแก้ปัญหาที่คล้ายกันพร้อมคำอธิบายเพิ่มเติมอยู่ในมุม c-sharp เพื่อความสมบูรณ์ฉันต้องการแชร์เวอร์ชันของฉัน: ด้วยusingและแฟล็กบิตย่อ

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;
    }

4

หากใครสนใจนี่คือวิธีที่ใช้ได้กับ 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);

3

ดูเหมือนว่าโซลูชันของ Daniel จะใช้งานได้แม้กับแอสเซมบลีต่อท้ายที่ฉันเคยใช้ AppDomain.SetData มาก่อน แต่ไม่ทราบวิธีรีเซ็ตแฟล็กการกำหนดค่าภายใน

แปลงเป็น C ++ / CLI สำหรับผู้ที่สนใจ

/// <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;
}

1

หากไฟล์กำหนดค่าของคุณเขียนด้วยคีย์ / ค่าใน "appSettings" คุณสามารถอ่านไฟล์อื่นด้วยรหัสดังกล่าว:

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

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

จากนั้นคุณสามารถอ่านส่วนการตั้งค่าเป็นชุดของ KeyValueConfigurationElement


1
ดังที่ได้กล่าวไปแล้วว่าฉันต้องการให้ConfigurationManager.GetSectionอ่านไฟล์ใหม่ที่ฉันสร้างขึ้น โซลูชันของคุณไม่ได้ทำเช่นนั้น
Daniel Hilgarth

@ แดเนียล: ทำไม? คุณสามารถระบุไฟล์ใดก็ได้ใน "configFilePath" ดังนั้นคุณต้องรู้ตำแหน่งของไฟล์ที่สร้างขึ้นใหม่ ฉันพลาดอะไรไปหรือเปล่า? หรือคุณจำเป็นต้องใช้ "ConfigurationManager.GetSection" จริงๆและไม่มีอะไรอื่น?
รอน

1
ใช่คุณพลาดบางอย่าง: ConfigurationManager.GetSectionใช้ app.config เริ่มต้น มันไม่ได้เกี่ยวกับการดูแลไฟล์ config OpenMappedExeConfigurationที่คุณเปิดด้วย
Daniel Hilgarth

1

การสนทนาที่ยอดเยี่ยมฉันได้เพิ่มความคิดเห็นเพิ่มเติมเกี่ยวกับวิธี ResetConfigMechanism เพื่อทำความเข้าใจเกี่ยวกับเวทมนตร์ที่อยู่เบื้องหลังคำสั่ง / การโทรในวิธีการ มีการตรวจสอบเส้นทางไฟล์ที่เพิ่มเข้ามาด้วย

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;
}

0

Daniel ถ้าเป็นไปได้ให้ลองใช้กลไกการกำหนดค่าอื่น ๆ เราเคยผ่านเส้นทางนี้ซึ่งเรามีไฟล์กำหนดค่าคงที่ / ไดนามิกที่แตกต่างกันขึ้นอยู่กับสภาพแวดล้อม / โปรไฟล์ / กลุ่มและมันก็ค่อนข้างยุ่งในตอนท้าย

คุณสามารถลองใช้ Profile WebService บางประเภทที่คุณระบุ URL บริการเว็บเพียงรายการเดียวจากไคลเอนต์และขึ้นอยู่กับรายละเอียดของไคลเอ็นต์ (คุณอาจมีการแทนที่ระดับกลุ่ม / ผู้ใช้) ซึ่งจะโหลดการกำหนดค่าทั้งหมดที่ต้องการ เราได้ใช้ MS Enterprise Library ในบางส่วนด้วย

นั่นคือคุณไม่ได้ปรับใช้การกำหนดค่ากับไคลเอนต์ของคุณและคุณสามารถจัดการแยกต่างหากจากไคลเอนต์ของคุณ


3
ขอบคุณสำหรับคำตอบ. อย่างไรก็ตามเหตุผลทั้งหมดคือเพื่อหลีกเลี่ยงการจัดส่งไฟล์กำหนดค่า รายละเอียดการกำหนดค่าสำหรับโมดูลถูกโหลดจากฐานข้อมูล แต่เนื่องจากฉันต้องการให้นักพัฒนาโมดูลได้รับความสะดวกสบายจากกลไกการกำหนดค่า. NET เริ่มต้นฉันต้องการรวมการกำหนดค่าโมดูลเหล่านั้นไว้ในไฟล์กำหนดค่าเดียวที่รันไทม์และทำให้เป็นไฟล์กำหนดค่าเริ่มต้น เหตุผลนั้นง่ายมาก: มีไลบรารีจำนวนมากที่สามารถกำหนดค่าผ่าน app.config (เช่น WCF, EntLib, EF, ... ) ถ้าฉันจะแนะนำกลไกการกำหนดค่าอื่นการกำหนดค่าจะ (ต่อ)
Daniel Hilgarth
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.