The Problem That Wouldn't Go Away
Let me paint you a familiar picture. You're deep in code, building out your API endpoints, and you write:
return Ok("User created successfully");
Simple enough, right?
But then QA comes back: "Hey, the /users endpoint says 'User created' but the /products endpoint says 'Product added successfully'. Can we be consistent?"
So you fix it. Then another bug report: "The error message for duplicate emails is different from duplicate usernames."
You fix that too. Then: "Can we add status codes to all error messages?"
At this point, you've spent more time fixing inconsistent messages than actually building features. Sound familiar?
I got tired of this. Every project, same problem. Scattered messages, inconsistent formats, forgotten logging, missing correlation IDs. There had to be a better way.
The Simple Idea
It started with a simple thought: "Why not create a helper class with pre-defined messages?"
// Instead of this everywhere:
return BadRequest("User not found");
return Ok("Success");
return StatusCode(500, "Error occurred");
// What if I could just write:
return Msg.Crud.Created("User").ToApiResponse();
return Msg.Crud.NotFound("User").ToApiResponse();
Clean. Discoverable. Consistent.
I thought: "I'll whip this up in a weekend."
Narrator: He did not whip it up in a weekend.
Scope Creep (The Good Kind)
As I started gathering industry-standard messages, something happened. What began as "CRUD messages" kept expanding:
Week 1: CRUD messages (Create, Read, Update, Delete)
Msg.Crud.Created("User")
Msg.Crud.NotFound("Product")
Week 2: Validation messages
Msg.Validation.RequiredField("Email")
Msg.Validation.InvalidFormat("Phone")
Week 3: Authentication messages
Msg.Auth.LoginFailed()
Msg.Auth.SessionExpired()
By Week 4, I had 13 different message categories:
Msg.Auth.* // Authentication
Msg.Crud.* // CRUD operations
Msg.Validation.* // Input validation
Msg.System.* // System errors
Msg.Database.* // Database errors
Msg.File.* // File operations
Msg.Network.* // Network issues
Msg.Payment.* // Payment processing
Msg.Email.* // Email operations
Msg.Search.* // Search results
Msg.Import.* // Import operations
Msg.Export.* // Export operations
Msg.Custom(code) // Your custom messages
I realized: This isn't just a helper class anymore. This is a library.
The Questions That Changed Everything
At this point, I had a decision to make. I could release what I had—a simple collection of messages—and call it done. Or I could ask harder questions:
Question 1: "Where will this actually be used?"
Console apps? Sure. But mostly? ASP.NET Core APIs.
That meant I needed:
- Proper HTTP status codes (not just strings)
- Integration with
IActionResult - Support for Minimal APIs
- JSON serialization
Question 2: "How will developers actually use it?"
They need IntelliSense. They need discoverability. They need fluent APIs:
return Msg.Crud.Created("User")
.WithData(user)
.WithMetadata("source", "API")
.WithCorrelationId(traceId)
.Log(_logger)
.ToApiResponse();
Each method returns a new immutable instance. Chainable. Type-safe. Beautiful.
Question 3: "What about the output?"
APIs need JSON. Console apps need colored output. Legacy systems need XML. So:
var message = Msg.Crud.Created("User");
// Different outputs, same message
var json = message.ToJson();
var xml = message.ToXml();
message.ToConsole(useColors: true);
var result = message.ToApiResponse();
Question 4: "What about customization?"
Not everyone needs the same fields. Development needs verbose logging. Production needs minimal, privacy-safe responses:
// Development: Everything for debugging
builder.Services.AddEasyMessages(
config,
EasyMessagesPresets.Development
);
// Production: Minimal, fast, private
builder.Services.AddEasyMessages(
config,
EasyMessagesPresets.Production
);
The Architecture Emerged
Once I started asking these questions, the architecture fell into place:
1. Message Registry - Static, thread-safe storage of all message templates
2. Facades - Msg.Auth.*, Msg.Crud.* for discoverability
3. Fluent API - Chainable extension methods
4. Formatters - JSON, XML, Console, PlainText
5. Interceptors - Auto-add correlation IDs, log automatically
6. ASP.NET Core Integration - First-class DI and middleware support
The library structure became:
- Core library: Message primitives, formatters, registry
- AspNetCore library: DI, interceptors, presets
This kept it modular. Want just the core? Install one package. Need full ASP.NET Core integration? Install both.
The AI-Powered Testing Revolution
Here's where it got interesting. I had built this entire library, but writing tests? That's the boring part everyone hates.
Then I thought: "What if I use AI to help write tests?"
I fed Claude (yes, I integrated AI in my workflow) my code and said: "Write comprehensive tests for this."
The result? 108 tests across 9 test files:
- Unit tests for all configuration classes
- Integration tests for DI
- Validation tests for all edge cases
- Backward compatibility tests
- Theory tests for multiple scenarios
The AI didn't just write tests—it found edge cases I hadn't considered:
- "What if someone passes an empty locale string?"
- "What if multiple validation errors occur?"
- "What if the config file doesn't exist?"
I reviewed every test, fixed what needed fixing, and ended up with better test coverage than I would have written manually. And it took hours instead of days.
The Performance Obsession
Once the library worked, I obsessed over performance. Every allocation mattered. Every string operation counted.
Optimization 1: String Caching
Before:
message.Type.ToString().ToLowerInvariant() // Called every time
After:
private static readonly Dictionary<MessageType, string> Cache = new()
{
[MessageType.Success] = "success",
[MessageType.Error] = "error",
// ...
};
Result: 5-10% throughput increase, 24 bytes saved per call.
Optimization 2: Smart Metadata Handling
Before:
// Always included, even when empty
Metadata = message.Metadata
After:
// Only include if non-empty
Metadata = message.Metadata?.Count > 0 ? message.Metadata : null
Result: Smaller JSON payloads, ~96 bytes saved per message.
Optimization 3: Conditional String Replacement
Before:
// Always replaced, even if placeholder doesn't exist
title = title.Replace(placeholder, value);
After:
// Only replace if placeholder exists
if (title.Contains(placeholder, StringComparison.OrdinalIgnoreCase))
{
title = title.Replace(placeholder, value, StringComparison.OrdinalIgnoreCase);
}
Result: 10-15% faster for parameterized messages.
Combined Impact
Under load (10,000 requests/sec):
- Before: ~2.1 MB/sec allocations
- After: ~1.5 MB/sec allocations
- Improvement: 28% reduction in GC pressure, 18-30% throughput increase
I documented every optimization in PERFORMANCE_OPTIMIZATIONS.md.
Make It Work, Make It Fast, Make It Clean
I followed this mantra religiously:
Phase 1: Make It Work
Build the core functionality. Get it working end-to-end. Don't optimize yet.
Duration: 2 weeks
Phase 2: Make It Fast
Profile. Benchmark. Optimize hot paths. Reduce allocations.
Duration: 1 week
Phase 3: Make It Clean
Refactor. Document. Write guides. Polish the API.
Duration: 2 weeks
Total: ~5 weeks from idea to v0.1.0-beta.1
The Documentation Philosophy
I wanted the docs to be amazing. Not just "here's the API reference" but actually helpful.
So I wrote:
- 15 comprehensive guides (30,000+ lines)
- 200+ working code examples
- How-to guides for common scenarios
- Troubleshooting sections for every feature
- Architecture deep-dive for contributors
Every feature has:
- What it is - Clear explanation
- Why it exists - The problem it solves
- How to use it - Working examples
- When to use it - Best practices
- When NOT to use it - Common pitfalls
Let me tell you a secret between you and me. Even though I documented all the process, code, architecture it was a mess so I decided to first gather all stuff and then make use of AI to organize it. You can checkout my repo to understand more. Yea, I am not a real dev but just an imposter like you no shame “Zehaha”
Example from the docs:
### [✓] DO:
Use facades instead of message codes:
// Good
Msg.Auth.LoginFailed()
// Bad
MessageRegistry.Get("AUTH_001")
### [❌] DON'T:
Don't expose sensitive data in production:
// Bad
return Msg.Auth.LoginFailed()
.WithMetadata("password", password) // Security risk!
The Launch
I published v0.1.0-beta.1 to NuGet. No fanfare. No marketing. Just:
- GitHub repo
- Comprehensive docs
- A Reddit post
Week 1: 50 downloads
Week 2: 150 downloads
Week 3: 300 downloads
Month 1: 500+ downloads
Not viral by any means, but actual developers using it in real projects. That felt amazing.
What I Learned
1. Scope Management Is Hard
What started as a weekend project became 5 weeks of work. But the expanded scope made it actually useful instead of just "neat."
2. AI Is a Superpower for Testing
I'm not embarrassed to say AI wrote most of my tests. I reviewed them all, but it saved me days of work and found edge cases I missed.
3. Performance Matters, Even for Libraries
Nobody wants a library that slows down their API. Every allocation counts. Profile. Optimize. Document.
4. Documentation Is Your Product
Code is for computers. Docs are for humans. I spent as much time on docs as code. Worth it.
5. Open Source Is Scary and Rewarding
Putting your code out there is terrifying. What if it's bad? What if no one uses it? What if someone finds a bug?
But then someone opens an issue. Someone submits a PR. Someone says "this solved my problem."
That's why we do this.
What's Next?
The library is in beta. It works. It's fast. It's documented.
What comes next depends on you.
If developers use it, I'll keep improving it:
- More message codes based on feedback
- Additional ASP.NET Core middleware
- Better localization support
- Performance benchmarks
- More presets
But if no one uses it? That's okay too. I learned a ton. I built something I'm proud of. I contributed to the ecosystem.
Try It Yourself
dotnet add package RecurPixel.EasyMessages.AspNetCore --version 0.1.0-beta.1
Then in your API:
// Program.cs
builder.Services.AddEasyMessages(builder.Configuration);
// Controller
[HttpPost]
public IActionResult Create(CreateUserDto dto)
{
if (!ModelState.IsValid)
return Msg.Validation.Failed()
.WithData(new { errors = ModelState })
.ToApiResponse();
var user = _service.Create(dto);
return Msg.Crud.Created("User")
.WithData(user)
.Log(_logger)
.ToCreated($"/api/users/{user.Id}");
}
That's it. Consistent messages. Automatic logging. Standard responses.
Links
- Docs: https://recurpixel.github.io/RecurPixel.EasyMessages
- GitHub: https://github.com/RecurPixel/RecurPixel.EasyMessages
- NuGet: https://nuget.org/profiles/RecurPixel
Final Thoughts
Building my first .NET package taught me:
- Start simple, but ask hard questions
- Scope creep can be good (if intentional)
- AI is a tool, not a replacement
- Performance matters
- Docs are as important as code
- Open source is scary but worth it
If you're thinking about building a library, just start. It won't be perfect. It might take longer than expected. But you'll learn more than any tutorial could teach you.
And who knows? Maybe your "weekend project" becomes something developers actually use.
What's your "simple idea that got complicated"? Let me know in the comments!
P.S. If you use EasyMessages and it helps you, ⭐ the repo on GitHub. It makes a huge difference for discoverability.
P.P.S. Found a bug? Open an issue. I promise I won't cry. (Much.)
Top comments (0)