Hello, for this post, I have another very interesting thing to share. It's a new feature of C# language, or it's called Roslyn's. Because it only supports C# language, this feature is called Source Generator.
Usually, we can use Reflection to check the structure of our code during run-time and use this information. in program development in the way before can be difficult to do, for example
- Create a mapping to Adapt/Proxy one object to another.
- JSON Serialization that reads JSON as a POCO (POCO - Plain Old C# Object).
- Do a factory that automatically finds the Implement Interface classes we need from within an assembly or string name in the configuration file and generates an instance, or
- Create an Object Relational Mapping (ORM) system that can map C# classes into tables in a database.
But with this Source Generator system, we can use Roslyn 's ability to analyze the structure of Assembly that we used to do with Reflection since Compile . Let 's borrow the image from the blog introducing C# Source Generators and paste it here. how it works when and how
During this study So I created an SQLite-sg project too. It's a flat ORM. Ignoring Relationship, it saves/loads SQLite classes and tables by using Source Gen to generate code like you wrote it yourself, instead of using it. Reflection at Runtime. If interested, you can check the code from Github.
Differences from using a traditional Code Generator
Actually, we can create a tool that reads Assembly (.dll/.exe of .NET) with Reflection and then generates Source Code as well, but this method of using Code Generator has limitations.
- Gen can only be done after the compile is done
because we read the code structure with Reflection to be able to read the structure. Our code must be compiled first only, causing problems that follow. We can't get the generated code to be used with the project being compiled
. for the case of Serialization / ORM is to extract all the types that you want to serialize or map into another project, then compile this project first and use the Generate Code into another project and compile. Manage multiple projects instead
- Visual Studio can't see the intellisense of the code that is generated or see it as a delay once
because the generated source code is a step that occurs after compiling, so Visual Studio will see the source code only when it occurs. Generate is done (that is, need to compile another project and then run the Generate Source Code tool first) so we can't see the current IntelliSense. may cause the code to be wrong And it makes it difficult for people who are addicted to this IntelliSense system, but it depends on the project structure.
- Generate source code is usually required as a Custom Tool and must be installed separately.
because most We won't be able to use the T4 Template alone and often have to use it as a Custom Tool to help generate code. We will have to install this tool for every developer and it's a bit more restrictive if it's custom. Tool can do Gen Source Code, there must be a file In order to be used to set up the Custom Tool to work with, even if there is no need to use the file at all, for example, if the source code can be created by itself without a template, etc.
And then comes Source Generators (SourceGen).
The Source Gen system is out to solve the three straight problems of the Custom Tools exactly as mentioned above.
- The process of Generate Source Code is part of the Build process
where Roslyn (Compiler) will call our code. The code listed as Source Gen is run after Roslyn has parseed our code and has a Syntax Tree + Semantics released, and the generated code can then be added as part of an assembly that is being compiled. In the last step as well
- Intellisense can be used in realtime
because the code that has been generated is included in the assembly that is being built, so Visual Studio's IntelliSense system can see the code that has been generated immediately because Visual Studio will have to do Parse/Build our code all the time. (so it can be seen that What's there to show?)
As in this picture, the TimeSeriesTableMapping class did not exist in the first place But when I add an Attribute Table which determines the use of Source Generator with this Class TimeSeries and creates a class. TimeSeriesTableMapping is out and Visual Studio can see it right away.
- The Gen Tool can be installed as Nuget,
the Source Gen uses the project reference that wants to use the Source Gen to find the Tool used for Gen, so you can make the Tool Nuget and install it with Nuget. Normal, without any additional settings It works like a normal library installation.
How to create a Source Gen project
In order to use the Source Code, it must be created as a Project .NETStandrd2 "only" Class Library. persisted for a long time before it was usable (choose .NET 5) and
(For the code in this example It's from a project I tried with Source Gen to create a mapping for saving/loading data from a SQLite database called SQLite-sg .)
Then add Nuget: Microsoft.CodeAnalysis.Common, Microsoft.CodeAnalysis.CSharp, Microsoft.CSharp and create a normal class and paste the Attribute that Generator and implement the Implement Interface ISourceGenerator as required. The development team recommends two codes. The part is the generator part and the visitor part.
The code part of the visitor is told to Roslyn in the Initialize function, and Roslyn executes it when Roslyn is slowly chasing the parse code of the project we're going to run the Source Gen with, so we don't have to. Have to write code to sit and find by yourself that there is a Class/Method/Type What will be the goals of making our Source Gen for example if we are writing a Source Gen for Generate Class that saves the Data Class as a table in the database? You may be interested only in classes for which the Attribute Table is specified, for example.
- You can see that the code we will see the target is a Syntax Node, not a type in the case of doing Reflection. Code in this section. is that we check that in this syntax node we are visiting Is it a class declaration? And in C# 9.0, variables can be declared from is .
- You can see that we look at What is the attribute in this class declaration code and if the attribute name is Table or TableAttribute? (When we put an Attribute, it can be put as Table or TableAttribute. Compiler will be treated as an Attribute named TableAttribute as well). We will keep the reference of this class in this step. i have created SQLiteTableMappingParser at once
And in the Generate part, we will use the information that we collect in this Visit step. For example, we may store all references of Class Syntax (Code declaring Class) in a List and in the Generate step. We then go look at the list and create the source code, etc. In the example, in the Parse function, it reads the data from the source code of the target class and generates the mapping first, then uses the context command. .AddSource to insert the Generate code into the compiled target project You will see that you can name the file as well.
The part of the project that implements the Source Generator is to add references to the project that is the Source Generator just like adding references, but we need to modify the project file in two parts:
- Change LangVersion to Preview
- Set the ProjectReference Attribute OutputItemType to Analyzer and ReferenceOutputAssembly to false to tell Roslyn that this project is a project that we don't want to code inside. But it's a project that will analyze the code of this project one more time.
This will be able to use. (I've been puzzled for a very long time. Read many blogs before you can get it. Hehe)
Source Gen Restrictions
Since the Source Gen is working in the build stage and is not really an assembly, there will be differences in the work. and limitations are appropriate as follows:
- Reading the structure and code of the program is a level reading Syntax/Semantic
For those of you who haven't learned Compiler, you may not be familiar with these two terms, but in conclusion, what we can use to read the structure. It's not in the form of a Type/Method at all because it hasn't been compiled yet, but it reads the source code reading behavior like reading a text file. For example, we don't see it as a class named MyData, but we see it as " Keyword Class" followed by "Identifier named MyData" like this instead of the code used to understand the structure of the program using Reflection, it will not work at all when seeing the structure as Syntax/Semantic like this and need to be rethought according to the constraints of Syntax/Semantic in Roslyn's Object Model again
- The type name might be invalid. For
.NET primitive types such as Int32, C# can also use alias int , which uses reflection to check the type of declared variable. whether stated System.Int32, Int32 (using using System;) or int will get the same Type,
but at the Syntax/Semantic level, it's not yet a step to link Types together. The Compiler (actually Parser) will understand only that a Member is declared and a type is specified. We can read out the name of the type that the coder directly specified. For example, if the coder puts uint, we check the type of this member. Get System.UInt32 So we will get the word "uint" which is like a string instead. We have to understand for ourselves what type it is.
When Gen Code can be a bit of a problem. Especially the code that we will read the structure out. Type is invoked across the namespace and using using so that the namespace name is not specified when invoked.
For a very straight forward solution, we simply put all Using of the file we read into the source we gen, and match the Type name to the one in the source code, but it works, but using Using to rename the Type name. There will be another problem as well. must be resolved on a case by case basis.
- Attribute code doesn't work, need Workaround help
, this is not confirmed yet. But from the trial itself, it was found that the property of the Attribute could not be directly used. I thought that it was probably because the Attribute had not yet started working. (It's not being an instance of a class, it's just a text attribute calling code.) So we can't get the property of the attribute out like when using Reflection
here. Workaround can be modified by designing the Attribute. New by setting the value that we want to read in the constructor of all attributes and setting the property in the constructor instead. Normally, we can use the Default Constructor (no Parameter) and set the Attribute value separately.
When using it, who Calling an Attribute must be:
[MyAttribute(property1: "value")] is to run the constructor in a way that must specify a parameter
instead of writing
[MyAttribute(Property1="Value")] is called Default Constructor and set Property1
, it doesn't affect much. Because it was written almost no different from the original. But it's much easier to read with Roslyn's Semantic.
Therefore, it is possible that an Attribute that writes a Property as a Function should also not work. Because it still doesn't work, try to search and see if there are some people asking about this. But still haven't got a clear answer whether Roslyn has run the code of the Attribute yet or when the Source Gen is running and how to get the property of the attribute out?
- The debugging is not yet complete.
To debug the Source Gen, you will need to code. Debugger.Launch itself where it needs to be debugged and if not removed Every time we type something, even 1-2 characters, the debugger will be called immediately because Visual Studio builds our project in the background all the time. This makes the development of the Source Gen tool a bit clunky, but it can still be done and Edit & Continue won't work
. For use specifically for debugging and then putting the code into a Source Gen project is also a good idea. Personally, I use RoslynPad to test the code in the syntax reading part, then copy and paste it as well, then debug the whole solution again, something like this.