Jun 30 2009

Microsoft addresses one of my WF issues in 4.0

Category: .NetRory Primrose @ 08:32

I just read on Guy Burstein’s blog that WF 4.0 introduces a WorkflowInvoker class. This class makes it much easier to execute synchronous workflows without having to write all the plumbing code required by the WorkflowInstance class. This is really great news. See this post for the details.

Tags:

Jun 24 2009

A new job and almost a new foot

Category: PersonalRory Primrose @ 10:47

I had been chasing a job opportunity for a while as I was getting close to the time where I would like a change. The deal was completed just before I came back from a wonderful overseas holiday a couple of weeks ago.

Before starting in the new position I was able to do a handover to my colleagues in the old job. The next day had me in hospital for some planned foot surgery and then in the new job the day after that. It has been a whirlwind couple of weeks but I am already enjoying the fresh winds of change as I settle into a new company and project.

I hope to get back into some more regular blogging now that I am getting back into the groove.

Tags:

Jun 15 2009

Generating Sandcastle documentation with TeamBuild

Category: Rory Primrose @ 11:00

Automatically generating technical documentation from code comments is really easy with Sandcastle and SHFB. If you are using TeamBuild to provide continuous integration then this is a great place to ensure up to date documentation is being produced. There a two ways that Sandcastle can be used to generate documentation. The first is dynamically without a SHFB project file and the second is with a SHFB project file.

Dynamically creating documentation is an easy solution that essentially documents all dll files found in the build directory with some known exclusions. This has the advantage that you don’t need to manage the documentation configuration as assemblies are added and removed from the solution. The disadvantages is that it potentially generates documentation for more assemblies than intended, namely the dependencies for the solution. The dynamic documentation generation is really good for framework/toolkit type solutions that don’t have external dependencies. The dynamic solution tells SHFB the information that it requires that would otherwise be defined via a project file. The MSBuild script looks something like the following.

<Target Name="BuildSandcastleWithDynamicProjectDefinition">

  <!-- Uses Sandcastle Help File Builder to build a CHM documentation for all assemblies in the output directory. -->

 

  <Message Text="Setting empty DocumentationName to ProductName '$(ProductName)'" />

 

  <CreateProperty Value="$(ProductName)"

                  Condition="$(DocumentationName) == ''">

    <Output TaskParameter="Value"

            PropertyName="DocumentationName"/>

  </CreateProperty>

 

  <!-- Find all assemblies in the output directory -->

  <CreateItem Include="$(OutDir)*.dll"

              Exclude="$(OutDir)*Tests.dll;$(OutDir)*_Accessor.dll">

    <Output ItemName="Assemblies"

            TaskParameter="Include" />

  </CreateItem>

 

  <!-- Document assemblies that have corresponding XML documentation files -->

  <CreateItem Include="@(Assemblies)"

              Condition="Exists('%(Assemblies.RelativeDir)%(Assemblies.Filename).xml')">

    <Output ItemName="AssembliesToDocument"

            TaskParameter="Include" />

  </CreateItem>

 

  <!-- Include assemblies that are missing XML documentation files as dependencies -->

  <CreateItem Include="@(Assemblies)"

              Condition="!Exists('%(Assemblies.RelativeDir)%(Assemblies.Filename).xml')">

    <Output ItemName="DependenciesToDocument"

            TaskParameter="Include" />

  </CreateItem>

 

  <!-- Include all files in the \Documentation subdirectory of a project as content -->

  <CreateItem Include="$(SolutionRoot)\**\Documentation\*"

              Exclude="$(SolutionRoot)\**\Thumbs.db">

    <Output ItemName="DocumentationContent"

            TaskParameter="Include" />

  </CreateItem>

 

  <!-- Check if the assemblies for corresponding XML files exist -->

  <Error Text="Assembly not found %(AssembliesToDocument.RelativeDir)%(AssembliesToDocument.Filename).dll, but XML was."

         Condition="!Exists('%(AssembliesToDocument.RelativeDir)%(AssembliesToDocument.Filename).dll')" />

 

  <PropertyGroup>

    <SandcastleBuilderPath>$(ProgramFiles)\EWSoftware\Sandcastle Help File Builder\SandcastleBuilderConsole.exe</SandcastleBuilderPath>

    <SandcastleBuilderArguments>-new @(AssembliesToDocument -> '-assembly=&quot;%(RelativeDir)%(Filename).dll&quot;',' ') @(DependenciesToDocument -> '-dependency=&quot;%(RelativeDir)%(Filename).dll&quot;',' ') @(DocumentationContent -> '-addcontent=&quot;%(Fullpath)*.*,html\Documentation&quot;',' ') -outputpath=&quot;$(OutDir).&quot; -HelpTitle=&quot;Documentation for $(ProductName)&quot; -Language=&quot;en-AU&quot; -HtmlHelpName=&quot;$(DocumentationName)&quot; -FooterText=&quot;Version: $(VersionNumber) &lt;br /&gt; Build Number: $(BuildNumber)&quot; -KeepLogFile=&quot;false&quot; -RootNamespaceContainer=&quot;true&quot; -SyntaxFilters=&quot;CSharp&quot;</SandcastleBuilderArguments>

  </PropertyGroup>

 

  <!-- Execute sandcastle -->

  <Message Text="Running Sandcastle: &quot;$(SandcastleBuilderPath)&quot; $(SandcastleBuilderArguments)" />

  <Exec Command="&quot;$(SandcastleBuilderPath)&quot; $(SandcastleBuilderArguments)" />

 

</Target>

Dynamically creating documentation is easy, but the resultant documentation can become dirty as external dependencies appear in the build directory. This is when using a SHFB project file becomes an advantage. This allows the solution to contain the specific definition of what gets included in the Sandcastle generated documentation. The best option for managing the SHFB project file is to add it as a solution item in Visual Studio. The MSBuild script for using the SHFB project file looks like the following

<Target Name="BuildSandcastleProjectFile">

 

  <Message Text="Building Sandcastle documentation using the project '$(SandcastleProjectFilePath)'" />

 

  <Message Text="Setting empty DocumentationName to ProductName '$(ProductName)'" />

 

  <CreateProperty Value="$(ProductName)"

                  Condition="$(DocumentationName) == ''">

    <Output TaskParameter="Value"

            PropertyName="DocumentationName"/>

  </CreateProperty>

 

  <PropertyGroup>

    <SandcastleBuilderArguments>-outputpath=&quot;$(OutDir).&quot; -HelpTitle=&quot;Documentation for $(ProductName)&quot; -Language=&quot;en-AU&quot; -HtmlHelpName=&quot;$(DocumentationName)&quot; -FooterText=&quot;Build Number: $(BuildNumber)&quot; -KeepLogFile=&quot;false&quot; -RootNamespaceContainer=&quot;true&quot; -SyntaxFilters=&quot;CSharp&quot;</SandcastleBuilderArguments>

  </PropertyGroup>

 

  <!-- Execute sandcastle -->

  <Message Text="Running Sandcastle: &quot;$(SandcastleBuilderPath)&quot; &quot;$(SandcastleProjectFilePath)&quot; $(SandcastleBuilderArguments)" />

  <Exec Command="&quot;$(SandcastleBuilderPath)&quot; &quot;$(SandcastleProjectFilePath)&quot; $(SandcastleBuilderArguments)" />

 

</Target>

These targets run the implementation for generating the documentation. The following targets are used to orchestrate this work.

 

<PropertyGroup>

 

  <ProductName></ProductName>

  <!-- Enables/Disables generation of Sandcastle Documentation -->

  <BuildDocumentation>true</BuildDocumentation>

 

  <!--

  Defines the name of the Sandcastle project file to build

  If not defined, a search is run for *.SHFB files. The build fails if multiple project definitions are found.

  -->

  <SandcastleProjectFile></SandcastleProjectFile>

  <SandcastleProjectFilePath Condition="'$(SandcastleProjectFile)' != ''">$(SolutionRoot)\$(SandcastleProjectFile)</SandcastleProjectFilePath>

 

  <!-- Defines the documentation name -->

  <DocumentationName></DocumentationName>

 

  <SandcastleBuilderPath>$(ProgramFiles)\EWSoftware\Sandcastle Help File Builder\SandcastleBuilderConsole.exe</SandcastleBuilderPath>

 

  <!-- Used to store the updated version number for generating Sandcastle documentation -->

  <EmptyVersionNumber>0.0.0.0</EmptyVersionNumber>

  <VersionNumber>$(EmptyVersionNumber)</VersionNumber>

 

</PropertyGroup>

 

<Target Name="GenerateDocumentation"

        Condition="$(BuildDocumentation)">

 

  <GetBuildProperties TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

                      BuildUri="$(BuildUri)">

    <Output TaskParameter="TestSuccess"

            PropertyName="TestSuccess" />

  </GetBuildProperties>

 

  <Message Text="Skipping Sandcastle documentation generation"

           Condition="$(TestSuccess) == false" />

 

  <CallTarget Targets="FindSandcastleProjectFile"

              Condition="$(TestSuccess)" />

 

  <CallTarget Targets="GenerateSandcastleDocumentation"

              Condition="$(TestSuccess)" />

 

</Target>

 

<Target Name="FindSandcastleProjectFile"

        Condition="'$(SandcastleProjectFilePath)' == ''">

 

  <Message Text="No Sandcastle project defined. Searching %24(SolutionRoot)\**\*.SHFB" />

 

  <ItemGroup>

 

    <SandcastleProjectDefinitionFiles Include="$(SolutionRoot)\**\*.SHFB" />

 

  </ItemGroup>

 

  <Message Text="Searching failed to find a Sandcastle project file."

           Condition="'@(SandcastleProjectDefinitionFiles)' == ''" />

  <Message Text="Found the following Sandcastle project files: %0D%0A @(SandcastleProjectDefinitionFiles -> '%(FullPath)', '%0D%0A')"

           Condition="'@(SandcastleProjectDefinitionFiles)' != ''" />

 

  <StringComparison Comparison="Contains"

                    Param1="@(SandcastleProjectDefinitionFiles)"

                    Param2=";"

                    Condition="'@(SandcastleProjectDefinitionFiles)' != ''">

    <Output TaskParameter="Result"

            ItemName="MultipleSandcastleProjectsFound" />

  </StringComparison>

 

  <Error Text="Multiple Sandcastle project definitions found."

         Condition="'@(SandcastleProjectDefinitionFiles)' != '' And @(MultipleSandcastleProjectsFound) == true" />

 

  <PropertyGroup>

 

    <SandcastleProjectFilePath>@(SandcastleProjectDefinitionFiles)</SandcastleProjectFilePath>

 

  </PropertyGroup>

 

</Target>

 

<Target Name="GenerateSandcastleDocumentation">

 

  <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

             BuildUri="$(BuildUri)"

             Name="Generating Sandcastle documentation"

             Message="Generating Sandcastle documentation">

    <Output TaskParameter="Id"

            PropertyName="SandcastleBuildStepId" />

  </BuildStep>

 

  <!-- Check if builder and files exist -->

  <Error Text="Sandcastle Help File Builder is not found at $(SandcastleBuilderPath)."

         Condition="!Exists('$(SandcastleBuilderPath)')"  />

 

  <Error Text="No ProductName property value has been defined."

         Condition="'$(ProductName)' == ''" />

 

  <Error Text="Sandcastle project file '$(SandcastleProjectFilePath)' does not exist"

         Condition="'$(SandcastleProjectFilePath)' != '' And !Exists('$(SandcastleProjectFilePath)')" />

 

  <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

             BuildUri="$(BuildUri)"

             Id="$(SandcastleBuildStepId)"

             Message="Compiling Sandcastle documentation project"

             Condition="'$(SandcastleProjectFilePath)' != ''" />

 

  <CallTarget Targets="BuildSandcastleProjectFile"

              Condition="'$(SandcastleProjectFilePath)' != ''" />

 

  <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

             BuildUri="$(BuildUri)"

             Id="$(SandcastleBuildStepId)"

             Message="Compiling Sandcastle documentation without a project"

             Condition="'$(SandcastleProjectFilePath)' == ''" />

 

  <CallTarget Targets="BuildSandcastleWithDynamicProjectDefinition"

              Condition="'$(SandcastleProjectFilePath)' == ''" />

 

  <BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

             BuildUri="$(BuildUri)"

             Id="$(SandcastleBuildStepId)"

             Status="Succeeded" />

 

