Skip to content

Instantly share code, notes, and snippets.

@in-async
Created January 20, 2022 06:58
Show Gist options
  • Save in-async/529aa320ae81eb9aa2eed9d9e8d6de6d to your computer and use it in GitHub Desktop.
Save in-async/529aa320ae81eb9aa2eed9d9e8d6de6d to your computer and use it in GitHub Desktop.
SourceGenerator を利用して、エンティティのダーティ値を取得するコードを自動生成する案
<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>
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($"}}");
}
}
}
}
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($"}}");
}
}
}
}
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