Feb 16 2012

Quick Poll–UX of UI navigation for list with a single item

Category: Software DesignRory Primrose @ 17:31

I’m after some feedback from the community regarding the UX of UI navigation when dealing with a list of items where there is only one item in the list.

I have a scenario in a UI where there is a list of subscriptions for a user account. Most of the time (~>90%) there will only be one subscription. There are two options for handling this.

  1. Always display the list when the user navigates to the list UI and force the user to manually select the only item available
  2. Automatically redirect the user to the item display screen if there is only one item

Option #1 is consistent but includes an unnecessary navigation (+ human intervention). Option #2 is streamlined, but provides an inconsistent UX when the list changes to have a second item.

I have leant towards #2 because of the expected metrics of my specific scenario in addition to it being more streamlined. The point of inconsistency is a thorn in my side however.

Thoughts? Votes? Any UX experts want to shed some opinions?

Tags:

Feb 7 2012

Finding solutions not covered by automated builds

Category: Rory Primrose @ 12:01

I am slowing building a set of automated tasks in my current role as a TFS administrator to verify the state of TFS. My latest task looks for solutions that are not covered by automated builds.

It’s a fairly straight forward task that enumerates solution files and matches them to build definitions across all projects and collections in a TFS instance.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Linq;
using System.Xml;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.Framework.Common;
using Microsoft.TeamFoundation.VersionControl.Client;

namespace TFSVerifier
{
    [Export(typeof(ITask))]
    public class FindSolutionsWithoutBuildsTask : ITask
    {
        private Uri TfsAddress = new Uri("http://[TfsAddressHere]:8080/tfs", UriKind.Absolute);

        public string Name
        {
            get { return GetType().Name; }
        }

        public void Execute()
        {
            TfsTeamProjectCollection server = new TfsTeamProjectCollection(TfsAddress);

            server.EnsureAuthenticated();

            TfsConfigurationServer configurationServer = server.ConfigurationServer;

            ReadOnlyCollection<CatalogNode> collectionNodes = configurationServer.CatalogNode.QueryChildren(
                new[] { CatalogResourceTypes.ProjectCollection },
                false, CatalogQueryOptions.None);

            collectionNodes.ToList().ForEach(x => ProcessCollection(server, x));
        }

        private void ProcessCollection(TfsTeamProjectCollection server, CatalogNode collectionNode)
        {
            Guid collectionId = new Guid(collectionNode.Resource.Properties["InstanceId"]);
            TfsTeamProjectCollection teamProjectCollection = server.ConfigurationServer.GetTeamProjectCollection(collectionId);

            ReadOnlyCollection<CatalogNode> projectNodes = collectionNode.QueryChildren(
                new[] { CatalogResourceTypes.TeamProject },
                false, CatalogQueryOptions.None);

            projectNodes.ToList().ForEach(x => ProcessProject(server, collectionNode, x));
        }

        private void ProcessProject(TfsTeamProjectCollection server, CatalogNode collectionNode, CatalogNode projectNode)
        {
            String projectName = projectNode.Resource.DisplayName;
            
            VersionControlServer versionControl = server.GetService<VersionControlServer>();
            ItemSpec spec = new ItemSpec("$/" + projectName + "/*.sln", RecursionType.Full);
            ItemSet set = versionControl.GetItems(spec, VersionSpec.Latest, DeletedState.NonDeleted, ItemType.File, false);

            if (set.Items.Any() == false)
            {
                return;
            }

            IEnumerable<String> solutionsInProject = from x in set.Items
                                                     select x.ServerItem;

            IBuildServer buildServer = server.GetService<IBuildServer>();
            IBuildDefinition[] definitions = buildServer.QueryBuildDefinitions(projectNode.Resource.DisplayName, QueryOptions.Definitions);
            IEnumerable<String> projectsBeingBuild = ProjectsBuiltInProject(definitions);
            IEnumerable<String> projectsNotBeingBuild = solutionsInProject.Except(projectsBeingBuild);

            if (projectsNotBeingBuild.Any())
            {
                Console.WriteLine(collectionNode.Resource.DisplayName + ": " + projectName);

                Console.ForegroundColor = ConsoleColor.Yellow;

                projectsNotBeingBuild.ToList().ForEach(x => Console.WriteLine(x));

                Console.ForegroundColor = ConsoleColor.Gray;
            }
        }

        private IEnumerable<String> ProjectsBuiltInProject(IBuildDefinition[] definitions)
        {
            foreach (IBuildDefinition definition in definitions)
            {
                IEnumerable<String> projectsToBuild = ProjectsToBuild(definition);

                foreach (String projectToBuild in projectsToBuild)
                {
                    yield return projectToBuild;
                }
            }
        }

