Skip to content

Commit dc0b6c9

Browse files
authored
Opt-in parameter to disable auditable fields (#238)
1 parent 6f269a9 commit dc0b6c9

File tree

4 files changed

+84
-37
lines changed

4 files changed

+84
-37
lines changed

docs/09_GenerateDtosAttribute.md

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# GenerateDtos Attribute Reference
22

3-
The `[GenerateDtos]` and `[GenerateAuditableDtos]` attributes automatically generate standard CRUD DTOs (Create, Update, Response, Query, Upsert, Patch) for domain models, eliminating the need to manually write repetitive DTO classes.
3+
The `[GenerateDtos]` attribute automatically generates standard CRUD DTOs (Create, Update, Response, Query, Upsert, Patch) for domain models, eliminating the need to manually write repetitive DTO classes.
44

55
## GenerateDtos Attribute
66

@@ -27,6 +27,7 @@ public class User
2727
| `OutputType` | `OutputType`| The output type for generated DTOs (default: Record). |
2828
| `Namespace` | `string?` | Custom namespace for generated DTOs (default: same as source type). |
2929
| `ExcludeProperties` | `string[]` | Properties to exclude from all generated DTOs. |
30+
| `ExcludeAuditFields` | `bool` | Automatically exclude common audit fields (default: false). See [Excluding Audit Fields](#excluding-audit-fields). |
3031
| `Prefix` | `string?` | Custom prefix for generated DTO names. |
3132
| `Suffix` | `string?` | Custom suffix for generated DTO names. |
3233
| `IncludeFields` | `bool` | Include public fields from the source type (default: false). |
@@ -56,32 +57,62 @@ public class User
5657
| `Struct` | Generate as structs |
5758
| `RecordStruct`| Generate as record structs |
5859

59-
## GenerateAuditableDtos Attribute
60+
## Excluding Audit Fields
6061

61-
A specialized version of `GenerateDtos` that automatically excludes common audit fields: `CreatedDate`, `UpdatedDate`, `CreatedAt`, `UpdatedAt`, `CreatedBy`, `UpdatedBy`.
62+
Use the `ExcludeAuditFields` property to automatically exclude common audit/tracking fields from the generated DTOs.
63+
64+
When `ExcludeAuditFields = true`, the following fields are automatically excluded:
65+
- `CreatedDate`, `UpdatedDate`
66+
- `CreatedAt`, `UpdatedAt`
67+
- `CreatedBy`, `UpdatedBy`
68+
- `CreatedById`, `UpdatedById`
6269

6370
### Usage
6471

6572
```csharp
66-
[GenerateAuditableDtos(Types = DtoTypes.Create | DtoTypes.Update)]
73+
[GenerateDtos(Types = DtoTypes.Create | DtoTypes.Update, ExcludeAuditFields = true)]
6774
public class AuditableEntity
6875
{
76+
public int Id { get; set; }
6977
public string Name { get; set; }
7078
public string Description { get; set; }
71-
public DateTime CreatedAt { get; set; }
72-
public DateTime UpdatedAt { get; set; }
73-
public string CreatedBy { get; set; }
74-
public string UpdatedBy { get; set; }
79+
public DateTime CreatedAt { get; set; } // Will be excluded
80+
public DateTime UpdatedAt { get; set; } // Will be excluded
81+
public string CreatedBy { get; set; } // Will be excluded
82+
public string UpdatedBy { get; set; } // Will be excluded
7583
}
7684
```
7785

78-
### Parameters
86+
You can combine `ExcludeAuditFields` with `ExcludeProperties` to exclude additional properties:
87+
88+
```csharp
89+
[GenerateDtos(ExcludeAuditFields = true, ExcludeProperties = new[] { "InternalNotes", "SecretKey" })]
90+
public class Product
91+
{
92+
public int Id { get; set; }
93+
public string Name { get; set; }
94+
public string InternalNotes { get; set; } // Will be excluded
95+
public string SecretKey { get; set; } // Will be excluded
96+
public DateTime CreatedAt { get; set; } // Will be excluded (audit field)
97+
}
98+
```
99+
100+
## Obsolete: GenerateAuditableDtos Attribute
79101

80-
Same as `GenerateDtos` with the addition of automatic exclusion of audit fields.
102+
> **?? Deprecated:** The `[GenerateAuditableDtos]` attribute has been replaced by `[GenerateDtos]` with `ExcludeAuditFields = true`. The old attribute will be removed in a future version.
103+
>
104+
> **Migration:**
105+
> ```csharp
106+
> // Old way (deprecated):
107+
> [GenerateAuditableDtos(Types = DtoTypes.Create)]
108+
>
109+
> // New way:
110+
> [GenerateDtos(Types = DtoTypes.Create, ExcludeAuditFields = true)]
111+
> ```
81112
82113
## Multiple Attribute Usage
83114
84-
Both attributes support multiple applications for fine-grained control:
115+
The attribute supports multiple applications for fine-grained control:
85116
86117
```csharp
87118
[GenerateDtos(Types = DtoTypes.Response, ExcludeProperties = new[] { "Password", "InternalNotes" })]

src/Facet.Attributes/GenerateDtosAttribute.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public enum OutputType
3030
}
3131

3232
/// <summary>
33-
/// Generates standard CRUD DTOs (Create, Update, Response, Query, Upsert) for a domain model.
33+
/// Generates standard CRUD DTOs (Create, Update, Response, Query, Upsert, Patch) for a domain model.
3434
/// Can be applied multiple times with different configurations for fine-grained control.
3535
/// </summary>
3636
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
@@ -52,10 +52,19 @@ public class GenerateDtosAttribute : Attribute
5252
public string? Namespace { get; set; }
5353

5454
/// <summary>
55-
/// Additional properties to exclude from all generated DTOs.
55+
/// Properties to exclude from all generated DTOs.
5656
/// </summary>
5757
public string[] ExcludeProperties { get; set; } = Array.Empty<string>();
5858

59+
/// <summary>
60+
/// When true, automatically excludes common audit fields from generated DTOs.
61+
/// <para>
62+
/// Excluded fields: CreatedDate, UpdatedDate, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy, CreatedById, UpdatedById.
63+
/// </para>
64+
/// Default is false.
65+
/// </summary>
66+
public bool ExcludeAuditFields { get; set; } = false;
67+
5968
/// <summary>
6069
/// Custom prefix for generated DTO names (default: none).
6170
/// </summary>
@@ -89,10 +98,21 @@ public class GenerateDtosAttribute : Attribute
8998
}
9099

91100
/// <summary>
92-
/// Predefined attribute for common auditable entity scenarios.
93-
/// Automatically excludes common audit fields: CreatedDate, UpdatedDate, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy.
94-
/// Can be applied multiple times with different configurations for fine-grained control.
101+
/// Obsolete. Use <see cref="GenerateDtosAttribute"/> with <c>ExcludeAuditFields = true</c> instead.
95102
/// </summary>
103+
/// <remarks>
104+
/// This attribute has been replaced by <see cref="GenerateDtosAttribute"/> with the <c>ExcludeAuditFields</c> property.
105+
/// </remarks>
106+
/// <example>
107+
/// <code>
108+
/// // Old way (deprecated):
109+
/// [GenerateAuditableDtos(Types = DtoTypes.Create)]
110+
///
111+
/// // New way:
112+
/// [GenerateDtos(Types = DtoTypes.Create, ExcludeAuditFields = true)]
113+
/// </code>
114+
/// </example>
115+
[Obsolete("Use GenerateDtosAttribute with ExcludeAuditFields = true instead. This attribute will be removed in a future version.")]
96116
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
97117
public class GenerateAuditableDtosAttribute : Attribute
98118
{

src/Facet/Generators/FacetGenerators/GenerateDtosGenerator.cs

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Facet.Generators;
1515
public sealed class GenerateDtosGenerator : IIncrementalGenerator
1616
{
1717
private const string GenerateDtosAttributeName = "Facet.GenerateDtosAttribute";
18+
// Keep for backward compatibility with obsolete attribute
1819
private const string GenerateAuditableDtosAttributeName = "Facet.GenerateAuditableDtosAttribute";
1920

2021
// Diagnostic for generator internal errors
@@ -46,19 +47,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4647
.ForAttributeWithMetadataName(
4748
GenerateDtosAttributeName,
4849
predicate: static (node, _) => node is TypeDeclarationSyntax,
49-
transform: static (ctx, token) => GetGenerateDtosModels(ctx, token))
50+
transform: static (ctx, token) => GetGenerateDtosModels(ctx, token, forceExcludeAuditFields: false))
5051
.Where(static m => m is not null)
5152
.SelectMany(static (models, _) => models!);
5253

54+
// Obsolete attribute: GenerateAuditableDtos (kept for backward compatibility)
5355
var generateAuditableDtosTargets = context.SyntaxProvider
5456
.ForAttributeWithMetadataName(
5557
GenerateAuditableDtosAttributeName,
5658
predicate: static (node, _) => node is TypeDeclarationSyntax,
57-
transform: static (ctx, token) => GetGenerateAuditableDtosModels(ctx, token))
59+
transform: static (ctx, token) => GetGenerateDtosModels(ctx, token, forceExcludeAuditFields: true))
5860
.Where(static m => m is not null)
5961
.SelectMany(static (models, _) => models!);
6062

61-
var allTargets = generateDtosTargets.Collect().Combine(generateAuditableDtosTargets.Collect())
63+
var allTargets = generateDtosTargets.Collect()
64+
.Combine(generateAuditableDtosTargets.Collect())
6265
.Select(static (combined, _) => combined.Left.Concat(combined.Right));
6366

6467
context.RegisterSourceOutput(allTargets, (spc, models) =>
@@ -86,17 +89,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
8689
});
8790
}
8891

89-
private static IEnumerable<GenerateDtosTargetModel>? GetGenerateDtosModels(GeneratorAttributeSyntaxContext context, CancellationToken token)
90-
{
91-
return GetDtosModels(context, token, isAuditable: false);
92-
}
93-
94-
private static IEnumerable<GenerateDtosTargetModel>? GetGenerateAuditableDtosModels(GeneratorAttributeSyntaxContext context, CancellationToken token)
95-
{
96-
return GetDtosModels(context, token, isAuditable: true);
97-
}
98-
99-
private static IEnumerable<GenerateDtosTargetModel>? GetDtosModels(GeneratorAttributeSyntaxContext context, CancellationToken token, bool isAuditable)
92+
private static IEnumerable<GenerateDtosTargetModel>? GetGenerateDtosModels(GeneratorAttributeSyntaxContext context, CancellationToken token, bool forceExcludeAuditFields)
10093
{
10194
token.ThrowIfCancellationRequested();
10295
if (context.TargetSymbol is not INamedTypeSymbol sourceSymbol) return null;
@@ -109,7 +102,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
109102
{
110103
token.ThrowIfCancellationRequested();
111104

112-
var model = GetDtosModel(context, attribute, sourceSymbol, isAuditable, token);
105+
var model = GetDtosModel(context, attribute, sourceSymbol, forceExcludeAuditFields, token);
113106
if (model != null)
114107
{
115108
models.Add(model);
@@ -119,7 +112,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
119112
return models.Count > 0 ? models : null;
120113
}
121114

122-
private static GenerateDtosTargetModel? GetDtosModel(GeneratorAttributeSyntaxContext context, AttributeData attribute, INamedTypeSymbol sourceSymbol, bool isAuditable, CancellationToken token)
115+
private static GenerateDtosTargetModel? GetDtosModel(GeneratorAttributeSyntaxContext context, AttributeData attribute, INamedTypeSymbol sourceSymbol, bool forceExcludeAuditFields, CancellationToken token)
123116
{
124117
token.ThrowIfCancellationRequested();
125118

@@ -135,6 +128,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
135128
var generateConstructors = GetNamedArg(attribute.NamedArguments, "GenerateConstructors", true);
136129
var generateProjections = GetNamedArg(attribute.NamedArguments, "GenerateProjections", true);
137130
var useFullName = GetNamedArg(attribute.NamedArguments, "UseFullName", false);
131+
132+
// New property: ExcludeAuditFields (only on GenerateDtosAttribute, not on obsolete attribute)
133+
var excludeAuditFields = forceExcludeAuditFields || GetNamedArg(attribute.NamedArguments, "ExcludeAuditFields", false);
138134

139135
// Fix the ExcludeProperties handling
140136
var userExcludeProperties = new List<string>();
@@ -153,7 +149,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
153149
// Build exclusion list
154150
var excludeProperties = new HashSet<string>(userExcludeProperties, System.StringComparer.OrdinalIgnoreCase);
155151

156-
if (isAuditable)
152+
if (excludeAuditFields)
157153
{
158154
foreach (var field in DefaultAuditFields)
159155
{
@@ -223,8 +219,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
223219
catch (Exception ex)
224220
{
225221
// Return null to skip this model, but the error is captured in the exception
226-
// Note: In incremental generators, we can't report diagnostics from the transform phase.
227-
// Consider adding error information to the model in the future to report in output phase.
228222
System.Diagnostics.Debug.WriteLine($"GenerateDtos error for {sourceSymbol.Name}: {ex.Message}");
229223
return null;
230224
}

test/Facet.Tests/TestModels/GenerateDtosTestEntities.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public class PatchTestEntity
3333
public decimal Price { get; set; }
3434
}
3535

36-
[GenerateAuditableDtos(Types = DtoTypes.Create | DtoTypes.Update | DtoTypes.Response, OutputType = OutputType.Class)]
36+
// Uses ExcludeAuditFields = true to automatically exclude audit fields
37+
[GenerateDtos(Types = DtoTypes.Create | DtoTypes.Update | DtoTypes.Response, OutputType = OutputType.Class, ExcludeAuditFields = true)]
3738
public class TestProduct
3839
{
3940
public int Id { get; set; }
@@ -92,7 +93,8 @@ public class TestCustomNaming
9293
public DateTime PublishedAt { get; set; }
9394
}
9495

95-
[GenerateAuditableDtos(Types = DtoTypes.All, OutputType = OutputType.RecordStruct)]
96+
// Uses ExcludeAuditFields = true with RecordStruct output
97+
[GenerateDtos(Types = DtoTypes.All, OutputType = OutputType.RecordStruct, ExcludeAuditFields = true)]
9698
public class TestCompactEntity
9799
{
98100
public int Id { get; set; }

0 commit comments

Comments
 (0)