Repository navigation
[API Proposal]: New AssemblyName-like type for TypeName parsing #100867
Description
Background and Motivation
While working on the implementation of TypeName API (#97566), it turned out that it's impossible to set AssemblyName.CultureName without allocating a CultrueInfo instance (using public NS2.0 compatibile APIs).
runtime/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs
Line 83 in 0764864
| set => _cultureInfo = (value == null) ? null : new CultureInfo(value); |
The type name parser needs to support parsing from untrusted input and such non-predefined culture name may be used as a vector of attack, so the existing AssemblyName is not suitable for our needs.
Since AssemblyName suffers from some other design issues (mutability, a LOT of obsolete properties), I would like to introduce a new, immutable, lightweight version of AssemblyName type.
Proposed API
It's a concept and I need the help of API review board to choose the right name. Some of my ideas: ReadOnlyAssemblyName, AssemblyId, LibraryName.
namespace System.Reflection.Metadata;
public sealed class AssemblyNameInfo : IEquatable<AssemblyNameInfo>
{
public AssemblyNameInfo(string name, Version? version = null, string? cultureName = null, AssemblyNameFlags flags = AssemblyNameFlags.None,
Collections.Immutable.ImmutableArray<byte> publicKeyOrToken = default);
public string Name { get; }
public string FullName { get; }
public Version? Version { get; }
public string? CultureName { get; }
public AssemblyNameFlags Flags { get; }
public ImmutableArray<byte> PublicKeyOrToken { get; }
public AssemblyName ToAssemblyName();
public static AssemblyNameInfo Parse(ReadOnlySpan<char> assemblyName);
public static bool TryParse(ReadOnlySpan<char> assemblyName, [NotNullWhenAttribute(true)] out AssemblyNameInfo? result);
}The approved TypeName API needs following changes:
public sealed partial class TypeName : IEquatable<TypeName>
{
- public string AssemblySimpleName { get; }
- public System.Reflection.AssemblyName? GetAssemblyName();
+ public AssemblyNameInfo? AssemblyName { get; }
}Usage Examples
Building high-level, opinionated parser on top of the low-level API:
AssemblyName GetAssemblyName(string unsafeInput)
{
if (!AssemblyNameInfo.TryParse(unsafeInput, out AssemblyNameInfo info))
{
throw new ArgumentException("Invalid name!");
}
if (info.Name.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0)
{
throw new InvalidOperationException("AssemblyName must not contain path characters.");
}
// the following line throws CultureNotFoundException for non-predefined cultures
_ = CultureInfo.GetCultureInfo(info.CultureName, predefinedOnly: true);
return info.ToAssemblyName();
}Alternative Designs
The proposed design has one property PublickKeyOrToken that can express the public key or its token. It's a design choice inherited from AssemblyName that uses AssemblyNameFlags.PublicKey flag to express what given byte array is.
We could have two dedicated properties and ctor arguments to express that: PublicKey and PublicKeyToken.
Risks
Providing low quality design may require introducing another AssemblyName-like type in the future.
Activity
dotnet-policy-service commentedon Apr 10, 2024
Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.
teo-tsirpanis commentedon Apr 10, 2024
How about naming it
AssemblyNameInfo? Similar toTimeZoneandTimeZoneInfo.jkotas commentedon Apr 10, 2024
Should this have a public constructor that allows you to set all constituent parts?
jkotas commentedon Apr 10, 2024
Should this have a FromAssemblyName method too?
MichalStrehovsky commentedon Apr 10, 2024
If you're looking for code that could use this, I'd want to switch the managed type system to this AssemblyName:
runtime/src/coreclr/tools/Common/TypeSystem/Common/IAssemblyDesc.cs
Lines 11 to 18 in 674ba3f
We're using AssemblyName right now and it's probably the only reason why we cannot turn on culture invariant mode on tools that use the managed type system. AssemblyName is unusable when culture invariant mode is enabled due to the cultureinfo.
adamsitnik commentedon Apr 11, 2024
Great idea, I am going to use it as the name in the proposal.
Not in my use case, but it makes total sense to have it.
Not in my use case and a very similar internal type from NativeAOT RuntimeAssemblyName also did not need that, so for now I would say no.
Great!
adamsitnik commentedon Apr 12, 2024
I've tried to implement the API today and found out following things:
ctoraccepts a byte array, then the user may mutate that and in order to create an immutable array, we need to perform a copy. But, if thectoraccepts an immutable array, the users can useImmutableCollectionsMarshal.AsImmutableArray(array), pass the result to the ctor and avoid the extra copy.AssemblyName.FullNameis called and a public key is present, a validation is being performed and it may throwSecurityException. I believe it's better to validate it in the ctor and throwArgumentExceptionruntime/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs
Lines 260 to 269 in 7b534da
runtime/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameHelpers.StrongName.cs
Lines 19 to 20 in 7b534da
AssemblyName.FullNameis called and a public key token is present, a validation of the length is being performed and it may throwArgumentException. I believe it's better to validate it in the ctor.runtime/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs
Line 269 in 7b534da
runtime/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyNameFormatter.cs
Lines 62 to 63 in 7b534da
AssemblyName.GetPublicKeyTokenis called, it may compute the token if public key is present. Since we want to make it immutable this work needs to be performed at ctor.runtime/src/libraries/System.Private.CoreLib/src/System/Reflection/AssemblyName.cs
Line 215 in 7b534da
Please let me know if you believe that my observations are wrong.
jkotas commentedon Apr 14, 2024
This type should not be doing any validation of the PublicKey or PublicKeyToken; or try to compute PublicKeyToken from PublicKey. It should treat the PublicKeyToken/PublicKey as an opaque blob. (BTW: We have been going in this direction for the assembly loader in modern .NET. Strong names and strong name mismatches are ignored by the assembly loader, and the publickey/publickeytoken are just treated as opaque blobs that are passed around for backward compatibility.)
We may want to have just a single property
PublicKeyOrTokenand useAssemblyNameFlags.PublicKeyto determine whether it is key or token.The existing behavior of AssemblyName.FullName that converts PublicKey to PublicKeyToken is questionable. It is perfectly fine to have full assembly name with public key. I do not think we want copy this behavior in this type. The full name should match what you have constructed the AssemblyNameInfo with, without any conversions.
MichalStrehovsky commentedon Apr 15, 2024
How would be go about implementing
IEquatable<AssemblyNameInfo>in the presence of PublicKey/PublicKeyToken? If we have twoAssemblyNameInfos for the same assembly, but one using PublicKey and the other using PublicKeyToken, would they not be equal? Or would we ignore public key in equality checks? (And if we ignore it, do we have any concerns when S.R.Metadata is used on desktop, since it still ships as netstandard?)jkotas commentedon Apr 15, 2024
I would expect that
IEquatable<AssemblyNameInfo>would look for exact match of all components (the name can be compared as case insensitive).IEquatable<AssemblyNameInfo>would not be usable for ref/def binding. As you have pointed out, the .NET universe has number of different implementations of ref/def bindings.bartonjs commentedon Apr 16, 2024
Video
Neme12 commentedon Apr 20, 2024
I really think
TypeNameshould beTypeNameInfoto matchAssemblyNameInfofor consistency if that's whatAssemblyNameInfowill be called. (Not that I thinkTypeNameInfois a great name - butAssemblyNameInfoisn't either).Neme12 commentedon Apr 20, 2024
Looking at
AssemblyNameInfo, it seems it's only a bag of properties with a constructor with parameters that match each of the property. Should this be arecordwithinitproperties then? That has the benefit that users will be able to usewithto get another instance with one property changed. But that would requireIEquatable<T>and also wouldn't allow any validation across multiple properties, if there is anything like that.Neme12 commentedon Apr 20, 2024
I really think the types should implement
IParsable<TSelf>since they already have Parse methods. Why not? They already have the functionality, so it's not any maintenance burden to just add the interface. My view is that a class should always implement standard interfaces corresponding to the methods it already provides, so that it can be used by someone when they're working with the interface or in a generic context. Otherwise it can't be since interfaces aren't duck typed in C#. Please consider taking this viewpoint and always adding standard interfaces corresponding to the functionality the class already provides. The same way that if a type has anEquals(T other)method, it should implementIEquatable<T>, or if it has aDispose()method, it should implementIDisposable(unless it's a ref struct of course).EDIT: I do admit that it doesn't exactly match the interface because it also takes an
IFormatProviderand a format string, but .NET does this in many places, including on core types likebool, where there are no formatting options so the public method that is exposed only takes the input, and it implements the interface explicitly,ignoring the format string (although I personally think it should throw if the format string is not empty, because it won't be respected anyway, and because we might have the possibility of adding a few formatting options in the future via the format string (e.g. regular vs strict byEDIT2: Scratch that, I was wrong, IParsable doesn't take a format string, that's only the case for IFormattable. It only takes a culture, which is fine to ignore.randscharacters), and if it already works and always does the default thing, then we can't do that as that would be a big breaking change).