Add SQL Server integration example

This commit is contained in:
fiodarsazanavets
2026-01-19 21:16:12 +00:00
parent 08f3632722
commit 3dfa5a8c4c
13 changed files with 250 additions and 134 deletions
@@ -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);
}