Skip to content
Closed
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
78 changes: 58 additions & 20 deletions .github/workflows/dotnet-core.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,66 @@
name: .NET Core CI
name: .NET CI

on:
push:
branches: [ master ]
branches:
- master
pull_request:
branches: [ master ]
branches:
- master

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
name: Build and test
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Setup .NET SDK
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7
with:
dotnet-version: 10.0.x

- name: Restore
run: dotnet restore src/EFCore.UpdateGraph.slnx

- name: Build
run: dotnet build src/EFCore.UpdateGraph.slnx --configuration Release --no-restore

- name: Test
run: dotnet test src/EFCore.UpdateGraph.slnx --configuration Release --no-build --verbosity normal

publish:
name: Publish NuGet package
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

steps:
- uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.301
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
- name: Publish SimpleGraph Update
uses: brandedoutcast/publish-nuget@v2.5.2
with:
PROJECT_FILE_PATH: src/Diwink.Extensions.EntityFrameworkCore/Diwink.Extensions.EntityFrameworkCore.csproj
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Setup .NET SDK
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7
with:
dotnet-version: 10.0.x

- name: Restore package project
run: dotnet restore src/Diwink.Extensions.EntityFrameworkCore/Diwink.Extensions.EntityFrameworkCore.csproj

- name: Pack
run: dotnet pack src/Diwink.Extensions.EntityFrameworkCore/Diwink.Extensions.EntityFrameworkCore.csproj --configuration Release --no-restore --output ./artifacts

- name: Skip publish when NUGET_API_KEY is not configured
if: env.NUGET_API_KEY == ''
run: echo "NUGET_API_KEY is not configured. Skipping package publish."

- name: Publish to NuGet.org
if: env.NUGET_API_KEY != ''
run: dotnet nuget push "./artifacts/*.nupkg" --api-key "$NUGET_API_KEY" --source "https://api.nuget.org/v3/index.json" --skip-duplicate
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
.idea/
*.iml
*.rsuser
*.suo
*.user
Expand Down
50 changes: 0 additions & 50 deletions EfCore.UpdateGraph.sln

This file was deleted.

40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,42 @@ dbContext.SaveChanges();

```

Please don't hesitate to contribute or give us your feedback and/or advice :rose: :rose:
## v2 Rebuild (EF Core 10+)

The v2 rebuild (`src/`) targets .NET 10 and EF Core 10.x with explicit,
contract-driven relationship semantics.

### Supported Relationship Patterns

| Pattern | Add | Update | Remove | Outcome |
|---------|-----|--------|--------|---------|
| Pure many-to-many (skip nav) | Link created | Properties updated | Link removed | Related entity preserved |
| Payload many-to-many (join entity) | Association inserted | Payload updated | Association deleted | Related entities preserved |
| Required one-to-one | Dependent inserted | Properties updated | Dependent deleted | Cascade delete |
| Optional one-to-one | Dependent inserted | Properties updated | FK nulled | Dependent preserved |

### Rejection Behavior

| Scenario | Exception |
|----------|-----------|
| Unsupported relationship mutated (e.g., one-to-many) | `UnsupportedNavigationMutatedException` |
| Unloaded navigation with mutations in updated graph | `UnloadedNavigationMutationException` |
| Mixed supported + unsupported mutations | `PartialMutationNotAllowedException` |
| Unsupported relationship unchanged | Silently skipped |

### v2 Usage

```csharp
var updated = BuildDesiredState(); // detached graph

var existing = await dbContext.Courses
.Include(c => c.Tags)
.Include(c => c.Policy)
.Include(c => c.MentorAssignments)
.FirstAsync(c => c.Id == id);

dbContext.InsertUpdateOrDeleteGraph(updated, existing);
await dbContext.SaveChangesAsync();
```

Please don't hesitate to contribute or give us your feedback and/or advice :rose: :rose:
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Diwink.Extensions.EntityFrameworkCore.TestModel.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Diwink.Extensions.EntityFrameworkCore.TestModel.Configurations;

public class CourseConfiguration : IEntityTypeConfiguration<Course>
{
/// <summary>
/// Configures the EF Core model mapping for the <c>Course</c> entity.
/// </summary>
/// <param name="builder">The entity type builder for configuring <c>Course</c>.</param>
/// <remarks>
/// - Sets <c>Id</c> as the primary key.
/// - Marks <c>Title</c> and <c>Code</c> as required with maximum lengths of 300 and 50 respectively.
/// - Adds a unique composite index on <c>{ CatalogId, Code }</c>.
/// - Configures a required one-to-one relationship to <c>CoursePolicy</c> with <c>CoursePolicy.CourseId</c> as the foreign key and cascade delete.
/// - Configures a many-to-many relationship with <c>Tag</c> using the join table named <c>CourseTopicTag</c>.
/// </remarks>
public void Configure(EntityTypeBuilder<Course> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.Title).IsRequired().HasMaxLength(300);
builder.Property(c => c.Code).IsRequired().HasMaxLength(50);
builder.HasIndex(c => new { c.CatalogId, c.Code }).IsUnique();

// Required one-to-one: CoursePolicy
builder.HasOne(c => c.Policy)
.WithOne(p => p.Course)
.HasForeignKey<CoursePolicy>(p => p.CourseId)
.OnDelete(DeleteBehavior.Cascade);

// Pure many-to-many via skip navigation
builder.HasMany(c => c.Tags)
.WithMany(t => t.Courses)
.UsingEntity("CourseTopicTag");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Diwink.Extensions.EntityFrameworkCore.TestModel.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Diwink.Extensions.EntityFrameworkCore.TestModel.Configurations;

public class CourseMentorAssignmentConfiguration : IEntityTypeConfiguration<CourseMentorAssignment>
{
/// <summary>
/// Configures the EF Core model for the CourseMentorAssignment entity.
/// </summary>
/// <remarks>
/// Defines a composite primary key (CourseId, MentorId), configures property constraints for Role, AssignedOnUtc and AllocationPercent,
/// and establishes required relationships to Course and Mentor with cascade delete behavior.
/// </remarks>
/// <param name="builder">The EntityTypeBuilder for CourseMentorAssignment used to apply the configuration.</param>
public void Configure(EntityTypeBuilder<CourseMentorAssignment> builder)
{
builder.HasKey(a => new { a.CourseId, a.MentorId });

builder.Property(a => a.Role).IsRequired().HasMaxLength(100);
builder.Property(a => a.AssignedOnUtc).IsRequired();
builder.Property(a => a.AllocationPercent).HasPrecision(5, 2);

builder.HasOne(a => a.Course)
.WithMany(c => c.MentorAssignments)
.HasForeignKey(a => a.CourseId)
.OnDelete(DeleteBehavior.Cascade);

builder.HasOne(a => a.Mentor)
.WithMany(m => m.CourseAssignments)
.HasForeignKey(a => a.MentorId)
.OnDelete(DeleteBehavior.Cascade);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Diwink.Extensions.EntityFrameworkCore.TestModel.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Diwink.Extensions.EntityFrameworkCore.TestModel.Configurations;

public class CoursePolicyConfiguration : IEntityTypeConfiguration<CoursePolicy>
{
/// <summary>
/// Configures the CoursePolicy entity model: sets CourseId as the primary key and makes PolicyVersion required with a maximum length of 50 characters.
/// </summary>
/// <param name="builder">The EntityTypeBuilder for configuring the CoursePolicy entity.</param>
public void Configure(EntityTypeBuilder<CoursePolicy> builder)
{
builder.HasKey(p => p.CourseId);
builder.Property(p => p.PolicyVersion).IsRequired().HasMaxLength(50);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Diwink.Extensions.EntityFrameworkCore.TestModel.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Diwink.Extensions.EntityFrameworkCore.TestModel.Configurations;

public class LearningCatalogConfiguration : IEntityTypeConfiguration<LearningCatalog>
{
/// <summary>
/// Configures the EF Core mapping for the LearningCatalog entity.
/// </summary>
/// <param name="builder">The <see cref="EntityTypeBuilder{LearningCatalog}"/> used to configure keys, properties, and relationships for the LearningCatalog entity.</param>
/// <remarks>
/// Sets the primary key to <c>Id</c>, requires <c>Name</c> with a maximum length of 200 characters, configures a one-to-many relationship to <c>Courses</c> with cascade delete, and configures a many-to-many relationship to <c>Tags</c> using the join table named "CatalogTopicTag".
/// </remarks>
public void Configure(EntityTypeBuilder<LearningCatalog> builder)
{
builder.HasKey(c => c.Id);
builder.Property(c => c.Name).IsRequired().HasMaxLength(200);

// One-to-many (unsupported in v2 — present for FR-018/FR-019 testing)
builder.HasMany(c => c.Courses)
.WithOne(c => c.Catalog)
.HasForeignKey(c => c.CatalogId)
.OnDelete(DeleteBehavior.Cascade);

// Pure many-to-many via skip navigation
builder.HasMany(c => c.Tags)
.WithMany()
.UsingEntity("CatalogTopicTag");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Diwink.Extensions.EntityFrameworkCore.TestModel.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Diwink.Extensions.EntityFrameworkCore.TestModel.Configurations;

public class MentorConfiguration : IEntityTypeConfiguration<Mentor>
{
/// <summary>
/// Configures the EF Core mapping for the Mentor entity.
/// </summary>
/// <remarks>
/// Sets the primary key, requires DisplayName (max length 200) and Status (max length 50),
/// and configures an optional one-to-one relationship to MentorWorkspace using MentorWorkspace.MentorId
/// with delete behavior set to SetNull.
/// </remarks>
/// <param name="builder">The <see cref="EntityTypeBuilder{Mentor}"/> used to configure the Mentor entity.</param>
public void Configure(EntityTypeBuilder<Mentor> builder)
{
builder.HasKey(m => m.Id);
builder.Property(m => m.DisplayName).IsRequired().HasMaxLength(200);
builder.Property(m => m.Status).IsRequired().HasMaxLength(50);

// Optional one-to-one: MentorWorkspace
builder.HasOne(m => m.Workspace)
.WithOne(w => w.Mentor)
.HasForeignKey<MentorWorkspace>(w => w.MentorId)
.OnDelete(DeleteBehavior.SetNull);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Diwink.Extensions.EntityFrameworkCore.TestModel.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Diwink.Extensions.EntityFrameworkCore.TestModel.Configurations;

public class MentorWorkspaceConfiguration : IEntityTypeConfiguration<MentorWorkspace>
{
/// <summary>
/// Configures the Entity Framework Core mapping for the MentorWorkspace entity.
/// </summary>
/// <param name="builder">The EntityTypeBuilder for MentorWorkspace used to set the primary key and property constraints (DeskCode and Building).</param>
public void Configure(EntityTypeBuilder<MentorWorkspace> builder)
{
builder.HasKey(w => w.Id);
builder.Property(w => w.DeskCode).IsRequired().HasMaxLength(20);
builder.Property(w => w.Building).IsRequired().HasMaxLength(100);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Diwink.Extensions.EntityFrameworkCore.TestModel.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Diwink.Extensions.EntityFrameworkCore.TestModel.Configurations;

public class TopicTagConfiguration : IEntityTypeConfiguration<TopicTag>
{
/// <summary>
/// Configures the EF Core mapping for the TopicTag entity.
/// </summary>
/// <param name="builder">The EntityTypeBuilder for TopicTag used to configure the primary key, property constraints, and indexes.</param>
public void Configure(EntityTypeBuilder<TopicTag> builder)
{
builder.HasKey(t => t.Id);
builder.Property(t => t.Label).IsRequired().HasMaxLength(100);
builder.HasIndex(t => t.Label).IsUnique();
}
}
Loading
Loading