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>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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.Authentication.JwtBearer" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
using OnlineShop.ApiService;
|
using OnlineShop.ApiService;
|
||||||
|
using OnlineShop.ServiceDefaults.Dtos;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -23,9 +26,121 @@ builder.Services.AddAuthentication(options =>
|
|||||||
.AddJwtBearer()
|
.AddJwtBearer()
|
||||||
.ConfigureApiJwt();
|
.ConfigureApiJwt();
|
||||||
|
|
||||||
|
builder.AddSqlServerClient("sqldb");
|
||||||
|
|
||||||
var app = builder.Build();
|
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();
|
app.UseExceptionHandler();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
@@ -33,23 +148,36 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.MapOpenApi();
|
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) =>
|
||||||
app.MapGet("/weatherforecast", () =>
|
|
||||||
{
|
{
|
||||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
connection.Open();
|
||||||
new WeatherForecast
|
|
||||||
(
|
var command = new SqlCommand(@"
|
||||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
USE Shop;
|
||||||
Random.Shared.Next(-20, 55),
|
SELECT
|
||||||
summaries[Random.Shared.Next(summaries.Length)]
|
Title,
|
||||||
))
|
Summary,
|
||||||
.ToArray();
|
Price
|
||||||
return forecast;
|
FROM Products", connection);
|
||||||
})
|
var products = new List<ProductDto>();
|
||||||
.WithName("GetWeatherForecast");
|
|
||||||
|
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();
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,22 @@ var idp = builder.AddKeycloakContainer(
|
|||||||
|
|
||||||
var cache = builder.AddRedis("cache");
|
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")
|
var apiService = builder.AddProject<Projects.OnlineShop_ApiService>("apiservice")
|
||||||
.WithHttpHealthCheck("/health")
|
.WithHttpHealthCheck("/health")
|
||||||
.WithReference(idp)
|
.WithReference(idp)
|
||||||
.WaitFor(idp);
|
.WaitFor(idp)
|
||||||
|
.WaitFor(sqldb)
|
||||||
|
.WithReference(sqldb);
|
||||||
|
|
||||||
var webFrontend = builder
|
var webFrontend = builder
|
||||||
.AddProject<Projects.OnlineShop_Web>("webfrontend")
|
.AddProject<Projects.OnlineShop_Web>("webfrontend")
|
||||||
.WithExternalHttpEndpoints()
|
.WithExternalHttpEndpoints()
|
||||||
.WithHttpHealthCheck("/health")
|
.WithHttpHealthCheck("/health")
|
||||||
.WithReference(apiService)
|
.WithReference(apiService)
|
||||||
|
.WaitFor(apiService)
|
||||||
.WithReference(idp, env: "Identity__ClientSecret")
|
.WithReference(idp, env: "Identity__ClientSecret")
|
||||||
.WaitFor(idp)
|
.WaitFor(idp)
|
||||||
.WithReference(cache)
|
.WithReference(cache)
|
||||||
|
|||||||
@@ -10,14 +10,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Aspire.Hosting.Redis" Version="13.0.1" />
|
<PackageReference Include="Aspire.Hosting.Redis" Version="13.0.1" />
|
||||||
|
<PackageReference Include="Aspire.Hosting.SqlServer" Version="13.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OnlineShop.ApiService\OnlineShop.ApiService.csproj" />
|
<ProjectReference Include="..\OnlineShop.ApiService\OnlineShop.ApiService.csproj" />
|
||||||
<ProjectReference Include="..\OnlineShop.Web\OnlineShop.Web.csproj" />
|
<ProjectReference Include="..\OnlineShop.Web\OnlineShop.Web.csproj" />
|
||||||
<ProjectReference
|
<ProjectReference Include="..\OnlineShop.MailDev.Hosting\OnlineShop.MailDev.Hosting.csproj" IsAspireProjectResource="false" />
|
||||||
Include="..\OnlineShop.MailDev.Hosting\OnlineShop.MailDev.Hosting.csproj"
|
|
||||||
IsAspireProjectResource="false" />
|
|
||||||
</ItemGroup>
|
</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
|
<span class="bi bi-house-door-fill" aria-hidden="true"></span> Home
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</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>
|
</nav>
|
||||||
</div>
|
</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 "/"
|
@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(
|
builder.Services.AddHttpClient(
|
||||||
"OidcBackchannel", o => o.BaseAddress = new("http://idp"));
|
"OidcBackchannel", o => o.BaseAddress = new("http://idp"));
|
||||||
|
|
||||||
builder.Services.AddHttpClient<WeatherApiClient>(client =>
|
builder.Services.AddHttpClient<ProductsApiClient>(client =>
|
||||||
{
|
{
|
||||||
client.BaseAddress = new("https+http://apiservice");
|
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