</Target>

Tags: ,

Jun 1 2009

GhostDoc has been acquired by SubMain

Category: Applications | .NetRory Primrose @ 21:26

Just like Reflector, SubMain has just acquired GhostDoc and has released an updated version. Looks like SubMain has already put in a decent amount of effort for GhostDoc. This is appears to be significantly different in contrast to what Red Gate has published for Reflector. Looks like it will be interesting times ahead for these tools.

Tags:

Jun 1 2009

How to stream data in WCF service operations

Category: .NetRory Primrose @ 20:09

Bruce Zhang has put together a great post about stream operations in WCF.

I have never liked the limitations imposed with streaming in WCF although I do agree with the design. The biggest issue for me is that WCF needs to be configured for a maximum transfer size. The main problem here is that a service and a client will probably not know the maximum size of data that a service could process. To know the maximum size would require a business rule in the service design and that would not be very common for a service that handles large amounts of data via a stream.I prefer a chunking service design as you get around all of these limitations with the one disadvantage of creating a chatty service. This introduces a little overhead but I think it is worth it for the benefit of avoiding WCF streaming restrictions.

Tags: ,

May 24 2009

Canadian signage

Category: PersonalRory Primrose @ 09:20

It’s been a while since I came across some signs worthy of posting since this and this but there are some good ones in Canada.

20090523-072426

20090523-091846

Tags:

May 4 2009

Removing TeamBuild report noise

Category: .NetRory Primrose @ 10:16

A patch came out in January which fixes the noise issue in TeamBuild reports. Build reports now only display one build step per project build by the solution after this patch is applied.

Tags: ,

May 4 2009

Injecting a version number into WiX via TeamBuild

Category: .NetRory Primrose @ 05:32

This has been an interesting nut to crack. There are several published ways to inject a version number into WiX as part of a TeamBuild process. This is the way that I have found has the best results.

The following target overrides the ResolveSolutionPathsForEndToEndIteration MSBuild target to inject a $(VersionNumber) value if a custom version number has been defined or calculated in the build script. The default implementation of ResolveSolutionPathsForEndToEndIteration is called if no version number has been defined or calculated.

<?xml version="1.0" encoding="utf-8"?>

<Project xmlns="http://schemas.Microsoft.com/developer/msbuild/2003">

 

  <PropertyGroup>

 

    <EmptyVersionNumber>0.0.0.0</EmptyVersionNumber>

    <VersionNumber>$(EmptyVersionNumber)</VersionNumber>

 

  </PropertyGroup>

 

  <!-- ResolveSolutionPathsForEndToEndIteration

 

    Override ResolveSolutionPathsForEndToEndIteration to inject VersionNumber property through to Wix Projects

 

  -->

  <Target Name="ResolveSolutionPathsForEndToEndIteration"

          Condition=" '$(IsDesktopBuild)' != 'true' ">

 

    <CallTarget Condition="'$(VersionNumber)' != '$(EmptyVersionNumber)'"

                Targets="ResolveSolutionPathsForEndToEndIterationWithVersionNumber" />

 

    <CallTarget Condition="'$(VersionNumber)' == '$(EmptyVersionNumber)'"

                Targets="ResolveSolutionPathsForEndToEndIterationDefault" />

 

  </Target>

 

  <!-- ResolveSolutionPathsForEndToEndIterationWithVersionNumber

 

    Extends the default ResolveSolutionPathsForEndToEndIteration implementation to inject

    $(VersionNumber) into the solution configuration prior to it being built

 

  -->

  <Target Name="ResolveSolutionPathsForEndToEndIterationWithVersionNumber">

 

    <!-- Ensure SolutionFile property has been set -->

    <Error Condition="'$(SolutionFile)'==''"

           Text="SolutionFile property must be set" />

 

    <Message Text="Reconfiguring solution to accept VersionNumber property. Version Number is $(VersionNumber)" />

 

    <CreateItem

      Include="$(BuildProjectFolderPath)/../../$(SolutionFile)"

      AdditionalMetadata="Properties=VersionNumber=$(VersionNumber);">

      <Output

        TaskParameter="Include"

        ItemName="MySolutionToBuild"/>

    </CreateItem>

 

    <WorkspaceItemConverterTask

      Condition=" '@(MySolutionToBuild)' != '' "

      TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

      BuildUri="$(BuildUri)"

      WorkspaceName="$(WorkspaceName)"

      WorkspaceOwner="$(WorkspaceOwner)"

      ServerItems="@(MySolutionToBuild)">

      <Output TaskParameter="LocalItems"

              ItemName="LocalSolutionToBuild" />

    </WorkspaceItemConverterTask>

 

    <WorkspaceItemConverterTask

      Condition=" '@(SolutionToPublish)' != '' "

      TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

      BuildUri="$(BuildUri)"

      WorkspaceName="$(WorkspaceName)"

      WorkspaceOwner="$(WorkspaceOwner)"

      ServerItems="@(SolutionToPublish)">

      <Output TaskParameter="LocalItems"

              ItemName="LocalSolutionToPublish" />

    </WorkspaceItemConverterTask>

 

  </Target>

 

  <!-- ResolveSolutionPathsForEndToEndIterationDefault

 

    Runs the default ResolveSolutionPathsForEndToEndIteration implementation

    This is done when no VersionNumber has been defined

 

  -->

  <Target Name="ResolveSolutionPathsForEndToEndIterationDefault">

 

    <WorkspaceItemConverterTask

      Condition=" '@(SolutionToBuild)' != '' "

      TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

      BuildUri="$(BuildUri)"

      WorkspaceName="$(WorkspaceName)"

      WorkspaceOwner="$(WorkspaceOwner)"

      ServerItems="@(SolutionToBuild)">

      <Output TaskParameter="LocalItems"

              ItemName="LocalSolutionToBuild" />

    </WorkspaceItemConverterTask>

 

    <WorkspaceItemConverterTask

      Condition=" '@(SolutionToPublish)' != '' "

      TeamFoundationServerUrl="$(TeamFoundationServerUrl)"

      BuildUri="$(BuildUri)"

      WorkspaceName="$(WorkspaceName)"

      WorkspaceOwner="$(WorkspaceOwner)"

      ServerItems="@(SolutionToPublish)">

      <Output TaskParameter="LocalItems"

              ItemName="LocalSolutionToPublish" />

    </WorkspaceItemConverterTask>

 

  </Target>

 

</Project>

The only disadvantage of this solution is that the WiX project needs to be manually updated to include the VersionNumber property. Once this is done, the VersionNumber needs to be defined in the DefineConstants property which is available in the project properties GUI. For example:

<Project DefaultTargets="Build"

         xmlns="http://schemas.Microsoft.com/developer/msbuild/2003">

  <PropertyGroup>

    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>

    <ProductVersion>3.0</ProductVersion>

    <ProjectGuid>{1169cfc1-c022-40ec-b4b0-665acc3d45f2}</ProjectGuid>

    <SchemaVersion>2.0</SchemaVersion>

    <OutputName>MyProject.Deployment</OutputName>

    <OutputType>Package</OutputType>

    <WixTargetsPath Condition=" '$(WixTargetsPath)' == '' ">$(MSBuildExtensionsPath)\Microsoft\WiX\v3.0\Wix.targets</WixTargetsPath>

    <WixToolPath>$(ProgramFiles)\Windows Installer XML v3\bin\</WixToolPath>

    <SccProjectName>SAK</SccProjectName>

    <SccProvider>SAK</SccProvider>

    <SccAuxPath>SAK</SccAuxPath>

    <SccLocalPath>SAK</SccLocalPath>

    <Name>MyProject.Deployment</Name>

    <RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>

    <VersionNumber>0.0.0.0</VersionNumber>

  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">

    <DefineConstants>Debug;VersionNumber=$(VersionNumber)</DefineConstants>

  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">

    <DefineConstants>Debug;VersionNumber=$(VersionNumber)</DefineConstants>

  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

    <OutputPath>bin\Debug\</OutputPath>

    <IntermediateOutputPath>obj\Debug\</IntermediateOutputPath>

    <DefineConstants>Debug</DefineConstants>

    <SuppressIces>

    </SuppressIces>

    <SuppressValidation>True</SuppressValidation>

    <CompilerAdditionalOptions>

    </CompilerAdditionalOptions>

    <LinkerAdditionalOptions>

    </LinkerAdditionalOptions>

  </PropertyGroup>

  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">

    <OutputPath>bin\Release\</OutputPath>

    <IntermediateOutputPath>obj\Release\</IntermediateOutputPath>

    <SuppressIces>

    </SuppressIces>

    <SuppressValidation>True</SuppressValidation>

    <CompilerAdditionalOptions>

    </CompilerAdditionalOptions>

    <LinkerAdditionalOptions>

    </LinkerAdditionalOptions>

    <Cultures>

    </Cultures>

    <DefineConstants>Debug</DefineConstants>

    <LeaveTemporaryFiles>False</LeaveTemporaryFiles>

    <LibBindFiles>False</LibBindFiles>

    <SuppressPdbOutput>False</SuppressPdbOutput>

    <SuppressSpecificWarnings>

    </SuppressSpecificWarnings>

    <TreatWarningsAsErrors>True</TreatWarningsAsErrors>

    <VerboseOutput>False</VerboseOutput>

    <WixVariables>

    </WixVariables>

    <SuppressAllWarnings>False</SuppressAllWarnings>

    <Pedantic>False</Pedantic>

  </PropertyGroup>

</Project>

This solution makes the version number available to the WiX script.

<?xml version="1.0" encoding="utf-8"?>

<?define COMPANY = "MyCompany" ?>

<?define PRODUCTNAME = "MyProject" ?>

<?define PRODUCTNAME_SHORT = "MyProject" ?>

<Wix xmlns="http://schemas.Microsoft.com/wix/2006/wi"

     xmlns:iis="http://schemas.Microsoft.com/wix/IIsExtension"

     xmlns:util="http://schemas.Microsoft.com/wix/UtilExtension">

  <Product Id="9e97adf1-8bd1-4b2b-a016-7fceecd408e1"

           Name="$(var.PRODUCTNAME)"

           Language="1033"

           Version="$(var.VersionNumber)"

           Manufacturer="$(var.COMPANY)"

           UpgradeCode="85a0e6ea-87b4-4f75-8a2d-21c2b78fa773">

    <Package InstallerVersion="200"

             Compressed="yes"

             Comments="$(var.VersionNumber)" />

  </Product>

</Wix>

This method allows the build script to pass a version number into WiX as a build property. A better solution to this is to extract the version number from the AssemblyInfo.Community Server from a project in the solution rather than manually providing the version number as a build property. Ideally the projects should reference a common ProductInfo.Community Server that contains the version number to apply to all projects in the solution. This process can then be extended so that the version number in this common file is updated by the build script before the solution is built.

Tags: ,

Apr 30 2009

Getting InnerXML using XPath

Category: .NetRory Primrose @ 08:33

