├── .gitattributes
├── .gitignore
├── CodeGenerator
├── CodeGenerator.csproj
└── Program.cs
├── FasterActions.sln
├── FasterActions
├── FasterActions.csproj
├── ParameterBinder.cs
├── Properties
│ └── launchSettings.json
├── RequestDelegateClosure.Generated.cs
├── RequestDelegateClosure.cs
├── RequestDelegateFactory2.cs
└── ResultInvoker.cs
├── README.md
└── Sample
├── Program.cs
├── Properties
└── launchSettings.json
├── Results.cs
├── Sample.csproj
├── TodoApi.cs
├── TodoDbContext.cs
├── appsettings.Development.json
└── appsettings.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Make sh files under the build directory always have LF as line endings
8 | ###############################################################################
9 | *.sh eol=lf
10 |
11 | ###############################################################################
12 | # Make gradlew always have LF as line endings
13 | ###############################################################################
14 | gradlew eol=lf
15 |
16 | ###############################################################################
17 | # Set default behavior for command prompt diff.
18 | #
19 | # This is need for earlier builds of msysgit that does not have it on by
20 | # default for csharp files.
21 | # Note: This is only used by command line
22 | ###############################################################################
23 | #*.cs diff=csharp
24 |
25 | ###############################################################################
26 | # Set the merge driver for project and solution files
27 | #
28 | # Merging from the command prompt will add diff markers to the files if there
29 | # are conflicts (Merging from VS is not affected by the settings below, in VS
30 | # the diff markers are never inserted). Diff markers may cause the following
31 | # file extensions to fail to load in VS. An alternative would be to treat
32 | # these files as binary and thus will always conflict and require user
33 | # intervention with every merge. To do so, just uncomment the entries below
34 | ###############################################################################
35 | #*.sln merge=binary
36 | #*.csproj merge=binary
37 | #*.vbproj merge=binary
38 | #*.vcxproj merge=binary
39 | #*.vcproj merge=binary
40 | #*.dbproj merge=binary
41 | #*.fsproj merge=binary
42 | #*.lsproj merge=binary
43 | #*.wixproj merge=binary
44 | #*.modelproj merge=binary
45 | #*.sqlproj merge=binary
46 | #*.wwaproj merge=binary
47 |
48 | ###############################################################################
49 | # behavior for image files
50 | #
51 | # image files are treated as binary by default.
52 | ###############################################################################
53 | #*.jpg binary
54 | #*.png binary
55 | #*.gif binary
56 |
57 | ###############################################################################
58 | # diff behavior for common document formats
59 | #
60 | # Convert binary document formats to text before diffing them. This feature
61 | # is only available from the command line. Turn it on by uncommenting the
62 | # entries below.
63 | ###############################################################################
64 | #*.doc diff=astextplain
65 | #*.DOC diff=astextplain
66 | #*.docx diff=astextplain
67 | #*.DOCX diff=astextplain
68 | #*.dot diff=astextplain
69 | #*.DOT diff=astextplain
70 | #*.pdf diff=astextplain
71 | #*.PDF diff=astextplain
72 | #*.rtf diff=astextplain
73 | #*.RTF diff=astextplain
74 |
75 | ###############################################################################
76 | # Make sure jQuery files always have LF as line endings (to pass SRI checks)
77 | ###############################################################################
78 | jquery*.js eol=lf
79 | jquery*.map eol=lf
80 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Folders
2 | artifacts/
3 | bin/
4 | obj/
5 | .dotnet/
6 | .nuget/
7 | .packages/
8 | .tools/
9 | .vs/
10 | node_modules/
11 | BenchmarkDotNet.Artifacts/
12 | .gradle/
13 | src/SignalR/clients/**/dist/
14 | modules/
15 | .ionide/
16 |
17 | # File extensions
18 | *.aps
19 | *.binlog
20 | *.dll
21 | *.DS_Store
22 | *.exe
23 | *.idb
24 | *.lib
25 | *.log
26 | *.pch
27 | *.pdb
28 | *.pidb
29 | *.psess
30 | *.res
31 | *.snk
32 | *.so
33 | *.suo
34 | *.tlog
35 | *.user
36 | *.userprefs
37 | *.vspx
38 |
39 | # Specific files, typically generated by tools
40 | msbuild.ProjectImports.zip
41 | StyleCop.Cache
42 | UpgradeLog.htm
43 | .idea
44 | *.svclog
45 | *.db
46 |
--------------------------------------------------------------------------------
/CodeGenerator/CodeGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/CodeGenerator/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace CodeGenerator
5 | {
6 | class Program
7 | {
8 | static void Main(string[] args)
9 | {
10 | Console.WriteLine(new ClosureGenerator().Generate());
11 | }
12 | }
13 |
14 | class ClosureGenerator
15 | {
16 | private readonly StringBuilder _codeBuilder = new StringBuilder();
17 | private int _indent;
18 | private int _column;
19 |
20 | public void Indent()
21 | {
22 | _indent++;
23 | }
24 |
25 | public void Unindent()
26 | {
27 | _indent--;
28 | }
29 |
30 | public string Generate()
31 | {
32 | WriteLine("#nullable enable");
33 | WriteLine("#pragma warning disable CS1998");
34 | Write($@"//------------------------------------------------------------------------------
35 | //
36 | // This code was generated by a tool.
37 | // Runtime Version:{Environment.Version}
38 | //
39 | // Changes to this file may cause incorrect behavior and will be lost if
40 | // the code is regenerated.
41 | //
42 | //------------------------------------------------------------------------------");
43 | WriteLine("");
44 | WriteLine("");
45 |
46 | WriteLine("namespace Microsoft.AspNetCore.Http");
47 | WriteLine("{");
48 | Indent();
49 |
50 | for (int arity = 0; arity <= 16; arity++)
51 | {
52 | GenerateDelegateClosure(arity, hasReturnType: false);
53 | GenerateDelegateClosure(arity, hasReturnType: true);
54 | }
55 |
56 | Unindent();
57 | WriteLine("}"); // namespace
58 |
59 | return _codeBuilder.ToString();
60 | }
61 |
62 | private void GenerateTypeOnlyDelegateClosure(int arity, bool hasReturnType = true)
63 | {
64 | var typeName = hasReturnType ? "TypeOnlyFuncRequestDelegateClosure" : "TypeOnlyActionRequestDelegateClosure";
65 |
66 | Write($"sealed class {typeName}");
67 | WriteGenericParameters(arity, hasReturnType);
68 |
69 | Write(" : Microsoft.AspNetCore.Http.RequestDelegateClosure");
70 | WriteLine();
71 | WriteLine("{");
72 | Indent();
73 | Write("public override bool HasBody => ");
74 | for (int j = 0; j < arity; j++)
75 | {
76 | if (j > 0)
77 | {
78 | Write(" || ");
79 | }
80 | Write($"Microsoft.AspNetCore.Http.ParameterBinder.HasBodyBasedOnType");
81 | }
82 | if (arity == 0)
83 | {
84 | Write("false");
85 | }
86 | Write(";");
87 | WriteLine();
88 | WriteLine();
89 | for (int j = 0; j < arity; j++)
90 | {
91 | WriteLine($"private readonly string _name{j};");
92 | }
93 | Write("private readonly ");
94 | WriteFuncOrActionType(arity, hasReturnType);
95 |
96 | WriteLine(" _delegate;");
97 | WriteLine();
98 | Write($"public {typeName}(");
99 | WriteFuncOrActionType(arity, hasReturnType);
100 |
101 | WriteLine(" @delegate, System.Reflection.ParameterInfo[] parameters)");
102 | WriteLine("{");
103 | Indent();
104 | WriteLine("_delegate = @delegate;");
105 | for (int j = 0; j < arity; j++)
106 | {
107 | WriteLine($"_name{j} = parameters[{j}].Name!;");
108 | }
109 | Unindent();
110 | WriteLine("}"); //ctor
111 |
112 | WriteLine();
113 | WriteLine("public override System.Threading.Tasks.Task ProcessRequestAsync(Microsoft.AspNetCore.Http.HttpContext httpContext)");
114 | WriteLine("{");
115 | Indent();
116 | for (int j = 0; j < arity; j++)
117 | {
118 | WriteLine($"if (!Microsoft.AspNetCore.Http.ParameterBinder.TryBindValueBasedOnType(httpContext, _name{j}, out var arg{j}))");
119 | WriteLine("{");
120 | Indent();
121 | WriteLine($"Microsoft.AspNetCore.Http.ParameterLog.ParameterBindingFailed(httpContext, _name{j});");
122 | WriteLine("httpContext.Response.StatusCode = 400;");
123 | WriteLine("return System.Threading.Tasks.Task.CompletedTask;");
124 | Unindent();
125 | WriteLine("}");
126 | WriteLine();
127 | }
128 |
129 | WriteDelegateCall(arity, hasReturnType);
130 |
131 | WriteLine();
132 |
133 | if (hasReturnType)
134 | {
135 | WriteLine("return Microsoft.AspNetCore.Http.ResultInvoker.Instance.Invoke(httpContext, result);");
136 | }
137 | else
138 | {
139 | WriteLine("return System.Threading.Tasks.Task.CompletedTask;");
140 | }
141 |
142 | Unindent();
143 | WriteLine("}"); // ProcessRequestAsync
144 |
145 | WriteLine();
146 | WriteLine("public override async System.Threading.Tasks.Task ProcessRequestWithBodyAsync(Microsoft.AspNetCore.Http.HttpContext httpContext)");
147 | WriteLine("{");
148 | Indent();
149 |
150 | if (arity > 0)
151 | {
152 | WriteLine("var success = false;");
153 | }
154 |
155 | for (int j = 0; j < arity; j++)
156 | {
157 | WriteLine($"(T{j}? arg{j}, success) = await Microsoft.AspNetCore.Http.ParameterBinder.BindBodyBasedOnType(httpContext, _name{j});");
158 | WriteLine();
159 | WriteLine("if (!success)");
160 | WriteLine("{");
161 | Indent();
162 | WriteLine($"Microsoft.AspNetCore.Http.ParameterLog.ParameterBindingFailed(httpContext, _name{j});");
163 | WriteLine("httpContext.Response.StatusCode = 400;");
164 | WriteLine("return;");
165 | Unindent();
166 | WriteLine("}");
167 | WriteLine();
168 | }
169 |
170 | WriteDelegateCall(arity, hasReturnType);
171 |
172 | if (hasReturnType)
173 | {
174 | WriteLine();
175 | WriteLine("await Microsoft.AspNetCore.Http.ResultInvoker.Instance.Invoke(httpContext, result);");
176 | }
177 |
178 | Unindent();
179 | WriteLine("}");
180 |
181 | Unindent();
182 | WriteLine("}");
183 | WriteLine();
184 | }
185 |
186 | private void GenerateDelegateClosure(int arity, bool hasReturnType = false)
187 | {
188 | var typeName = hasReturnType ? "FuncRequestDelegateClosure" : "ActionRequestDelegateClosure";
189 |
190 | Write($"sealed class {typeName}");
191 | WriteGenericParameters(arity, hasReturnType, writeDam: true);
192 | Write(" : Microsoft.AspNetCore.Http.RequestDelegateClosure");
193 | WriteLine();
194 | WriteLine("{");
195 | Indent();
196 | Write("public override bool HasBody => ");
197 | for (int j = 0; j < arity; j++)
198 | {
199 | if (j > 0)
200 | {
201 | Write(" || ");
202 | }
203 | Write($"_parameterBinder{j}.IsBody");
204 | }
205 | if (arity == 0)
206 | {
207 | Write("false");
208 | }
209 | Write(";");
210 | WriteLine();
211 | WriteLine();
212 | for (int j = 0; j < arity; j++)
213 | {
214 | WriteLine($"private readonly Microsoft.AspNetCore.Http.ParameterBinder _parameterBinder{j};");
215 | }
216 | Write("private readonly ");
217 | WriteFuncOrActionType(arity, hasReturnType);
218 | WriteLine(" _delegate;");
219 | WriteLine();
220 | Write($"public {typeName}(");
221 | WriteFuncOrActionType(arity, hasReturnType);
222 | WriteLine(" @delegate, System.Reflection.ParameterInfo[] parameters, System.IServiceProvider serviceProvider)");
223 | WriteLine("{");
224 | Indent();
225 | WriteLine("_delegate = @delegate;");
226 |
227 | for (int j = 0; j < arity; j++)
228 | {
229 | WriteLine($"_parameterBinder{j} = Microsoft.AspNetCore.Http.ParameterBinder.Create(parameters[{j}], serviceProvider);");
230 | }
231 | Unindent();
232 | WriteLine("}"); //ctor
233 |
234 | WriteLine();
235 | WriteLine("public override System.Threading.Tasks.Task ProcessRequestAsync(Microsoft.AspNetCore.Http.HttpContext httpContext)");
236 | WriteLine("{");
237 | Indent();
238 | for (int j = 0; j < arity; j++)
239 | {
240 | WriteLine($"if (!_parameterBinder{j}.TryBindValue(httpContext, out var arg{j}))");
241 | WriteLine("{");
242 | Indent();
243 | WriteLine($"Microsoft.AspNetCore.Http.ParameterLog.ParameterBindingFailed(httpContext, _parameterBinder{j});");
244 | WriteLine("httpContext.Response.StatusCode = 400;");
245 | WriteLine("return System.Threading.Tasks.Task.CompletedTask;");
246 | Unindent();
247 | WriteLine("}");
248 | WriteLine();
249 | }
250 |
251 | WriteDelegateCall(arity, hasReturnType);
252 |
253 | WriteLine();
254 |
255 | if (hasReturnType)
256 | {
257 | WriteLine("return Microsoft.AspNetCore.Http.ResultInvoker.Instance.Invoke(httpContext, result);");
258 | }
259 | else
260 | {
261 | WriteLine("return System.Threading.Tasks.Task.CompletedTask;");
262 | }
263 |
264 | Unindent();
265 | WriteLine("}"); // ProcessRequestAsync
266 |
267 | WriteLine();
268 | WriteLine("public override async System.Threading.Tasks.Task ProcessRequestWithBodyAsync(Microsoft.AspNetCore.Http.HttpContext httpContext)");
269 | WriteLine("{");
270 | Indent();
271 |
272 | if (arity > 0)
273 | {
274 | WriteLine("var success = false;");
275 | }
276 |
277 | for (int j = 0; j < arity; j++)
278 | {
279 | WriteLine($"(T{j}? arg{j}, success) = await _parameterBinder{j}.BindBodyOrValueAsync(httpContext);");
280 | WriteLine();
281 | WriteLine("if (!success)");
282 | WriteLine("{");
283 | Indent();
284 | WriteLine($"Microsoft.AspNetCore.Http.ParameterLog.ParameterBindingFailed(httpContext, _parameterBinder{j});");
285 | WriteLine("httpContext.Response.StatusCode = 400;");
286 | WriteLine("return;");
287 | Unindent();
288 | WriteLine("}");
289 | WriteLine();
290 | }
291 |
292 | WriteDelegateCall(arity, hasReturnType);
293 |
294 | if (hasReturnType)
295 | {
296 | WriteLine();
297 | WriteLine("await Microsoft.AspNetCore.Http.ResultInvoker.Instance.Invoke(httpContext, result);");
298 | }
299 |
300 | Unindent();
301 | WriteLine("}");
302 |
303 | Unindent();
304 | WriteLine("}");
305 | WriteLine();
306 | }
307 |
308 | private void WriteGenericParameters(int arity, bool hasReturnType, bool writeDam = false)
309 | {
310 | if (arity > 0 || hasReturnType)
311 | {
312 | Write("<");
313 | }
314 |
315 | for (int j = 0; j < arity; j++)
316 | {
317 | if (j > 0)
318 | {
319 | Write(", ");
320 | }
321 | if (writeDam)
322 | {
323 | Write("[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)]");
324 | }
325 | Write($"T{j}");
326 | }
327 | if (hasReturnType)
328 | {
329 | if (arity == 0)
330 | {
331 | if (writeDam)
332 | {
333 | Write("[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)]");
334 | }
335 | Write("R>");
336 | }
337 | else
338 | {
339 | Write(", ");
340 | if (writeDam)
341 | {
342 | Write("[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)]");
343 | }
344 | Write("R>");
345 | }
346 | }
347 | else
348 | {
349 | if (arity > 0)
350 | {
351 | Write(">");
352 | }
353 | }
354 | }
355 |
356 | private void WriteDelegateCall(int arity, bool hasReturnType)
357 | {
358 | if (hasReturnType)
359 | {
360 | Write("R? result = _delegate(");
361 | }
362 | else
363 | {
364 | Write("_delegate(");
365 | }
366 | for (int j = 0; j < arity; j++)
367 | {
368 | if (j > 0)
369 | {
370 | Write(", ");
371 | }
372 | Write($"arg{j}!");
373 | }
374 | WriteLine(");");
375 | }
376 |
377 | private void WriteFuncOrActionType(int arity, bool hasReturnType)
378 | {
379 | if (hasReturnType)
380 | {
381 | Write("System.Func<");
382 | }
383 | else
384 | {
385 | Write("System.Action");
386 | if (arity > 0)
387 | {
388 | Write("<");
389 | }
390 | }
391 | for (int j = 0; j < arity; j++)
392 | {
393 | if (j > 0)
394 | {
395 | Write(", ");
396 | }
397 | Write($"T{j}");
398 | }
399 |
400 | if (hasReturnType)
401 | {
402 | if (arity == 0)
403 | {
404 | Write("R?>");
405 | }
406 | else
407 | {
408 | Write(", R?>");
409 | }
410 | }
411 | else
412 | {
413 | if (arity > 0)
414 | {
415 | Write(">");
416 | }
417 | }
418 | }
419 |
420 | private void WriteLine()
421 | {
422 | WriteLine("");
423 | }
424 |
425 | private void WriteLineNoIndent(string value)
426 | {
427 | _codeBuilder.AppendLine(value);
428 | }
429 |
430 | private void WriteNoIndent(string value)
431 | {
432 | _codeBuilder.Append(value);
433 | }
434 |
435 | private void Write(string value)
436 | {
437 | if (_indent > 0 && _column == 0)
438 | {
439 | _codeBuilder.Append(new string(' ', _indent * 4));
440 | }
441 | _codeBuilder.Append(value);
442 | _column += value.Length;
443 | }
444 |
445 | private void WriteLine(string value)
446 | {
447 | if (_indent > 0 && _column == 0)
448 | {
449 | _codeBuilder.Append(new string(' ', _indent * 4));
450 | }
451 | _codeBuilder.AppendLine(value);
452 | _column = 0;
453 | }
454 | }
455 | }
456 |
--------------------------------------------------------------------------------
/FasterActions.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31306.274
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeGenerator", "CodeGenerator\CodeGenerator.csproj", "{05EB56F9-F501-4CF7-864A-44D050222919}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FasterActions", "FasterActions\FasterActions.csproj", "{D70ABCE9-E444-4489-8D8A-AC083711FF1F}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "Sample\Sample.csproj", "{F01F47B0-EB37-46BE-B9C8-5BB93B6DFB0C}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {05EB56F9-F501-4CF7-864A-44D050222919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {05EB56F9-F501-4CF7-864A-44D050222919}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {05EB56F9-F501-4CF7-864A-44D050222919}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {05EB56F9-F501-4CF7-864A-44D050222919}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {D70ABCE9-E444-4489-8D8A-AC083711FF1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {D70ABCE9-E444-4489-8D8A-AC083711FF1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {D70ABCE9-E444-4489-8D8A-AC083711FF1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {D70ABCE9-E444-4489-8D8A-AC083711FF1F}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {F01F47B0-EB37-46BE-B9C8-5BB93B6DFB0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {F01F47B0-EB37-46BE-B9C8-5BB93B6DFB0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {F01F47B0-EB37-46BE-B9C8-5BB93B6DFB0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {F01F47B0-EB37-46BE-B9C8-5BB93B6DFB0C}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {F8E8B578-BB40-4EE8-994D-155019611676}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/FasterActions/FasterActions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | preview
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/FasterActions/ParameterBinder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Runtime.CompilerServices;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using Microsoft.AspNetCore.Http.Metadata;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Logging;
12 |
13 | namespace Microsoft.AspNetCore.Http
14 | {
15 | ///
16 | /// represents 2 kinds of parameters:
17 | /// 1. Ones that are fast and synchronous. This can be something like reading something
18 | /// that's pre-materialized on the HttpContext (query string, header, route value etc). (invoked via BindValue)
19 | /// 2. Ones that are asynchronous and potentially IO bound. An example is reading a JSON body
20 | /// from an http request (or reading a file). (invoked via BindBodyAsync)
21 | ///
22 | public abstract class ParameterBinder<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]T>
23 | {
24 | // Try parse methdods that may be defined on T
25 | private delegate bool TryParse(string s, out T value);
26 |
27 | public abstract bool IsBody { get; }
28 | public abstract string Name { get; }
29 |
30 | public abstract bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value);
31 | public abstract ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext);
32 |
33 | private static readonly TryParse? _tryParse = FindTryParseMethod();
34 |
35 | // This needs to be inlinable in order for the JIT to see the newobj call in order
36 | // to enable devirtualization the method might currently be too big for this...
37 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
38 | public static ParameterBinder Create(ParameterInfo parameterInfo, IServiceProvider serviceProvider)
39 | {
40 | var parameterCustomAttributes = Attribute.GetCustomAttributes(parameterInfo);
41 |
42 | // No attributes fast path
43 | if (parameterCustomAttributes.Length == 0)
44 | {
45 | return GetParameterBinderBaseOnType(parameterInfo, serviceProvider);
46 | }
47 |
48 | return GetBinderBaseOnAttributes(parameterInfo, parameterCustomAttributes, serviceProvider);
49 | }
50 |
51 | [MethodImpl(MethodImplOptions.NoInlining)]
52 | private static ParameterBinder GetBinderBaseOnAttributes(ParameterInfo parameterInfo, Attribute[] parameterCustomAttributes, IServiceProvider serviceProvider)
53 | {
54 | if (parameterCustomAttributes.OfType().FirstOrDefault() is { } routeAttribute)
55 | {
56 | return new RouteParameterBinder(routeAttribute.Name ?? parameterInfo.Name!);
57 | }
58 | else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } queryAttribute)
59 | {
60 | return new QueryParameterBinder(queryAttribute.Name ?? parameterInfo.Name!);
61 | }
62 | else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } headerAttribute)
63 | {
64 | return new HeaderParameterBinder(headerAttribute.Name ?? parameterInfo.Name!);
65 | }
66 | else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } bodyAttribute)
67 | {
68 | return new BodyParameterBinder(parameterInfo.Name!, bodyAttribute.AllowEmpty);
69 | }
70 | else if (parameterCustomAttributes.Any(a => a is IFromServiceMetadata))
71 | {
72 | return new ServicesParameterBinder(parameterInfo.Name!);
73 | }
74 |
75 | return GetParameterBinderBaseOnType(parameterInfo, serviceProvider);
76 | }
77 |
78 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
79 | private static ParameterBinder GetParameterBinderBaseOnType(ParameterInfo parameterInfo, IServiceProvider serviceProvider)
80 | {
81 | if (typeof(T) == typeof(string) ||
82 | typeof(T) == typeof(byte) ||
83 | typeof(T) == typeof(short) ||
84 | typeof(T) == typeof(int) ||
85 | typeof(T) == typeof(long) ||
86 | typeof(T) == typeof(decimal) ||
87 | typeof(T) == typeof(double) ||
88 | typeof(T) == typeof(float) ||
89 | typeof(T) == typeof(Guid) ||
90 | typeof(T) == typeof(DateTime) ||
91 | typeof(T) == typeof(DateTimeOffset))
92 | {
93 | return new RouteOrQueryParameterBinder(parameterInfo.Name!);
94 | }
95 | else if (typeof(T) == typeof(HttpContext))
96 | {
97 | return new HttpContextParameterBinder(parameterInfo.Name!);
98 | }
99 | else if (typeof(T) == typeof(CancellationToken))
100 | {
101 | return new CancellationTokenParameterBinder(parameterInfo.Name!);
102 | }
103 | else if (typeof(T).IsEnum || _tryParse != null) // Slow fallback for unknown types
104 | {
105 | return new RouteOrQueryParameterBinder(parameterInfo.Name!);
106 | }
107 | else if (serviceProvider.GetService() is IServiceProviderIsService serviceProviderIsService && serviceProviderIsService.IsService(typeof(T)))
108 | {
109 | return new ServicesParameterBinder(parameterInfo.Name!);
110 | }
111 |
112 | return new BodyParameterBinder(parameterInfo.Name!, allowEmpty: false);
113 | }
114 |
115 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
116 | public static bool TryParseValue(string rawValue, [MaybeNullWhen(false)] out T value)
117 | {
118 | if (typeof(T) == typeof(string))
119 | {
120 | value = (T)(object)rawValue;
121 | return true;
122 | }
123 |
124 | if (typeof(T) == typeof(byte))
125 | {
126 | bool result = byte.TryParse(rawValue, out var parsedValue);
127 | value = (T)(object)parsedValue;
128 | return result;
129 | }
130 |
131 | if (typeof(T) == typeof(byte?))
132 | {
133 | if (byte.TryParse(rawValue, out var parsedValue))
134 | {
135 | value = (T)(object)parsedValue;
136 | return true;
137 | }
138 |
139 | value = default;
140 | return false;
141 | }
142 |
143 | if (typeof(T) == typeof(short))
144 | {
145 | bool result = short.TryParse(rawValue, out var parsedValue);
146 | value = (T)(object)parsedValue;
147 | return result;
148 | }
149 |
150 | if (typeof(T) == typeof(short?))
151 | {
152 | if (short.TryParse(rawValue, out var parsedValue))
153 | {
154 | value = (T)(object)parsedValue;
155 | return true;
156 | }
157 |
158 | value = default;
159 | return false;
160 | }
161 |
162 | if (typeof(T) == typeof(int))
163 | {
164 | bool result = int.TryParse(rawValue, out var parsedValue);
165 | value = (T)(object)parsedValue;
166 | return result;
167 | }
168 |
169 | if (typeof(T) == typeof(int?))
170 | {
171 | if (int.TryParse(rawValue, out var parsedValue))
172 | {
173 | value = (T)(object)parsedValue;
174 | return true;
175 | }
176 |
177 | value = default;
178 | return false;
179 | }
180 |
181 | if (typeof(T) == typeof(long))
182 | {
183 | bool result = long.TryParse(rawValue, out var parsedValue);
184 | value = (T)(object)parsedValue;
185 | return result;
186 | }
187 |
188 | if (typeof(T) == typeof(long?))
189 | {
190 | if (long.TryParse(rawValue, out var parsedValue))
191 | {
192 | value = (T)(object)parsedValue;
193 | return true;
194 | }
195 |
196 | value = default;
197 | return false;
198 | }
199 |
200 | if (typeof(T) == typeof(double))
201 | {
202 | bool result = double.TryParse(rawValue, out var parsedValue);
203 | value = (T)(object)parsedValue;
204 | return result;
205 | }
206 |
207 | if (typeof(T) == typeof(double?))
208 | {
209 | if (double.TryParse(rawValue, out var parsedValue))
210 | {
211 | value = (T)(object)parsedValue;
212 | return true;
213 | }
214 |
215 | value = default;
216 | return false;
217 | }
218 |
219 | if (typeof(T) == typeof(float))
220 | {
221 | bool result = float.TryParse(rawValue, out var parsedValue);
222 | value = (T)(object)parsedValue;
223 | return result;
224 | }
225 |
226 | if (typeof(T) == typeof(float?))
227 | {
228 | if (float.TryParse(rawValue, out var parsedValue))
229 | {
230 | value = (T)(object)parsedValue;
231 | return true;
232 | }
233 |
234 | value = default;
235 | return false;
236 | }
237 |
238 | if (typeof(T) == typeof(decimal))
239 | {
240 | bool result = decimal.TryParse(rawValue, out var parsedValue);
241 | value = (T)(object)parsedValue;
242 | return result;
243 | }
244 |
245 | if (typeof(T) == typeof(decimal?))
246 | {
247 | if (decimal.TryParse(rawValue, out var parsedValue))
248 | {
249 | value = (T)(object)parsedValue;
250 | return true;
251 | }
252 |
253 | value = default;
254 | return false;
255 | }
256 |
257 | if (typeof(T) == typeof(Guid))
258 | {
259 | bool result = Guid.TryParse(rawValue, out var parsedValue);
260 | value = (T)(object)parsedValue;
261 | return result;
262 | }
263 |
264 | if (typeof(T) == typeof(Guid?))
265 | {
266 | if (Guid.TryParse(rawValue, out var parsedValue))
267 | {
268 | value = (T)(object)parsedValue;
269 | return true;
270 | }
271 |
272 | value = default;
273 | return false;
274 | }
275 |
276 | if (typeof(T) == typeof(DateTime))
277 | {
278 | bool result = DateTime.TryParse(rawValue, out var parsedValue);
279 | value = (T)(object)parsedValue;
280 | return result;
281 | }
282 |
283 | if (typeof(T) == typeof(DateTime?))
284 | {
285 | if (DateTime.TryParse(rawValue, out var parsedValue))
286 | {
287 | value = (T)(object)parsedValue;
288 | return true;
289 | }
290 |
291 | value = default;
292 | return false;
293 | }
294 |
295 | if (typeof(T) == typeof(DateTimeOffset))
296 | {
297 | bool result = DateTimeOffset.TryParse(rawValue, out var parsedValue);
298 | value = (T)(object)parsedValue;
299 | return result;
300 | }
301 |
302 | if (typeof(T) == typeof(DateTimeOffset?))
303 | {
304 | if (DateTimeOffset.TryParse(rawValue, out var parsedValue))
305 | {
306 | value = (T)(object)parsedValue;
307 | return true;
308 | }
309 |
310 | value = default;
311 | return false;
312 | }
313 |
314 | if (typeof(T).IsEnum)
315 | {
316 | // This fails because we don't have the the right generic constraints for T
317 | // return Enum.TryParse(rawValue, out value);
318 |
319 | // This unforunately does boxing :(
320 | if (Enum.TryParse(typeof(T), rawValue, out var result))
321 | {
322 | value = (T?)result;
323 | return value != null;
324 | }
325 |
326 | value = default;
327 | return false;
328 | }
329 |
330 | if (_tryParse == null)
331 | {
332 | value = default;
333 | return false;
334 | }
335 |
336 | return _tryParse(rawValue, out value);
337 | }
338 |
339 | private static TryParse? FindTryParseMethod()
340 | {
341 | var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
342 |
343 | var methodInfo = type.GetMethod("TryParse", BindingFlags.Public | BindingFlags.Static, new[] { typeof(string), type.MakeByRefType() });
344 |
345 | if (methodInfo != null)
346 | {
347 | return methodInfo.CreateDelegate();
348 | }
349 |
350 | return null;
351 | }
352 | }
353 |
354 | sealed class RouteParameterBinder : ParameterBinder
355 | {
356 | public RouteParameterBinder(string name)
357 | {
358 | Name = name;
359 | }
360 |
361 | public override bool IsBody => false;
362 |
363 | public override string Name { get; }
364 |
365 | public override ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext)
366 | {
367 | bool success = TryBindValue(httpContext, out var value);
368 |
369 | return new((value, success));
370 | }
371 |
372 | public override bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value)
373 | {
374 | var rawValue = httpContext.Request.RouteValues[Name]?.ToString() ?? "";
375 |
376 | return TryParseValue(rawValue, out value);
377 | }
378 | }
379 |
380 | sealed class RouteOrQueryParameterBinder : ParameterBinder
381 | {
382 | public RouteOrQueryParameterBinder(string name)
383 | {
384 | Name = name;
385 | }
386 |
387 | public override bool IsBody => false;
388 |
389 | public override string Name { get; }
390 |
391 | public override ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext)
392 | {
393 | bool success = TryBindValue(httpContext, out var value);
394 |
395 | return new((value, success));
396 | }
397 |
398 | public override bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value)
399 | {
400 | return TryBindValue(httpContext, Name, out value);
401 | }
402 |
403 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
404 | public static bool TryBindValue(HttpContext httpContext, string name, [MaybeNullWhen(false)] out T value)
405 | {
406 | var rawValue = httpContext.Request.RouteValues[name]?.ToString() ?? httpContext.Request.Query[name].ToString();
407 |
408 | return TryParseValue(rawValue, out value);
409 | }
410 | }
411 |
412 | sealed class QueryParameterBinder : ParameterBinder
413 | {
414 | public QueryParameterBinder(string name)
415 | {
416 | Name = name;
417 | }
418 |
419 | public override bool IsBody => false;
420 |
421 | public override string Name { get; }
422 |
423 | public override ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext)
424 | {
425 | bool success = TryBindValue(httpContext, out var value);
426 |
427 | return new((value, success));
428 | }
429 |
430 | public override bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value)
431 | {
432 | var rawValue = httpContext.Request.Query[Name].ToString();
433 |
434 | return TryParseValue(rawValue, out value);
435 | }
436 | }
437 |
438 | sealed class HeaderParameterBinder : ParameterBinder
439 | {
440 | public HeaderParameterBinder(string name)
441 | {
442 | Name = name;
443 | }
444 |
445 | public override bool IsBody => false;
446 |
447 | public override string Name { get; }
448 |
449 | public override ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext)
450 | {
451 | bool success = TryBindValue(httpContext, out var value);
452 |
453 | return new((value, success));
454 | }
455 |
456 | public override bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value)
457 | {
458 | var rawValue = httpContext.Request.Query[Name].ToString();
459 |
460 | return TryParseValue(rawValue, out value);
461 | }
462 | }
463 |
464 | sealed class ServicesParameterBinder : ParameterBinder
465 | {
466 | public ServicesParameterBinder(string name)
467 | {
468 | Name = name;
469 | }
470 |
471 | public override bool IsBody => false;
472 |
473 | public override string Name { get; }
474 |
475 | public override ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext)
476 | {
477 | #pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
478 | return new((httpContext.RequestServices.GetRequiredService(), true));
479 | #pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
480 | }
481 |
482 | public override bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value)
483 | {
484 | #pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
485 | value = httpContext.RequestServices.GetRequiredService();
486 | #pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
487 | return true;
488 | }
489 |
490 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
491 | public static bool TryBindValue(HttpContext httpContext, string name, [MaybeNullWhen(false)] out T value)
492 | {
493 | #pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
494 | value = httpContext.RequestServices.GetRequiredService();
495 | #pragma warning restore CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
496 | return true;
497 | }
498 | }
499 |
500 | sealed class HttpContextParameterBinder : ParameterBinder
501 | {
502 | public HttpContextParameterBinder(string name)
503 | {
504 | Name = name;
505 | }
506 |
507 | public override bool IsBody => false;
508 |
509 | public override string Name { get; }
510 |
511 | public override ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext)
512 | {
513 | return new(((T)(object)httpContext, true));
514 | }
515 |
516 | public override bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value)
517 | {
518 | value = (T)(object)httpContext;
519 | return true;
520 | }
521 |
522 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
523 | public static bool TryBindValue(HttpContext httpContext, string name, [MaybeNullWhen(false)] out T value)
524 | {
525 | value = (T)(object)httpContext;
526 | return true;
527 | }
528 | }
529 |
530 | sealed class CancellationTokenParameterBinder : ParameterBinder
531 | {
532 | public CancellationTokenParameterBinder(string name)
533 | {
534 | Name = name;
535 | }
536 |
537 | public override bool IsBody => false;
538 |
539 | public override string Name { get; }
540 |
541 | public override ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext)
542 | {
543 | return new(((T)(object)httpContext.RequestAborted, true));
544 | }
545 |
546 | public override bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value)
547 | {
548 | value = (T)(object)httpContext.RequestAborted;
549 | return true;
550 | }
551 |
552 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
553 | public static bool TryBindValue(HttpContext httpContext, string name, [MaybeNullWhen(false)] out T value)
554 | {
555 | value = (T)(object)httpContext.RequestAborted;
556 | return true;
557 | }
558 | }
559 |
560 | sealed class BodyParameterBinder : ParameterBinder
561 | {
562 | private readonly bool _allowEmpty;
563 |
564 | public BodyParameterBinder(string name, bool allowEmpty)
565 | {
566 | Name = name;
567 | _allowEmpty = allowEmpty;
568 | }
569 |
570 | public override bool IsBody => true;
571 |
572 | public override string Name { get; }
573 |
574 | public override ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext)
575 | {
576 | if (_allowEmpty && httpContext.Request.ContentLength == 0)
577 | {
578 | return new((default, true));
579 | }
580 |
581 | return BindBodyOrValueAsync(httpContext, default);
582 | }
583 |
584 | public static async ValueTask<(T?, bool)> BindBodyOrValueAsync(HttpContext httpContext, string? name)
585 | {
586 | try
587 | {
588 | return (await httpContext.Request.ReadFromJsonAsync(), true);
589 | }
590 | catch (IOException ex)
591 | {
592 | Log.RequestBodyIOException(httpContext, ex);
593 | return (default, false);
594 | }
595 | catch (InvalidDataException ex)
596 | {
597 | Log.RequestBodyInvalidDataException(httpContext, ex);
598 | return (default, false);
599 | }
600 | }
601 |
602 | public override bool TryBindValue(HttpContext httpContext, [MaybeNullWhen(false)] out T value)
603 | {
604 | throw new NotSupportedException("Synchronous value binding isn't supported");
605 | }
606 |
607 | private static class Log
608 | {
609 | private static readonly Action _requestBodyIOException = LoggerMessage.Define(
610 | LogLevel.Debug,
611 | new EventId(1, "RequestBodyIOException"),
612 | "Reading the request body failed with an IOException.");
613 |
614 | private static readonly Action _requestBodyInvalidDataException = LoggerMessage.Define(
615 | LogLevel.Debug,
616 | new EventId(2, "RequestBodyInvalidDataException"),
617 | "Reading the request body failed with an InvalidDataException.");
618 |
619 | private static readonly Action _parameterBindingFailed = LoggerMessage.Define(
620 | LogLevel.Debug,
621 | new EventId(3, "ParamaterBindingFailed"),
622 | @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}"".");
623 |
624 | public static void RequestBodyIOException(HttpContext httpContext, IOException exception)
625 | {
626 | _requestBodyIOException(GetLogger(httpContext), exception);
627 | }
628 |
629 | public static void RequestBodyInvalidDataException(HttpContext httpContext, InvalidDataException exception)
630 | {
631 | _requestBodyInvalidDataException(GetLogger(httpContext), exception);
632 | }
633 |
634 | public static void ParameterBindingFailed(HttpContext httpContext, ParameterBinder binder)
635 | {
636 | _parameterBindingFailed(GetLogger(httpContext), typeof(T).Name, binder.Name, "", null);
637 | }
638 |
639 | private static ILogger GetLogger(HttpContext httpContext)
640 | {
641 | var loggerFactory = httpContext.RequestServices.GetRequiredService();
642 | return loggerFactory.CreateLogger(typeof(RequestDelegateFactory));
643 | }
644 | }
645 | }
646 | }
647 |
--------------------------------------------------------------------------------
/FasterActions/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:49676/",
7 | "sslPort": 44392
8 | }
9 | },
10 | "profiles": {
11 | "FasterActions": {
12 | "commandName": "Project",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | },
17 | "applicationUrl": "https://localhost:5001;http://localhost:5000"
18 | },
19 | "IIS Express": {
20 | "commandName": "IISExpress",
21 | "launchBrowser": true,
22 | "environmentVariables": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | }
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/FasterActions/RequestDelegateClosure.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using System;
4 | using System.Reflection;
5 | using System.Threading.Tasks;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace Microsoft.AspNetCore.Http
10 | {
11 | ///
12 | /// This type captures the state required to execute the request. Processing requests with and without the body
13 | /// are separated as an optimization step.
14 | ///
15 | public abstract class RequestDelegateClosure
16 | {
17 | public abstract bool HasBody { get; }
18 |
19 | public abstract Task ProcessRequestAsync(HttpContext httpContext);
20 | public abstract Task ProcessRequestWithBodyAsync(HttpContext httpContext);
21 | }
22 |
23 | internal static class ParameterLog
24 | {
25 | private static readonly Action _parameterBindingFailed = LoggerMessage.Define(
26 | LogLevel.Debug,
27 | new EventId(3, "ParamaterBindingFailed"),
28 | @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}"".");
29 |
30 | public static void ParameterBindingFailed(HttpContext httpContext, ParameterBinder binder)
31 | {
32 | _parameterBindingFailed(GetLogger(httpContext), typeof(T).Name, binder.Name, "", null);
33 | }
34 |
35 | public static void ParameterBindingFailed(HttpContext httpContext, string name)
36 | {
37 | _parameterBindingFailed(GetLogger(httpContext), typeof(T).Name, name, "", null);
38 | }
39 |
40 | private static ILogger GetLogger(HttpContext httpContext)
41 | {
42 | var loggerFactory = httpContext.RequestServices.GetRequiredService();
43 | return loggerFactory.CreateLogger(typeof(RequestDelegateFactory));
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/FasterActions/RequestDelegateFactory2.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using System;
4 | using System.Reflection;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Http.Metadata;
7 |
8 | namespace Microsoft.AspNetCore.Http
9 | {
10 | public static partial class RequestDelegateFactory2
11 | {
12 | public static RequestDelegate CreateRequestDelegate(Action func, IServiceProvider serviceProvider)
13 | {
14 | return CreateRequestDelegateCore(new ActionRequestDelegateClosure(func, func.Method.GetParameters(), serviceProvider));
15 | }
16 |
17 | public static RequestDelegate CreateRequestDelegate(Func func, IServiceProvider serviceProvider)
18 | {
19 | return CreateRequestDelegateCore(new FuncRequestDelegateClosure(func, func.Method.GetParameters(), serviceProvider));
20 | }
21 |
22 | public static RequestDelegate CreateRequestDelegate(Func func, IServiceProvider serviceProvider)
23 | {
24 | var parameters = func.Method.GetParameters();
25 |
26 | RequestDelegateClosure closure = new FuncRequestDelegateClosure(func, parameters, serviceProvider);
27 |
28 | return CreateRequestDelegateCore(closure);
29 | }
30 |
31 | public static RequestDelegate CreateRequestDelegate(Func func, IServiceProvider serviceProvider)
32 | {
33 | var parameters = func.Method.GetParameters();
34 |
35 | RequestDelegateClosure closure = new FuncRequestDelegateClosure(func, parameters, serviceProvider);
36 |
37 | return CreateRequestDelegateCore(closure);
38 | }
39 |
40 | private static bool HasBindingAttributes(ParameterInfo[] parameterInfos)
41 | {
42 | foreach (var parameterInfo in parameterInfos)
43 | {
44 | foreach (var a in Attribute.GetCustomAttributes(parameterInfo))
45 | {
46 | if (a is IFromRouteMetadata or IFromQueryMetadata or IFromHeaderMetadata or IFromServiceMetadata or IFromBodyMetadata)
47 | {
48 | return true;
49 | }
50 | }
51 | }
52 |
53 | return false;
54 | }
55 |
56 | // This overload isn't linker friendly
57 | public static RequestDelegate CreateRequestDelegate(MethodInfo method, IServiceProvider serviceProvider)
58 | {
59 | var parameters = method.GetParameters();
60 | var parameterTypes = new Type[parameters.Length];
61 | bool hasAttributes = HasBindingAttributes(parameters);
62 | for (int i = 0; i < parameterTypes.Length; i++)
63 | {
64 | parameterTypes[i] = parameters[i].ParameterType;
65 | }
66 |
67 | // This won't be needed in real life
68 | RequestDelegateClosure closure = new DefaultClosure();
69 |
70 | // We will support up to 16 arguments, then we'll give up
71 |
72 | if (parameters.Length > 16) throw new NotSupportedException("More than 16 arguments isn't supported");
73 |
74 | bool hasReturnType = method.ReturnType != typeof(void);
75 |
76 | var methodInvokerTypes = new Type[hasReturnType ? parameters.Length + 1 : parameters.Length];
77 | parameterTypes.CopyTo(methodInvokerTypes, 0);
78 |
79 | if (hasReturnType)
80 | {
81 | methodInvokerTypes[^1] = method.ReturnType;
82 |
83 | if (parameterTypes.Length == 0)
84 | {
85 | var type = typeof(FuncRequestDelegateClosure<>).MakeGenericType(methodInvokerTypes);
86 |
87 | var @delegate = method.CreateDelegate(typeof(Func<>).MakeGenericType(methodInvokerTypes));
88 |
89 | closure = (RequestDelegateClosure)Activator.CreateInstance(type, parameters, @delegate, serviceProvider)!;
90 | }
91 | else if (parameterTypes.Length == 1)
92 | {
93 | var type = typeof(FuncRequestDelegateClosure<,>).MakeGenericType(methodInvokerTypes);
94 |
95 | var @delegate = method.CreateDelegate(typeof(Func<,>).MakeGenericType(methodInvokerTypes));
96 |
97 | closure = (RequestDelegateClosure)Activator.CreateInstance(type, @delegate, parameters, serviceProvider)!;
98 | }
99 | else if (parameterTypes.Length == 2)
100 | {
101 | var type = typeof(FuncRequestDelegateClosure<,,>).MakeGenericType(methodInvokerTypes);
102 |
103 | var @delegate = method.CreateDelegate(typeof(Func<,,>).MakeGenericType(methodInvokerTypes));
104 |
105 | closure = (RequestDelegateClosure)Activator.CreateInstance(type, @delegate, parameters, serviceProvider)!;
106 | }
107 | else if (parameterTypes.Length == 3)
108 | {
109 | var type = typeof(FuncRequestDelegateClosure<,,,>).MakeGenericType(methodInvokerTypes);
110 |
111 | var @delegate = method.CreateDelegate(typeof(Func<,,,>).MakeGenericType(methodInvokerTypes));
112 |
113 | closure = (RequestDelegateClosure)Activator.CreateInstance(type, @delegate, parameters, serviceProvider)!;
114 | }
115 | }
116 | else
117 | {
118 | if (parameterTypes.Length == 1)
119 | {
120 | var type = typeof(ActionRequestDelegateClosure<>).MakeGenericType(methodInvokerTypes);
121 |
122 | var @delegate = method.CreateDelegate(typeof(Action<,>).MakeGenericType(methodInvokerTypes));
123 |
124 | closure = (RequestDelegateClosure)Activator.CreateInstance(type, @delegate, parameters, serviceProvider)!;
125 | }
126 | }
127 |
128 | return CreateRequestDelegateCore(closure);
129 | }
130 |
131 | public static RequestDelegate CreateRequestDelegate(Delegate @delegate)
132 | {
133 | // It's expensive to get the Method from a delegate https://github.com/dotnet/runtime/blob/64303750a9198a49f596bcc3aa13de804e421579/src/coreclr/System.Private.CoreLib/src/System/Delegate.CoreCLR.cs#L164
134 | var method = @delegate.Method;
135 | var parameters = method.GetParameters();
136 | var parameterTypes = new Type[parameters.Length];
137 | for (int i = 0; i < parameterTypes.Length; i++)
138 | {
139 | parameterTypes[i] = parameters[i].ParameterType;
140 | }
141 |
142 | // This won't be needed in real life
143 | RequestDelegateClosure closure = new DefaultClosure();
144 |
145 | // We will support up to 16 arguments, then we'll give up
146 |
147 | if (parameters.Length > 16) throw new NotSupportedException("More than 16 arguments isn't supported");
148 |
149 | bool hasReturnType = method.ReturnType != typeof(void);
150 |
151 | var methodInvokerTypes = new Type[hasReturnType ? parameters.Length + 1 : parameters.Length];
152 | parameterTypes.CopyTo(methodInvokerTypes, 0);
153 |
154 | if (hasReturnType)
155 | {
156 | methodInvokerTypes[^1] = method.ReturnType;
157 |
158 | if (parameterTypes.Length == 0)
159 | {
160 | var type = typeof(FuncRequestDelegateClosure<>).MakeGenericType(methodInvokerTypes);
161 |
162 | closure = (RequestDelegateClosure)Activator.CreateInstance(type, @delegate)!;
163 | }
164 | else if (parameterTypes.Length == 1)
165 | {
166 | var type = typeof(FuncRequestDelegateClosure<,>).MakeGenericType(methodInvokerTypes);
167 |
168 | closure = (RequestDelegateClosure)Activator.CreateInstance(type, @delegate, parameters)!;
169 | }
170 | }
171 | else
172 | {
173 | if (parameterTypes.Length == 1)
174 | {
175 | var type = typeof(ActionRequestDelegateClosure<>).MakeGenericType(methodInvokerTypes);
176 |
177 | closure = (RequestDelegateClosure)Activator.CreateInstance(type, @delegate, parameters)!;
178 | }
179 | }
180 |
181 | return CreateRequestDelegateCore(closure);
182 | }
183 |
184 | private static RequestDelegate CreateRequestDelegateCore(RequestDelegateClosure closure)
185 | {
186 | if (closure.HasBody)
187 | {
188 | return closure.ProcessRequestWithBodyAsync;
189 | }
190 |
191 | return closure.ProcessRequestAsync;
192 | }
193 |
194 | class DefaultClosure : RequestDelegateClosure
195 | {
196 | public override bool HasBody => false;
197 |
198 | public override Task ProcessRequestAsync(HttpContext httpContext)
199 | {
200 | return httpContext.Response.WriteAsync("Hello World");
201 | }
202 |
203 | public override Task ProcessRequestWithBodyAsync(HttpContext httpContext)
204 | {
205 | throw new NotImplementedException();
206 | }
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/FasterActions/ResultInvoker.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using System;
4 | using System.Diagnostics.CodeAnalysis;
5 | using System.Threading.Tasks;
6 |
7 | namespace Microsoft.AspNetCore.Http
8 | {
9 | ///
10 | /// a wrapper around a function pointer that processes the result.
11 | ///
12 | public abstract class ResultInvoker<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]T>
13 | {
14 | public static readonly ResultInvoker Instance = Create();
15 |
16 | public abstract Task Invoke(HttpContext httpContext, T? result);
17 |
18 | private static ResultInvoker Create()
19 | {
20 | if (typeof(T) == typeof(void))
21 | {
22 | return new CompletedTaskResultInvoker();
23 | }
24 | else if (typeof(T) == typeof(string))
25 | {
26 | return new StringResultInvoker();
27 | }
28 | else if (typeof(T) == typeof(Task))
29 | {
30 | return new TaskInvoker();
31 | }
32 | else if (typeof(T) == typeof(ValueTask))
33 | {
34 | return new ValueTaskInvoker();
35 | }
36 | if (typeof(T) == typeof(IResult))
37 | {
38 | return new IResultInvoker();
39 | }
40 | else if (typeof(T) == typeof(Task))
41 | {
42 | return new TaskOfStringInvoker();
43 | }
44 | else if (typeof(T) == typeof(Task))
45 | {
46 | return new TaskOfIResultInvoker();
47 | }
48 | else if (typeof(T) == typeof(ValueTask))
49 | {
50 | return new ValueTaskOfStringInvoker();
51 | }
52 | else if (typeof(T) == typeof(ValueTask))
53 | {
54 | return new ValueTaskOfIResultInvoker();
55 | }
56 | else if (typeof(T).IsGenericType)
57 | {
58 | if (typeof(T).GetGenericTypeDefinition() == typeof(Task<>))
59 | {
60 | return TaskOfTInvokerCache.Instance.Invoker;
61 | }
62 | else if (typeof(T).GetGenericTypeDefinition() == typeof(ValueTask<>))
63 | {
64 | return ValueTaskOfTInvokerCache.Instance.Invoker;
65 | }
66 | }
67 | else if (typeof(T).IsAssignableTo(typeof(IResult)))
68 | {
69 | return new IResultInvoker();
70 | }
71 |
72 | return new DefaultInvoker();
73 | }
74 | }
75 |
76 | sealed class CompletedTaskResultInvoker : ResultInvoker
77 | {
78 | public override Task Invoke(HttpContext httpContext, T? result)
79 | {
80 | return Task.CompletedTask;
81 | }
82 | }
83 |
84 | sealed class IResultInvoker : ResultInvoker
85 | {
86 | public override Task Invoke(HttpContext httpContext, T? result)
87 | {
88 | if (result == null) throw new ArgumentNullException(nameof(result));
89 |
90 | return ((IResult)(object)result).ExecuteAsync(httpContext);
91 | }
92 | }
93 |
94 | sealed class StringResultInvoker : ResultInvoker
95 | {
96 | public override Task Invoke(HttpContext httpContext, T? result)
97 | {
98 | return httpContext.Response.WriteAsync((string)(object)result!);
99 | }
100 | }
101 |
102 | sealed class TaskInvoker : ResultInvoker
103 | {
104 | public override Task Invoke(HttpContext httpContext, T? result)
105 | {
106 | if (result == null) throw new ArgumentNullException(nameof(result));
107 |
108 | return (Task)(object)result;
109 | }
110 | }
111 |
112 | sealed class ValueTaskInvoker : ResultInvoker
113 | {
114 | public override Task Invoke(HttpContext httpContext, T? result)
115 | {
116 | return ((ValueTask)(object)result!).AsTask();
117 | }
118 | }
119 |
120 | sealed class TaskOfIResultInvoker : ResultInvoker
121 | {
122 | public override async Task Invoke(HttpContext httpContext, T? result)
123 | {
124 | if (result == null) throw new ArgumentNullException(nameof(result));
125 |
126 | await (await (Task)(object)result).ExecuteAsync(httpContext);
127 | }
128 | }
129 |
130 | sealed class TaskOfStringInvoker : ResultInvoker
131 | {
132 | public override async Task Invoke(HttpContext httpContext, T? result)
133 | {
134 | if (result == null) throw new ArgumentNullException(nameof(result));
135 |
136 | await httpContext.Response.WriteAsync(await (Task)(object)result);
137 | }
138 | }
139 |
140 | // TTask = Task
141 | sealed class TaskOfTInvokerCache
142 | {
143 | public static readonly TaskOfTInvokerCache Instance = new();
144 |
145 | public TaskOfTInvokerCache()
146 | {
147 | // Task
148 | // We need to use MakeGenericType to resolve the T in Task. This is still an issue for AOT support
149 | // because it won't see the instantiation of the TaskOfTInvoker.
150 |
151 | var resultType = typeof(TTask).GetGenericArguments()[0];
152 |
153 | Type type;
154 |
155 | // Task where T : IResult
156 | if (resultType.IsAssignableTo(typeof(IResult)))
157 | {
158 | type = typeof(TaskOfTDerivedIResultInvoker<,>).MakeGenericType(typeof(TTask), resultType);
159 | }
160 | else
161 | {
162 | type = typeof(TaskOfTInvoker<,>).MakeGenericType(typeof(TTask), resultType);
163 | }
164 |
165 | Invoker = (ResultInvoker)Activator.CreateInstance(type)!;
166 | }
167 |
168 | public ResultInvoker Invoker { get; }
169 | }
170 |
171 | sealed class TaskOfTInvoker : ResultInvoker
172 | {
173 | public override async Task Invoke(HttpContext httpContext, T? result)
174 | {
175 | if (result == null) throw new ArgumentNullException(nameof(result));
176 |
177 | await httpContext.Response.WriteAsJsonAsync(await (Task)(object)result);
178 | }
179 | }
180 |
181 | sealed class TaskOfTDerivedIResultInvoker : ResultInvoker where TaskResult : IResult
182 | {
183 | public override async Task Invoke(HttpContext httpContext, T? result)
184 | {
185 | if (result == null) throw new ArgumentNullException(nameof(result));
186 |
187 | await (await (Task)(object)result).ExecuteAsync(httpContext);
188 | }
189 | }
190 |
191 | sealed class ValueTaskOfIResultInvoker : ResultInvoker
192 | {
193 | public override async Task Invoke(HttpContext httpContext, T? result)
194 | {
195 | await (await (ValueTask)(object)result!).ExecuteAsync(httpContext);
196 | }
197 | }
198 |
199 | sealed class ValueTaskOfStringInvoker : ResultInvoker
200 | {
201 | public override async Task Invoke(HttpContext httpContext, T? result)
202 | {
203 | await httpContext.Response.WriteAsync(await (ValueTask)(object)result!);
204 | }
205 | }
206 |
207 | // TTask = ValueTask
208 | sealed class ValueTaskOfTInvokerCache
209 | {
210 | public static readonly ValueTaskOfTInvokerCache Instance = new();
211 |
212 | public ValueTaskOfTInvokerCache()
213 | {
214 | // ValueTask
215 | // We need to use MakeGenericType to resolve the T in Task. This is still an issue for AOT support
216 | // because it won't see the instantiation of the TaskOfTInvoker.
217 | var resultType = typeof(TTask).GetGenericArguments()[0];
218 |
219 | Type type;
220 |
221 | // ValueTask where T : IResult
222 | if (resultType.IsAssignableTo(typeof(IResult)))
223 | {
224 | type = typeof(ValueTaskOfTDerivedIResultInvoker<,>).MakeGenericType(typeof(TTask), resultType);
225 | }
226 | else
227 | {
228 | type = typeof(ValueTaskOfTInvoker<,>).MakeGenericType(typeof(TTask), resultType);
229 | }
230 |
231 | Invoker = (ResultInvoker)Activator.CreateInstance(type)!;
232 | }
233 |
234 | public ResultInvoker Invoker { get; }
235 | }
236 |
237 | sealed class ValueTaskOfTInvoker : ResultInvoker
238 | {
239 | public override async Task Invoke(HttpContext httpContext, T? result)
240 | {
241 | await httpContext.Response.WriteAsJsonAsync(await (ValueTask)(object)result!);
242 | }
243 | }
244 |
245 | sealed class ValueTaskOfTDerivedIResultInvoker : ResultInvoker where TaskResult : IResult
246 | {
247 | public override async Task Invoke(HttpContext httpContext, T? result)
248 | {
249 | await (await (ValueTask)(object)result!).ExecuteAsync(httpContext);
250 | }
251 | }
252 |
253 | sealed class DefaultInvoker : ResultInvoker
254 | {
255 | public override Task Invoke(HttpContext httpContext, T? result)
256 | {
257 | return httpContext.Response.WriteAsJsonAsync(result);
258 | }
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FasterActions
2 |
3 | This is a repository for exploring faster code generation techniques that take advantage of the JIT's de-virtualization and inlining
4 | to have a more zero-cost abstraction around model binding and "action" (from the MVC sense) invocation.
--------------------------------------------------------------------------------
/Sample/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Builder;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.Logging;
8 |
9 | // Boot the web application
10 | var app = WebApplication.Create(args);
11 |
12 | var options = new DbContextOptionsBuilder().UseSqlite("Data Source=Todos.db").Options;
13 |
14 | // This makes sure the database and tables are created
15 | using (var db = new TodoDbContext(options))
16 | {
17 | db.Database.EnsureCreated();
18 | }
19 |
20 | // Register the routes
21 | TodoApi.MapRoutes(app, options);
22 |
23 | // Create a RequestDelegate, the ASP.NET primitive for handling requests from the
24 | // specified delegate
25 | RequestDelegate rd = RequestDelegateFactory2.CreateRequestDelegate(typeof(Foo).GetMethod(nameof(Foo.Hello))!, app.Services);
26 |
27 | // Map this delegate to the path /
28 | app.MapGet("/", rd);
29 |
30 | // Run the application
31 | app.Run();
32 |
33 |
34 | class Foo
35 | {
36 | public static async ValueTask Hello(string name, Options options, PageInfo pi) => new() { Message = $"Hello {name}" };
37 | }
38 |
39 | class Data
40 | {
41 | public string Message { get; init; } = default!;
42 | }
43 |
44 | enum Options
45 | {
46 | One,
47 | Two
48 | }
49 |
50 | record PageInfo(int PageIndex)
51 | {
52 | public static bool TryParse(string s, out PageInfo page)
53 | {
54 | if (int.TryParse(s, out var value))
55 | {
56 | page = new PageInfo(value);
57 | return true;
58 | }
59 |
60 | page = default;
61 | return false;
62 | }
63 | }
--------------------------------------------------------------------------------
/Sample/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:1210",
7 | "sslPort": 44300
8 | }
9 | },
10 | "profiles": {
11 | "Sample": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchBrowser": true,
15 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "IIS Express": {
21 | "commandName": "IISExpress",
22 | "launchBrowser": true,
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sample/Results.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.AspNetCore.Routing;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | public static class Results
8 | {
9 | public static IResult NotFound() => new StatusCodeResult(404);
10 | public static IResult Ok() => new StatusCodeResult(200);
11 | public static IResult Status(int statusCode) => new StatusCodeResult(statusCode);
12 | public static OkResult Ok(T value) => new(value);
13 | public static CreatedAtRouteResult CreatedAt(T value, string endpointName, object values) => new(value, endpointName, values);
14 |
15 | public class OkResult : IResult
16 | {
17 | private readonly T _value;
18 |
19 | public OkResult(T value)
20 | {
21 | _value = value;
22 | }
23 |
24 | public Task ExecuteAsync(HttpContext httpContext)
25 | {
26 | return httpContext.Response.WriteAsJsonAsync(_value);
27 | }
28 | }
29 |
30 | public class CreatedAtRouteResult : IResult
31 | {
32 | private readonly T _value;
33 | private readonly string _endpointName;
34 | private readonly object _values;
35 |
36 | public CreatedAtRouteResult(T value, string endpointName, object values)
37 | {
38 | _value = value;
39 | _endpointName = endpointName;
40 | _values = values;
41 | }
42 |
43 | public Task ExecuteAsync(HttpContext httpContext)
44 | {
45 | var linkGenerator = httpContext.RequestServices.GetRequiredService();
46 |
47 | httpContext.Response.Headers.Location = linkGenerator.GetPathByName(_endpointName, _values);
48 |
49 | return httpContext.Response.WriteAsJsonAsync(_value);
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/Sample/Sample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Sample/TodoApi.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Routing;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.AspNetCore.Http;
5 | using static Results;
6 |
7 | class TodoApi
8 | {
9 | public static void MapRoutes(IEndpointRouteBuilder routes, DbContextOptions options)
10 | {
11 | routes.MapGet("/todos", RequestDelegateFactory2.CreateRequestDelegate(async () =>
12 | {
13 | using var db = new TodoDbContext(options);
14 | return await db.Todos.ToListAsync();
15 | },
16 | routes.ServiceProvider));
17 |
18 | routes.MapGet("/todos/{id}", RequestDelegateFactory2.CreateRequestDelegate(async (int id) =>
19 | {
20 | using var db = new TodoDbContext(options);
21 | return await db.Todos.FindAsync(id) is Todo todo ? Ok(todo) : NotFound();
22 | },
23 | routes.ServiceProvider))
24 | .WithMetadata(new EndpointNameMetadata("todos"));
25 |
26 | routes.MapPost("/todos", RequestDelegateFactory2.CreateRequestDelegate(async (Todo todo) =>
27 | {
28 | using var db = new TodoDbContext(options);
29 | await db.Todos.AddAsync(todo);
30 | await db.SaveChangesAsync();
31 |
32 | return CreatedAt(todo, "todos", new { id = todo.Id });
33 | },
34 | routes.ServiceProvider));
35 |
36 | routes.MapDelete("/todos/{id}", RequestDelegateFactory2.CreateRequestDelegate(async (int id) =>
37 | {
38 | using var db = new TodoDbContext(options);
39 | var todo = await db.Todos.FindAsync(id);
40 | if (todo is null)
41 | {
42 | return NotFound();
43 | }
44 |
45 | db.Todos.Remove(todo);
46 | await db.SaveChangesAsync();
47 |
48 | return Ok();
49 | },
50 | routes.ServiceProvider));
51 | }
52 | }
--------------------------------------------------------------------------------
/Sample/TodoDbContext.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | public class TodoDbContext : DbContext
5 | {
6 | public TodoDbContext(DbContextOptions options) : base(options) { }
7 |
8 | public DbSet Todos { get; set; }
9 | }
10 |
11 | public class Todo
12 | {
13 | public int Id { get; set; }
14 | [Required]
15 | public string Title { get; set; }
16 | public bool IsComplete { get; set; }
17 | }
--------------------------------------------------------------------------------
/Sample/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sample/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------