Appearance
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 codeAsyncFrame
- a frame that has at least oneawait
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:
cs
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 aTask
or is marked with theasync
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:
cs
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:
cs
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:
cs
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:
cs
public class GetInstanceFrame : SyncFrame, IResolverFrame
{
private static readonly MethodInfo _resolveMethod =
ReflectionHelper.GetMethod<Instance>(x => x.Resolve(null));
private readonly string _name;
private Variable _scope;
public GetInstanceFrame(Instance instance)
{
Variable = new ServiceVariable(instance, this, ServiceDeclaration.ServiceType);
_name = instance.Name;
}
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 null)
{
throw new InvalidCastException(
$"{typeof(GetInstanceFrame).GetFullName()}.{nameof(Next)} must not be null.");
}
}
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;
}
}
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.