ReSharper SDK Adventures Part 6 — Read-only inteface generator
Welcome to another instalment of the ReSharper SDK Adventures! In this part of the series, we are going to talk about code generation and, specifically, about extending the Generate menu with your own actions. As an example, we are going to go through a step-by-step process of creating an action that would generate a read-only interface.
The full source code for the generator action is available here.
Read-only interfaces? In C#?
Indeed! Of course, C# does not have any built-in immutability mechanisms, but that won’t be a problem. Consider a simple
Let’s say we want to pass this
Person into a method that’s not allowed to modify the instance being passed in. How can we do it? Well, one of the ways is to make properties
private set;, but what if there are cases when we do want
Person to be modifiable? The solution is to define a read-only interface similar to
Notice how the interface properties only have
get; and not
set;? The Person class can now implement this interface and be passed into methods that aren’t allowed to change it:
So now that we know what we want, let’s see how one would go about adding such an item to ReSharper’s Generate menu.
Generate item provider
We’ll start with the simplest possible construct: a generator item provider. This is a class that is decorated with the
[GeneratorProvider] attribute and it implements the
IGenerateActionProvider interface. Its purpose is to provide one or more generator workflows through a single method called
CreateWorkflow(). In the context of generators, a workflow is a set of steps that involve both showing the generator UI (in our case, we’ll let the user pick which properties make it into the interface) as well as generating the actual code.
Our implementation of the generate item provider is rather simple:
The above code is one of the places where an
IDataContext is available, which is a great place to grab hold of the
PsiIconManager and get the icon for our generated item. In this case, for obvious reasons we’ll go with the Interface icon. This icon, once acquired, is simply passed into the constructor of our workflow, which we’ll discuss very soon.
It is sometimes benefitial to invoke generators as actions. This allows, among other things, to bind a Generate action directly to a keyboard shortcut. To implement this, we need to create an action, i.e. a class decorated with the
[ActionHandler] attribute. There’s also a special base class,
GenerateActionBase<T>, that is used for providing generator actions. The
T variable relates to the provider class we made earlier. So, for our generator, we’ll define an action as follows:
In the above, we specify a textual representation of the action, as well as indicate that a menu should be shown even if our action is the only one in it. The critical piece of information is the argument passed to the
ActionHandler: this is the action ID, and it is used in the workflow definition that follows.
The workflow class is a kind of glue that binds things together and allows us to indicate two main things: the action id (as we’ve just defined it) as well as the generator builder where all the generator action actually happens. Please note that the generator builder is referred to by name or, more precisely, by kind. Here is the definition of our workflow:
Just to reiterate, in the above,
"ReadOnlyInterface"string refers to the generator builder (we’ll meet it in a moment).
"Generate.ReadOnlyInterface"is the action ID.
Other entries are used to provide textual information as well as specify the action group where our action belongs – in this case, a group related to CLR languages.
The generator builder is the part that makes explicit which elements and options appear in the generator dialog, and what code actually gets generated. Our implementation of a read-only interface consists of these steps:
Find all suitable class properties and present a dialog box for user to pick them
Create the new interface and insert it into the same file
Ensure our original class implements the interface
The generator builder is just a class that is decorated with the
[GeneratorBuilder] attribute that indicates the generator kind and also the language the builder corresponds to, e.g.:
This class is expected to override only one method called
Process() where generation takes place after the user presses the Ok button on the Generate dialog. I’ll use CPS (continutation passing style) here to better demonstrate the sequence of operations as we build the interface. Let’s start with the outline:
In the above,
ReadOnlyInterfaceBuilderWorkflow is a static class that uses the CPS paradigm. Its
Start() method performs a rudimentary check and then sends us off to create the new interface:
We create an empty interface to begin with…
… and before adding its members we add it to the containing namespace:
Now that the interface has been added to the file we’re in, we can take each of the properties the user selected and add the appropriate member. I’ve cleverly omitted the process of gathering those properties to begin with because that will appear in the subsequent section – for now, we just assume that the
CSharpGeneratorContext that we’ve been taking through all our method calls contains an
InputElements property that has all the items the user selected.
Thus, we add properties to the type as follows:
And, finally, the easy part — making sure that the original class implements the interface:
And that’s it! As you can see, we’ve used
context.ClassDeclaration to get at the original class, which sure makes manipulating things easier! This is almost it for the generator, but where did those properties in
context.InputElements come from?
Generator element providers
The items that appear as check boxes in the Generate UI and subsequently appear in
context.InputElements are supplied by generator element providers. Such a provider is a class decorated with the
[GeneratorElementProvider] attribute and inheriting from
TGeneratorContext depends on the language. In our case, the provider looks as follows:
This class has only one important method called
Populate(). This method gets a
CSharpGeneratorContext as a parameter, and can use that context both to get at the object of our code generation efforts (i.e., the class we’re working with) as well as exposing the
ProvidedElements collection that we can populate with, in our case, all suitable properties.
The type of each element is
IGeneratorElement. This interface has a number of concrete implementors (e.g., for XAML events). The type we’ll use is a simple
T is the type of declared element that we’ll be providing. Since generators typically provide a wide variety of members, and not just properties, we may as well use something very wide-ranging, such as e.g.
ITypeOwner. All that matters is that we can correctly supply the declared element of the member.
So here’s what the implementation of
Populate() looks like. First, we do a few rudimentary checks:
And subsequently we go through each of the available property declarations, filter out unsuitable ones, wrap the rest in
GeneratorDeclaredElement entities and add them to the
Now, how does the generator builder know to use this item provider? It’s all in that kind string that we decorated both the builder and element provider classes with. That’s what made the magic happen.
Making a generator item is not that difficult, though it is perhaps a bit more convoluted than a simple context action. The possibilities for code generation are truly limitless, so you can simply take the source code, tweak it and you’ll be making your own generators in no time at all. Good luck!
Subscribe to Blog updates
Thanks, we've got you!
Eager, Lazy and Explicit Loading with Entity Framework Core
Entity Framework Core (EF Core) supports a number of ways to load related data. There’s eager loading, lazy loading, and explicit loading. Each of these approaches have their own advantages and drawbacks. In this post, let’s have a quick look at each of these ways to load data for navigational prope…
OSS Power-Ups: bUnit – Webinar Recording
The recording of our webinar, OSS Power-Ups: bUnit, with Egil Hansen and Steven Giesel, is available. This was the twelfth episode of our OSS Power-Ups series, where we put a spotlight on open-source .NET projects. Subscribe to our community newsletter to receive notifications about future webi…
Accelerating Your Testing Workflow with Unit Test Creation and Navigation
Unit tests play an important role in our daily development workflow. They help us ensure our codebase's correctness when writing new functionality or performing refactorings to improve readability and maintainability. In the process, we often create new test files that accompany the p…
Introducing Predictive Debugging: A Game-Changing Look into the Future
With the introduction of debugging tools, software developers were empowered to interactively investigate the control flow of software programs to find bugs in live environments. At JetBrains, we've always strived to improve the art of debugging. Besides the more standard things you expect from a de…