├── .appveyor.yml ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── NuGet.Config ├── README.md ├── StartupModules.sln ├── src └── StartupModules │ ├── ConfigureMiddlewareContext.cs │ ├── ConfigureServicesContext.cs │ ├── IApplicationInitializer.cs │ ├── Internal │ ├── InlineMiddlewareConfiguration.cs │ └── ModulesStartupFilter.cs │ ├── StartupModule.cs │ ├── StartupModuleRunner.cs │ ├── StartupModules.csproj │ ├── StartupModulesExtensions.cs │ └── StartupModulesOptions.cs └── test ├── StartupModules.Tests ├── FunctionalTests.cs ├── Properties │ └── launchSettings.json ├── StartupModuleTests.cs ├── StartupModules.Tests.csproj ├── StartupModulesExtensionsTests.cs ├── StartupModulesOptionsTests.cs └── StartupRunnerTests.cs └── TestWebApp ├── FooAppInitializer.cs ├── HangfireStartupModule.cs ├── Program.cs ├── Properties └── launchSettings.json └── TestWebApp.csproj /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | pull_requests: 3 | do_not_increment_build_number: true 4 | image: Visual Studio 2022 5 | nuget: 6 | disable_publish_on_pr: true 7 | build_script: 8 | - cmd: dotnet restore 9 | - cmd: dotnet build 10 | - cmd: dotnet test .\test\StartupModules.Tests 11 | - cmd: dotnet pack .\src\StartupModules -c Release -o .\artifacts 12 | test: off 13 | init: 14 | - git config --global core.autocrlf input 15 | environment: 16 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 17 | DOTNET_CLI_TELEMETRY_OPTOUT: true 18 | CI: true 19 | branches: 20 | only: 21 | - master 22 | artifacts: 23 | - path: .\src\StartupModules\artifacts\**\*.nupkg 24 | name: NuGet -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = true 23 | file_header_template = unset 24 | 25 | # this. and Me. preferences 26 | dotnet_style_qualification_for_event = false:warning 27 | dotnet_style_qualification_for_field = false:silent 28 | dotnet_style_qualification_for_method = false:warning 29 | dotnet_style_qualification_for_property = false:warning 30 | 31 | # Language keywords vs BCL types preferences 32 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 33 | dotnet_style_predefined_type_for_member_access = true:warning 34 | 35 | # Parentheses preferences 36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 40 | 41 | # Modifier preferences 42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 43 | 44 | # Expression-level preferences 45 | dotnet_style_coalesce_expression = true:suggestion 46 | dotnet_style_collection_initializer = true:suggestion 47 | dotnet_style_explicit_tuple_names = true:suggestion 48 | dotnet_style_null_propagation = true:suggestion 49 | dotnet_style_object_initializer = true:suggestion 50 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 51 | dotnet_style_prefer_auto_properties = true:silent 52 | dotnet_style_prefer_compound_assignment = true:suggestion 53 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 54 | dotnet_style_prefer_conditional_expression_over_return = true:silent 55 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 56 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 57 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 58 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 59 | dotnet_style_prefer_simplified_interpolation = true:suggestion 60 | 61 | # Field preferences 62 | dotnet_style_readonly_field = true:suggestion 63 | 64 | # Parameter preferences 65 | dotnet_code_quality_unused_parameters = all:suggestion 66 | 67 | #### C# Coding Conventions #### 68 | 69 | # var preferences 70 | csharp_style_var_elsewhere = true:suggestion 71 | csharp_style_var_for_built_in_types = true:suggestion 72 | csharp_style_var_when_type_is_apparent = true:suggestion 73 | 74 | # Expression-bodied members 75 | csharp_style_expression_bodied_accessors = true:silent 76 | csharp_style_expression_bodied_constructors = false:silent 77 | csharp_style_expression_bodied_indexers = true:silent 78 | csharp_style_expression_bodied_lambdas = true:silent 79 | csharp_style_expression_bodied_local_functions = when_on_single_line:silent 80 | csharp_style_expression_bodied_methods = when_on_single_line:silent 81 | csharp_style_expression_bodied_operators = when_on_single_line:silent 82 | csharp_style_expression_bodied_properties = true:silent 83 | 84 | # Pattern matching preferences 85 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 86 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 87 | csharp_style_prefer_switch_expression = true:suggestion 88 | 89 | # Null-checking preferences 90 | csharp_style_conditional_delegate_call = true:suggestion 91 | 92 | # Modifier preferences 93 | csharp_prefer_static_local_function = true:suggestion 94 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent 95 | 96 | # Code-block preferences 97 | csharp_prefer_braces = true:suggestion 98 | csharp_prefer_simple_using_statement = true:suggestion 99 | 100 | # Expression-level preferences 101 | csharp_prefer_simple_default_expression = true:suggestion 102 | csharp_style_deconstructed_variable_declaration = true:suggestion 103 | csharp_style_inlined_variable_declaration = true:suggestion 104 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 105 | csharp_style_prefer_index_operator = true:suggestion 106 | csharp_style_prefer_range_operator = true:suggestion 107 | csharp_style_throw_expression = true:suggestion 108 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 109 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 110 | 111 | # 'using' directive preferences 112 | csharp_using_directive_placement = outside_namespace:silent 113 | 114 | # Namespace declarations 115 | csharp_style_namespace_declarations=file_scoped:suggestion 116 | 117 | #### C# Formatting Rules #### 118 | 119 | # New line preferences 120 | csharp_new_line_before_catch = true 121 | csharp_new_line_before_else = true 122 | csharp_new_line_before_finally = true 123 | csharp_new_line_before_members_in_anonymous_types = true 124 | csharp_new_line_before_members_in_object_initializers = true 125 | csharp_new_line_before_open_brace = all 126 | csharp_new_line_between_query_expression_clauses = true 127 | 128 | # Indentation preferences 129 | csharp_indent_block_contents = true 130 | csharp_indent_braces = false 131 | csharp_indent_case_contents = true 132 | csharp_indent_case_contents_when_block = false 133 | csharp_indent_labels = one_less_than_current 134 | csharp_indent_switch_labels = true 135 | 136 | # Space preferences 137 | csharp_space_after_cast = false 138 | csharp_space_after_colon_in_inheritance_clause = true 139 | csharp_space_after_comma = true 140 | csharp_space_after_dot = false 141 | csharp_space_after_keywords_in_control_flow_statements = true 142 | csharp_space_after_semicolon_in_for_statement = true 143 | csharp_space_around_binary_operators = before_and_after 144 | csharp_space_around_declaration_statements = false 145 | csharp_space_before_colon_in_inheritance_clause = true 146 | csharp_space_before_comma = false 147 | csharp_space_before_dot = false 148 | csharp_space_before_open_square_brackets = false 149 | csharp_space_before_semicolon_in_for_statement = false 150 | csharp_space_between_empty_square_brackets = false 151 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 152 | csharp_space_between_method_call_name_and_opening_parenthesis = false 153 | csharp_space_between_method_call_parameter_list_parentheses = false 154 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 155 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 156 | csharp_space_between_method_declaration_parameter_list_parentheses = false 157 | csharp_space_between_parentheses = false 158 | csharp_space_between_square_brackets = false 159 | 160 | # Wrapping preferences 161 | csharp_preserve_single_line_blocks = true 162 | csharp_preserve_single_line_statements = true 163 | 164 | #### Naming styles #### 165 | 166 | # Naming rules 167 | 168 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 169 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 170 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 171 | 172 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 173 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 174 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 175 | 176 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 177 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 178 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 179 | 180 | dotnet_naming_rule.private_constant_should_be_pascal_case.severity = suggestion 181 | dotnet_naming_rule.private_constant_should_be_pascal_case.symbols = private_constant 182 | dotnet_naming_rule.private_constant_should_be_pascal_case.style = pascal_case 183 | 184 | dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.severity = suggestion 185 | dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.symbols = private_or_internal_static_field 186 | dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.style = pascal_case 187 | 188 | dotnet_naming_rule.private_or_internal_field_should_be_begins_with__.severity = suggestion 189 | dotnet_naming_rule.private_or_internal_field_should_be_begins_with__.symbols = private_or_internal_field 190 | dotnet_naming_rule.private_or_internal_field_should_be_begins_with__.style = begins_with__ 191 | 192 | # Symbol specifications 193 | 194 | dotnet_naming_symbols.interface.applicable_kinds = interface 195 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal 196 | dotnet_naming_symbols.interface.required_modifiers = 197 | 198 | dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field 199 | dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private 200 | dotnet_naming_symbols.private_or_internal_field.required_modifiers = 201 | 202 | dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field 203 | dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private 204 | dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static 205 | 206 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 207 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal 208 | dotnet_naming_symbols.types.required_modifiers = 209 | 210 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 211 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal 212 | dotnet_naming_symbols.non_field_members.required_modifiers = 213 | 214 | dotnet_naming_symbols.private_constant.applicable_kinds = field, local 215 | dotnet_naming_symbols.private_constant.applicable_accessibilities = private, local 216 | dotnet_naming_symbols.private_constant.required_modifiers = const 217 | 218 | # Naming styles 219 | 220 | dotnet_naming_style.pascal_case.required_prefix = 221 | dotnet_naming_style.pascal_case.required_suffix = 222 | dotnet_naming_style.pascal_case.word_separator = 223 | dotnet_naming_style.pascal_case.capitalization = pascal_case 224 | 225 | dotnet_naming_style.begins_with_i.required_prefix = I 226 | dotnet_naming_style.begins_with_i.required_suffix = 227 | dotnet_naming_style.begins_with_i.word_separator = 228 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 229 | 230 | dotnet_naming_style.begins_with__.required_prefix = _ 231 | dotnet_naming_style.begins_with__.required_suffix = 232 | dotnet_naming_style.begins_with__.word_separator = 233 | dotnet_naming_style.begins_with__.capitalization = camel_case 234 | 235 | [*.{xml,csproj}] 236 | indent_size = 2 237 | indent_style = space 238 | tab_width = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | .nuget/ 4 | packages/ 5 | artifacts/ 6 | pack/ 7 | PublishProfiles/ 8 | .vs/ 9 | .vscode/ 10 | debugSettings.json 11 | project.lock.json 12 | *.user 13 | *.suo 14 | nuget.exe 15 | *.userprefs 16 | *DS_Store 17 | *.*sdf 18 | *.ipch 19 | .settings 20 | *.sln.ide 21 | .build/ 22 | *.ldf 23 | /test/StartupModules.Tests/coverage 24 | /test/StartupModules.Tests/coverage.json 25 | test/StartupModules.Tests/coverage.opencover.xml 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | dotnet: 6.0 4 | env: 5 | global: 6 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | - DOTNET_CLI_TELEMETRY_OPTOUT: true 8 | branches: 9 | only: 10 | - master 11 | script: 12 | - dotnet restore 13 | - dotnet build 14 | - dotnet test test/StartupModules.Tests -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Henk Mollema 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Startup Modules 2 | Startup modules for ASP.NET Core. 3 | 4 | Create modular and focused Startup-like classes for each of your application's features/components/modules and keep your startup file sane. 5 | 6 | | Windows | Linux | NuGet | 7 | | ------- | ----- | ----- | 8 | | [![Windows](https://ci.appveyor.com/api/projects/status/jq76jr3b5cmxs5v6/branch/master?svg=true)](https://ci.appveyor.com/project/henkmollema/startupmodules/branch/master) | [![Linux](https://travis-ci.org/henkmollema/StartupModules.svg?branch=master)](https://travis-ci.org/henkmollema/StartupModules) | [![NuGet](https://img.shields.io/nuget/v/StartupModules.svg?style=flat-square)](https://www.nuget.org/packages/StartupModules) 9 | 10 | ## Installation 11 | 12 | #### StartupModules is [available on NuGet](https://www.nuget.org/packages/StartupModules) 13 | 14 | ## Getting started 15 | 16 | ### Create a startup module 17 | 18 | Creating a startup module is easy. Create a new class and implement the `IStartupModule` interface: 19 | 20 | ```cs 21 | public class MyStartupModule : IStartupModule 22 | { 23 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) 24 | { 25 | } 26 | 27 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) 28 | { 29 | } 30 | } 31 | ``` 32 | 33 | You'll notice the familiair `ConfigureServices` and `Configure` methods. A convient context-object is passed to both of them providing useful services while configuring your application, such as `IHostingEnvironment` and `IConfiguration`. A scoped `IServiceProvider` is present in the `ConfigureMiddlewareContext` as well. 34 | 35 | ### Configure startup modules 36 | 37 | You can configure startup modules using the `UseStartupModules` when building the web host in `Program.cs`: 38 | 39 | ```cs 40 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 41 | WebHost.CreateDefaultBuilder(args) 42 | .UseStartupModules() 43 | .UseStartup(); 44 | ``` 45 | 46 | This will automatically discover startup modules (and application initializers) in the entry assembly of your application. You can also specify an array of assemblies to discover startup modules from: 47 | 48 | ```cs 49 | .UseStartupModules(typeof(Startup).Assembly, typeof(SomeTypeInAnotherAssembly).Assembly) 50 | ``` 51 | 52 | You can have more control of the configuration using the overload with the options: 53 | 54 | ```cs 55 | .UseStartupModules(options => 56 | { 57 | // Discover from entry assembly 58 | o.DiscoverStartupModules(); 59 | 60 | // Discover from specific assemblies 61 | o.DiscoverStartupModules(typeof(Startup).Assembly, typeof(SomeTypeInAnotherAssembly).Assembly); 62 | 63 | // Add individual startup modules 64 | o.AddStartupModule(); 65 | }) 66 | ``` 67 | 68 | ### Application Initializers 69 | Application initializers allow you to write asynchronous startup logic for your application, such as configuring your Entity Framework database context and executing migrations. Applications initializers are, just like startup modules, discovered automatically as well. 70 | 71 | ```cs 72 | public class DatabaseInitializer : IApplicationInitializer 73 | { 74 | private readonly AppDbContext _dbContext; 75 | 76 | public DatabaseInitializer(AppDbContext dbContext) 77 | { 78 | _dbContext = dbContext; 79 | } 80 | 81 | public async Task Task Invoke() 82 | { 83 | await _dbContext.MigrateAsync(); 84 | } 85 | } 86 | ``` 87 | 88 | You can specify any number of dependencies available in your application via the constructor of an application initializer. The dependencies will be resolved from a **scoped** service provider instance and will be disposed after application startup. This prevents common pitfalls such as a resolving a singleton database context and leaking its connection for the lifetime off the application and avoids the hassle of creating a service provider scope yourself. 89 | -------------------------------------------------------------------------------- /StartupModules.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31912.275 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9BF5D119-F23D-4E38-ACBC-E62FF9459BDC}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StartupModules", "src\StartupModules\StartupModules.csproj", "{4D9975C6-C93B-4722-AC88-80412E3CB43C}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{22006F10-F5E1-4F57-A626-6EBDBE4A2AC8}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWebApp", "test\TestWebApp\TestWebApp.csproj", "{8E8BB8C2-7392-4724-BC63-D59F00F84874}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StartupModules.Tests", "test\StartupModules.Tests\StartupModules.Tests.csproj", "{1956C266-87A1-4A29-9CAC-C024BE9F087A}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9CE5BB1B-0E26-4A9F-8E32-AEB913E6F6E4}" 17 | ProjectSection(SolutionItems) = preProject 18 | .appveyor.yml = .appveyor.yml 19 | .editorconfig = .editorconfig 20 | .gitignore = .gitignore 21 | .travis.yml = .travis.yml 22 | LICENSE = LICENSE 23 | NuGet.Config = NuGet.Config 24 | README.md = README.md 25 | EndProjectSection 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Debug|x64 = Debug|x64 31 | Debug|x86 = Debug|x86 32 | Release|Any CPU = Release|Any CPU 33 | Release|x64 = Release|x64 34 | Release|x86 = Release|x86 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Debug|x64.Build.0 = Debug|Any CPU 41 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Debug|x86.Build.0 = Debug|Any CPU 43 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Release|x64.ActiveCfg = Release|Any CPU 46 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Release|x64.Build.0 = Release|Any CPU 47 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Release|x86.ActiveCfg = Release|Any CPU 48 | {4D9975C6-C93B-4722-AC88-80412E3CB43C}.Release|x86.Build.0 = Release|Any CPU 49 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Debug|x64.Build.0 = Debug|Any CPU 53 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Debug|x86.Build.0 = Debug|Any CPU 55 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Release|x64.ActiveCfg = Release|Any CPU 58 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Release|x64.Build.0 = Release|Any CPU 59 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Release|x86.ActiveCfg = Release|Any CPU 60 | {8E8BB8C2-7392-4724-BC63-D59F00F84874}.Release|x86.Build.0 = Release|Any CPU 61 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Debug|x64.ActiveCfg = Debug|Any CPU 64 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Debug|x64.Build.0 = Debug|Any CPU 65 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Debug|x86.ActiveCfg = Debug|Any CPU 66 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Debug|x86.Build.0 = Debug|Any CPU 67 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Release|x64.ActiveCfg = Release|Any CPU 70 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Release|x64.Build.0 = Release|Any CPU 71 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Release|x86.ActiveCfg = Release|Any CPU 72 | {1956C266-87A1-4A29-9CAC-C024BE9F087A}.Release|x86.Build.0 = Release|Any CPU 73 | EndGlobalSection 74 | GlobalSection(SolutionProperties) = preSolution 75 | HideSolutionNode = FALSE 76 | EndGlobalSection 77 | GlobalSection(NestedProjects) = preSolution 78 | {4D9975C6-C93B-4722-AC88-80412E3CB43C} = {9BF5D119-F23D-4E38-ACBC-E62FF9459BDC} 79 | {8E8BB8C2-7392-4724-BC63-D59F00F84874} = {22006F10-F5E1-4F57-A626-6EBDBE4A2AC8} 80 | {1956C266-87A1-4A29-9CAC-C024BE9F087A} = {22006F10-F5E1-4F57-A626-6EBDBE4A2AC8} 81 | EndGlobalSection 82 | GlobalSection(ExtensibilityGlobals) = postSolution 83 | SolutionGuid = {9C162947-6EAC-4F17-821B-977558D7E88B} 84 | EndGlobalSection 85 | EndGlobal 86 | -------------------------------------------------------------------------------- /src/StartupModules/ConfigureMiddlewareContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace StartupModules; 6 | 7 | /// 8 | /// Provides access to useful services during middleware configuration. 9 | /// 10 | public class ConfigureMiddlewareContext 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | public ConfigureMiddlewareContext(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, IServiceProvider serviceProvider, StartupModulesOptions options) 16 | { 17 | Configuration = configuration; 18 | HostingEnvironment = hostingEnvironment; 19 | ServiceProvider = serviceProvider; 20 | Options = options; 21 | } 22 | 23 | /// 24 | /// Gets the application instance. 25 | /// 26 | public IConfiguration Configuration { get; } 27 | 28 | /// 29 | /// Gets the application instance. 30 | /// 31 | public IWebHostEnvironment HostingEnvironment { get; } 32 | 33 | /// 34 | /// Gets the instance scoped for the lifetime of application startup. 35 | /// 36 | public IServiceProvider ServiceProvider { get; } 37 | 38 | /// 39 | /// Gets the . 40 | /// 41 | public StartupModulesOptions Options { get; } 42 | } 43 | -------------------------------------------------------------------------------- /src/StartupModules/ConfigureServicesContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace StartupModules; 5 | 6 | /// 7 | /// Provides access to useful services during application services configuration. 8 | /// 9 | public class ConfigureServicesContext 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | public ConfigureServicesContext(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, StartupModulesOptions options) 15 | { 16 | Configuration = configuration; 17 | HostingEnvironment = hostingEnvironment; 18 | Options = options; 19 | } 20 | 21 | /// 22 | /// Gets the application instance. 23 | /// 24 | public IConfiguration Configuration { get; } 25 | 26 | /// 27 | /// Gets the application instance. 28 | /// 29 | public IWebHostEnvironment HostingEnvironment { get; } 30 | 31 | /// 32 | /// Gets the . 33 | /// 34 | public StartupModulesOptions Options { get; } 35 | } 36 | -------------------------------------------------------------------------------- /src/StartupModules/IApplicationInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StartupModules; 4 | 5 | /// 6 | /// Represents a class that initializes application services during startup. 7 | /// 8 | public interface IApplicationInitializer 9 | { 10 | /// 11 | /// Invokes the instance. 12 | /// 13 | Task Invoke(); 14 | } 15 | -------------------------------------------------------------------------------- /src/StartupModules/Internal/InlineMiddlewareConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace StartupModules.Internal; 6 | 7 | /// 8 | /// Provides a with inline middleware configuration. 9 | /// 10 | public class InlineMiddlewareConfiguration : IStartupModule 11 | { 12 | private readonly Action _action; 13 | 14 | /// 15 | /// Initializes a new instance of . 16 | /// 17 | /// 18 | public InlineMiddlewareConfiguration(Action action) 19 | { 20 | _action = action; 21 | } 22 | 23 | /// 24 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) => _action(app, context); 25 | 26 | /// 27 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) { } 28 | } 29 | -------------------------------------------------------------------------------- /src/StartupModules/Internal/ModulesStartupFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace StartupModules.Internal; 7 | 8 | /// 9 | /// A startup filter that invokes the configured 's and 's. 10 | /// 11 | public class ModulesStartupFilter : IStartupFilter 12 | { 13 | private readonly StartupModuleRunner _runner; 14 | private readonly IConfiguration _configuration; 15 | private readonly IWebHostEnvironment _hostingEnvironment; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public ModulesStartupFilter(StartupModuleRunner runner, IConfiguration configuration, IWebHostEnvironment hostingEnvironment) 21 | { 22 | _runner = runner; 23 | _configuration = configuration; 24 | _hostingEnvironment = hostingEnvironment; 25 | } 26 | 27 | /// 28 | public Action Configure(Action next) => app => 29 | { 30 | _runner.Configure(app, _configuration, _hostingEnvironment); 31 | _runner.RunApplicationInitializers(app.ApplicationServices).GetAwaiter().GetResult(); 32 | next(app); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/StartupModules/StartupModule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace StartupModules; 5 | 6 | /// 7 | /// Defines a startup module to configure application services and middleware during startup. 8 | /// An application can define multiple startup modules for each of its modules/components/features. 9 | /// 10 | public interface IStartupModule 11 | { 12 | /// 13 | /// A callback to configure the application's services. 14 | /// 15 | /// The instance to configure the application's services. 16 | /// The instance which provides access to useful services. 17 | void ConfigureServices(IServiceCollection services, ConfigureServicesContext context); 18 | 19 | /// 20 | /// A callback to configure the middleware pipeline of the application. 21 | /// 22 | /// The instance to configure the middleware pipeline of the application. 23 | /// The instance which provides access to useful services. 24 | void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context); 25 | } 26 | -------------------------------------------------------------------------------- /src/StartupModules/StartupModuleRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace StartupModules; 10 | 11 | /// 12 | /// A runner for 's discoverd via . 13 | /// 14 | public class StartupModuleRunner 15 | { 16 | private readonly StartupModulesOptions _options; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The to discover 's. 22 | public StartupModuleRunner(StartupModulesOptions options) 23 | { 24 | _options = options; 25 | } 26 | 27 | /// 28 | /// Calls on the 29 | /// discoverd 's. 30 | /// 31 | public void ConfigureServices(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment hostingEnvironment) 32 | { 33 | var ctx = new ConfigureServicesContext(configuration, hostingEnvironment, _options); 34 | 35 | foreach (var cfg in _options.StartupModules) 36 | { 37 | cfg.ConfigureServices(services, ctx); 38 | } 39 | } 40 | 41 | /// 42 | /// Calls on the 43 | /// discovered . 44 | /// 45 | public void Configure(IApplicationBuilder app, IConfiguration configuration, IWebHostEnvironment hostingEnvironment) 46 | { 47 | using var scope = app.ApplicationServices.CreateScope(); 48 | var ctx = new ConfigureMiddlewareContext(configuration, hostingEnvironment, scope.ServiceProvider, _options); 49 | foreach (var cfg in _options.StartupModules) 50 | { 51 | cfg.Configure(app, ctx); 52 | } 53 | } 54 | 55 | /// 56 | /// Invokes the discovered instances. 57 | /// 58 | /// The application's root service provider. 59 | public async Task RunApplicationInitializers(IServiceProvider serviceProvider) 60 | { 61 | using var scope = serviceProvider.CreateScope(); 62 | var applicationInitializers = _options.ApplicationInitializers 63 | .Select(t => 64 | { 65 | try 66 | { 67 | return ActivatorUtilities.CreateInstance(scope.ServiceProvider, t); 68 | } 69 | catch (Exception ex) 70 | { 71 | throw new InvalidOperationException($"Failed to create instace of {nameof(IApplicationInitializer)} '{t.Name}'.", ex); 72 | } 73 | }) 74 | .Cast(); 75 | 76 | foreach (var initializer in applicationInitializers) 77 | { 78 | try 79 | { 80 | await initializer.Invoke(); 81 | } 82 | catch (Exception ex) 83 | { 84 | throw new InvalidOperationException($"An exception occured during the execution of {nameof(IApplicationInitializer)} '{initializer.GetType().Name}'.", ex); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/StartupModules/StartupModules.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4.0.0 4 | net6.0 5 | Henk Mollema 6 | Startup modules for ASP.NET Core. 7 | Copyright © Henk Mollema 2020 8 | true 9 | aspnetcore;startup;dependency injection;modules;modular 10 | https://github.com/henkmollema/StartupModules 11 | MIT 12 | git 13 | https://github.com/henkmollema/StartupModules 14 | embedded 15 | latest 16 | enable 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/StartupModules/StartupModulesExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using StartupModules.Internal; 8 | 9 | namespace StartupModules; 10 | 11 | /// 12 | /// Provides extensions to configure the startup modules infrastructure and configure or discover instances. 13 | /// 14 | public static class StartupModulesExtensions 15 | { 16 | /// 17 | /// Configures startup modules and automatically discovers 's from the applications entry assembly. 18 | /// 19 | /// The instance. 20 | /// The instance. 21 | public static IWebHostBuilder UseStartupModules(this IWebHostBuilder builder) => 22 | UseStartupModules(builder, options => options.DiscoverStartupModules()); 23 | 24 | /// 25 | /// Configures startup modules and automatically discovers 's from the specified assemblies. 26 | /// 27 | /// The instance. 28 | /// The assemblies to discover startup modules from. 29 | /// The instance. 30 | public static IWebHostBuilder UseStartupModules(this IWebHostBuilder builder, params Assembly[] assemblies) => 31 | UseStartupModules(builder, options => options.DiscoverStartupModules(assemblies)); 32 | 33 | /// 34 | /// Configures startup modules with the specified configuration for . 35 | /// 36 | /// The instance. 37 | /// A callback to configure . 38 | /// The instance. 39 | public static IWebHostBuilder UseStartupModules(this IWebHostBuilder builder, Action configure) => 40 | builder.ConfigureServices((hostContext, services) => 41 | services.AddStartupModules(hostContext.Configuration, hostContext.HostingEnvironment, configure)); 42 | 43 | /// 44 | /// Configures startup modules with the specified configuration for . 45 | /// 46 | /// The instance. 47 | /// The instance. 48 | public static WebApplicationBuilder UseStartupModules(this WebApplicationBuilder builder) => 49 | builder.UseStartupModules(options => options.DiscoverStartupModules()); 50 | 51 | /// 52 | /// Configures startup modules with the specified configuration for . 53 | /// 54 | /// The instance. 55 | /// The assemblies to discover startup modules from. 56 | /// The instance. 57 | public static WebApplicationBuilder UseStartupModules(this WebApplicationBuilder builder, params Assembly[] assemblies) => 58 | builder.UseStartupModules(options => options.DiscoverStartupModules(assemblies)); 59 | 60 | /// 61 | /// Configures startup modules with the specified configuration for . 62 | /// 63 | /// The instance. 64 | /// A callback to configure . 65 | /// The instance. 66 | public static WebApplicationBuilder UseStartupModules(this WebApplicationBuilder builder, Action configure) 67 | { 68 | builder.Services.AddStartupModules(builder.Configuration, builder.Environment, configure); 69 | return builder; 70 | } 71 | 72 | /// 73 | /// Configures startup modules with the specified configuration for . 74 | /// 75 | /// The service collection to add the StartupModules services to. 76 | /// The application's configuration. 77 | /// The application's environment information. 78 | /// A callback to configure . 79 | public static void AddStartupModules(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment environment, Action configure) 80 | { 81 | if (services == null) 82 | { 83 | throw new ArgumentNullException(nameof(services)); 84 | } 85 | if (configuration == null) 86 | { 87 | throw new ArgumentNullException(nameof(configuration)); 88 | } 89 | if (environment == null) 90 | { 91 | throw new ArgumentNullException(nameof(environment)); 92 | } 93 | 94 | var options = new StartupModulesOptions(); 95 | configure(options); 96 | 97 | if (options.StartupModules.Count == 0 && options.ApplicationInitializers.Count == 0) 98 | { 99 | // Nothing to do here 100 | return; 101 | } 102 | 103 | var runner = new StartupModuleRunner(options); 104 | services.AddSingleton(sp => ActivatorUtilities.CreateInstance(sp, runner)); 105 | 106 | var configureServicesContext = new ConfigureServicesContext(configuration, environment, options); 107 | runner.ConfigureServices(services, configuration, environment); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/StartupModules/StartupModulesOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Microsoft.AspNetCore.Builder; 6 | using StartupModules.Internal; 7 | 8 | namespace StartupModules; 9 | 10 | /// 11 | /// Specifies options for startup modules. 12 | /// 13 | public class StartupModulesOptions 14 | { 15 | /// 16 | /// Gets a collection of instances to configure the application with. 17 | /// 18 | public ICollection StartupModules { get; } = new List(); 19 | 20 | /// 21 | /// Gets a collection of types to initialize the application with. 22 | /// 23 | public ICollection ApplicationInitializers { get; } = new List(); 24 | 25 | /// 26 | /// Gets the settings. 27 | /// 28 | public IDictionary Settings { get; set; } = new Dictionary(); 29 | 30 | /// 31 | /// Discovers implementations from the entry assembly of the application. 32 | /// 33 | public void DiscoverStartupModules() => DiscoverStartupModules(Assembly.GetEntryAssembly()!); 34 | 35 | /// 36 | /// Discovers implementations from the specified assemblies. 37 | /// 38 | public void DiscoverStartupModules(params Assembly[] assemblies) 39 | { 40 | if (assemblies == null || assemblies.Length == 0 || assemblies.All(a => a == null)) 41 | { 42 | throw new ArgumentException("No assemblies to discover startup modules from specified.", nameof(assemblies)); 43 | } 44 | 45 | foreach (var type in assemblies.SelectMany(a => a.ExportedTypes)) 46 | { 47 | if (typeof(IStartupModule).IsAssignableFrom(type)) 48 | { 49 | var instance = Activate(type); 50 | StartupModules.Add(instance); 51 | } 52 | else if (typeof(IApplicationInitializer).IsAssignableFrom(type)) 53 | { 54 | ApplicationInitializers.Add(type); 55 | } 56 | } 57 | } 58 | 59 | /// 60 | /// Adds the instance of type . 61 | /// 62 | /// The type of the . 63 | /// The instance. 64 | public void AddStartupModule(T startupModule) where T : IStartupModule 65 | => StartupModules.Add(startupModule); 66 | 67 | /// 68 | /// Adds the implementation of type . 69 | /// The type will be activated using the default, parameterless constructor. 70 | /// 71 | /// The type of the . 72 | public void AddStartupModule() where T : IStartupModule 73 | => AddStartupModule(typeof(T)); 74 | 75 | /// 76 | /// Adds a by the specified type. 77 | /// The type will be activated using the default, parameterless constructor. 78 | /// 79 | /// The type of the . 80 | public void AddStartupModule(Type type) 81 | { 82 | if (typeof(IStartupModule).IsAssignableFrom(type)) 83 | { 84 | var instance = Activate(type); 85 | StartupModules.Add(instance); 86 | } 87 | else 88 | { 89 | throw new ArgumentException( 90 | $"Specified startup module '{type.Name}' does not implement {nameof(IStartupModule)}.", 91 | nameof(type)); 92 | } 93 | } 94 | 95 | /// 96 | /// Adds an inline middleware configuration to the application. 97 | /// 98 | /// A callback to configure the middleware pipeline. 99 | public void ConfigureMiddleware(Action action) => 100 | StartupModules.Add(new InlineMiddlewareConfiguration(action)); 101 | 102 | private IStartupModule Activate(Type type) 103 | { 104 | try 105 | { 106 | return (IStartupModule)Activator.CreateInstance(type)!; 107 | } 108 | catch (Exception ex) 109 | { 110 | throw new InvalidOperationException($"Failed to create instance for {nameof(IStartupModule)} type '{type.Name}'.", ex); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/StartupModules.Tests/FunctionalTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.TestHost; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Xunit; 8 | 9 | namespace StartupModules.Tests; 10 | 11 | public class FunctionalTests 12 | { 13 | [Fact] 14 | public async Task ConfiguresServicesAndMiddleware() 15 | { 16 | var webHostBuilder = new WebHostBuilder() 17 | .UseStartupModules(c => c.AddStartupModule()) 18 | .Configure(_ => { }); 19 | 20 | using var server = new TestServer(webHostBuilder); 21 | var client = server.CreateClient(); 22 | var msg = await client.GetStringAsync("/message"); 23 | 24 | Assert.Equal("Hello, World!", msg); 25 | } 26 | } 27 | 28 | public class FooStartupModule : IStartupModule 29 | { 30 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) 31 | { 32 | services.AddSingleton(); 33 | } 34 | 35 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) 36 | { 37 | app.Map("/message", sub => sub.Run(async ctx => 38 | { 39 | var msgProvider = ctx.RequestServices.GetRequiredService(); 40 | await ctx.Response.WriteAsync(msgProvider.GetMessage()); 41 | })); 42 | } 43 | } 44 | 45 | public class MessageProvider 46 | { 47 | public string GetMessage() => "Hello, World!"; 48 | } 49 | -------------------------------------------------------------------------------- /test/StartupModules.Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:50128/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "StartupModules.Tests": { 19 | "commandName": "Project" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /test/StartupModules.Tests/StartupModuleTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Xunit; 7 | 8 | namespace StartupModules.Tests; 9 | 10 | public class StartupModuleTests 11 | { 12 | [Fact] 13 | public async Task ConfiguresContext() 14 | { 15 | var hostBuilder = CreateBuilder().UseStartupModules(o => o.AddStartupModule()); 16 | using var host = hostBuilder.Build(); 17 | await host.StartAsync(); 18 | await host.StopAsync(); 19 | } 20 | 21 | [Fact] 22 | public async Task DiscoversFromSpecifiedAssembly() 23 | { 24 | var hostBuilder = CreateBuilder().UseStartupModules(o => o.DiscoverStartupModules(typeof(FooStartupModule).Assembly)); 25 | using var host = hostBuilder.Build(); 26 | await host.StartAsync(); 27 | await host.StopAsync(); 28 | } 29 | 30 | [Fact] 31 | public async Task DiscoversFromEntryAssembly() 32 | { 33 | // Equivalent of calling UseStartupModules() 34 | var hostBuilder = CreateBuilder().UseStartupModules(o => o.DiscoverStartupModules()); 35 | using var host = hostBuilder.Build(); 36 | await host.StartAsync(); 37 | await host.StopAsync(); 38 | } 39 | 40 | [Fact] 41 | public async Task DiscoversFromEntryAssemblyWithDefaultMethod() 42 | { 43 | // Equivalent of calling UseStartupModules() 44 | var hostBuilder = CreateBuilder().UseStartupModules(); 45 | using var host = hostBuilder.Build(); 46 | await host.StartAsync(); 47 | await host.StopAsync(); 48 | } 49 | 50 | private IWebHostBuilder CreateBuilder() => new WebHostBuilder().UseKestrel().Configure(_ => { }); 51 | 52 | public class FooStartupModule : IStartupModule 53 | { 54 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) 55 | { 56 | Assert.NotNull(services); 57 | Assert.NotNull(context); 58 | Assert.NotNull(context.Configuration); 59 | Assert.NotNull(context.HostingEnvironment); 60 | } 61 | 62 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) 63 | { 64 | Assert.NotNull(app); 65 | Assert.NotNull(context); 66 | Assert.NotNull(context.Configuration); 67 | Assert.NotNull(context.HostingEnvironment); 68 | Assert.NotNull(context.ServiceProvider); 69 | } 70 | } 71 | 72 | public class FooApplicationInitializer : IApplicationInitializer 73 | { 74 | private readonly IHostApplicationLifetime _applicationLifetime; 75 | 76 | public FooApplicationInitializer(IHostApplicationLifetime applicationLifetime) 77 | { 78 | _applicationLifetime = applicationLifetime; 79 | } 80 | 81 | public Task Invoke() 82 | { 83 | Assert.NotNull(_applicationLifetime); 84 | return Task.CompletedTask; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/StartupModules.Tests/StartupModules.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | false 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/StartupModules.Tests/StartupModulesExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using StartupModules.Internal; 6 | using Xunit; 7 | 8 | namespace StartupModules.Tests; 9 | 10 | public class StartupModulesExtensionsTests 11 | { 12 | [Fact] 13 | public void Noops_WhenNoStartupModulesAndApplicationInitializers() 14 | { 15 | var builder = new WebHostBuilder() 16 | .Configure(_ => { }) 17 | .UseStartupModules(_ => { }) 18 | .Build(); 19 | 20 | Assert.Empty(builder.Services.GetService>().Where(x => x is ModulesStartupFilter)); 21 | } 22 | 23 | [Fact] 24 | public void Noops_WhenNoStartupModulesAndApplicationInitializersInEntryAssembly() 25 | { 26 | var builder = new WebHostBuilder() 27 | .Configure(_ => { }) 28 | .UseStartupModules() 29 | .Build(); 30 | 31 | Assert.Empty(builder.Services.GetService>().Where(x => x is ModulesStartupFilter)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/StartupModules.Tests/StartupModulesOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using StartupModules.Internal; 5 | using Xunit; 6 | 7 | namespace StartupModules.Tests; 8 | 9 | public class StartupModulesOptionsTests 10 | { 11 | [Fact] 12 | public void AddStartupModule_FromGenericType() 13 | { 14 | var options = new StartupModulesOptions(); 15 | options.AddStartupModule(); 16 | 17 | var module = Assert.Single(options.StartupModules); 18 | Assert.IsType(module); 19 | } 20 | 21 | [Fact] 22 | public void AddStartupModule_FromType() 23 | { 24 | var options = new StartupModulesOptions(); 25 | options.AddStartupModule(typeof(FooStartupModule)); 26 | 27 | var module = Assert.Single(options.StartupModules); 28 | Assert.IsType(module); 29 | } 30 | 31 | [Fact] 32 | public void AddStartupModule_FromInstance() 33 | { 34 | var options = new StartupModulesOptions(); 35 | options.AddStartupModule(new FooStartupModule()); 36 | 37 | var module = Assert.Single(options.StartupModules); 38 | Assert.IsType(module); 39 | } 40 | 41 | [Fact] 42 | public void ThrowsException_WhenTypeNotIStartupModule() 43 | { 44 | var options = new StartupModulesOptions(); 45 | var ex = Assert.Throws("type", () => options.AddStartupModule(typeof(decimal))); 46 | var expectedEx = new ArgumentException($"Specified startup module '{typeof(decimal).Name}' does not implement {nameof(IStartupModule)}.", "type"); 47 | Assert.Equal(expectedEx.Message, ex.Message); 48 | } 49 | 50 | [Fact] 51 | public void ConfigureMiddleware_AddsStartupModule() 52 | { 53 | var options = new StartupModulesOptions(); 54 | options.ConfigureMiddleware((app, ctx) => { }); 55 | 56 | var module = Assert.Single(options.StartupModules); 57 | Assert.IsType(module); 58 | module.ConfigureServices(null, null); 59 | module.Configure(null, null); 60 | } 61 | 62 | [Fact] 63 | public void ThrowsException_WhenNoParameterlessConstructor() 64 | { 65 | var options = new StartupModulesOptions(); 66 | var ex = Assert.Throws(() => options.AddStartupModule()); 67 | var expectedMsg = $"Failed to create instance for {nameof(IStartupModule)} type '{typeof(StartupModuleWithCtor).Name}'."; 68 | Assert.Equal(expectedMsg, ex.Message); 69 | } 70 | 71 | [Fact] 72 | public void ThrowsException_WithErrorConstructor() 73 | { 74 | var options = new StartupModulesOptions(); 75 | var ex = Assert.Throws(() => options.AddStartupModule()); 76 | var expectedMsg = $"Failed to create instance for {nameof(IStartupModule)} type '{typeof(StartupModuleWithErrorCtor).Name}'."; 77 | Assert.Equal(expectedMsg, ex.Message); 78 | } 79 | 80 | // Internal to avoid automatic discovery 81 | internal class FooStartupModule : IStartupModule 82 | { 83 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) => throw new NotImplementedException(); 84 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) => throw new NotImplementedException(); 85 | } 86 | 87 | // Internal to avoid automatic discovery 88 | internal class StartupModuleWithCtor : IStartupModule 89 | { 90 | public StartupModuleWithCtor(object _) { } 91 | 92 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) => throw new NotImplementedException(); 93 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) => throw new NotImplementedException(); 94 | } 95 | 96 | // Internal to avoid automatic discovery 97 | internal class StartupModuleWithErrorCtor : IStartupModule 98 | { 99 | public StartupModuleWithErrorCtor() 100 | { 101 | throw new ArgumentException(); 102 | } 103 | 104 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) => throw new NotImplementedException(); 105 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) => throw new NotImplementedException(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/StartupModules.Tests/StartupRunnerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | 6 | namespace StartupModules.Tests; 7 | 8 | public class StartupRunnerTests 9 | { 10 | [Fact] 11 | public void ConfiguresServices() 12 | { 13 | // Arrange 14 | var options = new StartupModulesOptions(); 15 | options.AddStartupModule(); 16 | var runner = new StartupModuleRunner(options); 17 | var services = new ServiceCollection(); 18 | 19 | // Act 20 | runner.ConfigureServices(services, null, null); 21 | 22 | // Assert 23 | var sd = Assert.Single(services); 24 | Assert.Equal(typeof(MyStartupModule.MyService), sd.ImplementationType); 25 | } 26 | 27 | [Fact] 28 | public void Configures() 29 | { 30 | // Arrange 31 | var options = new StartupModulesOptions(); 32 | var startupModule = new MyStartupModule(); 33 | options.AddStartupModule(startupModule); 34 | var runner = new StartupModuleRunner(options); 35 | 36 | // Act 37 | runner.Configure(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()), null, null); 38 | 39 | // Assert 40 | Assert.True(startupModule.Configured); 41 | } 42 | 43 | [Fact] 44 | public async Task RunsApplicationInitializers() 45 | { 46 | // Arrange 47 | var options = new StartupModulesOptions(); 48 | options.ApplicationInitializers.Add(typeof(MyAppInitializer)); 49 | var runner = new StartupModuleRunner(options); 50 | 51 | // Act 52 | await runner.RunApplicationInitializers(new ServiceCollection().BuildServiceProvider()); 53 | 54 | // Assert 55 | // wat do ¯\_(ツ)_/¯ 56 | } 57 | } 58 | 59 | public class MyStartupModule : IStartupModule 60 | { 61 | public class MyService { } 62 | 63 | public bool Configured { get; private set; } 64 | 65 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) => services.AddSingleton(); 66 | 67 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) => Configured = true; 68 | } 69 | 70 | public class MyAppInitializer : IApplicationInitializer 71 | { 72 | public Task Invoke() => Task.CompletedTask; 73 | } 74 | -------------------------------------------------------------------------------- /test/TestWebApp/FooAppInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using StartupModules; 3 | 4 | namespace TestWebApp; 5 | 6 | public class FooAppInitializer : IApplicationInitializer 7 | { 8 | public Task Invoke() 9 | { 10 | return Task.CompletedTask; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/TestWebApp/HangfireStartupModule.cs: -------------------------------------------------------------------------------- 1 | using Hangfire.MemoryStorage; 2 | using StartupModules; 3 | using Webenable.Hangfire.Contrib; 4 | 5 | namespace TestWebApp; 6 | 7 | public class HangfireStartupModule : IStartupModule 8 | { 9 | 10 | public void ConfigureServices(IServiceCollection services, ConfigureServicesContext context) 11 | { 12 | if (context.HostingEnvironment.IsDevelopment()) 13 | { 14 | // Do something based on this 15 | } 16 | 17 | if ((bool)context.Options.Settings["AddHangfire"] == true) 18 | { 19 | services.AddHangfireContrib(c => c.UseMemoryStorage()); 20 | } 21 | } 22 | 23 | public void Configure(IApplicationBuilder app, ConfigureMiddlewareContext context) { } 24 | } 25 | -------------------------------------------------------------------------------- /test/TestWebApp/Program.cs: -------------------------------------------------------------------------------- 1 | using StartupModules; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | builder.UseStartupModules(); 5 | var app = builder.Build(); 6 | 7 | app.MapGet("/", () => "Hello, World!"); 8 | app.Run(); -------------------------------------------------------------------------------- /test/TestWebApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "TestWebApp": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "applicationUrl": "http://localhost:5000", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/TestWebApp/TestWebApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | enable 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | --------------------------------------------------------------------------------