├── .editorconfig
├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ └── merge-dependabot.yml
├── .gitignore
├── AssemblyTemplate
├── AssemblyTemplate.csproj
├── AsyncErrorHandler.cs
└── Template.cs
├── AssemblyToProcess
├── AssemblyToProcess.csproj
├── AsyncErrorHandler.cs
└── Target.cs
├── AssemblyWithHandlerInReference
├── AssemblyWithHandlerInReference.csproj
├── Class1.cs
└── Target.cs
├── AsyncErrorHandler.Fody
├── AsyncErrorHandler.Fody.csproj
├── HandleMethodFinder.cs
├── MethodProcessor.cs
├── ModuleWeaver.cs
├── StateMachineChecker.cs
└── StateMachineTypesFinder.cs
├── AsyncErrorHandler.sln
├── AsyncErrorHandler
├── AsyncErrorHandler.csproj
└── key.snk
├── CommonAssemblyInfo.cs
├── Directory.Build.props
├── License.txt
├── Tests
├── AssemblyExtensions.cs
├── InSameAssemblyTests.cs
├── Tests.csproj
└── WithHandlerInReferenceTests.cs
├── appveyor.yml
├── global.json
├── key.snk
├── package_icon.png
└── readme.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 |
9 | [*.cs]
10 | indent_size = 4
11 |
12 | # Sort using and Import directives with System.* appearing first
13 | dotnet_sort_system_directives_first = true
14 |
15 | # Avoid "this." and "Me." if not necessary
16 | dotnet_style_qualification_for_field = false:error
17 | dotnet_style_qualification_for_property = false:error
18 | dotnet_style_qualification_for_method = false:error
19 | dotnet_style_qualification_for_event = false:error
20 |
21 | # Use language keywords instead of framework type names for type references
22 | dotnet_style_predefined_type_for_locals_parameters_members = true:error
23 | dotnet_style_predefined_type_for_member_access = true:error
24 |
25 | # Suggest more modern language features when available
26 | dotnet_style_object_initializer = true:suggestion
27 | dotnet_style_collection_initializer = true:suggestion
28 | dotnet_style_coalesce_expression = false:suggestion
29 | dotnet_style_null_propagation = true:suggestion
30 | dotnet_style_explicit_tuple_names = true:suggestion
31 |
32 | # Prefer "var" everywhere
33 | csharp_style_var_for_built_in_types = true:error
34 | csharp_style_var_when_type_is_apparent = true:error
35 | csharp_style_var_elsewhere = true:error
36 |
37 | # Prefer method-like constructs to have a block body
38 | csharp_style_expression_bodied_methods = false:none
39 | csharp_style_expression_bodied_constructors = false:none
40 | csharp_style_expression_bodied_operators = false:none
41 |
42 | # Prefer property-like constructs to have an expression-body
43 | csharp_style_expression_bodied_properties = true:suggestion
44 | csharp_style_expression_bodied_indexers = true:suggestion
45 | csharp_style_expression_bodied_accessors = true:none
46 |
47 | # Suggest more modern language features when available
48 | csharp_style_pattern_matching_over_is_with_cast_check = true:error
49 | csharp_style_pattern_matching_over_as_with_null_check = true:error
50 | csharp_style_inlined_variable_declaration = true:suggestion
51 | csharp_style_throw_expression = true:suggestion
52 | csharp_style_conditional_delegate_call = true:suggestion
53 |
54 | # Newline settings
55 | #csharp_new_line_before_open_brace = all:error
56 | csharp_new_line_before_else = true
57 | csharp_new_line_before_catch = true
58 | csharp_new_line_before_finally = true
59 | csharp_new_line_before_members_in_object_initializers = true
60 | csharp_new_line_before_members_in_anonymous_types = true
61 |
62 | #braces
63 | #csharp_prefer_braces = true:error
64 |
65 | # msbuild
66 | [*.{csproj,targets,props}]
67 | indent_size = 2
68 |
69 | # Xml files
70 | [*.{xml,config,nuspec,resx,vsixmanifest}]
71 | indent_size = 2
72 | resharper_xml_wrap_tags_and_pi = true:error
73 |
74 | # JSON files
75 | [*.json]
76 | indent_size = 2
77 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text
3 |
4 | # Don't check these into the repo as LF to work around TeamCity bug
5 | *.xml -text
6 | *.targets -text
7 |
8 | # Custom for Visual Studio
9 | *.cs diff=csharp
10 | *.sln merge=union
11 | *.csproj merge=union
12 | *.vbproj merge=union
13 | *.fsproj merge=union
14 | *.dbproj merge=union
15 |
16 | # Denote all files that are truly binary and should not be modified.
17 | *.dll binary
18 | *.exe binary
19 | *.png binary
20 | *.ico binary
21 | *.snk binary
22 | *.pdb binary
23 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: nuget
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/workflows/merge-dependabot.yml:
--------------------------------------------------------------------------------
1 | name: merge-dependabot
2 | on:
3 | pull_request:
4 | jobs:
5 | automerge:
6 | runs-on: ubuntu-latest
7 | if: github.actor == 'dependabot[bot]'
8 | steps:
9 | - name: Dependabot Auto Merge
10 | uses: ahmadnassri/action-dependabot-auto-merge@v2.6.6
11 | with:
12 | target: minor
13 | github-token: ${{ secrets.GITHUB_TOKEN }}
14 | command: squash and merge
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Visual Studio
3 | #################
4 |
5 | ## Ignore Visual Studio temporary files, build results, and
6 | ## files generated by popular Visual Studio add-ons.
7 |
8 | # User-specific files
9 | *.suo
10 | *.user
11 | *.sln.docstates
12 | *.pidb
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Rr]elease/
18 | *_i.c
19 | *_p.c
20 | *.ilk
21 | *.meta
22 | *.obj
23 | *.pch
24 | *.pgc
25 | *.pgd
26 | *.rsp
27 | *.sbr
28 | *.tlb
29 | *.tli
30 | *.tlh
31 | *.tmp
32 | *.vspscc
33 | .builds
34 | *.dotCover
35 |
36 | *.DotSettings
37 | *.ncrunchsolution
38 |
39 | ## If you have NuGet Package Restore enabled, uncomment this
40 | packages/
41 | ForSample/
42 | nugets/
43 |
44 | # Visual Studio profiler
45 | *.psess
46 | *.vsp
47 |
48 | # ReSharper is a .NET coding add-in
49 | _ReSharper*
50 |
51 | # Others
52 | [Bb]in
53 | [Oo]bj
54 | sql
55 | TestResults
56 | *.Cache
57 | ClientBin
58 | stylecop.*
59 | ~$*
60 | *.dbmdl
61 | Generated_Code #added for RIA/Silverlight projects
62 |
63 | # Backup & report files from converting an old project file to a newer
64 | # Visual Studio version. Backup files are not needed, because we have git ;-)
65 | _UpgradeReport_Files/
66 | Backup*/
67 | UpgradeLog*.XML
68 |
69 |
70 | ############
71 | ## Windows
72 | ############
73 |
74 | # Windows image file caches
75 | Thumbs.db
76 |
77 | # Folder config file
78 | Desktop.ini
79 |
80 | .vs
81 | .idea/
82 |
--------------------------------------------------------------------------------
/AssemblyTemplate/AssemblyTemplate.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net48;net8.0
5 | true
6 |
7 |
--------------------------------------------------------------------------------
/AssemblyTemplate/AsyncErrorHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 |
4 | public static class AsyncErrorHandler
5 | {
6 | public static void HandleException(Exception exception)
7 | {
8 | Debug.WriteLine(exception);
9 | }
10 | }
--------------------------------------------------------------------------------
/AssemblyTemplate/Template.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | public class Template
4 | {
5 | public async Task Method()
6 | {
7 | await Task.Delay(1);
8 | }
9 |
10 | public async Task MethodWithThrow()
11 | {
12 | await Task.Delay(1);
13 | throw new();
14 | }
15 |
16 | public async Task MethodGeneric()
17 | {
18 | await Task.Delay(1);
19 | return 1;
20 | }
21 |
22 | public async Task MethodWithThrowGeneric()
23 | {
24 | await Task.Delay(1);
25 | throw new();
26 | }
27 | }
--------------------------------------------------------------------------------
/AssemblyToProcess/AssemblyToProcess.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net48;net8.0
5 | true
6 |
7 |
--------------------------------------------------------------------------------
/AssemblyToProcess/AsyncErrorHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | public static class AsyncErrorHandler
4 | {
5 | public static Exception Exception;
6 | public static void HandleException(Exception exception)
7 | {
8 | Exception = exception;
9 | }
10 | }
--------------------------------------------------------------------------------
/AssemblyToProcess/Target.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | public class Target
4 | {
5 | public async Task Method()
6 | {
7 | await Task.Delay(1);
8 | }
9 | public async Task MethodWithThrow()
10 | {
11 | await Task.Delay(1);
12 | throw new();
13 | }
14 | public async Task MethodGeneric()
15 | {
16 | await Task.Delay(1);
17 | return 1;
18 | }
19 | public async Task MethodWithThrowGeneric()
20 | {
21 | await Task.Delay(1);
22 | throw new();
23 | }
24 | }
--------------------------------------------------------------------------------
/AssemblyWithHandlerInReference/AssemblyWithHandlerInReference.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net48;net8.0
5 | true
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/AssemblyWithHandlerInReference/Class1.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | public class Class1
4 | {
5 | // ReSharper disable once UnusedMember.Local
6 | Type x = typeof (AsyncErrorHandler);
7 | }
--------------------------------------------------------------------------------
/AssemblyWithHandlerInReference/Target.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | public class Target
4 | {
5 | public async Task Method()
6 | {
7 | await Task.Delay(1);
8 | }
9 | public async Task MethodWithThrow()
10 | {
11 | await Task.Delay(1);
12 | throw new();
13 | }
14 | public async Task MethodGeneric()
15 | {
16 | await Task.Delay(1);
17 | return 1;
18 | }
19 | public async Task MethodWithThrowGeneric()
20 | {
21 | await Task.Delay(1);
22 | throw new();
23 | }
24 | }
--------------------------------------------------------------------------------
/AsyncErrorHandler.Fody/AsyncErrorHandler.Fody.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/AsyncErrorHandler.Fody/HandleMethodFinder.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Fody;
4 | using Mono.Cecil;
5 |
6 | public class HandleMethodFinder
7 | {
8 | public ModuleDefinition ModuleDefinition;
9 | public MethodReference HandleMethod;
10 | public IAssemblyResolver AssemblyResolver;
11 |
12 | public void Execute()
13 | {
14 | var errorHandler = GetTypeDefinition();
15 | var handleMethod = errorHandler.Methods.FirstOrDefault(_ => _.Name == "HandleException");
16 | if (handleMethod == null)
17 | {
18 | throw new WeavingException($"Could not find 'HandleException' method on '{errorHandler.FullName}'.");
19 | }
20 | if (!handleMethod.IsPublic)
21 | {
22 | throw new WeavingException("Method 'AsyncErrorHandler.HandleException' is not public.");
23 | }
24 | if (!handleMethod.IsStatic)
25 | {
26 | throw new WeavingException("Method 'AsyncErrorHandler.HandleException' is not static.");
27 | }
28 | if (handleMethod.Parameters.Count != 1)
29 | {
30 | throw new WeavingException("Method 'AsyncErrorHandler.HandleException' must have only 1 parameter that is of type 'System.Exception'.");
31 | }
32 | var parameterDefinition = handleMethod.Parameters.First();
33 | var parameterType = parameterDefinition.ParameterType;
34 | if (parameterType.FullName != "System.Exception")
35 | {
36 | throw new WeavingException("Method 'AsyncErrorHandler.HandleException' must have only 1 parameter that is of type 'System.Exception'.");
37 | }
38 | HandleMethod = ModuleDefinition.ImportReference(handleMethod);
39 | }
40 |
41 | TypeDefinition GetTypeDefinition()
42 | {
43 | foreach (var module in GetAllModulesToSearch())
44 | {
45 | var errorHandler = module.GetTypes().FirstOrDefault(_ => _.Name == "AsyncErrorHandler");
46 | if (errorHandler != null)
47 | {
48 | return errorHandler;
49 | }
50 | }
51 | var error =
52 | """
53 | Could not find type 'AsyncErrorHandler'. Expected to find a class with the following signature.
54 | public static class AsyncErrorHandler
55 | {
56 | public static void HandleException(Exception exception)
57 | {
58 | Debug.WriteLine("Exception occurred: " + exception.Message);
59 | }
60 | }
61 | """;
62 |
63 | throw new WeavingException(error);
64 | }
65 |
66 | IEnumerable GetAllModulesToSearch()
67 | {
68 | yield return ModuleDefinition;
69 |
70 | foreach (var reference in ModuleDefinition.AssemblyReferences.Where(x => !IsMicrosoftAssembly(x)))
71 | {
72 | yield return ModuleDefinition.AssemblyResolver.Resolve(reference).MainModule;
73 | }
74 | }
75 |
76 | static bool IsMicrosoftAssembly(AssemblyNameReference reference)
77 | {
78 | return reference.FullName.EndsWith("b77a5c561934e089");
79 | }
80 | }
--------------------------------------------------------------------------------
/AsyncErrorHandler.Fody/MethodProcessor.cs:
--------------------------------------------------------------------------------
1 | using Mono.Cecil;
2 | using Mono.Cecil.Cil;
3 | using Mono.Cecil.Rocks;
4 |
5 | public class MethodProcessor
6 | {
7 | public HandleMethodFinder HandleMethodFinder;
8 |
9 | public void Process(MethodDefinition method)
10 | {
11 | method.Body.SimplifyMacros();
12 |
13 | var instructions = method.Body.Instructions;
14 | for (var index = 0; index < instructions.Count; index++)
15 | {
16 | var line = instructions[index];
17 | if (line.OpCode != OpCodes.Call)
18 | {
19 | continue;
20 | }
21 |
22 | if (line.Operand is not MethodReference methodReference)
23 | {
24 | continue;
25 | }
26 |
27 | if (!IsSetExceptionMethod(methodReference))
28 | {
29 | continue;
30 | }
31 |
32 | var previous = instructions[index-1];
33 | instructions.Insert(index, Instruction.Create(OpCodes.Call, HandleMethodFinder.HandleMethod));
34 | index++;
35 | if (previous.Operand is not VariableDefinition variableDefinition)
36 | {
37 | throw new($"Expected VariableDefinition but got '{previous.Operand.GetType().Name}'.");
38 | }
39 | instructions.Insert(index, Instruction.Create(previous.OpCode, variableDefinition));
40 | index++;
41 |
42 | }
43 | method.Body.OptimizeMacros();
44 | }
45 |
46 | public static bool IsSetExceptionMethod(MethodReference methodReference)
47 | {
48 | return
49 | methodReference.Name == "SetException" &&
50 | methodReference.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.AsyncTaskMethodBuilder");
51 | }
52 | }
--------------------------------------------------------------------------------
/AsyncErrorHandler.Fody/ModuleWeaver.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Fody;
5 |
6 | public class ModuleWeaver : BaseModuleWeaver
7 | {
8 | public override void Execute()
9 | {
10 | var initializeMethodFinder = new HandleMethodFinder
11 | {
12 | ModuleDefinition = ModuleDefinition,
13 | AssemblyResolver = AssemblyResolver
14 | };
15 | initializeMethodFinder.Execute();
16 |
17 | var stateMachineFinder = new StateMachineTypesFinder
18 | {
19 | ModuleDefinition = ModuleDefinition,
20 | };
21 | stateMachineFinder.Execute();
22 |
23 | var methodProcessor = new MethodProcessor
24 | {
25 | HandleMethodFinder = initializeMethodFinder,
26 | };
27 |
28 | foreach (var stateMachine in stateMachineFinder.AllTypes)
29 | {
30 | try
31 | {
32 | var moveNext = stateMachine.Methods.First(_ => _.Name == "MoveNext");
33 | methodProcessor.Process(moveNext);
34 | }
35 | catch (Exception exception)
36 | {
37 | throw new($"Failed to process '{stateMachine.FullName}'.", exception);
38 | }
39 | }
40 | }
41 |
42 | public override IEnumerable GetAssembliesForScanning()
43 | {
44 | yield break;
45 | }
46 | }
--------------------------------------------------------------------------------
/AsyncErrorHandler.Fody/StateMachineChecker.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Mono.Cecil;
3 |
4 | public static class StateMachineChecker
5 | {
6 | public static bool IsStateMachine(this TypeDefinition typeDefinition)
7 | {
8 | return typeDefinition.IsIAsyncStateMachine() &&
9 | typeDefinition.IsCompilerGenerated();
10 | }
11 |
12 | public static bool IsCompilerGenerated(this TypeDefinition typeDefinition)
13 | {
14 | return typeDefinition.CustomAttributes.Any(_ => _.Constructor.DeclaringType.Name == "CompilerGeneratedAttribute");
15 | }
16 |
17 | public static bool IsIAsyncStateMachine(this TypeDefinition typeDefinition)
18 | {
19 | return typeDefinition.Interfaces.Any(_ => _.InterfaceType.Name =="IAsyncStateMachine");
20 | }
21 | }
--------------------------------------------------------------------------------
/AsyncErrorHandler.Fody/StateMachineTypesFinder.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Mono.Cecil;
3 |
4 | public class StateMachineTypesFinder
5 | {
6 | public List AllTypes;
7 | public ModuleDefinition ModuleDefinition;
8 |
9 | public void Execute()
10 | {
11 | AllTypes = new();
12 | GetTypes(ModuleDefinition.Types);
13 | }
14 |
15 | void GetTypes(IEnumerable typeDefinitions)
16 | {
17 | foreach (var typeDefinition in typeDefinitions)
18 | {
19 | GetTypes(typeDefinition.NestedTypes);
20 | if (typeDefinition.IsStateMachine())
21 | {
22 | AllTypes.Add(typeDefinition);
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/AsyncErrorHandler.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31825.309
5 | MinimumVisualStudioVersion = 16.0.29201.188
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncErrorHandler.Fody", "AsyncErrorHandler.Fody\AsyncErrorHandler.Fody.csproj", "{C3578A7B-09A6-4444-9383-0DEAFA4958BD}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{5A86453B-96FB-4B6E-A283-225BB9F753D3}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyToProcess", "AssemblyToProcess\AssemblyToProcess.csproj", "{7DEC4E2D-F872-434E-A267-0BAD65299950}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyTemplate", "AssemblyTemplate\AssemblyTemplate.csproj", "{CC2EB345-6E0F-4523-9230-C4F6D129D411}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssemblyWithHandlerInReference", "AssemblyWithHandlerInReference\AssemblyWithHandlerInReference.csproj", "{BAB0CB71-BD17-4D16-97AD-751FF53515F4}"
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncErrorHandler", "AsyncErrorHandler\AsyncErrorHandler.csproj", "{EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}"
17 | ProjectSection(ProjectDependencies) = postProject
18 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD} = {C3578A7B-09A6-4444-9383-0DEAFA4958BD}
19 | EndProjectSection
20 | EndProject
21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2751CBDA-427C-4DE4-8F0C-45173F84365D}"
22 | ProjectSection(SolutionItems) = preProject
23 | Directory.Build.props = Directory.Build.props
24 | EndProjectSection
25 | EndProject
26 | Global
27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
28 | Debug|Any CPU = Debug|Any CPU
29 | Debug|Mixed Platforms = Debug|Mixed Platforms
30 | Debug|x86 = Debug|x86
31 | Release|Any CPU = Release|Any CPU
32 | Release|Mixed Platforms = Release|Mixed Platforms
33 | Release|x86 = Release|x86
34 | EndGlobalSection
35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
36 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
39 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
40 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Debug|x86.ActiveCfg = Debug|Any CPU
41 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Debug|x86.Build.0 = Debug|Any CPU
42 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Release|Any CPU.Build.0 = Release|Any CPU
44 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
45 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Release|Mixed Platforms.Build.0 = Release|Any CPU
46 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Release|x86.ActiveCfg = Release|Any CPU
47 | {C3578A7B-09A6-4444-9383-0DEAFA4958BD}.Release|x86.Build.0 = Release|Any CPU
48 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
51 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
52 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Debug|x86.ActiveCfg = Debug|Any CPU
53 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
54 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Release|Any CPU.Build.0 = Release|Any CPU
55 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
56 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Release|Mixed Platforms.Build.0 = Release|Any CPU
57 | {5A86453B-96FB-4B6E-A283-225BB9F753D3}.Release|x86.ActiveCfg = Release|Any CPU
58 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Debug|Any CPU.Build.0 = Debug|Any CPU
60 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
61 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
62 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Debug|x86.ActiveCfg = Debug|Any CPU
63 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Release|Any CPU.ActiveCfg = Release|Any CPU
64 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Release|Any CPU.Build.0 = Release|Any CPU
65 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
66 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Release|Mixed Platforms.Build.0 = Release|Any CPU
67 | {7DEC4E2D-F872-434E-A267-0BAD65299950}.Release|x86.ActiveCfg = Release|Any CPU
68 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
69 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Debug|Any CPU.Build.0 = Debug|Any CPU
70 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
71 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
72 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Debug|x86.ActiveCfg = Debug|Any CPU
73 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Release|Any CPU.ActiveCfg = Release|Any CPU
74 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Release|Any CPU.Build.0 = Release|Any CPU
75 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
76 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Release|Mixed Platforms.Build.0 = Release|Any CPU
77 | {CC2EB345-6E0F-4523-9230-C4F6D129D411}.Release|x86.ActiveCfg = Release|Any CPU
78 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
80 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
81 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
82 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Debug|x86.ActiveCfg = Debug|Any CPU
83 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
84 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Release|Any CPU.Build.0 = Release|Any CPU
85 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
86 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Release|Mixed Platforms.Build.0 = Release|Any CPU
87 | {BAB0CB71-BD17-4D16-97AD-751FF53515F4}.Release|x86.ActiveCfg = Release|Any CPU
88 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
89 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Debug|Any CPU.Build.0 = Debug|Any CPU
90 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
91 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
92 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Debug|x86.ActiveCfg = Debug|Any CPU
93 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Debug|x86.Build.0 = Debug|Any CPU
94 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Release|Any CPU.ActiveCfg = Release|Any CPU
95 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Release|Any CPU.Build.0 = Release|Any CPU
96 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
97 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
98 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Release|x86.ActiveCfg = Release|Any CPU
99 | {EF9EC1C1-6D40-4FA1-A03A-AE010D3B731E}.Release|x86.Build.0 = Release|Any CPU
100 | EndGlobalSection
101 | GlobalSection(SolutionProperties) = preSolution
102 | HideSolutionNode = FALSE
103 | EndGlobalSection
104 | GlobalSection(ExtensibilityGlobals) = postSolution
105 | SolutionGuid = {9898D83A-8477-4981-9CF4-96E9E6E0456C}
106 | EndGlobalSection
107 | EndGlobal
108 |
--------------------------------------------------------------------------------
/AsyncErrorHandler/AsyncErrorHandler.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net48;netstandard2.0
5 | true
6 | key.snk
7 | Simon Cropp
8 | An extension for Fody to integrate error handling into async and TPL code.
9 | Async, Exception Handling, ILWeaving, Fody, Cecil
10 | $(SolutionDir)nugets
11 | https://raw.githubusercontent.com/Fody/AsyncErrorHandler/master/package_icon.png
12 | https://github.com/Fody/AsyncErrorHandler
13 | MIT
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/AsyncErrorHandler/key.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fody/AsyncErrorHandler/d80cbdf315f6d74a7ba5d7a6bc66053f80e15dde/AsyncErrorHandler/key.snk
--------------------------------------------------------------------------------
/CommonAssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | [assembly: AssemblyTitle("AsyncErrorHandler")]
4 | [assembly: AssemblyProduct("AsyncErrorHandler")]
5 | [assembly: AssemblyVersion("1.1.1")]
6 | [assembly: AssemblyFileVersion("1.1.1")]
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.3.0
5 | true
6 | preview
7 | NU5118
8 | true
9 | all
10 | low
11 |
12 |
--------------------------------------------------------------------------------
/License.txt:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) Simon Cropp and contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Tests/AssemblyExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 |
4 | public static class AssemblyExtensions
5 | {
6 | public static dynamic GetInstance(this Assembly assembly, string className)
7 | {
8 | var type = assembly.GetType(className, true);
9 | //dynamic instance = FormatterServices.GetUninitializedObject(type);
10 | return Activator.CreateInstance(type);
11 | }
12 | }
--------------------------------------------------------------------------------
/Tests/InSameAssemblyTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Threading.Tasks;
4 | using Fody;
5 | using Xunit;
6 |
7 | public class InSameAssemblyTests
8 | {
9 | FieldInfo exceptionField;
10 | dynamic target;
11 |
12 | public InSameAssemblyTests()
13 | {
14 | var weaver = new ModuleWeaver();
15 |
16 | var testResult = weaver.ExecuteTestRun(
17 | "AssemblyToProcess.dll",
18 | assemblyName: "InSameAssembly",
19 | runPeVerify: false);
20 | target = testResult.GetInstance("Target");
21 | var errorHandler = testResult.Assembly.GetType("AsyncErrorHandler");
22 | exceptionField = errorHandler.GetField("Exception");
23 | }
24 |
25 | [Fact]
26 | public async Task Method()
27 | {
28 | ClearException();
29 | await target.Method();
30 | Assert.Null(GetException());
31 | }
32 |
33 | [Fact]
34 | public async Task MethodWithThrow()
35 | {
36 | ClearException();
37 | try
38 | {
39 | await target.MethodWithThrow();
40 | }
41 | catch
42 | {
43 | }
44 |
45 | Assert.NotNull(GetException());
46 | }
47 |
48 | [Fact]
49 | public async Task MethodGeneric()
50 | {
51 | ClearException();
52 | await target.MethodGeneric();
53 | Assert.Null(GetException());
54 | }
55 |
56 | [Fact]
57 | public async Task MethodWithThrowGeneric()
58 | {
59 | ClearException();
60 | try
61 | {
62 | await target.MethodWithThrowGeneric();
63 | }
64 | catch
65 | {
66 | }
67 |
68 | Assert.NotNull(GetException());
69 | }
70 |
71 | void ClearException()
72 | {
73 | exceptionField.SetValue(null, null);
74 | }
75 |
76 | Exception GetException()
77 | {
78 | return (Exception) exceptionField.GetValue(null);
79 | }
80 | }
--------------------------------------------------------------------------------
/Tests/Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net48;net8.0
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Tests/WithHandlerInReferenceTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Threading.Tasks;
4 | using Fody;
5 | using Xunit;
6 |
7 | public class WithHandlerInReferenceTests
8 | {
9 | FieldInfo exceptionField;
10 | dynamic target;
11 |
12 | public WithHandlerInReferenceTests()
13 | {
14 | var weaver = new ModuleWeaver();
15 |
16 | var testResult = weaver.ExecuteTestRun("AssemblyWithHandlerInReference.dll", runPeVerify: false);
17 | target = testResult.GetInstance("Target");
18 | var errorHandler = Type.GetType("AsyncErrorHandler, AssemblyToProcess");
19 | exceptionField = errorHandler.GetField("Exception");
20 | }
21 |
22 | [Fact]
23 | public async Task Method()
24 | {
25 | ClearException();
26 | await target.Method();
27 | Assert.Null(GetException());
28 | }
29 |
30 | [Fact]
31 | public async Task MethodWithThrow()
32 | {
33 | ClearException();
34 | try
35 | {
36 | await target.MethodWithThrow();
37 | }
38 | catch
39 | {
40 | }
41 | Assert.NotNull(GetException());
42 | }
43 |
44 | [Fact]
45 | public async Task MethodGeneric()
46 | {
47 | ClearException();
48 | await target.MethodGeneric();
49 | Assert.Null(GetException());
50 | }
51 |
52 | [Fact]
53 | public async Task MethodWithThrowGeneric()
54 | {
55 | ClearException();
56 | try
57 | {
58 | await target.MethodWithThrowGeneric();
59 | }
60 | catch
61 | {
62 | }
63 | Assert.NotNull(GetException());
64 | }
65 |
66 | void ClearException()
67 | {
68 | exceptionField.SetValue(null, null);
69 | }
70 |
71 | Exception GetException()
72 | {
73 | return (Exception) exceptionField.GetValue(null);
74 | }
75 | }
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | image: Visual Studio 2022
2 | environment:
3 | DOTNET_NOLOGO: true
4 | DOTNET_CLI_TELEMETRY_OPTOUT: true
5 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
6 | build_script:
7 | - pwsh: |
8 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./dotnet-install.ps1"
9 | ./dotnet-install.ps1 -JSonFile global.json -Architecture x64 -InstallDir 'C:\Program Files\dotnet'
10 | - dotnet build --configuration Release
11 | - dotnet test --configuration Release --no-build --no-restore
12 | test: off
13 | artifacts:
14 | - path: nugets\**\*.nupkg
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "8.0.101",
4 | "allowPrerelease": true,
5 | "rollForward": "latestFeature"
6 | }
7 | }
--------------------------------------------------------------------------------
/key.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fody/AsyncErrorHandler/d80cbdf315f6d74a7ba5d7a6bc66053f80e15dde/key.snk
--------------------------------------------------------------------------------
/package_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fody/AsyncErrorHandler/d80cbdf315f6d74a7ba5d7a6bc66053f80e15dde/package_icon.png
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | #
AsyncErrorHandler.Fody
2 |
3 | [](https://www.nuget.org/packages/AsyncErrorHandler.Fody/)
4 |
5 | Fody.AsyncErrorHandler is a [Fody](https://github.com/Fody/Home/) extension for weaving exception handling code into applications which use async code.
6 |
7 | **See [Milestones](../../milestones?state=closed) for release notes.**
8 |
9 |
10 | ### This is an add-in for [Fody](https://github.com/Fody/Home/)
11 |
12 | **It is expected that all developers using Fody [become a Patron on OpenCollective](https://opencollective.com/fody/contribute/patron-3059). [See Licensing/Patron FAQ](https://github.com/Fody/Home/blob/master/pages/licensing-patron-faq.md) for more information.**
13 |
14 |
15 | ## Usage
16 |
17 | See also [Fody usage](https://github.com/Fody/Home/blob/master/pages/usage.md).
18 |
19 |
20 | ### NuGet package
21 |
22 | https://nuget.org/packages/AsyncErrorHandler.Fody/
23 |
24 | ```powershell
25 | PM> Install-Package Fody
26 | PM> Install-Package AsyncErrorHandler.Fody
27 | ```
28 |
29 | The `Install-Package Fody` is required since NuGet always defaults to the oldest, and most buggy, version of any dependency.
30 |
31 |
32 | ### Add to FodyWeavers.xml
33 |
34 | Add `` to [FodyWeavers.xml](https://github.com/Fody/Home/blob/master/pages/usage.md#add-fodyweaversxml)
35 |
36 | ```xml
37 |
38 |
39 |
40 | ```
41 |
42 |
43 | ## Why?
44 |
45 | Because writing plumbing code is dumb and repetitive.
46 |
47 |
48 | ## How?
49 |
50 | IL-weaving after the code is compiled, bro.
51 |
52 | For example, imagine you've got this code to serialize an object to the filesystem:
53 |
54 | ```csharp
55 | public class DataStorage
56 | {
57 | public async Task WriteFile(string key, object value)
58 | {
59 | var jsonValue = JsonConvert.SerializeObject(value);
60 | using (var file = await folder.OpenStreamForWriteAsync(key, CreationCollisionOption.ReplaceExisting))
61 | using (var stream = new StreamWriter(file))
62 | await stream.WriteAsync(jsonValue);
63 | }
64 | }
65 | ```
66 |
67 | After the code builds, the weaver could scan your assembly looking for code which behaves a certain way, and rewrite it to include the necessary handling code:
68 |
69 | ```csharp
70 | public class DataStorage
71 | {
72 | public async Task WriteFile(string key, object value)
73 | {
74 | try
75 | {
76 | var jsonValue = JsonConvert.SerializeObject(value);
77 | using (var file = await folder.OpenStreamForWriteAsync(key, CreationCollisionOption.ReplaceExisting))
78 | using (var stream = new StreamWriter(file))
79 | await stream.WriteAsync(jsonValue);
80 | }
81 | catch (Exception exception)
82 | {
83 | AsyncErrorHandler.HandleException(exception);
84 | }
85 | }
86 | }
87 | ```
88 |
89 | And your application could provide its own implementation of the error handling module:
90 |
91 |
92 | ```csharp
93 | public static class AsyncErrorHandler
94 | {
95 | public static void HandleException(Exception exception)
96 | {
97 | Debug.WriteLine(exception);
98 | }
99 | }
100 | ```
101 |
102 | Which allows you to intercept the exceptions at runtime.
103 |
104 |
105 | ## What it really does
106 |
107 | So the above example is actually a little misleading. It shows "in effect" what is inject. In reality the injected code is a little more complicated.
108 |
109 |
110 | ### What async actually produces
111 |
112 | So given a method like this
113 |
114 | ```csharp
115 | public async Task Method()
116 | {
117 | await Task.Delay(1);
118 | }
119 | ```
120 |
121 | The compile will produce this
122 |
123 | ```csharp
124 | [AsyncStateMachine(typeof(d__0)), DebuggerStepThrough]
125 | public Task Method()
126 | {
127 | d__0 d__;
128 | d__.<>4__this = this;
129 | d__.<>t__builder = AsyncTaskMethodBuilder.Create();
130 | d__.<>1__state = -1;
131 | d__.<>t__builder.Start<d__0>(ref d__);
132 | return d__.<>t__builder.Task;
133 | }
134 | ```
135 |
136 | So "Method" has become a stub that calls into a state machine.
137 |
138 | The state machine will look like this
139 |
140 | ```csharp
141 | [CompilerGenerated]
142 | struct d__0 : IAsyncStateMachine
143 | {
144 | // Fields
145 | public int <>1__state;
146 | public Target <>4__this;
147 | public AsyncTaskMethodBuilder <>t__builder;
148 | private object <>t__stack;
149 | private TaskAwaiter <>u__$awaiter1;
150 |
151 | // Methods
152 | private void MoveNext();
153 | [DebuggerHidden]
154 | private void SetStateMachine(IAsyncStateMachine param0);
155 | }
156 | ```
157 |
158 | The method we care about is `MoveNext`. It will look something like this
159 |
160 | ```csharp
161 | void MoveNext()
162 | {
163 | try
164 | {
165 | TaskAwaiter awaiter;
166 | bool flag = true;
167 | switch (this.<>1__state)
168 | {
169 | case -3:
170 | goto Label_009F;
171 |
172 | case 0:
173 | break;
174 |
175 | default:
176 | awaiter = Task.Delay(1).GetAwaiter();
177 | if (awaiter.IsCompleted)
178 | {
179 | goto Label_006F;
180 | }
181 | this.<>1__state = 0;
182 | this.<>u__$awaiter1 = awaiter;
183 | this.<>t__builder.AwaitUnsafeOnCompletedd__0>(ref awaiter, ref this);
184 | flag = false;
185 | return;
186 | }
187 | awaiter = this.<>u__$awaiter1;
188 | this.<>u__$awaiter1 = new TaskAwaiter();
189 | this.<>1__state = -1;
190 | Label_006F:
191 | awaiter.GetResult();
192 | awaiter = new TaskAwaiter();
193 | }
194 | catch (Exception exception)
195 | {
196 | this.<>1__state = -2;
197 | this.<>t__builder.SetException(exception);
198 | return;
199 | }
200 | Label_009F:
201 | this.<>1__state = -2;
202 | this.<>t__builder.SetResult();
203 | }
204 | ```
205 |
206 | Most of that can be ignored. The important thing to note is that it is swallowing exceptions in a catch. And passing that exception to a `SetException` method.
207 |
208 | So when AsyncErrorHandler does its weaving it searches for `SetException(exception);` and then modifies the catch to look like this.
209 |
210 | ```csharp
211 | catch (Exception exception)
212 | {
213 | this.<>1__state = -2;
214 | AsyncErrorHandler.HandleException(exception);
215 | this.<>t__builder.SetException(exception);
216 | return;
217 | }
218 | ```
219 |
220 |
221 | ## Icon
222 |
223 | Icon courtesy of [The Noun Project](https://thenounproject.com)
224 |
--------------------------------------------------------------------------------