Skip to content

Instantly share code, notes, and snippets.

@bent-rasmussen
Last active January 4, 2022 01:08
Show Gist options
  • Save bent-rasmussen/3cf86f8c0bc0b4fad1afead8fd902ecc to your computer and use it in GitHub Desktop.
Save bent-rasmussen/3cf86f8c0bc0b4fad1afead8fd902ecc to your computer and use it in GitHub Desktop.
EventSource fluent source code generator - eliminates boilerplate and ensures consistency
<Query Kind="Program">
<NuGetReference>Observito.Trace.EventSourceFormatter</NuGetReference>
<Namespace>Observito.Trace.EventSourceFormatter</Namespace>
<Namespace>System.Collections.Immutable</Namespace>
<Namespace>System.Diagnostics.Tracing</Namespace>
</Query>
#nullable enable
void Main()
{
// TODO translate from event source to code that generates event source (dynamically compile code and analyze assembly)
// TODO handle attributes from Observito
var builder =
new TypedEventSourceBuilder("Foo-Bar-Baz", "BarBaz")
.AddTask("ExternalEvent", task =>
{
task.Receive(ev => ev.Reflect(new { json = "" }));
})
.AddTask("SendEmail", task =>
{
task.Start(ev => ev.Reflect(new { to = "" }));
task.Stop();
task.Error(ev => ev.Reflect(new { messge = "", stackTrace = "" }));
});
builder.Freeze();
builder.ToSourceCode().Dump();
}
public class TypedEventSourceBuilder
{
private ImmutableList<TypedTaskBuilder>.Builder _tasks;
private readonly string _eventSourceName;
private readonly string _eventSourceClassNamePrefix;
public TypedEventSourceBuilder(string eventSourceName, string eventSourceClassNamePrefix)
{
if (eventSourceName.Length == 0) throw new ArgumentException(nameof(eventSourceName));
if (eventSourceClassNamePrefix.Length == 0) throw new ArgumentException(nameof(eventSourceClassNamePrefix));
_eventSourceName = eventSourceName;
_eventSourceClassNamePrefix = eventSourceClassNamePrefix;
_tasks = ImmutableList.CreateBuilder<TypedTaskBuilder>();
}
public bool IsFrozen { get; private set; }
public void Validate()
{
throw new NotImplementedException();
}
public void Freeze()
{
if (!IsFrozen)
{
var taskIdCounter = 1;
var eventIdCounter = 1;
//var eventIdBuilder = ImmutableDictionary.CreateBuilder<(int, EventOpcode), int>();
foreach (var task in _tasks)
{
task.Id = taskIdCounter;
foreach (var ev in task.Events)
{
ev.Id = eventIdCounter;
if (ev.Opcode == EventOpcode.Info)
{
if (ev.Level == EventLevel.Error)
ev.Name = $"{task.Name}Error";
else if (ev.Level == EventLevel.Critical)
ev.Name = $"{task.Name}Error";
else
ev.Name = task.Name;
}
else
ev.Name = $"{task.Name}{ev.Opcode}"; // TODO override names
eventIdCounter++;
}
taskIdCounter++;
}
foreach (var task in _tasks)
{
task.Freeze();
}
IsFrozen = true;
}
}
private void ThrowIfFrozen()
{
if (this.IsFrozen)
throw new InvalidOperationException("Cannot mutate once frozen");
}
public IImmutableList<TypedTaskBuilder> Tasks => _tasks.ToImmutable();
public string EventSourceName => _eventSourceName;
public string EventSourceClassNamePrefix => _eventSourceClassNamePrefix;
public TypedEventSourceBuilder AddTask(string name, Action<TypedTaskBuilder> configureTask)
{
ThrowIfFrozen();
var builder = new TypedTaskBuilder(name);
configureTask(builder);
_tasks.Add(builder);
return this;
}
// Source generation
public string ToSourceCode()
{
var builder = new IndentedStringBuilder();
ToSourceCode(builder);
return builder.ToString();
}
public void ToSourceCode(IndentedStringBuilder builder)
{
if (!IsFrozen) throw new InvalidOperationException("Freeze before generating source code");
builder.AppendLine($"[EventSource(Name = \"{_eventSourceName}\")]");
builder.AppendLine($"public sealed class {_eventSourceClassNamePrefix}EventSource : EventSource");
builder.AppendLine($"{{");
using (builder.Scope())
{
builder.AppendLine($"public static readonly {_eventSourceClassNamePrefix}EventSource Log = new();");
if (_tasks.Any())
{
builder.AppendLine();
builder.AppendLine($"public sealed class Tasks");
builder.AppendLine($"{{");
using (builder.Scope())
{
foreach (var task in _tasks)
{
builder.AppendLine($"public const EventTask {task.Name} = (EventTask){task.Id};");
}
}
builder.AppendLine($"}}");
bool firstTask;
if (_tasks.Any(t => t.Events.Any()))
{
builder.AppendLine();
builder.AppendLine($"public sealed class Events");
builder.AppendLine($"{{");
using (builder.Scope())
{
firstTask = true;
foreach (var task in _tasks)
{
if (!firstTask)
builder.AppendLine();
foreach (var ev in task.Events)
{
builder.AppendLine($"public const int {ev.Name} = {ev.Id};");
}
firstTask = false;
}
}
builder.AppendLine($"}}");
builder.AppendLine();
firstTask = true;
foreach (var task in _tasks)
{
if (!firstTask)
builder.AppendLine();
task.ToSourceCode(builder);
firstTask = false;
}
}
}
}
builder.AppendLine($"}}");
}
}
public class TypedTaskBuilder
{
private ImmutableList<TypedEventBuilder>.Builder _events;
public TypedTaskBuilder(string name)
{
Name = name;
_events = ImmutableList.CreateBuilder<TypedEventBuilder>();
}
public IImmutableList<TypedEventBuilder> Events => _events.ToImmutable();
public int? Id { get; set; }
public string Name { get; private set; }
public bool IsFrozen { get; private set; }
public void Freeze()
{
if (!IsFrozen)
{
foreach (var ev in _events)
{
ev.Freeze();
}
IsFrozen = true;
}
}
private void ThrowIfFrozen()
{
if (this.IsFrozen)
throw new InvalidOperationException("Cannot mutate once frozen");
}
public TypedEventBuilder AddEvent(Action<TypedEventBuilder>? configure = null)
{
ThrowIfFrozen();
var builder = new TypedEventBuilder();
configure?.Invoke(builder);
_events.Add(builder);
return builder;
}
// Succintness
public TypedEventBuilder Start(Action<TypedEventBuilder>? configure = null) =>
this.AddEvent(ev =>
{
ev.Opcode = EventOpcode.Start;
configure?.Invoke(ev);
});
public TypedEventBuilder Stop(Action<TypedEventBuilder>? configure = null) =>
this.AddEvent(ev =>
{
ev.Opcode = EventOpcode.Stop;
configure?.Invoke(ev);
});
public TypedEventBuilder Error(Action<TypedEventBuilder>? configure = null) =>
this.AddEvent(ev =>
{
ev.Opcode = EventOpcode.Info;
ev.Level = EventLevel.Error;
configure?.Invoke(ev);
});
public TypedEventBuilder Receive(Action<TypedEventBuilder>? configure = null) =>
this.AddEvent(ev =>
{
ev.Opcode = EventOpcode.Receive;
configure?.Invoke(ev);
});
public TypedEventBuilder DataCollectionStart(Action<TypedEventBuilder>? configure = null) =>
this.AddEvent(ev =>
{
ev.Opcode = EventOpcode.DataCollectionStart;
configure?.Invoke(ev);
});
public TypedEventBuilder DataCollectionStop(Action<TypedEventBuilder>? configure = null) =>
this.AddEvent(ev =>
{
ev.Opcode = EventOpcode.DataCollectionStop;
configure?.Invoke(ev);
});
// Source generation
public void ToSourceCode(IndentedStringBuilder builder)
{
if (!IsFrozen) throw new InvalidOperationException("Freeze before generating source code");
builder.AppendLine($"// {Name}");
builder.AppendLine();
var isFirst = true;
foreach (var ev in Events)
{
if (!isFirst)
builder.AppendLine();
ev.ToSourceCode(builder, this);
isFirst = false;
}
}
}
public class TypedEventBuilder
{
private int? _id;
private string? _name;
private Type? _dataType;
private string? _message;
private EventOpcode _opcode;
private EventLevel _level;
private EventChannel _channel;
public TypedEventBuilder()
{
_level = EventLevel.Informational;
}
private void ThrowIfFrozen()
{
if (this.IsFrozen)
throw new InvalidOperationException("Cannot mutate once frozen");
}
private void Update<T>(ref T reference, T value)
{
ThrowIfFrozen();
reference = value;
}
public bool IsFrozen { get; private set; }
public void Freeze()
{
if (!IsFrozen)
{
IsFrozen = true;
}
}
public int? Id { get => _id; set => Update(ref _id, value); }
/// <summary>Name is auto-generated but can be manually set; ensure no collisions and conventional name.</summary>
public string? Name { get => _name; set => Update(ref _name, value); }
public Type? DataType { get => _dataType; set => Update(ref _dataType, value); }
public string? Message { get => _message; set => Update(ref _message, value); }
public EventOpcode Opcode { get => _opcode; set => Update(ref _opcode, value); }
public EventLevel Level { get => _level; set => Update(ref _level, value); }
public EventChannel Channel { get => _channel; set => Update(ref _channel, value); }
public TypedEventBuilder Reflect<T>()
{
ThrowIfFrozen();
DataType = typeof(T);
return this;
}
public TypedEventBuilder Reflect<T>(T _)
{
ThrowIfFrozen();
return Reflect<T>();
}
// Source generation
public void ToSourceCode(IndentedStringBuilder builder, TypedTaskBuilder task)
{
if (!IsFrozen) throw new InvalidOperationException("Freeze before generating source code");
// Attribute
builder.Append($"[Event(");
builder.Append($"Events.{Name}");
builder.Append($", ");
builder.Append($"Level = EventLevel.{Level}");
builder.Append($", ");
builder.Append($"Task = Tasks.{task.Name}");
builder.Append($", ");
builder.Append($"Opcode = EventOpcode.{Opcode}");
if (Channel != EventChannel.None)
{
builder.Append($", ");
builder.Append($"Channel = Channel.{Channel}");
}
if (Message is not null)
{
builder.Append($", ");
builder.Append($"Message = \"{Message}\"");
}
builder.Append($")]");
builder.AppendLine();
// Event
builder.Append($"public void {Name}(");
var parameterNames = new List<string>();
if (DataType is not null)
{
var isFirst = true;
//var propInfos = DataType.GetProperties().Select(p => new { Name = p.Name, Type = p.PropertyType.Name });
foreach (var prop in DataType.GetProperties())
{
var paramName = prop.Name;
var typeName = prop.PropertyType.Namespace == "System" ? prop.PropertyType.Name : prop.PropertyType.FullName;
parameterNames.Add(paramName);
if (!isFirst) builder.Append($", ");
builder.Append($"{typeName} {paramName}");
isFirst = false;
}
}
builder.Append($") => WriteEvent(Events.{Name}");
if (DataType is not null)
{
foreach (var paramName in parameterNames)
builder.Append($", {paramName}");
}
builder.Append($");");
//public void StripeEventStart(string json) => WriteEvent(Events.StripeEventStart, json);
builder.AppendLine();
}
}
public sealed class IndentedStringBuilder
{
public IndentedStringBuilder(uint spacing = 4)
{
_builder = new StringBuilder();
_spacing = spacing;
_spacer = ' ';
}
private readonly StringBuilder _builder;
private uint _depth;
private uint _spacing;
private char _spacer;
private bool IsLineStart => _builder.Length == 0 ? true : _builder[^1] == '\n'; // || _builder[^1] == '\r';
private void Pad()
{
var n = _depth * _spacing;
for (var i = 0; i < n; i++)
_builder.Append(_spacer);
}
public void Append(object? value)
{
if (IsLineStart)
{
Pad();
}
_builder.Append(value);
}
public void AppendLine()
{
_builder.AppendLine();
}
public void AppendLine(string? value)
{
var str = value?.ToString();
if (str is not null)
{
if (str.Contains('\n', StringComparison.OrdinalIgnoreCase) || str.Contains('\n', StringComparison.OrdinalIgnoreCase))
{
// TODO span-based zero-allocation
var reader = new StringReader(str);
while (true)
{
var line = reader.ReadLine();
if (line is null) break;
Append(line);
}
}
else
{
Append(value);
_builder.AppendLine();
}
}
}
public void Indent() => Interlocked.Increment(ref _depth);
public void Unindent() => Interlocked.Decrement(ref _depth);
public IDisposable Scope() => new IndentationScope(this);
public override string ToString() => _builder.ToString();
private struct IndentationScope : IDisposable
{
private readonly IndentedStringBuilder _builder;
public IndentationScope(IndentedStringBuilder builder)
{
_builder = builder;
_builder.Indent();
}
public void Dispose()
{
_builder.Unindent();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment