Skip to content

Commit a398104

Browse files
authored
VCST-4451: Configure Virto Commerce Settings via appsettings (#2973)
1 parent 6f8c0be commit a398104

25 files changed

+522
-100
lines changed

docs/release-information/update-to-version-3-1000/vc-net10-update.ps1

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,13 @@ function Update-Latest-Packages ($projectFile) {
129129
try {
130130
$latestVersion = (Find-Package $packageName -Source https://www.nuget.org/api/v2).Version
131131

132-
if ($packageName.StartsWith("VirtoCommerce.")) {
132+
if ($packageName.StartsWith("VirtoCommerce.Platform")) {
133+
$latestVersion = $platformVersion
134+
}
135+
elseif ($packageName.StartsWith("VirtoCommerce.")) {
133136
$latestVersion = $versionPrefix
134137
}
138+
135139
$predefinedVersions["$packageName"] = $latestVersion
136140
$version = $latestVersion
137141
} catch {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace VirtoCommerce.Platform.Core.Settings;
2+
3+
/// <summary>
4+
/// Provides settings overrides from configuration (appsettings/environment variables/KeyVault).
5+
/// Supports both Global and Tenant (ObjectType/ObjectId) lookups.
6+
/// </summary>
7+
public interface ISettingsOverrideProvider
8+
{
9+
bool TryGetCurrentValue(SettingDescriptor descriptor, string objectType, string objectId, out object value);
10+
bool TryGetDefaultValue(SettingDescriptor descriptor, string objectType, string objectId, out object value);
11+
}

src/VirtoCommerce.Platform.Data/Extensions/DbContextCommandExtensions.cs

Lines changed: 18 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Data;
4+
using System.Data.Common;
35
using System.Threading.Tasks;
46
using Microsoft.EntityFrameworkCore;
57
using Microsoft.EntityFrameworkCore.Storage;
@@ -8,83 +10,32 @@ namespace VirtoCommerce.Platform.Data.Extensions
810
{
911
public static class DbContextCommandExtensions
1012
{
11-
public static async Task<int> ExecuteNonQueryAsync(this DbContext context, string rawSql, params object[] parameters)
13+
public static Task<int> ExecuteNonQueryAsync(this DbContext context, string rawSql, params object[] parameters)
1214
{
13-
var conn = context.Database.GetDbConnection();
14-
await using var command = conn.CreateCommand();
15-
16-
command.CommandText = rawSql;
17-
if (parameters != null)
18-
{
19-
foreach (var p in parameters)
20-
{
21-
command.Parameters.Add(p);
22-
}
23-
}
24-
25-
if (context.Database.CurrentTransaction != null)
26-
{
27-
command.Transaction = context.Database.CurrentTransaction.GetDbTransaction();
28-
}
29-
30-
var wasOpen = conn.State == ConnectionState.Open;
31-
if (!wasOpen)
32-
{
33-
await conn.OpenAsync();
34-
}
35-
36-
try
37-
{
38-
return await command.ExecuteNonQueryAsync();
39-
}
40-
finally
41-
{
42-
if (!wasOpen)
43-
{
44-
await conn.CloseAsync();
45-
}
46-
}
15+
return ExecuteCommandAsync(context, rawSql, parameters, command => command.ExecuteNonQueryAsync());
4716
}
4817

49-
public static async Task<T> ExecuteScalarAsync<T>(this DbContext context, string rawSql, params object[] parameters)
18+
public static Task<T> ExecuteScalarAsync<T>(this DbContext context, string rawSql, params object[] parameters)
5019
{
51-
var conn = context.Database.GetDbConnection();
52-
await using var command = conn.CreateCommand();
20+
return ExecuteCommandAsync(context, rawSql, parameters, async command => (T)await command.ExecuteScalarAsync());
21+
}
5322

54-
command.CommandText = rawSql;
55-
if (parameters != null)
23+
public static Task<T[]> ExecuteArrayAsync<T>(this DbContext context, string rawSql, params object[] parameters)
24+
{
25+
return ExecuteCommandAsync(context, rawSql, parameters, async command =>
5626
{
57-
foreach (var p in parameters)
27+
var result = new List<T>();
28+
await using var reader = await command.ExecuteReaderAsync();
29+
while (await reader.ReadAsync())
5830
{
59-
command.Parameters.Add(p);
31+
result.Add(await reader.GetFieldValueAsync<T>(0));
6032
}
61-
}
62-
63-
if (context.Database.CurrentTransaction != null)
64-
{
65-
command.Transaction = context.Database.CurrentTransaction.GetDbTransaction();
66-
}
67-
68-
var wasOpen = conn.State == ConnectionState.Open;
69-
if (!wasOpen)
70-
{
71-
await conn.OpenAsync();
72-
}
7333

74-
try
75-
{
76-
return (T)await command.ExecuteScalarAsync();
77-
}
78-
finally
79-
{
80-
if (!wasOpen)
81-
{
82-
await conn.CloseAsync();
83-
}
84-
}
34+
return result.ToArray();
35+
});
8536
}
8637

87-
public static async Task<T[]> ExecuteArrayAsync<T>(this DbContext context, string rawSql, params object[] parameters)
38+
private static async Task<T> ExecuteCommandAsync<T>(DbContext context, string rawSql, object[] parameters, Func<DbCommand, Task<T>> executeFunc)
8839
{
8940
var conn = context.Database.GetDbConnection();
9041
await using var command = conn.CreateCommand();
@@ -111,15 +62,7 @@ public static async Task<T[]> ExecuteArrayAsync<T>(this DbContext context, strin
11162

11263
try
11364
{
114-
var result = new List<T>();
115-
await using var reader = await command.ExecuteReaderAsync();
116-
while (await reader.ReadAsync())
117-
{
118-
result.Add(await reader.GetFieldValueAsync<T>(0));
119-
}
120-
121-
return [.. result];
122-
65+
return await executeFunc(command);
12366
}
12467
finally
12568
{
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Linq;
4+
using Microsoft.Extensions.Configuration;
5+
using VirtoCommerce.Platform.Core.Settings;
6+
7+
namespace VirtoCommerce.Platform.Data.Settings;
8+
9+
/// <summary>
10+
/// Reads settings overrides from configuration keys:
11+
/// VirtoCommerce:Settings:Override:{CurrentValue|DefaultValue}:{Global|Tenants}:{...}:{SettingName} = value
12+
/// </summary>
13+
public class ConfigurationSettingsOverrideProvider : ISettingsOverrideProvider
14+
{
15+
private const string Root = "VirtoCommerce:Settings:Override";
16+
private readonly IConfiguration _configuration;
17+
18+
public ConfigurationSettingsOverrideProvider(IConfiguration configuration)
19+
{
20+
_configuration = configuration;
21+
}
22+
23+
public bool TryGetCurrentValue(SettingDescriptor descriptor, string objectType, string objectId, out object value)
24+
{
25+
return TryGet(descriptor, "CurrentValue", objectType, objectId, out value);
26+
}
27+
28+
public bool TryGetDefaultValue(SettingDescriptor descriptor, string objectType, string objectId, out object value)
29+
{
30+
return TryGet(descriptor, "DefaultValue", objectType, objectId, out value);
31+
}
32+
33+
private bool TryGet(SettingDescriptor descriptor, string bucket, string objectType, string objectId, out object value)
34+
{
35+
value = null;
36+
ArgumentNullException.ThrowIfNull(descriptor);
37+
38+
var name = descriptor.Name;
39+
40+
if (!TryGetFromTenantAndGlobal(descriptor, bucket, objectType, objectId, name, out value))
41+
{
42+
// Virto Cloud doesn't support dots in setting names, try replacing them with underscores
43+
var virtoCloudName = descriptor.Name.Replace(".", "_");
44+
return TryGetFromTenantAndGlobal(descriptor, bucket, objectType, objectId, virtoCloudName, out value);
45+
}
46+
47+
return true;
48+
}
49+
50+
private bool TryGetFromTenantAndGlobal(SettingDescriptor descriptor, string bucket, string objectType, string objectId, string name, out object value)
51+
{
52+
// Tenant-specific override first
53+
if (!string.IsNullOrEmpty(objectType) && !string.IsNullOrEmpty(objectId))
54+
{
55+
var tenantPath = $"{Root}:{bucket}:Tenants:{objectType}:{objectId}:{name}";
56+
if (TryReadSectionValue(_configuration.GetSection(tenantPath), descriptor, out value))
57+
{
58+
return true;
59+
}
60+
}
61+
62+
// Global override
63+
var globalPath = $"{Root}:{bucket}:Global:{name}";
64+
if (TryReadSectionValue(_configuration.GetSection(globalPath), descriptor, out value))
65+
{
66+
return true;
67+
}
68+
69+
return false;
70+
}
71+
72+
private static bool TryReadSectionValue(IConfigurationSection section, SettingDescriptor descriptor, out object value)
73+
{
74+
value = null;
75+
76+
var children = section.GetChildren().ToArray();
77+
78+
if (children.Length == 0)
79+
{
80+
// Scalar
81+
if (string.IsNullOrEmpty(section.Value))
82+
{
83+
return false;
84+
}
85+
86+
var convertedValue = ConvertToSettingValue(descriptor, section.Value);
87+
if (convertedValue == null)
88+
{
89+
return false;
90+
}
91+
value = convertedValue;
92+
93+
return true;
94+
}
95+
else
96+
{
97+
// Complex
98+
var rawComplex = children.Select(c => c.Value).ToArray();
99+
var convertedComplexValue = ConvertToSettingValue(descriptor, rawComplex);
100+
if (convertedComplexValue == null)
101+
{
102+
return false;
103+
}
104+
105+
value = convertedComplexValue;
106+
return true;
107+
}
108+
}
109+
110+
private static object ConvertToSettingValue(SettingDescriptor descriptor, object raw)
111+
{
112+
if (descriptor.IsDictionary)
113+
{
114+
return ConvertToAllowedValues(descriptor.ValueType, raw);
115+
}
116+
else if (raw is string str)
117+
{
118+
return ConvertScalar(descriptor.ValueType, str);
119+
}
120+
121+
return null;
122+
}
123+
124+
private static object[] ConvertToAllowedValues(SettingValueType valueType, object raw)
125+
{
126+
if (raw == null)
127+
{
128+
return Array.Empty<object>();
129+
}
130+
131+
if (raw is string[] arr)
132+
{
133+
return arr.Select(x => ConvertScalar(valueType, x)).ToArray();
134+
}
135+
136+
if (raw is object[] objectArray)
137+
{
138+
return objectArray
139+
.Select(item => item is string str ? ConvertScalar(valueType, str) : item)
140+
.ToArray();
141+
}
142+
143+
if (raw is string s)
144+
{
145+
return [ConvertScalar(valueType, s)];
146+
}
147+
148+
return Array.Empty<object>();
149+
}
150+
151+
private static object ConvertScalar(SettingValueType valueType, string str)
152+
{
153+
if (string.IsNullOrWhiteSpace(str))
154+
{
155+
return null;
156+
}
157+
158+
try
159+
{
160+
return valueType switch
161+
{
162+
SettingValueType.Boolean => bool.Parse(str),
163+
SettingValueType.DateTime => DateTime.Parse(str, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind),
164+
SettingValueType.Decimal => decimal.Parse(str, NumberStyles.Number, CultureInfo.InvariantCulture),
165+
SettingValueType.Integer or SettingValueType.PositiveInteger => int.Parse(str, NumberStyles.Integer, CultureInfo.InvariantCulture),
166+
_ => str
167+
};
168+
}
169+
catch
170+
{
171+
return str;
172+
}
173+
}
174+
}

src/VirtoCommerce.Platform.Data/Settings/ServiceCollectionExtenions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ public static class ServiceCollectionExtenions
77
{
88
public static IServiceCollection AddSettings(this IServiceCollection services)
99
{
10+
services.AddSingleton<ISettingsOverrideProvider, ConfigurationSettingsOverrideProvider>();
1011
services.AddSingleton<ISettingsManager, SettingsManager>();
1112
services.AddSingleton<ISettingsRegistrar>(context => context.GetService<ISettingsManager>());
1213
services.AddSingleton<ISettingsSearchService, SettingsSearchService>();

0 commit comments

Comments
 (0)