I recently set out to publish my first NuGet package (and also my first published article) as a weekend learning challenge.
My goal wasn't to build something big, just to understand the full process from writing a small library to automating its release on NuGet.
The result (pun intended) was StrongResult, a lightweight, immutable implementation of the result pattern for C#.
What You'll find here
In this post, I'll walk you through:
- Why I built StrongResult and what it does
- How it implements the result pattern in a simple, modern way
- How to publish and automate releases to NuGet with GitHub Actions
- What I learned from the process
Why a Result Pattern?
The result pattern is a clean, type-safe way to represent the outcome of operations.
Instead of relying on exceptions or ambiguous return values, it makes success, failure, and warnings explicit and predictable.
It's a concept borrowed from functional programming and modernized for C#.
You can think of it as a more intentional, structured way to handle outcomes.
Project Goals
- Learn NuGet publishing: My main goal was to understand the workflow for creating and publishing a package.
- Keep it simple: I wanted a minimal, easy to understand implementation.
- Make it useful: Even though it was a learning project, I wanted the result to be production quality.
What Is StrongResult?
StrongResult provides two main types:
-
Result
for operations without a return value. -
Result<T>
for operations that return a value.
It's designed to be small, explicit, and dependency free. A personal take on the result pattern.
Installing StrongResult
You can install the package directly from NuGet:
dotnet add package StrongResult
The Result Pattern Implementation
Although the main goal was to learn about NuGet publishing, I wanted the library to be genuinely useful.
StrongResult offers a lightweight, immutable implementation of the result pattern.
Result Kinds
Every result is classified as one of four kinds:
- HardSuccess: Operation completed successfully, no warnings.
- PartialSuccess: Operation succeeded, but with warnings.
- ControlledError: Operation failed in a controlled or expected way, possibly with warnings.
- HardFailure: Operation failed due to an unrecoverable error.
This makes it easy to distinguish between outcomes and handle them appropriately.
Immutability and Thread Safety
All result types are immutable and thread-safe, so you can safely use them in concurrent or async contexts.
Factory Methods
StrongResult uses static factory methods for clarity and type-safety:
-
Ok()
- Hard success (with or without value). -
PartialSuccess()
- Success with warnings. -
ControlledError()
- Expected failure. -
Fail()
- Hard failure or exception.
Examples
Non-Generic Results
using StrongResult.NonGeneric;
var ok = Result.Ok();
var partial = Result.PartialSuccess(Warning.Create("W1", "Minor issue"));
var controlled = Result.ControlledError(
Error.Create("E1", "Validation failed"),
Warning.Create("W2", "Extra info")
);
var fail = Result.Fail(Error.Create("E2", "Hard failure"));
var failFromException = Result.Fail(new InvalidOperationException("Exception failure"));
Generic Results
using StrongResult.Generic;
var ok = Result<string>.Ok("value");
var partial = Result<string>.PartialSuccess("value", Warning.Create("W1", "Minor issue"));
var controlled = Result<string>.ControlledError(
Error.Create("E1", "Validation failed"),
Warning.Create("W2", "Extra info")
);
var fail = Result<string>.Fail(Error.Create("E2", "Hard failure"));
var failFromException = Result<string>.Fail(new InvalidOperationException("Exception failure"));
Errors and Warnings
Errors and warnings are first-class citizens in StrongResult.
They're represented by the IError
and IWarning
interfaces:
-
IError: Defines a
Code
andMessage
for explicit, typed error handling. - IWarning: Same, for attaching diagnostic info to successful results.
Example:
Error.Create("E1", "An error occurred");
Warning.Create("W1", "This is a warning");
This keeps diagnostics consistent and human-readable throughout your codebase.
Exception Handling
The Fail
method can also take an exception and convert it into an error:
var result = Result.Fail(new InvalidOperationException("Something went wrong"));
For generic results:
var result = Result<string>.Fail(new InvalidOperationException("Something went wrong"));
This ensures all exceptions are captured and handled consistently.
Handling Results
You can easily check whether a result succeeded and access its details:
if (result.IsSuccess)
{
// Use result.Value for generic results
}
else
{
// Handle result.Error and result.Warnings
}
Detecting Result Kinds
if (result.Kind == ResultKind.PartialSuccess)
{
// Handle warnings
}
else if (result.Kind == ResultKind.ControlledError)
{
// Handle controlled errors
}
Fluent APIs and Async Workflows
StrongResult also provides fluent APIs for chaining operations functionally:
var result = Result<int>.Ok(5)
.Map(x => x * 2)
.Bind(x => x > 0
? Result<string>.Ok($"Value is {x}")
: Result<string>.Fail(Error.Create("E2", "Negative value"))
)
.OnSuccess(value => Console.WriteLine($"Success: {value}"))
.OnFailure(error => Console.WriteLine($"Error: {error.Code} - {error.Message}"))
.OnWarnings(warnings => Console.WriteLine($"Returned with {warnings.Count} warnings"))
.ForEachWarning(warning => Console.WriteLine($"Warning: {warning.Code} - {warning.Message}"));
Async versions are also available:
await Result<int>.Ok(5)
.MapAsync(async x => x * 2)
.BindAsync(async x => x > 0
? Result<string>.Ok($"Value is {x}")
: Result<string>.Fail(Error.Create("E2", "Negative value"))
)
.OnSuccessAsync(async value => await Console.Out.WriteLineAsync($"Success: {value}"))
.OnFailureAsync(async error => await Console.Out.WriteLineAsync($"Error: {error.Code} - {error.Message}"))
.OnWarningsAsync(async warnings => await Console.Out.WriteLineAsync($"Returned with {warnings.Count} warnings"))
.ForEachWarningAsync(async warning => await Console.Out.WriteLineAsync($"Warning: {warning.Code} - {warning.Message}"));
This makes StrongResult a great fit for modern async/await codebases.
Publishing to NuGet
After writing the code and tests, I followed the official NuGet publishing guide.
The process was straightforward:
- Add metadata in your
.csproj
. - Run
dotnet pack
. - Push with
dotnet nuget push
.
You can find StrongResult here:
👉 https://www.nuget.org/packages/StrongResult
Automating Releases with GitHub Actions
To automate releases, I set up a GitHub Actions workflow that builds and publishes the package whenever a new version tag (like v1.0.0
) is pushed.
Why Tag-Based Releases?
This ensures consistent versioning your NuGet version always matches your Git tag.
Tag and push like this:
git tag v1.0.0 && git push origin v1.0.0
Make sure your NuGet API key is stored in
Settings > Secrets > Actions > New repository secret > NUGET_API_KEY
Here's the actual workflow:
name: Publish NuGet Package
on:
push:
tags:
- "v*"
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- run: dotnet restore StrongResult/StrongResult.csproj
- run: dotnet build StrongResult/StrongResult.csproj --configuration Release --no-restore
- run: cp README.md StrongResult/README.md
- name: Get version from tag
id: get_version
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- run: dotnet pack StrongResult/StrongResult.csproj --configuration Release --no-build --output ./nupkg /p:PackageVersion=${{ steps.get_version.outputs.version }}
- run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
What I Learned
-
NuGet structure: How to prepare a
.csproj
for packaging. - Documentation matters: Good examples and README make adoption easier.
- CI/CD automation: How to safely publish with tags and GitHub secrets.
- Version control discipline: Tags keep releases consistent and traceable.
Next Steps
In future versions, I plan to:
- Port to typescript/javascript as an npm package
- Provide JSON serialization support.
- Introduce nullable-friendly helpers.
Feedback and PRs are always welcome!
Final Thoughts
This was a fun weekend project and a great way to learn about NuGet publishing.
If you're interested in a simple, modern result pattern for .NET, give StrongResult a try or use it as inspiration for your own learning journey.
Check out the code: GitHub - StrongResult
Top comments (0)