Created
January 20, 2022 06:58
-
-
Save in-async/529aa320ae81eb9aa2eed9d9e8d6de6d to your computer and use it in GitHub Desktop.
SourceGenerator を利用して、エンティティのダーティ値を取得するコードを自動生成する案
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<TargetFramework>netstandard2.0</TargetFramework> | |
<LangVersion>9.0</LangVersion> | |
<Nullable>enable</Nullable> | |
<IsRoslynComponent>true</IsRoslynComponent> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" PrivateAssets="all" /> | |
</ItemGroup> | |
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
using Microsoft.CodeAnalysis.Text; | |
namespace FieldTracking { | |
[Generator] | |
public class FieldTrackingGenerator : ISourceGenerator { | |
private const string _attrSource = @" | |
using System; | |
using System.Collections.Generic; | |
namespace FieldTracking { | |
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] | |
sealed class FieldTrackingAttribute : Attribute { } | |
public interface IFieldTracker { | |
IEnumerable<KeyValuePair<string, object>> GetChanges(); | |
void AcceptChanges(); | |
} | |
} | |
"; | |
public void Initialize(GeneratorInitializationContext context) { | |
context.RegisterForPostInitialization(t => t.AddSource("FieldTracking.g.cs", _attrSource)); | |
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); | |
} | |
public void Execute(GeneratorExecutionContext context) { | |
if (!(context.SyntaxContextReceiver is SyntaxReceiver receiver)) { return; } | |
foreach ((TypeDeclarationSyntax typeSyntax, ITypeSymbol typeSymbol, List<DirtiablePropertySource> props) in receiver.Targets) { | |
StringBuilder source = new(); | |
source.AppendLine("using System;"); | |
source.AppendLine("using System.Collections.Generic;"); | |
source.AppendLine($"namespace {typeSymbol.ContainingNamespace.ToDisplayString()} {{"); | |
source.AppendLine($" partial {typeSyntax.Keyword.ValueText} {typeSymbol.Name} : FieldTracking.IFieldTracker {{"); | |
foreach (DirtiablePropertySource prop in props) { | |
prop.Write(source); | |
} | |
source.AppendLine($" public IEnumerable<KeyValuePair<string, object>> GetChanges() {{"); | |
foreach (DirtiablePropertySource prop in props) { | |
source.AppendLine($" if ({prop.DirtyName}) {{ yield return new(nameof({prop.PropertyName}), {prop.FieldName}); }}"); | |
} | |
source.AppendLine($" }}"); | |
source.AppendLine($" public void AcceptChanges() {{"); | |
foreach (DirtiablePropertySource prop in props) { | |
source.AppendLine($" {prop.DirtyName} = false;"); | |
} | |
source.AppendLine($" }}"); | |
source.AppendLine($" }}"); | |
source.AppendLine($"}}"); | |
context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(source.ToString(), Encoding.UTF8)); | |
} | |
} | |
private sealed class SyntaxReceiver : ISyntaxContextReceiver { | |
public List<(TypeDeclarationSyntax TypeSyntax, ITypeSymbol TypeSymbol, List<DirtiablePropertySource> Properties)> Targets { get; } = new(); | |
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { | |
if (!(context.Node is TypeDeclarationSyntax typeSyntax)) { return; } | |
if (typeSyntax.AttributeLists.Count is 0) { return; } | |
if (!typeSyntax.Modifiers.Any(t => t.IsKind(SyntaxKind.PartialKeyword))) { return; } | |
IEnumerable<AttributeSyntax> attrs = typeSyntax.AttributeLists.SelectMany(t => t.Attributes); | |
AttributeSyntax attr = attrs.FirstOrDefault(t => t.Name.ToString() switch { | |
"FieldTracking" => true, | |
"FieldTrackingAttribute" => true, | |
"FieldTracking.FieldTracking" => true, | |
"FieldTracking.FieldTrackingAttribute" => true, | |
_ => false, | |
}); | |
if (attr is null) { return; } | |
if (!(context.SemanticModel.GetDeclaredSymbol(typeSyntax) is ITypeSymbol typeSymbol)) { return; } | |
List<DirtiablePropertySource> props = new(); | |
foreach (ISymbol member in typeSymbol.GetMembers()) { | |
if (!(member is IFieldSymbol field)) { continue; } | |
if (!DirtiablePropertySource.TryCreate(field, out DirtiablePropertySource prop)) { continue; } | |
props.Add(prop); | |
} | |
Targets.Add((typeSyntax, typeSymbol, props)); | |
} | |
} | |
private readonly struct DirtiablePropertySource { | |
private static readonly char[] s_trimStartChars = new[] { '_' }; | |
/// <remarks> | |
/// プロパティ化されるフィールドの条件: | |
/// * 非 readonly | |
/// * '_' プリフィクスを除いたフィールド名が 1 文字以上 | |
/// * '_' プリフィクスを除いたフィールド名の最初の文字が小文字 | |
/// </remarks> | |
public static bool TryCreate(IFieldSymbol field, out DirtiablePropertySource result) { | |
if (field.IsReadOnly) { goto Ignore; } | |
string fieldName = field.Name; | |
string propName; | |
{ | |
string tmp = fieldName.TrimStart(s_trimStartChars); | |
if (tmp.Length < 1) { goto Ignore; } | |
if (!char.IsLower(tmp[0])) { goto Ignore; } | |
propName = char.ToUpper(tmp[0]) + tmp.Substring(1); | |
} | |
string dirtyName = fieldName + "Dirty"; | |
string typeName = field.Type.ToDisplayString(); | |
result = new(typeName, fieldName, propName, dirtyName); | |
return true; | |
Ignore: | |
result = default; | |
return false; | |
} | |
private DirtiablePropertySource( | |
string typeName | |
, string fieldName | |
, string propertyName | |
, string dirtyName | |
) { | |
TypeName = typeName; | |
FieldName = fieldName; | |
PropertyName = propertyName; | |
DirtyName = dirtyName; | |
} | |
public readonly string TypeName; | |
public readonly string FieldName; | |
public readonly string PropertyName; | |
public readonly string DirtyName; | |
public void Write(StringBuilder source) { | |
source.AppendLine($"private bool {DirtyName};"); | |
source.AppendLine($"public {TypeName} {PropertyName} {{"); | |
source.AppendLine($" get => {FieldName};"); | |
source.AppendLine($" set {{"); | |
source.AppendLine($" {FieldName} = value;"); | |
source.AppendLine($" {DirtyName} = true;"); | |
source.AppendLine($" }}"); | |
source.AppendLine($"}}"); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
using Microsoft.CodeAnalysis.Text; | |
namespace FieldTracking { | |
[Generator] | |
public class PropertyTrackingGenerator : ISourceGenerator { | |
private const string _attrSource = @" | |
using System; | |
using System.Collections.Generic; | |
namespace FieldTracking { | |
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] | |
sealed class PropertyTrackingAttribute : Attribute { } | |
public interface IPropertyTracker { | |
IEnumerable<KeyValuePair<string, object>> GetChanges(); | |
void AcceptChanges(); | |
} | |
} | |
"; | |
public void Initialize(GeneratorInitializationContext context) { | |
context.RegisterForPostInitialization(t => t.AddSource("PropertyTracking.g.cs", _attrSource)); | |
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); | |
} | |
public void Execute(GeneratorExecutionContext context) { | |
if (!(context.SyntaxContextReceiver is SyntaxReceiver receiver)) { return; } | |
foreach ((TypeDeclarationSyntax typeSyntax, ITypeSymbol typeSymbol, List<DirtiablePropertySource> props) in receiver.Targets) { | |
StringBuilder source = new(); | |
source.AppendLine("using System;"); | |
source.AppendLine("using System.Collections.Generic;"); | |
source.AppendLine($"namespace {typeSymbol.ContainingNamespace.ToDisplayString()} {{"); | |
source.AppendLine($" {typeSyntax.Modifiers} {typeSyntax.Keyword.ValueText} {typeSymbol.Name}Tracking : {typeSymbol.Name}, FieldTracking.IPropertyTracker {{"); | |
switch (typeSyntax) { | |
case ClassDeclarationSyntax classSyntax: | |
foreach (ConstructorDeclarationSyntax ctorSyntax in classSyntax.ChildNodes().Where(t => t is ConstructorDeclarationSyntax)) { | |
ParameterListSyntax paramList = ctorSyntax.ParameterList; | |
source.AppendLine($" {ctorSyntax.Modifiers} {typeSymbol.Name}Tracking{paramList} : base({string.Join(", ", paramList.Parameters.Select(t => t.Identifier))}) {{ }}"); | |
} | |
break; | |
case RecordDeclarationSyntax recordSyntax: { | |
ParameterListSyntax? paramList = recordSyntax.ParameterList; | |
if (paramList is null) { break; } | |
source.AppendLine($" public {typeSymbol.Name}Tracking{paramList} : base({string.Join(", ", paramList.Parameters.Select(t => t.Identifier))}) {{ }}"); | |
} | |
break; | |
default: | |
throw new NotSupportedException(); | |
} | |
foreach (DirtiablePropertySource prop in props) { | |
prop.Write(source); | |
} | |
source.AppendLine($" public IEnumerable<KeyValuePair<string, object>> GetChanges() {{"); | |
foreach (DirtiablePropertySource prop in props) { | |
source.AppendLine($" if ({prop.DirtyName}) {{ yield return new(nameof({prop.PropertyName}), {prop.PropertyName}); }}"); | |
} | |
source.AppendLine($" }}"); | |
source.AppendLine($" public void AcceptChanges() {{"); | |
foreach (DirtiablePropertySource prop in props) { | |
source.AppendLine($" {prop.DirtyName} = false;"); | |
} | |
source.AppendLine($" }}"); | |
source.AppendLine($" }}"); | |
source.AppendLine($"}}"); | |
context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(source.ToString(), Encoding.UTF8)); | |
} | |
} | |
private sealed class SyntaxReceiver : ISyntaxContextReceiver { | |
public List<(TypeDeclarationSyntax TypeSyntax, ITypeSymbol TypeSymbol, List<DirtiablePropertySource> Properties)> Targets { get; } = new(); | |
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { | |
if (!(context.Node is TypeDeclarationSyntax typeSyntax)) { return; } | |
if (typeSyntax.AttributeLists.Count is 0) { return; } | |
if (typeSyntax.Modifiers.Any(t => t.IsKind(SyntaxKind.SealedKeyword))) { return; } | |
IEnumerable<AttributeSyntax> attrs = typeSyntax.AttributeLists.SelectMany(t => t.Attributes); | |
AttributeSyntax attr = attrs.FirstOrDefault(t => t.Name.ToString() switch { | |
"PropertyTracking" => true, | |
"PropertyTrackingAttribute" => true, | |
"FieldTracking.PropertyTracking" => true, | |
"FieldTracking.PropertyTrackingAttribute" => true, | |
_ => false, | |
}); | |
if (attr is null) { return; } | |
if (!(context.SemanticModel.GetDeclaredSymbol(typeSyntax) is ITypeSymbol typeSymbol)) { return; } | |
List<DirtiablePropertySource> newProps = new(); | |
foreach (ISymbol member in typeSymbol.GetMembers()) { | |
if (!(member is IPropertySymbol prop)) { continue; } | |
if (!DirtiablePropertySource.TryCreate(prop, out DirtiablePropertySource newProp)) { continue; } | |
newProps.Add(newProp); | |
} | |
Targets.Add((typeSyntax, typeSymbol, newProps)); | |
} | |
} | |
private readonly struct DirtiablePropertySource { | |
private static readonly char[] s_trimStartChars = new[] { '_' }; | |
public static bool TryCreate(IPropertySymbol prop, out DirtiablePropertySource result) { | |
if (prop.IsReadOnly) { goto Ignore; } | |
if (!prop.IsVirtual) { goto Ignore; } | |
string propName = prop.Name; | |
string dirtyName = propName + "Dirty"; | |
string typeName = prop.Type.ToDisplayString(); | |
result = new(typeName, propName, dirtyName); | |
return true; | |
Ignore: | |
result = default; | |
return false; | |
} | |
private DirtiablePropertySource( | |
string typeName | |
, string propertyName | |
, string dirtyName | |
) { | |
TypeName = typeName; | |
PropertyName = propertyName; | |
DirtyName = dirtyName; | |
} | |
public readonly string TypeName; | |
public readonly string PropertyName; | |
public readonly string DirtyName; | |
public void Write(StringBuilder source) { | |
source.AppendLine($"private bool {DirtyName};"); | |
source.AppendLine($"public override {TypeName} {PropertyName} {{"); | |
source.AppendLine($" get => base.{PropertyName};"); | |
source.AppendLine($" set {{"); | |
source.AppendLine($" base.{PropertyName} = value;"); | |
source.AppendLine($" {DirtyName} = true;"); | |
source.AppendLine($" }}"); | |
source.AppendLine($"}}"); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Usage { | |
// FieldTracking 案 | |
[FieldTracking] | |
public partial record Account( | |
int AccountId | |
, string name | |
, DateTime CreatedAt | |
) | |
{ | |
private string name = name; | |
} | |
// PropertyTracking 案 | |
[PropertyTracking] | |
public record AccountRecord( | |
int AccountId | |
, string name | |
, DateTime CreatedAt | |
) | |
{ | |
public virtual string Name { get; set; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment