Proposal: Expand supported Caller Info Attributes · dotnet/csharplang · Discussion #87 (original) (raw)

Feb 14, 2017

· 63 comments· 20 replies

Copied from dotnet/roslyn#351

Problem:

The currently supported caller info attributes can only provide the name of the method, the name of the source file and the line number of the call in the source file. This is fine for scenarios such as simplifying implementations of INotifyPropertyChanged. However, if using these attributes for logging the amount of information available is quite limited.

Solution:

Expand the number of supported caller info attributes to allow embedding additional diagnostic information. The following list is quite expansive to discuss/argue over the potential possibilities.

CallerColumnNumberAttribute: The column number of where the method is invoked.
CallerTypeNameAttribute: The simple name of the declaring type of the calling method.
CallerNamespaceAttribute: The namespace of the declaring type of the calling method.
CallerFullTypeNameAttribute: The full name of the declaring type of the calling method.
CallerTypeAttribute: The declaring type of the calling method. This is replaced by the compiler by ldtoken of the type followed by a call to Type.GetTypeFromHandle.
CallerMethodAttribute: The MethodBase of the calling method. This is replaced by the compiler by ldtoken of the method reference followed by a call to MethodBase.GetMethodFromHandle.

Example usage:

using System; using System.Diagnostics; using System.Runtime.CompilerServices;

namespace Project1 { public static class Program { private static void Foo([CallerMemberName] string memberName = null, [CallerTypeName] string typeName = null, [CallerNamespaceName] string namespaceName = null, [CallerFullTypeName] string fullTypeName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = 0, [CallerColumnNumber] int columnNumber = 0, [CallerType] Type type, [CallerMethod] MethodBase method) { Debug.Assert(memberName == "Main"); Debug.Assert(typeName == "Program"); Debug.Assert(namespaceName == "Project1"); Debug.Assert(fullTypeName == "Project1.Program"); Debug.Assert(filePath == "c:\foo\bar\Program.cs"); Debug.Assert(lineNumber == 29); Debug.Assert(columnNumber == 12); Debug.Assert(type == typeof(Program)); Debug.Assert(method == typeof(Program).GetMethod("Main")); }

    public static void Main() {
        Foo();
    }
}

}


It was also suggested by @AdamSpeight2008 that a caller-info struct be supported for situations where you want several of these values but don't want to have to declare/annotate multiple parameters.

Class CallerInfo Public ReadOnly MemberName As String Public ReadOnly TypeName As String Public ReadOnly Namespace As String Public ReadOnly FullTypeName As String Public ReadOnly FilePath As String Public ReadOnly LineNumber As Integer Public ReadOnly ColumnNumber As Integer Public ReadOnly Type As Type Public ReadOnly Method As MethodBase End Class

Public Sub Foo( ByVal info As CallerInfo = Nothing) End Sub

LDM history:

You must be logged in to vote

I'm not sure I see a need for CallerColumnNumberAttribute. What's the use case you see for that one?

By supporting just CallerTypeAttribute, wouldn't we gain access to the same information as what would be in CallerTypeNameAttribute, CallerNamespaceAttribute, and CallerFullTypeNameAttribute?

CallerTypeNameAttribute is available from Type.Name, CallerNamespaceAttribute is available from Type.Namespace and CallerFullTypeNameAttribute is available through Type.FullName.

I do like the idea of having a CallerInfo struct that can be used in place of having to specify each individual attribute. This would be an "all or nothing" scenario in the sense that if I use the struct, I get all of the values filled in. If I don't want/need all of them, I can still specify them individually.

You must be logged in to vote

0 replies

Would it be possible to make the value lazy, so they are produced when need / used.

You must be logged in to vote

0 replies

@AdamSpeight2008 I'm not sure that would work. From MSDN, "Caller Info values are emitted as literals into the Intermediate Language (IL) at compile time." That would prevent them from being lazily evaluated since they aren't evaluated at run time.

You must be logged in to vote

0 replies

I'm not sure I see a need for CallerColumnNumberAttribute. What's the use case you see for that one?

