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:
8public class GenerateEnumViewModelAttribute : CompilationAspect
9{
10public Type EnumType { get; }
11public string TargetNamespace { get; }
12
13public GenerateEnumViewModelAttribute(Type enumType, string targetNamespace)
14{
15 this.EnumType = enumType;
16 this.TargetNamespace = targetNamespace;
17}
18
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.
6[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
7
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:
21public override void BuildAspect(IAspectBuilder<ICompilation> builder)
22{
23 ImplementViewModel(this);
24
25 foreach (var secondaryInstance in builder.AspectInstance.SecondaryInstances)
26 {
27 ImplementViewModel((GenerateEnumViewModelAttribute)secondaryInstance.Aspect);
28 }
29
30 void ImplementViewModel(
31 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:
34 (INamedType)TypeFactory.GetType(aspectInstance.EnumType);
35
36if (enumType.TypeKind != TypeKind.Enum)
37{
38 builder.Diagnostics.Report(
39 DiagnosticDefinitions.NotAnEnumError.WithArguments(enumType));
40 builder.SkipAspect();
41 return;
42}
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).
45// Introduce the ViewModel type.
46var viewModelType = builder
47 .WithNamespace(this.TargetNamespace)
48 .IntroduceClass(
49 enumType.Name + "ViewModel",
50 buildType:
51 type =>
52 {
53 type.Accessibility = enumType.Accessibility;
54 type.IsSealed = true;
55 });
56
57// Introduce the _value field.
58viewModelType.IntroduceField("_value", enumType,
59 IntroductionScope.Instance,
60 buildField: field => { field.Writeability = Writeability.ConstructorOnly; });
61
62// Introduce the constructor.
63viewModelType.IntroduceConstructor(
64 nameof(this.ConstructorTemplate),
65 args: new { T = enumType });
For details, see Introducing types and Introducing members.
Here is the T# template of the constructor:
105[Template]
106public void ConstructorTemplate<[CompileTime] T>(T value) =>
107 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:
70// Get the field type and decides the template.
71var isFlags = enumType.Attributes.Any(a => a.Type.Is(typeof(FlagsAttribute)));
72var template = isFlags ? nameof(this.IsFlagTemplate) : nameof(this.IsMemberTemplate);
73
74// Introduce a property into the view-model type for each enum member.
75foreach (var member in enumType.Fields)
76{
77 viewModelType.IntroduceProperty(
78 template,
79 tags: new { member },
80 buildProperty: p => p.Name = "Is" + member.Name);
81}
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:
86// Template for the non-flags enum member.
87[Template]
88public bool IsMemberTemplate => meta.This._value == ((IField)meta.Tags["member"]!).Value;
89
Here is the template for the flags variant:
90// Template for a flag enum member.
91[Template]
92public bool IsFlagTemplate
93{
94 get
95 {
96 var field = (IField)meta.Tags["member"]!;
97
98 // Note that the next line does not work for the "zero" flag, but currently Metalama
99 // does not expose the constant value of the enum member so we cannot test its
100 // value at compile time.
101 return (meta.This._value & field.Value) == ((IField)meta.Tags["member"]!).Value;
102 }
103}
104
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[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
7
8public class GenerateEnumViewModelAttribute : CompilationAspect
9{
10 public Type EnumType { get; }
11 public string TargetNamespace { get; }
12
13 public GenerateEnumViewModelAttribute(Type enumType, string targetNamespace)
14 {
15 this.EnumType = enumType;
16 this.TargetNamespace = targetNamespace;
17 }
18
19
20
21 public override void BuildAspect(IAspectBuilder<ICompilation> builder)
22 {
23 ImplementViewModel(this);
24
25 foreach (var secondaryInstance in builder.AspectInstance.SecondaryInstances)
26 {
27 ImplementViewModel((GenerateEnumViewModelAttribute)secondaryInstance.Aspect);
28 }
29
30 void ImplementViewModel(
31 GenerateEnumViewModelAttribute aspectInstance)
32 {
33 var enumType =
34 (INamedType)TypeFactory.GetType(aspectInstance.EnumType);
35
36 if (enumType.TypeKind != TypeKind.Enum)
37 {
38 builder.Diagnostics.Report(
39 DiagnosticDefinitions.NotAnEnumError.WithArguments(enumType));
40 builder.SkipAspect();
41 return;
42 }
43
44
45 // Introduce the ViewModel type.
46 var viewModelType = builder
47 .WithNamespace(this.TargetNamespace)
48 .IntroduceClass(
49 enumType.Name + "ViewModel",
50 buildType:
51 type =>
52 {
53 type.Accessibility = enumType.Accessibility;
54 type.IsSealed = true;
55 });
56
57 // Introduce the _value field.
58 viewModelType.IntroduceField("_value", enumType,
59 IntroductionScope.Instance,
60 buildField: field => { field.Writeability = Writeability.ConstructorOnly; });
61
62 // Introduce the constructor.
63 viewModelType.IntroduceConstructor(
64 nameof(this.ConstructorTemplate),
65 args: new { T = enumType });
66
67
68
69
70 // Get the field type and decides the template.
71 var isFlags = enumType.Attributes.Any(a => a.Type.Is(typeof(FlagsAttribute)));
72 var template = isFlags ? nameof(this.IsFlagTemplate) : nameof(this.IsMemberTemplate);
73
74 // Introduce a property into the view-model type for each enum member.
75 foreach (var member in enumType.Fields)
76 {
77 viewModelType.IntroduceProperty(
78 template,
79 tags: new { member },
80 buildProperty: p => p.Name = "Is" + member.Name);
81 }
82
83 }
84 }
85
86 // Template for the non-flags enum member.
87 [Template]
88 public bool IsMemberTemplate => meta.This._value == ((IField)meta.Tags["member"]!).Value;
89
90 // Template for a flag enum member.
91 [Template]
92 public bool IsFlagTemplate
93 {
94 get
95 {
Info CS9258: 'field' is a contextual keyword in property accessors starting in language version preview. Use '@field' instead.
96 var field = (IField)meta.Tags["member"]!;
97
98 // Note that the next line does not work for the "zero" flag, but currently Metalama
99 // does not expose the constant value of the enum member so we cannot test its
100 // value at compile time.
Info CS9258: 'field' is a contextual keyword in property accessors starting in language version preview. Use '@field' instead.
101 return (meta.This._value & field.Value) == ((IField)meta.Tags["member"]!).Value;
102 }
103 }
104
105 [Template]
106 public void ConstructorTemplate<[CompileTime] T>(T value) =>
107 meta.This._value = value!;
108}