        private IEnumerable<String> ProjectsToBuild(IBuildDefinition definition)
        {
            XmlDocument doc = new XmlDocument();
            XmlNamespaceManager manager = new XmlNamespaceManager(doc.NameTable);

           manager.AddNamespace("y", "clr-namespace:System.Collections.Generic;assembly=mscorlib");
            manager.AddNamespace("x", "clr-namespace:Microsoft.TeamFoundation.Build.Workflow.Activities;assembly=Microsoft.TeamFoundation.Build.Workflow");

            doc.LoadXml(definition.ProcessParameters);

            XmlNode node = doc.SelectSingleNode("//y:Dictionary/x:BuildSettings/@ProjectsToBuild", manager);

            if (node == null)
            {
                return new List<String>();
            }

            String projectsToBuild = node.Value;
            String[] projects = projectsToBuild.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

            return projects;
        }
    }
}

Enjoy

Tags:

Jan 29 2012

Refreshing an expired STSTestCert WIF certificate

Category: .Net | Applications | PersonalRory Primrose @ 18:32

I have been using WIF for the last couple of years on a few of my projects and the STSTestCert gets a bit of a workout on my development machines. This certificate is only valid for 12 months. All the applications that use this test certificate will fail to execute authentication requests once this certificate has expired.

Here is the easiest way to renew the certificate.

  1. Open up MMC and attach the Certificate Manager plugin for the local machine.
  2. Navigate to Certificates (Local Computer) -> Personal -> Certificates.
  3. Select and delete the expired STSTestCert certificate.
  4. Open VS with elevated rights
  5. Add a new solution
  6. Add a new STS project to that solution using the Tools -> Add STS Reference… menu item
  7. Continue through the wizard
  8. Refresh the MMC console and you should now have a fresh STSTestCert

image

Tags:

Jan 29 2012

How much do I love the yield statement?

Category: .Net | Software DesignRory Primrose @ 17:15

Quite simply, a lot. The yield statement seems to be such a simple part of C# yet it can provide such amazing power (being delayed enumeration). Outside of that power however, it can provide beautiful simplicity.

Take the following abstract class for example:

namespace MyApplication.Diagnostics
{
    using System;
    using System.Collections.Generic;

    public abstract class DiagnosticTask
    {
        public abstract IEnumerable<DiagnosticTaskResult> ExecuteAll();

        public abstract DiagnosticTaskResult ExecuteStep(Guid id);

        public virtual String Description
        {
            get
            {
                return String.Empty;
            }
        }

        public abstract Guid Id
        {
            get;
        }

        public abstract IEnumerable<DiagnosticTaskStep> Steps
        {
            get;
        }

        public virtual Boolean SupportsSingleStepExecution
        {
            get
            {
                return false;
            }
        }

        public abstract String Title
        {
            get;
        }
    }
}

The design of this abstract class allows for a diagnostic task to have multiple steps. My first implementation of this class however is one that only had a single hard-coded step. Enter the wonderful yield return statement.

public override IEnumerable<DiagnosticTaskStep> Steps
{
    get
    {
        yield return _validateStep;
    }
}

This is so much more elegant than creating a new collection to return the single predetermined item. I can’t say how much I love the yield statement.

Tags:

Jan 24 2012

Configuring a TFS 2010 build agent to compile SharePoint 2010 projects

Category: .NetRory Primrose @ 07:09

I’ve been doing several TFS consulting gigs over the last couple of years. The one thing that keeps popping up is the requirement to build several types of platforms using TeamBuild/TFSBuild. My preference is to isolate build agents and their customisations. This means for example that I have a mix of build agents like the following:

  • Standard (full VS install + any common additions - WiX for example)
  • SharePoint
  • BizTalk

Tagging the build agents and configuring the build definitions for those tags then allows the build to be directed to the correct build agent.

The next consideration is that the customisations to the build agent will ideally contain just what is required to build solutions and run unit tests. A SharePoint build agent for example should not have a full SharePoint installed on it. Instead it can just have the required assembly dependencies registered on it to allow the build to succeed.

There is a PowerShell script floating around that assists in harvesting SharePoint assemblies from a development server to then install and configure them on the SharePoint build agent. The original script was published by Microsoft (see here) and subsequently updated by the SharePoint Developer Team to include additional assemblies. The later post also provides a great walkthrough for creating SharePoint build definitions.

We are finding in my current consultation that the client is referencing even more assemblies. Complicating the matter is that several of the assemblies exist in the GAC as both x86 and x64. The PowerShell script does not handle this case and throws an error.

I have updated the PowerShell script to include the following changes:

  • Renders all matching files when multiple files match the file name (used to then retarget the included files, see next point)
  • Allowed full file paths to be included (including GAC paths in order to specify a particular GAC entry)
  • Skipped harvesting files that have already been harvested
    • Includes skipping the recursive searching for a filename which was the slowest part of the script
  • Added defensive code for updating the registry to point to installed assemblies (previous script threw an error if the registry key already existed)

Here is my version of the SharePoint harvest script.

#==============================================================================
# This script collects and installs the files required to build SharePoint
# projects on a TFS build server. It supports both TFS 2010 and TFS 2008.
#
# Step 1. Run the script on a developer machine where VS 2010 and SP 2010 are installed.
#         Use the "-Collect" parameter to collect files required for installation.
#
# Step 2. Copy the script and folder of collected files to a TFS build server.
#         Use the "-Install" parameter to install the files.
#
# Copyright (c) 2010 Microsoft Corp.
#==============================================================================

param (
  [switch]$Collect, 
  [switch]$Install, 
  [switch]$Quiet
)

#==============================================================================
# Script variables
#==============================================================================

# Name of the script file
$MyScriptFileName = Split-Path $MyInvocation.MyCommand.Path -Leaf

# Directory from which script is run
$MyScriptDirectory = Split-Path $MyInvocation.MyCommand.Path

function InitializeScriptVariables()
{
  # Program Files path is different on 32- and 64-bit machines.
  $Script:ProgramFilesPath = GetProgramFilesPath
  
  # SharePoint assemblies that can be referenced from SharePoint projects.
  # If you need more assemblies, add them here.
  $Script:SharePoint14ReferenceAssemblies = @( 
"Microsoft.Office.Policy.dll", 
"Microsoft.Office.Server.dll", 
"C:\Windows\Assembly\GAC_64\Microsoft.Office.Server.Search\14.0.0.0__71e9bce111e9429c\Microsoft.Office.Server.Search.dll",
"Microsoft.Office.Server.UserProfiles.dll", 
"Microsoft.Office.Workflow.Feature.dll",
"C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Workflow.Feature\14.0.0.0__71e9bce111e9429c\Microsoft.Office.Workflow.Feature.dll",
"Microsoft.SharePoint.Client.dll", 
"Microsoft.SharePoint.Client.Runtime.dll", 
"Microsoft.SharePoint.Client.ServerRuntime.dll", 
"Microsoft.SharePoint.dll", 
"Microsoft.SharePoint.IdentityModel.dll",
"Microsoft.SharePoint.Linq.dll", 
"Microsoft.SharePoint.Portal.dll", 
"Microsoft.SharePoint.Publishing.dll",
"C:\Windows\Assembly\GAC_64\Microsoft.SharePoint.Search\14.0.0.0__71e9bce111e9429c\Microsoft.SharePoint.Search.dll", 
"Microsoft.SharePoint.Security.dll", 
"Microsoft.SharePoint.Taxonomy.dll", 
"Microsoft.SharePoint.WorkflowActions.dll", 
"Microsoft.Web.CommandUI.dll",
"System.Web.DataVisualization.dll"
  )  

  # Destination path where SharePoint14 assemblies will be copied
  $Script:SharePoint14ReferenceAssemblyPath = Join-Path $ProgramFilesPath "Reference Assemblies\Microsoft\SharePoint14"

  # Folder where we collect assemblies and use as a source for installation
  $Script:FilesFolder = "Files"
  $Script:FilesPath = Join-Path $MyScriptDirectory $FilesFolder

  # Path to .NET Framework 2.0 GAC
  $Script:Gac20Path = Join-Path $Env:SystemRoot "Assembly"

  # Path to .NET Framework 4.0 GAC
  $Script:Gac40Path = Join-Path $Env:SystemRoot "Microsoft.NET\Assembly"

  # SharePoint project MSBuild extensions.
  $Script:MSBuildSharePointTools = @(
    "Microsoft.VisualStudio.SharePoint.targets",
    "Microsoft.VisualStudio.SharePoint.Tasks.dll"
  )

  # SharePoint project MSBuild extensions path.
  $Script:MSBuildSharePointToolsPath = Join-Path $ProgramFilesPath "MSBuild\Microsoft\VisualStudio\v10.0\SharePointTools"

  # SharePoint project assemblies required to create WSP files.
  $Script:SharePointProjectAssemblies = @(
    "Microsoft.VisualStudio.SharePoint.dll",
    "Microsoft.VisualStudio.SharePoint.Designers.Models.dll",
    "Microsoft.VisualStudio.SharePoint.Designers.Models.Features.dll",
    "Microsoft.VisualStudio.SharePoint.Designers.Models.Packages.dll"
  )

  # Assemblies required for DSL. It is a dependency of the SharePoint Tools model assemblies.
  # They are present on TFS 2010 Build. We need them only for TFS 2008 Build.
  $Script:DslAssemblies = @(
    "Microsoft.VisualStudio.Modeling.Sdk.10.0.dll"
  )

  # Name of the gacutil config file.
  $Script:GacUtilConfigFileName = "gacutil.exe.config"

  # Files needed to install assemblies to the GAC on TFS build machine.
  $Script:GacUtilFiles = @(
    "gacutil.exe",
    $GacUtilConfigFileName
  )

  # Path where the GacUtils.exe can be found on the development machine
  $Script:GacUtilFilePath = Join-Path $ProgramFilesPath "Microsoft SDKs\Windows\v7.0A\Bin\NETFX 4.0 Tools"

  # TfsBuildService.exe.config file name to update MSBuildPath for TFS 2008 
  $Script:TfsBuildServiceConfigFileName = "TfsBuildService.exe.config"

  # TfsBuildService.exe.config full file path to update MSBuildPath for TFS 2008 
  $Script:TfsBuildServiceConfigFilePath = Join-Path $ProgramFilesPath "Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\$TfsBuildServiceConfigFileName"
}

