Tutorial
The instructions on this page are based on sample code that demonstrates how to define code examples, check for their successful execution, and generate text files containing both the examples' source code and their console outputs. Finally, it is showed how such text files can be exploited as the source of a code element in XML comments documenting a sample library.
The sample describing the intended work-flow for documenting by code examples is located here. It includes what follows.
Project SampleClassLibrary, containing some code to be exemplified. The code is documented through XML comments including code examples generated via the Novacta.Documentation.CodeExamples library.
Project SampleClassLibrary.CodeExamples, a console application in which the code examples are defined. Note that this project references both SampleClassLibrary and Novacta.Documentation.CodeExamples.
Project SampleClassLibrary.Documentation, that generates the documentation for SampleClassLibrary exploiting the code examples defined in SampleClassLibrary.CodeExamples. This last project can be successfully loaded by installing the Sandcastle Help File Builder.
Develop the code to be documented by examples
In this tutorial, project SampleClassLibrary contains the code that needs to be documented by examples.
You can exemplify both synchronous and asynchronous operations.
For instance, the sample library includes class IntegerArrayOperation. The source code of this type is the following.
using System;
using System.Threading.Tasks;
namespace SampleClassLibrary.Advanced
{
/// <summary>
/// Provides a method to operate on arrays of integers.
/// </summary>
public static class IntegerArrayOperation
{
/// <summary>
/// Applies the specified function to the given array of operands.
/// </summary>
/// <param name="funcs">The functions to evaluate at each operand.</param>
/// <param name="operands">The array of operands.</param>
/// <returns>The results of the operations.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="funcs"/> is <b>null</b>.<br/>
/// -or-<br/>
/// <paramref name="operands"/> is <b>null</b>.
/// </exception>
/// <example>
/// <para>
/// In the following example, integers in a given array are both squared and
/// doubled by executing the
/// <see cref="Operate(Func{int, int}[], int[])"/> method.
/// In addition, input validation is also checked.
/// </para>
/// <para>
/// <code
/// language="cs"
/// source="..\..\samples\SampleClassLibrary.CodeExamples\Advanced\IntegerArrayOperationExample.cs.txt"/>
/// </para>
/// </example>
public static async Task<int[][]> Operate(Func<int, int>[] funcs, int[] operands)
{
if (funcs == null)
{
throw new ArgumentNullException(nameof(funcs));
}
if (operands == null)
{
throw new ArgumentNullException(nameof(operands));
}
int[][] results = new int[funcs.Length][];
Task<int[]>[] tasks = new Task<int[]>[funcs.Length];
for (int j = 0; j < funcs.Length; j++)
{
int jLocal = j; // capture a copy for this iteration
results[jLocal] = new int[operands.Length];
tasks[jLocal] = new Task<int[]>(() =>
{
for (int i = 0; i < operands.Length; i++)
{
results[jLocal][i] = IntegerOperation.Operate(funcs[jLocal], operands[i]);
}
return results[jLocal];
});
}
foreach (var task in tasks)
{
task.Start();
}
await Task.WhenAll(tasks);
return results;
}
}
}Here the asynchronous method Operate(FuncInt32, Int32, Int32) is documented through XML comments. Such comments include a code example, defined through the following XML element.
<code
language="cs"
source="..\..\samples\SampleClassLibrary.CodeExamples\Advanced\IntegerArrayOperationExample.cs.txt"/>Note how its source attribute is set to the path of a text file. In subsequent steps, you will learn how to generate such kind of files via the Novacta.Documentation.CodeExamples library.
Similarly, the synchronous method Operate(FuncInt32, Int32, Int32) is documented through XML comments that includes an additional code example:
using System;
namespace SampleClassLibrary
{
/// <summary>
/// Provides a method to operate on integers.
/// </summary>
public static class IntegerOperation
{
/// <summary>
/// Applies the specified function to the given operand.
/// </summary>
/// <param name="func">The function.</param>
/// <param name="operand">The operand.</param>
/// <returns>The result of the operation.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="func"/> is <b>null</b>.</exception>
/// <example>
/// <para>
/// In the following example, an integer is squared
/// executing the <see cref="Operate(Func{int, int}, int)"/> method.
/// In addition, input validation is also checked.
/// </para>
/// <para>
/// <code
/// language="cs"
/// source="..\..\samples\SampleClassLibrary.CodeExamples\IntegerOperationExample.cs.txt"/>
/// </para>
/// </example>
public static int Operate(Func<int, int> func, int operand)
{
if (func==null)
{
throw new ArgumentNullException(nameof(func));
}
return func(operand);
}
}
}Create a console application and define your code examples
Code examples must be defined in a .NET 10 console application. In this tutorial, such application is represented by project SampleClassLibrary.CodeExamples.
Add to your console project a reference to the Novacta.Documentation.CodeExamples NuGet package, and a reference to the project containing the code to be exemplified. In this tutorial, this is SampleClassLibrary.
Define your asynchronous code examples
To create a new example for asynchronous operations, define a class that implements the IAsyncCodeExample interface. This is equivalent to implement method Main, that has no parameters and returns Task.
Inside the body of such method, add the code you want exemplified. Use class SystemConsole to output the desired content for the example.
For instance, the following class define an example for type SampleClassLibrary.AdvancedIntegerArrayOperation.
using System;
using System.Threading.Tasks;
using Novacta.Documentation.CodeExamples;
using SampleClassLibrary.Advanced;
namespace SampleClassLibrary.CodeExamples.Advanced
{
/// <summary>
/// An example showing how to exploit class <see cref="IntegerArrayOperation"/>.
/// </summary>
public class IntegerArrayOperationExample : IAsyncCodeExample
{
/// <summary>
/// The method encapsulating the code to be exemplified.
/// </summary>
public async Task Main()
{
// Define an operator that squares its operand
Func<int, int> square = (int operand) => operand * operand;
// Define an operator that doubles its operand
Func<int, int> doubleIt = (int operand) => 2 * operand;
// Define an array of operands
int[] operands = new int[3] { 2, 4, 8 };
// Operate on it
int[][] results = await IntegerArrayOperation.Operate([square, doubleIt], operands);
// Show results
for (int j = 0; j < results.Length; j++)
{
for (int i = 0; i < results[j].Length; i++)
{
Console.WriteLine(
"The result of {0} {1} is {2}.",
j == 0 ? "squaring" : "doubling",
operands[i],
results[j][i]);
}
}
// Check that an operator cannot be null
try
{
await IntegerArrayOperation.Operate(null, new int[1]);
}
catch (Exception e)
{
Console.WriteLine();
Console.WriteLine("Cannot apply a null function:");
Console.WriteLine(e.Message);
}
// Check that an array of operands cannot be null
try
{
await IntegerArrayOperation.Operate([square, doubleIt], null);
}
catch (Exception e)
{
Console.WriteLine();
Console.WriteLine("Cannot apply a function to a null array:");
Console.WriteLine(e.Message);
}
}
}
}When the console application is executed, the console output of such example will be automatically captured and inserted, as a comment, in a text file also containing the example's source code, as follows.
using System;
using System.Threading.Tasks;
using SampleClassLibrary.Advanced;
namespace SampleClassLibrary.CodeExamples.Advanced
{
/// <summary>
/// An example showing how to exploit class <see cref="IntegerArrayOperation"/>.
/// </summary>
public class IntegerArrayOperationExample
{
/// <summary>
/// The method encapsulating the code to be exemplified.
/// </summary>
public async Task Main()
{
// Define an operator that squares its operand
Func<int, int> square = (int operand) => operand * operand;
// Define an operator that doubles its operand
Func<int, int> doubleIt = (int operand) => 2 * operand;
// Define an array of operands
int[] operands = new int[3] { 2, 4, 8 };
// Operate on it
int[][] results = await IntegerArrayOperation.Operate([square, doubleIt], operands);
// Show results
for (int j = 0; j < results.Length; j++)
{
for (int i = 0; i < results[j].Length; i++)
{
Console.WriteLine(
"The result of {0} {1} is {2}.",
j == 0 ? "squaring" : "doubling",
operands[i],
results[j][i]);
}
}
// Check that an operator cannot be null
try
{
await IntegerArrayOperation.Operate(null, new int[1]);
}
catch (Exception e)
{
Console.WriteLine();
Console.WriteLine("Cannot apply a null function:");
Console.WriteLine(e.Message);
}
// Check that an array of operands cannot be null
try
{
await IntegerArrayOperation.Operate([square, doubleIt], null);
}
catch (Exception e)
{
Console.WriteLine();
Console.WriteLine("Cannot apply a function to a null array:");
Console.WriteLine(e.Message);
}
}
}
}
// Executing method Main() produces the following output:
//
// The result of squaring 2 is 4.
// The result of squaring 4 is 16.
// The result of squaring 8 is 64.
// The result of doubling 2 is 4.
// The result of doubling 4 is 8.
// The result of doubling 8 is 16.
//
// Cannot apply a null function:
// Value cannot be null. (Parameter 'funcs')
//
// Cannot apply a function to a null array:
// Value cannot be null. (Parameter 'operands')This is the text file exploited as the source of the <code> element documenting method Operate(FuncInt32, Int32, Int32). Note how the references to the IAsyncCodeExample interface and the Novacta.Documentation.CodeExamples namespace are automatically deleted from the file.
Define your synchronous code examples
Create a new synchronous example by defining a class that implements the ICodeExample interface. In practice, this means implementing the parameterless Main, method, which returns void.
In the method body, add the code you want to demonstrate. Use the SystemConsole to produce the example’s desired output.
For instance, the following class define an example for type SampleClassLibraryIntegerOperation.
using System;
using Novacta.Documentation.CodeExamples;
namespace SampleClassLibrary.CodeExamples
{
/// <summary>
/// An example showing how to exploit class <see cref="IntegerOperation"/>.
/// </summary>
public class IntegerOperationExample : ICodeExample
{
/// <summary>
/// The method encapsulating the code to be exemplified.
/// </summary>
public void Main()
{
// Define an operator that squares its operand
Func<int, int> square = (int operand) => operand * operand;
// Define an operand
int integer = 2;
// Operate on it
Console.WriteLine("Squaring {0}...", integer);
int result = IntegerOperation.Operate(square, integer);
Console.WriteLine("...the result is {0}.", result);
// Check that an operator cannot be null
try
{
IntegerOperation.Operate(null, 0);
}
catch (Exception e)
{
Console.WriteLine();
Console.WriteLine("Cannot apply a null function:");
Console.WriteLine(e.Message);
}
}
}
}When the console application runs, the example’s console output is automatically captured and inserted as a comment in the text file that contains the example’s source code, as shown below.
using System;
namespace SampleClassLibrary.CodeExamples
{
/// <summary>
/// An example showing how to exploit class <see cref="IntegerOperation"/>.
/// </summary>
public class IntegerOperationExample
{
/// <summary>
/// The method encapsulating the code to be exemplified.
/// </summary>
public void Main()
{
// Define an operator that squares its operand
Func<int, int> square = (int operand) => operand * operand;
// Define an operand
int integer = 2;
// Operate on it
Console.WriteLine("Squaring {0}...", integer);
int result = IntegerOperation.Operate(square, integer);
Console.WriteLine("...the result is {0}.", result);
// Check that an operator cannot be null
try
{
IntegerOperation.Operate(null, 0);
}
catch (Exception e)
{
Console.WriteLine();
Console.WriteLine("Cannot apply a null function:");
Console.WriteLine(e.Message);
}
}
}
}
// Executing method Main() produces the following output:
//
// Squaring 2...
// ...the result is 4.
//
// Cannot apply a null function:
// Value cannot be null. (Parameter 'func')This is the text file exploited as the source of the <code> element documenting method Operate(FuncInt32, Int32, Int32). References to the ICodeExample interface and the Novacta.Documentation.CodeExamples namespace are automatically deleted from the file.
Execute code examples
In your Program class, the implementation of method Main should resemble what follows.
using Novacta.Documentation.CodeExamples;
using System;
using System.Threading.Tasks;
namespace SampleClassLibrary.CodeExamples
{
class Program
{
static async Task Main(string[] args)
{
string codeBase = @"..\..\..\..\SampleClassLibrary.CodeExamples";
string defaultNamespace = "SampleClassLibrary.CodeExamples";
var analyzer = new CodeExamplesAnalyzer(
codeBase,
defaultNamespace);
// Analyze synchronous and asynchronous code examples
analyzer.Run();
await analyzer.RunAsync();
Console.ReadKey();
}
}
}Such code instantiates an object of type CodeExamplesAnalyzer by passing to the constructor two pieces of information: the path of the folder containing the examples' source code files, and the default namespace of the console application where the examples are defined: these are returned by properties CodeBase and DefaultNamespace, respectively. Finally, two methods are called on the analyzer object.
Method Run(Boolean, Assembly) will look for synchronous code examples, execute them individually, and capture their console outputs. It will also search for the corresponding source code files, assuming that the following conditions hold true.
Each type implementing ICodeExample is defined in a C# file named from the type name.
Types in the DefaultNamespace have their source code files stored in the CodeBase folder.
Source code files of types in nested namespaces are stored in a directory tree under the CodeBase folder, reflecting the project namespace hierarchy.
If a code example is executed successfully and its source code can be found, a text file is created that can be used as the source of <code> XML elements in C# documentation comments.
Analogously, method RunAsync(Boolean, Assembly) will search for asynchronous code examples, i.e. those implementing the IAsyncCodeExample, interface, and will execute them individually, capturing their console outputs and looking for the corresponding source code files.
When the application is executed, its console output shows a report in which the examples are listed, signaling if any source code file cannot be found, or any execution is not successful. For instance, by executing the sample application defined in project SampleClassLibrary.CodeExamples, the following is reported.
