Damian Hickey

Mostly software and .NET related. Mostly.

Re-namespacing an assembly with Mono.Cecil and powershell

Recently (and regularly) encountered a diamond dependency problem where I had library 1 that depended on version A of an assembly and library 2 that depends on same assembly but version 2. This resulted in a clash at the application level. Keeping both libriaries up-to-date with latest version of the assembly was attempted but the updates were always out of sync. The way the assemblies were being consumed was such that types were exposed through the libriaries public API. So ILMerging and internalizing wasn't an option either. (ILMerging and non-internalizing will result in a namespace clash).

So instead, I decided to rename the dependency in one of the projects, including the namespace of all the types using Mono.Cecil and a powershell script:

# Powershell needs to be configured to be able to load .net4 assemblies http://devonenote.com/2011/01/configure-powershell-to-use-dotnet-4-0/
function Get-ScriptDirectory
{
	$Invocation = (Get-Variable MyInvocation -Scope 1).Value
	Split-Path $Invocation.MyCommand.Path
}

$loc = Get-ScriptDirectory
[Reflection.Assembly]::LoadFrom("Mono.Cecil.dll")
$dir = "$loc\packages\Package.1.0.0\lib"
$assembly_dll = "$dir\TheAssembly.dll"
$assemblyDefinition = [Mono.Cecil.AssemblyDefinition]::ReadAssembly($assembly_dll);
$assemblyDefinition.Name.Name = "NewName"
$assemblyDefinition.MainModule.Name = $assemblyDefinition.MainModule.Name.Replace("TheAssembly", "NewName")
# Change the namespace root of all the main module types
foreach($typeDefinition in $assemblyDefinition.MainModule.Types){
	$typeDefinition.Namespace = $typeDefinition.Namespace.Replace("RootNamespace", "NewRootNamespace");
}
$assemblyDefinition.Write("$dir\NewName.dll");

 

One side affect of this is that it will break debug symbols.

This is something I would do as a last resort, but useful to know nonetheless.

Rename a Visual Studio Project using PowerShell

I seem to be mostly posting about PowerShell these days... but it's because I'm find all these neat ways to make my life easier.

Renaming a project within Visual Studio is easy enough. If, like me, your project folder name is the same as the project and you'd like that to be renamed to. Doing it directly in Windows Explorer will result in a couple of problems: the solution won't find the project so you have to remove and re-add it; projects that referenced the renamed project will have to have their references re-added.

And then your root namespace, assembly name, and assembly title need to be fixed too (assuming these are the same as the project name, which is usually the case).

clickety-clickety-clickety...

The script below solve this problem. It:

  1. hg renames the project file
  2. hg renames the folder
  3. changes the assembly title in AssemblyInfo.cs
  4. changes the assembly root namespace in the project file
  5. changes the asssembly name in the project file
  6. updates all other .csproj file's reference path to the renamed project
  7. updates the path to the renamed project in the solution file
  8. is designed to be called from the same working folder as the .sln
  9. expects all project folders to be immediate child folders of the working folder

Note: This a definite 'works for me' script so will probably need some adjusting to work for you.