#==============================================================================
# Top-level script routines
#==============================================================================

# Install SharePoint project files on TFS build server
function InstallSharePointProjectFiles() {
  WriteText "Installing SharePoint project files for TFS Build."
  WriteText ""

  WriteText "Checking that the .Net Framework 4.0 is installed:"
  WriteText "Found .Net Framework version $(GetDotNet40Version)"
  WriteText ""

  if (TestSharePoint14) {
    WriteText "SharePoint 14 is installed on this machine."
    WriteText "Skipping installation of SharePoint 14 reference assemblies."
    WriteText ""
  } else {
    WriteText "Copying the SharePoint reference assemblies:"
    $SharePoint14ReferenceAssemblies | CopyFileToFolder $SharePoint14ReferenceAssemblyPath
    WriteText ""

    WriteText "Setting the SharePoint reference assemblies path in the registry:"
    AddSharePoint14ReferencePathToRegistry
    WriteText ""
  }

  WriteText "Copying the SharePoint project MSBuild extensions:"
  $MSBuildSharePointTools | CopyFileToFolder $MSBuildSharePointToolsPath
  WriteText ""

  WriteText "Installing the SharePoint project assemblies in the GAC:"
  FixGacUtilConfigVersion
  $SharePointProjectAssemblies | InstallAssemblyToGac40
  WriteText ""

  WriteText "Installing the DSL assemblies in the GAC:"
  $DslAssemblies | InstallAssemblyToGac40 -SkipIfExists
  WriteText ""

  WriteText "Fixing MSBuildPath in $TfsBuildServiceConfigFileName for TFS 2008:"
  FixTfsBuildServiceConfigMSBuildPath
  WriteText ""

  WriteText "Installation is complete."
  WriteText ""
}

# Collect SharePoint project files on developement machine for TFS build server.
function CollectSharePointProjectFiles() {
  WriteText "Collecting SharePoint project files for TFS Build."
  WriteText ""

  WriteText "Checking that the .Net Framework 4.0 is installed:"
  WriteText "Found .Net Framework version: $(GetDotNet40Version)"
  WriteText ""

  WriteText "Creating the $FilesFolder directory:"  
  $FilesPathInfo = [System.IO.Directory]::CreateDirectory($FilesPath)
  WriteText $FilesPathInfo.FullName
  WriteText ""

  WriteText "Collecting the DSL assemblies:"
  $DslAssemblies | CopyFileFromFolder $Gac40Path -Recurse
  WriteText ""

  WriteText "Collecting the SharePoint reference assemblies:"
  $SharePoint14ReferenceAssemblies | CopyFileFromFolder $Gac20Path -Recurse
  WriteText ""

  WriteText "Collecting the SharePoint project MSBuild extensions:"
  $MSBuildSharePointTools | CopyFileFromFolder $MSBuildSharePointToolsPath
  WriteText ""

  WriteText "Collecting the SharePoint project assemblies:"
  $SharePointProjectAssemblies | CopyFileFromFolder $Gac40Path -Recurse
  WriteText ""

  WriteText "Collecting the gacutil.exe files:"
  $GacUtilFiles | CopyFileFromFolder $GacUtilFilePath
  WriteText ""

  WriteText "Colection is complete."
  WriteText ""
}

