diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/AspNetCoreConfigurationSeries.sln b/aspnetcore-webapi/AspNetCoreConfigurationSeries/AspNetCoreConfigurationSeries.sln new file mode 100644 index 000000000..f12cc5610 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/AspNetCoreConfigurationSeries.sln @@ -0,0 +1,214 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConfigurationBasics", "ConfigurationBasics", "{E440020E-AEC5-2003-E9BF-47A625837F4A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "ConfigurationBasics\ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationBasics.Tests", "ConfigurationBasics\ConfigurationBasics.Tests\ConfigurationBasics.Tests.csproj", "{C0E70EFC-A182-40D2-901F-9A583B9AEF5C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OptionsPattern", "OptionsPattern", "{5D588406-17D5-0299-8AE0-6BF58B0530F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "OptionsPattern\ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{E2160BF6-266C-4027-804C-542E3447C0E6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptionsPattern.Tests", "OptionsPattern\OptionsPattern.Tests\OptionsPattern.Tests.csproj", "{79ECBFF8-DCD6-4C93-8996-2F072103AE5F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OptionsValidation", "OptionsValidation", "{1391FDFE-BD35-8919-74EB-F3B38CB2D00D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "OptionsValidation\ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{2516A86F-DBE7-44A4-8FCA-FC9F734EC904}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptionsValidation.Tests", "OptionsValidation\OptionsValidation.Tests\OptionsValidation.Tests.csproj", "{A6453D0E-163F-4410-9EBF-17E4DE82874B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConfigurationProviders", "ConfigurationProviders", "{8591B607-85C4-7281-F2D9-E7CB135B7177}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "ConfigurationProviders\ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{FCDD4279-65E2-45FB-9C75-AB3B774AB42E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationProviders.Tests", "ConfigurationProviders\ConfigurationProviders.Tests\ConfigurationProviders.Tests.csproj", "{59272CC8-3F27-4A7A-9D8B-0112CC0225DB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomConfigurationProvider", "CustomConfigurationProvider", "{199531CA-AC53-58B9-48ED-1EE607A3B125}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "CustomConfigurationProvider\ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomConfigurationProvider.Tests", "CustomConfigurationProvider\CustomConfigurationProvider.Tests\CustomConfigurationProvider.Tests.csproj", "{8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SecuringDataLocally", "SecuringDataLocally", "{E878789B-BF1A-09CD-D764-9880293A342A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "SecuringDataLocally\ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecuringDataLocally.Tests", "SecuringDataLocally\SecuringDataLocally.Tests\SecuringDataLocally.Tests.csproj", "{07D4908B-1FE0-49BD-96B4-5244943691A2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Debug|x64.ActiveCfg = Debug|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Debug|x64.Build.0 = Debug|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Debug|x86.ActiveCfg = Debug|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Debug|x86.Build.0 = Debug|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Release|Any CPU.Build.0 = Release|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Release|x64.ActiveCfg = Release|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Release|x64.Build.0 = Release|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Release|x86.ActiveCfg = Release|Any CPU + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011}.Release|x86.Build.0 = Release|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Debug|x64.ActiveCfg = Debug|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Debug|x64.Build.0 = Debug|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Debug|x86.ActiveCfg = Debug|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Debug|x86.Build.0 = Debug|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Release|Any CPU.Build.0 = Release|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Release|x64.ActiveCfg = Release|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Release|x64.Build.0 = Release|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Release|x86.ActiveCfg = Release|Any CPU + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C}.Release|x86.Build.0 = Release|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Debug|x64.ActiveCfg = Debug|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Debug|x64.Build.0 = Debug|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Debug|x86.ActiveCfg = Debug|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Debug|x86.Build.0 = Debug|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Release|Any CPU.Build.0 = Release|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Release|x64.ActiveCfg = Release|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Release|x64.Build.0 = Release|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Release|x86.ActiveCfg = Release|Any CPU + {E2160BF6-266C-4027-804C-542E3447C0E6}.Release|x86.Build.0 = Release|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Debug|x64.ActiveCfg = Debug|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Debug|x64.Build.0 = Debug|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Debug|x86.ActiveCfg = Debug|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Debug|x86.Build.0 = Debug|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Release|Any CPU.Build.0 = Release|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Release|x64.ActiveCfg = Release|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Release|x64.Build.0 = Release|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Release|x86.ActiveCfg = Release|Any CPU + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F}.Release|x86.Build.0 = Release|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Debug|x64.ActiveCfg = Debug|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Debug|x64.Build.0 = Debug|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Debug|x86.ActiveCfg = Debug|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Debug|x86.Build.0 = Debug|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Release|Any CPU.Build.0 = Release|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Release|x64.ActiveCfg = Release|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Release|x64.Build.0 = Release|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Release|x86.ActiveCfg = Release|Any CPU + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904}.Release|x86.Build.0 = Release|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Debug|x64.Build.0 = Debug|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Debug|x86.Build.0 = Debug|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Release|Any CPU.Build.0 = Release|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Release|x64.ActiveCfg = Release|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Release|x64.Build.0 = Release|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Release|x86.ActiveCfg = Release|Any CPU + {A6453D0E-163F-4410-9EBF-17E4DE82874B}.Release|x86.Build.0 = Release|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Debug|x64.ActiveCfg = Debug|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Debug|x64.Build.0 = Debug|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Debug|x86.ActiveCfg = Debug|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Debug|x86.Build.0 = Debug|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Release|Any CPU.Build.0 = Release|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Release|x64.ActiveCfg = Release|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Release|x64.Build.0 = Release|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Release|x86.ActiveCfg = Release|Any CPU + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E}.Release|x86.Build.0 = Release|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Debug|x64.ActiveCfg = Debug|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Debug|x64.Build.0 = Debug|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Debug|x86.ActiveCfg = Debug|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Debug|x86.Build.0 = Debug|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Release|Any CPU.Build.0 = Release|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Release|x64.ActiveCfg = Release|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Release|x64.Build.0 = Release|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Release|x86.ActiveCfg = Release|Any CPU + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB}.Release|x86.Build.0 = Release|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Debug|x64.ActiveCfg = Debug|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Debug|x64.Build.0 = Debug|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Debug|x86.ActiveCfg = Debug|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Debug|x86.Build.0 = Debug|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Release|Any CPU.Build.0 = Release|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Release|x64.ActiveCfg = Release|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Release|x64.Build.0 = Release|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Release|x86.ActiveCfg = Release|Any CPU + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4}.Release|x86.Build.0 = Release|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Debug|x64.ActiveCfg = Debug|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Debug|x64.Build.0 = Debug|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Debug|x86.ActiveCfg = Debug|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Debug|x86.Build.0 = Debug|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Release|Any CPU.Build.0 = Release|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Release|x64.ActiveCfg = Release|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Release|x64.Build.0 = Release|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Release|x86.ActiveCfg = Release|Any CPU + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130}.Release|x86.Build.0 = Release|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Debug|x64.ActiveCfg = Debug|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Debug|x64.Build.0 = Debug|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Debug|x86.ActiveCfg = Debug|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Debug|x86.Build.0 = Debug|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Release|Any CPU.Build.0 = Release|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Release|x64.ActiveCfg = Release|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Release|x64.Build.0 = Release|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Release|x86.ActiveCfg = Release|Any CPU + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3}.Release|x86.Build.0 = Release|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Debug|x64.Build.0 = Debug|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Debug|x86.Build.0 = Debug|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Release|Any CPU.Build.0 = Release|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Release|x64.ActiveCfg = Release|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Release|x64.Build.0 = Release|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Release|x86.ActiveCfg = Release|Any CPU + {07D4908B-1FE0-49BD-96B4-5244943691A2}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {72C6FAF1-FD5C-4864-A8E6-C4AA41B0A011} = {E440020E-AEC5-2003-E9BF-47A625837F4A} + {C0E70EFC-A182-40D2-901F-9A583B9AEF5C} = {E440020E-AEC5-2003-E9BF-47A625837F4A} + {E2160BF6-266C-4027-804C-542E3447C0E6} = {5D588406-17D5-0299-8AE0-6BF58B0530F0} + {79ECBFF8-DCD6-4C93-8996-2F072103AE5F} = {5D588406-17D5-0299-8AE0-6BF58B0530F0} + {2516A86F-DBE7-44A4-8FCA-FC9F734EC904} = {1391FDFE-BD35-8919-74EB-F3B38CB2D00D} + {A6453D0E-163F-4410-9EBF-17E4DE82874B} = {1391FDFE-BD35-8919-74EB-F3B38CB2D00D} + {FCDD4279-65E2-45FB-9C75-AB3B774AB42E} = {8591B607-85C4-7281-F2D9-E7CB135B7177} + {59272CC8-3F27-4A7A-9D8B-0112CC0225DB} = {8591B607-85C4-7281-F2D9-E7CB135B7177} + {4DF72404-A31D-4FAC-B8A4-DCFF95FDCEA4} = {199531CA-AC53-58B9-48ED-1EE607A3B125} + {8CC5F09B-CEC3-4B32-91E8-D59AFDCAE130} = {199531CA-AC53-58B9-48ED-1EE607A3B125} + {42D6CA51-79C0-4A3A-B45E-5D0025B2E0F3} = {E878789B-BF1A-09CD-D764-9880293A342A} + {07D4908B-1FE0-49BD-96B4-5244943691A2} = {E878789B-BF1A-09CD-D764-9880293A342A} + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ConfigurationBasics.Tests/ConfigurationBasics.Tests.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ConfigurationBasics.Tests/ConfigurationBasics.Tests.csproj new file mode 100644 index 000000000..8f4da1b47 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ConfigurationBasics.Tests/ConfigurationBasics.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ConfigurationBasics.Tests/ReadingConfigurationTests.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ConfigurationBasics.Tests/ReadingConfigurationTests.cs new file mode 100644 index 000000000..d887bfbf0 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ConfigurationBasics.Tests/ReadingConfigurationTests.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Configuration; +using ProjectConfigurationDemo.Models; + +namespace ConfigurationBasics.Tests; + +public class ReadingConfigurationTests +{ + private static IConfiguration BuildConfiguration() => + new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Logging:LogLevel:Default"] = "Information", + ["Logging:LogLevel:Microsoft"] = "Warning", + ["ConnectionStrings:sqlConnection"] = "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True" + }) + .Build(); + + [Fact] + public void GivenAConfiguration_WhenReadWithTheIndexer_ThenItReturnsTheValueAtThatKeyPath() + { + var configuration = BuildConfiguration(); + + var logLevel = configuration["Logging:LogLevel:Default"]; + + Assert.Equal("Information", logLevel); + } + + [Fact] + public void GivenAConfiguration_WhenReadWithGetValue_ThenItConvertsToTheRequestedType() + { + var configuration = BuildConfiguration(); + + var logLevel = configuration.GetValue("Logging:LogLevel:Default"); + + Assert.Equal("Information", logLevel); + } + + [Fact] + public void GivenAConfiguration_WhenGetConnectionStringIsCalled_ThenItReadsTheConnectionStringsSection() + { + var configuration = BuildConfiguration(); + + var connectionString = configuration.GetConnectionString("sqlConnection"); + + Assert.Equal("Server=.;Database=CodeMazeCommerce;Trusted_Connection=True", connectionString); + } + + [Fact] + public void GivenAConfiguration_WhenBindIsCalled_ThenItPopulatesTheStronglyTypedObject() + { + var configuration = BuildConfiguration(); + var logLevelConfiguration = new LoggingLevelConfiguration(); + + configuration.Bind("Logging:LogLevel", logLevelConfiguration); + + Assert.Equal("Information", logLevelConfiguration.Default); + } + + [Fact] + public void GivenAConfiguration_WhenAKeyIsMissing_ThenTheIndexerReturnsNullRatherThanThrowing() + { + var configuration = BuildConfiguration(); + + var missing = configuration["Logging:LogLevel:NotThere"]; + + Assert.Null(missing); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo.sln b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo.sln new file mode 100644 index 000000000..072813330 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{D8638F89-19EC-4916-83C6-27EA52E28F84}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationBasics.Tests", "ConfigurationBasics.Tests\ConfigurationBasics.Tests.csproj", "{9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Debug|x64.ActiveCfg = Debug|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Debug|x64.Build.0 = Debug|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Debug|x86.ActiveCfg = Debug|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Debug|x86.Build.0 = Debug|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Release|Any CPU.Build.0 = Release|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Release|x64.ActiveCfg = Release|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Release|x64.Build.0 = Release|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Release|x86.ActiveCfg = Release|Any CPU + {D8638F89-19EC-4916-83C6-27EA52E28F84}.Release|x86.Build.0 = Release|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Debug|x64.Build.0 = Debug|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Debug|x86.ActiveCfg = Debug|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Debug|x86.Build.0 = Debug|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Release|Any CPU.Build.0 = Release|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Release|x64.ActiveCfg = Release|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Release|x64.Build.0 = Release|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Release|x86.ActiveCfg = Release|Any CPU + {9F1EF799-C7E1-485D-BD6F-DDBCC3091C04}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Controllers/HomeController.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Controllers/HomeController.cs new file mode 100644 index 000000000..edc9c5444 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Controllers/HomeController.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.Controllers; + +public class HomeController(ILogger logger, IConfiguration configuration) : Controller +{ + private readonly ILogger _logger = logger; + private readonly IConfiguration _configuration = configuration; + + public IActionResult Index() + { + var logLevelConfiguration = new LoggingLevelConfiguration(); + + _configuration.Bind("Logging:LogLevel", logLevelConfiguration); + + var homeModel = new HomeModel + { + DefaultLogLevel = logLevelConfiguration.Default + }; + + return View(homeModel); + } + + public IActionResult Privacy() => View(); + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() => + View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/ErrorViewModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/ErrorViewModel.cs new file mode 100644 index 000000000..d3daed82d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/ErrorViewModel.cs @@ -0,0 +1,8 @@ +namespace ProjectConfigurationDemo.Models; + +public class ErrorViewModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/HomeModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/HomeModel.cs new file mode 100644 index 000000000..9c6b9346c --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/HomeModel.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class HomeModel +{ + public string? DefaultLogLevel { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs new file mode 100644 index 000000000..9cc59d60f --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class LoggingLevelConfiguration +{ + public string? Default { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Program.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Program.cs new file mode 100644 index 000000000..65df0ebb4 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Program.cs @@ -0,0 +1,13 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +app.MapStaticAssets(); +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}") + .WithStaticAssets(); + +app.Run(); diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj new file mode 100644 index 000000000..a3a34b647 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Properties/launchSettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Properties/launchSettings.json new file mode 100644 index 000000000..92ac1b057 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Home/Index.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Home/Index.cshtml new file mode 100644 index 000000000..40b46f575 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Home/Index.cshtml @@ -0,0 +1,9 @@ +@model HomeModel +@{ + ViewData["Title"] = "Home Page"; +} +
+

