C# 中两种配置文件机制
2024-03-17 17:33:41

介绍

在C#中,有两种配置文件的机制,分别是用户配置文件和应用程序配置文件,它们分别对应产生user.configapp.config文件。
这两种机制的主要区别在于:

  • 存储位置:应用程序配置文件位于应用程序的根目录下,而用户配置文件位于用户的应用程序数据文件夹下。因此,用户配置文件是与具体用户相关的,而应用程序配置文件是与应用程序相关的。
  • 读写权限:应用程序配置文件是只读的,而用户配置文件是可写的。因此,在应用程序配置文件中存储的配置信息是全局的,只能在应用程序部署时进行修改,而在用户配置文件中存储的配置信息是与具体用户相关的,可以在运行时根据用户的偏好进行修改。
  • 使用方式:应用程序配置文件主要用于存储应用程序的全局配置信息,而用户配置文件主要用于存储用户的偏好设置。在代码中,可以使用ConfigurationManager类来读取应用程序配置文件中的配置信息,而使用Settings类来读取和写入用户配置文件中的配置信息。

user.config

用户配置文件(user.config)是用于存储应用程序的用户配置信息,例如窗口尺寸、字体样式、颜色方案等等。用户配置文件通常位于用户的应用程序数据文件夹下,并且是可写的。在Visual Studio中,可以使用“项目属性”窗口来定义这些用户配置信息,系统会自动生成一个Settings.settings文件,并在运行时自动创建和维护相应的user.config文件。在代码中,可以使用Settings类来读取和写入用户配置文件中的配置信息。

ApplicationSettingsBase 类可以用于管理应用程序的设置,通过使用 ApplicationSettingsBase 类,我们可以轻松地读取和写入应用程序级别的设置,而无需编写大量的代码来实现这些功能。
要使用 ApplicationSettingsBase,通常需要创建一个自定义设置类,该类继承自 ApplicationSettingsBase。在自定义设置类中,可以定义代表应用程序可配置设置的属性。通过使用 [DefaultSettingValue][UserScopedSetting]等属性,可以为每个设置项指定默认值和作用域。
先看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
using System.Configuration;

internal class AppSettings : ApplicationSettingsBase
{
internal bool IsActive
{
get => (bool)this[nameof(IsActive)];
set => this[nameof(IsActive)] = value;
}
}

运行后会发生异常

1
The settings property 'IsActive' was not found.

因为使用它需要几个基本条件:

  1. 属性访问权限必须是public
  2. 必须提供范围注解:[UserScopedSetting]或者[ApplicationScopedSetting]
  3. 必须提供默认值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    internal class AppSettings : ApplicationSettingsBase
    {
    [UserScopedSetting()]
    [DefaultSettingValue("true")]
    public bool IsActive
    {
    get => (bool)this[nameof(IsActive)];
    set => this[nameof(IsActive)] = value;
    }
    }

用户级设置

UserScopedSetting 注解表示这个属性是用户级的设置
所谓用户级的设置,是指属性存储在当前进程用户的相关位置,该设置不会与其他用户共享设置。

1
2
3
4
5
6
7
8
9
10
internal class AppSettings : ApplicationSettingsBase
{
[UserScopedSetting()]
[DefaultSettingValue("true")]
public bool IsActive
{
get => (bool)this[nameof(IsActive)];
set => this[nameof(IsActive)] = value;
}
}

使用方法

1
2
3
var settings = new AppSettings();
var isActive = settings.IsActive;
settings.Save();

调用 Save 方法后会持久化设置(仅会保存用户设置)。
配置文件在哪个位置呢?可以使用这段代码查看

1
2
3
4
5
6
7
public static string GetDefaultExeConfigPath(ConfigurationUserLevel userLevel)
{
var userConfig = System.Configuration.ConfigurationManager.OpenExeConfiguration(userLevel);
return userConfig.FilePath;
}

var path = GetDefaultExeConfigPath(ConfigurationUserLevel.PerUserRoamingAndLocal);

得到的路径大概是这样:

1
C:\Users\Admin\AppData\Local\MyCompany\MyApp_Url_lab5ev2xnbcazbd33m1aiehm2mtop5ce\1.0.0.0\user.config

存储到 Roaming 目录

默认情况下属性将存储在当前用户目录下,如果要存储到Roaming目录,需要使用 SettingsManageability 标注。

1
2
3
4
5
6
7
8
9
10
11
internal class AppSettings : ApplicationSettingsBase
{
[UserScopedSetting()]
[SettingsManageability(SettingsManageability.Roaming)]
[DefaultSettingValue("true")]
public bool IsActive
{
get => (bool)this[nameof(IsActive)];
set => this[nameof(IsActive)] = value;
}
}

这样配置文件路径将会在:

1
C:\Users\Admin\AppData\Roaming\...

应用级设置

与用户级设置对应的是应用级的设置,用 ApplicationScopedSetting 注解类标注。
应用级的设置是不可以持久化的,默认值就是属性注解中的值。

存储位置

