Skip to content

Writing a Custom Aspect in Code

MaudChiva edited this page Apr 15, 2022 · 3 revisions

Writing your aspects in Visual Studio (usually with C#, but this is not an obligation) is very interesting since you can benefit from Visual Studio's autocompletion and you can debug it at run time. However, compared to writing an aspect in XML, you're less impervious to change since you're not referencing a specific version of the CodeModeler API, plus the code is less accessible to other developers using your aspect (unless you make it part of the whole solution).

Example

For instance, say we developed a custom aspect named Sample.Tracking. This aspect will be a class in a standard .NET class library project. In fact, this class library project can be part of the same Visual Studio solution than the CodeModeler project. The assembly must reference at least CodeModeler.Runtime.dll, CodeModeler.Model.dll, and CodeModeler.Producers.dll assemblies (add NuGet packages SoftFluent.CodeModeler.Runtime and SoftFluent.CodeModeler.Model)

Example - Picture 354

Here is the C# code for the TrackingAspect.cs file:

using System.Collections;
using System.Collections.Generic;
using System.Xml;
using CodeModeler.Model;
using CodeModeler.Runtime.Utilities;
 
namespace Sample.Tracking
{
    public class TrackingAspect : IProjectTemplate
    {
        public static readonly XmlLineInfoDocument Descriptor;
        public const string NamespaceUri = "http://www.mycompany.com/aspects/myaspect/2013/1";
 
        static TrackingAspect()
        {
            Descriptor = new XmlLineInfoDocument();
            Descriptor.LoadXml(
@"<cm:project xmlns:cm='" + Constants.CodeModelerNamespaceUri + @"' defaultNamespace='MyAspect'>
    <cm:pattern name='My Aspect' namespaceUri='" + NamespaceUri + @"' preferredPrefix='mya' step='Tables'>
        <cm:message class='_doc'>This is some description for our custom aspect.</cf:message>
            <cm:descriptor name='enable' typeName='boolean' category='My Custom Aspect' targets='Entity' defaultValue='false' displayName='Add the MyCustomProperty' description='Description for our custom descriptor.' />
    </cm:pattern>
  </cm:project>");
        }
 
        public Project Project { get; private set; }
        public XmlElement Element { get; private set; }
 
        public XmlLineInfoDocument Run(IDictionary context)
        {
            if (context == null || !context.Contains("Project"))
            {
                // we are probably called for meta data inspection only
                return Descriptor;
            }
 
            // the dictionary contains at least these two entries
            Element = (XmlElement)context["Element"];
            Project = (Project)context["Project"];
 
            // if defined on project, do it on all entities. this is more a debug option
            bool allEntities = XmlUtilities.GetAttribute(Project.Element, "trackable", NamespaceUri, true);
 
            IEnumerable<XmlElement> elements;
            if (allEntities)
            {
                elements = Project.Package.RootModelPart.SelectElements("//*");
            }
            else
            {
                elements = Project.Package.RootModelPart.SelectElements("trackable", NamespaceUri, true);
            }
 
            foreach (var element in elements)
            {
                // use a CodeModeler utility method to get entity .NET type full name
                var entityFullName = Entity.GetClrTypeName(Project, Project.Package.RootModelPart, element);
                if (entityFullName == null) // attribute was probably not placed on an entity property
                    continue;
 
                var entity = Project.Entities[entityFullName];
                if (entity == null || entity.GetData(NamespaceUri + ":done", false))
                    continue;
 
                // TODO: implement logic
                entity.Data[NamespaceUri + ":done"] = true;
            }
 
            // Xml sent back here can be merged into M(x), but this is not recommended.
            // Instead, since we have no specific Xml to send back, but aspect description
            return Descriptor;
        }
    }
}

A compiled aspect must implement the IProjectTemplate interface:

public interface IProjectTemplate
{
    XmlLineInfoDocument Run(IDictionary context);
}

This interface requests an Xml document from the aspect. How it works is this Xml document will be merged into the Mx model at inference time.

Although it’s possible to inject concepts like entities or enumerations, expressed as Xml elements, directly from this interface, the best practice is to only return descriptors, and act on the in-memory model when it’s possible (when there’s a project instance in the context). That’s what the sample does.

Once the aspect has been compiled, you can select it using the “Add Existing AspectAspects folder node context menu:

Example - Picture 355

You can see the descriptor is presented here.

Once the aspect has been added, you will see it in the Aspects folder node and in the References folder node (as it’s a .NET assembly):

Example - Picture 356

Now, if we use that simple model:

Example - Picture 357

We can set specific aspect attributes using the Visual Property grid (“Aspects and Producers Properties” tab), according to the descriptors declared by the aspect C# code:

Example - Picture 358

Clone this wiki locally