Open sandboxFocusImprove this doc

ToString example, step 2: Adding code fixes and refactorings

In the previous step, we've seen how to create a trivial aspect that generates the ToString method on-the-fly during compilation. In this example, we'll add more features to the aspect:

  • a [NotToString] attribute to opt-out a property from the ToString method,
  • three additions to the lightbulb menu (also named refactoring menu):
  • on each property, a suggestion to add [NotToString]
  • on a type that has [ToString], a suggestion to switch from aspect implementation to manual implementation
  • on a type that does not have [ToString], a suggestion to apply the aspect as a live template.

Suggesting to apply the aspect as a live template

Implementing the [NotToString] opt-opt attribute

When you're building an aspect, it's a good practice to provide users with simple ways to customize its behavior. In the case of [ToString] aspect, a typical requirement is to be able to remove a property from the generated interpolated string.

First, let's define a plain old custom attribute.

1[AttributeUsage( AttributeTargets.Field | AttributeTargets.Property )]
2public class NotToStringAttribute : Attribute;

To exclude fields and properties that have this attribute, we just have to add a filter to our query. Since we're going to reuse this query later, we extract it into a compile-time method of our aspect:

52[CompileTime]
53private static IEnumerable<IFieldOrProperty> GetIncludedProperties( INamedType target )
54    => target.AllFieldsAndProperties
55        .Where(
56            f => f is
57            {
58                IsStatic: false, IsImplicitlyDeclared: false, Accessibility: Accessibility.Public
59            } )
60        .Where( p => !p.Attributes.Any( typeof(NotToStringAttribute) ) );
61

We then update the IntroducedToString template method to use the GetIncludedProperties method.

Suggesting to add [NotToString]

How do we make sure that our users know that they can use our [NotToString] attribute?

Code fixes and code refactorings, which appear in the lightbulb or screwdriver menu, are an excellent way to improve the discoverability of our aspect-oriented API, and therefore the overall developer experience of our product.

We'll now see how we can add an item to the lightbulb menu of each property included in the ToString generated by our aspect.

To add code fixes and refactorings, we must implement the BuildAspect method. We then identify all relevant properties (those returned by the GetIncludedProperties method), and call the Suggest method. This method excepts an argument of type CodeFix.

To create the code fix, we use the CodeFixFactory.AddAttribute method, which simply adds a custom attribute to the specified declaration.

14// For each property, suggest a code fix to remove from ToString.
15foreach ( var property in GetIncludedProperties( builder.Target ) )
16{
17    builder.Diagnostics.Suggest(
18        CodeFixFactory.AddAttribute(
19            property,
20            typeof(NotToStringAttribute),
21            "Exclude from [ToString]" ),
22        property );
23}
24

To learn more about code fixes and refactorings, see Offering code fixes and refactorings.

Suggesting to switch to a manual implementation

There is no silver bullet, and our [ToString] aspect does not pretend to cover all situations.

Of course, when users hit some limitation of our aspect, they could remove the aspect and re-implement the ToString by hand. However, wouldn't it be easier to directly edit the source code generated by the aspect? That's what we are going to show here.

Our objective is to create a code refactoring that will apply the aspect directly to the source code, in the editor, and remove both the [ToString] and [NotToString] attributes.

As in the previous code fix, this can be achieved by calling the Suggest method -- this time, adding a suggestion to the type itself. The main difference is that, instead of using one of the ready-made code fixes offered by the CodeFixFactory class, we will have to create our own. To do this, we need to create a CodeFix instance and to supply a delegate that will apply the code fix in three steps:

  1. Apply the aspect.
  2. Remove any [ToString] attribute.
  3. Remove any [NotToString] attribute.

Here is the code:

28// Suggest to switch to manual implementation.
29if ( builder.AspectInstance.Predecessors[0].Instance is IAttribute attribute )
30{
31    builder.Diagnostics.Suggest(
32        new CodeFix(
33            "Switch to manual implementation",
34            async codeFixBuilder =>
35            {
36                await codeFixBuilder.ApplyAspectAsync( builder.Target, this );
37
38                await codeFixBuilder.RemoveAttributesAsync(
39                    builder.Target,
40                    typeof(ToStringAttribute) );
41
42                await codeFixBuilder.RemoveAttributesAsync(
43                    builder.Target,
44                    typeof(NotToStringAttribute) );
45            } ),
46        attribute );
47}
48