Yacd 1.0.1
dotnet add package Yacd --version 1.0.1
NuGet\Install-Package Yacd -Version 1.0.1
<PackageReference Include="Yacd" Version="1.0.1" />
<PackageVersion Include="Yacd" Version="1.0.1" />
<PackageReference Include="Yacd" />
paket add Yacd --version 1.0.1
#r "nuget: Yacd, 1.0.1"
#:package Yacd@1.0.1
#addin nuget:?package=Yacd&version=1.0.1
#tool nuget:?package=Yacd&version=1.0.1
Yacd — Yet Another Client Detector
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 structwhoseReadOnlySpan<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 structtokenizer backed bySearchValues<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
FrozenDictionarywithAlternateLookup<ReadOnlySpan<char>>. Apple devices use a hardcodedFrozenDictionary. Both support zero-alloc span-based lookups. - All ref structs — The parser, tokenizer, detectors, and result type are all
ref structwhere 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:
- Sets the
Accept-CHresponse header so browsers send Client Hints on subsequent requests - Parses
User-Agent+ anySec-CH-UA-*headers into aUserAgentResult - 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 | Versions 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. |
-
net10.0
- CommunityToolkit.HighPerformance (>= 8.4.0)
-
net9.0
- CommunityToolkit.HighPerformance (>= 8.4.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.