Last active
June 2, 2023 09:16
-
-
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
This file contains 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
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>(); | |
} |
This file contains 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
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>(); | |
} |
This source generator looks pretty good https://github.com/JasonBock/InlineMapping.
Ah nice. Definitely feels like there's an opportunity for a language feature to really get this nice and terse.
You should add a comment above to make it clear the associated type's generated members get the attributes from the source type too.
Any value of the idea around two modes still you think? i.e. passthrough vs. copy?
would this work, if we use a record for DTO?
You need to redefine the record matching properties and attributes on those properties.
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
I still like the name adjacent better I think, but you know me, I over index on naming 😉