diff --git a/KArtSell.sln b/KArtSell.sln index 75d43065..1f4b8127 100644 --- a/KArtSell.sln +++ b/KArtSell.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -18,43 +19,195 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KArtSell.Modules.ModelOpera EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KArtSell.ModelOperations.UnitTests", "tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj", "{B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KArtSell.Integration.Tests", "tests\KArtSell.Integration.Tests\KArtSell.Integration.Tests.csproj", "{1223B6C2-4D20-4558-A5C0-C02B99F4A109}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KArtSell.BuildingBlocks", "src\KArtSell.BuildingBlocks\KArtSell.BuildingBlocks.csproj", "{89901704-B4A4-4C6C-9FB9-21726EF97568}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KArtSell.Host", "src\KArtSell.Host\KArtSell.Host.csproj", "{6C936661-4907-4C75-9167-B9017F9AA7E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KArtSell.Modules.SignalEngine", "src\KArtSell.Modules.SignalEngine\KArtSell.Modules.SignalEngine.csproj", "{B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KArtSell.Modules.ModelOperations", "src\KArtSell.Modules.ModelOperations\KArtSell.Modules.ModelOperations.csproj", "{215F2FBC-B2D9-47E0-9807-A75392D17BBA}" +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 {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Debug|x64.Build.0 = Debug|Any CPU + {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Debug|x86.Build.0 = Debug|Any CPU {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Release|Any CPU.ActiveCfg = Release|Any CPU {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Release|Any CPU.Build.0 = Release|Any CPU + {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Release|x64.ActiveCfg = Release|Any CPU + {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Release|x64.Build.0 = Release|Any CPU + {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Release|x86.ActiveCfg = Release|Any CPU + {5F0F6CB9-ECB2-5B9E-8FCF-2AF378C6BB82}.Release|x86.Build.0 = Release|Any CPU {C203AEFE-E523-50DF-A0BC-66E41914E411}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C203AEFE-E523-50DF-A0BC-66E41914E411}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C203AEFE-E523-50DF-A0BC-66E41914E411}.Debug|x64.ActiveCfg = Debug|Any CPU + {C203AEFE-E523-50DF-A0BC-66E41914E411}.Debug|x64.Build.0 = Debug|Any CPU + {C203AEFE-E523-50DF-A0BC-66E41914E411}.Debug|x86.ActiveCfg = Debug|Any CPU + {C203AEFE-E523-50DF-A0BC-66E41914E411}.Debug|x86.Build.0 = Debug|Any CPU {C203AEFE-E523-50DF-A0BC-66E41914E411}.Release|Any CPU.ActiveCfg = Release|Any CPU {C203AEFE-E523-50DF-A0BC-66E41914E411}.Release|Any CPU.Build.0 = Release|Any CPU + {C203AEFE-E523-50DF-A0BC-66E41914E411}.Release|x64.ActiveCfg = Release|Any CPU + {C203AEFE-E523-50DF-A0BC-66E41914E411}.Release|x64.Build.0 = Release|Any CPU + {C203AEFE-E523-50DF-A0BC-66E41914E411}.Release|x86.ActiveCfg = Release|Any CPU + {C203AEFE-E523-50DF-A0BC-66E41914E411}.Release|x86.Build.0 = Release|Any CPU {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Debug|x64.Build.0 = Debug|Any CPU + {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Debug|x86.Build.0 = Debug|Any CPU {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Release|Any CPU.ActiveCfg = Release|Any CPU {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Release|Any CPU.Build.0 = Release|Any CPU + {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Release|x64.ActiveCfg = Release|Any CPU + {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Release|x64.Build.0 = Release|Any CPU + {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Release|x86.ActiveCfg = Release|Any CPU + {7F4AD582-8828-5F37-A4F3-E0F8ED300BFF}.Release|x86.Build.0 = Release|Any CPU {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Debug|x64.ActiveCfg = Debug|Any CPU + {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Debug|x64.Build.0 = Debug|Any CPU + {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Debug|x86.ActiveCfg = Debug|Any CPU + {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Debug|x86.Build.0 = Debug|Any CPU {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Release|Any CPU.ActiveCfg = Release|Any CPU {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Release|Any CPU.Build.0 = Release|Any CPU + {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Release|x64.ActiveCfg = Release|Any CPU + {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Release|x64.Build.0 = Release|Any CPU + {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Release|x86.ActiveCfg = Release|Any CPU + {C1E481A2-B1C9-5647-8600-DBE8D854552F}.Release|x86.Build.0 = Release|Any CPU {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Debug|x64.ActiveCfg = Debug|Any CPU + {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Debug|x64.Build.0 = Debug|Any CPU + {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Debug|x86.ActiveCfg = Debug|Any CPU + {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Debug|x86.Build.0 = Debug|Any CPU {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Release|Any CPU.ActiveCfg = Release|Any CPU {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Release|Any CPU.Build.0 = Release|Any CPU + {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Release|x64.ActiveCfg = Release|Any CPU + {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Release|x64.Build.0 = Release|Any CPU + {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Release|x86.ActiveCfg = Release|Any CPU + {FAA2A1FD-EB0C-50F6-BC9F-D571E1CF570E}.Release|x86.Build.0 = Release|Any CPU {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Debug|x64.ActiveCfg = Debug|Any CPU + {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Debug|x64.Build.0 = Debug|Any CPU + {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Debug|x86.ActiveCfg = Debug|Any CPU + {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Debug|x86.Build.0 = Debug|Any CPU {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Release|Any CPU.ActiveCfg = Release|Any CPU {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Release|Any CPU.Build.0 = Release|Any CPU + {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Release|x64.ActiveCfg = Release|Any CPU + {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Release|x64.Build.0 = Release|Any CPU + {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Release|x86.ActiveCfg = Release|Any CPU + {E62B968F-28A3-5055-8E5E-BFB52EC0175E}.Release|x86.Build.0 = Release|Any CPU {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Debug|x64.Build.0 = Debug|Any CPU + {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Debug|x86.Build.0 = Debug|Any CPU {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Release|Any CPU.ActiveCfg = Release|Any CPU {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Release|Any CPU.Build.0 = Release|Any CPU + {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Release|x64.ActiveCfg = Release|Any CPU + {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Release|x64.Build.0 = Release|Any CPU + {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Release|x86.ActiveCfg = Release|Any CPU + {A9A7D32E-7F4D-4B90-8C57-3D6A2E1A2401}.Release|x86.Build.0 = Release|Any CPU {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Debug|x64.ActiveCfg = Debug|Any CPU + {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Debug|x64.Build.0 = Debug|Any CPU + {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Debug|x86.ActiveCfg = Debug|Any CPU + {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Debug|x86.Build.0 = Debug|Any CPU {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Release|Any CPU.Build.0 = Release|Any CPU + {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Release|x64.ActiveCfg = Release|Any CPU + {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Release|x64.Build.0 = Release|Any CPU + {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Release|x86.ActiveCfg = Release|Any CPU + {B0B8E43F-805E-4CA1-9D68-4E7B3F2B3502}.Release|x86.Build.0 = Release|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Debug|x64.ActiveCfg = Debug|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Debug|x64.Build.0 = Debug|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Debug|x86.ActiveCfg = Debug|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Debug|x86.Build.0 = Debug|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Release|Any CPU.Build.0 = Release|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Release|x64.ActiveCfg = Release|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Release|x64.Build.0 = Release|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Release|x86.ActiveCfg = Release|Any CPU + {1223B6C2-4D20-4558-A5C0-C02B99F4A109}.Release|x86.Build.0 = Release|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Debug|x64.ActiveCfg = Debug|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Debug|x64.Build.0 = Debug|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Debug|x86.ActiveCfg = Debug|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Debug|x86.Build.0 = Debug|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Release|Any CPU.Build.0 = Release|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Release|x64.ActiveCfg = Release|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Release|x64.Build.0 = Release|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Release|x86.ActiveCfg = Release|Any CPU + {89901704-B4A4-4C6C-9FB9-21726EF97568}.Release|x86.Build.0 = Release|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Debug|x64.Build.0 = Debug|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Debug|x86.Build.0 = Debug|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Release|Any CPU.Build.0 = Release|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Release|x64.ActiveCfg = Release|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Release|x64.Build.0 = Release|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Release|x86.ActiveCfg = Release|Any CPU + {6C936661-4907-4C75-9167-B9017F9AA7E8}.Release|x86.Build.0 = Release|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Debug|x64.ActiveCfg = Debug|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Debug|x64.Build.0 = Debug|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Debug|x86.ActiveCfg = Debug|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Debug|x86.Build.0 = Debug|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Release|Any CPU.Build.0 = Release|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Release|x64.ActiveCfg = Release|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Release|x64.Build.0 = Release|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Release|x86.ActiveCfg = Release|Any CPU + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954}.Release|x86.Build.0 = Release|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Debug|x64.ActiveCfg = Debug|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Debug|x64.Build.0 = Debug|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Debug|x86.ActiveCfg = Debug|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Debug|x86.Build.0 = Debug|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Release|Any CPU.Build.0 = Release|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Release|x64.ActiveCfg = Release|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Release|x64.Build.0 = Release|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Release|x86.ActiveCfg = Release|Any CPU + {215F2FBC-B2D9-47E0-9807-A75392D17BBA}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1223B6C2-4D20-4558-A5C0-C02B99F4A109} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {89901704-B4A4-4C6C-9FB9-21726EF97568} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {6C936661-4907-4C75-9167-B9017F9AA7E8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {B44E86C4-3ACB-46AA-85F8-5D3FC1AAA954} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {215F2FBC-B2D9-47E0-9807-A75392D17BBA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/src/KArtSell.BuildingBlocks/Reliability/DapperOutboxMessageReader.cs b/src/KArtSell.BuildingBlocks/Reliability/DapperOutboxMessageReader.cs new file mode 100644 index 00000000..592cb54e --- /dev/null +++ b/src/KArtSell.BuildingBlocks/Reliability/DapperOutboxMessageReader.cs @@ -0,0 +1,89 @@ +using Dapper; +using KArtSell.BuildingBlocks.Data; + +namespace KArtSell.BuildingBlocks.Reliability; + +public sealed class DapperOutboxMessageReader(IDbConnectionFactory connectionFactory) +{ + private const string SelectUnpublishedSql = """ + select + message_id as MessageId, + event_type as EventType, + schema_version as SchemaVersion, + payload_json::text as PayloadJson, + correlation_id as CorrelationId, + occurred_at as OccurredAt, + payload_hash as PayloadHash, + attempt as Attempt + from building_blocks.outbox_message + where published_at is null + and occurred_at >= @CutoffTime + order by occurred_at asc + limit @BatchSize + """; + + private const string MarkPublishedSql = """ + update building_blocks.outbox_message + set published_at = @PublishedAt, attempt = attempt + 1 + where message_id = @MessageId + """; + + private const string InsertInboxSql = """ + insert into building_blocks.inbox_message + (consumer, message_id, received_at, payload_hash) + values (@Consumer, @MessageId, @ReceivedAt, @PayloadHash) + on conflict (consumer, message_id) do nothing + """; + + public async Task> GetUnpublishedAsync( + DateTimeOffset cutoffTime, + int batchSize, + CancellationToken cancellationToken) + { + await using var connection = await connectionFactory.OpenAsync(cancellationToken); + var messages = await connection.QueryAsync( + new CommandDefinition( + SelectUnpublishedSql, + new { CutoffTime = cutoffTime, BatchSize = batchSize }, + cancellationToken: cancellationToken)); + return messages.ToList(); + } + + public async Task MarkPublishedAsync( + Guid messageId, + DateTimeOffset publishedAt, + CancellationToken cancellationToken) + { + await using var connection = await connectionFactory.OpenAsync(cancellationToken); + await connection.ExecuteAsync( + new CommandDefinition( + MarkPublishedSql, + new { MessageId = messageId, PublishedAt = publishedAt }, + cancellationToken: cancellationToken)); + } + + public async Task InsertInboxAsync( + string consumer, + Guid messageId, + DateTimeOffset receivedAt, + string payloadHash, + CancellationToken cancellationToken) + { + await using var connection = await connectionFactory.OpenAsync(cancellationToken); + await connection.ExecuteAsync( + new CommandDefinition( + InsertInboxSql, + new { Consumer = consumer, MessageId = messageId, ReceivedAt = receivedAt, PayloadHash = payloadHash }, + cancellationToken: cancellationToken)); + } + + public sealed record OutboxMessageRow( + Guid MessageId, + string EventType, + int SchemaVersion, + string PayloadJson, + string CorrelationId, + DateTime OccurredAt, + string PayloadHash, + int Attempt); +} diff --git a/src/KArtSell.Host/Jobs/OutboxPollerJob.cs b/src/KArtSell.Host/Jobs/OutboxPollerJob.cs new file mode 100644 index 00000000..b626ed13 --- /dev/null +++ b/src/KArtSell.Host/Jobs/OutboxPollerJob.cs @@ -0,0 +1,75 @@ +using Hangfire; +using KArtSell.BuildingBlocks.Reliability; +using KArtSell.BuildingBlocks.Time; +using Microsoft.Extensions.Logging; + +namespace KArtSell.Host.Jobs; + +public sealed class OutboxPollerJob( + DapperOutboxMessageReader reader, + IClock clock, + ILogger logger) +{ + private const int DefaultBatchSize = 100; + private const int MaxAttemptsBeforeDq = 3; + + private static readonly Action LogPolled = + LoggerMessage.Define( + LogLevel.Information, + new EventId(1, nameof(LogPolled)), + "Outbox poller processed {MessageCount} messages with {FailureCount} permanent failures."); + + private static readonly Action LogMessagePublished = + LoggerMessage.Define( + LogLevel.Debug, + new EventId(2, nameof(LogMessagePublished)), + "Published outbox message {MessageId} ({EventType}, attempt {Attempt})."); + + private static readonly Action LogMessageDequed = + LoggerMessage.Define( + LogLevel.Warning, + new EventId(3, nameof(LogMessageDequed)), + "Outbox message {MessageId} ({EventType}) exceeded max attempts ({MaxAttempts}), moved to dead letter queue."); + + private static readonly Action LogProcessError = + LoggerMessage.Define( + LogLevel.Error, + new EventId(4, nameof(LogProcessError)), + "Failed to process outbox message {MessageId}"); + + [Queue("q-research")] + [DisableConcurrentExecution(timeoutInSeconds: 60)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)] + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + var now = clock.UtcNow; + var cutoffTime = now.AddMinutes(-5); + + var messages = await reader.GetUnpublishedAsync(cutoffTime, DefaultBatchSize, cancellationToken); + + var failureCount = 0; + foreach (var message in messages) + { + try + { + if (message.Attempt >= MaxAttemptsBeforeDq) + { + LogMessageDequed(logger, message.MessageId, message.EventType, MaxAttemptsBeforeDq, null); + failureCount++; + continue; + } + + await reader.MarkPublishedAsync(message.MessageId, now, cancellationToken); + await reader.InsertInboxAsync("outbox-poller", message.MessageId, now, message.PayloadHash, cancellationToken); + LogMessagePublished(logger, message.MessageId, message.EventType, message.Attempt + 1, null); + } + catch (Exception ex) + { + LogProcessError(logger, message.MessageId, ex); + failureCount++; + } + } + + LogPolled(logger, messages.Count, failureCount, null); + } +} diff --git a/src/KArtSell.Host/Program.cs b/src/KArtSell.Host/Program.cs index fcc0d8be..072f38b1 100644 --- a/src/KArtSell.Host/Program.cs +++ b/src/KArtSell.Host/Program.cs @@ -2,6 +2,7 @@ using FastEndpoints; using Hangfire; using Hangfire.PostgreSql; using KArtSell.BuildingBlocks.Capabilities; +using KArtSell.Host.Jobs; using KArtSell.BuildingBlocks.Data; using KArtSell.BuildingBlocks.Reliability; using KArtSell.BuildingBlocks.Time; @@ -44,6 +45,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddProblemDetails(); @@ -126,6 +128,12 @@ app.UseFastEndpoints(config => config.Endpoints.RoutePrefix = "api"); app.Services.RegisterModelOperationsSchedules(modelOperationsDispatcherEnabled, modelOperationsDispatcherCron); +RecurringJob.AddOrUpdate( + "outbox-poller", + job => job.ExecuteAsync(CancellationToken.None), + "* * * * *", + new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); + app.MapGet("/health/live", () => Results.Ok(new { status = "ok", diff --git a/src/KArtSell.Host/appsettings.json b/src/KArtSell.Host/appsettings.json index e3fcaeb1..144a7e6c 100644 --- a/src/KArtSell.Host/appsettings.json +++ b/src/KArtSell.Host/appsettings.json @@ -1,4 +1,11 @@ { + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://127.0.0.1:5002" + } + } + }, "ConnectionStrings": { "Postgres": "Host=localhost;Port=5432;Database=kartsell;Username=kartsell;Password=kartsell" }, diff --git a/tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj b/tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj new file mode 100644 index 00000000..dba072bb --- /dev/null +++ b/tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj @@ -0,0 +1,14 @@ + + + false + true + $(NoWarn);CA1859;DAP005 + + + + + + + + + diff --git a/tests/KArtSell.Integration.Tests/OutboxPollerJobTests.cs b/tests/KArtSell.Integration.Tests/OutboxPollerJobTests.cs new file mode 100644 index 00000000..f6a36126 --- /dev/null +++ b/tests/KArtSell.Integration.Tests/OutboxPollerJobTests.cs @@ -0,0 +1,267 @@ +using System.Data; +using System.Data.Common; +using Dapper; +using KArtSell.BuildingBlocks.Data; +using KArtSell.BuildingBlocks.Hashing; +using KArtSell.BuildingBlocks.Reliability; +using KArtSell.BuildingBlocks.Time; +using KArtSell.Host.Jobs; +using Microsoft.Extensions.Logging; +using Npgsql; +using Xunit; + +namespace KArtSell.Integration.Tests; + +public sealed class OutboxPollerJobTests : IAsyncLifetime +{ + private readonly string _connectionString; + private readonly NpgsqlDataSource _dataSource; + private readonly IDbConnectionFactory _connectionFactory; + private readonly IClock _clock = new SystemClock(); + private readonly ILogger _logger = new NoOpLogger(); + + public OutboxPollerJobTests() + { + _connectionString = Environment.GetEnvironmentVariable("KARTSELL_POSTGRES") + ?? "Host=localhost;Port=5432;Database=kartselldb;Username=kartsell;Password=kartsell4321@!"; + _dataSource = new NpgsqlDataSourceBuilder(_connectionString).Build(); + _connectionFactory = new NpgsqlConnectionFactory(_dataSource); + } + + public async Task InitializeAsync() + { + // Verify database is accessible + await using var connection = await _dataSource.OpenConnectionAsync(); + await using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 1"; + await cmd.ExecuteScalarAsync(); + } + + public async Task DisposeAsync() + { + await _dataSource.DisposeAsync(); + } + + [Fact] + public async Task ExecuteAsync_ProcessesUnpublishedMessages_MarksAsPublished() + { + // Arrange: Insert unpublished message + var messageId = Guid.NewGuid(); + var now = _clock.UtcNow; + const string payload = """{"decision_id":"test"}"""; + var hash = ContentHasher.Sha256(payload); + + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + await connection.ExecuteAsync(""" + insert into building_blocks.outbox_message + (message_id, event_type, schema_version, payload_json, correlation_id, occurred_at, payload_hash) + values (@MessageId, @EventType, 1, cast(@PayloadJson as jsonb), @CorrelationId, @OccurredAt, @Hash) + """, + new + { + MessageId = messageId, + EventType = "TestEvent", + PayloadJson = payload, + CorrelationId = Guid.NewGuid().ToString(), + OccurredAt = now.AddMinutes(-2), + Hash = hash + }); + } + + var reader = new DapperOutboxMessageReader(_connectionFactory); + var job = new OutboxPollerJob(reader, _clock, _logger); + + // Act + await job.ExecuteAsync(CancellationToken.None); + + // Assert: Verify published_at is set AND inbox_message is created + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + var published = await connection.QuerySingleOrDefaultAsync( + "select published_at from building_blocks.outbox_message where message_id = @MessageId", + new { MessageId = messageId }); + + Assert.NotNull(published); + Assert.True(published > now.AddSeconds(-5).DateTime, "published_at should be recent"); + + // Verify inbox_message is created + var inboxMsg = await connection.QuerySingleOrDefaultAsync<(string Consumer, Guid MessageId)?>( + "select consumer, message_id from building_blocks.inbox_message where message_id = @MessageId", + new { MessageId = messageId }); + + Assert.NotNull(inboxMsg); + Assert.Equal("outbox-poller", inboxMsg!.Value.Consumer); + Assert.Equal(messageId, inboxMsg.Value.MessageId); + } + + // Cleanup + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + await connection.ExecuteAsync( + "delete from building_blocks.outbox_message where message_id = @MessageId", + new { MessageId = messageId }); + } + } + + [Fact] + public async Task ExecuteAsync_SkipsMessagesBeyondCutoff_OnlyProcessesRecentMessages() + { + // Arrange: Insert two messages - one old, one recent + var oldMessageId = Guid.NewGuid(); + var recentMessageId = Guid.NewGuid(); + var now = _clock.UtcNow; + const string payload = """{"data":"test"}"""; + var hash = ContentHasher.Sha256(payload); + + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + await connection.ExecuteAsync(""" + insert into building_blocks.outbox_message + (message_id, event_type, schema_version, payload_json, correlation_id, occurred_at, payload_hash) + values (@MessageId, @EventType, 1, cast(@PayloadJson as jsonb), @CorrelationId, @OccurredAt, @Hash) + """, + new + { + MessageId = oldMessageId, + EventType = "OldEvent", + PayloadJson = payload, + CorrelationId = Guid.NewGuid().ToString(), + OccurredAt = now.AddMinutes(-10), + Hash = hash + }); + + await connection.ExecuteAsync(""" + insert into building_blocks.outbox_message + (message_id, event_type, schema_version, payload_json, correlation_id, occurred_at, payload_hash) + values (@MessageId, @EventType, 1, cast(@PayloadJson as jsonb), @CorrelationId, @OccurredAt, @Hash) + """, + new + { + MessageId = recentMessageId, + EventType = "RecentEvent", + PayloadJson = payload, + CorrelationId = Guid.NewGuid().ToString(), + OccurredAt = now.AddMinutes(-2), + Hash = hash + }); + } + + var reader = new DapperOutboxMessageReader(_connectionFactory); + var job = new OutboxPollerJob(reader, _clock, _logger); + + // Act + await job.ExecuteAsync(CancellationToken.None); + + // Assert: Only recent message should be published (and have inbox entry) + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + var oldPublished = await connection.QuerySingleOrDefaultAsync( + "select published_at from building_blocks.outbox_message where message_id = @MessageId", + new { MessageId = oldMessageId }); + var recentPublished = await connection.QuerySingleOrDefaultAsync( + "select published_at from building_blocks.outbox_message where message_id = @MessageId", + new { MessageId = recentMessageId }); + + Assert.Null(oldPublished); + Assert.NotNull(recentPublished); + + // Verify inbox entry exists ONLY for recent message + var oldInbox = await connection.QuerySingleOrDefaultAsync( + "select message_id from building_blocks.inbox_message where message_id = @MessageId", + new { MessageId = oldMessageId }); + var recentInbox = await connection.QuerySingleOrDefaultAsync( + "select message_id from building_blocks.inbox_message where message_id = @MessageId", + new { MessageId = recentMessageId }); + + Assert.Null(oldInbox); + Assert.Equal(recentMessageId, recentInbox); + } + + // Cleanup + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + await connection.ExecuteAsync( + "delete from building_blocks.outbox_message where message_id in (@OldId, @RecentId)", + new { OldId = oldMessageId, RecentId = recentMessageId }); + } + } + + [Fact] + public async Task ExecuteAsync_SkipsMessagesExceedingMaxAttempts_LogsAsDeadLetter() + { + // Arrange: Insert message with high attempt count + var messageId = Guid.NewGuid(); + var now = _clock.UtcNow; + const string payload = """{"error":"too many retries"}"""; + var hash = ContentHasher.Sha256(payload); + + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + await connection.ExecuteAsync(""" + insert into building_blocks.outbox_message + (message_id, event_type, schema_version, payload_json, correlation_id, occurred_at, payload_hash, attempt) + values (@MessageId, @EventType, 1, cast(@PayloadJson as jsonb), @CorrelationId, @OccurredAt, @Hash, @Attempt) + """, + new + { + MessageId = messageId, + EventType = "FailedEvent", + PayloadJson = payload, + CorrelationId = Guid.NewGuid().ToString(), + OccurredAt = now.AddMinutes(-2), + Hash = hash, + Attempt = 3 + }); + } + + var reader = new DapperOutboxMessageReader(_connectionFactory); + var job = new OutboxPollerJob(reader, _clock, _logger); + + // Act + await job.ExecuteAsync(CancellationToken.None); + + // Assert: Message should NOT be published (remains unpublished) and NO inbox entry + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + var published = await connection.QuerySingleOrDefaultAsync( + "select published_at from building_blocks.outbox_message where message_id = @MessageId", + new { MessageId = messageId }); + + Assert.Null(published); + + // Verify no inbox entry for dead-lettered message + var inboxMsg = await connection.QuerySingleOrDefaultAsync( + "select message_id from building_blocks.inbox_message where message_id = @MessageId", + new { MessageId = messageId }); + + Assert.Null(inboxMsg); + } + + // Cleanup + await using (var connection = await _connectionFactory.OpenAsync(CancellationToken.None)) + { + await connection.ExecuteAsync( + "delete from building_blocks.outbox_message where message_id = @MessageId", + new { MessageId = messageId }); + } + } + + private sealed class NoOpLogger : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => null; + public bool IsEnabled(LogLevel logLevel) => false; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { } + } +} + +// Database factory for tests +public sealed class NpgsqlConnectionFactory : IDbConnectionFactory +{ + private readonly NpgsqlDataSource _dataSource; + + public NpgsqlConnectionFactory(NpgsqlDataSource dataSource) => _dataSource = dataSource; + + public async ValueTask OpenAsync(CancellationToken cancellationToken) + => await _dataSource.OpenConnectionAsync(cancellationToken); +}