MimeSpy 2.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package MimeSpy --version 2.0.0
                    
NuGet\Install-Package MimeSpy -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MimeSpy" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MimeSpy" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="MimeSpy" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MimeSpy --version 2.0.0
                    
#r "nuget: MimeSpy, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MimeSpy@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MimeSpy&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=MimeSpy&version=2.0.0
                    
Install as a Cake Tool

MimeSpy

A library to detect mime type from file headers.

Usage

var spy = new MimeSpy();
IReadOnlyList<Dossier> results = spy.Spy(bytes);

foreach (var result in results)
{
    Console.WriteLine($"{result.MimeType} ({string.Join(", ", result.Extensions)}): {result.Name} ({result.Puid})");
}

MimeSpy identifies formats using PRONOM, the UK National Archives' file-format signature registry, through two methods that trade how much of the file you can give them for how confirmed the answer is:

  • Spy is the practical default for a quick "what kind of file is this" guess from a partial read. It evaluates a signature's header (start-of-file) portion and skips any trailer check it would otherwise need - so it also reports formats PRONOM can only fully confirm by a trailer near the end of the file (PNG, JPEG, GIF, and plain ZIP among them). That means a match here isn't guaranteed to hold up once the whole file is checked - a truncated or corrupted file could pass the header check and still fail Investigate.
  • Investigate requires the complete file. It evaluates end-anchored signatures for real, and runs PRONOM's container-signature engine to resolve a generic ZIP/OLE2 match to its specific format (docx vs xlsx vs plain zip, doc vs xls, ...). Investigate(Stream) needs a seekable stream at position 0, and doesn't read it into memory: past 16 MiB it reads 8 MiB from each end, plus the container parts it needs, so a 50 GB file costs about the same as a 20 MB one. See docs/adr/0022 for the trade-off that involves.

A single header can genuinely match more than one format, so callers should expect more than one Dossier back from either of these - see docs/adr/0001-ambiguous-matches-returned-as-ties.md. The split, what each one can and can't see, and why, is covered in docs/adr/0017 and docs/adr/0019.

Both take a ReadOnlySpan<byte> or a Stream, and the Stream overloads have async twins (SpyAsync/InvestigateAsync). There's no async overload of the ReadOnlySpan<byte> form - see docs/adr/0014-async-overload-only-on-the-stream-api.md.

Spy's two overloads aren't the same guess

Spy(ReadOnlySpan<byte>) always does the header-only guess above, no matter how much of the span you hand it - a span's length doesn't tell MimeSpy whether it's the complete file or a truncated prefix, so it never tries to infer that. If you already know you're holding the complete file, call Investigate(ReadOnlySpan<byte>) directly instead of asking Spy to guess at something you already know.

Spy(Stream) is smarter, because a Stream can answer that question honestly (a seekable stream must be at position 0 - the stream is the whole file): if the stream is seekable and its reported length is small (≤ 8 MB), Spy reads it in full and returns Investigate's fully-confirmed answer - real end-of-file checks, real container disambiguation - instead of a partial-read guess. Only a non-seekable or larger stream falls back to the bounded, header-only peek. See docs/adr/0019 for why this is gated on a fact the stream already reports, not on trying Investigate and detecting whether it "worked".

Limiting which formats are considered

If you already know you'll only ever hand MimeSpy one broad category of file - say, a service that only ever ingests images - MimeSpyOptions lets a MimeSpy instance skip the rest of PRONOM's catalog entirely on every call, rather than matching against it and discarding the result:

var spy = new MimeSpy(new MimeSpyOptions { MimeTypes = ["image/", "audio/"] });

MimeTypes patterns are matched by case-insensitive starting-with against each format's own MIME type - "image/" catches every image subtype, "application/pdf" catches that one exactly - and are additive: a format is included if it matches any pattern. The default (no options at all, or an empty MimeTypes list) considers every cataloged format, identical to new MimeSpy(). The narrowed index is built once, at construction, not per call.

Dependency injection

The default MimeSpy() constructor takes no arguments, and a MimeSpy instance - filtered or not - has no mutable state once built, so every method is safe to call concurrently from multiple requests on the same instance. Register it as a singleton, with no MimeSpy-specific extension method required:

services.AddSingleton<IMimeSpy, MimeSpy>();

Consumers then take an IMimeSpy constructor dependency like any other singleton service. The interface exists purely as a mockable seam for callers whose own flow calls these methods inline rather than taking a pre-computed Dossier list as input. To register a filtered instance instead, resolve MimeSpyOptions through DI or build the singleton yourself:

services.AddSingleton<IMimeSpy>(new MimeSpy(new MimeSpyOptions { MimeTypes = ["image/"] }));

Performance

MimeSpy.Benchmarks has the full BenchmarkDotNet suite and its own README; these are a couple of representative numbers from it (per-call mean / allocated, Investigate unless noted).

Spy vs Investigate on the same file - usually close (the extra work Investigate does, EOF confirmation and container disambiguation, is cheap for most formats), but not always:

File Investigate Spy
sample.mp3 244 µs / 675 KB 120 µs / 134 KB
sample.zip 89 µs / 109 KB 83 µs / 91 KB

MimeSpyOptions scoping to image/ - an order of magnitude, on both a file it matches and one it doesn't (the filtered instance still has to conclude "not an image," just over a far smaller candidate set):

File Unfiltered MimeTypes = ["image/"]
sample.png 84 µs / 74 KB 5.9 µs / 8.2 KB
sample.pdf 751 µs / 138 KB 55 µs / 40 KB

Building & testing

dotnet build
dotnet test

Tests use xunit v3 on the Microsoft.Testing.Platform runner; both test projects build as executables, so dotnet run --project MimeSpy.Tests (or MimeSpy.IntegrationTests) also works.

MimeSpy.Tests is mostly hand-built byte arrays covering specific signature-matching and disambiguation rules. MimeSpy.IntegrationTests runs Investigate() against real, genuinely-encoded sample files - one per mainstream format - committed under MimeSpy.IntegrationTests/Fixtures/; see that project's README for where each one came from.

MimeSpy.Benchmarks has the full BenchmarkDotNet suite behind the Performance numbers above, plus a comparison against Mime-Detective, the closest actively-maintained .NET equivalent - see that project's README.

See docs/releasing.md for how to cut a new NuGet release.

Data

Both signature tables are PRONOM's own data, published by the UK National Archives: Resources/pronom-droid-signatures.xml is DROID's signature file (format/extension/mime-type metadata plus the byte-sequence signatures themselves), and Resources/pronom-container-signatures.xml is DROID's container signature file (the rules behind resolving a generic ZIP/OLE2 match to a specific format - docx vs xlsx vs plain zip, doc vs xls, ...). Both are embedded as resources under Resources/ in their native XML and parsed once into in-memory indexes at first use.

License

MIT.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.3.1 113 10/25/2026
2.3.0 100 10/25/2026
2.2.0 97 10/25/2026
2.0.0 113 10/24/2026
0.1.2 138 9/6/2026
0.1.1 122 9/6/2026
0.1.0 154 9/6/2026