Last active
February 16, 2022 01:43
-
-
Save DamianEdwards/4aea2fd9600ef402dc805aaa4dd98438 to your computer and use it in GitHub Desktop.
Potential ASP.NET Core Minimal APIs use of C# Discriminated Unions (DU)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Ref: https://github.com/cston/csharplang/blob/DiscriminatedUnions/proposals/tagged-unions.md | |
using System.ComponentModel.DataAnnotations; | |
using Microsoft.EntityFrameworkCore; | |
using MiniValidation; | |
var builder = WebApplication.CreateBuilder(args); | |
var connectionString = builder.Configuration.GetConnectionString("TodoDb") ?? "Data Source=todos.db"; | |
builder.Services.AddSqlite<TodoDb>(connectionString); | |
builder.Services.AddEndpointsApiExplorer(); | |
builder.Services.AddSwaggerGen(); | |
var app = builder.Build(); | |
app.MapSwagger(); | |
app.UseSwaggerUI(); | |
// Example of exsting anonymous delegate return type inferance in C# 10: Task<List<Todo>> | |
app.MapGet("/todos", async (TodoDb db) => | |
await db.Todos.ToListAsync()) | |
.WithName("GetAllTodos"); | |
// Example of potential multiple return types of anonymous delegate inferred as anonymous DU by compiler. | |
// Inferred delegate return DU: (Task<Todo> | Task<NotFoundResult>) | |
// Question: Could/would/should this be Task<Todo | NotFoundResult> instead? | |
app.MapGet("/todos/{id}", async (int id, TodoDb db) => | |
await db.Todos.FindAsync(id) | |
is Todo todo | |
? todo | |
: Results.NotFound()) | |
.WithName("GetTodoById"); | |
// The above endpoint declaration today requires all return paths return IResult and as such | |
// the framework can't infer the actual various return types and how they should be represented | |
// in a generated OpenAPI document, so the developer is required to manually annotate the endpoint | |
// with metadata which essentially restates the return paths they wrote in code (example below). With | |
// inferred anonymous DUs the actual return types could be preserved and the framework could use the | |
// type information to automatically generate the matching OpenAPI metadata. | |
// app.MapGet("/todos/{id}", async (int id, TodoDb db) => | |
// await db.Todos.FindAsync(id) | |
// is Todo todo | |
// ? Results.Ok(todo) | |
// : Results.NotFound()) | |
// .WithName("GetTodoById") | |
// .Produces<Todo>(StatusCodes.Status200OK) | |
// .Produces(StatusCodes.Status404NotFound); | |
// Inferred delegate return DU: Task<(ValidationProblemResult | CreatedResult)> | |
app.MapPost("/todos", async (Todo todo, TodoDb db) => | |
{ | |
if (!MiniValidator.TryValidate(todo, out var errors)) | |
return Results.ValidationProblem(errors); | |
db.Todos.Add(todo); | |
await db.SaveChangesAsync(); | |
return Results.Created($"/todos/{todo.Id}", todo); | |
}) | |
.WithName("CreateTodo"); | |
// The developer can choose to optionally declare the returned DU and have the compiler enforce that | |
// the delegate body return statements match the declared return types. | |
app.MapPut("/todos/{id}", async Task<(ValidationProblemResult | NotFoundResult | NoContentResult)> (int id, Todo inputTodo, TodoDb db) => | |
{ | |
if (!MiniValidator.TryValidate(inputTodo, out var errors)) | |
return Results.ValidationProblem(errors); | |
var todo = await db.Todos.FindAsync(id); | |
if (todo is null) return Results.NotFound(); | |
todo.Title = inputTodo.Title; | |
todo.IsComplete = inputTodo.IsComplete; | |
await db.SaveChangesAsync(); | |
return Results.NoContent(); | |
}) | |
.WithName("UpdateTodo"); | |
// Inferred delegate return DU: (Task<NoContentResult> | Task<NotFoundResult>) | |
app.MapPut("/todos/{id}/mark-complete", async (int id, TodoDb db) => | |
{ | |
if (await db.Todos.FindAsync(id) is Todo todo) | |
{ | |
todo.IsComplete = true; | |
await db.SaveChangesAsync(); | |
return Results.NoContent(); | |
} | |
else | |
{ | |
return Results.NotFound(); | |
} | |
}) | |
.WithName("MarkComplete"); | |
// Inferred delegate return DU: Task<(NoContentResult | NotFoundResult)> | |
app.MapPut("/todos/{id}/mark-incomplete", async (int id, TodoDb db) => | |
{ | |
if (await db.Todos.FindAsync(id) is Todo todo) | |
{ | |
todo.IsComplete = false; | |
await db.SaveChangesAsync(); | |
return Results.NoContent(); | |
} | |
else | |
{ | |
return Results.NotFound(); | |
} | |
}) | |
.WithName("MarkIncomplete"); | |
// Developer could also choose to explicitly declare the DU types and return those instead. | |
// The below example would rely on implicit conversion of the values returned to the declared DU return type. | |
app.MapDelete("/todos/{id}", async Task<DeleteResult<Todo>> (int id, TodoDb db) => | |
{ | |
if (await db.Todos.FindAsync(id) is Todo todo) | |
{ | |
db.Todos.Remove(todo); | |
await db.SaveChangesAsync(); | |
return Results.Ok(todo); | |
// If explicit cast is required: | |
// return (DeleteResult<Todo>)Results.Ok(todo); | |
} | |
return Results.NotFound(); | |
}) | |
.WithName("DeleteTodo"); | |
app.Run(); | |
enum struct DeleteResult<T> { OkResult<T> Ok, NotFoundResult NotFound } | |
class Todo | |
{ | |
public int Id { get; set; } | |
[Required] | |
public string? Title { get; set; } | |
public bool IsComplete { get; set; } | |
} | |
class TodoDb : DbContext | |
{ | |
public TodoDb(DbContextOptions<TodoDb> options) | |
: base(options) { } | |
public DbSet<Todo> Todos => Set<Todo>(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment