EasyMessages v1.0.0 Release Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Release RecurPixel.EasyMessages as v1.0.0 (stable), move .Log(ILogger) into Core, deprecate RecurPixel.EasyMessages.AspNetCore as v0.1.0 (final), and set up CI/CD + Docker test pipeline.

Architecture: Move the logging extension from the AspNetCore package into Core (it only needs Microsoft.Extensions.Logging.Abstractions, which is cross-platform). Bump versions, rewrite docs as a single-package story, promote the ConsoleSample to a full Playground, and wire up GitHub Actions workflows mirroring the RecurPixel.Notify project.

Tech Stack: .NET 8/9/10, xUnit, NSubstitute, FluentAssertions, GitHub Actions, Docker (mcr.microsoft.com/dotnet/sdk:latest)


File Map

Action Path
CREATE src/RecurPixel.EasyMessages/Extensions/MessageExtensions.Logging.cs
CREATE tests/RecurPixel.EasyMessages.Tests/Unit/MessageLoggingExtensionsTests.cs
CREATE samples/EasyMessages.Playground/EasyMessages.Playground.csproj
CREATE samples/EasyMessages.Playground/Program.cs
CREATE .github/workflows/ci.yml
CREATE .github/workflows/publish-nuget.yml
CREATE .github/workflows/docs.yml
CREATE Dockerfile.test
CREATE test-workflow.sh
MODIFY src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj
MODIFY src/RecurPixel.EasyMessages.AspNetCore/RecurPixel.EasyMessages.AspNetCore.csproj
MODIFY src/RecurPixel.EasyMessages.AspNetCore/README.md
MODIFY src/RecurPixel.EasyMessages/README.md
MODIFY README.md (root)
MODIFY CHANGELOG.md
MODIFY RecurPixel.EasyMessages.sln
DELETE src/RecurPixel.EasyMessages.AspNetCore/Extensions/MessageLoggingExtensions.cs
DELETE samples/ConsoleSample/ (entire directory — replaced by Playground)
DELETE samples/WebApiSample/ (entire directory — AspNetCore only)

Task 1: Add Logging Dependency to Core

Files:

  • Modify: src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj

  • Step 1: Add Microsoft.Extensions.Logging.Abstractions to Core .csproj

In src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj, add inside the existing <ItemGroup> that has System.Text.Json:

<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />

The <ItemGroup> block should now read:

<ItemGroup>
  <PackageReference Include="System.Text.Json" Version="10.0.0" />
  <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
  • Step 2: Verify it restores cleanly
dotnet restore src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj

Expected: Restore succeeded. with no errors.


Task 2: Write Failing Tests for .Log(ILogger) in Core

Files:

  • Create: tests/RecurPixel.EasyMessages.Tests/Unit/MessageLoggingExtensionsTests.cs

  • Step 1: Create the test file

using Microsoft.Extensions.Logging;
using RecurPixel.EasyMessages;
using RecurPixel.EasyMessages.Core;
using Xunit;

namespace RecurPixel.EasyMessages.Tests.Unit;

[Collection("MessageRegistry")]
public class MessageLoggingExtensionsTests : UnitTestBase
{
    private class TestLogger : ILogger
    {
        public List<LogLevel> LoggedLevels { get; } = new();
        public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
        public bool IsEnabled(LogLevel logLevel) => true;
        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
            Exception? exception, Func<TState, Exception?, string> formatter)
            => LoggedLevels.Add(logLevel);
    }

    [Fact]
    public void Log_ErrorMessage_UsesLogError()
    {
        Dispose();
        var logger = new TestLogger();
        var message = MessageRegistry.Get("AUTH_001"); // Type = Error, HTTP 401

        message.Log(logger);

        Assert.Single(logger.LoggedLevels);
        Assert.Equal(LogLevel.Error, logger.LoggedLevels[0]);
    }

    [Fact]
    public void Log_WarningMessage_UsesLogWarning()
    {
        Dispose();
        var logger = new TestLogger();
        var custom = new Dictionary<string, MessageTemplate>
        {
            ["TEST_WARN"] = new() { Type = MessageType.Warning, Title = "Warning", Description = "A warning" }
        };
        MessageRegistry.LoadCustomMessages(custom);
        var message = MessageRegistry.Get("TEST_WARN");

        message.Log(logger);

        Assert.Single(logger.LoggedLevels);
        Assert.Equal(LogLevel.Warning, logger.LoggedLevels[0]);
    }

    [Fact]
    public void Log_SuccessMessage_UsesLogInformation()
    {
        Dispose();
        var logger = new TestLogger();
        var custom = new Dictionary<string, MessageTemplate>
        {
            ["TEST_OK"] = new() { Type = MessageType.Success, Title = "OK", Description = "Success" }
        };
        MessageRegistry.LoadCustomMessages(custom);
        var message = MessageRegistry.Get("TEST_OK");

        message.Log(logger);

        Assert.Single(logger.LoggedLevels);
        Assert.Equal(LogLevel.Information, logger.LoggedLevels[0]);
    }

    [Fact]
    public void Log_InfoMessage_UsesLogInformation()
    {
        Dispose();
        var logger = new TestLogger();
        var custom = new Dictionary<string, MessageTemplate>
        {
            ["TEST_INFO"] = new() { Type = MessageType.Info, Title = "Info", Description = "Informational" }
        };
        MessageRegistry.LoadCustomMessages(custom);
        var message = MessageRegistry.Get("TEST_INFO");

        message.Log(logger);

        Assert.Single(logger.LoggedLevels);
        Assert.Equal(LogLevel.Information, logger.LoggedLevels[0]);
    }

    [Fact]
    public void Log_CriticalMessage_UsesLogCritical()
    {
        Dispose();
        var logger = new TestLogger();
        var custom = new Dictionary<string, MessageTemplate>
        {
            ["TEST_CRIT"] = new() { Type = MessageType.Critical, Title = "Critical", Description = "Critical error" }
        };
        MessageRegistry.LoadCustomMessages(custom);
        var message = MessageRegistry.Get("TEST_CRIT");

        message.Log(logger);

        Assert.Single(logger.LoggedLevels);
        Assert.Equal(LogLevel.Critical, logger.LoggedLevels[0]);
    }

    [Fact]
    public void Log_ReturnsSameMessageInstance_ForChaining()
    {
        Dispose();
        var logger = new TestLogger();
        var message = MessageRegistry.Get("AUTH_001");

        var returned = message.Log(logger);

        Assert.Same(message, returned);
    }

    [Fact]
    public void Log_ChainsWithToJson()
    {
        Dispose();
        var logger = new TestLogger();
        var message = MessageRegistry.Get("AUTH_001");

        var json = message.Log(logger).ToJson();

        Assert.NotNull(json);
        Assert.Contains("AUTH_001", json);
        Assert.Single(logger.LoggedLevels);
    }

    [Fact]
    public void Log_NullLogger_UsesNullLoggerAndDoesNotThrow()
    {
        Dispose();
        var message = MessageRegistry.Get("AUTH_001");

        var returned = message.Log(null);

        Assert.Same(message, returned);
    }
}
  • Step 2: Run the tests to confirm they fail (method not yet in Core)
dotnet test tests/RecurPixel.EasyMessages.Tests/RecurPixel.EasyMessages.Tests.csproj --filter "MessageLoggingExtensionsTests" --configuration Release

Expected: FAIL — 'Message' does not contain a definition for 'Log' or similar compile error.


Task 3: Implement .Log(ILogger) in Core

Files:

  • Create: src/RecurPixel.EasyMessages/Extensions/MessageExtensions.Logging.cs

  • Step 1: Create MessageExtensions.Logging.cs

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace RecurPixel.EasyMessages;

public static partial class MessageExtensions
{
    public static Message Log(this Message message, ILogger? logger = null)
    {
        var target = logger ?? NullLogger.Instance;

        target.Log(
            MapToLogLevel(message.Type),
            "[{Code}] {Title}: {Description}",
            message.Code,
            message.Title,
            message.Description
        );

        return message;
    }

    private static LogLevel MapToLogLevel(MessageType type) =>
        type switch
        {
            MessageType.Success  => LogLevel.Information,
            MessageType.Info     => LogLevel.Information,
            MessageType.Warning  => LogLevel.Warning,
            MessageType.Error    => LogLevel.Error,
            MessageType.Critical => LogLevel.Critical,
            _                    => LogLevel.Information,
        };
}
  • Step 2: Run the new tests to confirm they pass
dotnet test tests/RecurPixel.EasyMessages.Tests/RecurPixel.EasyMessages.Tests.csproj --filter "MessageLoggingExtensionsTests" --configuration Release

Expected: All 8 tests PASS.

  • Step 3: Run the full test suite to confirm nothing broke
dotnet test --configuration Release

Expected: All 237+ tests PASS, zero failures.

  • Step 4: Commit
git add src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj
git add src/RecurPixel.EasyMessages/Extensions/MessageExtensions.Logging.cs
git add tests/RecurPixel.EasyMessages.Tests/Unit/MessageLoggingExtensionsTests.cs
git commit -m "feat: move .Log(ILogger) into Core package"

Task 4: Remove .Log() from AspNetCore

Files:

  • Delete: src/RecurPixel.EasyMessages.AspNetCore/Extensions/MessageLoggingExtensions.cs

  • Step 1: Delete the file

git rm src/RecurPixel.EasyMessages.AspNetCore/Extensions/MessageLoggingExtensions.cs
  • Step 2: Verify the solution still builds

