Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Azure.ResourceManager.Resources;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Models.Command;

namespace Azure.Mcp.Core.Areas.Subscription.Commands;
Expand Down Expand Up @@ -65,7 +66,7 @@ internal static List<SubscriptionInfo> MapToSubscriptionInfos(List<SubscriptionD
s.DisplayName,
s.State?.ToString(),
s.TenantId?.ToString(),
hasDefault && s.SubscriptionId.Equals(defaultSubscriptionId, StringComparison.OrdinalIgnoreCase)
hasDefault && s.SubscriptionId.Equals(defaultSubscriptionId, StringComparisons.SubscriptionId)
)).ToList();

// Sort so the default subscription appears first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Azure.Mcp.Core.Services.Azure.Subscription;
using Azure.Mcp.Core.Services.Azure.Tenant;
using Azure.ResourceManager.Resources;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Models.Resource;
using Microsoft.Mcp.Core.Models.ResourceGroup;
using Microsoft.Mcp.Core.Options;
Expand Down Expand Up @@ -65,7 +66,7 @@ public async Task<List<ResourceGroupInfo>> GetResourceGroups(string subscription
var cachedResults = await _cacheService.GetAsync<List<ResourceGroupInfo>>(CacheGroup, cacheKey, s_cacheDuration, cancellationToken);
if (cachedResults != null)
{
return cachedResults.FirstOrDefault(rg => rg.Name.Equals(resourceGroupName, StringComparison.OrdinalIgnoreCase));
return cachedResults.FirstOrDefault(rg => rg.Name.Equals(resourceGroupName, StringComparisons.ResourceGroup));
}

var rg = await GetResourceGroupResource(subscription, resourceGroupName, tenant, retryPolicy, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public bool IsSubscriptionId(string subscription, string? tenant = null)
public async Task<string> GetSubscriptionIdByName(string subscriptionName, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default)
{
var subscriptions = await GetSubscriptions(tenant, retryPolicy, cancellationToken);
var subscription = subscriptions.FirstOrDefault(s => s.DisplayName.Equals(subscriptionName, StringComparison.OrdinalIgnoreCase)) ??
var subscription = subscriptions.FirstOrDefault(s => s.DisplayName.Equals(subscriptionName, StringComparisons.SubscriptionDisplayName)) ??
throw new KeyNotFoundException($"Could not find subscription with name {subscriptionName}");

return subscription.SubscriptionId;
Expand All @@ -103,7 +103,7 @@ public async Task<string> GetSubscriptionIdByName(string subscriptionName, strin
public async Task<string> GetSubscriptionNameById(string subscriptionId, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default)
{
var subscriptions = await GetSubscriptions(tenant, retryPolicy, cancellationToken);
var subscription = subscriptions.FirstOrDefault(s => s.SubscriptionId.Equals(subscriptionId, StringComparison.OrdinalIgnoreCase)) ??
var subscription = subscriptions.FirstOrDefault(s => s.SubscriptionId.Equals(subscriptionId, StringComparisons.SubscriptionId)) ??
throw new KeyNotFoundException($"Could not find subscription with ID {subscriptionId}");

return subscription.DisplayName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Azure.ResourceManager;
using Azure.ResourceManager.Resources;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Services.Azure.Authentication;
using Microsoft.Mcp.Core.Services.Caching;

Expand Down Expand Up @@ -107,7 +108,7 @@ public async Task<string> GetTenantIdByName(string tenantName, CancellationToken
public async Task<string> GetTenantNameById(string tenantId, CancellationToken cancellationToken)
{
var tenants = await GetTenants(cancellationToken);
var tenant = tenants.FirstOrDefault(t => t.Data.TenantId?.ToString().Equals(tenantId, StringComparison.OrdinalIgnoreCase) == true) ??
var tenant = tenants.FirstOrDefault(t => t.Data.TenantId?.ToString().Equals(tenantId, StringComparisons.TenantId) == true) ??
throw new KeyNotFoundException($"Could not find tenant with ID {tenantId}");

string? tenantName = tenant.Data.DisplayName ??
Expand Down
44 changes: 44 additions & 0 deletions core/Microsoft.Mcp.Core/src/Helpers/StringComparisons.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Mcp.Core.Helpers;

/// <summary>
/// Centralizes the <see cref="StringComparison"/> values to use when comparing
/// well-known string values, making the intended comparison semantics explicit
/// at each call site.
/// </summary>
public static class StringComparisons

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of this type is to make it clear at the call site (string.Equals(...), etc.) that the expected comparison is being used. Also, if we're using the wrong comparison we can change it here to update it for all consumers.

I didn't move all usages of StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase to this file in order to keep the size of the change manageable. More can be added in a future update.

{
/// <summary>
/// The comparison to use when comparing Azure tenant IDs. Tenant IDs are
/// GUIDs whose casing is not significant, so they are compared
/// case-insensitively.
/// </summary>
public static StringComparison TenantId => StringComparison.OrdinalIgnoreCase;

/// <summary>
/// The comparison to use when comparing Azure subscription IDs. Subscription
/// IDs are GUIDs whose casing is not significant, so they are compared
/// case-insensitively.
/// </summary>
public static StringComparison SubscriptionId => StringComparison.OrdinalIgnoreCase;

/// <summary>
/// The comparison to use when comparing Azure subscription display names,
/// which are matched case-insensitively.
/// </summary>
public static StringComparison SubscriptionDisplayName => StringComparison.OrdinalIgnoreCase;

/// <summary>
/// The comparison to use when comparing Azure resource group names, which
/// Azure Resource Manager treats case-insensitively.
/// </summary>
public static StringComparison ResourceGroup => StringComparison.OrdinalIgnoreCase;

/// <summary>
/// The comparison to use when matching an Azure resource by its name. Azure
/// resource names are matched case-insensitively for lookup purposes.
/// </summary>
public static StringComparison ResourceName => StringComparison.OrdinalIgnoreCase;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are Azure resources always a case-insensitive comparison? This is the only one that gives me concern as it's a broad category.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may be doing case insensitive lookup/queries but I don't think we can assume two names differ only in case refer to the same resource.

https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web;
using Microsoft.Mcp.Core.Helpers;

namespace Microsoft.Mcp.Core.Services.Azure.Authentication;

Expand Down Expand Up @@ -33,7 +34,7 @@ public Task<TokenCredential> GetTokenCredentialAsync(string? tenantId, Cancellat
if (tenantId is not null)
{
if (httpContext.User.FindFirst("tid")?.Value is string tidClaim
&& tidClaim != tenantId)
&& !string.Equals(tidClaim, tenantId, StringComparisons.TenantId))
{
_logger.LogWarning(
"The requested token tenant '{GetTokenTenant}' does not match the tenant of the authenticated user '{TidClaim}'. Going to throw.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Services.Azure.Authentication;

namespace Azure.Mcp.Tools.AppLens.Services;
Expand Down Expand Up @@ -107,7 +108,7 @@ internal async Task<FindResourceIdResult> FindResourceAsync(
if (!string.IsNullOrEmpty(resourceGroup))
{
var rgFiltered = filteredResults
.Where(r => r.ResourceGroup.Equals(resourceGroup, StringComparison.OrdinalIgnoreCase))
.Where(r => r.ResourceGroup.Equals(resourceGroup, StringComparisons.ResourceGroup))
.ToImmutableArray();

if (rgFiltered.Length == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Azure.ResourceManager.RecoveryServicesBackup.Models;
using Azure.ResourceManager.Resources;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Options;

using SdkBackupStatusResult = Azure.ResourceManager.RecoveryServicesBackup.Models.BackupStatusResult;
Expand Down Expand Up @@ -135,7 +136,7 @@ public async Task<List<BackupVaultInfo>> ListVaultsAsync(
List<BackupVaultInfo> FilterByResourceGroup(List<BackupVaultInfo> vaults) =>
string.IsNullOrEmpty(resourceGroup)
? vaults
: vaults.Where(v => string.Equals(v.ResourceGroup, resourceGroup, StringComparison.OrdinalIgnoreCase)).ToList();
: vaults.Where(v => string.Equals(v.ResourceGroup, resourceGroup, StringComparisons.ResourceGroup)).ToList();

if (VaultTypeResolver.IsRsv(vaultType))
{
Expand Down Expand Up @@ -658,7 +659,7 @@ public async Task<List<UnprotectedResourceInfo>> FindUnprotectedResourcesAsync(

// Apply optional resource group filter
if (!string.IsNullOrEmpty(resourceGroup) &&
!string.Equals(resource.Id?.ResourceGroupName, resourceGroup, StringComparison.OrdinalIgnoreCase))
!string.Equals(resource.Id?.ResourceGroupName, resourceGroup, StringComparisons.ResourceGroup))
{
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.Text.Json;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Tests;
using Microsoft.Mcp.Tests.Client;
using Microsoft.Mcp.Tests.Client.Helpers;
Expand All @@ -17,7 +18,7 @@ public class AzureIsvCommandTests(ITestOutputHelper output, TestProxyFixture fix
public async Task Should_list_datadog_monitored_resources()
{
// Skipping test if Tenant is not 'Customer LED Tenant'
if (Settings.TenantId != "888d76fa-54b2-4ced-8ee5-aac1585adee7" && Settings.TestMode != TestMode.Playback)
if (!string.Equals(Settings.TenantId, "888d76fa-54b2-4ced-8ee5-aac1585adee7", StringComparisons.TenantId) && Settings.TestMode != TestMode.Playback)
{
Assert.Skip("Test skipped because Tenant is not 'Customer LED Tenant'.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Azure.Mcp.Tools.Compute.Services;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Extensions;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Models.Command;
using Microsoft.Mcp.Core.Models.Option;

Expand Down Expand Up @@ -132,7 +133,7 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
cancellationToken);

var matchingDisks = disks
.Where(d => string.Equals(d.Name, options.Disk, StringComparison.OrdinalIgnoreCase))
.Where(d => string.Equals(d.Name, options.Disk, StringComparisons.ResourceName))
.ToList();

if (matchingDisks.Count == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ private async Task<CosmosDBAccountResource> GetCosmosAccountAsync(
await foreach (var account in subscriptionResource.GetCosmosDBAccountsAsync(cancellationToken))
{
// Cosmos DB account names are case-insensitive in Azure, so compare accordingly.
if (account.Data.Name.Equals(accountName, StringComparison.OrdinalIgnoreCase))
if (account.Data.Name.Equals(accountName, StringComparisons.ResourceName))
{
return account;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Azure.ResourceManager.EventGrid;
using Azure.ResourceManager.EventGrid.Models;
using Azure.ResourceManager.Resources;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Options;

namespace Azure.Mcp.Tools.EventGrid.Services;
Expand Down Expand Up @@ -374,7 +375,7 @@ private async Task GetSubscriptionsFromAllTopics(

await foreach (var topic in resourceGroupResource.Value.GetEventGridTopics().GetAllAsync(cancellationToken: cancellationToken))
{
if (topic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase))
if (topic.Data.Name.Equals(topicName, StringComparisons.ResourceName))
{
return topic;
}
Expand All @@ -385,7 +386,7 @@ private async Task GetSubscriptionsFromAllTopics(
// Search in all resource groups
await foreach (var topic in subscriptionResource.GetEventGridTopicsAsync(cancellationToken: cancellationToken))
{
if (topic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase))
if (topic.Data.Name.Equals(topicName, StringComparisons.ResourceName))
{
return topic;
}
Expand All @@ -408,7 +409,7 @@ private async Task GetSubscriptionsFromAllTopics(

await foreach (var systemTopic in resourceGroupResource.Value.GetSystemTopics().GetAllAsync(cancellationToken: cancellationToken))
{
if (systemTopic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase))
if (systemTopic.Data.Name.Equals(topicName, StringComparisons.ResourceName))
{
return systemTopic;
}
Expand All @@ -419,7 +420,7 @@ private async Task GetSubscriptionsFromAllTopics(
// Search in all resource groups
await foreach (var systemTopic in subscriptionResource.GetSystemTopicsAsync(cancellationToken: cancellationToken))
{
if (systemTopic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase))
if (systemTopic.Data.Name.Equals(topicName, StringComparisons.ResourceName))
{
return systemTopic;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Net;
using Azure.ResourceManager.FileShares;
using Azure.ResourceManager.Resources;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Options;

namespace Azure.Mcp.Tools.FileShares.Services;
Expand Down Expand Up @@ -435,7 +436,7 @@ public async Task<FileShareSnapshotInfo> GetSnapshotAsync(

await foreach (var snapshotResource in snapshotCollection.WithCancellation(cancellationToken))
{
if (snapshotResource.Data.Name.Equals(snapshotId, StringComparison.OrdinalIgnoreCase) ||
if (snapshotResource.Data.Name.Equals(snapshotId, StringComparisons.ResourceName) ||
snapshotResource.Data.Id.ToString().Equals(snapshotId, StringComparison.OrdinalIgnoreCase))
{
return FileShareSnapshotInfo.FromResource(snapshotResource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public async Task<KnowledgeIndexSchema> GetKnowledgeIndexSchema(

// Find the index by name using async enumerable
var index = await indexesClient.GetIndicesAsync(cancellationToken: cancellationToken)
.Where(i => string.Equals(i.Name, indexName, StringComparison.OrdinalIgnoreCase))
.Where(i => string.Equals(i.Name, indexName, StringComparisons.ResourceName))
.FirstOrDefaultAsync(cancellationToken: cancellationToken);

if (index == null)
Expand Down Expand Up @@ -648,7 +648,7 @@ public async Task<List<AiResourceInformation>> ListAiResourcesAsync(
{
var resourceInfo = await BuildResourceInformation(account, subscriptionResource.Data.DisplayName, cancellationToken);
resources.Add(resourceInfo);
if (account.Data.Id.ResourceGroupName?.Equals(resourceGroup, StringComparison.OrdinalIgnoreCase) == true)
if (account.Data.Id.ResourceGroupName?.Equals(resourceGroup, StringComparisons.ResourceGroup) == true)
{
var retrieved = await BuildResourceInformation(account, subscriptionResource.Data.DisplayName, cancellationToken);
resources.Add(retrieved);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ private async Task<ActivityLogListResponse> MakeActivityLogRequestAsync(string u
// Find the workspace
var matchingWorkspace = workspaces.FirstOrDefault(w =>
isId ? w.CustomerId.Equals(workspace, StringComparison.OrdinalIgnoreCase)
: w.Name.Equals(workspace, StringComparison.OrdinalIgnoreCase));
: w.Name.Equals(workspace, StringComparisons.ResourceName));

if (matchingWorkspace == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Core.Services.Azure.Subscription;
using Azure.Mcp.Core.Services.Azure.Tenant;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Options;

namespace Azure.Mcp.Tools.Monitor.Services;
Expand Down Expand Up @@ -42,7 +43,7 @@ public async Task<ResourceIdentifier> ResolveResourceIdAsync(

// Get all resources matching the name
var allMatchingResources = await subscriptionResource.GetGenericResourcesAsync(cancellationToken: cancellationToken)
.Where(r => r.Data.Name?.Equals(resourceName, StringComparison.OrdinalIgnoreCase) == true)
.Where(r => r.Data.Name?.Equals(resourceName, StringComparisons.ResourceName) == true)
.ToListAsync(cancellationToken: cancellationToken);

if (allMatchingResources.Count == 0)
Expand All @@ -57,7 +58,7 @@ public async Task<ResourceIdentifier> ResolveResourceIdAsync(
if (!string.IsNullOrEmpty(resourceGroup))
{
filteredResources = filteredResources.Where(r =>
r.Data.Id?.ResourceGroupName?.Equals(resourceGroup, StringComparison.OrdinalIgnoreCase) == true);
r.Data.Id?.ResourceGroupName?.Equals(resourceGroup, StringComparisons.ResourceGroup) == true);
}

// Filter by resource type if provided
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Azure.Search.Documents.KnowledgeBases.Models;
using Azure.Search.Documents.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Helpers;
using Microsoft.Mcp.Core.Options;
using Microsoft.Mcp.Core.Services.Azure.Authentication;
using Microsoft.Mcp.Core.Services.Caching;
Expand Down Expand Up @@ -215,7 +216,7 @@ public async Task<List<KnowledgeBaseInfo>> ListKnowledgeBases(
var result = await searchClient.GetKnowledgeBaseAsync(knowledgeBaseName, cancellationToken: cancellationToken);
if (result?.Value != null)
{
if (result.Value.Name.Equals(knowledgeBaseName, StringComparison.OrdinalIgnoreCase))
if (result.Value.Name.Equals(knowledgeBaseName, StringComparisons.ResourceName))
{
bases.Add(new(result.Value.Name, result.Value.Description, [.. result.Value.KnowledgeSources.Select(ks => ks.Name)]));
}
Expand Down
Loading