Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Last active June 2, 2023 09:16
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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>();
}
@DamianEdwards
Copy link

I still like the name adjacent better I think, but you know me, I over index on naming 😉

@davidfowl
Copy link
Author

This source generator looks pretty good https://github.com/JasonBock/InlineMapping.

@DamianEdwards
Copy link

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?

@libin85
Copy link

libin85 commented Sep 25, 2021

would this work, if we use a record for DTO?

@davidfowl
Copy link
Author

You need to redefine the record matching properties and attributes on those properties.

@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