All articles

Azure for .NET Developers — Part 1: Subscriptions, Tenants, and Entra ID Explained

The Azure mental model for C# developers: tenant vs subscription vs resource group, what app registrations really are, and how DefaultAzureCredential kills hardcoded secrets.

0 · log in to like, save & follow Share on LinkedIn Share on X
Azure for .NET Developers — Part 1: Subscriptions, Tenants, and Entra ID Explained

Most .NET developers meet Azure for the first time under deadline pressure: someone says "just deploy it to Azure," and suddenly we are staring at tenants, subscriptions, app registrations, and four different IDs that all look like GUIDs. None of it is hard, but the vocabulary is unfamiliar, and the portal does a poor job of explaining how the pieces relate. Before we deploy anything, it pays to spend twenty minutes building the right mental model — everything else in this series gets easier once we have it.

The Azure hierarchy, mapped to .NET concepts

Azure organizes everything into a strict containment hierarchy: a tenant contains subscriptions, a subscription contains resource groups, and a resource group contains resources. If we squint, it maps surprisingly well onto concepts we already know:

Azure concept Rough .NET analogy What it actually is
Tenant Your organization's GitHub org An instance of Microsoft Entra ID: one directory of users, groups, and app identities
Subscription A solution (.sln) A billing and access boundary; the invoice arrives per subscription
Resource group A project (.csproj) A logical folder of resources that share a lifecycle — deploy together, delete together
Resource A NuGet package reference A single service instance: a web app, a database, a storage account

The analogy breaks down if pushed too far, but the lifecycle intuition holds: just as we would not scatter one feature's classes across five projects, we should not scatter one application's resources across five resource groups. A good default is one resource group per application per environment — rg-myapp-dev, rg-myapp-prod. Deleting a resource group deletes everything inside it, which makes cleanup after experiments a one-liner instead of a scavenger hunt.

Tenants and subscriptions cause the most confusion because a personal Microsoft account gets one of each and we never notice the distinction. In a company, though, there is typically one tenant and many subscriptions — one per department, per environment, or per cost center. Identity lives at the tenant level; money and resources live at the subscription level.

What is the difference between a tenant and a subscription in Azure?

A tenant is an identity boundary: it is a dedicated instance of Microsoft Entra ID that holds your organization's users, groups, and application identities. A subscription is a billing and resource boundary: every Azure resource lives in exactly one subscription, and that subscription determines who pays for it. One tenant can contain many subscriptions, but each subscription trusts exactly one tenant for authentication.

Entra ID and app registrations, minus the ceremony

Microsoft Entra ID is the new name for Azure Active Directory — the docs renamed it in 2023, and half the internet still says "Azure AD," so treat the terms as synonyms. It is not on-premises Active Directory; there are no domain controllers or group policies. It is an identity provider: it issues OAuth 2.0 / OpenID Connect tokens, and Azure services accept those tokens.

An app registration is how we tell Entra ID "this application exists and is allowed to request tokens." When we register an app, Entra ID assigns it an application (client) ID, and we can then create credentials for it — a client secret or, better, a certificate.

When do we actually need one?

  • We need an app registration when our code runs outside Azure and must authenticate as itself: a CI pipeline pushing to a container registry, an on-premises service reading Key Vault, a desktop tool calling Microsoft Graph. We also need one when our web app signs users in with Entra ID.
  • We do not need one when our code runs inside Azure. App Service, Container Apps, Functions, and VMs all support managed identities — Azure creates and rotates the credential for us, and there is no secret to leak. If we find ourselves pasting a client secret into an App Service setting, we should almost certainly be using a managed identity instead.

That second point is worth internalizing early. The single most common Azure anti-pattern among .NET teams is minting client secrets for services that could have used managed identities from day one.

Finding your IDs with the az CLI

Every Azure SDK call eventually needs some subset of four values: tenant ID, subscription ID, and — only for app registrations — client ID and client secret. The portal shows them (Entra ID → Overview for the tenant; Subscriptions blade for the subscription), but the CLI is faster and scriptable:

# Sign in; opens a browser for interactive auth
az login

# Tenant and subscription for the current context
az account show --query "{tenantId:tenantId, subscriptionId:id, name:name}" -o table

# List all subscriptions you can see, and switch if needed
az account list -o table
az account set --subscription "My Dev Subscription"

If we genuinely need an app registration — say, for a non-Azure CI runner — one command creates the registration, the secret, and a role assignment in one go:

az ad sp create-for-rbac \
  --name "myapp-ci" \
  --role Contributor \
  --scopes /subscriptions/<subscription-id>/resourceGroups/rg-myapp-dev

The output includes appId (the client ID), password (the client secret, shown exactly once), and tenant. Scope the role as narrowly as possible — a specific resource group, not the whole subscription — and set a secret expiry. Note that for GitHub Actions specifically, federated credentials (OpenID Connect) have made even this secret unnecessary; we will use that in part 2.

DefaultAzureCredential: why hardcoded secrets are obsolete

Older Azure code was full of connection strings and account keys pasted into config files. The modern Azure SDK replaces all of that with one type from the Azure.Identity package: DefaultAzureCredential. It tries a chain of authentication sources in order — environment variables, workload identity, managed identity, Visual Studio, the Azure CLI, and a few others — and uses the first one that works.

The practical consequence is that the same line of code authenticates everywhere with zero configuration changes:

  • On our laptop, it picks up our az login session.
  • In GitHub Actions with OIDC, it picks up the workload identity federation.
  • Deployed to App Service, it picks up the managed identity.

No secrets in appsettings.json, no secrets in environment variables, nothing to rotate or accidentally commit. Here is a complete .NET 10 console program that lists the resource groups in our subscription using Azure.Identity and Azure.ResourceManager (the ARM management SDK):

using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.Resources;

var credential = new DefaultAzureCredential();
var armClient = new ArmClient(credential);

SubscriptionResource subscription =
    await armClient.GetDefaultSubscriptionAsync();

Console.WriteLine($"Subscription: {subscription.Data.DisplayName}");

await foreach (ResourceGroupResource rg in
    subscription.GetResourceGroups().GetAllAsync())
{
    Console.WriteLine($"  {rg.Data.Name,-30} {rg.Data.Location}");
}

Two NuGet packages make this work: Azure.Identity and Azure.ResourceManager. Run it after az login and it just works — no IDs pasted anywhere, because the credential chain resolved our CLI session and GetDefaultSubscriptionAsync used the CLI's active subscription. The full project is in the 01-fundamentals folder of the companion repository.

When we do need to pin a specific tenant or subscription — common when we belong to several — we pass options rather than switching accounts:

var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
{
    TenantId = "00000000-0000-0000-0000-000000000000"
});

A few habits worth forming now

Three practices cost nothing today and save real pain later. First, name resources with a consistent convention from the start (rg-, app-, sql- prefixes plus app and environment); renaming resources later ranges from annoying to impossible. Second, tag resource groups with an owner and environment — six months from now, az group list will show mystery groups, and tags are how we avoid the "can we delete this?" Slack thread. Third, treat client secrets as a last resort: managed identity inside Azure, workload identity federation for CI, and az login locally cover almost every scenario without a single stored secret.

What's next

With the hierarchy, identity model, and DefaultAzureCredential in place, we can deploy something real. In part 2 we take an ASP.NET Core application to Azure App Service the right way: provisioning with the az CLI, deploying via GitHub Actions, wiring configuration and Key Vault references, and using deployment slots for zero-downtime releases.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.