Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Last active June 2, 2023 09:16
Show Gist options
  • Save davidfowl/26cc407a9d914e7fcd6236cbf613b3da to your computer and use it in GitHub Desktop.
Save davidfowl/26cc407a9d914e7fcd6236cbf613b3da to your computer and use it in GitHub Desktop.
Associated types: A demo of what DTOs could look like as a language feature
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/todos", async (CreateTodo createTodo, TodoDb db) =>
{
// This is where the implicit conversion comes in
db.Todos.Add(createTodo);
await db.SaveChangesAsync();
});
app.Run();
// The following is similar to https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys
// CreateTodo is the DTO used for creating a new Todo.
// It only exposes the title to prevent overbinding.
// This is the proposed syntax. We want to "associate" CreateTodo with the Todo type.
// This isn't an inheritance relationship, it's one where the properties map so that
// associated class CreateTodo : Todo { Title }
// This is what it would look like without compiler syntax (something a source generator can produce)
// [Associated(typeof(Todo), nameof(Todo.Title))]
// partial class CreateTodo { }
// The compiler generates this code for CreateTodo
class CreateTodo
{
private readonly Todo _todo = new();
// Attributes are copied from the associated type
[Required]
public string Title { get => _todo.Title; set => _todo.Title = value; }
// The conversion here lets you pass around CreateTodo to things
// that take a Todo (the associated type)
public static implicit operator Todo(CreateTodo createTodo)
{
return createTodo._todo;
}
}
// This is the database model (EntityFramework in this case)
class Todo
{
public int Id { get; set; }
[Required]
public string Title { get; set; } = default!;
public bool IsComplete { get; set; }
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions options)
: base(options)
{
}
public DbSet<Todo> Todos => Set<Todo>();
}
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/todos", async (CreateTodo createTodo, TodoDb db) =>
{
// This is the magic of roles being an erased wrapper. We can get a type safe view of
// Todo for de-serialization, but since it's erased, we can pass CreateTodo to anything that takes
// a Todo
db.Todos.Add(createTodo);
await db.SaveChangesAsync();
});
app.Run();
// CreateTodo is the DTO used for creating a new Todo.
// It only exposes the title to prevent overposting (https://en.m.wikipedia.org/wiki/Mass_assignment_vulnerability)
role CreateTodo : Todo
{
// This role exposes a single property Title that maps to the underlying name property
// I made up the existing keyword but maybe matching by name is fine
public existing string Title { get; }
}
// This is the database model (EntityFramework in this case)
class Todo
{
public int Id { get; set; }
[Required]
public string Title { get; set; } = default!;
public bool IsComplete { get; }
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions options)
: base(options)
{
}
public DbSet<Todo> Todos => Set<Todo>();
}
@libin85
Copy link

libin85 commented Sep 26, 2021

Sorry, dont know if I understood what you mean by redefine the properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment