CacheGen Proof Of Concept

So, I finally have CacheGen to where I can probably integrate it into this website. I did some rough concurrency testing (spawning 60 threads accessing the cache with random clearing). It's a rough test, but it does show that there isn't anything obviously wrong with it at least.

So, the code it generates is brilliantly simple as well. Some good use cases for this:

  • Keep all your cache settings in one place
  • Statically typed and named! No more remembering manual casts or magic strings
  • Make your caching logic testable! It generates code against an easily mockable interface
  • Switch out your caching layer with ease.

Now, I'm only going to elaborate on the last point. "Why would I ever want to change out my caching layer!?"

Here's why. You built Bookface 1.0 and a few dozen users are on it. People start talking though and suddenly you have a few thousand(or more). You page response times have crept up into the seconds range. Something must be done. After upgrading servers, and expanding some of the hardware, you find the bottleneck. Your web server's caches are being cleared too often. There isn't anything you can do though, the memory is maxed out as it is. So, obvious choice: Use something like memcached for distributed caching on a dedicated server or two.

What's makes using memcached or something so hard? It requires code changes! Luckily for you, you used CacheGen though. Why? All of your caching is in one place, and your interface to the caching method(CacheMechanism) is in one single simple class. It's trivial to implement a two-level cache between ASP.Net and memcached at this point and all of your code relying on your cache will just magically work without being changed.

This is what I think makes CacheGen especially awesome. It manages your caching settings, makes everything statically typed, AND lets you have an almost unreal amount of flexibility.

It's not quite ready for primetime yet. I've proved that it should work, the thing now to do is clean up the API some and add some more unit testing to see if I can catch more bugs.

Anyway, I don't expect this process to take too long. I plan to tag an alpha release for this relatively soon (within the month)

Posted: 12/8/2012 7:21:17 AM

Making Caching Awesome Again

So, I've recently began a new project. It's no where near stable, or even usable. But I think it's a good enough time to introduce it.

It's called CacheGen and it's hosted at bitbucket and BSD licensed.

Basically, it's a T4 template that generates really awesome helpers for caching. It's geared at ASP.Net, but my goal is to make it so it can be used in other ways as well. So, how does it work? You give it a list of specifications for something you want to cache. Let's say you have a string that's really expensive to generate, but doesn't belong in a database. To go on my theme of markdown being slow, let's call it MarkdownTranslation

You tell the T4 template to create a cache item for it like this:

var tmp=new CacheObject
{
  Name="MarkdownTransform",
  ValueType="string";
}
var cache=new CacheGen("Earlz.Example.MyCache");
cache.Items.Add(tmp);
Write(cache.ToString();

And then, you can use the MyCache class like this:

var text=MyCache.MarkdownTransform ?? (MyCache.MarkdownTransform=Markdown.Translate(foobar));

Much much easier. But, there is a flaw in this. It's possible that we could consume double the server resources required for this. This wouldn't be a problem in this case, but imagine a very heavy SQL query or something. This isn't quite what we want when things are really expensive. So, let's use some other fancy syntax

var text=MyCache.MarkdownTransformCache.LockGetOrLoad(()=>Markdown.Translate(foobar));

What this will do instead is raise a flag so that requests for MarkdownTransform will block and during this time, it will execute the expensive code and other threads will sleep. This way, it only gets executed once. And, when it finally gets the results back, the cache will be loaded with it and the other threads will be able to access it. So, instead of computing the expensive thing multiple times, instead we just hold out other requests for a little while so the expensive thing is done only once.

Sure, you can do this same thing with ASP.Net's cache, but how much code would it require for each time you did this? Hence why it's T4. Also, it's completely statically typed. No casting!

Now, what if you wanted to cache the markdown for multiple posts? Or just want to cache entire post objects? Well, I thought of this too:

You'd make the CacheObject like this in T4:

Name="Transforms";
KeyType="ObjectId";
ValueType="BlogPostData";

Then, you could apply the same lock-first type behavior like so:

var post=MyCache.TransformsCache.LockGetOrLoad(objectid, ()=>LoadPost());

Or, if you prefer the (probably safer) possibly execute twice behavior:

var post=MyCache.Transforms[objectid] ?? (MyCache.Transforms[objectid] = LoadPost());

These are the ideas anyway. None of it is actually implemented yet, and a lot more research must be done to ensure that this is a sane way to go about it. But, you can expect to get something similar to this.

Posted: 12/6/2012 5:17:43 AM

How to unit test T4 code generators

So, you're like me and have a greater than 500 line T4 template that is a steaming pile of... code. And of course, no syntax highlighting without addons, no intellisense, generally horrible Visual Studio support, and near impossible to unit test.

Well, my friends, I have just the thing for you! After beating my head against a wall for several days, I've found salvation!

To use my method requires some "neat" code, and it requires a few assumptions:

  • You're using T4 to generate C# code
  • You're then taking this C# code and including it in your project
  • You're ready to do some major refactoring for an awesome experience in the end (and your T4 template will be so much cleaner afterwards!)
  • You can have at least 2 tiles tied to T4 (one T4 template and one include file)

The corner stone of making T4 testable is to separate out the "logic" and the "content"; where the content is the generated C# code, and the logic is what you do to generate it.

To do this and enforce this separation cleanly, you must have two files. One of these files is the T4 "view", and another file is the logic, which is capable of being normally compiled outside of T4.

Example Untestable T4 Code

Lets start with a simple example. You have a simple T4 template which takes a file like so:

foo=bar
biz=baz

and turns it into

public class GeneratedClass
{
  public foo="bar";
  public biz="baz";
}

Here is a simple(and untestable) T4 template for it.:

<#@ template language="C#v3.5" hostspecific="true"#>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>

using System;
namespace Earlz.SampleT4
{
    public class GeneratedClass
    {
<#
    string filename="fields.txt";

    string path=Path.GetDirectoryName(Host.TemplateFile);
    var f=File.OpenText(Path.Combine(path, filename));
    string text=f.ReadToEnd();
    text=text.Replace("\r", ""); //strip extra line endings (if needed)
    var lines=text.Split('\n');
    foreach(var line in lines)
    {
        var parts=line.Split('='); //split for each element
#>
        public string <#= parts[0] #> = @"<#= parts[1].Replace("\"", "\"\"") #>";
<#
    }
#>
    }
}

There are a few rumored methods of testing this T4 file:

  • Run an integration test manually comparing the code (very very brittle)
  • I can't think of anything else worth mentioning

The Great Refactor

Now, What I suggest:

Let's refactor this and make it so we can eliminate some logic out of our "view". In this case, the view should worry about getting the code into the generated file and that's it. The logic on the other hand should work at a level of abstraction.

Here's what we're going to do:

  • Eliminate most of this code from the view
  • Create a new file for the logic
  • Use a very clever trick so that our logic file will compile outside of T4 and work when included in T4
  • Make it so that instead of just outputting text, we're building an object model that happens to be easily translatable to text

Because I came prepared, I already have the object model abstraction built. you can catch it at the bottom of the last code snippet. (It's easy to rip out and put elsewhere)

Compiling Code In and Out of T4

So, we create a new T4 logic file and name it something like "GenerateClassLogic.tt.cs". Now, you may be thinking "but you can't use the same code in T4 and a regular project!" *WRONG!

I came across a very nifty trick. Behold:

//<#+
/*That line above is very carefully constructed to be awesome and make it so this works!*/
#if NOT_IN_T4
//Apparently T4 places classes into another class, making namespaces impossible
namespace MyNamespace.Foo.Bar
{
    using System;
    using System.Linq;
    using System.Text;
    using System.Collections.Generic;
    using System.IO;
#endif
//regular ol' C# classes and code...

#if NOT_IN_T4
} //end the namespace
#endif
//#>

The key parts are the first and last lines. These begin with a comment so that the C# compiler will ignore them outside of T4, but inside of T4 these instruct the transformer to include this as a "class feature" (which ends up being a nested class)

Note here also that you must define a compile symbol. This is super easy to do. If you add it to your project, then it won't carry over to the T4 template though, making this an easy way to add in a few key things that can't be done without knowing if we're executing within T4 or not.

So, now we have a T4 logic file that will compile inside and outside of T4. Perfect for unit testing! All we need now is some logic!

The Result

Here is what I came up with:

//<#+
/*That line above is very carefully constructed to be awesome and make it so this works!*/
#if NOT_IN_T4
//Apparently T4 places classes into another class, making namespaces impossible
namespace Earlz.SampleT4.Internal
{
    using System;
    using System.Linq;
    using System.Text;
    using System.Collections.Generic;
    using System.IO;
#endif
    //regular ol' C# classes and code...

    public class GenerateClassFromText : ClassGenerator
    {
        public GenerateClassFromText(string text)
        {
            Init(text);
        }
        public GenerateClassFromText(string templatefile, string filename)
        {
            string path=Path.GetDirectoryName(templatefile);
            var f=File.OpenText(Path.Combine(path, filename));
            Init(f.ReadToEnd());
        }
        public void Init(string text)
        {
            text=text.Replace("\r", ""); //strip extra line endings (if needed)
            var lines=text.Split('\n');
            foreach(var line in lines)
            {
                var parts=line.Split('='); //split for each element
                var field=new Field
                {
                    Accessibility="public",
                    Name=parts[0],
                    Type="string",
                    InitialValue=string.Format("@\"{0}\"",parts[1].Replace("\"", "\"\""))
                };
                Fields.Add(field);
            }
        }

    }

    //shove this all into one file so we don't force implementers to hand combine this or copy over more than 2 files
    public class ClassGenerator : CodeElement
    {
        virtual public List<Property> Properties
        {
            get;
            private set;
        }
        virtual public List<Method> Methods
        {
            get;
            private set;
        }
        virtual public List<Field> Fields
        {
            get;
            private set;
        }
        virtual public string Namespace
        {
            get;set;
        }
        virtual public string OtherCode
        {
            get;set;
        }
        public virtual string BaseClass
        {
            get;set;
        }
        public ClassGenerator()
        {
            Properties=new List<Property>();
            Methods=new List<Method>();
            Fields=new List<Field>();
            Accessibility="";
        }
        public override string ToString ()
        {
            StringBuilder sb=new StringBuilder();
            sb.Append("namespace "+Namespace);
            sb.AppendLine("{");
            sb.AppendLine(PrefixDocs);
            sb.Append(GetTab(1)+Accessibility+" class "+Name);
            if(string.IsNullOrEmpty(BaseClass))
            {
                sb.AppendLine();
            }
            else
            {
                sb.AppendLine(": "+BaseClass);
            }
            sb.AppendLine(GetTab(1)+"{");
            foreach(var p in Properties)
            {
                sb.AppendLine(p.ToString());
            }
            foreach(var m in Methods)
            {
                sb.AppendLine(m.ToString());
            }
            foreach(var f in Fields)
            {
                sb.AppendLine(f.ToString());
            }
            sb.AppendLine(OtherCode);
            sb.AppendLine(GetTab(1)+"}");
            sb.AppendLine("}");
            return sb.ToString();
        }

    }
    abstract public class CodeElement
    {
        public const string Tab="    ";
        public string Name
        {
            get;
            set;
        }
        public string Accessibility
        {
            get;
            set;
        }
        string prefixdocs;
        virtual public string PrefixDocs
        {
            get
            {
                return prefixdocs;
            }
            set
            {
                prefixdocs=GetTab(2)+"///<summary>\n"+GetTab(2)+"///"+value+"\n"+GetTab(2)+"///</summary>";
            }
        }
        public override string ToString ()
        {
            throw new NotImplementedException();
        }
        public static string GetTab(int nest)
        {
            string tmp="";
            for(int i=0;i<nest;i++)
            {
                tmp+=Tab;
            }
            return tmp;
        }
        protected CodeElement()
        {
            Accessibility="";
            PrefixDocs="";
        }
    }
    public class Property : CodeElement
    {
        public string Type
        {
            get;set;
        }
        public string GetMethod
        {
            get;
            set;
        }
        public string SetMethod
        {
            get;
            set;
        }
        public override string ToString ()
        {
            string tmp=GetTab(2)+PrefixDocs+"\n";
            tmp+=GetTab(2)+CodeElement.Tab+Accessibility+" "+Type+" "+Name+"{\n";
            if(GetMethod!=null)
            {
                tmp+=GetTab(2)+GetMethod+"\n";
            }
            if(SetMethod!=null)
            {
                tmp+=GetTab(2)+SetMethod+"\n";
            }
            tmp+=GetTab(2)+"}\n";
            return tmp;
        }
        public Property()
        {
            GetMethod="get;";
            SetMethod="set;";
        }
    }
    public class Field : CodeElement
    {
        public string Type
        {
            get;
            set;
        }
        public string InitialValue
        {
            get;
            set;
        }
        public override string ToString ()
        {
            string tmp=GetTab(2)+PrefixDocs+"\n";
            tmp+=GetTab(2)+Accessibility+" " +Type+" " +Name;
            if(InitialValue!=null)
            {
                tmp+="="+InitialValue+";";
            }else{
                tmp+=";";
            }
            return tmp;
        }
    }
    public class Method : CodeElement
    {
        public string ReturnType
        {
            get;
            set;
        }
        public List<MethodParam> Params
        {
            get;set;
        }
        public string Body
        {
            get;set;
        }
        public Method()
        {
            Params=new List<MethodParam>();
            Body="";
            ReturnType="void";
        }
        public override string ToString ()
        {
            string tmp=GetTab(2)+PrefixDocs+"\n";
            tmp=GetTab(2)+Accessibility+" "+ReturnType+" "+Name+"(";
            for(int i=0;i<Params.Count;i++)
            {
                tmp+=Params[i].ToString();
                if(i==Params.Count-1)
                {
                    tmp+=")";
                }
                else
                {
                    tmp+=", ";
                }
            }
            if(Params.Count==0)
            {
                tmp+=")";
            }
            tmp+="\n"+GetTab(2)+"{\n";
            tmp+=Body;
            tmp+="\n"+GetTab(2)+"}";
            return tmp;
        }
    }
    public class MethodParam
    {
        public string Name{get;set;}
        public string Type{get;set;}
        public override string ToString ()
        {
            return Type+" "+Name;
        }
    }
#if NOT_IN_T4
} //end the namespace
#endif
//#>

Wow, so much cleaner! Plus, go to modify it in Visual Studio. What's that? Theirs actually intellisense!? Yes! There is! It will also throw compiler errors when you screw stuff up.

Also, I trimmed the T4 view down significantly as well and of course put in an include statement for our logic file:

<#@ template language="C#v3.5" hostspecific="true"#>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>

<#
    string filename="fields.txt";
    var gen=new GenerateClassFromText(Host.TemplateFile, filename);
    gen.Namespace="Earlz.SampleT4";
    gen.Name="GeneratedClass";
    gen.Accessibility="public";
#>

<#= gen.ToString() #>

<#@ include file="GenerateClassLogic.tt.cs" #>

Wow that's simple!

Now note: None of the stuff in your "view" is testable. You should always keep that in mind and keep it as simple as humanely possible.

If you'll notice, other than a bit of boilerplate XML documentation, the generated code is exactly the same. This is intentional :)

Unit Testing T4

What were we trying to do again? Ah yes. Tests. Let's add some NUnit tests for this!

And so, by simply adding a reference to our project in the NUnit test project, we instantly get access to the logic of the code generator. Here is the quick testing I did:

[TestFixture]
public class CodeGeneratorTests
{
    string TestText=
@"Foo=Bar
Biz=Baz
TestQuotes=""foo bar""";

    [Test]
    public void EnsureFieldWritten()
    {

        var gen=new GenerateClassFromText(TestText);
        Assert.IsTrue(gen.Fields.Any(x=>x.Name=="Foo"));
        Assert.IsTrue(gen.Fields.Any(x=>x.Name=="Biz"));
    }
    [Test]
    public void EnsureFieldsPublic()
    {
        var gen=new GenerateClassFromText(TestText);
        var tmp=gen.Fields.Single(x=>x.Name=="Foo");
        Assert.AreEqual(tmp.Accessibility, "public");
    }
    [Test]
    public void EnsureFieldsQuoted()
    {
        var gen=new GenerateClassFromText(TestText);
        var tmp=gen.Fields.Single(x=>x.Name=="TestQuotes");
        Assert.AreEqual(tmp.InitialValue, @"@""""""foo bar""""""");
    }
}

Conclusion and Remarks

So, in conclusion, we've learned that T4 is actually capable of taming. (when I first learned T4, I wouldn't have thought it possible either) The main thing to do is maintain a separation of "content" and logic. Now of course, there are some gotchas to watch for:

  • This only works when your T4 generator's target is to generate C# code
  • When you add a reference to something, you must add it to both the logic file and the view
  • It's still very difficult to reference external assemblies(particularly project assemblies). This doesn't solve that problem at all
  • You must define a compiler symbol for your project if you wish to run unit tests against it.
  • When other people use your T4 template outside of your project(and thus don't need to test it), they must ensure that the logic file is not compiled outside of the T4
  • Unit testing that a piece of code is generated is very difficult and brittle(hence the need for abstractions where possible to make this easier)
  • My abstraction mechanisms for building objects aren't really that good. They're good enough for me, but please someone improve them! (if you do it I'll link to you from here)

Happy code generation!

Posted: 11/21/2012 3:46:10 AM