This example shows how to build an aspect that generates view-model classes for enum
types. For each enum member Foo
, the aspect will generate an IsFoo
property.
For instance, let's take the following input enum
:
1internal enum Visibility
2{
3 Visible,
4 Hidden,
5 Collapsed
6}
The aspect will generate the following output:
1using System;
2
3namespace ViewModels
4{
5 internal sealed class VisibilityViewModel
6 {
7 private readonly Visibility _value;
8
9 public VisibilityViewModel(Visibility value)
10 {
11 this._value = value;
12 }
13 public bool IsCollapsed
14 {
15 get
16 {
17 return _value == Visibility.Collapsed;
18 }
19 }
20
21 public bool IsHidden
22 {
23 get
24 {
25 return _value == Visibility.Hidden;
26 }
27 }
28
29 public bool IsVisible
30 {
31 get
32 {
33 return _value == Visibility.Visible;
34 }
35 }
36 }
37}
Step 1. Creating the aspect class and its properties
We want to use the aspect as an assembly-level custom attribute, as follows:
1[assembly: GenerateEnumViewModel(typeof(StringOptions), "ViewModels")]
2[assembly: GenerateEnumViewModel(typeof(Visibility), "ViewModels")]
To create an assembly-level aspect, we need to derive the CompilationAspect class.
We add two properties to the aspect class: EnumType
and TargetNamespace
. We initialize them from the constructor:
10public class GenerateEnumViewModelAttribute : CompilationAspect
11{
12public Type EnumType { get; }
13public string TargetNamespace { get; }
14
15public GenerateEnumViewModelAttribute(Type enumType, string targetNamespace)
16{
17 this.EnumType = enumType;
18 this.TargetNamespace = targetNamespace;
19}
Step 2. Coping with several instances
By default, Metalama supports only a single instance of each aspect class per target declaration. To allow for several instances, we must first add the [AttributeUsage] custom attribute to our aspect.
7[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
Regardless of the number of [assembly: GenerateEnumViewModel]
attributes found in the project, Metalama will call BuildAspect
, the aspect entry point method, for only one of these instances. The other instances are known as secondary instances and are available under builder.AspectInstance.SecondaryInstances
.
Therefore, we will add most of the implementation in a local function named ImplementViewModel
and invoke this method for both the primary and the secondary methods. Our BuildAspect
method starts like this:
23public override void BuildAspect(IAspectBuilder<ICompilation> builder)
24{
25 ImplementViewModel(this);
26
27 foreach (var secondaryInstance in builder.AspectInstance.SecondaryInstances)
28 {
29 ImplementViewModel((GenerateEnumViewModelAttribute)secondaryInstance.Aspect);
30 }
31
32 void ImplementViewModel(GenerateEnumViewModelAttribute aspectInstance)
The next steps will show how to build the ImplementViewModel
local function.
Step 3. Validating inputs
It's good practice to verify all assumptions and report clear error messages when the aspect user provides unexpected inputs. Here, we verify that the given type is indeed an enum
type.
First, we declare the error messages as a static field of a compile-time class:
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4
5[CompileTime]
6internal static class DiagnosticDefinitions
7{
8 public static readonly DiagnosticDefinition<INamedType> NotAnEnumError =
9 new("ENUM01",
10 Severity.Error,
11 "The type '{0}' is not an enum.");
12}
Then, we check the code and report the error if something is incorrect:
37var enumType =
38 (INamedType)TypeFactory.GetType(aspectInstance.EnumType);
39
40if (enumType.TypeKind != TypeKind.Enum)
41{
42 builder.Diagnostics.Report(
43 DiagnosticDefinitions.NotAnEnumError.WithArguments(enumType));
44 builder.SkipAspect();
45 return;
46}
For details on reporting errors, see Reporting and suppressing diagnostics.
Step 4. Introducing the class, the value field, and the constructor
We can now introduce the view-model class using the IntroduceClass method. This returns an object that we can use to add members to the value field (using IntroduceField) and the constructor (using IntroduceConstructor).
50// Introduce the ViewModel type.
51var viewModelType = builder
52 .WithNamespace(this.TargetNamespace)
53 .IntroduceClass(
54 enumType.Name + "ViewModel",
55 buildType:
56 type =>
57 {
58 type.Accessibility = enumType.Accessibility;
59 type.IsSealed = true;
60 });
61
62// Introduce the _value field.
63viewModelType.IntroduceField("_value", enumType,
64 IntroductionScope.Instance,
65 buildField: field => { field.Writeability = Writeability.ConstructorOnly; });
66
67// Introduce the constructor.
68viewModelType.IntroduceConstructor(
69 nameof(this.ConstructorTemplate),
70 args: new { T = enumType });
For details, see Introducing types and Introducing members.
Here is the T# template of the constructor:
110[Template]
111public void ConstructorTemplate<[CompileTime] T>(T value) =>
112 meta.This._value = value!;
Note that this template accepts a compile-time generic parameter T
, which represents the enum
type. The value of this parameter is set in the call to IntroduceConstructor by setting the args
parameter.
In this template, meta.This._value
compiles to this._value
. The C# compiler does not complain because meta.This
returns a dynamic
value, so we can have anything on the right hand of this expression. Metalama then just replaces meta.This
with this
.
Step 5. Introducing the view-model properties
We can finally add the IsFoo
properties. Depending on whether the enum is a multi-value bit map (i.e., [Flags]
) or a single-value type, we need different strategies.
Here is the code that adds the properties:
74// Get the field type and decides the template.
75var isFlags = enumType.Attributes.Any(a => a.Type.Is(typeof(FlagsAttribute)));
76var template = isFlags ? nameof(this.IsFlagTemplate) : nameof(this.IsMemberTemplate);
77
78// Introduce a property into the view-model type for each enum member.
79foreach (var member in enumType.Fields)
80{
81 viewModelType.IntroduceProperty(
82 template,
83 tags: new { member },
84 buildProperty: p => p.Name = "Is" + member.Name);
85}
The code first selects the proper template depending on the nature of the enum type.
Then, it enumerates the enum members, and for each member, calls the IntroduceProperty method. Note that we are passing a member
tag, which will be used by the templates.
Here is the template for the non-flags variant:
90// Template for the non-flags enum member.
91[Template]
92public bool IsMemberTemplate => meta.This._value == ((IField)meta.Tags["member"]!).Value;
93
Here is the template for the flags variant:
94// Template for a flag enum member.
95[Template]
96public bool IsFlagTemplate
97{
98 get
99 {
100 var field = (IField)meta.Tags["member"]!;
101
102 // Note that the next line does not work for the "zero" flag, but currently Metalama
103 // does not expose the constant value of the enum member so we cannot test its
104 // value at compile time.
105 return (meta.This._value & field.Value) == ((IField)meta.Tags["member"]!).Value;
106 }
107}
108
In this template, meta.Tags["member"]
refers to the member
tag passed by BuildAspect
.
Complete aspect
Putting all the pieces together, here is the complete code of the aspect:
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5
6//
7[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
8//
9//
10public class GenerateEnumViewModelAttribute : CompilationAspect
11{
12 public Type EnumType { get; }
13 public string TargetNamespace { get; }
14
15 public GenerateEnumViewModelAttribute(Type enumType, string targetNamespace)
16 {
17 this.EnumType = enumType;
18 this.TargetNamespace = targetNamespace;
19 }
20//
21
22//
23 public override void BuildAspect(IAspectBuilder<ICompilation> builder)
24 {
25 ImplementViewModel(this);
26
27 foreach (var secondaryInstance in builder.AspectInstance.SecondaryInstances)
28 {
29 ImplementViewModel((GenerateEnumViewModelAttribute)secondaryInstance.Aspect);
30 }
31
32 void ImplementViewModel(GenerateEnumViewModelAttribute aspectInstance)
33//
34
35 {
36 //
37 var enumType =
38 (INamedType)TypeFactory.GetType(aspectInstance.EnumType);
39
40 if (enumType.TypeKind != TypeKind.Enum)
41 {
42 builder.Diagnostics.Report(
43 DiagnosticDefinitions.NotAnEnumError.WithArguments(enumType));
44 builder.SkipAspect();
45 return;
46 }
47 //
48
49 //
50 // Introduce the ViewModel type.
51 var viewModelType = builder
52 .WithNamespace(this.TargetNamespace)
53 .IntroduceClass(
54 enumType.Name + "ViewModel",
55 buildType:
56 type =>
57 {
58 type.Accessibility = enumType.Accessibility;
59 type.IsSealed = true;
60 });
61
62 // Introduce the _value field.
63 viewModelType.IntroduceField("_value", enumType,
64 IntroductionScope.Instance,
65 buildField: field => { field.Writeability = Writeability.ConstructorOnly; });
66
67 // Introduce the constructor.
68 viewModelType.IntroduceConstructor(
69 nameof(this.ConstructorTemplate),
70 args: new { T = enumType });
71 //
72
73 //
74 // Get the field type and decides the template.
75 var isFlags = enumType.Attributes.Any(a => a.Type.Is(typeof(FlagsAttribute)));
76 var template = isFlags ? nameof(this.IsFlagTemplate) : nameof(this.IsMemberTemplate);
77
78 // Introduce a property into the view-model type for each enum member.
79 foreach (var member in enumType.Fields)
80 {
81 viewModelType.IntroduceProperty(
82 template,
83 tags: new { member },
84 buildProperty: p => p.Name = "Is" + member.Name);
85 }
86 //
87 }
88 }
89
90 // Template for the non-flags enum member.
91 [Template]
92 public bool IsMemberTemplate => meta.This._value == ((IField)meta.Tags["member"]!).Value;
93
94 // Template for a flag enum member.
95 [Template]
96 public bool IsFlagTemplate
97 {
98 get
99 {
Info CS9258: 'field' is a contextual keyword in property accessors starting in language version preview. Use '@field' instead.
100 var field = (IField)meta.Tags["member"]!;
101
102 // Note that the next line does not work for the "zero" flag, but currently Metalama
103 // does not expose the constant value of the enum member so we cannot test its
104 // value at compile time.
Info CS9258: 'field' is a contextual keyword in property accessors starting in language version preview. Use '@field' instead.
105 return (meta.This._value & field.Value) == ((IField)meta.Tags["member"]!).Value;
106 }
107 }
108
109 //
110 [Template]
111 public void ConstructorTemplate<[CompileTime] T>(T value) =>
112 meta.This._value = value!;
113 //
114}