Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 0.20.1 <small>2026-04-30</small>

## 💅 Improvements
- Parameters with an `env` attribute now automatically read their default value from the corresponding environment variable, but only when the caller has not already supplied a value for that parameter. Read more about it [here](./docs/environment-variables.md).

<!-- CHANGELOG_BOUNDARY -->

# 0.20.0 <small>2026-04-29</small>

## 💅 Improvements
Expand Down
58 changes: 58 additions & 0 deletions docs/environment-variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Environment Variable Parameters

Parameters can read their values from environment variables at load time. This lets you keep
sensitive information — connection strings, API keys, passwords — out of your arrangements and
common settings entirely.

## Configuration

Add an `env` attribute to any `<add>` element inside `<parameters>`, pointing to the environment
variable whose value should be used:

```xml
<parameters>
<add name="ConnectionString" env="MY_DB_CONNECTION_STRING" />
<add name="ApiKey" env="MY_API_KEY" />
</parameters>
```

When the arrangement is loaded, if no external caller has already supplied a value for the
parameter, the module reads the named environment variable and uses it as the parameter's value.

## How It Works

- If a caller (e.g. a form submission or a query-string value) already provides a value for
the parameter, the environment variable is **not** consulted — caller-supplied values take
precedence.
- If the parameter has no value and `env` is set, `Environment.GetEnvironmentVariable(env)` is
called and the result becomes the parameter's value for that request.
- If the environment variable is not set (returns `null`), the parameter remains empty.

## Where It Applies

Environment variable resolution runs in both contexts:

- **Common settings** — the shared arrangement loaded from the module's site settings.
- **Content item arrangements** — per-content-item XML/JSON arrangements.

## Typical Use Case

Store a database password in an environment variable on the server and reference it in your
arrangement without ever writing the literal value into Orchard content or settings:

```xml
<connections>
<add name="input"
provider="sqlserver"
server="myserver"
database="mydb"
user="myuser"
password="@[DbPassword]" />
</connections>
<parameters>
<add name="DbPassword" env="DB_PASSWORD" />
</parameters>
```

Setting `input="false"` ensures the parameter is never exposed to end-user input — it is
resolved from the environment and injected into the arrangement internally.
4 changes: 4 additions & 0 deletions src/OrchardCore.Transformalize/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public static class Common {
public const string OrchardConnectionName = "orchard";
public const string Default = "[default]";

public const char PlaceHolderMarker = '@';
public const char PlaceHolderOpen = '[';
public const char PlaceHolderClose = ']';

public const string TaskReferrer = "TaskReferrer";
public const string TaskContentItemId = "TaskContentItemId";
public const string ReportContentItemId = "ReportContentItemId";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<TargetFramework>net10.0</TargetFramework>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<RootNamespace>TransformalizeModule</RootNamespace>
<Version>0.20.0</Version>
<FileVersion>0.20.0</FileVersion>
<AssemblyVersion>0.20.0</AssemblyVersion>
<Version>0.20.1</Version>
<FileVersion>0.20.1</FileVersion>
<AssemblyVersion>0.20.1</AssemblyVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Dale Newman</Authors>
<Copyright>Copyright © 2013-2026</Copyright>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class ParameterModifier : ICustomizer {
private const string ParameterNameAttribute = "name";
private const string ParameterValueAttribute = "value";
private const string ParameterInputAttribute = "input";
private const string ParameterEnvAttribute = "env";

public ParameterModifier(
IPlaceHolderReplacer placeHolderReplacer
Expand Down Expand Up @@ -64,34 +65,48 @@ private void MergeParameters(IEnumerable<INode> nodes, IDictionary<string, strin
foreach (var parameter in nodes) {
string name = null;
object value = null;
string env = null;
foreach (var attribute in parameter.Attributes) {
if (attribute.Name == ParameterNameAttribute) {
name = attribute.Value.ToString();
} else if (attribute.Name == ParameterValueAttribute) {
value = attribute.Value;
} else if (attribute.Name == ParameterEnvAttribute) {
env = attribute.Value?.ToString();
}
}
if (name != null && value != null) {

if (name == null) continue;

if (parameters.ContainsKey(name)) {
// arrangement and external parameter match
if (parameters.ContainsKey(name)) {
// arrangement and external parameter match

// if the arrangment parameter says input is false, we remove it as it is not permitted
if (parameter.TryAttribute(ParameterInputAttribute, out var inputAttr) && inputAttr.Value.Equals("false")) {
parameters.Remove(name);
continue;
}
// if the arrangement parameter says input is false, we remove it as it is not permitted
if (parameter.TryAttribute(ParameterInputAttribute, out var inputAttr) && inputAttr.Value.Equals("false")) {
parameters.Remove(name);
continue;
}

// the external parameter will set the arrangement parameter's value attribute
if (parameter.TryAttribute(ParameterValueAttribute, out var valueAttr)) {
valueAttr.Value = parameters[name];
} else {
parameter.Attributes.Add(new Attribute("value", parameters[name]));
}

} else {
// caller hasn't provided a value — resolve from env var if value is absent/empty
var effectiveValue = string.IsNullOrWhiteSpace(value?.ToString()) && !string.IsNullOrEmpty(env)
? System.Environment.GetEnvironmentVariable(env)
: value?.ToString();

// the external parameter will set the arrangement parameter's value attribute
if (effectiveValue != null) {
parameters[name] = effectiveValue;
if (parameter.TryAttribute(ParameterValueAttribute, out var valueAttr)) {
valueAttr.Value = parameters[name];
valueAttr.Value = effectiveValue;
} else {
parameter.Attributes.Add(new Attribute("value", parameters[name]));
parameter.Attributes.Add(new Attribute("value", effectiveValue));
}

} else { // attribute value is going to set the parameter
parameters[name] = value.ToString();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public async Task<ILifetimeScope> CreateScopeAsync(string arrangement, ContentIt
dependancies.Add(Serializer);
}

dependancies.Add(new ParameterModifier(new PlaceHolderReplacer('@', '[', ']')));
dependancies.Add(new ParameterModifier(new PlaceHolderReplacer(Common.PlaceHolderMarker, Common.PlaceHolderOpen, Common.PlaceHolderClose)));

// these were registered by the ShorthandModule are are used to expand shorthand transforms and validators into "longhand".
dependancies.Add(ctx.ResolveNamed<IDependency>(TransformModule.FieldsName));
Expand Down
23 changes: 11 additions & 12 deletions src/OrchardCore.Transformalize/Services/SettingsService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
using TransformalizeModule.Models;
using TransformalizeModule.Services.Contracts;
using OrchardCore.ContentManagement;
using OrchardCore.Entities;
using TransformalizeModule.Services.Modifiers;
using OrchardCore.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using Transformalize.Configuration;
using StackExchange.Profiling;
using TransformalizeModule.Ext;
Expand All @@ -20,9 +16,14 @@ namespace TransformalizeModule.Services {
/// </summary>
public class SettingsService : ISettingsService {

public TransformalizeSettings Settings { get; }
private readonly IDbConnectionAccessor _dbConnectionAccessor;
private readonly IStore _store;
private readonly CombinedLogger<SettingsService> _logger;

public Process Process { get; set; }
public Transformalize.ConfigurationFacade.Process ProcessFacade { get; set; }
public TransformalizeSettings Settings { get; }

public Dictionary<string, Parameter> Parameters { get; } = new Dictionary<string, Parameter>();
private readonly Dictionary<string, Transformalize.ConfigurationFacade.Parameter> ParametersFacade = new Dictionary<string, Transformalize.ConfigurationFacade.Parameter>(StringComparer.OrdinalIgnoreCase);

Expand All @@ -38,10 +39,6 @@ public class SettingsService : ISettingsService {
public Dictionary<string, Field> Fields { get; } = new Dictionary<string, Field>();
private readonly Dictionary<string, Transformalize.ConfigurationFacade.Field> FieldsFacade = new Dictionary<string, Transformalize.ConfigurationFacade.Field>(StringComparer.OrdinalIgnoreCase);

private readonly IDbConnectionAccessor _dbConnectionAccessor;
private readonly IStore _store;
private readonly CombinedLogger<SettingsService> _logger;

public SettingsService(
ISiteService siteService,
IDbConnectionAccessor dbConnectionAccessor,
Expand All @@ -62,8 +59,9 @@ CombinedLogger<SettingsService> logger
Process = new Process();
ProcessFacade = new Transformalize.ConfigurationFacade.Process();
} else {
Process = new Process(Settings.CommonArrangement);
ProcessFacade = new Transformalize.ConfigurationFacade.Process(Settings.CommonArrangement);
var modifier = new ParameterModifier(new Cfg.Net.Environment.PlaceHolderReplacer(Common.PlaceHolderMarker, Common.PlaceHolderOpen, Common.PlaceHolderClose));
Process = new Process(Settings.CommonArrangement, modifier);
ProcessFacade = new Transformalize.ConfigurationFacade.Process(Settings.CommonArrangement, dependencies: [modifier]);
}

// parameters
Expand Down Expand Up @@ -343,5 +341,6 @@ public void ApplyCommonSettings(Transformalize.ConfigurationFacade.Process proce
}

}

}
}
8 changes: 4 additions & 4 deletions src/Site/Site.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
<DockerfileTag>transformalize.orchard</DockerfileTag>
<Version>0.20.0</Version>
<FileVersion>0.20.0</FileVersion>
<AssemblyVersion>0.20.0</AssemblyVersion>
<ReleaseVersion>0.20.0</ReleaseVersion>
<Version>0.20.1</Version>
<FileVersion>0.20.1</FileVersion>
<AssemblyVersion>0.20.1</AssemblyVersion>
<ReleaseVersion>0.20.1</ReleaseVersion>
<RazorRuntimeCompilation>true</RazorRuntimeCompilation>
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
<Nullable>enable</Nullable>
Expand Down
Loading