Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ implementation demonstrating best practices for DEMA Consulting .NET libraries.
## Key Files

- **`requirements.yaml`** - All requirements with test linkage (enforced via `dotnet reqstream --enforce`)
- **`.editorconfig`** - Code style (file-scoped namespaces, 4-space indent, UTF-8+BOM, LF endings)
- **`.editorconfig`** - Code style (file-scoped namespaces, 4-space indent, UTF-8, LF endings)
- **`.cspell.json`, `.markdownlint-cli2.jsonc`, `.yamllint.yaml`** - Linting configs

## Requirements
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ This project enforces code style through `.editorconfig`. Key requirements:

- **Indentation**: 4 spaces for C#, 2 spaces for YAML/JSON/XML
- **Line Endings**: LF (Unix-style)
- **Encoding**: UTF-8 with BOM
- **Encoding**: UTF-8
- **Namespaces**: Use file-scoped namespace declarations
- **Braces**: Required for all control statements
- **Naming Conventions**:
Expand Down Expand Up @@ -142,7 +142,7 @@ Examples:

- Write tests that are clear and focused
- Use modern MSTest v4 assertions:
- `Assert.HasCount(collection, expectedCount)`
- `Assert.HasCount(expectedCount, collection)`
- `Assert.IsEmpty(collection)`
- `Assert.DoesNotContain(item, collection)`
- Always clean up resources (use `try/finally` for console redirection)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This template demonstrates:

- **Simple Library Structure**: Demo class with example methods
- **Multi-Platform Support**: Builds and runs on Windows and Linux
- **Multi-Runtime Support**: Targets .NET 8, 9, and 10
- **Multi-Runtime Support**: Targets .NET Standard 2.0, .NET 8, 9, and 10
- **MSTest V4**: Modern unit testing with MSTest framework version 4
- **Comprehensive CI/CD**: GitHub Actions workflows with quality checks and builds
- **Documentation Generation**: Automated build notes, user guide, code quality reports,
Expand Down
26 changes: 24 additions & 2 deletions docs/guide/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ Console.WriteLine(result); // Output: Hello, World!

The `DemoClass` provides demonstration functionality for the template library.

#### Constants

##### DefaultPrefix

```csharp
public const string DefaultPrefix = "Hello";
```

The greeting prefix used when no custom prefix is specified.

#### Constructors

##### DemoClass()
Expand All @@ -59,11 +69,22 @@ Initializes a new instance of the `DemoClass` class with a custom prefix.

**Parameters:**

- `prefix` (string): The prefix to use in greetings. Must not be null.
- `prefix` (string): The prefix to use in greetings. Must not be null or empty.

**Exceptions:**

- `ArgumentNullException`: Thrown when `prefix` is null.
- `ArgumentException`: Thrown when `prefix` is an empty string.

#### Properties

##### Prefix

```csharp
public string Prefix { get; }
```

Gets the greeting prefix used by this instance.

#### Methods

Expand All @@ -77,7 +98,7 @@ Returns a greeting message for the specified name.

**Parameters:**

- `name` (string): The name to greet. Must not be null.
- `name` (string): The name to greet. Must not be null or empty.

**Returns:**

Expand All @@ -86,6 +107,7 @@ A string containing the greeting message in the format "{prefix}, {name}!".
**Exceptions:**

- `ArgumentNullException`: Thrown when `name` is null.
- `ArgumentException`: Thrown when `name` is an empty string.

**Example:**

Expand Down
24 changes: 24 additions & 0 deletions requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ sections:
.NET library best practices for DEMA Consulting libraries.
tests:
- DemoMethod_ReturnsGreeting_WithDefaultPrefix
- DemoMethod_ReturnsGreeting_WithCustomPrefix

- id: TMPL-REQ-008
title: The library shall support a customizable greeting prefix.
justification: |
Consumers may need to produce greetings with a prefix other than the
default, so the library must allow the prefix to be specified at
construction time.
tests:
- DemoMethod_ReturnsGreeting_WithCustomPrefix

