Add Aspire Extension (#1109)

* WireMock.Net.Aspire

* .

* xxx

* nuget

* [CodeFactor] Apply fixes

* ut

* t

* **WireMock.Net.Aspire**

* .

* t

* .

* .

* .

* TESTS

* docker utils

* Install .NET Aspire workload

* 4

* 4!

* projects: '**/test/**/*.csproj'

* script: 'dotnet workload install aspire'

* projects: '**/test/**/*.csproj'

* coverage

* WithWatchStaticMappings

* Admin

* typo

* port

* fix

* .

* x

* ...

* wait

* readme

* x

* 2

* async

* <Version>0.0.1-preview-03</Version>

* ...

* fix aspire

* admin/pwd

* Install .NET Aspire workload

* 0.0.1-preview-04

* WaitForHealthAsync

* ...

* IsHealthyAsync

* .

* add eps

* name: 'Execute Aspire Tests'

* name: Install .NET Aspire workload

* .

* dotnet test

* remove duplicate

* .

* cc

* dotnet tool install --global coverlet.console

* -*

* merge

* /d:sonar.pullrequest.provider=github

* <Version>0.0.1-preview-05</Version>

* // Copyright © WireMock.Net

* .

---------

Co-authored-by: codefactor-io <support@codefactor.io>
This commit is contained in:
Stef Heyenrath
2024-07-27 18:53:59 +02:00
committed by GitHub
parent 69c829fae0
commit 4b12f3419f
70 changed files with 2849 additions and 31 deletions

View File

@@ -19,9 +19,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: 'Run Unit Tests'
run: |
dotnet test './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj' -c Release --framework net8.0
- name: 'Execute Tests'
run: dotnet test './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj' -c Release --framework net8.0
linux-build-and-run:
name: Run Tests on Linux
@@ -33,6 +32,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: 'Run Unit Tests'
run: |
dotnet test './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj' -c Release --framework net8.0
- name: 'Execute Tests'
run: dotnet test './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj' -c Release --framework net8.0
- name: Install .NET Aspire workload
run: dotnet workload install aspire
- name: 'Execute .NET Aspire Tests'
run: dotnet test './test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj' -c Release

View File

@@ -1,5 +1,5 @@
## WireMock.Net
Lightweight Http Mocking Server for .NET, inspired by [WireMock(http://WireMock.org) from the Java landscape.
Lightweight Http Mocking Server for .NET, inspired by WireMock.org (from the Java landscape).
### :star: Key Features
* HTTP response stubbing, matchable on URL/Path, headers, cookies and body content patterns
@@ -11,6 +11,7 @@ Lightweight Http Mocking Server for .NET, inspired by [WireMock(http://WireMock.
* Stateful behaviour simulation
* Response templating / transformation using Handlebars and extensions
* Can be used locally or in CI/CD scenarios
* Can be used for Aspire Distributed Application testing
### :star: Stubbing
A core feature of WireMock.Net is the ability to return predefined HTTP responses for requests matching criteria.
@@ -32,6 +33,12 @@ WireMock.Net can be used in several ways:
You can use your favorite test framework and use WireMock within your tests, see
[Wiki : UnitTesting](https://github.com/StefH/WireMock.Net/wiki/Using-WireMock-in-UnitTests).
### Unit/Integration Testing using Testcontainers.DotNet
See [Wiki : WireMock.Net.Testcontainers](https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock.Net.Testcontainers) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
### Unit/Integration Testing using an an Aspire Distributed Application
See [Wiki : WireMock.Net.Aspire](https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock.Net.Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
#### As a dotnet tool
It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet tool](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-dotnet-tool).

View File

@@ -13,6 +13,7 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
* Stateful behaviour simulation
* Response templating / transformation using Handlebars and extensions
* Can be used locally or in CI/CD scenarios
* Can be used for Aspire Distributed Application testing
## :memo: Blogs
- [mstack.nl : Generate C# Code from Mapping(s)](https://mstack.nl/blog/20230201-wiremock.net-tocode/)
@@ -46,6 +47,7 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
| &nbsp;&nbsp;**WireMock.Net.RestClient** | [![NuGet Badge WireMock.Net.RestClient](https://buildstats.info/nuget/WireMock.Net.RestClient)](https://www.nuget.org/packages/WireMock.Net.RestClient) | [![MyGet Badge WireMock.Net.RestClient](https://buildstats.info/myget/wiremock-net/WireMock.Net.RestClient?includePreReleases=true)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.RestClient)
| &nbsp;&nbsp;**WireMock.Net.xUnit** | [![NuGet Badge WireMock.Net.xUnit](https://buildstats.info/nuget/WireMock.Net.xUnit)](https://www.nuget.org/packages/WireMock.Net.xUnit) | [![MyGet Badge WireMock.Net.xUnit](https://buildstats.info/myget/wiremock-net/WireMock.Net.xUnit?includePreReleases=true)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.xUnit)
| &nbsp;&nbsp;**WireMock.Net.Testcontainers** | [![NuGet Badge WireMock.Net.Testcontainers](https://buildstats.info/nuget/WireMock.Net.Testcontainers)](https://www.nuget.org/packages/WireMock.Net.Testcontainers) | [![MyGet Badge WireMock.Net.Testcontainers](https://buildstats.info/myget/wiremock-net/WireMock.Net.Testcontainers?includePreReleases=true)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Testcontainers)
| &nbsp;&nbsp;**WireMock.Net.Aspire** | [![NuGet Badge WireMock.Net.Aspire](https://buildstats.info/nuget/WireMock.Net.Aspire)](https://www.nuget.org/packages/WireMock.Net.Aspire) | [![MyGet Badge WireMock.Net.Aspire](https://buildstats.info/myget/wiremock-net/WireMock.Net.Aspire?includePreReleases=true)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Aspire)
| &nbsp;&nbsp;**WireMock.Org.RestClient** | [![NuGet Badge WireMock.Org.RestClient](https://buildstats.info/nuget/WireMock.Org.RestClient)](https://www.nuget.org/packages/WireMock.Org.RestClient) | [![MyGet Badge WireMock.Org.RestClient](https://buildstats.info/myget/wiremock-net/WireMock.Org.RestClient?includePreReleases=true)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Org.RestClient)
@@ -73,7 +75,10 @@ You can use your favorite test framework and use WireMock within your tests, see
[Wiki : UnitTesting](https://github.com/StefH/WireMock.Net/wiki/Using-WireMock-in-UnitTests).
### Unit/Integration Testing using Testcontainers.DotNet
You can use [Wiki : WireMock.Net.Testcontainers](https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock.Net.Testcontainers) to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
See [Wiki : WireMock.Net.Testcontainers](https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock.Net.Testcontainers) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
### Unit/Integration Testing using an an Aspire Distributed Application
See [Wiki : WireMock.Net.Aspire](https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock.Net.Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
### As a dotnet tool
It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet tool](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-dotnet-tool).

View File

@@ -113,6 +113,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMockAzureQueueProxy", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Console.GrpcClient", "examples\WireMock.Net.Console.GrpcClient\WireMock.Net.Console.GrpcClient.csproj", "{B1580A38-84E7-44BE-8FE7-3EE5031D74A1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples - Aspire", "examples - Aspire", "{AD474543-0715-49F2-A284-936B060BF736}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Aspire", "src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj", "{CAB42D88-B4E4-4887-B684-9F3E09D085A1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireApp1.ServiceDefaults", "examples-Aspire\AspireApp1.ServiceDefaults\AspireApp1.ServiceDefaults.csproj", "{42113E6B-DC43-4E80-9967-1E4233568E87}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireApp1.ApiService", "examples-Aspire\AspireApp1.ApiService\AspireApp1.ApiService.csproj", "{84624E1F-DF07-4315-89B0-51776BE99E13}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireApp1.Web", "examples-Aspire\AspireApp1.Web\AspireApp1.Web.csproj", "{A34F1575-7C33-4548-8CEF-8D8D8B84153C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireApp1.AppHost", "examples-Aspire\AspireApp1.AppHost\AspireApp1.AppHost.csproj", "{7373B7DC-47ED-45A5-969D-D7DDBA529B53}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Aspire.Tests", "test\WireMock.Net.Aspire.Tests\WireMock.Net.Aspire.Tests.csproj", "{CE602F57-FEF8-4559-A9E0-6200BE1BF398}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Aspire.TestAppHost", "test\WireMock.Net.Aspire.TestAppHost\WireMock.Net.Aspire.TestAppHost.csproj", "{F1B5999D-D22E-48A6-AB86-18A7876BD32E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireApp1.AppHostOriginal", "examples-Aspire\AspireApp1.AppHostOriginal\AspireApp1.AppHostOriginal.csproj", "{C9210DA3-F390-4598-8512-349A473FE9C9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -267,6 +285,38 @@ Global
{B1580A38-84E7-44BE-8FE7-3EE5031D74A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1580A38-84E7-44BE-8FE7-3EE5031D74A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1580A38-84E7-44BE-8FE7-3EE5031D74A1}.Release|Any CPU.Build.0 = Release|Any CPU
{CAB42D88-B4E4-4887-B684-9F3E09D085A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CAB42D88-B4E4-4887-B684-9F3E09D085A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CAB42D88-B4E4-4887-B684-9F3E09D085A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CAB42D88-B4E4-4887-B684-9F3E09D085A1}.Release|Any CPU.Build.0 = Release|Any CPU
{42113E6B-DC43-4E80-9967-1E4233568E87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42113E6B-DC43-4E80-9967-1E4233568E87}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42113E6B-DC43-4E80-9967-1E4233568E87}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42113E6B-DC43-4E80-9967-1E4233568E87}.Release|Any CPU.Build.0 = Release|Any CPU
{84624E1F-DF07-4315-89B0-51776BE99E13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84624E1F-DF07-4315-89B0-51776BE99E13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84624E1F-DF07-4315-89B0-51776BE99E13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84624E1F-DF07-4315-89B0-51776BE99E13}.Release|Any CPU.Build.0 = Release|Any CPU
{A34F1575-7C33-4548-8CEF-8D8D8B84153C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A34F1575-7C33-4548-8CEF-8D8D8B84153C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A34F1575-7C33-4548-8CEF-8D8D8B84153C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A34F1575-7C33-4548-8CEF-8D8D8B84153C}.Release|Any CPU.Build.0 = Release|Any CPU
{7373B7DC-47ED-45A5-969D-D7DDBA529B53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7373B7DC-47ED-45A5-969D-D7DDBA529B53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7373B7DC-47ED-45A5-969D-D7DDBA529B53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7373B7DC-47ED-45A5-969D-D7DDBA529B53}.Release|Any CPU.Build.0 = Release|Any CPU
{CE602F57-FEF8-4559-A9E0-6200BE1BF398}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE602F57-FEF8-4559-A9E0-6200BE1BF398}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE602F57-FEF8-4559-A9E0-6200BE1BF398}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE602F57-FEF8-4559-A9E0-6200BE1BF398}.Release|Any CPU.Build.0 = Release|Any CPU
{F1B5999D-D22E-48A6-AB86-18A7876BD32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1B5999D-D22E-48A6-AB86-18A7876BD32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1B5999D-D22E-48A6-AB86-18A7876BD32E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1B5999D-D22E-48A6-AB86-18A7876BD32E}.Release|Any CPU.Build.0 = Release|Any CPU
{C9210DA3-F390-4598-8512-349A473FE9C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C9210DA3-F390-4598-8512-349A473FE9C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9210DA3-F390-4598-8512-349A473FE9C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9210DA3-F390-4598-8512-349A473FE9C9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -311,6 +361,14 @@ Global
{1EA72C0F-92E9-486B-8FFE-53F992BFC4AA} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{7FC0B409-2682-40EE-B3B9-3930D6769D01} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{B1580A38-84E7-44BE-8FE7-3EE5031D74A1} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{CAB42D88-B4E4-4887-B684-9F3E09D085A1} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{42113E6B-DC43-4E80-9967-1E4233568E87} = {AD474543-0715-49F2-A284-936B060BF736}
{84624E1F-DF07-4315-89B0-51776BE99E13} = {AD474543-0715-49F2-A284-936B060BF736}
{A34F1575-7C33-4548-8CEF-8D8D8B84153C} = {AD474543-0715-49F2-A284-936B060BF736}
{7373B7DC-47ED-45A5-969D-D7DDBA529B53} = {AD474543-0715-49F2-A284-936B060BF736}
{CE602F57-FEF8-4559-A9E0-6200BE1BF398} = {0BB8B634-407A-4610-A91F-11586990767A}
{F1B5999D-D22E-48A6-AB86-18A7876BD32E} = {0BB8B634-407A-4610-A91F-11586990767A}
{C9210DA3-F390-4598-8512-349A473FE9C9} = {AD474543-0715-49F2-A284-936B060BF736}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}

View File

@@ -14,10 +14,14 @@ jobs:
echo "BuildId = $(buildId)"
displayName: 'Print buildId'
- task: CmdLine@2
displayName: 'Install .NET Aspire workload'
inputs:
script: 'dotnet workload install aspire'
- script: |
dotnet tool install --global dotnet-sonarscanner
dotnet tool install --global dotnet-coverage
dotnet tool install --global coverlet.console
displayName: 'Install dotnet tools'
- task: PowerShell@2
@@ -31,10 +35,10 @@ jobs:
- script: |
dotnet dev-certs https --trust || true
displayName: 'dotnet dev-certs https'
# See: https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage
# See: https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage
- script: |
dotnet sonarscanner begin /k:"WireMock-Net_WireMock.Net" /o:"wiremock-net" /d:sonar.branch.name=$(Build.SourceBranchName) /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$(SONAR_TOKEN)" /d:sonar.dotnet.excludeTestProjects=true /d:sonar.cs.vscoveragexml.reportsPaths=**/wiremock-coverage.xml /d:sonar.verbose=true
dotnet sonarscanner begin /k:"WireMock-Net_WireMock.Net" /o:"wiremock-net" /d:sonar.branch.name=$(Build.SourceBranchName) /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$(SONAR_TOKEN)" /d:sonar.pullrequest.provider=github /d:sonar.dotnet.excludeTestProjects=true /d:sonar.cs.vscoveragexml.reportsPaths=**/wiremock-coverage-*.xml /d:sonar.verbose=true
displayName: 'Begin analysis on SonarCloud'
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
@@ -42,14 +46,21 @@ jobs:
displayName: 'Build Unit tests'
inputs:
command: 'build'
projects: './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj'
arguments: '--configuration Debug --framework net8.0 --no-incremental'
projects: '**/test/**/*.csproj'
arguments: '--configuration Debug --framework net8.0'
- task: CmdLine@2
inputs:
script: 'dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage.xml"'
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-1.xml"
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj --configuration Debug --no-build" -f xml -o "wiremock-coverage-2.xml"
displayName: 'Execute Unit Tests with Coverage'
- task: CmdLine@2
displayName: 'Merge coverage files'
inputs:
script: 'dotnet coverage merge **/wiremock-coverage-*.xml --output ./test/wiremock-coverage.xml --output-format xml'
- script: |
dotnet sonarscanner end /d:sonar.token="$(SONAR_TOKEN)"
displayName: 'End analysis on SonarCloud'
@@ -60,8 +71,8 @@ jobs:
condition: and(succeeded(), eq(variables['RUN_WHITESOURCE'], 'yes'))
- script: |
bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) -f ./test/WireMock.Net.Tests/coverage.8.0.opencover.xml
displayName: 'codecov'
bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) -f ./test/wiremock-coverage.xml
displayName: 'Upload coverage results to codecov'
- task: PublishTestResults@2
condition: and(succeeded(), eq(variables['PUBLISH_TESTRESULTS'], 'yes'))
@@ -70,7 +81,7 @@ jobs:
testResultsFiles: '**/*.trx'
- task: PublishBuildArtifacts@1
displayName: Publish coverage file
displayName: Publish coverage files
inputs:
PathtoPublish: './test/WireMock.Net.Tests/coverage.net8.0.opencover.xml'

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.ServiceDefaults\AspireApp1.ServiceDefaults.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,52 @@
var builder = WebApplication.CreateBuilder(args);
// Add service defaults & Aspire components.
builder.AddServiceDefaults();
// Add services to the container.
builder.Services.AddProblemDetails();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseExceptionHandler();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});
app.MapGet("/weatherforecast2", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
});
app.MapDefaultEndpoints();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

View File

@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "http://localhost:5433",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:7365;http://localhost:5433",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
using AspireApp1.AppHost;
var builder = DistributedApplication.CreateBuilder(args);
//IResourceBuilder<ProjectResource> apiService = builder.AddProject<Projects.AspireApp1_ApiService>("apiservice");
var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings");
Console.WriteLine($"MappingsPath: {mappingsPath}");
var wiremock = builder
.AddWireMock("apiservice", WireMockServerArguments.DefaultPort)
.WithMappingsPath(mappingsPath)
.WithReadStaticMappings()
.WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
builder.AddProject<Projects.AspireApp1_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(wiremock);
builder.Build().Run();

View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17194;http://localhost:15256",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21232",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22019"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15256",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20086"
}
}
}
}

View File

@@ -0,0 +1,36 @@
using WireMock.Client.Builders;
namespace AspireApp1.AppHost;
internal class WeatherForecastApiMock
{
public static async Task BuildAsync(AdminApiMappingBuilder builder)
{
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
builder.Given(b => b
.WithRequest(request => request
.UsingGet()
.WithPath("/weatherforecast2")
)
.WithResponse(response => response
.WithHeaders(h => h.Add("Content-Type", "application/json"))
.WithBodyAsJson(() => Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray())
)
);
await builder.BuildAndPostAsync();
}
}
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);

View File

@@ -0,0 +1,41 @@
{
"Guid": "873d495f-940e-4b86-a1f4-4f0fc7be8b8b",
"Request": {
"Path": "/weatherforecast",
"Methods": [
"get"
]
},
"Response": {
"BodyAsJson": [
{
"date": "2024-05-24",
"temperatureC": -17,
"summary": "Balmy"
},
{
"date": "2024-05-25",
"temperatureC": -13,
"summary": "Mild"
},
{
"date": "2024-05-26",
"temperatureC": 31,
"summary": "Bracing"
},
{
"date": "2024-05-27",
"temperatureC": 6,
"summary": "Hot"
},
{
"date": "2024-05-28",
"temperatureC": -2,
"summary": "Mild"
}
],
"Headers": {
"Content-Type": "application/json"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
var builder = DistributedApplication.CreateBuilder(args);
IResourceBuilder<ProjectResource> apiService = builder.AddProject<Projects.AspireApp1_ApiService>("apiservice");
builder.AddProject<Projects.AspireApp1_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService);
builder.Build().Run();

View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17194;http://localhost:15256",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21232",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22019"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15256",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19163",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20086"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireSharedProject>true</IsAspireSharedProject>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.3.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,112 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.Hosting;
// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
// This project should be referenced by each service project in your solution.
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();
// Turn on service discovery by default
http.AddServiceDiscovery();
});
return builder;
}
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation()
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
//.AddGrpcClientInstrumentation()
.AddHttpClientInstrumentation();
});
builder.AddOpenTelemetryExporters();
return builder;
}
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
if (useOtlpExporter)
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
//{
// builder.Services.AddOpenTelemetry()
// .UseAzureMonitor();
//}
return builder;
}
public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
{
builder.Services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Adding health checks endpoints to applications in non-development environments has security implications.
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
if (app.Environment.IsDevelopment())
{
// All health checks must pass for app to be considered ready to accept traffic after starting
app.MapHealthChecks("/health");
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
}
return app;
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="8.0.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.AppHost\AspireApp1.AppHost.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Aspire.Hosting.Testing" />
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
using System.Net;
namespace AspireApp1.Tests;
public class WebTests
{
[Fact]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
// Arrange
var appHost = await DistributedApplicationTestingBuilder.CreateAsync<Projects.AspireApp1_AppHost>();
await using var app = await appHost.BuildAsync();
await app.StartAsync();
// Act
var httpClient = app.CreateHttpClient("webfrontend");
var response = await httpClient.GetAsync("/");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.ServiceDefaults\AspireApp1.ServiceDefaults.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="AspireApp1.Web.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,23 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">AspireApp1</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,102 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,38 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@requestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
public HttpContext? HttpContext { get; set; }
private string? requestId;
private bool ShowRequestId => !string.IsNullOrEmpty(requestId);
protected override void OnInitialized()
{
requestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -0,0 +1,86 @@
@page "/weather"
@attribute [StreamRendering]
@* @attribute [OutputCache(Duration = 5)] *@
@inject WeatherApiClient WeatherApi
@inject WeatherApiClient2 WeatherApi2
<PageTitle>Weather</PageTitle>
<h1>Weather in Den Bosch</h1>
@if (forecasts1 == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts1)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
<h1>Weather in New York</h1>
@if (forecasts2 == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts2)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts1;
private WeatherForecast[]? forecasts2;
protected override async Task OnInitializedAsync()
{
var forecastsTask1 = WeatherApi.GetWeatherAsync();
var forecastsTask2 = WeatherApi2.GetWeatherAsync();
await Task.WhenAll(forecastsTask1, forecastsTask2);
forecasts1 = await forecastsTask1;
forecasts2 = await forecastsTask2;
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,11 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.OutputCaching
@using Microsoft.JSInterop
@using AspireApp1.Web
@using AspireApp1.Web.Components

View File

@@ -0,0 +1,49 @@
using AspireApp1.Web;
using AspireApp1.Web.Components;
var builder = WebApplication.CreateBuilder(args);
// Add service defaults & Aspire components.
builder.AddServiceDefaults();
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddOutputCache();
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
// This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
// Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
client.BaseAddress = new("https+http://apiservice");
});
builder.Services.AddHttpClient<WeatherApiClient2>(client =>
{
// This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
// Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
client.BaseAddress = new("https+http://apiservice");
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.UseOutputCache();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapDefaultEndpoints();
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5124",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7263;http://localhost:5124",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,62 @@
namespace AspireApp1.Web;
public class WeatherApiClient(HttpClient httpClient)
{
public string GetBaseAddress()
{
return httpClient.BaseAddress?.ToString() ?? "???";
}
public async Task<WeatherForecast[]> GetWeatherAsync(int maxItems = 10, CancellationToken cancellationToken = default)
{
List<WeatherForecast>? forecasts = null;
await foreach (var forecast in httpClient.GetFromJsonAsAsyncEnumerable<WeatherForecast>("/weatherforecast", cancellationToken))
{
if (forecasts?.Count >= maxItems)
{
break;
}
if (forecast is not null)
{
forecasts ??= [];
forecasts.Add(forecast);
}
}
return forecasts?.ToArray() ?? [];
}
}
public class WeatherApiClient2(HttpClient httpClient)
{
public string GetBaseAddress()
{
return httpClient.BaseAddress?.ToString() ?? "???";
}
public async Task<WeatherForecast[]> GetWeatherAsync(int maxItems = 10, CancellationToken cancellationToken = default)
{
List<WeatherForecast>? forecasts = null;
await foreach (var forecast in httpClient.GetFromJsonAsAsyncEnumerable<WeatherForecast>("/weatherforecast2", cancellationToken))
{
if (forecasts?.Count >= maxItems)
{
break;
}
if (forecast is not null)
{
forecasts ??= [];
forecasts.Add(forecast);
}
}
return forecasts?.ToArray() ?? [];
}
}
public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,25 @@
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e51240;
}
.validation-message {
color: #e51240;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
<PackageReference Include="Grpc.Net.Client" Version="2.59.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.60.0" />
<PackageReference Include="Grpc.Tools" Version="2.60.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -0,0 +1,250 @@
%!PS-Adobe-3.0 EPSF-3.0
%%Creator: cairo 1.15.10 (http://cairographics.org)
%%CreationDate: Sat Jun 8 11:27:10 2024
%%Pages: 1
%%DocumentData: Clean7Bit
%%LanguageLevel: 2
%%BoundingBox: 0 0 150 143
%%EndComments
%%BeginProlog
50 dict begin
/q { gsave } bind def
/Q { grestore } bind def
/cm { 6 array astore concat } bind def
/w { setlinewidth } bind def
/J { setlinecap } bind def
/j { setlinejoin } bind def
/M { setmiterlimit } bind def
/d { setdash } bind def
/m { moveto } bind def
/l { lineto } bind def
/c { curveto } bind def
/h { closepath } bind def
/re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto
0 exch rlineto 0 rlineto closepath } bind def
/S { stroke } bind def
/f { fill } bind def
/f* { eofill } bind def
/n { newpath } bind def
/W { clip } bind def
/W* { eoclip } bind def
/BT { } bind def
/ET { } bind def
/BDC { mark 3 1 roll /BDC pdfmark } bind def
/EMC { mark /EMC pdfmark } bind def
/cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def
/Tj { show currentpoint cairo_store_point } bind def
/TJ {
{
dup
type /stringtype eq
{ show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse
} forall
currentpoint cairo_store_point
} bind def
/cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore
cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def
/Tf { pop /cairo_font exch def /cairo_font_matrix where
{ pop cairo_selectfont } if } bind def
/Td { matrix translate cairo_font_matrix matrix concatmatrix dup
/cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point
/cairo_font where { pop cairo_selectfont } if } bind def
/Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def
cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def
/g { setgray } bind def
/rg { setrgbcolor } bind def
/d1 { setcachedevice } bind def
/cairo_data_source {
CairoDataIndex CairoData length lt
{ CairoData CairoDataIndex get /CairoDataIndex CairoDataIndex 1 add def }
{ () } ifelse
} def
/cairo_flush_ascii85_file { cairo_ascii85_file status { cairo_ascii85_file flushfile } if } def
/cairo_image { image cairo_flush_ascii85_file } def
/cairo_imagemask { imagemask cairo_flush_ascii85_file } def
%%EndProlog
%%BeginSetup
%%EndSetup
%%Page: 1 1
%%BeginPageSetup
%%PageBoundingBox: 0 0 150 143
%%EndPageSetup
q 0 0 150 143 rectclip
1 0 0 -1 0 143 cm q
0.403922 0.113725 0.47451 rg
16.574 140.176 m 16.199 138.824 15.449 136.199 15 134.324 c 14.551 132.449
14.023 130.426 13.801 129.75 c 13.426 128.477 l 16.125 128.023 l 18.75
127.5 l 21.375 125.25 l 23.926 123 l 24.824 120.301 l 25.648 117.676 l 25.273
114.676 l 24.898 111.75 l 22.949 109.273 l 21 106.727 l 17.477 105.375
l 15.449 104.625 13.801 104.023 13.727 103.949 c 13.574 103.875 14.398 100.875
15.449 97.199 c 17.324 90.602 l 20.25 84.824 l 23.102 79.125 l 26.25 75.074
l 28.051 72.824 30.977 69.301 32.926 67.352 c 36.375 63.676 l 42.074 59.625
l 45.227 57.449 48.676 55.273 49.801 54.824 c 51.824 54.074 l 50.926 52.574
l 77.102 52.574 l 79.648 45.074 l 80.625 45.074 l 81.148 45.074 81.75 45.375
81.898 45.75 c 82.125 46.426 l 87.676 46.949 l 93.227 47.477 l 92.625 46.574
l 92.102 45.676 l 93.449 45.977 l 94.801 46.199 l 95.699 49.199 l 96.523
52.199 l 105.301 52.426 l 114.074 52.648 l 113.176 54 l 116.773 55.875
l 118.727 56.926 122.773 59.551 125.625 61.801 c 130.875 65.773 l 135.602
71.398 l 140.25 76.949 l 143.102 82.199 l 144.602 85.051 146.625 89.324
147.523 91.574 c 149.176 95.699 l 149.176 116.25 l 149.25 136.801 l 148.352
139.648 l 147.523 142.574 l 17.398 142.574 l h
36 112.199 m 36 101.324 l 38.25 101.324 l 39.523 101.324 40.5 101.176 40.5
101.023 c 40.5 100.801 39.824 99.449 38.926 98.023 c 37.352 95.324 l 33.75
95.324 l 33.75 123.074 l 36 123.074 l h
57.75 109.199 m 57.75 95.324 l 55.5 95.324 l 55.5 115.727 l 52.648 115.426
l 49.727 115.199 l 52.426 119.102 l 55.051 123.074 l 57.75 123.074 l h
84 122.324 m 84 121.574 l 72.75 121.574 l 72.75 109.574 l 82.5 109.574
l 82.5 108.074 l 72.75 108.074 l 72.75 96.824 l 83.25 96.824 l 83.25 95.324
l 69 95.324 l 69 122.102 l 69.523 122.551 l 69.977 123.074 l 84 123.074
l h
102.602 109.727 m 102.75 96.824 l 111 96.824 l 111 95.324 l 90.75 95.324
l 90.75 96.824 l 99 96.824 l 99 122.102 l 99.523 122.625 l 99.824 122.926
100.574 123 101.25 122.926 c 102.375 122.699 l h
102.602 109.727 m f
0.0117647 0.6 0.8 rg
14.102 44.551 m 10.648 34.199 l 16.273 28.5 l 21.824 22.875 l 21 20.773
l 20.625 19.648 20.25 17.699 20.25 16.5 c 20.25 14.324 l 22.199 14.324
l 24 19.949 l 25.727 25.5 l 26.25 25.199 l 26.551 24.977 27.375 22.727 28.051
20.176 c 28.801 17.551 29.926 13.426 30.676 10.949 c 31.426 8.477 32.477
5.023 32.926 3.301 c 33.824 0.074 l 36.449 0.074 l 40.727 13.125 l 45 26.25
l 45 30.977 l 43.875 30.074 l 42.75 29.176 l 41.625 31.801 l 41.023 33.301
40.5 34.801 40.5 35.102 c 40.5 35.398 39.301 39.602 37.801 44.324 c 35.023
52.949 l 33.898 53.926 l 32.699 54.824 l 17.477 54.824 l h
14.102 44.551 m f
51.148 53.176 m 51.074 52.199 49.648 47.551 48.074 42.824 c 45.148 34.199
l 51.227 28.051 l 57.375 21.824 l 59.25 21.824 l 59.25 24.074 l 60.602
24.074 l 62.176 18.301 l 63.074 15.074 64.574 9.75 65.551 6.449 c 67.352
0.449 l 73.801 0.227 l 80.25 0 l 80.25 0.977 l 80.25 1.5 77.926 9.227 75
18.074 c 72.148 26.926 68.324 38.773 66.523 44.324 c 63.301 54.449 l 57.301
54.676 l 51.301 54.898 l h
51.148 53.176 m f
8.102 26.324 m 6.898 22.574 l 10.352 22.574 l 10.801 24.301 l 11.023 25.199
11.25 26.852 11.25 28.051 c 11.25 30.074 l 9.301 30.074 l h
8.102 26.324 m f
17.398 8.398 m 16.875 6.824 16.5 4.648 16.5 3.523 c 16.5 1.574 l 18.449
1.574 l 20.023 6.449 l 21.602 11.324 l 18.227 11.324 l h
17.398 8.398 m f
0.952941 0.423529 0.133333 rg
66.75 53.324 m 66.75 51.824 l 77.023 51.824 l 78.977 45.074 l 81.75 45.074
l 81.75 46.5 l 83.102 46.801 l 84.375 47.176 l 83.102 47.25 l 81.898 47.324
l 79.352 54.824 l 66.75 54.824 l h
66.75 53.324 m f
94.273 54.301 m 93.824 53.477 92.852 49.574 92.625 47.551 c 92.398 45.824
l 95.023 45.824 l 95.852 48.75 l 96.375 50.324 96.75 52.352 96.75 53.25
c 96.75 54.824 l 95.699 54.824 l 95.102 54.824 94.426 54.602 94.273 54.301
c h
94.273 54.301 m f
111 53.324 m 111 51.824 l 113.926 51.824 l 113.773 53.324 l 113.699 54.824
l 111 54.824 l h
111 53.324 m f
87.227 28.801 m 86.25 28.426 l 86.25 22.426 l 87.375 18.227 l 88.051 15.824
89.477 10.801 90.602 7.051 c 92.551 0.074 l 95.852 0.074 l 96.676 3.676
l 97.051 5.625 98.102 8.773 98.926 10.727 c 99.676 12.602 100.5 14.926
100.574 15.977 c 100.875 17.773 l 96.074 23.551 l 91.273 29.324 l 89.699
29.25 l 88.875 29.25 87.676 29.023 87.227 28.801 c h
87.227 28.801 m f
120 26.699 m 120 24.898 121.949 16.949 124.125 9.824 c 124.727 7.949 125.551
5.023 126 3.301 c 126.824 0.074 l 129.676 0.074 l 130.199 2.699 l 130.426
4.125 131.625 8.023 132.75 11.398 c 134.926 17.477 l 130.273 23.023 l 125.625
28.574 l 120 28.574 l h
120 26.699 m f
0.968627 0.576471 0.117647 rg
60.75 54 m 60.75 53.477 63.301 45.227 66.375 35.699 c 69.449 26.25 73.273
14.398 74.852 9.449 c 77.773 0.449 l 86.551 0.227 l 95.324 0 l 94.801 2.102
l 94.5 3.227 93.523 6.75 92.625 9.824 c 89.926 18.898 88.352 25.426 88.648
26.324 c 88.875 27.148 l 94.727 20.25 l 100.5 13.426 l 100.574 15.148 l
100.574 16.125 101.398 18.977 102.301 21.449 c 104.102 25.949 l 105.227
23.324 l 105.824 21.898 107.773 16.051 109.574 10.426 c 112.875 0.074 l
129.676 0.074 l 127.727 7.051 l 126.676 10.801 125.102 16.727 124.199 20.176
c 123.227 23.551 122.699 26.324 122.926 26.324 c 123.148 26.324 125.699
23.625 128.477 20.25 c 133.648 14.176 l 135.75 20.477 l 136.875 23.852
138.523 28.875 139.426 31.574 c 140.398 34.273 141.449 37.648 141.824 39.148
c 142.125 40.648 143.102 43.352 143.852 45.148 c 144.676 46.949 145.5 49.727
145.801 51.301 c 146.324 54.074 l 128.398 54.074 l 126.824 48.301 l 125.926
45.074 124.426 39.75 123.449 36.449 c 121.648 30.523 l 120.898 30.227 l
120.074 30 l 119.551 32.852 l 119.324 34.426 118.273 38.102 117.301 41.102
c 116.324 44.023 115.5 47.023 115.5 47.625 c 115.5 48.301 114.977 50.176
114.301 51.824 c 113.102 54.824 l 94.727 54.824 l 93.824 51.676 l 93.301
49.949 92.699 47.625 92.477 46.574 c 92.324 45.523 91.352 42.148 90.449
39.074 c 89.477 36 88.426 32.324 88.125 31.051 c 87.602 28.574 l 86.398
28.574 l 85.875 31.051 l 85.574 32.324 84.523 35.926 83.551 39 c 82.574
42 81.75 44.926 81.75 45.449 c 81.75 46.5 l 83.102 46.801 l 84.375 47.176
l 81.227 47.324 l 79.273 54.074 l 76.199 54.074 l 74.551 54.074 70.352
54.301 66.977 54.602 c 60.75 55.051 l h
60.75 54 m f
0.160784 0.670588 0.886275 rg
5.773 18.977 m 2.625 9.148 0 0.898 0 0.602 c 0 0.074 l 17.773 0.074 l 19.727
6.301 l 20.773 9.676 22.426 15.074 23.324 18.301 c 24.977 24.148 l 18.676
30.523 l 15.227 33.977 12.227 36.824 12 36.824 c 11.773 36.824 9 28.801
5.773 18.977 c h
5.773 18.977 m f
45 33.676 m 44.398 31.875 43.727 30.148 43.426 29.699 c 42.898 28.949 33.75
1.5 33.75 0.602 c 33.75 0.074 l 52.199 0.074 l 54 5.926 l 54.898 9.074
56.551 14.477 57.602 17.926 c 59.551 24.074 l 53.25 30.449 l 49.727 33.977
46.727 36.824 46.426 36.824 c 46.199 36.824 45.523 35.398 45 33.676 c h
45 33.676 m f
0.811765 0.0941176 0.992157 rg
22.199 116.699 m 22.199 121.574 18.227 125.551 13.352 125.551 c 8.477 125.551
4.5 121.574 4.5 116.699 c 4.5 111.824 8.477 107.852 13.352 107.852 c 18.227
107.852 22.199 111.824 22.199 116.699 c h
22.199 116.699 m f
31.5 123.824 m 31.5 122.324 l 38.25 122.324 l 38.25 125.324 l 31.5 125.324
l h
31.5 123.824 m f
46.727 116.176 m 43.426 111.074 40.273 106.125 39.676 105.074 c 38.699
103.199 l 38.176 105.824 l 37.727 108.449 l 37.574 107.176 l 37.5 105.824
l 36 105.824 l 36 100.574 l 39.75 100.574 l 39.75 99.75 l 39.75 99.301
39.227 98.477 38.625 97.875 c 37.5 96.75 l 37.5 93.074 l 39.676 93.074 l
40.574 94.801 l 41.102 95.699 44.023 100.352 47.176 105.074 c 52.875 113.625
l 53.102 111.148 l 53.324 108.676 l 54.375 110.102 l 55.426 111.449 l 55.5
113.926 l 55.5 116.324 l 52.727 116.398 l 49.875 116.477 l 50.852 116.852
l 51.75 117.227 l 51.75 120.074 l 53.852 120.074 l 54.301 121.199 l 54.676
122.324 l 60 122.324 l 60 125.324 l 52.801 125.324 l h
46.727 116.176 m f
67.273 124.801 m 66.75 124.352 l 66.75 93.074 l 69.75 93.074 l 69.75 122.324
l 86.25 122.324 l 86.25 125.324 l 67.727 125.324 l h
67.273 124.801 m f
97.273 124.875 m 96.75 124.352 l 96.75 99.074 l 94.5 99.074 l 94.5 96.824
l 99.75 96.824 l 99.75 122.324 l 102 122.324 l 102 96.824 l 107.25 96.824
l 107.25 99.074 l 105 99.074 l 104.852 112.051 l 104.625 124.949 l 101.25
125.176 l 97.801 125.398 l h
97.273 124.875 m f
72 115.574 m 72 109.574 l 77.25 109.574 l 77.25 111.824 l 75 111.824 l
75 119.324 l 77.25 119.324 l 77.25 121.574 l 72 121.574 l h
72 115.574 m f
72 102.449 m 72 96.824 l 77.25 96.824 l 77.25 99.074 l 75 99.074 l 75 105.824
l 77.25 105.824 l 77.25 108.074 l 72 108.074 l h
72 102.449 m f
88.051 98.023 m 87.824 97.426 87.75 96.148 87.898 95.176 c 88.125 93.449
l 89.477 93.227 l 90.75 93 l 90.75 99.074 l 88.426 99.074 l h
88.051 98.023 m f
131.25 67.199 m 129.449 65.324 128.102 63.824 128.324 63.824 c 128.551
63.824 130.199 65.324 132 67.199 c 133.801 69.074 135.148 70.574 134.926
70.574 c 134.699 70.574 133.051 69.074 131.25 67.199 c h
131.25 67.199 m f
34.875 65.324 m 36.449 63.676 37.949 62.324 38.176 62.324 c 38.398 62.324
37.199 63.676 35.625 65.324 c 34.051 66.977 32.551 68.324 32.324 68.324
c 32.102 68.324 33.301 66.977 34.875 65.324 c h
34.875 65.324 m f
83.102 46.801 m 84 46.648 85.5 46.648 86.477 46.801 c 88.125 47.102 l 81.375
47.102 l h
83.102 46.801 m f
1 g
31.5 108.824 m 31.5 93.074 l 39.602 93.074 l 39.977 94.051 l 40.199 94.574
43.125 99.375 46.574 104.625 c 52.875 114.227 l 53.102 103.648 l 53.324
93.074 l 60 93.074 l 60 124.574 l 52.574 124.574 l 48.523 118.426 l 46.273
114.977 43.051 110.023 41.324 107.324 c 38.25 102.449 l 38.25 124.574 l
31.5 124.574 l h
31.5 108.824 m f
67.5 108.824 m 67.5 93.074 l 85.5 93.074 l 85.5 99.074 l 74.25 99.074 l
74.25 105.824 l 84.75 105.824 l 84.75 111.824 l 74.25 111.824 l 74.25 119.324
l 86.25 119.324 l 86.25 124.574 l 67.5 124.574 l h
67.5 108.824 m f
97.5 111.824 m 97.5 99.074 l 88.5 99.074 l 88.5 93.074 l 113.25 93.074
l 113.25 99.074 l 104.25 99.074 l 104.25 124.574 l 97.5 124.574 l h
97.5 111.824 m f
Q Q
showpage
%%Trailer
end
%%EOF

View File

@@ -0,0 +1,317 @@
%!PS-Adobe-3.0 EPSF-3.0
%%Creator: cairo 1.15.10 (http://cairographics.org)
%%CreationDate: Sat Jun 8 11:28:29 2024
%%Pages: 1
%%DocumentData: Clean7Bit
%%LanguageLevel: 2
%%BoundingBox: 0 0 149 134
%%EndComments
%%BeginProlog
50 dict begin
/q { gsave } bind def
/Q { grestore } bind def
/cm { 6 array astore concat } bind def
/w { setlinewidth } bind def
/J { setlinecap } bind def
/j { setlinejoin } bind def
/M { setmiterlimit } bind def
/d { setdash } bind def
/m { moveto } bind def
/l { lineto } bind def
/c { curveto } bind def
/h { closepath } bind def
/re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto
0 exch rlineto 0 rlineto closepath } bind def
/S { stroke } bind def
/f { fill } bind def
/f* { eofill } bind def
/n { newpath } bind def
/W { clip } bind def
/W* { eoclip } bind def
/BT { } bind def
/ET { } bind def
/BDC { mark 3 1 roll /BDC pdfmark } bind def
/EMC { mark /EMC pdfmark } bind def
/cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def
/Tj { show currentpoint cairo_store_point } bind def
/TJ {
{
dup
type /stringtype eq
{ show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse
} forall
currentpoint cairo_store_point
} bind def
/cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore
cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def
/Tf { pop /cairo_font exch def /cairo_font_matrix where
{ pop cairo_selectfont } if } bind def
/Td { matrix translate cairo_font_matrix matrix concatmatrix dup
/cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point
/cairo_font where { pop cairo_selectfont } if } bind def
/Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def
cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def
/g { setgray } bind def
/rg { setrgbcolor } bind def
/d1 { setcachedevice } bind def
/cairo_data_source {
CairoDataIndex CairoData length lt
{ CairoData CairoDataIndex get /CairoDataIndex CairoDataIndex 1 add def }
{ () } ifelse
} def
/cairo_flush_ascii85_file { cairo_ascii85_file status { cairo_ascii85_file flushfile } if } def
/cairo_image { image cairo_flush_ascii85_file } def
/cairo_imagemask { imagemask cairo_flush_ascii85_file } def
%%EndProlog
%%BeginSetup
%%EndSetup
%%Page: 1 1
%%BeginPageSetup
%%PageBoundingBox: 0 0 149 134
%%EndPageSetup
q 0 0 149 134 rectclip
1 0 0 -1 0 134 cm q
0.317647 0.168627 0.831373 rg
10.82 132.262 m 3.246 129.863 -1.703 121.086 0.547 113.738 c 1.07 111.938
5.496 103.613 10.371 95.137 c 32.047 57.711 33.098 55.762 32.047 55.539
c 30.695 55.238 30.695 53.137 32.047 53.137 c 32.645 53.137 33.32 52.238
33.621 51.113 c 34.223 48.562 36.02 45.637 36.996 45.637 c 37.371 45.637
37.52 46.164 37.297 46.762 c 36.996 47.363 36.996 47.887 37.223 47.887
c 38.047 47.887 45.32 34.387 44.871 33.637 c 44.645 33.262 45.02 32.887
45.77 32.887 c 46.82 32.887 47.57 34.238 48.848 38.363 c 51.621 47.211 51.695
46.988 40.297 66.637 c 16.145 108.336 11.496 116.586 11.496 117.789 c 11.496
118.461 12.395 119.738 13.445 120.562 c 15.395 122.137 15.922 122.137 74.645
122.137 c 133.82 122.137 l 135.621 120.262 l 136.672 119.289 137.496 118.086
137.496 117.562 c 137.496 116.738 128.195 100.461 109.598 68.664 c 105.172
61.164 103.746 58.086 103.746 55.988 c 103.746 53.137 l 107.871 53.137
l 110.121 53.137 111.996 52.836 111.996 52.387 c 111.996 52.012 112.746
51.637 113.645 51.637 c 114.547 51.637 115.07 51.863 114.77 52.086 c 114.098
52.762 114.621 53.664 132.098 83.887 c 140.645 98.512 147.996 112.086 148.445
113.961 c 150.547 121.914 145.223 130.539 137.047 132.562 c 131.871 133.914
15.098 133.613 10.82 132.262 c h
10.82 132.262 m f
77.496 55.012 m 77.496 52.539 77.645 52.387 80.121 52.387 c 82.598 52.387
82.746 52.539 82.746 55.012 c 82.746 57.488 82.598 57.637 80.121 57.637
c 77.645 57.637 77.496 57.488 77.496 55.012 c h
77.496 55.012 m f
56.27 20.887 m 54.621 15.113 54.547 12.113 56.047 13.312 c 56.871 14.062
57.547 13.461 59.496 10.086 c 61.598 6.336 67.145 0.637 68.645 0.637 c
69.621 0.637 69.32 3.863 67.82 8.586 c 67.07 10.988 65.645 15.789 64.672
19.164 c 62.871 25.387 l 60.172 25.387 l 57.547 25.387 57.473 25.312 56.27
20.887 c h
56.27 20.887 m f
0.0117647 0.6 0.8 rg
48.996 55.914 m 48.996 54.938 48.473 53.062 47.871 51.863 c 46.973 49.988
46.895 49.164 47.57 47.664 c 48.246 46.086 48.172 45.113 47.195 42.562
c 46.445 40.762 45.621 37.914 45.32 36.113 c 44.797 33.336 44.871 32.887
45.922 32.887 c 46.598 32.887 49.297 30.789 51.848 28.164 c 56.57 23.438
l 55.371 20.211 l 54.098 16.613 53.871 11.887 54.922 11.887 c 55.82 11.887
58.445 17.512 59.121 20.738 c 59.422 22.012 59.871 23.137 60.098 23.137
c 60.395 23.137 61.445 20.363 62.348 16.914 c 63.32 13.539 64.895 8.586
65.723 5.887 c 67.371 1.086 67.445 1.012 70.672 0.414 c 74.645 -0.336 80.496
0.336 80.496 1.613 c 80.496 2.137 79.145 6.711 77.496 11.887 c 75.77 17.062
71.945 29.062 68.871 38.586 c 65.797 48.039 63.246 56.211 63.246 56.738
c 63.246 57.336 61.145 57.637 56.121 57.637 c 49.145 57.637 48.996 57.637
48.996 55.914 c h
48.996 55.914 m f
14.348 45.113 m 10.895 34.762 l 16.52 29.062 l 21.695 23.738 21.996 23.289
21.246 21.336 c 20.195 18.414 20.27 14.887 21.473 14.887 c 22.07 14.887
23.195 17.211 24.246 20.512 c 25.223 23.586 26.195 25.914 26.496 25.762
c 26.797 25.539 27.621 23.289 28.297 20.738 c 29.047 18.113 30.172 13.988
30.922 11.512 c 31.672 9.039 32.723 5.586 33.172 3.863 c 33.77 1.613 34.445
0.637 35.348 0.637 c 36.395 0.637 37.598 3.336 40.973 13.688 c 45.32 27.039
46.371 32.512 44.121 30.637 c 43.145 29.887 42.848 30.113 41.871 32.363
c 41.27 33.863 40.746 35.512 40.746 36.039 c 40.746 36.562 39.996 38.887
39.172 41.137 c 38.27 43.387 37.52 46.086 37.445 47.137 c 37.371 48.188
36.848 50.438 36.246 52.238 c 35.27 55.387 l 17.723 55.387 l h
14.348 45.113 m f
8.348 26.887 m 7.145 23.137 l 8.871 23.137 l 10.672 23.137 11.496 24.789
11.496 28.613 c 11.496 31.988 9.621 30.938 8.348 26.887 c h
8.348 26.887 m f
17.645 8.961 m 15.32 1.387 17.871 -0.488 20.27 7.012 c 21.848 11.887 l
20.195 11.887 l 18.848 11.887 18.32 11.211 17.645 8.961 c h
17.645 8.961 m f
0.952941 0.423529 0.133333 rg
66.996 55.012 m 66.996 52.387 l 77.195 52.387 l 78.77 47.363 l 79.672 44.512
81.32 38.961 82.52 34.988 c 83.723 30.711 84.996 27.637 85.598 27.637 c
86.121 27.637 86.496 26.664 86.496 25.312 c 86.496 22.988 87.172 20.062
90.848 7.539 c 92.57 1.539 93.02 0.637 94.445 0.637 c 95.871 0.637 96.246
1.238 96.922 4.238 c 97.297 6.188 98.348 9.336 99.172 11.289 c 101.723
17.289 101.57 17.812 95.945 24.562 c 90.77 30.711 l 92.797 38.211 l 93.922
42.262 95.27 47.062 95.871 48.711 c 96.473 50.438 96.996 52.988 96.996
54.336 c 96.996 56.738 96.848 56.887 94.52 56.887 c 92.121 56.887 l 89.945
49.238 l 88.746 44.961 87.547 41.289 87.246 40.914 c 87.02 40.613 86.348
42.113 85.82 44.289 c 85.223 46.461 84.172 50.363 83.348 52.988 c 81.922
57.637 l 66.996 57.637 l h
66.996 55.012 m f
111.246 53.887 m 111.246 52.914 111.77 52.387 112.598 52.387 c 114.395
52.387 114.996 52.988 114.473 54.336 c 113.871 55.988 111.246 55.613 111.246
53.887 c h
111.246 53.887 m f
120.246 27.262 m 120.246 25.461 122.195 17.512 124.371 10.387 c 124.973
8.512 125.797 5.586 126.246 3.863 c 127.297 -0.336 129.695 -0.711 130.445
3.262 c 130.672 4.688 131.871 8.586 132.996 11.961 c 135.172 18.039 l 130.52
23.586 l 126.246 28.688 125.645 29.137 123.098 29.137 c 120.547 29.137
120.246 28.914 120.246 27.262 c h
120.246 27.262 m f
0.454902 0.333333 0.866667 rg
132.246 123.039 m 132.246 122.363 132.996 121.387 133.895 121.012 c 134.723
120.562 135.922 119.586 136.445 118.762 c 137.348 117.414 136.297 115.312
126.621 98.512 c 115.82 79.762 l 96.172 79.387 l 76.598 79.012 l 70.672
68.887 l 66.172 61.164 64.746 58.086 64.746 55.988 c 64.746 53.137 l 77.348
53.137 l 79.223 47.289 l 80.27 44.137 81.848 38.887 82.82 35.664 c 84.395
30.262 85.746 28.539 86.797 30.863 c 87.172 31.539 87.32 31.539 87.695
30.863 c 88.746 28.461 90.172 30.188 91.672 35.664 c 92.57 38.887 94.07
44.062 95.047 47.137 c 96.848 52.762 l 102.473 52.988 l 106.598 53.137 108.246
53.512 108.547 54.262 c 108.848 55.012 108.473 55.387 107.496 55.387 c
106.672 55.387 105.996 55.613 105.996 55.988 c 105.996 56.289 108.621 60.938
111.848 66.414 c 131.422 99.863 140.047 115.012 140.348 116.512 c 140.723
118.613 138.695 122.586 136.82 123.637 c 134.496 124.836 132.246 124.539
132.246 123.039 c h
78.098 54.863 m 76.52 54.711 73.82 54.711 72.098 54.863 c 70.297 55.012
71.57 55.164 74.871 55.164 c 78.172 55.164 79.598 55.012 78.098 54.863
c h
78.098 54.863 m f
10.223 121.836 m 8.047 119.137 7.973 116.887 9.996 116.887 c 11.57 116.887
12.547 119.289 12.098 122.211 c 11.871 124.012 11.871 124.012 10.223 121.836
c h
10.223 121.836 m f
10.746 112.012 m 10.746 111.488 21.172 93.188 26.57 84.262 c 28.52 80.961
34.371 70.914 39.547 61.914 c 48.246 46.688 48.922 45.789 49.371 47.887
c 49.672 49.164 50.422 50.438 51.02 50.664 c 51.77 50.961 51.996 51.711
51.695 53.211 c 51.395 54.938 51.621 55.312 52.973 55.613 c 53.871 55.762
53.195 55.988 51.473 56.062 c 49.746 56.062 48.246 55.762 48.098 55.312
c 47.945 54.789 45.473 58.461 42.695 63.336 c 39.848 68.289 37.297 72.414
36.996 72.637 c 36.77 72.863 36.098 74.137 35.57 75.488 c 34.973 76.762
34.297 77.887 33.996 77.887 c 33.695 77.887 33.172 78.711 32.945 79.762
c 32.645 80.812 32.121 81.637 31.746 81.637 c 31.297 81.637 30.996 82.164
30.996 82.762 c 30.996 83.363 30.473 83.887 29.871 83.887 c 29.195 83.887
28.746 84.562 28.746 85.688 c 28.746 86.738 28.297 88.012 27.695 88.539
c 27.172 89.137 26.422 90.336 26.195 91.238 c 25.895 92.137 25.145 92.887
24.547 92.887 c 23.945 92.887 23.496 93.562 23.496 94.312 c 23.496 96.562
22.672 98.137 21.547 98.137 c 20.945 98.137 20.496 98.812 20.496 99.562
c 20.496 101.812 19.672 103.387 18.547 103.387 c 17.945 103.387 17.496
104.137 17.496 105.262 c 17.496 106.613 17.121 107.137 15.996 107.137 c
14.797 107.137 14.496 107.664 14.496 109.762 c 14.496 112.086 14.27 112.387
12.621 112.387 c 11.57 112.387 10.746 112.238 10.746 112.012 c h
10.746 112.012 m f
16.973 52.688 m 16.746 52.086 16.895 51.188 17.422 50.664 c 18.02 50.062
18.246 50.363 18.246 51.863 c 18.246 54.113 17.723 54.488 16.973 52.688
c h
16.973 52.688 m f
45.547 36.262 m 45.621 34.988 46.07 34.387 46.973 34.387 c 48.695 34.387
48.621 36.414 46.82 37.312 c 45.621 37.988 45.395 37.836 45.547 36.262
c h
45.547 36.262 m f
56.195 21.789 m 54.848 16.836 54.621 13.387 55.746 13.387 c 56.195 13.387
56.496 13.914 56.496 14.512 c 56.496 15.113 56.797 15.637 57.246 15.637
c 57.621 15.637 57.77 16.164 57.547 16.762 c 57.246 17.363 57.473 17.887
57.996 17.887 c 58.52 17.887 58.746 18.414 58.52 19.012 c 58.223 19.613
58.371 20.289 58.82 20.512 c 59.195 20.812 59.348 21.336 59.121 21.711
c 58.82 22.086 59.121 22.613 59.797 22.836 c 60.77 23.211 60.695 23.438
59.645 24.711 c 58.973 25.461 58.223 26.137 57.922 26.137 c 57.695 26.137
56.871 24.188 56.195 21.789 c h
56.195 21.789 m f
77.645 1.988 m 78.098 0.637 79.746 0.414 79.746 1.688 c 79.746 2.363 79.223
2.887 78.547 2.887 c 77.871 2.887 77.496 2.512 77.645 1.988 c h
77.645 1.988 m f
72.848 0.113 m 73.746 -0.039 75.246 -0.039 76.223 0.113 c 77.121 0.262
76.371 0.414 74.496 0.414 c 72.621 0.414 71.871 0.262 72.848 0.113 c h
72.848 0.113 m f
0.968627 0.576471 0.117647 rg
58.746 56.512 m 58.746 55.914 59.27 55.387 59.871 55.387 c 60.473 55.387
60.996 55.012 60.996 54.488 c 60.996 53.961 63.547 45.789 66.621 36.336
c 69.695 26.812 73.52 14.961 75.098 10.012 c 78.02 1.012 l 86.797 0.789
l 95.57 0.562 l 95.047 2.664 l 94.746 3.789 93.77 7.312 92.871 10.387 c
90.172 19.461 88.598 25.988 88.895 26.887 c 89.047 27.336 91.746 24.637
94.973 20.812 c 99.695 15.188 100.746 14.289 100.82 15.711 c 100.82 16.688
101.645 19.539 102.547 22.012 c 104.348 26.512 l 105.473 23.887 l 106.07
22.461 108.02 16.613 109.82 10.988 c 113.121 0.637 l 129.922 0.637 l 127.973
7.613 l 126.922 11.363 125.348 17.289 124.445 20.738 c 123.473 24.113 122.945
26.887 123.172 26.887 c 123.395 26.887 125.945 24.188 128.723 20.812 c
133.895 14.738 l 135.996 21.039 l 137.121 24.414 138.77 29.438 139.672 32.137
c 140.645 34.836 141.695 38.211 142.07 39.711 c 142.371 41.211 143.348
43.914 144.098 45.711 c 144.922 47.512 145.746 50.289 146.047 51.863 c 146.57
54.637 l 128.645 54.637 l 127.07 48.863 l 123.473 35.961 121.895 31.086
121.145 30.789 c 120.695 30.637 120.098 31.836 119.871 33.414 c 119.57
34.988 118.746 37.988 117.996 40.012 c 116.945 42.938 115.145 50.137 114.621
53.137 c 114.32 55.086 112.895 55.387 104.195 55.387 c 95.047 55.387 l
93.246 49.988 l 92.27 46.988 91.371 44.289 91.297 43.988 c 91.223 43.613
91.07 43.164 90.996 42.863 c 90.922 42.488 90.77 41.961 90.621 41.512 c
90.547 41.137 90.395 40.613 90.32 40.387 c 90.098 40.012 89.645 38.062
89.27 36.262 c 89.121 35.664 88.82 34.988 88.598 34.688 c 88.297 34.461
88.297 33.711 88.52 33.113 c 88.82 32.289 88.52 32.062 87.547 32.289 c 86.797
32.438 86.047 32.961 86.047 33.488 c 85.973 33.938 85.82 34.539 85.746
34.762 c 85.672 34.988 85.445 35.512 85.371 35.887 c 85.297 36.336 85.07
36.789 84.996 37.012 c 84.922 37.238 84.77 37.613 84.695 37.988 c 84.621
38.289 84.32 38.961 84.02 39.488 c 83.645 39.938 83.57 40.688 83.871 41.062
c 84.098 41.438 83.945 41.961 83.496 42.262 c 83.047 42.562 82.895 43.086
83.121 43.461 c 83.422 43.836 83.348 44.586 82.973 45.113 c 82.672 45.562
82.371 46.164 82.297 46.387 c 82.297 46.613 81.848 47.738 81.395 48.863
c 80.871 49.988 80.27 51.789 80.047 52.762 c 79.598 54.488 79.223 54.637
74.945 54.637 c 69.172 54.637 66.77 55.238 67.297 56.586 c 67.598 57.414
66.695 57.637 63.246 57.637 c 59.723 57.637 58.746 57.414 58.746 56.512
c h
58.746 56.512 m f
0.160784 0.670588 0.886275 rg
6.02 19.539 m 2.871 9.711 0.246 1.461 0.246 1.164 c 0.246 0.863 4.223 0.637
9.172 0.637 c 18.02 0.637 l 19.973 6.863 l 21.02 10.238 22.672 15.637 23.57
18.863 c 25.223 24.711 l 18.922 31.086 l 15.473 34.539 12.473 37.387 12.246
37.387 c 12.02 37.387 9.246 29.363 6.02 19.539 c h
6.02 19.539 m f
45.246 34.238 m 44.645 32.438 43.973 30.711 43.672 30.262 c 43.145 29.512
33.996 2.062 33.996 1.164 c 33.996 0.863 38.121 0.637 43.223 0.637 c 52.445
0.637 l 54.246 6.488 l 55.145 9.637 56.797 15.039 57.848 18.488 c 59.797
24.637 l 53.496 31.012 l 49.973 34.539 46.973 37.387 46.672 37.387 c 46.445
37.387 45.77 35.961 45.246 34.238 c h
45.246 34.238 m f
0.584314 0.490196 0.894118 rg
12.02 123.562 m 10.52 122.664 8.496 118.988 8.496 117.113 c 8.496 116.363
12.996 107.961 18.547 98.512 c 36.547 67.539 48.172 47.664 48.547 47.289
c 48.996 46.836 49.371 47.664 50.496 52.238 c 51.32 55.387 l 58.973 55.387
l 66.621 55.461 l 72.695 66.113 l 78.77 76.762 l 98.348 76.988 l 117.848
77.137 l 128.797 96.188 l 140.57 116.664 141.172 118.164 137.945 121.914
c 136.145 124.012 l 115.07 124.238 l 94.52 124.461 93.996 124.387 93.996
122.961 c 93.996 122.137 94.598 121.312 95.348 121.086 c 96.172 120.789
97.145 119.812 97.672 118.914 c 98.496 117.336 97.672 115.539 87.77 98.438
c 76.895 79.762 l 56.797 79.539 l 34.52 79.312 30.246 79.836 30.246 82.836
c 30.246 85.312 29.496 86.887 28.297 86.887 c 27.695 86.887 27.246 87.637
27.246 88.688 c 27.246 89.738 26.797 91.012 26.195 91.539 c 25.672 92.137
24.922 93.336 24.695 94.238 c 24.395 95.137 23.645 95.887 23.047 95.887
c 22.445 95.887 21.996 96.562 21.996 97.312 c 21.996 99.562 21.172 101.137
20.047 101.137 c 19.445 101.137 18.996 101.887 18.996 102.938 c 18.996
103.988 18.547 105.262 17.945 105.789 c 17.422 106.387 16.672 107.586 16.445
108.488 c 16.145 109.387 15.395 110.137 14.797 110.137 c 14.195 110.137
13.746 110.887 13.746 111.938 c 13.746 112.988 13.223 114.262 12.621 114.938
c 10.895 116.664 11.27 118.613 13.746 120.262 c 17.57 122.812 15.996 125.887
12.02 123.562 c h
12.02 123.562 m f
0.72549 0.666667 0.933333 rg
11.348 122.211 m 7.297 118.613 7.746 117.188 19.223 97.387 c 24.848 87.711
30.246 79.164 31.07 78.414 c 32.57 77.211 34.672 77.137 55.895 77.289 c
79.145 77.512 l 90.02 96.188 l 100.973 115.164 101.945 117.414 99.996 120.938
c 98.348 124.164 96.547 124.387 75.246 124.387 c 52.895 124.387 52.895
124.387 57.32 119.961 c 58.598 118.688 59.422 117.414 59.27 117.113 c 57.02
113.062 39.32 84.414 37.973 82.613 c 35.27 79.012 34.672 79.238 29.645
85.762 c 29.348 86.211 28.598 87.637 28.07 88.988 c 27.547 90.262 26.797
91.387 26.422 91.387 c 26.047 91.387 25.746 91.836 25.746 92.438 c 25.746
93.039 25.297 94.012 24.77 94.539 c 24.246 95.062 23.348 96.637 22.82 97.988
c 22.297 99.262 21.547 100.387 21.246 100.387 c 20.945 100.387 20.422 101.211
20.195 102.188 c 19.973 103.238 19.297 104.512 18.77 105.039 c 18.32 105.562
17.047 107.664 15.996 109.688 c 15.02 111.711 13.746 113.738 13.297 114.262
c 12.32 115.238 12.02 116.363 12.172 118.836 c 12.246 119.961 12.922 120.562
14.496 120.938 c 16.07 121.238 16.746 121.762 16.746 122.887 c 16.746 125.062
14.195 124.762 11.348 122.211 c h
11.348 122.211 m f
0.862745 0.835294 0.964706 rg
12.848 123.188 m 10.445 121.914 9.246 119.961 9.246 117.336 c 9.246 115.312
27.621 82.762 30.473 79.613 c 32.422 77.438 37.672 77.211 39.473 79.238
c 40.07 79.914 45.395 88.312 51.172 97.762 c 62.945 116.961 63.395 118.312
59.57 122.211 c 57.395 124.387 l 36.098 124.387 l 18.848 124.312 14.496
124.086 12.848 123.188 c h
12.848 123.188 m f
Q Q
showpage
%%Trailer
end
%%EOF

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,96 @@
// Copyright © WireMock.Net
using System.Globalization;
using System.Net.Http.Headers;
using System.Text;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RestEase;
using WireMock.Client;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
/// <summary>
/// Some WireMock.Net extension methods for working with <see cref="DistributedApplication"/>.
/// Based on https://github.com/dotnet/aspire/blob/main/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs
/// </summary>
public static class DistributedApplicationExtensions
{
/// <summary>
/// Create a RestEase Admin client which can be used to call the admin REST endpoint.
/// </summary>
/// <param name="app">The <see cref="DistributedApplication"/>.</param>
/// <param name="resourceName">The resourceName of the resource.</param>
/// <param name="endpointName">The resourceName of the endpoint on the resource to communicate with.</param>
/// <returns>A <see cref="IWireMockAdminApi"/></returns>
public static IWireMockAdminApi CreateWireMockAdminClient(this DistributedApplication app, string resourceName, string? endpointName = default)
{
ThrowIfNotStarted(app);
var (resource, endpointUri) = GetResourceAndEndpointUri(app, resourceName);
var api = RestClient.For<IWireMockAdminApi>(endpointUri);
if (resource.Arguments.HasBasicAuthentication)
{
api.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{resource.Arguments.AdminUsername}:{resource.Arguments.AdminPassword}")));
}
return api;
}
private static (WireMockServerResource WireMockServerResource, string EndpointUri) GetResourceAndEndpointUri(IHost app, string resourceName, string? endpointName = default)
{
var wireMockServerResource = GetWireMockServerResource(app, resourceName);
EndpointReference? endpoint;
if (!string.IsNullOrEmpty(endpointName))
{
endpoint = GetEndpointOrDefault(wireMockServerResource, endpointName);
}
else
{
endpoint = GetEndpointOrDefault(wireMockServerResource, "http") ?? GetEndpointOrDefault(wireMockServerResource, "https");
}
if (endpoint is null)
{
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Endpoint '{0}' for resource '{1}' not found.", endpointName, resourceName), nameof(endpointName));
}
return (wireMockServerResource, endpoint.Url);
}
private static WireMockServerResource GetWireMockServerResource(IHost app, string resourceName)
{
var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var resource = applicationModel.Resources
.OfType<WireMockServerResource>()
.SingleOrDefault(r => string.Equals(r.Name, resourceName, StringComparison.OrdinalIgnoreCase));
if (resource is null)
{
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "WireMockServerResource with name '{0}' not found.", resourceName), nameof(resourceName));
}
return resource;
}
private static EndpointReference? GetEndpointOrDefault(IResourceWithEndpoints wireMockServerResource, string endpointName)
{
var reference = wireMockServerResource.GetEndpoint(endpointName);
return reference.IsAllocated ? reference : null;
}
private static void ThrowIfNotStarted(IHost app)
{
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
if (!lifetime.ApplicationStarted.IsCancellationRequested)
{
throw new InvalidOperationException("The application must be started before resolving endpoints or connection strings");
}
}
}

View File

@@ -0,0 +1,6 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WireMock.Net.Aspire.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
// Needed for Moq in the UnitTest project
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>0.0.1-preview-05</Version>
<ImplicitUsings>enable</ImplicitUsings>
<Description>Aspire extension to start a WireMock.Net server to stub an api.</Description>
<AssemblyTitle>WireMock.Net.Aspire</AssemblyTitle>
<Authors>Stef Heyenrath</Authors>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>WireMock.Net.Aspire</AssemblyName>
<PackageId>WireMock.Net.Aspire</PackageId>
<PackageTags>dotnet;aspire;wiremock;extension</PackageTags>
<ProjectGuid>{B6269AAC-170A-4346-8B9A-579DED3D9A12}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>WireMock.Net-LogoAspire.png</PackageIcon>
<ApplicationIcon>../../resources/WireMock.Net-LogoAspire.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="../../resources/WireMock.Net-Logo.png" />
<None Include="../../resources/WireMock.Net-LogoAspire.png" Pack="true" PackagePath="" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.RestClient\WireMock.Net.RestClient.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,112 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
using WireMock.Client.Builders;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
/// <summary>
/// Represents the arguments required to configure and start a WireMock.Net Server.
/// </summary>
public class WireMockServerArguments
{
internal const int HttpContainerPort = 80;
/// <summary>
/// The default HTTP port where WireMock.Net is listening.
/// </summary>
public const int DefaultPort = 9091;
private const string DefaultLogger = "WireMockConsoleLogger";
/// <summary>
/// The HTTP port where WireMock.Net is listening.
/// If not defined, .NET Aspire automatically assigns a random port.
/// </summary>
public int? HttpPort { get; set; }
/// <summary>
/// The admin username.
/// </summary>
[MemberNotNullWhen(true, nameof(HasBasicAuthentication))]
public string? AdminUsername { get; set; }
/// <summary>
/// The admin password.
/// </summary>
[MemberNotNullWhen(true, nameof(HasBasicAuthentication))]
public string? AdminPassword { get; set; }
/// <summary>
/// Defines if the static mappings should be read at startup.
///
/// Default value is <c>false</c>.
/// </summary>
public bool ReadStaticMappings { get; set; }
/// <summary>
/// Watch the static mapping files + folder for changes when running.
///
/// Default value is <c>false</c>.
/// </summary>
public bool WithWatchStaticMappings { get; set; }
/// <summary>
/// Specifies the path for the (static) mapping json files.
/// </summary>
public string? MappingsPath { get; set; }
/// <summary>
/// Indicates whether the admin interface has Basic Authentication.
/// </summary>
public bool HasBasicAuthentication => !string.IsNullOrEmpty(AdminUsername) && !string.IsNullOrEmpty(AdminPassword);
/// <summary>
/// Optional delegate that will be invoked to configure the WireMock.Net resource using the <see cref="AdminApiMappingBuilder"/>.
/// </summary>
public Func<AdminApiMappingBuilder, Task>? ApiMappingBuilder { get; set; }
/// <summary>
/// Converts the current instance's properties to an array of command-line arguments for starting the WireMock.Net server.
/// </summary>
/// <returns>An array of strings representing the command-line arguments.</returns>
public string[] GetArgs()
{
var args = new Dictionary<string, string>();
Add(args, "--WireMockLogger", DefaultLogger);
if (HasBasicAuthentication)
{
Add(args, "--AdminUserName", AdminUsername!);
Add(args, "--AdminPassword", AdminPassword!);
}
if (ReadStaticMappings)
{
Add(args, "--ReadStaticMappings", "true");
}
if (WithWatchStaticMappings)
{
Add(args, "--ReadStaticMappings", "true");
Add(args, "--WatchStaticMappings", "true");
Add(args, "--WatchStaticMappingsInSubdirectories", "true");
}
return args
.SelectMany(k => new[] { k.Key, k.Value })
.ToArray();
}
private static void Add(IDictionary<string, string> args, string argument, string value)
{
args[argument] = value;
}
private static void Add(IDictionary<string, string> args, string argument, Func<string> action)
{
args[argument] = action();
}
}

View File

@@ -0,0 +1,162 @@
// Copyright © WireMock.Net
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Net.Aspire;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
/// <summary>
/// Provides extension methods for adding WireMock.Net Server resources to the application model.
/// </summary>
public static class WireMockServerBuilderExtensions
{
// Linux only (https://github.com/dotnet/aspire/issues/854)
private const string DefaultLinuxImage = "sheyenrath/wiremock.net-alpine";
private const string DefaultLinuxMappingsPath = "/app/__admin/mappings";
/// <summary>
/// Adds a WireMock.Net Server resource to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="port">The HTTP port for the WireMock Server.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, int? port = null)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue);
return builder.AddWireMock(name, callback =>
{
callback.HttpPort = port;
});
}
/// <summary>
/// Adds a WireMock.Net Server resource to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="arguments">The arguments to start the WireMock.Net Server.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, WireMockServerArguments arguments)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(arguments);
var wireMockContainerResource = new WireMockServerResource(name, arguments);
var resourceBuilder = builder
.AddResource(wireMockContainerResource)
.WithImage(DefaultLinuxImage)
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
.WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort);
if (!string.IsNullOrEmpty(arguments.MappingsPath))
{
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
}
resourceBuilder = resourceBuilder.WithArgs(ctx =>
{
foreach (var arg in arguments.GetArgs())
{
ctx.Args.Add(arg);
}
});
return resourceBuilder;
}
/// <summary>
/// Adds a WireMock.Net Server resource to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="callback">A callback that allows for setting the <see cref="WireMockServerArguments"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, Action<WireMockServerArguments> callback)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(callback);
var arguments = new WireMockServerArguments();
callback(arguments);
return builder.AddWireMock(name, arguments);
}
/// <summary>
/// Defines if the static mappings should be read at startup.
///
/// Default set to <c>false</c>.
/// </summary>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithReadStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.ReadStaticMappings = true;
return wiremock;
}
/// <summary>
/// Watch the static mapping files + folder for changes when running.
///
/// Default set to <c>false</c>.
/// </summary>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithWatchStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.WithWatchStaticMappings = true;
return wiremock;
}
/// <summary>
/// Specifies the path for the (static) mapping json files.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="mappingsPath">The local path.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithMappingsPath(this IResourceBuilder<WireMockServerResource> wiremock, string mappingsPath)
{
return Guard.NotNull(wiremock)
.WithBindMount(Guard.NotNullOrWhiteSpace(mappingsPath), DefaultLinuxMappingsPath);
}
/// <summary>
/// Set the admin username and password for accessing the admin interface from WireMock.Net via HTTP.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="username">The admin username.</param>
/// <param name="password">The admin password.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithAdminUserNameAndPassword(this IResourceBuilder<WireMockServerResource> wiremock, string username, string password)
{
Guard.NotNull(wiremock);
wiremock.Resource.Arguments.AdminUsername = Guard.NotNull(username);
wiremock.Resource.Arguments.AdminPassword = Guard.NotNull(password);
return wiremock;
}
/// <summary>
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
/// <returns></returns>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, Task> configure)
{
Guard.NotNull(wiremock);
wiremock.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
return wiremock;
}
}

View File

@@ -0,0 +1,52 @@
// Copyright © WireMock.Net
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
using RestEase;
using WireMock.Client;
using WireMock.Client.Extensions;
namespace WireMock.Net.Aspire;
internal class WireMockServerLifecycleHook(ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook
{
public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var wireMockServerResources = appModel.Resources
.OfType<WireMockServerResource>()
.Where(resource => resource.Arguments.ApiMappingBuilder is not null)
.ToArray();
if (wireMockServerResources.Length == 0)
{
return;
}
foreach (var wireMockServerResource in wireMockServerResources)
{
var endpoint = wireMockServerResource.GetEndpoint();
if (endpoint.IsAllocated)
{
var adminApi = CreateWireMockAdminApi(wireMockServerResource);
var logger = loggerService.GetLogger(wireMockServerResource);
logger.LogInformation("Checking Health status from WireMock.Net");
await adminApi.WaitForHealthAsync(cancellationToken: cancellationToken);
logger.LogInformation("Calling ApiMappingBuilder to add mappings to WireMock.Net");
var mappingBuilder = adminApi.GetMappingBuilder();
await wireMockServerResource.Arguments.ApiMappingBuilder!.Invoke(mappingBuilder);
}
}
}
private static IWireMockAdminApi CreateWireMockAdminApi(WireMockServerResource resource)
{
var adminApi = RestClient.For<IWireMockAdminApi>(resource.GetEndpoint().Url);
return resource.Arguments.HasBasicAuthentication ?
adminApi.WithAuthorization(resource.Arguments.AdminUsername!, resource.Arguments.AdminPassword!) :
adminApi;
}
}

View File

@@ -0,0 +1,33 @@
// Copyright © WireMock.Net
using Stef.Validation;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting.ApplicationModel;
/// <summary>
/// A resource that represents a WireMock.Net Server.
/// </summary>
public class WireMockServerResource : ContainerResource, IResourceWithServiceDiscovery
{
internal WireMockServerArguments Arguments { get; }
/// <summary>
/// Initializes a new instance of the <see cref="WireMockServerResource"/> class.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="arguments">The arguments to start the WireMock.Net Server.</param>
public WireMockServerResource(string name, WireMockServerArguments arguments) : base(name)
{
Arguments = Guard.NotNull(arguments);
}
/// <summary>
/// Gets an endpoint reference.
/// </summary>
/// <returns>An <see cref="EndpointReference"/> object representing the endpoint reference.</returns>
public EndpointReference GetEndpoint()
{
return new EndpointReference(this, "http");
}
}

View File

@@ -2,8 +2,6 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using DotNet.Testcontainers.Containers;
using JetBrains.Annotations;
using RestEase;
@@ -103,4 +101,4 @@ public sealed class WireMockContainer : DockerContainer
}
private Uri GetPublicUri() => new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(ContainerPort)).Uri;
}
}

View File

@@ -0,0 +1,16 @@
using WireMock.Net.Aspire.TestAppHost;
var builder = DistributedApplication.CreateBuilder(args);
var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings");
builder
.AddWireMock("wiremock-service")
.WithAdminUserNameAndPassword($"user-{Guid.NewGuid()}", $"pwd-{Guid.NewGuid()}")
.WithMappingsPath(mappingsPath)
.WithWatchStaticMappings()
.WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
await builder
.Build()
.RunAsync();

View File

@@ -0,0 +1,36 @@
using WireMock.Client.Builders;
namespace WireMock.Net.Aspire.TestAppHost;
internal class WeatherForecastApiMock
{
public static async Task BuildAsync(AdminApiMappingBuilder builder)
{
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
builder.Given(b => b
.WithRequest(request => request
.UsingGet()
.WithPath("/weatherforecast2")
)
.WithResponse(response => response
.WithHeaders(h => h.Add("Content-Type", "application/json"))
.WithBodyAsJson(() => Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray())
)
);
await builder.BuildAndPostAsync();
}
}
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<ItemGroup>
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="WireMockMappings\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
{
"Guid": "173d495f-940e-4b86-a1f4-4f0fc7be8b8b",
"Request": {
"Path": "/weatherforecast",
"Methods": [
"get"
]
},
"Response": {
"BodyAsJson": [
{
"date": "2024-05-24",
"temperatureC": -10,
"summary": "Freezing"
},
{
"date": "2024-05-25",
"temperatureC": 33,
"summary": "Hot"
}
],
"Headers": {
"Content-Type": "application/json"
}
}
}

View File

@@ -0,0 +1,62 @@
// Copyright © WireMock.Net
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace WireMock.Net.Aspire.Tests;
[ExcludeFromCodeCoverage]
internal static class DockerUtils
{
public static bool IsDockerRunningLinuxContainerMode()
{
return IsDockerRunning() && IsLinuxContainerMode();
}
private static bool IsDockerRunning()
{
try
{
var processInfo = new ProcessStartInfo("docker", "info")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
var process = Process.Start(processInfo);
process?.WaitForExit();
return process?.ExitCode == 0;
}
catch (Exception ex)
{
Console.WriteLine($"Error checking Docker status: {ex.Message}");
return false;
}
}
private static bool IsLinuxContainerMode()
{
try
{
var processInfo = new ProcessStartInfo("docker", "version --format '{{.Server.Os}}'")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
var process = Process.Start(processInfo);
var output = process?.StandardOutput.ReadToEnd();
process?.WaitForExit();
return output?.Contains("linux", StringComparison.OrdinalIgnoreCase) == true;
}
catch (Exception ex)
{
Console.WriteLine($"Error checking Docker container mode: {ex.Message}");
return false;
}
}
}

