Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .aspire/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"appHostPath": "../examples/Aspire.AppHost/Examples.Aspire.AppHost.csproj"
}
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "4.8.13",
"commands": [
"reportgenerator"
]
}
}
}
8 changes: 4 additions & 4 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ jobs:
packages: read

steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: csharp
build-mode: none
Expand All @@ -23,10 +23,10 @@ jobs:
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x
- name: Build
run: dotnet build
- name: Test
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.attic
TestReport

## Visual Studio
# User-specific files
Expand Down
38 changes: 38 additions & 0 deletions .runsettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<RunConfiguration>
<MaxCpuCount>0</MaxCpuCount>
</RunConfiguration>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat Code Coverage">
<Configuration>
<Format>cobertura</Format>
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,TestSDKAutoGeneratedCode</ExcludeByAttribute>
<Exclude>[*.UnitTest]*,[*.Example]*Tests</Exclude>
<Include>[*]Hj.ReverseProxy*</Include>
<UseSourceLink>false</UseSourceLink>
<IncludeTestAssembly>true</IncludeTestAssembly>
<SkipAutoProps>true</SkipAutoProps>
<ExcludeAssembliesWithoutSources>MissingAll,MissingAny,None</ExcludeAssembliesWithoutSources>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
<xUnit>
<AppDomain>denied</AppDomain>
<DiagnosticMessages>false</DiagnosticMessages>
<FailSkips>false</FailSkips>
<InternalDiagnosticMessages>false</InternalDiagnosticMessages>
<LongRunningTestSeconds>0</LongRunningTestSeconds>
<MaxParallelThreads>0</MaxParallelThreads>
<MethodDisplay>classAndMethod</MethodDisplay>
<MethodDisplayOptions>none</MethodDisplayOptions>
<NoAutoReporters>false</NoAutoReporters>
<ParallelizeAssembly>true</ParallelizeAssembly>
<ParallelizeTestCollections>true</ParallelizeTestCollections>
<PreEnumerateTheories>true</PreEnumerateTheories>
<ReporterSwitch>quiet</ReporterSwitch>
<StopOnFail>false</StopOnFail>
</xUnit>
</RunSettings>
7 changes: 6 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"recommendations": [
"editorconfig.editorconfig",
"ms-dotnettools.csdevkit"
"github.copilot-chat",
"github.vscode-github-actions",
"ms-vscode.powershell",
"ms-dotnettools.csdevkit",
"ms-vscode.live-server",
"microsoft-aspire.aspire-vscode"
]
}
21 changes: 21 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"configurations": [
{
"type": "aspire",
"request": "launch",
"name": "Aspire: Launch",
"program": "${workspaceFolder}/examples/Aspire.AppHost/Examples.Aspire.AppHost.csproj",
"debuggers": {
"project": {
"console": "integratedTerminal",
"logging": {
"moduleLoad": false
}
},
"apphost": {
"stopAtEntry": true
}
}
}
]
}
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"**/obj": true
},
"git.enableCommitSigning": true,
"dotnet.unitTests.runSettingsPath": ".runsettings",
"csharp.debug.symbolOptions.searchNuGetOrgSymbolServer": true,
"csharp.debug.symbolOptions.searchMicrosoftSymbolServer": true,
"csharp.debug.justMyCode": true,
Expand Down
30 changes: 30 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "run tests",
"type": "shell",
"command": "${workspaceFolder}/Update-TestCoverage.ps1",
"problemMatcher": "$msCompile",
"group": {
"kind": "test",
"isDefault": true
}
},
{
"label": "view test report",
"command": "",
"type": "process",
"windows": {
"command": "explorer"
},
"osx": {
"command": "open"
},
"args": [
"${workspaceFolder}/TestReport/index.html"
],
"problemMatcher": []
}
]
}
155 changes: 155 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Copilot instructions

## Project Overview
This repository is a .NET library that combines Microsoft YARP (Yet Another Reverse Proxy) with runtime configuration APIs and automatic self-signed ephemeral certificate generation. It's designed to simplify development and testing of reverse proxy scenarios. The `examples/` directory is set up to use Aspire. Aspire is an orchestrator for the entire application and will take care of configuring dependencies, building, and running the application. The resources that make up the application are defined in `examples/Aspire.AppHost/Program.cs` including application code and external dependencies.

The `src/ReverseProxy` directory implements the core functionality of the library. The `src/ReverseProxy.Aspire` directory is an extension that integrates `src/ReverseProxy` into an Aspire solution for managing YARP route/cluster configuration.

### Key Components
1. **ReverseProxy Core** (`src/ReverseProxy`)
- **ReverseProxyApp**: Manages routes/clusters using YARP's configuration system
- **Certificate Management**: Automatic generation of self-signed ephemeral certificates with caching and trust added by a local CA
- **Runtime API**: Web API endpoints for managing routes/clusters at runtime

2. **ReverseProxy.Aspire** (`src/ReverseProxy.Aspire`)
- Integrates with the Aspire orchestration platform for route/cluster configuration for services without requiring the **Runtime API**
- `WithReverseProxyReference()`: Extension for configuring Aspire resources
- `ServiceDiscoveryStartupFilter`: Auto-discovers and configures annotated Aspire resources as YARP routes/clusters

3. **Examples** (`examples/`)
- AppHost: Aspire app host demonstrating the reverse proxy in action
- ReverseProxy: Example reverse proxy service using the library
- Website: Example backend service discoverable by the proxy

## Architecture Patterns

### Configuration Layering
- **InMemoryConfigProvider**: Holds runtime-added routes/clusters
- **IProxyConfigProvider**: Base abstraction; implementations are merged in ReverseProxyApp
- Multiple providers can coexist; routes/clusters are combined across all

### Dependency Injection Pattern
Services use primary constructor injection with sealed internal classes:
```csharp
internal sealed class ReverseProxyApp(
IConfigValidator configValidator,
InMemoryConfigProvider inMemoryConfigProvider,
IEnumerable<IProxyConfigProvider> proxyConfigProviders)
```

Use the extension methods in `src/ReverseProxy/ReverseProxyExtensions.cs` to configure services:
- `ConfigureReverseProxy()`: Registers core services
- `UseSelfSignedCertificate()`: Enables automatic certificate generation on Kestrel

### Certificate Management
- **Lazy Generation**: Certificates are created on-demand via ServerCertificateSelector
- **Caching**: IMemoryCache prevents redundant certificate generation
- **Wildcard Support**: Handles both `*.example.com` and specific domain names
- **Self-Signed CA**: Generated once and installed into the trusted root store on the platform; certificates derive trust from it

## General recommendations for working with Aspire
1. Before making any changes in the `examples/` directory, always run the apphost using `aspire run` and inspect the state of resources to make sure you are building from a known state.
1. Changes to the `examples/Aspire.AppHost/Program.cs` file will require a restart of the application to take effect.
2. Make changes incrementally and run the aspire application using the `aspire run` command to validate changes.
3. Use the Aspire MCP tools to check the status of resources and debug issues.

## Running the example application
To run the example application run the following command:

```
aspire run
```

If there is already an instance of the application running it will prompt to stop the existing instance. You only need to restart the application if code in `examples/Aspire.AppHost/Program.cs` is changed, but if you experience problems it can be useful to reset everything to the starting state.

## Checking Aspire resources
To check the status of Aspire resources defined in the AppHost model use the _list resources_ tool. This will show you the current state of each resource and if there are any issues. If a resource is not running as expected you can use the _execute resource command_ tool to restart it or perform other actions.

## Listing Aspire integrations
IMPORTANT! When a user asks you to add a resource to the AppHost model you should first use the _list integrations_ tool to get a list of the current versions of all the available integrations. You should try to use the version of the integration which aligns with the version of the Aspire.AppHost.Sdk. Some integration versions may have a preview suffix. Once you have identified the correct integration you should always use the _get integration docs_ tool to fetch the latest documentation for the integration and follow the links to get additional guidance.

## Debugging Aspire issues
IMPORTANT! Aspire is designed to capture rich logs and telemetry for all resources defined in the AppHost model. Use the following diagnostic tools when debugging issues with the application before making changes to make sure you are focusing on the right things.

1. _list structured logs_; use this tool to get details about structured logs.
2. _list console logs_; use this tool to get details about console logs.
3. _list traces_; use this tool to get details about traces.
4. _list trace structured logs_; use this tool to get logs related to a trace

## Other Aspire MCP tools
1. _select apphost_; use this tool if working with multiple app hosts within a workspace.
2. _list apphosts_; use this tool to get details about active app hosts.

## Updating the Aspire AppHost
The user may request that you update the Aspire apphost. You can do this using the `aspire update` command. This will update the apphost to the latest version and some of the Aspire specific packages in referenced projects, however you may need to manually update other packages in the solution to ensure compatibility. You can consider using the `dotnet-outdated` with the users consent. To install the `dotnet-outdated` tool use the following command:

```
dotnet tool install --global dotnet-outdated-tool
```

## Aspire workload
IMPORTANT! The aspire workload is obsolete. You should never attempt to install or use the Aspire workload.

## Official Aspire documentation
IMPORTANT! Always prefer official documentation when available. The following sites contain the official documentation for Aspire and related components.

1. https://aspire.dev
2. https://learn.microsoft.com/dotnet/aspire
3. https://nuget.org (for specific integration package details)

## Testing Strategy

### Test Framework & Patterns
- **xUnit** with **SutFactory** for mocking
- **SutFactory** (HenrikJensen.SutFactory): Custom builder pattern for automatic arrangement of the dependency graph while creating the System Under Test instance
- Reduces boilerplate; use `SutBuilder` → `InputBuilder` → `Instance<T>()` or if no spy instances are inspected, the preferred pattern `SystemUnderTest.For<T>(arrange => { })` where 'arrange' is an InputBuilder.
- **Arrange-Act-Assert**: Standard pattern with setup helpers (see `SetHappyPath(InputBuilder arrange)` in tests)

### Key Test Files
- `test/ReverseProxy.UnitTest/ReverseProxy/ReverseProxyApiTests.cs`: Tests the API endpoints
- `test/ReverseProxy.UnitTest/ReverseProxy/ReverseProxyAppTests.cs`: Tests configuration management
- `test/ReverseProxy.UnitTest/Certificate/CertificateAppTests.cs`: Tests certificate generation
- `test/ReverseProxy.UnitTest/Certificate/CertificateConfigTests.cs`: Tests configuration management
- `test/ReverseProxy.Aspire.UnitTest/ServiceDiscoveryTests.cs`: Tests the Aspire service discovery

### Running Tests
To test the library run the following command from the solution root:

```
dotnet test
```

## Build & Development

### Multi-Targeting
Projects target both `net8.0` and `net10.0`. Compatibility with Windows, macOS and Linux must be ensured when adding and changing code. Use dotnet test to verify.

### Code Analysis & Style
- **EnforceCodeStyleInBuild**: Code style violations fail the build
- Configuration: `stylecop.json` requires `xmlHeader: true` for packable projects

### Package Management
Central package versioning via `Directory.Packages.props`:
- Add new dependencies by adding `<PackageVersion>` entries there
- Projects reference by `<PackageReference Include="Name" />` (version auto-inherited)

### Global Usings
Common namespaces are globally imported via `Directory.Build.props` and project `.csproj` files

## Integration Points

### YARP Integration
- Uses `Yarp.ReverseProxy` NuGet package
- Core types: `RouteConfig`, `ClusterConfig`, `DestinationConfig`
- Validation via `IConfigValidator`

### Aspire Integration
- DI service registration via `IServiceCollection`.
- Startup filters for configuring reverse proxy routes/clusters using injected service discovery environment variables.
- Resource builders follow Aspire conventions

### Extension Points
- **IProxyConfigProvider**: Implement to add custom configuration sources
- **ICertificateConfig**: Customize certificate generation options (algorithm, subject name)
- **IFileStore**: Abstract file storage for certificates

13 changes: 2 additions & 11 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
<Project>

<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0">
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.1.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageVersion Include="HenrikJensen.SutFactory" Version="1.0.0-beta2" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.analyzers" Version="1.25.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions nuget.config → NuGet.config
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
Expand All @@ -10,4 +10,4 @@
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
</configuration>
Loading
Loading