- id: TMPL-REQ-009
title: The library shall reject null and empty arguments with an appropriate ArgumentException.
justification: |
Null-safety and non-empty validation are fundamental API contracts: passing null or an
empty string to the constructor or to DemoMethod is a programming error. Throwing
ArgumentNullException for null arguments and ArgumentException for empty-string arguments
immediately surfaces the mistake to callers and prevents malformed output (such as
", World!" or "Hello, !") and obscure failures deeper in the call stack.
tests:
- DemoMethod_ThrowsArgumentNullException_ForNullInput
- DemoMethod_ThrowsArgumentException_ForEmptyInput
- Constructor_ThrowsArgumentNullException_ForNullPrefix
- Constructor_ThrowsArgumentException_ForEmptyPrefix

- title: Platform Support
requirements:
Expand Down
49 changes: 42 additions & 7 deletions src/TemplateDotNetLibrary/DemoClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,69 @@ namespace TemplateDotNetLibrary;
/// </summary>
public class DemoClass
{
/// <summary>
/// The greeting prefix used when no custom prefix is specified.
/// </summary>
public const string DefaultPrefix = "Hello";

/// <summary>
/// The prefix prepended to every greeting produced by this instance.
/// </summary>
private readonly string _prefix;

/// <summary>
/// Initializes a new instance of the <see cref="DemoClass"/> class with default prefix.
/// Initializes a new instance of the <see cref="DemoClass"/> class with the default prefix.
/// </summary>
/// <remarks>
/// The prefix is set to <see cref="DefaultPrefix"/>.
/// </remarks>
public DemoClass()
: this("Hello")
: this(DefaultPrefix)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DemoClass"/> class with a custom prefix.
/// </summary>
/// <param name="prefix">The prefix to use in greetings.</param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="prefix"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="prefix"/> is an empty string.
/// </exception>
public DemoClass(string prefix)
{
ArgumentNullException.ThrowIfNull(prefix);
// Validate that the prefix is non-null and non-empty before storing it
ArgumentException.ThrowIfNullOrEmpty(prefix);
_prefix = prefix;
}

/// <summary>
/// Demo method that returns a greeting message.
/// Gets the greeting prefix used by this instance.
/// </summary>
/// <param name="name">The name to greet.</param>
/// <returns>A greeting message.</returns>
public string Prefix => _prefix;

/// <summary>
/// Returns a greeting message that combines the instance prefix with the given name.
/// </summary>
/// <param name="name">The name to include in the greeting.</param>
/// <returns>
/// A greeting string in the format <c>{prefix}, {name}!</c>,
/// for example <c>Hello, World!</c>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="name"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="name"/> is an empty string.
/// </exception>
public string DemoMethod(string name)
{
ArgumentNullException.ThrowIfNull(name);
// Validate that the name is non-null and non-empty before building the greeting
ArgumentException.ThrowIfNullOrEmpty(name);

// Combine the prefix and name into the standard greeting format
return $"{_prefix}, {name}!";
}
}
101 changes: 91 additions & 10 deletions test/TemplateDotNetLibrary.Tests/DemoClassTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ namespace TemplateDotNetLibrary.Tests;
public class DemoClassTests
{
/// <summary>
/// Test that DemoMethod returns the expected greeting with default prefix.
/// Proves that DemoMethod produces the expected "{prefix}, {name}!" format
/// when the default constructor is used.
/// </summary>
[TestMethod]
public void DemoMethod_ReturnsGreeting_WithDefaultPrefix()
Expand All @@ -19,12 +20,13 @@ public void DemoMethod_ReturnsGreeting_WithDefaultPrefix()
// Act
var result = demo.DemoMethod(name);

// Assert
// Assert – greeting must be exactly "Hello, World!"
Assert.AreEqual("Hello, World!", result);
}

