65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MSBuild 进行 App.Config 类型字符串验证

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (14投票s)

2009 年 10 月 17 日

Ms-PL

8分钟阅读

viewsIcon

53440

downloadIcon

351

如何使用自定义 MSBuild 任务在编译时验证 app.config 文件中的字符串类型名称。


图片 © 保留所有权利,由 Flickr 会员 dr_odio 提供。

目录

引言

如果您使用过任何一种可以通过配置文件进行配置的 IOC 框架,您就会明白我说的那种情况,即有时检测由于类型名称错误而导致的配置错误可能很麻烦。因此,在没有真正需要无需重新编译即可进行扩展的情况下,避免使用配置文件,并在代码中编写服务定位的类型注册是很明智的。但有时确实有必要利用配置的灵活性,在这种情况下,无论您是在组合应用程序中挂接模块,还是在 IOC 容器中注册类型,最好在编译时捕获错误,而不是在运行时捕获。很久以前,微软将构建过程解耦,并推出了独立的工具 MSBuild。它提供了许多机会来检查项目、生成代码以及在构建时执行辅助任务。如果您不熟悉 MSBuild,我们建议从以下资源开始:

今天我们将探讨如何创建一个自定义任务,该任务将检查 app.config 文件,以确保类型字符串在编译时是可解析的。

工作原理

App.Config 类型验证器是一个自定义 MSBuild 任务。它在编译时检查您的 app.config 文件,并验证类型字符串是否可解析。

为了演示,让我们来看一个简单的例子。以下是 app.config 文件的一段摘录。

<configuration>
	<configSections>
		<section name="name1" type="Foo.BahType, Foo"/>
	</configSections>
</configuration>

这里我们定义了一个节,它引用了 Foo 程序集中的 BahType 类型。在编译时,AppConfigVerifier 将尝试解析 BahType 类型。如果类型无法解析,则会发生构建错误。

图:由于缺少程序集而导致的构建错误。

我们可以控制类型如何解析。在后面的章节中,我们将看到如何使用 XML 注释来排除和包含类型和程序集名称。

使用方法

为了在项目中 M. 使用类型验证器,请按照以下步骤操作:

  • 将下载中包含的 External Targets 目录复制到您的 VS Solutions 目录。
  • 在您的项目文件中添加一个 Import 元素,其中包含 app.config,并指定 Verifier.targets 项目的路径,如以下摘录所示。
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <!-- AppConfigVerifier http://appconfigverifier.codeplex.com/ -->
  <Import Project="External Targets\Verifier.targets" />
	
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>-->
</Project>

就是这样。现在,当项目构建时,app.config 文件中的类型字符串将自动得到验证。

创建自定义 MSBuild 任务

AppConfigVerifier 解决方案包含一个名为 AppConfigVerifier.MSBuild 的项目。该项目作为我们自定义任务的宿主。

图:AppConfigVerifier.MSBuild 项目

通过将 Verifier.targets 文件导入到项目中,我们可以在宿主项目构建时触发名为 AppConfigVerifier.MSBuild.Verifier 的自定义任务的执行。

以下摘录来自 Verifier.targets 文件。

<Project ToolsVersion="3.5" DefaultTargets="Build" 
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
	<UsingTask TaskName="AppConfigVerifier.MSBuild.Verifier" 
	    AssemblyFile="AppConfigVerfier.MSBuild.dll" />
	<PropertyGroup>
		<RunVerifier>true</RunVerifier>
	</PropertyGroup>
	<PropertyGroup>
		<BuildDependsOn>
			$(BuildDependsOn);
			VerifyAppConfig
		</BuildDependsOn>
	</PropertyGroup>
	<Target Name="VerifyAppConfig" Condition="'$(RunVerifier)' == 'true'">
		<ItemGroup>
			<DefaultReferencePath Include=" @(MainAssembly->'%(FullPath)')"/>
		</ItemGroup>
		<Verifier References="@(ReferencePath)" 
			DefaultReferences="@(DefaultReferencePath)" 
			Config="$(MSBuildProjectDirectory)\$(AppConfig)">
		</Verifier>
	</Target>
</Project>

UsingTask 元素标识了 MSBuild 在构建时实例化的类型。此类 AppConfigVerifier.MSBuild.Verifier 继承自 Microsoft.Build.Utilities.Task,并重写了名为 Execute 的方法;该方法作为类型验证的入口点。

请注意,我们在 VerifyAppConfig 中使用简写“Verifier”来指定自定义任务。这是完整类型名称 AppConfigVerifier.MSBuild.Verifier 的缩写。如果我们有多个名为 Verifier 的任务,我们显然必须在目标中使用完整的任务名称以避免冲突。

Verifier 元素的内容允许我们在构建时将信息传递给我们的自定义任务。这包括引用路径列表、默认引用路径以及要验证的配置文件的名称。

Verifier 的 Execute 方法是我们自定义任务的入口点,并在以下摘录中显示:

public class Verifier : Task
	{
		bool hasError;

		public ITaskItem[] References
		{
			set
			{
				references = value.Select(item => item.ItemSpec).ToArray();
			}
		}

		string[] references = new string[0];
        
		public ITaskItem[] DefaultReferences
		{
			set
			{
				defaultReferences = value.Select(item => item.ItemSpec).ToArray();
			}
		}

		string[] defaultReferences = new string[0];

		[Required]
		public ITaskItem Config
		{
			set
			{
				configFile = value.ItemSpec;
			}
		}

		string configFile;

		public override bool Execute()
		{
			if (!File.Exists(configFile))
			{
				LogError(configFile + " not found");
				return false;
			}

			string content;
			try
			{
				content = File.ReadAllText(configFile);
			}
			catch (SecurityException ex)
			{
				Log.LogError("Security error when accessing " 
					+ configFile + " : " + ex.Message);
				return false;
			}
			catch (IOException ex)
			{
				Log.LogError("Read error when accessing " 
					+ configFile + " : " + ex.Message);
				return false;
			}

			var global = new GlobalContext(content);
			List listDefaultRef = defaultReferences.ToList();
			listDefaultRef.Add(typeof(String).Assembly.Location); //mscorlib
			var creator = new InclusionExclusionRuleSetCreator(references, listDefaultRef.ToArray());
			var scanner = new ConfigScanner();
			global.AddScanner(scanner);
			global.AddRuleSetCreator(creator);
			global.ErrorReceived += (sender, cont, startLocation, endLocation, message) =>
					{
						hasError = true;
						Log.LogError(null, null, null, configFile, startLocation.Line + 1, 
							startLocation.Column + 1, endLocation.Line + 1,
							endLocation.Column + 1, message);
					};
			global.WarningReceived += (sender, cont, startLocation, endLocation, message) 
				=> Log.LogWarning(null, null, null, configFile, startLocation.Line + 1, 
					startLocation.Column + 1, endLocation.Line + 1, endLocation.Column + 1, message);

			global.Run();
			return !hasError;
		}
        
		void LogError(string message)
		{
			hasError = true;
			Log.LogError(message);
		}

		
	}
}

在这里,我们进行了一些基本的任务数据验证。之后,Verifier 创建了一个 GlobalContext 的新实例。

图:GlobalContext 和相关类型。

GlobalContext 允许我们关联用于标识类型字符串的实例(称为 Scanners)和用于创建用于确定类型字符串是否有效的规则集的实例(称为 RuleSetCreators)。该过程可以概括为:GlobalContext 使用所有关联的扫描器来检索类型标记。然后,每个标记都根据 RuleSetCreators 提供的规则进行评估。规则会引发事件,触发 GlobalContext 报告 TypeToken 无效。

代码如下:

public void Run()
{
	string content = this.content;
	IEnumerable<TypeToken> tokens = scanners.SelectMany(
		scanner => scanner.Scan(content)).Distinct(new TokenComparer());

	var globalInfo = new GlobalInfo(this.content);
	globalInfo.Log.ErrorReceived += globalInfo_ErrorReceived;
	globalInfo.Log.WarningReceived += globalInfo_WarningReceived;
	try
	{
		foreach (TypeToken token in tokens)
		{
			var localContext = new LocalContext(token, globalInfo);
			IEnumerable<Rule> ruleSet = creators.SelectMany(
				creator => creator.CreateRules(localContext));
			foreach (Rule rule in ruleSet)
			{
				rule.Apply(localContext);
			}
		}
	}
	finally
	{
		globalInfo.Log.ErrorReceived -= globalInfo_ErrorReceived;
		globalInfo.Log.WarningReceived -= globalInfo_WarningReceived;
	}
}

当将规则应用于 TypeToken 时,如果 TypeToken 被视为无效,则会引发 ErrorReceived 事件。GlobalContext 使用 Microsoft.Build.Utilities.TaskLoggingHelper 将构建错误推送到用户。Visual Studio 会监听 MSBuild 的日志记录系统,因此当记录错误时,它会显示在 Visual Studio 错误列表窗口中。这可以在 Verifier 类中的以下摘录中看到:

global.ErrorReceived += (sender, cont, startLocation, endLocation, message) =>
	{
		hasError = true;
		Log.LogError(null, null, null, configFile, startLocation.Line + 1, 
			startLocation.Column + 1, endLocation.Line + 1,
			endLocation.Column + 1, message);
	};

扫描

为了在配置文件中定位类型字符串段,我们使用 ConfigScanner。此类仅使用正则表达式(“type=\\\"([^\"]+)\\\"”)来查找类型字符串。对于每个找到的匹配项,都会实例化一个 TypeToken。TypeToken 包含实际的类型字符串以及字符串在文件中的位置。这样,我们就能向用户报告验证失败的行位置。

public override IEnumerable<TypeToken> Scan(string content)
		{
			var matches = Regex.Matches(content, "type=\"([^\"]+)\"").OfType<Match>();
			var typeTokens = matches.Select(
				m => new TypeToken(Location.GetLocation(m.Groups[1].Index, content), m.Groups[1].Value.Trim()));
			return typeTokens.AsEnumerable();
		} 

Verifier 自定义任务在设计时考虑了可扩展性。可以创建扫描器来执行其他验证任务。

图:ConfigScanner 查找类型字符串。

规则

开箱即用,AppConfigVerifier 提供了一个 InclusionExclusionRuleSetCreator 规则集创建器实现。其功能是查找 TypeVerification 标记,特别是“Include”、“IncludeAssembly”、“Exclude”和“ExcludeAssembly”。这些标记用于覆盖 AppConfigVerifier 的默认行为,允许我们指定类型和程序集的覆盖。

图:InclusionExclusionRuleSetCreator

放置在 app.config 中的 TypeVerification 定义可以重新定义,使得后续的类型字符串根据 app.config 文件中前面的 TypeVerification 规则以不同的方式进行验证。为了实现这一点,RuleTokenComparer 用于比较 RuleToken 的位置。

图:规则决定是否验证类型字符串。

覆盖默认行为

AppConfigVerifier 的默认行为是确保所有类型字符串在构建时都可以解析。但显然我们需要一种方法来指定那些出于某种原因无法在构建时解析的类型名称。我们通过使用以下格式的 XML 注释来实现这一点:

<!-- TypeVerification [Exclude|Include|ExcludeAssembly|IncludeAssembly] : [Type|Assembly][.*] -->

当类型名称字符串仅指定为命名空间限定的类型名称时,假定该类型存在于 app.config 的项目或 mscorlib 中。这主要是为了避免扫描所有引用的程序集来查找类型,这可能会大大减慢构建过程。在这种希望或必须使用简短形式的情况下,请添加一个 TypeVerification Exclude 定义,如以下摘录所示。

<!-- TypeVerification Exclude : SomeNamespace.* -->

以下是演示项目 AppConfigVerifier.MSBuild.Test 的摘录。它显示了各种有效和无效的类型引用。

<configuration>
	<configSections>
		<section name="name" type="System.String, mscorlib"/>

		<section name="name2" type="System.Text.RegularExpressions.Regex, System"/>
		<section name="name4" type="System.Text.RegularExpressions.Regex2, System"/>
		<!-- TypeVerification ExcludeAssembly : Foo -->
		<section name="name3" type="Foo.BahType, Foo"/>
		<section name="name5" type="System.Int32"/>
		<section name="name5" type="System.Int33"/>
		<section name="name5" type="AppConfigVerifier.MSBuild.Test.Class1"/>
	</configSections>
</configuration>

在构建时,我们会看到那些无法解析的类型显示为构建错误。

图:无法在编译时解析的类型显示为构建错误。

与语法错误一样,例如,我们可以通过双击错误列表中的错误直接导航到配置文件中的相应行。

在构建时解析类型

在构建过程中,我们创建一个临时的 AppDomain 来解析 app.config 文件中的类型名称。这由 AssemblyLoader 类执行。在这个项目的后期开发阶段,作者在构建完成后不久遇到了 Visual Studio 的一些奇怪行为。为了在不将类型加载到当前 AppDomain 的情况下解析类型,通常的做法是订阅 AppDomain.CurrentDomain.AssemblyResolve 事件,并使用第二个 AppDomain 来解析类型。以下摘录对此进行了演示:

public class AssemblyLoader : IDisposable
{
	class CrossDomainData : MarshalByRefObject
	{
		public bool HasType(string path, string typeName)
		{
			Assembly typeAssembly = Assembly.LoadFrom(path);
			return typeAssembly.GetType(typeName, false, false) != null;
		}
	}

	readonly AppDomain subDomain;

	public AssemblyLoader()
	{
		var appDomainSetup = new AppDomainSetup
             	    {
             		    ApplicationBase = Path.GetDirectoryName(
             			    GetType().Assembly.Location)
             	    };
		subDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, appDomainSetup);
		AppDomain.CurrentDomain.AssemblyResolve += subDomain_AssemblyResolve;
	}

	Assembly subDomain_AssemblyResolve(object sender, ResolveEventArgs args)
	{
		return Assembly.Load(args.Name);
	}

	#region IDisposable Members

	volatile bool disposed;

	public void Dispose()
	{
		Dispose(true);
		GC.SuppressFinalize(this);
	}

	~AssemblyLoader()
	{
		Dispose(false);
	}

	void Dispose(bool disposing)
	{
		if (!disposed)
		{
			disposed = true;
			if (!disposing)
			{
			}
			AppDomain.Unload(subDomain);
			AppDomain.CurrentDomain.AssemblyResolve -= subDomain_AssemblyResolve;
		}
	}

	#endregion

	public bool HasType(string assemblyPath, string typeName)
	{
		if (disposed)
		{
			throw new InvalidOperationException("AssemblyLoader already disposed");
		}
		if (typeName == null)
		{
			throw new ArgumentNullException("typeName");
		}
		var crossData = (CrossDomainData)subDomain.CreateInstanceAndUnwrap(
			GetType().Assembly.FullName, typeof(CrossDomainData).FullName);
		return crossData.HasType(assemblyPath, typeName);
	}
}

