public ClassDeclarationSyntax GenerateClassDeclaration(RecordDeclarationSyntax record, bool initFromConstructor) { var leadingClassTrivia = record.GetLeadingTrivia(); var trailingClassTrivia = record.GetTrailingTrivia(); var classSyntax = ClassDeclaration(record.Identifier).WithModifiers(record.Modifiers); var parameters = record.ParameterList?.Parameters ?? new SeparatedSyntaxList <ParameterSyntax>(); foreach (ParameterSyntax parameter in parameters) { if (parameter.Type != null) { classSyntax = classSyntax.AddMembers( PropertyDeclaration(parameter.Type, parameter.Identifier) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))) .AddModifiers(Token(SyntaxKind.PublicKeyword)) ); } } if (initFromConstructor) { var constructorDeclaration = ConstructorDeclaration(record.Identifier).AddModifiers(Token(SyntaxKind.PublicKeyword)); constructorDeclaration = constructorDeclaration.AddParameterListParameters(GenerateConstructorParameters(parameters).ToArray()); var constructorDeclarationSyntax = constructorDeclaration.WithBody(Block(GenerateAssignmentStatements(parameters))); classSyntax = classSyntax.AddMembers(constructorDeclarationSyntax); } var result = (ClassDeclarationSyntax)classSyntax.NormalizeWhitespacesSingleLineProperties().WithLeadingTrivia(leadingClassTrivia).WithTrailingTrivia(trailingClassTrivia); return(result !); }
/// <summary> /// Returns a StringBuilder with the contents of a generated source file for extension to a partial record plus its companion notifier class. /// </summary> /// <param name="recordSymbol"></param> /// <param name="recordDeclarationSyntax"></param> /// <param name="properties"></param> /// <param name="attributeSymbol"></param> /// <param name="notifySymbol"></param> /// <param name="discriminatorString"></param> /// <param name="context"></param> /// <param name="compilation"></param> /// <returns></returns> private StringBuilder ProcessRecordAndClass(INamedTypeSymbol recordSymbol, RecordDeclarationSyntax recordDeclarationSyntax, List <PropertyDeclarationSyntax> properties, ISymbol attributeSymbol, string discriminatorString, GeneratorExecutionContext context, Compilation compilation) { string namespaceName = recordSymbol.ContainingNamespace.ToDisplayString(); bool isDerived = recordSymbol.BaseType != null && recordSymbol.BaseType.ContainingNamespace.Name == recordSymbol.ContainingNamespace.Name; string leadingTrivia = recordDeclarationSyntax.GetLeadingTrivia().ToString(); List <(PropertyDeclarationSyntax syntax, IPropertySymbol symbol)> propertySymbols = new List <(PropertyDeclarationSyntax syntax, IPropertySymbol symbol)>(); foreach (var property in properties) { SemanticModel model = compilation.GetSemanticModel(property.SyntaxTree); propertySymbols.Add((property, model.GetDeclaredSymbol(property))); } // begin building the generated source StringBuilder source = new StringBuilder(); source.AppendLinesIndented(0, "using System;"); source.AppendLinesIndented(0, "using System.Collections.Generic;"); source.AppendLinesIndented(0, "using System.ComponentModel.DataAnnotations;"); source.AppendLinesIndented(0, "using System.Linq;"); source.AppendLinesIndented(0, "using Vectis.Events;"); source.AppendLinesIndented(0, ""); source.AppendLinesIndented(0, $"namespace {namespaceName}"); source.AppendLinesIndented(0, "{"); ProcessPartialRecordAddons(); source.AppendLinesIndented(1, ""); ProcessCompanionClass(); source.AppendLinesIndented(1, "}"); source.AppendLinesIndented(0, "}"); return(source); // Adds functionality to the record. void ProcessPartialRecordAddons() { source.AppendLinesIndented(1, leadingTrivia, true); if (recordSymbol.GetAttributes().Any(ad => ad.AttributeClass.Name == $"{PolymorphicRecordAttributeName}Attribute")) { var children = GetChildRecords(recordSymbol); for (int i = 0; i < children.Length; i++) { source.AppendLinesIndented(1, $"[MessagePack.Union({i}, typeof({children[i].Name}))]"); } } source.AppendLinesIndented(1, $"public {(recordSymbol.IsAbstract ? "abstract " : "")}partial record {recordSymbol.Name}"); source.AppendLinesIndented(1, "{"); // Build GetPropertyValuePairs() { source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Returns a list of <see cref=\"CreateObjectEvent.PropertyValuePair\"/> for each property of the record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"private protected {(isDerived ? "override" : "virtual")} List<CreateObjectEvent.PropertyValuePair> GetPropertyValuePairs()"); source.AppendLinesIndented(2, "{"); source.AppendLinesIndented(3, "List<CreateObjectEvent.PropertyValuePair> properties = new();"); if (isDerived) { source.AppendLinesIndented(3, "properties.AddRange(base.GetPropertyValuePairs());"); } foreach (var propertySymbol in propertySymbols.Select(p => p.symbol)) { source.AppendLinesIndented(3, $"properties.Add(new() {{ PropertyName = \"{propertySymbol.Name}\", Value = $\"{{{propertySymbol.Name}}}\" }});"); } source.AppendLinesIndented(3, "return properties;"); source.AppendLinesIndented(2, "}"); } // Build ApplyUpdatePropertyEvent() { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Applies an <see cref=\"UpdatePropertyEvent\" /> returning a new a record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, "/// <param name=\"updatePropertyEvent\"></param>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"public {(isDerived ? "override" : "virtual")} ViewModelBase ApplyUpdatePropertyEvent(UpdatePropertyEvent updatePropertyEvent) => ApplyUpdatePropertyEvent(updatePropertyEvent, \"{recordSymbol.Name}\");"); } // Build ApplyUpdatePropertyEvent(UpdatePropertyEvent updatePropertyEvent, string recordName) { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Applies an <see cref=\"UpdatePropertyEvent\" /> returning a new a record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, "/// <param name=\"updatePropertyEvent\"></param>"); source.AppendLinesIndented(2, "/// <param name=\"recordName\"></param>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"private protected {(isDerived ? "override" : "virtual")} ViewModelBase ApplyUpdatePropertyEvent(UpdatePropertyEvent updatePropertyEvent, string recordName)"); source.AppendLinesIndented(2, "{"); source.AppendLinesIndented(3, $"var source = this{(isDerived ? "" : " with { EventId = updatePropertyEvent.Id }")};"); source.AppendLinesIndented(3, ""); source.AppendLinesIndented(3, "return updatePropertyEvent.PropertyName.ToLower() switch"); source.AppendLinesIndented(3, "{"); foreach (var propertySymbol in propertySymbols.Select(p => p.symbol)) { AttributeData attributeData = propertySymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); TypedConstant readOnlyOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "ReadOnly").Value; if (readOnlyOpt.IsNull || !Convert.ToBoolean(readOnlyOpt.Value)) { source.AppendLinesIndented(4, $"\"{propertySymbol.Name.ToLower()}\" => source with {{ {propertySymbol.Name} = {GetPropertyUpdateSetter(propertySymbol, "updatePropertyEvent.NextValue")} }},"); } } if (isDerived) { source.AppendLinesIndented(4, "_ => base.ApplyUpdatePropertyEvent(updatePropertyEvent, recordName),"); } else { source.AppendLinesIndented(4, $"_ => throw new Exception($\"Cannot set property '{{updatePropertyEvent.PropertyName}}' on record of type '{{recordName}}'\"),"); } source.AppendLinesIndented(3, "};"); source.AppendLinesIndented(2, "}"); } // Build ApplyDeleteObjectEvent() and ApplyUndeleteObjectEvent() if (!isDerived) { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Applies a <see cref=\"CreateObjectEvent\" /> returning a new a record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, "/// <param name=\"deleteObjectEvent\"></param>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"public static ViewModelBase ApplyCreateObjectEvent(CreateObjectEvent createObjectEvent)"); source.AppendLinesIndented(2, "{"); var children = GetChildRecords(recordSymbol); for (int i = 0; i < children.Length; i++) { var discriminatorAttribute = children[i].GetAttributes().Where(ad => ad.AttributeClass.Name == $"{TypeDiscriminatorAttributeName}Attribute").FirstOrDefault(); var discriminator = discriminatorAttribute.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Discriminator").Value.Value.ToString(); source.AppendLinesIndented(3, $"if (createObjectEvent.TypeDiscriminator.ToLower() == \"{discriminator.ToLower()}\") return {children[i].Name}.ApplyTypedCreateObjectEvent(createObjectEvent);"); } source.AppendLinesIndented(3, "throw new Exception($\"Cannot Create a record from a CreateObjectEvent containing a TypeDiscriminator of '{createObjectEvent.TypeDiscriminator}'\");"); source.AppendLinesIndented(2, "}"); source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Applies a <see cref=\"DeleteObjectEvent\" /> returning a new a record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, "/// <param name=\"deleteObjectEvent\"></param>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"public ViewModelBase ApplyDeleteObjectEvent(DeleteObjectEvent deleteObjectEvent) => this with {{ EventId = deleteObjectEvent.Id, Deleted = true }};"); source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Applies a <see cref=\"UndeleteObjectEvent\" /> returning a new a record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, "/// <param name=\"undeleteObjectEvent\"></param>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"public ViewModelBase ApplyUndeleteObjectEvent(UndeleteObjectEvent undeleteObjectEvent) => this with {{ EventId = undeleteObjectEvent.Id, Deleted = false }};"); } // Build GetNotifier(), GetCreateObjectEvent(), and ApplyTypedCreateObjectEvent(CreateObjectEvent) only if the record is not abstract if (!recordSymbol.IsAbstract) { // Build GetNotifier { source.AppendLinesIndented(2, $""); source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Returns a <see cref=\"{GetNotifierClassName(recordSymbol.Name)}\"/> intialized with the same parameters held in this <see cref=\"{recordSymbol.Name}\"/>."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"public {GetNotifierClassName(recordSymbol.Name)} GetNotifier()"); source.AppendLinesIndented(2, "{"); source.AppendLinesIndented(3, $"return new {GetNotifierClassName(recordSymbol.Name)}(this);"); source.AppendLinesIndented(2, "}"); source.AppendLinesIndented(2, ""); } // Build GetCreateObjectEvent() { source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Returns a <see cref=\"CreateObjectEvent\"/> populated with the details in this record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"public CreateObjectEvent GetCreateObjectEvent()"); source.AppendLinesIndented(2, "{"); source.AppendLinesIndented(3, "return new()"); source.AppendLinesIndented(3, "{"); source.AppendLinesIndented(4, "Id = $\"{ViewModelEvent.NewId()}\","); source.AppendLinesIndented(4, "UserId = $\"\","); source.AppendLinesIndented(4, "IPAddress = $\"\","); source.AppendLinesIndented(4, "Timestamp = DateTime.Now,"); source.AppendLinesIndented(4, $"TypeDiscriminator = \"{discriminatorString}\","); source.AppendLinesIndented(4, "ObjectId = $\"{Id}\","); source.AppendLinesIndented(4, "Properties = GetPropertyValuePairs().ToArray()"); source.AppendLinesIndented(3, "};"); source.AppendLinesIndented(2, "}"); } // Build ApplyTypedCreateObjectEvent(CreateObjectEvent) { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Returns a <see cref=\"CreateObjectEvent\"/> populated with the details in this record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"internal static {recordSymbol.Name} ApplyTypedCreateObjectEvent(CreateObjectEvent createObjectEvent) => UpdateFromCreateObjectEvent(new(), createObjectEvent.Properties.ToList(), true).record;"); } } // Build ApplyTypedCreateObjectEvent(CreateObjectEvent, <current record>) { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, $"/// <summary>"); source.AppendLinesIndented(2, $"/// Returns a <see cref=\"CreateObjectEvent\"/> populated with the details in this record."); source.AppendLinesIndented(2, $"/// </summary>"); source.AppendLinesIndented(2, $"/// <returns></returns>"); source.AppendLinesIndented(2, $"internal static ({recordSymbol.Name} record, List<CreateObjectEvent.PropertyValuePair> properties) UpdateFromCreateObjectEvent({recordSymbol.Name} record, List<CreateObjectEvent.PropertyValuePair> properties, bool isTargetRecord)"); source.AppendLinesIndented(2, "{"); if (isDerived) { source.AppendLinesIndented(3, $"var tuple = {recordSymbol.BaseType.Name}.UpdateFromCreateObjectEvent(record, properties, false);"); source.AppendLinesIndented(3, $"record = tuple.record as {recordSymbol.Name};"); source.AppendLinesIndented(3, $"properties = tuple.properties;"); source.AppendLinesIndented(3, ""); } source.AppendLinesIndented(3, "foreach (var property in properties.ToList())"); source.AppendLinesIndented(3, "{"); var elseText = ""; foreach (var propertySymbol in propertySymbols.Select(p => p.symbol)) { source.AppendLinesIndented(4, $"{elseText}if (property.PropertyName.ToLower() == \"{propertySymbol.Name.ToLower()}\")"); source.AppendLinesIndented(4, "{"); source.AppendLinesIndented(5, $"record = record with {{ {propertySymbol.Name} = {GetPropertyUpdateSetter(propertySymbol, "property.Value")} }};"); source.AppendLinesIndented(5, "properties.Remove(property);"); source.AppendLinesIndented(4, "}"); elseText = "else "; } source.AppendLinesIndented(4, $"else if (isTargetRecord) "); source.AppendLinesIndented(4, "{"); source.AppendLinesIndented(5, $"throw new Exception($\"Cannot Create a '{recordSymbol.Name}' with a CreateObjectEvent containing a PropertyName of '{{property.PropertyName}}'\");"); source.AppendLinesIndented(4, "}"); source.AppendLinesIndented(3, "}"); source.AppendLinesIndented(3, ""); source.AppendLinesIndented(3, "return (record, properties);"); source.AppendLinesIndented(2, "}"); } source.AppendLinesIndented(1, "}"); } // Adds the companion class void ProcessCompanionClass() { source.AppendLinesIndented(1, $"/// <inheritdoc cref=\"{recordSymbol.Name}\"/>"); source.AppendLinesIndented(1, $"/// <remarks>Companion edittable view model class to <see cref=\"{recordSymbol.Name}\"/>.</remarks>"); source.AppendLinesIndented(1, $"public {(recordSymbol.IsAbstract ? "abstract " : "")}class {GetNotifierClassName(recordSymbol.Name)}{(isDerived ? " : " + GetNotifierClassName(recordSymbol.BaseType.Name) : "")}"); source.AppendLinesIndented(1, "{"); // Add the UpdatedEventHander delegate only if this is not a derived record (indicating that it's the base record) if (!isDerived) { source.AppendLinesIndented(2, "/// <summary>"); source.AppendLinesIndented(2, "/// Represents method that will handle the <see cref=\"PropertyUpdated\"/> event raised by the view model in response to a property being updated."); source.AppendLinesIndented(2, "/// </summary>"); source.AppendLinesIndented(2, "/// <param name=\"sender\"></param>"); source.AppendLinesIndented(2, "/// <param name=\"e\"></param>"); source.AppendLinesIndented(2, "public delegate void UpdatedPropertyEventHandler(object sender, ViewModelEvent e);"); } if (!recordSymbol.IsAbstract) { source.AppendLinesIndented(2, "/// <summary>"); source.AppendLinesIndented(2, "/// Occurs when a property is updated."); source.AppendLinesIndented(2, "/// </summary>"); source.AppendLinesIndented(2, "/// <param name=\"sender\"></param>"); source.AppendLinesIndented(2, "/// <param name=\"e\"></param>"); source.AppendLinesIndented(2, "public event UpdatedPropertyEventHandler PropertyUpdated;"); } // Add properties, including a copy of the originator record only if this is a non-abstract class { if (!isDerived) { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, "/// <summary>"); source.AppendLinesIndented(2, $"/// The backing <see cref=\"{recordSymbol.Name}\"/> from which this object was created."); source.AppendLinesIndented(2, "/// </summary>"); source.AppendLinesIndented(2, $"public readonly ViewModelBase OriginatorRecord;"); } // Create companion properties for each of the base record's fields foreach (var propertySymbol in propertySymbols) { ProcessCompanionClassProperty(recordSymbol.Name, propertySymbol.syntax, propertySymbol.symbol); } } // Build the constructor which calls the base constructor if this is a derived record { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, $"public {GetNotifierClassName(recordSymbol.Name)}({recordSymbol.Name} record){(isDerived ? " : base(record)" : "")}"); source.AppendLinesIndented(2, "{"); if (!isDerived) { source.AppendLinesIndented(3, $"OriginatorRecord = record;"); } foreach (var propertySymbol in propertySymbols.Select(p => p.symbol)) { source.AppendLinesIndented(3, $"{propertySymbol.Name} = record.{propertySymbol.Name};"); } source.AppendLinesIndented(2, "}"); } // Build GetRecord() if (!recordSymbol.IsAbstract) { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, "/// <summary>"); source.AppendLinesIndented(2, $"/// Builds a <see cref=\"{recordSymbol.Name}\"/> copying the values from this class."); source.AppendLinesIndented(2, "/// </summary>"); source.AppendLinesIndented(2, "/// <returns></returns>"); source.AppendLinesIndented(2, $"public {recordSymbol.Name} GetRecord() => GetRecord(new {recordSymbol.Name}());"); } // Build GetTypedRecord<T>() { source.AppendLinesIndented(2, ""); source.AppendLinesIndented(2, "/// <summary>"); source.AppendLinesIndented(2, $"/// Builds a record of type T copying the values from this class."); source.AppendLinesIndented(2, "/// </summary>"); source.AppendLinesIndented(2, "/// <returns></returns>"); source.AppendLinesIndented(2, $"private protected {recordSymbol.Name} GetRecord({recordSymbol.Name} record)");//{(isDerived ? "override" : "virtual")} source.AppendLinesIndented(2, "{"); if (isDerived) { source.AppendLinesIndented(3, $"return (base.GetRecord(record) as {recordSymbol.Name}) with"); } else { source.AppendLinesIndented(3, $"return (record as {recordSymbol.Name}) with"); } source.AppendLinesIndented(3, "{"); foreach (var propertySymbol in propertySymbols.Select(p => p.symbol)) { source.AppendLinesIndented(4, $"{propertySymbol.Name} = this.{propertySymbol.Name},"); } source.AppendLinesIndented(3, "};"); source.AppendLinesIndented(2, "}"); } return; // Adds a property to the companion class void ProcessCompanionClassProperty(string recordName, PropertyDeclarationSyntax propertyDeclarationSyntax, IPropertySymbol propertySymbol) { // get the name and type of the field ITypeSymbol fieldType = propertySymbol.Type; var fieldName = "_" + propertySymbol.Name.Substring(0, 1).ToLower() + propertySymbol.Name.Substring(1); // get the ViewModel attribute from the field, and any associated data AttributeData attributeData = propertySymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); TypedConstant readOnlyOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "ReadOnly").Value; source.AppendLinesIndented(2, ""); if (readOnlyOpt.IsNull || !Convert.ToBoolean(readOnlyOpt.Value)) { source.AppendLinesIndented(2, $"private {fieldType} {fieldName};"); source.AppendLinesIndented(2, $"/// <inheritdoc cref=\"{recordName}.{propertySymbol.Name}\" />"); foreach (var attribute in propertyDeclarationSyntax.AttributeLists.ToList().Where(attr => attr.ToString().Substring(1, ViewModelAttributeName.Length) != ViewModelAttributeName)) { source.AppendLinesIndented(2, attribute.ToString()); } source.AppendLinesIndented(2, $"public {fieldType} {propertySymbol.Name}"); source.AppendLinesIndented(2, "{"); source.AppendLinesIndented(3, $"get => {fieldName};"); source.AppendLinesIndented(3, "set"); source.AppendLinesIndented(3, "{"); source.AppendLinesIndented(4, $"if ({fieldName} != value)"); source.AppendLinesIndented(4, "{"); source.AppendLinesIndented(5, $"var updatePropertyEvent = OriginatorRecord.BuildUpdatePropertyEvent(\"{propertySymbol.Name}\", {fieldName}, value);"); source.AppendLinesIndented(5, $"{fieldName} = value;"); source.AppendLinesIndented(5, $"PropertyUpdated?.Invoke(this, updatePropertyEvent);"); source.AppendLinesIndented(4, "}"); source.AppendLinesIndented(3, "}"); source.AppendLinesIndented(2, "}"); } else { source.AppendLinesIndented(2, $"/// <inheritdoc cref=\"{recordName}.{propertySymbol.Name}\" />"); source.AppendLinesIndented(2, $"public readonly {fieldType} {propertySymbol.Name};"); } } } }