Skip to main content

Command Palette

Search for a command to run...

Top 10 Unexpected NuGet Packages for .NET Developers

Updated
•11 min read•View as Markdown
Top 10 Unexpected NuGet Packages for .NET Developers

The NuGet packages that save the most time are often not general-purpose frameworks. They solve one awkward production problem—geospatial formats, filesystem testability, regular-expression data generation, schema inspection, or type-safe tree traversal—well enough that you do not have to build another internal utility.

This selection includes ten packages I have shipped in commercial systems. That experience is not a substitute for evaluating maintenance status, target frameworks, transitive dependencies, licensing, and fit for your own deployment constraints.

WireMock.Grpc.Protobuf

I built WireMock.Grpc.Protobuf to configure gRPC mocks in WireMock.Net with the generated Google.Protobuf message types already used by the client. Its WithBodyAsGoogleProtobuf extension methods let you match requests against an expected message or a typed predicate and build responses from typed protobuf objects. This removes the need to load .proto files at runtime, refer to message types by strings, or convert payloads through JSON. It is useful for component tests that exercise a real gRPC client against a mock server while keeping request and response setup tied to the C# contracts.

Suppose your greet.Greeter service contract generates HelloRequest and HelloReply types in the Greeting.Contracts namespace. You can match an exact request and return a typed response like this:

using Greeting.Contracts;
using WireMock.Net.Google.Protobuf.Request;
using WireMock.Net.Google.Protobuf.Response;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using WireMock.Settings;

using var server = WireMockServer.Start(new WireMockServerSettings
{
    UseHttp2 = true
});

server
    .Given(Request.Create()
        .UsingPost()
        .WithHttpVersion("2")
        .WithPath("/greet.Greeter/SayHello")
        .WithBodyAsGoogleProtobuf(new HelloRequest { Name = "StepOne" }))
    .RespondWith(Response.Create()
        .WithHeader("Content-Type", "application/grpc")
        .WithTrailingHeader("grpc-status", "0")
        .WithBodyAsGoogleProtobuf(new HelloReply { Message = "Hello, StepOne!" }));

If the test only cares about selected fields, replace the request builder above with a typed predicate:

Request.Create()
    .UsingPost()
    .WithHttpVersion("2")
    .WithPath("/greet.Greeter/SayHello")
    .WithBodyAsGoogleProtobuf((HelloRequest request) => request.Name.StartsWith("Step"));

The server needs HTTP/2 enabled. WithBodyAsGoogleProtobuf already produces a binary gRPC response, so do not add WithTransformer() to that response.

NuGet
GitHub

Geo

I once had to retrieve data from MongoDB using a geospatial query, then run a geospatial search over that data in SQL Server. A strange setup, but that is "microservices" for you.

The code was finished and worked locally on Windows, then failed spectacularly in staging. It turned out that Linux cannot work with Microsoft.SqlServer.Types.SqlGeometry. Nothing helped, not even dotMorten's mock package, dotMorten.Microsoft.SqlServer.Types. The problem was too low-level, buried in the client library's DLL bindings.

In essence, I needed to serialize a geographic object into a SQL query so that the DBMS could construct the geometry object itself. It looks roughly like this:

DECLARE @g geometry;
DECLARE @h geometry;
SET @g = geometry::STGeomFromText('POLYGON((0 0, 2 0, 2 2, 0 2, 0 0))', 0);
SET @h = geometry::STGeomFromText('POINT(1 1)', 0);

At the query level, the DBMS works with WKT strings.
Well-Known Text (WKT) plays a role similar to JSON for geospatial data: it is a text format for representing vector geometry and describing coordinate systems. Here is a useful diagram with examples:

Examples of serializing objects to WKT strings

Examples of serializing objects to WKT strings

I solved the problem with OOP after finding the Geo library. Geo is a library for spatial objects that models the geographic domain. It lets you construct and serialize geographic objects like this:

var settings = new WktWriterSettings
{
    LinearRing = false;
    Triangle = false;
    DimensionFlag = true;
    NullOrdinate = Coordinate.NullOrdinate.ToString(CultureInfo.InvariantCulture);
    MaxDimesions = 4;
};
var writer = new WktWriter(settings);
var pointString = writer.Write(new Point(68.389, 73.89));

NuGet
GitHub

CoordinateSharp

As a programmer who rarely worked with coordinates, I had assumed that points came in two forms: [x, y] in 2D and [x, y, z] in 3D. THAT WAS IT. A game-development job test showed me just how wrong I was.

The assignment required handling coordinates for any object in the game world. It turns out that coordinates have different formats, and you can convert between them—at minimum, (latitude, longitude) <-> Cartesian (x, y).

Finding CoordinateSharp saved me from reinventing another wheel. A kind developer named Justin wrote a simple .NET library for geographic-coordinate conversion, parsing, formatting, and related tasks. For example, you can construct the coordinates of Seattle at 10:10 a.m. on June 5, 2018, like this:

//Seattle coordinates on 5 Jun 2018 @ 10:10 AM (UTC)
//Signed-Decimal Degree    47.6062, -122.3321
//Degrees Minutes Seconds  N 47º 36' 22.32" W 122º 19' 55.56"
/***********************************************************/
var c = new Coordinate(47.6062, -122.3321, new DateTime(2018, 6, 5, 10, 10, 0));

The coordinate-conversion service I submitted for that game-development test looked like this, and it was accepted:

internal sealed class CoordinatesConverter : ICoordinatesConverter
{
    public double[] ToGeo(int x, int y)
    {
        var coordinate = Cartesian.CartesianToLatLong(x, y, 0);
        return [coordinate.Longitude.ToDouble(), coordinate.Latitude.ToDouble()];
    }

    public int[] ToCartesian(double lat, double lng)
    {
        var cartesian = new Coordinate(lat, lng).Cartesian;
        return [double.ConvertToInteger<int>(cartesian.X), double.ConvertToInteger<int>(cartesian.Y)];
    }
}

NuGet
GitHub

TestableIO.System.IO.Abstractions

Have you ever written code that works with files and then discovered that you cannot unit test it? You want to verify what gets written, when, where, and how, but you cannot.

Take a look at System.IO.Abstractions. It makes testing I/O operations easier.

The idea is simple: static System.IO methods such as File.WriteAllText become available through a set of dedicated abstractions. The code under the hood is the same, but now it is injectable and testable.

For example, I use the package in my hydrascript programming language to mock the file system and isolate the logic that dumps debug files:

services.AddSingleton<IFileSystem, FileSystem>();
// ...
internal sealed class DumpingService(
    IFileSystem fileSystem,
    IOptions<FileInfo> fileInfo) : IDumpingService
{
    public void Dump(string? contents, string fileExtension)
    {
        var fileNameWithExtension = fileInfo.Value.Name;
        var originalFileExtension = fileInfo.Value.Extension;
        var fileName = fileNameWithExtension.Replace(originalFileExtension, string.Empty);
        var path = Path.Combine(
            fileInfo.Value.DirectoryName ?? string.Empty,
            ZString.Concat(fileName, '.', fileExtension));
        fileSystem.File.WriteAllText(path, contents);
    }
}

Explore HydraScript on GitHub.

NuGet
GitHub

EnvironmentAbstractions

The file system is not the only thing you can mock and inject through DI. You can do the same with the environment-variable API. EnvironmentAbstractions provides abstractions over the static System.Environment class so that .NET applications can consume this logic through interfaces.

Two interfaces are available: IEnvironmentProvider and IEnvironmentVariableProvider. IEnvironmentProvider mirrors the contents of System.Environment, while IEnvironmentVariableProvider exposes only the environment-variable API.

I use this package in hydrascript to implement runtime access to environment variables:

services.AddSingleton(SystemEnvironmentVariableProvider.Instance);
// ...
public sealed class EnvFrame(IEnvironmentVariableProvider provider) : IFrame
{
    public object? this[string id]
    {
        get => provider.GetEnvironmentVariable(id) ?? string.Empty;
        set => provider.SetEnvironmentVariable(id, value?.ToString());
    }
}

NuGet
GitHub

Fare

Fare stands for Finite Automata and Regular Expressions.

You have probably never heard of this NuGet package. Fare is a port of the Java libraries dk.brics.automaton and xeger that generates text from a given regular expression. For example:

using Fare;
var regex = "[a-zA-Z]+";
var xeger = new Xeger(regex);
var text = xeger.Generate();

The obvious use case is testing: you can generate phone numbers, email addresses, and specification-compliant data. It is also convenient because AutoFixture includes the package transitively.

For example, I use Fare in hydrascript to generate a test token stream for the lexer:

public record LexerInput([property: MinLength(10), MaxLength(25)] TokenInput[] TokenInputs) : IReadOnlyList<string>
{
    public IEnumerator<string> GetEnumerator() =>
        TokenInputs.Select(x => x.Value).GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public int Count => TokenInputs.Length;

    public string this[int index] => TokenInputs[index].Value;

    public override string ToString() =>
        TokenInputs.Aggregate(
            TokenInput.AdditiveIdentity,
            (x, y) => x + y).Value;
}

public record TokenInput(
    [property: RegularExpression(TokenInput.Pattern)]
    string Value) :
    IAdditiveIdentity<TokenInput, TokenInput>,
    IAdditionOperators<TokenInput, TokenInput, TokenInput>
{
    [StringSyntax(StringSyntaxAttribute.Regex)]
    public const string Pattern = "[a-zA-Z]+|[0-9]+|[+]{2}";

    public static TokenInput operator +(TokenInput left, TokenInput right) =>
        new(left.Value + " " + right.Value);

    public static TokenInput AdditiveIdentity { get; } = new(string.Empty);
}
// ...
var fixture = new AutoFixture.Fixture();
var lexerInput = fixture.Create<LexerInput>().ToString();

NuGet
GitHub

NUT — Number To Text

Fintech systems often need to convert a numeric amount into words. For example, we may want a Translate function like this:

Translate(100m).Should().Be("Сто рублей ноль копеек");

The NUT package solves this problem. It supports several currencies and languages and offers a rich set of options:

using Nut;
Console.WriteLine(100m.ToText("rub", "ru", new Options { MainUnitFirstCharUpper = true });
// Сто рублей ноль копеек

NuGet
GitHub

DbMocker

Sometimes you just want to write a straightforward SQL query without an ORM. Perhaps nonfunctional performance requirements demand it, or perhaps the local senior developer simply wants it.

But despite ADO.NET abstractions such as DbConnection and DbCommand, it is not obvious how to test this code.

Integration tests can seem like the only option, at which point their most devoted advocates may begin celebrating and declaring that unit tests are unnecessary.

DbMocker solves the problem and lets you write real unit tests for an ADO.NET-based DAL. Suppose, for example, that a service compares the row counts in two tables. You can mock it like this:

var mockDbConnection = new MockDbConnection();
mockDbConnection.Mocks
    .When(cmd => cmd.CommandText.Contains("count(*) from t1"))
    .ReturnsTable(
        MockTable.WithColumns("Count")
            .AddRow(1));
mockDbConnection.Mocks
    .When(cmd => cmd.CommandText.Contains("count(*) from t2"))
    .ReturnsTable(
        MockTable.WithColumns("Count")
            .AddRow(2));

NuGet
GitHub

DatabaseSchemaReader

In 2024, I worked on automating validation for data transfers between databases. Given the constraints and requirements, we decided to perform cumulative hashing inside the DBMS and compare the results in the C# client.

I needed to generate SQL scripts from the database schema to hash a specific table. To prototype quickly and defend the proof of concept to management, I chose a ready-made solution and found DatabaseSchemaReader.

The library provides a .NET Standard facade for reading database metadata through an ADO.NET connection. According to its documentation, it supports many servers:

  • SqlServer

  • Oracle

  • MySql

  • PostgreSql

  • SQLite

  • SqlServerCe 4

  • DB2

  • Firebird

  • Intersystems Cache

  • Ingres

  • Sybase AnyWhere (ASA)

  • Sybase UltraLite

  • Sybase ASE

  • Access 97 and Access 2007

  • VistaDB

Getting started and reading a schema is simple:

using (var connection = new SqlConnection("cnn string"))
{
    var dbReader = new DatabaseReader(connection);
    var schema = dbReader.ReadAll();
    foreach (var table in schema.Tables)
    {
        //do something with your model
    }
}

The API has a convenient object-oriented model that makes unit tests a pleasure rather than a chore:

var table = new DatabaseTable
{
    Name = "t",
    Columns =
    {
        new DatabaseColumn { Name = "a" }
    }
};

While testing in production, I hit an unpleasant issue: schema-reading speed varies between database systems. PostgreSQL was extremely slow in this setup. The solution was caching the schema in an XML file on disk, because the library supports serialization and deserialization:

using (var stream = File.Open("cache.xml", FileMode.Create))
{
    var serializer = new XmlSerializer(typeof(DatabaseSchema));
    serializer.Serialize(stream, dbSchema);
}
// ...
using (var stream = File.Open("cache.xml", FileMode.Open))
{
    var serializer = new XmlSerializer(typeof(DatabaseSchema));
    var dbSchema = serializer.Deserialize(stream) as DatabaseSchema;
    DatabaseSchemaFixer.UpdateReferences(dbSchema);
    DatabaseSchemaFixer.UpdateReferences(dbSchema);
}

NuGet
GitHub

Visitor.NET

Visitor is one of the trickier Gang of Four patterns.

Common C# implementations either couple visitors to the full hierarchy or introduce limitations through dynamic type conversion.

I built an implementation that combines the acyclic version of the pattern with generic variance, allowing multimethod-like calls to be resolved at compile time.

I described its history and mechanics in "A Visitor You Have Never Seen Before: Visitor.NET".

The package is used extensively in hydrascript for static analysis and intermediate-representation code generation while traversing abstract syntax tree nodes. For example, this is how the compiler checks whether every branch of a function body contains a return:

internal class ReturnAnalyzer : VisitorBase<IAbstractSyntaxTreeNode, bool>,
    IVisitor<FunctionDeclaration, ReturnAnalyzerResult>,
    IVisitor<IfStatement, bool>,
    IVisitor<ReturnStatement, bool>
{
    private readonly List<ReturnStatement> _returnStatements = [];

    public ReturnAnalyzerResult Visit(FunctionDeclaration visitable)
    {
        IAbstractSyntaxTreeNode astNode = visitable;
        var codePathEndedWithReturn = Visit(astNode);
        var returnStatements = new List<ReturnStatement>(_returnStatements);
        ReturnAnalyzerResult result = new(codePathEndedWithReturn, returnStatements);
        _returnStatements.Clear();
        return result;
    }

    public override bool Visit(IAbstractSyntaxTreeNode visitable)
    {
        for (var i = 0; i < visitable.Count; i++)
        {
            var visitableResult = visitable[i].Accept(This);
            if (visitableResult)
                return true;
        }
        return false;
    }

    public bool Visit(IfStatement visitable)
    {
        var thenReturns = visitable.Then.Accept(This);
        if (visitable.Else is null)
            return false;
        var elseReturns = visitable.Else.Accept(This);
        return thenReturns && elseReturns;
    }

    public bool Visit(ReturnStatement visitable)
    {
        _returnStatements.Add(visitable);
        return true;
    }
}

public sealed record ReturnAnalyzerResult(
    bool CodePathEndedWithReturn,
    IReadOnlyList<ReturnStatement> ReturnStatements);

NuGet
GitHub

How to Evaluate a Lesser-Known NuGet Package

These packages cover real commercial software problems in the following areas:

  • gRPC mocking

  • Geospatial data

  • Files and environment variables

  • Databases

  • Regular expressions

  • Converting numbers to words

  • Type-safe visitor traversal for data processing

The selection criterion is narrow usefulness, not universal adoption. Before bringing any package into production, reproduce the relevant example, inspect its release and issue history, and compare the dependency with the amount of code you would otherwise own.

Follow StepOne on GitHub for open-source C#/.NET projects, compiler experiments, production-focused examples, and new releases.