-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Proposal] Capture names (or full code) of interpolated values if string interpolation is passed in as a FormattableString #142
Comments
What would you expect to be the captured names here?
|
@paulomorgado As per above, there are two options:
I am pretty sure there are reasons why second one isn't a good idea -- waiting for more feedback. |
@ashmind, there can't be two. which one do you propose and why? |
Not when implemented, but within the discussion there can definitely be two options. I believe we'll probably end up with 1 if this is actually implemented, but 2 has some interesting scenarios of its own. So I'm waiting for more feedback to see what other people think. |
It would be better to see string interpolation handling & packaging driven by user-code rather than hard-wiring into the compiler and expanding on Suppose there is a class FormattableStringBuilder fsb = $"Processed {position} in {elapsedMs:000} ms."); it could generate the following code to hand off the various bits of the interpolated string to the builder type/implementation: var fsb = new FormattableStringBuilder(2);
fsb.AddText("Processed ");
fsb.AddArgument(position, null, "position");
fsb.AddText(" in ");
fsb.AddArgument(elapsedMs, "000", "elapsedMs");
fsb.AddText(" ms."); The
Here is an example of what class FormattableStringBuilder
{
public FormattableStringBuilder(int capacity) { /* ... */ }
public void AddText(string text) { /* ... */ }
public void AddArgument(object value) { /* ... */ }
public void AddArgument(object value, string format) { /* ... */ }
public void AddArgument(object value, string format, string source) { /* ... */ }
} The details can vary based on further discussion and refinement but the idea I want float here with this approach is that it enables the implementation of the builder to decide everything from formatting and allocation to how the names would be captured in the |
@atifaziz, the names are never captured. Only the value of the expressions (that may or may not "have a name") is captured. And you can do a lot with what's provided out of the box:
When called by this code:
The compiler will generate this caller code:
So, what are you missing here? What's the use case? |
@paulomorgado I'm aware that names are not captured and I'm not missing anything. In fact, I'm not sure how your comment is related to mine at all. I was proposing an alternative to the potential solution by @ashmind:
|
👍 Even having just the content of the holes as strings would be a nice improvement! |
There's already some precedent for this kind of functionality in Linq. Check out new [] { 1, 2, 3 }.Where(n => n > 2)
dbContext.Cats.Where(cat => cat.Name == "Tiger")
Analogously, in this instance: writer.Write($"Hello, {name}")
log.Write($"Hello, {name}") ...the first
Unfortunately, the runtime allocations involved might make it unappealing to use in the case of structured logging, which was the basis of @ashmind's original post. More investigation of the scenarios and costs would have to happen before a proposal could be built around this, but I thought I'd put it out there just in case it helps someone else to move the thread forwards. |
Why don't you just convert the interpolated string into the contents of an expression lambda? |
@gafter Was your question for @nblumhardt or me? If it was to me, then my initial proposal has the following benefits over expressions:
|
We do not expect to do anything like this. |
Why was this issue closed? |
Because "We do not expect to do anything like this." |
Repeating that sentence does not make it any more coherent. |
To avoid people spending time and energy getting their hopes up for something that is not going to happen. |
Granted closing the issue achieves that. At the same time leaving things without any reason or explanation of WHY the feature is declined works to increase frustration. |
@urig There are billions of things we don't do for every one thing we do. We don't need a billion reasons. We need reasons for the things we choose to do. But if I were to look for a reason: I don't think it is reasonable for every conversion from interpolated string to FormattableString to pay the cost to store data for those occasional users who might want it. |
@gafter The reason is important. The way your original decision was worded strongly discouraged me from providing any Roslyn feedback in the future. I don't mind the rejection, but why spend my free time doing well-structured proposals to get a single sentence? Which gives me no idea on what I did wrong -- or how to avoid wasting your (and my) time again. I suppose it depends on your situation. If you are overloaded with ideas, then I understand why it doesn't matter if you lose a few people along the way. However I'll still give my two cents on what I would like to see in a proper Roslyn decision:
|
|
Fortunately and unfortunately we get more proposals than we can discuss in any depth. Folks on the LDM spend time looking at proposals here on GitHub, and bring up ones that they feel should warrant a discussion. This may be because they agree with the proposal, or feel that it raises a scenario that is worth trying to address, or similar. We call this "championing" a feature. This is our primary filtering mechanism, and it helps us focus our limited time on proposals that have a chance. I realize how annoying it is to spend time on a proposal and then have it ignored or summarily dismissed. I wish there was a realistic process by which we could formulate a "why not" for every well-worked-out proposal that we do not choose to adopt into the language. Neal closing this issue is the closest we can get - he realizes the issue won't make it to a discussion, and closes it as a signal to the participants on the thread to help prioritize their efforts. I'm sorry if this comes off as non-appreciative: it is not! We deeply value the discussions and proposals here, and they have a big influence on where we take the language. This goes for feature proposals, and equally for the many posts pointing out flaws and inconsistencies in proposals - ours and others'. I want to thank everyone who spends time and creative energy here, enriching the discussion and helping drive the languages forward. If you feel a proposal really deserves our attention and isn't getting it, I recommend submitting it on UserVoice as well: https://visualstudio.uservoice.com/forums/121579-visual-studio-2015/category/30931-languages-c Whereas the feedback here on GitHub is rich on technical commentary and insights, UserVoice has the crucial benefit of allowing voting. It also tends to reach a more representative set of developers. Top UserVoice suggestions are sure to grab our attention, whether or not we are able to do something about them. In this way, the two forums supplement each other well. Again, thanks for all the great ideas! I love the intensity, quality and volume of discussion, and it breaks my heart that we (the language designers) cannot engage deeply in all of it. |
@gafter Thank you for the detailed answer. It really helped me to understand the situation. I didn't know about the importance of a champion, or that making an implementation is encouraged even before the idea is explicitly marked as Up For Grabs. That's exactly why I wanted details -- things that you take for granted are not always obvious to people outside the team. For example based on your answer it seems that the proposals are closed if they aren't likely for the nearest future (C# 7, 8?), while I interpreted closed as "not suitable for C# at all". |
@MadsTorgersen Thank you for taking time to clarify this -- I really appreciate your sentiment and the quality and openness of C# design process. I will definitely consider User Voice for future proposals I find important. |
@ashmind I generally close things that I perceive as never being able to make the bar for a future release (not just 7 and 8). For this particular issue, if it is really important to you I recommend prototyping it with a different target type than |
@gafter Isn't that what I proposed? Or did you mean something else altogether? |
…ble with current compiler (dotnet/roslyn#142).
…ble with current compiler (dotnet/roslyn#142).
…ble with current compiler (dotnet/roslyn#142).
@ashmind Have you created a suggestion on UserVoice? If so, would you mind sharing the link? |
Sorry, I don't remember anymore -- I think I didn't, intending to consider the feedback and prototype "different target type than FormattableString", but haven't got to it yet. |
It could be good If we make a proposal with another type in mind. I've just was working on logging in my project, and found it to be useful. |
There's already a proposal for improved string interpolation that will most likely make it into .NET 6 and C# 10 here. It looks remarkably similar to what @atifaziz proposed 6 years ago 😅 I haven't read through the details, so i can't say whether it would solve this use case. I think it would be a missed opportunity if it isn't 🤔🤷♂️ // @333fred |
@khellang it looks to me like it misses any support for structured logging. Maybe using logging for the example scenario was just for convenience? The reduction in allocations for non-logging scenarios looks great. Can't see it being useful for modern logging libraries though, unfortunately. |
I can't say for sure, but judging by the example(s) in the spec and during API review, it seems like logging is a primary candidate for this API. That's why I'm saying it would be a missed opportunity to not improve on the current |
We have no plans on changing |
Maybe it was worded badly, but I'm not saying you should do anything to the design of |
Logging is certainly an important scenario that we are trying to address. The proposal specifically calls out that structured logging is possible via format specifiers: For example, |
Hello, @333fred Great to hear about that proposal. I think using Perhaps expression could be passed in as string as-is? Say adding another (optional) parameter to AppendFormatted. And log library author would decide how to encode that parameter, for example, strip out all spaces/operators. Perhaps this should be copied to dotnet/runtime#50635? |
Hi @333fred! Thanks for your reply. As @Mart-Bogdan suggests, format specifiers already have a role, e.g. Structured logging libraries today also already avoid allocations when the log level is not enabled - To fully address the structured logging case, it might be necessary to extend the syntax of interpolated format strings in C#. How about this kind of direction? $"The time is {DateTime.Now#Time}" There aren't currently any ambiguities I'm aware of, $"The time is {DateTime.Now#Time:HH:mm}" The name ( What do you think? |
Yes and no. You can potentially avoid allocations from creating the string, but you do execute whatever expression is provided to the method ( As to |
BTW, if you want to chat in a more real-time manner, I don't have a twitter, but I am 333fred on the C# community discord (https://discord.gg/csharp). Feel free to hit me up there, in the roslyn channel. |
Thanks for the reply! The lazy argument evaluation is definitely a rather pleasant addition 👍
In logging scenarios, (interpolated) format strings have to be accepted by an abstraction, so consumers will need to consider the lowest-common-denominator implementation or risk inconsistent behavior, and this would rule out a format-specifier-based approach today, at least. Adding support at the language level seems like it would side-step this and provide more consistent semantics. Another point for inclusion in the language - doing so would provide a significant incentive for libraries like MEL, Serilog, NLog and so on, to do the work to embrace it and build support in. C# would really be moving the state-of-the-art for structured logging forwards, rather than just adding a new implementation option with a new set of pros/cons for users to understand. (Happy with async comms, here - just starting out on a new work day with much to do! Really interested to see where this all lands, though, and hope this is all useful feedback :-)) |
It absolutely is, thanks for it! As I said, I had a theory, and knowing that this has been explored in the past is definitely useful. |
@nblumhardt a question for you: in order to make |
Awesome 👍 Disallowing characters like In the case of Serilog, names can be prefixed with var user = new {Name = "nblumhardt", Id = 42};
Log.Information("Hello {@User}", user);
// The event has a structured `User` property attached, carrying `Name` and `Id` properties "Everything from |
AFAIK |
@nblumhardt we discussed the idea in LDM today: https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-28.md#specific-extensions-for-structured-logging. While we didn't accept specific syntax for structure specifiers, I'd be happy help show a proof of concept builder type that would allow |
Thanks for the update, @333fred 👍 I completely understand the reasoning, but, I don't think the ergonomics of the tuple-based workaround will be adequate to encourage large-scale adoption through something like Serilog. We already have a broad, consistent ecosystem based around message templates, complete with analyzers etc., and so to want to fragment that by introducing another API, we'd need to achieve something substantially better. I think it's worth considering removing "structured logging" from the explicit goals of the feature. To move structured logging in C# forwards (and not just create sideways fragmentation), the feature would need to give the structured logging scenario enough weight/importance to justify first-class support. |
(Also - thanks for considering and proposing this, nonetheless! 🙏 ) |
I know it's a year later, but would you still be able/willing to show/share this? The reason I ask is that the code in our codebase has hundreds, maybe thousands of methods that look like the following:
I came up with an idea of using
but that's obviously absolutely hideous in terms of allocations and nobody should ever use the above code. My understanding (and please correct me if I'm wrong - it's based on this blog post) is that implementing your idea of I have two questions:
|
A builder can be an argument to an extension method, and if so it will work as you expect it to.
Likely yes. Overload resolution will find the As to the builder itself, it would look something like this: [InterpolatedStringHandler]
public struct TupleHandler
{
public TupleHandler(int literalLength, int formattedCount, <your params here>, out bool shouldFormat)
{
...
}
public void AppendLiteral(string s) { ... }
public void AppendFormatted<T>((string Name, T Value) formatHole) { .... }
} |
This is a "lightweight" solution to my use case. Capturing the expressions used in the interpolated string. And it seems to work. Maybe someone already posted this or it is more of a quirk than a feature. [InterpolatedStringHandler]
public readonly ref struct LogInterpolatedStringHandler {
private readonly StringBuilder _builder;
private readonly Dictionary<string, (Type, object)> _values = new();
public int FormattedCount { get; }
public IReadOnlyDictionary<string, (Type, object)> Values => _values;
public LogInterpolatedStringHandler(int literalLength, int formattedCount) {
_builder = new StringBuilder(literalLength);
FormattedCount = formattedCount;
}
public void AppendLiteral(string s) => _builder.Append(s);
public void AppendFormatted<T>(T t, [CallerArgumentExpression(nameof(t))] string name = null!) {
_builder.Append('{').Append(name).Append('}');
_values.TryAdd(name, (typeof(T), t!));
}
public void AppendFormatted<T>(T t, string format, [CallerArgumentExpression(nameof(t))] string name = null!) {
_builder.Append('{').Append(name).Append(':').Append(format).Append('}');
_values.TryAdd(name, (typeof(T), t!));
}
internal string GetFormattedText() => _builder.ToString();
}
public class LogInterpolatedStringHandlerTest {
[Fact]
public void Test() {
var now = DateTime.Now;
var v = Build($"begin {now} {now:dd} {DateTime.Now:dd} end");
Assert.Equal("begin {now} {now:dd} {DateTime.Now:dd} end", v.GetFormattedText());
var (typeNow, nowValue) = Assert.Contains("now", v.Values);
Assert.Equal(now, nowValue);
Assert.Equal(typeof(DateTime), typeNow);
var (typeExp, expValue) = Assert.Contains("DateTime.Now", v.Values);
Assert.InRange((DateTime)expValue, now, now + TimeSpan.FromSeconds(10));
Assert.Equal(typeof(DateTime), typeExp);
}
private static LogInterpolatedStringHandler Build(LogInterpolatedStringHandler builder) => builder;
} I posted this in case anyone else stumbles upon this thread (like I have), looking for something similar. |
This proposal was already answered on CodePlex with "too late for C#6", but since C#7 is taking input at the moment, I think it is worth mentioning this again.
Problem:
Current design for
FormattableString
is:Consider two following usages:
In addition to saving the log string, Serilog saves format values as properties on the log entry object. However with current
FormattableString
the names of these values (position
,elapsedMs
) would not be available.Dapper does not really need the names (it can generate parameters named
@p1
and@p2
), but proper names can improve readability of SQL traces and general SQL debugging.And another (weird) use case for capturing names of the provided values:
Compare this with current:
Potential solution
Add
IReadOnlyList<string> Names { get; }
to theFormattableString
.Those could be generated based on anonymous class property name algorithm:
Or just literally contain passed in code as a string, e.g.
$"{x+5}" // Names = "x+5"
, though in this case something likeSnippets
would be a better name.Since each call site will have a fixed set of names/snippets, each list can be cached per call site once and reused between calls. This means that even the names/snippets aren't used, the performance cost would be minimal.
Pros
FormattableString
or recording the parameters somewhere.FormattableString
by making it a light-weight way to pass identifier-named value -- e.g. validation.Cons
The text was updated successfully, but these errors were encountered: