Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

App.Config Type String Verification with MSBuild

0.00/5 (No votes)
17 Oct 2009 1  
How to use an MSBuild custom task to provide compile time verification of string type names in app.config files.

Image © All Rights Reserved by Flickr member dr_odio

Contents

Introduction

If you’ve ever used any kind of IOC framework where configuration can be performed via a config file, you’ll know what I mean when I say that sometimes, detecting config errors that result from bad type names can be troublesome. Because of this, where there does not exist a real need for extensibility without recompilation, it is prudent to avoid config, and to wire type registrations for service location in code. Sometimes though there is a genuine need for the added flexibility afforded by config, and in such cases, whether you are hooking up modules in a composite application, or registering types in your IOC container, it is always preferable to catch errors at compile time, rather than at runtime. Some time ago Microsoft decoupled the build process and produced the standalone tool MSBuild. It offers a host of opportunities to inspect projects, generate code, and perform auxiliary tasks at build-time. If you are new to MSBuild we recommend beginning with the following resources:

Today we will look at creating a custom task that inspects app.config files to make sure that type strings are resolvable at compile time.

How It Works

The App.Config Type verifier is a custom MSBuild task. It examines your app.config file at compile time and verifies that string type names are resolvable.

To demonstrate, let’s take a look at a simple example. The following is an excerpt from an app.config file.

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

Here we have a section defined that refers to a type called BahType in assembly Foo. At compile time, the AppConfigVerifier will attempt to resolve the type BahType. If the type is unresolvable a build error will ensue.

Figure: Build error from missing assembly.

We are able to control how types are resolved. In a later section we will see how we can use XML comments to exclude and include type and assembly names.

Using It

In order to use the Type Verifier in a project follow the following steps:

  • Copy the External Targets directory included in the download to your VS Solutions directory
  • Add an Import element to your project file containing the app.config, with the path to the Verifier.targets project specified, as demonstrated in the following excerpt.
<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>

And that’s it. Now when the project is built, type strings in the app.config file will be verified automatically.

Creation of a Custom MSBuild Task

The AppConfigVerifier solution contains a project called AppConfigVerifier.MSBuild. This project serves as a host for our custom task.

Figure: AppConfigVerifier.MSBuild project

By importing the file Verifier.targets into a project, we are able to trigger the execution of a custom task called AppConfigVerifier.MSBuild.Verifier. This occurs when the host project’s build occurs.

The following excerpt is taken from the Verifier.targets file:

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

The UsingTask element identifies the type which is instanciated by the MSBuild at build time. This class AppConfigVerifier.MSBuild.Verifier extends Microsoft.Build.Utilities.Task, and overrides a method called Execute; which serves as the entry point to do our type verification.

Notice that we specify the custom task in the VerifyAppConfig using the shortened “Verifier”. This is shorthand for the full type name AppConfigVerifier.MSBuild.Verifier. If we had multiple tasks called Verifier, we would obviously have to use the full task name in our targets to avoid collisions.

The contents of the Verifier element allow us to pass information to our custom task at build time. This includes the list of reference paths, the default reference path, and the name of the config file to be verified.

The Verifier’s execute method is the entry point for our custom task and is shown in the following excerpt:

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

		
	}
}

Here we perform some rudimentary validation of the task data. Following this the Verifier creates a new instance of GlobalContext.

Figure: The GlobalContext and associated types.

The GlobalContext allows us to associate instances used for identifying type strings, called Scanners, and instances for creating sets of rules that are used to determine if a type string is valid, known as RuleSetCreators. The process can be summed up as follows: GlobalContext uses all associated scanners to retrieve the type tokens. Each token is then evaluated against the rules provided by the RuleSetCreators. Rules raise events that trigger the GlobalContext to report that a TypeToken is invalid.

In code it looks like this:

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

When applying a rule to a TypeToken, if the TypeToken is deemed invalid then an ErrorReceived event will be raised. The GlobalContext uses the Microsoft.Build.Utilities.TaskLoggingHelper to push the build error to the user. Visual Studio listens to the logging system of MSBuild, so that when an error is logged it shows up in the Visual Studio Error List window. This can be seen in the following excerpt from the Verifier class:

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

Scanning

In order to locate the type string segments in a config file we use a ConfigScanner. This class simply uses a regular expression ("type=\"([^\"]+)\"" ) to locate type strings. For each match identified a TypeToken is instanciated. TypeToken’s hold the actual type string and the location of the string in the file. This way, we are able to report to the user the line position of a verification failure.

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

The Verifier custom task is designed with extendibility in mind. Scanners can be created in order to perform other verification tasks.

Figure: ConfigScanner find type string.

Rules

Out of the box, AppConfigVerifier comes with a single implementation of RuleSetCreator called InclusionExclusionRuleSetCreator. Its function is to look for TypeVerification tokens, in particular “Include”, “IncludeAssembly”, “Exclude”, and “ExcludeAssembly”. These tokens serve to override the default behaviour of the AppConfigVerifier by allowing us to specify overrides for types and assemblies.

Figure: InclusionExclusionRuleSetCreator

TypeVerification definitions that are placed in an app.config can be redefined, such that subsequent type strings will be validated in a different way depending on preceding TypeVerification rules in the app.config file. To achieve this, a RuleTokenComparer is used to compare the locations of RuleTokens.

Figure: Rules decide if a type string is validated.

Overriding Default Behaviour

The default behaviour of the AppConfigVerifier is to ensure that all type strings can be resolved at build time. But obviously we require the means to specify type names that, for whatever reason, are not resolvable at build time. The way we do this is to use XML comments of the format:

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

When a type name string is specified as just the namespace qualified type name, it is presumed that the type resides in the app.config’s project or in mscorlib. This is mostly to avoid scanning all referenced assemblies looking for the type, which could significantly slow down the build process. In such cases where you wish or must use the short form, add a TypeVerification Exclude definition for the type as demonstrated in the following excerpt:

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

The following is an excerpt from the demonstration project AppConfigVerifier.MSBuild.Test. It shows various valid and invalid type references.

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

At build time we see that those types that are unresolvable are displayed as build errors.

Figure: Types that are unable to be resolved at compile time are shown as build errors.

As with syntax errors for example, we are able to navigate directly to the line in the config file by double clicking on the error in the error list.

Resolving Types at Build Time

During a build, we create a temporary AppDomain in order to resolve the type names present in the app.config file. This is performed by the AssemblyLoader class. In the later stages of development of this project, the authors experienced some peculiar behaviour with Visual Studio shortly after a build had taken place. To resolve types without loading them into the current AppDomain it is common practice to subscribe from the AppDomain.CurrentDomain.AssemblyResolve event, and use a secondary AppDomain to resolve the types. The following excerpt demonstrates this:

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

As it turns out, by failing to unsubscribe from the AssemblyResolve event, it placed Visual Studio into a volatile state, causing it to crash unpredictably.

Unit Tests for the Custom Task

Various parts of the custom task were very amenable to unit testing, because they could be isolated with little effort. Here are the test results shown in Visual Studio.

Figure: Unit Test Results

Known Limitations

The app.config is not converted to an xml document representation, such as an XDocument. Because of this, XML comments are ignored for type names. So, even if you comment a section that includes type strings, AppConfigVerifier will still raise a build error if the types cannot be resolved.

When a type string does not contain the assembly name, then it is presumed to reside in either the app.config’s project or in mscorlib. As mentioned in this article, it is to avoid scanning all assemblies in the bin, which could significantly slow down the build process.

Conclusion

This article described how type strings present in app.config files are able to be verified using a custom MSBuild task. This allows us to avoid many failures that ordinarily result from unresolvable types at runtime. We have looked at creating a custom task, and how we are able to easily consume it by hosting it in its own project.

By extending the build process with custom MSBuild tasks, we are able to perform pre and post build verification of our projects. This offers us a tremendous opportunity to improve the robustness of our projects.

We hope you find this project useful. If so, then we’d appreciate it if you would rate it and/or leave feedback below.

Further Reading

History

October 2009

  • Initial Publication

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here