Building Custom Frames

INFO

If you're going to get into LamarCodeGeneration's 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);
    }
}

snippet source | anchor

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);
        }
    }
}

snippet source | anchor

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);
}

snippet source | anchor

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);
}

snippet source | anchor

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, IResolverFrame
{
    private static readonly MethodInfo _resolveMethod =
        ReflectionHelper.GetMethod<Instance>(x => x.Resolve(null));
    
    
    
    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; }
    
    public void WriteExpressions(LambdaDefinition definition)
    {
        var scope = definition.Scope();
        var expr = definition.ExpressionFor(Variable);

        var instance = Variable.Instance;

        var @call = Expression.Call(Expression.Constant(instance), _resolveMethod, scope);
        var assign = Expression.Assign(expr, Expression.Convert(@call, Variable.VariableType));
        definition.Body.Add(assign);

        if (Next is IResolverFrame next)
        {
            next.WriteExpressions(definition);
        }
        else
        {
            throw new InvalidCastException($"{Next.GetType().GetFullName()} does not implement {nameof(IResolverFrame)}");
        }
    }
}

snippet source | anchor

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.