# Writes instructions how to use the script.
# If -Quiet flag is provided it throws an exception.
function WriteHelp() {
  if ($Quiet) {
    throw "Specify either the -Install or -Collect parameter."
  } else {
    Write-Host ""
    Write-Host "This script collects and installs the files to build SharePoint projects on a TFS build server (2010 or 2008)."
    Write-Host ""
    Write-Host "Parameters:"
    Write-Host "  -Collect   - Collect files into the $FilesFolder folder on a development machine."
    Write-Host "  -Install   - Install files from the $FilesFolder folder on a TFS build server."
    Write-Host "  -Quiet     - Suppress messages. Errors will still be shown."
    Write-Host ""
    Write-Host "Usage:"
    Write-Host ""
    Write-Host "  .\$MyScriptFileName -Collect"
    Write-Host "     - Collects files into the $FilesFolder folder on a development machine."
    Write-Host ""
    Write-Host "  .\$MyScriptFileName -Install"
    Write-Host "     - Installs files from the $FilesFolder folder on a TFS build server."
    Write-Host ""
  }
}

#==============================================================================
# Utility functions
#==============================================================================

# Writes to Host if -Quiet flag is not provided
function WriteText($Value) {
  if (-Not $Quiet) {
    Write-Host $Value
  }
}

# Gets the Program Files (x86) path
function GetProgramFilesPath() {
  if (TestWindows64) {
    return ${Env:ProgramFiles(x86)}
  } else {
    return $Env:ProgramFiles
  }
}

# Installs assembly to the GAC
function InstallAssemblyToGac40([switch]$SkipIfExists) {
  begin {
    $GacUtilCommand = Join-Path $FilesPath "gacutil.exe"
  }
  process {
    $FileName = $_
    if ($SkipIfExists -and (TestAssemblyInGac40 $FileName)) {
      WriteText "Assembly $FileName is already in the GAC ($Gac40Path)."
      WriteText "Skipping installation in the GAC."
      return
    }
    
    $FileFullPath = Join-Path $FilesPath $FileName
    & "$GacUtilCommand" /i "$FileFullPath" /nologo

    # Verify that assembly was installed to the GAC
    if (-not (TestAssemblyInGac40 $FileName)) {
      throw "Assembly $FileName was not installed in the GAC ($Gac40Path)"
    }

    WriteText "$FileName [is installed in ==>] GAC 4.0"
  }
}

# Test if the assembly file name is already in the GAC
function TestAssemblyInGac40($AssemblyFileName) {
  $FileInfo = Get-ChildItem $Gac40Path -Recurse | Where { $_.Name -eq $AssemblyFileName }
  return ($FileInfo -ne $null)
}

# Copies file from the specified path or its sub-directory to the $FilesPath
function CopyFileFromFolder([string]$Path, [switch]$Recurse) {
  process {

    $SourcePath = $_
    $FileName = [System.IO.Path]::GetFileName($_)
    $DestinationPath = Join-Path $FilesPath $FileName

    if (-Not (Test-Path $DestinationPath))
    {
        # Check if a full file path has been provided as the source
        if (-not (Test-Path $SourcePath))
        {
            $FileInfo = Get-ChildItem $Path -Recurse:$Recurse | Where { $_.Name -eq $FileName }

            if ($FileInfo -eq $null) {
              throw "File $FileName was not found in $Path"
            }

            if ($FileInfo -is [array]) {
            
                WriteText "Multiple files found matching search critiera of '$FileName'"
                
                foreach ($match in $FileInfo)
                {      
                    WriteText $match.FullName
                }
                                
              throw "Multiple instances of $FileName were found in $Path"
            }
        
            $SourcePath = $FileInfo.FullName 
        }

        Copy-Item $SourcePath $FilesPath
        WriteText "$($SourcePath) [was copied to ==>] $FilesPath"
    }
    else
    {
        WriteText "$($DestinationPath) has already been identified, harvesting was skipped"
    }
  }
}

# Copies file to the specified path from the $FilesPath
function CopyFileToFolder([string]$Path) {
  begin {
    # Ensure that the folder exists
    $null = [System.IO.Directory]::CreateDirectory($Path)
  }
  process {
  # Get just the file name of the current context
    $FileName = [System.IO.Path]::GetFileName($_)
    $FileInfo = Get-ChildItem (Join-Path $FilesPath $FileName)

    if ($FileInfo -eq $null) {
      throw "File $FileName was not found in $FilesPath"
    }

    Copy-Item $FileInfo.FullName $Path
    WriteText "$($FileInfo.FullName) [was copied to ==>] $Path"
  }
}