View File

@@ -0,0 +1,75 @@
// Copyright © WireMock.Net
using System.Net.Http.Json;
using FluentAssertions;
using Projects;
using Xunit.Abstractions;
namespace WireMock.Net.Aspire.Tests;
public class IntegrationTests(ITestOutputHelper output)
{
private record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);
[Fact]
public async Task StartAppHostWithWireMockAndCreateHttpClientToCallTheMockedWeatherForecastEndpoint()
{
if (!DockerUtils.IsDockerRunningLinuxContainerMode())
{
output.WriteLine("Docker is not running in Linux container mode. Skipping test.");
return;
}
// Arrange
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
await using var app = await appHostBuilder.BuildAsync();
await app.StartAsync();
using var httpClient = app.CreateHttpClient("wiremock-service");
// Act 1
var weatherForecasts1 = await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast");
// Assert 1
weatherForecasts1.Should().BeEquivalentTo(new[]
{
new WeatherForecast(new DateOnly(2024, 5, 24), -10, "Freezing"),
new WeatherForecast(new DateOnly(2024, 5, 25), +33, "Hot")
});
// Act 2
var weatherForecasts2 = await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast2");
// Assert 2
weatherForecasts2.Should().HaveCount(5);
}
[Fact]
public async Task StartAppHostWithWireMockAndCreateWireMockAdminClientToCallTheAdminEndpoint()
{
if (!DockerUtils.IsDockerRunningLinuxContainerMode())
{
output.WriteLine("Docker is not running in Linux container mode. Skipping test.");
return;
}
// Arrange
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
await using var app = await appHostBuilder.BuildAsync();
await app.StartAsync();
var adminClient = app.CreateWireMockAdminClient("wiremock-service");
// Act 1
var settings = await adminClient.GetSettingsAsync();
// Assert 1
settings.Should().NotBeNull();
// Act 2
var mappings = await adminClient.GetMappingsAsync();
// Assert 2
mappings.Should().HaveCount(2);
}
}

View File

@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="8.0.0" />
<PackageReference Include="Codecov" Version="1.13.0" />
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\WireMock.Net.Aspire.TestAppHost\WireMock.Net.Aspire.TestAppHost.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Aspire.Hosting.Testing" />
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,134 @@
// Copyright © WireMock.Net
using FluentAssertions;
namespace WireMock.Net.Aspire.Tests;
public class WireMockServerArgumentsTests
{
[Fact]
public void DefaultValues_ShouldBeSetCorrectly()
{
// Arrange & Act
var args = new WireMockServerArguments();
// Assert
args.HttpPort.Should().BeNull();
args.AdminUsername.Should().BeNull();
args.AdminPassword.Should().BeNull();
args.ReadStaticMappings.Should().BeFalse();
args.WithWatchStaticMappings.Should().BeFalse();
args.MappingsPath.Should().BeNull();
}
[Fact]
public void HasBasicAuthentication_ShouldReturnTrue_WhenUsernameAndPasswordAreProvided()
{
// Arrange
var args = new WireMockServerArguments
{
AdminUsername = "admin",
AdminPassword = "password"
};
// Act & Assert
args.HasBasicAuthentication.Should().BeTrue();
}
[Fact]
public void HasBasicAuthentication_ShouldReturnFalse_WhenEitherUsernameOrPasswordIsNotProvided()
{
// Arrange
var argsWithUsernameOnly = new WireMockServerArguments { AdminUsername = "admin" };
var argsWithPasswordOnly = new WireMockServerArguments { AdminPassword = "password" };
// Act & Assert
argsWithUsernameOnly.HasBasicAuthentication.Should().BeFalse();
argsWithPasswordOnly.HasBasicAuthentication.Should().BeFalse();
}
[Fact]
public void GetArgs_WhenReadStaticMappingsIsTrue_ShouldContainReadStaticMappingsTrue()
{
// Arrange
var args = new WireMockServerArguments
{
ReadStaticMappings = true
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().ContainInOrder("--ReadStaticMappings", "true");
}
[Fact]
public void GetArgs_WhenReadStaticMappingsIsFalse_ShouldNotContainReadStaticMappingsTrue()
{
// Arrange
var args = new WireMockServerArguments
{
ReadStaticMappings = false
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().NotContain("--ReadStaticMappings", "true");
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void GetArgs_WhenWithWatchStaticMappingsIsTrue_ShouldContainWatchStaticMappingsTrue(bool readStaticMappings)
{
// Arrange
var args = new WireMockServerArguments
{
WithWatchStaticMappings = true,
ReadStaticMappings = readStaticMappings
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().ContainInOrder("--ReadStaticMappings", "true", "--WatchStaticMappings", "true", "--WatchStaticMappingsInSubdirectories", "true");
}
[Fact]
public void GetArgs_WhenWithWatchStaticMappingsIsFalse_ShouldNotContainWatchStaticMappingsTrue()
{
// Arrange
var args = new WireMockServerArguments
{
WithWatchStaticMappings = false
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().NotContain("--WatchStaticMappings", "true").And.NotContain("--WatchStaticMappingsInSubdirectories", "true");
}
[Fact]
public void GetArgs_ShouldIncludeAuthenticationDetails_WhenAuthenticationIsRequired()
{
// Arrange
var args = new WireMockServerArguments
{
AdminUsername = "admin",
AdminPassword = "password"
};
// Act
var commandLineArgs = args.GetArgs();
// Assert
commandLineArgs.Should().Contain("--AdminUserName", "admin");
commandLineArgs.Should().Contain("--AdminPassword", "password");
}
}

View File

@@ -0,0 +1,96 @@
// Copyright © WireMock.Net
using System.Net.Sockets;
using FluentAssertions;
using Moq;
namespace WireMock.Net.Aspire.Tests;
public class WireMockServerBuilderExtensionsTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t")]
public void AddWireMock_WithNullOrWhiteSpaceName_ShouldThrowException(string? name)
{
// Arrange
var builder = Mock.Of<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock(name!, 12345);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException()
{
// Arrange
const int invalidPort = -1;
var builder = Mock.Of<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock("ValidName", invalidPort);
// Assert
act.Should().Throw<ArgumentOutOfRangeException>().WithMessage("Specified argument was out of the range of valid values. (Parameter 'port')");
}
[Fact]
public void AddWireMock()
{
// Arrange
var name = $"apiservice{Guid.NewGuid()}";
const int port = 12345;
const string username = "admin";
const string password = "test";
var builder = DistributedApplication.CreateBuilder();
// Act
var wiremock = builder
.AddWireMock(name, port)
.WithAdminUserNameAndPassword(username, password)
.WithReadStaticMappings();
// Assert
wiremock.Resource.Should().NotBeNull();
wiremock.Resource.Name.Should().Be(name);
wiremock.Resource.Arguments.Should().BeEquivalentTo(new WireMockServerArguments
{
AdminPassword = password,
AdminUsername = username,
ReadStaticMappings = true,
WithWatchStaticMappings = false,
MappingsPath = null,
HttpPort = port
});
wiremock.Resource.Annotations.Should().HaveCount(4);
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
{
Image = "sheyenrath/wiremock.net-alpine",
Registry = null,
Tag = "latest"
});
var endpointAnnotation = wiremock.Resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault();
endpointAnnotation.Should().BeEquivalentTo(new EndpointAnnotation(
protocol: ProtocolType.Tcp,
uriScheme: "http",
transport: null,
name: null,
port: port,
targetPort: 80,
isExternal: null,
isProxied: true
));
wiremock.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
wiremock.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
}
}

View File

@@ -13,7 +13,6 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<!--https://developercommunity.visualstudio.com/content/problem/26347/unit-tests-fail-with-fileloadexception-newtonsoftj-1.html-->
@@ -70,7 +69,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="System.Threading" Version="4.3.0" />
<PackageReference Include="RestEase" Version="1.5.7" />
@@ -108,7 +106,7 @@
<PackageReference Include="JsonConverter.System.Text.Json" Version="0.5.0" />
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
<PackageReference Include="Grpc.Net.Client" Version="2.59.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.60.0" />
<PackageReference Include="Grpc.Tools" Version="2.60.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -132,10 +130,10 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="cert.pem">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Grpc\greet.proto">
<GrpcServices>Client</GrpcServices>
<GrpcServices>Client</GrpcServices>
</None>
<None Update="responsebody.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

View File

@@ -931,7 +931,7 @@ public class WireMockServerProxyTests
//Arrange
var wireMockServerSettings = new WireMockServerSettings
{
Urls = new[] { "http://localhost:9091" },
Urls = new[] { "http://localhost:19091" },
ProxyAndRecordSettings = new ProxyAndRecordSettings
{
Url = "http://postman-echo.com",
@@ -949,13 +949,13 @@ public class WireMockServerProxyTests
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("http://localhost:9091/post"),
RequestUri = new Uri("http://localhost:19091/post"),
Content = new StringContent(requestBody)
};
var request2 = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("http://localhost:9091/post"),
RequestUri = new Uri("http://localhost:19091/post"),
Content = new StringContent(requestBody)
};
server.ResetMappings();