user.config文件的完整路径如下所示:

1
<Profile Directory>\<Company Name>\<App Name>_<Evidence Type>_<Evidence Hash>\<Version>\user.config

其中,不同部分的含义如下:

  • <Profile Directory>:用户的应用程序数据目录,对应环境变量%LOCALAPPDATA%,如C:\Users\<UserName>\AppData\Local
  • <Company Name>:公司名称,通常是应用程序开发者的公司名或组织名。
  • <App Name>:应用程序名称,指示具体的应用程序。
  • <Evidence Type><Evidence Hash>:这些是关于应用程序证据(Evidence)的信息,用于标识不同应用程序的实例。证据信息可以包括应用程序的启动路径、域名等。这有助于区分不同实例的应用程序,以防止设置的冲突。
  • <Version>:应用程序的版本号,用于标识当前设置的版本。

升级文件

微软考虑的非常细致,在user.config路径中还有程序版本,这样当有多个版本的程序共存时,不会互相干扰。
但为了新版本的程序在首次运行时能够获取到旧的设置,需要先将设置文件升级

1
2
3
4
5
6
7
var configPath = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath;
if (!File.Exists(configPath))
{
Settings.Default.Upgrade();
Settings.Default.Reload();
Settings.Default.Save();
}

设置提供程序

ApplicationSettingsBase 自身不保留也不加载设置,而是由设置提供程序(派生自 SettingsProvider 的类)完成。
如果 ApplicationSettingsBase 的派生类未通过 SettingsProviderAttribute 指定设置提供程序,则默认使用提供程序 LocalFileSettingsProvider
一个最小的自定义提供程序的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System.Collections.Specialized;
using System.Configuration;

namespace MyApp
{
internal class CustomSettingsProvider : SettingsProvider
{
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
throw new NotImplementedException();
}

public override void Initialize(string name, NameValueCollection config)
{
if (string.IsNullOrEmpty(name))
{
name = nameof(CustomSettingsProvider);
}

base.Initialize(name, config);
}

public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
throw new NotImplementedException();
}

public override string ApplicationName { get; set; } = "myapp";
}
}

然后通过 SettingsProviderAttribute 注解标注

1
2
3
4
[SettingsProvider(typeof(CustomSettingsProvider))]
public class AppSettings : ApplicationSettingsBase
{
}

除非是自己需要完全控制配置文件的格式或文件路径,所以一般不需要自己实现。
自己实现提供程序需要考虑安全、版本等问题,比较繁琐。默认的LocalFileSettingsProvider已经足够应付绝大多数场景。

官方的实现 Settings

微软在 Visual Studio 中提供了对ApplicationSettingsBase的包装,并支持可视化操作,它就是Settings.settings文件。
创建文件:可以在 Visual Studio 中右击项目,选择 “Add”(添加) -> “New Item”(新建项),然后选择 “Settings File”(设置文件)。
VS会工程目录下生成3个文件

1
2
3
Settings.cs
Properties\Settings.settings
Properties\Settings.Designer.cs
  • 双击Settings.settings文件可以进行设置的可视化编辑。
  • Settings.Designer.cs文件是由编辑Settings.settings自动生成的,我们不应该修改它。
  • Settings.cs文件下有一个名为Settings的类,这里的代码是可以编辑的。

这个类继承自ApplicationSettingsBase,实行了单例模式,在程序直接使用即可。所以在实际开发中,我们按照微软的实践来即可,通常情况下不需要自己去实现ApplicationSettingsBase

实现自定义的类型

在VS中编辑设置的类型,只有一些如string等等的内置类型,如果要实现自己的类型如何做呢?这就需要用到 TypeConverter
在 VS2022 中,类型的下拉框并没有自定义选项,所以需要手动编辑Settings.settings文件

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="MyApp.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="UserOption1" Type="MyApp.MyOption" Scope="User">
<Value Profile="(Default)">user1</Value>
</Setting>
</Settings>
</SettingsFile>

Type属性修改为自定义类型的完全限定名。并且实现自己的TypeConverter。一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[TypeConverter(typeof(CustomSettingConverter))]
public class MyOption
{
public int Age { get; set; }
}

public class CustomSettingConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
var stringValue = value as string;
if (stringValue != null)
{
return new MyOption() { Age = 50 };
}

return base.ConvertFrom(context, culture, value);
}

public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string))
{
var opt = value as MyOption;
return opt?.Age.ToString();
}
return base.ConvertTo(context, culture, value, destinationType);
}
}

本质上,就是处理字符串与自定义类型之间的转换过程。

app.config

应用程序配置文件(app.config)是一个XML文件,用于存储应用程序的全局配置信息,例如数据库连接字符串、日志级别、应用程序设置等等。应用程序配置文件通常位于应用程序的根目录下,并且是只读的。在代码中,可以使用 ConfigurationManager 类来读取应用程序配置文件中的配置信息。

按照以下步骤来创建app.config文件:

  1. 右键单击项目名称,选择“添加”->“新建项”菜单项。
  2. 在“添加新项”对话框中,选择“应用程序配置文件”模板,并指定文件名为“app.config”。

文件内容:

1
2
3
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

你可以在其中添加配置信息。在app.config文件中添加配置信息,例如数据库连接字符串、日志级别、应用程序设置等等,并在代码中使用 ConfigurationManager 类来读取app.config文件中的配置信息。

AppSettings和ConnectionStrings属性

微软在 ConfigurationManager 上安排了两个属性:

  1. AppSettings
  2. ConnectionStrings

分别对应文件中AppSettingsSectionConnectionStringsSection两个配置节,用于存储应用程序的常规设置和数据库连接字符串信息。
AppSettingsSection 配置节用于存储应用程序的常规设置,例如日志级别、应用程序设置等等,例如:

1
2
3
4
<appSettings>
<add key="LogLevel" value="DEBUG"/>
<add key="MaxRetryCount" value="3"/>
</appSettings>

ConnectionStringsSection 配置节用于存储应用程序的数据库连接字符串信息,例如:

1
2
3
<connectionStrings>
<add name="MyDB" connectionString="server=myserver;database=mydb;uid=myuser;password=mypassword"/>
</connectionStrings>

在代码中,可以使用 ConfigurationManager 类来读取 app.config 文件中的配置信息。例如,以下代码演示了如何读取名为”LogLevel”的应用程序设置和名为”MyDB”的数据库连接字符串:

1
2
string logLevel = ConfigurationManager.AppSettings["LogLevel"];
string connectionString = ConfigurationManager.ConnectionStrings["MyDB"].ConnectionString;

自定义Section

使用自定义配置节之前,我们需要在app.config中注册该配置节(在<configSections>下定义),以便应用程序能够识别并正确地读取该配置节。

1
2
3
4
5
6
7
8
9
<configuration>
<configSections>
<section name="MySettings" type="System.Configuration.NameValueSectionHandler"/>
</configSections>
<MySettings>
<add key="MySetting1" value="Value1"/>
<add key="MySetting2" value="Value2"/>
</MySettings>
</configuration>
  • name:自定义配置节的名称。
  • type:自定义配置节的类型,C#提供了System.Configuration.SingleTagSectionHandlerSystem.Configuration.DictionarySectionHandlerSystem.Configuration.NameValueSectionHandler等等常用的节类型。

使用很简单

1
2
3
var section = (NameValueCollection)System.Configuration.ConfigurationManager.GetSection("MySettings");
var v1 = section["MySetting1"];
var v2 = section["MySetting2"];

键值对类型的选项可以用官方提供的 NameValueSectionHandler
但如果需要更复杂的类型,可以实现自己的节类,它应该继承自 ConfigurationSection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyCustomSection : ConfigurationSection
{
[ConfigurationProperty("MySetting1", IsRequired = true)]
public string MySetting1
{
get { return (string)this["MySetting1"]; }
set { this["MySetting1"] = value; }
}

[ConfigurationProperty("MySetting2", IsRequired = true)]
public int MySetting2
{
get { return (int)this["MySetting2"]; }
set { this["MySetting2"] = value; }
}
}

然后在 app.config 文件中添加一个名为”MyCustomSection”的自定义配置节:

1
2
3
4
5
6
<configuration>
<configSections>
<section name="MyCustomSection" type="MyNamespace.MyCustomSection, MyAssembly"/>
</configSections>
<MyCustomSection MySetting1="Value1" MySetting2="2"/>
</configuration>

注意type中的格式,前面是自定义节类型的完全名称,然后用逗号分隔,逗号后是程序集的名称。
这样就能得到一个自定义的类实例:

1
var mySection = ConfigurationManager.GetSection("MyCustomSection") as MyCustomSection;

如果设置节还有子类型的话也是可以做到的,更高级的用法可以自行查阅相关资料,本文就不展开了。

总结

当项目中有 app.config 文件时,对 .settings 的编辑会自动同步到 app.config 文件中。同步过去的应用范围设置会在applicationSettings节点。而通过ConfigurationManager.AppSettings['key']这种方式访问的是<appSettings>节点,且总是字符串类型。所以我有理由相信,在今天,已经不推荐用这种方式了。

在VS中,对 .settings 支持更多,可以可视化编辑、自动生成相关代码,是强类型的。
所以在实际开发中,建议通过 .settings 来管理设置。如果需要给用户编辑应用范围设置的机会,如数据库连接字符串,那么就新建一个 app.config 文件,这样 .settings 中的应用级设置会同步过来,并且复制到程序目录,使得用户编辑成为可能。否则,就可以不需要 app.config 文件。

参考

When using a Settings.settings file in .NET, where is the config actually stored?
Keep user’s settings after altering assembly/file version
Using Settings in C#
使用微软提供的Settings以及自定义SettingsProvider
在VisualBasic2005中使用My.Settings
“设置”页面,项目设计器
应用程序设置体系结构
应用程序设置特性
在Web.config或App.config中的添加自定义配置
Using Custom Classes with Application Settings