function Rename-Project
{
	# designed to run from the src folder
	param(
		[string]$projectName=$(throw "projectName required."),
		[string]$newProjectName=$(throw "newProjectName required.")
	)
	
	if(!(Test-Path $projectName)){
		Write-Error "No project folder '$projectName' found"
		return
	}
	
	if(!(Test-Path $projectName\$projectName.csproj)){
		Write-Error "No project '$projectName\$projectName.dll' found"
		return
	}
	
	if((Test-Path $newProjectName)){
		Write-Error "Project '$newProjectName' already exists"
		return
	}
	
	# project
	hg rename $projectName\$projectName.csproj $projectName\$newProjectName.csproj
	
	# folder
	hg rename $projectName $newProjectName
	
	# assembly title
	$assemblyInfoPath = "$newProjectName\Properties\AssemblyInfo.cs"
	(gc $assemblyInfoPath) -replace """$projectName""","""$newProjectName""" | sc $assemblyInfoPath
	
	# root namespace
	$projectFile = "$newProjectName\$newProjectName.csproj"
	(gc $projectFile) -replace "<RootNamespace>$projectName</RootNamespace>","<RootNamespace>$newProjectName</RootNamespace>" | sc $projectFile
	
	# assembly name
	(gc $projectFile) -replace "<AssemblyName>$projectName</AssemblyName>","<AssemblyName>$newProjectName</AssemblyName>" | sc $projectFile
	
	# other project references
	gci -Recurse -Include *.csproj |% { (gc $_) -replace "..\\$projectName\\$projectName.csproj", "..\$newProjectName\$newProjectName.csproj" | sc $_ }
	gci -Recurse -Include *.csproj |% { (gc $_) -replace "<Name>$projectName</Name>", "<Name>$newProjectName</Name>" | sc $_ }
	
	# solution 
	gci -Recurse -Include *.sln |% { (gc $_) -replace "\""$projectName\""", """$newProjectName""" | sc $_ }
	gci -Recurse -Include *.sln |% { (gc $_) -replace "\""$projectName\\$projectName.csproj\""", """$newProjectName\$newProjectName.csproj""" | sc $_ }
}

 

I'm not totally enamoured with the find/replace bits, but they seem to work.

Any improvements will of course be welcomed :)

PSake task to scan and execute all test projects

Another one for the toolbox. This script will execute all scan for all unit test projects and execute them. It assumes that all test project names end with 'Tests', the assembly name is the same as project name, and the project folder is at the solution root.

 

task RunTests -depends Compile {
	$xunitRunner = "tools\XUnitRunner\xunit.console.clr4.exe"
        $project = $_.BaseName
        Get-ChildItem ..\ -Recurse -Include *Tests.csproj | % {
		   .$xunitRunner "$srcDir\$project\bin\Release\$project.dll"
	}
}

 

Simple Powershell script to tidy NuGet's packages.config

This:

param([string]$path = ".")

$packagesConfigFiles = Get-ChildItem $path -Recurse | Where-Object {$_.Name -eq "packages.config"}

if($packagesConfigFiles -eq $null){
	Write-Error "No packages found"
	return
}

foreach($packagesConfig in $packagesConfigFiles){
	Write-Host "Tidying" $packagesConfig.FullName
	$xml = [xml](Get-Content $packagesConfig.FullName)
	$sortedNodes = $xml.packages | select-xml 'package' | Select-Object -expand Node | sort 'id'

	[xml]$sortedXml = "<?xml version='1.0' encoding='utf-8'?>
	<packages>
		$($sortedNodes | % { $_.OuterXml})
	</packages>"

	$sortedXml.Save($packagesConfig.FullName)
}

...will turn this:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Iesi.Collections" version="3.1.0.4000" />
  <package id="NHibernate" version="3.1.0.4000" />
  <package id="Castle.Core" version="2.5.2" />
  <package id="NHibernate.Castle" version="3.1.0.4000" />
  <package id="FluentNHibernate" version="1.2.0.712" />
  <package id="Microsoft.IdentityModel" version="6.1.7600" />
  <package id="NBuilder" version="2.3.0.0" />
  <package id="FluentMigrator" version="0.9.0.1" />
  <package id="Ninject" version="2.2.1.0" />
  <package id="Ninject.Extensions.Wcf" version="2.2.0.0" />
  <package id="Ninject.Web.Mvc2" version="2.2.0.1" />
  <package id="Common.Logging" version="1.2.0" />
  <package id="Rx-Core" version="1.0.2856.0" />
  <package id="Compare-NET-Objects" version="1.0.2.0" />
  <package id="StoryQ" version="2.0.5" />
  <package id="Moq" version="4.0.10827" />
  <package id="xunit" version="1.8.0.1545" />
  <package id="xunit.extensions" version="1.8.0.1545" />
</packages>

...into this:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Castle.Core" version="2.5.2" />
  <package id="Common.Logging" version="1.2.0" />
  <package id="Compare-NET-Objects" version="1.0.2.0" />
  <package id="FluentMigrator" version="0.9.0.1" />
  <package id="FluentNHibernate" version="1.2.0.712" />
  <package id="Iesi.Collections" version="3.1.0.4000" />
  <package id="Microsoft.IdentityModel" version="6.1.7600" />
  <package id="Moq" version="4.0.10827" />
  <package id="NBuilder" version="2.3.0.0" />
  <package id="NHibernate" version="3.1.0.4000" />
  <package id="NHibernate.Castle" version="3.1.0.4000" />
  <package id="Ninject" version="2.2.1.0" />
  <package id="Ninject.Extensions.Wcf" version="2.2.0.0" />
  <package id="Ninject.Web.Mvc2" version="2.2.0.1" />
  <package id="Rx-Core" version="1.0.2856.0" />
  <package id="StoryQ" version="2.0.5" />
  <package id="xunit" version="1.8.0.1545" />
  <package id="xunit.extensions" version="1.8.0.1545" />
</packages>

To be honest, I'd prefer if NuGet did this itself.

Script to strip strong names from a project's references.

In my (never-ending) quest for true continous integrations nirvana, I find that version numbers in the a projects reference...

<Reference Include="System.Reactive, Version=1.0.2787.104, Culture=neutral, etc">
	<HintPath>..\..\libs\RxFramework\System.Reactive.dll</HintPath>
</Reference>

... sometimes obstruct me. Especially when dropping in a new version and attempting to build from a script.

This script below will recursively go through all the .csproj files from a base directory and strip out the strong name above, leaving:

<Reference Include="System.Reactive">
	<HintPath>..\..\libs\RxFramework\System.Reactive.dll</HintPath>
</Reference>

Le script:

param(
	[string]$path = $(throw "path required.")
)

[Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") | Out-Null
$csprojs = Get-ChildItem . -Recurse | Where-Object {$_.Extension -match "csproj"} | Select-Object FullName
$ns = "http://schemas.microsoft.com/developer/msbuild/2003"
foreach($csproj in $csprojs){
	$projectModified = $false
	$path = $csproj.FullName
	$csprojXmlDoc = [System.Xml.Linq.XDocument]::Load($path)
	$csprojXmlDoc.Descendants("{$ns}Reference") | 
		Where-Object {$_.Attribute("Include").Value.Contains(",") } | 
		ForEach { 
			$index = $_.Attribute("Include").Value.IndexOf(',')
			$newAttributeValue =  $_.Attribute("Include").Value.SubString(0, $index)
			Write-Host ($path + ": " + $_.Attribute("Include").Value + " -> $newAttributeValue")
			$_.Attribute("Include").Value = $newAttributeValue
			$projectModified = $true
			}
	if($projectModified -eq $true){
		$csprojXmlDoc.Save($path)
	}
}

PowerShell script to restore a project's packages and tools

Requires nuget.exe to be in the solution root and commited to source control.

# Tools
.\NuGet.exe i DotCover -s \\myserver\Dev\NuGetPackages -o Tools
.\NuGet.exe i StyleCopCmd -s \\myserver\Dev\NuGetPackages -o Tools

# Dependencies
$packageConfigs = Get-ChildItem . -Recurse | where{$_.Name -eq "packages.config"}
foreach($packageConfig in $packageConfigs){
  Write-Host "Restoring" $packageConfig.FullName
  .\nuget.exe i $packageConfig.FullName -o Source\Packages
}