InterpolatedStringHandler

I'm experimenting with the InterpolatedStringHandler. The idea, in simple terms is to do away with the interpolatedStringBuilder that we see in the examples for logging and replace it with this. One should then be able to create a generic logging aspect that utilises MEL ILogger and in the calling app just log plain text , or substitute a structured logger like Serilog. It transpires that this requires Ststem.Runtime.CompilerServices.Unsafe. technically this is marked as being compatible with .NetStandard 2.0 which logically one ought to create the class library containing the aspect with for maximum compatibility. However it would appear that this is somewhat deceptive. If I base the class library on .net 6.0 this issue goes away. This begs the following esoteric question. Given that Metalama is is in many ways a 'Postsharp for the .net Core world' should we be assiduously trying to enforce as much backward compatibility as possible?
11 Replies
Whit
Whit2y ago
I had another issue in Slack that was similar in that I wanted to be able to capture the values of arguments but save them into a dictionary instead of a strictly interpolated string. From that idea came the "ExpressionDictionaryBuilder" that implements against a Dictionary<string, IExpression> internally, implementing INotNullExpressionBuilder and IExpressionBuilder. The magic is in the ToExpression method in that I use the ExpressionBuilder to essentially just write out a new dictionary that uses string.Format to convert the value of anything passed in to a string:
public void AddText(string key, string text)
{
if (string.IsNullOrEmpty(text))
return;
_items.Add(key, ExpressionFactory.Literal(text));
}

public void AddExpression(string key, IExpression expression)
{
_items.Add(key, expression);
}

public IExpression ToExpression()
{
var builder = new ExpressionBuilder();
builder.AppendVerbatim("new ");
builder.AppendTypeName(typeof(Dictionary<string,string>));
builder.AppendVerbatim("{");

var i = meta.CompileTime(0);

foreach (var kvp in Items)
{
if (i > 0)
{
builder.AppendVerbatim(",");
}

builder.AppendVerbatim("{");
builder.AppendExpression(kvp.Key);
builder.AppendVerbatim(",");
builder.AppendVerbatim("string.Format(\"{0}\",");
builder.AppendExpression(kvp.Value);
builder.AppendVerbatim(")");
builder.AppendVerbatim("}");

i++;
}

builder.AppendVerbatim("}");

return builder.ToExpression();
}
public void AddText(string key, string text)
{
if (string.IsNullOrEmpty(text))
return;
_items.Add(key, ExpressionFactory.Literal(text));
}

public void AddExpression(string key, IExpression expression)
{
_items.Add(key, expression);
}

public IExpression ToExpression()
{
var builder = new ExpressionBuilder();
builder.AppendVerbatim("new ");
builder.AppendTypeName(typeof(Dictionary<string,string>));
builder.AppendVerbatim("{");

var i = meta.CompileTime(0);

foreach (var kvp in Items)
{
if (i > 0)
{
builder.AppendVerbatim(",");
}

builder.AppendVerbatim("{");
builder.AppendExpression(kvp.Key);
builder.AppendVerbatim(",");
builder.AppendVerbatim("string.Format(\"{0}\",");
builder.AppendExpression(kvp.Value);
builder.AppendVerbatim(")");
builder.AppendVerbatim("}");

i++;
}

builder.AppendVerbatim("}");

return builder.ToExpression();
}
From there, usage is similar to the logging example:
var builder = new ExpressionDictionaryBuilder();

foreach(var p in meta.Target.Parameters)
{
builder.AddExpression(p.Name, p);
}
return builder;
var builder = new ExpressionDictionaryBuilder();

foreach(var p in meta.Target.Parameters)
{
builder.AddExpression(p.Name, p);
}
return builder;
And then in my case, since I'm using TelemetryClient:
var paramValues = BuildParameterDiictionary(true); //Above snippet
_telemetryClient.TrackTrace((string)successMessage.ToValue(), SeverityLevel.Information, paramValues.ToExpression.Value);
var paramValues = BuildParameterDiictionary(true); //Above snippet
_telemetryClient.TrackTrace((string)successMessage.ToValue(), SeverityLevel.Information, paramValues.ToExpression.Value);
I've got to imagine you could do something awfully similar and essentially use the ExpressionBuilder to piece together an ILogger expression with the structured string and each of the values passed in (as evaluated at runtime) to the params.
Gael Fraiteur
Gael Fraiteur2y ago
Why and how is System.Runtime.CompilerServices.Unsafe needed? I don't get your backward compatibility question.
domsinclair
domsinclair2y ago
There's a new repo on .y github within which there is an initial proof of concept app, look at the interpolated string handler which should answer your question, I hope.
Gael Fraiteur
Gael Fraiteur2y ago
I still don't see a use of Unsafe however I see a use of InterpolatedStringHandlerAttribute which does not exist in .NET Standard 2.0. It's not the only class that has this problem and the solution is always the same: You simply need to define it in your code:
namespace System.Runtime.CompilerServices
{
public class InterpolatedStringHandlerAttribute : System.Attribute {}
}
namespace System.Runtime.CompilerServices
{
public class InterpolatedStringHandlerAttribute : System.Attribute {}
}
domsinclair
domsinclair2y ago
I've completed most of this now, One issue to correct which I suspect is a fault on my part. However you can switch between MEL Ilogger and Serilog. Repo is here https://github.com/domsinclair/VtlSoftware.LoggingWithStringHandler
GitHub
GitHub - domsinclair/VtlSoftware.LoggingWithStringHandler
Contribute to domsinclair/VtlSoftware.LoggingWithStringHandler development by creating an account on GitHub.
domsinclair
domsinclair2y ago
Basics now working well.
Gael Fraiteur
Gael Fraiteur2y ago
I'm advertising your article in the May newsletter but I did not see you mentioned string handlers. Should I update the blog post to mention the project?
domsinclair
domsinclair2y ago
That's an interesting point for a couple of reasons. It (the interpolated string handler ) seems to work quite well, certainly the github sample that I reference in the last section seems to indicate that it does and switching frameworks in the actual console app is pretty straightforward. That console app itself could probably be finesses and I suspect the appsettings.json could be configured to better support more than one logging framework. That's still to be worked on and then documented. The second point arises out of your own roadmap. Assuming that I can get this working acceptably with a number of frameworks then I hope it would make a reasonable substitute for a number of the old postsharp backend providers that were in the patterns library. My plan, as you know is to open source the thing anyway, but that will be contingent on the official release of 2023.1 as I haven't found an elegant way to remove the need for a foreach loop. I also need to properly document that so that end users can see exactly how to use it, which also begs the question do I do that with the tools I have or give Writerside a spin as its markdown looks quite promising. Only issue with that is a need to get Rider. I don't think it will hurt to mention it simply because one is still using interpolated strings, it's just that they way they're being handled is different
Gael Fraiteur
Gael Fraiteur2y ago
OK I will add some language like the lawyers say
domsinclair
domsinclair2y ago
Which can off course be highly redacted at a later stage!
Gael Fraiteur
Gael Fraiteur2y ago
I highly recommend checking out Dom Sinclair's delightful article, The Art of Logging. And if that was not enough, Dom is currently working on a semantic logging aspect for Serilog that utilizes the new interpolated string handler feature of C# 10.
GitHub
GitHub - domsinclair/VtlSoftware.LoggingWithStringHandler
Contribute to domsinclair/VtlSoftware.LoggingWithStringHandler development by creating an account on GitHub.
Explore string interpolation handlers
This advanced tutorial shows how you can write a custom string interpolation handler that hooks into the runtime processing of an interpolated string.
Want results from more Discord servers?
Add your server