结果证明,未能取消订阅 AssemblyResolve 事件,导致 Visual Studio 进入不稳定的状态,并导致其不可预测地崩溃。

自定义任务的单元测试

自定义任务的各个部分非常适合单元测试,因为它们可以轻松地进行隔离。以下是 Visual Studio 中显示的测试结果。

图:单元测试结果

已知限制

app.config 未转换为 XML 文档表示,例如 XDocument。因此,XML 注释会被忽略,不适用于类型名称。所以,即使您注释掉了包含类型字符串的部分,如果类型无法解析,AppConfigVerifier 仍然会引发构建错误。

当类型字符串不包含程序集名称时,则假定它存在于 app.config 项目或 mscorlib 中。正如本文所述,这是为了避免扫描 bin 中的所有程序集,这可能会大大减慢构建过程。

结论

本文介绍了如何使用自定义 MSBuild 任务来验证 app.config 文件中存在的类型字符串。这使我们能够避免许多通常由于运行时类型无法解析而导致的故障。我们探讨了如何创建自定义任务,以及如何通过将其托管在自己的项目中来轻松使用它。

通过使用自定义 MSBuild 任务扩展构建过程,我们可以对项目执行构建前和构建后验证。这为我们改进项目健壮性提供了巨大的机会。

我们希望您发现这个项目很有用。如果是这样,如果您能给它评分和/或在下方留下反馈,我们将不胜感激。

延伸阅读

历史记录

2009 年 10 月

  • 初次发布
© . All rights reserved.