/// <summary>
/// Test that DemoMethod returns the expected greeting with custom prefix.
/// Proves that DemoMethod uses the custom prefix supplied at construction
/// time instead of the default.
/// </summary>
[TestMethod]
public void DemoMethod_ReturnsGreeting_WithCustomPrefix()
Expand All @@ -36,30 +38,109 @@ public void DemoMethod_ReturnsGreeting_WithCustomPrefix()
// Act
var result = demo.DemoMethod(name);

// Assert
// Assert – greeting must use the custom prefix "Hi"
Assert.AreEqual("Hi, Alice!", result);
}

/// <summary>
/// Test that DemoMethod throws ArgumentNullException for null input.
/// Proves that DemoMethod throws ArgumentNullException (not a base
/// ArgumentException) when a null name is supplied.
/// </summary>
[TestMethod]
public void DemoMethod_ThrowsArgumentNullException_ForNullInput()
{
// Arrange
var demo = new DemoClass();

// Act & Assert
_ = Assert.Throws<ArgumentNullException>(() => demo.DemoMethod(null!));
// Act & Assert – exact exception type proves null is explicitly rejected
Assert.ThrowsExactly<ArgumentNullException>(() => demo.DemoMethod(null!));
}

/// <summary>
/// Test that constructor throws ArgumentNullException for null prefix.
/// Proves that DemoMethod throws ArgumentException (not ArgumentNullException)
/// when an empty string name is supplied.
/// </summary>
[TestMethod]
public void DemoMethod_ThrowsArgumentException_ForEmptyInput()
{
// Arrange
var demo = new DemoClass();

// Act & Assert – ArgumentException (not the null sub-type) must be thrown
Assert.ThrowsExactly<ArgumentException>(() => demo.DemoMethod(string.Empty));
}

/// <summary>
/// Proves that the custom-prefix constructor throws ArgumentNullException
/// (not a base ArgumentException) when a null prefix is supplied.
/// </summary>
[TestMethod]
public void Constructor_ThrowsArgumentNullException_ForNullPrefix()
{
// Act & Assert
_ = Assert.Throws<ArgumentNullException>(() => new DemoClass(null!));
// Act & Assert – exact exception type proves null is explicitly rejected
Assert.ThrowsExactly<ArgumentNullException>(() => new DemoClass(null!));
}

/// <summary>
/// Proves that the custom-prefix constructor throws ArgumentException
/// (not ArgumentNullException) when an empty string prefix is supplied.
/// </summary>
[TestMethod]
public void Constructor_ThrowsArgumentException_ForEmptyPrefix()
{
// Act & Assert – ArgumentException (not the null sub-type) must be thrown
Assert.ThrowsExactly<ArgumentException>(() => new DemoClass(string.Empty));
}

/// <summary>
/// Proves that the DefaultPrefix constant exposes the value "Hello",
/// which is the expected default greeting prefix.
/// </summary>
[TestMethod]
public void DemoClass_DefaultPrefix_IsHello()
{
// Arrange
const string expected = "Hello";

// Act – read the public constant directly
var actual = DemoClass.DefaultPrefix;

// Assert – constant value must not silently change
Assert.AreEqual(expected, actual);
}

/// <summary>
/// Proves that the Prefix property returns the custom value provided to
/// the constructor, confirming the property reflects what was stored.
/// </summary>
[TestMethod]
public void DemoClass_Prefix_ReturnsCustomPrefix()
{
// Arrange
const string customPrefix = "Greetings";
var demo = new DemoClass(customPrefix);

// Act
var actual = demo.Prefix;

// Assert – Prefix must exactly match the value passed at construction
Assert.AreEqual(customPrefix, actual);
}

/// <summary>
/// Proves that the default constructor stores DefaultPrefix in the Prefix
/// property, tying the constant and the property together explicitly.
/// </summary>
[TestMethod]
public void DemoClass_DefaultConstructor_SetsDefaultPrefix()
{
// Arrange
var demo = new DemoClass();

// Act
var actual = demo.Prefix;

// Assert – default constructor must yield exactly DemoClass.DefaultPrefix
Assert.AreEqual(DemoClass.DefaultPrefix, actual);
}
}