Yacd 1.0.1

dotnet add package Yacd --version 1.0.1
                    
NuGet\Install-Package Yacd -Version 1.0.1
                    
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="Yacd" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Yacd" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Yacd" />
                    
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 Yacd --version 1.0.1
                    
#r "nuget: Yacd, 1.0.1"
                    
#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 Yacd@1.0.1
                    
#: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=Yacd&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Yacd&version=1.0.1
                    
Install as a Cake Tool

Yacd — Yet Another Client Detector

NuGet NuGet Downloads

A performance-oriented user agent parser for .NET. Designed to minimize allocations and maximize throughput for high-traffic server workloads.

Design

Yacd is built around a few key principles:

  • Zero allocation — The result is a readonly ref struct whose ReadOnlySpan<char> properties slice directly into the original UA string or static constants. Nothing is copied or heap-allocated during a parse.
  • No regex — All detection uses deterministic string matching (StartsWith, Equals, Contains). No backtracking, no compiled regex overhead.
  • Single-pass tokenization — A ref struct tokenizer backed by SearchValues<char> splits the UA into segments in one pass. Each segment is visited once by each detector.
  • Compile-time device data — ~40K Android device models are source-generated from CSV into a FrozenDictionary with AlternateLookup<ReadOnlySpan<char>>. Apple devices use a hardcoded FrozenDictionary. Both support zero-alloc span-based lookups.
  • All ref structs — The parser, tokenizer, detectors, and result type are all ref struct where possible, keeping the entire parse on the stack.

Quick start

using Yacd;

var parser = UserAgentParser.Instance; // singleton, thread-safe

var ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_2 like Mac OS X) ...";
var info = parser.Parse(ua);

// info is a zero-allocation readonly ref struct
Console.WriteLine(info.BrowserName);    // Safari
Console.WriteLine(info.BrowserVersion); // 17.1
Console.WriteLine(info.OsFamily);       // iOS (iPhone) / iPadOS (iPad)
Console.WriteLine(info.DeviceBrand);    // Apple
Console.WriteLine(info.DeviceModel);    // iPhone
Console.WriteLine(info.IsMobile);       // True

With Client Hints

Chromium-based browsers are freezing the User-Agent string, replacing real OS versions, device models, and full browser versions with fixed values. To get accurate data, pass the Sec-CH-UA-* request headers alongside the UA string:

var hints = new ClientHints
{
    Ua = request.Headers["Sec-CH-UA"],
    FullVersionList = request.Headers["Sec-CH-UA-Full-Version-List"],
    Mobile = request.Headers["Sec-CH-UA-Mobile"],
    Platform = request.Headers["Sec-CH-UA-Platform"],
    PlatformVersion = request.Headers["Sec-CH-UA-Platform-Version"],
    Model = request.Headers["Sec-CH-UA-Model"],
};

var info = parser.Parse(userAgent, hints);
// Browser, OS version, and device model now reflect the real values
// instead of the frozen UA placeholders (Android 10, model "K", etc.)

When Client Hints headers are present they override the corresponding UA-parsed values. When absent (Safari, Firefox, bots), parsing falls back to the UA string automatically.

ASP.NET Core middleware

The Yacd.AspNetCore package handles Client Hints negotiation and parsing automatically:

using Yacd.AspNetCore;

var app = builder.Build();
app.UseYacd();

The middleware:

  1. Sets the Accept-CH response header so browsers send Client Hints on subsequent requests
  2. Parses User-Agent + any Sec-CH-UA-* headers into a UserAgentResult
  3. Makes the result available via HttpContext.GetUserAgent()
app.MapGet("/", (HttpContext ctx) =>
{
    var ua = ctx.GetUserAgent();
    return new { ua?.BrowserName, ua?.OsFamily, ua?.DeviceModel, ua?.IsMobile };
});

Heap-friendly DTO

If you need to store or cache the result beyond the stack:

UserAgentResult result = UserAgentResult.Create(info);
// result is a regular class with string properties — safe to cache, serialize, etc.

What it detects

Property Type Examples
BrowserName ReadOnlySpan<char> Chrome, Safari, Firefox, Edge, Opera
BrowserVersion ReadOnlySpan<char> 120.0.0.0, 17.1
OsFamily ReadOnlySpan<char> Windows, iPadOS, iOS, Android, macOS, Linux
OsMajor/Minor/Patch int 10, 15, 7
DeviceBrand ReadOnlySpan<char> Apple, Samsung, Google, Huawei
DeviceName ReadOnlySpan<char> iPhone, Galaxy S24 Ultra, Pixel 8 Pro
DeviceModel ReadOnlySpan<char> SM-S918B, iPhone16,2
DeviceType ReadOnlySpan<char> smartphone, tablet, desktop
ClientType ReadOnlySpan<char> browser, bot
IsMobile bool true/false
IsWebView bool true/false

Benchmarks

Compared against DeviceDetector.NET v6.5.0 (with and without its DictionaryCache). DeviceDetector.NET's YAML/regex rules are pre-loaded via a warmup parse in GlobalSetup, and the cached variant's DictionaryCache is pre-primed with all benchmark UAs — so these numbers reflect steady-state performance, not cold-start.

BenchmarkDotNet v0.14.0, Ubuntu 24.04.4 LTS (Noble Numbat)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 10.0.103
  [Host]     : .NET 10.0.3 (10.0.326.7603), X64 RyuJIT AVX2
  DefaultJob : .NET 10.0.3 (10.0.326.7603), X64 RyuJIT AVX2

| Method                          | Categories | Mean             | Ratio     | Gen0      | Gen1      | Allocated  | Alloc Ratio |
|-------------------------------- |----------- |-----------------:|----------:|----------:|----------:|-----------:|------------:|
| Yacd_Multi                      | Multi      |       5,366.7 ns |      1.00 |         - |         - |          - |          NA |
| DeviceDetectorNET_Multi         | Multi      | 107,593,217.1 ns | 20,063.14 | 3400.0000 | 1400.0000 | 60950915 B |          NA |
| DeviceDetectorNET_Cached_Multi  | Multi      | 107,740,775.1 ns | 20,090.66 | 3400.0000 | 1400.0000 | 60949139 B |          NA |
|                                 |            |                  |           |           |           |            |             |
| Yacd_Single                     | Single     |         502.2 ns |      1.00 |         - |         - |          - |          NA |
| DeviceDetectorNET_Single        | Single     |   8,651,437.1 ns | 17,232.69 |  265.6250 |  109.3750 |  5102864 B |          NA |
| DeviceDetectorNET_Cached_Single | Single     |   8,460,398.6 ns | 16,852.17 |  265.6250 |  109.3750 |  5102648 B |          NA |

DeviceDetector.NET uses regex-based matching with YAML rule files, which accounts for the difference in throughput and memory. Yacd avoids regex entirely and returns spans into the original string, resulting in zero managed allocations per parse.

Reproduce with:

dotnet run --project benchmarks/Yacd.Benchmarks -c Release

Requirements

  • .NET 9.0+

Project structure

src/Yacd/                  Main library (zero-alloc core parser)
src/Yacd.AspNetCore/       ASP.NET Core middleware (Accept-CH + auto-parsing)
src/Yacd.SourceGen/        Source generator (Android device CSV → FrozenDictionary)
tests/Yacd.Tests/          xUnit tests
benchmarks/Yacd.Benchmarks/     BenchmarkDotNet comparisons
data/supported_devices.csv       ~40K Android device entries

License

MIT

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
1.0.1 125 3/21/2026
1.0.0 111 3/10/2026