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
44 changes: 40 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ published as individual NuGet packages. Each project corresponds to one NuGet pa
|---|---|---|
| `SharedCode.Core` | `SharedCode.Core` | Core utilities: extensions, specifications, domain primitives, text, calendar, security, threading, reactive |
| `SharedCode.Core.Tests` | *(test only)* | Unit tests for `SharedCode.Core` |
| `SharedCode.Data.Tests` | *(test only)* | Unit tests for `SharedCode.Data` |
| `SharedCode.Data` | `SharedCode.Data` | Data access abstractions and helpers (repository pattern, paging, query results) |
| `SharedCode.Data.CosmosDb` | `SharedCode.Data.CosmosDb` | Azure Cosmos DB integration |
| `SharedCode.Data.EntityFramework` | `SharedCode.Data.EntityFramework` | Entity Framework Core integration (auditable contexts, EF repository, specifications) |
Expand Down Expand Up @@ -37,7 +38,7 @@ dotnet test --logger GitHubActions --verbosity normal SharedCode.sln
- **Target frameworks**:
- Primary/current: `.NET 10` (`net10.0`)
- Compatibility targets: `.NET 9` (`net9.0`), `.NET 8` (`net8.0`), and `.NET Standard 2.0/2.1` where applicable
- **Language version**: `preview` (latest C# features enabled)
- **Language version**: `preview` (latest C# features enabled — C# 13 and beyond)
- **Nullable reference types**: enabled (`<Nullable>enable</Nullable>`)
- **Implicit usings**: enabled
- **Warnings as errors**: all warnings are treated as errors
Expand Down Expand Up @@ -93,6 +94,33 @@ public static DateTime AddBusinessDays(this DateTime @this, int days) { ... }
- Prefer `ArgumentException.ThrowIfNullOrEmpty(param)` for strings (.NET 7+)
- When targeting older frameworks via polyfill, wrap with `#if NET6_0_OR_GREATER`

### C# 13 / .NET 10 Modern Features

Use these features wherever they improve clarity. The solution targets `LangVersion: preview`, so
all C# 13 features are available:

- **Collection expressions** (`[..]`) — prefer over `new List<T> { }` or `new T[] { }` literals
```csharp
string[] names = ["Alice", "Bob"];
List<int> ids = [1, 2, 3];
```
- **`params ReadOnlySpan<T>`** — prefer for methods that accept a variable number of arguments when
performance matters and heap allocation should be avoided
- **Primary constructors** — prefer for simple types that store injected dependencies:
```csharp
public sealed class MyService(ILogger<MyService> logger) { ... }
```
- **`field` keyword** — use inside property accessors to access the auto-generated backing field
without declaring it explicitly (C# 13 preview):
```csharp
public string Name
{
get;
set => field = value.Trim();
}
```
- **`allows ref struct`** — add to generic constraints when the type parameter may be a ref struct

### Code Analysis Suppressions

- Suppress with `[SuppressMessage("Category", "RuleId:Title", Justification = "reason")]`
Expand Down Expand Up @@ -135,14 +163,22 @@ Services implement `IDependencyRegister` to self-register via assembly scanning

## Testing Conventions

Unit tests live in `SharedCode.Core.Tests/` and follow these rules:
Unit tests live in `SharedCode.Core.Tests/` and `SharedCode.Data.Tests/` and follow these rules:

- **Framework**: MSTest (`[TestClass]`, `[TestMethod]`, `[DataRow]`)
- **Framework**: MSTest (`[TestClass]`, `[TestMethod]`, `[DataRow]`, `[DataTestMethod]`)
- **Assertions**: AwesomeAssertions
- **Pattern**: Arrange / Act / Assert with blank lines separating each block
- **File location**: mirror the source structure
(e.g., `Calendar/DateTimeExtensionsTests.cs` for `Calendar/DateTimeExtensions.cs`)
- Suppress `CA1515` on test classes — MSTest requires them to be `public`
- `CA1515` (`public` types as internal) is suppressed at the test-project level via `.editorconfig` — MSTest requires test classes to be `public`

### `SharedCode.Data.Tests`

The `SharedCode.Data.Tests` project mirrors the structure of `SharedCode.Data` and tests:

- Repository base types (`QueryRepository`, `CommandRepository`)
- Paging helpers (`PagingDescriptor`, `PageBoundry`)
- Query result types (`QueryResult`)

## Shared Build Configuration

Expand Down
107 changes: 107 additions & 0 deletions .github/prompts/add-test-class.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
mode: edit
description: Add a new MSTest test class for a SharedCode source file following project conventions.
---

# Add a Test Class

Add a new MSTest test class that exercises a source file in the SharedCode library.

## Steps

1. **Identify the source file** you want to test and locate it in the solution.

2. **Choose the right test project**

| Source project | Test project |
|---|---|
| `SharedCode.Core` | `SharedCode.Core.Tests` |
| `SharedCode.Data` | `SharedCode.Data.Tests` |

3. **Mirror the source folder structure**

Place the new file in the same relative subfolder as the source:

| Source file | Test file |
|---|---|
| `SharedCode.Core/Calendar/DateTimeExtensions.cs` | `SharedCode.Core.Tests/Calendar/DateTimeExtensionsTests.cs` |
| `SharedCode.Data/Paging/PagingDescriptor.cs` | `SharedCode.Data.Tests/PagingDescriptorTests.cs` |

4. **Write the test class** following these rules:
- Annotate with `[TestClass]`
- Do **not** add a per-class `CA1515` suppression — it is already disabled at the project level via `.editorconfig`
- Use `[TestMethod]` for single-scenario tests
- Use `[DataTestMethod]` + `[DataRow(...)]` for parameterized tests
- Follow the **Arrange / Act / Assert** pattern with blank lines separating each block
- Use **MSTest assertions** (`Assert.AreEqual`, `Assert.IsTrue`, `Assert.IsNotNull`, `Assert.ThrowsExactly`)
- Name test methods as `<MemberUnderTest>_<Scenario>_<ExpectedOutcome>`

5. **Verify zero warnings**: `dotnet build SharedCode.sln`

## Template — single-scenario test

```csharp
namespace SharedCode.Tests.<Folder>;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Tests for <see cref="<TypeUnderTest>" />.
/// </summary>
[TestClass]
public class <TypeUnderTest>Tests
{
/// <summary>
/// Tests that <Member> does [expected behavior].
/// </summary>
[TestMethod]
public void <Member>_<Scenario>_<ExpectedOutcome>()
{
// Arrange
var sut = <create instance or value>;

// Act
var result = sut.<Member>(...);

// Assert
Assert.AreEqual(<expected>, result);
}
}
```

## Template — parameterized test

```csharp
/// <summary>
/// Tests that <Member> returns the expected result for various inputs.
/// </summary>
[DataTestMethod]
[DataRow(<input1>, <expected1>)]
[DataRow(<input2>, <expected2>)]
public void <Member>_<Scenario>_<ExpectedOutcome>(<InputType> input, <ExpectedType> expected)
{
// Arrange
var sut = <create instance or value>;

// Act
var result = sut.<Member>(input);

// Assert
Assert.AreEqual(expected, result);
}
Comment thread
Copilot marked this conversation as resolved.
```

## Template — exception test

```csharp
/// <summary>
/// Tests that <Member> throws <ExceptionType> when [condition].
/// </summary>
[TestMethod]
public void <Member>_<Condition>_Throws<ExceptionType>()
{
// Act / Assert
_ = Assert.ThrowsExactly<<ExceptionType>>(
() => <sut>.<Member>(<args>));
}
```
2 changes: 1 addition & 1 deletion .github/prompts/create-module.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ belong here.
<Description>A library of [short description] shared for free use to help with common scenarios.</Description>
<PackageTags>shared code, c#, [relevant tags]</PackageTags>
<RootNamespace>SharedCode.<ModuleName></RootNamespace>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
Expand Down
70 changes: 70 additions & 0 deletions .github/prompts/improve-coverage.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
mode: edit
description: Identify untested public members and add test coverage for them.
---

# Improve Test Coverage

Identify public members in a SharedCode source file (or folder) that have no test coverage
and add tests for them.

## Process

### 1 — Identify what is missing

For each `.cs` file in the source project:

1. List every `public` method, property, and indexer.
2. Open the corresponding `*Tests.cs` file in the test project (if it exists).
3. Note every member that has **no** `[TestMethod]` exercising it.
4. If no test file exists at all, every public member needs coverage.

### 2 — Prioritize

Cover members in this order:
1. Pure logic methods (no I/O or infrastructure) — easiest to test
2. Guard-clause paths (`ArgumentNullException`, `ArgumentException`)
3. Edge cases (empty collections, null-optional parameters, boundary values)
4. Happy paths for remaining members

### 3 — Write the tests

Follow the conventions in `add-test-class.prompt.md`:
- `[TestMethod]` for single scenarios
- `[DataRow]` for parameterized scenarios
- MSTest assertions (`Assert.AreEqual`, `Assert.IsTrue`, `Assert.ThrowsExactly`)
- Arrange / Act / Assert blocks separated by blank lines

### 4 — Verify

```bash
dotnet test SharedCode.sln
```

Zero failures required before merging.

## Checklist per source file

Run through these questions for each public member:

- [ ] Is there a happy-path test?
- [ ] Is there a null-argument test (if the member accepts reference-type parameters)?
- [ ] Is there a boundary/edge-case test (empty string, zero, `int.MaxValue`, etc.)?
- [ ] Is the test parameterized with `[DataRow]` instead of repeated copy-paste?

## Common coverage gaps in this solution

| Source file | Members typically missing coverage |
|---|---|
| `AssemblyExtensions.cs` | `GetAttribute<T>` (found / not found) |
| `EventHandlerExtensions.cs` | `Raise` overloads (null handler, non-null handler) |
| `Extensions.cs` | `IsBetween`, `In`, `IfNotNull`, `IsNull<T>`, `ChangeType<T>` |
| `FunctionExtensions.cs` | `Memoize` (cache hit, cache miss) |
| `TypeExtensions.cs` | `GetDisplayName`, `IsNullable`, `IsSubclassOfRawGeneric` |
| `PropertySupport.cs` | `ExtractPropertyName` |
| `Linq/` | All `IEnumerable<T>` extension methods |
| `Security/` | Hashing / encryption helpers |
| `Text/` | All string extension methods |
| `Threading/` | All task/threading helpers |
| `Domain/` | `ValueObject` equality |
| `Specifications/` | `InMemorySpecificationEvaluator` |
4 changes: 3 additions & 1 deletion .github/prompts/maintain-copilot-instructions.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ Run this prompt after adding new modules, changing conventions, or updating the

- [ ] `add-extension-method.prompt.md` — namespace table is complete and templates compile
- [ ] `add-specification.prompt.md` — builder API still matches `ISpecificationBuilder<T>`
- [ ] `add-test-class.prompt.md` — assertion style matches the project's test conventions
- [ ] `fix-code-analysis.prompt.md` — rule table covers the analyzers actually in use
- [ ] `create-module.prompt.md` — scaffolding steps still match the solution structure
- [ ] `improve-coverage.prompt.md` — coverage gap table is accurate
- [ ] Each prompt's `description` front-matter field is accurate

### `.vscode/mcp.json`

- [ ] Listed MCP servers are still current and useful
- [ ] Docker image tags or npx package versions are up to date
- [ ] npx package versions are up to date
- [ ] Any new MCP servers that would improve development tasks should be added

## Process
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@ jobs:
dotnet-version: 10.0.x

- name: Run tests
run: dotnet test --logger GitHubActions SharedCode.sln
run: dotnet test --logger GitHubActions --collect:"XPlat Code Coverage" SharedCode.sln

- name: Upload coverage reports
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-reports
path: '**/coverage.cobertura.xml'
if-no-files-found: ignore
26 changes: 21 additions & 5 deletions .github/workflows/maintain-copilot-instructions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ jobs:
fi
done <<< "$projects"

# Check that required prompt files exist
required_prompts=(
".github/prompts/add-extension-method.prompt.md"
".github/prompts/add-specification.prompt.md"
".github/prompts/add-test-class.prompt.md"
".github/prompts/create-module.prompt.md"
".github/prompts/fix-code-analysis.prompt.md"
".github/prompts/improve-coverage.prompt.md"
".github/prompts/maintain-copilot-instructions.prompt.md"
)
for prompt in "${required_prompts[@]}"; do
if [[ ! -f "$prompt" ]]; then
missing_list="${missing_list}\n- Missing prompt: \`${prompt}\`"
fi
done

if [[ -n "$missing_list" ]]; then
echo "has_missing=true" >> "$GITHUB_OUTPUT"
else
Expand All @@ -74,18 +90,18 @@ jobs:
const body = [
'## Copilot Instructions May Be Outdated',
'',
'The following projects are present in `SharedCode.sln` but are not yet',
'documented in `.github/copilot-instructions.md`:',
'The following projects or prompt files are missing from the Copilot configuration:',
'',
missing,
'',
'### What to do',
'',
'1. Open `.github/copilot-instructions.md` and add the missing projects to',
'1. Open `.github/copilot-instructions.md` and add any missing projects to',
' the module table.',
'2. Use the prompt at `.github/prompts/maintain-copilot-instructions.prompt.md`',
'2. Ensure all required prompt files exist under `.github/prompts/`.',
'3. Use the prompt at `.github/prompts/maintain-copilot-instructions.prompt.md`',
' to guide a full review of all Copilot configuration files.',
'3. Close this issue once the instructions are up to date.',
'4. Close this issue once the instructions are up to date.',
].join('\n');

const label = 'copilot-instructions';
Expand Down
24 changes: 17 additions & 7 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,28 @@
],
"servers": {
"github": {
"command": "docker",
"command": "npx",
"args": [
"run",
"--interactive",
"--rm",
"--env",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"
"-y",
"@github/github-mcp-server"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github-token}"
}
},
"sequential-thinking": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sequential-thinking"
]
},
"dotnet-skills": {
"command": "npx",
"args": [
"-y",
"@microsoft/dotnet-mcp-server"
]
}
}
}
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
</PropertyGroup>
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
<Exec Command="git describe --long --always --dirty --exclude=* --abbrev=8" ConsoleToMSBuild="True" IgnoreExitCode="False">
<!--<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput" />-->
<Exec Command="git describe --long --always --dirty --exclude=* --abbrev=8" ConsoleToMSBuild="True" IgnoreExitCode="True">
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput" />
</Exec>
</Target>
</Project>
Loading
Loading