Skip to main content

Pulumi Pitfalls in C# - Part 2

· 4 min read

The is the second in a series of posts about some of the pitfalls I've encountered whilst writing C# code to deploy with Pulumi. This post will cover some common mistakes when working with the Input<T> and Output<T> types.

WTH is Input<T> and Output<T>?

When you first start writing Pulumi programs in C#, you'll likely come across the Input<T> and Output<T> types. The simplest way to describe these is T (generics) could either be a plain value of T or a Task<T> which needs to be eventually awaited by the Pulumi engine. Because only the Pulumi engine knows if Input<T> or Output<T> is "known", we are not able to access the underlying value safely without using other methods in Pulumi like Apply().

One other thing to note is that Input<T> is effectively a small wrapper around Output<T> to better represent that an Input is something to pass into a resource, and an Output is something that is returned. For the rest of this post, I'll refer to Output<T> as it's the most common type you'll come across.

Pulumi describe further in their documentation about inputs and ouputs.

There are many quirks to be aware of when working with Output<T>. Here is one of the more common ones that I see frequently:

Output<string> != string

One of my pet peeves is that the ToString() method on an Output<T> is overriden, however it returns a warning about using it as the result. This can lead to some confusion which might make you think you can use Output<string> as a regular string type when doing things like string interpolation, but it does not work like you'd expect. Here is an example:

Example

Create an Output<string> and write it to the console
var myOutput = Output.Create("Hello, Pulumi!");
Console.WriteLine($"Output: {myOutput}");

What you may expect this to return is Output: Hello, Pulumi!, however it will actually a return a message more like:

Output: Calling [ToString] on an [Output<T>] is not supported.

To get the value of an Output<T> as an Output<string> consider:
1. o.Apply(v => $\"prefix{v}suffix\")
2. Output.Format($\"prefix{hostname}suffix\");

See https://www.pulumi.com/docs/concepts/inputs-outputs for more details."
This function may throw in a future version of Pulumi.

This is the ToString() override result.

One more extreme example of this that can cause issues is if you use this to provide the name argument to a resource.

Create a resource with a name from an Output<string>
var resourceName = Output.Create("Hello, Pulumi!");

var myResource = new SomeResource(resourceName, new SomeResourceArgs());

The C# compiler will be happy with this, but at runtime this will cause the Pulumi program to store the logical resource name as the warning message noted earlier, rather than Hello, Pulumi!. This becomes even more problematic if you create multiple resources this way, as they will conflict with the same, odd name.

Messy

Solution

One of the simplest solutions is that for the logical resource naming (not physical resource naming), you should always use a string value. These names are only logical names stored in Pulumi state and should be descriptive of the resource you're creating.

For other cases where you may want to access the underlying value, you may have to use the Apply() method.

Fortunately, there is a soon to be released change that allows changing ToString() throw exception if used on an Output. This will help catch these issues at runtime and may become the default behaviour in future.

Another solution would be to consider utilising a Roslyn Analyzer to catch these issues at compilation time. However this isn't something that exists today as far as I'm aware.