The test project references both Core and AspNetCore. Since Core now has .Log(), and AspNetCore references Core, the method is still accessible from the test project.

dotnet build --configuration Release

Expected: Build succeeded with zero errors.

  • Step 3: Run the full test suite
dotnet test --configuration Release

Expected: All tests PASS.

  • Step 4: Commit
git add src/RecurPixel.EasyMessages.AspNetCore/Extensions/MessageLoggingExtensions.cs
git commit -m "refactor: remove .Log() from AspNetCore — now lives in Core"

Task 5: Version Bumps

Files:

  • Modify: src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj
  • Modify: src/RecurPixel.EasyMessages.AspNetCore/RecurPixel.EasyMessages.AspNetCore.csproj

  • Step 1: Bump Core to 1.0.0

In src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj, change:

<Version>0.1.0-beta.1</Version>

to:

<Version>1.0.0</Version>

Also update the <Description> block — remove 📦 BETA RELEASE prefix and the beta-specific language:

<Description>
  A centralized message management library for .NET applications.

  100+ pre-built, standardized messages for authentication, CRUD, validation,
  and system errors. Zero configuration. Fluent API. Works in console apps,
  background jobs, web APIs, and class libraries.

  Documentation: https://recurpixel.github.io/RecurPixel.EasyMessages/
  Feedback: https://github.com/RecurPixel/RecurPixel.EasyMessages/issues
</Description>

Also update <PackageTags> — remove beta:

<PackageTags>messages;api;error-handling;errors;logging;netstandard;net8;net9;net10;console;messaging;fluent</PackageTags>
  • Step 2: Bump AspNetCore to 0.1.0 and mark deprecated in description

In src/RecurPixel.EasyMessages.AspNetCore/RecurPixel.EasyMessages.AspNetCore.csproj, change:

<Version>0.1.0-beta.1</Version>

to:

<Version>0.1.0</Version>

Replace the <Description> block with:

<Description>
  ⚠️ DEPRECATED — This package is no longer maintained.

  Use RecurPixel.EasyMessages (Core) v1.0.0 instead. The Core package
  works in all .NET applications including ASP.NET Core web APIs.

  Migration: replace .ToApiResponse() with StatusCode(msg.HttpStatusCode ?? 200, msg.ToJsonObject())
  See: https://github.com/RecurPixel/RecurPixel.EasyMessages

  Legacy ASP.NET Core integration for RecurPixel.EasyMessages.
</Description>

Also update <PackageReleaseNotes>:

<PackageReleaseNotes>
  v0.1.0 (Final) - This package is deprecated. Use RecurPixel.EasyMessages v1.0.0 instead.
  See migration guide: https://github.com/RecurPixel/RecurPixel.EasyMessages
</PackageReleaseNotes>
  • Step 3: Verify both projects pack without errors
dotnet pack src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj --configuration Release --output ./artifacts
dotnet pack src/RecurPixel.EasyMessages.AspNetCore/RecurPixel.EasyMessages.AspNetCore.csproj --configuration Release --output ./artifacts
ls ./artifacts

Expected:

RecurPixel.EasyMessages.1.0.0.nupkg
RecurPixel.EasyMessages.1.0.0.snupkg
RecurPixel.EasyMessages.AspNetCore.0.1.0.nupkg
RecurPixel.EasyMessages.AspNetCore.0.1.0.snupkg
  • Step 4: Clean up artifacts
rm -rf ./artifacts
  • Step 5: Commit
git add src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj
git add src/RecurPixel.EasyMessages.AspNetCore/RecurPixel.EasyMessages.AspNetCore.csproj
git commit -m "chore: bump Core to v1.0.0, AspNetCore to v0.1.0 (deprecated)"

Task 6: Update CHANGELOG

Files:

  • Modify: CHANGELOG.md

  • Step 1: Add the v1.0.0 entry at the top of CHANGELOG.md

Insert this block immediately after the first # Changelog heading line:

## [1.0.0] - 2026-05-10

### 🎉 Stable Release

First production-ready release of EasyMessages Core.

### Added
- `.Log(ILogger? logger = null)` extension method moved into Core — works in console apps, background jobs, and web APIs. Requires `Microsoft.Extensions.Logging.Abstractions` (automatically installed as a dependency).
- Log level mapping: `Success`/`Info` → `LogInformation`, `Warning` → `LogWarning`, `Error` → `LogError`, `Critical` → `LogCritical`.

### Changed
- Version: `0.1.0-beta.1` → `1.0.0`
- APIs are now under **semantic versioning** — no breaking changes without a major version bump.

### Deprecated
- `RecurPixel.EasyMessages.AspNetCore` — published as `v0.1.0` (final). No future updates.
  - Migration: `return StatusCode(msg.HttpStatusCode ?? 200, msg.ToJsonObject());`
  - See migration table below.

### Migration from AspNetCore Package

```bash
dotnet remove package RecurPixel.EasyMessages.AspNetCore
dotnet add package RecurPixel.EasyMessages --version 1.0.0
Before After
Msg.Auth.LoginFailed().ToApiResponse() StatusCode(msg.HttpStatusCode ?? 200, msg.ToJsonObject())
Msg.Crud.Created("User").ToMinimalApiResult() Results.Json(msg.ToJsonObject(), statusCode: msg.HttpStatusCode ?? 200)
builder.Services.AddEasyMessages() Remove — not needed
.Log(_logger) Same — .Log() is now in Core


- [ ] **Step 2: Commit**

```bash
git add CHANGELOG.md
git commit -m "docs: add v1.0.0 CHANGELOG entry with migration guide"

Task 7: Update AspNetCore README

Files:

  • Modify: src/RecurPixel.EasyMessages.AspNetCore/README.md

  • Step 1: Replace the entire README content

Replace the full file with:

# RecurPixel.EasyMessages.AspNetCore

> ⚠️ **DEPRECATED** — This package is no longer maintained.
>
> Use **[RecurPixel.EasyMessages](https://www.nuget.org/packages/RecurPixel.EasyMessages)** (Core) v1.0.0 instead.
> The Core package works in all .NET applications including ASP.NET Core web APIs.

---

## Migration Guide

```bash
# Remove deprecated package
dotnet remove package RecurPixel.EasyMessages.AspNetCore

# Install Core
dotnet add package RecurPixel.EasyMessages --version 1.0.0

Replace each call

Before (AspNetCore) After (Core only)
Msg.Auth.LoginFailed().ToApiResponse() StatusCode(msg.HttpStatusCode ?? 200, msg.ToJsonObject())
Msg.Crud.Created("User").ToMinimalApiResult() Results.Json(msg.ToJsonObject(), statusCode: msg.HttpStatusCode ?? 200)
builder.Services.AddEasyMessages() Remove from Program.cs — not needed
.Log(_logger) Same — .Log() is now in Core

Example before

// Program.cs
builder.Services.AddEasyMessages();

// Controller
public IActionResult Login(LoginDto dto)
{
    if (!_authService.Validate(dto))
        return Msg.Auth.LoginFailed().Log(_logger).ToApiResponse();

    return Msg.Auth.LoginSuccess().WithData(new { token }).ToApiResponse();
}

Example after

// Program.cs — no AddEasyMessages() call needed

// Controller
public IActionResult Login(LoginDto dto)
{
    if (!_authService.Validate(dto))
    {
        var msg = Msg.Auth.LoginFailed().Log(_logger);
        return StatusCode(msg.HttpStatusCode ?? 401, msg.ToJsonObject());
    }

    var success = Msg.Auth.LoginSuccess().WithData(new { token });
    return StatusCode(success.HttpStatusCode ?? 200, success.ToJsonObject());
}

Why Was This Deprecated?

Beta feedback showed 6:1 adoption of Core over AspNetCore. The Core package already produces JSON, has HTTP status codes on every message, and the .Log(ILogger) integration — everything needed for web APIs.


Documentation


MIT License — RecurPixel


- [ ] **Step 2: Commit**

```bash
git add src/RecurPixel.EasyMessages.AspNetCore/README.md
git commit -m "docs: add deprecation notice and migration guide to AspNetCore README"

Task 8: Rewrite Core README and Root README

Files:

  • Modify: src/RecurPixel.EasyMessages/README.md
  • Modify: README.md (root)

  • Step 1: Update Core package README (src/RecurPixel.EasyMessages/README.md)

Replace the ## Installation section version reference:

## Installation

```bash
dotnet add package RecurPixel.EasyMessages --version 1.0.0

Add `.Log(ILogger)` to the feature list (find the `## Quick Start` or features section and add):

```markdown
- ✅ **ILogger Integration** — `.Log(logger)` chains into any `ILogger` — console, web, background jobs

Add a web API usage example that does NOT use the AspNetCore package. Find the ### ASP.NET Core Web API section and replace it with:

### ASP.NET Core Web API

```csharp
// No setup needed — just install RecurPixel.EasyMessages

[HttpPost("login")]
public IActionResult Login(LoginDto dto)
{
    if (!_authService.Validate(dto))
    {
        var msg = Msg.Auth.LoginFailed().Log(_logger);
        return StatusCode(msg.HttpStatusCode ?? 401, msg.ToJsonObject());
    }

    var success = Msg.Auth.LoginSuccess().WithData(new { token = _authService.GenerateToken(dto) });
    return StatusCode(success.HttpStatusCode ?? 200, success.ToJsonObject());
}
  • Step 2: Update root README.md

Find the ## 📦 Packages section and replace the two-package table with a single-package section:

## 📦 Package

| Package | Version | Description |
|---------|---------|-------------|
| [**RecurPixel.EasyMessages**](https://www.nuget.org/packages/RecurPixel.EasyMessages) | [![NuGet](https://img.shields.io/nuget/v/RecurPixel.EasyMessages.svg)](https://www.nuget.org/packages/RecurPixel.EasyMessages) | Works in console apps, background jobs, web APIs, and class libraries |

```bash
dotnet add package RecurPixel.EasyMessages --version 1.0.0

Remove the NuGet badge for AspNetCore from the top of the root README:

```markdown
# EasyMessages

[![NuGet Core](https://img.shields.io/nuget/v/RecurPixel.EasyMessages.svg?label=NuGet)](https://www.nuget.org/packages/RecurPixel.EasyMessages)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-purple)](https://dotnet.microsoft.com/)

Update the Roadmap section to reflect current state:

## 🚦 Roadmap

- [x] **v0.1.0-beta** - Core functionality, 100+ messages
- [x] **v1.0.0** - Production-ready, stable API, ILogger integration (Current)
- [ ] **v1.x** - Additional integrations (Serilog, FluentValidation, etc.)

Replace the ## ✨ Current Status: Beta Release section with:

## ✨ Status: Stable (v1.0.0)

APIs are stable and under semantic versioning. Ready for production use.

**What's included:**
- ✅ 100+ built-in messages across 10 categories
- ✅ JSON, XML, Console, PlainText formatters
- ✅ ILogger integration (`.Log(logger)`)
- ✅ Parameter substitution, custom messages, interceptors
- ✅ Tested on .NET 8, 9, 10
  • Step 3: Commit
git add src/RecurPixel.EasyMessages/README.md README.md
git commit -m "docs: rewrite README as single-package story for v1.0.0"

Task 9: Replace ConsoleSample + WebApiSample with Playground

Files:

  • Delete: samples/ConsoleSample/ directory
  • Delete: samples/WebApiSample/ directory
  • Create: samples/EasyMessages.Playground/EasyMessages.Playground.csproj
  • Create: samples/EasyMessages.Playground/Program.cs
  • Modify: RecurPixel.EasyMessages.sln

  • Step 1: Remove old sample directories
git rm -r samples/ConsoleSample/
git rm -r samples/WebApiSample/
  • Step 2: Create Playground .csproj

Create samples/EasyMessages.Playground/EasyMessages.Playground.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\RecurPixel.EasyMessages\RecurPixel.EasyMessages.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
  </ItemGroup>
</Project>
  • Step 3: Create Program.cs

Create samples/EasyMessages.Playground/Program.cs:

using Microsoft.Extensions.Logging;
using RecurPixel.EasyMessages;
using RecurPixel.EasyMessages.Configuration;
using RecurPixel.EasyMessages.Formatters;
using RecurPixel.EasyMessages.Interceptors;

var loggerFactory = LoggerFactory.Create(b => b.AddConsole().SetMinimumLevel(LogLevel.Trace));
var logger = loggerFactory.CreateLogger("Playground");

PrintSection("1. Authentication Messages");
Msg.Auth.LoginFailed().ToConsole();
Msg.Auth.LoginSuccessful().WithData(new { token = "abc123" }).ToConsole();
Msg.Auth.Unauthorized().ToConsole();

PrintSection("2. CRUD Messages");
Msg.Crud.Created("User").WithData(new { id = 42, name = "Jane" }).ToConsole();
Msg.Crud.NotFound("User").WithMetadata("requestedId", 999).ToConsole();
Msg.Crud.Updated("User").ToConsole();
Msg.Crud.Deleted("User").ToConsole();

PrintSection("3. Validation Messages");
Msg.Validation.RequiredField("Email").ToConsole();
Msg.Validation.InvalidEmail().ToConsole();

PrintSection("4. Output Formatters");
Console.WriteLine("--- JSON ---");
Console.WriteLine(Msg.Auth.LoginFailed().ToJson());

Console.WriteLine("--- PlainText ---");
Console.WriteLine(Msg.Auth.LoginFailed().ToPlainText());

Console.WriteLine("--- XML ---");
Console.WriteLine(Msg.Auth.LoginFailed().ToXml());

PrintSection("5. .Log(ILogger) Chaining");
var json = Msg.Auth.LoginFailed()
    .WithMetadata("attempt", 3)
    .Log(logger)         // logs at Error level via ILogger
    .ToJson();
Console.WriteLine($"JSON after log: {json[..60]}...");

PrintSection("6. Parameter Substitution");
Console.WriteLine(Msg.Crud.Created("Order").WithParams(new { resource = "Order" }).ToJson());

PrintSection("7. Fluent Chaining");
var msg = Msg.Crud.NotFound("Product")
    .WithData(new { requestedId = 42 })
    .WithMetadata("userId", "usr_001")
    .WithCorrelationId(Guid.NewGuid().ToString())
    .WithStatusCode(404);
Console.WriteLine($"Code: {msg.Code}, Status: {msg.HttpStatusCode}, Metadata keys: {msg.Metadata.Count}");

PrintSection("8. Interceptors");
InterceptorRegistry.Register(new LoggingTimestampInterceptor());
Msg.System.Error().ToConsole();

PrintSection("9. Formatter Options");
Console.WriteLine(Msg.Auth.LoginFailed().ToJson(FormatterConfiguration.Verbose));
Console.WriteLine(Msg.Auth.LoginFailed().ToJson(FormatterConfiguration.Minimal));

Console.WriteLine("\n✅ All features working correctly.");

static void PrintSection(string title)
{
    Console.WriteLine();
    Console.WriteLine(new string('=', 60));
    Console.WriteLine($"  {title}");
    Console.WriteLine(new string('=', 60));
}

public class LoggingTimestampInterceptor : IMessageInterceptor
{
    public Message OnBeforeFormat(Message message)
    {
        Console.WriteLine($"  [Interceptor] Before format: {message.Code}");
        return message;
    }

    public Message OnAfterFormat(Message message) => message;
}
  • Step 4: Update RecurPixel.EasyMessages.sln

In RecurPixel.EasyMessages.sln, make the following changes:

Remove these two project entries (lines 20-22 area):

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiSample", "samples\WebApiSample\WebApiSample.csproj", "{A8DF3D7A-B924-425F-85AC-3E1021C38645}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleSample", "samples\ConsoleSample\ConsoleSample.csproj", "{CCC79C15-15A7-4854-81E5-E871695551B3}"
EndProject

Add the Playground entry in their place (keeping the same GUID as ConsoleSample to preserve history):

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyMessages.Playground", "samples\EasyMessages.Playground\EasyMessages.Playground.csproj", "{CCC79C15-15A7-4854-81E5-E871695551B3}"
EndProject

In the GlobalSection(ProjectConfigurationPlatforms) — remove all lines referencing {A8DF3D7A-B924-425F-85AC-3E1021C38645} (WebApiSample, 6 lines). Leave the {CCC79C15-...} lines as-is — they now point to Playground.

In the GlobalSection(NestedProjects) — remove the line:

{A8DF3D7A-B924-425F-85AC-3E1021C38645} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}

The {CCC79C15-...} = {5D20AA90-...} line stays (nests Playground under samples folder).

  • Step 5: Verify the solution builds and the Playground runs
dotnet build RecurPixel.EasyMessages.sln --configuration Release
dotnet run --project samples/EasyMessages.Playground/EasyMessages.Playground.csproj

Expected: Build succeeds. Running the playground prints all 9 sections without exceptions.

  • Step 6: Commit
git add samples/EasyMessages.Playground/
git add RecurPixel.EasyMessages.sln
git commit -m "feat: add EasyMessages.Playground, remove ConsoleSample and WebApiSample"

Task 10: CI Workflow

Files:

  • Create: .github/workflows/ci.yml

  • Step 1: Create the workflow directory and file

mkdir -p .github/workflows

Create .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "10.0.x"

      - name: Restore dependencies
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Run tests
        run: dotnet test --configuration Release --no-build --verbosity normal

      - name: Pack and verify Core package
        run: |
          mkdir -p ./nupkgs
          dotnet pack src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj \
            --configuration Release --no-restore --output ./nupkgs
          pkg_count=$(ls ./nupkgs/*.nupkg | wc -l)
          echo "Packed $pkg_count .nupkg file(s)"
          if [ "$pkg_count" -lt 1 ]; then
            echo "❌ Expected at least 1 package, got $pkg_count"
            exit 1
          fi
          echo "✅ Core package packed successfully"
          ls -lh ./nupkgs/
  • Step 2: Commit
git add .github/workflows/ci.yml
git commit -m "ci: add GitHub Actions CI workflow"

Task 11: Publish Workflow

Files:

  • Create: .github/workflows/publish-nuget.yml

  • Step 1: Create publish-nuget.yml

Create .github/workflows/publish-nuget.yml:

name: Publish to NuGet

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest

    permissions:
      contents: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "10.0.x"

      - name: Restore dependencies
        run: dotnet restore

      - name: Build (Release)
        run: dotnet build --configuration Release --no-restore

      - name: Run tests
        run: dotnet test --configuration Release --no-build --verbosity normal

      - name: Pack NuGet packages
        run: |
          mkdir -p ./nupkgs
          dotnet pack src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj \
            --configuration Release --no-restore --output ./nupkgs
          dotnet pack src/RecurPixel.EasyMessages.AspNetCore/RecurPixel.EasyMessages.AspNetCore.csproj \
            --configuration Release --no-restore --output ./nupkgs

      - name: Push packages in dependency order
        env:
          NUGET_API_KEY: $
        run: |
          set -e

          if [ -z "$NUGET_API_KEY" ]; then
            echo "❌ NUGET_API_KEY not set"
            exit 1
          fi

          push_package() {
            local pkg_name=$1
            local nupkg=$(ls ./nupkgs/${pkg_name}.*.nupkg 2>/dev/null | head -1)
            if [ -z "$nupkg" ]; then
              echo "❌ $pkg_name -- .nupkg not found"
              exit 1
            fi
            echo "  → Pushing $pkg_name"
            dotnet nuget push "$nupkg" \
              --api-key "$NUGET_API_KEY" \
              --source "https://api.nuget.org/v3/index.json" \
              --skip-duplicate \
              --no-symbols
          }

          echo "🚀 Tier 0 — Core (no dependencies)"
          push_package "RecurPixel.EasyMessages"
          echo "✅ Core pushed"

          echo "⏳ Waiting 120s for NuGet indexing..."
          sleep 120

          echo "🚀 Tier 1 — AspNetCore (depends on Core, final deprecated release)"
          push_package "RecurPixel.EasyMessages.AspNetCore"
          echo "✅ AspNetCore pushed"

          echo ""
          echo "✅ All packages published!"
          echo ""
          echo "⚠️  Remember: manually mark RecurPixel.EasyMessages.AspNetCore as deprecated"
          echo "   on nuget.org after packages appear (takes ~15 min to index)."
          echo "   Go to: https://www.nuget.org/packages/RecurPixel.EasyMessages.AspNetCore"
          echo "   → Manage Package → Deprecate"

      - name: Upload packages as release assets
        uses: softprops/action-gh-release@v1
        with:
          files: |
            ./nupkgs/*.nupkg
            ./nupkgs/*.snupkg
        env:
          GITHUB_TOKEN: $

      - name: Print release summary
        run: |
          echo ""
          echo "📦 Release Summary:"
          echo "  Version tag: $"
          echo "  Core v1.0.0:        https://www.nuget.org/packages/RecurPixel.EasyMessages"
          echo "  AspNetCore v0.1.0:  https://www.nuget.org/packages/RecurPixel.EasyMessages.AspNetCore"
  • Step 2: Commit
git add .github/workflows/publish-nuget.yml
git commit -m "ci: add NuGet publish workflow (two-tier, Core then AspNetCore)"

Task 12: Docs Workflow

Files:

  • Create: .github/workflows/docs.yml

  • Step 1: Create docs.yml

Create .github/workflows/docs.yml:

name: Deploy docs to GitHub Pages

on:
  push:
    branches: [main]
    paths:
      - "docs/**"
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: docs
    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
          bundler-cache: true
          working-directory: docs

      - name: Build Jekyll site
        run: bundle exec jekyll build
        env:
          JEKYLL_ENV: production

      - uses: actions/upload-pages-artifact@v3
        with:
          path: docs/_site

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: $
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4
  • Step 2: Commit
git add .github/workflows/docs.yml
git commit -m "ci: add GitHub Pages docs deployment workflow"

Task 13: Docker Test Suite

Files:

  • Create: Dockerfile.test
  • Create: test-workflow.sh

  • Step 1: Create Dockerfile.test

Create Dockerfile.test in the repo root:

# Mirrors GitHub Actions ubuntu-latest environment for local CI validation
FROM mcr.microsoft.com/dotnet/sdk:latest

WORKDIR /workspace

COPY . .

RUN dotnet restore

RUN dotnet build --configuration Release --no-restore

RUN dotnet test --configuration Release --no-build --verbosity normal

RUN rm -rf ./nupkgs && mkdir -p ./nupkgs && \
    dotnet pack src/RecurPixel.EasyMessages/RecurPixel.EasyMessages.csproj \
      --configuration Release --no-restore --output ./nupkgs

RUN echo "📦 Verifying Core package..." && \
    nupkg_count=$(find ./nupkgs -name "RecurPixel.EasyMessages.*.nupkg" ! -name "*.snupkg" | wc -l) && \
    echo "Core .nupkg files found: $nupkg_count" && \
    if [ "$nupkg_count" -ne 1 ]; then \
      echo "❌ ERROR: Expected 1 Core package, found $nupkg_count" && \
      find ./nupkgs -name "*.nupkg" | sort && \
      exit 1; \
    fi && \
    echo "✅ Core package present!" && \
    echo "" && \
    echo "📋 Package:" && \
    find ./nupkgs -name "RecurPixel.EasyMessages.*.nupkg" ! -name "*.snupkg" | nl && \
    echo "" && \
    echo "📊 Size: $(du -sh ./nupkgs | cut -f1)"

CMD ["/bin/bash"]
  • Step 2: Create test-workflow.sh

Create test-workflow.sh in the repo root:

#!/bin/bash
# Test the publish workflow locally using Docker before pushing to GitHub

set -e

PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$PROJECT_DIR"

echo "🐳 Testing EasyMessages CI pipeline locally via Docker"
echo "======================================================="
echo ""

if ! command -v docker &> /dev/null; then
    echo "❌ Docker not found. Install Docker Desktop or Docker CLI."
    exit 1
fi

echo "📦 Building Docker test image..."
docker build -f Dockerfile.test -t easymessages-test:latest .

if [ $? -eq 0 ]; then
    echo ""
    echo "✅ Docker build successful — all tests pass, package verified!"
    echo ""
    echo "📋 Next Steps:"
    echo "  1. Review test output above"
    echo "  2. Push to GitHub: git push origin main"
    echo "  3. Create a GitHub Release tagged v1.0.0"
    echo "  4. GitHub Actions will automatically run tests and publish to NuGet"
    echo "  5. After ~15 min, manually deprecate AspNetCore on nuget.org:"
    echo "     https://www.nuget.org/packages/RecurPixel.EasyMessages.AspNetCore"
    echo ""
else
    echo ""
    echo "❌ Docker build failed. Fix errors above before pushing."
    exit 1
fi
  • Step 3: Make the script executable
chmod +x test-workflow.sh
  • Step 4: Commit
git add Dockerfile.test test-workflow.sh
git commit -m "ci: add Dockerfile.test and test-workflow.sh for local CI validation"

Task 14: Final Verification

  • Step 1: Run the complete test suite one final time
dotnet test --configuration Release --verbosity normal

Expected: All tests PASS (237+ tests across net8.0, net9.0, net10.0). Zero failures. Zero skipped.

  • Step 2: Build with warnings-as-errors
dotnet build --configuration Release /warnaserror

Expected: Build succeeded with 0 Warning(s) and 0 Error(s).

  • Step 3: Run the Playground end-to-end
dotnet run --project samples/EasyMessages.Playground/EasyMessages.Playground.csproj

Expected: All 9 sections print without exceptions. Section 5 (Log(ILogger) Chaining) should produce console log output from the ILogger.

  • Step 4: Run local Docker test
./test-workflow.sh

Expected: ✅ Docker build successful printed at the end.

  • Step 5: Check git log is clean
git log --oneline -10
git status

Expected: Working tree clean. Commits visible in log.

  • Step 6: Final commit if any cleanup needed, then push
git push origin main
  • Step 7: Create GitHub Release

Go to: https://github.com/RecurPixel/RecurPixel.EasyMessages/releases/new

  • Tag: v1.0.0
  • Title: v1.0.0 — Stable Release
  • Description: Copy the [1.0.0] section from CHANGELOG.md
  • Mark as latest release (not pre-release)
  • Publish — this triggers the publish-nuget.yml workflow automatically

  • Step 8: Mark AspNetCore as deprecated on NuGet.org

After ~15 minutes (NuGet indexing):

  1. Go to https://www.nuget.org/packages/RecurPixel.EasyMessages.AspNetCore
  2. Click Manage Package (requires NuGet.org account login)
  3. Click Deprecate
  4. Select reason: Other
  5. Message: Use RecurPixel.EasyMessages v1.0.0 instead. See https://github.com/RecurPixel/RecurPixel.EasyMessages for migration guide.
  6. Optionally set Alternate Package: RecurPixel.EasyMessages v1.0.0
  7. Submit
  • Step 9: Verify both packages on NuGet

  • Core: https://www.nuget.org/packages/RecurPixel.EasyMessages — should show 1.0.0
  • AspNetCore: https://www.nuget.org/packages/RecurPixel.EasyMessages.AspNetCore — should show 0.1.0 with deprecation warning

Self-Review Notes

  • All 9 MessageLoggingExtensionsTests have complete test code — no placeholders
  • MapToLogLevel is private to MessageExtensions (same partial class) — no naming conflict with AspNetCore’s old LoggingExtensions.MapToLogLevel
  • HttpStatusCode is int? on Message — migration guide uses ?? 200 and ?? 401 correctly
  • NuGet deprecation cannot be done via .csproj — Task 14 Step 8 covers the manual nuget.org step
  • WebApiSample GUID {A8DF3D7A-...} is fully removed from .sln in Task 9
  • Playground uses {CCC79C15-...} (former ConsoleSample GUID) to preserve solution history
  • Docker verifies exactly 1 Core .nupkg — AspNetCore is packed separately only at publish time