diff --git a/.gitignore b/.gitignore
index 0cdd3e3..c5b5934 100644
--- a/.gitignore
+++ b/.gitignore
@@ -438,3 +438,6 @@ Thumbs.db
**/*.DotSettings.user
/.claude/do_not_commit/
/nupkg/
+
+# Internal specs — not tracked in source control
+docs/specs/
diff --git a/Directory.Build.props b/Directory.Build.props
index 97f2a39..511e584 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 1.2.1
+ 1.3.0
MIT
https://github.com/layeredcraft/optimized-enums
git
diff --git a/Directory.Packages.props b/Directory.Packages.props
index decd7bd..3efcf87 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,5 +26,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/LayeredCraft.OptimizedEnums.slnx b/LayeredCraft.OptimizedEnums.slnx
index 250d03d..f4a50c0 100644
--- a/LayeredCraft.OptimizedEnums.slnx
+++ b/LayeredCraft.OptimizedEnums.slnx
@@ -31,6 +31,7 @@
+
@@ -54,11 +55,13 @@
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index c991fda..43b2636 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
|---------|-------|-----------|
| **LayeredCraft.OptimizedEnums** | [](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums) | [](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums/) |
| **LayeredCraft.OptimizedEnums.SystemTextJson** | [](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums.SystemTextJson) | [](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums.SystemTextJson/) |
-| **LayeredCraft.OptimizedEnums.EFCore** | _coming soon_ | |
+| **LayeredCraft.OptimizedEnums.EFCore** | [](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums.EFCore) | [](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums.EFCore/) |
| **LayeredCraft.OptimizedEnums.Dapper** | _coming soon_ | |
| **LayeredCraft.OptimizedEnums.AutoFixture** | _coming soon_ | |
@@ -119,6 +119,42 @@ public sealed partial class OrderStatus : OptimizedEnum
Two strategies are available: `ByName` (serializes as the member name string) and `ByValue` (serializes as the underlying value). See the [JSON Serialization docs](https://layeredcraft.github.io/optimized-enums/usage/json-serialization/) for full details.
+## Entity Framework Core
+
+Add `LayeredCraft.OptimizedEnums.EFCore` for source-generated, zero-reflection EF Core value converter support. One package is all you need — it pulls in the core package automatically:
+
+```bash
+dotnet add package LayeredCraft.OptimizedEnums.EFCore
+```
+
+Decorate your class with `[OptimizedEnumEfCore]` and the generator emits concrete `ValueConverter` classes and registration helpers:
+
+```csharp
+using LayeredCraft.OptimizedEnums;
+using LayeredCraft.OptimizedEnums.EFCore;
+
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+public sealed partial class OrderStatus : OptimizedEnum
+{
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+ public static readonly OrderStatus Paid = new(2, nameof(Paid));
+ public static readonly OrderStatus Shipped = new(3, nameof(Shipped));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+}
+```
+
+Register conversions with the global convention hook in your `DbContext`:
+
+```csharp
+protected override void ConfigureConventions(ModelConfigurationBuilder builder)
+{
+ builder.ConfigureOptimizedEnums();
+}
+```
+
+Two strategies are available: `ByValue` (stores the underlying value) and `ByName` (stores the member name string). See the [Entity Framework Core docs](https://layeredcraft.github.io/optimized-enums/usage/ef-core/) for full details.
+
## Installation
```bash
diff --git a/docs/advanced/diagnostics.md b/docs/advanced/diagnostics.md
index 6754db2..ef3f90f 100644
--- a/docs/advanced/diagnostics.md
+++ b/docs/advanced/diagnostics.md
@@ -124,6 +124,86 @@ public sealed partial class OrderStatus : OptimizedEnum { ...
public sealed partial class OrderStatus : OptimizedEnum { ... }
```
+## EFCore Diagnostics
+
+The `LayeredCraft.OptimizedEnums.EFCore` generator emits diagnostics with the `OE3xxx` prefix.
+
+### OE3001 — Not an OptimizedEnum
+
+**Message:** `The class '{0}' must inherit from OptimizedEnum to use [OptimizedEnumEfCore]`
+
+**Cause:** `[OptimizedEnumEfCore]` was applied to a class that does not inherit from `OptimizedEnum`.
+
+**Fix:** Remove the attribute, or make the class inherit from `OptimizedEnum` (directly or through an abstract intermediate base class).
+
+### OE3002 — Must Be Partial
+
+**Message:** `The class '{0}' must be declared as partial for [OptimizedEnumEfCore] source generation`
+
+**Cause:** A class decorated with `[OptimizedEnumEfCore]` is missing the `partial` keyword.
+
+**Fix:**
+```csharp
+// Before
+[OptimizedEnumEfCore]
+public sealed class OrderStatus : OptimizedEnum { ... }
+
+// After
+[OptimizedEnumEfCore]
+public sealed partial class OrderStatus : OptimizedEnum { ... }
+```
+
+### OE3003 — Unknown Storage Type
+
+**Message:** `The class '{0}' specifies an unknown OptimizedEnumEfCoreStorage value '{1}'; valid values are ByValue (0) and ByName (1)`
+
+**Cause:** An explicit integer cast was used to pass an undefined `OptimizedEnumEfCoreStorage` value to `[OptimizedEnumEfCore]`.
+
+**Fix:** Use only the defined enum members:
+```csharp
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)] // or ByName
+public sealed partial class OrderStatus : OptimizedEnum { ... }
+```
+
+### OE3004 — Unsupported Target
+
+**Cause:** `[OptimizedEnumEfCore]` was applied to a class configuration that the generator does not support. Two confirmed triggers:
+
+**Abstract class:**
+
+```csharp
+// Wrong — abstract base class
+[OptimizedEnumEfCore]
+public abstract partial class OrderStatusBase : OptimizedEnum { }
+
+// Correct — concrete derived class
+[OptimizedEnumEfCore]
+public sealed partial class OrderStatus : OrderStatusBase { ... }
+```
+
+**Enum nested inside a generic containing type:**
+
+```csharp
+// Wrong — containing type has type parameters
+public class Container
+{
+ [OptimizedEnumEfCore]
+ public sealed partial class Status : OptimizedEnum { ... }
+}
+```
+
+EF Core converters and extension methods are emitted at namespace scope. Generic type parameters from the containing type would not be in scope there, producing uncompilable references. Move the enum out of the generic container, or remove `[OptimizedEnumEfCore]` and register the conversion manually.
+
+Enums nested inside **non-generic** containing types are fully supported.
+
+### OE9003 — Internal Generator Error
+
+**Message:** `An unexpected error occurred while generating the EF Core support for '{0}': {1}`
+
+**Cause:** An unhandled exception occurred inside the EFCore generator. This is a generator bug.
+
+**Fix:** Report the issue with the full diagnostic message and the enum source code. As a workaround, the EFCore attribute can be temporarily removed from the affected type.
+
## Generator Not Running?
If you add the package but see no generated members, check:
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 8323cb5..7401f10 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -60,6 +60,30 @@ To add source-generated `System.Text.Json` converter support, install the System
See [JSON Serialization](../usage/json-serialization.md) for usage details.
+## Optional: Entity Framework Core
+
+To add source-generated EF Core value converter support, install the EFCore package. It declares the core package as a dependency:
+
+=== ".NET CLI"
+
+ ```bash
+ dotnet add package LayeredCraft.OptimizedEnums.EFCore
+ ```
+
+=== "Package Manager"
+
+ ```powershell
+ Install-Package LayeredCraft.OptimizedEnums.EFCore
+ ```
+
+=== "PackageReference"
+
+ ```xml
+
+ ```
+
+Supports EF Core 8, 9, and 10. See [Entity Framework Core](../usage/ef-core.md) for usage details.
+
## Verifying the Installation
After adding the package, define a type that inherits from `OptimizedEnum` and declare it `partial`. Build the project — the generator runs during compilation and produces the lookup members. You can inspect the generated output under `obj/` or via your IDE's "Go to definition" on any generated method.
diff --git a/docs/usage/ef-core.md b/docs/usage/ef-core.md
new file mode 100644
index 0000000..8eef131
--- /dev/null
+++ b/docs/usage/ef-core.md
@@ -0,0 +1,430 @@
+# Entity Framework Core
+
+The `LayeredCraft.OptimizedEnums.EFCore` package adds source-generated, zero-reflection Entity Framework Core value converter support for `OptimizedEnum` types. The generator emits concrete converter classes and registration helpers at compile time — no runtime reflection, no `Activator.CreateInstance`, and no dynamic proxy factories.
+
+## Installation
+
+Install the EFCore package. The core `LayeredCraft.OptimizedEnums` package is pulled in automatically:
+
+=== ".NET CLI"
+
+ ```bash
+ dotnet add package LayeredCraft.OptimizedEnums.EFCore
+ ```
+
+=== "PackageReference"
+
+ ```xml
+
+ ```
+
+**EF Core version support:** EF Core 8, 9, and 10.
+
+## Quick Start
+
+Decorate your `OptimizedEnum` class with `[OptimizedEnumEfCore]` and add `partial` if it isn't already:
+
+```csharp
+using LayeredCraft.OptimizedEnums;
+using LayeredCraft.OptimizedEnums.EFCore;
+
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+public sealed partial class OrderStatus : OptimizedEnum
+{
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+ public static readonly OrderStatus Paid = new(2, nameof(Paid));
+ public static readonly OrderStatus Shipped = new(3, nameof(Shipped));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+}
+```
+
+Then register the conversion in your `DbContext`. The simplest option is the global convention hook:
+
+```csharp
+public class AppDbContext : DbContext
+{
+ protected override void ConfigureConventions(ModelConfigurationBuilder builder)
+ {
+ builder.ConfigureOptimizedEnums();
+ }
+}
+```
+
+That's all you need for basic usage. All properties of type `OrderStatus` (including nullable `OrderStatus?`) are automatically converted to and from their database representation.
+
+## The Attribute
+
+`[OptimizedEnumEfCore]` controls whether the enum's default storage is `ByValue` or `ByName`:
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| `storage` | `OptimizedEnumEfCoreStorage` | `ByValue` | How to store the enum in the database |
+
+```csharp
+// Store by underlying value (default)
+[OptimizedEnumEfCore]
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+
+// Store by name string
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByName)]
+```
+
+The attribute may be applied to any `sealed partial` class that inherits from `OptimizedEnum`, directly or through abstract intermediate base classes.
+
+## Storage Strategies
+
+### ByValue
+
+Stores the enum's underlying `TValue` in the database column. The column type mirrors `TValue` (e.g., `int` → integer column, `string` → text column).
+
+```csharp
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+public sealed partial class OrderStatus : OptimizedEnum { ... }
+```
+
+- Write path: `enum.Value` — stores the integer/string value directly
+- Read path: looks up the instance via the generated `TryFromValue` table
+- Invalid stored values throw `InvalidOperationException` during materialization
+
+### ByName
+
+Stores the enum's `Name` string in the database column, regardless of `TValue`. The column is always a text/varchar column.
+
+```csharp
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByName)]
+public sealed partial class Currency : OptimizedEnum
+{
+ public static readonly Currency Usd = new("USD", nameof(Usd));
+ public static readonly Currency Eur = new("EUR", nameof(Eur));
+
+ private Currency(string value, string name) : base(value, name) { }
+}
+```
+
+- Write path: `enum.Name` — stores the string name (e.g., `"Usd"`, `"Eur"`)
+- Read path: looks up the instance via the generated `TryFromName` table
+- Invalid stored names throw `InvalidOperationException` during materialization
+
+### Choosing a Strategy
+
+| Consideration | ByValue | ByName |
+|---|---|---|
+| Column type | Matches `TValue` (int, string, etc.) | Always `string` |
+| Human-readable in DB | Only if `TValue` is string | Yes |
+| Refactor safety | Adding new members is safe; renaming a member doesn't change stored data | Adding new members is safe; **renaming a member changes stored data** |
+| Query filtering | Efficient for numeric comparisons | String comparisons |
+
+Use `ByValue` when your database is integer-keyed and you don't need to read the DB directly. Use `ByName` when human-readability matters or when `TValue` is already a meaningful string code.
+
+## Registration Approaches
+
+Three ways to register conversion, in order of increasing specificity:
+
+### 1. Global Convention (recommended)
+
+Applies the enum attribute's default storage mode to all properties of each opted-in enum type across the entire model. One call covers all entities.
+
+Override `ConfigureConventions` in your `DbContext`:
+
+```csharp
+using LayeredCraft.OptimizedEnums.EFCore;
+using Microsoft.EntityFrameworkCore;
+
+public class AppDbContext : DbContext
+{
+ public AppDbContext(DbContextOptions options) : base(options) { }
+
+ public DbSet Orders { get; set; }
+
+ protected override void ConfigureConventions(ModelConfigurationBuilder builder)
+ {
+ builder.ConfigureOptimizedEnums(); // registers all [OptimizedEnumEfCore] types
+ }
+}
+```
+
+EF Core's null lifting applies automatically: one registration covers both `OrderStatus` and `OrderStatus?` properties.
+
+### 2. Enum-Specific Property Helper
+
+Per-property extension methods are generated for each opted-in enum. Use these when you want a single property to use a different mode than the enum's attribute default:
+
+```csharp
+protected override void OnModelCreating(ModelBuilder modelBuilder)
+{
+ modelBuilder.Entity()
+ .Property(x => x.Status)
+ .HasOrderStatusConversionByName(); // overrides the ByValue default
+}
+```
+
+Both methods are always generated regardless of the enum's attribute default:
+
+```csharp
+// Generated for every opted-in enum (example: OrderStatus)
+builder.HasOrderStatusConversionByValue();
+builder.HasOrderStatusConversionByName();
+```
+
+The extension class is `internal` and lives in the enum's own namespace, so these methods are naturally scoped to the consuming assembly.
+
+### 3. Direct Converter
+
+You can also pass a converter instance directly using EF Core's standard API:
+
+```csharp
+protected override void OnModelCreating(ModelBuilder modelBuilder)
+{
+ modelBuilder.Entity()
+ .Property(x => x.Status)
+ .HasConversion(new OrderStatusValueConverter()); // ByValue
+ // or:
+ .HasConversion(new OrderStatusNameConverter()); // ByName
+}
+```
+
+### Precedence
+
+When multiple registrations exist for the same property, EF Core's own precedence applies:
+
+1. Explicit property override (`HasConversion(...)` or `HasXxxConversionByValue/ByName()` in `OnModelCreating`)
+2. Convention registered via `ConfigureConventions`
+
+Property-level configuration always wins over convention-level configuration.
+
+## What Gets Generated
+
+For each opted-in enum, the generator emits a single `.g.cs` file containing:
+
+**Converter classes** (always both, regardless of attribute default):
+
+```csharp
+// ByValue converter — converts between OrderStatus and int
+internal sealed class OrderStatusValueConverter
+ : ValueConverter
+{
+ public OrderStatusValueConverter()
+ : base(static v => v.Value, static v => FromValue(v)) { }
+
+ private static global::MyApp.Domain.OrderStatus FromValue(int v) =>
+ global::MyApp.Domain.OrderStatus.TryFromValue(v, out var result)
+ ? result!
+ : throw new InvalidOperationException($"'{v}' is not a valid value for OrderStatus.");
+}
+
+// ByName converter — converts between OrderStatus and string
+internal sealed class OrderStatusNameConverter
+ : ValueConverter
+{
+ public OrderStatusNameConverter()
+ : base(static v => v.Name, static v => FromName(v)) { }
+
+ private static global::MyApp.Domain.OrderStatus FromName(string v) =>
+ global::MyApp.Domain.OrderStatus.TryFromName(v, out var result)
+ ? result!
+ : throw new InvalidOperationException($"'{v}' is not a valid name for OrderStatus.");
+}
+```
+
+**Extension class** with property-builder helpers:
+
+```csharp
+internal static class MyApp_Domain_OrderStatusEfCoreExtensions
+{
+ public static PropertyBuilder HasOrderStatusConversionByValue(
+ this PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+
+ public static PropertyBuilder HasOrderStatusConversionByName(
+ this PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+}
+```
+
+**Shared conventions file** (emitted once per compilation):
+
+```csharp
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ public static class OptimizedEnumEfCoreConventionExtensions
+ {
+ public static ModelConfigurationBuilder ConfigureOptimizedEnums(
+ this ModelConfigurationBuilder builder)
+ {
+ builder.Properties()
+ .HaveConversion();
+ // ... one entry per opted-in enum
+ return builder;
+ }
+ }
+}
+```
+
+The conventions file is always emitted, even when no enums are opted in, so `builder.ConfigureOptimizedEnums()` compiles before any annotation is added.
+
+## Nullable Properties
+
+Nullable properties (`OrderStatus?`) work without any extra configuration. EF Core automatically lifts null through the converter:
+
+- `null` in the database → `null` in the CLR property
+- Non-null database value → converted to the appropriate `OrderStatus` instance
+- Non-null invalid value → `InvalidOperationException` during materialization
+
+No separate nullable converter class is generated — EF Core's null lifting handles this automatically from the non-nullable converter.
+
+```csharp
+public class Order
+{
+ public int Id { get; set; }
+ public OrderStatus Status { get; set; } // non-nullable
+ public OrderStatus? OptionalStatus { get; set; } // nullable — works automatically
+}
+```
+
+## Primary Keys, Foreign Keys, and Indexes
+
+The generated converters work for all standard EF Core property roles:
+
+```csharp
+// Primary key
+public class StatusRecord
+{
+ public OrderStatus Id { get; set; } // OrderStatus as PK
+ public string Description { get; set; }
+}
+
+// Foreign key
+public class Order
+{
+ public OrderStatus StatusCode { get; set; }
+ public StatusRecord Status { get; set; } // navigation
+}
+
+// Alternate key
+modelBuilder.Entity()
+ .HasAlternateKey(x => x.Status);
+
+// Index
+modelBuilder.Entity()
+ .HasIndex(x => x.Status);
+```
+
+Register the converters as normal (via `ConfigureOptimizedEnums()` or explicit property configuration) and these scenarios work without additional setup.
+
+## String-Valued Enums
+
+`ByValue` and `ByName` both work with `string`-typed `TValue`. The provider type is `string` for `ByValue` (stores the raw value) and also `string` for `ByName` (stores the name). If your value and name are different strings, choose the one you want in the database:
+
+```csharp
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+public sealed partial class Currency : OptimizedEnum
+{
+ // Value = ISO code, Name = property name
+ public static readonly Currency Usd = new("USD", nameof(Usd));
+ public static readonly Currency Eur = new("EUR", nameof(Eur));
+
+ private Currency(string value, string name) : base(value, name) { }
+}
+```
+
+With `ByValue`, the database stores `"USD"` / `"EUR"`. With `ByName`, it stores `"Usd"` / `"Eur"`.
+
+## Abstract Intermediate Base Classes
+
+The generator correctly resolves the `OptimizedEnum` base through one or more abstract intermediate classes. Annotate only the concrete sealed class:
+
+```csharp
+// Abstract base — not annotated, does not need to be partial
+public abstract class OrderStatusBase : OptimizedEnum
+ where TEnum : OptimizedEnum
+{
+ protected OrderStatusBase(int value, string name) : base(value, name) { }
+}
+
+// Concrete enum — annotated and partial
+[OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+public sealed partial class OrderStatus : OrderStatusBase
+{
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+ public static readonly OrderStatus Paid = new(2, nameof(Paid));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+}
+```
+
+Applying `[OptimizedEnumEfCore]` to an abstract class is an error (OE3004).
+
+## Nested Types
+
+Enums nested inside other types are fully supported:
+
+```csharp
+public partial class Outer
+{
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+ public sealed partial class Status : OptimizedEnum
+ {
+ public static readonly Status Active = new(1, nameof(Active));
+ public static readonly Status Inactive = new(2, nameof(Inactive));
+
+ private Status(int value, string name) : base(value, name) { }
+ }
+}
+```
+
+The generated converter and extension class names include the containing type name with a `_` separator to avoid collisions (e.g., `Outer_StatusValueConverter`, `MyApp_Domain_Outer_StatusEfCoreExtensions`).
+
+**Limitation:** Enums nested inside **generic** containing types are not supported and produce OE3004. Converters and extension methods are emitted at namespace scope; generic type parameters from the containing type would not be in scope there. Move the enum out of the generic container, or register the conversion manually.
+
+## Extension Class Naming
+
+The generated extension class is named by joining all namespace segments and containing-type names with underscores, suffixed with `EfCoreExtensions`:
+
+| Enum location | Extension class name |
+|---|---|
+| `MyApp.Domain.OrderStatus` | `MyApp_Domain_OrderStatusEfCoreExtensions` |
+| `MyApp.Domain.Outer.Status` | `MyApp_Domain_Outer_StatusEfCoreExtensions` |
+| `Priority` (global namespace) | `PriorityEfCoreExtensions` |
+
+This scheme prevents name collisions between enums with the same class name in different namespaces.
+
+## Invalid Value Behavior
+
+The generated converters throw `InvalidOperationException` for unrecognized provider values:
+
+```
+System.InvalidOperationException: '99' is not a valid value for OrderStatus.
+System.InvalidOperationException: 'Unknown' is not a valid name for OrderStatus.
+```
+
+This happens during EF Core's materialization phase (when reading from the database). The exception propagates through EF's normal error handling. Do not store values in the database that are not defined members of the enum.
+
+## AOT and Reflection-Free Design
+
+The generated code contains zero package-authored runtime reflection:
+
+- Converter classes are concrete and non-generic — no `MakeGenericType`
+- Lookup logic delegates to the source-generated `TryFromValue` / `TryFromName` static dictionary methods on the enum class
+- No `Activator.CreateInstance`, no `Delegate.CreateDelegate`, no runtime type-walking
+
+The `HasConversion()` call in the extension methods and the `HaveConversion()` call in `ConfigureOptimizedEnums()` use generic type parameters that are known at compile time. EF Core may use reflection internally to instantiate the converter class at startup, but the converter logic itself is entirely reflection-free.
+
+## Diagnostics
+
+The EFCore generator emits diagnostics with the `OE3xxx` prefix. See [Diagnostics — EFCore](../advanced/diagnostics.md#efcore-diagnostics) for the full list.
+
+## v1 Limitations
+
+The following are intentionally deferred to a future version:
+
+- **Generic property-builder helpers** — `HasOptimizedEnumConversionByValue()` cannot be implemented without reflection or static abstract interface members, because `TryFromValue` / `TryFromName` are generated on each concrete class and not accessible from a base-type constraint. Use the enum-specific helpers or the global convention instead.
+- **Custom `[OptimizedEnumIndex]` persistence** — per-property custom indexes defined via `[OptimizedEnumIndex]` are not mapped to database columns.
+- **Automatic schema hints** — no generated string-length, unicode, or column-type annotations. Apply these manually via fluent API if needed.
+- **Collection mapping** — e.g., `ICollection` properties are not specially handled.
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AnalyzerReleases.Shipped.md b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..9c6fa74
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AnalyzerReleases.Shipped.md
@@ -0,0 +1,2 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AnalyzerReleases.Unshipped.md b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..6ae8ecc
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,12 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+ Rule ID | Category | Severity | Notes
+---------|---------------------------|----------|-----------------------
+ OE3001 | OptimizedEnums.EFCore | Error | DiagnosticDescriptors
+ OE3002 | OptimizedEnums.EFCore | Error | DiagnosticDescriptors
+ OE3003 | OptimizedEnums.EFCore | Error | DiagnosticDescriptors
+ OE3004 | OptimizedEnums.EFCore | Error | DiagnosticDescriptors
+ OE9003 | OptimizedEnums.EFCore | Error | DiagnosticDescriptors
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AttributeSource.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AttributeSource.cs
new file mode 100644
index 0000000..dd0fb3d
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/AttributeSource.cs
@@ -0,0 +1,60 @@
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator;
+
+///
+/// Source text injected into every consuming compilation via
+/// RegisterPostInitializationOutput so that
+/// [OptimizedEnumEfCore] is available without a separate runtime assembly.
+///
+internal static class AttributeSource
+{
+ internal const string HintName = "OptimizedEnumEfCoreAttribute.g.cs";
+
+ internal const string Source = """
+ //------------------------------------------------------------------------------
+ //
+ // This code was generated by a tool.
+ //
+ // Changes to this file may cause incorrect behavior and will be lost if
+ // the code is regenerated.
+ //
+ //------------------------------------------------------------------------------
+
+ #nullable enable
+
+ namespace LayeredCraft.OptimizedEnums.EFCore
+ {
+ ///
+ /// Controls how an OptimizedEnum property is stored in the database.
+ ///
+ public enum OptimizedEnumEfCoreStorage
+ {
+ /// Store as the member's underlying Value (e.g. 1 for an int-valued enum).
+ ByValue = 0,
+
+ /// Store as the member's Name string (e.g. "Pending").
+ ByName = 1,
+ }
+
+ ///
+ /// Instructs the OptimizedEnums source generator to emit Entity Framework Core
+ /// value converters and comparer for the decorated OptimizedEnum class.
+ ///
+ [global::System.AttributeUsage(
+ global::System.AttributeTargets.Class,
+ AllowMultiple = false,
+ Inherited = false)]
+ public sealed class OptimizedEnumEfCoreAttribute : global::System.Attribute
+ {
+ /// Initializes a new instance.
+ public OptimizedEnumEfCoreAttribute(
+ OptimizedEnumEfCoreStorage storage = OptimizedEnumEfCoreStorage.ByValue)
+ {
+ Storage = storage;
+ }
+
+ /// Gets the default storage mode for this enum.
+ public OptimizedEnumEfCoreStorage Storage { get; }
+ }
+ }
+ """;
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Diagnostics/DiagnosticDescriptors.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Diagnostics/DiagnosticDescriptors.cs
new file mode 100644
index 0000000..3183d11
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Diagnostics/DiagnosticDescriptors.cs
@@ -0,0 +1,48 @@
+using Microsoft.CodeAnalysis;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator.Diagnostics;
+
+internal static class DiagnosticDescriptors
+{
+ private const string Category = "OptimizedEnums.EFCore";
+
+ internal static readonly DiagnosticDescriptor MustInheritOptimizedEnum = new(
+ "OE3001",
+ "OptimizedEnumEfCore requires an OptimizedEnum subclass",
+ "The class '{0}' must inherit from OptimizedEnum to use [OptimizedEnumEfCore]",
+ Category,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ internal static readonly DiagnosticDescriptor MustBePartial = new(
+ "OE3002",
+ "OptimizedEnum class must be partial for EF Core generation",
+ "The class '{0}' must be declared as partial for [OptimizedEnumEfCore] source generation",
+ Category,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ internal static readonly DiagnosticDescriptor UnknownStorageType = new(
+ "OE3003",
+ "Unknown OptimizedEnumEfCoreStorage value",
+ "The class '{0}' specifies an unknown OptimizedEnumEfCoreStorage value '{1}'; valid values are ByValue (0) and ByName (1)",
+ Category,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ internal static readonly DiagnosticDescriptor UnsupportedTarget = new(
+ "OE3004",
+ "Unsupported EF Core target usage",
+ "{0}",
+ Category,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ internal static readonly DiagnosticDescriptor GeneratorInternalError = new(
+ "OE9003",
+ "OptimizedEnums EFCore generator internal error",
+ "An unexpected error occurred while generating the EF Core support for '{0}': {1}",
+ Category,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Diagnostics/DiagnosticInfo.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Diagnostics/DiagnosticInfo.cs
new file mode 100644
index 0000000..c45f8a2
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Diagnostics/DiagnosticInfo.cs
@@ -0,0 +1,42 @@
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Models;
+using Microsoft.CodeAnalysis;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator.Diagnostics;
+
+internal sealed record DiagnosticInfo(
+ DiagnosticDescriptor DiagnosticDescriptor,
+ LocationInfo? LocationInfo = null,
+ params object?[] MessageArgs
+)
+{
+ public bool Equals(DiagnosticInfo? other) =>
+ other is not null
+ && DiagnosticDescriptor.Id == other.DiagnosticDescriptor.Id
+ && Equals(LocationInfo, other.LocationInfo)
+ && MessageArgs.SequenceEqual(other.MessageArgs);
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ hash.Add(DiagnosticDescriptor.Id);
+ hash.Add(LocationInfo);
+ foreach (var arg in MessageArgs)
+ hash.Add(arg);
+ return hash.ToHashCode();
+ }
+}
+
+internal static class DiagnosticInfoExtensions
+{
+ extension(DiagnosticInfo diagnosticInfo)
+ {
+ internal Diagnostic ToDiagnostic() =>
+ Diagnostic.Create(
+ diagnosticInfo.DiagnosticDescriptor,
+ diagnosticInfo.LocationInfo?.ToLocation(),
+ diagnosticInfo.MessageArgs);
+
+ internal void ReportDiagnostic(SourceProductionContext context) =>
+ context.ReportDiagnostic(diagnosticInfo.ToDiagnostic());
+ }
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Emitters/EfCoreEmitter.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Emitters/EfCoreEmitter.cs
new file mode 100644
index 0000000..43652bc
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Emitters/EfCoreEmitter.cs
@@ -0,0 +1,130 @@
+using System.Collections.Immutable;
+using System.Reflection;
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Diagnostics;
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Models;
+using Microsoft.CodeAnalysis;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator.Emitters;
+
+internal static class EfCoreEmitter
+{
+ private static string GeneratedCodeAttribute { get; } = BuildGeneratedCodeAttribute();
+
+ private static string BuildGeneratedCodeAttribute()
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ return $"""[global::System.CodeDom.Compiler.GeneratedCode("{asm.GetName().Name}", "{asm.GetName().Version}")]""";
+ }
+
+ internal static void GeneratePerEnum(SourceProductionContext context, EfCoreInfo info)
+ {
+ var converterPrefix = BuildConverterPrefix(info);
+ var extensionClassName = BuildExtensionClassName(info);
+ var hintName = info.FullyQualifiedClassName.Replace("global::", "") + ".EFCore.g.cs";
+ var namespaceLine = info.Namespace is not null ? $"namespace {info.Namespace};" : string.Empty;
+
+ // Fully-qualified converter names for use in extension methods and conventions
+ var converterFq = BuildFullyQualifiedTypeName(info, converterPrefix + "ValueConverter");
+ var nameConverterFq = BuildFullyQualifiedTypeName(info, converterPrefix + "NameConverter");
+
+ var model = new
+ {
+ GeneratedCodeAttribute,
+ NamespaceLine = namespaceLine,
+ ConverterPrefix = converterPrefix,
+ ExtensionClassName = extensionClassName,
+ info.ClassName,
+ info.FullyQualifiedClassName,
+ info.ValueTypeFullyQualified,
+ ConverterFq = converterFq,
+ NameConverterFq = nameConverterFq,
+ };
+
+ try
+ {
+ var source = TemplateHelper.Render("Templates.OptimizedEnumEfCore.scriban", model);
+ context.AddSource(hintName, source);
+ }
+ catch (Exception ex)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.GeneratorInternalError,
+ info.Location?.ToLocation(),
+ info.ClassName,
+ ex.Message));
+ }
+ }
+
+ internal static void GenerateConventions(
+ SourceProductionContext context,
+ ImmutableArray infos)
+ {
+ var enumEntries = infos
+ .Where(i => !i.Diagnostics.Any(d => d.DiagnosticDescriptor.DefaultSeverity == DiagnosticSeverity.Error))
+ .Select(i =>
+ {
+ var converterPrefix = BuildConverterPrefix(i);
+ var isByName = i.Storage == EfCoreStorage.ByName;
+ return new
+ {
+ FullyQualifiedClassName = i.FullyQualifiedClassName,
+ ConverterFq = BuildFullyQualifiedTypeName(i, converterPrefix + (isByName ? "NameConverter" : "ValueConverter")),
+ };
+ })
+ .ToArray();
+
+ var model = new
+ {
+ GeneratedCodeAttribute,
+ Enums = enumEntries,
+ };
+
+ try
+ {
+ var source = TemplateHelper.Render("Templates.OptimizedEnumEfCoreConventions.scriban", model);
+ context.AddSource("OptimizedEnumEfCoreConventions.g.cs", source);
+ }
+ catch (Exception ex)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.GeneratorInternalError,
+ null,
+ "conventions",
+ ex.Message));
+ }
+ }
+
+ private static string BuildConverterPrefix(EfCoreInfo info)
+ {
+ // For nested types: join containing type names + class name to avoid collisions
+ // e.g. Outer.Status -> "OuterStatus"
+ if (info.ContainingTypeSimpleNames.Length == 0)
+ return info.ClassName;
+
+ return string.Join("_", info.ContainingTypeSimpleNames) + "_" + info.ClassName;
+ }
+
+ private static string BuildExtensionClassName(EfCoreInfo info)
+ {
+ // Namespace segments + containing type names + class name, joined with _
+ // e.g. MyApp.Domain.OrderStatus -> "MyApp_Domain_OrderStatusEfCoreExtensions"
+ // e.g. MyApp.Domain.Outer.Status -> "MyApp_Domain_Outer_StatusEfCoreExtensions"
+ // e.g. Priority (global ns) -> "PriorityEfCoreExtensions"
+ var parts = new List();
+ if (info.Namespace is not null)
+ parts.AddRange(info.Namespace.Split('.'));
+ parts.AddRange(info.ContainingTypeSimpleNames);
+ parts.Add(info.ClassName);
+ return string.Join("_", parts) + "EfCoreExtensions";
+ }
+
+ private static string BuildFullyQualifiedTypeName(EfCoreInfo info, string typeName)
+ {
+ // Generated converter/extension classes are always emitted at namespace scope.
+ // e.g. global::MyApp.Domain.OrderStatusValueConverter
+ // e.g. global::MyApp.Domain.Outer_StatusValueConverter (nested enum — still namespace-scoped)
+ if (info.Namespace is null)
+ return $"global::{typeName}";
+ return $"global::{info.Namespace}.{typeName}";
+ }
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Emitters/TemplateHelper.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Emitters/TemplateHelper.cs
new file mode 100644
index 0000000..09e9121
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Emitters/TemplateHelper.cs
@@ -0,0 +1,60 @@
+using System.Collections.Concurrent;
+using System.Reflection;
+using Scriban;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator.Emitters;
+
+internal static class TemplateHelper
+{
+ private static readonly ConcurrentDictionary Cache = new();
+
+ internal static string Render(string resourceName, TModel model)
+ {
+ var template = Cache.GetOrAdd(resourceName, LoadTemplate);
+ return template.Render(model);
+ }
+
+ private static Template LoadTemplate(string relativePath)
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ var baseName = assembly.GetName().Name;
+
+ var templateName = relativePath
+ .TrimStart('.')
+ .Replace(Path.DirectorySeparatorChar, '.')
+ .Replace(Path.AltDirectorySeparatorChar, '.');
+
+ var manifestTemplateName = assembly
+ .GetManifestResourceNames()
+ .FirstOrDefault(x => x.EndsWith(templateName, StringComparison.Ordinal));
+
+ if (string.IsNullOrEmpty(manifestTemplateName))
+ {
+ var availableResources = string.Join(", ", assembly.GetManifestResourceNames());
+ throw new InvalidOperationException(
+ $"Did not find required resource ending in '{templateName}' in assembly '{baseName}'. "
+ + $"Available resources: {availableResources}");
+ }
+
+ using var stream = assembly.GetManifestResourceStream(manifestTemplateName);
+ if (stream == null)
+ throw new FileNotFoundException(
+ $"Template '{relativePath}' not found in embedded resources. "
+ + $"Manifest resource name: '{manifestTemplateName}'");
+
+ using var reader = new StreamReader(stream);
+ var templateContent = reader.ReadToEnd();
+
+ var template = Template.Parse(templateContent, relativePath);
+ if (!template.HasErrors)
+ return template;
+
+ var errors = string.Join(
+ "\n",
+ template.Messages.Select(m =>
+ $"{relativePath}({m.Span.Start.Line},{m.Span.Start.Column}): {m.Message}"));
+
+ throw new InvalidOperationException(
+ $"Failed to parse template '{relativePath}':\n{errors}");
+ }
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/LayeredCraft.OptimizedEnums.EFCore.Generator.csproj b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/LayeredCraft.OptimizedEnums.EFCore.Generator.csproj
new file mode 100644
index 0000000..559682b
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/LayeredCraft.OptimizedEnums.EFCore.Generator.csproj
@@ -0,0 +1,64 @@
+
+
+ netstandard2.0
+ enable
+ latest
+ $(DefineConstants);SCRIBAN_NO_SYSTEM_TEXT_JSON
+ false
+ true
+ true
+ true
+ LayeredCraft.OptimizedEnums.EFCore
+ LayeredCraft.OptimizedEnums.EFCore.Generator
+ LayeredCraft.OptimizedEnums.EFCore.Generator
+ LayeredCraft.OptimizedEnums.EFCore
+ Entity Framework Core source-generated converters for LayeredCraft.OptimizedEnums. Decorate your OptimizedEnum class with [OptimizedEnumEfCore] to get zero-reflection, AOT-safe EF Core value converters and comparer generated automatically.
+ enum;source-generator;smart-enum;dotnet;csharp;efcore;entity-framework;aot
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/EfCoreInfo.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/EfCoreInfo.cs
new file mode 100644
index 0000000..f5e1e9c
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/EfCoreInfo.cs
@@ -0,0 +1,42 @@
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Diagnostics;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator.Models;
+
+internal enum EfCoreStorage
+{
+ ByValue = 0,
+ ByName = 1,
+}
+
+internal sealed record EfCoreInfo(
+ string? Namespace,
+ string ClassName,
+ string FullyQualifiedClassName,
+ string ValueTypeFullyQualified,
+ bool ValueTypeIsReferenceType,
+ EquatableArray ContainingTypeSimpleNames,
+ EquatableArray ContainingTypeDeclarations,
+ EfCoreStorage Storage,
+ EquatableArray Diagnostics,
+ LocationInfo? Location
+)
+{
+ // Location intentionally excluded from equality — position-only changes should not
+ // bust the incremental cache and trigger unnecessary re-emission.
+ public bool Equals(EfCoreInfo? other) =>
+ other is not null
+ && Namespace == other.Namespace
+ && ClassName == other.ClassName
+ && FullyQualifiedClassName == other.FullyQualifiedClassName
+ && ValueTypeFullyQualified == other.ValueTypeFullyQualified
+ && ValueTypeIsReferenceType == other.ValueTypeIsReferenceType
+ && ContainingTypeSimpleNames == other.ContainingTypeSimpleNames
+ && ContainingTypeDeclarations == other.ContainingTypeDeclarations
+ && Storage == other.Storage
+ && Diagnostics == other.Diagnostics;
+
+ public override int GetHashCode() =>
+ HashCode.Combine(
+ HashCode.Combine(Namespace, ClassName, FullyQualifiedClassName, ValueTypeFullyQualified),
+ HashCode.Combine(ValueTypeIsReferenceType, ContainingTypeSimpleNames, ContainingTypeDeclarations, Storage, Diagnostics));
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/EquatableArray.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/EquatableArray.cs
new file mode 100644
index 0000000..8278069
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/EquatableArray.cs
@@ -0,0 +1,50 @@
+using System.Collections;
+using System.Collections.Immutable;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator.Models;
+
+internal readonly struct EquatableArray : IEquatable>, IEnumerable
+ where T : IEquatable
+{
+ private readonly ImmutableArray _array;
+
+ public EquatableArray(ImmutableArray array) => _array = array;
+
+ public static readonly EquatableArray Empty = new(ImmutableArray.Empty);
+
+ public int Length => _array.Length;
+
+ public T this[int index] => _array[index];
+
+ public bool Equals(EquatableArray other) => _array.SequenceEqual(other._array);
+
+ public override bool Equals(object? obj) =>
+ obj is EquatableArray other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ foreach (var item in _array)
+ hash.Add(item);
+ return hash.ToHashCode();
+ }
+
+ public T[] ToArray() => _array.IsDefaultOrEmpty ? Array.Empty() : _array.ToArray();
+
+ public ImmutableArray.Enumerator GetEnumerator() => _array.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_array).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_array).GetEnumerator();
+
+ public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right);
+
+ public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right);
+}
+
+internal static class EquatableArrayExtensions
+{
+ internal static EquatableArray ToEquatableArray(this IEnumerable source)
+ where T : IEquatable =>
+ new(source.ToImmutableArray());
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/LocationInfo.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/LocationInfo.cs
new file mode 100644
index 0000000..d009ca7
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Models/LocationInfo.cs
@@ -0,0 +1,38 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator.Models;
+
+internal sealed record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan);
+
+internal static class LocationInfoExtensions
+{
+ extension(LocationInfo locationInfo)
+ {
+ internal Location ToLocation() =>
+ Location.Create(locationInfo.FilePath, locationInfo.TextSpan, locationInfo.LineSpan);
+ }
+
+ extension(Location location)
+ {
+ internal LocationInfo? CreateLocationInfo() =>
+ location.SourceTree is null
+ ? null
+ : new LocationInfo(
+ location.SourceTree.FilePath,
+ location.SourceSpan,
+ location.GetLineSpan().Span);
+ }
+
+ extension(ISymbol symbol)
+ {
+ internal LocationInfo? CreateLocationInfo() =>
+ symbol.Locations.FirstOrDefault()?.CreateLocationInfo();
+ }
+
+ extension(SyntaxNode syntaxNode)
+ {
+ internal LocationInfo? CreateLocationInfo() =>
+ syntaxNode.GetLocation().CreateLocationInfo();
+ }
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/OptimizedEnumEfCoreGenerator.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/OptimizedEnumEfCoreGenerator.cs
new file mode 100644
index 0000000..dde74f7
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/OptimizedEnumEfCoreGenerator.cs
@@ -0,0 +1,49 @@
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Diagnostics;
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Emitters;
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Providers;
+using Microsoft.CodeAnalysis;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator;
+
+/// Source generator that emits EF Core value converters for OptimizedEnum types.
+[Generator]
+public sealed class OptimizedEnumEfCoreGenerator : IIncrementalGenerator
+{
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ context.RegisterPostInitializationOutput(static ctx =>
+ ctx.AddSource(AttributeSource.HintName, AttributeSource.Source));
+
+ var efCoreInfos = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ EfCoreSyntaxProvider.AttributeMetadataName,
+ EfCoreSyntaxProvider.Predicate,
+ EfCoreSyntaxProvider.Transform)
+ .WithTrackingName(TrackingNames.EfCoreSyntaxProvider_Extract)
+ .Where(static x => x is not null)
+ .Select(static (x, _) => x!)
+ .WithTrackingName(TrackingNames.EfCoreSyntaxProvider_FilterNotNull);
+
+ // Per-enum: emit converters, comparer, and enum-specific extension methods
+ context.RegisterSourceOutput(efCoreInfos, static (ctx, info) =>
+ {
+ foreach (var diagnostic in info.Diagnostics)
+ diagnostic.ReportDiagnostic(ctx);
+
+ if (info.Diagnostics.Any(d => d.DiagnosticDescriptor.DefaultSeverity == DiagnosticSeverity.Error))
+ return;
+
+ EfCoreEmitter.GeneratePerEnum(ctx, info);
+ });
+
+ // Shared: emit the ConfigureOptimizedEnums() convention hook once per compilation,
+ // collecting all valid opted-in enums.
+ var collected = efCoreInfos
+ .Collect()
+ .WithTrackingName(TrackingNames.EfCoreSyntaxProvider_Collect);
+
+ context.RegisterSourceOutput(collected, static (ctx, infos) =>
+ EfCoreEmitter.GenerateConventions(ctx, infos));
+ }
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Providers/EfCoreSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Providers/EfCoreSyntaxProvider.cs
new file mode 100644
index 0000000..fccd1f4
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Providers/EfCoreSyntaxProvider.cs
@@ -0,0 +1,239 @@
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Diagnostics;
+using LayeredCraft.OptimizedEnums.EFCore.Generator.Models;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator.Providers;
+
+internal static class EfCoreSyntaxProvider
+{
+ internal const string AttributeMetadataName =
+ "LayeredCraft.OptimizedEnums.EFCore.OptimizedEnumEfCoreAttribute";
+
+ private const string OptimizedEnumBaseMetadataName =
+ "LayeredCraft.OptimizedEnums.OptimizedEnum`2";
+
+ internal static bool Predicate(SyntaxNode node, CancellationToken _) =>
+ node is ClassDeclarationSyntax;
+
+ internal static EfCoreInfo? Transform(
+ GeneratorAttributeSyntaxContext context,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (context.TargetNode is not ClassDeclarationSyntax classDecl)
+ return null;
+
+ if (context.TargetSymbol is not INamedTypeSymbol classSymbol)
+ return null;
+
+ var attr = context.Attributes[0];
+ var diagnostics = new List();
+ var location = classDecl.CreateLocationInfo();
+ var className = classSymbol.Name;
+
+ // OE3004: abstract classes are not supported
+ if (classSymbol.IsAbstract)
+ {
+ diagnostics.Add(new DiagnosticInfo(
+ DiagnosticDescriptors.UnsupportedTarget,
+ location,
+ $"[OptimizedEnumEfCore] cannot be applied to abstract class '{className}'. Apply the attribute to concrete sealed partial derived classes."));
+
+ return new EfCoreInfo(
+ Namespace: null,
+ ClassName: className,
+ FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ ValueTypeFullyQualified: string.Empty,
+ ValueTypeIsReferenceType: false,
+ ContainingTypeSimpleNames: EquatableArray.Empty,
+ ContainingTypeDeclarations: EquatableArray.Empty,
+ Storage: EfCoreStorage.ByValue,
+ Diagnostics: diagnostics.ToEquatableArray(),
+ Location: location);
+ }
+
+ // OE3004: enums nested inside generic containing types cannot have converters generated.
+ // Converters and extension methods are emitted at namespace scope; generic type parameters
+ // from the containing type would not be in scope there, producing uncompilable references.
+ var genericContainer = FindGenericContainingType(classSymbol);
+ if (genericContainer is not null)
+ {
+ diagnostics.Add(new DiagnosticInfo(
+ DiagnosticDescriptors.UnsupportedTarget,
+ location,
+ $"[OptimizedEnumEfCore] cannot be applied to '{className}' because its containing type '{genericContainer.Name}' is generic. EF Core converter generation for enums nested inside generic types is not supported in v1."));
+
+ return new EfCoreInfo(
+ Namespace: null,
+ ClassName: className,
+ FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ ValueTypeFullyQualified: string.Empty,
+ ValueTypeIsReferenceType: false,
+ ContainingTypeSimpleNames: EquatableArray.Empty,
+ ContainingTypeDeclarations: EquatableArray.Empty,
+ Storage: EfCoreStorage.ByValue,
+ Diagnostics: diagnostics.ToEquatableArray(),
+ Location: location);
+ }
+
+ // OE3001: must inherit from OptimizedEnum<,>
+ var baseType = FindOptimizedEnumBase(classSymbol, context.SemanticModel.Compilation);
+ if (baseType is null)
+ {
+ diagnostics.Add(new DiagnosticInfo(
+ DiagnosticDescriptors.MustInheritOptimizedEnum,
+ location,
+ className));
+
+ return new EfCoreInfo(
+ Namespace: null,
+ ClassName: className,
+ FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ ValueTypeFullyQualified: string.Empty,
+ ValueTypeIsReferenceType: false,
+ ContainingTypeSimpleNames: EquatableArray.Empty,
+ ContainingTypeDeclarations: EquatableArray.Empty,
+ Storage: EfCoreStorage.ByValue,
+ Diagnostics: diagnostics.ToEquatableArray(),
+ Location: location);
+ }
+
+ // OE3002: must be partial
+ if (!classDecl.Modifiers.Any(static m => m.IsKind(SyntaxKind.PartialKeyword)))
+ {
+ diagnostics.Add(new DiagnosticInfo(
+ DiagnosticDescriptors.MustBePartial,
+ location,
+ className));
+
+ return new EfCoreInfo(
+ Namespace: null,
+ ClassName: className,
+ FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ ValueTypeFullyQualified: string.Empty,
+ ValueTypeIsReferenceType: false,
+ ContainingTypeSimpleNames: EquatableArray.Empty,
+ ContainingTypeDeclarations: EquatableArray.Empty,
+ Storage: EfCoreStorage.ByValue,
+ Diagnostics: diagnostics.ToEquatableArray(),
+ Location: location);
+ }
+
+ // Read Storage from the attribute constructor argument (defaults to ByValue = 0)
+ var storage = EfCoreStorage.ByValue;
+ if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is int rawValue)
+ {
+ if (rawValue != (int)EfCoreStorage.ByValue && rawValue != (int)EfCoreStorage.ByName)
+ {
+ diagnostics.Add(new DiagnosticInfo(
+ DiagnosticDescriptors.UnknownStorageType,
+ location,
+ className,
+ rawValue));
+
+ return new EfCoreInfo(
+ Namespace: null,
+ ClassName: className,
+ FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ ValueTypeFullyQualified: string.Empty,
+ ValueTypeIsReferenceType: false,
+ ContainingTypeSimpleNames: EquatableArray.Empty,
+ ContainingTypeDeclarations: EquatableArray.Empty,
+ Storage: EfCoreStorage.ByValue,
+ Diagnostics: diagnostics.ToEquatableArray(),
+ Location: location);
+ }
+
+ storage = (EfCoreStorage)rawValue;
+ }
+
+ var valueTypeSymbol = baseType.TypeArguments[1];
+
+ return new EfCoreInfo(
+ Namespace: GetNamespace(classSymbol),
+ ClassName: className,
+ FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ ValueTypeFullyQualified: valueTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ ValueTypeIsReferenceType: valueTypeSymbol.IsReferenceType,
+ ContainingTypeSimpleNames: GetContainingTypeSimpleNames(classSymbol),
+ ContainingTypeDeclarations: GetContainingTypeDeclarations(classSymbol),
+ Storage: storage,
+ Diagnostics: diagnostics.ToEquatableArray(),
+ Location: location);
+ }
+
+ private static INamedTypeSymbol? FindOptimizedEnumBase(
+ INamedTypeSymbol classSymbol,
+ Compilation compilation)
+ {
+ var optimizedEnumBase = compilation.GetTypeByMetadataName(OptimizedEnumBaseMetadataName);
+ if (optimizedEnumBase is null)
+ return null;
+
+ var current = classSymbol.BaseType;
+ while (current is not null)
+ {
+ if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, optimizedEnumBase))
+ return current;
+ current = current.BaseType;
+ }
+
+ return null;
+ }
+
+ private static INamedTypeSymbol? FindGenericContainingType(INamedTypeSymbol symbol)
+ {
+ var current = symbol.ContainingType;
+ while (current is not null)
+ {
+ if (current.TypeParameters.Length > 0)
+ return current;
+ current = current.ContainingType;
+ }
+ return null;
+ }
+
+ private static EquatableArray GetContainingTypeSimpleNames(INamedTypeSymbol symbol)
+ {
+ var result = new List();
+ var current = symbol.ContainingType;
+ while (current is not null)
+ {
+ result.Add(current.Name);
+ current = current.ContainingType;
+ }
+ result.Reverse();
+ return result.ToEquatableArray();
+ }
+
+ private static EquatableArray GetContainingTypeDeclarations(INamedTypeSymbol symbol)
+ {
+ var result = new List();
+ var current = symbol.ContainingType;
+ while (current is not null)
+ {
+ var keyword = (current.IsRecord, current.TypeKind) switch
+ {
+ (true, TypeKind.Struct) => "record struct",
+ (true, _) => "record",
+ (_, TypeKind.Struct) => "struct",
+ (_, TypeKind.Interface) => "interface",
+ _ => "class"
+ };
+ var staticModifier = current.IsStatic ? "static " : "";
+ var nameWithTypeParams = current.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
+ result.Add($"partial {staticModifier}{keyword} {nameWithTypeParams}");
+ current = current.ContainingType;
+ }
+ result.Reverse();
+ return result.ToEquatableArray();
+ }
+
+ private static string? GetNamespace(INamedTypeSymbol symbol) =>
+ symbol.ContainingNamespace.IsGlobalNamespace
+ ? null
+ : symbol.ContainingNamespace.ToDisplayString();
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Templates/OptimizedEnumEfCore.scriban b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Templates/OptimizedEnumEfCore.scriban
new file mode 100644
index 0000000..2fc9589
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Templates/OptimizedEnumEfCore.scriban
@@ -0,0 +1,60 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+{{ if namespace_line != "" }}
+{{ namespace_line }}
+{{ end }}
+{{ generated_code_attribute }}
+internal sealed class {{ converter_prefix }}ValueConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<{{ fully_qualified_class_name }}, {{ value_type_fully_qualified }}>
+{
+ public {{ converter_prefix }}ValueConverter()
+ : base(static v => v.Value, static v => FromValue(v))
+ { }
+
+ private static {{ fully_qualified_class_name }} FromValue({{ value_type_fully_qualified }} v) =>
+ {{ fully_qualified_class_name }}.TryFromValue(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid value for {{ class_name }}.");
+}
+
+{{ generated_code_attribute }}
+internal sealed class {{ converter_prefix }}NameConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<{{ fully_qualified_class_name }}, global::System.String>
+{
+ public {{ converter_prefix }}NameConverter()
+ : base(static v => v.Name, static v => FromName(v))
+ { }
+
+ private static {{ fully_qualified_class_name }} FromName(global::System.String v) =>
+ {{ fully_qualified_class_name }}.TryFromName(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid name for {{ class_name }}.");
+}
+
+{{ generated_code_attribute }}
+internal static class {{ extension_class_name }}
+{
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder<{{ fully_qualified_class_name }}> Has{{ class_name }}ConversionByValue(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder<{{ fully_qualified_class_name }}> builder)
+ {
+ builder.HasConversion<{{ converter_fq }}>();
+ return builder;
+ }
+
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder<{{ fully_qualified_class_name }}> Has{{ class_name }}ConversionByName(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder<{{ fully_qualified_class_name }}> builder)
+ {
+ builder.HasConversion<{{ name_converter_fq }}>();
+ return builder;
+ }
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Templates/OptimizedEnumEfCoreConventions.scriban b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Templates/OptimizedEnumEfCoreConventions.scriban
new file mode 100644
index 0000000..f68060c
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/Templates/OptimizedEnumEfCoreConventions.scriban
@@ -0,0 +1,27 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ {{ generated_code_attribute }}
+ public static class OptimizedEnumEfCoreConventionExtensions
+ {
+ public static global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder ConfigureOptimizedEnums(
+ this global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder builder)
+ {
+ {{ for enum in enums }}
+ builder.Properties<{{ enum.fully_qualified_class_name }}>()
+ .HaveConversion<{{ enum.converter_fq }}>();
+ {{ end }}
+ return builder;
+ }
+ }
+}
diff --git a/src/LayeredCraft.OptimizedEnums.EFCore.Generator/TrackingNames.cs b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/TrackingNames.cs
new file mode 100644
index 0000000..6c5e54c
--- /dev/null
+++ b/src/LayeredCraft.OptimizedEnums.EFCore.Generator/TrackingNames.cs
@@ -0,0 +1,10 @@
+// ReSharper disable InconsistentNaming
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Generator;
+
+internal static class TrackingNames
+{
+ internal const string EfCoreSyntaxProvider_Extract = nameof(EfCoreSyntaxProvider_Extract);
+ internal const string EfCoreSyntaxProvider_FilterNotNull = nameof(EfCoreSyntaxProvider_FilterNotNull);
+ internal const string EfCoreSyntaxProvider_Collect = nameof(EfCoreSyntaxProvider_Collect);
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/GeneratorTests/GeneratorTestHelpers.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/GeneratorTests/GeneratorTestHelpers.cs
new file mode 100644
index 0000000..a7ee618
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/GeneratorTests/GeneratorTestHelpers.cs
@@ -0,0 +1,148 @@
+using System.Text.RegularExpressions;
+using Basic.Reference.Assemblies;
+using LayeredCraft.OptimizedEnums;
+using LayeredCraft.OptimizedEnums.EFCore.Generator;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Tests.GeneratorTests;
+
+internal class VerifyTestOptions
+{
+ internal required string SourceCode { get; init; }
+ internal string CodePath { get; init; } = "Program.cs";
+ internal LanguageVersion LanguageVersion { get; init; } = LanguageVersion.CSharp13;
+ internal string AssemblyName { get; init; } = "TestsAssembly";
+ internal string? ExpectedDiagnosticId { get; init; }
+ internal int? ExpectedTrees { get; init; }
+}
+
+internal static class GeneratorTestHelpers
+{
+ internal static Task Verify(VerifyTestOptions options, CancellationToken cancellationToken = default)
+ {
+ var (driver, originalCompilation) = GenerateFromSource(options, cancellationToken);
+
+ var result = driver.GetRunResult();
+
+ result.Diagnostics
+ .Should()
+ .BeEmpty(
+ "code should be generated without errors, but found:\n"
+ + string.Join("\n---\n", result.Diagnostics.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}")));
+
+ var parseOptions = originalCompilation.SyntaxTrees.First().Options;
+ var reparsedTrees = result.GeneratedTrees
+ .Select(tree => CSharpSyntaxTree.ParseText(tree.GetText(), (CSharpParseOptions)parseOptions))
+ .ToArray();
+
+ var outputCompilation = originalCompilation.AddSyntaxTrees(reparsedTrees);
+ var errors = outputCompilation
+ .GetDiagnostics(cancellationToken)
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .ToList();
+
+ errors.Should().BeEmpty(
+ "generated code should compile without errors, but found:\n"
+ + string.Join("\n---\n", errors.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}")));
+
+ if (options.ExpectedTrees is not null)
+ result.GeneratedTrees.Length.Should().Be(options.ExpectedTrees);
+
+ return Verifier
+ .Verify(driver)
+ .UseDirectory("../Snapshots")
+ .DisableDiff()
+ .ScrubLinesWithReplace(line =>
+ {
+ if (line.Contains("global::System.CodeDom.Compiler.GeneratedCode"))
+ return RegexHelper.GeneratedCodeAttributeRegex().Replace(line, "REPLACED");
+ return line;
+ });
+ }
+
+ internal static Task VerifyFailure(VerifyTestOptions options, CancellationToken cancellationToken = default)
+ {
+ var (driver, _) = GenerateFromSource(options, cancellationToken);
+
+ var result = driver.GetRunResult();
+
+ result.Diagnostics.Should().NotBeEmpty("expected diagnostic errors to be generated");
+
+ if (options.ExpectedDiagnosticId is not null)
+ {
+ result.Diagnostics
+ .Should()
+ .Contain(
+ d => d.Id == options.ExpectedDiagnosticId,
+ $"expected diagnostic {options.ExpectedDiagnosticId} to be present, but found:\n"
+ + string.Join("\n---\n", result.Diagnostics.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}")));
+ }
+
+ return Verifier
+ .Verify(driver)
+ .UseDirectory("../Snapshots")
+ .DisableDiff()
+ .ScrubLinesWithReplace(line =>
+ {
+ if (line.Contains("global::System.CodeDom.Compiler.GeneratedCode"))
+ return RegexHelper.GeneratedCodeAttributeRegex().Replace(line, "REPLACED");
+ return line;
+ });
+ }
+
+ private static (GeneratorDriver driver, Compilation compilation) GenerateFromSource(
+ VerifyTestOptions options,
+ CancellationToken cancellationToken = default)
+ {
+ var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(options.LanguageVersion);
+
+ var syntaxTree = CSharpSyntaxTree.ParseText(
+ options.SourceCode,
+ parseOptions,
+ options.CodePath,
+ cancellationToken: cancellationToken);
+
+ List references =
+ [
+#if NET10_0_OR_GREATER
+ .. Net100.References.All.ToList(),
+#elif NET9_0
+ .. Net90.References.All.ToList(),
+#else
+ .. Net80.References.All.ToList(),
+#endif
+ MetadataReference.CreateFromFile(typeof(OptimizedEnum<,>).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Microsoft.EntityFrameworkCore.DbContext).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder).Assembly.Location),
+ ];
+
+ var compilation = CSharpCompilation.Create(
+ options.AssemblyName,
+ [syntaxTree],
+ references,
+ new CSharpCompilationOptions(
+ OutputKind.DynamicallyLinkedLibrary,
+ nullableContextOptions: NullableContextOptions.Enable));
+
+ // Run both generators: the main one produces FromName/FromValue,
+ // the EFCore one produces the converters and extensions.
+ var driver = CSharpGeneratorDriver.Create(
+ generators:
+ [
+ new LayeredCraft.OptimizedEnums.Generator.OptimizedEnumGenerator().AsSourceGenerator(),
+ new OptimizedEnumEfCoreGenerator().AsSourceGenerator(),
+ ],
+ parseOptions: parseOptions);
+
+ var updatedDriver = driver.RunGenerators(compilation, cancellationToken);
+
+ return (updatedDriver, compilation);
+ }
+}
+
+internal static partial class RegexHelper
+{
+ [GeneratedRegex("""(\d+\.\d+\.\d+\.\d+)""", RegexOptions.None, "en-US")]
+ internal static partial Regex GeneratedCodeAttributeRegex();
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/GeneratorTests/GeneratorVerifyTests.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/GeneratorTests/GeneratorVerifyTests.cs
new file mode 100644
index 0000000..9fbfbf7
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/GeneratorTests/GeneratorVerifyTests.cs
@@ -0,0 +1,294 @@
+namespace LayeredCraft.OptimizedEnums.EFCore.Tests.GeneratorTests;
+
+public class GeneratorVerifyTests
+{
+ [Fact]
+ public async Task ByValue_WithNamespace() =>
+ await GeneratorTestHelpers.Verify(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+ public sealed partial class OrderStatus : OptimizedEnum
+ {
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+ public static readonly OrderStatus Paid = new(2, nameof(Paid));
+ public static readonly OrderStatus Shipped = new(3, nameof(Shipped));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+ }
+ """,
+ // core enum .g.cs + EFCore per-enum .g.cs + attribute .g.cs + conventions .g.cs
+ ExpectedTrees = 4,
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task ByName_WithNamespace() =>
+ await GeneratorTestHelpers.Verify(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByName)]
+ public sealed partial class OrderStatus : OptimizedEnum
+ {
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+ public static readonly OrderStatus Paid = new(2, nameof(Paid));
+ public static readonly OrderStatus Shipped = new(3, nameof(Shipped));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedTrees = 4,
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task ByValue_GlobalNamespace() =>
+ await GeneratorTestHelpers.Verify(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+ public sealed partial class Priority : OptimizedEnum
+ {
+ public static readonly Priority Low = new(1, nameof(Low));
+ public static readonly Priority Medium = new(2, nameof(Medium));
+ public static readonly Priority High = new(3, nameof(High));
+
+ private Priority(int value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedTrees = 4,
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task ByName_GlobalNamespace() =>
+ await GeneratorTestHelpers.Verify(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByName)]
+ public sealed partial class Priority : OptimizedEnum
+ {
+ public static readonly Priority Low = new(1, nameof(Low));
+ public static readonly Priority Medium = new(2, nameof(Medium));
+ public static readonly Priority High = new(3, nameof(High));
+
+ private Priority(int value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedTrees = 4,
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task ByValue_StringValueType() =>
+ await GeneratorTestHelpers.Verify(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+ public sealed partial class Color : OptimizedEnum
+ {
+ public static readonly Color Red = new("red", nameof(Red));
+ public static readonly Color Green = new("green", nameof(Green));
+ public static readonly Color Blue = new("blue", nameof(Blue));
+
+ private Color(string value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedTrees = 4,
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task ByName_StringValueType() =>
+ await GeneratorTestHelpers.Verify(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByName)]
+ public sealed partial class Color : OptimizedEnum
+ {
+ public static readonly Color Red = new("red", nameof(Red));
+ public static readonly Color Green = new("green", nameof(Green));
+ public static readonly Color Blue = new("blue", nameof(Blue));
+
+ private Color(string value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedTrees = 4,
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task NestedType() =>
+ await GeneratorTestHelpers.Verify(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ public partial class Outer
+ {
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+ public sealed partial class Status : OptimizedEnum
+ {
+ public static readonly Status Active = new(1, nameof(Active));
+ public static readonly Status Inactive = new(2, nameof(Inactive));
+
+ private Status(int value, string name) : base(value, name) { }
+ }
+ }
+ """,
+ ExpectedTrees = 4,
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task IntermediateAbstractBase() =>
+ await GeneratorTestHelpers.Verify(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ public abstract partial class OrderStatusBase : OptimizedEnum
+ where TEnum : OptimizedEnum
+ {
+ protected OrderStatusBase(int value, string name) : base(value, name) { }
+ }
+
+ [OptimizedEnumEfCore(OptimizedEnumEfCoreStorage.ByValue)]
+ public sealed partial class OrderStatus : OrderStatusBase
+ {
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+ public static readonly OrderStatus Paid = new(2, nameof(Paid));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedTrees = 4,
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task Error_NotOptimizedEnum() =>
+ await GeneratorTestHelpers.VerifyFailure(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ [OptimizedEnumEfCore]
+ public sealed partial class NotAnEnum
+ {
+ }
+ """,
+ ExpectedDiagnosticId = "OE3001",
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task Error_NotPartial() =>
+ await GeneratorTestHelpers.VerifyFailure(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ [OptimizedEnumEfCore]
+ public sealed class OrderStatus : OptimizedEnum
+ {
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedDiagnosticId = "OE3002",
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task Error_UnknownStorageType() =>
+ await GeneratorTestHelpers.VerifyFailure(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ [OptimizedEnumEfCore((OptimizedEnumEfCoreStorage)99)]
+ public sealed partial class OrderStatus : OptimizedEnum
+ {
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedDiagnosticId = "OE3003",
+ },
+ TestContext.Current.CancellationToken);
+
+ [Fact]
+ public async Task Error_AbstractClass() =>
+ await GeneratorTestHelpers.VerifyFailure(
+ new VerifyTestOptions
+ {
+ SourceCode = """
+ using LayeredCraft.OptimizedEnums;
+ using LayeredCraft.OptimizedEnums.EFCore;
+
+ namespace MyApp.Domain;
+
+ [OptimizedEnumEfCore]
+ public abstract partial class OrderStatusBase : OptimizedEnum
+ {
+ protected OrderStatusBase(int value, string name) : base(value, name) { }
+ }
+ """,
+ ExpectedDiagnosticId = "OE3004",
+ },
+ TestContext.Current.CancellationToken);
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/ConversionTests.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/ConversionTests.cs
new file mode 100644
index 0000000..27b287f
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/ConversionTests.cs
@@ -0,0 +1,197 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Tests.IntegrationTests;
+
+///
+/// Conversion, null behavior, and API surface tests using the InMemory provider.
+///
+public class ConversionTests
+{
+ private static TestDbContext CreateContext(Action? modelConfig = null)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+ return new TestDbContext(options, modelConfig);
+ }
+
+ // ── ByValue conversion ─────────────────────────────────────────────────
+
+ [Fact]
+ public async Task ByValue_SaveAndLoad_RoundTrips()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, Status = OrderStatus.Paid });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.Status.Should().Be(OrderStatus.Paid);
+ }
+
+ // ── ByName conversion ──────────────────────────────────────────────────
+
+ [Fact]
+ public async Task ByName_SaveAndLoad_RoundTrips()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, Currency = Currency.Eur });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.Currency.Should().Be(Currency.Eur);
+ }
+
+ // ── Global convention hook ─────────────────────────────────────────────
+
+ [Fact]
+ public async Task GlobalConvention_AppliesEnumDefault()
+ {
+ // The convention converters registered in TestDbContext.ConfigureConventions
+ // mirror what the generated ConfigureOptimizedEnums() extension does.
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, Status = OrderStatus.Shipped });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.Status.Should().Be(OrderStatus.Shipped);
+ }
+
+ // ── Property override supersedes convention ────────────────────────────
+
+ [Fact]
+ public async Task PropertyOverride_ByName_SupersedesConvention()
+ {
+ // Override the convention ByValue converter with an explicit ByName converter
+ // on a single property — mirrors what HasOrderStatusConversionByName() would do.
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext(builder =>
+ {
+ builder.Entity()
+ .Property(x => x.Status)
+ .HasConversion(new OrderStatusByNameConverter());
+ });
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, Status = OrderStatus.Pending });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.Status.Should().Be(OrderStatus.Pending);
+ }
+
+ // ── Nullable properties ────────────────────────────────────────────────
+
+ [Fact]
+ public async Task NullableProperty_NullValue_RoundTripsNull()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, OptionalStatus = null });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.OptionalStatus.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task NullableProperty_NonNullValue_RoundTrips()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, OptionalStatus = OrderStatus.Paid });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.OptionalStatus.Should().Be(OrderStatus.Paid);
+ }
+
+ // ── Intermediate abstract base ─────────────────────────────────────────
+
+ [Fact]
+ public async Task IntermediateAbstractBase_SaveAndLoad_RoundTrips()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, ShipmentState = ShipmentState.Delivered });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.ShipmentState.Should().Be(ShipmentState.Delivered);
+ }
+
+ // ── Explicit property-level converter helpers ──────────────────────────
+ // These verify that attaching a specific converter on a single property
+ // works correctly — equivalent to the generated HasXxxConversionByValue/ByName
+ // extension methods.
+
+ [Fact]
+ public async Task ExplicitPropertyConverter_ByValue_Works()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext(builder =>
+ {
+ builder.Entity()
+ .Property(x => x.Status)
+ .HasConversion(new OrderStatusByValueConverter());
+ });
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, Status = OrderStatus.Shipped });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.Status.Should().Be(OrderStatus.Shipped);
+ }
+
+ [Fact]
+ public async Task ExplicitPropertyConverter_ByName_Works()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext(builder =>
+ {
+ builder.Entity()
+ .Property(x => x.Status)
+ .HasConversion(new OrderStatusByNameConverter());
+ });
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.Orders.Add(new Order { Id = 1, Status = OrderStatus.Paid });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.Orders.FindAsync([1], ct);
+ loaded!.Status.Should().Be(OrderStatus.Paid);
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/RelationalTests.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/RelationalTests.cs
new file mode 100644
index 0000000..4bf98d2
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/RelationalTests.cs
@@ -0,0 +1,116 @@
+using DotNet.Testcontainers.Builders;
+using Microsoft.EntityFrameworkCore;
+using Testcontainers.PostgreSql;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Tests.IntegrationTests;
+
+///
+/// Relational model tests (PK, FK, alternate key, index) using PostgreSQL via Testcontainers.
+///
+public class RelationalTests : IAsyncLifetime
+{
+ private PostgreSqlContainer _postgres = null!;
+ private string _connectionString = null!;
+
+ public async ValueTask InitializeAsync()
+ {
+ _postgres = new PostgreSqlBuilder()
+ .WithImage("postgres:16-alpine")
+ .Build();
+
+ await _postgres.StartAsync();
+ _connectionString = _postgres.GetConnectionString();
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await _postgres.DisposeAsync();
+ }
+
+ private RelationalTestDbContext CreateContext()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseNpgsql(_connectionString)
+ .Options;
+ return new RelationalTestDbContext(options);
+ }
+
+ [Fact]
+ public async Task PrimaryKey_AsEnum_WorksCorrectly()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.RelationalOrders.Add(new RelationalOrder
+ {
+ Id = OrderStatus.Pending,
+ AlternateKey = OrderStatus.Paid,
+ IndexedStatus = OrderStatus.Shipped,
+ });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.RelationalOrders.FindAsync([OrderStatus.Pending], ct);
+ loaded.Should().NotBeNull();
+ loaded!.Id.Should().Be(OrderStatus.Pending);
+ }
+
+ [Fact]
+ public async Task AlternateKey_AsEnum_WorksCorrectly()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.RelationalOrders.Add(new RelationalOrder
+ {
+ Id = OrderStatus.Pending,
+ AlternateKey = OrderStatus.Paid,
+ IndexedStatus = OrderStatus.Shipped,
+ });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.RelationalOrders
+ .FirstOrDefaultAsync(x => x.AlternateKey == OrderStatus.Paid, ct);
+ loaded.Should().NotBeNull();
+ loaded!.AlternateKey.Should().Be(OrderStatus.Paid);
+ }
+
+ [Fact]
+ public async Task Index_AsEnum_WorksCorrectly()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+ await ctx.Database.EnsureCreatedAsync(ct);
+
+ ctx.RelationalOrders.Add(new RelationalOrder
+ {
+ Id = OrderStatus.Pending,
+ AlternateKey = OrderStatus.Paid,
+ IndexedStatus = OrderStatus.Shipped,
+ });
+ await ctx.SaveChangesAsync(ct);
+
+ ctx.ChangeTracker.Clear();
+
+ var loaded = await ctx.RelationalOrders
+ .Where(x => x.IndexedStatus == OrderStatus.Shipped)
+ .ToListAsync(ct);
+ loaded.Should().HaveCount(1);
+ }
+
+ [Fact]
+ public async Task Schema_IsCreated_WithoutErrors()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var ctx = CreateContext();
+
+ // EnsureCreated should succeed — enum columns get proper column types
+ var created = await ctx.Database.EnsureCreatedAsync(ct);
+ created.Should().BeTrue();
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/TestDbContext.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/TestDbContext.cs
new file mode 100644
index 0000000..f47c9d5
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/TestDbContext.cs
@@ -0,0 +1,135 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Tests.IntegrationTests;
+
+public class Order
+{
+ public int Id { get; set; }
+ public OrderStatus Status { get; set; } = OrderStatus.Pending;
+ public OrderStatus? OptionalStatus { get; set; }
+ public Currency Currency { get; set; } = Currency.Usd;
+ public ShipmentState ShipmentState { get; set; } = ShipmentState.InTransit;
+}
+
+// Entity for relational key/index tests
+public class RelationalOrder
+{
+ public OrderStatus Id { get; set; } = OrderStatus.Pending; // PK as enum
+ public OrderStatus AlternateKey { get; set; } = OrderStatus.Pending;
+ public OrderStatus IndexedStatus { get; set; } = OrderStatus.Pending;
+ public int FkId { get; set; }
+}
+
+// ---------------------------------------------------------------------------
+// Manually-defined converters (mirror what the EFCore generator produces).
+// The generator snapshot tests verify the generated templates; these converters
+// exercise the same EF Core mechanics without requiring the generator to run
+// as an analyzer in this project.
+//
+// Use dictionary indexer (not TryGetValue/throw) so the lambdas remain valid
+// as expression trees, which EF Core requires for some provider scenarios.
+// ---------------------------------------------------------------------------
+
+internal sealed class OrderStatusByValueConverter : ValueConverter
+{
+ private static readonly Dictionary s_map = new()
+ {
+ [OrderStatus.Pending.Value] = OrderStatus.Pending,
+ [OrderStatus.Paid.Value] = OrderStatus.Paid,
+ [OrderStatus.Shipped.Value] = OrderStatus.Shipped,
+ };
+
+ public OrderStatusByValueConverter() : base(e => e.Value, v => s_map[v]) { }
+}
+
+internal sealed class OrderStatusByNameConverter : ValueConverter
+{
+ private static readonly Dictionary s_map = new()
+ {
+ [OrderStatus.Pending.Name] = OrderStatus.Pending,
+ [OrderStatus.Paid.Name] = OrderStatus.Paid,
+ [OrderStatus.Shipped.Name] = OrderStatus.Shipped,
+ };
+
+ public OrderStatusByNameConverter() : base(e => e.Name, n => s_map[n]) { }
+}
+
+internal sealed class CurrencyByNameConverter : ValueConverter
+{
+ private static readonly Dictionary s_map = new()
+ {
+ [Currency.Usd.Name] = Currency.Usd,
+ [Currency.Eur.Name] = Currency.Eur,
+ [Currency.Gbp.Name] = Currency.Gbp,
+ };
+
+ public CurrencyByNameConverter() : base(e => e.Name, n => s_map[n]) { }
+}
+
+internal sealed class ShipmentStateByValueConverter : ValueConverter
+{
+ private static readonly Dictionary s_map = new()
+ {
+ [ShipmentState.InTransit.Value] = ShipmentState.InTransit,
+ [ShipmentState.Delivered.Value] = ShipmentState.Delivered,
+ [ShipmentState.Returned.Value] = ShipmentState.Returned,
+ };
+
+ public ShipmentStateByValueConverter() : base(e => e.Value, v => s_map[v]) { }
+}
+
+// ---------------------------------------------------------------------------
+
+public class TestDbContext : DbContext
+{
+ private readonly Action? _modelConfig;
+
+ public DbSet Orders { get; set; } = null!;
+
+ public TestDbContext(DbContextOptions options, Action? modelConfig = null)
+ : base(options)
+ {
+ _modelConfig = modelConfig;
+ }
+
+ protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
+ {
+ // Manually register converters for each OptimizedEnum type used in this project.
+ // This replicates what the generated ConfigureOptimizedEnums() extension does.
+ configurationBuilder.Properties().HaveConversion();
+ configurationBuilder.Properties().HaveConversion();
+ configurationBuilder.Properties().HaveConversion();
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+ _modelConfig?.Invoke(modelBuilder);
+ }
+}
+
+// Separate context for relational tests (PK/FK/index/alternate key)
+public class RelationalTestDbContext : DbContext
+{
+ public DbSet RelationalOrders { get; set; } = null!;
+
+ public RelationalTestDbContext(DbContextOptions options)
+ : base(options) { }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(x => x.Id);
+ entity.HasAlternateKey(x => x.AlternateKey);
+ entity.HasIndex(x => x.IndexedStatus);
+ // Explicitly wire converter on each column (mirrors HasOrderStatusConversionByValue())
+ entity.Property(x => x.Id).HasConversion(new OrderStatusByValueConverter());
+ entity.Property(x => x.AlternateKey).HasConversion(new OrderStatusByValueConverter());
+ entity.Property(x => x.IndexedStatus).HasConversion(new OrderStatusByValueConverter());
+ });
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/TestEnums.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/TestEnums.cs
new file mode 100644
index 0000000..b84afa3
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/IntegrationTests/TestEnums.cs
@@ -0,0 +1,39 @@
+using LayeredCraft.OptimizedEnums;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Tests.IntegrationTests;
+
+// Integer-valued enum — stored by value
+public sealed partial class OrderStatus : OptimizedEnum
+{
+ public static readonly OrderStatus Pending = new(1, nameof(Pending));
+ public static readonly OrderStatus Paid = new(2, nameof(Paid));
+ public static readonly OrderStatus Shipped = new(3, nameof(Shipped));
+
+ private OrderStatus(int value, string name) : base(value, name) { }
+}
+
+// String-valued enum — stored by name
+public sealed partial class Currency : OptimizedEnum
+{
+ public static readonly Currency Usd = new("USD", nameof(Usd));
+ public static readonly Currency Eur = new("EUR", nameof(Eur));
+ public static readonly Currency Gbp = new("GBP", nameof(Gbp));
+
+ private Currency(string value, string name) : base(value, name) { }
+}
+
+// Enum through abstract intermediate base
+public abstract class ShipmentStateBase : OptimizedEnum
+ where TEnum : OptimizedEnum
+{
+ protected ShipmentStateBase(int value, string name) : base(value, name) { }
+}
+
+public sealed partial class ShipmentState : ShipmentStateBase
+{
+ public static readonly ShipmentState InTransit = new(1, nameof(InTransit));
+ public static readonly ShipmentState Delivered = new(2, nameof(Delivered));
+ public static readonly ShipmentState Returned = new(3, nameof(Returned));
+
+ private ShipmentState(int value, string name) : base(value, name) { }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/LayeredCraft.OptimizedEnums.EFCore.Tests.csproj b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/LayeredCraft.OptimizedEnums.EFCore.Tests.csproj
new file mode 100644
index 0000000..abc04ed
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/LayeredCraft.OptimizedEnums.EFCore.Tests.csproj
@@ -0,0 +1,58 @@
+
+
+ enable
+ enable
+ Exe
+ LayeredCraft.OptimizedEnums.EFCore.Tests
+ net8.0;net9.0;net10.0
+ true
+ false
+ default
+ MSB3243
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/ModuleInitializer.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/ModuleInitializer.cs
new file mode 100644
index 0000000..df95987
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/ModuleInitializer.cs
@@ -0,0 +1,9 @@
+using System.Runtime.CompilerServices;
+
+namespace LayeredCraft.OptimizedEnums.EFCore.Tests;
+
+public static class ModuleInitializer
+{
+ [ModuleInitializer]
+ public static void Init() => VerifySourceGenerators.Initialize();
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs
new file mode 100644
index 0000000..871fbdc
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs
@@ -0,0 +1,47 @@
+//HintName: OptimizedEnumEfCoreAttribute.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ ///
+ /// Controls how an OptimizedEnum property is stored in the database.
+ ///
+ public enum OptimizedEnumEfCoreStorage
+ {
+ /// Store as the member's underlying Value (e.g. 1 for an int-valued enum).
+ ByValue = 0,
+
+ /// Store as the member's Name string (e.g. "Pending").
+ ByName = 1,
+ }
+
+ ///
+ /// Instructs the OptimizedEnums source generator to emit Entity Framework Core
+ /// value converters and comparer for the decorated OptimizedEnum class.
+ ///
+ [global::System.AttributeUsage(
+ global::System.AttributeTargets.Class,
+ AllowMultiple = false,
+ Inherited = false)]
+ public sealed class OptimizedEnumEfCoreAttribute : global::System.Attribute
+ {
+ /// Initializes a new instance.
+ public OptimizedEnumEfCoreAttribute(
+ OptimizedEnumEfCoreStorage storage = OptimizedEnumEfCoreStorage.ByValue)
+ {
+ Storage = storage;
+ }
+
+ /// Gets the default storage mode for this enum.
+ public OptimizedEnumEfCoreStorage Storage { get; }
+ }
+}
\ No newline at end of file
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumEfCoreConventions.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumEfCoreConventions.g.verified.cs
new file mode 100644
index 0000000..2a13307
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumEfCoreConventions.g.verified.cs
@@ -0,0 +1,28 @@
+//HintName: OptimizedEnumEfCoreConventions.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+ public static class OptimizedEnumEfCoreConventionExtensions
+ {
+ public static global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder ConfigureOptimizedEnums(
+ this global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder builder)
+ {
+
+ builder.Properties()
+ .HaveConversion();
+
+ return builder;
+ }
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.EFCore.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.EFCore.g.verified.cs
new file mode 100644
index 0000000..f8a9862
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.EFCore.g.verified.cs
@@ -0,0 +1,59 @@
+//HintName: Priority.EFCore.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class PriorityValueConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public PriorityValueConverter()
+ : base(static v => v.Value, static v => FromValue(v))
+ { }
+
+ private static global::Priority FromValue(int v) =>
+ global::Priority.TryFromValue(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid value for Priority.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class PriorityNameConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public PriorityNameConverter()
+ : base(static v => v.Name, static v => FromName(v))
+ { }
+
+ private static global::Priority FromName(global::System.String v) =>
+ global::Priority.TryFromName(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid name for Priority.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal static class PriorityEfCoreExtensions
+{
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasPriorityConversionByValue(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasPriorityConversionByName(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs
new file mode 100644
index 0000000..8d45832
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs
@@ -0,0 +1,101 @@
+//HintName: Priority.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+partial class Priority
+{
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all =
+ global::System.Array.AsReadOnly(new global::Priority[]
+ {
+ Low,
+ Medium,
+ High
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names =
+ global::System.Array.AsReadOnly(new string[]
+ {
+ Low.Name,
+ Medium.Name,
+ High.Name
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values =
+ global::System.Array.AsReadOnly(new int[]
+ {
+ Low.Value,
+ Medium.Value,
+ High.Value
+ });
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byName =
+ new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal)
+ {
+ [Low.Name] = Low,
+ [Medium.Name] = Medium,
+ [High.Name] = High
+ };
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byValue =
+ new global::System.Collections.Generic.Dictionary(3)
+ {
+ [Low.Value] = Low,
+ [Medium.Value] = Medium,
+ [High.Value] = High
+ };
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList All => s_all;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Names => s_names;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Values => s_values;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public const int Count = 3;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::Priority FromName(string name)
+ {
+ if (!s_byName.TryGetValue(name, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{name}' is not a valid name for Priority");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Priority? result) =>
+ s_byName.TryGetValue(name, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::Priority FromValue(int value)
+ {
+ if (!s_byValue.TryGetValue(value, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{value}' is not a valid value for Priority");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Priority? result) =>
+ s_byValue.TryGetValue(value, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsName(string name) => s_byName.ContainsKey(name);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsValue(int value) => s_byValue.ContainsKey(value);
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.EFCore.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.EFCore.g.verified.cs
new file mode 100644
index 0000000..96f8499
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.EFCore.g.verified.cs
@@ -0,0 +1,61 @@
+//HintName: MyApp.Domain.Color.EFCore.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace MyApp.Domain;
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class ColorValueConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public ColorValueConverter()
+ : base(static v => v.Value, static v => FromValue(v))
+ { }
+
+ private static global::MyApp.Domain.Color FromValue(string v) =>
+ global::MyApp.Domain.Color.TryFromValue(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid value for Color.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class ColorNameConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public ColorNameConverter()
+ : base(static v => v.Name, static v => FromName(v))
+ { }
+
+ private static global::MyApp.Domain.Color FromName(global::System.String v) =>
+ global::MyApp.Domain.Color.TryFromName(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid name for Color.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal static class MyApp_Domain_ColorEfCoreExtensions
+{
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasColorConversionByValue(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasColorConversionByName(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs
new file mode 100644
index 0000000..ac41740
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs
@@ -0,0 +1,103 @@
+//HintName: MyApp.Domain.Color.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace MyApp.Domain;
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+partial class Color
+{
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all =
+ global::System.Array.AsReadOnly(new global::MyApp.Domain.Color[]
+ {
+ Red,
+ Green,
+ Blue
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names =
+ global::System.Array.AsReadOnly(new string[]
+ {
+ Red.Name,
+ Green.Name,
+ Blue.Name
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values =
+ global::System.Array.AsReadOnly(new string[]
+ {
+ Red.Value,
+ Green.Value,
+ Blue.Value
+ });
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byName =
+ new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal)
+ {
+ [Red.Name] = Red,
+ [Green.Name] = Green,
+ [Blue.Name] = Blue
+ };
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byValue =
+ new global::System.Collections.Generic.Dictionary(3)
+ {
+ [Red.Value] = Red,
+ [Green.Value] = Green,
+ [Blue.Value] = Blue
+ };
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList All => s_all;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Names => s_names;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Values => s_values;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public const int Count = 3;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::MyApp.Domain.Color FromName(string name)
+ {
+ if (!s_byName.TryGetValue(name, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{name}' is not a valid name for Color");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) =>
+ s_byName.TryGetValue(name, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::MyApp.Domain.Color FromValue(string value)
+ {
+ if (!s_byValue.TryGetValue(value, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{value}' is not a valid value for Color");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromValue(string value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) =>
+ s_byValue.TryGetValue(value, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsName(string name) => s_byName.ContainsKey(name);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsValue(string value) => s_byValue.ContainsKey(value);
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumEfCoreAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumEfCoreAttribute.g.verified.cs
new file mode 100644
index 0000000..871fbdc
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumEfCoreAttribute.g.verified.cs
@@ -0,0 +1,47 @@
+//HintName: OptimizedEnumEfCoreAttribute.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ ///
+ /// Controls how an OptimizedEnum property is stored in the database.
+ ///
+ public enum OptimizedEnumEfCoreStorage
+ {
+ /// Store as the member's underlying Value (e.g. 1 for an int-valued enum).
+ ByValue = 0,
+
+ /// Store as the member's Name string (e.g. "Pending").
+ ByName = 1,
+ }
+
+ ///
+ /// Instructs the OptimizedEnums source generator to emit Entity Framework Core
+ /// value converters and comparer for the decorated OptimizedEnum class.
+ ///
+ [global::System.AttributeUsage(
+ global::System.AttributeTargets.Class,
+ AllowMultiple = false,
+ Inherited = false)]
+ public sealed class OptimizedEnumEfCoreAttribute : global::System.Attribute
+ {
+ /// Initializes a new instance.
+ public OptimizedEnumEfCoreAttribute(
+ OptimizedEnumEfCoreStorage storage = OptimizedEnumEfCoreStorage.ByValue)
+ {
+ Storage = storage;
+ }
+
+ /// Gets the default storage mode for this enum.
+ public OptimizedEnumEfCoreStorage Storage { get; }
+ }
+}
\ No newline at end of file
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumEfCoreConventions.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumEfCoreConventions.g.verified.cs
new file mode 100644
index 0000000..06baa00
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumEfCoreConventions.g.verified.cs
@@ -0,0 +1,28 @@
+//HintName: OptimizedEnumEfCoreConventions.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+ public static class OptimizedEnumEfCoreConventionExtensions
+ {
+ public static global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder ConfigureOptimizedEnums(
+ this global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder builder)
+ {
+
+ builder.Properties()
+ .HaveConversion();
+
+ return builder;
+ }
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.EFCore.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.EFCore.g.verified.cs
new file mode 100644
index 0000000..d3d5e1f
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.EFCore.g.verified.cs
@@ -0,0 +1,61 @@
+//HintName: MyApp.Domain.OrderStatus.EFCore.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace MyApp.Domain;
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class OrderStatusValueConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public OrderStatusValueConverter()
+ : base(static v => v.Value, static v => FromValue(v))
+ { }
+
+ private static global::MyApp.Domain.OrderStatus FromValue(int v) =>
+ global::MyApp.Domain.OrderStatus.TryFromValue(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid value for OrderStatus.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class OrderStatusNameConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public OrderStatusNameConverter()
+ : base(static v => v.Name, static v => FromName(v))
+ { }
+
+ private static global::MyApp.Domain.OrderStatus FromName(global::System.String v) =>
+ global::MyApp.Domain.OrderStatus.TryFromName(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid name for OrderStatus.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal static class MyApp_Domain_OrderStatusEfCoreExtensions
+{
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasOrderStatusConversionByValue(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasOrderStatusConversionByName(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs
new file mode 100644
index 0000000..9d69f1d
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs
@@ -0,0 +1,103 @@
+//HintName: MyApp.Domain.OrderStatus.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace MyApp.Domain;
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+partial class OrderStatus
+{
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all =
+ global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[]
+ {
+ Pending,
+ Paid,
+ Shipped
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names =
+ global::System.Array.AsReadOnly(new string[]
+ {
+ Pending.Name,
+ Paid.Name,
+ Shipped.Name
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values =
+ global::System.Array.AsReadOnly(new int[]
+ {
+ Pending.Value,
+ Paid.Value,
+ Shipped.Value
+ });
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byName =
+ new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal)
+ {
+ [Pending.Name] = Pending,
+ [Paid.Name] = Paid,
+ [Shipped.Name] = Shipped
+ };
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byValue =
+ new global::System.Collections.Generic.Dictionary(3)
+ {
+ [Pending.Value] = Pending,
+ [Paid.Value] = Paid,
+ [Shipped.Value] = Shipped
+ };
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList All => s_all;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Names => s_names;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Values => s_values;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public const int Count = 3;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::MyApp.Domain.OrderStatus FromName(string name)
+ {
+ if (!s_byName.TryGetValue(name, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{name}' is not a valid name for OrderStatus");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) =>
+ s_byName.TryGetValue(name, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::MyApp.Domain.OrderStatus FromValue(int value)
+ {
+ if (!s_byValue.TryGetValue(value, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{value}' is not a valid value for OrderStatus");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.OrderStatus? result) =>
+ s_byValue.TryGetValue(value, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsName(string name) => s_byName.ContainsKey(name);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsValue(int value) => s_byValue.ContainsKey(value);
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs
new file mode 100644
index 0000000..871fbdc
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs
@@ -0,0 +1,47 @@
+//HintName: OptimizedEnumEfCoreAttribute.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ ///
+ /// Controls how an OptimizedEnum property is stored in the database.
+ ///
+ public enum OptimizedEnumEfCoreStorage
+ {
+ /// Store as the member's underlying Value (e.g. 1 for an int-valued enum).
+ ByValue = 0,
+
+ /// Store as the member's Name string (e.g. "Pending").
+ ByName = 1,
+ }
+
+ ///
+ /// Instructs the OptimizedEnums source generator to emit Entity Framework Core
+ /// value converters and comparer for the decorated OptimizedEnum class.
+ ///
+ [global::System.AttributeUsage(
+ global::System.AttributeTargets.Class,
+ AllowMultiple = false,
+ Inherited = false)]
+ public sealed class OptimizedEnumEfCoreAttribute : global::System.Attribute
+ {
+ /// Initializes a new instance.
+ public OptimizedEnumEfCoreAttribute(
+ OptimizedEnumEfCoreStorage storage = OptimizedEnumEfCoreStorage.ByValue)
+ {
+ Storage = storage;
+ }
+
+ /// Gets the default storage mode for this enum.
+ public OptimizedEnumEfCoreStorage Storage { get; }
+ }
+}
\ No newline at end of file
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumEfCoreConventions.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumEfCoreConventions.g.verified.cs
new file mode 100644
index 0000000..d9c446e
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumEfCoreConventions.g.verified.cs
@@ -0,0 +1,28 @@
+//HintName: OptimizedEnumEfCoreConventions.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+ public static class OptimizedEnumEfCoreConventionExtensions
+ {
+ public static global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder ConfigureOptimizedEnums(
+ this global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder builder)
+ {
+
+ builder.Properties()
+ .HaveConversion();
+
+ return builder;
+ }
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs
new file mode 100644
index 0000000..871fbdc
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#OptimizedEnumEfCoreAttribute.g.verified.cs
@@ -0,0 +1,47 @@
+//HintName: OptimizedEnumEfCoreAttribute.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ ///
+ /// Controls how an OptimizedEnum property is stored in the database.
+ ///
+ public enum OptimizedEnumEfCoreStorage
+ {
+ /// Store as the member's underlying Value (e.g. 1 for an int-valued enum).
+ ByValue = 0,
+
+ /// Store as the member's Name string (e.g. "Pending").
+ ByName = 1,
+ }
+
+ ///
+ /// Instructs the OptimizedEnums source generator to emit Entity Framework Core
+ /// value converters and comparer for the decorated OptimizedEnum class.
+ ///
+ [global::System.AttributeUsage(
+ global::System.AttributeTargets.Class,
+ AllowMultiple = false,
+ Inherited = false)]
+ public sealed class OptimizedEnumEfCoreAttribute : global::System.Attribute
+ {
+ /// Initializes a new instance.
+ public OptimizedEnumEfCoreAttribute(
+ OptimizedEnumEfCoreStorage storage = OptimizedEnumEfCoreStorage.ByValue)
+ {
+ Storage = storage;
+ }
+
+ /// Gets the default storage mode for this enum.
+ public OptimizedEnumEfCoreStorage Storage { get; }
+ }
+}
\ No newline at end of file
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#OptimizedEnumEfCoreConventions.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#OptimizedEnumEfCoreConventions.g.verified.cs
new file mode 100644
index 0000000..f6d9fa8
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#OptimizedEnumEfCoreConventions.g.verified.cs
@@ -0,0 +1,28 @@
+//HintName: OptimizedEnumEfCoreConventions.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+ public static class OptimizedEnumEfCoreConventionExtensions
+ {
+ public static global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder ConfigureOptimizedEnums(
+ this global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder builder)
+ {
+
+ builder.Properties()
+ .HaveConversion();
+
+ return builder;
+ }
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#Priority.EFCore.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#Priority.EFCore.g.verified.cs
new file mode 100644
index 0000000..f8a9862
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#Priority.EFCore.g.verified.cs
@@ -0,0 +1,59 @@
+//HintName: Priority.EFCore.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class PriorityValueConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public PriorityValueConverter()
+ : base(static v => v.Value, static v => FromValue(v))
+ { }
+
+ private static global::Priority FromValue(int v) =>
+ global::Priority.TryFromValue(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid value for Priority.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class PriorityNameConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public PriorityNameConverter()
+ : base(static v => v.Name, static v => FromName(v))
+ { }
+
+ private static global::Priority FromName(global::System.String v) =>
+ global::Priority.TryFromName(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid name for Priority.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal static class PriorityEfCoreExtensions
+{
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasPriorityConversionByValue(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasPriorityConversionByName(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#Priority.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#Priority.g.verified.cs
new file mode 100644
index 0000000..8d45832
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_GlobalNamespace#Priority.g.verified.cs
@@ -0,0 +1,101 @@
+//HintName: Priority.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+partial class Priority
+{
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all =
+ global::System.Array.AsReadOnly(new global::Priority[]
+ {
+ Low,
+ Medium,
+ High
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names =
+ global::System.Array.AsReadOnly(new string[]
+ {
+ Low.Name,
+ Medium.Name,
+ High.Name
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values =
+ global::System.Array.AsReadOnly(new int[]
+ {
+ Low.Value,
+ Medium.Value,
+ High.Value
+ });
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byName =
+ new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal)
+ {
+ [Low.Name] = Low,
+ [Medium.Name] = Medium,
+ [High.Name] = High
+ };
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byValue =
+ new global::System.Collections.Generic.Dictionary(3)
+ {
+ [Low.Value] = Low,
+ [Medium.Value] = Medium,
+ [High.Value] = High
+ };
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList All => s_all;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Names => s_names;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Values => s_values;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public const int Count = 3;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::Priority FromName(string name)
+ {
+ if (!s_byName.TryGetValue(name, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{name}' is not a valid name for Priority");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Priority? result) =>
+ s_byName.TryGetValue(name, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::Priority FromValue(int value)
+ {
+ if (!s_byValue.TryGetValue(value, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{value}' is not a valid value for Priority");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromValue(int value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::Priority? result) =>
+ s_byValue.TryGetValue(value, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsName(string name) => s_byName.ContainsKey(name);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsValue(int value) => s_byValue.ContainsKey(value);
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.EFCore.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.EFCore.g.verified.cs
new file mode 100644
index 0000000..96f8499
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.EFCore.g.verified.cs
@@ -0,0 +1,61 @@
+//HintName: MyApp.Domain.Color.EFCore.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace MyApp.Domain;
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class ColorValueConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public ColorValueConverter()
+ : base(static v => v.Value, static v => FromValue(v))
+ { }
+
+ private static global::MyApp.Domain.Color FromValue(string v) =>
+ global::MyApp.Domain.Color.TryFromValue(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid value for Color.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class ColorNameConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public ColorNameConverter()
+ : base(static v => v.Name, static v => FromName(v))
+ { }
+
+ private static global::MyApp.Domain.Color FromName(global::System.String v) =>
+ global::MyApp.Domain.Color.TryFromName(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid name for Color.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal static class MyApp_Domain_ColorEfCoreExtensions
+{
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasColorConversionByValue(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasColorConversionByName(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs
new file mode 100644
index 0000000..ac41740
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs
@@ -0,0 +1,103 @@
+//HintName: MyApp.Domain.Color.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace MyApp.Domain;
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+partial class Color
+{
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all =
+ global::System.Array.AsReadOnly(new global::MyApp.Domain.Color[]
+ {
+ Red,
+ Green,
+ Blue
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names =
+ global::System.Array.AsReadOnly(new string[]
+ {
+ Red.Name,
+ Green.Name,
+ Blue.Name
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values =
+ global::System.Array.AsReadOnly(new string[]
+ {
+ Red.Value,
+ Green.Value,
+ Blue.Value
+ });
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byName =
+ new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal)
+ {
+ [Red.Name] = Red,
+ [Green.Name] = Green,
+ [Blue.Name] = Blue
+ };
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byValue =
+ new global::System.Collections.Generic.Dictionary(3)
+ {
+ [Red.Value] = Red,
+ [Green.Value] = Green,
+ [Blue.Value] = Blue
+ };
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList All => s_all;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Names => s_names;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::System.Collections.Generic.IReadOnlyList Values => s_values;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public const int Count = 3;
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::MyApp.Domain.Color FromName(string name)
+ {
+ if (!s_byName.TryGetValue(name, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{name}' is not a valid name for Color");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromName(string name, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) =>
+ s_byName.TryGetValue(name, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static global::MyApp.Domain.Color FromValue(string value)
+ {
+ if (!s_byValue.TryGetValue(value, out var result))
+ throw new global::System.Collections.Generic.KeyNotFoundException(
+ $"'{value}' is not a valid value for Color");
+
+ return result;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool TryFromValue(string value, [global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out global::MyApp.Domain.Color? result) =>
+ s_byValue.TryGetValue(value, out result);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsName(string name) => s_byName.ContainsKey(name);
+
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+ public static bool ContainsValue(string value) => s_byValue.ContainsKey(value);
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumEfCoreAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumEfCoreAttribute.g.verified.cs
new file mode 100644
index 0000000..871fbdc
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumEfCoreAttribute.g.verified.cs
@@ -0,0 +1,47 @@
+//HintName: OptimizedEnumEfCoreAttribute.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ ///
+ /// Controls how an OptimizedEnum property is stored in the database.
+ ///
+ public enum OptimizedEnumEfCoreStorage
+ {
+ /// Store as the member's underlying Value (e.g. 1 for an int-valued enum).
+ ByValue = 0,
+
+ /// Store as the member's Name string (e.g. "Pending").
+ ByName = 1,
+ }
+
+ ///
+ /// Instructs the OptimizedEnums source generator to emit Entity Framework Core
+ /// value converters and comparer for the decorated OptimizedEnum class.
+ ///
+ [global::System.AttributeUsage(
+ global::System.AttributeTargets.Class,
+ AllowMultiple = false,
+ Inherited = false)]
+ public sealed class OptimizedEnumEfCoreAttribute : global::System.Attribute
+ {
+ /// Initializes a new instance.
+ public OptimizedEnumEfCoreAttribute(
+ OptimizedEnumEfCoreStorage storage = OptimizedEnumEfCoreStorage.ByValue)
+ {
+ Storage = storage;
+ }
+
+ /// Gets the default storage mode for this enum.
+ public OptimizedEnumEfCoreStorage Storage { get; }
+ }
+}
\ No newline at end of file
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumEfCoreConventions.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumEfCoreConventions.g.verified.cs
new file mode 100644
index 0000000..9fe3d22
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumEfCoreConventions.g.verified.cs
@@ -0,0 +1,28 @@
+//HintName: OptimizedEnumEfCoreConventions.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace LayeredCraft.OptimizedEnums.EFCore
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+ public static class OptimizedEnumEfCoreConventionExtensions
+ {
+ public static global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder ConfigureOptimizedEnums(
+ this global::Microsoft.EntityFrameworkCore.ModelConfigurationBuilder builder)
+ {
+
+ builder.Properties()
+ .HaveConversion();
+
+ return builder;
+ }
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.EFCore.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.EFCore.g.verified.cs
new file mode 100644
index 0000000..d3d5e1f
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.EFCore.g.verified.cs
@@ -0,0 +1,61 @@
+//HintName: MyApp.Domain.OrderStatus.EFCore.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace MyApp.Domain;
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class OrderStatusValueConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public OrderStatusValueConverter()
+ : base(static v => v.Value, static v => FromValue(v))
+ { }
+
+ private static global::MyApp.Domain.OrderStatus FromValue(int v) =>
+ global::MyApp.Domain.OrderStatus.TryFromValue(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid value for OrderStatus.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal sealed class OrderStatusNameConverter
+ : global::Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter
+{
+ public OrderStatusNameConverter()
+ : base(static v => v.Name, static v => FromName(v))
+ { }
+
+ private static global::MyApp.Domain.OrderStatus FromName(global::System.String v) =>
+ global::MyApp.Domain.OrderStatus.TryFromName(v, out var result)
+ ? result!
+ : throw new global::System.InvalidOperationException(
+ $"'{v}' is not a valid name for OrderStatus.");
+}
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.EFCore.Generator", "REPLACED")]
+internal static class MyApp_Domain_OrderStatusEfCoreExtensions
+{
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasOrderStatusConversionByValue(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+
+ public static global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder HasOrderStatusConversionByName(
+ this global::Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder builder)
+ {
+ builder.HasConversion();
+ return builder;
+ }
+}
diff --git a/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs
new file mode 100644
index 0000000..9d69f1d
--- /dev/null
+++ b/tests/LayeredCraft.OptimizedEnums.EFCore.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs
@@ -0,0 +1,103 @@
+//HintName: MyApp.Domain.OrderStatus.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+namespace MyApp.Domain;
+
+[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")]
+partial class OrderStatus
+{
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all =
+ global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[]
+ {
+ Pending,
+ Paid,
+ Shipped
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names =
+ global::System.Array.AsReadOnly(new string[]
+ {
+ Pending.Name,
+ Paid.Name,
+ Shipped.Name
+ });
+
+ private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values =
+ global::System.Array.AsReadOnly(new int[]
+ {
+ Pending.Value,
+ Paid.Value,
+ Shipped.Value
+ });
+
+ private static readonly global::System.Collections.Generic.Dictionary s_byName =
+ new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal)
+ {
+ [Pending.Name] = Pending,
+ [Paid.Name] = Paid,
+ [Shipped.Name] = Shipped
+ };
+
+ private static readonly global::System.Collections.Generic.Dictionary