Azure Pipelines, DevOps

Optional Object/Array Checks in Azure Pipelines

Sometimes when building Azure Pipelines in YAML you want to create a template with optional parameters and then test those parameters have a value.

For a string value this is straight forward e.g.

parameters:
- name: project
   type: string
   default: ''
${{ if eq(parameters.project, '') }}: 
or 
${{ if ne(parameters.project, '') }}:

When it comes to objects or arrays, creating the optional parameter is still straight forward e.g.

parameters:
- name: projects
   type: object
   default: []
- name: envs
   type: object
   default: {}

However, testing if they are empty is not quite so simple as you cannot just do an equality test like with string parameters e.g.

${{ if eq(parameters.envs, {}) }}: 
or
${{ if ne(parameters.envs, {}) }}:

${{ if eq(parameters.projects, []) }}: 
or
${{ if ne(parameters.projects, []) }}:

To get around this we can do a bit of trickery using convertToJson to provide a string representation of the object or array e.g.

${{ if eq(convertToJson(parameters.envs), '{}') }}: 
or 
${{ if ne(convertToJson(parameters.envs), '{}') }}:

${{ if eq(convertToJson(parameters.projects), '[]') }}:
or
${{ if ne(convertToJson(parameters.projects), '[]') }}:

Now this technique all sounds good but what about a real world example where it might be useful

So, I have a shared .NET build template (build-code.yml) and for some projects the dotnet test run requires some environment variables. We can pass the required environment variables in using the normal YAML object syntax e.g.

- template: build-code.yml
  parameters:
    version: '6.0.x'
    envs:
      appEndpoint: $(appEndpoint)
      databaseEndpoint: $(databaseEndpoint)
      apiEndpoint: $(apiEndpoint)

These are then used in build-code.yml template e.g.

parameters:
- name: version
  type: string
  default: ''
- name: artifactName
  type: string
  default: 'code'
- name: publishWebProjects
  type: boolean
  default: true
- name: zipAfterPublish
  type: boolean
  default: false
- name: envs
  type: object
  default: {}

variables: 
  buildConfiguration: 'Release'

steps:
- task: UseDotNet@2
  displayName: 'Use .NET version'
  inputs:
    packageType: 'sdk'
    ${{ if ne(parameters.version, '') }}:
      version: '${{ parameters.version }}'
    ${{ if eq(parameters.version, '') }}:
      useGlobalJson: true
- task: DotNetCoreCLI@2
  displayName: 'Restore Packages'
  inputs:
    command: restore
    projects: '**/*.csproj'
- task: DotNetCoreCLI@2
  displayName: 'Build Projects'
  inputs:
    command: 'build'
    projects: '**/*.csproj'
    arguments: '--no-restore --configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
  displayName: 'Run Tests'
  inputs:
    command: test
    projects: '**/*Tests.csproj'
    arguments: '--no-restore --no-build --configuration $(buildConfiguration)'
  ${{ if ne(convertToJson(parameters.envs), '{}') }}:
    env:
      ${{ parameters.envs }}

If the environment variables are not needed for a project then they can be simply omitted e.g.

- template: build-code.yml
  parameters:
    version: '6.0.x'

I don’t like to use too many optional parts in a template as it can get very complex but for this usage it works quite well