├── 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 | | [](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) |
10 | | [](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache)| `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCach.MemoryCache` |
11 | | [](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 |
--------------------------------------------------------------------------------