# Adds folder with SharePoint reference libraries to Registry to be found by MSBuild
function AddSharePoint14ReferencePathToRegistry() {
  $RegistryValue = $SharePoint14ReferenceAssemblyPath + "\"
  if (TestWindows64) {
    $RegistryPath = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v2.0.50727\AssemblyFoldersEx\SharePoint14"
  } else {
    $RegistryPath = "HKLM:\SOFTWARE\Microsoft\.NETFramework\v2.0.50727\AssemblyFoldersEx\SharePoint14"
  }
  # Do not set value if it is already exists and has the same value
  if (-Not (Test-Path $RegistryPath))
  {
      WriteText "Writing new registry key '$RegistryPath' as '$RegistryValue'"
    $null = New-Item -ItemType String $RegistryPath -Value $RegistryValue
  }
  else 
  {
      if (-Not ((Get-ItemProperty $RegistryPath)."(default)" -Eq $RegistryValue))
      {
          WriteText "Updating registry key '$RegistryPath' to '$RegistryValue'"
          Set-Item -Path $RegistryPath -Value $RegistryValue
      }
      else
      {
          WriteText "Registry key '$RegistryPath' ignored as it already has the correct value of '$RegistryValue'"
      }
  }
}

# Returns full path for the .Net 4.0 framework installation
function GetDotNet40Directory() {
  # Find a folder in Microsoft.NET that starts with 'v4.0'
  $DirectoryInfo = Get-ChildItem (Join-Path $Env:SystemRoot "Microsoft.NET\Framework") `
    | Where { $_ -is [System.IO.DirectoryInfo] } `
    | Where { $_.Name -like 'v4.0*' }

  if ($DirectoryInfo -eq $null) {
    throw ".Net 4.0 is not found on this machine."
  }

  return $DirectoryInfo
}

# Returns version of installed .Net 4.0 framework
function GetDotNet40Version() {
  return (GetDotNet40Directory).Name
}

# Returns $true if this is 64-bit version of Windows
function TestWindows64() {
  return (Get-WmiObject Win32_OperatingSystem | Select OSArchitecture).OSArchitecture.StartsWith("64")
}

# Returns $true if machine has SharePoint 14 installed
function TestSharePoint14() {
  return Test-Path "HKLM:\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\14.0"
}

# Changes .Net framework version in gacutil.exe.config to match current .Net framework version.
# It is required for gacutil.exe to work.
function FixGacUtilConfigVersion() {
  $ConfigFilePath = Join-Path $FilesPath $GacUtilConfigFileName
  if (-not (Test-Path $ConfigFilePath)) {
    throw "File $ConfigFilePath was not found"
  }

  $DotNet40Version = GetDotNet40Version
  $ConfigDotNet40Version = GetGacUtilConfigVersion
  if ($DotNet40Version -ne $ConfigDotNet40Version) {
    # Create a backup file
    $ConfigBackupFilePath = $ConfigFilePath + ".bak"
    Copy-Item $ConfigFilePath $ConfigBackupFilePath

    # Make the file writable
    $ConfigFileInfo = Get-ChildItem $ConfigFilePath
    $ConfigFileInfo.IsReadOnly = $false

    # Read the file content, change the version and override the original file.
    # Need a variable for content to avoid simultaneous read-write of the same file.
    $ConfigFileContent = Get-Content $ConfigFilePath
    $null = $ConfigFileContent `
      | ForEach { $_ -replace '"v4\.0.*?"', """$DotNet40Version""" } `
      | Out-File $ConfigFilePath -Encoding UTF8
    WriteText ".Net version in file $ConfigFilePath changed from '$ConfigDotNet40Version' to '$(GetGacUtilConfigVersion)'"
  }
}

# Returns version of .Net Framework stored in gacutil.exe.config
function GetGacUtilConfigVersion() {
  $ConfigFilePath = Join-Path $FilesPath $GacUtilConfigFileName
  $ConfigVersionMatch = Get-Content $ConfigFilePath | Where { $_ -match '"(v4\.0.*?)"' } 
  if ($ConfigVersionMatch -eq $null) {
    throw "No version info found in file $ConfigFilePath"
  }
  return $Matches[1]
}

# Changes MSBuild .Net framework version in TfsBuildService.exe.config to 
# match current .Net framework version.
function FixTfsBuildServiceConfigMSBuildPath() {
  if (-not (Test-Path $TfsBuildServiceConfigFilePath)) {
    WriteText "$TfsBuildServiceConfigFilePath was not found."
    WriteText "This is needed only for TFS 2008."
    return
  }

  $DotNet40Directory = (GetDotNet40Directory).FullName
  $ConfigMSBuildPath = GetTfsBuildServiceConfigMSBuildPath
  if ($DotNet40Directory -ne $ConfigMSBuildPath) {
    # Create a backup file
    $TfsBuildServiceConfigBackupFilePath = $TfsBuildServiceConfigFilePath + ".bak"
    Copy-Item $TfsBuildServiceConfigFilePath $TfsBuildServiceConfigBackupFilePath

    # Make the file writable
    $ConfigFileInfo = Get-ChildItem $TfsBuildServiceConfigFilePath
    $ConfigFileInfo.IsReadOnly = $false

    # Read the file content, change the MSBuildPath and override the original file.
    # Need a variable for content to avoid simultaneous read-write of the same file.
    $ConfigFileContent = Get-Content $TfsBuildServiceConfigFilePath
    $null = $ConfigFileContent `
      | ForEach { $_ -replace 'key="MSBuildPath"\s+value=".*?"', "key=""MSBuildPath"" value=""$DotNet40Directory""" } `
      | Out-File $TfsBuildServiceConfigFilePath -Encoding UTF8
    WriteText "MSBuildPath in file $TfsBuildServiceConfigFilePath changed from '$ConfigMSBuildPath' to '$(GetTfsBuildServiceConfigMSBuildPath)'"

    WriteText ""

    Restart-Service 'VSTFBUILD'
    WriteText "'VSTFBUILD' service was restarted to apply changes in the $TfsBuildServiceConfigFileName file"
  }
}

# Returns the .Net Framework path stored in TfsBuildService.exe.config
function GetTfsBuildServiceConfigMSBuildPath() {
  $ConfigVersionMatch = Get-Content $TfsBuildServiceConfigFilePath `
    | Where { $_ -match 'key="MSBuildPath"\s+value="(.*?)"' } 

  if ($ConfigVersionMatch -eq $null) {
    throw "No MSBuildPath key was found in file $TfsBuildServiceConfigFilePath"
  }
  return $Matches[1]
}

#==============================================================================
# Main script code
#==============================================================================

InitializeScriptVariables

if ($Install) {
  InstallSharePointProjectFiles
} elseif ($Collect) {
  CollectSharePointProjectFiles
} else {
  WriteHelp
}

Tags: , ,

Jan 12 2012

Integration testing with Azure development storage

Category: .NetRory Primrose @ 17:43

I’ve been working on some classes that write data to Azure table storage. These classes of course need to be tested. Unfortunately the development fabric only spins up when you F5 an Azure project. This is a little problematic when the execution is from a unit test framework.

Some quick searching brought up this post which provides 99% of the answer. The only hiccup with this solution is that it is targeting the 1.0 version of the Azure SDK. I have updated this code to work with the 1.6 version of the SDK.

namespace MyProduct.Server.DataAccess.Azure.IntegrationTests
{
    using System;
    using System.Diagnostics;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// The <see cref="Initialization"/>
    ///   class is used to run assembly initialization work for the test assembly.
    /// </summary>
    [TestClass]
    public class Initialization
    {
        #region Setup/Teardown

        /// <summary>
        /// The assembly initialize.
        /// </summary>
        /// <param name="context">
        /// The context.
        /// </param>
        [AssemblyInitialize]
        public static void AssemblyInitialize(TestContext context)
        {
            StartAzureDevelopmentStorage();
        }

        #endregion

        #region Static Helper Methods

        /// <summary>
        /// The start azure development storage.
        /// </summary>
        private static void StartAzureDevelopmentStorage()
        {
            Int32 count = Process.GetProcessesByName("DSService").Length;

            if (count == 0)
            {
                ProcessStartInfo start = new ProcessStartInfo
                                         {
                                             Arguments = "/devstore:start",
                                             FileName = @"C:\Program Files\Windows Azure Emulator\emulator\csrun.exe"
                                         };
                Process proc = new Process
                               {
                                   StartInfo = start
                               };

                proc.Start();
                proc.WaitForExit();
            }
        }

        #endregion
    }
}

Dropping this class into the test assembly will ensure that the Azure development storage is running before the integration tests are executed.

Tags:

Nov 9 2011

Performance Testing a Load Test

Category: .NetRory Primrose @ 17:59

Running a load test in Visual Studio is a great way to put your code under stress. The problem is that once you have identified that there is a potential performance issue, how do you then narrow it down to particular methods in your code base. Using a Performance Session is great for this.

The problem is that Visual Studio does not provide out of the box support for running a performance session against a load test. There is a reasonably easy workaround though. Any other profiling tool will also work using the same technique. The trick is to get the performance session to manually profile mstest.exe as it runs the load test.

The way to set this up is as follows:

  1. Set the application to profile as C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\MSTest.exe
  2. Set the command line arguments to /testcontainer:YourLoadTest.loadtest /noisolation /noresults (replacing the filename in red with your loadtest file name)
  3. Set the working directory to the $(TargetDir) of the project that contains the load test
  4. Ensure you that you have compiled the project

image

The /noisolation switch is critical otherwise the profiler is going to profile mstest.exe as it manages the execution of the load test using different process. This means you won’t be profiling the load test, just the harness.

The /noresults switch just makes it easy to work with because otherwise you need to configure a unique results file name for each run.

When you start the profiler, it will then kick of mstest which in turn will run your load test.

image

At the end of it, you then have your data.

image

Tags:

Nov 8 2011

Movember - Please donate

Category: PersonalRory Primrose @ 18:12

It’s not often that I post something personal on this blog. Movember is an important cause so I’ll make an exception.

It's Movember and time to focus on men's health. To show my commitment, I'm donating my face to the cause by growing a moustache for the entire month of November, and need your support. My Mo will spark conversations, and no doubt generate some laughs; all in the name of raising vital awareness and funds for prostate cancer and male depression.

Why am I so passionate about men's health?
*1 in 9 men will be diagnosed with prostate cancer in their lifetime
*This year 20,000 new cases of the disease will be diagnosed
*1 in 8 men will experience depression in their lifetime

I'm asking you to support my Movember campaign by making a donation by either:
*Donating online at: http://www.movember.com/m/2443216
*Writing a cheque payable to 'Movember', referencing my Registration ID: 2443216 and mailing it to: Movember, PO Box 60, East Melbourne, VIC, 8002, Australia

If you'd like to find out more about the type of work you'd be helping to fund by supporting Movember, take a look at the Programs We Fund section on the Movember website: http://au.movember.com/about

Thank you in advance for supporting my efforts to change the face of men's health.

Rory Primrose

Tags:

Nov 7 2011

AssemblyVersion regular expression

Category: .NetRory Primrose @ 06:40

It seems that I keep having to come up with a regular expression that validates and parses a version string. I often use this for customising TFS build workflows for extracting and using application version information.

The AssemblyVersionAttribute appears to have the following rules:

  • Major version is required
  • Minor version is optional and will default to 0 if not specified
  • Minor version cannot be *
  • Build and Revision versions are optional and may be *
  • Revision number cannot be specified if the build number is *

The following is the regular expression that covers all these rules.

^
# Major version must exist
(?<VersionPart>\d+)
(
  # Minor only supports numbers
  \.(?<VersionPart>\d+)

  (\.
    (
      # Build can be *
      (?<VersionPart>\*)
      |
      # or a number
      (
        (?<VersionPart>\d+)

        # with optional Revision that may be * or numeric
        (\.(?<VersionPart>\*|\d+))?
      )
    )
  )? # Build.Revision is optional
)?$ # Minor.Build.Revision is optional

The following is the same expression without the comments and whitespace.

^(?<VersionPart>\d+)(\.(?<VersionPart>\d+)(\.((?<VersionPart>\*)|((?<VersionPart>\d+)(\.(?<VersionPart>\*|\d+))?)))?)?$

Enjoy.

Tags:

Oct 27 2011

Getting the X509Certificate from a remote HTTPS resource

Category: .NetRory Primrose @ 11:45

How do you download the X509Certificate that secures a HTTPS channel? I want to do this for two reasons.

  1. Identify the reason for a trust failure
  2. Identify if the certificate is about to expire

I ran all sorts of searches on the net about how to get the certificate of the remote host. Unfortunately the results were not quite what I was after.

The answer is that the certificate is provided by HttpWebRequest.ServicePoint.Certificate. The tricky bit is that the certificate is only available once a response has come back from the host. This makes sense, but it is misleading that the certificate based on the response is stored against the original request object. It is also a little confusing that the certificate is available on the request when the attempt to get the response threw an exception (a failed certificate trust for example).

The following code shows how this works.

HttpWebRequest webRequest = HttpWebRequest.Create(address) as HttpWebRequest;

try
{
    if (webRequest == null)
    {
        throw new InvalidOperationException("Invalid request type encoutnered");
    }

    webRequest.Method = WebRequestMethods.Http.Head;

    // At this point webRequest.ServicePoint.Certificate == null
    using (WebResponse webResponse = webRequest.GetResponse())
    {
        // At this point certificate validation has passed
        // and webRequest.ServicePoint.Certificate != null
        HttpWebResponse response = webResponse as HttpWebResponse;

        if (response != null)
        {
            responseMessage = response.StatusDescription;
            outcome = DetermineOutcome(response);
        }
        else
        {
            throw new InvalidOperationException("Invalid response type encoutnered");
        }
    }
}
catch (WebException ex)
{
    AuthenticationException authFailure = ex.InnerException as AuthenticationException;

    if (authFailure != null)
    {
        responseMessage = authFailure.Message;

        if (ex.Status == WebExceptionStatus.TrustFailure)
        {
            // At this point certificate validation has failed
            // however webRequest.ServicePoint.Certificate != null
            // TODO: Identify reason for trust failure using webRequest.ServicePoint.Certificate
        }
    } 
    else
    {
        responseMessage = ex.Message;
    }
}

Tags: