Last active
January 4, 2022 01:08
-
-
Save bent-rasmussen/3cf86f8c0bc0b4fad1afead8fd902ecc to your computer and use it in GitHub Desktop.
EventSource fluent source code generator - eliminates boilerplate and ensures consistency
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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