Skip to content

Instantly share code, notes, and snippets.

@DamianEdwards
Last active February 16, 2022 01:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DamianEdwards/4aea2fd9600ef402dc805aaa4dd98438 to your computer and use it in GitHub Desktop.
Save DamianEdwards/4aea2fd9600ef402dc805aaa4dd98438 to your computer and use it in GitHub Desktop.
Potential ASP.NET Core Minimal APIs use of C# Discriminated Unions (DU)
// 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