DEV Community

Cover image for Publishing My First NuGet Package: StrongResult - A Weekend Learning Project
Saúl Hernández
Saúl Hernández

Posted on

Publishing My First NuGet Package: StrongResult - A Weekend Learning Project

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
Enter fullscreen mode Exit fullscreen mode

NuGet

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"));
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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 and Message 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");
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

For generic results:

var result = Result<string>.Fail(new InvalidOperationException("Something went wrong"));
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Detecting Result Kinds

if (result.Kind == ResultKind.PartialSuccess)
{
  // Handle warnings
}
else if (result.Kind == ResultKind.ControlledError)
{
  // Handle controlled errors
}
Enter fullscreen mode Exit fullscreen mode

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}"));
Enter fullscreen mode Exit fullscreen mode

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}"));
Enter fullscreen mode Exit fullscreen mode

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:

  1. Add metadata in your .csproj.
  2. Run dotnet pack.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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

GitHub stars

Top comments (0)