Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,77 @@
# Read Me

## DesignTime Integration

This package can generate GraphQL DataLoaders (and related extensions) at **EF Core design-time** when you scaffold migrations. This is done by registering a custom `IMigrationsCodeGenerator`.

### 1) Add the design-time package

Reference `Stackworx.EfCoreGraphQL.DesignTime` from the project EF tooling loads at design-time (commonly your **startup** project, but any project EF loads for design-time services works).

> Note: EF tooling loads design-time services from the *startup project* and the design-time assembly references it discovers.

### 2) Register `EfCoreMigrationsCodeGenerator`

Create a class that implements `IDesignTimeServices` (EF Core tooling discovers it automatically) and register the code generator:

```csharp
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Migrations.Design;
using Microsoft.Extensions.DependencyInjection;
using Stackworx.EfCoreGraphQL.DesignTime;

public sealed class DesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
=> services.AddSingleton<IMigrationsCodeGenerator, EfCoreMigrationsCodeGenerator>();
}
```

Example: see `sample/Sample.DesignTime/DesignTimeServices.cs`.

### 3) Configure the output directory (required)

During migration scaffolding EF Core may run with a working directory that **is not** your migrations/target project directory (especially when `--startup-project` differs from the target project). To avoid writing generated files to the wrong place, sidecar output requires an explicit directory.

Set this environment variable before running `dotnet ef`:

- `STACKWORX_EFCOREGRAPHQL_SIDECAR_OUTPUT_DIR`

It must point to an **existing directory**. A typical value is your migrations folder.

macOS / Linux (zsh/bash):

```zsh
export STACKWORX_EFCOREGRAPHQL_SIDECAR_OUTPUT_DIR="/absolute/path/to/YourProject/Migrations"
```

Windows (PowerShell):

```powershell
$env:STACKWORX_EFCOREGRAPHQL_SIDECAR_OUTPUT_DIR = "C:\\path\\to\\YourProject\\Migrations"
```

If this variable is not set (or points to a non-existent directory), migration scaffolding will fail with a clear error.

### 4) Scaffold a migration

Run EF migrations as usual. The generator runs when EF scaffolds/updates the *model snapshot*.

```zsh
dotnet ef migrations add InitialCreate \
--project ./src/Your.Migrations.Project \
--startup-project ./src/Your.Api.Project
```

### Generated files

Sidecar files are written into `STACKWORX_EFCOREGRAPHQL_SIDECAR_OUTPUT_DIR` with names based on the model snapshot name:

- `{ModelSnapshotName}.DataLoaders.g.cs`
- `{ModelSnapshotName}.DataLoaders.g.hash`

The `.hash` file prevents unnecessary regeneration when the snapshot hasn’t changed.

## Goal

