Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/check-dataverseconnection-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
53 changes: 39 additions & 14 deletions DataverseConnection.Tests/DataverseOptionsBinderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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<ArgumentException>(
() => DataverseOptionsBinder.Bind(options, Config(("DATAVERSE_CREDENTIAL_TYPE", "bogus"))));
() => DataverseOptionsBinder.Bind(options, Config((key, "bogus"))));
}
}
33 changes: 28 additions & 5 deletions DataverseConnection/Internal/DataverseOptionsBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ namespace DataverseConnection.Internal
/// </summary>
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";

/// <summary>
/// Reads the flat configuration keys <c>DATAVERSE_URL</c> and
/// <c>DATAVERSE_CREDENTIAL_TYPE</c> and applies them to <paramref name="options"/>.
/// Reads the flat configuration keys <c>DataverseUrl</c> and
/// <c>DataverseCredentialType</c>, with support for their legacy uppercase forms,
/// and applies them to <paramref name="options"/>.
/// </summary>
public static void Bind(DataverseOptions options, IConfiguration configuration)
{
Expand All @@ -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
Expand All @@ -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];
}
}
}
6 changes: 4 additions & 2 deletions DataverseConnection/Internal/ServiceClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion DataverseWhoAmI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ static async Task<int> 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<IConfiguration>(configuration);
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";
});
```
Expand Down Expand Up @@ -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"
}
```

Expand All @@ -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

Expand All @@ -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;
});
```
Expand Down
Loading