利用 Visual Studio 解决方案配置的简单用例






4.25/5 (4投票s)
自动切换客户端解决方案的 Test 或 Prod app.config 文件,用于将环境设置注入类库解决方案
引言
最近我需要修改一个包含 Windows 服务的解决方案。该服务使用了来自一个完全独立解决方案中的类库项目创建的 DLL。我想根据客户端应用程序(在这种情况下是服务,但在本文中是控制台应用程序)的解决方案配置,自动为类库提供环境配置,因为类库需要与数据库和 Web 服务的不同实例通信,具体取决于环境。本文将介绍一个具有以下要点的用例:
- 我们将有一个类库,它将与数据库和 Web 服务进行通信。为了让客户端定义数据库连接和 Web 服务 URL,我们将创建一个接口,供类库的客户端可选地实现,以提供上述数据库连接和 Web 服务 URL。
- 我们将使用类库的项目设置,这会在类库中创建一个 app.config 文件。这将是客户端解决方案在其 app.config 文件中需要模仿的结构。
- 客户端将定义两个 app.config 文件,一个用于 Release,一个用于 Debug。我们还将编辑 .csproj 文件,以便根据解决方案配置(客户端是在 Debug 还是 Release 中运行)自动使用正确的 app.config 文件。
- 类库将包含上述第 1 点中提到的接口的默认实现。如果客户端未创建实现该接口的类,则类库中的接口默认实现将获取数据库/Web 服务设置,只要客户端在其 app.config 文件中遵循正确的格式即可。
本文的代码可通过附加的 zip 文件获得,或者在 GitHub 上此处获取。
非常感谢 Juy Juka 在撰写本文过程中提供的指导。
背景
提供 API 的类库在测试和生产环境以及与 Web 服务和数据库通信。起初,更新客户端解决方案需要更改类库解决方案中的数据库连接字符串和 Web 服务 URL 进行测试,编译类库 DLL,对客户端解决方案进行代码更改,然后编译/运行客户端解决方案以使用新的类库 DLL。测试完成后,我必须将数据库连接字符串和 Web 服务 URL 切换回生产环境,并基本上重复相同的编译过程,然后才能将更改提交到存储库。如果我在更改为测试环境(数据库/Web 服务设置)后忘记编译类库解决方案,然后运行客户端解决方案,那么可能会发生糟糕的情况——当你认为自己处于测试模式但实际上不是时,情况通常不佳!
通过遵循引言中定义的步骤,可以基本消除上一段所述的工作量。在本文的其余部分,我们将同时构建一个类库解决方案和一个客户端解决方案。那么,我们开始吧!
Using the Code
类库解决方案
首先,我们将创建类库。在 Visual Studio 中,转到 **文件** > **新建** > **项目…**,在 Visual C# 模板中,创建一个新的类库项目。将项目和解决方案都命名为“SolutionConfigurationsClassLibrary
”。
我们最终将创建如下图所示的类(来自解决方案资源管理器截图)。FancyCalculator
从 DatabaseConnection
获取一个标量乘数,将其乘以此类库客户端提供的值,然后将结果以及 WebServiceClient
对象(我们假装将其转发给实际的 Web 服务)报告给客户端解决方案。IExternalDataAccessSetting
接口描述了将封装 DatabaseConnection
和 WebServiceClient
对象环境配置的类的预期功能。我们还创建了一个 DefaultExternalDataAccessSettings
类,以便客户端可以仅提供具有适当结构的 app.config 文件,而无需提供 IExternalDataAccessSetting
的实现。
在我们开始编码之前,让我们添加稍后在构建类库时需要用到的 System.Configuration
引用。右键单击 SolutionConfigurationsClassLibrary
项目下的“引用”节点。选择 **添加引用**。在出现的窗口左侧,转到 **程序集** > **框架**,在列表中找到 System.Configuration
,单击旁边的复选框,然后单击 **确定**。
接下来,我们需要在类库中创建项目设置(这将设置 app.config 文件)。我们在这些设置中输入的值(NOT_DEFINED
)实际上不会用于与任何东西通信,但它们将允许我们在类库中创建 DefaultExternalDataAccessSettings
类的代码。右键单击 SolutionConfigurationsClassLibrary
项目,然后单击 **属性**。在此屏幕上,单击 **设置** 选项卡,然后单击显示“此项目不包含默认设置文件。单击此处创建。”的文本——这将正是我们想要的。在出现的网格中,输入以下属性名称、类型、范围和值。
根据此 MSDN 文章,应用程序和用户作用域设置的区别在于,前者基本上是只读的,而后者是读写的。(使用 IntelliSense,您会注意到用户范围的设置有“get; set;
”,而应用程序范围的设置只有“get;
”)。请注意,通过为 PrimaryDatabase
选择“(连接字符串)”,它只允许应用程序范围。我们还希望 PrimaryWebService
是只读的,因此将其范围设置为应用程序。有关更多信息,包括加密 app.config 文件中的连接字符串,请参阅此处。
单击生成的 SolutionConfigurationsClassLibrary
的 app.config 文件。您将看到下面列出的代码。请注意,PrimaryDatabase
的 connectionString
和 PrimaryWebService
的值均为 NOT_DEFINED
。将此代码复制到一个空的文本编辑器(或稍后返回此文件),因为我们将在创建客户端解决方案时使用此 XML。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="applicationSettings"
type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SolutionConfigurationsClassLibrary.Properties.Settings"
type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<connectionStrings>
<add name="SolutionConfigurationsClassLibrary.Properties.Settings.PrimaryDatabase"
connectionString="NOT_DEFINED" />
</connectionStrings>
<applicationSettings>
<SolutionConfigurationsClassLibrary.Properties.Settings>
<setting name="PrimaryWebService" serializeAs="String">
<value>NOT_DEFINED</value>
</setting>
</SolutionConfigurationsClassLibrary.Properties.Settings>
</applicationSettings>
</configuration>
现在到代码。首先是 IExternalDataAccessSetting
接口。实现此接口的类必须能够为我们提供数据库名称和 Web 服务 URL。这两个要求/属性可以分别放在不同的接口中,但为了演示起见,我们将其放在一起。
public interface IExternalDataAccessSettings
{
string DatabaseName
{ get; }
string WebServiceUrl
{ get; }
}
接下来,我们将编写 DefaultExternalDataAccessSettings
类。在项目设置窗格中,我们将连接字符串和 Web 服务 URL 设置为 NOT_DEFINED
。客户端可以选择提供自己的 IExternalDataAccessSettings
实现,但如果它不提供,那么它仍然必须提供一个具有 PrimaryDatabase
和 PrimaryWebService
设置的 app.config 文件。如果客户端未在 app.config 文件中提供这些设置,那么此 DefaultExternalDataAccessSettings
实现将抛出异常。
public class DefaultExternalDataAccessSettings : IExternalDataAccessSettings
{
public virtual string DatabaseName
{
get
{
string databaseName = null;
var connectionName =
"SolutionConfigurationsClassLibrary.Properties.Settings.PrimaryDatabase";
if (ConfigurationManager.ConnectionStrings[connectionName] != null)
databaseName =
ConfigurationManager.ConnectionStrings[connectionName].ConnectionString;
else
throw new Exception("Client solution must define connection for " +
connectionName);
return databaseName;
}
}
public virtual string WebServiceUrl
{
get
{
var webserviceUrl = Properties.Settings.Default.PrimaryWebService;
if (webserviceUrl == null || (webserviceUrl == "NOT_DEFINED"))
throw new Exception("Client solution must define web service url");
return webserviceUrl;
}
}
}
这就是我们 DatabaseConnection
的代码(见下文)。IExternalDataAccessSettings
接口的实现者在第一个构造函数中提供,该实现者提供我们需要连接到数据库的数据库名称。如果客户端未提供 IExternalDataAccessSettings
的实现,则将调用第二个(无参数)构造函数,该构造函数使用我们上面创建的 DefaultExternalDataAccessSettings
。在实际应用程序中,GetValueToUseForCalculation
方法不会有根据数据库名称返回不同值的条件,但我们正在模拟在测试和生产环境中可能返回不同值。
public class DatabaseConnection
{
protected string DatabaseName;
public DatabaseConnection(IExternalDataAccessSettings settings)
{
this.DatabaseName = settings.DatabaseName;
}
public DatabaseConnection()
: this(new DefaultExternalDataAccessSettings())
{ }
public void Connect()
{
Console.WriteLine("Just connected to " + this.DatabaseName + " database!");
}
public int GetValueToUseForCalculation()
{
int value = 0;
if (this.DatabaseName == "TEST")
value = 5;
else if (this.DatabaseName == "PROD")
value = 10;
return value;
}
}
下面的代码显示了我们的 WebServiceClient
。此类库示例中此类库的目的是将消息(计算结果)报告给 Web 服务。在此示例中,我们实际上并未连接到 Web 服务——在这里,我们仅报告已发送模拟消息以及发送到的 URL。实际 URL 由 IExternalDataAccessSettings
的实现者提供,该实现者作为构造函数参数传入。与 DatabaseConnection
一样,我们也有一个无参数构造函数,客户端可以选择不提供自己的 IExternalDataAccessSettings
实现。在这种情况下,将再次使用 DefaultExternalDataAccessSettings
。
public class WebServiceClient
{
protected string WebServiceUrl;
public WebServiceClient(IExternalDataAccessSettings settings)
{
this.WebServiceUrl = settings.WebServiceUrl;
}
public WebServiceClient()
: this(new DefaultExternalDataAccessSettings())
{ }
public void SendMessage(string messageToSend)
{
if (string.IsNullOrEmpty(messageToSend) == true)
throw new Exception("Can't send an empty message. Sorry 'bout that!");
Console.WriteLine("Following message sent to " +
this.WebServiceUrl + " web service: " + messageToSend);
}
}
最后,将以下代码添加到实现 FancyCalculator
本身。此类接受两个依赖项,数据库连接和 Web 服务客户端,作为构造函数参数。然后,DoFancyStuffWithANumber
方法旨在由客户端调用并传入一个数字。FancyCalculator
从数据库连接中获取一个不同的数字,进行简单的计算(仅为演示目的),并将计算结果报告给 Web 服务。
public class FancyCalculator
{
protected DatabaseConnection DbConn;
protected WebServiceClient WsClient;
public FancyCalculator(DatabaseConnection dbConn, WebServiceClient wsClient)
{
this.DbConn = dbConn;
this.WsClient = wsClient;
}
public int DoFancyStuffWithANumber(int aNumber)
{
var multiplier = this.GetMultiplierValueFromDatabase();
var result = this.MakeCalculation(multiplier);
this.ReportResultsToWebService(result);
return result;
}
protected int GetMultiplierValueFromDatabase()
{
this.DbConn.Connect();
return this.DbConn.GetValueToUseForCalculation();
}
protected int MakeCalculation(int multiplierValue)
{
int result = multiplierValue * 10;
return result;
}
protected void ReportResultsToWebService(int result)
{
this.WsClient.SendMessage("Result of FancyCalculator: " + result.ToString());
}
}
客户端解决方案
现在我们开始构建将使用类库解决方案的应用程序。我们想为客户端创建一个全新的解决方案。因此,在 Visual Studio 中,转到 **文件** > **新建** > **项目…**,在 Visual C# 模板中,创建一个新的控制台应用程序项目。将项目和解决方案都命名为“SolutionConfigurationsClassLibraryClient
”。在我们的例子中,为了简单起见,类库解决方案(SolutionConfigurationsClassLibrary
)和客户端解决方案(SolutionConfigurationsClassLibraryClient
)将在同一个 Projects 目录中,如下图所示。
接下来,我们想从 SolutionConfigurationsClassLibraryClient
解决方案添加对 SolutionConfigurationsClassLibrary
类库解决方案的 **引用**。在 SolutionConfigurationsClassLibraryClient
解决方案的解决方案资源管理器中,右键单击 SolutionConfigurationsClassLibraryClient
项目在解决方案资源管理器中的 **引用** 节点。在出现的对话框中,我们想单击左侧的 **浏览** 选项卡,然后在屏幕上单击“**浏览…**”按钮。将出现一个资源管理器窗口,我们要找到 SolutionConfigurationsClassLibrary.dll 文件,选择它,然后单击 **添加**。这应该位于 \Projects\SolutionConfigurationsClassLibrary\SolutionConfigurationsClassLibrary\bin\Release,其中“Projects
”是您的 Visual Studio 项目目录的位置。
我们需要一种方法将数据库名称和 Web 服务 URL 设置注入到 SolutionConfigurationsClassLibrary
代码中,以便从 SolutionConfigurationsClassLibraryClient
的代码中使用 FancyCalculator
。为此,在解决方案资源管理器中创建一个名为“Config”的新目录。然后,找到 App.config 文件,将其复制到新的 Config 文件夹,并将该文件重命名为 App.debug.config。将您从 SolutionConfigurationsClassLibrary
的 app.config 文件复制的 XML 粘贴到 App.debug.config 文件中。然后将 PrimaryDatabase
的 connectionString
和 PrimaryWebService
的值更新为 TEST
。App.debug.config 文件应如下所示:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="applicationSettings"
type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SolutionConfigurationsClassLibrary.Properties.Settings"
type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<connectionStrings>
<add name="SolutionConfigurationsClassLibrary.Properties.Settings.PrimaryDatabase"
connectionString="TEST" providerName="" />
</connectionStrings>
<applicationSettings>
<SolutionConfigurationsClassLibrary.Properties.Settings>
<setting name="PrimaryWebService" serializeAs="String">
<value>TEST</value>
</setting>
</SolutionConfigurationsClassLibrary.Properties.Settings>
</applicationSettings>
</configuration>
现在,再次将 App.config 文件复制到 Config 目录,但这次将其命名为 App.release.config
。编辑这个新文件并执行相同的操作(粘贴您从 SolutionConfigurationsClassLibrary
的 app.config 文件复制的 XML),但这次将 PrimaryDatabase
的 connectionString
和 PrimaryWebService
的值设置为 PROD
。您的 App.release.config 文件应如下所示:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="applicationSettings"
type="System.Configuration.ApplicationSettingsGroup, System,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SolutionConfigurationsClassLibrary.Properties.Settings"
type="System.Configuration.ClientSettingsSection, System,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
requirePermission="false" />
</sectionGroup>
</configSections>
<connectionStrings>
<add name="SolutionConfigurationsClassLibrary.Properties.Settings.PrimaryDatabase"
connectionString="PROD" providerName="" />
</connectionStrings>
<applicationSettings>
<SolutionConfigurationsClassLibrary.Properties.Settings>
<setting name="PrimaryWebService" serializeAs="String">
<value>PROD</value>
</setting>
</SolutionConfigurationsClassLibrary.Properties.Settings>
</applicationSettings>
</configuration>
现在您的 Config 文件夹中应该有两个文件:App.debug.config 和 App.release.config。正如您可能已经猜到的,这两个文件包含我们测试和生产环境的数据库和 Web 服务设置。创建新的 Config 目录和两个 config 文件后,解决方案资源管理器应如下所示:
现在我们需要设置客户端项目以自动选择正确的 App.config 文件(我们刚刚在 Config 目录中创建的文件之一)。在解决方案资源管理器中,右键单击 SolutionConfigurationsClassLibraryClient
控制台应用程序项目,然后选择“**卸载项目**”。再次右键单击项目并选择“编辑 SolutionConfigurationsClassLibraryClient.csproj”。这是一个包含控制台应用程序项目一些配置的 XML 文件。滚动到底部,您会看到一个注释掉的 XML 部分,其中有一个“Target
”节点,代码为“<Target Name="AfterBuild"></Target>
”。调整注释,使其取消注释,并进行修改,使其包含您下面看到的其他 Delete/Copy 子节点:
<Target Name="AfterBuild">
<Delete Files="$(TargetDir)$(TargetFileName).config" />
<Copy SourceFiles="$(ProjectDir)\Config\App.$(Configuration).config"
DestinationFiles="$(TargetDir)$(TargetFileName).config" />
</Target>
在构建目录中,上述 XML 会删除现有的 SolutionConfigurationsClassLibraryClient.config 文件,并用 App.debug.config 或 App.release.config 文件(取决于我们的客户端解决方案配置)替换它。“$(Configuration)
”本质上是一个变量,它动态地包含当您运行 SolutionConfigurationsClassLibraryClient
解决方案的生成过程时,Visual Studio 中选择的 Configuration
值的。完成后,保存 .csproj 文件,再次右键单击项目,然后单击“**重新加载项目**”。完成此步骤后,正确的数据库名称和 Web 服务 URL 将提供给类库——当我们处于 Debug 配置时,将提供 TEST 设置;当我们处于 Release 配置时,将提供 PROD 设置……太棒了!
在重新加载的项目中,编辑客户端控制台应用程序的 Program.cs 文件,并输入下面显示的代码。这通过使用 ExternalDataAccessSettings
通过构造函数依赖注入来实例化 DatabaseConnection
和 WebServiceClient
对象,然后使用新创建的 DatabaseConnection
和 WebServiceClient
对象为 FancyCalculator
做同样的事情。然后运行计算器。您还必须在此文件的顶部添加一个“using SolutionConfigurationsClassLibrary;
”语句。
class Program
{
static void Main(string[] args)
{
DatabaseConnection conn = new DatabaseConnection();
WebServiceClient ws = new WebServiceClient();
FancyCalculator calc = new FancyCalculator(conn, ws);
int result = calc.DoFancyStuffWithANumber(5);
Console.ReadLine();
}
}
现在,让我们将 SolutionConfigurationsClassLibraryClient
控制台应用程序的 Configuration
设置为 Debug
,然后运行它。因为 Config\App.debug.config 文件已被复制到 build 目录(并且因为 SolutionConfigurationsClassLibrary
的 DefaultExternalDataAccessSettings
类设置为 app.config 设置),所以使用了 TEST 数据库和 Web 服务设置。
最后,我们将 SolutionConfigurationsClassLibraryClient
控制台应用程序的 Configuration
设置为 Release
,然后再次运行它。这次,因为 Config\App.release.config 文件已被复制到构建目录(并且仍然因为 SolutionConfigurationsClassLibrary
的 DefaultExternalDataAccessSettings
类),所以使用了 PROD 数据库和 Web 服务设置。
当您在 Release 模式下运行 SolutionConfigurationsClassLibraryClient
控制台应用程序时,您可能会收到一条消息,提示“您正在调试 SolutionConfigurationsClassLibraryClient.exe 的 Release 版本…
”。您可以通过右键单击解决方案资源管理器中创建的 SolutionConfigurationsClassLibraryClient
控制台应用程序项目,然后转到 **属性** 来解决此问题。然后单击左侧的 **生成** 选项卡,并取消选中“**优化代码**”复选框,如下图所示。此时,您应该能够在 Release 模式下运行控制台应用程序,并在图中查看结果。
非 App.config 灵活性
虽然 app.config 文件是处理应用程序设置的最佳方法,但我们在这些解决方案中设置的结构使我们能够以任何我们想要的方式提供 PrimaryDatabase
或 PrimaryWebService
设置。
在客户端解决方案中,让我们创建一个实现 SolutionConfigurationsClassLibrary
中定义的 IExternalDataAccessSetting
接口的类。创建一个新类并将其命名为 ExternalDataAccessSettings
。ExternalDataAccessSettings
的代码如下所示。在 IExternalDataAccessSetting
接口所需的每个方法中,我们提供“override
”(不要与标记为 override
关键字的方法混淆,我们稍后会讲到)设置来替换 app.config 设置。在此文件的顶部,您还需要为“SolutionConfigurationsClassLibrary
”添加一个 using
语句。
public class ExternalDataAccessSettings : IExternalDataAccessSettings
{
public string DatabaseName
{
get
{ return "CONNECTION_STRING_OVERRIDE"; }
}
public string WebServiceUrl
{
get
{ return "URL_OVERRIDE"; }
}
}
将客户端解决方案的 Program 类更改为使用以下代码。这会实例化 ExternalDataAccessSettings
类,并将其提供给 DatabaseConnection
和 WebServiceClient
类库对象的构造函数。因为我们有这两个类的带参数版本构造函数,所以将使用客户端的 ExternalDataAccessSettings
而不是类库的 DefaultExternalDataAccessSettings
。
class Program
{
static void Main(string[] args)
{
var dataAccessSettings = new ExternalDataAccessSettings();
DatabaseConnection conn = new DatabaseConnection(dataAccessSettings);
WebServiceClient ws = new WebServiceClient(dataAccessSettings);
FancyCalculator calc = new FancyCalculator(conn, ws);
int result = calc.DoFancyStuffWithANumber(5);
Console.ReadLine();
}
}
正如预期的那样,此更改的输出显示了连接字符串和 Web 服务 URL 的覆盖设置,而无论我们在 Debug 或 Release 中运行客户端。
如果我们只想为 **数据库名称** 或 **Web 服务 URL** 提供非 app.config 设置,那么我们的 ExternalDataAccessSettings
可以继承自 DefaultExternalDataAccessSettings
,而不是将其声明为 IExternalDataAccessSetting
的实现者。然后,我们将声明要覆盖的属性,即 DatabaseName
或 WebServiceUrl
,并在相关的属性 getter 的签名中添加 override
关键字。这是可能的,因为我们在 DefaultExternalDataAccessSettings
实现中将属性 getter 标记为 virtual
。下面的示例显示了这一点。“CONNECTION_STRING_OVERRIDE
”将显示连接字符串,而不管 Debug/Release 解决方案配置如何,但 Web 服务 URL **仍将在 TEST 和 PROD 之间切换**,因为我们没有覆盖它。总而言之,可能将数据库设置的接口和实现与 Web 服务设置分开,并将每个实现提供给各自的 DatabaseConnection
和 WebServiceClient
类会更容易。
public class ExternalDataAccessSettings : DefaultExternalDataAccessSettings
{
public override string DatabaseName
{
get
{ return "CONNECTION_STRING_OVERRIDE"; }
}
}
结论
我们已经涵盖了一个相当简单的用例:设置一个客户端应用程序来引用一个需要为在多个环境中运行而配置的类库。通过使用特定于环境的客户端配置文件,并通过编辑客户端 .csproj 文件以动态切换提供给类库的配置文件的设置,客户端应用程序可以通过简单地切换客户端的解决方案配置(在 Debug 和 Release 之间)来在测试和生产环境中运行。
这对我个人帮助很大,因为我不再需要担心是否记得在将数据库和 Web 服务连接从测试切换到生产(反之亦然)后重新构建类库。我只需要切换到我需要的客户端解决方案配置,就可以开始工作了!
我还要补充一点,过去当我想到依赖注入时,我一直认为它用于为依赖对象提供必需的对象实例。但是,您也可以将配置视为可以通过依赖注入提供的依赖项。我认为这个 StackExchange 帖子提供了一些额外的见解——我们可以将配置包装在一个类中,然后它就变成了一个完全相同的场景。
进一步阅读/参考
以下是我在撰写本文时使用的资源,或者是我偶然发现并认为有用的资源:
- 如何为多个生成配置选择不同的 app.config
- MSBuild 内置变量列表
- 启用 app.debug.config app.release.config
- 使用预生成事件管理多个配置文件环境
- 如何在 Visual Studio C# .NET 中使用 App.config
- MSDN - 应用程序设置
- MSDN - 使用配置文件配置应用程序
- StackOverflow - 类库的 app.config(第二个答案)
历史
- 2016 年 4 月 1 日:初始版本