Auto generate dataloaders and extensions to match EF core navigations.
Expand Down Expand Up @@ -272,4 +344,3 @@ public sealed class StudentExtensions
}
}
```

18 changes: 10 additions & 8 deletions Stackworx.EfCoreGraphQL.slnx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
<Solution>
<Folder Name="/sample/">
<Project Path="sample\Sample.Generate\Sample.Generate.csproj" Type="Classic C#" />
<Project Path="sample\Sample\Sample.csproj" Type="Classic C#" />
<Project Path="sample/Sample.DesignTime/Sample.DesignTime.csproj" />
<Project Path="sample\Sample.Generate\Sample.Generate.csproj" Type="C#" />
<Project Path="sample\Sample\Sample.csproj" Type="C#" />
</Folder>
<Folder Name="/src/">
<Project Path="src\Stackworx.EfCoreGraphQL.Abstractions\Stackworx.EfCoreGraphQL.Abstractions.csproj" Type="Classic C#" />
<Project Path="src\Stackworx.EfCoreGraphQL.Validation.XunitV3\Stackworx.EfCoreGraphQL.Validation.XunitV3.csproj" Type="Classic C#" />
<Project Path="src\Stackworx.EfCoreGraphQL.Validation\Stackworx.EfCoreGraphQL.Validation.csproj" Type="Classic C#" />
<Project Path="src\Stackworx.EfCoreGraphQL\Stackworx.EfCoreGraphQL.csproj" Type="Classic C#" />
<Project Path="src/Stackworx.EFCoreGraphQL.DesignTime/Stackworx.EfCoreGraphQL.DesignTime.csproj" />
<Project Path="src\Stackworx.EfCoreGraphQL.Abstractions\Stackworx.EfCoreGraphQL.Abstractions.csproj" Type="C#" />
<Project Path="src\Stackworx.EfCoreGraphQL.Validation.XunitV3\Stackworx.EfCoreGraphQL.Validation.XunitV3.csproj" Type="C#" />
<Project Path="src\Stackworx.EfCoreGraphQL.Validation\Stackworx.EfCoreGraphQL.Validation.csproj" Type="C#" />
<Project Path="src\Stackworx.EfCoreGraphQL\Stackworx.EfCoreGraphQL.csproj" Type="C#" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests\Stackworx.EfCoreGraphQL.Tests\Stackworx.EfCoreGraphQL.Tests.csproj" Type="Classic C#" />
<Project Path="tests\Stackworx.EfCoreGraphQL.Validation.Tests\Stackworx.EfCoreGraphQL.Validation.Tests.csproj" Type="Classic C#" />
<Project Path="tests\Stackworx.EfCoreGraphQL.Tests\Stackworx.EfCoreGraphQL.Tests.csproj" Type="C#" />
<Project Path="tests\Stackworx.EfCoreGraphQL.Validation.Tests\Stackworx.EfCoreGraphQL.Validation.Tests.csproj" Type="C#" />
</Folder>
</Solution>
2 changes: 1 addition & 1 deletion dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.2",
"version": "10.0.3",
"commands": [
"dotnet-ef"
],
Expand Down
1 change: 1 addition & 0 deletions sample/Sample.DesignTime/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
db/
30 changes: 30 additions & 0 deletions sample/Sample.DesignTime/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Sample.DesignTime.Data;

using Microsoft.EntityFrameworkCore;

public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Author> Authors => Set<Author>();
public DbSet<Book> Books => Set<Book>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>(b =>
{
b.ToTable("Authors");
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired();
});

modelBuilder.Entity<Book>(b =>
{
b.ToTable("Books");
b.HasKey(x => x.Id);
b.Property(x => x.Title).IsRequired();

b.HasOne(x => x.Author)
.WithMany(x => x.Books)
.HasForeignKey(x => x.AuthorId);
});
}
}
19 changes: 19 additions & 0 deletions sample/Sample.DesignTime/Data/AppDbContextFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Sample.DesignTime.Data;

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

/// <summary>
/// Used by EF Core tooling (dotnet ef) to create the DbContext at design-time.
/// </summary>
public sealed class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args)
{
var opts = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite("DataSource=db/app.db")
.Options;

return new AppDbContext(opts);
}
}
9 changes: 9 additions & 0 deletions sample/Sample.DesignTime/Data/Author.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Sample.DesignTime.Data;

public class Author
{
public int Id { get; set; }
public string Name { get; set; } = null!;

public List<Book> Books { get; set; } = [];
}
10 changes: 10 additions & 0 deletions sample/Sample.DesignTime/Data/Book.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Sample.DesignTime.Data;

public class Book
{
public int Id { get; set; }
public string Title { get; set; } = null!;

public int AuthorId { get; set; }
public Author Author { get; set; } = null!;
}
16 changes: 16 additions & 0 deletions sample/Sample.DesignTime/DesignTimeServices.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Sample.DesignTime;

using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Migrations.Design;
using Microsoft.Extensions.DependencyInjection;
using Stackworx.EfCoreGraphQL.DesignTime;

/// <summary>
/// EF Core tooling will discover this type automatically (when the assembly is referenced)
/// and call it during design-time operations (e.g. migrations scaffolding).
/// </summary>
public sealed class DesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
=> services.AddSingleton<IMigrationsCodeGenerator, EfCoreMigrationsCodeGenerator>();
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions sample/Sample.DesignTime/Migrations/20260221160640_Initial.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Sample.DesignTime.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Authors",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Authors", x => x.Id);
});

migrationBuilder.CreateTable(
name: "Books",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(type: "TEXT", nullable: false),
AuthorId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Books", x => x.Id);
table.ForeignKey(
name: "FK_Books_Authors_AuthorId",
column: x => x.AuthorId,
principalTable: "Authors",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});

migrationBuilder.CreateIndex(
name: "IX_Books_AuthorId",
table: "Books",
column: "AuthorId");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Books");

migrationBuilder.DropTable(
name: "Authors");
}
}
}
Loading