Code generation is a big part of most modern frameworks. .NET is full of
code generators that you've probably used - maybe without even knowing
it. XAML files are converted to .g.cs files that are then fed to the
compiler at compilation time. WCF is bundled with a code generator that
will convert a WSDL file to a client to easily connect to existing web
services. Today's tutorial will cover how to use objects provided by the
.NET framework to build your own code generator.
The code we're going to generate today won't be especially useful,
however it will demonstrate a lot of the available functionality. I'm
going to use C# to generate another C# class that can hold part of
someone's Twitter feed.
The .NET namespace that makes all this possible is
System.CodeDom.
Much like the name implies, these classes allow you to build a "DOM" of
class hierarchies and then dump them out to text.
The first thing you should check out before starting is a Twitter feed.
There's a lot of available elements in a feed, however I'm only really
interested in a couple for this tutorial - date, text, and source.
All right, let's start generating some code. Like any other C# class,
we're going to need to start with a namespace.
using System.CodeDom;
using Microsoft.CSharp;
using System.IO;
namespace CodeGeneration
{
class Program
{
static void Main(string[] args)
{
CodeCompileUnit compileUnit = new CodeCompileUnit();
// Add a namespace.
CodeNamespace twitterNamespace = new CodeNamespace("TwitterClient");
compileUnit.Namespaces.Add(twitterNamespace);
// Write the code to a file.
using (var fileStream = new StreamWriter(File.Create(@"C:\outputfile.cs")))
{
var provider = new CSharpCodeProvider();
provider.GenerateCodeFromCompileUnit(compileUnit, fileStream, null);
}
}
}
}
The root of every code DOM is the
CodeCompileUnit.
Inside it I create and add a
CodeNamespace.
Lastly, I use a
CSharpCodeProvider
to write the code I just generated to a file.
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.1
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace TwitterClient {
}
Out of the box, there's not much going on. It created some comments at
the top and our namespace. We now need a class inside this namespace to
hold a single Tweet.
// Add a Tweet class.
CodeTypeDeclaration twitterClass = new CodeTypeDeclaration("Tweet");
twitterClass.IsClass = true;
twitterClass.Attributes = MemberAttributes.Public;
twitterNamespace.Types.Add(twitterClass);
Our generated class is now up to this:
namespace TwitterClient {
public class Tweet {
}
}
A class isn't very useful if it can't hold anything, let's add some
fields to hold the date, text, and source of the tweet.
// Create a field to hold the date.
CodeMemberField dateField = new CodeMemberField(typeof(DateTime), "_date");
dateField.Attributes = MemberAttributes.Private;
twitterClass.Members.Add(dateField);
// Create a field to hold the text.
CodeMemberField textField = new CodeMemberField(typeof(string), "_text");
dateField.Attributes = MemberAttributes.Private;
twitterClass.Members.Add(textField);
// Create a field to hold the source.
CodeMemberField sourceField = new CodeMemberField(typeof(string), "_source");
dateField.Attributes = MemberAttributes.Private;
twitterClass.Members.Add(sourceField);
We now need properties to access each of these fields. There's a lot of
syntax required to make a property, so I created a simple helper
function to make this a little cleaner.
/// <summary>
/// Creates a public property with getters and setters that wrap the
/// specified field.
/// </summary>
/// <param name="field">The field to get and set.</param>
/// <param name="name">The name of the property.</param>
/// <param name="type">The type of the property.</param>
/// <returns></returns>
static CodeMemberProperty CreateProperty(string field, string name, Type type)
{
CodeMemberProperty property = new CodeMemberProperty()
{
Name = name,
Type = new CodeTypeReference(type),
Attributes = MemberAttributes.Public
};
property.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(null, field),
new CodePropertySetValueReferenceExpression()));
property.GetStatements.Add(
new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(null, field)));
return property;
}
Since implicit properties are syntactic sugar added to C#, they're not
supported by the CodeDom. This means we need to explicitly create
getters and setters for our private fields we added earlier. The setter
is created by using a
CodeAssignStatement.
This object takes two parameters - essentially the left and right sides
of the assignment. The left side is the field we want to assign. I
passed null as the first parameter to
CodeFieldReferenceExpression
because I didn't want an object before the field name (object.fieldname)
since the field exists within this class. The right side of the
assignment is a specialexpression
to represent the value keyword.
Let's now call this function to add our properties.
// Add a property for date.
twitterClass.Members.Add(CreateProperty("_date", "Date", typeof(DateTime)));
// Add a property for text.
twitterClass.Members.Add(CreateProperty("_text", "Text", typeof(string)));
// Add a property for source.
twitterClass.Members.Add(CreateProperty("_source", "Source", typeof(string)));
If we combine all of the code and run it, our class now looks like this:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.1
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace TwitterClient {
public class Tweet {
private System.DateTime _date;
private string _text;
private string _source;
public virtual System.DateTime Date {
get {
return _date;
}
set {
_date = value;
}
}
public virtual string Text {
get {
return _text;
}
set {
_text = value;
}
}
public virtual string Source {
get {
return _source;
}
set {
_source = value;
}
}
}
}
That's it for a class that holds basic information about a tweet. You
may notice the
virtual
keyword applied to each property. I can't
figure out how to remove this, however it won't affect how the class is
used. We now need some way to store a collection of these. Let's create
a class that extends a generic List of Tweet objects.// Add a class to hold a collection of tweets.
twitterNamespace.Imports.Add(new CodeNamespaceImport("System.Collections.Generic"));
CodeTypeDeclaration twitterCollection = new CodeTypeDeclaration("Tweets");
twitterCollection.BaseTypes.Add(new CodeTypeReference("List",
new CodeTypeReference("Tweet")));
twitterNamespace.Types.Add(twitterCollection);
In order to make this file compile, we first need to include an import
to System.Collections.Generic. We then add a new type that extends List.
The List takes an argument for it's generic - in this case it's the type
for the generic - or our Tweet class. That's it for our code, we can run
this one last time and see our output.
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.1
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace TwitterClient {
using System.Collections.Generic;
public class Tweet {
private System.DateTime _date;
private string _text;
private string _source;
public virtual System.DateTime Date {
get {
return _date;
}
set {
_date = value;
}
}
public virtual string Text {
get {
return _text;
}
set {
_text = value;
}
}
public virtual string Source {
get {
return _source;
}
set {
_source = value;
}
}
}
public class Tweets : List<Tweet> {
}
}
For completeness, here's the entirety of the code required to generate
the above example:
using System.CodeDom;
using Microsoft.CSharp;
using System.IO;
using System;
namespace CodeGeneration
{
class Program
{
static void Main(string[] args)
{
CodeCompileUnit compileUnit = new CodeCompileUnit();
// Add a namespace.
CodeNamespace twitterNamespace = new CodeNamespace("TwitterClient");
compileUnit.Namespaces.Add(twitterNamespace);
// Add a Tweet class.
CodeTypeDeclaration twitterClass = new CodeTypeDeclaration("Tweet");
twitterClass.IsClass = true;
twitterClass.Attributes = MemberAttributes.Public;
twitterNamespace.Types.Add(twitterClass);
// Create a field to hold the date.
CodeMemberField dateField = new CodeMemberField(typeof(DateTime), "_date");
dateField.Attributes = MemberAttributes.Private;
twitterClass.Members.Add(dateField);
// Create a field to hold the text.
CodeMemberField textField = new CodeMemberField(typeof(string), "_text");
dateField.Attributes = MemberAttributes.Private;
twitterClass.Members.Add(textField);
// Create a field to hold the source.
CodeMemberField sourceField = new CodeMemberField(typeof(string), "_source");
dateField.Attributes = MemberAttributes.Private;
twitterClass.Members.Add(sourceField);
// Add a property for date.
twitterClass.Members.Add(CreateProperty("_date", "Date", typeof(DateTime)));
// Add a property for text.
twitterClass.Members.Add(CreateProperty("_text", "Text", typeof(string)));
// Add a property for source.
twitterClass.Members.Add(CreateProperty("_source", "Source", typeof(string)));
// Add a class to hold a collection of tweets.
twitterNamespace.Imports.Add(
new CodeNamespaceImport("System.Collections.Generic"));
CodeTypeDeclaration twitterCollection =
new CodeTypeDeclaration("Tweets");
twitterCollection.BaseTypes.Add(new CodeTypeReference("List",
new CodeTypeReference("Tweet")));
twitterNamespace.Types.Add(twitterCollection);
// Write the code to a file.
using (var fileStream = new StreamWriter(File.Create(@"C:\outputfile.cs")))
{
var provider = new CSharpCodeProvider();
provider.GenerateCodeFromCompileUnit(compileUnit, fileStream, null);
}
}
/// <summary>
/// Creates a public property with getters and setters that wrap the
/// specified field.
/// </summary>
/// <param name="field">The field to get and set.</param>
/// <param name="name">The name of the property.</param>
/// <param name="type">The type of the property.</param>
/// <returns></returns>
static CodeMemberProperty CreateProperty(string field, string name, Type type)
{
CodeMemberProperty property = new CodeMemberProperty()
{
Name = name,
Type = new CodeTypeReference(type),
Attributes = MemberAttributes.Public
};
property.SetStatements.Add(
new CodeAssignStatement(
new CodeFieldReferenceExpression(null, field),
new CodePropertySetValueReferenceExpression()));
property.GetStatements.Add(
new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(null, field)));
return property;
}
}
}
There you have it. This tutorial demonstrated how to generate classes,
fields, properties, generics, and inheritance. The sky's the limit on
how you can put code generation to use for you and your projects. If
you've got any questions or comments, please leave them below.
Source Files:
Comments
Post a Comment