Skip to content

Commit af389a1

Browse files
feat: Entity Framework Core (C#) support as 10th format (v1.5.0)
- NEW: EntityFrameworkParser — parses C# entity classes with data annotations ([Key], [Required], [MaxLength], [Table], nullable reference types) - NEW: EntityFrameworkGenerator — produces C# entity classes with proper data annotations, PascalCase naming, decimal precision/scale handling - Format identifier: 'ef' in CLI, MCP, and convert API - Sample fixture: fixtures/sample.ef.cs - 20 new tests (parsing, generation, cross-format: sql/prisma/django) - 253/253 tests passing - Updated CLI format choices, MCP format list - Bumped to v1.5.0
1 parent 32edeb6 commit af389a1

7 files changed

Lines changed: 677 additions & 2 deletions

File tree

‎fixtures/sample.ef.cs‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.ComponentModel.DataAnnotations.Schema;
4+
5+
namespace SchemaForge.Models;
6+
7+
[Table("users")]
8+
public class User
9+
{
10+
[Key]
11+
public int Id { get; set; }
12+
13+
[Required]
14+
[MaxLength(100)]
15+
public string Name { get; set; }
16+
17+
[Required]
18+
public string Email { get; set; }
19+
20+
public bool IsActive { get; set; }
21+
22+
public DateTime CreatedAt { get; set; }
23+
}
24+
25+
[Table("posts")]
26+
public class Post
27+
{
28+
[Key]
29+
public int Id { get; set; }
30+
31+
[Required]
32+
[MaxLength(200)]
33+
public string Title { get; set; }
34+
35+
public string? Content { get; set; }
36+
37+
public int AuthorId { get; set; }
38+
39+
[MaxLength(20)]
40+
public string? Status { get; set; }
41+
}
42+
43+
public enum Role
44+
{
45+
Admin,
46+
Editor,
47+
Viewer
48+
}

‎src/schemaforge/cli.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .mcp_server import mcp_command
1414

1515
# All supported format names (used for CLI choices and detection)
16-
_FORMATS = ["sql", "prisma", "drizzle", "typeorm", "django", "sqlalchemy", "alembic", "json_schema", "graphql"]
16+
_FORMATS = ["sql", "prisma", "drizzle", "typeorm", "django", "sqlalchemy", "alembic", "json_schema", "graphql", "ef"]
1717

1818

1919
@click.group()

‎src/schemaforge/convert.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from .generators.json_schema_generator import JSONSchemaGenerator
2323
from .parsers.graphql_parser import GraphQLParser
2424
from .generators.graphql_generator import GraphQLGenerator
25+
from .parsers.ef_parser import EntityFrameworkParser
26+
from .generators.ef_generator import EntityFrameworkGenerator
2527

2628
if TYPE_CHECKING:
2729
from .type_config import TypeConfig
@@ -37,6 +39,7 @@
3739
"alembic": (AlembicParser, AlembicGenerator),
3840
"json_schema": (JSONSchemaParser, JSONSchemaGenerator),
3941
"graphql": (GraphQLParser, GraphQLGenerator),
42+
"ef": (EntityFrameworkParser, EntityFrameworkGenerator),
4043
}
4144

4245

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Generator: SchemaForge IR → C# Entity Framework Core entity classes."""
2+
from __future__ import annotations
3+
4+
from ..ir import Schema, Column, ColumnType
5+
6+
from ._base import resolve_type, build_type_string
7+
8+
9+
class EntityFrameworkGenerator:
10+
"""Convert Schema IR to C# Entity Framework Core entity classes."""
11+
12+
_TYPE_MAP: dict[ColumnType, str] = {
13+
ColumnType.STRING: "string",
14+
ColumnType.INTEGER: "int",
15+
ColumnType.FLOAT: "double",
16+
ColumnType.BOOLEAN: "bool",
17+
ColumnType.DATETIME: "DateTime",
18+
ColumnType.DATE: "DateOnly",
19+
ColumnType.TIME: "TimeOnly",
20+
ColumnType.TEXT: "string",
21+
ColumnType.BLOB: "byte[]",
22+
ColumnType.JSON: "string",
23+
ColumnType.UUID: "Guid",
24+
ColumnType.ENUM: "string",
25+
ColumnType.DECIMAL: "decimal",
26+
}
27+
28+
def __init__(self, type_config=None):
29+
"""Initialize generator with optional type config."""
30+
self.type_config = type_config
31+
32+
def generate(self, schema: Schema) -> str:
33+
"""Generate C# entity classes from schema IR."""
34+
parts: list[str] = [
35+
"using System;",
36+
"using System.ComponentModel.DataAnnotations;",
37+
"using System.ComponentModel.DataAnnotations.Schema;",
38+
"",
39+
"namespace SchemaForge.Models;",
40+
"",
41+
]
42+
43+
for table in schema.tables:
44+
parts.append(self._generate_class(table))
45+
46+
return "\n".join(parts)
47+
48+
def _generate_class(self, table) -> str:
49+
"""Generate a single entity class."""
50+
lines: list[str] = []
51+
52+
# [Table] annotation if name differs from class name
53+
class_name = self._to_pascal(table.name)
54+
if table.name.lower() != class_name.lower():
55+
lines.append(f'[Table("{table.name}")]')
56+
57+
lines.append(f"public class {class_name}")
58+
lines.append("{")
59+
60+
for col in table.columns:
61+
lines.append(f" {self._property_def(col)}")
62+
63+
lines.append("}")
64+
return "\n".join(lines)
65+
66+
def _property_def(self, col: Column) -> str:
67+
"""Generate a C# property with data annotations."""
68+
annotations: list[str] = []
69+
cs_type = resolve_type(col, self._TYPE_MAP)
70+
prop_name = self._to_pascal(col.name)
71+
72+
# Data annotations
73+
if col.primary_key:
74+
annotations.append("[Key]")
75+
76+
if not col.nullable and not col.primary_key:
77+
annotations.append("[Required]")
78+
79+
if col.type == ColumnType.STRING and "length" in col.type_args:
80+
annotations.append(f"[MaxLength({col.type_args['length']})]")
81+
82+
if col.type == ColumnType.DECIMAL:
83+
p = col.type_args.get("precision", 10)
84+
s = col.type_args.get("scale", 2)
85+
annotations.append(f"[Column(TypeName = \"decimal({p},{s})\")]")
86+
87+
if col.unique and not col.primary_key:
88+
# EF Index attribute
89+
pass # Handled at the DbContext level, not on properties
90+
91+
# Handle nullable reference types
92+
is_nullable = col.nullable and cs_type != "byte[]"
93+
cs_type_str = f"{cs_type}?" if is_nullable else cs_type
94+
95+
# Default value
96+
default_str = ""
97+
if col.default is not None:
98+
if isinstance(col.default, bool):
99+
default_str = f" = {str(col.default).lower()};"
100+
elif isinstance(col.default, str) and col.default.startswith("fn:"):
101+
pass # Skip function defaults in C#
102+
elif isinstance(col.default, str):
103+
default_str = f' = "{col.default}";'
104+
elif isinstance(col.default, (int, float)):
105+
default_str = f" = {col.default};"
106+
107+
ann_str = " ".join(annotations)
108+
if ann_str:
109+
ann_str += "\n "
110+
111+
# Use List<T> for JSON columns (simple approximation)
112+
if col.type == ColumnType.JSON:
113+
cs_type_str = "string"
114+
115+
return f"{ann_str}public {cs_type_str} {prop_name} {{ get; set; }}{default_str}"
116+
117+
def _to_pascal(self, name: str) -> str:
118+
"""Convert snake_case to PascalCase."""
119+
parts = name.split("_")
120+
return "".join(p.capitalize() for p in parts if p)

‎src/schemaforge/mcp_server.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626

2727
# All supported formats
28-
_FORMATS = ["sql", "prisma", "drizzle", "typeorm", "django", "sqlalchemy", "alembic", "json_schema", "graphql"]
28+
_FORMATS = ["sql", "prisma", "drizzle", "typeorm", "django", "sqlalchemy", "alembic", "json_schema", "graphql", "ef"]
2929
_FORMAT_DESCRIPTIONS = {
3030
"sql": "SQL DDL (Data Definition Language)",
3131
"prisma": "Prisma schema",
@@ -36,6 +36,7 @@
3636
"alembic": "Alembic migration scripts (Python, generator-only)",
3737
"json_schema": "JSON Schema (draft 2020-12)",
3838
"graphql": "GraphQL SDL (Schema Definition Language)",
39+
"ef": "Entity Framework Core entities (C#)",
3940
}
4041

4142

0 commit comments

Comments
 (0)