Completes the set, mostly. I could see some use for capturing the position of multiple calls to the same method nested in some other expression, but that might be pretty esoteric.

By supporting just CallerTypeAttribute, wouldn't we gain access to the same information as what would be in CallerTypeNameAttribute, CallerNamespaceAttribute, and CallerFullTypeNameAttribute?

Yes. C# already supports CallerTypeNameAttribute, so I built this proposal from there.

My use cases would be covered if the only caller-info attributes were CallerMethod, CallerFilePath and CallerLineNumber. Everything else of value to me can be derived from that.

I'm not sure that would work. From MSDN, "Caller Info values are emitted as literals into the Intermediate Language (IL) at compile time." That would prevent them from being lazily evaluated since they aren't evaluated at run time.

Technically true, but it's possible that some of the work could be deferred until runtime. Specifically with CallerInfo where you accept a struct of the values. That struct could have a constructor that only accepts the RuntimeMethodHandle for the calling method pushed by ldtoken, the file name and the file line number. Then the actual method and type properties could be lazily evaluated by taking that RuntimeMethodHandle and loading the actual MethodBase and from that deriving the declaring Type.

You must be logged in to vote

0 replies

C# already supports CallerTypeNameAttribute

Do you mean CallerMemberNameAttribute?

So, right now we already have CallerFilePathAttribute, CallerMemberNameAttribute, and CallerLineNumberAttribute.

I think if we just added CallerTypeAttribute and CallerMemberAttribute (which return a Type and a MemberInfo, respectively), would that work? I changed to returning MemberInfo as it's the base class for MethodBase and I think would allow CallerMember to be more generally useful and work for properties as well.

Technically true, but it's possible that some of the work could be deferred until runtime.

I would think this wouldn't be desirable due to performance considerations. It also introduces different compile-time behavior between using the attributes directly and using the struct. Then again, it might not be possible to implement CallerTypeAttribute and CallerMemberAttribute at compile time, not sure about that.

You must be logged in to vote

0 replies

@scottdorman

Do you mean CallerMemberNameAttribute?

Oops, you're right. None of the attributes cover the type at the moment.

Then again, it might not be possible to implement CallerTypeAttribute and CallerMemberAttribute at compile time, not sure about that.

I don't see why not. They're not as simple as expressions as pushing a string literal or an integer literal, but they're still relatively easy. It's two IL opcodes to derive the Type or the MethodBase and push it onto the stack, ldtoken with the type or method token followed by either call Type.GetTypeFromHandle or call MethodBase.GetMethodFromHandle. The former is literally how typeof(T) works. The tokens themselves are deterministic at compile-time.

You must be logged in to vote

0 replies

@HaloFour typeof(T) is a runtime call though, not a compile time literal. Are you saying that the compiler would simply emit the required IL opcodes instead of emitting the literal? I guess the difference is that by emitting the literal value, there is no runtime cost to getting the information (other than just normal parameter access overhead) but there would be slight runtime cost associated with accessing CallerTypeAttribute and CallerMemberAttribute. Granted, I doubt it's significant enough of an overhead to make much difference, especially since these probably wouldn't have a high amount of access and something could probably be done to cache the call site information, similar to what's done with dynamic.

You must be logged in to vote

0 replies

@scottdorman

Yes, it would be a runtime call. The only way to avoid that would be to have those caller-info attributes applied to parameters of type RuntimeTypeHandle or RuntimeMethodHandle and have the compiler only emit ldtoken which is still evaluated at runtime but should be faster. But all you're doing there is offloading the inevitable to the callee which is going to have to invoke those methods to get something useful anyway.

Either way, this is a lot less overhead than what people are doing now to support getting this kind of information which is accepting a MethodBase and requiring the caller to call MethodBase.GetCurrentMethod manually, or by getting a stack trace in the callee and trying to find the calling method, assuming that the JIT isn't inlining it.

You must be logged in to vote

0 replies

Either way, this is a lot less overhead than what people are doing now to support getting this kind of information

Absolutely agree on that! :) Yes, this introduces a small amount of overhead, but significantly less than what's incurred now to get the same information.

You must be logged in to vote

0 replies

Another possibility is to do something similar to what we do with strings literals. Have a embedded dictionary lookup table.

You must be logged in to vote

0 replies

Building general purpose computing with CallerMemberName is extremely frustrating due to the lack of CallerTypeName, leads to all kinds of voodoo required to coerce types from somewhere that you can locate who someone is.

You must be logged in to vote

0 replies

@scottdorman [CallerType] and [CallerMember] would be exactly what I want.

You must be logged in to vote

0 replies

@dotnetchris same here. Even just the fully qualified caller type name as a parameter attribute would solve a multitude of problems.

You must be logged in to vote

0 replies

[CallerMember] would be my most preferred but I would completely be able to live with only [CallerTypeName]

You must be logged in to vote

0 replies

Basically I want more and more tools to be able to write programming to write my programming.

You must be logged in to vote

0 replies

This might be a whole other can of worms, but I think it would be extremely helpful if CallerMemberType could be used in generics as well. For example, you could have some function like this:

T MyFunc<[CallerMemberType] T>()
{
    //do stuff
}

Then you could use it like so:

public MyClass MyProperty
{
    get => MyFunc();
}

You must be logged in to vote

0 replies

Any information about CallerTypeName in .Net 6?

You must be logged in to vote

1 reply

@CodeAngry

This is a 2017 request. So I'd say check back in 2027,

You must be logged in to vote

0 replies

This really need some traction, as I keep coming back to needing this functionality when dealing with things like logging, or I'm working on "plugin-esque" applications.

I keep ending up creating messy "hacks" to get the information I need to meet some specs, and if this proposal was implemented I probalbly would have saved tens, if not hundreds of hours of development and maintainence of convoluted reflection -based logic

You must be logged in to vote

5 replies

@CyrusNajmabadi

FWIW, this post isn't super helpful. I cannot glean from it what in particular you need, and verify that adding it will be sufficient for you to "meet some specs".

It would be great to give crisp examples of what you're trying to do, how you do it currently, and which attributes in particular would make a substantive difference for you.

Thanks!

@dotnetchris

We need [CallerMemberTypeName] that's the bare minimum.

Optimally we get [CallerMember] and [CallerMemberType]

@CyrusNajmabadi

@HaloFour

@CyrusNajmabadi

As stated earlier in this discussion:

My use cases would be covered if the only caller-info attributes were CallerMethod, CallerFilePath and CallerLineNumber. Everything else of value to me can be derived from that.

Two are already covered, the missing one would be CallerMethod. That could return a MethodBase or a RuntimeMethodHandle. I believe every other value above could be derived from that, except for CallerColumnNumber, but that one might not be of value.

@cremor

@CyrusNajmabadi I aggree with @dotnetchris that [CallerMemberTypeName] is the bare minimum requirement, but only if the following two points are true:

If any of those points can't be fulfilled we need [CallerMember] (provides a MethodBase or RuntimeMethodHandle) or [CallerMemberType] (provides a Type) to be able to get the "real" full name ourselves.

This comment was marked as off-topic.

@frankhaugen

Maybe if we do it ourselves and raise a PR with awesome docs, tests and performance numbers. Do you have the free-time and skills for it?

Maybe if we do it ourselves and raise a PR with awesome docs, tests and performance numbers. Do you have the free-time and skills for it?

I feel like multiple sources have been submitted for this over the past half decade

You must be logged in to vote

0 replies

we are interested in CallerTypeNameAttribute too. Would be very nice for logging performancetracing etc
thank you

You must be logged in to vote

0 replies

We are also interested in the CallerTypeName attribute, specifically for logging needs. Would be nice to know what the current state of this is

You must be logged in to vote

2 replies

@TonyValenti

Related to call type name, it would be nice if that was smart enough to return either this.GetType or
Typeof(TSelf) (for static classes)

@CyrusNajmabadi

