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.
- Azure SDK Website: aka.ms/azsdk
- Azure SDK Intro (3-minute video): aka.ms/azsdk/intro
- Azure SDK Intro Deck (PowerPoint deck): aka.ms/azsdk/intro/deck
- Azure SDK Releases: aka.ms/azsdk/releases
- Azure SDK Blog: aka.ms/azsdk/blog
- Azure SDK Twitter: twitter.com/AzureSDK
- Azure SDK Design Guidelines: aka.ms/azsdk/guide
- Azure SDKs & Tools: azure.microsoft.com/downloads
- Azure SDK Central Repository: github.com/azure/azure-sdk
- Azure SDK for .NET: github.com/azure/azure-sdk-for-net
- Azure SDK for Java: github.com/azure/azure-sdk-for-java
- Azure SDK for Python: github.com/azure/azure-sdk-for-python
- Azure SDK for JavaScript/TypeScript: github.com/azure/azure-sdk-for-js
- Azure SDK for Android: github.com/Azure/azure-sdk-for-android
- Azure SDK for iOS: github.com/Azure/azure-sdk-for-ios
- Azure SDK for Go: github.com/Azure/azure-sdk-for-go
- Azure SDK for C: github.com/Azure/azure-sdk-for-c
- Azure SDK for C++: github.com/Azure/azure-sdk-for-cpp
0 comments