sandcastle

Donald Knuth wrote about literate programming years ago. Literate programming is about keeping the documentation near to the implementation. Over years, several products like doxygen are helping to solve this kind of problem. There exists a product named sandcastle for .NET based code. It is able to generate MSDN like documentation.

At first you have to download the latest version of sandcastle. After creating a .shfbproj file, the XML documentation file check box in the output section of the project build configuration has to be enabled.

convenience batch files

For convenience you can add some batch files to the solution root.

make debug build

REM --
REM -- This script builds the solution for the debug configuration
REM --

call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\VsDevCmd.bat"
msbuild SandCastleTest.sln /p:Configuration=Debug

IF "%1" == "--noPause" goto END

pause

:END

remove generated files

REM --
REM -- This script deletes all obj / bin folders from solution
REM --

FOR /F "tokens=*" %%G IN ('DIR /B /AD /S obj') DO RMDIR /S /Q "%%G"
FOR /F "tokens=*" %%G IN ('DIR /B /AD /S bin') DO RMDIR /S /Q "%%G"

REM -- delete generated folders from solution root directory
RMDIR /S /Q Help

generate help files and start local http server

@ECHO OFF
REM --
REM -- build documentation
REM --

CALL deepClean.bat
CALL make_debugBuild.bat --noPause

REM -- build documentation
CALL "C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\VsDevCmd.bat"
msbuild SandCastleTestDocumentation.shfbproj

REM -- open browser first ...
START "" http://localhost:8000
REM -- ... because the web server is blocking

cd Help

REM -- the next command creates a local web server and serves 
REM -- recursively all contents from the current folder.

REM -- this works, if python >3 is installed, 
REM -- and the path variable is up to date
python -m http.server

It is assumed, that python > 3.x is installed. You can choose an other web server, if you like.

Start

At the beginning there are a lot of warnings in the Error List. These are reminder for missing comments. When the documentation is build at this point, it will be very empty. Now is the time fill some contents.

Lets start with a simple [example][sandcastleTestBase].

A printable document is described by an interface.

namespace SandcastleTest.Base
{
    /// <summary>
    /// A printable document
    /// </summary>
    public interface IPrintable
    {
        /// <summary>
        /// Prints a <see cref="IPrintable"/>
        /// </summary>
        void Print();
    }
}

The <see> tag references a type within a documentation. If the cref attribute value is misspelled, you get a compiler warning.

If you apply the IPrintable interface to a Document,

using System;
using System.Text;

namespace SandcastleTest.Base
{
    /// <inheritdoc/>
    /// <summary>
    /// A document
    /// </summary>
    public class Document : IPrintable
    {
        /// <summary>
        /// Documents title. 
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// Content of a document
        /// </summary>
        public string Content { get; set; }

        /// <inheritdoc/>
        public void Print()
        {
            Console.WriteLine(ToString());
        }

        /// <summary>
        /// Get a string representation of a <see cref="Document"/>
        /// </summary>
        /// <returns>the documents <see cref="Title"/> and the <see cref="Content"/></returns>
        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.AppendLine(Title);
            sb.AppendLine(Content);

            return sb.ToString();
        }
    }
}

you can see, the interface method Print() does not has to be documented. The documentation is inherited from the interface. The <inheritdoc> tag is used for this application.

For namespace documentation a NamespaceDoc class has to be added to the namespace.

using System.Runtime.CompilerServices;

namespace SandcastleTest.Base
{
    /// <summary>
    /// this comment appears in the namespace documentation
    /// </summary>
    [CompilerGenerated]
    class NamespaceDoc
    {
        // this class is only used for namespace documentation
    }
}

The CompilerGenerated attribute prevents sandcastle to generate a documentation for the class itself.

documenting a larger project

In a more complex examples you have a deep inheritance hierarchy. When you are coming into a mature project / product you’ll find interfaces and/or abstract classes defining base behavior or base data structures. The goal is, to reuse documentation as much as possible.

For demonstration there is a data base class PocoBase, which defines a unique Id for all data entities used in an application.

using System;

namespace SandcastleTest.Generic.POCO
{
    /// <summary>
    /// Base class for all POCOs
    /// </summary>
    public abstract class PocoBase
    {
        private Guid _Id = Guid.Empty;