I previously posted about XML comments and the include element for documenting code in Visual Studio. I commented against that post that xpath queries should end in /*. This tells the query to select all child elements of the xml node identified. Unfortunately this does not cover all scenarios where external xml can be included in xml documentation.

Take the following xml for example.

<?xml version="1.0" encoding="utf-8"?>

<CommonDocumentation>

  <Remarks>

    <Remark name="FirstRemark">

      <para>

        This is a remark in a paragraph.

      </para>

    </Remark>

    <Remark name="SecondRemark">

        This is a remark with just text.

    </Remark>

    <Remark name="ThirdRemark">

      This is a remark with text and <b>xml elements</b>.

    </Remark>

  </Remarks>

</CommonDocumentation>

This xml is referenced by the include statements in the code below.

using System.Diagnostics;

 

namespace ClassLibrary1

{

    public class Class1

    {

        /// <summary>

        /// Does something random.

        /// </summary>

        /// <remarks>

        /// <include file="CommonDocumentation.xml" path="CommonDocumentation/Remarks/Remark[@name='FirstRemark']/*" />

        /// <include file="CommonDocumentation.xml" path="CommonDocumentation/Remarks/Remark[@name='SecondRemark']/text()" />

        /// <include file="CommonDocumentation.xml" path="CommonDocumentation/Remarks/Remark[@name='ThirdRemark']/node()" />

        /// </remarks>

        public void SomeRandomMethod()

        {

            Debug.WriteLine("Do something.");   

        }

    }

}

The code indicates the way in which the xpath queries will be able to correctly get the inner xml of the common documentation. Using a /* query against the SecondRemark or ThirdRemark nodes will produce incorrect results as the text will be ignored because /* only select element nodes. Using /text() against the FirstRemark and ThirdRemark nodes will also produce incorrect values as it will only select text nodes and ignore the element nodes.

The best solution is to use xpath queries that end in /node(). This will successfully select the inner xml of the target node regardless of the makeup of that xml. The xml can contain text, element nodes or a mixture.

Tags:

Apr 25 2009

Mocking a factory created instance to test its consumer

Category: .NetRory Primrose @ 19:26

I have faced a bit of a curly one tonight with a class that I want to unit test. Part of its implementation is to use an instance that is created from a factory class. To adequately test the class, I need to get the factory to return a mock. The factory class is a closed design so this is difficult to achieve. The factory does however use a configuration value to help it determine the concrete type to create.

The difficulty with this scenario is that Type.GetType (using the configuration value) isn’t able to load the mock type directly. The solution to this problem is to use a wrapper around the mock. In doing this, the type loaded by Type.GetType is a statically defined type that happens to hold a reference to the mocked object to which it simply forwards on the required calls.

Here is a slimmed down example.

using System;

using System.Configuration;

using Microsoft.VisualStudio.TestTools.UnitTesting;

using Rhino.Mocks;

 

namespace TestProject1

{

    public class ClassToTest

    {

        public void SomethingToTest()

        {

            IDependency dependency = DependencyFactory.Create();

 

            if (string.IsNullOrEmpty(dependency.GetValue()))

            {

                throw new InvalidOperationException();

            }

        }

    }

 

    public interface IDependency

    {

        String GetValue();

    }

 

    public static class DependencyFactory

    {

        public const string DependencyTypeConfigurationKey = "DependencyType";

 

        public static IDependency Create()

        {

            Type dependencyType = Type.GetType(ConfigurationManager.AppSettings[DependencyTypeConfigurationKey]);

            return (IDependency)Activator.CreateInstance(dependencyType);

        }

    }

 

    [TestClass]

    public class UnitTest1

    {

        /// <summary>

        /// Runs test for something to test throws exception when dependency returns an empty value.

        /// </summary>

        [TestMethod]

        [ExpectedException(typeof(InvalidOperationException))]

        public void SomethingToTestThrowsExceptionWhenDependencyReturnsAnEmptyValueTest()

        {

            ConfigurationManager.AppSettings[DependencyFactory.DependencyTypeConfigurationKey] =

                typeof(DependencyMockWrapper).AssemblyQualifiedName;

 

            MockRepository mock = new MockRepository();

            IDependency dependency = mock.CreateMock<IDependency>();

 

            using (mock.Record())

            {

                dependency.GetValue();

                LastCall.Return(String.Empty);

            }

 

            ClassToTest target = new ClassToTest();

 

            using (mock.Playback())

            {

                DependencyMockWrapper.MockInstance = dependency;

                target.SomethingToTest();

            }

        }

    }

 

    public class DependencyMockWrapper : IDependency

    {

        public string GetValue()

        {

            return MockInstance.GetValue();

        }

 

        public static IDependency MockInstance

        {

            get;

            set;

        }

    }

}

The ClassToTest is, as the name suggests, the class being tested. It makes a call to DependencyFactory to create an instance of something for it to use. Ideally we want this to be the mock, however we need to settle for a static wrapper around a mock. The DependencyFactory consults configuration to determine the type to create and it instantiates the type and returns it.

The unit test uses the DependencyMockWrapper class so that it can be loaded at runtime via Type.GetType, but also allows the unit test to inject the mock into the wrapper before it is used. The unit test sets up configuration to point to the wrapper, creates the mock with its expectations, and injects the mock into the wrapper. The key here is that the MockInstance property on the wrapper must be a static as the factory returns a new instance which the unit test can’t control and it still needs a reference to the mock.

As the class under test invokes the dependency created from the factory, the dependency (being the wrapper) simply forwards the call on to the mock. This satisfies the expectations set up when recording the mock.

Tags: