October 15th, 2021

Introducing the new Azure SDK Resource Management Libraries for .NET

Michael Nash
Principal Software Engineer

NOTE: Some code patterns covered in this blog post have evolved since the time of writing. For the latest guidance, see Resource management using the Azure SDK for .NET.

We’re excited to announce the preview release for .NET Azure.ResourceManager, which is the new base library for all management plane SDKs. Along with the base library, we’re also releasing preview versions for Compute, Network, Keyvault, Resources, and Storage management plane. Each of these SDKs follows the new Azure SDK guidelines. This post will highlight a few new features of the libraries. We encourage you to try out the libraries and provide feedback before they go GA!

Key concepts

Reducing Redundant Parameters

The previous .NET SDK generated all methods for a given resource on a flat client structure. The result was that all method calls required you to pass in the scope parameters similar to a set of static methods and classes. As an example nearly all methods on the old VirtualMachinesOperations object required you to pass in the resource group name and the virtual machine name.

Old

string subscriptionGuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
string resourceGroupName = "myRg";
string vmName = "myVm";
ComputeManagementClient computeClient = new ComputeManagementClient(subscriptionGuid, new DefaultAzureCredential());
await computeClient.VirtualMachines.StartPowerOff(resourceGroupName, vmName).WaitForCompletionAsync();
//do some stuff
await computeClient.VirtualMachines.StartPowerOn(resourceGroupName, vmName).WaitForCompletionAsync();

The new resource client classes solve this by taking in the context as a ResourceIdentifier, which eliminates the need to pass in scope parameters.

New

ResourceIdentifier vmId = new ResourceIdentifier("/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/myRg/providers/Microsoft.Compute/virtualMachines/myVm");
ArmClient armClient = new ArmClient(new DefaultAzureCredential());
VirtualMachine vm = armClient.GetVirtualMachine(vmId);
await vm.StartPowerOff().WaitForCompletionAsync();
//do some stuff
await vm.StartPowerOn().WaitForCompletionAsync();

Another issue was that each method you called on the VirtualMachinesOperations class is potentially operating at a different level such as ListAll() lists all VirtualMachines in the subscription used to construct the compute client. Whereas List() takes in a resource group name and might fail if you pass in a resource group that doesn’t belong to the subscription you used to construct the compute client. To illustrate what this looks like let’s take an example of shutting down all virtual machines at a subscription level and a resource group level.

Old

string subscriptionGuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
string resourceGroupName = "myRg";
string vmName = "myVm";
ComputeManagementClient computeClient = new ComputeManagementClient(subscriptionGuid, new DefaultAzureCredential());

//shutting down all VMs in the subscription
foreach(VirtualMachine vm in computeClient.VirtualMachines.ListAll())
{
    //we need to parse out the resource group from the id since each vm can be in a different resource group
    //we are doing that with a helper method GetResourceGroupFromId
    string vmResourceGroupName = GetResourceGroupFromId(vm.Id);
    await computeClient.VirtualMachines.StartPowerOff(vmResourceGroupName, vm.Name).WaitForCompletionAsync();
}

//shutting down all VMs in a resource group
foreach(VirtualMachine vm in computeClient.VirtualMachines.List(resourceGroupName))
{
    //in this case we can use the constant resource group name since we used List vs ListAll
    await computeClient.VirtualMachines.StartPowerOff(resourceGroupName, vm.Name).WaitForCompletionAsync();
}

In the new design, the methods that operate at different levels are now moved to those contexts such that ListAll() becomes a method on a Subscription object called GetAllVirtualMachines(). List() becomes a method on a ResourceGroup object called GetVirtualMachines().GetAll() and no longer has the issue of accidentally passing in mismatched resource group and subscription.

New

ResourceIdentifier vmId = new ResourceIdentifier("/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/myRg/providers/Microsoft.Compute/virtualMachines/myVm");
//The resource identifier class allows us to get the id of our parents all the way up the chain
ResourceIdentifier rgId = vmId.Parent;
ResourceIdentifier subscriptionId = rgId.Parent;
ArmClient armClient = new ArmClient(new DefaultAzureCredential());

//shutting down all VMs in the subscription
Subscription subscription = armClient.GetSubscription(subscriptionId);
foreach(VirtualMachine vm in subscription.GetAllVirtualMachines())
{
    //The virtual machine is now a resource client so no scope parameters needed
    await vm.StartPowerOff().WaitForCompletionAsync();
}

//shutting down all VMs in a resource group
ResourceGroup rg = armClient.GetResourceGroup(rgId);
foreach(VirtualMachine vm in rg.GetVirtualMachines().GetAll())
{
    //The virtual machine is now a resource client so no scope parameters needed
    await vm.StartPowerOff().WaitForCompletionAsync();    
}

Reducing Number of Clients

Let’s expand the example above and see what it looks like to shut down all VMs across all subscriptions. In the old design, we needed a new client ResourcesManagementClient since that is the client that will allow us to loop through all subscriptions.

Old

string subscriptionGuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
string resourceGroupName = "myRg";
string vmName = "myVm";
//the resources client requires a subscription id even though not all methods on the client will use it which can be confusing
ResourcesManagementClient resourcesClient = new ResourcesManagementClient(subscriptionGuid, new DefaultAzureCredential());

//this list call ignores the subscription id used to construct the client
foreach(Subscription sub in resourcesClient.Subscriptions.List())
{
    //need to construct a new client for each subscription returned
    ComputeManagementClient computeClient = new ComputeManagementClient(sub.SubscriptionId, new DefaultAzureCredential());
    foreach(VirtualMachine vm in computeClient.VirtualMachines.ListAll())
    {
        //we need to parse out the resource group from the id since each vm can be in a different resource group
        //we are doing that with a helper method GetResourceGroupFromId
        string vmResourceGroupName = GetResourceGroupFromId(vm.Id);
        await computeClient.VirtualMachines.StartPowerOff(vmResourceGroupName, vm.Name).WaitForCompletionAsync();
    }
}

The new design allows the context to cascade down from all levels removing the need to construct multiple clients and pass context between them.

New

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

foreach(Subscription sub in armClient.GetSubscriptions().GetAll())
{
    //The subscription is now a resource client so we can immediately access the methods and no scope parameters needed
    foreach(VirtualMachine vm in sub.GetAllVirtualMachines())
    {
        //The virtual machine is now a resource client so no scope parameters needed
        await vm.StartPowerOff().WaitForCompletionAsync();
    }
}

To accomplish these changes, we’re introducing three standard types for all resources in Azure:

[Resource].cs

This class represents a full resource client that contains a Data property exposing the details as a [Resource]Data type. It also has access to all the operations on that resource without needing to pass in scope parameters. Having access to these methods makes it convenient to directly execute operations on the result of list calls since everything is returned as a full resource.

[Resource]Data.cs

This class represents the model that makes up a given resource. Typically, this data is what gets returned from a service call such as HTTP GET and provides details about the underlying resource. Previously, this data was represented by a Model class.

[Resource]Collection.cs

This class represents the operations you can do on a collection of resources belonging to a specific parent resource. This object provides most of the logical collection operations.

Collection Behavior Collection Method
Iterate/List GetAll()
Index Get(string name)
Add CreateOrUpdate(string name, [Resource]Data data)
Contains CheckIfExists(string name)
TryGet GetIfExists(string name)

For most things, the parent will be a ResourceGroup. However, each parent / child relationship is represented this way. For example, a Subnet is a child of a VirtualNetwork and a ResourceGroup is a child of a Subscription.

Putting it all together

Imagine that our company requires all virtual machines to be tagged with the owner. We’re tasked with writing a program to add the tag to any missing virtual machines in a given resource group.

// First we construct our armClient
var armClient = new ArmClient(new DefaultAzureCredential());

// Next we get a resource group object
// ResourceGroup is a [Resource] object from above
Subscription subscription = await armClient.GetDefaultSubscriptionAsync();
ResourceGroup resourceGroup = await subscription.GetResourceGroups().GetAsync("myRgName");

// Next we get the collection for the virtual machines
// vmCollection is a [Resource]Collection object from above
VirtualMachineCollection vmCollection = resourceGroup.GetVirtualMachines();

// Next we loop over all VMs in the collection
// Each vm is a [Resource] object from above
await foreach(VirtualMachine vm in vmCollection.GetAllAsync())
{
    // We access the [Resource]Data properties from vm.Data
    if(!vm.Data.Tags.ContainsKey("owner"))
    {
        // We can also access all operations from vm since it is already scoped for us
        await vm.AddTagAsync("owner", GetOwner());
    }
}

Structured Resource Identifier

Resource IDs contain useful information about the resource itself, but they’re plain strings that must be parsed. Instead of implementing your own parsing logic, you can use a ResourceIdentifier object that will do the parsing for you: new ResourceIdentifer("myid");.

Example: Parsing an ID using a ResourceIdentifier object

string resourceId = "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/workshop2021-rg/providers/Microsoft.Network/virtualNetworks/myVnet/subnets/mySubnet";
ResourceIdentifier id = new ResourceIdentifier(resourceId);
Console.WriteLine($"Subscription: {id.SubscriptionId}");
Console.WriteLine($"ResourceGroup: {id.ResourceGroupName}");
Console.WriteLine($"Vnet: {id.Parent.Name}");
Console.WriteLine($"Subnet: {id.Name}");

However, keep in mind that some of those properties could be null. You can usually tell by the ID string itself what type of resource an ID represents, but if you’re unsure you can check if the properties are null or use the Try methods to retrieve the values as is shown below:

Example: ResourceIdentifier TryGet methods

string resourceId = "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/workshop2021-rg/providers/Microsoft.Network/virtualNetworks/myVnet/subnets/mySubnet";
// Assume we don't know what type of resource id we have we can cast to the base type
ResourceIdentifier id = new ResourceIdentifier(resourceId);
string property;
if (id.TryGetSubscriptionId(out property))
    Console.WriteLine($"Subscription: {property}");
if (id.TryGetResourceGroupName(out property))
    Console.WriteLine($"ResourceGroup: {property}");
// Parent is only null when we reach the top of the chain which is a Tenant
Console.WriteLine($"Vnet: {id.Parent.Name}");
// Name will never be null
Console.WriteLine($"Subnet: {id.Name}");

Managing Existing Resources By ID

Executing operations on resources that already exist is a common use case when using the management client libraries. In this scenario, you usually have the identifier of the resource you want to work on as a string. Although the new object hierarchy is great for provisioning and working within the scope of a given parent, it can lead to extra network calls if used incorrectly.

An example of how you would access an AvailabilitySet object and manage it directly with its ID is:

using Azure.Identity;
using Azure.ResourceManager;
using Azure.ResourceManager.Resources;
using Azure.ResourceManager.Compute;
using System;
using System.Threading.Tasks;

// Code omitted for brevity

string resourceId = "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/workshop2021-rg/providers/Microsoft.Compute/availabilitySets/ws2021availSet";
ResourceIdentifier id = new ResourceIdentifier(resourceId);
// We construct a new armClient to work with
ArmClient armClient = new ArmClient(new DefaultAzureCredential());

// Next we get the specific subscription this resource belongs to
Subscription subscription = await armClient.GetSubscriptions().GetAsync(id.SubscriptionId);

// Next we get the specific resource group this resource belongs to
ResourceGroup resourceGroup = await subscription.GetResourceGroups().GetAsync(id.ResourceGroupName);

// Finally we get the resource itself
// Note: for this last step in this example, Azure.ResourceManager.Compute is needed
AvailabilitySet availabilitySet = await resourceGroup.GetAvailabilitySets().GetAsync(id.Name);

// we have the data representing the availabilitySet
Console.WriteLine(availabilitySet.Data.Name);

This approach required many lines of code and three API calls to Azure. The same can be done with less code and without any API calls by using extension methods that we’ve provided on the client itself. These extension methods allow you to pass in a resource identifier and retrieve a scoped resource client. The object returned is a [Resource] mentioned above, and since it hasn’t reached out to Azure to retrieve the data yet the Data property will be null.

So, the previous example would end up looking like this:

string resourceId = "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/workshop2021-rg/providers/Microsoft.Compute/availabilitySets/ws2021availSet";
ResourceIdentifier id = new ResourceIdentifier(resourceId);
// We construct a new armClient to work with
ArmClient armClient = new ArmClient(new DefaultAzureCredential());

// Next we get the AvailabilitySet resource client from the armClient
// The method takes in a ResourceIdentifier or we can use the implicit cast from string
AvailabilitySet availabilitySet = armClient.GetAvailabilitySet(id);

// At this point availabilitySet.Data will be null and trying to access it will throw
// If we want to retrieve the objects data we can call get
availabilitySet = await availabilitySet.GetAsync();

// we now have the data representing the availabilitySet
Console.WriteLine(availabilitySet.Data.Name);

Check if a [Resource] exists

If you aren’t sure if a resource you want to get exists, or you just want to check if it exists, you can use GetIfExists() or CheckIfExists() methods, which can be invoked from any [Resource]Collection class.

GetIfExists() and GetIfExistsAsync() return a Response<T> where T is null if the specified resource doesn’t exist. CheckIfExists() and CheckIfExistsAsync() return Response<bool> where the bool will be false if the specified resource doesn’t exist. Both of these methods still give you access to the underlying raw response.

Before these methods were introduced, you would need to catch the RequestFailedException and inspect the status code for 404.

ArmClient armClient = new ArmClient(new DefaultAzureCredential());
Subscription subscription = await armClient.GetDefaultSubscriptionAsync();
string rgName = "myRgName";

try
{
    ResourceGroup myRG = await subscription.GetResourceGroups().GetAsync(rgName);
    // At this point, we are sure that myRG is a not null Resource Group, so we can use this object to perform any operations we want.
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
    Console.WriteLine($"Resource Group {rgName} does not exist.");
}

Now with these convenience methods we can do the following instead.

ArmClient armClient = new ArmClient(new DefaultAzureCredential());
Subscription subscription = await armClient.GetDefaultSubscriptionAsync();
string rgName = "myRgName";

bool exists = await subscription.GetResourceGroups().CheckIfExistsAsync(rgName);

if (exists)
{
    Console.WriteLine($"Resource Group {rgName} exists.");

    // We can get the resource group now that we know it exists.
    // This does introduce a small race condition where resource group could have been deleted between the check and the get.
    ResourceGroup myRG = await subscription.GetResourceGroups().GetAsync(rgName);
}
else
{
    Console.WriteLine($"Resource Group {rgName} does not exist.");
}

Another way to accomplish the same thing is by using GetIfExists(), which will avoid the race condition mentioned above:

ArmClient armClient = new ArmClient(new DefaultAzureCredential());
Subscription subscription = await armClient.GetDefaultSubscriptionAsync();
string rgName = "myRgName";

ResourceGroup myRG = await subscription.GetResourceGroups().GetIfExistsAsync(rgName);

if (myRG == null)
{
    Console.WriteLine($"Resource Group {rgName} does not exist.");
}
else
{
    // At this point, we are sure that myRG is a not null Resource Group, so we can use this object to perform any operations we want.
}

Conclusion

We hope these changes make management of your Azure resources much easier with the .NET SDK.

Resources

You can check out more detailed examples for each of the SDKs below. Resource Provider Samples
Resource Manager Samples
Compute Samples
Network Samples
Resources Samples
Storage Samples

Azure SDK Blog Contributions

Thanks for reading this Azure SDK blog post. We hope you learned something new, and we welcome you to share the post. We’re open to Azure SDK blog contributions from our readers. To get started, contact us at azsdkblog@microsoft.com with your idea, and we’ll set you up as a guest blogger.

Author

Michael Nash
Principal Software Engineer

0 comments

Discussion are closed.