Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
08ccb50
Initial plan
Copilot Feb 2, 2026
2966569
Initial commit - planning AOT compatibility changes for CredentialDes…
Copilot Feb 2, 2026
112e717
Implement AOT-compatible CredentialDescription for .NET 10+ with exte…
Copilot Feb 2, 2026
6b47d7d
Add changelog entry for AOT compatibility changes
Copilot Feb 2, 2026
882b8f4
Add comment explaining LangVersion preview requirement
Copilot Feb 2, 2026
72db344
Fix comment typos: CachedID -> Cached Id
Copilot Feb 2, 2026
8a8d3c8
Improve extension class documentation clarity
Copilot Feb 2, 2026
0e80eac
Add detailed remarks about C# 15 extension property syntax
Copilot Feb 2, 2026
60731d7
Enhance LangVersion preview documentation with migration plan
Copilot Feb 2, 2026
3b6093a
Address PR feedback: revert NuGet.config, use C# 14 instead of preview
Copilot Feb 3, 2026
a55edf7
Bump version to 11.0.0 for binary breaking change
Copilot Feb 3, 2026
12510c2
Tweaks
jmprieur Feb 3, 2026
a08ce07
Add Microsoft.Identity.Abstractions.AotTests project for .NET 10 AOT …
Copilot Feb 3, 2026
82cae95
Improve the AoT test
jmprieur Feb 3, 2026
64ea9b7
Add AOT test execution to CI pipeline
Copilot Feb 3, 2026
106848b
Update changelog with C# 14 requirement and troubleshooting guidance
Copilot Feb 3, 2026
6758c57
Change internal methods to protected internal for derived class access
Copilot Feb 3, 2026
25e322d
Address review feedback: simplify copy constructor, add CA1024 pragma…
Copilot Feb 3, 2026
1d19c66
Clarify extension properties are .NET 10+ by design choice, not langu…
Copilot Feb 3, 2026
0054e3f
Refine documentation: C# 14 available on all TFMs, used on .NET 10+ b…
Copilot Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ jobs:
- name: Build and test
run: dotnet test -p:TargetNetNext=${{ env.TargetNetNext }} -v m --configuration Release Microsoft.Identity.Abstractions.sln

# Run AOT tests to validate no reflection warnings (requires .NET 10)
- name: Publish and run AOT tests
run: |
cd test/Microsoft.Identity.Abstractions.AotTests
dotnet publish --runtime win-x64 -f net10.0 --configuration Release -v m
.\bin\Release\net10.0\win-x64\publish\Microsoft.Identity.Abstractions.AotTests.exe
shell: pwsh

# Pack the library
- name: Pack
run: dotnet pack -p:TargetNetNext=${{ env.TargetNetNext }} --configuration Release --no-restore --no-build -v m Microsoft.Identity.Abstractions.sln
6 changes: 4 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- This should be passed from the VSTS build -->
<MicrosoftIdentityAbstractionsVersion Condition="'$(MicrosoftIdentityAbstractionsVersion)' == ''">10.0.0</MicrosoftIdentityAbstractionsVersion>
<MicrosoftIdentityAbstractionsVersion Condition="'$(MicrosoftIdentityAbstractionsVersion)' == ''">11.0.0</MicrosoftIdentityAbstractionsVersion>
<!-- This will generate AssemblyVersion, AssemblyFileVersion and AssemblyInformationVersion -->
<Version>$(MicrosoftIdentityAbstractionsVersion)</Version>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\build\35MSSharedLib1024.snk</AssemblyOriginatorKeyFile>
Expand Down Expand Up @@ -39,7 +39,9 @@
<AssemblyOriginatorKeyFile>../../build/MSAL.snk</AssemblyOriginatorKeyFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
<!-- LangVersion set to 14 to enable C# 14 extension properties for .NET 10+ AOT compatibility.
Extension properties are required to make CredentialDescription AOT-safe while maintaining source compatibility. -->
<LangVersion>14</LangVersion>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>8.0.0</PackageValidationBaselineVersion>
<BannedApiAnalyzersVersion>4.14.0</BannedApiAnalyzersVersion>
Expand Down
37 changes: 36 additions & 1 deletion Microsoft.Identity.Abstractions.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157 d18.0
VisualStudioVersion = 18.0.11205.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Abstractions", "src\Microsoft.Identity.Abstractions\Microsoft.Identity.Abstractions.csproj", "{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}"
EndProject
Expand All @@ -25,27 +25,62 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D5BF0954-2
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Abstractions.Tests", "test\Microsoft.Identity.Abstractions.Tests\Microsoft.Identity.Abstractions.Tests.csproj", "{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Abstractions.AotTests", "test\Microsoft.Identity.Abstractions.AotTests\Microsoft.Identity.Abstractions.AotTests.csproj", "{39865C17-1E90-495D-BCB1-C171FCE7078D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|x64.ActiveCfg = Debug|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|x64.Build.0 = Debug|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|x86.ActiveCfg = Debug|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|x86.Build.0 = Debug|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|Any CPU.Build.0 = Release|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|x64.ActiveCfg = Release|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|x64.Build.0 = Release|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|x86.ActiveCfg = Release|Any CPU
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|x86.Build.0 = Release|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|x64.ActiveCfg = Debug|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|x64.Build.0 = Debug|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|x86.ActiveCfg = Debug|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|x86.Build.0 = Debug|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|Any CPU.Build.0 = Release|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|x64.ActiveCfg = Release|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|x64.Build.0 = Release|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|x86.ActiveCfg = Release|Any CPU
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|x86.Build.0 = Release|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|x64.ActiveCfg = Debug|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|x64.Build.0 = Debug|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|x86.ActiveCfg = Debug|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|x86.Build.0 = Debug|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|Any CPU.Build.0 = Release|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|x64.ActiveCfg = Release|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|x64.Build.0 = Release|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|x86.ActiveCfg = Release|Any CPU
{39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F} = {68824A9C-D009-491A-8A76-680A261A8C71}
{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2} = {D5BF0954-25F6-40ED-9896-8EDB99EBEF5A}
{39865C17-1E90-495D-BCB1-C171FCE7078D} = {D5BF0954-25F6-40ED-9896-8EDB99EBEF5A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {34D01AD8-BCE0-44B3-9197-439FF4A1D46D}
Expand Down
29 changes: 29 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
11.0.0
=======
## Breaking changes

Comment thread
jmprieur marked this conversation as resolved.
If you build code with the .NET 10 target framework and get this error:
```txt
error CS9260: Feature 'extensions' is not available in C# 13.0. Please use language version 14.0 or greater.
```
make sure you update the `LangVersion` property in your project to 14 or later.

### AOT/NativeAOT Compatibility for .NET 10+

Made `CredentialDescription` AOT-compatible for .NET 10+ by using C# 14 extension properties. This is a **binary breaking change** (though source compatible) for .NET 10+ targets:
- Removes `Certificate` and `CachedValue` as public properties from `CredentialDescription` when targeting .NET 10+
- Adds extension properties with the same names and signatures for .NET 10+, providing property-style access
- Maintains full source compatibility - no code changes required for consumers provided the .NET 10 code is built with C#14 or later.
- Prevents AOT/NativeAOT configuration binding issues with reference-typed properties
- Keeps existing behavior for older target frameworks (netstandard2.0, netstandard2.1, net462, net8.0, net9.0)

**Technical details:**
- For .NET 10+: `Certificate` and `CachedValue` are implemented as extension properties (not visible to config binders)
- For older TFMs: `Certificate` and `CachedValue` remain as regular public properties
- LangVersion updated to `14` to enable C# 14 extension property syntax
- Internal accessor methods (`GetCertificateInternal`, `SetCertificateInternal`, etc.) support extension properties

This enhancement ensures `CredentialDescription` works seamlessly in AOT/NativeAOT compilation scenarios while maintaining backward compatibility.

10.0.0
=======
## Breaking changes
Rename `IAuthorizationHeaderProvider2` to `BoundAuthorizationHeaderProvider`. This interface extends `IAuthorizationHeaderProvider` to create authorization headers with a token which is optionally bound to a certificate (for mTLS Pop). For details, see [PR #232](https://github.com/AzureAD/microsoft-identity-abstractions-for-dotnet/pull/232)

In practice, it's unlikely that this breaking change affects anybody as the renamed interface was new in 9.6.0, and not yet used to the team's knowledge.

## Improvements and fundamentals

9.6.0
======
## New features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ public CredentialDescription(CredentialDescription other)
#endif
Algorithm = other.Algorithm;
Base64EncodedValue = other.Base64EncodedValue;
CachedValue = other.CachedValue;
Certificate = other.Certificate;
// Copy the backing fields directly. This is safe to do in a copy-constructor, because the ID is NULL initially.
_cachedValue = other._cachedValue;
_certificate = other._certificate;
CertificateStorePath = other.CertificateStorePath;
CertificateDistinguishedName = other.CertificateDistinguishedName;
CertificateThumbprint = other.CertificateThumbprint;
Expand Down Expand Up @@ -77,14 +78,15 @@ public string Id
{
if (_cachedId == null)
{
Comment thread
jmprieur marked this conversation as resolved.
string certificateThumbprint = Certificate?.Thumbprint ?? "null";
// Use backing field directly to work in both .NET 10+ and older TFMs
string certificateThumbprint = _certificate?.Thumbprint ?? "null";

switch (SourceType)
{
case CredentialSource.Certificate:
if (Certificate != null)
if (_certificate != null)
{
_cachedId = $"Certificate={Certificate.Thumbprint}";
_cachedId = $"Certificate={_certificate.Thumbprint}";
}
else
{
Expand Down Expand Up @@ -126,9 +128,9 @@ public string Id
_cachedId = $"CustomSignedAssertion={CustomSignedAssertionProviderName}({parameterNames})";
break;
case CredentialSource.ManagedCertificate:
if (CachedValue != null)
if (_cachedValue != null)
{
_cachedId = $"ManagedCertificate={CachedValue};Thumbprint={certificateThumbprint}";
_cachedId = $"ManagedCertificate={_cachedValue};Thumbprint={certificateThumbprint}";
}
else
{
Expand Down Expand Up @@ -417,6 +419,49 @@ public string? ClientSecret
/// <remarks>If you want to use the default authority, don't provide a token exchange authority URL.</remarks>
public string? TokenExchangeAuthority { get; set; }

#if NET10_0_OR_GREATER
// For .NET 10+, use protected internal methods to avoid AOT issues with configuration binders
// Extension properties (defined in CredentialDescriptionExtensions.cs) provide property-style access
// Methods are protected internal to allow derived classes to access them

/// <summary>
/// Gets the certificate. For .NET 10+, use the Certificate extension property instead.
/// This method is protected internal to allow derived classes to access the certificate.
/// </summary>
#pragma warning disable CA1024 // Use properties where appropriate
protected internal X509Certificate2? GetCertificateInternal() => _certificate;
Comment thread
jmprieur marked this conversation as resolved.
#pragma warning restore CA1024 // Use properties where appropriate

/// <summary>
/// Sets the certificate. For .NET 10+, use the Certificate extension property instead.
/// This method is protected internal to allow derived classes to set the certificate.
/// </summary>
protected internal void SetCertificateInternal(X509Certificate2? value)
{
_certificate = value;
// Cached Id can depend on the certificate thumbprint. Set it to null so that it will be recomputed.
_cachedId = null;
Comment thread
jmprieur marked this conversation as resolved.
}

/// <summary>
/// Gets the cached value. For .NET 10+, use the CachedValue extension property instead.
/// This method is protected internal to allow derived classes to access the cached value.
/// </summary>
#pragma warning disable CA1024 // Use properties where appropriate
protected internal object? GetCachedValueInternal() => _cachedValue;
#pragma warning restore CA1024 // Use properties where appropriate

/// <summary>
/// Sets the cached value. For .NET 10+, use the CachedValue extension property instead.
/// This method is protected internal to allow derived classes to set the cached value.
/// </summary>
protected internal void SetCachedValueInternal(object? value)
{
_cachedValue = value;
// Cached Id can depend on the cached value. Set it to null so that it will be recomputed.
_cachedId = null;
}
#else
/// <summary>
/// When <see cref="SourceType"/> is <see cref="CredentialSource.Certificate"/>, you will use this property to provide the certificate yourself.
/// When <see cref="SourceType"/> is <see cref="CredentialSource.Base64Encoded"/> or <see cref="CredentialSource.KeyVault"/>
Expand Down Expand Up @@ -450,6 +495,7 @@ public virtual object? CachedValue
_cachedId = null;
}
}
#endif

/// <summary>
/// Skip this credential description. This is useful when, you specify a list of
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#if NET10_0_OR_GREATER
using System.Security.Cryptography.X509Certificates;

namespace Microsoft.Identity.Abstractions
{
/// <summary>
/// Provides extension properties for <see cref="CredentialDescription"/> instances (.NET 10+ only).
/// These extension properties provide property-style access to Certificate and CachedValue
/// while keeping them hidden from AOT/NativeAOT configuration binders.
/// </summary>
/// <remarks>
/// This uses C# 14 extension property syntax. While C# 14 extension properties as a language feature
/// are available on any target framework, for this library they are only exposed on .NET 10+ by design
/// to avoid binary breaking changes on older frameworks.
/// The extension block defines properties that appear as instance properties on CredentialDescription
/// but are not part of the type's public API surface visible to reflection-based tools like configuration binders.
/// </remarks>
public static class CredentialDescriptionExtensions
{
#pragma warning disable CA1034 // Do not nest type - this is intentional C# 14 extension block syntax
// C# 14 extension block syntax - defines extension properties on CredentialDescription
extension(CredentialDescription credential)
{
/// <summary>
/// When <see cref="CredentialDescription.SourceType"/> is <see cref="CredentialSource.Certificate"/>, you will use this property to provide the certificate yourself.
/// When <see cref="CredentialDescription.SourceType"/> is <see cref="CredentialSource.Base64Encoded"/> or <see cref="CredentialSource.KeyVault"/>
/// or <see cref="CredentialSource.Path"/> or <see cref="CredentialSource.StoreWithDistinguishedName"/> or <see cref="CredentialSource.StoreWithThumbprint"/>
/// after the certificate is retrieved by a <see cref="ICredentialsLoader"/>, it will be stored in this property and also in the <b>CachedValue</b> property.
/// </summary>
public X509Certificate2? Certificate
{
get => credential.GetCertificateInternal();
set => credential.SetCertificateInternal(value);
}

/// <summary>
/// When the credential is retrieved by a <see cref="ICredentialsLoader"/>, it will be stored in this property, where you can retrieve it. If the credential is a certificate,
/// it will also be stored in the <b>Certificate</b> property.
/// </summary>
public object? CachedValue
{
get => credential.GetCachedValueInternal();
set => credential.SetCachedValueInternal(value);
}
}
#pragma warning restore CA1034
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public enum CredentialSource
{
/// <summary>
/// Use this value if you provide a certificate yourself. When setting the <see cref="CredentialDescription.SourceType"/> property to this value,
/// you will also provide the <see cref="CredentialDescription.Certificate"/>.
/// you will also provide the <b>CredentialDescription.Certificate</b>.
/// </summary>
Certificate = 0,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.Identity.Abstractions
/// <summary>
/// Contract for credential loaders, implemented by classes like the DefaultCertificateLoader or the DefaultCredentialLoader
/// in Microsoft.Identity.Web. Credential loaders are used to load credentials from a <see cref="CredentialDescription"/>, the result
/// is then in the <see cref="CredentialDescription.CachedValue"/> property.
/// is then in the <b>CredentialDescription.CachedValue</b> property.
/// Credential loaders constitute an extensibility point. They delegate to credential source loaders, which are specified in the <see cref="CredentialSourceLoaders"/>
/// collection, choosing the one which <see cref="ICredentialSourceLoader.CredentialSource"/> matches the credential source of the
/// credential description to load.
Expand Down
Loading
Loading