diff --git a/aspnetcore-webapi/BasicWebApiSeries/BasicWebApiSeries.sln b/aspnetcore-webapi/BasicWebApiSeries/BasicWebApiSeries.sln new file mode 100644 index 000000000..6f6cf056b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/BasicWebApiSeries.sln @@ -0,0 +1,332 @@ + +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}") = "ServiceConfiguration", "ServiceConfiguration", "{D9D1C010-0649-F69F-F2F1-9505732C8135}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountOwnerServer", "ServiceConfiguration\AccountOwnerServer\AccountOwnerServer.csproj", "{59D4C354-9CA9-43FB-8236-D4F8034E5BB2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Logging", "Logging", "{592BFD43-8236-1486-20CA-62D2F7ED4E57}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountOwnerServer", "Logging\AccountOwnerServer\AccountOwnerServer.csproj", "{3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerService", "Logging\LoggerService\LoggerService.csproj", "{348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "Logging\Contracts\Contracts.csproj", "{F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RepositoryPatternWithEfCore", "RepositoryPatternWithEfCore", "{225D120C-FE9E-56C9-082E-4E9BEFF1B9C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountOwnerServer", "RepositoryPatternWithEfCore\AccountOwnerServer\AccountOwnerServer.csproj", "{2717507B-8A1F-4E89-8DED-76A6ADB79E61}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerService", "RepositoryPatternWithEfCore\LoggerService\LoggerService.csproj", "{56FFE5D0-878F-4D18-848D-B7457B8ECA2D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "RepositoryPatternWithEfCore\Contracts\Contracts.csproj", "{5F023375-15C2-4843-A38E-78C97A9AA0DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Entities", "RepositoryPatternWithEfCore\Entities\Entities.csproj", "{2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "RepositoryPatternWithEfCore\Repository\Repository.csproj", "{1C5636BE-7C1A-4512-9064-712C3B48ADD8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UsingRepositoryForGetRequests", "UsingRepositoryForGetRequests", "{8C336C94-9995-2443-DFAD-0DBDC4FD8F35}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountOwnerServer", "UsingRepositoryForGetRequests\AccountOwnerServer\AccountOwnerServer.csproj", "{B75E5A35-95B2-4913-B012-8BEEF83AAB06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerService", "UsingRepositoryForGetRequests\LoggerService\LoggerService.csproj", "{FF83F444-50D1-47A6-A367-FF9B502D1E4A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "UsingRepositoryForGetRequests\Contracts\Contracts.csproj", "{30476240-59E2-4280-BA03-59764CE15E67}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Entities", "UsingRepositoryForGetRequests\Entities\Entities.csproj", "{F435C953-2490-47A2-9A41-D6FAD7C10F80}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "UsingRepositoryForGetRequests\Repository\Repository.csproj", "{9EA34724-1DA9-49B6-B8FF-6929752226FA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UsingRepositoryForWriteRequests", "UsingRepositoryForWriteRequests", "{A2DD47B0-FD7E-9A5D-E48B-2546E6692334}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountOwnerServer", "UsingRepositoryForWriteRequests\AccountOwnerServer\AccountOwnerServer.csproj", "{DC440D6F-7F79-4D4A-82FD-C566AEB168E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerService", "UsingRepositoryForWriteRequests\LoggerService\LoggerService.csproj", "{558A5044-88C7-469C-B2BD-01FED3C8CC49}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "UsingRepositoryForWriteRequests\Contracts\Contracts.csproj", "{3F67E84F-6B51-4688-8434-7BA718C17857}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Entities", "UsingRepositoryForWriteRequests\Entities\Entities.csproj", "{769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "UsingRepositoryForWriteRequests\Repository\Repository.csproj", "{CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountOwnerServer.Tests", "UsingRepositoryForWriteRequests\AccountOwnerServer.Tests\AccountOwnerServer.Tests.csproj", "{EB2FFD0C-B624-4F50-860C-D54D7284408F}" +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 + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Debug|x64.ActiveCfg = Debug|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Debug|x64.Build.0 = Debug|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Debug|x86.ActiveCfg = Debug|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Debug|x86.Build.0 = Debug|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Release|Any CPU.Build.0 = Release|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Release|x64.ActiveCfg = Release|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Release|x64.Build.0 = Release|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Release|x86.ActiveCfg = Release|Any CPU + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2}.Release|x86.Build.0 = Release|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Debug|x64.ActiveCfg = Debug|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Debug|x64.Build.0 = Debug|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Debug|x86.ActiveCfg = Debug|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Debug|x86.Build.0 = Debug|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Release|Any CPU.Build.0 = Release|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Release|x64.ActiveCfg = Release|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Release|x64.Build.0 = Release|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Release|x86.ActiveCfg = Release|Any CPU + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F}.Release|x86.Build.0 = Release|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Debug|x64.ActiveCfg = Debug|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Debug|x64.Build.0 = Debug|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Debug|x86.ActiveCfg = Debug|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Debug|x86.Build.0 = Debug|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Release|Any CPU.Build.0 = Release|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Release|x64.ActiveCfg = Release|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Release|x64.Build.0 = Release|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Release|x86.ActiveCfg = Release|Any CPU + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5}.Release|x86.Build.0 = Release|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Debug|x64.ActiveCfg = Debug|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Debug|x64.Build.0 = Debug|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Debug|x86.Build.0 = Debug|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Release|Any CPU.Build.0 = Release|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Release|x64.ActiveCfg = Release|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Release|x64.Build.0 = Release|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Release|x86.ActiveCfg = Release|Any CPU + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA}.Release|x86.Build.0 = Release|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Debug|x64.ActiveCfg = Debug|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Debug|x64.Build.0 = Debug|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Debug|x86.ActiveCfg = Debug|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Debug|x86.Build.0 = Debug|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Release|Any CPU.Build.0 = Release|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Release|x64.ActiveCfg = Release|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Release|x64.Build.0 = Release|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Release|x86.ActiveCfg = Release|Any CPU + {2717507B-8A1F-4E89-8DED-76A6ADB79E61}.Release|x86.Build.0 = Release|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Debug|x64.ActiveCfg = Debug|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Debug|x64.Build.0 = Debug|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Debug|x86.ActiveCfg = Debug|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Debug|x86.Build.0 = Debug|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Release|Any CPU.Build.0 = Release|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Release|x64.ActiveCfg = Release|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Release|x64.Build.0 = Release|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Release|x86.ActiveCfg = Release|Any CPU + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D}.Release|x86.Build.0 = Release|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Debug|x64.Build.0 = Debug|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Debug|x86.Build.0 = Debug|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Release|Any CPU.Build.0 = Release|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Release|x64.ActiveCfg = Release|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Release|x64.Build.0 = Release|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Release|x86.ActiveCfg = Release|Any CPU + {5F023375-15C2-4843-A38E-78C97A9AA0DD}.Release|x86.Build.0 = Release|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Debug|x64.Build.0 = Debug|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Debug|x86.Build.0 = Debug|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Release|Any CPU.Build.0 = Release|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Release|x64.ActiveCfg = Release|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Release|x64.Build.0 = Release|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Release|x86.ActiveCfg = Release|Any CPU + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B}.Release|x86.Build.0 = Release|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Debug|x64.Build.0 = Debug|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Debug|x86.Build.0 = Debug|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Release|Any CPU.Build.0 = Release|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Release|x64.ActiveCfg = Release|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Release|x64.Build.0 = Release|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Release|x86.ActiveCfg = Release|Any CPU + {1C5636BE-7C1A-4512-9064-712C3B48ADD8}.Release|x86.Build.0 = Release|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Debug|x64.ActiveCfg = Debug|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Debug|x64.Build.0 = Debug|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Debug|x86.ActiveCfg = Debug|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Debug|x86.Build.0 = Debug|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Release|Any CPU.Build.0 = Release|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Release|x64.ActiveCfg = Release|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Release|x64.Build.0 = Release|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Release|x86.ActiveCfg = Release|Any CPU + {B75E5A35-95B2-4913-B012-8BEEF83AAB06}.Release|x86.Build.0 = Release|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Debug|x64.ActiveCfg = Debug|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Debug|x64.Build.0 = Debug|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Debug|x86.ActiveCfg = Debug|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Debug|x86.Build.0 = Debug|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Release|Any CPU.Build.0 = Release|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Release|x64.ActiveCfg = Release|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Release|x64.Build.0 = Release|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Release|x86.ActiveCfg = Release|Any CPU + {FF83F444-50D1-47A6-A367-FF9B502D1E4A}.Release|x86.Build.0 = Release|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Debug|x64.ActiveCfg = Debug|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Debug|x64.Build.0 = Debug|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Debug|x86.ActiveCfg = Debug|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Debug|x86.Build.0 = Debug|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Release|Any CPU.Build.0 = Release|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Release|x64.ActiveCfg = Release|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Release|x64.Build.0 = Release|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Release|x86.ActiveCfg = Release|Any CPU + {30476240-59E2-4280-BA03-59764CE15E67}.Release|x86.Build.0 = Release|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Debug|x64.ActiveCfg = Debug|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Debug|x64.Build.0 = Debug|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Debug|x86.ActiveCfg = Debug|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Debug|x86.Build.0 = Debug|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Release|Any CPU.Build.0 = Release|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Release|x64.ActiveCfg = Release|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Release|x64.Build.0 = Release|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Release|x86.ActiveCfg = Release|Any CPU + {F435C953-2490-47A2-9A41-D6FAD7C10F80}.Release|x86.Build.0 = Release|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Debug|x64.Build.0 = Debug|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Debug|x86.Build.0 = Debug|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Release|Any CPU.Build.0 = Release|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Release|x64.ActiveCfg = Release|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Release|x64.Build.0 = Release|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Release|x86.ActiveCfg = Release|Any CPU + {9EA34724-1DA9-49B6-B8FF-6929752226FA}.Release|x86.Build.0 = Release|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Debug|x64.Build.0 = Debug|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Debug|x86.ActiveCfg = Debug|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Debug|x86.Build.0 = Debug|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Release|Any CPU.Build.0 = Release|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Release|x64.ActiveCfg = Release|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Release|x64.Build.0 = Release|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Release|x86.ActiveCfg = Release|Any CPU + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5}.Release|x86.Build.0 = Release|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Debug|x64.ActiveCfg = Debug|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Debug|x64.Build.0 = Debug|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Debug|x86.ActiveCfg = Debug|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Debug|x86.Build.0 = Debug|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Release|Any CPU.Build.0 = Release|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Release|x64.ActiveCfg = Release|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Release|x64.Build.0 = Release|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Release|x86.ActiveCfg = Release|Any CPU + {558A5044-88C7-469C-B2BD-01FED3C8CC49}.Release|x86.Build.0 = Release|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Debug|x64.ActiveCfg = Debug|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Debug|x64.Build.0 = Debug|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Debug|x86.ActiveCfg = Debug|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Debug|x86.Build.0 = Debug|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Release|Any CPU.Build.0 = Release|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Release|x64.ActiveCfg = Release|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Release|x64.Build.0 = Release|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Release|x86.ActiveCfg = Release|Any CPU + {3F67E84F-6B51-4688-8434-7BA718C17857}.Release|x86.Build.0 = Release|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Debug|x64.Build.0 = Debug|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Debug|x86.ActiveCfg = Debug|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Debug|x86.Build.0 = Debug|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Release|Any CPU.Build.0 = Release|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Release|x64.ActiveCfg = Release|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Release|x64.Build.0 = Release|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Release|x86.ActiveCfg = Release|Any CPU + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5}.Release|x86.Build.0 = Release|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Debug|x64.Build.0 = Debug|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Debug|x86.Build.0 = Debug|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Release|Any CPU.Build.0 = Release|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Release|x64.ActiveCfg = Release|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Release|x64.Build.0 = Release|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Release|x86.ActiveCfg = Release|Any CPU + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5}.Release|x86.Build.0 = Release|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Debug|x64.Build.0 = Debug|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Debug|x86.ActiveCfg = Debug|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Debug|x86.Build.0 = Debug|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Release|Any CPU.Build.0 = Release|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Release|x64.ActiveCfg = Release|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Release|x64.Build.0 = Release|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Release|x86.ActiveCfg = Release|Any CPU + {EB2FFD0C-B624-4F50-860C-D54D7284408F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {59D4C354-9CA9-43FB-8236-D4F8034E5BB2} = {D9D1C010-0649-F69F-F2F1-9505732C8135} + {3BBF1DF5-1A13-4E68-90D2-5186CEE7173F} = {592BFD43-8236-1486-20CA-62D2F7ED4E57} + {348F08A4-6FC5-418D-B28A-33DC6AEBBDE5} = {592BFD43-8236-1486-20CA-62D2F7ED4E57} + {F38C5C71-C966-45DA-80C7-ECCF9A73D4CA} = {592BFD43-8236-1486-20CA-62D2F7ED4E57} + {2717507B-8A1F-4E89-8DED-76A6ADB79E61} = {225D120C-FE9E-56C9-082E-4E9BEFF1B9C1} + {56FFE5D0-878F-4D18-848D-B7457B8ECA2D} = {225D120C-FE9E-56C9-082E-4E9BEFF1B9C1} + {5F023375-15C2-4843-A38E-78C97A9AA0DD} = {225D120C-FE9E-56C9-082E-4E9BEFF1B9C1} + {2F32BCD7-09FD-402C-ABE9-46F8ACF42C3B} = {225D120C-FE9E-56C9-082E-4E9BEFF1B9C1} + {1C5636BE-7C1A-4512-9064-712C3B48ADD8} = {225D120C-FE9E-56C9-082E-4E9BEFF1B9C1} + {B75E5A35-95B2-4913-B012-8BEEF83AAB06} = {8C336C94-9995-2443-DFAD-0DBDC4FD8F35} + {FF83F444-50D1-47A6-A367-FF9B502D1E4A} = {8C336C94-9995-2443-DFAD-0DBDC4FD8F35} + {30476240-59E2-4280-BA03-59764CE15E67} = {8C336C94-9995-2443-DFAD-0DBDC4FD8F35} + {F435C953-2490-47A2-9A41-D6FAD7C10F80} = {8C336C94-9995-2443-DFAD-0DBDC4FD8F35} + {9EA34724-1DA9-49B6-B8FF-6929752226FA} = {8C336C94-9995-2443-DFAD-0DBDC4FD8F35} + {DC440D6F-7F79-4D4A-82FD-C566AEB168E5} = {A2DD47B0-FD7E-9A5D-E48B-2546E6692334} + {558A5044-88C7-469C-B2BD-01FED3C8CC49} = {A2DD47B0-FD7E-9A5D-E48B-2546E6692334} + {3F67E84F-6B51-4688-8434-7BA718C17857} = {A2DD47B0-FD7E-9A5D-E48B-2546E6692334} + {769D39CF-30EA-4BA8-BC9D-78C12C55F0F5} = {A2DD47B0-FD7E-9A5D-E48B-2546E6692334} + {CA5F716F-16C7-4E44-BCA9-7D9ED65BDCC5} = {A2DD47B0-FD7E-9A5D-E48B-2546E6692334} + {EB2FFD0C-B624-4F50-860C-D54D7284408F} = {A2DD47B0-FD7E-9A5D-E48B-2546E6692334} + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/BasicWebApiSeries/Database/README.md b/aspnetcore-webapi/BasicWebApiSeries/Database/README.md new file mode 100644 index 000000000..b5136f15f --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Database/README.md @@ -0,0 +1,38 @@ +# Database + +Part 1 of the ASP.NET Core Web API series. This folder holds the SQL Server schema the +rest of the series reads and writes, and nothing else: there is no .NET project here. + +`init.sql` creates the `AccountOwner` database, the `Owner` and `Account` tables, the +foreign key between them, and the sample data. It is re-runnable. + +## Windows, on LocalDB + +``` +sqlcmd -S "(localdb)\MSSQLLocalDB" -i init.sql +``` + +LocalDB comes with the Visual Studio Installer's *Data storage and processing* workload +(and with SQL Server Express media). It is not part of the .NET SDK. + +## Anywhere, in a container + +``` +docker compose up -d +sqlcmd -S localhost -U sa -P "YourStrong!Passw0rd" -C -i init.sql +``` + +`-C` trusts the container's self-signed certificate, which ODBC Driver 18 and the newer +`sqlcmd` builds require. + +## Why the ids are fixed + +The sample data hardcodes its GUIDs. Later parts of the series call endpoints with these +exact ids, so a reader who generates fresh ones cannot follow along. + +## Why nothing calls EnsureCreated() + +The schema comes from this script, with explicit lengths (`NVARCHAR(60)`, `NVARCHAR(100)`, +`NVARCHAR(45)`) and `DATE` columns. EF Core's default model for the same entities would +produce `nvarchar(max)` and `datetime2` instead. The script is the only thing that creates +the schema, in every folder of this series. diff --git a/aspnetcore-webapi/BasicWebApiSeries/Database/docker-compose.yml b/aspnetcore-webapi/BasicWebApiSeries/Database/docker-compose.yml new file mode 100644 index 000000000..7a2ea64c0 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Database/docker-compose.yml @@ -0,0 +1,9 @@ +services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: accountowner-sqlserver + environment: + ACCEPT_EULA: "Y" + MSSQL_SA_PASSWORD: "YourStrong!Passw0rd" + ports: + - "1433:1433" diff --git a/aspnetcore-webapi/BasicWebApiSeries/Database/init.sql b/aspnetcore-webapi/BasicWebApiSeries/Database/init.sql new file mode 100644 index 000000000..aa8433189 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Database/init.sql @@ -0,0 +1,76 @@ +/* + AccountOwner sample database - ASP.NET Core Web API series, part 1. + + Creates the AccountOwner database, the Owner and Account tables, the foreign key + between them, and the sample data the rest of the series calls its endpoints with. + + The script is re-runnable: it drops the tables before it creates them, so running + it a second time rebuilds the schema instead of failing on names that already exist. + + LocalDB (Windows): + sqlcmd -S "(localdb)\MSSQLLocalDB" -i init.sql + + Container (any OS), started with the docker-compose.yml beside this file: + sqlcmd -S localhost -U sa -P "" -C -i init.sql +*/ + +IF DB_ID('AccountOwner') IS NULL + CREATE DATABASE [AccountOwner]; +GO + +USE [AccountOwner]; +GO + +-- Account first: it holds the foreign key, so it has to go before its parent. +DROP TABLE IF EXISTS [dbo].[Account]; +DROP TABLE IF EXISTS [dbo].[Owner]; +GO + +CREATE TABLE [dbo].[Owner] +( + [OwnerId] UNIQUEIDENTIFIER NOT NULL, + [Name] NVARCHAR(60) NOT NULL, + [DateOfBirth] DATE NOT NULL, + [Address] NVARCHAR(100) NOT NULL, + CONSTRAINT [PK_Owner] PRIMARY KEY CLUSTERED ([OwnerId]) +); +GO + +CREATE TABLE [dbo].[Account] +( + [AccountId] UNIQUEIDENTIFIER NOT NULL, + [DateCreated] DATE NOT NULL, + [AccountType] NVARCHAR(45) NOT NULL, + [OwnerId] UNIQUEIDENTIFIER NOT NULL, + CONSTRAINT [PK_Account] PRIMARY KEY CLUSTERED ([AccountId]), + CONSTRAINT [FK_Account_Owner] FOREIGN KEY ([OwnerId]) + REFERENCES [dbo].[Owner] ([OwnerId]) + ON UPDATE CASCADE + ON DELETE NO ACTION +); +GO + +CREATE INDEX [IX_Account_OwnerId] ON [dbo].[Account] ([OwnerId]); +GO + +-- Owners first. Every Account row names an owner, and the foreign key rejects a row +-- whose owner does not exist yet. +INSERT INTO [dbo].[Owner] ([OwnerId], [Name], [DateOfBirth], [Address]) +VALUES + ('24fd81f8-d58a-4bcc-9f35-dc6cd5641906', N'John Keen', '1980-12-05', N'61 Wellfield Road'), + ('261e1685-cf26-494c-b17c-3546e65f5620', N'Anna Bosh', '1974-11-14', N'27 Colored Row'), + ('a3c1880c-674c-4d18-8f91-5d3608a2c937', N'Sam Query', '1990-04-22', N'91 Western Roads'), + ('f98e4d74-0f68-4aac-89fd-047f1aaca6b6', N'Martin Miller', '1983-05-21', N'3 Edgar Buildings'); +GO + +INSERT INTO [dbo].[Account] ([AccountId], [DateCreated], [AccountType], [OwnerId]) +VALUES + ('03e91478-5608-4132-a753-d494dafce00b', '2003-12-15', N'Domestic', 'f98e4d74-0f68-4aac-89fd-047f1aaca6b6'), + ('356a5a9b-64bf-4de0-bc84-5395a1fdc9c4', '1996-02-15', N'Domestic', '261e1685-cf26-494c-b17c-3546e65f5620'), + ('371b93f2-f8c5-4a32-894a-fc672741aa5b', '1999-05-04', N'Domestic', '24fd81f8-d58a-4bcc-9f35-dc6cd5641906'), + ('670775db-ecc0-4b90-a9ab-37cd0d8e2801', '1999-12-21', N'Savings', '24fd81f8-d58a-4bcc-9f35-dc6cd5641906'), + ('a3fbad0b-7f48-4feb-8ac0-6d3bbc997bfc', '2010-05-28', N'Domestic', 'a3c1880c-674c-4d18-8f91-5d3608a2c937'), + ('aa15f658-04bb-4f73-82af-82db49d0fbef', '1999-05-12', N'Foreign', '24fd81f8-d58a-4bcc-9f35-dc6cd5641906'), + ('c6066eb0-53ca-43e1-97aa-3c2169eec659', '1996-02-16', N'Foreign', '261e1685-cf26-494c-b17c-3546e65f5620'), + ('eccadf79-85fe-402f-893c-32d3f03ed9b1', '2010-06-20', N'Foreign', 'a3c1880c-674c-4d18-8f91-5d3608a2c937'); +GO diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer.sln b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer.sln new file mode 100644 index 000000000..5696d27b0 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32210.238 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccountOwnerServer", "AccountOwnerServer\AccountOwnerServer.csproj", "{0213FB50-5C53-4C34-8B2C-E515C7DC2579}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Contracts", "Contracts\Contracts.csproj", "{B731D3C2-9A89-40E4-B96B-AA300B4195BB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerService", "LoggerService\LoggerService.csproj", "{7CEC76F5-80AF-4B37-B6F1-94A743E5C748}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.Build.0 = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|Any CPU.Build.0 = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F8E1F895-37AB-455B-8D05-ED495BE48F88} + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/AccountOwnerServer.csproj b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/AccountOwnerServer.csproj new file mode 100644 index 000000000..1ad4397b6 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/AccountOwnerServer.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Controllers/WeatherForecastController.cs b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..37ff86661 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Controllers/WeatherForecastController.cs @@ -0,0 +1,29 @@ +using Contracts; +using Microsoft.AspNetCore.Mvc; + +namespace AccountOwnerServer.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private readonly ILoggerManager _logger; + + public WeatherForecastController(ILoggerManager logger) + { + _logger = logger; + } + + // One request produces one line per level in logs/_logfile.txt. + [HttpGet] + public IEnumerable Get() + { + _logger.LogInfo("Here is info message from the controller."); + _logger.LogDebug("Here is debug message from the controller."); + _logger.LogWarn("Here is warn message from the controller."); + _logger.LogError("Here is error message from the controller."); + + return ["value1", "value2"]; + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Controllers/WeatherForecastWithILoggerController.cs b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Controllers/WeatherForecastWithILoggerController.cs new file mode 100644 index 000000000..b59ea48e9 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Controllers/WeatherForecastWithILoggerController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; + +namespace AccountOwnerServer.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastWithILoggerController : ControllerBase + { + private readonly ILogger _logger; + + public WeatherForecastWithILoggerController(ILogger logger) + { + _logger = logger; + } + + // The platform way: no registration, and the message is a template with a named + // placeholder. WeatherForecastController takes the ILoggerManager wrapper instead, + // and both reach the same file because NLog is registered as a provider. + [HttpGet] + public IEnumerable Get() + { + _logger.LogInformation("Serving {Count} forecast values", 2); + + return ["value1", "value2"]; + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Extensions/ServiceExtensions.cs b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Extensions/ServiceExtensions.cs new file mode 100644 index 000000000..6662b38c9 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Extensions/ServiceExtensions.cs @@ -0,0 +1,24 @@ +using Contracts; +using LoggerService; + +namespace AccountOwnerServer.Extensions +{ + public static class ServiceExtensions + { + public static void ConfigureCors(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + }); + } + + public static void ConfigureLoggerService(this IServiceCollection services) + { + services.AddSingleton(); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Program.cs b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Program.cs new file mode 100644 index 000000000..83ca7ddf9 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Program.cs @@ -0,0 +1,39 @@ +using AccountOwnerServer.Extensions; +using Microsoft.AspNetCore.HttpOverrides; +using NLog.Web; + +var builder = WebApplication.CreateBuilder(args); + +// NLog is registered as a logging provider, so ILogger reaches the file targets in +// nlog.config as well as ILoggerManager. NLog finds nlog.config in the output directory +// on its own; there is nothing to load by hand. +builder.Logging.ClearProviders(); +builder.Host.UseNLog(); + +builder.Services.ConfigureCors(); +builder.Services.ConfigureLoggerService(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +// UseForwardedHeaders goes first: every component after it has to see the client's +// scheme and address, not the proxy's, or an HTTPS redirect behind a proxy loops. +app.UseForwardedHeaders(new ForwardedHeadersOptions +{ + ForwardedHeaders = ForwardedHeaders.All +}); + +if (!app.Environment.IsDevelopment()) + app.UseHsts(); + +app.UseHttpsRedirection(); + +app.UseCors("CorsPolicy"); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Properties/launchSettings.json b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Properties/launchSettings.json new file mode 100644 index 000000000..c2d94ddfd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/WeatherForecast.cs b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/WeatherForecast.cs new file mode 100644 index 000000000..41db3296b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace AccountOwnerServer +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} \ No newline at end of file diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/appsettings.Development.json b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/appsettings.json b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/nlog.config b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/nlog.config new file mode 100644 index 000000000..225d1ebc2 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/AccountOwnerServer/nlog.config @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/Contracts/Contracts.csproj b/aspnetcore-webapi/BasicWebApiSeries/Logging/Contracts/Contracts.csproj new file mode 100644 index 000000000..9ed914b5b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/Contracts/Contracts.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/Contracts/ILoggerManager.cs b/aspnetcore-webapi/BasicWebApiSeries/Logging/Contracts/ILoggerManager.cs new file mode 100644 index 000000000..ee1f08105 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/Contracts/ILoggerManager.cs @@ -0,0 +1,10 @@ +namespace Contracts +{ + public interface ILoggerManager + { + void LogInfo(string message); + void LogWarn(string message); + void LogDebug(string message); + void LogError(string message); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/LoggerService/LoggerManager.cs b/aspnetcore-webapi/BasicWebApiSeries/Logging/LoggerService/LoggerManager.cs new file mode 100644 index 000000000..d776ec860 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/LoggerService/LoggerManager.cs @@ -0,0 +1,18 @@ +using Contracts; +using NLog; + +namespace LoggerService +{ + public class LoggerManager : ILoggerManager + { + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + + public void LogDebug(string message) => Logger.Debug(message); + + public void LogError(string message) => Logger.Error(message); + + public void LogInfo(string message) => Logger.Info(message); + + public void LogWarn(string message) => Logger.Warn(message); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/Logging/LoggerService/LoggerService.csproj b/aspnetcore-webapi/BasicWebApiSeries/Logging/LoggerService/LoggerService.csproj new file mode 100644 index 000000000..049803efd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/Logging/LoggerService/LoggerService.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/README.md b/aspnetcore-webapi/BasicWebApiSeries/README.md new file mode 100644 index 000000000..f0dffd6b9 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/README.md @@ -0,0 +1,37 @@ +# Basic Web API Series + +The six-part ASP.NET Core Web API series on Code Maze, one folder per article, in reading +order. Each folder is the previous folder plus that article's work, so a reader can start +anywhere and diff forward. + +| # | Folder | Article | +|---|---|---| +| 1 | [`Database`](Database) | SQL Server With ASP.NET Core: Creating the Database | +| 2 | [`ServiceConfiguration`](ServiceConfiguration) | Service Configuration in ASP.NET Core Web API With Extension Methods | +| 3 | [`Logging`](Logging) | NLog in ASP.NET Core Web API: Logging to a File | +| 4 | [`RepositoryPatternWithEfCore`](RepositoryPatternWithEfCore) | Repository Pattern in .NET Core Web API | +| 5 | [`UsingRepositoryForGetRequests`](UsingRepositoryForGetRequests) | ASP.NET Core Web API GET Requests With a Repository | +| 6 | [`UsingRepositoryForWriteRequests`](UsingRepositoryForWriteRequests) | POST, PUT, and DELETE in ASP.NET Core Web API | + +`BasicWebApiSeries.sln` at this level builds and tests all of them at once. Each article +folder also has its own solution for working through one part on its own. + +## Before you run anything from part 4 onward + +Create the database first. `Database/init.sql` creates `AccountOwner`, both tables and the +sample data; `Database/README.md` has the LocalDB and container commands. The connection +string in each `appsettings.json` points at LocalDB by default. + +Nothing in this series calls `EnsureCreated()`. The script owns the schema, with the +explicit column lengths and `DATE` types the article documents. + +## Everything targets net10.0 + +`Microsoft.EntityFrameworkCore.SqlServer` 10.0.11 where a project touches the database, +`NLog.Web.AspNetCore` 6.2.0 for logging, `AutoMapper` 14.0.0 from part 5 onward. + +## Tests + +`UsingRepositoryForWriteRequests/AccountOwnerServer.Tests` runs the repository against a +real SQL Server in a Testcontainers container, seeded with the same `init.sql`. Where +Docker is not available the tests report as **skipped with a reason**, never as passed. diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer.sln b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer.sln new file mode 100644 index 000000000..78fe05931 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32210.238 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccountOwnerServer", "AccountOwnerServer\AccountOwnerServer.csproj", "{0213FB50-5C53-4C34-8B2C-E515C7DC2579}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contracts", "Contracts\Contracts.csproj", "{B731D3C2-9A89-40E4-B96B-AA300B4195BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggerService", "LoggerService\LoggerService.csproj", "{7CEC76F5-80AF-4B37-B6F1-94A743E5C748}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Entities", "Entities\Entities.csproj", "{16AAE663-A20E-493D-9087-F4F4016F6216}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "Repository\Repository.csproj", "{A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.Build.0 = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|Any CPU.Build.0 = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|Any CPU.Build.0 = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|Any CPU.Build.0 = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F8E1F895-37AB-455B-8D05-ED495BE48F88} + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/AccountOwnerServer.csproj b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/AccountOwnerServer.csproj new file mode 100644 index 000000000..c1e042b7e --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/AccountOwnerServer.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Controllers/WeatherForecastController.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..373185203 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Controllers/WeatherForecastController.cs @@ -0,0 +1,30 @@ +using Contracts; +using Microsoft.AspNetCore.Mvc; + +namespace AccountOwnerServer.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private readonly IRepositoryWrapper _repository; + + public WeatherForecastController(IRepositoryWrapper repository) + { + _repository = repository; + } + + // Proof that the wrapper resolves and both repositories reach the database. + // Part 5 replaces this with a real OwnerController. + [HttpGet] + public IActionResult Get() + { + var domesticAccounts = _repository.Account + .FindByCondition(a => a.AccountType == "Domestic") + .Count(); + var owners = _repository.Owner.FindAll().Count(); + + return Ok(new { owners, domesticAccounts }); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Extensions/ServiceExtensions.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Extensions/ServiceExtensions.cs new file mode 100644 index 000000000..4933589f5 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Extensions/ServiceExtensions.cs @@ -0,0 +1,39 @@ +using Contracts; +using LoggerService; +using Microsoft.EntityFrameworkCore; +using Entities; +using Repository; + +namespace AccountOwnerServer.Extensions +{ + public static class ServiceExtensions + { + public static void ConfigureCors(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + }); + } + + public static void ConfigureLoggerService(this IServiceCollection services) + { + services.AddSingleton(); + } + + public static void ConfigureSqlContext(this IServiceCollection services, IConfiguration config) + { + var connectionString = config.GetConnectionString("SqlConnection"); + + services.AddDbContext(o => o.UseSqlServer(connectionString)); + } + + public static void ConfigureRepositoryWrapper(this IServiceCollection services) + { + services.AddScoped(); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Program.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Program.cs new file mode 100644 index 000000000..3db4eaf38 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Program.cs @@ -0,0 +1,41 @@ +using AccountOwnerServer.Extensions; +using Microsoft.AspNetCore.HttpOverrides; +using NLog.Web; + +var builder = WebApplication.CreateBuilder(args); + +// NLog is registered as a logging provider, so ILogger reaches the file targets in +// nlog.config as well as ILoggerManager. NLog finds nlog.config in the output directory +// on its own; there is nothing to load by hand. +builder.Logging.ClearProviders(); +builder.Host.UseNLog(); + +builder.Services.ConfigureCors(); +builder.Services.ConfigureLoggerService(); +builder.Services.ConfigureSqlContext(builder.Configuration); +builder.Services.ConfigureRepositoryWrapper(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +// UseForwardedHeaders goes first: every component after it has to see the client's +// scheme and address, not the proxy's, or an HTTPS redirect behind a proxy loops. +app.UseForwardedHeaders(new ForwardedHeadersOptions +{ + ForwardedHeaders = ForwardedHeaders.All +}); + +if (!app.Environment.IsDevelopment()) + app.UseHsts(); + +app.UseHttpsRedirection(); + +app.UseCors("CorsPolicy"); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Properties/launchSettings.json b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Properties/launchSettings.json new file mode 100644 index 000000000..c2d94ddfd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/WeatherForecast.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/WeatherForecast.cs new file mode 100644 index 000000000..41db3296b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace AccountOwnerServer +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} \ No newline at end of file diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/appsettings.Development.json b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/appsettings.json b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/appsettings.json new file mode 100644 index 000000000..939dc1a95 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "SqlConnection": "Server=(localdb)\\MSSQLLocalDB;Database=AccountOwner;Trusted_Connection=True;TrustServerCertificate=True;" + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/nlog.config b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/nlog.config new file mode 100644 index 000000000..225d1ebc2 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/AccountOwnerServer/nlog.config @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/Contracts.csproj b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/Contracts.csproj new file mode 100644 index 000000000..6633e14a8 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/Contracts.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IAccountRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IAccountRepository.cs new file mode 100644 index 000000000..4286b58ad --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IAccountRepository.cs @@ -0,0 +1,8 @@ +using Entities.Models; + +namespace Contracts +{ + public interface IAccountRepository : IRepositoryBase + { + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/ILoggerManager.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/ILoggerManager.cs new file mode 100644 index 000000000..ee1f08105 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/ILoggerManager.cs @@ -0,0 +1,10 @@ +namespace Contracts +{ + public interface ILoggerManager + { + void LogInfo(string message); + void LogWarn(string message); + void LogDebug(string message); + void LogError(string message); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IOwnerRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IOwnerRepository.cs new file mode 100644 index 000000000..0aa0d07b9 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IOwnerRepository.cs @@ -0,0 +1,8 @@ +using Entities.Models; + +namespace Contracts +{ + public interface IOwnerRepository : IRepositoryBase + { + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IRepositoryBase.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IRepositoryBase.cs new file mode 100644 index 000000000..7a2b84ba3 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IRepositoryBase.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Contracts +{ + public interface IRepositoryBase + { + IQueryable FindAll(); + IQueryable FindByCondition(Expression> expression); + void Create(T entity); + void Update(T entity); + void Delete(T entity); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IRepositoryWrapper.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IRepositoryWrapper.cs new file mode 100644 index 000000000..390b08260 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Contracts/IRepositoryWrapper.cs @@ -0,0 +1,9 @@ +namespace Contracts +{ + public interface IRepositoryWrapper + { + IOwnerRepository Owner { get; } + IAccountRepository Account { get; } + Task SaveAsync(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Entities.csproj b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Entities.csproj new file mode 100644 index 000000000..3d1cecaf0 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Entities.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Models/Account.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Models/Account.cs new file mode 100644 index 000000000..b2393b53e --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Models/Account.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Entities.Models +{ + [Table("Account")] + public class Account + { + public Guid AccountId { get; set; } + + [Required(ErrorMessage = "Date created is required")] + public DateTime DateCreated { get; set; } + + [Required(ErrorMessage = "Account type is required")] + public string? AccountType { get; set; } + + [ForeignKey(nameof(Owner))] + public Guid OwnerId { get; set; } + public Owner? Owner { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Models/Owner.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Models/Owner.cs new file mode 100644 index 000000000..298b8a4b2 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/Models/Owner.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Entities.Models +{ + [Table("Owner")] + public class Owner + { + public Guid OwnerId { get; set; } + + [Required(ErrorMessage = "Name is required")] + [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")] + public string? Name { get; set; } + + [Required(ErrorMessage = "Date of birth is required")] + public DateTime DateOfBirth { get; set; } + + [Required(ErrorMessage = "Address is required")] + [StringLength(100, ErrorMessage = "Address cannot be longer than 100 characters")] + public string? Address { get; set; } + + public ICollection? Accounts { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/RepositoryContext.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/RepositoryContext.cs new file mode 100644 index 000000000..9dfb78e02 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Entities/RepositoryContext.cs @@ -0,0 +1,19 @@ +using Entities.Models; +using Microsoft.EntityFrameworkCore; + +namespace Entities +{ + public class RepositoryContext : DbContext + { + // DbContextOptions, not the non-generic DbContextOptions: + // that is what AddDbContext supplies, and the non-generic form + // fails the moment a second DbContext is registered. + public RepositoryContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Owners => Set(); + public DbSet Accounts => Set(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/LoggerService/LoggerManager.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/LoggerService/LoggerManager.cs new file mode 100644 index 000000000..d776ec860 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/LoggerService/LoggerManager.cs @@ -0,0 +1,18 @@ +using Contracts; +using NLog; + +namespace LoggerService +{ + public class LoggerManager : ILoggerManager + { + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + + public void LogDebug(string message) => Logger.Debug(message); + + public void LogError(string message) => Logger.Error(message); + + public void LogInfo(string message) => Logger.Info(message); + + public void LogWarn(string message) => Logger.Warn(message); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/LoggerService/LoggerService.csproj b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/LoggerService/LoggerService.csproj new file mode 100644 index 000000000..049803efd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/LoggerService/LoggerService.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/AccountRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/AccountRepository.cs new file mode 100644 index 000000000..a5efb8893 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/AccountRepository.cs @@ -0,0 +1,14 @@ +using Contracts; +using Entities; +using Entities.Models; + +namespace Repository +{ + public class AccountRepository : RepositoryBase, IAccountRepository + { + public AccountRepository(RepositoryContext repositoryContext) + : base(repositoryContext) + { + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/OwnerRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/OwnerRepository.cs new file mode 100644 index 000000000..a00c37d3d --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/OwnerRepository.cs @@ -0,0 +1,14 @@ +using Contracts; +using Entities; +using Entities.Models; + +namespace Repository +{ + public class OwnerRepository : RepositoryBase, IOwnerRepository + { + public OwnerRepository(RepositoryContext repositoryContext) + : base(repositoryContext) + { + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/Repository.csproj b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/Repository.csproj new file mode 100644 index 000000000..a2d04de58 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/Repository.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/RepositoryBase.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/RepositoryBase.cs new file mode 100644 index 000000000..2da56ec38 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/RepositoryBase.cs @@ -0,0 +1,27 @@ +using Contracts; +using Entities; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace Repository +{ + public abstract class RepositoryBase : IRepositoryBase where T : class + { + protected RepositoryContext RepositoryContext { get; set; } + public RepositoryBase(RepositoryContext repositoryContext) + { + RepositoryContext = repositoryContext; + } + + public IQueryable FindAll() => RepositoryContext.Set().AsNoTracking(); + + public IQueryable FindByCondition(Expression> expression) => + RepositoryContext.Set().Where(expression).AsNoTracking(); + + public void Create(T entity) => RepositoryContext.Set().Add(entity); + + public void Update(T entity) => RepositoryContext.Set().Update(entity); + + public void Delete(T entity) => RepositoryContext.Set().Remove(entity); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/RepositoryWrapper.cs b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/RepositoryWrapper.cs new file mode 100644 index 000000000..05b1d936d --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/RepositoryPatternWithEfCore/Repository/RepositoryWrapper.cs @@ -0,0 +1,18 @@ +using Contracts; +using Entities; + +namespace Repository +{ + public class RepositoryWrapper(RepositoryContext repositoryContext) : IRepositoryWrapper + { + private readonly RepositoryContext _repoContext = repositoryContext; + private IOwnerRepository? _owner; + private IAccountRepository? _account; + + public IOwnerRepository Owner => _owner ??= new OwnerRepository(_repoContext); + + public IAccountRepository Account => _account ??= new AccountRepository(_repoContext); + + public async Task SaveAsync() => await _repoContext.SaveChangesAsync(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer.sln b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer.sln new file mode 100644 index 000000000..0d0800916 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32210.238 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountOwnerServer", "AccountOwnerServer\AccountOwnerServer.csproj", "{0213FB50-5C53-4C34-8B2C-E515C7DC2579}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F8E1F895-37AB-455B-8D05-ED495BE48F88} + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/AccountOwnerServer.csproj b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/AccountOwnerServer.csproj new file mode 100644 index 000000000..a3a34b647 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/AccountOwnerServer.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Controllers/WeatherForecastController.cs b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..bc1f98351 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Controllers/WeatherForecastController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; + +namespace AccountOwnerServer.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateTime.Now.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} \ No newline at end of file diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Extensions/ServiceExtensions.cs b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Extensions/ServiceExtensions.cs new file mode 100644 index 000000000..4413bd1bb --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Extensions/ServiceExtensions.cs @@ -0,0 +1,16 @@ +namespace AccountOwnerServer.Extensions +{ + public static class ServiceExtensions + { + public static void ConfigureCors(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + }); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Program.cs b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Program.cs new file mode 100644 index 000000000..bc219f6ad --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Program.cs @@ -0,0 +1,32 @@ +using AccountOwnerServer.Extensions; +using Microsoft.AspNetCore.HttpOverrides; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.ConfigureCors(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +// UseForwardedHeaders goes first: every component after it has to see the client's +// scheme and address, not the proxy's, or an HTTPS redirect behind a proxy loops. +app.UseForwardedHeaders(new ForwardedHeadersOptions +{ + ForwardedHeaders = ForwardedHeaders.All +}); + +if (!app.Environment.IsDevelopment()) + app.UseHsts(); + +app.UseHttpsRedirection(); + +app.UseCors("CorsPolicy"); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Properties/launchSettings.json b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Properties/launchSettings.json new file mode 100644 index 000000000..c2d94ddfd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/WeatherForecast.cs b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/WeatherForecast.cs new file mode 100644 index 000000000..41db3296b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace AccountOwnerServer +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} \ No newline at end of file diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/appsettings.Development.json b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/appsettings.json b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/ServiceConfiguration/AccountOwnerServer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer.sln b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer.sln new file mode 100644 index 000000000..78fe05931 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32210.238 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccountOwnerServer", "AccountOwnerServer\AccountOwnerServer.csproj", "{0213FB50-5C53-4C34-8B2C-E515C7DC2579}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contracts", "Contracts\Contracts.csproj", "{B731D3C2-9A89-40E4-B96B-AA300B4195BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggerService", "LoggerService\LoggerService.csproj", "{7CEC76F5-80AF-4B37-B6F1-94A743E5C748}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Entities", "Entities\Entities.csproj", "{16AAE663-A20E-493D-9087-F4F4016F6216}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "Repository\Repository.csproj", "{A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.Build.0 = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|Any CPU.Build.0 = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|Any CPU.Build.0 = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|Any CPU.Build.0 = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F8E1F895-37AB-455B-8D05-ED495BE48F88} + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/AccountOwnerServer.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/AccountOwnerServer.csproj new file mode 100644 index 000000000..9a34815aa --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/AccountOwnerServer.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Controllers/OwnerController.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Controllers/OwnerController.cs new file mode 100644 index 000000000..c99f87fdf --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Controllers/OwnerController.cs @@ -0,0 +1,67 @@ +using AutoMapper; +using Contracts; +using Entities.DataTransferObjects; +using Microsoft.AspNetCore.Mvc; + +namespace AccountOwnerServer.Controllers +{ + [Route("api/owner")] + [ApiController] + public class OwnerController : ControllerBase + { + private readonly ILoggerManager _logger; + private readonly IRepositoryWrapper _repository; + private readonly IMapper _mapper; + + public OwnerController(ILoggerManager logger, IRepositoryWrapper repository, IMapper mapper) + { + _logger = logger; + _repository = repository; + _mapper = mapper; + } + + // No try/catch anywhere in this controller. Exceptions are handled once, by the + // IExceptionHandler registered in Program.cs. + [HttpGet] + public async Task GetAllOwners() + { + var owners = await _repository.Owner.GetAllOwnersAsync(); + _logger.LogInfo("Returned all owners from database."); + + var ownersResult = _mapper.Map>(owners); + return Ok(ownersResult); + } + + [HttpGet("{id}")] + public async Task GetOwnerById(Guid id) + { + var owner = await _repository.Owner.GetOwnerByIdAsync(id); + if (owner is null) + { + _logger.LogError($"Owner with id: {id}, hasn't been found in db."); + return NotFound(); + } + + _logger.LogInfo($"Returned owner with id: {id}"); + + var ownerResult = _mapper.Map(owner); + return Ok(ownerResult); + } + + [HttpGet("{id}/account")] + public async Task GetOwnerWithDetails(Guid id) + { + var owner = await _repository.Owner.GetOwnerWithDetailsAsync(id); + if (owner is null) + { + _logger.LogError($"Owner with id: {id}, hasn't been found in db."); + return NotFound(); + } + + _logger.LogInfo($"Returned owner with details for id: {id}"); + + var ownerResult = _mapper.Map(owner); + return Ok(ownerResult); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Errors/GlobalExceptionHandler.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Errors/GlobalExceptionHandler.cs new file mode 100644 index 000000000..01300562b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Errors/GlobalExceptionHandler.cs @@ -0,0 +1,29 @@ +using Contracts; +using Microsoft.AspNetCore.Diagnostics; + +namespace AccountOwnerServer.Errors +{ + // Registered once in Program.cs. Returning true is what stops the exception + // propagating; a handler that returns false falls through to the next one. + public class GlobalExceptionHandler(ILoggerManager logger) : IExceptionHandler + { + private readonly ILoggerManager _logger = logger; + + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + _logger.LogError($"Something went wrong: {exception.Message}"); + + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + httpContext.Response.ContentType = "application/json"; + + await httpContext.Response.WriteAsJsonAsync( + new { statusCode = httpContext.Response.StatusCode, message = "Internal Server Error" }, + cancellationToken); + + return true; + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Extensions/ServiceExtensions.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Extensions/ServiceExtensions.cs new file mode 100644 index 000000000..4933589f5 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Extensions/ServiceExtensions.cs @@ -0,0 +1,39 @@ +using Contracts; +using LoggerService; +using Microsoft.EntityFrameworkCore; +using Entities; +using Repository; + +namespace AccountOwnerServer.Extensions +{ + public static class ServiceExtensions + { + public static void ConfigureCors(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + }); + } + + public static void ConfigureLoggerService(this IServiceCollection services) + { + services.AddSingleton(); + } + + public static void ConfigureSqlContext(this IServiceCollection services, IConfiguration config) + { + var connectionString = config.GetConnectionString("SqlConnection"); + + services.AddDbContext(o => o.UseSqlServer(connectionString)); + } + + public static void ConfigureRepositoryWrapper(this IServiceCollection services) + { + services.AddScoped(); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/MappingProfile.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/MappingProfile.cs new file mode 100644 index 000000000..41521613a --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/MappingProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Entities.DataTransferObjects; +using Entities.Models; +using System.Linq; + +namespace AccountOwnerServer +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap(); + + CreateMap(); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Program.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Program.cs new file mode 100644 index 000000000..eee97e56b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Program.cs @@ -0,0 +1,49 @@ +using AccountOwnerServer.Errors; +using AccountOwnerServer.Extensions; +using Microsoft.AspNetCore.HttpOverrides; +using NLog.Web; + +var builder = WebApplication.CreateBuilder(args); + +// NLog is registered as a logging provider, so ILogger reaches the file targets in +// nlog.config as well as ILoggerManager. NLog finds nlog.config in the output directory +// on its own; there is nothing to load by hand. +builder.Logging.ClearProviders(); +builder.Host.UseNLog(); + +builder.Services.ConfigureCors(); +builder.Services.ConfigureLoggerService(); +builder.Services.ConfigureSqlContext(builder.Configuration); +builder.Services.ConfigureRepositoryWrapper(); +builder.Services.AddAutoMapper(typeof(Program)); + +// Exceptions are handled here, once, instead of in a try/catch inside every action. +builder.Services.AddExceptionHandler(); +builder.Services.AddProblemDetails(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +// UseForwardedHeaders goes first: every component after it has to see the client's +// scheme and address, not the proxy's, or an HTTPS redirect behind a proxy loops. +app.UseForwardedHeaders(new ForwardedHeadersOptions +{ + ForwardedHeaders = ForwardedHeaders.All +}); + +if (!app.Environment.IsDevelopment()) + app.UseHsts(); + +app.UseHttpsRedirection(); + +app.UseExceptionHandler(); + +app.UseCors("CorsPolicy"); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Properties/launchSettings.json b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Properties/launchSettings.json new file mode 100644 index 000000000..c2d94ddfd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/appsettings.Development.json b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/appsettings.json b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/appsettings.json new file mode 100644 index 000000000..939dc1a95 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "SqlConnection": "Server=(localdb)\\MSSQLLocalDB;Database=AccountOwner;Trusted_Connection=True;TrustServerCertificate=True;" + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/nlog.config b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/nlog.config new file mode 100644 index 000000000..225d1ebc2 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/AccountOwnerServer/nlog.config @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/Contracts.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/Contracts.csproj new file mode 100644 index 000000000..6633e14a8 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/Contracts.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IAccountRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IAccountRepository.cs new file mode 100644 index 000000000..825a4fba9 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IAccountRepository.cs @@ -0,0 +1,8 @@ +using Entities.Models; + +namespace Contracts +{ + public interface IAccountRepository + { + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/ILoggerManager.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/ILoggerManager.cs new file mode 100644 index 000000000..ee1f08105 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/ILoggerManager.cs @@ -0,0 +1,10 @@ +namespace Contracts +{ + public interface ILoggerManager + { + void LogInfo(string message); + void LogWarn(string message); + void LogDebug(string message); + void LogError(string message); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IOwnerRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IOwnerRepository.cs new file mode 100644 index 000000000..2ca65554b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IOwnerRepository.cs @@ -0,0 +1,11 @@ +using Entities.Models; + +namespace Contracts +{ + public interface IOwnerRepository + { + Task> GetAllOwnersAsync(); + Task GetOwnerByIdAsync(Guid ownerId); + Task GetOwnerWithDetailsAsync(Guid ownerId); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IRepositoryBase.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IRepositoryBase.cs new file mode 100644 index 000000000..7a2b84ba3 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IRepositoryBase.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Contracts +{ + public interface IRepositoryBase + { + IQueryable FindAll(); + IQueryable FindByCondition(Expression> expression); + void Create(T entity); + void Update(T entity); + void Delete(T entity); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IRepositoryWrapper.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IRepositoryWrapper.cs new file mode 100644 index 000000000..390b08260 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Contracts/IRepositoryWrapper.cs @@ -0,0 +1,9 @@ +namespace Contracts +{ + public interface IRepositoryWrapper + { + IOwnerRepository Owner { get; } + IAccountRepository Account { get; } + Task SaveAsync(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/DataTransferObjects/AccountDto.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/DataTransferObjects/AccountDto.cs new file mode 100644 index 000000000..7e01c9850 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/DataTransferObjects/AccountDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace Entities.DataTransferObjects +{ + public class AccountDto + { + public Guid Id { get; set; } + public DateTime DateCreated { get; set; } + public string? AccountType { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/DataTransferObjects/OwnerDto.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/DataTransferObjects/OwnerDto.cs new file mode 100644 index 000000000..1e8fb8e01 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/DataTransferObjects/OwnerDto.cs @@ -0,0 +1,12 @@ +namespace Entities.DataTransferObjects +{ + public class OwnerDto + { + public Guid Id { get; set; } + public string? Name { get; set; } + public DateTime DateOfBirth { get; set; } + public string? Address { get; set; } + + public IEnumerable? Accounts { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Entities.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Entities.csproj new file mode 100644 index 000000000..3d1cecaf0 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Entities.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Models/Account.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Models/Account.cs new file mode 100644 index 000000000..e17b19892 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Models/Account.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Entities.Models +{ + [Table("Account")] + public class Account + { + [Column("AccountId")] + public Guid Id { get; set; } + + [Required(ErrorMessage = "Date created is required")] + public DateTime DateCreated { get; set; } + + [Required(ErrorMessage = "Account type is required")] + public string? AccountType { get; set; } + + [ForeignKey(nameof(Owner))] + public Guid OwnerId { get; set; } + public Owner? Owner { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Models/Owner.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Models/Owner.cs new file mode 100644 index 000000000..1dada77ab --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/Models/Owner.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Entities.Models +{ + [Table("Owner")] + public class Owner + { + [Column("OwnerId")] + public Guid Id { get; set; } + + [Required(ErrorMessage = "Name is required")] + [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")] + public string? Name { get; set; } + + [Required(ErrorMessage = "Date of birth is required")] + public DateTime DateOfBirth { get; set; } + + [Required(ErrorMessage = "Address is required")] + [StringLength(100, ErrorMessage = "Address cannot be longer than 100 characters")] + public string? Address { get; set; } + + public ICollection? Accounts { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/RepositoryContext.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/RepositoryContext.cs new file mode 100644 index 000000000..9dfb78e02 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Entities/RepositoryContext.cs @@ -0,0 +1,19 @@ +using Entities.Models; +using Microsoft.EntityFrameworkCore; + +namespace Entities +{ + public class RepositoryContext : DbContext + { + // DbContextOptions, not the non-generic DbContextOptions: + // that is what AddDbContext supplies, and the non-generic form + // fails the moment a second DbContext is registered. + public RepositoryContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Owners => Set(); + public DbSet Accounts => Set(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/LoggerService/LoggerManager.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/LoggerService/LoggerManager.cs new file mode 100644 index 000000000..d776ec860 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/LoggerService/LoggerManager.cs @@ -0,0 +1,18 @@ +using Contracts; +using NLog; + +namespace LoggerService +{ + public class LoggerManager : ILoggerManager + { + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + + public void LogDebug(string message) => Logger.Debug(message); + + public void LogError(string message) => Logger.Error(message); + + public void LogInfo(string message) => Logger.Info(message); + + public void LogWarn(string message) => Logger.Warn(message); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/LoggerService/LoggerService.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/LoggerService/LoggerService.csproj new file mode 100644 index 000000000..049803efd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/LoggerService/LoggerService.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/AccountRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/AccountRepository.cs new file mode 100644 index 000000000..a5efb8893 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/AccountRepository.cs @@ -0,0 +1,14 @@ +using Contracts; +using Entities; +using Entities.Models; + +namespace Repository +{ + public class AccountRepository : RepositoryBase, IAccountRepository + { + public AccountRepository(RepositoryContext repositoryContext) + : base(repositoryContext) + { + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/OwnerRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/OwnerRepository.cs new file mode 100644 index 000000000..54d6b2fe1 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/OwnerRepository.cs @@ -0,0 +1,31 @@ +using Contracts; +using Entities; +using Entities.Models; +using Microsoft.EntityFrameworkCore; + +namespace Repository +{ + public class OwnerRepository : RepositoryBase, IOwnerRepository + { + public OwnerRepository(RepositoryContext repositoryContext) + : base(repositoryContext) + { + } + + // FindAll and FindByCondition stay synchronous: they compose a query, they do + // not execute one. The awaited call is the one that executes. + public async Task> GetAllOwnersAsync() => + await FindAll() + .OrderBy(ow => ow.Name) + .ToListAsync(); + + public async Task GetOwnerByIdAsync(Guid ownerId) => + await FindByCondition(owner => owner.Id.Equals(ownerId)) + .FirstOrDefaultAsync(); + + public async Task GetOwnerWithDetailsAsync(Guid ownerId) => + await FindByCondition(owner => owner.Id.Equals(ownerId)) + .Include(ac => ac.Accounts) + .FirstOrDefaultAsync(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/Repository.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/Repository.csproj new file mode 100644 index 000000000..a2d04de58 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/Repository.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/RepositoryBase.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/RepositoryBase.cs new file mode 100644 index 000000000..2da56ec38 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/RepositoryBase.cs @@ -0,0 +1,27 @@ +using Contracts; +using Entities; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace Repository +{ + public abstract class RepositoryBase : IRepositoryBase where T : class + { + protected RepositoryContext RepositoryContext { get; set; } + public RepositoryBase(RepositoryContext repositoryContext) + { + RepositoryContext = repositoryContext; + } + + public IQueryable FindAll() => RepositoryContext.Set().AsNoTracking(); + + public IQueryable FindByCondition(Expression> expression) => + RepositoryContext.Set().Where(expression).AsNoTracking(); + + public void Create(T entity) => RepositoryContext.Set().Add(entity); + + public void Update(T entity) => RepositoryContext.Set().Update(entity); + + public void Delete(T entity) => RepositoryContext.Set().Remove(entity); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/RepositoryWrapper.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/RepositoryWrapper.cs new file mode 100644 index 000000000..05b1d936d --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForGetRequests/Repository/RepositoryWrapper.cs @@ -0,0 +1,18 @@ +using Contracts; +using Entities; + +namespace Repository +{ + public class RepositoryWrapper(RepositoryContext repositoryContext) : IRepositoryWrapper + { + private readonly RepositoryContext _repoContext = repositoryContext; + private IOwnerRepository? _owner; + private IAccountRepository? _account; + + public IOwnerRepository Owner => _owner ??= new OwnerRepository(_repoContext); + + public IAccountRepository Account => _account ??= new AccountRepository(_repoContext); + + public async Task SaveAsync() => await _repoContext.SaveChangesAsync(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/AccountOwnerServer.Tests.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/AccountOwnerServer.Tests.csproj new file mode 100644 index 000000000..4f4d7c336 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/AccountOwnerServer.Tests.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/DockerFactAttribute.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/DockerFactAttribute.cs new file mode 100644 index 000000000..2e1e07874 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/DockerFactAttribute.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; + +namespace AccountOwnerServer.Tests +{ + // These tests need a SQL Server container. Where Docker is not available they report + // as skipped with a reason, never as passed: a green run that silently tested nothing + // is worse than a red one. + public sealed class DockerFactAttribute : FactAttribute + { + public DockerFactAttribute() + { + if (!DockerEnvironment.IsAvailable.Value) + { + Skip = "Docker is not available here, so the SQL Server container cannot start."; + } + } + } + + internal static class DockerEnvironment + { + public static readonly Lazy IsAvailable = new(Probe); + + private static bool Probe() + { + try + { + using var process = Process.Start(new ProcessStartInfo("docker", "version --format {{.Server.Version}}") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }); + + if (process is null) + { + return false; + } + + return process.WaitForExit(milliseconds: 20_000) && process.ExitCode == 0; + } + catch + { + return false; + } + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/GlobalUsings.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/GlobalUsings.cs new file mode 100644 index 000000000..c802f4480 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/OwnerRepositoryTests.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/OwnerRepositoryTests.cs new file mode 100644 index 000000000..66e35cede --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/OwnerRepositoryTests.cs @@ -0,0 +1,75 @@ +using Entities.Models; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; + +namespace AccountOwnerServer.Tests +{ + public class OwnerRepositoryTests(SqlServerFixture fixture) : IClassFixture + { + private readonly SqlServerFixture _fixture = fixture; + + [DockerFact] + public async Task WhenGetAllOwnersAsyncIsCalled_ThenItReturnsTheSeededOwnersByName() + { + var repository = _fixture.CreateWrapper(); + + var owners = (await repository.Owner.GetAllOwnersAsync()).ToList(); + + Assert.Equal(4, owners.Count); + Assert.Equal(["Anna Bosh", "John Keen", "Martin Miller", "Sam Query"], owners.Select(o => o.Name)); + } + + [DockerFact] + public async Task WhenGetOwnerWithDetailsAsyncIsCalled_ThenItIncludesTheAccounts() + { + var repository = _fixture.CreateWrapper(); + var johnKeen = Guid.Parse("24fd81f8-d58a-4bcc-9f35-dc6cd5641906"); + + var owner = await repository.Owner.GetOwnerWithDetailsAsync(johnKeen); + + Assert.NotNull(owner); + Assert.Equal(3, owner.Accounts!.Count); + } + + [DockerFact] + public async Task WhenAnOwnerIsCreated_ThenSaveAsyncPersistsIt() + { + var repository = _fixture.CreateWrapper(); + var owner = new Owner + { + Id = Guid.NewGuid(), + Name = "Ada Lovelace", + DateOfBirth = new DateTime(1815, 12, 10), + Address = "12 Analytical Engine Way" + }; + + repository.Owner.CreateOwner(owner); + await repository.SaveAsync(); + + var reloaded = await _fixture.CreateWrapper().Owner.GetOwnerByIdAsync(owner.Id); + Assert.NotNull(reloaded); + Assert.Equal("Ada Lovelace", reloaded.Name); + + // Leave the seeded data as we found it for the other tests. + var cleanup = _fixture.CreateWrapper(); + var toDelete = await cleanup.Owner.GetOwnerByIdAsync(owner.Id); + cleanup.Owner.DeleteOwner(toDelete!); + await cleanup.SaveAsync(); + } + + [DockerFact] + public async Task WhenAnOwnerWithAccountsIsDeleted_ThenTheForeignKeyRejectsIt() + { + var repository = _fixture.CreateWrapper(); + var johnKeen = Guid.Parse("24fd81f8-d58a-4bcc-9f35-dc6cd5641906"); + + var owner = await repository.Owner.GetOwnerByIdAsync(johnKeen); + repository.Owner.DeleteOwner(owner!); + + var exception = await Assert.ThrowsAsync(repository.SaveAsync); + + var sqlException = Assert.IsType(exception.InnerException); + Assert.Equal(547, sqlException.Number); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/SqlServerFixture.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/SqlServerFixture.cs new file mode 100644 index 000000000..1783eb296 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.Tests/SqlServerFixture.cs @@ -0,0 +1,57 @@ +using Entities; +using Microsoft.EntityFrameworkCore; +using Repository; +using Testcontainers.MsSql; + +namespace AccountOwnerServer.Tests +{ + // Starts one SQL Server container for the whole test class and creates the schema by + // running the same init.sql part 1 ships. Nothing here calls EnsureCreated: the script + // is the only thing that creates the schema, in the tests as in the article. + public class SqlServerFixture : IAsyncLifetime + { + private MsSqlContainer? _container; + + public string ConnectionString { get; private set; } = string.Empty; + + public async Task InitializeAsync() + { + if (!DockerEnvironment.IsAvailable.Value) + { + return; + } + + _container = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest").Build(); + await _container.StartAsync(); + + var script = await File.ReadAllTextAsync("init.sql"); + var result = await _container.ExecScriptAsync(script); + if (result.ExitCode != 0) + { + throw new InvalidOperationException($"init.sql failed: {result.Stderr}"); + } + + ConnectionString = _container.GetConnectionString() + .Replace("Database=master", "Database=AccountOwner"); + } + + public async Task DisposeAsync() + { + if (_container is not null) + { + await _container.DisposeAsync(); + } + } + + public RepositoryContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseSqlServer(ConnectionString) + .Options; + + return new RepositoryContext(options); + } + + public RepositoryWrapper CreateWrapper() => new(CreateContext()); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.sln b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.sln new file mode 100644 index 000000000..5940951cc --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer.sln @@ -0,0 +1,107 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32210.238 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AccountOwnerServer", "AccountOwnerServer\AccountOwnerServer.csproj", "{0213FB50-5C53-4C34-8B2C-E515C7DC2579}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contracts", "Contracts\Contracts.csproj", "{B731D3C2-9A89-40E4-B96B-AA300B4195BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoggerService", "LoggerService\LoggerService.csproj", "{7CEC76F5-80AF-4B37-B6F1-94A743E5C748}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Entities", "Entities\Entities.csproj", "{16AAE663-A20E-493D-9087-F4F4016F6216}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "Repository\Repository.csproj", "{A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AccountOwnerServer.Tests", "AccountOwnerServer.Tests\AccountOwnerServer.Tests.csproj", "{EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}" +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 + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|x64.ActiveCfg = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|x64.Build.0 = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|x86.ActiveCfg = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Debug|x86.Build.0 = Debug|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|Any CPU.Build.0 = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|x64.ActiveCfg = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|x64.Build.0 = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|x86.ActiveCfg = Release|Any CPU + {0213FB50-5C53-4C34-8B2C-E515C7DC2579}.Release|x86.Build.0 = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|x64.ActiveCfg = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|x64.Build.0 = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|x86.ActiveCfg = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Debug|x86.Build.0 = Debug|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|Any CPU.Build.0 = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|x64.ActiveCfg = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|x64.Build.0 = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|x86.ActiveCfg = Release|Any CPU + {B731D3C2-9A89-40E4-B96B-AA300B4195BB}.Release|x86.Build.0 = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|x64.ActiveCfg = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|x64.Build.0 = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|x86.ActiveCfg = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Debug|x86.Build.0 = Debug|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|Any CPU.Build.0 = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|x64.ActiveCfg = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|x64.Build.0 = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|x86.ActiveCfg = Release|Any CPU + {7CEC76F5-80AF-4B37-B6F1-94A743E5C748}.Release|x86.Build.0 = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|x64.ActiveCfg = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|x64.Build.0 = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|x86.ActiveCfg = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Debug|x86.Build.0 = Debug|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|Any CPU.Build.0 = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|x64.ActiveCfg = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|x64.Build.0 = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|x86.ActiveCfg = Release|Any CPU + {16AAE663-A20E-493D-9087-F4F4016F6216}.Release|x86.Build.0 = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|x64.ActiveCfg = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|x64.Build.0 = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|x86.ActiveCfg = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Debug|x86.Build.0 = Debug|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|Any CPU.Build.0 = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|x64.ActiveCfg = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|x64.Build.0 = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|x86.ActiveCfg = Release|Any CPU + {A0C0D726-5224-4B8F-8C05-BB2C12A78D9B}.Release|x86.Build.0 = Release|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Debug|x64.ActiveCfg = Debug|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Debug|x64.Build.0 = Debug|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Debug|x86.ActiveCfg = Debug|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Debug|x86.Build.0 = Debug|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Release|Any CPU.Build.0 = Release|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Release|x64.ActiveCfg = Release|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Release|x64.Build.0 = Release|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Release|x86.ActiveCfg = Release|Any CPU + {EEE4FB17-4B8F-45A0-9FC0-0AAA91E12EA9}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F8E1F895-37AB-455B-8D05-ED495BE48F88} + EndGlobalSection +EndGlobal diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/AccountOwnerServer.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/AccountOwnerServer.csproj new file mode 100644 index 000000000..9a34815aa --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/AccountOwnerServer.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Controllers/OwnerController.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Controllers/OwnerController.cs new file mode 100644 index 000000000..00d68a962 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Controllers/OwnerController.cs @@ -0,0 +1,130 @@ +using AutoMapper; +using Contracts; +using Entities.DataTransferObjects; +using Entities.Models; +using Microsoft.AspNetCore.Mvc; + +namespace AccountOwnerServer.Controllers +{ + [Route("api/owner")] + [ApiController] + public class OwnerController : ControllerBase + { + private readonly ILoggerManager _logger; + private readonly IRepositoryWrapper _repository; + private readonly IMapper _mapper; + + public OwnerController(ILoggerManager logger, IRepositoryWrapper repository, IMapper mapper) + { + _logger = logger; + _repository = repository; + _mapper = mapper; + } + + // No try/catch and no model-state guards. [ApiController] rejects an empty or + // invalid body with a 400 and a ValidationProblemDetails before the action runs, + // and everything else is handled by the IExceptionHandler in Program.cs. + [HttpGet] + public async Task GetAllOwners() + { + var owners = await _repository.Owner.GetAllOwnersAsync(); + _logger.LogInfo("Returned all owners from database."); + + var ownersResult = _mapper.Map>(owners); + return Ok(ownersResult); + } + + // The route is named because CreatedAtRoute looks it up by name to build the + // Location header. The name is OwnerById and it does not change with the method. + [HttpGet("{id}", Name = "OwnerById")] + public async Task GetOwnerById(Guid id) + { + var owner = await _repository.Owner.GetOwnerByIdAsync(id); + if (owner is null) + { + _logger.LogError($"Owner with id: {id}, hasn't been found in db."); + return NotFound(); + } + + _logger.LogInfo($"Returned owner with id: {id}"); + + var ownerResult = _mapper.Map(owner); + return Ok(ownerResult); + } + + [HttpGet("{id}/account")] + public async Task GetOwnerWithDetails(Guid id) + { + var owner = await _repository.Owner.GetOwnerWithDetailsAsync(id); + if (owner is null) + { + _logger.LogError($"Owner with id: {id}, hasn't been found in db."); + return NotFound(); + } + + _logger.LogInfo($"Returned owner with details for id: {id}"); + + var ownerResult = _mapper.Map(owner); + return Ok(ownerResult); + } + + [HttpPost] + public async Task CreateOwner([FromBody] OwnerForCreationDto owner) + { + var ownerEntity = _mapper.Map(owner); + + _repository.Owner.CreateOwner(ownerEntity); + await _repository.SaveAsync(); + + var createdOwner = _mapper.Map(ownerEntity); + + return CreatedAtRoute("OwnerById", new { id = createdOwner.Id }, createdOwner); + } + + [HttpPut("{id}")] + public async Task UpdateOwner(Guid id, [FromBody] OwnerForUpdateDto owner) + { + var ownerEntity = await _repository.Owner.GetOwnerByIdAsync(id); + if (ownerEntity is null) + { + _logger.LogError($"Owner with id: {id}, hasn't been found in db."); + return NotFound(); + } + + // Map onto the entity we just read: properties the DTO does not carry, the + // id and the accounts among them, keep the values they already have. + _mapper.Map(owner, ownerEntity); + + _repository.Owner.UpdateOwner(ownerEntity); + await _repository.SaveAsync(); + + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task DeleteOwner(Guid id) + { + var owner = await _repository.Owner.GetOwnerByIdAsync(id); + if (owner is null) + { + _logger.LogError($"Owner with id: {id}, hasn't been found in db."); + return NotFound(); + } + + // The pre-check buys a readable error message. The foreign key is the real + // guard: an account inserted between this check and the save makes the + // delete fail with a DbUpdateException wrapping SqlException 547. + var accounts = await _repository.Account.AccountsByOwnerAsync(id); + if (accounts.Any()) + { + _logger.LogError($"Cannot delete owner with id: {id}. It has related accounts."); + return BadRequest("Cannot delete owner. It has related accounts. Delete those accounts first"); + } + + _repository.Owner.DeleteOwner(owner); + await _repository.SaveAsync(); + + return NoContent(); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Errors/GlobalExceptionHandler.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Errors/GlobalExceptionHandler.cs new file mode 100644 index 000000000..01300562b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Errors/GlobalExceptionHandler.cs @@ -0,0 +1,29 @@ +using Contracts; +using Microsoft.AspNetCore.Diagnostics; + +namespace AccountOwnerServer.Errors +{ + // Registered once in Program.cs. Returning true is what stops the exception + // propagating; a handler that returns false falls through to the next one. + public class GlobalExceptionHandler(ILoggerManager logger) : IExceptionHandler + { + private readonly ILoggerManager _logger = logger; + + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + _logger.LogError($"Something went wrong: {exception.Message}"); + + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + httpContext.Response.ContentType = "application/json"; + + await httpContext.Response.WriteAsJsonAsync( + new { statusCode = httpContext.Response.StatusCode, message = "Internal Server Error" }, + cancellationToken); + + return true; + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Extensions/ServiceExtensions.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Extensions/ServiceExtensions.cs new file mode 100644 index 000000000..4933589f5 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Extensions/ServiceExtensions.cs @@ -0,0 +1,39 @@ +using Contracts; +using LoggerService; +using Microsoft.EntityFrameworkCore; +using Entities; +using Repository; + +namespace AccountOwnerServer.Extensions +{ + public static class ServiceExtensions + { + public static void ConfigureCors(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + }); + } + + public static void ConfigureLoggerService(this IServiceCollection services) + { + services.AddSingleton(); + } + + public static void ConfigureSqlContext(this IServiceCollection services, IConfiguration config) + { + var connectionString = config.GetConnectionString("SqlConnection"); + + services.AddDbContext(o => o.UseSqlServer(connectionString)); + } + + public static void ConfigureRepositoryWrapper(this IServiceCollection services) + { + services.AddScoped(); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/MappingProfile.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/MappingProfile.cs new file mode 100644 index 000000000..744334863 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/MappingProfile.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using Entities.DataTransferObjects; +using Entities.Models; +using System.Linq; + +namespace AccountOwnerServer +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap(); + + CreateMap(); + + CreateMap(); + + CreateMap(); + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Program.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Program.cs new file mode 100644 index 000000000..eee97e56b --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Program.cs @@ -0,0 +1,49 @@ +using AccountOwnerServer.Errors; +using AccountOwnerServer.Extensions; +using Microsoft.AspNetCore.HttpOverrides; +using NLog.Web; + +var builder = WebApplication.CreateBuilder(args); + +// NLog is registered as a logging provider, so ILogger reaches the file targets in +// nlog.config as well as ILoggerManager. NLog finds nlog.config in the output directory +// on its own; there is nothing to load by hand. +builder.Logging.ClearProviders(); +builder.Host.UseNLog(); + +builder.Services.ConfigureCors(); +builder.Services.ConfigureLoggerService(); +builder.Services.ConfigureSqlContext(builder.Configuration); +builder.Services.ConfigureRepositoryWrapper(); +builder.Services.AddAutoMapper(typeof(Program)); + +// Exceptions are handled here, once, instead of in a try/catch inside every action. +builder.Services.AddExceptionHandler(); +builder.Services.AddProblemDetails(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +// UseForwardedHeaders goes first: every component after it has to see the client's +// scheme and address, not the proxy's, or an HTTPS redirect behind a proxy loops. +app.UseForwardedHeaders(new ForwardedHeadersOptions +{ + ForwardedHeaders = ForwardedHeaders.All +}); + +if (!app.Environment.IsDevelopment()) + app.UseHsts(); + +app.UseHttpsRedirection(); + +app.UseExceptionHandler(); + +app.UseCors("CorsPolicy"); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Properties/launchSettings.json b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Properties/launchSettings.json new file mode 100644 index 000000000..c2d94ddfd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/appsettings.Development.json b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/appsettings.json b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/appsettings.json new file mode 100644 index 000000000..939dc1a95 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "SqlConnection": "Server=(localdb)\\MSSQLLocalDB;Database=AccountOwner;Trusted_Connection=True;TrustServerCertificate=True;" + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/nlog.config b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/nlog.config new file mode 100644 index 000000000..225d1ebc2 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/AccountOwnerServer/nlog.config @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/Contracts.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/Contracts.csproj new file mode 100644 index 000000000..6633e14a8 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/Contracts.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IAccountRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IAccountRepository.cs new file mode 100644 index 000000000..86473573c --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IAccountRepository.cs @@ -0,0 +1,9 @@ +using Entities.Models; + +namespace Contracts +{ + public interface IAccountRepository + { + Task> AccountsByOwnerAsync(Guid ownerId); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/ILoggerManager.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/ILoggerManager.cs new file mode 100644 index 000000000..ee1f08105 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/ILoggerManager.cs @@ -0,0 +1,10 @@ +namespace Contracts +{ + public interface ILoggerManager + { + void LogInfo(string message); + void LogWarn(string message); + void LogDebug(string message); + void LogError(string message); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IOwnerRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IOwnerRepository.cs new file mode 100644 index 000000000..5ba495962 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IOwnerRepository.cs @@ -0,0 +1,14 @@ +using Entities.Models; + +namespace Contracts +{ + public interface IOwnerRepository + { + Task> GetAllOwnersAsync(); + Task GetOwnerByIdAsync(Guid ownerId); + Task GetOwnerWithDetailsAsync(Guid ownerId); + void CreateOwner(Owner owner); + void UpdateOwner(Owner owner); + void DeleteOwner(Owner owner); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IRepositoryBase.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IRepositoryBase.cs new file mode 100644 index 000000000..7a2b84ba3 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IRepositoryBase.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace Contracts +{ + public interface IRepositoryBase + { + IQueryable FindAll(); + IQueryable FindByCondition(Expression> expression); + void Create(T entity); + void Update(T entity); + void Delete(T entity); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IRepositoryWrapper.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IRepositoryWrapper.cs new file mode 100644 index 000000000..390b08260 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Contracts/IRepositoryWrapper.cs @@ -0,0 +1,9 @@ +namespace Contracts +{ + public interface IRepositoryWrapper + { + IOwnerRepository Owner { get; } + IAccountRepository Account { get; } + Task SaveAsync(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/AccountDto.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/AccountDto.cs new file mode 100644 index 000000000..7e01c9850 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/AccountDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace Entities.DataTransferObjects +{ + public class AccountDto + { + public Guid Id { get; set; } + public DateTime DateCreated { get; set; } + public string? AccountType { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerDto.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerDto.cs new file mode 100644 index 000000000..1e8fb8e01 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerDto.cs @@ -0,0 +1,12 @@ +namespace Entities.DataTransferObjects +{ + public class OwnerDto + { + public Guid Id { get; set; } + public string? Name { get; set; } + public DateTime DateOfBirth { get; set; } + public string? Address { get; set; } + + public IEnumerable? Accounts { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerForCreationDto.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerForCreationDto.cs new file mode 100644 index 000000000..78ce53143 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerForCreationDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Entities.DataTransferObjects +{ + public class OwnerForCreationDto + { + [Required(ErrorMessage = "Name is required")] + [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")] + public string? Name { get; set; } + + [Required(ErrorMessage = "Date of birth is required")] + public DateTime DateOfBirth { get; set; } + + [Required(ErrorMessage = "Address is required")] + [StringLength(100, ErrorMessage = "Address cannot be longer than 100 characters")] + public string? Address { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerForUpdateDto.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerForUpdateDto.cs new file mode 100644 index 000000000..d22337d47 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/DataTransferObjects/OwnerForUpdateDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Entities.DataTransferObjects +{ + public class OwnerForUpdateDto + { + [Required(ErrorMessage = "Name is required")] + [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")] + public string? Name { get; set; } + + [Required(ErrorMessage = "Date of birth is required")] + public DateTime DateOfBirth { get; set; } + + [Required(ErrorMessage = "Address is required")] + [StringLength(100, ErrorMessage = "Address cannot be longer than 100 characters")] + public string? Address { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Entities.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Entities.csproj new file mode 100644 index 000000000..3d1cecaf0 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Entities.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Models/Account.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Models/Account.cs new file mode 100644 index 000000000..e17b19892 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Models/Account.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Entities.Models +{ + [Table("Account")] + public class Account + { + [Column("AccountId")] + public Guid Id { get; set; } + + [Required(ErrorMessage = "Date created is required")] + public DateTime DateCreated { get; set; } + + [Required(ErrorMessage = "Account type is required")] + public string? AccountType { get; set; } + + [ForeignKey(nameof(Owner))] + public Guid OwnerId { get; set; } + public Owner? Owner { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Models/Owner.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Models/Owner.cs new file mode 100644 index 000000000..1dada77ab --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/Models/Owner.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Entities.Models +{ + [Table("Owner")] + public class Owner + { + [Column("OwnerId")] + public Guid Id { get; set; } + + [Required(ErrorMessage = "Name is required")] + [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")] + public string? Name { get; set; } + + [Required(ErrorMessage = "Date of birth is required")] + public DateTime DateOfBirth { get; set; } + + [Required(ErrorMessage = "Address is required")] + [StringLength(100, ErrorMessage = "Address cannot be longer than 100 characters")] + public string? Address { get; set; } + + public ICollection? Accounts { get; set; } + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/RepositoryContext.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/RepositoryContext.cs new file mode 100644 index 000000000..9dfb78e02 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Entities/RepositoryContext.cs @@ -0,0 +1,19 @@ +using Entities.Models; +using Microsoft.EntityFrameworkCore; + +namespace Entities +{ + public class RepositoryContext : DbContext + { + // DbContextOptions, not the non-generic DbContextOptions: + // that is what AddDbContext supplies, and the non-generic form + // fails the moment a second DbContext is registered. + public RepositoryContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Owners => Set(); + public DbSet Accounts => Set(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/LoggerService/LoggerManager.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/LoggerService/LoggerManager.cs new file mode 100644 index 000000000..d776ec860 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/LoggerService/LoggerManager.cs @@ -0,0 +1,18 @@ +using Contracts; +using NLog; + +namespace LoggerService +{ + public class LoggerManager : ILoggerManager + { + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + + public void LogDebug(string message) => Logger.Debug(message); + + public void LogError(string message) => Logger.Error(message); + + public void LogInfo(string message) => Logger.Info(message); + + public void LogWarn(string message) => Logger.Warn(message); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/LoggerService/LoggerService.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/LoggerService/LoggerService.csproj new file mode 100644 index 000000000..049803efd --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/LoggerService/LoggerService.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/AccountRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/AccountRepository.cs new file mode 100644 index 000000000..30bff4532 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/AccountRepository.cs @@ -0,0 +1,18 @@ +using Contracts; +using Entities; +using Entities.Models; +using Microsoft.EntityFrameworkCore; + +namespace Repository +{ + public class AccountRepository : RepositoryBase, IAccountRepository + { + public AccountRepository(RepositoryContext repositoryContext) + : base(repositoryContext) + { + } + + public async Task> AccountsByOwnerAsync(Guid ownerId) => + await FindByCondition(a => a.OwnerId.Equals(ownerId)).ToListAsync(); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/OwnerRepository.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/OwnerRepository.cs new file mode 100644 index 000000000..ba5811589 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/OwnerRepository.cs @@ -0,0 +1,39 @@ +using Contracts; +using Entities; +using Entities.Models; +using Microsoft.EntityFrameworkCore; + +namespace Repository +{ + public class OwnerRepository : RepositoryBase, IOwnerRepository + { + public OwnerRepository(RepositoryContext repositoryContext) + : base(repositoryContext) + { + } + + // FindAll and FindByCondition stay synchronous: they compose a query, they do + // not execute one. The awaited call is the one that executes. + public async Task> GetAllOwnersAsync() => + await FindAll() + .OrderBy(ow => ow.Name) + .ToListAsync(); + + public async Task GetOwnerByIdAsync(Guid ownerId) => + await FindByCondition(owner => owner.Id.Equals(ownerId)) + .FirstOrDefaultAsync(); + + public async Task GetOwnerWithDetailsAsync(Guid ownerId) => + await FindByCondition(owner => owner.Id.Equals(ownerId)) + .Include(ac => ac.Accounts) + .FirstOrDefaultAsync(); + + // Create, Update and Delete only stage a change on the change tracker. There is + // nothing to await here; the awaited call is SaveAsync on the wrapper. + public void CreateOwner(Owner owner) => Create(owner); + + public void UpdateOwner(Owner owner) => Update(owner); + + public void DeleteOwner(Owner owner) => Delete(owner); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/Repository.csproj b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/Repository.csproj new file mode 100644 index 000000000..a2d04de58 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/Repository.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/RepositoryBase.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/RepositoryBase.cs new file mode 100644 index 000000000..2da56ec38 --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/RepositoryBase.cs @@ -0,0 +1,27 @@ +using Contracts; +using Entities; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace Repository +{ + public abstract class RepositoryBase : IRepositoryBase where T : class + { + protected RepositoryContext RepositoryContext { get; set; } + public RepositoryBase(RepositoryContext repositoryContext) + { + RepositoryContext = repositoryContext; + } + + public IQueryable FindAll() => RepositoryContext.Set().AsNoTracking(); + + public IQueryable FindByCondition(Expression> expression) => + RepositoryContext.Set().Where(expression).AsNoTracking(); + + public void Create(T entity) => RepositoryContext.Set().Add(entity); + + public void Update(T entity) => RepositoryContext.Set().Update(entity); + + public void Delete(T entity) => RepositoryContext.Set().Remove(entity); + } +} diff --git a/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/RepositoryWrapper.cs b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/RepositoryWrapper.cs new file mode 100644 index 000000000..05b1d936d --- /dev/null +++ b/aspnetcore-webapi/BasicWebApiSeries/UsingRepositoryForWriteRequests/Repository/RepositoryWrapper.cs @@ -0,0 +1,18 @@ +using Contracts; +using Entities; + +namespace Repository +{ + public class RepositoryWrapper(RepositoryContext repositoryContext) : IRepositoryWrapper + { + private readonly RepositoryContext _repoContext = repositoryContext; + private IOwnerRepository? _owner; + private IAccountRepository? _account; + + public IOwnerRepository Owner => _owner ??= new OwnerRepository(_repoContext); + + public IAccountRepository Account => _account ??= new AccountRepository(_repoContext); + + public async Task SaveAsync() => await _repoContext.SaveChangesAsync(); + } +}