Synchronize code to design (C#)
This article explains how to add support for code to design synchronization to C# templates.
Code to design synchronization allows templates in Intent Architect to detect user edits in C# files and translate those edits back into model changes in Intent Architect Designers.
Minimum required NuGet packages:
- Intent.SoftwareFactory.SDK 3.13.0
- Intent.Modules.Common.CSharp 3.10.0
Additionally, for applications using your module, they must also use at least version 4.10.0 of the Intent.OutputManager.RoslynWeaver module for the template's ISynchronizeCSharpCodeToModel.Accept(...) method to be called.
Overview of required implementation
- Add the
ISynchronizeCSharpCodeToModelinterface to your template's class. - Implement the
void Accept(ICSharpSemanticComparisonNode rootComparisonNode)method to store the providedICSharpSemanticComparisonNodefor later. This method is called by theIntent.OutputManager.RoslynWeavermodule if it finds semantic differences in the file it generated compared to the existing file on the file system. - Implement the
IReadOnlyCollection<ICodeToModelOperation> GetCodeToOperationModels()method. This is called by the Software Factory and returns any "code to model operations" (or "instructions") to be run in Intent Architect Designers to apply changes.
The void Accept(ICSharpSemanticComparisonNode rootComparisonNode) method
This method simply needs to store the provided ICSharpSemanticComparisonNode for use later, for example you could add the following to your template:
private ICSharpSemanticComparisonNode _rootComparisonNode;
public void Accept(ICSharpSemanticComparisonNode rootComparisonNode)
{
_rootComparisonNode = rootComparisonNode;
}
This method is called by the Intent.OutputManager.RoslynWeaver module if it finds any semantic differences between the generated and existing file.
Note
Only versions 4.10.0 and greater of the Intent.OutputManager.RoslynWeaver module have the logic present which calls this method.
The IReadOnlyCollection<ICodeToModelOperation> GetCodeToOperationModels() method
This method needs to return a list of "code of model operations" for the designers to apply when the user presses uses synchronize code to design option on the Software Factory.
The operations to return varies by each template as it depends on how a template's generated content relates to metadata in Intent Architect Designers, but in essence you will need to inspect the ICSharpSemanticComparisonNode and its ChildNodes (which are also ICSharpSemanticComparisonNodes), check if each node is an Addition, Removal or Update, possibly correlate with existing members of your class and then return operations as appropriate.
Creating code to model operations
To create operations, use the CodeToModelOperationFactory static class from the Intent.CodeToModelOperations namespace, this class has an Instance property with various factory methods available on it for various operation types, there are also extension methods available for this factory class from the Intent.Modules.Common namespace providing higher level convenient overloads for most operations.
Resolving type references from type names
For model operations to create or update elements with type references in Intent Architect Designers they require an ITypeReference.
ICSharpSemanticComparisonNode has Current and Generated properties each with string TypeName { get; } members. To convert from these strings to required ITypeReferences for operations, the TryGetTypeReference on IntentTemplateBase (which CSharpTemplateBase derives from) can be used. This method was created for this purpose and (amongst other logic) uses the type resolution infrastructure already available in templates to resolve actual types, be they built in type definitions (string, int, etc) or types from other template instances which have been referred to using the AddTypeSource method on the current template.
Example: DTO
Here is the (probably) simplest possible example of reverse synchronizing properties from an existing .cs file to Field elements in the Services designer in Intent Architect.
The code below was taken from DtoModelTemplatePartial.cs (permalink) available as open source.
public IReadOnlyCollection<ICodeToModelOperation> GetCodeToOperationModels()
{
var properties = _rootComparisonNode?
.ChildNodes.FirstOrDefault(x => x.SyntaxKind == CSharpSyntaxKind.NamespaceDeclaration)?
.ChildNodes.FirstOrDefault(x => x.SyntaxKind == CSharpSyntaxKind.ClassDeclaration)?
.ChildNodes.Where(x => x.SyntaxKind == CSharpSyntaxKind.PropertyDeclaration)
.ToArray();
if (properties == null || properties.Length == 0)
{
return [];
}
var changes = new List<ICodeToModelOperation>();
foreach (var property in properties)
{
var typeReference = TryGetTypeReference((property.Current ?? property.Generated)!.TypeName!, Model.InternalElement.Package, out var reference)
? CodeToModelOperationFactory.Instance.TypeReference(reference)
: null;
switch (property.DifferenceType)
{
case CSharpDifferenceType.Added:
changes.Add(CodeToModelOperationFactory.Instance.CreateChildElement(
parent: Model.InternalElement,
newElementId: Guid.NewGuid().ToString(),
name: property.Current!.Identifier!,
specialization: DTOFieldModel.SpecializationType,
specializationId: DTOFieldModel.SpecializationTypeId,
typeReference: typeReference));
break;
case CSharpDifferenceType.Changed:
var fieldToUpdate = Model.Fields.FirstOrDefault(x => string.Equals(x.Name, property.Generated!.Identifier, StringComparison.OrdinalIgnoreCase));
if (fieldToUpdate != null)
{
changes.Add(CodeToModelOperationFactory.Instance.UpdateElement(
element: fieldToUpdate.InternalElement,
name: property.Current!.Identifier.ToPascalCase(),
typeReference: typeReference));
}
break;
case CSharpDifferenceType.Removed:
var fieldToRemove = Model.Fields.FirstOrDefault(x => string.Equals(x.Name, property.Generated!.Identifier, StringComparison.OrdinalIgnoreCase));
if (fieldToRemove != null)
{
changes.Add(CodeToModelOperationFactory.Instance.DeleteElement(fieldToRemove.InternalElement));
}
break;
}
}
return changes;
}
Example: Domain Entity
As a more complex example, the below shows synchronizing a domain entity back to a Class in the Domain Designer in Intent Architect and covering synchronization of associations, attributes, methods and parameters.
The code below was taken from DomainEntityTemplatePartial.cs (permalink) available as open source.
public IReadOnlyCollection<ICodeToModelOperation> GetCodeToOperationModels()
{
var @class = _rootComparisonNode?
.ChildNodes.FirstOrDefault(x => x.SyntaxKind == CSharpSyntaxKind.NamespaceDeclaration)?
.ChildNodes.FirstOrDefault(x => x.SyntaxKind == CSharpSyntaxKind.ClassDeclaration);
if (@class == null)
{
return [];
}
var changes = new List<ICodeToModelOperation>();
foreach (var member in @class.ChildNodes)
{
var typeReference = TryGetTypeReference((member.Current ?? member.Generated).TypeName, Model.InternalElement.Package, out var typeNameReference)
? CodeToModelOperationFactory.Instance.TypeReference(typeNameReference)
: null;
switch (member.SyntaxKind)
{
case CSharpSyntaxKind.MethodDeclaration:
switch (member.DifferenceType)
{
case CSharpDifferenceType.Added:
{
var method = CodeToModelOperationFactory.Instance.CreateChildElement(
parent: Model.InternalElement,
newElementId: Guid.NewGuid().ToString(),
name: member.Current.Identifier!,
specialization: OperationModel.SpecializationType,
specializationId: OperationModel.SpecializationTypeId,
typeReference: typeReference);
changes.Add(method);
foreach (var parameter in member.ChildNodes.Where(x => x.SyntaxKind == CSharpSyntaxKind.Parameter))
{
changes.Add(CodeToModelOperationFactory.Instance.CreateChildElement(
parent: method,
newElementId: Guid.NewGuid().ToString(),
name: parameter.Current.Identifier!,
specialization: ParameterModel.SpecializationType,
specializationId: ParameterModel.SpecializationTypeId,
typeReference: TryGetTypeReference((parameter.Current ?? parameter.Generated).TypeName!, Model.InternalElement.Package, out var parameterReference)
? CodeToModelOperationFactory.Instance.TypeReference(parameterReference)
: null));
}
break;
}
case CSharpDifferenceType.Removed:
{
var operation = Model.Operations.SingleOrDefault(x => string.Equals(x.Name, member.Generated!.Identifier, StringComparison.OrdinalIgnoreCase));
if (operation != null)
{
changes.Add(CodeToModelOperationFactory.Instance.DeleteElement(operation.InternalElement));
}
break;
}
case CSharpDifferenceType.Changed:
{
var operation = Model.Operations.SingleOrDefault(x => string.Equals(x.Name, member.Generated!.Identifier, StringComparison.OrdinalIgnoreCase));
if (operation != null)
{
changes.Add(CodeToModelOperationFactory.Instance.UpdateElement(
element: operation.InternalElement,
name: member.Current.Identifier.ToPascalCase(),
typeReference: typeReference));
}
break;
}
}
break;
case CSharpSyntaxKind.PropertyDeclaration:
// Attributes
if (typeNameReference == null ||
typeNameReference.Element.SpecializationTypeId is TypeDefinitionModel.SpecializationTypeId or EnumModel.SpecializationTypeId)
{
switch (member.DifferenceType)
{
case CSharpDifferenceType.Added:
changes.Add(CodeToModelOperationFactory.Instance.CreateChildElement(
parent: Model.InternalElement,
name: member.Current.Identifier!,
specialization: AttributeModel.SpecializationType,
specializationId: AttributeModel.SpecializationTypeId,
typeReference: typeReference));
break;
case CSharpDifferenceType.Removed:
{
var existing = Model.Attributes.FirstOrDefault(x => string.Equals(x.Name, member.Generated!.Identifier, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
break;
}
changes.Add(CodeToModelOperationFactory.Instance.DeleteElement(existing.InternalElement));
break;
}
case CSharpDifferenceType.Changed:
{
var existing = Model.Attributes.FirstOrDefault(x => string.Equals(x.Name, member.Generated!.Identifier, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
break;
}
changes.Add(CodeToModelOperationFactory.Instance.UpdateElement(
element: existing.InternalElement,
name: member.Current.Identifier.ToPascalCase(),
typeReference: typeReference));
break;
}
}
break;
}
// Associations
switch (member.DifferenceType)
{
case CSharpDifferenceType.Added:
changes.Add(CodeToModelOperationFactory.Instance.CreateAssociation(
specialization: AssociationModel.SpecializationType,
specializationId: AssociationModel.SpecializationTypeId,
targetEndElement: (IElement)typeNameReference.Element,
targetEndName: member.Current.Identifier,
targetEndIsNullable: typeNameReference.IsNullable,
targetEndIsCollection: typeNameReference.IsCollection,
ownerEndElement: Model.InternalElement));
break;
case CSharpDifferenceType.Changed:
changes.Add(CodeToModelOperationFactory.Instance.CreateAssociation(
specialization: AssociationModel.SpecializationType,
specializationId: AssociationModel.SpecializationTypeId,
targetEndElement: (IElement)typeNameReference.Element,
targetEndName: member.Current.Identifier,
targetEndIsNullable: typeNameReference.IsNullable,
targetEndIsCollection: typeNameReference.IsCollection,
ownerEndElement: Model.InternalElement));
break;
}
break;
}
}
return changes;
}
Summary
This article has explained how to implement code to design synchronization for templates with working real world examples.