Skip to content
Tamim Khan edited this page May 17, 2020 · 3 revisions

Overview

Charming lets you generate ARM templates using C# by creating simple POCO objects. It is simply a specialised serialiser (currently uses the awesome Json.NET library under the hood for serialising) that serialises objects representing the various elements of an ARM template into valid json that can be readily used for deployment.

The generated json is in an expanded form, intended to be treated as a transpiled artifact and thereby eliminating the need for some of the typically used constructs within an ARM template such as parameters, variables, conditions, copy loops and functions. You can and would still reference some of the available template functions like resourceGroup(), resourceId(), etc. to pass around resource information and the library provides some nice helper methods to assist you in forming the function calls.

Charming provides the following core abstractions and types:

Resources

The Resource<TProperties> abstract class represents a resource where TProperties is a generic parameter type for the properties of that resource. Its base class Resource can also be used directly for resources that do not need a properties section.

The following snippet defines a log analytics workspace:

public class Workspace : Resource<WorkspaceProperties>
{
    public Workspace() =>
        (Type, ApiVersion) = ("Microsoft.OperationalInsights/workspaces", "2020-03-01-preview");
}

public class WorkspaceProperties
{
    public int? RetentionInDays { get; set; }
}

Note that the RetentionInDays property is defined as a nullable int?. This is necessary to avoid having retentionInDays property always be set to zero in the generated json, when it is not explicitly set. As a general rule of thumb, always prefer nullable types for property data types, so that if they are not set, then they will not appear in the generated json.

Some of the most commonly used resource types have already been defined in the Charming.Types nuget package and you can reference them instead of creating custom ones. See here for more details.

To add child resources:

var containers = new[]
{
    new StorageAccountBlobServiceContainer("container1"),
    new StorageAccountBlobServiceContainer("container2"),
};

var blobService = new StorageAccountBlobService("default")
    .WithResources(containers);

// or directly add to the list
blobService.Resources.Add(new StorageAccountBlobServiceContainer("container3"));

To add dependencies:

var vmScaleSet = new VirtualMachineScaleSet("sample-vm-scaleset")
    .WithDependencies(new VirtualNetwork("sample-vnet"));

// or directly add to the list
vmScaleSet.DependsOn.Add(new LoadBalancer("sample-lb"));

To add tags:

var tags = new Dictionary<string, string>
{
    ["environment"] = "dev",
    ["department"] = "finance",
};

var resourceGroup = new ResourceGroup("sample-rg").WithTags(tags);

// or directly add to the dictionary
resourceGroup.Tags.Add("version", "1.0");

Outputs

The Output class represents an output item of a template. It has a Key and a Value property and some static helper methods to create an Output object.

Note that the Value property can be one of these three types:

  • ArrayOutputValue: represents an array of either string or object output items
  • ObjectOutputValue: represents an object via a mapping of string and other OutputValues
  • StringOutputValue: represents a simple string value

To create an array output of string values:

var storageAccounts = new[]
{
    new StorageAccount("samplestorage1"),
    new StorageAccount("samplestorage2"),
};

var values = storageAccounts
    .Select(x => $"{Functions.Reference(x)}.primaryEndpoints.blob".Box());

var output = Output.Array("storageEndpoints", values);

To create an array output of object values:

var storageAccounts = new[]
{
    new StorageAccount("samplestorage1"),
    new StorageAccount("samplestorage2"),
};

var values = storageAccounts
    .Select(x => new Dictionary<string, string>
    {
        ["BlobEndpoint"] = $"{Functions.Reference(x)}.primaryEndpoints.blob".Box(),
        ["Status"] = $"{Functions.Reference(x)}.statusOfPrimary".Box(),
    });

var output = Output.Array("storageEndpoints", values);

To create an object output:

var storage = new StorageAccount("samplestorage");
var output = Output.Object("storage", Functions.ReferenceFull(storage).Box());

To create an object output with mutiple properties:

var storage = new StorageAccount("samplestorage");
var keyvault = new Vault("sample-kv");

var value = new Dictionary<string, string>
{
    ["storage"] = Functions.Reference(storage).Box(),
    ["keyvault"] = Functions.Reference(keyvault).Box(),
};

var output = Output.Object("deployment", value);

To create a string output:

var output = Output.String("resourceGroupName", Functions.ResourceGroup().Name.Box());

Note that both the Object and String helper methods have overloads that take a boolean isSecure to let you specify if you want to output a secureobject or securestring respectively instead.

Functions

Charming provides a bunch of convenience static methods hanging off the Functions class that help form a template function call string.

To construct the reference() function call:

var storage = new StorageAccount("samplestorage");
var simpleOutput = Output.Object("storage", Functions.Reference(storage).Box());
var fullOutput = Output.Object("storage", Functions.ReferenceFull(storage).Box());

Note that the Box() string extension method encloses the resulting string within square brackets [].

To construct the resourceId() function call:

var storage = new StorageAccount("samplestorage");
var output1 = Output.String("storageResourceId", Functions.ResourceId(storage).Box());

var otherResourceGroup = "other-rg";
var storageFromOtherResourceGroup = new StorageAccount("samplestorage");
var output2 = Output.String("storageResourceId", Functions.ResourceId(otherResourceGroup, storage).Box());

var otherSubscriptionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
var storageFromOtherSubscription = new StorageAccount("samplestorage");
var output3 = Output.String("storageResourceId", Functions.ResourceId(otherSubscriptionId, otherResourceGroup, storage).Box());

To construct the resourceGroup() function call:

var output1 = Output.Object("resourceGroup", Functions.ResourceGroup().Box());
var output2 = Output.String("resourceGroupId", Functions.ResourceGroup().Id.Box());

To construct the subscription() function call:

var output1 = Output.Object("subscription", Functions.Subscription().Box());
var output2 = Output.String("subscriptionId", Functions.Subscription().Id.Box());

Both ResourceGroup() and Subscription() methods return an object containing the relevant properties that can be call off that method. The examples above demonstrate the Id property being called.

If the function you need is not available, then you could simply create a string with the function call:

var deployment = new Deployment("sample-deploy");
deployment.Properties = new DeploymentProperties
{
    Mode = DeploymentMode.Incremental,
    TemplateLink = new TemplateLinkInfo
    {
        Uri = "uri(deployment().properties.templateLink.uri, 'azuredeploy.json')",
    },
};

Templates

The ITemplate interface represents a template. Charming comes with two implementations out of the box:

  • SubscriptionDeploymentTemplate: use this type to represent a template that is to deployed at a subscription level.
  • ResourceGroupDeploymentTemplate: use this type to represent a template that is to deployed at a resource group level.

The Schema property on the template is set by default to the location of the latest json schema version.

Once all resources and outputs have been wired up to the template object, then simply call ToJson() to generate the output json. This method has an overload that takes a SerializerOptions object that lets you configure the serialisation behaviour.

var template = new ResourceGroupDeploymentTemplate()
    .WithResources(new StorageAccount("samplestorage"));

var json = template.ToJson(new SerializerOptions { Indent = false });

Resource Types:

The accompanying Charming.Types nuget package provides a set of types representing commonly used Azure resources and the recommended approach is to add a reference to this package instead of the core Charming package.

All related resources are grouped under a single namespace, for instance adding using Charming.Types.KeyVault; would bring into scope the Vault resource type (representing a keyvault) as well as all its child resource types such as VaultSecret, VaultAccessPolicy, etc.

Note that the resource type names follow the naming from Azure resource provider namespaces.