Welcome

+ Our default logging level is @Model.DefaultLogLevel +

Learn about building Web apps with ASP.NET Core.

+
diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Home/Privacy.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Home/Privacy.cshtml new file mode 100644 index 000000000..d01477f38 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Shared/Error.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Shared/Error.cshtml new file mode 100644 index 000000000..8f923fdf4 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Shared/Error.cshtml @@ -0,0 +1,25 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the + ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app. +

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000..208bd9a35 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml @@ -0,0 +1,30 @@ + + + + + + @ViewData["Title"] - ProjectConfigurationDemo + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+ © @DateTime.Now.Year - ProjectConfigurationDemo - + Privacy +
+ + @await RenderSectionAsync("Scripts", required: false) + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/_ViewImports.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/_ViewImports.cshtml new file mode 100644 index 000000000..92266eb48 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using ProjectConfigurationDemo +@using ProjectConfigurationDemo.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/_ViewStart.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/_ViewStart.cshtml new file mode 100644 index 000000000..820a2f6e0 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.Development.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.Production.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.Production.json new file mode 100644 index 000000000..50c269845 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.Production.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "sqlConnection": "Server=ProductionServerName;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.json new file mode 100644 index 000000000..079b7edb2 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "sqlConnection": "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + }, + "OtherLoggingProvider": { + "LogLevel": { + "Default": "Debug" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/css/site.css b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/css/site.css new file mode 100644 index 000000000..d78436d57 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/css/site.css @@ -0,0 +1,49 @@ +html { + font-size: 14px; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; +} + +body { + margin: 0; + color: #212529; + background-color: #fff; +} + +header nav { + display: flex; + gap: 1rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e5e5e5; +} + +header nav a { + color: #212529; + text-decoration: none; +} + +.container { + max-width: 960px; + margin: 0 auto; + padding: 1.5rem; +} + +.text-center { + text-align: center; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; +} + +.text-danger { + color: #dc3545; +} + +footer { + max-width: 960px; + margin: 0 auto; + padding: 1rem 1.5rem; + border-top: 1px solid #e5e5e5; + color: #6c757d; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/favicon.ico b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/favicon.ico new file mode 100644 index 000000000..a3a799985 Binary files /dev/null and b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/favicon.ico differ diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/js/site.js b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/js/site.js new file mode 100644 index 000000000..104169066 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationBasics/ProjectConfigurationDemo/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your JavaScript code. diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ConfigurationProviders.Tests/ConfigurationProviders.Tests.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ConfigurationProviders.Tests/ConfigurationProviders.Tests.csproj new file mode 100644 index 000000000..de94ec341 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ConfigurationProviders.Tests/ConfigurationProviders.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ConfigurationProviders.Tests/ProviderOrderTests.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ConfigurationProviders.Tests/ProviderOrderTests.cs new file mode 100644 index 000000000..16982ced0 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ConfigurationProviders.Tests/ProviderOrderTests.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Configuration; + +namespace ConfigurationProviders.Tests; + +public class ProviderOrderTests : IDisposable +{ + private readonly string _directory = + Directory.CreateTempSubdirectory("ConfigurationProvidersTests").FullName; + + private string WriteJson(string level) + { + var path = Path.Combine(_directory, $"appsettings.{Guid.NewGuid():N}.json"); + File.WriteAllText(path, "{\"Logging\":{\"LogLevel\":{\"Default\":\"" + level + "\"}}}"); + return path; + } + + private string WriteIni(string level) + { + var path = Path.Combine(_directory, $"appsettings.{Guid.NewGuid():N}.ini"); + File.WriteAllText(path, $"[Logging:LogLevel]{Environment.NewLine}Default={level}{Environment.NewLine}"); + return path; + } + + // The article's central claim, executable: when two sources define the same + // key, the one added last wins. + [Fact] + public void GivenJsonThenIni_WhenBothDefineTheSameKey_ThenTheIniValueWins() + { + var json = WriteJson("Information"); + var ini = WriteIni("Debug"); + + var configuration = new ConfigurationBuilder() + .AddJsonFile(json) + .AddIniFile(ini) + .Build(); + + Assert.Equal("Debug", configuration["Logging:LogLevel:Default"]); + } + + [Fact] + public void GivenIniThenJson_WhenBothDefineTheSameKey_ThenTheJsonValueWins() + { + var json = WriteJson("Information"); + var ini = WriteIni("Debug"); + + var configuration = new ConfigurationBuilder() + .AddIniFile(ini) + .AddJsonFile(json) + .Build(); + + Assert.Equal("Information", configuration["Logging:LogLevel:Default"]); + } + + // The repo's appsettings.ini carried the header [Logging:Level], which writes + // a key the logging system never reads. The corrected header is the one that + // actually overrides the JSON value. + [Fact] + public void GivenTheCorrectedIniSectionHeader_WhenTheKeyIsRead_ThenItLandsOnTheLoggingKeyPath() + { + var ini = Path.Combine(_directory, $"header.{Guid.NewGuid():N}.ini"); + File.WriteAllText(ini, + $"[Logging:LogLevel]{Environment.NewLine}Default=Warning{Environment.NewLine}"); + + var configuration = new ConfigurationBuilder().AddIniFile(ini).Build(); + + Assert.Equal("Warning", configuration["Logging:LogLevel:Default"]); + Assert.Null(configuration["Logging:Level:Default"]); + } + + [Fact] + public void GivenEnvironmentVariables_WhenTheDoubleUnderscoreSeparatorIsUsed_ThenItBecomesAKeyPath() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["Logging:LogLevel:Default"] = "Information" }) + .AddEnvironmentVariables() + .Build(); + + Environment.SetEnvironmentVariable("Logging__LogLevel__Default", "Critical"); + try + { + var rebuilt = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["Logging:LogLevel:Default"] = "Information" }) + .AddEnvironmentVariables() + .Build(); + + Assert.Equal("Information", configuration["Logging:LogLevel:Default"]); + Assert.Equal("Critical", rebuilt["Logging:LogLevel:Default"]); + } + finally + { + Environment.SetEnvironmentVariable("Logging__LogLevel__Default", null); + } + } + + public void Dispose() + { + GC.SuppressFinalize(this); + try { Directory.Delete(_directory, recursive: true); } catch (IOException) { } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo.sln b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo.sln new file mode 100644 index 000000000..c74392b26 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{85175962-4C37-48FC-A151-5726F000290C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationProviders.Tests", "ConfigurationProviders.Tests\ConfigurationProviders.Tests.csproj", "{6C1033FF-D148-43F5-B436-6AAABBBF1456}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {85175962-4C37-48FC-A151-5726F000290C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Debug|x64.ActiveCfg = Debug|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Debug|x64.Build.0 = Debug|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Debug|x86.ActiveCfg = Debug|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Debug|x86.Build.0 = Debug|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Release|Any CPU.Build.0 = Release|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Release|x64.ActiveCfg = Release|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Release|x64.Build.0 = Release|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Release|x86.ActiveCfg = Release|Any CPU + {85175962-4C37-48FC-A151-5726F000290C}.Release|x86.Build.0 = Release|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Debug|x64.Build.0 = Debug|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Debug|x86.Build.0 = Debug|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Release|Any CPU.Build.0 = Release|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Release|x64.ActiveCfg = Release|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Release|x64.Build.0 = Release|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Release|x86.ActiveCfg = Release|Any CPU + {6C1033FF-D148-43F5-B436-6AAABBBF1456}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs new file mode 100644 index 000000000..ca4785208 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.ConfigurationValidation; + +public class TitleConfigurationValidation : IValidateOptions +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + + public ValidateOptionsResult Validate(string? name, TitleConfiguration options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (string.IsNullOrEmpty(options.WelcomeMessage) || options.WelcomeMessage.Length > 60) + builder.AddError("Welcome message must be defined and it must be less than 60 characters long.", + nameof(options.WelcomeMessage)); + + if (!_colors.Contains(options.Color)) + builder.AddError($"Provided title color '{options.Color}' is not among allowed colors.", + nameof(options.Color)); + + return builder.Build(); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Controllers/HomeController.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Controllers/HomeController.cs new file mode 100644 index 000000000..dd426ae2a --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Controllers/HomeController.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +namespace ProjectConfigurationDemo.Controllers; + +public class HomeController( + ILogger logger, + IOptionsSnapshot homePageTitleConfiguration, + ITitleColorService titleColorService) : Controller +{ + private readonly ILogger _logger = logger; + private readonly TitleConfiguration _homePageTitleConfiguration = homePageTitleConfiguration.Value; + private readonly ITitleColorService _titleColorService = titleColorService; + + public IActionResult Index() + { + var homeModel = new HomeModel + { + Configuration = _homePageTitleConfiguration + }; + + homeModel.Configuration.Color = _titleColorService.GetTitleColor(); + + return View(homeModel); + } + + public IActionResult Privacy() => View(); + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() => + View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/ErrorViewModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/ErrorViewModel.cs new file mode 100644 index 000000000..d3daed82d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/ErrorViewModel.cs @@ -0,0 +1,8 @@ +namespace ProjectConfigurationDemo.Models; + +public class ErrorViewModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/HomeModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/HomeModel.cs new file mode 100644 index 000000000..7838fe808 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/HomeModel.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class HomeModel +{ + public TitleConfiguration Configuration { get; set; } = new(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs new file mode 100644 index 000000000..9cc59d60f --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class LoggingLevelConfiguration +{ + public string? Default { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/TitleConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/TitleConfiguration.cs new file mode 100644 index 000000000..cf0cb4971 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Models/TitleConfiguration.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace ProjectConfigurationDemo.Models; + +public class TitleConfiguration +{ + [Required] + [MaxLength(60)] + public string WelcomeMessage { get; set; } = string.Empty; + + public bool ShowWelcomeMessage { get; set; } + + public string Color { get; set; } = string.Empty; + + public bool UseRandomTitleColor { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Program.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Program.cs new file mode 100644 index 000000000..946c4987b --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Program.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.ConfigurationValidation; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration + .AddIniFile("appsettings.ini", optional: true, reloadOnChange: true) + .AddIniFile($"appsettings.{builder.Environment.EnvironmentName}.ini", optional: true, reloadOnChange: true); + +builder.Services + .AddOptionsWithValidateOnStart() + .Bind(builder.Configuration.GetSection("Pages:HomePage")) + .ValidateDataAnnotations(); + +builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton, TitleConfigurationValidation>()); + +builder.Services.TryAddSingleton(); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +app.MapStaticAssets(); +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}") + .WithStaticAssets(); + +app.Run(); diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj new file mode 100644 index 000000000..fca27dbdd --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + d5a08119-f24b-42c7-a729-38895bb7b067 + + + + + + + + + + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Properties/launchSettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Properties/launchSettings.json new file mode 100644 index 000000000..92ac1b057 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Services/ITitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Services/ITitleColorService.cs new file mode 100644 index 000000000..2ee1036bf --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Services/ITitleColorService.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Services; + +public interface ITitleColorService +{ + string GetTitleColor(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Services/TitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Services/TitleColorService.cs new file mode 100644 index 000000000..1e9530488 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Services/TitleColorService.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.Services; + +public class TitleColorService(IOptionsMonitor titleConfiguration) : ITitleColorService +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + private readonly IOptionsMonitor _titleConfiguration = titleConfiguration; + + public string GetTitleColor() + { + var configuration = _titleConfiguration.CurrentValue; + + return configuration.UseRandomTitleColor + ? _colors[Random.Shared.Next(_colors.Length)] + : configuration.Color; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Home/Index.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Home/Index.cshtml new file mode 100644 index 000000000..c286342c5 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@model HomeModel +@{ + ViewData["Title"] = "Home Page"; +} +
+ @if (Model.Configuration.ShowWelcomeMessage) + { +

@Model.Configuration.WelcomeMessage

+ } +
diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Home/Privacy.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Home/Privacy.cshtml new file mode 100644 index 000000000..d01477f38 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Shared/Error.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Shared/Error.cshtml new file mode 100644 index 000000000..8f923fdf4 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Shared/Error.cshtml @@ -0,0 +1,25 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the + ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app. +

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000..208bd9a35 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml @@ -0,0 +1,30 @@ + + + + + + @ViewData["Title"] - ProjectConfigurationDemo + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+ © @DateTime.Now.Year - ProjectConfigurationDemo - + Privacy +
+ + @await RenderSectionAsync("Scripts", required: false) + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/_ViewImports.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/_ViewImports.cshtml new file mode 100644 index 000000000..92266eb48 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using ProjectConfigurationDemo +@using ProjectConfigurationDemo.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/_ViewStart.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/_ViewStart.cshtml new file mode 100644 index 000000000..820a2f6e0 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.Development.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.Production.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.Production.json new file mode 100644 index 000000000..50c269845 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.Production.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "sqlConnection": "Server=ProductionServerName;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.ini b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.ini new file mode 100644 index 000000000..5db26e0df --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.ini @@ -0,0 +1,7 @@ +[Logging:LogLevel] +Default=Information +Microsoft=Warning +Microsoft.Hosting.Lifetime=Information + +[ConnectionStrings] +sqlConnection=Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.json new file mode 100644 index 000000000..1310f75ca --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/appsettings.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "sqlConnection": "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + }, + "Pages": { + "HomePage": { + "WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page", + "ShowWelcomeMessage": true, + "Color": "black", + "UseRandomTitleColor": true + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/css/site.css b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/css/site.css new file mode 100644 index 000000000..d78436d57 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/css/site.css @@ -0,0 +1,49 @@ +html { + font-size: 14px; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; +} + +body { + margin: 0; + color: #212529; + background-color: #fff; +} + +header nav { + display: flex; + gap: 1rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e5e5e5; +} + +header nav a { + color: #212529; + text-decoration: none; +} + +.container { + max-width: 960px; + margin: 0 auto; + padding: 1.5rem; +} + +.text-center { + text-align: center; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; +} + +.text-danger { + color: #dc3545; +} + +footer { + max-width: 960px; + margin: 0 auto; + padding: 1rem 1.5rem; + border-top: 1px solid #e5e5e5; + color: #6c757d; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/favicon.ico b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/favicon.ico new file mode 100644 index 000000000..a3a799985 Binary files /dev/null and b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/favicon.ico differ diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/js/site.js b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/js/site.js new file mode 100644 index 000000000..104169066 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/ConfigurationProviders/ProjectConfigurationDemo/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your JavaScript code. diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/CustomConfigurationProvider.Tests.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/CustomConfigurationProvider.Tests.csproj new file mode 100644 index 000000000..b5a51c806 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/CustomConfigurationProvider.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/EFConfigurationProviderTests.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/EFConfigurationProviderTests.cs new file mode 100644 index 000000000..53d83b77c --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/EFConfigurationProviderTests.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.Configuration; +using ProjectConfigurationDemo.Models; +using Testcontainers.MsSql; + +namespace CustomConfigurationProvider.Tests; + +public class EFConfigurationProviderTests : IAsyncLifetime +{ + private const string SqlServerImage = "mcr.microsoft.com/mssql/server:2022-latest"; + + private readonly MsSqlContainer _sqlServer = new MsSqlBuilder(SqlServerImage).Build(); + + public async Task InitializeAsync() + { + if (DockerAvailability.IsAvailable || DockerAvailability.IsRequired) + await _sqlServer.StartAsync(); + } + + public async Task DisposeAsync() + { + if (DockerAvailability.IsAvailable || DockerAvailability.IsRequired) + await _sqlServer.DisposeAsync(); + } + + private ConfigurationManager BuildConfiguration() + { + var manager = new ConfigurationManager(); + + ((IConfigurationBuilder)manager).AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:sqlConnection"] = _sqlServer.GetConnectionString() + }); + + manager.AddEntityConfiguration(); + + return manager; + } + + [RequiresDockerFact] + public void GivenAnEmptyDatabase_WhenTheProviderLoads_ThenItSeedsAndServesTheDefaultValues() + { + var configuration = BuildConfiguration(); + + Assert.Equal("Welcome to the ProjectConfigurationDemo Home Page", + configuration["Pages:HomePage:WelcomeMessage"]); + Assert.Equal("black", configuration["Pages:HomePage:Color"]); + Assert.Equal("true", configuration["Pages:HomePage:UseRandomTitleColor"]); + } + + // The second run is the one that used to be subtly broken: the seeding branch + // built a case-insensitive dictionary and the ToDictionary branch did not, so + // the demo passed on the first run and was case-sensitive on every run after. + [RequiresDockerFact] + public void GivenAnAlreadySeededDatabase_WhenTheProviderLoadsAgain_ThenKeyLookupStaysCaseInsensitive() + { + _ = BuildConfiguration(); + + var secondRun = BuildConfiguration(); + + Assert.Equal("black", secondRun["Pages:HomePage:Color"]); + Assert.Equal("black", secondRun["pages:homepage:color"]); + Assert.Equal("black", secondRun.GetSection("pages:homepage")["color"]); + } + + [RequiresDockerFact] + public void GivenTheProviderIsRegistered_WhenTheSectionIsBound_ThenTheOptionsClassIsPopulated() + { + var configuration = BuildConfiguration(); + + var titleConfiguration = configuration.GetSection("Pages:HomePage").Get(); + + Assert.NotNull(titleConfiguration); + Assert.Equal("Welcome to the ProjectConfigurationDemo Home Page", titleConfiguration.WelcomeMessage); + Assert.True(titleConfiguration.UseRandomTitleColor); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/RequiresDockerFactAttribute.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/RequiresDockerFactAttribute.cs new file mode 100644 index 000000000..e3f94bd40 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/CustomConfigurationProvider.Tests/RequiresDockerFactAttribute.cs @@ -0,0 +1,62 @@ +using System.IO.Pipes; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace CustomConfigurationProvider.Tests; + +/// +/// A [Fact] that is skipped when no Docker daemon is reachable, so the suite stays +/// green on a developer machine without Docker and really runs where a daemon is +/// available, starting a SQL Server container. Set +/// CONFIGURATION_SERIES_REQUIRE_DOCKER=1 to turn a missing daemon into a failure +/// instead of a skip. +/// +public sealed class RequiresDockerFactAttribute : FactAttribute +{ + public RequiresDockerFactAttribute() + { + if (DockerAvailability.IsAvailable || DockerAvailability.IsRequired) + return; + + Skip = "Docker is not reachable on this machine, so the SQL Server container cannot start."; + } +} + +public static class DockerAvailability +{ + private static readonly Lazy Probe = new(Detect); + + public static bool IsAvailable => Probe.Value; + + public static bool IsRequired => + Environment.GetEnvironmentVariable("CONFIGURATION_SERIES_REQUIRE_DOCKER") == "1"; + + // Connect to the daemon rather than looking for a socket or a pipe on disk: + // Docker Desktop leaves its named pipe behind when it is not running, so the + // cheaper check reports a daemon that is not there. + private static bool Detect() + { + if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DOCKER_HOST"))) + return true; + + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using var pipe = new NamedPipeClientStream(".", "docker_engine", PipeDirection.InOut); + pipe.Connect(timeout: 1000); + + return pipe.IsConnected; + } + + using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + socket.Connect(new UnixDomainSocketEndPoint("/var/run/docker.sock")); + + return socket.Connected; + } + catch (Exception) + { + return false; + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo.sln b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo.sln new file mode 100644 index 000000000..588065f07 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{5EA1A2AB-57F4-45DB-992E-38153540DD19}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomConfigurationProvider.Tests", "CustomConfigurationProvider.Tests\CustomConfigurationProvider.Tests.csproj", "{27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Debug|x64.ActiveCfg = Debug|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Debug|x64.Build.0 = Debug|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Debug|x86.ActiveCfg = Debug|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Debug|x86.Build.0 = Debug|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Release|Any CPU.Build.0 = Release|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Release|x64.ActiveCfg = Release|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Release|x64.Build.0 = Release|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Release|x86.ActiveCfg = Release|Any CPU + {5EA1A2AB-57F4-45DB-992E-38153540DD19}.Release|x86.Build.0 = Release|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Debug|x64.ActiveCfg = Debug|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Debug|x64.Build.0 = Debug|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Debug|x86.ActiveCfg = Debug|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Debug|x86.Build.0 = Debug|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Release|Any CPU.Build.0 = Release|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Release|x64.ActiveCfg = Release|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Release|x64.Build.0 = Release|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Release|x86.ActiveCfg = Release|Any CPU + {27DEA374-3B60-4C1F-BD6C-F58ED7A9939F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs new file mode 100644 index 000000000..ca4785208 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.ConfigurationValidation; + +public class TitleConfigurationValidation : IValidateOptions +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + + public ValidateOptionsResult Validate(string? name, TitleConfiguration options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (string.IsNullOrEmpty(options.WelcomeMessage) || options.WelcomeMessage.Length > 60) + builder.AddError("Welcome message must be defined and it must be less than 60 characters long.", + nameof(options.WelcomeMessage)); + + if (!_colors.Contains(options.Color)) + builder.AddError($"Provided title color '{options.Color}' is not among allowed colors.", + nameof(options.Color)); + + return builder.Build(); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Controllers/HomeController.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Controllers/HomeController.cs new file mode 100644 index 000000000..dd426ae2a --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Controllers/HomeController.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +namespace ProjectConfigurationDemo.Controllers; + +public class HomeController( + ILogger logger, + IOptionsSnapshot homePageTitleConfiguration, + ITitleColorService titleColorService) : Controller +{ + private readonly ILogger _logger = logger; + private readonly TitleConfiguration _homePageTitleConfiguration = homePageTitleConfiguration.Value; + private readonly ITitleColorService _titleColorService = titleColorService; + + public IActionResult Index() + { + var homeModel = new HomeModel + { + Configuration = _homePageTitleConfiguration + }; + + homeModel.Configuration.Color = _titleColorService.GetTitleColor(); + + return View(homeModel); + } + + public IActionResult Privacy() => View(); + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() => + View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationDbContext.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationDbContext.cs new file mode 100644 index 000000000..c8a57f0eb --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationDbContext.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; + +namespace ProjectConfigurationDemo.Models; + +public class ConfigurationDbContext(string? connectionString) : DbContext +{ + private readonly string? _connectionString = connectionString; + + public DbSet ConfigurationEntities { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer(_connectionString); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationEntity.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationEntity.cs new file mode 100644 index 000000000..c1c67b7ff --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationEntity.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace ProjectConfigurationDemo.Models; + +public class ConfigurationEntity +{ + [Key] + public string Key { get; set; } = string.Empty; + + public string? Value { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationProvider.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationProvider.cs new file mode 100644 index 000000000..975612f02 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationProvider.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace ProjectConfigurationDemo.Models.ConfigurationProviders; + +public class EFConfigurationProvider(string? connectionString) : ConfigurationProvider +{ + private readonly string? _connectionString = connectionString; + + public override void Load() + { + using var dbContext = new ConfigurationDbContext(_connectionString); + + dbContext.Database.EnsureCreated(); + + Data = dbContext.ConfigurationEntities.Any() + ? dbContext.ConfigurationEntities.ToDictionary(c => c.Key, c => c.Value, StringComparer.OrdinalIgnoreCase) + : CreateAndSaveDefaultValues(dbContext); + } + + private static Dictionary CreateAndSaveDefaultValues(ConfigurationDbContext dbContext) + { + var configValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Pages:HomePage:WelcomeMessage", "Welcome to the ProjectConfigurationDemo Home Page" }, + { "Pages:HomePage:ShowWelcomeMessage", "true" }, + { "Pages:HomePage:Color", "black" }, + { "Pages:HomePage:UseRandomTitleColor", "true" } + }; + + dbContext.ConfigurationEntities.AddRange( + [.. configValues.Select(kvp => new ConfigurationEntity { Key = kvp.Key, Value = kvp.Value })]); + + dbContext.SaveChanges(); + + return configValues; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationSource.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationSource.cs new file mode 100644 index 000000000..9c842dd4b --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationSource.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Configuration; + +namespace ProjectConfigurationDemo.Models.ConfigurationProviders; + +public class EFConfigurationSource(string? connectionString) : IConfigurationSource +{ + private readonly string? _connectionString = connectionString; + + public IConfigurationProvider Build(IConfigurationBuilder builder) + => new EFConfigurationProvider(_connectionString); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/EntityConfigurationExtensions.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/EntityConfigurationExtensions.cs new file mode 100644 index 000000000..5a6f5f669 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/EntityConfigurationExtensions.cs @@ -0,0 +1,16 @@ +using ProjectConfigurationDemo.Models.ConfigurationProviders; + +namespace ProjectConfigurationDemo.Models; + +public static class EntityConfigurationExtensions +{ + public static ConfigurationManager AddEntityConfiguration(this ConfigurationManager manager) + { + var connectionString = manager.GetConnectionString("sqlConnection"); + + IConfigurationBuilder configBuilder = manager; + configBuilder.Add(new EFConfigurationSource(connectionString)); + + return manager; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ErrorViewModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ErrorViewModel.cs new file mode 100644 index 000000000..d3daed82d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/ErrorViewModel.cs @@ -0,0 +1,8 @@ +namespace ProjectConfigurationDemo.Models; + +public class ErrorViewModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/HomeModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/HomeModel.cs new file mode 100644 index 000000000..7838fe808 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/HomeModel.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class HomeModel +{ + public TitleConfiguration Configuration { get; set; } = new(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs new file mode 100644 index 000000000..9cc59d60f --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class LoggingLevelConfiguration +{ + public string? Default { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/TitleConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/TitleConfiguration.cs new file mode 100644 index 000000000..cf0cb4971 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Models/TitleConfiguration.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace ProjectConfigurationDemo.Models; + +public class TitleConfiguration +{ + [Required] + [MaxLength(60)] + public string WelcomeMessage { get; set; } = string.Empty; + + public bool ShowWelcomeMessage { get; set; } + + public string Color { get; set; } = string.Empty; + + public bool UseRandomTitleColor { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Program.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Program.cs new file mode 100644 index 000000000..6ef319fc2 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Program.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.ConfigurationValidation; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.AddEntityConfiguration(); + +builder.Services + .AddOptionsWithValidateOnStart() + .Bind(builder.Configuration.GetSection("Pages:HomePage")) + .ValidateDataAnnotations(); + +builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton, TitleConfigurationValidation>()); + +builder.Services.TryAddSingleton(); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +app.MapStaticAssets(); +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}") + .WithStaticAssets(); + +app.Run(); diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj new file mode 100644 index 000000000..6f7bd435c --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + f5fc4b7d-16ac-48b3-a865-d86096b35b16 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Properties/launchSettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Properties/launchSettings.json new file mode 100644 index 000000000..92ac1b057 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Services/ITitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Services/ITitleColorService.cs new file mode 100644 index 000000000..2ee1036bf --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Services/ITitleColorService.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Services; + +public interface ITitleColorService +{ + string GetTitleColor(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Services/TitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Services/TitleColorService.cs new file mode 100644 index 000000000..1e9530488 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Services/TitleColorService.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.Services; + +public class TitleColorService(IOptionsMonitor titleConfiguration) : ITitleColorService +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + private readonly IOptionsMonitor _titleConfiguration = titleConfiguration; + + public string GetTitleColor() + { + var configuration = _titleConfiguration.CurrentValue; + + return configuration.UseRandomTitleColor + ? _colors[Random.Shared.Next(_colors.Length)] + : configuration.Color; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Home/Index.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Home/Index.cshtml new file mode 100644 index 000000000..c286342c5 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@model HomeModel +@{ + ViewData["Title"] = "Home Page"; +} +
+ @if (Model.Configuration.ShowWelcomeMessage) + { +

@Model.Configuration.WelcomeMessage

+ } +
diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Home/Privacy.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Home/Privacy.cshtml new file mode 100644 index 000000000..d01477f38 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Shared/Error.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Shared/Error.cshtml new file mode 100644 index 000000000..8f923fdf4 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Shared/Error.cshtml @@ -0,0 +1,25 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the + ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app. +

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000..208bd9a35 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml @@ -0,0 +1,30 @@ + + + + + + @ViewData["Title"] - ProjectConfigurationDemo + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+ © @DateTime.Now.Year - ProjectConfigurationDemo - + Privacy +
+ + @await RenderSectionAsync("Scripts", required: false) + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/_ViewImports.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/_ViewImports.cshtml new file mode 100644 index 000000000..92266eb48 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using ProjectConfigurationDemo +@using ProjectConfigurationDemo.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/_ViewStart.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/_ViewStart.cshtml new file mode 100644 index 000000000..820a2f6e0 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.Development.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.Production.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.Production.json new file mode 100644 index 000000000..50c269845 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.Production.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "sqlConnection": "Server=ProductionServerName;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.json new file mode 100644 index 000000000..9db2c25d6 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "sqlConnection": "Server=(localdb)\\MSSQLLocalDB;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/css/site.css b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/css/site.css new file mode 100644 index 000000000..d78436d57 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/css/site.css @@ -0,0 +1,49 @@ +html { + font-size: 14px; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; +} + +body { + margin: 0; + color: #212529; + background-color: #fff; +} + +header nav { + display: flex; + gap: 1rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e5e5e5; +} + +header nav a { + color: #212529; + text-decoration: none; +} + +.container { + max-width: 960px; + margin: 0 auto; + padding: 1.5rem; +} + +.text-center { + text-align: center; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; +} + +.text-danger { + color: #dc3545; +} + +footer { + max-width: 960px; + margin: 0 auto; + padding: 1rem 1.5rem; + border-top: 1px solid #e5e5e5; + color: #6c757d; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/favicon.ico b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/favicon.ico new file mode 100644 index 000000000..a3a799985 Binary files /dev/null and b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/favicon.ico differ diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/js/site.js b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/js/site.js new file mode 100644 index 000000000..104169066 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/CustomConfigurationProvider/ProjectConfigurationDemo/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your JavaScript code. diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/OptionsPattern.Tests/OptionsPattern.Tests.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/OptionsPattern.Tests/OptionsPattern.Tests.csproj new file mode 100644 index 000000000..8f4da1b47 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/OptionsPattern.Tests/OptionsPattern.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/OptionsPattern.Tests/OptionsPatternTests.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/OptionsPattern.Tests/OptionsPatternTests.cs new file mode 100644 index 000000000..9611cf521 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/OptionsPattern.Tests/OptionsPatternTests.cs @@ -0,0 +1,92 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +namespace OptionsPattern.Tests; + +public class OptionsPatternTests +{ + private static IConfiguration BuildConfiguration() => + new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Pages:HomePage:WelcomeMessage"] = "Welcome to the Home Page", + ["Pages:HomePage:ShowWelcomeMessage"] = "true", + ["Pages:HomePage:Color"] = "red", + ["Pages:HomePage:UseRandomTitleColor"] = "true", + ["Pages:ProductPage:WelcomeMessage"] = "Welcome to the Product Page", + ["Pages:ProductPage:ShowWelcomeMessage"] = "true", + ["Pages:ProductPage:Color"] = "black", + ["Pages:ProductPage:UseRandomTitleColor"] = "false" + }) + .Build(); + + private static ServiceProvider BuildNamedOptionsProvider() + { + var configuration = BuildConfiguration(); + var services = new ServiceCollection(); + + services.Configure("HomePage", configuration.GetSection("Pages:HomePage")); + services.Configure("ProductPage", configuration.GetSection("Pages:ProductPage")); + services.AddSingleton(); + + return services.BuildServiceProvider(); + } + + [Fact] + public void GivenAnUnnamedRegistration_WhenIOptionsValueIsRead_ThenTheSectionIsBoundToTheClass() + { + var services = new ServiceCollection(); + services.Configure(BuildConfiguration().GetSection("Pages:HomePage")); + + var options = services.BuildServiceProvider().GetRequiredService>(); + + Assert.Equal("Welcome to the Home Page", options.Value.WelcomeMessage); + Assert.True(options.Value.ShowWelcomeMessage); + Assert.Equal("red", options.Value.Color); + } + + [Fact] + public void GivenTwoNamedRegistrations_WhenEachIsRead_ThenOneClassCarriesTwoDifferentInstances() + { + using var provider = BuildNamedOptionsProvider(); + var monitor = provider.GetRequiredService>(); + + var homePage = monitor.Get("HomePage"); + var productPage = monitor.Get("ProductPage"); + + Assert.Equal("Welcome to the Home Page", homePage.WelcomeMessage); + Assert.Equal("Welcome to the Product Page", productPage.WelcomeMessage); + Assert.True(homePage.UseRandomTitleColor); + Assert.False(productPage.UseRandomTitleColor); + } + + [Fact] + public void GivenAPageThatDoesNotUseARandomColor_WhenTheColorIsRequested_ThenTheConfiguredColorComesBack() + { + using var provider = BuildNamedOptionsProvider(); + var titleColorService = provider.GetRequiredService(); + + var color = titleColorService.GetTitleColor("ProductPage"); + + Assert.Equal("black", color); + } + + // This is the test that would have caught 'random.Next(7)' over eight colours: + // "pink" sits at index 7 and was unreachable. + [Fact] + public void GivenAPageThatUsesARandomColor_WhenTheColorIsRequestedManyTimes_ThenEveryColorCanBeSelected() + { + using var provider = BuildNamedOptionsProvider(); + var titleColorService = provider.GetRequiredService(); + string[] expected = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + + var seen = new HashSet(); + for (var i = 0; i < 20_000; i++) + seen.Add(titleColorService.GetTitleColor("HomePage")); + + Assert.Equal(expected.Order(), seen.Order()); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo.sln b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo.sln new file mode 100644 index 000000000..179e1789e --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{DC004E8F-0365-4446-A71D-7BD64A697C40}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptionsPattern.Tests", "OptionsPattern.Tests\OptionsPattern.Tests.csproj", "{A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Debug|x64.ActiveCfg = Debug|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Debug|x64.Build.0 = Debug|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Debug|x86.ActiveCfg = Debug|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Debug|x86.Build.0 = Debug|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Release|Any CPU.Build.0 = Release|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Release|x64.ActiveCfg = Release|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Release|x64.Build.0 = Release|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Release|x86.ActiveCfg = Release|Any CPU + {DC004E8F-0365-4446-A71D-7BD64A697C40}.Release|x86.Build.0 = Release|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Debug|x64.Build.0 = Debug|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Debug|x86.Build.0 = Debug|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Release|x64.ActiveCfg = Release|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Release|x64.Build.0 = Release|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Release|x86.ActiveCfg = Release|Any CPU + {A1B78DA2-B190-4CD6-84FE-9CE54FACA25C}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Controllers/HomeController.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Controllers/HomeController.cs new file mode 100644 index 000000000..061458f08 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Controllers/HomeController.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +namespace ProjectConfigurationDemo.Controllers; + +public class HomeController( + ILogger logger, + IOptionsSnapshot homePageTitleConfiguration, + ITitleColorService titleColorService) : Controller +{ + private readonly ILogger _logger = logger; + private readonly TitleConfiguration _homePageTitleConfiguration = homePageTitleConfiguration.Get("HomePage"); + private readonly ITitleColorService _titleColorService = titleColorService; + + public IActionResult Index() + { + var homeModel = new HomeModel + { + Configuration = _homePageTitleConfiguration + }; + + homeModel.Configuration.Color = _titleColorService.GetTitleColor("HomePage"); + + return View(homeModel); + } + + public IActionResult Privacy() => View(); + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() => + View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/ErrorViewModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/ErrorViewModel.cs new file mode 100644 index 000000000..d3daed82d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/ErrorViewModel.cs @@ -0,0 +1,8 @@ +namespace ProjectConfigurationDemo.Models; + +public class ErrorViewModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/HomeModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/HomeModel.cs new file mode 100644 index 000000000..7838fe808 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/HomeModel.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class HomeModel +{ + public TitleConfiguration Configuration { get; set; } = new(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs new file mode 100644 index 000000000..9cc59d60f --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class LoggingLevelConfiguration +{ + public string? Default { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/TitleConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/TitleConfiguration.cs new file mode 100644 index 000000000..74437a034 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Models/TitleConfiguration.cs @@ -0,0 +1,9 @@ +namespace ProjectConfigurationDemo.Models; + +public class TitleConfiguration +{ + public string WelcomeMessage { get; set; } = string.Empty; + public bool ShowWelcomeMessage { get; set; } + public string Color { get; set; } = string.Empty; + public bool UseRandomTitleColor { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Program.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Program.cs new file mode 100644 index 000000000..b51ea3c84 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Program.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.Configure("HomePage", + builder.Configuration.GetSection("Pages:HomePage")); +builder.Services.Configure("ProductPage", + builder.Configuration.GetSection("Pages:ProductPage")); + +builder.Services.TryAddSingleton(); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +app.MapStaticAssets(); +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}") + .WithStaticAssets(); + +app.Run(); diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj new file mode 100644 index 000000000..a3a34b647 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Properties/launchSettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Properties/launchSettings.json new file mode 100644 index 000000000..92ac1b057 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Services/ITitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Services/ITitleColorService.cs new file mode 100644 index 000000000..046d6382d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Services/ITitleColorService.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Services; + +public interface ITitleColorService +{ + string GetTitleColor(string pageTitleConfiguration); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Services/TitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Services/TitleColorService.cs new file mode 100644 index 000000000..e93e0c807 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Services/TitleColorService.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.Services; + +public class TitleColorService(IOptionsMonitor titleConfiguration) : ITitleColorService +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + private readonly IOptionsMonitor _titleConfiguration = titleConfiguration; + + public string GetTitleColor(string pageTitleConfiguration) + { + var configuration = _titleConfiguration.Get(pageTitleConfiguration); + + return configuration.UseRandomTitleColor + ? _colors[Random.Shared.Next(_colors.Length)] + : configuration.Color; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Home/Index.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Home/Index.cshtml new file mode 100644 index 000000000..c286342c5 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@model HomeModel +@{ + ViewData["Title"] = "Home Page"; +} +
+ @if (Model.Configuration.ShowWelcomeMessage) + { +

@Model.Configuration.WelcomeMessage

+ } +
diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Home/Privacy.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Home/Privacy.cshtml new file mode 100644 index 000000000..d01477f38 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Shared/Error.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Shared/Error.cshtml new file mode 100644 index 000000000..8f923fdf4 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Shared/Error.cshtml @@ -0,0 +1,25 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the + ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app. +

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000..208bd9a35 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml @@ -0,0 +1,30 @@ + + + + + + @ViewData["Title"] - ProjectConfigurationDemo + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+ © @DateTime.Now.Year - ProjectConfigurationDemo - + Privacy +
+ + @await RenderSectionAsync("Scripts", required: false) + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/_ViewImports.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/_ViewImports.cshtml new file mode 100644 index 000000000..92266eb48 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using ProjectConfigurationDemo +@using ProjectConfigurationDemo.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/_ViewStart.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/_ViewStart.cshtml new file mode 100644 index 000000000..820a2f6e0 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.Development.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.Production.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.Production.json new file mode 100644 index 000000000..50c269845 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.Production.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "sqlConnection": "Server=ProductionServerName;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.json new file mode 100644 index 000000000..b1b819eea --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/appsettings.json @@ -0,0 +1,27 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "sqlConnection": "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + }, + "Pages": { + "HomePage": { + "WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page", + "ShowWelcomeMessage": true, + "Color": "red", + "UseRandomTitleColor": true + }, + "ProductPage": { + "WelcomeMessage": "Welcome to the ProjectConfigurationDemo Product Page", + "ShowWelcomeMessage": true, + "Color": "black", + "UseRandomTitleColor": false + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/css/site.css b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/css/site.css new file mode 100644 index 000000000..d78436d57 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/css/site.css @@ -0,0 +1,49 @@ +html { + font-size: 14px; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; +} + +body { + margin: 0; + color: #212529; + background-color: #fff; +} + +header nav { + display: flex; + gap: 1rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e5e5e5; +} + +header nav a { + color: #212529; + text-decoration: none; +} + +.container { + max-width: 960px; + margin: 0 auto; + padding: 1.5rem; +} + +.text-center { + text-align: center; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; +} + +.text-danger { + color: #dc3545; +} + +footer { + max-width: 960px; + margin: 0 auto; + padding: 1rem 1.5rem; + border-top: 1px solid #e5e5e5; + color: #6c757d; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/favicon.ico b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/favicon.ico new file mode 100644 index 000000000..a3a799985 Binary files /dev/null and b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/favicon.ico differ diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/js/site.js b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/js/site.js new file mode 100644 index 000000000..104169066 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsPattern/ProjectConfigurationDemo/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your JavaScript code. diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/OptionsValidation.Tests/OptionsValidation.Tests.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/OptionsValidation.Tests/OptionsValidation.Tests.csproj new file mode 100644 index 000000000..8f4da1b47 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/OptionsValidation.Tests/OptionsValidation.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/OptionsValidation.Tests/OptionsValidationTests.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/OptionsValidation.Tests/OptionsValidationTests.cs new file mode 100644 index 000000000..390a2042c --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/OptionsValidation.Tests/OptionsValidationTests.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.ConfigurationValidation; +using ProjectConfigurationDemo.Models; + +namespace OptionsValidation.Tests; + +public class OptionsValidationTests +{ + private static ServiceProvider BuildProvider(Dictionary values) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); + var services = new ServiceCollection(); + + services + .AddOptionsWithValidateOnStart() + .Bind(configuration.GetSection("Pages:HomePage")) + .ValidateDataAnnotations(); + + services.TryAddEnumerable( + ServiceDescriptor.Singleton, TitleConfigurationValidation>()); + + return services.BuildServiceProvider(); + } + + private static Dictionary ValidConfiguration() => new() + { + ["Pages:HomePage:WelcomeMessage"] = "Welcome to the Home Page", + ["Pages:HomePage:ShowWelcomeMessage"] = "true", + ["Pages:HomePage:Color"] = "black", + ["Pages:HomePage:UseRandomTitleColor"] = "true" + }; + + [Fact] + public void GivenAValidConfiguration_WhenTheOptionsAreResolved_ThenTheyBindWithoutThrowing() + { + using var provider = BuildProvider(ValidConfiguration()); + + var options = provider.GetRequiredService>(); + + Assert.Equal("Welcome to the Home Page", options.Value.WelcomeMessage); + Assert.Equal("black", options.Value.Color); + } + + [Fact] + public void GivenAMissingWelcomeMessage_WhenTheOptionsAreResolved_ThenAnOptionsValidationExceptionIsThrown() + { + var values = ValidConfiguration(); + values["Pages:HomePage:WelcomeMessage"] = string.Empty; + + using var provider = BuildProvider(values); + + Assert.Throws( + () => provider.GetRequiredService>().Value); + } + + [Fact] + public void GivenAWelcomeMessageOverSixtyCharacters_WhenTheOptionsAreResolved_ThenAnOptionsValidationExceptionIsThrown() + { + var values = ValidConfiguration(); + values["Pages:HomePage:WelcomeMessage"] = new string('a', 61); + + using var provider = BuildProvider(values); + + Assert.Throws( + () => provider.GetRequiredService>().Value); + } + + [Fact] + public void GivenAColorOutsideTheAllowedList_WhenTheOptionsAreResolved_ThenTheValidatorsMessageIsReported() + { + var values = ValidConfiguration(); + values["Pages:HomePage:Color"] = "magenta"; + + using var provider = BuildProvider(values); + + var exception = Assert.Throws( + () => provider.GetRequiredService>().Value); + + Assert.Contains("is not among allowed colors", exception.Message); + } + + // The failure-accumulation change: the hand-written validator reports every + // broken rule in one pass instead of returning on the first one. + [Fact] + public void GivenTwoBrokenRules_WhenTheValidatorRuns_ThenBothFailuresAreReported() + { + var validator = new TitleConfigurationValidation(); + var options = new TitleConfiguration { WelcomeMessage = string.Empty, Color = "magenta" }; + + var result = validator.Validate(name: null, options); + + Assert.True(result.Failed); + Assert.Equal(2, result.Failures!.Count()); + } + + [Fact] + public void GivenAValidOptionsInstance_WhenTheValidatorRuns_ThenItSucceeds() + { + var validator = new TitleConfigurationValidation(); + var options = new TitleConfiguration { WelcomeMessage = "Welcome", Color = "blue" }; + + var result = validator.Validate(name: null, options); + + Assert.True(result.Succeeded); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo.sln b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo.sln new file mode 100644 index 000000000..3dd99953d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{B6411524-0F8C-49BD-8F26-8C0CECD23485}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptionsValidation.Tests", "OptionsValidation.Tests\OptionsValidation.Tests.csproj", "{68A3CD60-56D8-4852-9C4F-265420B3D362}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Debug|x64.ActiveCfg = Debug|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Debug|x64.Build.0 = Debug|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Debug|x86.ActiveCfg = Debug|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Debug|x86.Build.0 = Debug|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Release|Any CPU.Build.0 = Release|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Release|x64.ActiveCfg = Release|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Release|x64.Build.0 = Release|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Release|x86.ActiveCfg = Release|Any CPU + {B6411524-0F8C-49BD-8F26-8C0CECD23485}.Release|x86.Build.0 = Release|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Debug|x64.ActiveCfg = Debug|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Debug|x64.Build.0 = Debug|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Debug|x86.ActiveCfg = Debug|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Debug|x86.Build.0 = Debug|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Release|Any CPU.Build.0 = Release|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Release|x64.ActiveCfg = Release|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Release|x64.Build.0 = Release|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Release|x86.ActiveCfg = Release|Any CPU + {68A3CD60-56D8-4852-9C4F-265420B3D362}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs new file mode 100644 index 000000000..ca4785208 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.ConfigurationValidation; + +public class TitleConfigurationValidation : IValidateOptions +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + + public ValidateOptionsResult Validate(string? name, TitleConfiguration options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (string.IsNullOrEmpty(options.WelcomeMessage) || options.WelcomeMessage.Length > 60) + builder.AddError("Welcome message must be defined and it must be less than 60 characters long.", + nameof(options.WelcomeMessage)); + + if (!_colors.Contains(options.Color)) + builder.AddError($"Provided title color '{options.Color}' is not among allowed colors.", + nameof(options.Color)); + + return builder.Build(); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Controllers/HomeController.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Controllers/HomeController.cs new file mode 100644 index 000000000..dd426ae2a --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Controllers/HomeController.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +namespace ProjectConfigurationDemo.Controllers; + +public class HomeController( + ILogger logger, + IOptionsSnapshot homePageTitleConfiguration, + ITitleColorService titleColorService) : Controller +{ + private readonly ILogger _logger = logger; + private readonly TitleConfiguration _homePageTitleConfiguration = homePageTitleConfiguration.Value; + private readonly ITitleColorService _titleColorService = titleColorService; + + public IActionResult Index() + { + var homeModel = new HomeModel + { + Configuration = _homePageTitleConfiguration + }; + + homeModel.Configuration.Color = _titleColorService.GetTitleColor(); + + return View(homeModel); + } + + public IActionResult Privacy() => View(); + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() => + View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/ErrorViewModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/ErrorViewModel.cs new file mode 100644 index 000000000..d3daed82d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/ErrorViewModel.cs @@ -0,0 +1,8 @@ +namespace ProjectConfigurationDemo.Models; + +public class ErrorViewModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/HomeModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/HomeModel.cs new file mode 100644 index 000000000..7838fe808 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/HomeModel.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class HomeModel +{ + public TitleConfiguration Configuration { get; set; } = new(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs new file mode 100644 index 000000000..9cc59d60f --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class LoggingLevelConfiguration +{ + public string? Default { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/TitleConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/TitleConfiguration.cs new file mode 100644 index 000000000..cf0cb4971 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Models/TitleConfiguration.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace ProjectConfigurationDemo.Models; + +public class TitleConfiguration +{ + [Required] + [MaxLength(60)] + public string WelcomeMessage { get; set; } = string.Empty; + + public bool ShowWelcomeMessage { get; set; } + + public string Color { get; set; } = string.Empty; + + public bool UseRandomTitleColor { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Program.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Program.cs new file mode 100644 index 000000000..51051bd78 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Program.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.ConfigurationValidation; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddOptionsWithValidateOnStart() + .Bind(builder.Configuration.GetSection("Pages:HomePage")) + .ValidateDataAnnotations(); + +builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton, TitleConfigurationValidation>()); + +builder.Services.TryAddSingleton(); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +app.MapStaticAssets(); +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}") + .WithStaticAssets(); + +app.Run(); diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj new file mode 100644 index 000000000..a3a34b647 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Properties/launchSettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Properties/launchSettings.json new file mode 100644 index 000000000..92ac1b057 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Services/ITitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Services/ITitleColorService.cs new file mode 100644 index 000000000..2ee1036bf --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Services/ITitleColorService.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Services; + +public interface ITitleColorService +{ + string GetTitleColor(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Services/TitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Services/TitleColorService.cs new file mode 100644 index 000000000..1e9530488 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Services/TitleColorService.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.Services; + +public class TitleColorService(IOptionsMonitor titleConfiguration) : ITitleColorService +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + private readonly IOptionsMonitor _titleConfiguration = titleConfiguration; + + public string GetTitleColor() + { + var configuration = _titleConfiguration.CurrentValue; + + return configuration.UseRandomTitleColor + ? _colors[Random.Shared.Next(_colors.Length)] + : configuration.Color; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Home/Index.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Home/Index.cshtml new file mode 100644 index 000000000..c286342c5 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@model HomeModel +@{ + ViewData["Title"] = "Home Page"; +} +
+ @if (Model.Configuration.ShowWelcomeMessage) + { +

@Model.Configuration.WelcomeMessage

+ } +
diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Home/Privacy.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Home/Privacy.cshtml new file mode 100644 index 000000000..d01477f38 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Shared/Error.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Shared/Error.cshtml new file mode 100644 index 000000000..8f923fdf4 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Shared/Error.cshtml @@ -0,0 +1,25 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the + ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app. +

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000..208bd9a35 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml @@ -0,0 +1,30 @@ + + + + + + @ViewData["Title"] - ProjectConfigurationDemo + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+ © @DateTime.Now.Year - ProjectConfigurationDemo - + Privacy +
+ + @await RenderSectionAsync("Scripts", required: false) + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/_ViewImports.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/_ViewImports.cshtml new file mode 100644 index 000000000..92266eb48 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using ProjectConfigurationDemo +@using ProjectConfigurationDemo.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/_ViewStart.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/_ViewStart.cshtml new file mode 100644 index 000000000..820a2f6e0 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.Development.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.Production.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.Production.json new file mode 100644 index 000000000..50c269845 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.Production.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "sqlConnection": "Server=ProductionServerName;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.json new file mode 100644 index 000000000..1310f75ca --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/appsettings.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "sqlConnection": "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" + }, + "Pages": { + "HomePage": { + "WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page", + "ShowWelcomeMessage": true, + "Color": "black", + "UseRandomTitleColor": true + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/css/site.css b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/css/site.css new file mode 100644 index 000000000..d78436d57 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/css/site.css @@ -0,0 +1,49 @@ +html { + font-size: 14px; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; +} + +body { + margin: 0; + color: #212529; + background-color: #fff; +} + +header nav { + display: flex; + gap: 1rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e5e5e5; +} + +header nav a { + color: #212529; + text-decoration: none; +} + +.container { + max-width: 960px; + margin: 0 auto; + padding: 1.5rem; +} + +.text-center { + text-align: center; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; +} + +.text-danger { + color: #dc3545; +} + +footer { + max-width: 960px; + margin: 0 auto; + padding: 1rem 1.5rem; + border-top: 1px solid #e5e5e5; + color: #6c757d; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/favicon.ico b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/favicon.ico new file mode 100644 index 000000000..a3a799985 Binary files /dev/null and b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/favicon.ico differ diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/js/site.js b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/js/site.js new file mode 100644 index 000000000..104169066 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/OptionsValidation/ProjectConfigurationDemo/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your JavaScript code. diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/README.md b/aspnetcore-webapi/AspNetCoreConfigurationSeries/README.md new file mode 100644 index 000000000..70f5388f5 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/README.md @@ -0,0 +1,85 @@ +# Configuration in ASP.NET Core — the series samples + +This folder holds the code for the [Configuration in ASP.NET Core](https://code-maze.com/net-core-series/#configuration) +series. Each article gets its own folder with its own solution, so you can start at +the beginning or clone only the part you are reading. + +| Folder | Article | +| - | - | +| `ConfigurationBasics` | [How to Use IConfiguration in ASP.NET Core](https://code-maze.com/aspnet-configuration-basic-concepts/) | +| `OptionsPattern` | [Options Pattern in ASP.NET Core With IOptions](https://code-maze.com/aspnet-configuration-options/) | +| `OptionsValidation` | [Options Validation in ASP.NET Core](https://code-maze.com/aspnet-configuration-options-validation/) | +| `ConfigurationProviders` | [ASP.NET Core Configuration Providers](https://code-maze.com/aspnet-configuration-providers/) | +| `CustomConfigurationProvider` | [Custom Configuration Provider in ASP.NET Core With EF Core](https://code-maze.com/aspnet-configuration-creating-custom-provider/) | +| `SecuringDataLocally` | [User Secrets vs Environment Variables in ASP.NET Core](https://code-maze.com/aspnet-configuration-securing-sensitive-data/) | + +Every folder builds and its tests run on .NET 10: + +``` +cd OptionsPattern +dotnet build ProjectConfigurationDemo.sln +dotnet test ProjectConfigurationDemo.sln +``` + +`AspNetCoreConfigurationSeries.sln` in this folder is a convenience solution that +contains all six, so the whole series can be built and tested in one go. + +## The folders are not a strict accumulation + +Each folder is the end state of its own article, which is not always the previous +folder plus something new. Two steps deliberately take something away, because the +articles tell the reader to: + +- `OptionsValidation` drops the **named options** that `OptionsPattern` introduces. + Part 3 needs one options instance, not two, so `ITitleColorService.GetTitleColor()` + loses its parameter and the `Pages:ProductPage` section goes. The two folders' + `Services/TitleColorService.cs` files are meant to differ. +- `CustomConfigurationProvider` replaces part 4's INI registration with the EF Core + provider rather than adding to it, so `appsettings.ini` is not carried forward. + +## Running the samples that need a database + +`CustomConfigurationProvider` and `SecuringDataLocally` read their configuration +from SQL Server through a custom configuration provider, which runs before anything +else in the application, so a reachable server is needed to start them. + +`CustomConfigurationProvider/ProjectConfigurationDemo/appsettings.json` points at +LocalDB (`(localdb)\MSSQLLocalDB`), which ships with the Visual Studio workloads and +the SQL Server Express installer on Windows. On another platform, start a container +and point the connection string at it: + +``` +docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest +``` + +`SecuringDataLocally` ships with **no connection string in `appsettings.json` at +all** — that is the whole subject of its article. Supply it from a user secret +before running that project: + +``` +cd SecuringDataLocally/ProjectConfigurationDemo +dotnet user-secrets set "ConnectionStrings:sqlConnection" "Server=(localdb)\MSSQLLocalDB;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" +``` + +or from an environment variable, using the double underscore as the separator: + +``` +ConnectionStrings__sqlConnection=... +``` + +Each of the three projects that use the Secret Manager carries its **own** +`UserSecretsId`, so their secret stores are separate — which is the point the last +article makes about user secrets being scoped to a project. + +## The tests + +Every folder has a test project, and the four that need no database run anywhere. + +`CustomConfigurationProvider.Tests` and `SecuringDataLocally.Tests` start a real SQL +Server in a [Testcontainers](https://dotnet.testcontainers.org/) container, so they +need a running Docker daemon. Tests that need the container are marked +`[RequiresDockerFact]` and are **skipped**, not failed, when no daemon is reachable, +so `dotnet test` is green on a machine without Docker. Set +`CONFIGURATION_SERIES_REQUIRE_DOCKER=1` to turn a missing daemon into a failure +instead, which is the setting to use if you want the container tests to be +mandatory. diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo.sln b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo.sln new file mode 100644 index 000000000..512781d2b --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectConfigurationDemo", "ProjectConfigurationDemo\ProjectConfigurationDemo.csproj", "{5867396B-A23C-4D74-BA94-F765B4C4C9DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecuringDataLocally.Tests", "SecuringDataLocally.Tests\SecuringDataLocally.Tests.csproj", "{2476046C-BC32-4D5B-B841-F8052907AC32}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Debug|x64.Build.0 = Debug|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Debug|x86.Build.0 = Debug|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Release|Any CPU.Build.0 = Release|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Release|x64.ActiveCfg = Release|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Release|x64.Build.0 = Release|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Release|x86.ActiveCfg = Release|Any CPU + {5867396B-A23C-4D74-BA94-F765B4C4C9DD}.Release|x86.Build.0 = Release|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Debug|x64.ActiveCfg = Debug|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Debug|x64.Build.0 = Debug|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Debug|x86.ActiveCfg = Debug|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Debug|x86.Build.0 = Debug|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Release|Any CPU.Build.0 = Release|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Release|x64.ActiveCfg = Release|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Release|x64.Build.0 = Release|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Release|x86.ActiveCfg = Release|Any CPU + {2476046C-BC32-4D5B-B841-F8052907AC32}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs new file mode 100644 index 000000000..ca4785208 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/ConfigurationValidation/TitleConfigurationValidation.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.ConfigurationValidation; + +public class TitleConfigurationValidation : IValidateOptions +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + + public ValidateOptionsResult Validate(string? name, TitleConfiguration options) + { + var builder = new ValidateOptionsResultBuilder(); + + if (string.IsNullOrEmpty(options.WelcomeMessage) || options.WelcomeMessage.Length > 60) + builder.AddError("Welcome message must be defined and it must be less than 60 characters long.", + nameof(options.WelcomeMessage)); + + if (!_colors.Contains(options.Color)) + builder.AddError($"Provided title color '{options.Color}' is not among allowed colors.", + nameof(options.Color)); + + return builder.Build(); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Controllers/HomeController.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Controllers/HomeController.cs new file mode 100644 index 000000000..dd426ae2a --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Controllers/HomeController.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +namespace ProjectConfigurationDemo.Controllers; + +public class HomeController( + ILogger logger, + IOptionsSnapshot homePageTitleConfiguration, + ITitleColorService titleColorService) : Controller +{ + private readonly ILogger _logger = logger; + private readonly TitleConfiguration _homePageTitleConfiguration = homePageTitleConfiguration.Value; + private readonly ITitleColorService _titleColorService = titleColorService; + + public IActionResult Index() + { + var homeModel = new HomeModel + { + Configuration = _homePageTitleConfiguration + }; + + homeModel.Configuration.Color = _titleColorService.GetTitleColor(); + + return View(homeModel); + } + + public IActionResult Privacy() => View(); + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() => + View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationDbContext.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationDbContext.cs new file mode 100644 index 000000000..c8a57f0eb --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationDbContext.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; + +namespace ProjectConfigurationDemo.Models; + +public class ConfigurationDbContext(string? connectionString) : DbContext +{ + private readonly string? _connectionString = connectionString; + + public DbSet ConfigurationEntities { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer(_connectionString); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationEntity.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationEntity.cs new file mode 100644 index 000000000..c1c67b7ff --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationEntity.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace ProjectConfigurationDemo.Models; + +public class ConfigurationEntity +{ + [Key] + public string Key { get; set; } = string.Empty; + + public string? Value { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationProvider.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationProvider.cs new file mode 100644 index 000000000..975612f02 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationProvider.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace ProjectConfigurationDemo.Models.ConfigurationProviders; + +public class EFConfigurationProvider(string? connectionString) : ConfigurationProvider +{ + private readonly string? _connectionString = connectionString; + + public override void Load() + { + using var dbContext = new ConfigurationDbContext(_connectionString); + + dbContext.Database.EnsureCreated(); + + Data = dbContext.ConfigurationEntities.Any() + ? dbContext.ConfigurationEntities.ToDictionary(c => c.Key, c => c.Value, StringComparer.OrdinalIgnoreCase) + : CreateAndSaveDefaultValues(dbContext); + } + + private static Dictionary CreateAndSaveDefaultValues(ConfigurationDbContext dbContext) + { + var configValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Pages:HomePage:WelcomeMessage", "Welcome to the ProjectConfigurationDemo Home Page" }, + { "Pages:HomePage:ShowWelcomeMessage", "true" }, + { "Pages:HomePage:Color", "black" }, + { "Pages:HomePage:UseRandomTitleColor", "true" } + }; + + dbContext.ConfigurationEntities.AddRange( + [.. configValues.Select(kvp => new ConfigurationEntity { Key = kvp.Key, Value = kvp.Value })]); + + dbContext.SaveChanges(); + + return configValues; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationSource.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationSource.cs new file mode 100644 index 000000000..9c842dd4b --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ConfigurationProviders/EFConfigurationSource.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Configuration; + +namespace ProjectConfigurationDemo.Models.ConfigurationProviders; + +public class EFConfigurationSource(string? connectionString) : IConfigurationSource +{ + private readonly string? _connectionString = connectionString; + + public IConfigurationProvider Build(IConfigurationBuilder builder) + => new EFConfigurationProvider(_connectionString); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/EntityConfigurationExtensions.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/EntityConfigurationExtensions.cs new file mode 100644 index 000000000..5a6f5f669 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/EntityConfigurationExtensions.cs @@ -0,0 +1,16 @@ +using ProjectConfigurationDemo.Models.ConfigurationProviders; + +namespace ProjectConfigurationDemo.Models; + +public static class EntityConfigurationExtensions +{ + public static ConfigurationManager AddEntityConfiguration(this ConfigurationManager manager) + { + var connectionString = manager.GetConnectionString("sqlConnection"); + + IConfigurationBuilder configBuilder = manager; + configBuilder.Add(new EFConfigurationSource(connectionString)); + + return manager; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ErrorViewModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ErrorViewModel.cs new file mode 100644 index 000000000..d3daed82d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/ErrorViewModel.cs @@ -0,0 +1,8 @@ +namespace ProjectConfigurationDemo.Models; + +public class ErrorViewModel +{ + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/HomeModel.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/HomeModel.cs new file mode 100644 index 000000000..7838fe808 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/HomeModel.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class HomeModel +{ + public TitleConfiguration Configuration { get; set; } = new(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs new file mode 100644 index 000000000..9cc59d60f --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/LoggingLevelConfiguration.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Models; + +public class LoggingLevelConfiguration +{ + public string? Default { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/TitleConfiguration.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/TitleConfiguration.cs new file mode 100644 index 000000000..cf0cb4971 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Models/TitleConfiguration.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace ProjectConfigurationDemo.Models; + +public class TitleConfiguration +{ + [Required] + [MaxLength(60)] + public string WelcomeMessage { get; set; } = string.Empty; + + public bool ShowWelcomeMessage { get; set; } + + public string Color { get; set; } = string.Empty; + + public bool UseRandomTitleColor { get; set; } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Program.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Program.cs new file mode 100644 index 000000000..6ef319fc2 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Program.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.ConfigurationValidation; +using ProjectConfigurationDemo.Models; +using ProjectConfigurationDemo.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.AddEntityConfiguration(); + +builder.Services + .AddOptionsWithValidateOnStart() + .Bind(builder.Configuration.GetSection("Pages:HomePage")) + .ValidateDataAnnotations(); + +builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton, TitleConfigurationValidation>()); + +builder.Services.TryAddSingleton(); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +app.MapStaticAssets(); +app.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}") + .WithStaticAssets(); + +app.Run(); diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj new file mode 100644 index 000000000..3526addb8 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/ProjectConfigurationDemo.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + 77c3db5f-44fb-4632-bcb8-d535a49f7e20 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Properties/launchSettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Properties/launchSettings.json new file mode 100644 index 000000000..92ac1b057 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Services/ITitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Services/ITitleColorService.cs new file mode 100644 index 000000000..2ee1036bf --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Services/ITitleColorService.cs @@ -0,0 +1,6 @@ +namespace ProjectConfigurationDemo.Services; + +public interface ITitleColorService +{ + string GetTitleColor(); +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Services/TitleColorService.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Services/TitleColorService.cs new file mode 100644 index 000000000..1e9530488 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Services/TitleColorService.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Options; +using ProjectConfigurationDemo.Models; + +namespace ProjectConfigurationDemo.Services; + +public class TitleColorService(IOptionsMonitor titleConfiguration) : ITitleColorService +{ + private readonly string[] _colors = ["red", "green", "blue", "black", "purple", "yellow", "brown", "pink"]; + private readonly IOptionsMonitor _titleConfiguration = titleConfiguration; + + public string GetTitleColor() + { + var configuration = _titleConfiguration.CurrentValue; + + return configuration.UseRandomTitleColor + ? _colors[Random.Shared.Next(_colors.Length)] + : configuration.Color; + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Home/Index.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Home/Index.cshtml new file mode 100644 index 000000000..c286342c5 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@model HomeModel +@{ + ViewData["Title"] = "Home Page"; +} +
+ @if (Model.Configuration.ShowWelcomeMessage) + { +

@Model.Configuration.WelcomeMessage

+ } +
diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Home/Privacy.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Home/Privacy.cshtml new file mode 100644 index 000000000..d01477f38 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Home/Privacy.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Shared/Error.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Shared/Error.cshtml new file mode 100644 index 000000000..8f923fdf4 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Shared/Error.cshtml @@ -0,0 +1,25 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the + ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app. +

diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000..208bd9a35 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/Shared/_Layout.cshtml @@ -0,0 +1,30 @@ + + + + + + @ViewData["Title"] - ProjectConfigurationDemo + + + +
+ +
+
+
+ @RenderBody() +
+
+ +
+ © @DateTime.Now.Year - ProjectConfigurationDemo - + Privacy +
+ + @await RenderSectionAsync("Scripts", required: false) + + diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/_ViewImports.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/_ViewImports.cshtml new file mode 100644 index 000000000..92266eb48 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using ProjectConfigurationDemo +@using ProjectConfigurationDemo.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/_ViewStart.cshtml b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/_ViewStart.cshtml new file mode 100644 index 000000000..820a2f6e0 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.Development.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.Production.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.Production.json new file mode 100644 index 000000000..6cf0b42b2 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.Production.json @@ -0,0 +1,7 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.json b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.json new file mode 100644 index 000000000..d9d9a9bff --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/css/site.css b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/css/site.css new file mode 100644 index 000000000..d78436d57 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/css/site.css @@ -0,0 +1,49 @@ +html { + font-size: 14px; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; +} + +body { + margin: 0; + color: #212529; + background-color: #fff; +} + +header nav { + display: flex; + gap: 1rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid #e5e5e5; +} + +header nav a { + color: #212529; + text-decoration: none; +} + +.container { + max-width: 960px; + margin: 0 auto; + padding: 1.5rem; +} + +.text-center { + text-align: center; +} + +.display-4 { + font-size: 3.5rem; + font-weight: 300; +} + +.text-danger { + color: #dc3545; +} + +footer { + max-width: 960px; + margin: 0 auto; + padding: 1rem 1.5rem; + border-top: 1px solid #e5e5e5; + color: #6c757d; +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/favicon.ico b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/favicon.ico new file mode 100644 index 000000000..a3a799985 Binary files /dev/null and b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/favicon.ico differ diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/js/site.js b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/js/site.js new file mode 100644 index 000000000..104169066 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/ProjectConfigurationDemo/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your JavaScript code. diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/README.md b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/README.md new file mode 100644 index 000000000..d0ca34b7f --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/README.md @@ -0,0 +1,33 @@ +# SecuringDataLocally + +Sample for [User Secrets vs Environment Variables in ASP.NET Core](https://code-maze.com/aspnet-configuration-securing-sensitive-data/). + +The code is the same application as `CustomConfigurationProvider`, with one +deliberate difference: **`appsettings.json` carries no `ConnectionStrings` section**. +That is the article's subject. `Program.cs` still reads +`ConnectionStrings:sqlConnection` and hands it to the EF Core configuration provider, +so the application does not start until the value is supplied from outside the file. + +Supply it as a user secret: + +``` +cd ProjectConfigurationDemo +dotnet user-secrets set "ConnectionStrings:sqlConnection" "Server=(localdb)\MSSQLLocalDB;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" +``` + +or as an environment variable, where `__` is the separator that becomes `:` in the +configuration key path: + +``` +set "ConnectionStrings__sqlConnection=Server=(localdb)\MSSQLLocalDB;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" :: cmd +$env:ConnectionStrings__sqlConnection = "Server=(localdb)\MSSQLLocalDB;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True" # PowerShell +export ConnectionStrings__sqlConnection="Server=localhost;Database=CodeMazeCommerce;User Id=sa;Password=;TrustServerCertificate=True" # bash +``` + +This project has its **own** `UserSecretsId`, separate from the other two projects in +the series that use the Secret Manager, so a secret set here is not visible to them. + +The tests supply the connection string through configuration and point it at a +Testcontainers SQL Server container. They never put it back into `appsettings.json`. +Tests marked `[RequiresDockerFact]` are skipped when no Docker daemon is reachable; +set `CONFIGURATION_SERIES_REQUIRE_DOCKER=1` to make a missing daemon a failure. diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/ConnectionStringSourceTests.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/ConnectionStringSourceTests.cs new file mode 100644 index 000000000..5a10cde9d --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/ConnectionStringSourceTests.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Configuration; + +namespace SecuringDataLocally.Tests; + +public class ConnectionStringSourceTests +{ + internal static string AppSettingsPath => + Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + + // The article's subject, asserted rather than described: the checked-in + // appsettings.json carries no connection string at all, so the value has to come + // from a user secret or an environment variable. + [Fact] + public void GivenTheShippedAppSettingsFile_WhenTheConnectionStringIsRead_ThenThereIsNone() + { + var configuration = new ConfigurationBuilder() + .AddJsonFile(AppSettingsPath) + .Build(); + + Assert.Null(configuration.GetConnectionString("sqlConnection")); + Assert.Empty(configuration.GetSection("ConnectionStrings").GetChildren()); + } + + [Fact] + public void GivenAConnectionStringSuppliedOutsideTheFile_WhenItIsAddedLast_ThenItIsTheValueTheAppReads() + { + var configuration = new ConfigurationBuilder() + .AddJsonFile(AppSettingsPath) + .AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:sqlConnection"] = "Server=from-a-secret;Database=CodeMazeCommerce" + }) + .Build(); + + Assert.Equal("Server=from-a-secret;Database=CodeMazeCommerce", + configuration.GetConnectionString("sqlConnection")); + } + + [Fact] + public void GivenAnEnvironmentVariable_WhenTheDoubleUnderscoreSeparatorIsUsed_ThenItReachesTheConnectionStringsSection() + { + Environment.SetEnvironmentVariable( + "ConnectionStrings__sqlConnection", "Server=from-an-environment-variable"); + try + { + var configuration = new ConfigurationBuilder() + .AddJsonFile(AppSettingsPath) + .AddEnvironmentVariables() + .Build(); + + Assert.Equal("Server=from-an-environment-variable", + configuration.GetConnectionString("sqlConnection")); + } + finally + { + Environment.SetEnvironmentVariable("ConnectionStrings__sqlConnection", null); + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/RequiresDockerFactAttribute.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/RequiresDockerFactAttribute.cs new file mode 100644 index 000000000..18a4b74be --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/RequiresDockerFactAttribute.cs @@ -0,0 +1,62 @@ +using System.IO.Pipes; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace SecuringDataLocally.Tests; + +/// +/// A [Fact] that is skipped when no Docker daemon is reachable, so the suite stays +/// green on a developer machine without Docker and really runs where a daemon is +/// available, starting a SQL Server container. Set +/// CONFIGURATION_SERIES_REQUIRE_DOCKER=1 to turn a missing daemon into a failure +/// instead of a skip. +/// +public sealed class RequiresDockerFactAttribute : FactAttribute +{ + public RequiresDockerFactAttribute() + { + if (DockerAvailability.IsAvailable || DockerAvailability.IsRequired) + return; + + Skip = "Docker is not reachable on this machine, so the SQL Server container cannot start."; + } +} + +public static class DockerAvailability +{ + private static readonly Lazy Probe = new(Detect); + + public static bool IsAvailable => Probe.Value; + + public static bool IsRequired => + Environment.GetEnvironmentVariable("CONFIGURATION_SERIES_REQUIRE_DOCKER") == "1"; + + // Connect to the daemon rather than looking for a socket or a pipe on disk: + // Docker Desktop leaves its named pipe behind when it is not running, so the + // cheaper check reports a daemon that is not there. + private static bool Detect() + { + if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DOCKER_HOST"))) + return true; + + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using var pipe = new NamedPipeClientStream(".", "docker_engine", PipeDirection.InOut); + pipe.Connect(timeout: 1000); + + return pipe.IsConnected; + } + + using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + socket.Connect(new UnixDomainSocketEndPoint("/var/run/docker.sock")); + + return socket.Connected; + } + catch (Exception) + { + return false; + } + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/SecretSuppliedConfigurationTests.cs b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/SecretSuppliedConfigurationTests.cs new file mode 100644 index 000000000..d36254ee8 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/SecretSuppliedConfigurationTests.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Configuration; +using ProjectConfigurationDemo.Models; +using Testcontainers.MsSql; + +namespace SecuringDataLocally.Tests; + +// The folder must still RUN once the secret is supplied. The connection string +// reaches the EF configuration provider through configuration, never through +// appsettings.json. +public class SecretSuppliedConfigurationTests : IAsyncLifetime +{ + private const string SqlServerImage = "mcr.microsoft.com/mssql/server:2022-latest"; + + private readonly MsSqlContainer _sqlServer = new MsSqlBuilder(SqlServerImage).Build(); + + public async Task InitializeAsync() + { + if (DockerAvailability.IsAvailable || DockerAvailability.IsRequired) + await _sqlServer.StartAsync(); + } + + public async Task DisposeAsync() + { + if (DockerAvailability.IsAvailable || DockerAvailability.IsRequired) + await _sqlServer.DisposeAsync(); + } + + [RequiresDockerFact] + public void GivenTheSecretIsSupplied_WhenTheApplicationConfigurationIsBuilt_ThenTheEntityProviderLoads() + { + var manager = new ConfigurationManager(); + + ((IConfigurationBuilder)manager) + .AddJsonFile(ConnectionStringSourceTests.AppSettingsPath) + .AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:sqlConnection"] = _sqlServer.GetConnectionString() + }); + + manager.AddEntityConfiguration(); + + var titleConfiguration = manager.GetSection("Pages:HomePage").Get(); + + Assert.NotNull(titleConfiguration); + Assert.Equal("Welcome to the ProjectConfigurationDemo Home Page", titleConfiguration.WelcomeMessage); + } +} diff --git a/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/SecuringDataLocally.Tests.csproj b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/SecuringDataLocally.Tests.csproj new file mode 100644 index 000000000..b5a51c806 --- /dev/null +++ b/aspnetcore-webapi/AspNetCoreConfigurationSeries/SecuringDataLocally/SecuringDataLocally.Tests/SecuringDataLocally.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + +