Skip to content

Update to ASP.NET Core 7 #257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/src/TodoApp/bin/Debug/net6.0/TodoApp.dll",
"program": "${workspaceFolder}/src/TodoApp/bin/Debug/net7.0/TodoApp.dll",
"args": [],
"cwd": "${workspaceFolder}/src/TodoApp",
"stopAtEntry": false,
Expand Down
2 changes: 1 addition & 1 deletion .vsconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"components": [
"Microsoft.VisualStudio.Component.CoreEditor",
"Microsoft.VisualStudio.Workload.CoreEditor",
"Microsoft.NetCore.Component.Runtime.6.0",
"Microsoft.NetCore.Component.Runtime.7.0",
"Microsoft.NetCore.Component.SDK",
"Microsoft.VisualStudio.Component.Roslyn.Compiler",
"Microsoft.VisualStudio.Component.Roslyn.LanguageServices",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ with [User Secrets] instead.

Compiling the application yourself requires Git and the
[.NET SDK](https://www.microsoft.com/net/download/core "Download the .NET SDK")
to be installed (version `6.0.100` or later).
to be installed (version `7.0.100` or later).

To build and test the application locally from a terminal/command-line, run the
following set of commands:
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "6.0.402",
"version": "7.0.100",
"allowPrerelease": false,
"rollForward": "latestMajor"
}
Expand Down
176 changes: 99 additions & 77 deletions src/TodoApp/ApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Martin Costello, 2021. All rights reserved.
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.

using System.Security.Claims;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using TodoApp.Data;
Expand Down Expand Up @@ -62,84 +62,87 @@ public static IServiceCollection AddTodoApi(this IServiceCollection services)
/// </returns>
public static IEndpointRouteBuilder MapTodoApiRoutes(this IEndpointRouteBuilder builder)
{
// Get all Todo items
builder.MapGet("/api/items", async (
ITodoService service,
ClaimsPrincipal user,
CancellationToken cancellationToken) =>
{
return await service.GetListAsync(user.GetUserId(), cancellationToken);
})
.RequireAuthorization();

// Get a specific Todo item
builder.MapGet("/api/items/{id}", async (
Guid id,
ClaimsPrincipal user,
ITodoService service,
CancellationToken cancellationToken) =>
{
var model = await service.GetAsync(user.GetUserId(), id, cancellationToken);
return model is null ? Results.Problem("Item not found.", statusCode: StatusCodes.Status404NotFound) : Results.Json(model);
})
.Produces<TodoItemModel>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound)
.RequireAuthorization();

// Create a new Todo item
builder.MapPost("/api/items", async (
CreateTodoItemModel model,
ClaimsPrincipal user,
ITodoService service,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(model.Text))
var group = builder.MapGroup("/api/items")
.RequireAuthorization();
{
group.MapGet("/", async (
[AsParameters] TodoRequestContext context,
CancellationToken cancellationToken) =>
{
return Results.Problem("No item text specified.", statusCode: StatusCodes.Status400BadRequest);
}

var id = await service.AddItemAsync(user.GetUserId(), model.Text, cancellationToken);

return Results.Created($"/api/items/{id}", new { id });
})
.Produces(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest)
.RequireAuthorization();

// Mark a Todo item as completed
builder.MapPost("/api/items/{id}/complete", async (
Guid id,
ClaimsPrincipal user,
ITodoService service,
CancellationToken cancellationToken) =>
{
var wasCompleted = await service.CompleteItemAsync(user.GetUserId(), id, cancellationToken);

return wasCompleted switch
return await context.Service.GetListAsync(context.User, cancellationToken);
})
.WithSummary("Get all Todo items")
.WithDescription("Gets all of the current user's todo items.");

group.MapGet("/{id}", async Task<Results<Ok<TodoItemModel>, ProblemHttpResult>> (
Guid id,
[AsParameters] TodoRequestContext context,
CancellationToken cancellationToken) =>
{
true => Results.NoContent(),
false => Results.Problem("Item already completed.", statusCode: StatusCodes.Status400BadRequest),
_ => Results.Problem("Item not found.", statusCode: StatusCodes.Status404NotFound),
};
})
.Produces(StatusCodes.Status204NoContent)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status404NotFound)
.RequireAuthorization();

// Delete a Todo item
builder.MapDelete("/api/items/{id}", async (
Guid id,
ClaimsPrincipal user,
ITodoService service,
CancellationToken cancellationToken) =>
{
var wasDeleted = await service.DeleteItemAsync(user.GetUserId(), id, cancellationToken);
return wasDeleted ? Results.NoContent() : Results.Problem("Item not found.", statusCode: StatusCodes.Status404NotFound);
})
.Produces(StatusCodes.Status204NoContent)
.ProducesProblem(StatusCodes.Status404NotFound)
.RequireAuthorization();
var model = await context.Service.GetAsync(context.User, id, cancellationToken);
return model switch
{
null => TypedResults.Problem("Item not found.", statusCode: StatusCodes.Status404NotFound),
_ => TypedResults.Ok(model),
};
})
.ProducesProblem(StatusCodes.Status404NotFound)
.WithSummary("Get a specific Todo item")
.WithDescription("Gets the todo item with the specified ID.");

group.MapPost("/", async Task<Results<Created<CreatedTodoItemModel>, ProblemHttpResult>> (
CreateTodoItemModel model,
[AsParameters] TodoRequestContext context,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(model.Text))
{
return TypedResults.Problem("No item text specified.", statusCode: StatusCodes.Status400BadRequest);
}

var id = await context.Service.AddItemAsync(context.User, model.Text, cancellationToken);

return TypedResults.Created($"/api/items/{id}", new CreatedTodoItemModel() { Id = id });
})
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Create a new Todo item")
.WithDescription("Creates a new todo item for the current user and returns its ID.");

group.MapPost("/{id}/complete", async Task<Results<NoContent, ProblemHttpResult>> (
Guid id,
[AsParameters] TodoRequestContext context,
CancellationToken cancellationToken) =>
{
var wasCompleted = await context.Service.CompleteItemAsync(context.User, id, cancellationToken);

return wasCompleted switch
{
true => TypedResults.NoContent(),
false => TypedResults.Problem("Item already completed.", statusCode: StatusCodes.Status400BadRequest),
_ => TypedResults.Problem("Item not found.", statusCode: StatusCodes.Status404NotFound),
};
})
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status404NotFound)
.WithSummary("Mark a Todo item as completed")
.WithDescription("Marks the todo item with the specified ID as complete.");

group.MapDelete("/{id}", async Task<Results<NoContent, ProblemHttpResult>> (
Guid id,
[AsParameters] TodoRequestContext context,
CancellationToken cancellationToken) =>
{
var wasDeleted = await context.Service.DeleteItemAsync(context.User, id, cancellationToken);
return wasDeleted switch
{
true => TypedResults.NoContent(),
false => TypedResults.Problem("Item not found.", statusCode: StatusCodes.Status404NotFound),
};
})
.ProducesProblem(StatusCodes.Status404NotFound)
.WithSummary("Delete a Todo item")
.WithDescription("Deletes the todo item with the specified ID.");
};

// Redirect to Open API/Swagger documentation
builder.MapGet("/api", () => Results.Redirect("/swagger-ui/index.html"))
Expand All @@ -148,4 +151,23 @@ public static IEndpointRouteBuilder MapTodoApiRoutes(this IEndpointRouteBuilder

return builder;
}

private record struct TodoRequestContext(TodoUser User, ITodoService Service);

