From c4ffc384c74930350274068c019fa4f6b57d2be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20S=C3=B8rensen?= Date: Fri, 14 Aug 2026 11:21:43 +0200 Subject: [PATCH] Move keys to pascal while keeping legacy valid --- .../check-dataverseconnection-nuget.yml | 2 +- .../DataverseOptionsBinderTests.cs | 53 ++++++++++++++----- .../Internal/DataverseOptionsBinder.cs | 33 ++++++++++-- .../Internal/ServiceClientBuilder.cs | 6 ++- DataverseWhoAmI/Program.cs | 2 +- README.md | 16 +++--- 6 files changed, 81 insertions(+), 31 deletions(-) diff --git a/.github/workflows/check-dataverseconnection-nuget.yml b/.github/workflows/check-dataverseconnection-nuget.yml index 5ea60b7..5e32388 100644 --- a/.github/workflows/check-dataverseconnection-nuget.yml +++ b/.github/workflows/check-dataverseconnection-nuget.yml @@ -38,4 +38,4 @@ jobs: - name: Run DataverseWhoAmI run: dotnet run --project DataverseWhoAmI/DataverseWhoAmI.csproj env: - DATAVERSE_URL: ${{ secrets.DATAVERSE_URL }} + DataverseUrl: ${{ secrets.DATAVERSE_URL }} diff --git a/DataverseConnection.Tests/DataverseOptionsBinderTests.cs b/DataverseConnection.Tests/DataverseOptionsBinderTests.cs index 9afa6d5..4360e7c 100644 --- a/DataverseConnection.Tests/DataverseOptionsBinderTests.cs +++ b/DataverseConnection.Tests/DataverseOptionsBinderTests.cs @@ -17,37 +17,60 @@ private static IConfiguration Config(params (string Key, string Value)[] values) .Build(); } - [Fact] - public void Bind_ReadsUrlFromConfiguration() + [Theory] + [InlineData("DataverseUrl")] + [InlineData("DATAVERSE_URL")] + public void Bind_ReadsUrlFromConfiguration(string key) { var options = new DataverseOptions(); - DataverseOptionsBinder.Bind(options, Config(("DATAVERSE_URL", "https://org.crm4.dynamics.com"))); + DataverseOptionsBinder.Bind(options, Config((key, "https://org.crm4.dynamics.com"))); Assert.Equal("https://org.crm4.dynamics.com", options.DataverseUrl); } [Theory] - [InlineData("azcli", DataverseCredentialType.AzureCliCredential)] - [InlineData("devicecode", DataverseCredentialType.DeviceCodeCredential)] - [InlineData("browser", DataverseCredentialType.InteractiveBrowserCredential)] - [InlineData("AZCLI", DataverseCredentialType.AzureCliCredential)] - [InlineData(" DeviceCode ", DataverseCredentialType.DeviceCodeCredential)] - public void Bind_ParsesCredentialType_CaseInsensitively(string configured, DataverseCredentialType expected) + [InlineData("DataverseCredentialType", "azcli", DataverseCredentialType.AzureCliCredential)] + [InlineData("DataverseCredentialType", "devicecode", DataverseCredentialType.DeviceCodeCredential)] + [InlineData("DataverseCredentialType", "browser", DataverseCredentialType.InteractiveBrowserCredential)] + [InlineData("DataverseCredentialType", "AZCLI", DataverseCredentialType.AzureCliCredential)] + [InlineData("DataverseCredentialType", " DeviceCode ", DataverseCredentialType.DeviceCodeCredential)] + [InlineData("DATAVERSE_CREDENTIAL_TYPE", "azcli", DataverseCredentialType.AzureCliCredential)] + [InlineData("DATAVERSE_CREDENTIAL_TYPE", "devicecode", DataverseCredentialType.DeviceCodeCredential)] + [InlineData("DATAVERSE_CREDENTIAL_TYPE", "browser", DataverseCredentialType.InteractiveBrowserCredential)] + public void Bind_ParsesCredentialType_CaseInsensitively( + string key, + string configured, + DataverseCredentialType expected) { var options = new DataverseOptions(); - DataverseOptionsBinder.Bind(options, Config(("DATAVERSE_CREDENTIAL_TYPE", configured))); + DataverseOptionsBinder.Bind(options, Config((key, configured))); Assert.Equal(expected, options.CredentialType); } + [Fact] + public void Bind_PrefersPascalCaseKeys_WhenBothFormsArePresent() + { + var options = new DataverseOptions(); + + DataverseOptionsBinder.Bind(options, Config( + ("DataverseUrl", "https://pascal.crm4.dynamics.com"), + ("DATAVERSE_URL", "https://uppercase.crm4.dynamics.com"), + ("DataverseCredentialType", "azcli"), + ("DATAVERSE_CREDENTIAL_TYPE", "devicecode"))); + + Assert.Equal("https://pascal.crm4.dynamics.com", options.DataverseUrl); + Assert.Equal(DataverseCredentialType.AzureCliCredential, options.CredentialType); + } + [Fact] public void Bind_DoesNotOverwriteExplicitUrl() { var options = new DataverseOptions { DataverseUrl = "https://explicit.crm4.dynamics.com" }; - DataverseOptionsBinder.Bind(options, Config(("DATAVERSE_URL", "https://config.crm4.dynamics.com"))); + DataverseOptionsBinder.Bind(options, Config(("DataverseUrl", "https://config.crm4.dynamics.com"))); Assert.Equal("https://explicit.crm4.dynamics.com", options.DataverseUrl); } @@ -62,12 +85,14 @@ public void Bind_LeavesDefaultCredentialType_WhenKeyAbsent() Assert.Equal(DataverseCredentialType.InteractiveBrowserCredential, options.CredentialType); } - [Fact] - public void Bind_ThrowsForUnknownCredentialType() + [Theory] + [InlineData("DataverseCredentialType")] + [InlineData("DATAVERSE_CREDENTIAL_TYPE")] + public void Bind_ThrowsForUnknownCredentialType(string key) { var options = new DataverseOptions(); Assert.Throws( - () => DataverseOptionsBinder.Bind(options, Config(("DATAVERSE_CREDENTIAL_TYPE", "bogus")))); + () => DataverseOptionsBinder.Bind(options, Config((key, "bogus")))); } } diff --git a/DataverseConnection/Internal/DataverseOptionsBinder.cs b/DataverseConnection/Internal/DataverseOptionsBinder.cs index e4207ae..9506f91 100644 --- a/DataverseConnection/Internal/DataverseOptionsBinder.cs +++ b/DataverseConnection/Internal/DataverseOptionsBinder.cs @@ -10,9 +10,15 @@ namespace DataverseConnection.Internal /// internal static class DataverseOptionsBinder { + internal const string DataverseUrlKey = "DataverseUrl"; + internal const string LegacyDataverseUrlKey = "DATAVERSE_URL"; + internal const string DataverseCredentialTypeKey = "DataverseCredentialType"; + internal const string LegacyDataverseCredentialTypeKey = "DATAVERSE_CREDENTIAL_TYPE"; + /// - /// Reads the flat configuration keys DATAVERSE_URL and - /// DATAVERSE_CREDENTIAL_TYPE and applies them to . + /// Reads the flat configuration keys DataverseUrl and + /// DataverseCredentialType, with support for their legacy uppercase forms, + /// and applies them to . /// public static void Bind(DataverseOptions options, IConfiguration configuration) { @@ -21,12 +27,15 @@ public static void Bind(DataverseOptions options, IConfiguration configuration) if (string.IsNullOrWhiteSpace(options.DataverseUrl)) { - var url = configuration["DATAVERSE_URL"]; + var url = GetDataverseUrl(configuration); if (!string.IsNullOrWhiteSpace(url)) options.DataverseUrl = url; } - var credentialType = configuration["DATAVERSE_CREDENTIAL_TYPE"]; + var credentialType = GetFirstConfiguredValue( + configuration, + DataverseCredentialTypeKey, + LegacyDataverseCredentialTypeKey); if (!string.IsNullOrWhiteSpace(credentialType)) { options.CredentialType = credentialType.Trim().ToLowerInvariant() switch @@ -35,9 +44,23 @@ public static void Bind(DataverseOptions options, IConfiguration configuration) "devicecode" => DataverseCredentialType.DeviceCodeCredential, "azcli" => DataverseCredentialType.AzureCliCredential, _ => throw new ArgumentException( - $"Unknown DATAVERSE_CREDENTIAL_TYPE '{credentialType}'. Valid values: browser, devicecode, azcli.") + $"Unknown {DataverseCredentialTypeKey} '{credentialType}'. Valid values: browser, devicecode, azcli.") }; } } + + internal static string? GetDataverseUrl(IConfiguration configuration) => + GetFirstConfiguredValue(configuration, DataverseUrlKey, LegacyDataverseUrlKey); + + private static string? GetFirstConfiguredValue( + IConfiguration configuration, + string pascalCaseKey, + string legacyUppercaseKey) + { + var value = configuration[pascalCaseKey]; + return !string.IsNullOrWhiteSpace(value) + ? value + : configuration[legacyUppercaseKey]; + } } } diff --git a/DataverseConnection/Internal/ServiceClientBuilder.cs b/DataverseConnection/Internal/ServiceClientBuilder.cs index 4a73431..3a32628 100644 --- a/DataverseConnection/Internal/ServiceClientBuilder.cs +++ b/DataverseConnection/Internal/ServiceClientBuilder.cs @@ -26,11 +26,13 @@ public static ServiceClient Build( string? dataverseUrl = options.DataverseUrl; if (string.IsNullOrWhiteSpace(dataverseUrl)) { - dataverseUrl = configuration?["DATAVERSE_URL"]; + dataverseUrl = configuration is null + ? null + : DataverseOptionsBinder.GetDataverseUrl(configuration); } if (string.IsNullOrWhiteSpace(dataverseUrl)) - throw new InvalidOperationException("DataverseUrl must be provided via options or configuration (DATAVERSE_URL)."); + throw new InvalidOperationException("DataverseUrl must be provided via options or configuration (DataverseUrl or DATAVERSE_URL)."); var credentialCacheIdentity = CredentialCacheIdentities.GetValue( credential, diff --git a/DataverseWhoAmI/Program.cs b/DataverseWhoAmI/Program.cs index 24cf9d0..e79acee 100644 --- a/DataverseWhoAmI/Program.cs +++ b/DataverseWhoAmI/Program.cs @@ -20,7 +20,7 @@ static async Task Main(string[] args) .Build(); // Setup DI and register ServiceClient and interfaces. The library reads - // DATAVERSE_URL and DATAVERSE_CREDENTIAL_TYPE from configuration by default, + // DataverseUrl and DataverseCredentialType from configuration by default, // so no per-tool authentication code is required. var services = new ServiceCollection(); services.AddSingleton(configuration); diff --git a/README.md b/README.md index 17ed0ba..a70a6d0 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ var services = new ServiceCollection(); services.AddDataverse(options => { - // Optional when DATAVERSE_URL is available through IConfiguration. + // Optional when DataverseUrl is available through IConfiguration. options.DataverseUrl = "https://yourorg.crm4.dynamics.com"; }); ``` @@ -218,17 +218,17 @@ services.AddDataverseWithOrganizationServices(); services.AddDataverseFactory(); ``` -The library reads two flat keys (from `appsettings.json`, environment variables, or any other configuration source): +The library reads two flat PascalCase keys (from `appsettings.json`, environment variables, or any other configuration source). The legacy uppercase keys remain supported for backward compatibility: | Key | Required | Values | | --- | --- | --- | -| `DATAVERSE_URL` | Yes (unless set on `DataverseOptions.DataverseUrl`) | The environment URL, e.g. `https://yourorg.crm4.dynamics.com`. | -| `DATAVERSE_CREDENTIAL_TYPE` | No (defaults to `browser`) | `browser`, `devicecode`, or `azcli` (case-insensitive). | +| `DataverseUrl` (or legacy `DATAVERSE_URL`) | Yes (unless set on `DataverseOptions.DataverseUrl`) | The environment URL, e.g. `https://yourorg.crm4.dynamics.com`. | +| `DataverseCredentialType` (or legacy `DATAVERSE_CREDENTIAL_TYPE`) | No (defaults to `browser`) | `browser`, `devicecode`, or `azcli` (case-insensitive). | ```json { - "DATAVERSE_URL": "https://yourorg.crm4.dynamics.com", - "DATAVERSE_CREDENTIAL_TYPE": "browser" + "DataverseUrl": "https://yourorg.crm4.dynamics.com", + "DataverseCredentialType": "browser" } ``` @@ -240,7 +240,7 @@ The credential-type strings map to the [opinionated credential types](#selecting | `devicecode` | `DeviceCodeCredential` | | `azcli` | `AzureCliCredential` | -An unrecognized `DATAVERSE_CREDENTIAL_TYPE` throws at startup, listing the valid values. +An unrecognized `DataverseCredentialType` (or legacy `DATAVERSE_CREDENTIAL_TYPE`) throws at startup, listing the valid values. ### Overriding the defaults @@ -249,7 +249,7 @@ Values read from configuration are just the defaults. To do something specific ```csharp services.AddDataverseWithOrganizationServices(options => { - // Overrides DATAVERSE_CREDENTIAL_TYPE from configuration. + // Overrides DataverseCredentialType from configuration. options.CredentialType = DataverseCredentialType.AzureCliCredential; }); ```