在所有环境中(开发、测试、生产)使用单一 web.config 文件
您是否遇到过这样的问题:每次将网站发布到新环境时,都需要针对每个环境自定义 web.config 中的设置?本文介绍了一种解决方案,您只需设置一次 web.config,然后让代码完成其余工作。
引言
您是否遇到过这样的问题:每次将网站发布到新环境时,都需要针对每个环境自定义 web.config 中的设置?例如,从开发服务器迁移到测试服务器时,需要更改连接字符串。或者从测试迁移到生产时,需要更改 SMTP 主机。本文介绍了一种解决方案,您只需设置一次 web.config,然后让代码来决定它所处哪个环境。
背景
我发现在部署网站到不同环境时,我通常不会复制 web.config 文件。我担心会忘记该环境的一些自定义设置。有时这会导致问题,因为 web.config 文件中已进行的更改是新的。我决定需要一个解决方案,可以为每个环境设置 web.config 文件,然后您就无需担心其余问题。您只需发布单个 web.config 文件,然后让系统处理每个环境的自定义设置。
问题
我发现在生产和测试服务器上,我从未覆盖现有的 web.config 文件。每当在开发环境中修改 web.config 文件时,我都需要手动修改这些文件,以便为测试和生产环境提供正确的值。我真的不想再这样做了。
解决方案
通过 ASP.NET 2.0,我们在处理 web.config 文件方面获得了许多新功能。因此,我在 web.config 中创建了一个自定义节,用于定义哪些服务器属于哪些环境。接下来,我创建了另一个自定义节来定义每个环境。最后,我使用了一些自定义类来访问我们的配置设置。访问此自定义类(EnvironmentSettings
)一次即可为当前环境修复配置文件。我使用了 static
(VB 中为 Shared
)构造函数,以便代码只访问一次配置文件。然后,您可以使用静态 EnvironmentSettings
类来访问您的配置设置。
一些细节
这是 web.config 文件需要的第一部分:
<configSections>
<section name="environmentConfiguration"
type="ConfigIt.KeyValueConfigurationSection, ConfigIt"/>
<section name="dev" type="ConfigIt.KeyValueConfigurationSection, ConfigIt"/>
<section name="test" type="ConfigIt.KeyValueConfigurationSection, ConfigIt"/>
<section name="prod" type="ConfigIt.KeyValueConfigurationSection, ConfigIt"/>
</configSections>
configSections
会直接放入您的 web.config 文件中。它定义了我们将要添加的自定义节。
接下来,我们有一个自定义节来定义我们的服务器:
<environmentConfiguration>
<elements>
<add key="devpcname" value="dev" />
<add key="testpcname" value="test" />
<add key="prodpcname" value="prod" />
</elements>
</environmentConfiguration>
请注意,“environmentConfiguration
”是在上面的 configSections
中定义的。您可以在此处放置您的 PC 和服务器名称。由于代码默认假设找不到此处定义的机器名称,因此您实际上不必在此处放置开发 PC/服务器名称。重要的是要有您的测试和生产服务器名称。
接下来,我们有 dev
、test
和 prod
自定义节:
<dev>
<elements>
<add key="SystemEmailAddress" value="administrator@d.com" />
<add key="SMTPServer" value="devSMTP" />
</elements>
<connectionStrings>
<add name="YourDatabase"
connectionString="Database=YourDatabase;Server=devsqlpcname;uid=DevUser;pwd=DevUser;"
providerName="System.Data.SqlClient" />
</connectionStrings>
</dev>
<test>
<elements>
<add key="SystemEmailAddress" value="administrator@t.com" />
<add key="SMTPServer" value="testSMTP" />
</elements>
<connectionStrings>
<add name="YourDatabase"
connectionString="Database=YourDatabase;Server=testsqlpcname;uid=TestUser;pwd=TestUser;"
providerName="System.Data.SqlClient" />
</connectionStrings>
</test>
<prod>
<elements>
<add key="SystemEmailAddress" value="administrator@p.com" />
<add key="SMTPServer" value="prodSMTP" />
</elements>
<connectionStrings>
<add name="YourDatabase"
connectionString="Database=YourDatabase;Server=prodsqlpcname;uid=ProdUser;pwd=ProdUser;"
providerName="System.Data.SqlClient" />
</connectionStrings>
</prod>
因此,您不必使用 appSetting
,而是可以在正确环境的 elements
部分中添加一个新的键值对。请注意,每个环境都定义了一个 connectionStrings
部分。
接下来,让我们看一下 web.config 文件中正常的 connectionStrings
部分:
<connectionStrings configSource="ConnectionStrings.config" />
那里除了 configSource
什么都没有。configSource
告诉此部分,有一个外部文件包含 connectionStrings
部分。注意:外部配置文件仍然必须有一个 .config 扩展名。Web 浏览器无法访问这些扩展名。这是 ConnectionStrings.config 文件:
<?xml version="1.0" encoding="utf-8"?>
<connectionStrings>
<clear />
<add name="YourDatabase" connectionString=""
providerName="System.Data.SqlClient" />
</connectionStrings>
请注意,connectionString
属性是空的。它将被上面定义的正确环境部分的正确值覆盖。
好了,到目前为止,您可能会问这一切都是关于什么的?为什么我们需要一个外部文件?嗯,我告诉你,我第一次尝试这个的时候,我没有使用外部文件。但是有一个问题。web.config 文件会被更新,但 WebConfigurationManager.ConnectionStrings
即使文件已更改也不会重新加载。现在我有理由怀疑,那是因为 IIS 工作进程实际上在更改配置文件,但我并不确定。因此,在使用外部配置文件时,您可以设置一个名为“RestartOnExternalChanges
”的属性。当设置为 true
时,WebConfigurationManager.ConnectionStrings
确实会得到更新。
好了,您可能仍然想知道为什么您会关心这个变量?您不能只使用 EnvironmentSettings.ConnectionStrings
吗?答案是可以,但是……如果您当前使用的是 roleManager
,在 providers
中,有一个名为 connectionStringName
的属性。它使用 WebConfigurationManager.ConnectionStrings
,而不是您的自定义节。对于 profile
和 membership
也是如此。因此,如果您使用其中任何一个,WebConfigurationManager
获得正确的连接字符串至关重要。另外,如果您使用 SqlDataSource
,您将在 HTML 中将 connectionString
属性设置为“<%$ ConnectionStrings:NorthwindConnectionString %>
”;您没有使用自定义节 ConnectionStrings
。所以情况也是一样的。
SMTP(邮件服务器)名称与此情况相同,如果您使用 ASP.NET 2.0 登录控件。所有邮件功能都假定您已使用正确的结构设置了 web.config 文件,其中包括 SMTP 邮件服务器名称(主机)。
那么它是如何工作的?我已经讨论了带有自定义节的 web.config 文件,现在让我们继续讨论代码。我创建了一个单独的类库项目,以便可以轻松地将此功能添加到任何 Web 项目中。在类库内部,有两个类。
第一个是 EnvironmentSettings
类。该类的静态构造函数会查看自定义节,决定代码运行在哪个计算机上,获取正确的自定义环境,然后将连接字符串和 SMTP 的自定义设置与 web.config 当前拥有的内容进行比较。如果它们不匹配,自定义设置将覆盖配置文件中的当前设置并保存文件。由于这是一个 static
(VB 中为 Shared
)构造函数,它应该每个应用程序域只发生一次。在下载的示例中,您会看到我添加了一个 Global.asax 文件,其中有一个 application_Start
事件。在该事件中,我访问了静态类,以便调用构造函数。
类库中的另一个类是 KeyValueConfigurationSection
。该类定义了我们添加到 web.config 文件中的自定义节。它也是我们指向 web.config 的 configSections
部分的类。这是 C# 中的静态构造函数(我也提供了 VB.NET 源代码下载):
//Constructor
static EnvironmentSettings()
{
KeyValueConfigurationSection section = null;
// Get the current configuration file.
Configuration config =
System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~",null,null,
System.Environment.MachineName,
System.Environment.MachineName + @"\WebConfigUser","WebConfigPassword");
//Next Get which environment we are in based
//off what the machine name this code is running on
section = config.GetSection("environmentConfiguration")
as KeyValueConfigurationSection;
string machineName = System.Environment.MachineName.ToLower();
if (section.Elements[machineName] != null &&
section.Elements[machineName].Value != null &&
section.Elements[machineName].Value != string.Empty)
{
_environment = section.Elements[machineName].Value;
}
else
{ //Default to dev
_environment = "dev";
}
_elements = ((KeyValueConfigurationSection)config.GetSection(_environment)).Elements;
_connectionStrings =
((KeyValueConfigurationSection)config.GetSection(_environment)).ConnectionStrings;
//NOTE we only want to save the config file if something changed...
Boolean somethingChanged = false;
//Update the email stuff if environment specific value is different
_systemNet = (System.Net.Configuration.NetSectionGroup)
config.GetSectionGroup("system.net");
if (_systemNet.MailSettings.Smtp.Network.Host != null &&
_elements["SMTPServer"] != null &&
_systemNet.MailSettings.Smtp.Network.Host !=
_elements["SMTPServer"].Value)
{
_systemNet.MailSettings.Smtp.Network.Host = _elements["SMTPServer"].Value;
somethingChanged = true;
}
//Update the connection strings if the environment specific value is different
foreach (ConnectionStringSettings css in _connectionStrings)
{
Boolean foundIt = false;
foreach (ConnectionStringSettings css2 in config.ConnectionStrings.ConnectionStrings)
{
if (css.Name.Trim() == css2.Name.Trim())
{
foundIt = true;
if (css2.ConnectionString != css.ConnectionString)
{
css2.ConnectionString = css.ConnectionString;
config.ConnectionStrings.SectionInformation.RestartOnExternalChanges = true;
somethingChanged = true;
}
}
} //foreach css2
if (!foundIt)
{
config.ConnectionStrings.ConnectionStrings.Add(css);
config.ConnectionStrings.SectionInformation.RestartOnExternalChanges = true;
somethingChanged = true;
}
} //foreach css
if (somethingChanged)
{
config.Save(ConfigurationSaveMode.Modified);
}
}
最后一步是网站如何更新 web.config 文件。答案是您需要在运行此网站的计算机上创建一个本地用户。然后授予该用户读写权限,以便它可以更新 web.config 文件。注意:您可以只授予 ASPNET 用户权限,但我**不**建议这样做,因为它有点不安全。接下来,您需要将 IIS_WPG 工作进程组添加到该文件夹的读写权限。
以下是我 C# 示例 page_load
事件中访问配置属性的一些示例:
string databaseName = "YourDatabase";
//Getting config items from the EnvironmentSettings static var...
lbEnvior.Text = "You are running in the " +
EnvironmentSettings.Environment + " Environment";
lbEnvirSQL.Text = EnvironmentSettings.ConnectionStrings[databaseName].ConnectionString;
lbEnvirSMTP.Text = EnvironmentSettings.SystemNet.MailSettings.Smtp.Network.Host;
//Getting config items from WebConfigurationManager
lbWebConfigManSQL.Text =
System.Web.Configuration.WebConfigurationManager.
ConnectionStrings[databaseName].ConnectionString;
//Getting config items from the config file directly
System.Configuration.Configuration config =
System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~");
lbWebConfigDirectSQL.Text =
config.ConnectionStrings.ConnectionStrings[databaseName].ConnectionString;
System.Net.Configuration.NetSectionGroup _systemNet =
(System.Net.Configuration.NetSectionGroup)config.GetSectionGroup("system.net");
lbWebConfigDirectSMTP.Text = _systemNet.MailSettings.Smtp.Network.Host;
//Getting Config items from ConfigurationManager NOTE this is not suggested in a Web site.
lbConfigManSQL.Text = ConfigurationManager.ConnectionStrings[databaseName].ConnectionString;
步骤
注意:您需要管理员权限才能执行其中一些步骤。
- 在要运行网站的计算机上创建一个本地用户(我选择了 WebConfigUser)。右键单击“我的电脑”。单击“管理”。单击“本地用户和组”上的加号。右键单击“用户”文件夹。单击“新建用户”。输入用户名、全名和密码。然后,取消选中“用户下次登录时必须更改密码”,并选中“密码永不过期”。单击“创建”。
- 转到 web.config 所在的目录。右键单击文件夹,选择“属性”。选择“安全”选项卡。单击“添加”按钮。您可能需要单击“位置”按钮,以便您的位置是您的计算机,而不是域。单击“确定”。选中“修改”复选框。接下来,添加 IIS_WPG 组。确保选中“修改”复选框。单击“确定”。
- 在
EnvironmentSettings
类的构造函数中,有一个对OpenWebConfiguration
的调用。在此调用内部,您需要输入刚才创建的本地用户的用户名和密码。 - 在 web.config 中,添加一个
configSections
。您可以直接从下载的示例文件中复制。 - 在 web.config 中,添加
environmentConfiguration
自定义节。这是您列出服务器的地方。 - 在 web.config 中,添加
dev
、test
、prod
或您想如何称呼您的环境的自定义节。确保每个环境节都有一个ConnectionStrings
节。 - 在 Web.config 中,将现有的
ConnectionStrings
节指向一个外部文件。用正确的代码创建外部文件。SMTP 节也一样。下载的示例代码在这里应该有效。您只需要更改连接名称和连接字符串以匹配您的系统。 - 将类库项目(ConfigItC# 或 ConfigItVB)添加到您的 Web 项目解决方案中。在 Web 项目中添加对它的引用。
- 添加一个 global.asax 文件并添加一个
application_start
方法。您可以使用下载的示例文件。
精简版
如果您真的不认为自己会使用 WebConfigurationManager.ConnectionStrings
或 SMTP 相关的东西,您可以随时修改 EnvironmentSettings
类。删除 OpenWebconfiguration
以及所有比较和保存配置文件相关的代码。
结论
我希望您觉得这个跨环境管理 web.config 文件的方法有帮助。弄清楚这一点有点麻烦,但现在它终于奏效了,它确实为我省去了很多麻烦。我想感谢 Steve Rowe 在设置自定义配置设置的一些细节上提供的帮助。