I think many C # developers were looking forward to the appearance of primary constructors and record engines in C # 6.0 and were disappointed that this feature was delayed until version 7. By the end of working Thursday, the desire to have immutable types by any means overcame my patience and I decided to write a utility that generates them. Who cares - I ask under the cat.
The statement of the problem was seen very clearly, the record should contain:
- Properties with public getters
- Constructor with parameters to initialize all properties
- Copy () method with the same set of parameters, but with a default value for each
- Overloads Equals and GetHashCode, IEquatable implementation
- Operators == and! =
In general, everything is like in Scala case classes.
To describe the record, a slightly simplified syntax of C # was taken:
namespace Records { using System; record Test { Int32 Id; String Name; Nullable<Decimal> Amount; } }
Parsing the text is done with the help of Nemerle.PEG, this is the grammar:
grammar { ANY = !['\u0000'..'\u001F'] !'\u007F' ['\u0000'..'\uFFFF']; ws : void = ("\r\n" / "\n" / "\r" / "\t" / ' ')*; letter = [Lu, Ll, Lt, Lm, Lo]; digit = ['0'..'9']; keyword = "using" / "record" / "namespace"; identifier : string = letter (letter / digit)*; path : string = identifier ("." identifier)*; genericTypeDefinition : string = identifier ws"<"ws (genericTypeDefinition / identifier)(ws","ws (genericTypeDefinition / identifier))* ws">"; property : PropertyDefinition = !keyword (genericTypeDefinition / identifier) ws identifier ws";"; properties : List[PropertyDefinition] = (ws property ws)+; import : ImportDefinition = "using" ws path";"; record : RecordDefinition = "record" ws identifier ws "{" ws property (ws property)* ws "}"; nmspace : NamespaceDefinition = "namespace" ws path ws "{" (ws import)* ws record (ws record)* ws "}" ws !ANY; }
The resulting C # DOM parser generates C # source code using a CodeDOM, which is then compiled into an assembly using a CSharpCodeProvider.
For ease of implementation, a restriction was made - a new namespace should be found in each file (in the future I plan to remove this restriction). Otherwise, the language is flexible: the namespace can be immediately imported into other files, the declared types can be immediately used as field types in other record sets.
I will give a simple example of use.
Create a Units.rcs file with the following content:
namespace Units { using System; record Unit1 { Int32 Id; String Name; } record Unit2 { Int32 Id; Unit1 Unit; Decimal Amount; } }
as well as Delivery.rsc
namespace Delivery { using System; using Units; record Address { String CityName; String Street; String House; } record Package { Address Destination; Unit2 Contents; } }
In order to get the assembly you need to run the following command:
RecSharp -i Units.rcs Delivery.rcs -o Records.dll
The result will be an assembly that can be connected to the project and use the objects.
You can also use an extension for VisualStudio that generates sources like T4.
The project can be felt here:
RecSharp(in Releases there are binaries for those who do not want to install Nemerle)
Extension for VisualStudio:
RecSharp.VisualStudio(again, the release contains a compiled .vsix)
')
In perspective, I will probably move from CodeDOM to Roslyn, but after the first cursory inspection, its API for code generation looks more complicated than that of CodeDOM.
I would be glad if the utility will be useful to someone.