        /// <summary>
        /// Id for the entity
        /// </summary>
        /// <remarks>
        /// If the <see cref="_Id"/> is <see cref="Guid.Empty"/> 
        /// a new <see cref="Guid"/> is generated.
        /// </remarks>
        public Guid Id
        {
            get
            {
                if (_Id == Guid.Empty)
                    _Id = Guid.NewGuid();

                return _Id;
            }
            set
            {
                _Id = value;
            }
        }

        /// <summary>
        /// Gets a hascode for a <see cref="PocoBase"/>
        /// </summary>
        /// <returns> a hascode for a <see cref="PocoBase"/></returns>
        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }
    }
}

The documentation part will often be a bigger part of the code. You can fold the documentation in Visual Studio if you like. Most editors offer this feature, do get a more compact view on the code if necessary.

When a data access layer uses this pocos based on PocoBase you can define some base methods for data access.

using SandcastleTest.Generic.POCO;

using System;
using System.Collections.Generic;

namespace SandcastleTest.Generic.DAL
{
    /// <summary>
    /// A base interface for all CRUD operations
    /// </summary>
    /// <typeparam name="T"><inheritdoc cref="PocoBase" select="summary"/></typeparam>
    public interface ICreateReadUpdateDelete<T> where T : PocoBase
    {
        /// <summary>
        /// Create a new <paramref name="entity"/>
        /// </summary>
        /// <param name="entity"><inheritdoc cref="PocoBase" select="summary"/></param>
        void Create(T entity);

        /// <summary>
        /// Get a list of <typeparamref name="T"/>
        /// </summary>
        /// <returns>a list of <typeparamref name="T"/></returns>
        List<T> GetList();

        /// <summary>
        /// Get an entity by <see cref="PocoBase.Id"/>
        /// </summary>
        /// <returns>an entity of type <typeparamref name="T"/></returns>
        T GetEntity(Guid id);

        /// <summary>
        /// Update an entity of <typeparamref name="T"/>
        /// </summary>
        /// <param name="entity"><inheritdoc cref="PocoBase" select="summary"/></param>
        /// <returns>if the update succeeded, this method returns true, otherwise false.</returns>
        bool Update(T entity);

        /// <summary>
        /// Deletes an entity of  <typeparamref name="T"/>
        /// </summary>
        /// <param name="entity"><inheritdoc cref="PocoBase" select="summary"/></param>
        /// <returns>if the deletion succeeded, this method returns true, otherwise false.</returns>
        bool Delete(T entity);
    }
}

The summary documentation of PocoBase is reused with the <inheritdoc cref="PocoBase" select="summary"/> statement. The interface contains a base documentation for the generic behavior of managing CRUD operations.

The next abstraction layer is a implementation of these methods. The simplest example uses the file system as a storage.

using Newtonsoft.Json;

using SandcastleTest.Generic.POCO;

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace SandcastleTest.Generic.DAL.FileSystemStorage
{
    /// <inheritdoc/>
    /// <summary>
    /// Handling CRUD actions for file system storage.
    /// </summary>
    public abstract class Crud<T> : ICreateReadUpdateDelete<T>
        where T : PocoBase
    {
        /// <summary>
        /// Gets the file name of the current entity.
        /// The filename is constructed with the poco class name and the extension .json
        /// </summary>
        private string FileName => $"{typeof(T).Name }.json";

        /// <summary>
        /// Get all contents of the file, containing all the contents of the entity.
        /// </summary>
        private string Content => File.ReadAllText(FileName);

        /// <summary>
        /// Saves a raw string representation to file system.
        /// </summary>
        /// <param name="rawContent">a raw string representation</param>
        private void Store(string rawContent) => File.WriteAllText(FileName, rawContent);

        /// <summary>
        /// Stores a <see cref="List{T}"/> to  the file system
        /// </summary>
        /// <param name="allEntities">all entities</param>
        private void Store(List<T> allEntities) => Store(JsonConvert.SerializeObject(allEntities));

        /// <inheritdoc/>
        /// <exception cref="ArgumentNullException">is thrown, when <paramref name="entity"/> is null</exception>
        public void Create(T entity)
        {
            if (entity == null)
                throw new ArgumentNullException(nameof(entity));

            var allEntities = GetList();
            if (Exists(allEntities, entity))
                return; // no update on creation

            allEntities.Add(entity);

            Store(allEntities);
        }

        /// <inheritdoc/>
        public bool Delete(T entity)
        {
            var allEntities = GetList();
            if (!(Exists(allEntities, entity)))
                return false ;

            var itemToRemove = allEntities.SingleOrDefault(x => x.Id == entity.Id);
            if (itemToRemove == null)
                return false;

            if (allEntities.Remove(itemToRemove))
            {
                Store(allEntities);
                return true;
            }

            return false;
        }

        /// <inheritdoc/>
        public T GetEntity(Guid id) => GetList().SingleOrDefault(x => x.Id == id);

        /// <inheritdoc/>
        public List<T> GetList()
        {
            try
            {
                return JsonConvert.DeserializeObject<List<T>>(Content);
            }
            catch
            {
                // maybe file does not exists...
                return new List<T>();
            }
        }

        /// <inheritdoc/>
        public bool Update(T entity)
        {
            if (entity == null)
                throw new ArgumentNullException(nameof(entity));

            var allEntities = GetList();
            if (Exists(allEntities, entity))
            {
                allEntities.Replace(entity);
                Store(allEntities);

                return true;
            }
            return false;
        }

        /// <summary>
        /// Checks, if <paramref name="allEntities" /> contains a <paramref name="entityToCheck"/>
        /// </summary>
        /// <param name="allEntities">all entities</param>
        /// <param name="entityToCheck">an entity to check</param>
        /// <returns>true, if <paramref name="allEntities" /> contains a <paramref name="entityToCheck"/></returns>
        private bool Exists(List<T> allEntities, T entityToCheck)
        {
            if (allEntities == null)
                return false;

            return allEntities.Contains(entityToCheck, new PocoBaseEqualityComparer());
        }
    }
}

This code should just work. I know this is not performance friendly. For a demonstration it is just enough.

As you can see in the generated result, a more concrete class can inherit a part of the documentation of their base class and interface. You must try to write the base documentation as reusable as possible.

If you like to store a Customer on file system, the classes can look like following.

First a kind of Person is needed. It can be assumed, that in a bigger application this class is a base class for human like entities (e.g. Customer, Employee, Manager …).

namespace SandcastleTest.Generic.POCO
{
    /// <summary>
    /// A base class with base properties for a person.
    /// </summary>
    public abstract class Person :  PocoBase
    {
        /// <summary>
        /// First name of a person
        /// </summary>
        public string FirstName { get; set; }

        /// <summary>
        /// Last name of a person
        /// </summary>
        public string LastName { get; set; }

        /// <summary>
        /// Overrides a <see cref="ToString"/> representation of an person
        /// </summary>
        /// <returns>a <see cref="ToString"/> representation of an person</returns>
        public override string ToString()
        {
            return $"Person: {FirstName} - {LastName}";
        }
    }
}

Deriving from PocoBase gives every Person an identifier. The documentation for FistName and LastName can be inherited from deriving classes.

namespace SandcastleTest.Generic.POCO
{
    /// <summary>
    /// A customer
    /// </summary>
    public class Customer : Person
    {
        /// <summary>
        /// A customer number
        /// </summary>
        public string CustomerNumber { get; set; }

        /// <summary>
        /// Overrides the <see cref="ToString"/> method
        /// </summary>
        /// <returns>a string representation of a customer</returns>
        public override string ToString()
        {
            return $"Customer: {CustomerNumber} ({base.ToString()})";
        }
    }
}

For this example the Customer has a CustomerNumber. Customer related stuff can be added here, if needed. Every thing else is derived from the base class Person. The result looks promising.

The simplest class for doing CRUD operation on a Customer for a file system is just a empty subclass of Crud<T>.

using SandcastleTest.Generic.POCO;

namespace SandcastleTest.Generic.DAL.FileSystemStorage
{
    /// <summary>
    /// Provides methods to access a <see cref="Customer"/>
    /// </summary>
    public class CustomerAccess : Crud<Customer>
    {
    }
}

The interesting thing here is the generated documentation for the above class. You can see, that most method documentation is inherited from base classes and interfaces.

You can explore more from the test code, or you can browse through the generated result.