Would be nice to know what the current state of this is

Nothing has changed since hte last LDM meeting on the topic. If anything changes we will update this discussion. :)

I like the idea of a CallerInfo class, it would make debugging universally easier because it is not restricted to one specific aspect like the other Caller attributes

You must be logged in to vote

0 replies

CallerTypeName or CallerType would make things so much easier

You must be logged in to vote

0 replies

Lets keep this up.
CallerTypeName

You must be logged in to vote

2 replies

@TonyValenti

I would like just a plain old caller type.

@TheXenocide

Agreed, type would be more useful, you can get the name from the type easily enough (and decide whether you want fully qualified or short, include info about generic type params or not, etc.)

Turns out I too need [CallerTypeName] (context: a helper used in async tests). Parsing it out from [CallerFilePath] should be good enough™️ in my case, but isn't guaranteed to be correct, since you can have >1 type in a file. In async methods it's impossible to get the correct StackFrame from a StackTrace. So my only options are to rely on CallerFilePath, specify filenames manually or have unpleasant nameofs sprinkled around.

You must be logged in to vote

0 replies

only CallerTypeName needed for me, any news plz?

You must be logged in to vote

0 replies

This comment was marked as off-topic.

@colejohnson66

This comment was marked as off-topic.

@codemonkey493

This comment was marked as off-topic.

@dmitry-azaraev

This comment was marked as off-topic.

@CyrusNajmabadi

Please keep this discussion on topic. Discussions about other features, and your particular desire around them aren't appropriate to the discussion of the design here. Thanks.

Sometimes there are some special classes which need to do some initialization and no other class should do that, for those cases CallerMemberType would really be useful, as we have the nameof attribute already (for which refactoring works nicely) it could really be as simple as a string:

public class InitializedTypeGetter { private InitializedType? _init;

public void Init(InitializedType init, [CallerMemberType] string? caller = null)
{
    if (_init != null)
    {
        throw new InvalidOperationException("Already initialized");
    }

    if (caller != nameof(MySpecialInitializator))
    {
        throw new InvalidOperationException($"Can only be initialized by {nameof(MySpecialInitializator)}");
    }

    _init = init;
}

public InitializedType Init=> _init ?? throw new InvalidOperationException("Not yet initialized");

}

You must be logged in to vote

4 replies

@colejohnson66

That feels… wrong. A type shouldn’t care who’s using it. If you want initialization to only occur from one place, don’t expose that initialization logic.

@DomenPigeon

I don't aggree, there is a way to do exactly this already, you just need to put the InitializedTypeGetter and the MySpecialInitializator together in a new project (assembly) and then make the Init function internal and you get the desired functinality, which is perfectly acceptable. But that just isn't always practical and also having too many projects extends the build time.

@colejohnson66

Again: an “Init” function shouldn’t care who calls it. If the parameters are ok, then they’re ok.

@cyraid

@DomenPigeon Couldn't you just use Generics? Something like:

public void Init() { _init = typeof(T); // Could also use typeof(T).Name etc. if (_init != typeof(MySpecialInitializator) && !_init.IsSubclassOf(typeof(MySpecialInitializator))) throw new InvalidOperationException($"Can only be initialized by {nameof(MySpecialInitializator)}"); } // Procedure //

You could always use Generic constraints too:

public void Init() where T : MySpecialInitializator { } // Procedure //

This discussion was converted from issue #87 on September 08, 2020 19:36.

Heading

Bold

Italic

Quote

Code

Link


Numbered list

Unordered list

Task list


Attach files

Mention

Reference

Menu

Select a reply

Loading

Uh oh!

There was an error while loading. Please reload this page.

Create a new saved reply

👍 1 reacted with thumbs up emoji 👎 1 reacted with thumbs down emoji 😄 1 reacted with laugh emoji 🎉 1 reacted with hooray emoji 😕 1 reacted with confused emoji ❤️ 1 reacted with heart emoji 🚀 1 reacted with rocket emoji 👀 1 reacted with eyes emoji