Fork me on GitHub

Building Custom Frames


Note! If you're going to get into Lamar's code generation model, you probably want to be familiar and comfortable with both string interpolation in C# and the recent nameof operator.

To build a custom frame, you first need to create a new class that subclasses Frame, with these other more specific subclasses to start from as well:

  • SyncFrame - a frame that generates purely synchronous code
  • AsyncFrame - a frame that has at least one await call in the code generated

The one thing you absolutely have to do when you create a new Frame class is to override the GenerateCode() method. Take this example from Lamar itself for a frame that just injects a comment into the generated code:


public class CommentFrame : SyncFrame
{
    private readonly string _commentText;

    public CommentFrame(string commentText)
    {
        _commentText = commentText;
    }

    public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
    {
        writer.WriteComment(_commentText);
        
        // It's on you to call through to a possible next
        // frame to let it generate its code
        Next?.GenerateCode(method, writer);
    }
}

A couple things to note about the GenerateCode() method:

  • The GeneratedMethod will tell you information about the new method being generated like the return type and whether or not the method returns a Task or is marked with the async keyword.
  • You use the ISourceWriter argument to write new code into the generated method
  • It's your responsibility to call the Next?.GenerateCode() method to give the next frame a chance to write its code. Don't forget to do this step.

Inside a custom frame, you can also nest the code from the frames following yours in a method. See this frame from Lamar itself that calls a "no arg" constructor on a concrete class and returns a variable. In the case of a class that implements IDisposable, it should write a C# using block that surrounds the inner code:


public class NoArgCreationFrame : SyncFrame
{
    public NoArgCreationFrame(Type concreteType) 
    {
        // By creating the variable this way, we're
        // marking the variable as having been created
        // by this frame
        Output = new Variable(concreteType, this);
    }

    public Variable Output { get; }

    // You have to override this method
    public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
    {
        var creation = $"var {Output.Usage} = new {Output.VariableType.FullNameInCode()}()";

        if (Output.VariableType.CanBeCastTo<IDisposable>())
        {
            // there is an ISourceWriter shortcut for this, but this makes
            // a better code demo;)
            writer.Write($"BLOCK:using ({creation})");
            Next?.GenerateCode(method, writer);
            writer.FinishBlock();
        }
        else
        {
            writer.WriteLine(creation + ";");
            Next?.GenerateCode(method, writer);
        }
    }
}

Creating a Variable within a Frame

If the code generated by a Frame creates a new Variable in the generated code, it should set itself as the creator of that variable. You can do that by either passing a frame into a variable as its creator like this line from the NoArgCreationFrame shown above:


public NoArgCreationFrame(Type concreteType) 
{
    // By creating the variable this way, we're
    // marking the variable as having been created
    // by this frame
    Output = new Variable(concreteType, this);
}

Otherwise, you could also have written that code like this:


public NoArgCreationFrame(Type concreteType) 
{
    // By creating the variable this way, we're
    // marking the variable as having been created
    // by this frame
    Output = Create(concreteType);
}

Finding Dependent Variables

The other main thing you need to know is how to locate Variable objects your Frame needs to use. You accomplish that by overriding the FindVariables() method. Take this example below that is used within Lamar to generate code that resolves a service by calling a service locator method on a Lamar Scope (a nested container most likely) object:


public class GetInstanceFrame : SyncFrame
{
    private Variable _scope;
    private readonly string _name;

    public GetInstanceFrame(Instance instance)
    {
        Variable = new ServiceVariable(instance, this, ServiceDeclaration.ServiceType);
        
        _name = instance.Name;
    }

    public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
    {
        writer.Write($"var {Variable.Usage} = {_scope.Usage}.{nameof(Scope.GetInstance)}<{Variable.VariableType.FullNameInCode()}>(\"{_name}\");");
        Next?.GenerateCode(method, writer);
    }

    public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
    {
        _scope = chain.FindVariable(typeof(Scope));
        yield return _scope;
    }
    
    public ServiceVariable Variable { get; }
}

When you write a FindVariables() method, be sure to keep a reference to any variable you need for later, and return that variable as part of the enumeration from this method. Lamar uses the dependency relationship between frames, the variables they depend on, and the creators of those variables to correctly order and fill in any missing frames prior to generating code through the GenerateCode() method.