2

I have two classes like this:

public class test1: BaseClass
{
    public test1() : base()
    {
    }
...

public class BaseClass
{
    public BaseClass(
        [CallerMemberName]string membername ="",
        [CallerFilePath] string path = "")
    {
        var sf = new System.Diagnostics.StackTrace(1).GetFrame(0);
    }

If I specify test1 ctor with call to base - I get membername and path initialized properly, but if not - compiler generates default constructor call, and membername and path are both empty.

Is this a bug or a feature ?

(Visual Studio 2019 16.11.8, net core 3.1 or net 5.0).

TarmoPikaro
  • 4,723
  • 2
  • 50
  • 62
  • 2
    The docs are not explicit on whether this is the desired behavior or not. Certainly arguments can be made both ways, since the call is not explicit (note that the information being "wrong" is never an argument, since this is always possible as you can legally specify the argument values yourself to be whatever you want). A quick search turns up no applicable issue in [GH](https://github.com/dotnet/csharplang) either, although `CallerMemberName` precedes that repo, so I wouldn't expect one either. I think a new one to get the official word from the horse's mouth would not go amiss. – Jeroen Mostert Jan 04 '22 at 16:22
  • I'd guess this is expected since the compiler-generated default constructor call will explicitly pass in the values you specified in the optional parameters (i.e. empty strings) – DavidG Jan 04 '22 at 16:56

2 Answers2

0

This is likely to be expected behaviour since the compiler-generated default constructor call will explicitly pass in the values you specified in the optional parameters (i.e. empty strings). So if you instead had something like this:

public BaseClass(
    [CallerMemberName]string membername = "default-member-name",
    [CallerFilePath] string path = "default-path")
{
    var sf = new System.Diagnostics.StackTrace(1).GetFrame(0);
}

Then this code...

public class test1 : BaseClass
{
    public test1()
    {
    }
}

... gets converted to this:

public class test1 : BaseClass
{
    public test1()
        : base("default-member-name", "default-path")
    {
    }
}
DavidG
  • 113,891
  • 12
  • 217
  • 223
  • It's certainly behavior that's easily *explained*, but the fact that it has an obvious implementation doesn't necessarily imply it's the right or intended thing to do. The end of the story is still that invoking the base constructor explicitly has different behavior from having it invoked implicitly despite being invoked from the same call site, which is at the very least debatable. – Jeroen Mostert Jan 04 '22 at 17:24
  • @JeroenMostert It's a combination of the constructor being added *and* the default values being used since that overrides anything that the compiler would insert. Remember that the compiler will replace the values in the code as it is above with `public test1() : base(".ctor", "some-path")`. I don't think it's "right" or "wrong", it's just the way it is. It may be worthy of an issue on the repo for the devs to discuss of course. – DavidG Jan 04 '22 at 18:38
  • Exactly, but the question is whether it's right or wrong, and we don't know, so that's why this is technically not an answer :) The compiler makes a particular choice here that's defensible, but so is making the opposite choice. It's a given that the compiler must make *a* choice (it can't choose to *not* call the base constructor, since that would break other semantics, and it can't choose to *not* supply parameter values, since that's not how that works on the IL level), but the choice it makes doesn't appear to be documented, so it's hard to tell if it's intended. – Jeroen Mostert Jan 04 '22 at 19:26
0

I've googled "c# compiler github" and ended up on https://github.com/dotnet/roslyn

After searching by CallerMemberName - I've managed to find answer for this question:

https://github.com/dotnet/roslyn/issues/53757

It mentions that this is done by design.

But quickly scanning through the tickets lead me of thinking - "can I use attributes for same purpose ?"

Since what I was doing was unit testing - I've recoded my own attribute for that purpose: TestAttribute => FactAttribute and resolved from NUnit method info that attribute, and got back file path and method name.

public class FactAttribute : TestAttribute
{
    public string FunctionName { get; }
    public string FilePath { get;  }

    public FactAttribute( [CallerMemberName] string functionName = "", [CallerFilePath] string filePath = "")
    {
        FunctionName = functionName;
        FilePath = filePath;
    }
}

[TestFixture]
public class BaseClass
{
    /// <summary>
    /// Accesses private class type via reflection.
    /// </summary>
    /// <param name="_o">input object</param>
    /// <param name="propertyPath">List of properties in one string, comma separated.</param>
    /// <returns>output object</returns>
    object getPrivate(object _o, string propertyPath)
    {
        object o = _o;
        var flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
        
        foreach (var name in propertyPath.Split('.'))
        {
            System.Type type = o.GetType();

            if (char.IsUpper(name[0]))
                o = type.GetProperty(name, flags).GetValue(o);
            else
                o = type.GetField(name, flags).GetValue(o);
        }

        return o;
    }

    [SetUp]
    public void EachSpecSetup()
    {
        var mi = (MemberInfo)getPrivate(TestContext.CurrentContext.Test, "_test.Method.MethodInfo");
        FactAttribute attr = mi.GetCustomAttribute<FactAttribute>();
        string path = attr.FilePath;
        string funcName = attr.FunctionName;
    }

This allows to determine from which file and from which method call was directed from.

TarmoPikaro
  • 4,723
  • 2
  • 50
  • 62