Skip to content

Commit 6941f59

Browse files
Copilottomasherceg
authored andcommitted
Fix CancellationToken serialization in static commands - inject from request context
Co-authored-by: tomasherceg <5599524+tomasherceg@users.noreply.github.com> Fixed-by: Standa Lukeš
1 parent 2427f9b commit 6941f59

7 files changed

Lines changed: 84 additions & 1 deletion

src/Framework/Framework/Compilation/Binding/StaticCommandBindingCompiler.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ public override string ToString() =>
165165
StaticCommandParameterType.Argument => "?",
166166
StaticCommandParameterType.Inject => $"service({((Type)Arg!).ToCode(stripNamespace: true)})",
167167
StaticCommandParameterType.Invocation => Arg!.ToString()!,
168+
StaticCommandParameterType.CurrentCancellationToken => "cancellationToken",
168169
_ => "...invalid argument..."
169170
};
170171
}
@@ -174,6 +175,7 @@ public enum StaticCommandParameterType : byte
174175
Inject,
175176
Constant,
176177
DefaultValue,
177-
Invocation
178+
Invocation,
179+
CurrentCancellationToken
178180
}
179181
}

src/Framework/Framework/Compilation/Binding/StaticCommandExecutionPlanSerializer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ public static JsonNode SerializePlan(StaticCommandInvocationPlan plan)
5555
{
5656
array.Add(SerializePlan((StaticCommandInvocationPlan)arg.Arg!));
5757
}
58+
else if (arg.Type == StaticCommandParameterType.CurrentCancellationToken)
59+
{
60+
array.Add(null);
61+
}
5862
else throw new NotSupportedException(arg.Type.ToString());
5963
}
6064
return array;
@@ -160,6 +164,8 @@ public static StaticCommandInvocationPlan DeserializePlan(ref Utf8JsonReader jso
160164
new StaticCommandParameterPlan(type, methodParameters[i]!.DefaultValue),
161165
StaticCommandParameterType.Invocation =>
162166
new StaticCommandParameterPlan(type, DeserializePlan(ref json)),
167+
StaticCommandParameterType.CurrentCancellationToken =>
168+
new StaticCommandParameterPlan(type, null),
163169
_ => throw new NotSupportedException(type.ToString())
164170
};
165171
json.AssertRead();

src/Framework/Framework/Compilation/Binding/StaticCommandMethodTranslator.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ ValidationPathFormatter validationPathFormatter
7575
var argPlans = allArguments.Select((arg, index) => {
7676
if (arg.OriginalExpression.GetParameterAnnotation() is BindingParameterAnnotation { ExtensionParameter: InjectedServiceExtensionParameter service })
7777
return new StaticCommandParameterPlan(StaticCommandParameterType.Inject, ResolvedTypeDescriptor.ToSystemType(service.ParameterType));
78+
else if (arg.OriginalExpression.Type == typeof(System.Threading.CancellationToken))
79+
// CancellationToken cannot be serialized from the client - always provide from the request context
80+
return new StaticCommandParameterPlan(StaticCommandParameterType.CurrentCancellationToken, null);
7881
else if (arg.OriginalExpression is ConstantExpression constant)
7982
{
8083
if (constant.Value == method.GetParameters()[index - (method.IsStatic ? 0 : 1)].DefaultValue)

src/Framework/Framework/Hosting/StaticCommandExecutor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ IDotvvmRequestContext context
8080
#pragma warning disable CS0618
8181
(serviceLoader.GetStaticCommandService((Type)a.Arg!, context), null),
8282
#pragma warning restore CS0618
83+
StaticCommandParameterType.CurrentCancellationToken =>
84+
(context.RequestAborted, null),
8385
StaticCommandParameterType.Invocation =>
8486
(await Execute((StaticCommandInvocationPlan)a.Arg!, arguments, argumentValidationPaths, context), null),
8587
_ => throw new NotSupportedException("" + a.Type)

src/Tests/Binding/StaticCommandCompilationTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ public void StaticCommandCompilation_DateTimeResultAssignment()
147147
Assert.AreEqual("{options.viewModel.DateFrom(dotvvm.serialization.serializeDate(await dotvvm.staticCommandPostback(\"XXXX\",[],options),false));}", result);
148148
}
149149

150+
[TestMethod]
151+
public void StaticCommandCompilation_AsyncMethodWithCancellationToken_CancellationTokenIsNotInClientArgs()
152+
{
153+
// CancellationToken should be provided by the server from the request context, not sent from the client
154+
var result = CompileBinding("StaticCommands.GetLengthAsync(StringProp)", niceMode: false, typeof(TestViewModel));
155+
Assert.AreEqual("await dotvvm.staticCommandPostback(\"XXXX\",[options.viewModel.StringProp.state],options)", result);
156+
}
157+
150158
[TestMethod]
151159
public void StaticCommandCompilation_DateTimeAssignment()
152160
{
@@ -461,6 +469,9 @@ public static class StaticCommands
461469

462470
[AllowStaticCommand]
463471
public static DateTime GetDate() => DateTime.UtcNow;
472+
473+
[AllowStaticCommand]
474+
public static Task<int> GetLengthAsync(string str, System.Threading.CancellationToken cancellationToken = default) => Task.FromResult(str.Length);
464475
}
465476

466477
public abstract class TestInnerService<TOutput>

src/Tests/Binding/StaticCommandExecutorTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq.Expressions;
66
using System.Reflection;
77
using System.Text.Json;
8+
using System.Threading;
89
using System.Threading.Tasks;
910
using DotVVM.Framework.Compilation.Binding;
1011
using DotVVM.Framework.Compilation.Javascript;
@@ -247,5 +248,44 @@ public async Task Validation_CustomPrimitives()
247248
internal static void CustomPrimitivesValidation(TestViewModel viewModel, [RegularExpression(@"\d\d7", ErrorMessage = "Vehicle must have lucky number.")] VehicleNumber vehicle)
248249
{
249250
}
251+
252+
[TestMethod]
253+
public async Task CancellationToken_IsInjectedFromContext()
254+
{
255+
var plan = new StaticCommandInvocationPlan(
256+
((Func<CancellationToken, bool>)MethodWithCancellationToken).Method,
257+
[ new StaticCommandParameterPlan(StaticCommandParameterType.CurrentCancellationToken, null) ]
258+
);
259+
var result = await Invoke(plan);
260+
// The method should have been called successfully (not thrown an exception)
261+
Assert.IsTrue((bool)result);
262+
}
263+
264+
[TestMethod]
265+
public async Task CancellationToken_OptionalParameter_IsInjectedFromContext()
266+
{
267+
var plan = new StaticCommandInvocationPlan(
268+
((Func<string, CancellationToken, Task<string>>)MethodWithOptionalCancellationToken).Method,
269+
new[] {
270+
new StaticCommandParameterPlan(StaticCommandParameterType.Argument, typeof(string)),
271+
new StaticCommandParameterPlan(StaticCommandParameterType.CurrentCancellationToken, null)
272+
}
273+
);
274+
var result = await Invoke(plan, ("hello", "/Input"));
275+
Assert.AreEqual("hello", result);
276+
}
277+
278+
[AllowStaticCommand]
279+
internal static bool MethodWithCancellationToken(CancellationToken cancellationToken)
280+
{
281+
return !cancellationToken.IsCancellationRequested;
282+
}
283+
284+
[AllowStaticCommand]
285+
internal static async Task<string> MethodWithOptionalCancellationToken(string input, CancellationToken cancellationToken = default)
286+
{
287+
await Task.Yield();
288+
return input;
289+
}
250290
}
251291
}

src/Tests/Binding/StaticCommandPlanSerializationTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ public static void Method(float arg) { }
115115
public static void NotOverloadedMethod(int arg) { }
116116

117117
public static void MethodNotUsableInStaticCommand() { }
118+
119+
[AllowStaticCommand]
120+
public static Task MethodWithCancellationToken(int arg, System.Threading.CancellationToken cancellationToken) => Task.CompletedTask;
121+
}
122+
123+
[TestMethod]
124+
public void StaticCommandPlanSerialization_CancellationToken_DeserializedPlanIsIdentical()
125+
{
126+
var plan = MakeInvocationPlan(() => StaticCommandMethodCollection.MethodWithCancellationToken(0, default),
127+
new StaticCommandParameterPlan(StaticCommandParameterType.Constant, 42),
128+
new StaticCommandParameterPlan(StaticCommandParameterType.CurrentCancellationToken, null));
129+
var json = StaticCommandExecutionPlanSerializer.SerializePlan(plan);
130+
var deserializedPlan = Deserialize(json);
131+
132+
Assert.AreEqual(plan.Method, deserializedPlan.Method);
133+
Assert.AreEqual(2, deserializedPlan.Arguments.Length);
134+
Assert.AreEqual(StaticCommandParameterType.Constant, deserializedPlan.Arguments[0].Type);
135+
Assert.AreEqual(StaticCommandParameterType.CurrentCancellationToken, deserializedPlan.Arguments[1].Type);
136+
Assert.IsNull(deserializedPlan.Arguments[1].Arg);
118137
}
119138
}
120139
}

0 commit comments

Comments
 (0)