├── global.json ├── MyCSharp.HttpUserAgentParser.snk ├── src ├── HttpUserAgentParser │ ├── readme.md │ ├── HttpUserAgentType.cs │ ├── Providers │ │ ├── HttpUserAgentParserDefaultProvider.cs │ │ ├── IHttpUserAgentParserProvider.cs │ │ └── HttpUserAgentParserCachedProvider.cs │ ├── DependencyInjection │ │ ├── HttpUserAgentParserDependencyInjectionOptions.cs │ │ └── HttpUserAgentParserServiceCollectionExtensions.cs │ ├── HttpUserAgentParser.csproj │ ├── HttpUserAgentPlatformInformation.cs │ ├── LICENSE.txt │ ├── HttpUserAgentPlatformType.cs │ ├── HttpUserAgentInformationExtensions.cs │ ├── VectorExtensions.cs │ ├── HttpUserAgentInformation.cs │ ├── HttpUserAgentParser.cs │ └── HttpUserAgentStatics.cs ├── HttpUserAgentParser.AspNetCore │ ├── readme.md │ ├── IHttpUserAgentParserAccessor.cs │ ├── HttpContextExtensions.cs │ ├── HttpUserAgentParser.AspNetCore.csproj │ ├── DependencyInjection │ │ └── HttpUserAgentParserDependencyInjectionOptionsExtensions.cs │ ├── LICENSE.txt │ └── HttpUserAgentParserAccessor.cs └── HttpUserAgentParser.MemoryCache │ ├── readme.md │ ├── HttpUserAgentParser.MemoryCache.csproj │ ├── LICENSE.txt │ ├── DependencyInjection │ └── HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs │ ├── HttpUserAgentParserMemoryCachedProviderOptions.cs │ └── HttpUserAgentParserMemoryCachedProvider.cs ├── tests ├── HttpUserAgentParser.TestHelpers │ └── HttpUserAgentParser.TestHelpers.csproj ├── HttpUserAgentParser.AspNetCore.UnitTests │ ├── HttpContextTestHelpers.cs │ ├── HttpUserAgentParser.AspNetCore.UnitTests.csproj │ ├── DependencyInjection │ │ └── HttpUserAgentParserServiceCollectionExtensionsTests.cs │ ├── HttpContextExtensionsTests.cs │ └── HttpUserAgentParserAccessorTests.cs ├── HttpUserAgentParser.UnitTests │ ├── HttpUserAgentTypeTests.cs │ ├── HttpUserAgentParser.UnitTests.csproj │ ├── DependencyInjection │ │ ├── HttpUserAgentParserDependencyInjectionOptions.cs │ │ └── HttpUserAgentParserServiceCollectionExtensionsTests.cs │ ├── Providers │ │ ├── HttpUserAgentParserDefaultProviderTests.cs │ │ └── HttpUserAgentParserCachedProviderTests.cs │ ├── HttpUserAgentPlatformInformationTests.cs │ ├── HttpUserAgentPlatformTypeTests.cs │ ├── HttpUserAgentInformationExtensionsTests.cs │ ├── HttpUserAgentInformationTests.cs │ └── HttpUserAgentParserTests.cs └── HttpUserAgentParser.MemoryCache.UnitTests │ ├── HttpUserAgentParser.MemoryCache.UnitTests.csproj │ ├── DependencyInjection │ └── HttpUserAgentParserMemoryCacheServiceCollectionExtensionssTests.cs │ ├── HttpUserAgentParserMemoryCachedProviderAdditionalTests.cs │ ├── HttpUserAgentParserMemoryCachedProviderTests.cs │ └── HttpUserAgentParserMemoryCachedProviderOptionsTests.cs ├── NuGet.config ├── .vscode ├── tasks.json └── settings.json ├── perf └── HttpUserAgentParser.Benchmarks │ ├── Program.cs │ ├── HttpUserAgentParser.Benchmarks.csproj │ ├── HttpUserAgentParserBenchmarks.cs │ └── LibraryComparison │ └── LibraryComparisonBenchmarks.cs ├── version.json ├── .github ├── workflows │ ├── pr-validation.yml │ ├── release-publish.yml │ ├── build-and-test.yml │ └── main-build.yml └── release-drafter.yml ├── LICENSE ├── Justfile ├── Directory.Packages.props ├── MyCSharp.HttpUserAgentParser.sln ├── .gitignore ├── Directory.Build.props ├── README.md └── .editorconfig /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.101" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /MyCSharp.HttpUserAgentParser.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mycsharp/HttpUserAgentParser/HEAD/MyCSharp.HttpUserAgentParser.snk -------------------------------------------------------------------------------- /src/HttpUserAgentParser/readme.md: -------------------------------------------------------------------------------- 1 | # MyCSharp.HttpUserAgentParser 2 | 3 | Parsing HTTP User Agents with .NET 4 | 5 | https://github.com/mycsharp/HttpUserAgentParser 6 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.AspNetCore/readme.md: -------------------------------------------------------------------------------- 1 | # MyCSharp.HttpUserAgentParser 2 | 3 | Parsing HTTP User Agents with .NET 4 | 5 | https://github.com/mycsharp/HttpUserAgentParser 6 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.MemoryCache/readme.md: -------------------------------------------------------------------------------- 1 | # MyCSharp.HttpUserAgentParser 2 | 3 | Parsing HTTP User Agents with .NET 4 | 5 | https://github.com/mycsharp/HttpUserAgentParser 6 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.TestHelpers/HttpUserAgentParser.TestHelpers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "test", 6 | "type": "shell", 7 | "command": "dotnet test --nologo", 8 | "args": [], 9 | "problemMatcher": [ 10 | "$msCompile" 11 | ], 12 | "group": "build" 13 | } 14 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/HttpUserAgentType.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | namespace MyCSharp.HttpUserAgentParser; 4 | 5 | /// 6 | /// HTTP User Agent Types 7 | /// 8 | public enum HttpUserAgentType : byte 9 | { 10 | /// 11 | /// Unkown / not mapped 12 | /// 13 | Unknown, 14 | /// 15 | /// Browser 16 | /// 17 | Browser, 18 | /// 19 | /// Robot 20 | /// 21 | Robot 22 | } 23 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpContextTestHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests; 6 | 7 | public static class HttpContextTestHelpers 8 | { 9 | public static HttpContext GetHttpContext(string userAgent) 10 | { 11 | DefaultHttpContext context = new(); 12 | context.Request.Headers["User-Agent"] = userAgent; 13 | 14 | return context; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/HttpUserAgentTypeTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Xunit; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.UnitTests; 6 | 7 | public class HttpUserAgentTypeTests 8 | { 9 | [Theory] 10 | [InlineData(HttpUserAgentType.Unknown, 0)] 11 | [InlineData(HttpUserAgentType.Browser, 1)] 12 | [InlineData(HttpUserAgentType.Robot, 2)] 13 | public void TestValue(HttpUserAgentType type, byte value) 14 | { 15 | Assert.True((byte)type == value); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/Providers/HttpUserAgentParserDefaultProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | namespace MyCSharp.HttpUserAgentParser.Providers; 4 | 5 | /// 6 | /// Simple parse provider 7 | /// 8 | public class HttpUserAgentParserDefaultProvider : IHttpUserAgentParserProvider 9 | { 10 | /// 11 | /// returns the result of 12 | /// 13 | public HttpUserAgentInformation Parse(string userAgent) 14 | => HttpUserAgentParser.Parse(userAgent); 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // github copilot commit message instructions (preview) 3 | "github.copilot.chat.commitMessageGeneration.instructions": [ 4 | { "text": "Use conventional commit format: type(scope): description" }, 5 | { "text": "Use imperative mood: 'Add feature' not 'Added feature'" }, 6 | { "text": "Keep subject line under 50 characters" }, 7 | { "text": "Use types: feat, fix, docs, style, refactor, perf, test, chore, ci" }, 8 | { "text": "Include scope when relevant (e.g., api, ui, auth)" }, 9 | { "text": "Reference issue numbers with # prefix" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /perf/HttpUserAgentParser.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using System.Reflection; 4 | using BenchmarkDotNet.Configs; 5 | using BenchmarkDotNet.Running; 6 | 7 | // Needed for DeviceDetector.NET 8 | // https://github.com/totpero/DeviceDetector.NET/issues/44 9 | ManualConfig config = ManualConfig.Create(DefaultConfig.Instance) 10 | .WithOptions(ConfigOptions.DisableOptimizationsValidator); 11 | 12 | // dotnet run -c Release --framework net80 net90 --runtimes net90 13 | BenchmarkSwitcher.FromAssembly(Assembly.GetExecutingAssembly()).Run(args, config); 14 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/Providers/IHttpUserAgentParserProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | namespace MyCSharp.HttpUserAgentParser.Providers; 4 | 5 | /// 6 | /// Provides the basic parsing of user agent strings. 7 | /// 8 | public interface IHttpUserAgentParserProvider 9 | { 10 | /// 11 | /// Parsed the -string. 12 | /// 13 | /// The user agent to parse. 14 | /// The parsed user agent information 15 | HttpUserAgentInformation Parse(string userAgent); 16 | } 17 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.AspNetCore/IHttpUserAgentParserAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.AspNetCore; 6 | 7 | /// 8 | /// User Agent parser accessor 9 | /// 10 | public interface IHttpUserAgentParserAccessor 11 | { 12 | /// 13 | /// User agent value 14 | /// 15 | string? GetHttpContextUserAgent(HttpContext httpContext); 16 | 17 | /// 18 | /// Returns current 19 | /// 20 | HttpUserAgentInformation? Get(HttpContext httpContext); 21 | } 22 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "3.0", 4 | "nugetPackageVersion": { 5 | "semVer": 1 // optional. Set to either 1 or 2 to control how the NuGet package version string is generated. Default is 1. 6 | }, 7 | "publicReleaseRefSpec": [ 8 | "^refs/heads/main$", // we release out of main 9 | "^refs/tags/v\\d+\\.\\d+" // we release on tags like v1.0 or v2.0.0 via GitHub Release 10 | ], 11 | "cloudBuild": { 12 | "buildNumber": { 13 | "enabled": true 14 | } 15 | }, 16 | "release": { 17 | "versionIncrement": "build", 18 | "firstUnstableTag": "preview" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.AspNetCore/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Primitives; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.AspNetCore; 7 | 8 | /// 9 | /// Static extensions for 10 | /// 11 | public static class HttpContextExtensions 12 | { 13 | /// 14 | /// Returns the User-Agent header value 15 | /// 16 | public static string? GetUserAgentString(this HttpContext httpContext) 17 | { 18 | if (httpContext.Request.Headers.TryGetValue("User-Agent", out StringValues value)) 19 | return value; 20 | 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.DependencyInjection; 6 | 7 | /// 8 | /// Options for dependency injection 9 | /// 10 | /// 11 | /// Creates a new instance of 12 | /// 13 | /// 14 | public class HttpUserAgentParserDependencyInjectionOptions(IServiceCollection services) 15 | { 16 | /// 17 | /// Services container 18 | /// 19 | public IServiceCollection Services { get; } = services; 20 | } 21 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/HttpUserAgentParser.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/DependencyInjection/HttpUserAgentParserDependencyInjectionOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using MyCSharp.HttpUserAgentParser.DependencyInjection; 5 | using NSubstitute; 6 | using Xunit; 7 | 8 | namespace MyCSharp.HttpUserAgentParser.UnitTests.DependencyInjection; 9 | 10 | public class UserAgentParserDependencyInjectionOptionsTests 11 | { 12 | private readonly IServiceCollection _scMock = Substitute.For(); 13 | 14 | [Fact] 15 | public void Ctor_Should_Set_Property() 16 | { 17 | HttpUserAgentParserDependencyInjectionOptions options = new(_scMock); 18 | 19 | Assert.Equal(_scMock, options.Services); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/pr-validation.yml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | pull-requests: read 11 | 12 | jobs: 13 | validate-dotnet-8: 14 | name: Test on .NET 8.0 15 | uses: ./.github/workflows/build-and-test.yml 16 | with: 17 | dotnet-version: '8.0.x' 18 | upload-test-results: true 19 | 20 | validate-dotnet-9: 21 | name: Test on .NET 9.0 22 | uses: ./.github/workflows/build-and-test.yml 23 | with: 24 | dotnet-version: '9.0.x' 25 | upload-test-results: true 26 | 27 | validate-dotnet-10: 28 | name: Test on .NET 10.0 29 | uses: ./.github/workflows/build-and-test.yml 30 | with: 31 | dotnet-version: '10.0.x' 32 | upload-test-results: true 33 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParser.MemoryCache.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/Providers/HttpUserAgentParserDefaultProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using MyCSharp.HttpUserAgentParser.Providers; 4 | using Xunit; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.UnitTests.Providers; 7 | 8 | public class HttpUserAgentParserDefaultProviderTests 9 | { 10 | [Theory] 11 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62")] 12 | public void Parse(string userAgent) 13 | { 14 | HttpUserAgentParserDefaultProvider provider = new(); 15 | 16 | HttpUserAgentInformation providerUserAgentInfo = provider.Parse(userAgent); 17 | HttpUserAgentInformation userAgentInfo = HttpUserAgentInformation.Parse(userAgent); 18 | 19 | Assert.Equal(userAgentInfo, providerUserAgentInfo); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParser.AspNetCore.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/HttpUserAgentPlatformInformationTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using System.Text.RegularExpressions; 4 | using Xunit; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.UnitTests; 7 | 8 | public partial class HttpUserAgentPlatformInformationTests 9 | { 10 | [Theory] 11 | [InlineData("Batman", HttpUserAgentPlatformType.Android)] 12 | [InlineData("Robin", HttpUserAgentPlatformType.Windows)] 13 | public void Ctor(string name, HttpUserAgentPlatformType platform) 14 | { 15 | Regex regex = EmptyRegex(); 16 | 17 | HttpUserAgentPlatformInformation info = new(regex, name, platform); 18 | 19 | Assert.Equal(regex, info.Regex); 20 | Assert.Equal(name, info.Name); 21 | Assert.Equal(platform, info.PlatformType); 22 | } 23 | 24 | [GeneratedRegex("", RegexOptions.None, matchTimeoutMilliseconds: 1000)] 25 | private static partial Regex EmptyRegex(); 26 | } 27 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/HttpUserAgentParser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTTP User Agent Parser 5 | Parses user agents for Browser, Platform and Bots. 6 | 7 | 8 | 9 | true 10 | readme.md 11 | LICENSE.txt 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/HttpUserAgentPlatformTypeTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Xunit; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.UnitTests; 6 | 7 | public class HttpUserAgentPlatformTypeTests 8 | { 9 | [Theory] 10 | [InlineData(HttpUserAgentPlatformType.Unknown, 0)] 11 | [InlineData(HttpUserAgentPlatformType.Generic, 1)] 12 | [InlineData(HttpUserAgentPlatformType.Windows, 2)] 13 | [InlineData(HttpUserAgentPlatformType.Linux, 3)] 14 | [InlineData(HttpUserAgentPlatformType.Unix, 4)] 15 | [InlineData(HttpUserAgentPlatformType.IOS, 5)] 16 | [InlineData(HttpUserAgentPlatformType.MacOS, 6)] 17 | [InlineData(HttpUserAgentPlatformType.BlackBerry, 7)] 18 | [InlineData(HttpUserAgentPlatformType.Android, 8)] 19 | [InlineData(HttpUserAgentPlatformType.Symbian, 9)] 20 | public void TestValue(HttpUserAgentPlatformType type, byte value) 21 | { 22 | Assert.True((byte)type == value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/HttpUserAgentPlatformInformation.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using System.Text.RegularExpressions; 4 | 5 | namespace MyCSharp.HttpUserAgentParser; 6 | 7 | /// 8 | /// Information about the user agent platform 9 | /// 10 | /// 11 | /// Creates a new instance of 12 | /// 13 | public readonly struct HttpUserAgentPlatformInformation(Regex regex, string name, HttpUserAgentPlatformType platformType) 14 | { 15 | /// 16 | /// Regex-pattern that matches this user agent string 17 | /// 18 | public Regex Regex { get; } = regex; 19 | 20 | /// 21 | /// Name of the platform 22 | /// 23 | public string Name { get; } = name; 24 | 25 | /// 26 | /// Specific platform type aka family 27 | /// 28 | public HttpUserAgentPlatformType PlatformType { get; } = platformType; 29 | } 30 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.AspNetCore/HttpUserAgentParser.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTTP User Agent Parser Extensions for ASP.NET Core 5 | HTTP User Agent Parser Extensions for ASP.NET Core 6 | 7 | 8 | 9 | true 10 | readme.md 11 | LICENSE.txt 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.MemoryCache/HttpUserAgentParser.MemoryCache.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTTP User Agent Parser Extensions for IMemoryCache 5 | HTTP User Agent Parser Extensions for IMemoryCache 6 | 7 | 8 | 9 | true 10 | readme.md 11 | LICENSE.txt 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 MyCSharp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.AspNetCore.UnitTests/DependencyInjection/HttpUserAgentParserServiceCollectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection; 5 | using MyCSharp.HttpUserAgentParser.DependencyInjection; 6 | using Xunit; 7 | 8 | namespace MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests.DependencyInjection; 9 | 10 | public class HttpUserAgentParserDependencyInjectionOptionsExtensionsTests 11 | { 12 | [Fact] 13 | public void AddHttpUserAgentParserAccessor() 14 | { 15 | ServiceCollection services = new(); 16 | HttpUserAgentParserDependencyInjectionOptions options = new(services); 17 | 18 | options.AddHttpUserAgentParserAccessor(); 19 | 20 | Assert.Single(services); 21 | 22 | Assert.Equal(typeof(IHttpUserAgentParserAccessor), services[0].ServiceType); 23 | Assert.Equal(typeof(HttpUserAgentParserAccessor), services[0].ImplementationType); 24 | Assert.Equal(ServiceLifetime.Singleton, services[0].Lifetime); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.AspNetCore/DependencyInjection/HttpUserAgentParserDependencyInjectionOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using MyCSharp.HttpUserAgentParser.DependencyInjection; 5 | using MyCSharp.HttpUserAgentParser.Providers; 6 | 7 | namespace MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection; 8 | 9 | /// 10 | /// Dependency injection extensions for ASP.NET Core environments 11 | /// 12 | public static class HttpUserAgentParserDependencyInjectionOptionsExtensions 13 | { 14 | /// 15 | /// Registers as . 16 | /// Requires a registered 17 | /// 18 | public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParserAccessor( 19 | this HttpUserAgentParserDependencyInjectionOptions options) 20 | { 21 | options.Services.AddSingleton(); 22 | return options; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 MyCSharp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.AspNetCore/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 MyCSharp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.MemoryCache/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 MyCSharp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/HttpUserAgentPlatformType.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | namespace MyCSharp.HttpUserAgentParser; 4 | 5 | /// 6 | /// Platform types 7 | /// 8 | public enum HttpUserAgentPlatformType : byte 9 | { 10 | /// 11 | /// Unknown / not mapped 12 | /// 13 | Unknown = 0, 14 | /// 15 | /// Generic 16 | /// 17 | Generic, 18 | /// 19 | /// Windows 20 | /// 21 | Windows, 22 | /// 23 | /// Linux 24 | /// 25 | Linux, 26 | /// 27 | /// Unix 28 | /// 29 | Unix, 30 | /// 31 | /// Apple iOS 32 | /// 33 | IOS, 34 | /// 35 | /// MacOS 36 | /// 37 | MacOS, 38 | /// 39 | /// BlackBerry 40 | /// 41 | BlackBerry, 42 | /// 43 | /// Android 44 | /// 45 | Android, 46 | /// 47 | /// Symbian 48 | /// 49 | Symbian, 50 | /// 51 | /// ChromeOS 52 | /// 53 | ChromeOS 54 | } 55 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/Providers/HttpUserAgentParserCachedProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using System.Collections.Concurrent; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.Providers; 6 | 7 | /// 8 | /// In process cache provider for 9 | /// 10 | public class HttpUserAgentParserCachedProvider : IHttpUserAgentParserProvider 11 | { 12 | /// 13 | /// internal cache 14 | /// 15 | private readonly ConcurrentDictionary _cache = new(StringComparer.OrdinalIgnoreCase); 16 | 17 | /// 18 | /// Parses the user agent or uses the internal cached information 19 | /// 20 | public HttpUserAgentInformation Parse(string userAgent) 21 | => _cache.GetOrAdd(userAgent, static ua => HttpUserAgentParser.Parse(ua)); 22 | 23 | /// 24 | /// Total count of entries in cache 25 | /// 26 | public int CacheEntryCount => _cache.Count; 27 | 28 | /// 29 | /// returns true if given user agent is in cache 30 | /// 31 | public bool HasCacheEntry(string userAgent) => _cache.ContainsKey(userAgent); 32 | } 33 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpContextExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using MyCSharp.HttpUserAgentParser.AspNetCore; 5 | using MyCSharp.HttpUserAgentParser.Providers; 6 | using NSubstitute; 7 | using Xunit; 8 | 9 | namespace MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests; 10 | 11 | public class HttpContextExtensionsTests 12 | { 13 | [Fact] 14 | public void GetUserAgentString_Returns_Value_When_Present() 15 | { 16 | HttpContext ctx = HttpContextTestHelpers.GetHttpContext("UA"); 17 | Assert.Equal("UA", ctx.GetUserAgentString()); 18 | } 19 | 20 | [Fact] 21 | public void GetUserAgentString_Returns_Null_When_Absent() 22 | { 23 | DefaultHttpContext ctx = new(); 24 | Assert.Null(ctx.GetUserAgentString()); 25 | } 26 | 27 | [Fact] 28 | public void Accessor_Get_Returns_Null_When_Header_Missing() 29 | { 30 | var provider = Substitute.For(); 31 | HttpUserAgentParserAccessor accessor = new(provider); 32 | DefaultHttpContext ctx = new(); 33 | 34 | Assert.Null(accessor.Get(ctx)); 35 | provider.DidNotReceiveWithAnyArgs().Parse(default!); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.MemoryCache.UnitTests/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensionssTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection; 5 | using MyCSharp.HttpUserAgentParser.Providers; 6 | using Xunit; 7 | 8 | namespace MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests.DependencyInjection; 9 | 10 | public class HttpUserAgentParserMemoryCacheServiceCollectionExtensionssTests 11 | { 12 | [Fact] 13 | public void AddHttpUserAgentMemoryCachedParser() 14 | { 15 | ServiceCollection services = new(); 16 | 17 | services.AddHttpUserAgentMemoryCachedParser(); 18 | 19 | Assert.Equal(2, services.Count); 20 | 21 | Assert.IsType(services[0].ImplementationInstance); 22 | Assert.Equal(ServiceLifetime.Singleton, services[0].Lifetime); 23 | 24 | Assert.Equal(typeof(IHttpUserAgentParserProvider), services[1].ServiceType); 25 | Assert.Equal(typeof(HttpUserAgentParserMemoryCachedProvider), services[1].ImplementationType); 26 | Assert.Equal(ServiceLifetime.Singleton, services[1].Lifetime); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.MemoryCache/DependencyInjection/HttpUserAgentParserMemoryCacheServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using MyCSharp.HttpUserAgentParser.DependencyInjection; 5 | using MyCSharp.HttpUserAgentParser.Providers; 6 | 7 | namespace MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection; 8 | 9 | /// 10 | /// Dependency injection extensions for IMemoryCache 11 | /// 12 | public static class HttpUserAgentParserMemoryCacheServiceCollectionExtensions 13 | { 14 | /// 15 | /// Registers as singleton to 16 | /// 17 | public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentMemoryCachedParser( 18 | this IServiceCollection services, Action? options = null) 19 | { 20 | HttpUserAgentParserMemoryCachedProviderOptions providerOptions = new(); 21 | options?.Invoke(providerOptions); 22 | 23 | // register options 24 | services.AddSingleton(providerOptions); 25 | 26 | // register cache provider 27 | return services.AddHttpUserAgentParser(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.AspNetCore.UnitTests/HttpUserAgentParserAccessorTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using MyCSharp.HttpUserAgentParser.Providers; 5 | using NSubstitute; 6 | using Xunit; 7 | 8 | namespace MyCSharp.HttpUserAgentParser.AspNetCore.UnitTests; 9 | 10 | public class HttpUserAgentParserAccessorTests 11 | { 12 | private readonly IHttpUserAgentParserProvider _parserMock = Substitute.For(); 13 | 14 | [Theory] 15 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62")] 16 | public void Get(string userAgent) 17 | { 18 | // arrange 19 | HttpUserAgentInformation userAgentInformation = HttpUserAgentInformation.Parse(userAgent); 20 | _parserMock.Parse(userAgent).Returns(userAgentInformation); 21 | 22 | // act 23 | HttpContext httpContext = HttpContextTestHelpers.GetHttpContext(userAgent); 24 | 25 | HttpUserAgentParserAccessor accessor = new(_parserMock); 26 | HttpUserAgentInformation? info = accessor.Get(httpContext); 27 | 28 | // assert 29 | Assert.NotNull(info); 30 | Assert.Equal(userAgentInformation, info); 31 | 32 | // verify 33 | _parserMock.Received(1).Parse(userAgent); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderAdditionalTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.Caching.Memory; 4 | using Xunit; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests; 7 | 8 | public class HttpUserAgentParserMemoryCachedProviderAdditionalTests 9 | { 10 | [Fact] 11 | public void Options_Defaults_Are_Set() 12 | { 13 | HttpUserAgentParserMemoryCachedProviderOptions options = new(); 14 | Assert.NotNull(options.CacheOptions); 15 | Assert.NotNull(options.CacheEntryOptions); 16 | Assert.True(options.CacheOptions.SizeLimit is null || options.CacheOptions.SizeLimit >= 0); 17 | Assert.NotEqual(default, options.CacheEntryOptions.SlidingExpiration); 18 | } 19 | 20 | [Fact] 21 | public void Provider_Caches_Entries_And_Resolves_Twice() 22 | { 23 | HttpUserAgentParserMemoryCachedProvider provider = new(new HttpUserAgentParserMemoryCachedProviderOptions(new MemoryCacheOptions { SizeLimit = 10 })); 24 | string ua = "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"; 25 | HttpUserAgentInformation a = provider.Parse(ua); 26 | HttpUserAgentInformation b = provider.Parse(ua); 27 | 28 | Assert.Equal(a.Name, b.Name); 29 | Assert.Equal(a.Version, b.Version); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/HttpUserAgentInformationExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | namespace MyCSharp.HttpUserAgentParser; 4 | 5 | /// 6 | /// Extensions for 7 | /// 8 | public static class HttpUserAgentInformationExtensions 9 | { 10 | /// 11 | /// Tests if is of 12 | /// 13 | public static bool IsType(this in HttpUserAgentInformation userAgent, HttpUserAgentType type) => userAgent.Type == type; 14 | 15 | /// 16 | /// Tests if is of type 17 | /// 18 | public static bool IsRobot(this in HttpUserAgentInformation userAgent) => IsType(userAgent, HttpUserAgentType.Robot); 19 | 20 | /// 21 | /// Tests if is of type 22 | /// 23 | public static bool IsBrowser(this in HttpUserAgentInformation userAgent) => IsType(userAgent, HttpUserAgentType.Browser); 24 | 25 | /// 26 | /// returns true if agent is a mobile device 27 | /// 28 | /// checks if is null 29 | public static bool IsMobile(this in HttpUserAgentInformation userAgent) => userAgent.MobileDeviceType is not null; 30 | } 31 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using MyCSharp.HttpUserAgentParser.Providers; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.AspNetCore; 7 | 8 | /// 9 | /// User Agent parser accessor. Implements 10 | /// 11 | /// 12 | /// Creates a new instance of 13 | /// 14 | public class HttpUserAgentParserAccessor(IHttpUserAgentParserProvider httpUserAgentParser) 15 | : IHttpUserAgentParserAccessor 16 | { 17 | private readonly IHttpUserAgentParserProvider _httpUserAgentParser = httpUserAgentParser; 18 | 19 | /// 20 | /// User agent of current 21 | /// 22 | public string? GetHttpContextUserAgent(HttpContext httpContext) 23 | => httpContext.GetUserAgentString(); 24 | 25 | /// 26 | /// Returns current of current 27 | /// 28 | public HttpUserAgentInformation? Get(HttpContext httpContext) 29 | { 30 | string? httpUserAgent = GetHttpContextUserAgent(httpContext); 31 | if (string.IsNullOrEmpty(httpUserAgent)) 32 | { 33 | return null; 34 | } 35 | 36 | return _httpUserAgentParser.Parse(httpUserAgent); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParser.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | disable 6 | 7 | 8 | 9 | 10 | $(MSBuildProjectName) 11 | $(MSBuildProjectName) 12 | 13 | 14 | 15 | $(DefineConstants);OS_WIN 16 | 17 | 18 | 19 | $(NoWarn);CS8002 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Xunit; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests; 6 | 7 | public class HttpUserAgentParserMemoryCachedProviderTests 8 | { 9 | [Fact] 10 | public void Parse() 11 | { 12 | HttpUserAgentParserMemoryCachedProviderOptions cachedProviderOptions = new(); 13 | HttpUserAgentParserMemoryCachedProvider provider = new(cachedProviderOptions); 14 | 15 | // create first 16 | const string userAgentOne = 17 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62"; 18 | 19 | HttpUserAgentInformation infoOne = provider.Parse(userAgentOne); 20 | 21 | Assert.Equal("Edge", infoOne.Name); 22 | Assert.Equal("90.0.818.62", infoOne.Version); 23 | 24 | // check duplicate 25 | 26 | HttpUserAgentInformation infoDuplicate = provider.Parse(userAgentOne); 27 | 28 | Assert.Equal("Edge", infoDuplicate.Name); 29 | Assert.Equal("90.0.818.62", infoDuplicate.Version); 30 | 31 | // create second 32 | 33 | const string userAgentTwo = "Mozilla/5.0 (Android 4.4; Tablet; rv:41.0) Gecko/41.0 Firefox/41.0"; 34 | 35 | HttpUserAgentInformation infoTwo = provider.Parse(userAgentTwo); 36 | 37 | Assert.Equal("Firefox", infoTwo.Name); 38 | Assert.Equal("41.0", infoTwo.Version); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.MemoryCache.UnitTests/HttpUserAgentParserMemoryCachedProviderOptionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.Caching.Memory; 4 | using Xunit; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.MemoryCache.UnitTests; 7 | 8 | public class HttpUserAgentParserMemoryCachedProviderOptionsTests 9 | { 10 | [Fact] 11 | public void Ctor() 12 | { 13 | MemoryCacheOptions cacheOptions = new(); 14 | MemoryCacheEntryOptions cacheEntryOptions = new(); 15 | 16 | HttpUserAgentParserMemoryCachedProviderOptions options = new(cacheOptions, cacheEntryOptions); 17 | 18 | Assert.Equal(cacheOptions, options.CacheOptions); 19 | Assert.Equal(cacheEntryOptions, options.CacheEntryOptions); 20 | } 21 | 22 | [Fact] 23 | public void Ctor_MemoryCacheOptions() 24 | { 25 | MemoryCacheOptions cacheOptions = new(); 26 | 27 | HttpUserAgentParserMemoryCachedProviderOptions options = new(cacheOptions); 28 | 29 | Assert.Equal(cacheOptions, options.CacheOptions); 30 | Assert.NotNull(options.CacheEntryOptions); 31 | } 32 | 33 | [Fact] 34 | public void Ctor_MemoryCacheEntryOptions() 35 | { 36 | MemoryCacheEntryOptions cacheEntryOptions = new(); 37 | 38 | HttpUserAgentParserMemoryCachedProviderOptions options = new(cacheEntryOptions); 39 | 40 | Assert.NotNull(options.CacheOptions); 41 | Assert.Equal(cacheEntryOptions, options.CacheEntryOptions); 42 | } 43 | 44 | [Fact] 45 | public void Ctor_Empty() 46 | { 47 | HttpUserAgentParserMemoryCachedProviderOptions options = new(); 48 | 49 | Assert.NotNull(options.CacheOptions); 50 | Assert.NotNull(options.CacheEntryOptions); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParserBenchmarks.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Jobs; 5 | using MyCSharp.HttpUserAgentParser; 6 | 7 | #if OS_WIN 8 | using BenchmarkDotNet.Diagnostics.Windows.Configs; 9 | #endif 10 | 11 | namespace HttpUserAgentParser.Benchmarks; 12 | 13 | [MemoryDiagnoser] 14 | [SimpleJob(RuntimeMoniker.Net80)] 15 | [SimpleJob(RuntimeMoniker.Net90)] 16 | #if OS_WIN 17 | [EtwProfiler] // needs admin-rights 18 | #endif 19 | public class HttpUserAgentParserBenchmarks 20 | { 21 | private string[] _testUserAgentMix; 22 | private HttpUserAgentInformation[] _results; 23 | 24 | [GlobalSetup] 25 | public void GlobalSetup() 26 | { 27 | _testUserAgentMix = GetTestUserAgents().ToArray(); 28 | _results = new HttpUserAgentInformation[_testUserAgentMix.Length]; 29 | } 30 | 31 | private static IEnumerable GetTestUserAgents() 32 | { 33 | yield return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"; 34 | yield return "APIs-Google (+https://developers.google.com/webmasters/APIs-Google.html)"; 35 | yield return "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0"; 36 | yield return "yeah I'm unknown user agent, just to bring some fun to the mix"; 37 | } 38 | 39 | [Benchmark] 40 | public void Parse() 41 | { 42 | string[] testUserAgentMix = _testUserAgentMix; 43 | HttpUserAgentInformation[] results = _results; 44 | 45 | for (int i = 0; i < testUserAgentMix.Length; ++i) 46 | { 47 | results[i] = MyCSharp.HttpUserAgentParser.HttpUserAgentParser.Parse(testUserAgentMix[i]); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/Providers/HttpUserAgentParserCachedProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using MyCSharp.HttpUserAgentParser.Providers; 4 | using Xunit; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.UnitTests.Providers; 7 | 8 | public class HttpUserAgentParserCachedProviderTests 9 | { 10 | [Fact] 11 | public void Parse() 12 | { 13 | HttpUserAgentParserCachedProvider provider = new(); 14 | 15 | Assert.Equal(0, provider.CacheEntryCount); 16 | 17 | // create first 18 | const string userAgentOne = 19 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62"; 20 | 21 | HttpUserAgentInformation infoOne = provider.Parse(userAgentOne); 22 | 23 | Assert.Equal("Edge", infoOne.Name); 24 | Assert.Equal("90.0.818.62", infoOne.Version); 25 | 26 | Assert.Equal(1, provider.CacheEntryCount); 27 | Assert.True(provider.HasCacheEntry(userAgentOne)); 28 | 29 | // check duplicate 30 | 31 | HttpUserAgentInformation infoDuplicate = provider.Parse(userAgentOne); 32 | 33 | Assert.Equal("Edge", infoDuplicate.Name); 34 | Assert.Equal("90.0.818.62", infoDuplicate.Version); 35 | 36 | Assert.Equal(1, provider.CacheEntryCount); 37 | Assert.True(provider.HasCacheEntry(userAgentOne)); 38 | 39 | // create second 40 | 41 | const string userAgentTwo = "Mozilla/5.0 (Android 4.4; Tablet; rv:41.0) Gecko/41.0 Firefox/41.0"; 42 | 43 | HttpUserAgentInformation infoTwo = provider.Parse(userAgentTwo); 44 | 45 | Assert.Equal("Firefox", infoTwo.Name); 46 | Assert.Equal("41.0", infoTwo.Version); 47 | 48 | Assert.Equal(2, provider.CacheEntryCount); 49 | Assert.True(provider.HasCacheEntry(userAgentTwo)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/DependencyInjection/HttpUserAgentParserServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using MyCSharp.HttpUserAgentParser.Providers; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.DependencyInjection; 7 | 8 | /// 9 | /// Dependency injection extensions 10 | /// 11 | public static class HttpUserAgentParserServiceCollectionExtensions 12 | { 13 | /// 14 | /// Registers as singleton to 15 | /// 16 | public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParser( 17 | this IServiceCollection services) 18 | { 19 | return AddHttpUserAgentParser(services); 20 | } 21 | 22 | /// 23 | /// Registers as singleton to 24 | /// 25 | public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentCachedParser( 26 | this IServiceCollection services) 27 | { 28 | return AddHttpUserAgentParser(services); 29 | } 30 | 31 | /// 32 | /// Registers as singleton to 33 | /// 34 | public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParser( 35 | this IServiceCollection services) where TProvider : class, IHttpUserAgentParserProvider 36 | { 37 | // create options 38 | HttpUserAgentParserDependencyInjectionOptions options = new(services); 39 | 40 | // add provider 41 | services.AddSingleton(); 42 | 43 | return options; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProviderOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.Caching.Memory; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.MemoryCache; 6 | 7 | /// 8 | /// Provider options for 9 | /// 10 | /// Default of is 256. 11 | /// Default of is 1 day 12 | /// 13 | /// 14 | public class HttpUserAgentParserMemoryCachedProviderOptions 15 | { 16 | /// 17 | /// Cache options 18 | /// 19 | public MemoryCacheOptions CacheOptions { get; } 20 | 21 | /// 22 | /// Cache entry options 23 | /// 24 | public MemoryCacheEntryOptions CacheEntryOptions { get; } 25 | 26 | /// 27 | /// Creates a new instance of 28 | /// 29 | public HttpUserAgentParserMemoryCachedProviderOptions(MemoryCacheOptions cacheOptions) 30 | : this(cacheOptions, null) { } 31 | 32 | /// 33 | /// Creates a new instance of 34 | /// 35 | public HttpUserAgentParserMemoryCachedProviderOptions(MemoryCacheEntryOptions cacheEntryOptions) 36 | : this(null, cacheEntryOptions) { } 37 | 38 | /// 39 | /// Creates a new instance of 40 | /// 41 | public HttpUserAgentParserMemoryCachedProviderOptions( 42 | MemoryCacheOptions? cacheOptions = null, MemoryCacheEntryOptions? cacheEntryOptions = null) 43 | { 44 | CacheEntryOptions = cacheEntryOptions ?? new MemoryCacheEntryOptions 45 | { 46 | // defaults 47 | SlidingExpiration = TimeSpan.FromDays(1) 48 | }; 49 | 50 | CacheOptions = cacheOptions ?? new MemoryCacheOptions 51 | { 52 | // defaults 53 | SizeLimit = 256 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/DependencyInjection/HttpUserAgentParserServiceCollectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | using MyCSharp.HttpUserAgentParser.DependencyInjection; 5 | using MyCSharp.HttpUserAgentParser.Providers; 6 | using Xunit; 7 | 8 | namespace MyCSharp.HttpUserAgentParser.UnitTests.DependencyInjection; 9 | 10 | public class HttpUserAgentParserMemoryCacheServiceCollectionExtensions 11 | { 12 | public class TestHttpUserAgentParserProvider : IHttpUserAgentParserProvider 13 | { 14 | public HttpUserAgentInformation Parse(string userAgent) => throw new NotSupportedException(); 15 | } 16 | 17 | [Fact] 18 | public void AddHttpUserAgentParser() 19 | { 20 | ServiceCollection services = new(); 21 | 22 | services.AddHttpUserAgentParser(); 23 | 24 | Assert.Single(services); 25 | Assert.Equal(typeof(IHttpUserAgentParserProvider), services[0].ServiceType); 26 | Assert.Equal(typeof(HttpUserAgentParserDefaultProvider), services[0].ImplementationType); 27 | Assert.Equal(ServiceLifetime.Singleton, services[0].Lifetime); 28 | } 29 | 30 | [Fact] 31 | public void AddHttpUserAgentCachedParser() 32 | { 33 | ServiceCollection services = new(); 34 | 35 | services.AddHttpUserAgentCachedParser(); 36 | 37 | Assert.Single(services); 38 | Assert.Equal(typeof(IHttpUserAgentParserProvider), services[0].ServiceType); 39 | Assert.Equal(typeof(HttpUserAgentParserCachedProvider), services[0].ImplementationType); 40 | Assert.Equal(ServiceLifetime.Singleton, services[0].Lifetime); 41 | } 42 | 43 | [Fact] 44 | public void AddHttpUserAgentParser_With_Generic() 45 | { 46 | ServiceCollection services = new(); 47 | 48 | services.AddHttpUserAgentParser(); 49 | 50 | Assert.Single(services); 51 | Assert.Equal(typeof(IHttpUserAgentParserProvider), services[0].ServiceType); 52 | Assert.Equal(typeof(TestHttpUserAgentParserProvider), services[0].ImplementationType); 53 | Assert.Equal(ServiceLifetime.Singleton, services[0].Lifetime); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/release-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | packages: write 10 | 11 | jobs: 12 | publish-nuget: 13 | name: Publish to NuGet.org 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: | 26 | 8.0.x 27 | 9.0.x 28 | 10.0.x 29 | 30 | - name: Restore dependencies 31 | run: dotnet restore 32 | 33 | - name: Build 34 | run: dotnet build --configuration Release --no-restore 35 | 36 | - name: Test 37 | run: dotnet test --configuration Release --no-build --verbosity normal 38 | 39 | - name: Pack NuGet packages 40 | run: dotnet pack --configuration Release --no-build --output ./artifacts 41 | 42 | - name: Verify packages 43 | run: | 44 | echo "Packages to be published:" 45 | ls -la ./artifacts/*.nupkg 46 | 47 | # Verify package count 48 | PACKAGE_COUNT=$(ls ./artifacts/*.nupkg | wc -l) 49 | if [ "$PACKAGE_COUNT" -eq 0 ]; then 50 | echo "Error: No packages found!" 51 | exit 1 52 | fi 53 | 54 | echo "Found $PACKAGE_COUNT package(s) to publish" 55 | 56 | - name: Publish to NuGet.org 57 | env: 58 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 59 | run: | 60 | for package in ./artifacts/*.nupkg; do 61 | echo "Publishing $package to NuGet.org..." 62 | dotnet nuget push "$package" \ 63 | --api-key "$NUGET_API_KEY" \ 64 | --source https://api.nuget.org/v3/index.json \ 65 | --skip-duplicate 66 | done 67 | 68 | echo "All packages published successfully!" 69 | 70 | - name: Upload published packages as artifacts 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: published-nuget-packages 74 | path: ./artifacts/*.nupkg 75 | retention-days: 90 76 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser.MemoryCache/HttpUserAgentParserMemoryCachedProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Microsoft.Extensions.Caching.Memory; 4 | using MyCSharp.HttpUserAgentParser.Providers; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.MemoryCache; 7 | 8 | /// 9 | /// 10 | /// Creates a new instance of . 11 | /// 12 | /// The options used to set expiration and size limit 13 | public class HttpUserAgentParserMemoryCachedProvider( 14 | HttpUserAgentParserMemoryCachedProviderOptions options) : IHttpUserAgentParserProvider 15 | { 16 | private readonly Microsoft.Extensions.Caching.Memory.MemoryCache _memoryCache = new(options.CacheOptions); 17 | private readonly HttpUserAgentParserMemoryCachedProviderOptions _options = options; 18 | 19 | /// 20 | public HttpUserAgentInformation Parse(string userAgent) 21 | { 22 | CacheKey key = GetKey(userAgent); 23 | 24 | return _memoryCache.GetOrCreate(key, static entry => 25 | { 26 | CacheKey key = (entry.Key as CacheKey)!; 27 | entry.SlidingExpiration = key.Options.CacheEntryOptions.SlidingExpiration; 28 | entry.SetSize(1); 29 | 30 | return HttpUserAgentParser.Parse(key.UserAgent); 31 | }); 32 | } 33 | 34 | [ThreadStatic] 35 | private static CacheKey? s_tKey; 36 | 37 | private CacheKey GetKey(string userAgent) 38 | { 39 | CacheKey key = s_tKey ??= new CacheKey(); 40 | 41 | key.UserAgent = userAgent; 42 | key.Options = _options; 43 | 44 | return key; 45 | } 46 | 47 | private class CacheKey : IEquatable // required for IMemoryCache 48 | { 49 | public string UserAgent { get; set; } = null!; 50 | 51 | public HttpUserAgentParserMemoryCachedProviderOptions Options { get; set; } = null!; 52 | 53 | public bool Equals(CacheKey? other) => string.Equals(UserAgent, other?.UserAgent, StringComparison.OrdinalIgnoreCase); 54 | public override bool Equals(object? obj) => Equals(obj as CacheKey); 55 | 56 | public override int GetHashCode() => UserAgent.GetHashCode(StringComparison.Ordinal); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/HttpUserAgentInformationExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Xunit; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.UnitTests; 6 | 7 | public class HttpUserAgentInformationExtensionsTests 8 | { 9 | [Theory] 10 | [InlineData("Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36 EdgA/46.3.4.5155", HttpUserAgentType.Browser, true)] 11 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62", HttpUserAgentType.Browser, false)] 12 | [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML,like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html)", HttpUserAgentType.Robot, false)] 13 | [InlineData("APIs-Google (+https://developers.google.com/webmasters/APIs-Google.html)", HttpUserAgentType.Robot, false)] 14 | [InlineData("WhatsApp/2.22.20.72 A", HttpUserAgentType.Robot, false)] 15 | [InlineData("WhatsApp/2.22.19.78 I", HttpUserAgentType.Robot, false)] 16 | [InlineData("WhatsApp/2.2236.3 N", HttpUserAgentType.Robot, false)] 17 | [InlineData("Invalid user agent", HttpUserAgentType.Unknown, false)] 18 | public void IsType(string userAgent, HttpUserAgentType expectedType, bool isMobile) 19 | { 20 | HttpUserAgentInformation info = HttpUserAgentInformation.Parse(userAgent); 21 | 22 | if (expectedType == HttpUserAgentType.Browser) 23 | { 24 | Assert.True(info.IsType(HttpUserAgentType.Browser)); 25 | Assert.False(info.IsType(HttpUserAgentType.Robot)); 26 | Assert.False(info.IsType(HttpUserAgentType.Unknown)); 27 | 28 | Assert.True(info.IsBrowser()); 29 | Assert.False(info.IsRobot()); 30 | } 31 | else if (expectedType == HttpUserAgentType.Robot) 32 | { 33 | Assert.False(info.IsType(HttpUserAgentType.Browser)); 34 | Assert.True(info.IsType(HttpUserAgentType.Robot)); 35 | Assert.False(info.IsType(HttpUserAgentType.Unknown)); 36 | 37 | Assert.False(info.IsBrowser()); 38 | Assert.True(info.IsRobot()); 39 | } 40 | else if (expectedType == HttpUserAgentType.Unknown) 41 | { 42 | Assert.False(info.IsType(HttpUserAgentType.Browser)); 43 | Assert.False(info.IsType(HttpUserAgentType.Robot)); 44 | Assert.True(info.IsType(HttpUserAgentType.Unknown)); 45 | 46 | Assert.False(info.IsBrowser()); 47 | Assert.False(info.IsRobot()); 48 | } 49 | 50 | Assert.Equal(isMobile, info.IsMobile()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Release Drafter Configuration 2 | # Documentation: https://github.com/release-drafter/release-drafter 3 | 4 | name-template: 'Version $RESOLVED_VERSION' 5 | tag-template: 'v$RESOLVED_VERSION' 6 | 7 | # Categories for organizing release notes 8 | categories: 9 | - title: '🚀 Features' 10 | labels: 11 | - 'feature' 12 | - 'enhancement' 13 | - title: '🐛 Bug Fixes' 14 | labels: 15 | - 'bug' 16 | - 'fix' 17 | - title: '🔧 Maintenance' 18 | labels: 19 | - 'maintenance' 20 | - 'chore' 21 | - 'refactor' 22 | - 'dependencies' 23 | - title: '📚 Documentation' 24 | labels: 25 | - 'documentation' 26 | - 'docs' 27 | - title: '⚡ Performance' 28 | labels: 29 | - 'performance' 30 | - title: '🔒 Security' 31 | labels: 32 | - 'security' 33 | 34 | # Exclude certain labels from release notes 35 | exclude-labels: 36 | - 'skip-changelog' 37 | - 'wip' 38 | 39 | # Change template (how each PR is listed) 40 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 41 | change-title-escapes: '\<*_&' # Escape special markdown characters 42 | 43 | # Template for the release body 44 | template: | 45 | ## What's Changed 46 | 47 | $CHANGES 48 | 49 | ## 📦 NuGet Packages 50 | 51 | The following packages are included in this release: 52 | 53 | - `MyCSharp.HttpUserAgentParser` 54 | - `MyCSharp.HttpUserAgentParser.AspNetCore` 55 | - `MyCSharp.HttpUserAgentParser.MemoryCache` 56 | 57 | ### Installation 58 | 59 | ```bash 60 | dotnet add package MyCSharp.HttpUserAgentParser 61 | dotnet add package MyCSharp.HttpUserAgentParser.AspNetCore 62 | dotnet add package MyCSharp.HttpUserAgentParser.MemoryCache 63 | ``` 64 | 65 | ## Contributors 66 | 67 | $CONTRIBUTORS 68 | 69 | --- 70 | 71 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 72 | 73 | # Automatically label PRs based on modified files 74 | autolabeler: 75 | - label: 'documentation' 76 | files: 77 | - '*.md' 78 | - 'docs/**/*' 79 | - label: 'bug' 80 | branch: 81 | - '/fix\/.+/' 82 | title: 83 | - '/fix/i' 84 | - label: 'feature' 85 | branch: 86 | - '/feature\/.+/' 87 | title: 88 | - '/feature/i' 89 | - label: 'dependencies' 90 | files: 91 | - '**/packages.lock.json' 92 | - '**/*.csproj' 93 | - 'Directory.Packages.props' 94 | - 'Directory.Build.props' 95 | - label: 'github-actions' 96 | files: 97 | - '.github/workflows/**/*' 98 | - label: 'tests' 99 | files: 100 | - 'tests/**/*' 101 | - '**/*Tests.cs' 102 | - '**/*Test.cs' 103 | - label: 'performance' 104 | files: 105 | - 'perf/**/*' 106 | - '**/*Benchmark*.cs' 107 | 108 | # Version resolver (uses version from workflow input) 109 | version-resolver: 110 | default: patch 111 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test (Reusable) 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | dotnet-version: 7 | description: '.NET version to use (can be multi-line for multiple versions)' 8 | required: false 9 | type: string 10 | default: | 11 | 8.0.x 12 | 9.0.x 13 | 10.0.x 14 | configuration: 15 | description: 'Build configuration' 16 | required: false 17 | type: string 18 | default: 'Release' 19 | upload-test-results: 20 | description: 'Whether to upload test results as artifacts' 21 | required: false 22 | type: boolean 23 | default: false 24 | create-pack: 25 | description: 'Whether to pack NuGet packages' 26 | required: false 27 | type: boolean 28 | default: false 29 | outputs: 30 | version: 31 | description: 'The calculated version from NBGV' 32 | value: ${{ jobs.build.outputs.version }} 33 | 34 | jobs: 35 | build: 36 | name: Build and Test 37 | runs-on: ubuntu-latest 38 | outputs: 39 | version: ${{ steps.nbgv.outputs.SemVer2 }} 40 | 41 | steps: 42 | - name: Checkout code 43 | uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 0 # Required for GitVersion 46 | 47 | - name: Setup .NET 48 | uses: actions/setup-dotnet@v4 49 | with: 50 | dotnet-version: ${{ inputs.dotnet-version }} 51 | global-json-file: ./global.json 52 | 53 | - name: Calculate Version with NBGV 54 | uses: dotnet/nbgv@master 55 | id: nbgv 56 | with: 57 | setAllVars: true 58 | 59 | - name: Version Info 60 | run: | 61 | echo "Calculated version: ${{ steps.nbgv.outputs.SemVer2 }}" 62 | 63 | - name: Restore dependencies 64 | run: dotnet restore 65 | 66 | - name: Build 67 | run: dotnet build --configuration ${{ inputs.configuration }} --no-restore /p:Version=${{ steps.nbgv.outputs.SemVer2 }} 68 | 69 | - name: Test 70 | run: dotnet test --configuration ${{ inputs.configuration }} --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" 71 | 72 | - name: Upload test results 73 | if: always() && inputs.upload-test-results 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: test-results-${{ inputs.dotnet-version }} 77 | path: '**/TestResults/**/*.trx' 78 | 79 | - name: Pack NuGet packages 80 | if: inputs.create-pack 81 | run: dotnet pack --configuration ${{ inputs.configuration }} --no-build --output ./artifacts /p:PackageVersion=${{ steps.nbgv.outputs.SemVer2 }} 82 | 83 | - name: Upload NuGet packages 84 | if: inputs.create-pack 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: nuget-packages 88 | path: ./artifacts/*.nupkg 89 | retention-days: 30 90 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | # Justfile .NET - Benjamin Abt 2025 - https://benjamin-abt.com 2 | # https://github.com/BenjaminAbt/templates/blob/main/justfile/dotnet 3 | 4 | set shell := ["pwsh", "-c"] 5 | 6 | # ===== Configurable defaults ===== 7 | CONFIG := "Debug" 8 | TFM := "net10.0" 9 | BENCH_PRJ := "perf/HttpUserAgentParser.Benchmarks/HttpUserAgentParser.Benchmarks.csproj" 10 | 11 | # ===== Default / Help ===== 12 | default: help 13 | 14 | help: 15 | # Overview: 16 | just --list 17 | # Usage: 18 | # just build 19 | # just test 20 | # just bench 21 | 22 | # ===== Basic .NET Workflows ===== 23 | restore: 24 | dotnet restore 25 | 26 | build *ARGS: 27 | dotnet build --configuration "{{CONFIG}}" --nologo --verbosity minimal {{ARGS}} 28 | 29 | rebuild *ARGS: 30 | dotnet build --configuration "{{CONFIG}}" --nologo --verbosity minimal --no-incremental {{ARGS}} 31 | 32 | clean: 33 | dotnet clean --configuration "{{CONFIG}}" --nologo 34 | 35 | run *ARGS: 36 | dotnet run --project --framework "{{TFM}}" --configuration "{{CONFIG}}" --no-launch-profile {{ARGS}} 37 | 38 | # ===== Quality / Tests ===== 39 | format: 40 | dotnet format --verbosity minimal 41 | 42 | format-check: 43 | dotnet format --verify-no-changes --verbosity minimal 44 | 45 | test *ARGS: 46 | dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal {{ARGS}} 47 | 48 | test-cov: 49 | dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal /p:CollectCoverage=true /p:CoverletOutputFormat="cobertura,lcov,opencover" /p:CoverletOutput="./TestResults/coverage/coverage" 50 | 51 | 52 | test-filter QUERY: 53 | dotnet test --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal --filter "{{QUERY}}" 54 | 55 | # ===== Packaging / Release ===== 56 | pack *ARGS: 57 | dotnet pack --configuration "{{CONFIG}}" --nologo --verbosity minimal -o "./artifacts/packages" {{ARGS}} 58 | 59 | publish *ARGS: 60 | dotnet publish --configuration "{{CONFIG}}" --framework "{{TFM}}" --nologo --verbosity minimal -o "./artifacts/publish/{{TFM}}" {{ARGS}} 61 | 62 | publish-sc RID *ARGS: 63 | dotnet publish --configuration "{{CONFIG}}" --framework "{{TFM}}" --runtime "{{RID}}" --self-contained true -p:PublishSingleFile=true -p:PublishTrimmed=false --nologo --verbosity minimal -o "./artifacts/publish/{{TFM}}-{{RID}}" {{ARGS}} 64 | 65 | # ===== Benchmarks ===== 66 | bench *ARGS: 67 | dotnet run --configuration Release --project "{{BENCH_PRJ}}" --framework "{{TFM}}" {{ARGS}} 68 | 69 | # ===== Housekeeping ===== 70 | clean-artifacts: 71 | if (Test-Path "./artifacts") { Remove-Item "./artifacts" -Recurse -Force } 72 | 73 | clean-all: 74 | just clean 75 | just clean-artifacts 76 | # Optionally: git clean -xdf 77 | 78 | # ===== Combined Flows ===== 79 | fmt-build: 80 | just format 81 | just build 82 | 83 | ci: 84 | just clean 85 | just restore 86 | just format-check 87 | just build 88 | just test-cov 89 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/HttpUserAgentInformationTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using System.Text.RegularExpressions; 4 | using Xunit; 5 | 6 | namespace MyCSharp.HttpUserAgentParser.UnitTests; 7 | 8 | public partial class HttpUserAgentInformationTests 9 | { 10 | [Theory] 11 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62")] 12 | public void Parse(string userAgent) 13 | { 14 | HttpUserAgentInformation ua1 = HttpUserAgentParser.Parse(userAgent); 15 | HttpUserAgentInformation ua2 = HttpUserAgentInformation.Parse(userAgent); 16 | 17 | Assert.Equal(ua2, ua1); 18 | } 19 | 20 | [Theory] 21 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.62")] 22 | public void CreateForRobot(string userAgent) 23 | { 24 | HttpUserAgentInformation ua = HttpUserAgentInformation.CreateForRobot(userAgent, "Chrome"); 25 | 26 | Assert.Equal(userAgent, ua.UserAgent); 27 | Assert.Equal(HttpUserAgentType.Robot, ua.Type); 28 | Assert.Null(ua.Platform); 29 | Assert.Equal("Chrome", ua.Name); 30 | Assert.Null(ua.Version); 31 | Assert.Null(ua.MobileDeviceType); 32 | } 33 | 34 | [Theory] 35 | [InlineData("Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36 EdgA/46.3.4.5155")] 36 | public void CreateForBrowser(string userAgent) 37 | { 38 | HttpUserAgentPlatformInformation platformInformation = new(TextRegex(), "Android", HttpUserAgentPlatformType.Android); 39 | 40 | HttpUserAgentInformation ua = HttpUserAgentInformation.CreateForBrowser(userAgent, 41 | platformInformation, "Edge", "46.3.4.5155", "Android"); 42 | 43 | Assert.Equal(userAgent, ua.UserAgent); 44 | Assert.Equal(HttpUserAgentType.Browser, ua.Type); 45 | Assert.Equal(platformInformation, ua.Platform); 46 | Assert.Equal("Edge", ua.Name); 47 | Assert.Equal("46.3.4.5155", ua.Version); 48 | Assert.Equal("Android", ua.MobileDeviceType); 49 | } 50 | 51 | [Theory] 52 | [InlineData("Invalid user agent")] 53 | public void CreateForUnknown(string userAgent) 54 | { 55 | HttpUserAgentPlatformInformation platformInformation = new(TextRegex(), "Batman", HttpUserAgentPlatformType.Linux); 56 | 57 | HttpUserAgentInformation ua = 58 | HttpUserAgentInformation.CreateForUnknown(userAgent, platformInformation, deviceName: null); 59 | 60 | Assert.Equal(userAgent, ua.UserAgent); 61 | Assert.Equal(HttpUserAgentType.Unknown, ua.Type); 62 | Assert.Equal(platformInformation, ua.Platform); 63 | Assert.Null(ua.Name); 64 | Assert.Null(ua.Version); 65 | Assert.Null(ua.MobileDeviceType); 66 | } 67 | 68 | [GeneratedRegex("", RegexOptions.None, matchTimeoutMilliseconds: 1000)] 69 | private static partial Regex TextRegex(); 70 | } 71 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/VectorExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.Intrinsics; 5 | using System.Runtime.Intrinsics.Arm; 6 | using System.Runtime.Intrinsics.X86; 7 | 8 | namespace MyCSharp.HttpUserAgentParser; 9 | 10 | internal static class VectorExtensions 11 | { 12 | extension(ref char c) 13 | { 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public Vector128 ReadVector128AsBytes(int offset) 16 | { 17 | ref short ptr = ref Unsafe.As(ref c); 18 | 19 | #if NET10_0_OR_GREATER 20 | return Vector128.NarrowWithSaturation( 21 | Vector128.LoadUnsafe(ref ptr, (uint)offset), 22 | Vector128.LoadUnsafe(ref ptr, (uint)(offset + Vector128.Count)) 23 | ).AsByte(); 24 | #else 25 | if (Sse2.IsSupported) 26 | { 27 | return Sse2.PackUnsignedSaturate( 28 | Vector128.LoadUnsafe(ref ptr, (uint)offset), 29 | Vector128.LoadUnsafe(ref ptr, (uint)(offset + Vector128.Count))); 30 | } 31 | else if (AdvSimd.Arm64.IsSupported) 32 | { 33 | return AdvSimd.Arm64.UnzipEven( 34 | Vector128.LoadUnsafe(ref ptr, (uint)offset).AsByte(), 35 | Vector128.LoadUnsafe(ref ptr, (uint)(offset + Vector128.Count)).AsByte()); 36 | } 37 | else 38 | { 39 | return Vector128.Narrow( 40 | Vector128.LoadUnsafe(ref ptr, (uint)offset), 41 | Vector128.LoadUnsafe(ref ptr, (uint)(offset + Vector128.Count)) 42 | ).AsByte(); 43 | } 44 | #endif 45 | } 46 | 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | public Vector256 ReadVector256AsBytes(int offset) 49 | { 50 | ref short ptr = ref Unsafe.As(ref c); 51 | 52 | #if NET10_0_OR_GREATER 53 | return Vector256.NarrowWithSaturation( 54 | Vector256.LoadUnsafe(ref ptr, (uint)offset), 55 | Vector256.LoadUnsafe(ref ptr, (uint)offset + (uint)Vector256.Count) 56 | ).AsByte(); 57 | #else 58 | if (Avx2.IsSupported) 59 | { 60 | Vector256 tmp = Avx2.PackUnsignedSaturate( 61 | Vector256.LoadUnsafe(ref ptr, (uint)offset), 62 | Vector256.LoadUnsafe(ref ptr, (uint)offset + (uint)Vector256.Count)); 63 | 64 | Vector256 tmp1 = Avx2.Permute4x64(tmp.AsInt64(), 0b_11_01_10_00); 65 | 66 | return tmp1.AsByte(); 67 | } 68 | else 69 | { 70 | return Vector256.Narrow( 71 | Vector256.LoadUnsafe(ref ptr, (uint)offset), 72 | Vector256.LoadUnsafe(ref ptr, (uint)offset + (uint)Vector256.Count) 73 | ).AsByte(); 74 | } 75 | #endif 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /perf/HttpUserAgentParser.Benchmarks/LibraryComparison/LibraryComparisonBenchmarks.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Columns; 5 | using BenchmarkDotNet.Configs; 6 | using BenchmarkDotNet.Diagnosers; 7 | using DeviceDetectorNET; 8 | using MyCSharp.HttpUserAgentParser; 9 | using MyCSharp.HttpUserAgentParser.Providers; 10 | 11 | namespace HttpUserAgentParser.Benchmarks.LibraryComparison; 12 | 13 | [ShortRunJob] 14 | [MemoryDiagnoser] 15 | [CategoriesColumn] 16 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 17 | public class LibraryComparisonBenchmarks 18 | { 19 | public record TestData(string Label, string UserAgent) 20 | { 21 | public override string ToString() => Label; 22 | } 23 | 24 | [ParamsSource(nameof(GetTestUserAgents))] 25 | public TestData Data { get; set; } 26 | 27 | public IEnumerable GetTestUserAgents() 28 | { 29 | yield return new("Chrome Win10", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"); 30 | yield return new("Google-Bot", "APIs-Google (+https://developers.google.com/webmasters/APIs-Google.html)"); 31 | } 32 | 33 | [Benchmark(Baseline = true, Description = "MyCSharp")] 34 | [BenchmarkCategory("Basic")] 35 | public HttpUserAgentInformation MyCSharpBasic() 36 | { 37 | HttpUserAgentInformation info = MyCSharp.HttpUserAgentParser.HttpUserAgentParser.Parse(Data.UserAgent); 38 | return info; 39 | } 40 | 41 | private static readonly HttpUserAgentParserCachedProvider s_myCSharpCachedProvider = new(); 42 | 43 | [Benchmark(Baseline = true, Description = "MyCSharp")] 44 | [BenchmarkCategory("Cached")] 45 | public HttpUserAgentInformation MyCSharpCached() 46 | { 47 | return s_myCSharpCachedProvider.Parse(Data.UserAgent); 48 | } 49 | 50 | [Benchmark(Description = "UAParser")] 51 | [BenchmarkCategory("Basic")] 52 | public UAParser.ClientInfo UAParserBasic() 53 | { 54 | UAParser.ClientInfo info = UAParser.Parser.GetDefault().Parse(Data.UserAgent); 55 | return info; 56 | } 57 | 58 | private static readonly UAParser.Parser s_uaParser = UAParser.Parser.GetDefault(new UAParser.ParserOptions { UseCompiledRegex = true }); 59 | 60 | [Benchmark(Description = "UAParser")] 61 | [BenchmarkCategory("Cached")] 62 | public UAParser.ClientInfo UAParserCached() 63 | { 64 | UAParser.ClientInfo info = s_uaParser.Parse(Data.UserAgent); 65 | return info; 66 | } 67 | 68 | [Benchmark(Description = "DeviceDetector.NET")] 69 | [BenchmarkCategory("Basic")] 70 | public object DeviceDetectorNETBasic() 71 | { 72 | DeviceDetector dd = new(Data.UserAgent); 73 | dd.Parse(); 74 | 75 | var info = new 76 | { 77 | Client = dd.GetClient(), 78 | OS = dd.GetOs(), 79 | Device = dd.GetDeviceName(), 80 | Brand = dd.GetBrandName(), 81 | Model = dd.GetModel() 82 | }; 83 | 84 | return info; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/HttpUserAgentInformation.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | namespace MyCSharp.HttpUserAgentParser; 4 | 5 | /// 6 | /// Analyzed user agent 7 | /// 8 | public readonly struct HttpUserAgentInformation 9 | { 10 | /// 11 | /// Full User Agent string 12 | /// 13 | public string UserAgent { get; } 14 | 15 | /// 16 | /// Type of user agent, see 17 | /// 18 | public HttpUserAgentType Type { get; } 19 | 20 | /// 21 | /// Platform of user agent, see 22 | /// 23 | public HttpUserAgentPlatformInformation? Platform { get; } 24 | 25 | /// 26 | /// Browser or Bot Name of user agent e.g. "Chrome", "Edge".. 27 | /// 28 | public string? Name { get; } 29 | 30 | /// 31 | /// Version of Browser or Bot Name of user agent e.g. "79.0", "83.0.125.4" 32 | /// 33 | public string? Version { get; } 34 | 35 | /// 36 | /// Device Type of user agent, e.g. "Android", "Apple iPhone" 37 | /// 38 | public string? MobileDeviceType { get; } 39 | 40 | /// 41 | /// Creates a new instance of 42 | /// 43 | private HttpUserAgentInformation(string userAgent, HttpUserAgentPlatformInformation? platform, HttpUserAgentType type, string? name, string? version, string? deviceName) 44 | { 45 | UserAgent = userAgent; 46 | Type = type; 47 | Name = name; 48 | Platform = platform; 49 | Version = version; 50 | MobileDeviceType = deviceName; 51 | } 52 | 53 | /// 54 | /// Parses given User Agent 55 | /// 56 | public static HttpUserAgentInformation Parse(string userAgent) => HttpUserAgentParser.Parse(userAgent); 57 | 58 | /// 59 | /// Creates for a robot 60 | /// 61 | internal static HttpUserAgentInformation CreateForRobot(string userAgent, string robotName) 62 | => new(userAgent, platform: null, HttpUserAgentType.Robot, robotName, version: null, deviceName: null); 63 | 64 | /// 65 | /// Creates for a browser 66 | /// 67 | internal static HttpUserAgentInformation CreateForBrowser(string userAgent, HttpUserAgentPlatformInformation? platform, string? browserName, string? browserVersion, string? deviceName) 68 | => new(userAgent, platform, HttpUserAgentType.Browser, browserName, browserVersion, deviceName); 69 | 70 | /// 71 | /// Creates for an unknown agent type 72 | /// 73 | internal static HttpUserAgentInformation CreateForUnknown(string userAgent, HttpUserAgentPlatformInformation? platform, string? deviceName) 74 | => new(userAgent, platform, HttpUserAgentType.Unknown, name: null, version: null, deviceName); 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/main-build.yml: -------------------------------------------------------------------------------- 1 | name: Main Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | 12 | jobs: 13 | build-and-test: 14 | name: Build, Test and Pack 15 | uses: ./.github/workflows/build-and-test.yml 16 | with: 17 | create-pack: true 18 | 19 | create-draft-release: 20 | name: Create Draft Release 21 | needs: build-and-test 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: write 25 | 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | 30 | - name: Download NuGet packages 31 | uses: actions/download-artifact@v4 32 | with: 33 | name: nuget-packages 34 | path: ./artifacts 35 | 36 | - name: Check if tag exists 37 | id: check-tag 38 | run: | 39 | TAG="v${{ needs.build-and-test.outputs.version }}" 40 | if git rev-parse "$TAG" >/dev/null 2>&1; then 41 | echo "exists=true" >> $GITHUB_OUTPUT 42 | echo "⚠️ Tag $TAG already exists" 43 | else 44 | echo "exists=false" >> $GITHUB_OUTPUT 45 | echo "✅ Tag $TAG does not exist yet" 46 | fi 47 | 48 | - name: Delete existing draft releases 49 | if: steps.check-tag.outputs.exists == 'false' 50 | env: 51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | run: | 53 | echo "Checking for existing draft releases..." 54 | gh release list --json isDraft,tagName --jq '.[] | select(.isDraft) | .tagName' | while read -r tag_name; do 55 | echo "Deleting existing draft release: $tag_name" 56 | gh release delete "$tag_name" --yes --cleanup-tag || echo "Failed to delete release $tag_name" 57 | done 58 | 59 | - name: Create Draft Release 60 | if: steps.check-tag.outputs.exists == 'false' 61 | uses: release-drafter/release-drafter@v6 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | with: 65 | config-name: release-drafter.yml 66 | version: v${{ needs.build-and-test.outputs.version }} 67 | tag: v${{ needs.build-and-test.outputs.version }} 68 | name: Version ${{ needs.build-and-test.outputs.version }} 69 | publish: false 70 | prerelease: false 71 | 72 | - name: Upload packages to draft release 73 | if: steps.check-tag.outputs.exists == 'false' 74 | env: 75 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | run: | 77 | TAG="v${{ needs.build-and-test.outputs.version }}" 78 | # Wait a moment for the release to be created 79 | sleep 2 80 | 81 | # Upload new artifacts to the draft release 82 | echo "📦 Uploading new NuGet packages..." 83 | for file in ./artifacts/*.nupkg; do 84 | gh release upload "$TAG" "$file" --clobber 85 | done 86 | echo "✅ Uploaded NuGet packages to draft release" 87 | 88 | - name: Summary 89 | if: steps.check-tag.outputs.exists == 'false' 90 | run: | 91 | echo "✅ Draft release created/updated" >> $GITHUB_STEP_SUMMARY 92 | echo "Version: v${{ needs.build-and-test.outputs.version }}" >> $GITHUB_STEP_SUMMARY 93 | echo "" >> $GITHUB_STEP_SUMMARY 94 | echo "### Next steps:" >> $GITHUB_STEP_SUMMARY 95 | echo "1. Go to [Releases](../../releases)" >> $GITHUB_STEP_SUMMARY 96 | echo "2. Review the draft release" >> $GITHUB_STEP_SUMMARY 97 | echo "3. Edit release notes if needed" >> $GITHUB_STEP_SUMMARY 98 | echo "4. Publish release to trigger production deployment" >> $GITHUB_STEP_SUMMARY 99 | 100 | - name: Release already exists 101 | if: steps.check-tag.outputs.exists == 'true' 102 | run: | 103 | echo "ℹ️ Release v${{ needs.build-and-test.outputs.version }} already exists" >> $GITHUB_STEP_SUMMARY 104 | echo "No action taken" >> $GITHUB_STEP_SUMMARY 105 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers 30 | 31 | 32 | 33 | 34 | 35 | 36 | all 37 | runtime; build; native; contentfiles; analyzers; buildtransitive 38 | 39 | 40 | all 41 | runtime; build; native; contentfiles; analyzers; buildtransitive 42 | 43 | 44 | 45 | 46 | all 47 | runtime; build; native; contentfiles; analyzers 48 | 49 | 50 | all 51 | runtime; build; native; contentfiles; analyzers 52 | 53 | 54 | all 55 | runtime; build; native; contentfiles; analyzers 56 | 57 | 58 | all 59 | runtime; build; native; contentfiles; analyzers 60 | 61 | 62 | all 63 | runtime; build; native; contentfiles; analyzers 64 | 65 | 66 | all 67 | runtime; build; native; contentfiles; analyzers 68 | 69 | 70 | all 71 | runtime; build; native; contentfiles; analyzers 72 | 73 | 74 | -------------------------------------------------------------------------------- /MyCSharp.HttpUserAgentParser.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.32804.182 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{008A2BAB-78B4-42EB-A5D4-DE434438CEF0}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpUserAgentParser.AspNetCore", "src\HttpUserAgentParser.AspNetCore\HttpUserAgentParser.AspNetCore.csproj", "{45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpUserAgentParser", "src\HttpUserAgentParser\HttpUserAgentParser.csproj", "{3357BEC0-8216-409E-A539-F9A71DBACB81}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpUserAgentParser.UnitTests", "tests\HttpUserAgentParser.UnitTests\HttpUserAgentParser.UnitTests.csproj", "{F16697F7-74B4-441D-A0C0-1A0572AC3AB0}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpUserAgentParser.AspNetCore.UnitTests", "tests\HttpUserAgentParser.AspNetCore.UnitTests\HttpUserAgentParser.AspNetCore.UnitTests.csproj", "{75960783-8BF9-479C-9ECF-E9653B74C9A2}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F54C9296-4EF7-40F0-9F20-F23A2270ABC9}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpUserAgentParser.MemoryCache", "src\HttpUserAgentParser.MemoryCache\HttpUserAgentParser.MemoryCache.csproj", "{3C8CCD44-F47C-4624-8997-54C42F02E376}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpUserAgentParser.MemoryCache.UnitTests", "tests\HttpUserAgentParser.MemoryCache.UnitTests\HttpUserAgentParser.MemoryCache.UnitTests.csproj", "{39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{5738CE0D-5E6E-47CB-BFF5-08F45A2C33AD}" 23 | ProjectSection(SolutionItems) = preProject 24 | .editorconfig = .editorconfig 25 | .gitignore = .gitignore 26 | .github\workflows\ci.yml = .github\workflows\ci.yml 27 | Directory.Build.props = Directory.Build.props 28 | Directory.Packages.props = Directory.Packages.props 29 | global.json = global.json 30 | Justfile = Justfile 31 | LICENSE = LICENSE 32 | NuGet.config = NuGet.config 33 | README.md = README.md 34 | version.json = version.json 35 | EndProjectSection 36 | EndProject 37 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{FAAD18A0-E1B8-448D-B611-AFBDA8A89808}" 38 | EndProject 39 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpUserAgentParser.Benchmarks", "perf\HttpUserAgentParser.Benchmarks\HttpUserAgentParser.Benchmarks.csproj", "{A0D213E9-6408-46D1-AFAF-5096C2F6E027}" 40 | EndProject 41 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpUserAgentParser.TestHelpers", "tests\HttpUserAgentParser.TestHelpers\HttpUserAgentParser.TestHelpers.csproj", "{165EE915-1A4F-4875-90CE-1A2AE1540AE7}" 42 | EndProject 43 | Global 44 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 45 | Debug|Any CPU = Debug|Any CPU 46 | Release|Any CPU = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 49 | {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {3357BEC0-8216-409E-A539-F9A71DBACB81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {3357BEC0-8216-409E-A539-F9A71DBACB81}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {3357BEC0-8216-409E-A539-F9A71DBACB81}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {3357BEC0-8216-409E-A539-F9A71DBACB81}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {F16697F7-74B4-441D-A0C0-1A0572AC3AB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {F16697F7-74B4-441D-A0C0-1A0572AC3AB0}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {F16697F7-74B4-441D-A0C0-1A0572AC3AB0}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {F16697F7-74B4-441D-A0C0-1A0572AC3AB0}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {75960783-8BF9-479C-9ECF-E9653B74C9A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {75960783-8BF9-479C-9ECF-E9653B74C9A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {75960783-8BF9-479C-9ECF-E9653B74C9A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {75960783-8BF9-479C-9ECF-E9653B74C9A2}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {3C8CCD44-F47C-4624-8997-54C42F02E376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {3C8CCD44-F47C-4624-8997-54C42F02E376}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {3C8CCD44-F47C-4624-8997-54C42F02E376}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {3C8CCD44-F47C-4624-8997-54C42F02E376}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {A0D213E9-6408-46D1-AFAF-5096C2F6E027}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {A0D213E9-6408-46D1-AFAF-5096C2F6E027}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {A0D213E9-6408-46D1-AFAF-5096C2F6E027}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {A0D213E9-6408-46D1-AFAF-5096C2F6E027}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {165EE915-1A4F-4875-90CE-1A2AE1540AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {165EE915-1A4F-4875-90CE-1A2AE1540AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {165EE915-1A4F-4875-90CE-1A2AE1540AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {165EE915-1A4F-4875-90CE-1A2AE1540AE7}.Release|Any CPU.Build.0 = Release|Any CPU 81 | EndGlobalSection 82 | GlobalSection(SolutionProperties) = preSolution 83 | HideSolutionNode = FALSE 84 | EndGlobalSection 85 | GlobalSection(NestedProjects) = preSolution 86 | {45927CF7-1BF4-479B-BBAA-8AD9CA901AE4} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} 87 | {3357BEC0-8216-409E-A539-F9A71DBACB81} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} 88 | {F16697F7-74B4-441D-A0C0-1A0572AC3AB0} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} 89 | {75960783-8BF9-479C-9ECF-E9653B74C9A2} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} 90 | {3C8CCD44-F47C-4624-8997-54C42F02E376} = {008A2BAB-78B4-42EB-A5D4-DE434438CEF0} 91 | {39FC1EC2-2AD3-411F-A545-AB6CCB94FB7E} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} 92 | {A0D213E9-6408-46D1-AFAF-5096C2F6E027} = {FAAD18A0-E1B8-448D-B611-AFBDA8A89808} 93 | {165EE915-1A4F-4875-90CE-1A2AE1540AE7} = {F54C9296-4EF7-40F0-9F20-F23A2270ABC9} 94 | EndGlobalSection 95 | GlobalSection(ExtensibilityGlobals) = postSolution 96 | SolutionGuid = {E8B0C994-0BF2-4692-9E22-E48B265B2804} 97 | EndGlobalSection 98 | EndGlobal 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | ##**/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # Visual Studio Trace Files 107 | *.e2e 108 | 109 | # TFS 2012 Local Workspace 110 | $tf/ 111 | 112 | # Guidance Automation Toolkit 113 | *.gpState 114 | 115 | # ReSharper is a .NET coding add-in 116 | _ReSharper*/ 117 | *.[Rr]e[Ss]harper 118 | *.DotSettings.user 119 | 120 | # JustCode is a .NET coding add-in 121 | .JustCode 122 | 123 | # TeamCity is a build add-in 124 | _TeamCity* 125 | 126 | # DotCover is a Code Coverage Tool 127 | *.dotCover 128 | 129 | # AxoCover is a Code Coverage Tool 130 | .axoCover/* 131 | !.axoCover/settings.json 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # Note: Comment the next line if you want to checkin your web deploy settings, 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/[Pp]ackages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/[Pp]ackages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/[Pp]ackages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | *.appx 204 | 205 | # Visual Studio cache files 206 | # files ending in .cache can be ignored 207 | *.[Cc]ache 208 | # but keep track of directories ending in .cache 209 | !*.[Cc]ache/ 210 | 211 | # Others 212 | ClientBin/ 213 | ~$* 214 | *~ 215 | *.dbmdl 216 | *.dbproj.schemaview 217 | *.jfm 218 | *.pfx 219 | *.publishsettings 220 | orleans.codegen.cs 221 | 222 | # Including strong name files can present a security risk 223 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 224 | #*.snk 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | ServiceFabricBackup/ 241 | 242 | # SQL Server files 243 | *.mdf 244 | *.ldf 245 | *.ndf 246 | 247 | # Business Intelligence projects 248 | *.rdl.data 249 | *.bim.layout 250 | *.bim_*.settings 251 | 252 | # Microsoft Fakes 253 | FakesAssemblies/ 254 | 255 | # GhostDoc plugin setting file 256 | *.GhostDoc.xml 257 | 258 | # Node.js Tools for Visual Studio 259 | .ntvs_analysis.dat 260 | node_modules/ 261 | 262 | # Visual Studio 6 build log 263 | *.plg 264 | 265 | # Visual Studio 6 workspace options file 266 | *.opt 267 | 268 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 269 | *.vbw 270 | 271 | # Visual Studio LightSwitch build output 272 | **/*.HTMLClient/GeneratedArtifacts 273 | **/*.DesktopClient/GeneratedArtifacts 274 | **/*.DesktopClient/ModelManifest.xml 275 | **/*.Server/GeneratedArtifacts 276 | **/*.Server/ModelManifest.xml 277 | _Pvt_Extensions 278 | 279 | # Paket dependency manager 280 | .paket/paket.exe 281 | paket-files/ 282 | 283 | # FAKE - F# Make 284 | .fake/ 285 | 286 | # JetBrains Rider 287 | .idea/ 288 | *.sln.iml 289 | 290 | # CodeRush 291 | .cr/ 292 | 293 | # Python Tools for Visual Studio (PTVS) 294 | __pycache__/ 295 | *.pyc 296 | 297 | # Cake - Uncomment if you are using it 298 | # tools/** 299 | # !tools/packages.config 300 | 301 | # Tabs Studio 302 | *.tss 303 | 304 | # Telerik's JustMock configuration file 305 | *.jmconfig 306 | 307 | # BizTalk build output 308 | *.btp.cs 309 | *.btm.cs 310 | *.odx.cs 311 | *.xsd.cs 312 | 313 | # OpenCover UI analysis results 314 | OpenCover/ 315 | 316 | # Azure Stream Analytics local run output 317 | ASALocalRun/ 318 | 319 | # MSBuild Binary and Structured Log 320 | *.binlog 321 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | MyCSharp.HttpUserAgentParser 4 | MyCSharp, BenjaminAbt, gfoidl 5 | MyCSharp.de 6 | 7 | 8 | 9 | true 10 | true 11 | true 12 | 13 | $(MSBuildProjectName.EndsWith('Tests')) 14 | $(MSBuildProjectName.EndsWith('UnitTests')) 15 | $(MSBuildProjectName.EndsWith('IntegrationTests')) 16 | $(MsBuildProjectName.EndsWith('Benchmarks')) 17 | 18 | 19 | 20 | net8.0;net9.0;net10.0 21 | MyCSharp.$(MSBuildProjectName) 22 | MyCSharp.$(MSBuildProjectName) 23 | 24 | 25 | 26 | true 27 | $(MSBuildThisFileDirectory)MyCSharp.HttpUserAgentParser.snk 28 | 29 | 30 | 00240000048000009400000006020000002400005253413100040000010001003d5c022c088a46d41d5a5bf7591f3a3dcba30f76b0f43a312b6e45bb419d32283175cbd8bfd83134b123da6db83479e50596fb6bbe0e8c6cef50c01c64a0861c963daaf6905920f44ffe1ce44b3cfcb9c23779f34bc90c7b04e74e36a19bb58af3a69456d49b56993969dba9f8e9e935c2757844a11066d1091477f10cd923b7 31 | 32 | 33 | 34 | 35 | preview 36 | embedded 37 | enable 38 | en-US 39 | enable 40 | true 41 | 42 | 43 | 44 | 45 | true 46 | true 47 | 48 | 49 | 50 | false 51 | true 52 | 2.12 53 | true 54 | 55 | HTTP User Agent Parser for .NET 56 | https://github.com/mycsharp/HttpUserAgentParser 57 | https://github.com/mycsharp/HttpUserAgentParser 58 | UserAgent, User Agent, Parse, Browser, Client, Detector, Detection, Console, ASP, Desktop, Mobile 59 | 60 | 61 | 62 | true 63 | 64 | 65 | 66 | 67 | true 68 | 69 | 70 | 71 | true 72 | all 73 | low 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | all 83 | runtime; build; native; contentfiles; analyzers 84 | 85 | 86 | 87 | 88 | 89 | all 90 | runtime; build; native; contentfiles; analyzers; buildtransitive 91 | 92 | 93 | all 94 | runtime; build; native; contentfiles; analyzers; buildtransitive 95 | 96 | 97 | 98 | 99 | 100 | true 101 | lcov,opencover,cobertura 102 | $(MSBuildThisFileDirectory)TestResults/coverage/$(MSBuildProjectName). 103 | GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute 104 | **/*Program.cs;**/*Startup.cs;**/*GlobalUsings.cs 105 | true 106 | 107 | 108 | 96 109 | line 110 | total 111 | 112 | 113 | 114 | 115 | [MyCSharp.HttpUserAgentParser]* 116 | 117 | 118 | [MyCSharp.HttpUserAgentParser.MemoryCache]* 119 | 120 | 121 | [MyCSharp.HttpUserAgentParser.AspNetCore]* 122 | 123 | 124 | 125 | 126 | all 127 | runtime; build; native; contentfiles; analyzers 128 | 129 | 130 | all 131 | runtime; build; native; contentfiles; analyzers 132 | 133 | 134 | all 135 | runtime; build; native; contentfiles; analyzers 136 | 137 | 138 | all 139 | runtime; build; native; contentfiles; analyzers 140 | 141 | 142 | all 143 | runtime; build; native; contentfiles; analyzers 144 | 145 | 146 | all 147 | runtime; build; native; contentfiles; analyzers 148 | 149 | 150 | all 151 | runtime; build; native; contentfiles; analyzers 152 | 153 | 154 | 155 | 156 | 157 | 159 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MyCSharp.HttpUserAgentParser 2 | 3 | Parsing HTTP User Agents with .NET 4 | 5 | ## NuGet 6 | 7 | | NuGet | 8 | |-| 9 | | [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) | 10 | | [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.MemoryCache.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.MemoryCache)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache)| `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCach.MemoryCache` | 11 | | [![MyCSharp.HttpUserAgentParser.AspNetCore](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.AspNetCore.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.AspNetCore)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.AspNetCore) | `dotnet add package MyCSharp.HttpUserAgentParser.AspNetCore` | 12 | 13 | 14 | ## Usage 15 | 16 | ```csharp 17 | string userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"; 18 | HttpUserAgentInformation info = HttpUserAgentParser.Parse(userAgent); // alias HttpUserAgentInformation.Parse() 19 | ``` 20 | returns 21 | ```csharp 22 | UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36" 23 | Type = HttpUserAgentType.Browser 24 | Platform = { 25 | Name = "Windows 10", 26 | PlatformType = HttpUserAgentPlatformType.Windows 27 | } 28 | Name = "Chrome" 29 | Version = "90.0.4430.212" 30 | MobileDeviceType = null 31 | ``` 32 | 33 | ### Dependency Injection and Caching 34 | 35 | For dependency injection mechanisms, the `IHttpUserAgentParserProvider` interface exists, for which built-in or custom caching mechanisms can be used. The use is always: 36 | 37 | ```csharp 38 | private IHttpUserAgentParserProvider _parser; 39 | public void MyMethod(string userAgent) 40 | { 41 | HttpUserAgentInformation info = _parser.Parse(userAgent); 42 | } 43 | ``` 44 | 45 | If no cache is required but dependency injection is still desired, the default cache provider can simply be used. This registers the `HttpUserAgentParserDefaultProvider`, which does not cache at all. 46 | 47 | ```csharp 48 | public void ConfigureServices(IServiceCollection services) 49 | { 50 | services.AddHttpUserAgentParser(); // uses HttpUserAgentParserDefaultProvider and does not cache 51 | } 52 | ``` 53 | 54 | Likewise, an In Process Cache mechanism is provided, based on a `ConcurrentDictionary`. 55 | 56 | ```csharp 57 | public void ConfigureServices(IServiceCollection services) 58 | { 59 | services.AddHttpUserAgentCachedParser(); // uses `HttpUserAgentParserCachedProvider` 60 | // or 61 | // services.AddHttpUserAgentParser(); 62 | } 63 | ``` 64 | 65 | This is especially recommended for tests. For web applications, the `IMemoryCache` implementation should be used, which offers a timed expiration of the entries. 66 | 67 | The package [MyCSharp.HttpUserAgentParser.MemoryCache](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache) is required to use the IMemoryCache. This enables the registration of the `IMemoryCache` implementation: 68 | 69 | 70 | ```csharp 71 | public void ConfigureServices(IServiceCollection services) 72 | { 73 | services.AddHttpUserAgentMemoryCachedParser(); 74 | 75 | // or use options 76 | 77 | services.AddHttpUserAgentMemoryCachedParser(options => 78 | { 79 | options.CacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes(60); // default is 1 day 80 | 81 | // limit the total entries in the MemoryCache 82 | // each unique user agent string counts as one entry 83 | options.CacheOptions.SizeLimit = 1024; // default is null (= no limit) 84 | }); 85 | } 86 | ``` 87 | 88 | > `AddHttpUserAgentMemoryCachedParser` registers `HttpUserAgentParserMemoryCachedProvider` as singleton which contains an isolated `MemoryCache` object. 89 | 90 | ### ASP.NET Core 91 | 92 | For ASP.NET Core applications, an accessor pattern (`IHttpUserAgentParserAccessor`) implementation can be registered additionally that independently retrieves the user agent based on the `HttpContextAccessor`. This requires the package [MyCSharp.HttpUserAgentParser.AspNetCore](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.AspNetCore) 93 | 94 | ```csharp 95 | public void ConfigureServices(IServiceCollection services) 96 | { 97 | services 98 | .AddHttpUserAgentMemoryCachedParser() // registers Parser, returns HttpUserAgentParserDependencyInjectionOptions 99 | // or use any other Parser registration like services.AddHttpUserAgentParser(); above 100 | .AddHttpUserAgentParserAccessor(); // registers IHttpUserAgentParserAccessor, uses IHttpUserAgentParserProvider 101 | } 102 | ``` 103 | 104 | Now you can use 105 | 106 | ```csharp 107 | public void MyMethod(IHttpUserAgentParserAccessor parserAccessor) 108 | { 109 | HttpUserAgentInformation info = parserAccessor.Get(); 110 | } 111 | ``` 112 | 113 | ## Benchmark 114 | 115 | ```shell 116 | BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6691/22H2/2022Update) 117 | AMD Ryzen 9 9950X 4.30GHz, 1 CPU, 32 logical and 16 physical cores 118 | .NET SDK 10.0.101 119 | [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v4 120 | ShortRun : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v4 121 | 122 | Job=ShortRun IterationCount=3 LaunchCount=1 123 | WarmupCount=3 124 | 125 | | Method | Categories | Data | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | 126 | |------------------- |----------- |------------- |----------------:|-----------------:|---------------:|----------:|--------:|---------:|---------:|---------:|-----------:|------------:| 127 | | MyCSharp | Basic | Chrome Win10 | 939.54 ns | 113.807 ns | 6.238 ns | 1.00 | 0.01 | 0.0019 | - | - | 48 B | 1.00 | 128 | | UAParser | Basic | Chrome Win10 | 9,120,055.21 ns | 2,108,412.449 ns | 115,569.201 ns | 9,707.23 | 120.28 | 671.8750 | 609.3750 | 109.3750 | 11659008 B | 242,896.00 | 129 | | DeviceDetector.NET | Basic | Chrome Win10 | 5,099,680.21 ns | 5,313,448.322 ns | 291,248.033 ns | 5,428.01 | 270.28 | 296.8750 | 140.6250 | 31.2500 | 5034130 B | 104,877.71 | 130 | | | | | | | | | | | | | | | 131 | | MyCSharp | Basic | Google-Bot | 226.47 ns | 20.818 ns | 1.141 ns | 1.00 | 0.01 | - | - | - | - | NA | 132 | | UAParser | Basic | Google-Bot | 9,007,285.42 ns | 491,694.016 ns | 26,951.408 ns | 39,772.36 | 202.28 | 687.5000 | 640.6250 | 125.0000 | 12015474 B | NA | 133 | | DeviceDetector.NET | Basic | Google-Bot | 6,056,996.61 ns | 567,479.924 ns | 31,105.490 ns | 26,745.13 | 166.88 | 546.8750 | 132.8125 | 23.4375 | 8862491 B | NA | 134 | | | | | | | | | | | | | | | 135 | | MyCSharp | Cached | Chrome Win10 | 24.59 ns | 2.222 ns | 0.122 ns | 1.00 | 0.01 | - | - | - | - | NA | 136 | | UAParser | Cached | Chrome Win10 | 162,917.93 ns | 36,544.250 ns | 2,003.114 ns | 6,625.90 | 76.03 | 2.1973 | - | - | 37488 B | NA | 137 | | | | | | | | | | | | | | | 138 | | MyCSharp | Cached | Google-Bot | 17.42 ns | 1.077 ns | 0.059 ns | 1.00 | 0.00 | - | - | - | - | NA | 139 | | UAParser | Cached | Google-Bot | 126,321.45 ns | 3,171.908 ns | 173.863 ns | 7,253.51 | 23.01 | 2.6855 | - | - | 45856 B | NA | 140 | ``` 141 | 142 | ## Disclaimer 143 | 144 | This library is inspired by [UserAgentService by DannyBoyNg](https://github.com/DannyBoyNg/UserAgentService) and contains optimizations for our requirements on [myCSharp.de](https://mycsharp.de). 145 | We decided to fork the project, because we want a general restructuring with corresponding breaking changes. 146 | 147 | ## Maintained 148 | 149 | by [@BenjaminAbt](https://github.com/BenjaminAbt) and [@gfoidl](https://github.com/gfoidl) 150 | 151 | ## License 152 | 153 | MIT License 154 | 155 | Copyright (c) 2021-2025 MyCSharp 156 | 157 | Permission is hereby granted, free of charge, to any person obtaining a copy 158 | of this software and associated documentation files (the "Software"), to deal 159 | in the Software without restriction, including without limitation the rights 160 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 161 | copies of the Software, and to permit persons to whom the Software is 162 | furnished to do so, subject to the following conditions: 163 | 164 | The above copyright notice and this permission notice shall be included in all 165 | copies or substantial portions of the Software. 166 | 167 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 168 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 169 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 170 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 171 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 172 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 173 | SOFTWARE. 174 | 175 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/HttpUserAgentParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Runtime.CompilerServices; 6 | using System.Runtime.InteropServices; 7 | using System.Runtime.Intrinsics; 8 | 9 | namespace MyCSharp.HttpUserAgentParser; 10 | 11 | #pragma warning disable MA0049 // Type name should not match containing namespace 12 | 13 | /// 14 | /// Parser logic for user agents 15 | /// 16 | public static class HttpUserAgentParser 17 | { 18 | /// 19 | /// Parses given user agent 20 | /// 21 | public static HttpUserAgentInformation Parse(string userAgent) 22 | { 23 | // prepare 24 | userAgent = Cleanup(userAgent); 25 | 26 | // analyze 27 | if (TryGetRobot(userAgent, out string? robotName)) 28 | { 29 | return HttpUserAgentInformation.CreateForRobot(userAgent, robotName); 30 | } 31 | 32 | HttpUserAgentPlatformInformation? platform = GetPlatform(userAgent); 33 | string? mobileDeviceType = GetMobileDevice(userAgent); 34 | 35 | if (TryGetBrowser(userAgent, out (string Name, string? Version)? browser)) 36 | { 37 | return HttpUserAgentInformation.CreateForBrowser(userAgent, platform, browser?.Name, browser?.Version, mobileDeviceType); 38 | } 39 | 40 | return HttpUserAgentInformation.CreateForUnknown(userAgent, platform, mobileDeviceType); 41 | } 42 | 43 | /// 44 | /// pre-cleanup of user agent 45 | /// 46 | public static string Cleanup(string userAgent) => userAgent.Trim(); 47 | 48 | /// 49 | /// returns the platform or null 50 | /// 51 | public static HttpUserAgentPlatformInformation? GetPlatform(string userAgent) 52 | { 53 | ReadOnlySpan ua = userAgent.AsSpan(); 54 | foreach ((string Token, string Name, HttpUserAgentPlatformType PlatformType) platform in HttpUserAgentStatics.s_platformRules) 55 | { 56 | if (ContainsIgnoreCase(ua, platform.Token)) 57 | { 58 | return new HttpUserAgentPlatformInformation( 59 | HttpUserAgentStatics.GetPlatformRegexForToken(platform.Token), 60 | platform.Name, platform.PlatformType); 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | 67 | /// 68 | /// returns true if platform was found 69 | /// 70 | public static bool TryGetPlatform(string userAgent, [NotNullWhen(true)] out HttpUserAgentPlatformInformation? platform) 71 | { 72 | platform = GetPlatform(userAgent); 73 | return platform is not null; 74 | } 75 | 76 | /// 77 | /// returns the browser or null 78 | /// 79 | public static (string Name, string? Version)? GetBrowser(string userAgent) 80 | { 81 | ReadOnlySpan ua = userAgent.AsSpan(); 82 | 83 | foreach ((string Name, string DetectToken, string? VersionToken) browserRule in HttpUserAgentStatics.s_browserRules) 84 | { 85 | if (!TryIndexOf(ua, browserRule.DetectToken, out int detectIndex)) 86 | { 87 | continue; 88 | } 89 | 90 | // Version token may differ (e.g., Safari uses "Version/") 91 | 92 | int versionSearchStart; 93 | // For rules without a specific version token, ensure pattern Token/ 94 | if (string.IsNullOrEmpty(browserRule.VersionToken)) 95 | { 96 | int afterDetect = detectIndex + browserRule.DetectToken.Length; 97 | if (afterDetect >= ua.Length || ua[afterDetect] != '/') 98 | { 99 | // Likely a misspelling or partial token (e.g., Edgg, Oprea, Chromee) 100 | continue; 101 | } 102 | } 103 | if (!string.IsNullOrEmpty(browserRule.VersionToken)) 104 | { 105 | if (TryIndexOf(ua, browserRule.VersionToken!, out int vtIndex)) 106 | { 107 | versionSearchStart = vtIndex + browserRule.VersionToken!.Length; 108 | } 109 | else 110 | { 111 | // If specific version token wasn't found, fall back to detect token area 112 | versionSearchStart = detectIndex + browserRule.DetectToken.Length; 113 | } 114 | } 115 | else 116 | { 117 | versionSearchStart = detectIndex + browserRule.DetectToken.Length; 118 | } 119 | 120 | ReadOnlySpan search = ua.Slice(versionSearchStart); 121 | if (TryExtractVersion(search, out Range range)) 122 | { 123 | string? version = search[range].ToString(); 124 | return (browserRule.Name, version); 125 | } 126 | 127 | // If we didn't find a version for this rule, try next rule 128 | } 129 | 130 | return null; 131 | } 132 | 133 | /// 134 | /// returns true if browser was found 135 | /// 136 | public static bool TryGetBrowser(string userAgent, [NotNullWhen(true)] out (string Name, string? Version)? browser) 137 | { 138 | browser = GetBrowser(userAgent); 139 | return browser is not null; 140 | } 141 | 142 | /// 143 | /// returns the robot or null 144 | /// 145 | public static string? GetRobot(string userAgent) 146 | { 147 | ReadOnlySpan ua = userAgent.AsSpan(); 148 | foreach ((string key, string value) in HttpUserAgentStatics.Robots) 149 | { 150 | if (ContainsIgnoreCase(ua, key)) 151 | { 152 | return value; 153 | } 154 | } 155 | 156 | return null; 157 | } 158 | 159 | /// 160 | /// returns true if robot was found 161 | /// 162 | public static bool TryGetRobot(string userAgent, [NotNullWhen(true)] out string? robotName) 163 | { 164 | robotName = GetRobot(userAgent); 165 | return robotName is not null; 166 | } 167 | 168 | /// 169 | /// returns the device or null 170 | /// 171 | public static string? GetMobileDevice(string userAgent) 172 | { 173 | ReadOnlySpan ua = userAgent.AsSpan(); 174 | foreach ((string key, string value) in HttpUserAgentStatics.Mobiles) 175 | { 176 | if (ContainsIgnoreCase(ua, key)) 177 | { 178 | return value; 179 | } 180 | } 181 | 182 | return null; 183 | } 184 | 185 | /// 186 | /// returns true if device was found 187 | /// 188 | public static bool TryGetMobileDevice(string userAgent, [NotNullWhen(true)] out string? device) 189 | { 190 | device = GetMobileDevice(userAgent); 191 | return device is not null; 192 | } 193 | 194 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 195 | private static bool ContainsIgnoreCase(ReadOnlySpan haystack, ReadOnlySpan needle) 196 | => TryIndexOf(haystack, needle, out _); 197 | 198 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 199 | private static bool TryIndexOf(ReadOnlySpan haystack, ReadOnlySpan needle, out int index) 200 | { 201 | index = haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase); 202 | return index >= 0; 203 | } 204 | 205 | /// 206 | /// Extracts a dotted numeric version. 207 | /// Accepts digits and dots; skips common separators ('/', ' ', ':', '=') until first digit. 208 | /// Returns false if no version-like token is found. 209 | /// 210 | private static bool TryExtractVersion(ReadOnlySpan haystack, out Range range) 211 | { 212 | range = default; 213 | 214 | // Vectorization is used in a optimistic way and specialized to common (trimmed down) user agents. 215 | // When the first two char-vectors don't yield any success, we fall back to the scalar path. 216 | // This penalized not found versions, but has an advantage for found versions. 217 | // Vector512 is left out, because there are no common inputs with length 128 or more. 218 | // 219 | // Two short (same size as char) vectors are read, then packed to byte vectors on which the 220 | // operation is done. For short / chart the higher byte is not of interest and zero or outside 221 | // the target characters, thus with bytes we can process twice as much elements at once. 222 | 223 | if (Vector256.IsHardwareAccelerated && haystack.Length >= 2 * Vector256.Count) 224 | { 225 | ref char ptr = ref MemoryMarshal.GetReference(haystack); 226 | 227 | Vector256 vec = ptr.ReadVector256AsBytes(0); 228 | Vector256 between0and9 = Vector256.LessThan(vec - Vector256.Create((byte)'0'), Vector256.Create((byte)('9' - '0' + 1))); 229 | 230 | if (between0and9 == Vector256.Zero) 231 | { 232 | goto Scalar; 233 | } 234 | 235 | uint bitMask = between0and9.ExtractMostSignificantBits(); 236 | int idx = (int)uint.TrailingZeroCount(bitMask); 237 | Debug.Assert(idx is >= 0 and <= 32); 238 | int start = idx; 239 | 240 | Vector256 byteMask = between0and9 | Vector256.Equals(vec, Vector256.Create((byte)'.')); 241 | byteMask = ~byteMask; 242 | 243 | if (byteMask == Vector256.Zero) 244 | { 245 | goto Scalar; 246 | } 247 | 248 | bitMask = byteMask.ExtractMostSignificantBits(); 249 | bitMask >>= start; 250 | 251 | idx = start + (int)uint.TrailingZeroCount(bitMask); 252 | Debug.Assert(idx is >= 0 and <= 32); 253 | int end = idx; 254 | 255 | range = new Range(start, end); 256 | return true; 257 | } 258 | else if (Vector128.IsHardwareAccelerated && haystack.Length >= 2 * Vector128.Count) 259 | { 260 | ref char ptr = ref MemoryMarshal.GetReference(haystack); 261 | 262 | Vector128 vec = ptr.ReadVector128AsBytes(0); 263 | Vector128 between0and9 = Vector128.LessThan(vec - Vector128.Create((byte)'0'), Vector128.Create((byte)('9' - '0' + 1))); 264 | 265 | if (between0and9 == Vector128.Zero) 266 | { 267 | goto Scalar; 268 | } 269 | 270 | uint bitMask = between0and9.ExtractMostSignificantBits(); 271 | int idx = (int)uint.TrailingZeroCount(bitMask); 272 | Debug.Assert(idx is >= 0 and <= 16); 273 | int start = idx; 274 | 275 | Vector128 byteMask = between0and9 | Vector128.Equals(vec, Vector128.Create((byte)'.')); 276 | byteMask = ~byteMask; 277 | 278 | if (byteMask == Vector128.Zero) 279 | { 280 | goto Scalar; 281 | } 282 | 283 | bitMask = byteMask.ExtractMostSignificantBits(); 284 | bitMask >>= start; 285 | 286 | idx = start + (int)uint.TrailingZeroCount(bitMask); 287 | Debug.Assert(idx is >= 0 and <= 16); 288 | int end = idx; 289 | 290 | range = new Range(start, end); 291 | return true; 292 | } 293 | 294 | Scalar: 295 | { 296 | // Limit search window to avoid scanning entire UA string unnecessarily 297 | const int Windows = 128; 298 | if (haystack.Length > Windows) 299 | { 300 | haystack = haystack.Slice(0, Windows); 301 | } 302 | 303 | int start = -1; 304 | int i = 0; 305 | 306 | for (; i < haystack.Length; ++i) 307 | { 308 | char c = haystack[i]; 309 | if (char.IsBetween(c, '0', '9')) 310 | { 311 | start = i; 312 | break; 313 | } 314 | } 315 | 316 | if (start < 0) 317 | { 318 | // No digit found => no version 319 | return false; 320 | } 321 | 322 | haystack = haystack.Slice(i + 1); 323 | for (i = 0; i < haystack.Length; ++i) 324 | { 325 | char c = haystack[i]; 326 | if (!(char.IsBetween(c, '0', '9') || c == '.')) 327 | { 328 | break; 329 | } 330 | } 331 | 332 | i += start + 1; // shift back the previous domain 333 | 334 | if (i == start) 335 | { 336 | return false; 337 | } 338 | 339 | range = new Range(start, i); 340 | return true; 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | # From https://raw.githubusercontent.com/dotnet/roslyn/master/.editorconfig 3 | # https://github.com/BenjaminAbt/templates/blob/main/editorconfig/.editorconfig 4 | 5 | ############################### 6 | # Core EditorConfig Options # 7 | ############################### 8 | 9 | # top-most EditorConfig file 10 | root = true # stop .editorconfig files search on current file. 11 | 12 | # All files 13 | # Don't use tabs for indentation. 14 | [*] 15 | indent_style = space 16 | trim_trailing_whitespace = true # Remove trailing whitespace 17 | insert_final_newline = true # Ensure file ends with a newline 18 | max_line_length = 120 # Maximum line length for readability 19 | 20 | ############################### 21 | # Markdown # 22 | ############################### 23 | 24 | [*.md] 25 | indent_size = 4 26 | trim_trailing_whitespace = false 27 | max_line_length = off 28 | 29 | ############################### 30 | # Bicep # 31 | ############################### 32 | 33 | [*.bicep] 34 | indent_size = 2 35 | max_line_length = off 36 | 37 | ############################### 38 | # XML # 39 | ############################### 40 | 41 | [*.xml] 42 | indent_size = 2 43 | max_line_length = off 44 | 45 | ############################### 46 | # TypeScript # 47 | ############################### 48 | 49 | [*.ts] 50 | indent_size = 2 51 | 52 | ############################### 53 | # JavaScript # 54 | ############################### 55 | 56 | [*.js] 57 | indent_size = 2 58 | 59 | ############################### 60 | # HTML # 61 | ############################### 62 | 63 | [*.html] 64 | indent_size = 2 65 | 66 | ############################### 67 | # CSS/SCSS # 68 | ############################### 69 | 70 | [*.{css,scss}] 71 | indent_size = 2 72 | 73 | ############################### 74 | # YAML # 75 | ############################### 76 | 77 | [*.{yml,yaml}] 78 | indent_size = 2 79 | max_line_length = off 80 | 81 | ############################### 82 | # JSON # 83 | ############################### 84 | 85 | [*.json] 86 | indent_size = 2 87 | max_line_length = off 88 | 89 | ############################### 90 | # PowerShell # 91 | ############################### 92 | 93 | [*.ps1] 94 | indent_size = 2 95 | 96 | ############################### 97 | # Shell # 98 | ############################### 99 | 100 | [*.sh] 101 | end_of_line = lf 102 | 103 | [*.{cmd,bat}] 104 | end_of_line = crlf 105 | 106 | ############################### 107 | # .NET project files # 108 | ############################### 109 | 110 | # Xml project files 111 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 112 | indent_size = 2 113 | 114 | # Xml config files 115 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 116 | indent_size = 2 117 | 118 | ############################### 119 | # C# / VB # 120 | ############################### 121 | 122 | # Code files 123 | [*.{cs,csx,vb,vbx}] 124 | indent_size = 4 125 | file_header_template = Copyright © https://myCSharp.de - all rights reserved 126 | 127 | # Organize usings 128 | dotnet_sort_system_directives_first = true 129 | 130 | # this. preferences 131 | dotnet_style_qualification_for_field = false:silent 132 | dotnet_style_qualification_for_property = false:silent 133 | dotnet_style_qualification_for_method = false:silent 134 | dotnet_style_qualification_for_event = false:silent 135 | 136 | # Language keywords vs BCL types preferences 137 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 138 | dotnet_style_predefined_type_for_member_access = true:suggestion 139 | 140 | # Parentheses preferences 141 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 142 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 143 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 144 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 145 | 146 | # Modifier preferences 147 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 148 | dotnet_style_readonly_field = true:warning 149 | 150 | # Expression-level preferences 151 | dotnet_style_object_initializer = true:suggestion 152 | dotnet_style_collection_initializer = true:suggestion 153 | dotnet_style_explicit_tuple_names = true:suggestion 154 | dotnet_style_null_propagation = true:suggestion 155 | dotnet_style_coalesce_expression = true:suggestion 156 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning 157 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 158 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 159 | dotnet_style_prefer_auto_properties = true:suggestion 160 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 161 | dotnet_style_prefer_conditional_expression_over_return = true:silent 162 | 163 | # Style Definitions 164 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning 165 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 166 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 167 | 168 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 169 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 170 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 171 | 172 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 173 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 174 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 175 | 176 | dotnet_naming_rule.constant_should_be_pascal_case.severity = suggestion 177 | dotnet_naming_rule.constant_should_be_pascal_case.symbols = constant 178 | dotnet_naming_rule.constant_should_be_pascal_case.style = pascal_case 179 | 180 | dotnet_naming_rule.private_or_internal_static_field_should_be_static_field.severity = suggestion 181 | dotnet_naming_rule.private_or_internal_static_field_should_be_static_field.symbols = private_or_internal_static_field 182 | dotnet_naming_rule.private_or_internal_static_field_should_be_static_field.style = static_field 183 | 184 | dotnet_naming_rule.private_or_internal_field_should_be_instance_field.severity = suggestion 185 | dotnet_naming_rule.private_or_internal_field_should_be_instance_field.symbols = private_or_internal_field 186 | dotnet_naming_rule.private_or_internal_field_should_be_instance_field.style = instance_field 187 | 188 | dotnet_naming_style.pascal_case.required_prefix = 189 | dotnet_naming_style.pascal_case.required_suffix = 190 | dotnet_naming_style.pascal_case.word_separator = 191 | dotnet_naming_style.pascal_case.capitalization = pascal_case 192 | 193 | dotnet_naming_style.begins_with_i.required_prefix = I 194 | dotnet_naming_style.begins_with_i.required_suffix = 195 | dotnet_naming_style.begins_with_i.word_separator = 196 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 197 | 198 | dotnet_naming_style.static_field.required_prefix = s_ 199 | dotnet_naming_style.static_field.required_suffix = 200 | dotnet_naming_style.static_field.word_separator = 201 | dotnet_naming_style.static_field.capitalization = camel_case 202 | 203 | dotnet_naming_style.instance_field.required_prefix = _ 204 | dotnet_naming_style.instance_field.required_suffix = 205 | dotnet_naming_style.instance_field.word_separator = 206 | dotnet_naming_style.instance_field.capitalization = camel_case 207 | 208 | # Symbol specifications 209 | 210 | dotnet_naming_symbols.interface.applicable_kinds = interface 211 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 212 | dotnet_naming_symbols.interface.required_modifiers = 213 | 214 | dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field 215 | dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected 216 | dotnet_naming_symbols.private_or_internal_field.required_modifiers = 217 | 218 | dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field 219 | dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected 220 | dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static 221 | 222 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 223 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 224 | dotnet_naming_symbols.types.required_modifiers = 225 | 226 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 227 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 228 | dotnet_naming_symbols.non_field_members.required_modifiers = 229 | 230 | dotnet_naming_symbols.constant.applicable_kinds = field 231 | dotnet_naming_symbols.constant.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 232 | dotnet_naming_symbols.constant.required_modifiers = const 233 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 234 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 235 | dotnet_style_prefer_compound_assignment = true:suggestion 236 | dotnet_style_prefer_simplified_interpolation = true:suggestion 237 | dotnet_style_namespace_match_folder = true:suggestion 238 | dotnet_style_allow_multiple_blank_lines_experimental = true:silent 239 | dotnet_style_allow_statement_immediately_after_block_experimental = true:silent 240 | dotnet_code_quality_unused_parameters = all:suggestion 241 | 242 | # var preferences 243 | csharp_style_var_for_built_in_types = false:warning 244 | csharp_style_var_when_type_is_apparent = false:warning 245 | csharp_style_var_elsewhere = false:warning 246 | # Expression-bodied members 247 | csharp_style_expression_bodied_methods = false:silent 248 | csharp_style_expression_bodied_constructors = false:silent 249 | csharp_style_expression_bodied_operators = false:silent 250 | csharp_style_expression_bodied_properties = true:silent 251 | csharp_style_expression_bodied_indexers = true:silent 252 | csharp_style_expression_bodied_accessors = true:silent 253 | # Pattern matching preferences 254 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 255 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 256 | # Null-checking preferences 257 | csharp_style_throw_expression = true:suggestion 258 | csharp_style_conditional_delegate_call = true:suggestion 259 | # Modifier preferences 260 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 261 | # Expression-level preferences 262 | csharp_prefer_braces = true:suggestion 263 | csharp_style_deconstructed_variable_declaration = true:suggestion 264 | csharp_prefer_simple_default_expression = true:suggestion 265 | csharp_style_inlined_variable_declaration = true:suggestion 266 | 267 | # New line preferences 268 | csharp_new_line_before_open_brace = all 269 | csharp_new_line_before_else = true 270 | csharp_new_line_before_catch = true 271 | csharp_new_line_before_finally = true 272 | csharp_new_line_before_members_in_object_initializers = true 273 | csharp_new_line_before_members_in_anonymous_types = true 274 | csharp_new_line_between_query_expression_clauses = true 275 | 276 | # Indentation preferences 277 | csharp_indent_case_contents = true 278 | csharp_indent_switch_labels = true 279 | csharp_indent_labels = one_less_than_current 280 | 281 | # Space preferences 282 | csharp_space_after_cast = false 283 | csharp_space_after_keywords_in_control_flow_statements = true 284 | csharp_space_between_method_call_parameter_list_parentheses = false 285 | csharp_space_between_method_declaration_parameter_list_parentheses = false 286 | csharp_space_between_parentheses = false 287 | csharp_space_before_colon_in_inheritance_clause = true 288 | csharp_space_after_colon_in_inheritance_clause = true 289 | csharp_space_around_binary_operators = before_and_after 290 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 291 | csharp_space_between_method_call_name_and_opening_parenthesis = false 292 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 293 | 294 | # Wrapping preferences 295 | csharp_preserve_single_line_statements = true 296 | csharp_preserve_single_line_blocks = true 297 | csharp_using_directive_placement = outside_namespace:silent 298 | csharp_prefer_simple_using_statement = true:warning 299 | csharp_style_namespace_declarations = file_scoped:suggestion 300 | csharp_style_prefer_method_group_conversion = true:silent 301 | csharp_style_prefer_top_level_statements = false:silent 302 | csharp_style_expression_bodied_lambdas = true:silent 303 | csharp_style_expression_bodied_local_functions = false:silent 304 | csharp_style_prefer_null_check_over_type_check = true:suggestion 305 | csharp_style_prefer_index_operator = true:suggestion 306 | csharp_style_prefer_range_operator = true:silent 307 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 308 | csharp_style_prefer_tuple_swap = true:suggestion 309 | csharp_style_prefer_utf8_string_literals = true:suggestion 310 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 311 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 312 | csharp_prefer_static_local_function = true:suggestion 313 | csharp_style_prefer_readonly_struct = true:warning 314 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent 315 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent 316 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent 317 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent 318 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent 319 | csharp_style_prefer_switch_expression = true:suggestion 320 | csharp_style_prefer_pattern_matching = true:silent 321 | csharp_style_prefer_not_pattern = true:suggestion 322 | csharp_style_prefer_extended_property_pattern = true:suggestion 323 | 324 | # ------------------------------------------------------ 325 | # CA Style 326 | 327 | # CA1050: Types are declared in namespaces to prevent name collisions and as a way to organize related types in an object hierarchy. 328 | dotnet_diagnostic.CA1050.severity = warning 329 | 330 | # CA1507: Use nameof in place of string 331 | # Avoiding hard-coded strings in code improves maintainability and reduces the risk of errors during refactoring. 332 | dotnet_diagnostic.CA1507.severity = warning 333 | 334 | # CA1825: Avoid unnecessary zero-length array allocations. Use Array.Empty() instead. 335 | # Array.Empty() is more memory-efficient as it reuses a single cached empty array instance rather than creating new ones. 336 | dotnet_diagnostic.CA1825.severity = warning 337 | 338 | # CA1850: It is more efficient to use the static 'HashData' method over creating and managing a HashAlgorithm instance to call 'ComputeHash'. 339 | # This improves performance by avoiding unnecessary instance creation and lifecycle management of HashAlgorithm objects. 340 | dotnet_diagnostic.CA1850.severity = warning 341 | 342 | # CA1860: Prefer using 'IsEmpty', 'Count' or 'Length' properties whichever available, rather than calling 'Enumerable.Any()'. 343 | # These direct property accesses are more performant and communicate intent more clearly than using LINQ extension methods. 344 | dotnet_diagnostic.CA1860.severity = warning 345 | 346 | # CS1998: This async method lacks 'await' operators and will run synchronously. 347 | # Setting this to error prevents misleading code that suggests asynchronous behavior but actually runs synchronously. 348 | dotnet_diagnostic.CS1998.severity = error 349 | 350 | # CA2016: Forward the CancellationToken parameter to methods that take one 351 | # Ensuring proper propagation of cancellation tokens throughout the call chain is critical for responsive and cancellable operations. 352 | dotnet_diagnostic.CA2016.severity = error 353 | 354 | # ------------------------------------------------------ 355 | # IDE 356 | 357 | # IDE0060: Avoid unused parameters in your code. 358 | # Silenced to allow for interface implementations where not all parameters may be needed in every implementation or like URL design in ASP.NET Core. 359 | dotnet_diagnostic.IDE0060.severity = silent 360 | 361 | # IDE0130: Namespace does not match folder structure 362 | # Enforces consistent organization where namespaces reflect folder structure, improving code discoverability. 363 | dotnet_diagnostic.IDE0130.severity = warning 364 | 365 | # IDE0039: Use local function instead of lambda 366 | # Local functions improve readability and performance over lambdas for method-local callable code. 367 | dotnet_diagnostic.IDE0039.severity = warning 368 | 369 | # IDE0270: Null check can be simplified 370 | # Disabled to allow developers to choose their preferred null-checking style based on context and readability. 371 | dotnet_diagnostic.IDE0270.severity = none 372 | 373 | # IDE0305: Use collection expression for fluent 374 | # Silenced because collection expression style is often a matter of preference and readability context. 375 | dotnet_diagnostic.IDE0305.severity = silent 376 | 377 | # IDE1006: Naming rule violation: These words must begin with upper case characters 378 | # Enforces consistent naming conventions across the codebase for better readability and maintainability. 379 | dotnet_diagnostic.IDE1006.severity = warning 380 | 381 | # ------------------------------------------------------ 382 | # Roslyn 383 | 384 | # RCS0063: Remove unnecessary blank line 385 | # Promotes cleaner, more consistent code formatting by eliminating superfluous whitespace. 386 | dotnet_diagnostic.RCS0063.severity = warning 387 | 388 | # RCS1021: Use expression-bodied lambda. 389 | # Silenced to allow both statement and expression-bodied lambda syntax based on complexity and readability. 390 | dotnet_diagnostic.RCS1021.severity = silent 391 | 392 | # RCS1049: Simplify boolean comparison 393 | # Silenced to allow explicit boolean comparisons (e.g., x == true) when they improve readability. 394 | dotnet_diagnostic.RCS1049.severity = silent 395 | 396 | # RCS1123: Add parentheses when necessary 397 | # Enforces explicit operator precedence through parentheses, preventing subtle bugs and improving readability. 398 | dotnet_diagnostic.RCS1123.severity = warning 399 | 400 | # RCS1163: Unused parameter 401 | # Silenced because parameters may be kept for API consistency or documentation purposes even when unused or URL design in ASP.NET Core. 402 | dotnet_diagnostic.RCS1163.severity = silent 403 | 404 | # RCS1194: Implement exception constructors 405 | # Silenced to allow custom exception classes with only the constructors needed for the specific use case. 406 | dotnet_diagnostic.RCS1194.severity = silent 407 | 408 | # ------------------------------------------------------ 409 | # Meziantou.Analyzer 410 | 411 | # MA0007: Add a comma after the last value 412 | # Disabled as trailing commas in C# are not conventional and would make the code less familiar to most developers. 413 | dotnet_diagnostic.MA0007.severity = none 414 | 415 | # MA0016: Prefer using collection abstraction instead of implementation 416 | # Disabled to allow direct use of concrete collection types when their specific capabilities are needed. 417 | dotnet_diagnostic.MA0016.severity = none 418 | 419 | # MA0017: Abstract types should not have public or internal constructors 420 | # Disabled to permit protected constructors in abstract classes which are valid for inheritance scenarios. 421 | dotnet_diagnostic.MA0017.severity = none 422 | 423 | # MA0018: Do not declare static members on generic types 424 | # Disabled in favor of using the standard CA1000 (Do not declare static members on generic types) rule to handle this case. 425 | dotnet_diagnostic.MA0018.severity = none 426 | 427 | # MA0029: Combine LINQ methods 428 | # Disabled because LINQ method chaining can be more readable and potential performance impacts need case-by-case review. 429 | dotnet_diagnostic.MA0029.severity = none 430 | 431 | # MA0040: Forward the CancellationToken parameter to methods that take one 432 | # Enforces proper cancellation token propagation for responsive applications and services. 433 | dotnet_diagnostic.MA0040.severity = warning 434 | 435 | # MA0048: File name must match type name 436 | # Silenced to allow flexibility in file naming, particularly for partial classes or multiple types in one file. 437 | dotnet_diagnostic.MA0048.severity = silent 438 | 439 | # MA0051: Method is too long 440 | # Set as suggestion to encourage smaller, more focused methods while allowing flexibility for complex logic. 441 | dotnet_diagnostic.MA0051.severity = suggestion 442 | 443 | # MA0154: Use langword in XML comment 444 | # Disabled to allow flexibility in documentation style and format. 445 | dotnet_diagnostic.MA0154.severity = none 446 | 447 | # ------------------------------------------------------ 448 | # Xunit 449 | 450 | # xUnit1006: Theory methods should have parameters 451 | # Silenced to allow theories that might dynamically generate test cases without explicit parameters. 452 | dotnet_diagnostic.xUnit1006.severity = silent 453 | 454 | # xUnit1048: Support for 'async void' unit tests is being removed 455 | # Set as error to ensure future compatibility with xUnit v3 by requiring proper async Task signatures. 456 | dotnet_diagnostic.xUnit1048.severity = error 457 | -------------------------------------------------------------------------------- /src/HttpUserAgentParser/HttpUserAgentStatics.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using System.Collections.Frozen; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace MyCSharp.HttpUserAgentParser; 7 | 8 | /// 9 | /// Parser settings 10 | /// 11 | public static class HttpUserAgentStatics 12 | { 13 | /// 14 | /// Regex defauls for platform mappings 15 | /// 16 | private const RegexOptions DefaultPlatformsRegexFlags = RegexOptions.IgnoreCase | RegexOptions.Compiled; 17 | 18 | /// 19 | /// Creates default platform mapping regex 20 | /// 21 | private static Regex CreateDefaultPlatformRegex(string key) => new(Regex.Escape($"{key}"), 22 | DefaultPlatformsRegexFlags, matchTimeout: TimeSpan.FromMilliseconds(1000)); 23 | 24 | /// 25 | /// Platforms 26 | /// 27 | public static readonly HashSet Platforms = 28 | [ 29 | new(CreateDefaultPlatformRegex("windows nt 10.0"), "Windows 10", HttpUserAgentPlatformType.Windows), 30 | new(CreateDefaultPlatformRegex("windows nt 6.3"), "Windows 8.1", HttpUserAgentPlatformType.Windows), 31 | new(CreateDefaultPlatformRegex("windows nt 6.2"), "Windows 8", HttpUserAgentPlatformType.Windows), 32 | new(CreateDefaultPlatformRegex("windows nt 6.1"), "Windows 7", HttpUserAgentPlatformType.Windows), 33 | new(CreateDefaultPlatformRegex("windows nt 6.0"), "Windows Vista", HttpUserAgentPlatformType.Windows), 34 | new(CreateDefaultPlatformRegex("windows nt 5.2"), "Windows 2003", HttpUserAgentPlatformType.Windows), 35 | new(CreateDefaultPlatformRegex("windows nt 5.1"), "Windows XP", HttpUserAgentPlatformType.Windows), 36 | new(CreateDefaultPlatformRegex("windows nt 5.0"), "Windows 2000", HttpUserAgentPlatformType.Windows), 37 | new(CreateDefaultPlatformRegex("windows nt 4.0"), "Windows NT 4.0", HttpUserAgentPlatformType.Windows), 38 | new(CreateDefaultPlatformRegex("winnt4.0"), "Windows NT 4.0", HttpUserAgentPlatformType.Windows), 39 | new(CreateDefaultPlatformRegex("winnt 4.0"), "Windows NT", HttpUserAgentPlatformType.Windows), 40 | new(CreateDefaultPlatformRegex("winnt"), "Windows NT", HttpUserAgentPlatformType.Windows), 41 | new(CreateDefaultPlatformRegex("windows 98"), "Windows 98", HttpUserAgentPlatformType.Windows), 42 | new(CreateDefaultPlatformRegex("win98"), "Windows 98", HttpUserAgentPlatformType.Windows), 43 | new(CreateDefaultPlatformRegex("windows 95"), "Windows 95", HttpUserAgentPlatformType.Windows), 44 | new(CreateDefaultPlatformRegex("win95"), "Windows 95", HttpUserAgentPlatformType.Windows), 45 | new(CreateDefaultPlatformRegex("windows phone"), "Windows Phone", HttpUserAgentPlatformType.Windows), 46 | new(CreateDefaultPlatformRegex("windows"), "Unknown Windows OS", HttpUserAgentPlatformType.Windows), 47 | new(CreateDefaultPlatformRegex("android"), "Android", HttpUserAgentPlatformType.Android), 48 | new(CreateDefaultPlatformRegex("blackberry"), "BlackBerry", HttpUserAgentPlatformType.BlackBerry), 49 | new(CreateDefaultPlatformRegex("iphone"), "iOS", HttpUserAgentPlatformType.IOS), 50 | new(CreateDefaultPlatformRegex("ipad"), "iOS", HttpUserAgentPlatformType.IOS), 51 | new(CreateDefaultPlatformRegex("ipod"), "iOS", HttpUserAgentPlatformType.IOS), 52 | new(CreateDefaultPlatformRegex("cros"), "ChromeOS", HttpUserAgentPlatformType.ChromeOS), 53 | new(CreateDefaultPlatformRegex("os x"), "Mac OS X", HttpUserAgentPlatformType.MacOS), 54 | new(CreateDefaultPlatformRegex("ppc mac"), "Power PC Mac", HttpUserAgentPlatformType.MacOS), 55 | new(CreateDefaultPlatformRegex("freebsd"), "FreeBSD", HttpUserAgentPlatformType.Linux), 56 | new(CreateDefaultPlatformRegex("ppc"), "Macintosh", HttpUserAgentPlatformType.Linux), 57 | new(CreateDefaultPlatformRegex("linux"), "Linux", HttpUserAgentPlatformType.Linux), 58 | new(CreateDefaultPlatformRegex("debian"), "Debian", HttpUserAgentPlatformType.Linux), 59 | new(CreateDefaultPlatformRegex("sunos"), "Sun Solaris", HttpUserAgentPlatformType.Generic), 60 | new(CreateDefaultPlatformRegex("beos"), "BeOS", HttpUserAgentPlatformType.Generic), 61 | new(CreateDefaultPlatformRegex("apachebench"), "ApacheBench", HttpUserAgentPlatformType.Generic), 62 | new(CreateDefaultPlatformRegex("aix"), "AIX", HttpUserAgentPlatformType.Generic), 63 | new(CreateDefaultPlatformRegex("irix"), "Irix", HttpUserAgentPlatformType.Generic), 64 | new(CreateDefaultPlatformRegex("osf"), "DEC OSF", HttpUserAgentPlatformType.Generic), 65 | new(CreateDefaultPlatformRegex("hp-ux"), "HP-UX", HttpUserAgentPlatformType.Windows), 66 | new(CreateDefaultPlatformRegex("netbsd"), "NetBSD", HttpUserAgentPlatformType.Generic), 67 | new(CreateDefaultPlatformRegex("bsdi"), "BSDi", HttpUserAgentPlatformType.Generic), 68 | new(CreateDefaultPlatformRegex("openbsd"), "OpenBSD", HttpUserAgentPlatformType.Unix), 69 | new(CreateDefaultPlatformRegex("gnu"), "GNU/Linux", HttpUserAgentPlatformType.Linux), 70 | new(CreateDefaultPlatformRegex("unix"), "Unknown Unix OS", HttpUserAgentPlatformType.Unix), 71 | new(CreateDefaultPlatformRegex("symbian"), "Symbian OS", HttpUserAgentPlatformType.Symbian), 72 | ]; 73 | 74 | /// 75 | /// Fast-path platform token rules for zero-allocation Contains checks 76 | /// Sorted by frequency for better performance (most common platforms first) 77 | /// 78 | internal static readonly (string Token, string Name, HttpUserAgentPlatformType PlatformType)[] s_platformRules = 79 | [ 80 | // Most common: Windows (specific versions before generic) 81 | ("windows nt 10.0", "Windows 10", HttpUserAgentPlatformType.Windows), 82 | ("windows nt 6.1", "Windows 7", HttpUserAgentPlatformType.Windows), 83 | ("windows nt 6.3", "Windows 8.1", HttpUserAgentPlatformType.Windows), 84 | ("windows nt 6.2", "Windows 8", HttpUserAgentPlatformType.Windows), 85 | ("windows nt 6.0", "Windows Vista", HttpUserAgentPlatformType.Windows), 86 | // Android (very common on mobile) 87 | ("android", "Android", HttpUserAgentPlatformType.Android), 88 | // iOS devices (very common) 89 | ("iphone", "iOS", HttpUserAgentPlatformType.IOS), 90 | ("ipad", "iOS", HttpUserAgentPlatformType.IOS), 91 | ("ipod", "iOS", HttpUserAgentPlatformType.IOS), 92 | // ChromeOS (must be before "os x" to avoid false match with "CrOS") 93 | ("cros", "ChromeOS", HttpUserAgentPlatformType.ChromeOS), 94 | // Mac OS (common) 95 | ("os x", "Mac OS X", HttpUserAgentPlatformType.MacOS), 96 | // Linux (common) 97 | ("linux", "Linux", HttpUserAgentPlatformType.Linux), 98 | // Other Windows versions 99 | ("windows phone", "Windows Phone", HttpUserAgentPlatformType.Windows), 100 | ("windows nt 5.2", "Windows 2003", HttpUserAgentPlatformType.Windows), 101 | ("windows nt 5.1", "Windows XP", HttpUserAgentPlatformType.Windows), 102 | ("windows nt 5.0", "Windows 2000", HttpUserAgentPlatformType.Windows), 103 | ("windows nt 4.0", "Windows NT 4.0", HttpUserAgentPlatformType.Windows), 104 | ("winnt4.0", "Windows NT 4.0", HttpUserAgentPlatformType.Windows), 105 | ("winnt 4.0", "Windows NT", HttpUserAgentPlatformType.Windows), 106 | ("winnt", "Windows NT", HttpUserAgentPlatformType.Windows), 107 | ("windows 98", "Windows 98", HttpUserAgentPlatformType.Windows), 108 | ("win98", "Windows 98", HttpUserAgentPlatformType.Windows), 109 | ("windows 95", "Windows 95", HttpUserAgentPlatformType.Windows), 110 | ("win95", "Windows 95", HttpUserAgentPlatformType.Windows), 111 | ("windows", "Unknown Windows OS", HttpUserAgentPlatformType.Windows), 112 | // Less common platforms 113 | ("blackberry", "BlackBerry", HttpUserAgentPlatformType.BlackBerry), 114 | ("ppc mac", "Power PC Mac", HttpUserAgentPlatformType.MacOS), 115 | ("debian", "Debian", HttpUserAgentPlatformType.Linux), 116 | ("freebsd", "FreeBSD", HttpUserAgentPlatformType.Linux), 117 | ("ppc", "Macintosh", HttpUserAgentPlatformType.Linux), 118 | ("gnu", "GNU/Linux", HttpUserAgentPlatformType.Linux), 119 | ("unix", "Unknown Unix OS", HttpUserAgentPlatformType.Unix), 120 | ("openbsd", "OpenBSD", HttpUserAgentPlatformType.Unix), 121 | ("symbian", "Symbian OS", HttpUserAgentPlatformType.Symbian), 122 | ("sunos", "Sun Solaris", HttpUserAgentPlatformType.Generic), 123 | ("beos", "BeOS", HttpUserAgentPlatformType.Generic), 124 | ("apachebench", "ApacheBench", HttpUserAgentPlatformType.Generic), 125 | ("aix", "AIX", HttpUserAgentPlatformType.Generic), 126 | ("irix", "Irix", HttpUserAgentPlatformType.Generic), 127 | ("osf", "DEC OSF", HttpUserAgentPlatformType.Generic), 128 | ("hp-ux", "HP-UX", HttpUserAgentPlatformType.Windows), 129 | ("netbsd", "NetBSD", HttpUserAgentPlatformType.Generic), 130 | ("bsdi", "BSDi", HttpUserAgentPlatformType.Generic), 131 | ]; 132 | 133 | // Precompiled platform regex map to attach to PlatformInformation without per-call allocations 134 | private static readonly FrozenDictionary s_platformRegexMap = s_platformRules 135 | .ToFrozenDictionary(p => p.Token, p => CreateDefaultPlatformRegex(p.Token), StringComparer.OrdinalIgnoreCase); 136 | 137 | internal static Regex GetPlatformRegexForToken(string token) => s_platformRegexMap[token]; 138 | 139 | /// 140 | /// Regex defauls for browser mappings 141 | /// 142 | private const RegexOptions DefaultBrowserRegexFlags = RegexOptions.IgnoreCase | RegexOptions.Compiled; 143 | /// 144 | /// Creates default browser mapping regex 145 | /// 146 | private static Regex CreateDefaultBrowserRegex(string key) 147 | => new($@"{key}.*?([0-9\.]+)", DefaultBrowserRegexFlags, matchTimeout: TimeSpan.FromMilliseconds(1000)); 148 | 149 | /// 150 | /// Browsers 151 | /// 152 | public static readonly FrozenDictionary Browsers = new Dictionary() 153 | { 154 | { CreateDefaultBrowserRegex("OPR"), "Opera" }, 155 | { CreateDefaultBrowserRegex("Flock"), "Flock" }, 156 | { CreateDefaultBrowserRegex("Edge"), "Edge" }, 157 | { CreateDefaultBrowserRegex("EdgA"), "Edge" }, 158 | { CreateDefaultBrowserRegex("Edg"), "Edge" }, 159 | { CreateDefaultBrowserRegex("Vivaldi"), "Vivaldi" }, 160 | { CreateDefaultBrowserRegex("Brave Chrome"), "Brave" }, 161 | { CreateDefaultBrowserRegex("Chrome"), "Chrome" }, 162 | { CreateDefaultBrowserRegex("CriOS"), "Chrome" }, 163 | { CreateDefaultBrowserRegex("Opera.*?Version"), "Opera" }, 164 | { CreateDefaultBrowserRegex("Opera"), "Opera" }, 165 | { CreateDefaultBrowserRegex("MSIE"), "Internet Explorer" }, 166 | { CreateDefaultBrowserRegex("Internet Explorer"), "Internet Explorer" }, 167 | { CreateDefaultBrowserRegex("Trident.* rv"), "Internet Explorer" }, 168 | { CreateDefaultBrowserRegex("Shiira"), "Shiira" }, 169 | { CreateDefaultBrowserRegex("Firefox"), "Firefox" }, 170 | { CreateDefaultBrowserRegex("FxiOS"), "Firefox" }, 171 | { CreateDefaultBrowserRegex("Chimera"), "Chimera" }, 172 | { CreateDefaultBrowserRegex("Phoenix"), "Phoenix" }, 173 | { CreateDefaultBrowserRegex("Firebird"), "Firebird" }, 174 | { CreateDefaultBrowserRegex("Camino"), "Camino" }, 175 | { CreateDefaultBrowserRegex("Netscape"), "Netscape" }, 176 | { CreateDefaultBrowserRegex("OmniWeb"), "OmniWeb" }, 177 | { CreateDefaultBrowserRegex("Version"), "Safari" }, // https://github.com/mycsharp/HttpUserAgentParser/issues/34 178 | { CreateDefaultBrowserRegex("Mozilla"), "Mozilla" }, 179 | { CreateDefaultBrowserRegex("Konqueror"), "Konqueror" }, 180 | { CreateDefaultBrowserRegex("icab"), "iCab" }, 181 | { CreateDefaultBrowserRegex("Lynx"), "Lynx" }, 182 | { CreateDefaultBrowserRegex("Links"), "Links" }, 183 | { CreateDefaultBrowserRegex("hotjava"), "HotJava" }, 184 | { CreateDefaultBrowserRegex("amaya"), "Amaya" }, 185 | { CreateDefaultBrowserRegex("IBrowse"), "IBrowse" }, 186 | { CreateDefaultBrowserRegex("Maxthon"), "Maxthon" }, 187 | { CreateDefaultBrowserRegex("ipod touch"), "Apple iPod" }, 188 | { CreateDefaultBrowserRegex("Ubuntu"), "Ubuntu Web Browser" }, 189 | }.ToFrozenDictionary(); 190 | 191 | /// 192 | /// Fast-path browser token rules. If these fail to extract a version, code will fall back to regex rules. 193 | /// Sorted by specificity first, then frequency - more specific tokens must come before generic ones 194 | /// (e.g., Edge/Opera before Chrome, since Edge/Opera UAs contain "Chrome") 195 | /// 196 | internal static readonly (string Name, string DetectToken, string? VersionToken)[] s_browserRules = 197 | [ 198 | // Most specific browsers first (contain Chrome/Mozilla in their UA) 199 | ("Opera", "OPR", null), 200 | ("Opera", "Opera", "Version/"), 201 | ("Opera", "Opera", null), 202 | ("Edge", "Edg", null), 203 | ("Edge", "Edge", null), 204 | ("Edge", "EdgA", null), 205 | ("Edge", "EdgiOS", null), 206 | ("Brave", "Brave Chrome", null), 207 | ("Vivaldi", "Vivaldi", null), 208 | ("Flock", "Flock", null), 209 | // Common browsers 210 | ("Chrome", "Chrome", null), 211 | ("Chrome", "CriOS", null), 212 | ("Safari", "Version/", "Version/"), 213 | ("Firefox", "Firefox", null), 214 | ("Firefox", "FxiOS", null), 215 | // Internet Explorer (legacy but still in use - MSIE before Trident to avoid false matches) 216 | ("Internet Explorer", "MSIE", "MSIE "), 217 | ("Internet Explorer", "Trident", "rv:"), 218 | ("Internet Explorer", "Internet Explorer", null), 219 | // Less common browsers 220 | ("Maxthon", "Maxthon", null), 221 | ("Netscape", "Netscape", null), 222 | ("Konqueror", "Konqueror", null), 223 | ("OmniWeb", "OmniWeb", null), 224 | ("Shiira", "Shiira", null), 225 | ("Chimera", "Chimera", null), 226 | ("Camino", "Camino", null), 227 | ("Firebird", "Firebird", null), 228 | ("Phoenix", "Phoenix", null), 229 | ("iCab", "icab", null), 230 | ("Lynx", "Lynx", null), 231 | ("Links", "Links", null), 232 | ("HotJava", "hotjava", null), 233 | ("Amaya", "amaya", null), 234 | ("IBrowse", "IBrowse", null), 235 | ("Apple iPod", "ipod touch", null), 236 | ("Ubuntu Web Browser", "Ubuntu", null), 237 | ]; 238 | 239 | /// 240 | /// Mobiles 241 | /// 242 | public static readonly FrozenDictionary Mobiles = new Dictionary(StringComparer.InvariantCultureIgnoreCase) 243 | { 244 | // Legacy 245 | { "mobileexplorer", "Mobile Explorer" }, 246 | { "palmsource", "Palm" }, 247 | { "palmscape", "Palmscape" }, 248 | // Phones and Manufacturers 249 | { "motorola", "Motorola" }, 250 | { "nokia", "Nokia" }, 251 | { "palm", "Palm" }, 252 | { "ipad", "Apple iPad" }, 253 | { "ipod", "Apple iPod" }, 254 | { "iphone", "Apple iPhone" }, 255 | { "sony", "Sony Ericsson" }, 256 | { "ericsson", "Sony Ericsson" }, 257 | { "blackberry", "BlackBerry" }, 258 | { "cocoon", "O2 Cocoon" }, 259 | { "blazer", "Treo" }, 260 | { "lg", "LG" }, 261 | { "amoi", "Amoi" }, 262 | { "xda", "XDA" }, 263 | { "mda", "MDA" }, 264 | { "vario", "Vario" }, 265 | { "htc", "HTC" }, 266 | { "samsung", "Samsung" }, 267 | { "sharp", "Sharp" }, 268 | { "sie-", "Siemens" }, 269 | { "alcatel", "Alcatel" }, 270 | { "benq", "BenQ" }, 271 | { "ipaq", "HP iPaq" }, 272 | { "mot-", "Motorola" }, 273 | { "playstation portable", "PlayStation Portable" }, 274 | { "playstation 3", "PlayStation 3" }, 275 | { "playstation vita", "PlayStation Vita" }, 276 | { "hiptop", "Danger Hiptop" }, 277 | { "nec-", "NEC" }, 278 | { "panasonic", "Panasonic" }, 279 | { "philips", "Philips" }, 280 | { "sagem", "Sagem" }, 281 | { "sanyo", "Sanyo" }, 282 | { "spv", "SPV" }, 283 | { "zte", "ZTE" }, 284 | { "sendo", "Sendo" }, 285 | { "nintendo dsi", "Nintendo DSi" }, 286 | { "nintendo ds", "Nintendo DS" }, 287 | { "nintendo 3ds", "Nintendo 3DS" }, 288 | { "wii", "Nintendo Wii" }, 289 | { "open web", "Open Web" }, 290 | { "openweb", "OpenWeb" }, 291 | // Operating Systems 292 | { "android", "Android" }, 293 | { "symbian", "Symbian" }, 294 | { "SymbianOS", "SymbianOS" }, 295 | { "elaine", "Palm" }, 296 | { "series60", "Symbian S60" }, 297 | { "windows ce", "Windows CE" }, 298 | // Browsers 299 | { "obigo", "Obigo" }, 300 | { "netfront", "Netfront Browser" }, 301 | { "openwave", "Openwave Browser" }, 302 | { "mobilexplorer", "Mobile Explorer" }, 303 | { "operamini", "Opera Mini" }, 304 | { "opera mini", "Opera Mini" }, 305 | { "opera mobi", "Opera Mobile" }, 306 | { "fennec", "Firefox Mobile" }, 307 | // Other 308 | { "digital paths", "Digital Paths" }, 309 | { "avantgo", "AvantGo" }, 310 | { "xiino", "Xiino" }, 311 | { "novarra", "Novarra Transcoder" }, 312 | { "vodafone", "Vodafone" }, 313 | { "docomo", "NTT DoCoMo" }, 314 | { "o2", "O2" }, 315 | // Fallback 316 | { "mobile", "Generic Mobile" }, 317 | { "wireless", "Generic Mobile" }, 318 | { "j2me", "Generic Mobile" }, 319 | { "midp", "Generic Mobile" }, 320 | { "cldc", "Generic Mobile" }, 321 | { "up.link", "Generic Mobile" }, 322 | { "up.browser", "Generic Mobile" }, 323 | { "smartphone", "Generic Mobile" }, 324 | { "cellphone", "Generic Mobile" }, 325 | }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); 326 | 327 | /// 328 | /// Robots 329 | /// 330 | public static readonly (string Key, string Value)[] Robots = 331 | [ 332 | ( "googlebot", "Googlebot" ), 333 | ( "meta-externalagent", "meta-externalagent" ), 334 | ( "openai.com/searchbot", "OAI-SearchBot" ), 335 | ( "CCBot", "CCBot" ), 336 | ( "archive.org/details/archive.org_bot", "archive.org" ), 337 | ( "opensiteexplorer.org/dotbot", "DotBot" ), 338 | ( "awario.com/bots.html", "AwarioBot" ), 339 | ( "Turnitin", "Turnitin" ), 340 | ( "openai.com/gptbot", "GPTBot" ), 341 | ( "perplexity.ai/perplexitybot", "PerplexityBot" ), 342 | ( "developer.amazon.com/support/amazonbot", "Amazonbot" ), 343 | ( "trendictionbot", "trendictionbot" ), 344 | ( "openai.com/searchbot", "OAI-SearchBot" ), 345 | ( "Bytespider", "Bytespider" ), 346 | ( "MojeekBot", "MojeekBot" ), 347 | ( "SeekportBot", "SeekportBot" ), 348 | ( "googleweblight", "Google Web Light" ), 349 | ( "PetalBot", "PetalBot"), 350 | ( "DuplexWeb-Google", "DuplexWeb-Google"), 351 | ( "Storebot-Google", "Storebot-Google"), 352 | ( "msnbot", "MSNBot"), 353 | ( "baiduspider", "Baiduspider"), 354 | ( "Google Favicon", "Google Favicon"), 355 | ( "Jobboerse", "Jobboerse"), 356 | ( "bingbot", "BingBot"), 357 | ( "BingPreview", "Bing Preview"), 358 | ( "slurp", "Slurp"), 359 | ( "yahoo", "Yahoo"), 360 | ( "ask jeeves", "Ask Jeeves"), 361 | ( "fastcrawler", "FastCrawler"), 362 | ( "infoseek", "InfoSeek Robot 1.0"), 363 | ( "lycos", "Lycos"), 364 | ( "YandexBot", "YandexBot"), 365 | ( "YandexImages", "YandexImages"), 366 | ( "mediapartners-google", "Mediapartners Google"), 367 | ( "apis-google", "APIs Google"), 368 | ( "CRAZYWEBCRAWLER", "Crazy Webcrawler"), 369 | ( "AdsBot-Google-Mobile", "AdsBot Google Mobile"), 370 | ( "adsbot-google", "AdsBot Google"), 371 | ( "feedfetcher-google", "FeedFetcher-Google"), 372 | ( "google-read-aloud", "Google-Read-Aloud"), 373 | ( "curious george", "Curious George"), 374 | ( "ia_archiver", "Alexa Crawler"), 375 | ( "MJ12bot", "Majestic"), 376 | ( "Uptimebot", "Uptimebot"), 377 | ( "CheckMarkNetwork", "CheckMark"), 378 | ( "facebookexternalhit", "Facebook"), 379 | ( "adscanner", "AdScanner"), 380 | ( "AhrefsBot", "Ahrefs"), 381 | ( "BLEXBot", "BLEXBot"), 382 | ( "DotBot", "OpenSite"), 383 | ( "Mail.RU_Bot", "Mail.ru"), 384 | ( "MegaIndex", "MegaIndex"), 385 | ( "SemrushBot", "SEMRush"), 386 | ( "SEOkicks", "SEOkicks"), 387 | ( "seoscanners.net", "SEO Scanners"), 388 | ( "Sistrix", "Sistrix" ), 389 | ( "WhatsApp", "WhatsApp" ), 390 | ( "CensysInspect", "CensysInspect" ), 391 | ( "InternetMeasurement", "InternetMeasurement" ), 392 | ( "Barkrowler", "Barkrowler" ), 393 | ( "BrightEdge", "BrightEdge" ), 394 | ( "ImagesiftBot", "ImagesiftBot" ), 395 | ( "Cotoyogi", "Cotoyogi" ), 396 | ( "Applebot", "Applebot" ), 397 | ( "360Spider", "360Spider" ), 398 | ( "GeedoProductSearch", "GeedoProductSearch" ) 399 | ]; 400 | 401 | /// 402 | /// Tools 403 | /// 404 | public static readonly FrozenDictionary Tools = new Dictionary(StringComparer.OrdinalIgnoreCase) 405 | { 406 | { "curl", "curl" } 407 | } 408 | .ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); 409 | } 410 | -------------------------------------------------------------------------------- /tests/HttpUserAgentParser.UnitTests/HttpUserAgentParserTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright © https://myCSharp.de - all rights reserved 2 | 3 | using Xunit; 4 | 5 | namespace MyCSharp.HttpUserAgentParser.UnitTests; 6 | 7 | public class HttpUserAgentParserTests 8 | { 9 | [Theory] 10 | // IE 11 | [InlineData("Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Trident/4.0;)", "Internet Explorer", "7.0", "Windows Vista", HttpUserAgentPlatformType.Windows, null)] 12 | [InlineData("Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)", "Internet Explorer", "8.0", "Windows XP", HttpUserAgentPlatformType.Windows, null)] 13 | [InlineData("Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)", "Internet Explorer", "8.0", "Windows 7", HttpUserAgentPlatformType.Windows, null)] 14 | [InlineData("Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.0)", "Internet Explorer", "9.0", "Windows Vista", HttpUserAgentPlatformType.Windows, null)] 15 | [InlineData("Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)", "Internet Explorer", "9.0", "Windows 7", HttpUserAgentPlatformType.Windows, null)] 16 | [InlineData("Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", "Internet Explorer", "10.0", "Windows 7", HttpUserAgentPlatformType.Windows, null)] 17 | [InlineData("Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2)", "Internet Explorer", "10.0", "Windows 8", HttpUserAgentPlatformType.Windows, null)] 18 | [InlineData("Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko", "Internet Explorer", "11.0", "Windows 7", HttpUserAgentPlatformType.Windows, null)] 19 | [InlineData("Mozilla/5.0 (Windows NT 6.2; Trident/7.0; rv:11.0) like Gecko", "Internet Explorer", "11.0", "Windows 8", HttpUserAgentPlatformType.Windows, null)] 20 | [InlineData("Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", "Internet Explorer", "11.0", "Windows 8.1", HttpUserAgentPlatformType.Windows, null)] 21 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko", "Internet Explorer", "11.0", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 22 | // Chrome 23 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 24 | [InlineData("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 25 | [InlineData("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 26 | [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] 27 | [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", "Chrome", "90.0.4430.212", "Linux", HttpUserAgentPlatformType.Linux, null)] 28 | [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/90.0.4430.78 Mobile/15E148 Safari/604.1", "Chrome", "90.0.4430.78", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPhone")] 29 | [InlineData("Mozilla/5.0 (iPad; CPU OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/90.0.4430.78 Mobile/15E148 Safari/604.1", "Chrome", "90.0.4430.78", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPad")] 30 | [InlineData("Mozilla/5.0 (iPod; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/90.0.4430.78 Mobile/15E148 Safari/604.1", "Chrome", "90.0.4430.78", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPod")] 31 | [InlineData("Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36", "Chrome", "90.0.4430.210", "Android", HttpUserAgentPlatformType.Android, "Android")] 32 | [InlineData("Mozilla/5.0 (Linux; Android 10; SM-A205U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36", "Chrome", "90.0.4430.210", "Android", HttpUserAgentPlatformType.Android, "Android")] 33 | [InlineData("Mozilla/5.0 (Linux; Android 10; LM-Q720) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36", "Chrome", "90.0.4430.210", "Android", HttpUserAgentPlatformType.Android, "Android")] 34 | [InlineData("Mozilla/5.0 (X11; CrOS x86_64 15917.71.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.132 Safari/537.36", "Chrome", "127.0.6533.132", "ChromeOS", HttpUserAgentPlatformType.ChromeOS, null)] 35 | // Safari 36 | [InlineData("Mozilla/5.0 (Windows; U; Windows NT 10.0; en-US) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/11.0 Safari/605.1.15", "Safari", "11.0", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 37 | [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15", "Safari", "14.1", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] 38 | [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", "Safari", "14.0", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPhone")] 39 | [InlineData("Mozilla/5.0 (iPod touch; CPU iPhone 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", "Safari", "14.0", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPod")] 40 | // Edge 41 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.51", "Edge", "90.0.818.51", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 42 | [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.51", "Edge", "90.0.818.51", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] 43 | [InlineData("Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.210 Mobile Safari/537.36 EdgA/46.3.4.5155", "Edge", "46.3.4.5155", "Android", HttpUserAgentPlatformType.Android, "Android")] 44 | [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 14_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 EdgiOS/46.3.13 Mobile/15E148 Safari/605.1.15", "Edge", "46.3.13", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPhone")] 45 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64; Xbox; Xbox One) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edge/44.18363.8131", "Edge", "44.18363.8131", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 46 | // Firefox 47 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0", "Firefox", "88.0", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 48 | [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11.3; rv:88.0) Gecko/20100101 Firefox/88.0", "Firefox", "88.0", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] 49 | [InlineData("Mozilla/5.0 (X11; Linux i686; rv:88.0) Gecko/20100101 Firefox/88.0", "Firefox", "88.0", "Linux", HttpUserAgentPlatformType.Linux, null)] 50 | [InlineData("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0", "Firefox", "88.0", "Linux", HttpUserAgentPlatformType.Linux, null)] 51 | [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 11_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/33.0 Mobile/15E148 Safari/605.1.15", "Firefox", "33.0", "iOS", HttpUserAgentPlatformType.IOS, "Apple iPhone")] 52 | [InlineData("Mozilla/5.0 (Android 11; Mobile; rv:68.0) Gecko/68.0 Firefox/88.0", "Firefox", "88.0", "Android", HttpUserAgentPlatformType.Android, "Android")] 53 | // Opera 54 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 OPR/76.0.4017.107", "Opera", "76.0.4017.107", "Windows 10", HttpUserAgentPlatformType.Windows, null)] 55 | [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 OPR/76.0.4017.107", "Opera", "76.0.4017.107", "Mac OS X", HttpUserAgentPlatformType.MacOS, null)] 56 | [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 OPR/76.0.4017.107", "Opera", "76.0.4017.107", "Linux", HttpUserAgentPlatformType.Linux, null)] 57 | public void BrowserTests(string ua, string name, string version, string platformName, HttpUserAgentPlatformType platformType, string? mobileDeviceType) 58 | { 59 | HttpUserAgentInformation uaInfo = HttpUserAgentInformation.Parse(ua); 60 | 61 | Assert.Equal(name, uaInfo.Name); 62 | Assert.Equal(version, uaInfo.Version); 63 | Assert.Equal(ua, uaInfo.UserAgent); 64 | 65 | Assert.Equal(HttpUserAgentType.Browser, uaInfo.Type); 66 | 67 | HttpUserAgentPlatformInformation platform = uaInfo.Platform.GetValueOrDefault(); 68 | Assert.Equal(platformType, platform.PlatformType); 69 | Assert.Equal(platformName, platform.Name); 70 | 71 | Assert.Equal(mobileDeviceType, uaInfo.MobileDeviceType); 72 | 73 | Assert.True(uaInfo.IsBrowser()); 74 | Assert.Equal(mobileDeviceType is not null, uaInfo.IsMobile()); 75 | Assert.False(uaInfo.IsRobot()); 76 | } 77 | 78 | [Theory] 79 | // Google https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers 80 | [InlineData("APIs-Google (+https://developers.google.com/webmasters/APIs-Google.html)", "APIs Google")] 81 | [InlineData("Mediapartners-Google", "Mediapartners Google")] 82 | [InlineData("Mozilla/5.0 (Linux; Android 5.0; SM-G920A) AppleWebKit (KHTML, like Gecko) Chrome Mobile Safari (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html)", "AdsBot Google Mobile")] 83 | [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML,like Gecko) Version/9.0 Mobile/13B143 Safari/601.1 (compatible; AdsBot-Google-Mobile; +http://www.google.com/mobile/adsbot.html)", "AdsBot Google Mobile")] 84 | [InlineData("AdsBot-Google (+http://www.google.com/adsbot.html)", "AdsBot Google")] 85 | [InlineData("Googlebot-Image/1.0", "Googlebot")] 86 | [InlineData("Googlebot-News", "Googlebot")] 87 | [InlineData("Googlebot-Video/1.0", "Googlebot")] 88 | [InlineData("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "Googlebot")] 89 | [InlineData("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/1.2.3 Safari/537.36", "Googlebot")] 90 | [InlineData("Googlebot/2.1 (+http://www.google.com/bot.html)", "Googlebot")] 91 | [InlineData("Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "Googlebot")] 92 | [InlineData("Mediapartners-Google/2.1; +http://www.google.com/bot.html)", "Mediapartners Google")] 93 | [InlineData("FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)", "FeedFetcher-Google")] 94 | [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36 (compatible; Google-Read-Aloud; +https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers)", "Google-Read-Aloud")] 95 | [InlineData("Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers)", "Google-Read-Aloud")] 96 | [InlineData("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012; DuplexWeb-Google/1.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Mobile Safari/537.36", "DuplexWeb-Google")] 97 | [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36 Google Favicon", "Google Favicon")] 98 | [InlineData("Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko; googleweblight) Chrome/38.0.1025.166 Mobile Safari/535.19", "Google Web Light")] 99 | [InlineData("Mozilla/5.0 (X11; Linux x86_64; Storebot-Google/1.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36", "Storebot-Google")] 100 | [InlineData("Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012; Storebot-Google/1.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Mobile Safari/537.36", "Storebot-Google")] 101 | // Bing 102 | [InlineData("Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "BingBot")] 103 | [InlineData("Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "BingBot")] 104 | [InlineData("Mozilla/5.0 (Windows Phone 8.1; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 530) like Gecko (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "BingBot")] 105 | [InlineData("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/1.2.3.4 Safari/537.36 Edg/1.2.3.4", "BingBot")] 106 | [InlineData("Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/1.2.3.4  Mobile Safari/537.36 Edg/1.2.3.4 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", "BingBot")] 107 | [InlineData("Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)", "Baiduspider")] 108 | [InlineData("Mozilla/5.0 (compatible; MJ12bot/v1.4.5; http://www.majestic12.co.uk/bot.php?+)", "Majestic")] 109 | [InlineData("Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)", "Slurp")] 110 | [InlineData("Mozilla/5.0 (compatible; MegaIndex.ru/2.0; +http://megaindex.com/crawler)", "MegaIndex")] 111 | [InlineData("Mozilla/5.0 (compatible; AhrefsBot/5.2; +http://ahrefs.com/robot/)", "Ahrefs")] 112 | [InlineData("Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)", "SEMRush")] 113 | [InlineData("Mozilla/5.0 (X11; U; Linux Core i7-4980HQ; de; rv:32.0; compatible; JobboerseBot; http://www.jobboerse.com/bot.htm) Gecko/20100101 Firefox/38.0", "Jobboerse")] 114 | [InlineData("Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/)", "Majestic")] 115 | [InlineData("Mozilla/5.0 (compatible; SemrushBot/2~bl; +http://www.semrush.com/bot.html)", "SEMRush")] 116 | [InlineData("Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)", "YandexBot")] 117 | [InlineData("Mozilla/5.0 (compatible; YandexImages/3.0; +http://yandex.com/bots)", "YandexImages")] 118 | [InlineData("Mozilla/5.0 (compatible; Yahoo! Slurp/3.0; http://help.yahoo.com/help/us/ysearch/slurp)", "Slurp")] 119 | [InlineData("msnbot/1.0 (+http://search.msn.com/msnbot.htm)", "MSNBot")] 120 | [InlineData("msnbot/2.0b (+http://search.msn.com/msnbot.htm)", "MSNBot")] 121 | [InlineData("Mozilla/5.0 (compatible; AhrefsBot/5.0; +http://ahrefs.com/robot/)", "Ahrefs")] 122 | [InlineData("Mozilla/5.0 (compatible; seoscanners.net/1; +spider@seoscanners.net)", "SEO Scanners")] 123 | [InlineData("Mozilla/5.0 (compatible; SEOkicks-Robot; +http://www.seokicks.de/robot.html)", "SEOkicks")] 124 | [InlineData("facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)", "Facebook")] 125 | [InlineData("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) BingPreview/1.0b", "Bing Preview")] 126 | [InlineData("CheckMarkNetwork/1.0 (+http://www.checkmarknetwork.com/spider.html)", "CheckMark")] 127 | [InlineData("Mozilla/5.0 (compatible; BLEXBot/1.0; +http://webmeup-crawler.com/)", "BLEXBot")] 128 | [InlineData("Mozilla/5.0 (compatible; Linux x86_64; Mail.RU_Bot/Fast/2.0; +http://go.mail.ru/help/robots)", "Mail.ru")] 129 | [InlineData("Mozilla/5.0 (compatible; adscanner/)", "AdScanner")] 130 | [InlineData("Mozilla/5.0 (compatible; SISTRIX Crawler; http://crawler.sistrix.net/)", "Sistrix")] 131 | [InlineData("Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://aspiegel.com/petalbot)", "PetalBot")] 132 | [InlineData("WhatsApp/2.22.20.72 A", "WhatsApp")] 133 | [InlineData("WhatsApp/2.22.19.78 I", "WhatsApp")] 134 | [InlineData("WhatsApp/2.2236.3 N", "WhatsApp")] 135 | [InlineData("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Amazonbot/0.1; +developer.amazon.com/support/amazonbot) Chrome/119.0.6045.214 Safari/537.36", "Amazonbot")] 136 | [InlineData("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +openai.com/gptbot)", "GPTBot")] 137 | [InlineData("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/116.0.1938.76 Safari/537.36", "BingBot")] 138 | [InlineData("Mozilla/5.0 (compatible; AwarioBot/1.0; +awario.com/bots.html)", "AwarioBot")] 139 | [InlineData("Mozilla/5.0 (compatible; DotBot/1.2; +opensiteexplorer.org/dotbot; help@moz.com)", "DotBot")] 140 | [InlineData("Mozilla/5.0 (Windows NT 10.0; Win64; x64; trendictionbot0.5.0; trendiction search; http://www.trendiction.de/bot; please let us know of any problems; web at trendiction.com) Gecko/20100101 Firefox/125.0", "trendictionbot")] 141 | [InlineData("Mozilla/5.0 (Linux; Android 5.0) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; Bytespider; spider-feedback@bytedance.com)", "Bytespider")] 142 | [InlineData("Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +perplexity.ai/perplexitybot)", "PerplexityBot")] 143 | [InlineData("Turnitin (bit.ly/2UvnfoQ)", "Turnitin")] 144 | [InlineData("meta-externalagent/1.1 (+developers.facebook.com/docs/sharing/webmasters/crawler)", "meta-externalagent")] 145 | [InlineData("CCBot/2.0 (commoncrawl.org/faq)", "CCBot")] 146 | [InlineData("Mozilla/5.0 (compatible; SeekportBot; +bot.seekport.com)", "SeekportBot")] 147 | [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36; compatible; OAI-SearchBot/1.0; +openai.com/searchbot", "OAI-SearchBot")] 148 | [InlineData("Mozilla/5.0 (compatible; archive.org_bot +http://archive.org/details/archive.org_bot)", "archive.org")] 149 | [InlineData("Mozilla/5.0 (compatible; MojeekBot/0.11; +mojeek.com/bot.html)", "MojeekBot")] 150 | [InlineData("Mozilla/5.0 (compatible; CensysInspect/1.1; +https://about.censys.io/)", "CensysInspect")] 151 | [InlineData("Mozilla/5.0 (compatible; InternetMeasurement/1.0; +https://internet-measurement.com/)", "InternetMeasurement")] 152 | [InlineData("Mozilla/5.0 (compatible; Barkrowler/0.9; +https://babbar.tech/crawler)", "Barkrowler")] 153 | [InlineData("BrightEdge Crawler/1.0 (crawler@brightedge.com)", "BrightEdge")] 154 | [InlineData("Mozilla/5.0 (compatible; ImagesiftBot; +imagesift.com)", "ImagesiftBot")] 155 | [InlineData("Mozilla/5.0 (compatible; Cotoyogi/4.0; +https://ds.rois.ac.jp/center8/crawler/)", "Cotoyogi")] 156 | [InlineData("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot)", "Applebot")] 157 | [InlineData("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36; 360Spider", "360Spider")] 158 | [InlineData("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko; GeedoProductSearch; +https://geedo.com/product-search.html) Chrome/134.0.0.0 Safari/537.36", "GeedoProductSearch")] 159 | public void BotTests(string ua, string name) 160 | { 161 | HttpUserAgentInformation uaInfo = HttpUserAgentInformation.Parse(ua); 162 | 163 | Assert.Equal(name, uaInfo.Name); 164 | Assert.Null(uaInfo.Version); 165 | Assert.Equal(ua, uaInfo.UserAgent); 166 | 167 | Assert.Equal(HttpUserAgentType.Robot, uaInfo.Type); 168 | 169 | Assert.Null(uaInfo.Platform); 170 | Assert.Null(uaInfo.MobileDeviceType); 171 | 172 | Assert.False(uaInfo.IsBrowser()); 173 | Assert.False(uaInfo.IsMobile()); 174 | Assert.True(uaInfo.IsRobot()); 175 | } 176 | 177 | [Theory] 178 | [InlineData("")] 179 | [InlineData("???")] 180 | [InlineData("NotAUserAgent")] 181 | [InlineData("Mozilla")] 182 | [InlineData("Mozilla/")] 183 | [InlineData("()")] 184 | [InlineData("UserAgent/")] 185 | [InlineData("Bot/123 (")] 186 | [InlineData("123456")] 187 | [InlineData("curl")] 188 | [InlineData("invalid/useragent")] 189 | [InlineData("Mozilla (Windows)")] 190 | [InlineData("Chrome/ABC")] 191 | [InlineData(";;!!##")] 192 | [InlineData("Safari/ ")] 193 | [InlineData("Opera( )")] 194 | [InlineData("Mozilla/5.0 (X11; ) Gecko")] 195 | [InlineData("FakeUA/1.0 (Test)???")] 196 | [InlineData("Mozilla/ (iPhone; U; CPU iPhone OS like Mac OS X) AppleWebKit/ (KHTML, like Gecko) Version/ Mobile/ Safari/")] 197 | [InlineData("Mozzila/5.0 (Windows NT 10.0; Win64; x64)")] 198 | [InlineData("Chorme/91.0.4472.124 (Windows NT 10.0; Win64; x64)")] 199 | [InlineData("FireFoxx/89.0 (Macintosh; Intel Mac OS X 10_15_7)")] 200 | [InlineData("Safarii/14.1 (iPhone; CPU iPhone OS 14_6 like Mac OS X)")] 201 | [InlineData("InternetExploder/11.0 (Windows NT 6.1; WOW64)")] 202 | [InlineData("Bravee/1.25.72 (Windows NT 10.0; Win64; x64)")] 203 | [InlineData("Mozzila/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0)")] 204 | [InlineData("Chromee/99.0.4758.102 (X11; Linux x86_64)")] 205 | [InlineData("FirreFox/100.0 (Windows NT 10.0; rv:100.0)")] 206 | [InlineData("Saffari/605.1.15 (iPad; CPU OS 14_6 like Mac OS X)")] 207 | [InlineData("Edgg/103.0.1264.37 (Macintosh; Intel Mac OS X 11_5_2)")] 208 | [InlineData("Chorome/91.0.4472.124 (Linux; Android 10; SM-G973F)")] 209 | [InlineData("Edgee/18.18363 (Windows 10 1909; Win64; x64)")] 210 | public void InvalidUserAgent(string userAgent) 211 | { 212 | HttpUserAgentInformation info = HttpUserAgentInformation.Parse(userAgent); 213 | 214 | // Invalid or malformed UAs must be classified as Unknown 215 | Assert.Equal(HttpUserAgentType.Unknown, info.Type); 216 | Assert.Null(info.Name); 217 | Assert.Null(info.Version); 218 | 219 | // Parser trims input via Cleanup, so compare to trimmed UA 220 | Assert.Equal(userAgent.Trim(), info.UserAgent); 221 | 222 | // Should not be considered a browser or a robot 223 | Assert.False(info.IsBrowser()); 224 | Assert.False(info.IsRobot()); 225 | } 226 | 227 | [Fact] 228 | public void Cleanup_Trims_Input() 229 | { 230 | string input = " Mozilla/5.0 "; 231 | Assert.Equal("Mozilla/5.0", HttpUserAgentParser.Cleanup(input)); 232 | } 233 | 234 | [Fact] 235 | public void TryGetPlatform_True_And_False() 236 | { 237 | bool ok = HttpUserAgentParser.TryGetPlatform("Mozilla/5.0 (Windows NT 10.0)", out HttpUserAgentPlatformInformation? platform); 238 | Assert.True(ok); 239 | Assert.NotNull(platform); 240 | Assert.Equal(HttpUserAgentPlatformType.Windows, platform!.Value.PlatformType); 241 | 242 | ok = HttpUserAgentParser.TryGetPlatform("UnknownAgent", out platform); 243 | Assert.False(ok); 244 | Assert.Null(platform); 245 | } 246 | 247 | [Fact] 248 | public void TryGetRobot_True_And_False() 249 | { 250 | bool ok = HttpUserAgentParser.TryGetRobot("Googlebot/2.1 (+http://www.google.com/bot.html)", out string? robot); 251 | Assert.True(ok); 252 | Assert.Equal("Googlebot", robot); 253 | 254 | ok = HttpUserAgentParser.TryGetRobot("NoBotHere", out robot); 255 | Assert.False(ok); 256 | Assert.Null(robot); 257 | } 258 | 259 | [Fact] 260 | public void TryGetMobileDevice_True_And_False() 261 | { 262 | bool ok = HttpUserAgentParser.TryGetMobileDevice("(iPhone; CPU iPhone OS)", out string? device); 263 | Assert.True(ok); 264 | Assert.Equal("Apple iPhone", device); 265 | 266 | ok = HttpUserAgentParser.TryGetMobileDevice("Desktop Machine", out device); 267 | Assert.False(ok); 268 | Assert.Null(device); 269 | } 270 | 271 | [Fact] 272 | public void TryGetBrowser_False_When_Token_Without_Slash() 273 | { 274 | // Contains DetectToken (Edg) but not followed by '/', should be ignored by fast-path and no regex fallback here 275 | (string Name, string? Version)? browser; 276 | bool ok = HttpUserAgentParser.TryGetBrowser("Mozilla Edg 123 something", out browser); 277 | Assert.False(ok); 278 | Assert.Null(browser); 279 | } 280 | 281 | [Fact] 282 | public void GetBrowser_Trident_Without_RV_Falls_Back_To_Detect_Token() 283 | { 284 | // Trident present but no rv:, fallback should extract version after DetectToken (Trident/7.0) 285 | (string Name, string? Version)? browser = HttpUserAgentParser.GetBrowser("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Trident/7.0 like Gecko"); 286 | Assert.NotNull(browser); 287 | Assert.Equal("Internet Explorer", browser!.Value.Name); 288 | Assert.Equal("7.0", browser.Value.Version); 289 | } 290 | 291 | [Fact] 292 | public void GetBrowser_LongToken_NoDigits_Within_Window_Does_Not_Parse_Version() 293 | { 294 | // Build UA: Detect token present (Chrome), but after '/' there are no digits within first 200 chars 295 | string longJunk = new('a', 200); 296 | string ua = $"Mozilla/5.0 Chrome/{longJunk} versionafterwindow1.2"; 297 | 298 | (string Name, string? Version)? browser = HttpUserAgentParser.GetBrowser(ua); 299 | Assert.Null(browser); // Should fail to extract version and continue, ending with no browser match 300 | } 301 | } 302 | --------------------------------------------------------------------------------