-
Notifications
You must be signed in to change notification settings - Fork 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
[API proposal] Attribute for passing caller identity implicitly #4984
Comments
Tagging subscribers to this area: @dotnet/area-system-runtime Issue DetailsRationaleNumber of libraries, including the new minimal ASP.NET host, use Proposalnamespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public sealed class CallerIdentityAttribute : Attribute
{
public CallerIdentityAttribute();
}
} This attribute can be applied to parameters of type
|
@jaredpar Thoughts on this proposal? cc @davidfowl |
So in the case of ASP.NET Core, it would look like this: public class WebApplication
{
public static WebApplicationBuilder CreateBuilder(string[] args, [CallerIdentityAttribute]Assembly assembly = null);
} Usage: var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", context => context.Response.WriteAsync("Hello World!"));
app.Run(); The compiler would do this (since we're in Main): var builder = WebApplication.CreateBuilder(args, typeof(Program).Assembly);
var app = builder.Build();
app.MapGet("/", context => context.Response.WriteAsync("Hello World!"));
app.Run(); That's slick. I like it. |
We should consider marking |
Should this be in csharplang? |
Agree. This should start in csharplang. |
Also see: #87 If those attributes were collapsed into a single attribute that passed a |
I expect that we will frequently need to add trimming annotations like |
Overall this looks pretty reasonable to me. For the class Program {
static void Main() {
PrintMe();
var x = () => PrintMe();
x();
}
static void PrintMe([CallerIdentityAttribute]Type type = null) => Console.WriteLine(type.FullName);
} Does this print
Or
|
Good question. I think it depends on the usage. Should this work? class Program {
static void Main() {
PrintMe();
var x = () => PrintMe();
x();
}
static void PrintMe([CallerIdentityAttribute]Type type = null)
{
var m = type.GetMethod(nameof(PrintMe), BindingFlags.Public | BindingFlags.Static); // Does this return null?
}
} |
That is explicitly undefined 😄. The compiler does not specify how lambdas are emitted, except in special cases, because we want to be free to change it between versions. Hence the relationship in terms of metadata between I know that often the language / framework says an item in metadata is undefined but in reality it never changes hence developers feel safe in taking dependencies on it. Lambda metadata is one area we've changed several times though and broken developers who took dependencies on it. |
I think it makes more sense to emit the declared types/methods, given that it is not specified how lambdas and other similar constructs are emitted. |
To make example in #4984 (comment) work as written, how are we going to retrofit the existing method to use the new pattern? public class WebApplication
{
// Existing method
public static WebApplicationBuilder CreateBuilder(string[] args);
// New method
public static WebApplicationBuilder CreateBuilder(string[] args, [CallerIdentityAttribute] Assembly assembly = null);
} I assume that |
Believe we had the same overload issue with |
Yeah, it'd be nice if you had: Debug.Assert(condition); that we could change the method: public static void Assert([DoesNotReturnIf(false)] bool condition, string? message) to instead be: public static void Assert([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression("condition")] string? message = null) such that the compiler would emit: Debug.Assert(condition, "condition"); Unfortunately, we already have the |
So we need to add this overload now (to WebApplication) so we can make this work in the future. |
We already have the existing overload. How does adding the new overload now help (assuming the attribute would have similar semantics to the existing attributes in terms of overload resolution)? |
I mean for WebApplication (a new API being added by ASP.NET Core), not Debug.Assert |
Ok, but if you add both: M(string[] args);
M(string[] args, Assembly? assembly = null); today, calls to If what you mean is you would need to add |
This is what I meant, yes |
The overload issue is a problem here because we're effectively stuck with the overload we don't want to be used anymore. With The imperfect solution I'd been thinking of in my head was ... yet another feature!!! 😄 Essentially imagine an attribute named That would allow us to solve this problem rather easily: public class Debug {
[BinaryCompatOnly]
public static void Assert([DoesNotReturnIf(false)] bool condition) { ... }
public static void Assert([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression("condition")] string? message = null) { ... } The burden would be on the API author to ensure they didn't create source breaks when doing this. For the majority use case here though that works out just fine. Given that the extra parameters are optional and we fill in the values existing invocations will just work. The problem comes in though when we get into method group conversions. Action<string> action = Debug.Assert; That would not work because the new overload isn't compatible with |
|
That's a fascinating idea, and I'd want to look at it in conjunction with other ideas that could affect overload resolution. We've had other ideas around attributes that could serve as tiebreakers if two APIs would otherwise be considered ambiguous, and this seems like it might either be a good proving ground for such an attribute, or a good thing to bundle at the same time. |
In my defense, I've only been back from vacation for three hours now. 😄 |
What if the old overload was only kept in runtime assemblies, but removed from reference assemblies? I think this would mean switching to the new overloads with just recompilation, while maintaining both binary and source compatibility (except for method group conversions, as mentioned above). It's only a practical solution for large libraries (where it's worth having separate reference assemblies), but that's also where compatibility matters the most.
For reference, #4867 is a recent discussion on that topic. |
That is a viable option but only in a specific subset of scenarios. Mostly when the code is written in C# and it involves method invocation. It becomes unviable in C# method group conversions, F# (likely at least because they have diff overload resolution and method group rules), when the method is an instance method on a non-sealed type, etc ... The instance method on an unsealed type is the easiest deal breaker. Have to assume that someone derived from you and bound that method to an interface implementation. |
It would be still a problem if it was NOT conditional though. To be fair, at the moment, overloading So the only reason for this to exist is to maintain backward compatibility. I'd say we should never remove those from the candidate set and prefer the ones with caller info because, as far as the compiler concerns, those are "more specific" overloads since we know how to provide the missing arg(s) with proper values. |
I just learned about this proposal from the C-sharp language group. I love the idea but I would recommend using the name "CallerAssemblyAttribute" instead of ...Identity... When i read the name "identity" my first thought was it had something to do with permissions, users, authorization, or security. |
This proposal isn't just about passing the caller's |
In that case, I'd recommend three different attributes. CallerAssemblyInfo, TypeInfo, MethodInfo. |
In the case of xUnit.net, we'd probably also want to hide the version that has all the extra junk on it, because the extra junk causes usability problems (and, specifically, I mean in concert with [CallerArgumentExpression], but it would apply to this scenario as well). So it'd be more like this for us: public class Assert {
[BinaryCompatOnly]
public static void Equal<T>(T expected, T actual) { ... }
[EditorBrowsable(false)]
public static void Equal<T>(
T expected,
T actual,
[CAE("expected") string? expectedExpression = null,
[CAE("actual") string? actualExpression = null) { ... } I assume this would give us the "illusion" in usability of the end user only ever seeing (Related discussion: xunit/xunit#1596) |
@HaloFour Fair. It's probably robust to just have the compiler fill in |
Outside of caller parameters, and more in line with #6041 (which was closed as a duplicate of this, hence posting here, though it does seem not entirely related), I often find myself needing the assembly name of the current type -- for example, to compose RCL wwwroot content paths of the form |
With |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
I have an additional scenario/request for this feature, but I'm not sure it's an easy or even reasonable one, but I'd like to at least describe the scenario so it's on the radar. I am not using NativeAOT nor trimming in this context. Just crossgen/R2R. This is in the context of Paint.NET's plugin systems and APIs. I very occasionally have a need to establish the identity of the calling method -- but, I also want to make sure that the caller cannot lie, spoof, or just get it wrong, and it'd be nice to not add additional visible complexity to things. My proposal is to add a public sealed class ClassThatPluginsUse
{
// Maybe this would be required to have [MethodImpl(MethodImplOptions.AggressiveInlining)]
public ClassThatPluginsUse() : this((object?)null) { }
private ClassThatPluginsUse(object? ignored, [CallerIdentity(SkipFrames = 1)] Assembly? callingAssembly = null)
{
// ... do something that depends on who the caller is
}
} I want the plugin to do I'm not going to make the argument that this would be a security feature 😂 This about wanting to be able to change behavior based on the app version that the plugin is compiled against. Call it a "plugin version contract" if you will. I can then call into I currently have a need for this in an API where I forgot to check a constructor parameter for public sealed class TabContainerControlInfo
: StatefulControlInfo<TabContainerState, TabContainerStateProperty>
{
// The private-protected base class constructor accepts `null` and this is normal. This parameterless constructor is the right way to do that.
public TabContainerControlInfo()
: base((TabContainerStateProperty?)null)
{
}
// But I forgot to do a null-check here on the stateProperty parameter
public TabContainerControlInfo(TabContainerStateProperty stateProperty)
: base(stateProperty) // should be e.g. `stateProperty ?? throw new ArgumentNullException(nameof(stateProperty))`
{
}
... This has resulted in some confusion and a bunch of wasted debugging time by plugin authors who were then getting a But I can't add the null check here without breaking the 10+ plugins that are already using this API incorrectly. Those plugins work fine today because they weren't using the I want to add the null-check, but only if the caller (the plugin) was compiled against the new version of my app. It's very hard to achieve this otherwise; I prototyped a system where I would use a An alternative way of versioning this API would be to In Paint.NET I try to maintain strict binary compatibility for plugins, and there are plugins that are almost 20 years old that still work fine today. This is the best experience for the end-user/customer: breaking a plugin to correct an API does not really affect the plugin author, it only makes things worse for the end-user/customer by breaking their work/creative-flow. Having additional tools for versioning the plugin APIs makes it easier for me to uphold all of this. |
@rickbrew I'm not sure how that would work though. This attribute, like all other caller info attributes, would need to be filled in at the call site by the compiler. How would the compiler insert the info from 2 frames up? And moreover, as with all caller info attributes, the user can simply provide whatever value they want manually if they so desire, so it really wouldn't be spoof-free either. It very much feels like, to get the functionality you want, you need to actually do the stack walk with a |
Right, I understand -- but note that the constructor taking the |
Also, by describing my scenario it may result in someone realizing "oh that can actually be achieved in this other way that is feasible to add support for". |
This doesn't seem to help though? What is the compiler actually going to insert at the call site of the method? |
I don't know I'm not really a compiler person ¯\(ツ)/¯ I'm not really trying to propose an implementation strategy, and I did note that I didn't know if it was feasible. I thought the scenario could be an interesting one to chew on. It's not unheard of for an API to forget to validate parameters but in a way that doesn't cause problems because they're passed on to other APIs where those parameter values are okay. |
I'm also describing this because it seems there's a push to deprecate things like |
The problem with The API proposed here would be guaranteed to give the caller (unless the caller goes out of the way to spoof this but then ¯(ツ)/¯). The SkipFrames extension would just reintroduce the same problem because it would have to do a stack walk at runtime. There are no plans to deprecate |
Rationale
Number of libraries, including the new minimal ASP.NET host, use
Assembly.GetCallingAssembly()
for convenience.Assembly.GetCallingAssembly()
is slow and unreliable API, and also not compatible with AOT technologies (e.g. dotnet/runtime#53825). We need a fast, reliable and AOT-friendly replacement ofAssembly.GetCallingAssembly()
.Proposal
This attribute can be applied to parameters of type
System.Reflection.Assembly
,System.Reflection.Type
,System.Reflection.MethodBase
. When the C# compiler sees parameter tagged with this attribute, it will provide default value for it, according to the caller context:System.Reflection.Assembly
:typeof(containing_type).Assembly
System.Reflection.Type
:typeof(containing_type)
System.Reflection.MethodBase
:MethodBase.GetMethodFromHandle(ldtoken containing_method)
Design meetings
The text was updated successfully, but these errors were encountered: