mirror of
https://github.com/fiodarsazanavets/aspire-13-examples.git
synced 2026-06-20 12:23:14 +00:00
Add SQL Server integration example
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace OnlineShop.ApiService.Model;
|
||||
|
||||
public class Product
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
[MaxLength(100)]
|
||||
string Title { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(2100)]
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
|
||||
public decimal Price { get; set; }
|
||||
|
||||
public DateTime DateAdded { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
@@ -11,6 +11,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Microsoft.Data.SqlClient" Version="13.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using OnlineShop.ApiService;
|
||||
using OnlineShop.ServiceDefaults.Dtos;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -23,9 +26,121 @@ builder.Services.AddAuthentication(options =>
|
||||
.AddJwtBearer()
|
||||
.ConfigureApiJwt();
|
||||
|
||||
builder.AddSqlServerClient("sqldb");
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var connection = scope.ServiceProvider.GetRequiredService<SqlConnection>();
|
||||
connection.Open();
|
||||
|
||||
var createDbCommand = new SqlCommand(@"
|
||||
IF NOT EXISTS (SELECT *
|
||||
FROM sys.databases
|
||||
WHERE name = 'Shop')
|
||||
BEGIN
|
||||
CREATE DATABASE Shop;
|
||||
END;", connection);
|
||||
|
||||
createDbCommand.ExecuteNonQuery();
|
||||
|
||||
var createTableCommand = new SqlCommand(@"
|
||||
USE Shop;
|
||||
IF NOT EXISTS (SELECT *
|
||||
FROM sysobjects
|
||||
WHERE name='Products' and xtype='U')
|
||||
CREATE TABLE Products (
|
||||
Id INT PRIMARY KEY IDENTITY,
|
||||
Title VARCHAR(100) NOT NULL,
|
||||
Summary NVARCHAR(2100) NOT NULL,
|
||||
Price DECIMAL(18,2) NOT NULL,
|
||||
DateAdded DATE NOT NULL,
|
||||
)", connection);
|
||||
|
||||
createTableCommand.ExecuteNonQuery();
|
||||
|
||||
var checkDataCommand =
|
||||
new SqlCommand(
|
||||
"SELECT COUNT(*) FROM Products",
|
||||
connection);
|
||||
|
||||
var count = (int)checkDataCommand
|
||||
.ExecuteScalar();
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
var insertCommand =
|
||||
new SqlCommand("""
|
||||
INSERT INTO Products (Title, Summary, Price, DateAdded)
|
||||
VALUES
|
||||
(
|
||||
'Wireless Optical Mouse',
|
||||
N'Ergonomic wireless optical mouse with adjustable DPI and long battery life, suitable for everyday office and home use.',
|
||||
24.99,
|
||||
'2025-01-05'
|
||||
),
|
||||
(
|
||||
'Mechanical Gaming Keyboard',
|
||||
N'RGB backlit mechanical keyboard with blue switches, anti-ghosting keys, and durable aluminum frame.',
|
||||
129.99,
|
||||
'2025-01-06'
|
||||
),
|
||||
(
|
||||
'27-inch 4K Monitor',
|
||||
N'27-inch UHD 4K monitor with IPS panel, 3840x2160 resolution, HDR support, and ultra-thin bezels.',
|
||||
399.00,
|
||||
'2025-01-07'
|
||||
),
|
||||
(
|
||||
'USB-C Docking Station',
|
||||
N'Multi-port USB-C docking station with HDMI, DisplayPort, Ethernet, USB 3.0 ports, and 100W power delivery.',
|
||||
179.50,
|
||||
'2025-01-08'
|
||||
),
|
||||
(
|
||||
'External SSD 1TB',
|
||||
N'Portable 1TB external SSD with USB 3.2 Gen 2 support, delivering fast read/write speeds in a compact design.',
|
||||
149.99,
|
||||
'2025-01-09'
|
||||
),
|
||||
(
|
||||
'Noise-Cancelling Headphones',
|
||||
N'Over-ear wireless headphones with active noise cancellation, high-fidelity sound, and 30-hour battery life.',
|
||||
249.00,
|
||||
'2025-01-10'
|
||||
),
|
||||
(
|
||||
'Webcam Full HD 1080p',
|
||||
N'Full HD 1080p webcam with built-in microphone, autofocus, and low-light correction for video conferencing.',
|
||||
69.99,
|
||||
'2025-01-11'
|
||||
),
|
||||
(
|
||||
'Gaming Laptop Backpack',
|
||||
N'Water-resistant backpack designed for gaming laptops up to 17 inches, featuring padded compartments and USB charging port.',
|
||||
59.95,
|
||||
'2025-01-12'
|
||||
),
|
||||
(
|
||||
'Wi-Fi 6 Router',
|
||||
N'Dual-band Wi-Fi 6 router offering high-speed wireless connectivity, improved range, and support for multiple devices.',
|
||||
199.00,
|
||||
'2025-01-13'
|
||||
),
|
||||
(
|
||||
'Portable Laser Printer',
|
||||
N'Compact monochrome laser printer suitable for small offices, offering fast printing speeds and wireless connectivity.',
|
||||
289.99,
|
||||
'2025-01-14'
|
||||
);
|
||||
""",
|
||||
connection);
|
||||
|
||||
insertCommand.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
app.UseExceptionHandler();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
@@ -33,23 +148,36 @@ if (app.Environment.IsDevelopment())
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
|
||||
app.MapGet("/", () => "API service is running.");
|
||||
|
||||
app.MapGet("/", () => "API service is running. Navigate to /weatherforecast to see sample data.");
|
||||
app.MapGet("/products",
|
||||
([FromServices] SqlConnection connection) =>
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
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;
|
||||
})
|
||||
.WithName("GetWeatherForecast");
|
||||
var command = new SqlCommand(@"
|
||||
USE Shop;
|
||||
SELECT
|
||||
Title,
|
||||
Summary,
|
||||
Price
|
||||
FROM Products", connection);
|
||||
var products = new List<ProductDto>();
|
||||
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
products.Add(new ProductDto(
|
||||
Title: reader.GetString(0),
|
||||
Summary: reader.GetString(1),
|
||||
Price: reader.GetDecimal(2)
|
||||
));
|
||||
}
|
||||
|
||||
return products.ToArray();
|
||||
}
|
||||
});
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
|
||||
@@ -13,17 +13,22 @@ var idp = builder.AddKeycloakContainer(
|
||||
|
||||
var cache = builder.AddRedis("cache");
|
||||
|
||||
var sql = builder.AddSqlServer("sql").WithLifetime(ContainerLifetime.Persistent);
|
||||
var sqldb = sql.AddDatabase("sqldb");
|
||||
|
||||
var apiService = builder.AddProject<Projects.OnlineShop_ApiService>("apiservice")
|
||||
.WithHttpHealthCheck("/health")
|
||||
.WithReference(idp)
|
||||
.WaitFor(idp);
|
||||
|
||||
.WaitFor(idp)
|
||||
.WaitFor(sqldb)
|
||||
.WithReference(sqldb);
|
||||
|
||||
var webFrontend = builder
|
||||
.AddProject<Projects.OnlineShop_Web>("webfrontend")
|
||||
.WithExternalHttpEndpoints()
|
||||
.WithHttpHealthCheck("/health")
|
||||
.WithReference(apiService)
|
||||
.WaitFor(apiService)
|
||||
.WithReference(idp, env: "Identity__ClientSecret")
|
||||
.WaitFor(idp)
|
||||
.WithReference(cache)
|
||||
|
||||
@@ -10,14 +10,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.Redis" Version="13.0.1" />
|
||||
<PackageReference Include="Aspire.Hosting.SqlServer" Version="13.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OnlineShop.ApiService\OnlineShop.ApiService.csproj" />
|
||||
<ProjectReference Include="..\OnlineShop.Web\OnlineShop.Web.csproj" />
|
||||
<ProjectReference
|
||||
Include="..\OnlineShop.MailDev.Hosting\OnlineShop.MailDev.Hosting.csproj"
|
||||
IsAspireProjectResource="false" />
|
||||
<ProjectReference Include="..\OnlineShop.MailDev.Hosting\OnlineShop.MailDev.Hosting.csproj" IsAspireProjectResource="false" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace OnlineShop.ServiceDefaults.Dtos;
|
||||
|
||||
public record ProductDto(
|
||||
string Title,
|
||||
string Summary,
|
||||
decimal Price
|
||||
);
|
||||
@@ -13,17 +13,5 @@
|
||||
<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="counter">
|
||||
<span class="bi bi-plus-square-fill" aria-hidden="true"></span> Counter
|
||||
</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>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
@page "/counter"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,47 @@
|
||||
@page "/"
|
||||
@using OnlineShop.ServiceDefaults.Dtos
|
||||
@attribute [StreamRendering(true)]
|
||||
@attribute [OutputCache(Duration = 5)]
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
@inject ProductsApiClient ProductsApi
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
<PageTitle>Products</PageTitle>
|
||||
|
||||
<h1>Products</h1>
|
||||
|
||||
@if (products == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Price (USD)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var product in products)
|
||||
{
|
||||
<tr>
|
||||
<td>@product.Title</td>
|
||||
<td>@product.Price</td>
|
||||
<td>@product.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private ProductDto[]? products;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
products = await ProductsApi.GetProductsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
Welcome to your new app.
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
@page "/weather"
|
||||
@attribute [StreamRendering(true)]
|
||||
@attribute [OutputCache(Duration = 5)]
|
||||
|
||||
@inject WeatherApiClient WeatherApi
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates showing data loaded from a backend API service.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th aria-label="Temperature in Celsius">Temp. (C)</th>
|
||||
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
forecasts = await WeatherApi.GetWeatherAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using OnlineShop.ServiceDefaults.Dtos;
|
||||
|
||||
namespace OnlineShop.Web;
|
||||
|
||||
public class ProductsApiClient(HttpClient httpClient)
|
||||
{
|
||||
public async Task<ProductDto[]> GetProductsAsync(int maxItems = 10, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<ProductDto>? products = null;
|
||||
|
||||
await foreach (var product in httpClient.GetFromJsonAsAsyncEnumerable<ProductDto>("/products", cancellationToken))
|
||||
{
|
||||
if (products?.Count >= maxItems)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (product is not null)
|
||||
{
|
||||
products ??= [];
|
||||
products.Add(product);
|
||||
}
|
||||
}
|
||||
|
||||
return products?.ToArray() ?? [];
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ builder.Services.AddOutputCache();
|
||||
builder.Services.AddHttpClient(
|
||||
"OidcBackchannel", o => o.BaseAddress = new("http://idp"));
|
||||
|
||||
builder.Services.AddHttpClient<WeatherApiClient>(client =>
|
||||
builder.Services.AddHttpClient<ProductsApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new("https+http://apiservice");
|
||||
});
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
namespace OnlineShop.Web;
|
||||
|
||||
public class WeatherApiClient(HttpClient httpClient)
|
||||
{
|
||||
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 record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||
{
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
Reference in New Issue
Block a user