Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.

Commit 78d95ce

Browse files
authored
Add Microsoft.AdaptiveExpressions.Core, fork of AdaptiveExpressions on System.Text.Json (and AOT compatible) (#6783)
* AdaptiveExpressions on STJ and AOT compatible * Undo copy of Converters * Move to netstandard2.1 to get newer string overloads * Restore the .sln header * Fix possible project config issue for pipelines * Revert "Move to netstandard2.1 to get newer string overloads" This reverts commit 72a11ac. * Improve JsonNodeMemory for adaptive cards templating scenario * Make some AOT quality of life improvements * Use UnconditionalSuppressMessage instead; fix some trim warnings * Rename to Microsoft.AdaptiveExpressions.Core * Change TargetFramework to net8.0 so that the AOT tests run * Fix package name * CR feedback
1 parent 10947aa commit 78d95ce

265 files changed

Lines changed: 35641 additions & 10 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Microsoft.Bot.Builder.sln

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Microsoft Visual Studio Solution File, Format Version 12.00
2-
# Visual Studio Version 16
3-
VisualStudioVersion = 16.0.29123.88
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.10.34916.146
44
MinimumVisualStudioVersion = 10.0.40219.1
55
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{4269F3C3-6B42-419B-B64A-3E6DC0F1574A}"
66
ProjectSection(SolutionItems) = preProject
@@ -111,8 +111,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
111111
ProjectSection(SolutionItems) = preProject
112112
.editorconfig = .editorconfig
113113
bot.png = bot.png
114-
bot_icon.png = bot_icon.png
115114
BotBuilder-DotNet.ruleset = BotBuilder-DotNet.ruleset
115+
bot_icon.png = bot_icon.png
116116
CodeCoverage.runsettings = CodeCoverage.runsettings
117117
libraries\copySchemas.cmd = libraries\copySchemas.cmd
118118
Directory.Build.props = Directory.Build.props
@@ -233,7 +233,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Str
233233
EndProject
234234
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Streaming.Tests.Client", "tests\Microsoft.Bot.Connector.Streaming.Tests.Client\Microsoft.Bot.Connector.Streaming.Tests.Client.csproj", "{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}"
235235
EndProject
236-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Connector.Streaming.Perf", "tests\Microsoft.Bot.Connector.Streaming.Perf\Microsoft.Bot.Connector.Streaming.Perf.csproj", "{B49A3201-5BEE-426C-A082-D92D52172E06}"
236+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Streaming.Perf", "tests\Microsoft.Bot.Connector.Streaming.Perf\Microsoft.Bot.Connector.Streaming.Perf.csproj", "{B49A3201-5BEE-426C-A082-D92D52172E06}"
237+
EndProject
238+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AdaptiveExpressions.Core", "libraries\Microsoft.AdaptiveExpressions.Core\Microsoft.AdaptiveExpressions.Core.csproj", "{0D974CFD-0E69-448F-B375-BBDA457FD975}"
239+
EndProject
240+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AdaptiveExpressions.Core.Tests", "tests\Microsoft.AdaptiveExpressions.Core.Tests\Microsoft.AdaptiveExpressions.Core.Tests.csproj", "{F90C5110-0FF5-4775-9750-CF4E73B8AB32}"
241+
EndProject
242+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AdaptiveExpressions.Core.AOT.Tests", "tests\Microsoft.AdaptiveExpressions.Core.AOT.Tests\Microsoft.AdaptiveExpressions.Core.AOT.Tests.csproj", "{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}"
243+
EndProject
244+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AdaptiveExpressions", "AdaptiveExpressions", "{DBB8EB4C-04FF-4DB2-8782-7A7BC8D7F96F}"
245+
EndProject
246+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AdaptiveExpressions", "AdaptiveExpressions", "{064C57E9-24B8-4600-B72D-4C02CFBBEB95}"
237247
EndProject
238248
Global
239249
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -923,6 +933,30 @@ Global
923933
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release|Any CPU.Build.0 = Release|Any CPU
924934
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
925935
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
936+
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
937+
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Debug|Any CPU.Build.0 = Debug|Any CPU
938+
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
939+
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
940+
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Release|Any CPU.ActiveCfg = Release|Any CPU
941+
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Release|Any CPU.Build.0 = Release|Any CPU
942+
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
943+
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
944+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
945+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Debug|Any CPU.Build.0 = Debug|Any CPU
946+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
947+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
948+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Release|Any CPU.ActiveCfg = Release|Any CPU
949+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Release|Any CPU.Build.0 = Release|Any CPU
950+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
951+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
952+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
953+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Debug|Any CPU.Build.0 = Debug|Any CPU
954+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
955+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
956+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Release|Any CPU.ActiveCfg = Release|Any CPU
957+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Release|Any CPU.Build.0 = Release|Any CPU
958+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
959+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
926960
EndGlobalSection
927961
GlobalSection(SolutionProperties) = preSolution
928962
HideSolutionNode = FALSE
@@ -964,8 +998,8 @@ Global
964998
{E4E13301-9193-4106-B0E3-41276B478E7C} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
965999
{76391566-9F22-4994-8B0F-02EFC0E9E228} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
9661000
{1BC05915-044E-4776-8956-B44BBEFF2F84} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
967-
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
968-
{8DC1257B-7650-40EB-97A2-C1CBA306DA6A} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
1001+
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED} = {064C57E9-24B8-4600-B72D-4C02CFBBEB95}
1002+
{8DC1257B-7650-40EB-97A2-C1CBA306DA6A} = {DBB8EB4C-04FF-4DB2-8782-7A7BC8D7F96F}
9691003
{D5E70443-4BA2-42ED-992A-010268440B08} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
9701004
{84E3B6A2-42D9-498A-9CD2-1C5F5BE0D526} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
9711005
{52CDBBA9-E5AF-433C-80F0-5EF3C8B14946} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
@@ -1026,6 +1060,11 @@ Global
10261060
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
10271061
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
10281062
{B49A3201-5BEE-426C-A082-D92D52172E06} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
1063+
{0D974CFD-0E69-448F-B375-BBDA457FD975} = {DBB8EB4C-04FF-4DB2-8782-7A7BC8D7F96F}
1064+
{F90C5110-0FF5-4775-9750-CF4E73B8AB32} = {064C57E9-24B8-4600-B72D-4C02CFBBEB95}
1065+
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580} = {064C57E9-24B8-4600-B72D-4C02CFBBEB95}
1066+
{DBB8EB4C-04FF-4DB2-8782-7A7BC8D7F96F} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
1067+
{064C57E9-24B8-4600-B72D-4C02CFBBEB95} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
10291068
EndGlobalSection
10301069
GlobalSection(ExtensibilityGlobals) = postSolution
10311070
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## AdaptiveExpressions on System.Text.Json overview
2+
3+
To make AdaptiveExpressions library work with [AOT compilation](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=net7%2Cwindows), it was necessary to migrate from Newtonsoft to System.Text.Json. This has user-visible API and implementation differences.
4+
5+
### Expanded IMemory interface and constrained expression return types
6+
7+
The expression system calls out to the IMemory object to get and set values into a user-provided object graph. It is expected that TryGetValue returns only primitive types like number, string. But TryGetValue can also return lists or other objects. In many cases those objects are opaque and are just passed back into IMemory.SetValue or returned back to the caller, but in other cases the expression engine used to try to manipulate the objects or serialize them to json. Because all such methods would require reflection, those responsibilities are now delegated back to IMemory and the expression engine only understands primitive types, a small number of List types (`IList` and `List<object>`), and System.Text.Json types (e.g. `JsonArray` and `JsonObject`).
8+
9+
In some cases the expression engine will create a list (e.g. if you foreach over an object), and in this case the expression will create a `List<object>` and the user-implemented IMemory is expected to handle this externally-created object as well.
10+
11+
### Added JsonNodeMemory
12+
13+
For callers that don't want to implement IMemory, the easiest way to migrate to AOT is to use JsonObjects to store the data you want to evaluate expressions against and wrap them in JsonNodeMemory to pass in to evaluation functions. You can also use JsonSerializer.SerializeToNode to serialize an existing non-json object into JsonNode in an AOT-compatible way.
14+
15+
### Many methods now take JsonTypeInfo
16+
17+
System.Text.Json supports AOT by not relying on reflection, and instead the link between a type and its converter is via `JsonTypeInfo`. You get one of these using the [STJ source generator](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0).
18+
19+
Implementation within and APIs on AdaptiveExpressions that would have needed to do json serialization on unknown types now are marked as `[RequiresDynamicCode]` and `[RequiresUnreferencedCode]` and there are overloads of those methods and types that take JsonTypeInfo which can be used instead.
20+
21+
### Testing AOT mode
22+
23+
It's difficult to publish & test component in AOT compilation mode, so for testing components in AOT mode we turn on AOT warnings and ensure that the tests and product code are AOT-warning free to convince ourselves that the component will behave correctly when compiled as AOT with trimming on.
24+
25+
AdaptiveExpressionsSTJ.Tests are using some of the AOT patterns but still test SimpleObjectMemory paths and other non-AOT compatible paths. AdaptiveExpressionsSTJ.AOT.Tests is a copy of most of the tests from AdaptiveExpressionsSTJ.Tests but rewritten to be going through exclusively AOT-compatible modes. Functionally this means only testing against the JsonNodeMemory backing implementation and thus some of the round-trip tests aren't particularly interesting but they remain to show others how to convert such code in the future.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
using System.Text.Json.Serialization;
8+
using Microsoft.AdaptiveExpressions.Core.Properties;
9+
using Microsoft.Recognizers.Text.DataTypes.TimexExpression;
10+
11+
namespace Microsoft.AdaptiveExpressions.Core
12+
{
13+
/// <summary>
14+
/// Json serializer context for all AdaptiveExpressions types.
15+
/// </summary>
16+
[JsonSerializable(typeof(IntExpression))]
17+
[JsonSerializable(typeof(TimexProperty))]
18+
[JsonSerializable(typeof(DateTime))]
19+
[JsonSerializable(typeof(int))]
20+
[JsonSerializable(typeof(double))]
21+
internal partial class AdaptiveExpressionsSerializerContext : JsonSerializerContext
22+
{
23+
}
24+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Globalization;
7+
8+
namespace Microsoft.AdaptiveExpressions.Core.BuiltinFunctions
9+
{
10+
/// <summary>
11+
/// Returns the absolute value of the specified number.
12+
/// </summary>
13+
internal class Abs : NumberTransformEvaluator
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="Abs"/> class.
17+
/// </summary>
18+
public Abs()
19+
: base(ExpressionType.Abs, Function)
20+
{
21+
}
22+
23+
private static object Function(IReadOnlyList<object> args)
24+
{
25+
return Math.Abs(Convert.ToDouble(args[0], CultureInfo.InvariantCulture));
26+
}
27+
}
28+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Microsoft.AdaptiveExpressions.Core.Memory;
6+
7+
namespace Microsoft.AdaptiveExpressions.Core.BuiltinFunctions
8+
{
9+
/// <summary>
10+
/// Used to access the variable value corresponding to the path.
11+
/// </summary>
12+
internal class Accessor : ExpressionEvaluator
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="Accessor"/> class.
16+
/// </summary>
17+
public Accessor()
18+
: base(ExpressionType.Accessor, Evaluator, ReturnType.Object, Validator)
19+
{
20+
}
21+
22+
private static (object value, string error) Evaluator(Expression expression, IMemory state, Options options)
23+
{
24+
var (path, left, error) = FunctionUtils.TryAccumulatePath(expression, state, options);
25+
26+
if (error != null)
27+
{
28+
return (null, error);
29+
}
30+
31+
if (left == null)
32+
{
33+
// fully converted to path, so we just delegate to memory scope
34+
return FunctionUtils.WrapGetValue(state, path, options);
35+
}
36+
else
37+
{
38+
// stop at somewhere, so we figure out what's left
39+
var (newScope, err) = left.TryEvaluate(state, options);
40+
if (err != null)
41+
{
42+
return (null, err);
43+
}
44+
45+
return FunctionUtils.WrapGetValue(state.CreateMemoryFrom(newScope), path, options);
46+
}
47+
}
48+
49+
private static void Validator(Expression expression)
50+
{
51+
var children = expression.Children;
52+
if (children.Length == 0
53+
|| !(children[0] is Constant cnst)
54+
|| cnst.ReturnType != ReturnType.String)
55+
{
56+
throw new ArgumentException($"{expression} must have a string as first argument.");
57+
}
58+
59+
if (children.Length > 2)
60+
{
61+
throw new ArgumentException($"{expression} has more than 2 children.");
62+
}
63+
64+
if (children.Length == 2 && (children[1].ReturnType & ReturnType.Object) == 0)
65+
{
66+
throw new ArgumentException($"{expression} must have an object as its second argument.");
67+
}
68+
}
69+
}
70+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Globalization;
6+
7+
namespace Microsoft.AdaptiveExpressions.Core.BuiltinFunctions
8+
{
9+
/// <summary>
10+
/// Return the result from adding two or more numbers (pure number case) or concatting two or more strings (other case).
11+
/// </summary>
12+
internal class Add : ExpressionEvaluator
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="Add"/> class.
16+
/// </summary>
17+
public Add()
18+
: base(ExpressionType.Add, Evaluator(), ReturnType.String | ReturnType.Number, Validator)
19+
{
20+
}
21+
22+
private static EvaluateExpressionDelegate Evaluator()
23+
{
24+
return FunctionUtils.ApplySequenceWithError(
25+
args =>
26+
{
27+
object result = null;
28+
string error = null;
29+
var firstItem = args[0];
30+
var secondItem = args[1];
31+
var stringConcat = !firstItem.IsNumber() || !secondItem.IsNumber();
32+
33+
if ((firstItem == null && secondItem.IsNumber())
34+
|| (secondItem == null && firstItem.IsNumber()))
35+
{
36+
error = "Operator '+' or add cannot be applied to operands of type 'number' and null object.";
37+
}
38+
else
39+
{
40+
if (stringConcat)
41+
{
42+
result = $"{firstItem?.ToString()}{secondItem?.ToString()}";
43+
}
44+
else
45+
{
46+
result = EvalAdd(args[0], args[1]);
47+
}
48+
}
49+
50+
return (result, error);
51+
}, FunctionUtils.VerifyNumberOrStringOrNull);
52+
}
53+
54+
private static object EvalAdd(object a, object b)
55+
{
56+
if (a == null)
57+
{
58+
throw new ArgumentNullException(nameof(a));
59+
}
60+
61+
if (b == null)
62+
{
63+
throw new ArgumentNullException(nameof(b));
64+
}
65+
66+
if (a.IsInteger() && b.IsInteger())
67+
{
68+
return Convert.ToInt64(a, CultureInfo.InvariantCulture) + Convert.ToInt64(b, CultureInfo.InvariantCulture);
69+
}
70+
71+
return FunctionUtils.CultureInvariantDoubleConvert(a) + FunctionUtils.CultureInvariantDoubleConvert(b);
72+
}
73+
74+
private static void Validator(Expression expression)
75+
{
76+
FunctionUtils.ValidateArityAndAnyType(expression, 2, int.MaxValue, ReturnType.String | ReturnType.Number);
77+
}
78+
}
79+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
6+
namespace Microsoft.AdaptiveExpressions.Core.BuiltinFunctions
7+
{
8+
/// <summary>
9+
/// Add a number of days to a timestamp.
10+
/// AddDays function takes a timestamp string, an interval integer,
11+
/// an optional format string whose default value "yyyy-MM-ddTHH:mm:ss.fffZ"
12+
/// and an optional locale string whose default value is Thread.CurrentThread.CurrentCulture.Name.
13+
/// </summary>
14+
internal class AddDays : TimeTransformEvaluator
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="AddDays"/> class.
18+
/// </summary>
19+
public AddDays()
20+
: base(ExpressionType.AddDays, Function)
21+
{
22+
}
23+
24+
private static DateTime Function(DateTime time, int interval)
25+
{
26+
return time.AddDays(interval);
27+
}
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
6+
namespace Microsoft.AdaptiveExpressions.Core.BuiltinFunctions
7+
{
8+
/// <summary>
9+
/// Add a number of hours to a timestamp.
10+
/// AddHours function takes a timestamp string, an interval integer,
11+
/// an optional format string whose default value "yyyy-MM-ddTHH:mm:ss.fffZ"
12+
/// and an optional locale string whose default value is Thread.CurrentThread.CurrentCulture.Name.
13+
/// </summary>
14+
internal class AddHours : TimeTransformEvaluator
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="AddHours"/> class.
18+
/// </summary>
19+
public AddHours()
20+
: base(ExpressionType.AddHours, Function)
21+
{
22+
}
23+
24+
private static DateTime Function(DateTime time, int interval)
25+
{
26+
return time.AddHours(interval);
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)