diff --git a/AGENTS.md b/AGENTS.md
index 4506302..f54c4f6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ef6e8f2..6062608 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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**:
@@ -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)
diff --git a/README.md b/README.md
index 92e91c7..51c802e 100644
--- a/README.md
+++ b/README.md
@@ -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,
diff --git a/docs/guide/guide.md b/docs/guide/guide.md
index 71dbf6d..cb199f8 100644
--- a/docs/guide/guide.md
+++ b/docs/guide/guide.md
@@ -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()
@@ -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
@@ -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:**
@@ -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:**
diff --git a/requirements.yaml b/requirements.yaml
index c205cce..1ed0922 100644
--- a/requirements.yaml
+++ b/requirements.yaml
@@ -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:
diff --git a/src/TemplateDotNetLibrary/DemoClass.cs b/src/TemplateDotNetLibrary/DemoClass.cs
index 0035a85..3dd0524 100644
--- a/src/TemplateDotNetLibrary/DemoClass.cs
+++ b/src/TemplateDotNetLibrary/DemoClass.cs
@@ -5,13 +5,24 @@ namespace TemplateDotNetLibrary;
///
public class DemoClass
{
+ ///
+ /// The greeting prefix used when no custom prefix is specified.
+ ///
+ public const string DefaultPrefix = "Hello";
+
+ ///
+ /// The prefix prepended to every greeting produced by this instance.
+ ///
private readonly string _prefix;
///
- /// Initializes a new instance of the class with default prefix.
+ /// Initializes a new instance of the class with the default prefix.
///
+ ///
+ /// The prefix is set to .
+ ///
public DemoClass()
- : this("Hello")
+ : this(DefaultPrefix)
{
}
@@ -19,20 +30,44 @@ public DemoClass()
/// Initializes a new instance of the class with a custom prefix.
///
/// The prefix to use in greetings.
+ ///
+ /// Thrown when is .
+ ///
+ ///
+ /// Thrown when is an empty string.
+ ///
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;
}
///
- /// Demo method that returns a greeting message.
+ /// Gets the greeting prefix used by this instance.
///
- /// The name to greet.
- /// A greeting message.
+ public string Prefix => _prefix;
+
+ ///
+ /// Returns a greeting message that combines the instance prefix with the given name.
+ ///
+ /// The name to include in the greeting.
+ ///
+ /// A greeting string in the format {prefix}, {name}!,
+ /// for example Hello, World!.
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ ///
+ /// Thrown when is an empty string.
+ ///
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}!";
}
}
diff --git a/test/TemplateDotNetLibrary.Tests/DemoClassTests.cs b/test/TemplateDotNetLibrary.Tests/DemoClassTests.cs
index 389b915..c70659b 100644
--- a/test/TemplateDotNetLibrary.Tests/DemoClassTests.cs
+++ b/test/TemplateDotNetLibrary.Tests/DemoClassTests.cs
@@ -7,7 +7,8 @@ namespace TemplateDotNetLibrary.Tests;
public class DemoClassTests
{
///
- /// 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.
///
[TestMethod]
public void DemoMethod_ReturnsGreeting_WithDefaultPrefix()
@@ -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);
}
///
- /// 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.
///
[TestMethod]
public void DemoMethod_ReturnsGreeting_WithCustomPrefix()
@@ -36,12 +38,13 @@ 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);
}
///
- /// Test that DemoMethod throws ArgumentNullException for null input.
+ /// Proves that DemoMethod throws ArgumentNullException (not a base
+ /// ArgumentException) when a null name is supplied.
///
[TestMethod]
public void DemoMethod_ThrowsArgumentNullException_ForNullInput()
@@ -49,17 +52,95 @@ public void DemoMethod_ThrowsArgumentNullException_ForNullInput()
// Arrange
var demo = new DemoClass();
- // Act & Assert
- _ = Assert.Throws(() => demo.DemoMethod(null!));
+ // Act & Assert – exact exception type proves null is explicitly rejected
+ Assert.ThrowsExactly(() => demo.DemoMethod(null!));
}
///
- /// Test that constructor throws ArgumentNullException for null prefix.
+ /// Proves that DemoMethod throws ArgumentException (not ArgumentNullException)
+ /// when an empty string name is supplied.
+ ///
+ [TestMethod]
+ public void DemoMethod_ThrowsArgumentException_ForEmptyInput()
+ {
+ // Arrange
+ var demo = new DemoClass();
+
+ // Act & Assert – ArgumentException (not the null sub-type) must be thrown
+ Assert.ThrowsExactly(() => demo.DemoMethod(string.Empty));
+ }
+
+ ///
+ /// Proves that the custom-prefix constructor throws ArgumentNullException
+ /// (not a base ArgumentException) when a null prefix is supplied.
///
[TestMethod]
public void Constructor_ThrowsArgumentNullException_ForNullPrefix()
{
- // Act & Assert
- _ = Assert.Throws(() => new DemoClass(null!));
+ // Act & Assert – exact exception type proves null is explicitly rejected
+ Assert.ThrowsExactly(() => new DemoClass(null!));
+ }
+
+ ///
+ /// Proves that the custom-prefix constructor throws ArgumentException
+ /// (not ArgumentNullException) when an empty string prefix is supplied.
+ ///
+ [TestMethod]
+ public void Constructor_ThrowsArgumentException_ForEmptyPrefix()
+ {
+ // Act & Assert – ArgumentException (not the null sub-type) must be thrown
+ Assert.ThrowsExactly(() => new DemoClass(string.Empty));
+ }
+
+ ///
+ /// Proves that the DefaultPrefix constant exposes the value "Hello",
+ /// which is the expected default greeting prefix.
+ ///
+ [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);
+ }
+
+ ///
+ /// Proves that the Prefix property returns the custom value provided to
+ /// the constructor, confirming the property reflects what was stored.
+ ///
+ [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);
+ }
+
+ ///
+ /// Proves that the default constructor stores DefaultPrefix in the Prefix
+ /// property, tying the constant and the property together explicitly.
+ ///
+ [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);
}
}