diff --git a/docs/src/content/docs/packages/dto.md b/docs/src/content/docs/packages/dto.md index 30eaa55..9916ba7 100644 --- a/docs/src/content/docs/packages/dto.md +++ b/docs/src/content/docs/packages/dto.md @@ -230,7 +230,7 @@ This page covers the core mode + generated shape. Detailed reference and feature - [`PaginatedResponse`](/ZibStack.NET/packages/dto/paginated/) — the standard list wrapper. - [CRUD API (`[CrudApi]`)](/ZibStack.NET/packages/dto/crud-api/) — full endpoint generation. - [Response DTOs, mapping, `ApplyWithChanges`](/ZibStack.NET/packages/dto/response-mapping/) — `FromEntity` / `ProjectFrom`, nested + flatten, Diff, DtoMapper, Swagger. -- [JSON serializer & custom validation](/ZibStack.NET/packages/dto/json-and-validation/) — `PatchField` JSON registration + FluentValidation. +- [JSON serializer & custom validation](/ZibStack.NET/packages/dto/json-and-validation/) — `PatchField` JSON registration + `IDtoValidator`. ## Related guides diff --git a/docs/src/content/docs/packages/dto/json-and-validation.md b/docs/src/content/docs/packages/dto/json-and-validation.md index e11a2ba..199cefe 100644 --- a/docs/src/content/docs/packages/dto/json-and-validation.md +++ b/docs/src/content/docs/packages/dto/json-and-validation.md @@ -1,6 +1,6 @@ --- title: JSON serializer & custom validation -description: PatchField JSON converter registration (System.Text.Json + Newtonsoft) and how to plug in your own validator (manual or FluentValidation). +description: PatchField JSON converter registration (System.Text.Json + Newtonsoft) and how to plug in your own validator via IDtoValidator. --- ## JSON serializer support @@ -38,43 +38,6 @@ public class Player { ... } When a validator is set, `Validate()` delegates entirely to it -- the default generated rules are replaced. -### FluentValidation - -When FluentValidation is installed, the generator additionally produces: - -- `FluentDtoValidator` -- base class bridging FluentValidation with `IDtoValidator` -- `{RequestName}CreateBaseValidator` -- contains the generated required/null rules -- `{RequestName}UpdateBaseValidator` -- contains the generated null rules - -Inherit to extend: - -```csharp -public class MyCreateValidator : CreatePlayerRequestCreateBaseValidator -{ - public MyCreateValidator() - { - RuleFor(x => x.Name) - .Must(f => !f.HasValue || f.Value.Length >= 3) - .WithMessage("Name must be at least 3 characters."); - } -} - -[CreateDto(Validator = typeof(MyCreateValidator))] -public class Player { ... } -``` - -Or start from scratch: - -```csharp -public class MyCreateValidator : FluentDtoValidator -{ - public MyCreateValidator() - { - // your rules only - } -} -``` - ## Related guides - [Full CRUD with SQLite](/ZibStack.NET/guides/crud-sqlite/) — end-to-end project with `[CrudApi]`, relationships, Query DSL, observability, and PatchField tri-state demo diff --git a/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.AttributeSources.cs b/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.AttributeSources.cs index a7238f8..c95649b 100644 --- a/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.AttributeSources.cs +++ b/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.AttributeSources.cs @@ -365,29 +365,6 @@ public interface ICanValidate DtoValidationResult Validate(); } } -"; - - private const string FluentDtoValidatorSource = @"// -#nullable enable - -namespace ZibStack.NET.Dto -{ - /// - /// Base class that bridges FluentValidation with ZibStack.NET.Dto validation. - /// Extend this instead of AbstractValidator to use with [Dto(CreateValidator/UpdateValidator)]. - /// - public abstract class FluentDtoValidator : FluentValidation.AbstractValidator, IDtoValidator - { - DtoValidationResult IDtoValidator.Validate(T instance) - { - var fluentResult = base.Validate(instance); - var result = new DtoValidationResult(); - foreach (var failure in fluentResult.Errors) - result.AddError(failure.PropertyName, failure.ErrorMessage); - return result; - } - } -} "; private const string PatchFieldSchemaFilterSource = @"// diff --git a/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.Generation.cs b/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.Generation.cs index 581f21c..941d94f 100644 --- a/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.Generation.cs +++ b/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.Generation.cs @@ -23,7 +23,7 @@ private static void EmitTypeGenAttribute(StringBuilder sb, bool hasTypeGen) sb.AppendLine("[global::ZibStack.NET.TypeGen.GenerateTypes(global::ZibStack.NET.TypeGen.TypeTarget.OpenApi)]"); } - private static string GenerateSource(DtoClassInfo classInfo, bool hasFluentValidation) + private static string GenerateSource(DtoClassInfo classInfo) { var sb = new StringBuilder(); sb.AppendLine("// "); @@ -45,31 +45,14 @@ private static string GenerateSource(DtoClassInfo classInfo, bool hasFluentValid { case DtoKind.Create: GenerateCreateRequestClass(sb, classInfo); - if (hasFluentValidation) - { - sb.AppendLine(); - GenerateFluentBaseValidator(sb, classInfo, isCreate: true, combined: false); - } break; case DtoKind.Update: GenerateUpdateRequestClass(sb, classInfo); - if (hasFluentValidation) - { - sb.AppendLine(); - GenerateFluentBaseValidator(sb, classInfo, isCreate: false, combined: false); - } break; case DtoKind.Combined: GenerateCombinedRequestClass(sb, classInfo); - if (hasFluentValidation) - { - sb.AppendLine(); - GenerateFluentBaseValidator(sb, classInfo, isCreate: true, combined: true); - sb.AppendLine(); - GenerateFluentBaseValidator(sb, classInfo, isCreate: false, combined: true); - } break; } @@ -987,45 +970,6 @@ private static void GenerateApplyWithChangesMethod(StringBuilder sb, List !p.IsIgnoredFrom(1) && !p.IsReadOnly).ToList() - : classInfo.Properties.Where(p => - !p.IsIgnoredFrom(2) && !p.IsReadOnly && !p.IsInitOnly).ToList(); - - sb.AppendLine($"public class {requestName}{validatorSuffix} : ZibStack.NET.Dto.FluentDtoValidator<{requestName}>"); - sb.AppendLine("{"); - sb.AppendLine($" public {requestName}{validatorSuffix}()"); - sb.AppendLine(" {"); - - foreach (var prop in props) - { - var jsonName = prop.JsonName; - - if (isCreate && prop.IsRequired) - { - sb.AppendLine($" RuleFor(x => x.{prop.PropertyName}).Must(f => f.HasValue).WithMessage(\"Property '{jsonName}' is required.\");"); - - if (NeedsNullCheck(prop)) - { - sb.AppendLine($" RuleFor(x => x.{prop.PropertyName}).Must(f => !f.HasValue || f.Value is not null).WithMessage(\"Property '{jsonName}' cannot be null.\");"); - } - } - else if (NeedsNullCheck(prop)) - { - sb.AppendLine($" RuleFor(x => x.{prop.PropertyName}).Must(f => !f.HasValue || f.Value is not null).WithMessage(\"Property '{jsonName}' cannot be null.\");"); - } - } - - sb.AppendLine(" }"); - sb.AppendLine("}"); - } - private static string GenerateCreateDtoForSource(DtoForInfo info) { var sb = new StringBuilder(); diff --git a/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.cs b/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.cs index 96d25f1..3a210d3 100644 --- a/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.cs +++ b/packages/ZibStack.NET.Dto/src/ZibStack.NET.Dto/DtoGenerator.cs @@ -82,10 +82,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) spc.AddSource("PatchFieldSchemaFilter.g.cs", PatchFieldSchemaFilterV10Source); }); - // Detect FluentValidation and emit adapter - var hasFluentValidation = context.CompilationProvider.Select(static (compilation, _) => - compilation.GetTypeByMetadataName("FluentValidation.AbstractValidator`1") is not null); - // Parse fluent IDtoConfigurator once and feed it into every downstream pipeline. // CrudImplied / CrudApi / allDtoClasses callbacks combine with this so per-property // overrides + CrudApi options can mix with attribute markers in either direction. @@ -107,12 +103,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var hasEfCore = context.CompilationProvider.Select(static (compilation, _) => compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions") is not null); - context.RegisterSourceOutput(hasFluentValidation, static (spc, hasFluent) => - { - if (hasFluent) - spc.AddSource("FluentDtoValidator.g.cs", FluentDtoValidatorSource); - }); - // Run diagnostics on all types with ZibStack.Dto attributes var diagnosticsTargets = context.SyntaxProvider .CreateSyntaxProvider( @@ -264,34 +254,34 @@ void VisitType(INamedTypeSymbol type) .Combine(updateDtoClasses.Collect()) .Combine(combinedDtoClasses.Collect()); - context.RegisterSourceOutput(allDtoClasses.Combine(hasFluentValidation).Combine(hasTypeGen).Combine(fluentConfig), static (spc, pair) => + context.RegisterSourceOutput(allDtoClasses.Combine(hasTypeGen).Combine(fluentConfig), static (spc, pair) => { - var (((((createInfos, updateInfos), combinedInfos), hasFluent), hasTg), fluent) = pair; + var ((((createInfos, updateInfos), combinedInfos), hasTg), fluent) = pair; var nestedSeen = new HashSet(); foreach (var classInfo in createInfos) { classInfo.HasTypeGen = hasTg; ApplyFluentOverridesByClassName(classInfo, DtoKind.Create, fluent); - var source = GenerateSource(classInfo, hasFluent); + var source = GenerateSource(classInfo); spc.AddSource($"{classInfo.FullyQualifiedName}.Create.g.cs", source); - EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen, hasFluent); + EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen); } foreach (var classInfo in updateInfos) { classInfo.HasTypeGen = hasTg; ApplyFluentOverridesByClassName(classInfo, DtoKind.Update, fluent); - var source = GenerateSource(classInfo, hasFluent); + var source = GenerateSource(classInfo); spc.AddSource($"{classInfo.FullyQualifiedName}.Update.g.cs", source); - EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen, hasFluent); + EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen); } foreach (var classInfo in combinedInfos) { classInfo.HasTypeGen = hasTg; ApplyFluentOverridesByClassName(classInfo, DtoKind.Combined, fluent); - var source = GenerateSource(classInfo, hasFluent); + var source = GenerateSource(classInfo); spc.AddSource($"{classInfo.FullyQualifiedName}.Combined.g.cs", source); - EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen, hasFluent); + EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen); } }); @@ -376,26 +366,26 @@ void VisitType(INamedTypeSymbol type) .Where(static info => info is not null) .Select(static (info, _) => info!); - context.RegisterSourceOutput(crudImpliedDtos.Combine(hasFluentValidation).Combine(hasQueryDsl).Combine(hasEfCore).Combine(hasTypeGen).Combine(fluentConfig), static (spc, pair) => + context.RegisterSourceOutput(crudImpliedDtos.Combine(hasQueryDsl).Combine(hasEfCore).Combine(hasTypeGen).Combine(fluentConfig), static (spc, pair) => { - var (((((implied, hasFluent), hasDsl), hasEf), hasTg), fluent) = pair; + var ((((implied, hasDsl), hasEf), hasTg), fluent) = pair; var nestedSeen = new HashSet(); foreach (var classInfo in implied.CreateDtos) { classInfo.HasTypeGen = hasTg; ApplyFluentOverridesByClassName(classInfo, DtoKind.Create, fluent); - var source = GenerateSource(classInfo, hasFluent); + var source = GenerateSource(classInfo); spc.AddSource($"{classInfo.FullyQualifiedName}.Create.CrudImplied.g.cs", source); - EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen, hasFluent); + EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen); } foreach (var classInfo in implied.UpdateDtos) { classInfo.HasTypeGen = hasTg; ApplyFluentOverridesByClassName(classInfo, DtoKind.Update, fluent); - var source = GenerateSource(classInfo, hasFluent); + var source = GenerateSource(classInfo); spc.AddSource($"{classInfo.FullyQualifiedName}.Update.CrudImplied.g.cs", source); - EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen, hasFluent); + EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen); } foreach (var responseInfo in implied.ResponseDtos) { @@ -436,25 +426,25 @@ void VisitType(INamedTypeSymbol type) .Where(static info => info is not null) .Select(static (info, _) => info!); - context.RegisterSourceOutput(modelImpliedDtos.Combine(hasFluentValidation).Combine(hasQueryDsl).Combine(hasEfCore).Combine(hasTypeGen).Combine(fluentConfig), static (spc, pair) => + context.RegisterSourceOutput(modelImpliedDtos.Combine(hasQueryDsl).Combine(hasEfCore).Combine(hasTypeGen).Combine(fluentConfig), static (spc, pair) => { - var (((((implied, hasFluent), hasDsl), hasEf), hasTg), fluent) = pair; + var ((((implied, hasDsl), hasEf), hasTg), fluent) = pair; var nestedSeen = new HashSet(); foreach (var classInfo in implied.CreateDtos) { classInfo.HasTypeGen = hasTg; ApplyFluentOverridesByClassName(classInfo, DtoKind.Create, fluent); - var source = GenerateSource(classInfo, hasFluent); + var source = GenerateSource(classInfo); spc.AddSource($"{classInfo.FullyQualifiedName}.Create.Model.g.cs", source); - EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen, hasFluent); + EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen); } foreach (var classInfo in implied.UpdateDtos) { classInfo.HasTypeGen = hasTg; ApplyFluentOverridesByClassName(classInfo, DtoKind.Update, fluent); - var source = GenerateSource(classInfo, hasFluent); + var source = GenerateSource(classInfo); spc.AddSource($"{classInfo.FullyQualifiedName}.Update.Model.g.cs", source); - EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen, hasFluent); + EmitDeduplicatedNested(spc, classInfo.AutoNestedDtos, nestedSeen); } foreach (var responseInfo in implied.ResponseDtos) { @@ -485,10 +475,10 @@ void VisitType(INamedTypeSymbol type) // fluent is the sole source of truth. For classes with markers, this pipeline // is a no-op — those go through the attribute pipelines above (which themselves // pick up fluent overrides via ApplyFluentOverridesByClassName). - context.RegisterSourceOutput(fluentConfig.Combine(hasFluentValidation).Combine(hasQueryDsl).Combine(hasEfCore).Combine(hasTypeGen), + context.RegisterSourceOutput(fluentConfig.Combine(hasQueryDsl).Combine(hasEfCore).Combine(hasTypeGen), static (spc, pair) => { - var ((((parsed, hasFluent), hasDsl), hasEf), hasTg) = pair; + var (((parsed, hasDsl), hasEf), hasTg) = pair; if (parsed is null) return; var nestedSeen = new HashSet(); @@ -502,8 +492,8 @@ void VisitType(INamedTypeSymbol type) if (info is not null) { info.HasTypeGen = hasTg; - spc.AddSource($"{info.FullyQualifiedName}.Create.Fluent.g.cs", GenerateSource(info, hasFluent)); - EmitDeduplicatedNested(spc, info.AutoNestedDtos, nestedSeen, hasFluent); + spc.AddSource($"{info.FullyQualifiedName}.Create.Fluent.g.cs", GenerateSource(info)); + EmitDeduplicatedNested(spc, info.AutoNestedDtos, nestedSeen); } } if (tc.Update) @@ -512,8 +502,8 @@ void VisitType(INamedTypeSymbol type) if (info is not null) { info.HasTypeGen = hasTg; - spc.AddSource($"{info.FullyQualifiedName}.Update.Fluent.g.cs", GenerateSource(info, hasFluent)); - EmitDeduplicatedNested(spc, info.AutoNestedDtos, nestedSeen, hasFluent); + spc.AddSource($"{info.FullyQualifiedName}.Update.Fluent.g.cs", GenerateSource(info)); + EmitDeduplicatedNested(spc, info.AutoNestedDtos, nestedSeen); } } if (tc.CreateOrUpdate) @@ -522,8 +512,8 @@ void VisitType(INamedTypeSymbol type) if (info is not null) { info.HasTypeGen = hasTg; - spc.AddSource($"{info.FullyQualifiedName}.Combined.Fluent.g.cs", GenerateSource(info, hasFluent)); - EmitDeduplicatedNested(spc, info.AutoNestedDtos, nestedSeen, hasFluent); + spc.AddSource($"{info.FullyQualifiedName}.Combined.Fluent.g.cs", GenerateSource(info)); + EmitDeduplicatedNested(spc, info.AutoNestedDtos, nestedSeen); } } if (tc.Response) @@ -668,17 +658,17 @@ void VisitType(INamedTypeSymbol type) }); } - private static void EmitDeduplicatedNested(SourceProductionContext spc, List nested, HashSet seen, bool hasFluent) + private static void EmitDeduplicatedNested(SourceProductionContext spc, List nested, HashSet seen) { foreach (var n in nested) { var key = $"{n.RequestName}:{n.Kind}"; if (!seen.Add(key)) continue; - var source = GenerateSource(n, hasFluent); + var source = GenerateSource(n); var hintName = n.FullyQualifiedName.Replace("?", "_").Replace("<", "_").Replace(">", "_"); spc.AddSource($"{hintName}.{n.Kind}.AutoNested.g.cs", source); // Recurse for deeply nested - EmitDeduplicatedNested(spc, n.AutoNestedDtos, seen, hasFluent); + EmitDeduplicatedNested(spc, n.AutoNestedDtos, seen); } }