I created MergeSolution task for MsBuild. It is for merging multiple solutions into one solution file. I'm using PLINQ to to parse and process all solutions in parallel. I'm using my DisposableProject class to access project files inside every solution to get some project-specific values. DisposableProject class was created to fix race condition issues because some project files are included in multiple solutions and also to fix huge memory consumption during merging process.
/// <summary>
/// Adding disposable functionality to Microsof.Build.BuildEngine.Project class.
/// Every project file is locked during processing to get a rid of huge memory consumption and race condition issues.
/// Project files are unloaded from build engine to save memory.
/// </summary>
public class DisposableProject : Project, IDisposable
{
// Build engine - is it really needed?
private static Engine buildEngine = new Engine();
// Event used for thread safe Mutex creation
static AutoResetEvent loadingEvent = new AutoResetEvent(true);
// Project file name
private string FileName {get;set;}
// Dictionary with Mutex name - Mutex
static ConcurrentDictionary<string, Mutex> mutexLocks = new ConcurrentDictionary<string, Mutex>();
/// <summary>
/// DisposableProject constructor. Project file is loaded and named Mutex is created to lock file by its file name.
/// </summary>
/// <param name="projectFileName">MsBuild project file name</param>
public DisposableProject(string projectFileName)
: base(buildEngine)
{
// Use file name as mutex name
FileName = GetMutexName(projectFileName);
// Lock file during processing
// Lock Mutex creation because multiple threads can create duplicate Mutexes causing sync exceptions
loadingEvent.WaitOne();
// Check if mutex object already exists
if(!mutexLocks.ContainsKey(FileName))
mutexLocks[FileName] = new Mutex(false, FileName);
// Unlock mutex creation
loadingEvent.Set();
// Wait for file
mutexLocks[FileName].WaitOne();
// Load project file
this.Load(projectFileName, ProjectLoadSettings.IgnoreMissingImports);
}
/// <summary>
/// Unload MsBuild project file from memory and Mutex (file lock) is released.
/// </summary>
public void Dispose()
{
// Unload from build engine to save memory
this.ParentEngine.UnloadProject(this);
// Unlock file
mutexLocks[FileName].ReleaseMutex();
}
/// <summary>
/// Create mutex name from file name.
/// </summary>
/// <param name="pathName">File name</param>
/// <returns>Mutex name</returns>
private string GetMutexName(string pathName)
{
// Replace directory separator chars with underscore
return pathName.Replace(Path.DirectorySeparatorChar, '_');
}
}
Sometimes my MergeSolutions task fail with following exception during loading of the first project file:
InvalidProjectFileException: Invalid static method invocation syntax: "$([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToStandardLibraries($(TargetFrameworkIdentifier), $(TargetFrameworkVersion), $(TargetFrameworkProfile)))". Static method invocation should be of the form: $([FullTypeName]::Method()), e.g. $([System.IO.Path]::Combine(`a`, `b`))\r
at Microsoft.Build.BuildEngine.Shared.ProjectErrorUtilities.VerifyThrowInvalidProject(Boolean condition, String errorSubCategoryResourceName, XmlNode xmlNode, String resourceName, Object arg0)\r
at Microsoft.Build.BuildEngine.Expander.Function.ExtractPropertyFunction(String expressionFunction, Object propertyValue)\r
at Microsoft.Build.BuildEngine.Expander.ExpandPropertyBody(String propertyBody, Object propertyValue, BuildPropertyGroup properties, ExpanderOptions options)\r
at Microsoft.Build.BuildEngine.Expander.ExpandPropertiesLeaveTypedAndEscaped(String expression, XmlNode expressionNode)\r
at Microsoft.Build.BuildEngine.Expander.ExpandAllIntoStringLeaveEscaped(String expression, XmlNode expressionNode)\r
at Microsoft.Build.BuildEngine.BuildPropertyGroup.Evaluate(BuildPropertyGroup evaluatedPropertyBag, Hashtable conditionedPropertiesTable, ProcessingPass pass)\r
at Microsoft.Build.BuildEngine.Project.ProcessProjectChildren(XmlElement projectElement, String projectDirectoryLocation, Boolean importedProject)\r
at Microsoft.Build.BuildEngine.Project.ProcessImportElement(XmlElement importElement, String projectDirectoryLocation, Boolean importedProject)\r
at Microsoft.Build.BuildEngine.Project.ProcessProjectChildren(XmlElement projectElement, String projectDirectoryLocation, Boolean importedProject)\r
at Microsoft.Build.BuildEngine.Project.ProcessImportElement(XmlElement importElement, String projectDirectoryLocation, Boolean importedProject)\r
at Microsoft.Build.BuildEngine.Project.ProcessProjectChildren(XmlElement projectElement, String projectDirectoryLocation, Boolean importedProject)\r
at Microsoft.Build.BuildEngine.Project.ProcessMainProjectElement()\r
at Microsoft.Build.BuildEngine.Project.InternalLoadFromXmlDocument(XmlDocument projectXml, ProjectLoadSettings projectLoadSettings)\r
at Microsoft.Build.BuildEngine.Project.Load(String projectFileName, BuildEventContext buildEventContext, ProjectLoadSettings projectLoadSettings)\r
at XXX.Build.Common.Types.DisposableProject..ctor(String projectFileName) in D:\XXX\DisposableProject.cs:line 35\r
MergeSolution task always throw this exception at the beginning of merging process and it happen only sometimes. Is is BuildEngine initialization issue, I guess. BuildEngine constructor is probably not thread-safe. I'm using default MSBuild constructor. Any ideas how to solve it?