├── .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 | --------------------------------------------------------------------------------