Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# GitHub Copilot Instructions

## 1. Repository Context & Mission

**Repository:** `HttpUserAgentParser` (mycsharp)

**Primary Goal:**
Provide a **high-performance, stable, and broadly compatible .NET library** for parsing HTTP User-Agent strings, including integrations for ASP.NET Core and MemoryCache.

**Core Design Principles:**
- API stability over convenience
- Predictable performance characteristics
- Minimal allocations in hot paths
- Full test coverage for all observable behavior

---

## 2. Repository Structure (Authoritative)

Copilot must understand and respect the architectural boundaries:

- `src/HttpUserAgentParser`
→ Core parsing logic and public APIs

- `src/HttpUserAgentParser.AspNetCore`
→ ASP.NET Core integration (middleware, extensions)

- `src/HttpUserAgentParser.MemoryCache`
→ Caching extensions and cache-aware abstractions

- `tests/*`
→ Unit tests for **all** shipped packages
→ Tests define expected behavior and are the source of truth

- `perf/*`
→ Benchmarks for performance-sensitive code paths

---

## 3. Standard .NET CLI Commands

Use these commands consistently:

- Clean:
`dotnet clean --nologo`

- Restore:
`dotnet restore`

- Build:
`dotnet build --nologo`

- Test (all):
`dotnet test --nologo`

- Test (single project):
`dotnet test <path-to-csproj> --nologo`

---

## 4. Autonomous Execution Rules (Critical)

Copilot is expected to work **independently and end-to-end** without human intervention.

### Mandatory Quality Gates (Never Skip)
- The solution **must compile** after every change
- **All tests must pass**
- New behavior **must include unit tests**
- Public APIs **must not break** existing users unless explicitly intended
- Changes must be **minimal, focused, and intentional**

If any gate fails:
1. Diagnose the root cause
2. Fix the issue
3. Re-run the full validation cycle

---

## 5. Change Strategy & Scope Control

When solving a task, Copilot should:

1. **Analyze existing code first**
- Prefer extension over modification
- Reuse established patterns and helpers
2. **Avoid architectural rewrites**
- No refactors unless explicitly required
3. **Preserve backward compatibility**
- No breaking changes to public APIs
- No silent behavioral changes

If multiple solutions are possible:
- Prefer the **simplest**, **most explicit**, and **least invasive** option

---

## 6. Testing Requirements

Every functional change must be fully tested:

- Unit tests are mandatory for:
- New features
- Bug fixes
- Edge cases and regressions
- Prefer existing utilities from:
`tests/HttpUserAgentParser.TestHelpers`

Tests define correctness. If behavior is unclear, tests take precedence over assumptions.

---

## 7. Performance Guidelines

- Treat parsing logic as performance-critical
- Avoid unnecessary allocations and LINQ in hot paths
- Prefer spans, pooling, and cached results where appropriate
- Update or add benchmarks in `perf/*` for performance-relevant changes

---

## 8. Output Expectations

Copilot should deliver:
- Compilable, production-ready code
- Complete test coverage for new behavior
- Clear, intentional commits without unrelated changes

**Do not stop early.**
A task is only complete when **all quality gates pass** and the solution is fully validated.
56 changes: 44 additions & 12 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "test",
"type": "shell",
"command": "dotnet test --nologo",
"args": [],
"problemMatcher": [
"$msCompile"
],
"group": "build"
}
"version": "2.0.0",
"tasks": [
{
"label": "clean",
"type": "shell",
"command": "dotnet clean",
"problemMatcher": "$msCompile"
},
{
"label": "restore",
"type": "shell",
"command": "dotnet restore",
"problemMatcher": "$msCompile"
},
{
"label": "build",
"type": "shell",
"command": "dotnet build --nologo",
"problemMatcher": "$msCompile",
"group": "build"
},
{
"label": "test",
"type": "shell",
"command": "dotnet test --nologo",
"problemMatcher": "$msCompile",
"group": "test"
},
{
"label": "ci:validate",
"dependsOn": [
"clean",
"restore",
"build",
"test"
],
"dependsOrder": "sequence",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Parsing HTTP User Agents with .NET

## NuGet

| NuGet |
|-|
| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) |
| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.MemoryCache.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.MemoryCache)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache)| `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCach.MemoryCache` |
| NuGet | Install |
|-|-|
| [![MyCSharp.HttpUserAgentParser](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser) | `dotnet add package MyCSharp.HttpUserAgentParser` |
| [![MyCSharp.HttpUserAgentParser.MemoryCache](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.MemoryCache.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.MemoryCache)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.MemoryCache) | `dotnet add package MyCSharp.HttpUserAgentParser.MemoryCache` |
| [![MyCSharp.HttpUserAgentParser.AspNetCore](https://img.shields.io/nuget/v/MyCSharp.HttpUserAgentParser.AspNetCore.svg?logo=nuget&label=MyCSharp.HttpUserAgentParser.AspNetCore)](https://www.nuget.org/packages/MyCSharp.HttpUserAgentParser.AspNetCore) | `dotnet add package MyCSharp.HttpUserAgentParser.AspNetCore` |


Expand Down Expand Up @@ -104,9 +104,9 @@ public void ConfigureServices(IServiceCollection services)
Now you can use

```csharp
public void MyMethod(IHttpUserAgentParserAccessor parserAccessor)
public void MyMethod(IHttpUserAgentParserAccessor parserAccessor, HttpContext httpContext)
{
HttpUserAgentInformation info = parserAccessor.Get();
HttpUserAgentInformation? info = parserAccessor.Get(httpContext);
}
```

Expand Down Expand Up @@ -152,7 +152,7 @@ by [@BenjaminAbt](https://github.com/BenjaminAbt) and [@gfoidl](https://github.c

MIT License

Copyright (c) 2021-2025 MyCSharp
Copyright (c) 2021-2026 MyCSharp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,26 @@
namespace MyCSharp.HttpUserAgentParser.AspNetCore.DependencyInjection;

/// <summary>
/// Dependency injection extensions for ASP.NET Core environments
/// Extension methods for <see cref="HttpUserAgentParserDependencyInjectionOptions"/> to add ASP.NET Core integration.
/// </summary>
public static class HttpUserAgentParserDependencyInjectionOptionsExtensions
{
/// <summary>
/// Registers <see cref="HttpUserAgentParserAccessor"/> as <see cref="IHttpUserAgentParserAccessor"/>.
/// Requires a registered <see cref="IHttpUserAgentParserProvider"/>
/// Registers <see cref="HttpUserAgentParserAccessor"/> as a singleton implementation of <see cref="IHttpUserAgentParserAccessor"/>.
/// </summary>
/// <param name="options">The options instance from the parser registration.</param>
/// <returns>The same options instance for method chaining.</returns>
/// <remarks>
/// Requires a registered <see cref="IHttpUserAgentParserProvider"/>.
/// Call this after <c>AddHttpUserAgentParser()</c> or <c>AddHttpUserAgentCachedParser()</c>.
/// </remarks>
/// <example>
/// <code>
/// IServiceCollection services = new ServiceCollection();
/// services.AddHttpUserAgentParser()
/// .AddHttpUserAgentParserAccessor();
/// </code>
/// </example>
public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentParserAccessor(
this HttpUserAgentParserDependencyInjectionOptions options)
{
Expand Down
15 changes: 13 additions & 2 deletions src/HttpUserAgentParser.AspNetCore/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@
namespace MyCSharp.HttpUserAgentParser.AspNetCore;

/// <summary>
/// Static extensions for <see cref="HttpContext"/>
/// Extension methods for <see cref="HttpContext"/> to access User-Agent information.
/// </summary>
public static class HttpContextExtensions
{
/// <summary>
/// Returns the User-Agent header value
/// Gets the User-Agent header value from the HTTP request.
/// </summary>
/// <param name="httpContext">The HTTP context.</param>
/// <returns>The User-Agent string, or <see langword="null"/> if not present.</returns>
/// <example>
/// <code>
/// string? userAgent = httpContext.GetUserAgentString();
/// if (userAgent != null)
/// {
/// HttpUserAgentInformation info = HttpUserAgentParser.Parse(userAgent);
/// }
/// </code>
/// </example>
public static string? GetUserAgentString(this HttpContext httpContext)
{
if (httpContext.Request.Headers.TryGetValue("User-Agent", out StringValues value))
Expand Down
28 changes: 20 additions & 8 deletions src/HttpUserAgentParser.AspNetCore/HttpUserAgentParserAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,37 @@
namespace MyCSharp.HttpUserAgentParser.AspNetCore;

/// <summary>
/// User Agent parser accessor. Implements <see cref="IHttpContextAccessor.HttpContext"/>
/// Default implementation of <see cref="IHttpUserAgentParserAccessor"/> for ASP.NET Core applications.
/// </summary>
/// <remarks>
/// Creates a new instance of <see cref="HttpUserAgentParserAccessor"/>
/// Extracts and parses the User-Agent header from HTTP requests.
/// Register via <c>services.AddHttpUserAgentParser().AddHttpUserAgentParserAccessor()</c>.
/// </remarks>
/// <param name="httpUserAgentParser">The parser provider to use for parsing.</param>
public class HttpUserAgentParserAccessor(IHttpUserAgentParserProvider httpUserAgentParser)
: IHttpUserAgentParserAccessor
{
private readonly IHttpUserAgentParserProvider _httpUserAgentParser = httpUserAgentParser;

/// <summary>
/// User agent of current <see cref="IHttpContextAccessor"/>
/// </summary>
/// <inheritdoc/>
/// <example>
/// <code>
/// string? userAgent = accessor.GetHttpContextUserAgent(httpContext);
/// </code>
/// </example>
public string? GetHttpContextUserAgent(HttpContext httpContext)
=> httpContext.GetUserAgentString();

/// <summary>
/// Returns current <see cref="HttpUserAgentInformation"/> of current <see cref="IHttpContextAccessor"/>
/// </summary>
/// <inheritdoc/>
/// <example>
/// <code>
/// HttpUserAgentInformation? info = accessor.Get(httpContext);
/// if (info != null)
/// {
/// Console.WriteLine(info.Value.Name);
/// }
/// </code>
/// </example>
public HttpUserAgentInformation? Get(HttpContext httpContext)
{
string? httpUserAgent = GetHttpContextUserAgent(httpContext);
Expand Down
13 changes: 10 additions & 3 deletions src/HttpUserAgentParser.AspNetCore/IHttpUserAgentParserAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@
namespace MyCSharp.HttpUserAgentParser.AspNetCore;

/// <summary>
/// User Agent parser accessor
/// Provides access to User-Agent parsing functionality within an ASP.NET Core context.
/// </summary>
public interface IHttpUserAgentParserAccessor
{
/// <summary>
/// User agent value
/// Gets the User-Agent header value from the specified HTTP context.
/// </summary>
/// <param name="httpContext">The HTTP context to extract the User-Agent from.</param>
/// <returns>The User-Agent string, or <see langword="null"/> if not present.</returns>
string? GetHttpContextUserAgent(HttpContext httpContext);

/// <summary>
/// Returns current <see cref="HttpUserAgentInformation"/>
/// Parses the User-Agent from the specified HTTP context.
/// </summary>
/// <param name="httpContext">The HTTP context to extract and parse the User-Agent from.</param>
/// <returns>
/// An <see cref="HttpUserAgentInformation"/> instance if the User-Agent header is present;
/// otherwise, <see langword="null"/>.
/// </returns>
HttpUserAgentInformation? Get(HttpContext httpContext);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,30 @@
namespace MyCSharp.HttpUserAgentParser.MemoryCache.DependencyInjection;

/// <summary>
/// Dependency injection extensions for IMemoryCache
/// Extension methods for registering <see cref="HttpUserAgentParserMemoryCachedProvider"/> with dependency injection.
/// </summary>
public static class HttpUserAgentParserMemoryCacheServiceCollectionExtensions
{
/// <summary>
/// Registers <see cref="HttpUserAgentParserCachedProvider"/> as singleton to <see cref="IHttpUserAgentParserProvider"/>
/// Registers <see cref="HttpUserAgentParserMemoryCachedProvider"/> as a singleton implementation of <see cref="IHttpUserAgentParserProvider"/>.
/// </summary>
/// <param name="services">The service collection to add the services to.</param>
/// <param name="options">Optional action to configure the cache options.</param>
/// <returns>Options for further configuration.</returns>
/// <remarks>
/// <para>Default configuration: 256 entries maximum, 1 day sliding expiration.</para>
/// <para>Use the <paramref name="options"/> parameter to customize cache behavior.</para>
/// </remarks>
/// <example>
/// <code>
/// IServiceCollection services = new ServiceCollection();
/// services.AddHttpUserAgentMemoryCachedParser(opts =>
/// {
/// opts.CacheOptions.SizeLimit = 512;
/// opts.CacheEntryOptions.SlidingExpiration = TimeSpan.FromHours(6);
/// });
/// </code>
/// </example>
public static HttpUserAgentParserDependencyInjectionOptions AddHttpUserAgentMemoryCachedParser(
this IServiceCollection services, Action<HttpUserAgentParserMemoryCachedProviderOptions>? options = null)
{
Expand Down
Loading