private readonly struct TodoUser
{
private TodoUser(string id)
{
Id = id;
}

public string Id { get; }

public static implicit operator string(TodoUser value) => value.Id;

public static ValueTask<TodoUser> BindAsync(HttpContext context)
{
return ValueTask.FromResult(new TodoUser(context.User.GetUserId()));
}
}
}
6 changes: 3 additions & 3 deletions src/TodoApp/AuthenticationEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static class AuthenticationEndpoints
public static IServiceCollection AddGitHubAuthentication(this IServiceCollection services)
{
return services
.AddAuthentication(options => options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = SignInPath;
Expand All @@ -44,8 +44,8 @@ public static IServiceCollection AddGitHubAuthentication(this IServiceCollection
{
options.AccessDeniedPath = DeniedPath;
options.CallbackPath = SignInPath + "-github";
options.ClientId = configuration["GitHub:ClientId"];
options.ClientSecret = configuration["GitHub:ClientSecret"];
options.ClientId = configuration["GitHub:ClientId"] ?? string.Empty;
options.ClientSecret = configuration["GitHub:ClientSecret"] ?? string.Empty;
options.EnterpriseDomain = configuration["GitHub:EnterpriseDomain"];

options.Scope.Add("user:email");
Expand Down
9 changes: 9 additions & 0 deletions src/TodoApp/Models/CreatedTodoItemModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Martin Costello, 2021. All rights reserved.
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.

namespace TodoApp.Models;

public class CreatedTodoItemModel
{
public string Id { get; set; } = string.Empty;
}
6 changes: 3 additions & 3 deletions src/TodoApp/TodoApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CA1050</NoWarn>
<RootNamespace>TodoApp</RootNamespace>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<TypeScriptToolsVersion>latest</TypeScriptToolsVersion>
<UserSecretsId>TodoApp</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="6.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.10" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.8.4" PrivateAssets="all" />
<PackageReference Include="NodaTime" Version="3.1.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
Expand Down
24 changes: 24 additions & 0 deletions startvs.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@ECHO OFF
SETLOCAL

:: This command launches a Visual Studio solution with environment variables required to use a local version of the .NET SDK.

:: This tells .NET to use the same dotnet.exe that the build script uses.
SET DOTNET_ROOT=%~dp0.dotnetcli
SET DOTNET_ROOT(x86)=%~dp0.dotnetcli\x86

:: Put our local dotnet.exe on PATH first so Visual Studio knows which one to use.
SET PATH=%DOTNET_ROOT%;%PATH%

SET sln=%~dp0TodoApp.sln

IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" (
echo The .NET SDK has not yet been installed. Run `%~dp0build.ps1` to install it
exit /b 1
)

IF "%VSINSTALLDIR%" == "" (
start "" "%sln%"
) else (
"%VSINSTALLDIR%\Common7\IDE\devenv.com" "%sln%"
)
29 changes: 29 additions & 0 deletions startvscode.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@ECHO OFF
SETLOCAL

:: This command launches Visual Studio Code with environment variables required to use a local version of the .NET SDK.

:: This tells .NET to use the same dotnet.exe that the build script uses.
SET DOTNET_ROOT=%~dp0.dotnetcli
SET DOTNET_ROOT(x86)=%~dp0.dotnetcli\x86

:: Put our local dotnet.exe on PATH first so Visual Studio Code knows which one to use.
SET PATH=%DOTNET_ROOT%;%PATH%

:: Sets the Target Framework for Visual Studio Code.
SET TARGET=net7.0

SET FOLDER=%~1

IF NOT EXIST "%DOTNET_ROOT%\dotnet.exe" (
echo The .NET SDK has not yet been installed. Run `%~dp0build.ps1` to install it
exit /b 1
)

IF "%FOLDER%"=="" (
code .
) else (
code "%FOLDER%"
)

exit /b 1
4 changes: 2 additions & 2 deletions tests/TodoApp.Tests/RemoteAuthorizationEventsFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ public RemoteAuthorizationEventsFilter(IHttpClientFactory httpClientFactory)

private IHttpClientFactory HttpClientFactory { get; }

public void PostConfigure(string name, GitHubAuthenticationOptions options)
public void PostConfigure(string? name, GitHubAuthenticationOptions options)
{
// Use HttpClientFactory for HTTP requests so that the tests
// can intercept the request and return canned responses.
options.Backchannel = HttpClientFactory.CreateClient(name);
options.Backchannel = HttpClientFactory.CreateClient(name ?? string.Empty);

// Configure the GitHub provider to redirect back to the
// test application, rather than GitHub's own login pages.
Expand Down
4 changes: 2 additions & 2 deletions tests/TodoApp.Tests/TodoApp.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
<PropertyGroup>
<IsPackable>false</IsPackable>
<RootNamespace>TodoApp</RootNamespace>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="coverlet.msbuild" Version="3.2.0" PrivateAssets="All" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" />
<PackageReference Include="JustEat.HttpClientInterception" Version="3.1.2" />
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.Playwright" Version="1.27.2" />
<PackageReference Include="ReportGenerator" Version="5.1.10" />
Expand Down
8 changes: 4 additions & 4 deletions tests/TodoApp.Tests/TodoAppFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
// Also override the default options for the GitHub OAuth provider
var config = new[]
{
KeyValuePair.Create("DataDirectory", dataDirectory),
KeyValuePair.Create("GitHub:ClientId", "github-id"),
KeyValuePair.Create("GitHub:ClientSecret", "github-secret"),
KeyValuePair.Create("GitHub:EnterpriseDomain", string.Empty)
KeyValuePair.Create<string, string?>("DataDirectory", dataDirectory),
KeyValuePair.Create<string, string?>("GitHub:ClientId", "github-id"),
KeyValuePair.Create<string, string?>("GitHub:ClientSecret", "github-secret"),
KeyValuePair.Create<string, string?>("GitHub:EnterpriseDomain", string.Empty)
};

configBuilder.AddInMemoryCollection(config);
Expand Down