Skip to content

Commit 612b8db

Browse files
committed
Initial commit
Signed-off-by: kvmw <[email protected]>
0 parents  commit 612b8db

File tree

13 files changed

+346
-0
lines changed

13 files changed

+346
-0
lines changed

.editorconfig

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
root = true
2+
3+
[*.cs]
4+
# Indentation
5+
indent_style = space
6+
indent_size = 4
7+
8+
# New lines
9+
end_of_line = lf
10+
insert_final_newline = true
11+
charset = utf-8
12+
13+
# C# formatting
14+
csharp_new_line_before_open_brace = all
15+
csharp_new_line_before_else = true
16+
csharp_new_line_before_catch = true
17+
csharp_new_line_before_finally = true
18+
csharp_new_line_before_members_in_object_initializers = true
19+
csharp_new_line_before_members_in_anonymous_types = true
20+
21+
# Naming conventions
22+
dotnet_naming_rule.interface_should_be_prefixed_with_i.severity = warning
23+
dotnet_naming_rule.interface_should_be_prefixed_with_i.symbols = interface
24+
dotnet_naming_rule.interface_should_be_prefixed_with_i.style = prefix_interface_with_i
25+
26+
dotnet_naming_symbols.interface.applicable_kinds = interface
27+
dotnet_naming_style.prefix_interface_with_i.required_prefix = I
28+
29+
# Code style
30+
dotnet_style_qualification_for_field = false:suggestion
31+
dotnet_style_qualification_for_property = false:suggestion
32+
dotnet_style_qualification_for_method = false:suggestion
33+
dotnet_style_qualification_for_event = false:suggestion
34+
35+
# Expression-level preferences
36+
dotnet_style_object_initializer = true:suggestion
37+
dotnet_style_collection_initializer = true:suggestion
38+
dotnet_style_explicit_tuple_names = true:suggestion
39+
dotnet_style_null_propagation = true:suggestion
40+
dotnet_style_coalesce_expression = true:suggestion
41+
42+
[*.{json,yml,yaml}]
43+
indent_style = space
44+
indent_size = 2

.github/workflows/ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: ['main']
6+
pull_request:
7+
branches: ['main']
8+
9+
jobs:
10+
check:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Setup .NET
18+
uses: actions/setup-dotnet@v4
19+
with:
20+
dotnet-version: '8.0.x'
21+
22+
- name: Restore dependencies
23+
run: dotnet restore
24+
25+
- name: Check code formatting
26+
run: dotnet format --verify-no-changes
27+
28+
- name: Build
29+
run: dotnet build --no-restore --configuration Release
30+
31+
- name: Test
32+
run: dotnet test --no-build --verbosity normal
33+

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bin/
2+
obj/

Cook.csproj

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Description>cook project</Description>
6+
<Nullable>enable</Nullable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<NoWarn>SA1402;SA1600;SA0001;SA1633</NoWarn>
9+
</PropertyGroup>
10+
11+
<PropertyGroup>
12+
<SteeltoeVersion>4.0.*-*</SteeltoeVersion>
13+
</PropertyGroup>
14+
15+
<ItemGroup>
16+
<PackageReference Include="Steeltoe.Configuration.ConfigServer" Version="$(SteeltoeVersion)" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507">
21+
<PrivateAssets>all</PrivateAssets>
22+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
23+
</PackageReference>
24+
</ItemGroup>
25+
26+
</Project>

Cook.slnx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<Solution>
2+
<Project Path="cook.csproj" />
3+
</Solution>

LICENSE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
SOFTWARE LICENSE AGREEMENT
3+
4+
Copyright (c) VMware LLC. All rights reserved.
5+
6+
You are hereby granted a non-exclusive, worldwide, royalty-free license under VMware LLC’s copyrights to use, copy, modify, and distribute this software in source code or binary form for use in connection with VMware LLC products.
7+
8+
This copyright notice shall be included in all copies or substantial portions of the software.
9+
10+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Program.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Steeltoe.Configuration.ConfigServer;
2+
3+
var builder = WebApplication.CreateBuilder(args);
4+
5+
builder.AddConfigServer();
6+
builder.Services.AddHttpClient();
7+
builder.Services.AddScoped<Cook.ResourceService>();
8+
9+
var app = builder.Build();
10+
11+
app.UseHttpsRedirection();
12+
13+
app.MapGet("/restaurant", (IConfiguration config) =>
14+
{
15+
var special = config["cook:special"] ?? "none";
16+
return $"Today's special is: {special}";
17+
});
18+
19+
app.MapGet("/restaurant/secret-menu", (IConfiguration config) =>
20+
{
21+
var secretMenu = config["secretMenu"] ?? "none";
22+
return secretMenu;
23+
});
24+
25+
app.MapGet("/restaurant/dessert-menu", async (Cook.ResourceService resourceService) =>
26+
{
27+
try
28+
{
29+
return await resourceService.GetContentAsync("dessert.json");
30+
}
31+
catch (Exception ex)
32+
{
33+
return $"Error fetching resource: {ex.Message}";
34+
}
35+
});
36+
37+
app.Run();

Properties/launchSettings.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"launchUrl": "swagger",
9+
"applicationUrl": "http://localhost:8080",
10+
"environmentVariables": {
11+
"ASPNETCORE_ENVIRONMENT": "Development"
12+
}
13+
},
14+
"https": {
15+
"commandName": "Project",
16+
"dotnetRunMessages": true,
17+
"launchBrowser": true,
18+
"launchUrl": "swagger",
19+
"applicationUrl": "https://localhost:7053;http://localhost:8080",
20+
"environmentVariables": {
21+
"ASPNETCORE_ENVIRONMENT": "Development"
22+
}
23+
}
24+
}
25+
}

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Config Server .NET sample
2+
3+
![CI](https://github.com/spring-cloud-services-samples/cook-dotnet/actions/workflows/ci.yml/badge.svg)
4+
5+
Sample .NET application demonstrating the use of Config Server in Tanzu Platform for Cloud Foundry.
6+
7+
For information on the Config Server product in Tanzu Platform for Cloud Foundry, please [see the documentation](https://techdocs.broadcom.com/us/en/vmware-tanzu/spring/spring-cloud-services-for-cloud-foundry/3-3/scs-tanzu/config-server-index.html).
8+
9+
## Building and Deploying
10+
11+
- Create a Config Server instance:
12+
13+
```
14+
cf create-service p.config-server standard cook-config-server -c '{ "git": { "uri": "https://github.com/spring-cloud-services-samples/cook-config" } }'
15+
```
16+
17+
- Push the application:
18+
19+
```
20+
cf push
21+
```
22+
23+
## Trying It Out
24+
25+
Visit `[ROUTE]/restaurant`, where `[ROUTE]` is the route bound to the application. The "special" of the day will be taken from the configuration repository and the value of `cook.special`.
26+
27+
```
28+
$ curl https://cook.apps.example.cf-app.com/restaurant
29+
30+
Today's special is: Cake a la mode
31+
```

ResourceService.cs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
namespace Cook;
2+
3+
using System.Net.Http.Headers;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
public class ResourceService
8+
{
9+
private readonly IConfiguration configuration;
10+
private readonly HttpClient httpClient;
11+
private OAuth2TokenResponse? cachedToken;
12+
13+
public ResourceService(IConfiguration configuration, HttpClient httpClient)
14+
{
15+
this.configuration = configuration;
16+
this.httpClient = httpClient;
17+
}
18+
19+
public async Task<string> GetContentAsync(string name)
20+
{
21+
try
22+
{
23+
var configServerUri = this.configuration["spring:cloud:config:uri"];
24+
25+
var appName = this.configuration["spring:application:name"] ?? "application";
26+
var profile = this.configuration["spring:cloud:config:env"] ?? "default";
27+
var label = this.configuration["spring:cloud:config:label"] ?? "main";
28+
29+
var accessToken = await this.GetOrRefreshAccessTokenAsync();
30+
31+
var request = new HttpRequestMessage(HttpMethod.Get, $"{configServerUri}/{appName}/{profile}/{label}/{name}");
32+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
33+
34+
var response = await this.httpClient.SendAsync(request);
35+
if (response.IsSuccessStatusCode)
36+
{
37+
return await response.Content.ReadAsStringAsync();
38+
}
39+
else
40+
{
41+
throw new HttpRequestException($"Failed to fetch resource: {response.StatusCode} - {response.ReasonPhrase}");
42+
}
43+
}
44+
catch (Exception ex)
45+
{
46+
throw new InvalidOperationException($"Error fetching resource: {ex.Message}", ex);
47+
}
48+
}
49+
50+
private async Task<string> GetOrRefreshAccessTokenAsync()
51+
{
52+
if (this.cachedToken != null && DateTime.UtcNow < this.cachedToken.ExpirationTime)
53+
{
54+
return this.cachedToken.AccessToken;
55+
}
56+
57+
var clientId = this.configuration["spring:cloud:config:clientId"];
58+
var clientSecret = this.configuration["spring:cloud:config:clientSecret"];
59+
var accessTokenUri = this.configuration["spring:cloud:config:accessTokenUri"];
60+
61+
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret) || string.IsNullOrEmpty(accessTokenUri))
62+
{
63+
throw new InvalidOperationException("OAuth2 credentials not configured");
64+
}
65+
66+
var tokenRequest = new FormUrlEncodedContent(new[]
67+
{
68+
new KeyValuePair<string, string>("grant_type", "client_credentials"),
69+
new KeyValuePair<string, string>("client_id", clientId),
70+
new KeyValuePair<string, string>("client_secret", clientSecret),
71+
});
72+
73+
var tokenResponse = await this.httpClient.PostAsync(accessTokenUri, tokenRequest);
74+
if (!tokenResponse.IsSuccessStatusCode)
75+
{
76+
throw new HttpRequestException($"Failed to get access token: {tokenResponse.StatusCode} - {tokenResponse.ReasonPhrase}");
77+
}
78+
79+
this.cachedToken = this.ExtractTokenData(await tokenResponse.Content.ReadAsStringAsync());
80+
81+
return this.cachedToken.AccessToken;
82+
}
83+
84+
private OAuth2TokenResponse ExtractTokenData(string tokenResponse)
85+
{
86+
return JsonSerializer.Deserialize<OAuth2TokenResponse>(tokenResponse) ?? new OAuth2TokenResponse();
87+
}
88+
}
89+
90+
public class OAuth2TokenResponse
91+
{
92+
[JsonPropertyName("access_token")]
93+
public string AccessToken { get; set; } = string.Empty;
94+
95+
[JsonPropertyName("expires_in")]
96+
public int ExpiresIn { get; set; } = 3600;
97+
98+
public DateTime ExpirationTime => DateTime.UtcNow.AddSeconds(this.ExpiresIn - 10); // Subtract 10 seconds for safety
99+
}

0 commit comments

Comments
 (0)