Skip to content

Commit

Permalink
Merge pull request #137 from dotnet/main
Browse files Browse the repository at this point in the history
✅ Merge `main` into `live`
  • Loading branch information
IEvangelist authored Dec 7, 2023
2 parents d5d1241 + a43e721 commit a746eb8
Show file tree
Hide file tree
Showing 32 changed files with 935 additions and 4 deletions.
Binary file added docs/database/media/app-home-screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 120 additions & 0 deletions docs/database/quickstart-sql-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: Connect an ASP.NET Core app to SQL Server using .NET Aspire and Entity Framework Core
description: Learn how to connect an ASP.NET Core app to .NET Aspire storage components.
ms.date: 12/01/2023
ms.topic: tutorial
---

# Tutorial: Connect an ASP.NET Core app to SQL Server using .NET Aspire and Entity Framework Core

In this tutorial, you create an ASP.NET Core app that uses a .NET Aspire Entity Framework Core SQL Server component to connect to SQL Server to read and write support ticket data. [Entity Framework Core](/ef/core/) is a lightweight, extensible, open source object-relational mapper that enables .NET developers to work with databases using .NET objects. You'll learn how to:

> [!div class="checklist"]
>
> - Create a basic .NET app that is set up to use .NET Aspire components
> - Add a .NET Aspire component to connect to SQL Server
> - Configure and use .NET Aspire Component features to read and write from the database
[!INCLUDE [aspire-prereqs](../includes/aspire-prereqs.md)]

## Create the sample solution

1. At the top of Visual Studio, navigate to **File** > **New** > **Project**.
1. In the dialog window, search for *Blazor* and select **Blazor Web App**. Choose **Next**.
1. On the **Configure your new project** screen:
- Enter a **Project Name** of **AspireSQLEFCore**.
- Leave the rest of the values at their defaults and select **Next**.
1. On the **Additional information** screen:
- Make sure **.NET 8.0** is selected.
- Ensure the **Interactive render mode** is set to **None**.
- Check the **Enlist in .NET Aspire orchestration** option and select **Create**.

Visual Studio creates a new ASP.NET Core solution that is structured to use .NET Aspire. The solution consists of the following projects:

- **AspireSQLEFCore**: A Blazor project that depends on service defaults.
- **AspireSQLEFCore.AppHost**: An orchestrator project designed to connect and configure the different projects and services of your app. The orchestrator should be set as the startup project.
- **AspireSQLEFCore.ServiceDefaults**: A shared class library to hold configurations that can be reused across the projects in your solution.

## Create the database model and context classes

To represent a user submitted support request, add the following `SupportTicket` model class at the root of the _AspireSQLEFCore_ project.

:::code source="snippets/tutorial/AspireSQLEFCore/AspireSQLEFCore/SupportTicket.cs":::

Add the following `TicketDbContext` data context class at the root of the **AspireSQLEFCore** project. The class inherits <xref:System.Data.Entity.DbContext?displayProperty=fullName> to work with Entity Framework and represent your database.

:::code source="snippets/tutorial/AspireSQLEFCore/AspireSQLEFCore/TicketContext.cs":::

## Add the .NET Aspire component to the Blazor app

Add the [.NET Aspire Entity Framework Core Sql Server library](/dotnet/aspire/database/sql-server-entity-framework-component?tabs=dotnet-cli) package to your _AspireSQLEFCore_ project:

```dotnetcli
dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer --prerelease
```

Your _AspireSQLEFCore_ project is now set up to use .NET Aspire components. Here's the updated _AspireSQLEFCore.csproj_ file:

:::code language="csharp" source="snippets/tutorial/AspireSQLEFCore/AspireSQLEFCore/AspireSQLEFCore.csproj" highlight="10, 11":::

## Configure the .NET Aspire component

In the _Program.cs_ file of the _AspireSQLEFCore_ project, add a call to the <xref:Microsoft.Extensions.Hosting.AspireSqlServerEFCoreSqlClientExtensions.AddSqlServerDbContext%2A> extension method after the creation of the `builder` but before the call to `AddServiceDefaults`. For more information, see [.NET Aspire service defaults](../service-defaults.md). Provide the name of your connection string as a parameter.

:::code language="csharp" source="snippets/tutorial/AspireSQLEFCore/AspireSQLEFCore/Program.cs" range="1-14" highlight="5":::

This method accomplishes the following tasks:

- Registers a `TicketDbContext` with the DI container for connecting to the containerized Azure SQL Database.
- Automatically enable corresponding health checks, logging, and telemetry.

## Migrate and seed the database

While developing locally, you need to create a database inside the SQL Server container. Update the _Program.cs_ file with the following code to automatically run Entity Framework migrations during startup.

:::code language="csharp" source="snippets/tutorial/AspireSQLEFCore/AspireSQLEFCore/Program.cs" range="1-30" highlight="16-30":::

## Create the form

The app requires a form for the user to be able to submit support ticket information and save the entry to the database.

Use the following Razor markup to create a basic form, replacing the contents of the _Home.razor_ file in the _AspireSQLEFCore/Components/Pages_ directory:

:::code language="razor" source="snippets/tutorial/AspireSQLEFCore/AspireSQLEFCore/Components/Pages/Home.razor":::

For more information about creating forms in Blazor, see [ASP.NET Core Blazor forms overview](/aspnet/core/blazor/forms).

## Configure the AppHost

The _AspireSQLEFCore.AppHost_ project is the orchestrator for your app. It's responsible for connecting and configuring the different projects and services of your app. The orchestrator should be set as the startup project.

Add the [.NET Aspire Entity Framework Core Sql Server library](/dotnet/aspire/database/sql-server-entity-framework-component?tabs=dotnet-cli) package to your _AspireStorage.AppHost_ project:

```dotnetcli
dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer --prerelease
```

Replace the contents of the _Program.cs_ file in the _AspireSQLEFCore.AppHost_ project with the following code:

:::code language="csharp" source="snippets/tutorial/AspireSQLEFCore/AspireSQLEFCore.AppHost/Program.cs":::

The preceding code adds a SQL Server Container resource to your app and configures a connection to a database called `sqldata`. The Entity Framework classes you configured earlier will automatically use this connection when migrating and connecting to the database. The `sqlpassword` variable represents the password for the default database user in the SQL Server container.

Set a `sqlpassword` key in the [user secrets](/aspnet/core/security/app-secrets?view=aspnetcore-8.0&tabs=windows) store of the _AspireSQLEFCore.AppHost_ project using the `dotnet user-secrets` command in the AppHost project directory. Passwords must meet the [Password Policy](/sql/relational-databases/security/password-policy?view=sql-server-ver16#password-complexity) complexity requirements.

```dotnetcli
dotnet user-secrets set sqlpassword <password>
```

## Run and test the app locally

The sample app is now ready for testing. Verify that the submitted form data is persisted to the database by completing the following steps:

1. Select the run button at the top of Visual Studio (or <kbd>F5</kbd>) to launch your .NET Aspire app dashboard in the browser.
1. On the projects page, in the **AspireSQLEFCore** row, click the link in the **Endpoints** column to open the UI of your app.

:::image type="content" source="media/app-home-screen.png" lightbox="media/app-home-screen.png" alt-text="A screenshot showing the home page of the .NET Aspire support application.":::

1. Enter sample data into the `Title` and `Description` form fields.
1. Select the **Submit** button, and the form submits the support ticket for processing — and clears the form.
1. The data you submitted displays in the table at the bottom of the page when the page reloads.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>65600b1c-627d-4255-a706-bf7e21108831</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="8.0.0-preview.1.23557.2" />
<PackageReference Include="Aspire.Microsoft.Data.SqlClient"
Version="8.0.0-preview.1.23551.7" />
<PackageReference Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer"
Version="8.0.0-preview.1.23557.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AspireSQLEFCore\AspireSQLEFCore.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var builder = DistributedApplication.CreateBuilder(args);

var sqlpassword = builder.Configuration["sqlpassword"];

var sql = builder.AddSqlServerContainer("sql", sqlpassword).AddDatabase("sqldata");

builder.AddProject<Projects.AspireSQLEFCore>("aspiresql")
.WithReference(sql);

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15026",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16053"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<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.0.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0-preview.1.23557.2" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.7.0-alpha.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.7.0-alpha.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.6.0-beta.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.6.0-beta.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.6.0-beta.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.5.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

namespace Microsoft.Extensions.Hosting;

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.UseServiceDiscovery();
});

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.AddRuntimeInstrumentation()
.AddBuiltInMeters();
})
.WithTracing(tracing =>
{
if (builder.Environment.IsDevelopment())
{
// We want to view all traces in development
tracing.SetSampler(new AlwaysOnSampler());
}
tracing.AddAspNetCoreInstrumentation()
.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.Configure<OpenTelemetryLoggerOptions>(logging => logging.AddOtlpExporter());
builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter());
builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
}

// Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
// builder.Services.AddOpenTelemetry()
// .WithMetrics(metrics => metrics.AddPrometheusExporter());

// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.Exporter package)
// 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)
{
// Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package)
// app.MapPrometheusScrapingEndpoint();

// 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;
}

private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) =>
meterProviderBuilder.AddMeter(
"Microsoft.AspNetCore.Hosting",
"Microsoft.AspNetCore.Server.Kestrel",
"System.Net.Http");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34308.178
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireSQLEFCore", "AspireSQLEFCore\AspireSQLEFCore.csproj", "{27667A42-D629-4B1A-81EE-AE302F40679C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireSQLEFCore.AppHost", "AspireSQLEFCore.AppHost\AspireSQLEFCore.AppHost.csproj", "{0510325F-3687-49E2-B91D-474E6B12179D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspireSQLEFCore.ServiceDefaults", "AspireSQLEFCore.ServiceDefaults\AspireSQLEFCore.ServiceDefaults.csproj", "{ED32A3C9-E38B-4712-AD04-0547680EEE24}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{27667A42-D629-4B1A-81EE-AE302F40679C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27667A42-D629-4B1A-81EE-AE302F40679C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27667A42-D629-4B1A-81EE-AE302F40679C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27667A42-D629-4B1A-81EE-AE302F40679C}.Release|Any CPU.Build.0 = Release|Any CPU
{0510325F-3687-49E2-B91D-474E6B12179D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0510325F-3687-49E2-B91D-474E6B12179D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0510325F-3687-49E2-B91D-474E6B12179D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0510325F-3687-49E2-B91D-474E6B12179D}.Release|Any CPU.Build.0 = Release|Any CPU
{ED32A3C9-E38B-4712-AD04-0547680EEE24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED32A3C9-E38B-4712-AD04-0547680EEE24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED32A3C9-E38B-4712-AD04-0547680EEE24}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED32A3C9-E38B-4712-AD04-0547680EEE24}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E0576FB-7481-4D05-B9FA-B2B6DAE4DD1A}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer"
Version="8.0.0-preview.1.23557.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AspireSQLEFCore.ServiceDefaults\AspireSQLEFCore.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit a746eb8

Please sign in to comment.