diff --git a/.github/workflows/package_push_nuget.org.yml b/.github/workflows/package_push_nuget.org.yml new file mode 100644 index 000000000..49cc9a214 --- /dev/null +++ b/.github/workflows/package_push_nuget.org.yml @@ -0,0 +1,42 @@ +name: Package Push Nuget +on: + release: + types: [ created ] + +jobs: + package-build: + name: packeg build and push + runs-on: ubuntu-latest + steps: + - name: git pull + uses: actions/checkout@v2 + + - name: run a one-line script + run: env + + - name: setting dotnet version + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: dependencies + - run: git clone -b main https://github.com/masastack/MASA.BuildingBlocks.git ./src/BuildingBlocks + + - name: restore + run: dotnet restore + + - name: build + run: dotnet build --no-restore /p:ContinuousIntegrationBuild=true + + - name: test + run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Exclude="[*.Tests]*" + + - name: codecov + uses: codecov/codecov-action@v1 + + - name: pack + run: dotnet pack --include-symbols -p:PackageVersion=$GITHUB_REF_NAME + + - name: package push + run: dotnet nuget push "**/*.symbols.nupkg" -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 4e8ff5524..000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,32 +0,0 @@ -image: mcr.microsoft.com/dotnet/sdk:6.0.100-preview.7 - -stages: - - build - - tests - - nuget - -build: - stage: build - only: - - branches - script: - - dotnet build - retry: 2 - -# tests: -# stage: tests -# only: -# - branches -# script: -# - dotnet test --collect:"XPlat Code Coverage" -# retry: 2 - -nuget: - stage: nuget - only: - - branches - script: - - dotnet build - - dotnet pack --include-symbols -p:PackageVersion=0.0.$CI_PIPELINE_ID - - dotnet nuget push "**/*.symbols.nupkg" --source gitlab - retry: 2 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..5a6352e61 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/BuildingBlocks/MASA.BuildingBlocks"] + path = src/BuildingBlocks/MASA.BuildingBlocks + url = https://github.com/masastack/MASA.BuildingBlocks.git diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..a0865029c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,24 @@ + + + $(AssemblyName) + packageIcon.png + masastack + © masastack Corporation. All rights reserved. + packageIcon.png + https://github.com/masastack/MASA.Contrib + git + true + $(MSBuildThisFileDirectory) + LICENSE.txt + + + + True + + + + True + + + + diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/MASA.Contrib.sln b/MASA.Contrib.sln index 41a0d7273..02a1db545 100644 --- a/MASA.Contrib.sln +++ b/MASA.Contrib.sln @@ -40,14 +40,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DDD", "DDD", "{21180442-A6A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Data", "Data", "{E33ADF54-4D35-49B7-BDA6-412587CA39FF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Data.Uow.EF", "src\Data\MASA.Contrib.Data.Uow.EF\MASA.Contrib.Data.Uow.EF.csproj", "{626631CD-4FD5-424E-A678-27653F38CA3E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Data.Uow.EF.Tests", "test\MASA.Contrib.Data.Uow.EF.Tests\MASA.Contrib.Data.Uow.EF.Tests.csproj", "{63BB9F58-316E-4F20-8F45-B45D28FC2476}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Dispatcher.Events", "src\Dispatcher\MASA.Contrib.Dispatcher.Events\MASA.Contrib.Dispatcher.Events.csproj", "{1B44F2E7-28F5-4E88-B8A8-3F336800FD5C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest", "test\MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest\MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.csproj", "{0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests", "test\MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests\MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests.csproj", "{D55A7D3B-9C24-4029-BC03-41B28AD11DB6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Dispatcher.Events.CheckMethodsParameterNotNull.Tests", "test\MASA.Contrib.Dispatcher.Events.CheckMethodsParameterNotNull.Tests\MASA.Contrib.Dispatcher.Events.CheckMethodsParameterNotNull.Tests.csproj", "{89B2A693-CD8A-4EA2-9991-2CEE44C4D04B}" @@ -88,11 +82,63 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Dispatcher.Int EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MASA.Contrib.DDD.Domain", "MASA.Contrib.DDD.Domain", "{13EDB361-AF88-4F89-B4AB-46622BCCBC37}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contribs.DDD.Domain.Entities", "test\MASA.Contribs.DDD.Domain.Entities\MASA.Contribs.DDD.Domain.Entities.csproj", "{647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MASA.Contrib.DDD.Domain.Repository.EF", "MASA.Contrib.DDD.Domain.Repository.EF", "{880E8263-AECC-4793-BD28-7CD03650D124}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository", "test\MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository\MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.csproj", "{7A1493EC-196F-4389-A966-02E526453578}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Data.UoW.EF", "src\Data\MASA.Contrib.Data.UoW.EF\MASA.Contrib.Data.UoW.EF.csproj", "{1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Data.UoW.EF.Tests", "test\MASA.Contrib.Data.UoW.EF.Tests\MASA.Contrib.Data.UoW.EF.Tests.csproj", "{1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.BasicAbility.Dcc", "src\BasicAbility\MASA.Contrib.BasicAbility.Dcc\MASA.Contrib.BasicAbility.Dcc.csproj", "{FDF1C618-4C68-485E-A881-4FFF04EDF6E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Configuration", "src\Configuration\MASA.Contrib.Configuration\MASA.Contrib.Configuration.csproj", "{C056C688-8FFC-42BC-B4EA-EF3808A8A12C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.ReadWriteSpliting.CQRS.Tests", "test\MASA.Contrib.ReadWriteSpliting.CQRS.Tests\MASA.Contrib.ReadWriteSpliting.CQRS.Tests.csproj", "{428CDAF3-957A-4017-82EA-70737F205546}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Configuration.Tests", "test\MASA.Contrib.Configuration.Tests\MASA.Contrib.Configuration.Tests.csproj", "{DB93B639-899D-4B2C-AF8A-47B4BC6B3776}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.BasicAbility.Dcc.Tests", "test\MASA.Contrib.BasicAbility.Dcc.Tests\MASA.Contrib.BasicAbility.Dcc.Tests.csproj", "{85BCA106-4A6F-4BEE-A748-E61A24D12DBD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MASA.Contrib.Configuration", "MASA.Contrib.Configuration", "{9EEE31DA-3165-4CB3-AAE9-27CC3A4DE669}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Service.MinimalAPIs.Tests", "test\MASA.Contrib.Service.MinimalAPIs.Tests\MASA.Contrib.Service.MinimalAPIs.Tests.csproj", "{A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contribs.DDD.Domain.Entities.Tests", "test\MASA.Contribs.DDD.Domain.Entities.Tests\MASA.Contribs.DDD.Domain.Entities.Tests.csproj", "{B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Data.Contracts.EF.Tests", "test\MASA.Contrib.Data.Contracts.EF.Tests\MASA.Contrib.Data.Contracts.EF.Tests.csproj", "{5A163042-B03A-4063-85FF-22D4C5BB5B90}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests", "test\MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests\MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests.csproj", "{84EFF9E1-6852-458F-8D57-62E3F084EA0F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests", "test\MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests\MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests.csproj", "{427822F2-7A20-4E3A-B45C-43CE855003C1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFind.Tests", "test\MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests\MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFind.Tests.csproj", "{99067BDF-2C6A-47F8-913D-3FF9F2A69F98}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests", "test\MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests\MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests.csproj", "{A4DE46BD-1FA4-494B-80DA-6EB370686F17}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests", "test\MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests\MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests.csproj", "{7909A736-6C1E-4622-9BE7-37EF0839FA05}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests", "test\MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests\MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests.csproj", "{71E02AFA-06A0-4527-923C-6666B3D66542}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests", "test\MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests\MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.csproj", "{1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildingBlocks", "BuildingBlocks", "{DC578D74-98F0-4F19-A230-CFA8DAEE0AF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.Configuration", "src\BuildingBlocks\MASA.BuildingBlocks\src\Configuration\MASA.BuildingBlocks.Configuration\MASA.BuildingBlocks.Configuration.csproj", "{374B7E56-A815-4F56-A4C2-6094B5A97EE7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.Data.Contracts", "src\BuildingBlocks\MASA.BuildingBlocks\src\Data\MASA.BuildingBlocks.Data.Contracts\MASA.BuildingBlocks.Data.Contracts.csproj", "{7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.Data.UoW", "src\BuildingBlocks\MASA.BuildingBlocks\src\Data\MASA.BuildingBlocks.Data.UoW\MASA.BuildingBlocks.Data.UoW.csproj", "{E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.DDD.Domain", "src\BuildingBlocks\MASA.BuildingBlocks\src\DDD\MASA.BuildingBlocks.DDD.Domain\MASA.BuildingBlocks.DDD.Domain.csproj", "{2971858E-2CCA-4688-B8CA-84F130AD5AA9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.Dispatcher.Events", "src\BuildingBlocks\MASA.BuildingBlocks\src\Dispatcher\MASA.BuildingBlocks.Dispatcher.Events\MASA.BuildingBlocks.Dispatcher.Events.csproj", "{2503F67B-63BB-4364-8B31-1DD3049C92B7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.Dispatcher.IntegrationEvents", "src\BuildingBlocks\MASA.BuildingBlocks\src\Dispatcher\MASA.BuildingBlocks.Dispatcher.IntegrationEvents\MASA.BuildingBlocks.Dispatcher.IntegrationEvents.csproj", "{88BC8170-9123-48C0-A914-11D3CE805196}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.ReadWriteSpliting.CQRS", "src\BuildingBlocks\MASA.BuildingBlocks\src\ReadWriteSpliting\MASA.BuildingBlocks.ReadWriteSpliting.CQRS\MASA.BuildingBlocks.ReadWriteSpliting.CQRS.csproj", "{316B1D0A-9CF7-4E5C-A39A-8A389B075A19}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.SearchEngine.AutoComplete", "src\BuildingBlocks\MASA.BuildingBlocks\src\SearchEngine\MASA.BuildingBlocks.SearchEngine.AutoComplete\MASA.BuildingBlocks.SearchEngine.AutoComplete.csproj", "{2F4986D6-3F56-4C05-8A1D-399594F96093}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MASA.BuildingBlocks.Service.MinimalAPIs", "src\BuildingBlocks\MASA.BuildingBlocks\src\Service\MASA.BuildingBlocks.Service.MinimalAPIs\MASA.BuildingBlocks.Service.MinimalAPIs.csproj", "{E72E105D-B15F-4D69-9A13-CAA49D4889D6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -110,22 +156,6 @@ Global {ED301FA5-4E70-460B-A0D4-1D79D135769F}.Release|Any CPU.Build.0 = Release|Any CPU {ED301FA5-4E70-460B-A0D4-1D79D135769F}.Release|x64.ActiveCfg = Release|Any CPU {ED301FA5-4E70-460B-A0D4-1D79D135769F}.Release|x64.Build.0 = Release|Any CPU - {626631CD-4FD5-424E-A678-27653F38CA3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {626631CD-4FD5-424E-A678-27653F38CA3E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {626631CD-4FD5-424E-A678-27653F38CA3E}.Debug|x64.ActiveCfg = Debug|Any CPU - {626631CD-4FD5-424E-A678-27653F38CA3E}.Debug|x64.Build.0 = Debug|Any CPU - {626631CD-4FD5-424E-A678-27653F38CA3E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {626631CD-4FD5-424E-A678-27653F38CA3E}.Release|Any CPU.Build.0 = Release|Any CPU - {626631CD-4FD5-424E-A678-27653F38CA3E}.Release|x64.ActiveCfg = Release|Any CPU - {626631CD-4FD5-424E-A678-27653F38CA3E}.Release|x64.Build.0 = Release|Any CPU - {63BB9F58-316E-4F20-8F45-B45D28FC2476}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63BB9F58-316E-4F20-8F45-B45D28FC2476}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63BB9F58-316E-4F20-8F45-B45D28FC2476}.Debug|x64.ActiveCfg = Debug|Any CPU - {63BB9F58-316E-4F20-8F45-B45D28FC2476}.Debug|x64.Build.0 = Debug|Any CPU - {63BB9F58-316E-4F20-8F45-B45D28FC2476}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63BB9F58-316E-4F20-8F45-B45D28FC2476}.Release|Any CPU.Build.0 = Release|Any CPU - {63BB9F58-316E-4F20-8F45-B45D28FC2476}.Release|x64.ActiveCfg = Release|Any CPU - {63BB9F58-316E-4F20-8F45-B45D28FC2476}.Release|x64.Build.0 = Release|Any CPU {1B44F2E7-28F5-4E88-B8A8-3F336800FD5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1B44F2E7-28F5-4E88-B8A8-3F336800FD5C}.Debug|Any CPU.Build.0 = Debug|Any CPU {1B44F2E7-28F5-4E88-B8A8-3F336800FD5C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -134,14 +164,6 @@ Global {1B44F2E7-28F5-4E88-B8A8-3F336800FD5C}.Release|Any CPU.Build.0 = Release|Any CPU {1B44F2E7-28F5-4E88-B8A8-3F336800FD5C}.Release|x64.ActiveCfg = Release|Any CPU {1B44F2E7-28F5-4E88-B8A8-3F336800FD5C}.Release|x64.Build.0 = Release|Any CPU - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}.Debug|x64.ActiveCfg = Debug|Any CPU - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}.Debug|x64.Build.0 = Debug|Any CPU - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}.Release|Any CPU.Build.0 = Release|Any CPU - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}.Release|x64.ActiveCfg = Release|Any CPU - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6}.Release|x64.Build.0 = Release|Any CPU {D55A7D3B-9C24-4029-BC03-41B28AD11DB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D55A7D3B-9C24-4029-BC03-41B28AD11DB6}.Debug|Any CPU.Build.0 = Debug|Any CPU {D55A7D3B-9C24-4029-BC03-41B28AD11DB6}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -294,22 +316,214 @@ Global {761C3313-A669-465F-A384-9E118FCE4F89}.Release|Any CPU.Build.0 = Release|Any CPU {761C3313-A669-465F-A384-9E118FCE4F89}.Release|x64.ActiveCfg = Release|Any CPU {761C3313-A669-465F-A384-9E118FCE4F89}.Release|x64.Build.0 = Release|Any CPU - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}.Debug|Any CPU.Build.0 = Debug|Any CPU - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}.Debug|x64.ActiveCfg = Debug|Any CPU - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}.Debug|x64.Build.0 = Debug|Any CPU - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}.Release|Any CPU.ActiveCfg = Release|Any CPU - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}.Release|Any CPU.Build.0 = Release|Any CPU - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}.Release|x64.ActiveCfg = Release|Any CPU - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32}.Release|x64.Build.0 = Release|Any CPU - {7A1493EC-196F-4389-A966-02E526453578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A1493EC-196F-4389-A966-02E526453578}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A1493EC-196F-4389-A966-02E526453578}.Debug|x64.ActiveCfg = Debug|Any CPU - {7A1493EC-196F-4389-A966-02E526453578}.Debug|x64.Build.0 = Debug|Any CPU - {7A1493EC-196F-4389-A966-02E526453578}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A1493EC-196F-4389-A966-02E526453578}.Release|Any CPU.Build.0 = Release|Any CPU - {7A1493EC-196F-4389-A966-02E526453578}.Release|x64.ActiveCfg = Release|Any CPU - {7A1493EC-196F-4389-A966-02E526453578}.Release|x64.Build.0 = Release|Any CPU + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}.Debug|x64.ActiveCfg = Debug|Any CPU + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}.Debug|x64.Build.0 = Debug|Any CPU + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}.Release|Any CPU.Build.0 = Release|Any CPU + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}.Release|x64.ActiveCfg = Release|Any CPU + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F}.Release|x64.Build.0 = Release|Any CPU + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}.Debug|x64.Build.0 = Debug|Any CPU + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}.Release|Any CPU.Build.0 = Release|Any CPU + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}.Release|x64.ActiveCfg = Release|Any CPU + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7}.Release|x64.Build.0 = Release|Any CPU + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8}.Debug|x64.ActiveCfg = Debug|Any CPU + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8}.Debug|x64.Build.0 = Debug|Any CPU + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8}.Release|Any CPU.Build.0 = Release|Any CPU + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8}.Release|x64.ActiveCfg = Release|Any CPU + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8}.Release|x64.Build.0 = Release|Any CPU + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C}.Debug|x64.ActiveCfg = Debug|Any CPU + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C}.Debug|x64.Build.0 = Debug|Any CPU + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C}.Release|Any CPU.Build.0 = Release|Any CPU + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C}.Release|x64.ActiveCfg = Release|Any CPU + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C}.Release|x64.Build.0 = Release|Any CPU + {428CDAF3-957A-4017-82EA-70737F205546}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {428CDAF3-957A-4017-82EA-70737F205546}.Debug|Any CPU.Build.0 = Debug|Any CPU + {428CDAF3-957A-4017-82EA-70737F205546}.Debug|x64.ActiveCfg = Debug|Any CPU + {428CDAF3-957A-4017-82EA-70737F205546}.Debug|x64.Build.0 = Debug|Any CPU + {428CDAF3-957A-4017-82EA-70737F205546}.Release|Any CPU.ActiveCfg = Release|Any CPU + {428CDAF3-957A-4017-82EA-70737F205546}.Release|Any CPU.Build.0 = Release|Any CPU + {428CDAF3-957A-4017-82EA-70737F205546}.Release|x64.ActiveCfg = Release|Any CPU + {428CDAF3-957A-4017-82EA-70737F205546}.Release|x64.Build.0 = Release|Any CPU + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776}.Debug|x64.ActiveCfg = Debug|Any CPU + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776}.Debug|x64.Build.0 = Debug|Any CPU + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776}.Release|Any CPU.Build.0 = Release|Any CPU + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776}.Release|x64.ActiveCfg = Release|Any CPU + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776}.Release|x64.Build.0 = Release|Any CPU + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD}.Debug|x64.ActiveCfg = Debug|Any CPU + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD}.Debug|x64.Build.0 = Debug|Any CPU + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD}.Release|Any CPU.Build.0 = Release|Any CPU + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD}.Release|x64.ActiveCfg = Release|Any CPU + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD}.Release|x64.Build.0 = Release|Any CPU + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}.Debug|x64.Build.0 = Debug|Any CPU + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}.Release|Any CPU.Build.0 = Release|Any CPU + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}.Release|x64.ActiveCfg = Release|Any CPU + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD}.Release|x64.Build.0 = Release|Any CPU + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}.Debug|x64.ActiveCfg = Debug|Any CPU + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}.Debug|x64.Build.0 = Debug|Any CPU + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}.Release|Any CPU.Build.0 = Release|Any CPU + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}.Release|x64.ActiveCfg = Release|Any CPU + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826}.Release|x64.Build.0 = Release|Any CPU + {5A163042-B03A-4063-85FF-22D4C5BB5B90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A163042-B03A-4063-85FF-22D4C5BB5B90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A163042-B03A-4063-85FF-22D4C5BB5B90}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A163042-B03A-4063-85FF-22D4C5BB5B90}.Debug|x64.Build.0 = Debug|Any CPU + {5A163042-B03A-4063-85FF-22D4C5BB5B90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A163042-B03A-4063-85FF-22D4C5BB5B90}.Release|Any CPU.Build.0 = Release|Any CPU + {5A163042-B03A-4063-85FF-22D4C5BB5B90}.Release|x64.ActiveCfg = Release|Any CPU + {5A163042-B03A-4063-85FF-22D4C5BB5B90}.Release|x64.Build.0 = Release|Any CPU + {84EFF9E1-6852-458F-8D57-62E3F084EA0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84EFF9E1-6852-458F-8D57-62E3F084EA0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84EFF9E1-6852-458F-8D57-62E3F084EA0F}.Debug|x64.ActiveCfg = Debug|Any CPU + {84EFF9E1-6852-458F-8D57-62E3F084EA0F}.Debug|x64.Build.0 = Debug|Any CPU + {84EFF9E1-6852-458F-8D57-62E3F084EA0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84EFF9E1-6852-458F-8D57-62E3F084EA0F}.Release|Any CPU.Build.0 = Release|Any CPU + {84EFF9E1-6852-458F-8D57-62E3F084EA0F}.Release|x64.ActiveCfg = Release|Any CPU + {84EFF9E1-6852-458F-8D57-62E3F084EA0F}.Release|x64.Build.0 = Release|Any CPU + {427822F2-7A20-4E3A-B45C-43CE855003C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {427822F2-7A20-4E3A-B45C-43CE855003C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {427822F2-7A20-4E3A-B45C-43CE855003C1}.Debug|x64.ActiveCfg = Debug|Any CPU + {427822F2-7A20-4E3A-B45C-43CE855003C1}.Debug|x64.Build.0 = Debug|Any CPU + {427822F2-7A20-4E3A-B45C-43CE855003C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {427822F2-7A20-4E3A-B45C-43CE855003C1}.Release|Any CPU.Build.0 = Release|Any CPU + {427822F2-7A20-4E3A-B45C-43CE855003C1}.Release|x64.ActiveCfg = Release|Any CPU + {427822F2-7A20-4E3A-B45C-43CE855003C1}.Release|x64.Build.0 = Release|Any CPU + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98}.Debug|x64.ActiveCfg = Debug|Any CPU + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98}.Debug|x64.Build.0 = Debug|Any CPU + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98}.Release|Any CPU.Build.0 = Release|Any CPU + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98}.Release|x64.ActiveCfg = Release|Any CPU + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98}.Release|x64.Build.0 = Release|Any CPU + {A4DE46BD-1FA4-494B-80DA-6EB370686F17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4DE46BD-1FA4-494B-80DA-6EB370686F17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4DE46BD-1FA4-494B-80DA-6EB370686F17}.Debug|x64.ActiveCfg = Debug|Any CPU + {A4DE46BD-1FA4-494B-80DA-6EB370686F17}.Debug|x64.Build.0 = Debug|Any CPU + {A4DE46BD-1FA4-494B-80DA-6EB370686F17}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4DE46BD-1FA4-494B-80DA-6EB370686F17}.Release|Any CPU.Build.0 = Release|Any CPU + {A4DE46BD-1FA4-494B-80DA-6EB370686F17}.Release|x64.ActiveCfg = Release|Any CPU + {A4DE46BD-1FA4-494B-80DA-6EB370686F17}.Release|x64.Build.0 = Release|Any CPU + {7909A736-6C1E-4622-9BE7-37EF0839FA05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7909A736-6C1E-4622-9BE7-37EF0839FA05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7909A736-6C1E-4622-9BE7-37EF0839FA05}.Debug|x64.ActiveCfg = Debug|Any CPU + {7909A736-6C1E-4622-9BE7-37EF0839FA05}.Debug|x64.Build.0 = Debug|Any CPU + {7909A736-6C1E-4622-9BE7-37EF0839FA05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7909A736-6C1E-4622-9BE7-37EF0839FA05}.Release|Any CPU.Build.0 = Release|Any CPU + {7909A736-6C1E-4622-9BE7-37EF0839FA05}.Release|x64.ActiveCfg = Release|Any CPU + {7909A736-6C1E-4622-9BE7-37EF0839FA05}.Release|x64.Build.0 = Release|Any CPU + {71E02AFA-06A0-4527-923C-6666B3D66542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71E02AFA-06A0-4527-923C-6666B3D66542}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71E02AFA-06A0-4527-923C-6666B3D66542}.Debug|x64.ActiveCfg = Debug|Any CPU + {71E02AFA-06A0-4527-923C-6666B3D66542}.Debug|x64.Build.0 = Debug|Any CPU + {71E02AFA-06A0-4527-923C-6666B3D66542}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71E02AFA-06A0-4527-923C-6666B3D66542}.Release|Any CPU.Build.0 = Release|Any CPU + {71E02AFA-06A0-4527-923C-6666B3D66542}.Release|x64.ActiveCfg = Release|Any CPU + {71E02AFA-06A0-4527-923C-6666B3D66542}.Release|x64.Build.0 = Release|Any CPU + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}.Debug|x64.Build.0 = Debug|Any CPU + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}.Release|Any CPU.Build.0 = Release|Any CPU + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}.Release|x64.ActiveCfg = Release|Any CPU + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C}.Release|x64.Build.0 = Release|Any CPU + {374B7E56-A815-4F56-A4C2-6094B5A97EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {374B7E56-A815-4F56-A4C2-6094B5A97EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {374B7E56-A815-4F56-A4C2-6094B5A97EE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {374B7E56-A815-4F56-A4C2-6094B5A97EE7}.Debug|x64.Build.0 = Debug|Any CPU + {374B7E56-A815-4F56-A4C2-6094B5A97EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {374B7E56-A815-4F56-A4C2-6094B5A97EE7}.Release|Any CPU.Build.0 = Release|Any CPU + {374B7E56-A815-4F56-A4C2-6094B5A97EE7}.Release|x64.ActiveCfg = Release|Any CPU + {374B7E56-A815-4F56-A4C2-6094B5A97EE7}.Release|x64.Build.0 = Release|Any CPU + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}.Debug|x64.ActiveCfg = Debug|Any CPU + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}.Debug|x64.Build.0 = Debug|Any CPU + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}.Release|Any CPU.Build.0 = Release|Any CPU + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}.Release|x64.ActiveCfg = Release|Any CPU + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B}.Release|x64.Build.0 = Release|Any CPU + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}.Debug|x64.Build.0 = Debug|Any CPU + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}.Release|Any CPU.Build.0 = Release|Any CPU + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}.Release|x64.ActiveCfg = Release|Any CPU + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A}.Release|x64.Build.0 = Release|Any CPU + {2971858E-2CCA-4688-B8CA-84F130AD5AA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2971858E-2CCA-4688-B8CA-84F130AD5AA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2971858E-2CCA-4688-B8CA-84F130AD5AA9}.Debug|x64.ActiveCfg = Debug|Any CPU + {2971858E-2CCA-4688-B8CA-84F130AD5AA9}.Debug|x64.Build.0 = Debug|Any CPU + {2971858E-2CCA-4688-B8CA-84F130AD5AA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2971858E-2CCA-4688-B8CA-84F130AD5AA9}.Release|Any CPU.Build.0 = Release|Any CPU + {2971858E-2CCA-4688-B8CA-84F130AD5AA9}.Release|x64.ActiveCfg = Release|Any CPU + {2971858E-2CCA-4688-B8CA-84F130AD5AA9}.Release|x64.Build.0 = Release|Any CPU + {2503F67B-63BB-4364-8B31-1DD3049C92B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2503F67B-63BB-4364-8B31-1DD3049C92B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2503F67B-63BB-4364-8B31-1DD3049C92B7}.Debug|x64.ActiveCfg = Debug|Any CPU + {2503F67B-63BB-4364-8B31-1DD3049C92B7}.Debug|x64.Build.0 = Debug|Any CPU + {2503F67B-63BB-4364-8B31-1DD3049C92B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2503F67B-63BB-4364-8B31-1DD3049C92B7}.Release|Any CPU.Build.0 = Release|Any CPU + {2503F67B-63BB-4364-8B31-1DD3049C92B7}.Release|x64.ActiveCfg = Release|Any CPU + {2503F67B-63BB-4364-8B31-1DD3049C92B7}.Release|x64.Build.0 = Release|Any CPU + {88BC8170-9123-48C0-A914-11D3CE805196}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88BC8170-9123-48C0-A914-11D3CE805196}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88BC8170-9123-48C0-A914-11D3CE805196}.Debug|x64.ActiveCfg = Debug|Any CPU + {88BC8170-9123-48C0-A914-11D3CE805196}.Debug|x64.Build.0 = Debug|Any CPU + {88BC8170-9123-48C0-A914-11D3CE805196}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88BC8170-9123-48C0-A914-11D3CE805196}.Release|Any CPU.Build.0 = Release|Any CPU + {88BC8170-9123-48C0-A914-11D3CE805196}.Release|x64.ActiveCfg = Release|Any CPU + {88BC8170-9123-48C0-A914-11D3CE805196}.Release|x64.Build.0 = Release|Any CPU + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19}.Debug|x64.ActiveCfg = Debug|Any CPU + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19}.Debug|x64.Build.0 = Debug|Any CPU + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19}.Release|Any CPU.Build.0 = Release|Any CPU + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19}.Release|x64.ActiveCfg = Release|Any CPU + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19}.Release|x64.Build.0 = Release|Any CPU + {2F4986D6-3F56-4C05-8A1D-399594F96093}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F4986D6-3F56-4C05-8A1D-399594F96093}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F4986D6-3F56-4C05-8A1D-399594F96093}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F4986D6-3F56-4C05-8A1D-399594F96093}.Debug|x64.Build.0 = Debug|Any CPU + {2F4986D6-3F56-4C05-8A1D-399594F96093}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F4986D6-3F56-4C05-8A1D-399594F96093}.Release|Any CPU.Build.0 = Release|Any CPU + {2F4986D6-3F56-4C05-8A1D-399594F96093}.Release|x64.ActiveCfg = Release|Any CPU + {2F4986D6-3F56-4C05-8A1D-399594F96093}.Release|x64.Build.0 = Release|Any CPU + {E72E105D-B15F-4D69-9A13-CAA49D4889D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E72E105D-B15F-4D69-9A13-CAA49D4889D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E72E105D-B15F-4D69-9A13-CAA49D4889D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {E72E105D-B15F-4D69-9A13-CAA49D4889D6}.Debug|x64.Build.0 = Debug|Any CPU + {E72E105D-B15F-4D69-9A13-CAA49D4889D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E72E105D-B15F-4D69-9A13-CAA49D4889D6}.Release|Any CPU.Build.0 = Release|Any CPU + {E72E105D-B15F-4D69-9A13-CAA49D4889D6}.Release|x64.ActiveCfg = Release|Any CPU + {E72E105D-B15F-4D69-9A13-CAA49D4889D6}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -329,10 +543,7 @@ Global {ED301FA5-4E70-460B-A0D4-1D79D135769F} = {593A3114-D1E0-47ED-BC37-58E08886175B} {21180442-A6A5-4239-A2AD-33FF5BB80E72} = {42DF7AAC-362C-48F4-B76A-BDEEEFF17CC9} {E33ADF54-4D35-49B7-BDA6-412587CA39FF} = {42DF7AAC-362C-48F4-B76A-BDEEEFF17CC9} - {626631CD-4FD5-424E-A678-27653F38CA3E} = {E33ADF54-4D35-49B7-BDA6-412587CA39FF} - {63BB9F58-316E-4F20-8F45-B45D28FC2476} = {38E6C400-90C0-493E-9266-C1602E229F1B} {1B44F2E7-28F5-4E88-B8A8-3F336800FD5C} = {FBD326D3-E59C-433E-A88E-14E179E3093D} - {0FF80D58-98D2-43E9-8EAF-7F47C31CB0B6} = {2BE750A5-8AC7-457C-9BB2-6E3D5E2D23B8} {D55A7D3B-9C24-4029-BC03-41B28AD11DB6} = {2BE750A5-8AC7-457C-9BB2-6E3D5E2D23B8} {89B2A693-CD8A-4EA2-9991-2CEE44C4D04B} = {2BE750A5-8AC7-457C-9BB2-6E3D5E2D23B8} {2E172027-1B85-474E-A238-21B2DBDB895F} = {2BE750A5-8AC7-457C-9BB2-6E3D5E2D23B8} @@ -353,9 +564,35 @@ Global {E893C913-98A0-4BB3-A32B-3871BE3C5C53} = {880E8263-AECC-4793-BD28-7CD03650D124} {761C3313-A669-465F-A384-9E118FCE4F89} = {38E6C400-90C0-493E-9266-C1602E229F1B} {13EDB361-AF88-4F89-B4AB-46622BCCBC37} = {38E6C400-90C0-493E-9266-C1602E229F1B} - {647A9FC3-4C21-4CD1-AD6A-FADFEB976E32} = {13EDB361-AF88-4F89-B4AB-46622BCCBC37} {880E8263-AECC-4793-BD28-7CD03650D124} = {38E6C400-90C0-493E-9266-C1602E229F1B} - {7A1493EC-196F-4389-A966-02E526453578} = {880E8263-AECC-4793-BD28-7CD03650D124} + {1265AE3C-B5FD-4339-8A7D-BC598E6E1C9F} = {E33ADF54-4D35-49B7-BDA6-412587CA39FF} + {1B16DD58-0847-45A7-AF93-53EBFBEDAAE7} = {38E6C400-90C0-493E-9266-C1602E229F1B} + {FDF1C618-4C68-485E-A881-4FFF04EDF6E8} = {5DFAF4A2-ECB5-46E4-904D-1EA5F48B2D48} + {C056C688-8FFC-42BC-B4EA-EF3808A8A12C} = {59DA3D5F-9E39-4173-8C31-126967CC189F} + {428CDAF3-957A-4017-82EA-70737F205546} = {38E6C400-90C0-493E-9266-C1602E229F1B} + {DB93B639-899D-4B2C-AF8A-47B4BC6B3776} = {9EEE31DA-3165-4CB3-AAE9-27CC3A4DE669} + {85BCA106-4A6F-4BEE-A748-E61A24D12DBD} = {38E6C400-90C0-493E-9266-C1602E229F1B} + {9EEE31DA-3165-4CB3-AAE9-27CC3A4DE669} = {38E6C400-90C0-493E-9266-C1602E229F1B} + {A5C1EF6B-A3B5-4D0C-8373-F854EE7EF4AD} = {38E6C400-90C0-493E-9266-C1602E229F1B} + {B29ABF5D-AFA8-4480-B74E-3ACB6FAAA826} = {13EDB361-AF88-4F89-B4AB-46622BCCBC37} + {5A163042-B03A-4063-85FF-22D4C5BB5B90} = {38E6C400-90C0-493E-9266-C1602E229F1B} + {84EFF9E1-6852-458F-8D57-62E3F084EA0F} = {9EEE31DA-3165-4CB3-AAE9-27CC3A4DE669} + {427822F2-7A20-4E3A-B45C-43CE855003C1} = {9EEE31DA-3165-4CB3-AAE9-27CC3A4DE669} + {99067BDF-2C6A-47F8-913D-3FF9F2A69F98} = {880E8263-AECC-4793-BD28-7CD03650D124} + {A4DE46BD-1FA4-494B-80DA-6EB370686F17} = {880E8263-AECC-4793-BD28-7CD03650D124} + {7909A736-6C1E-4622-9BE7-37EF0839FA05} = {880E8263-AECC-4793-BD28-7CD03650D124} + {71E02AFA-06A0-4527-923C-6666B3D66542} = {880E8263-AECC-4793-BD28-7CD03650D124} + {1A86AE9B-A57D-43D2-9E8C-5ED0C1E6041C} = {2BE750A5-8AC7-457C-9BB2-6E3D5E2D23B8} + {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} = {42DF7AAC-362C-48F4-B76A-BDEEEFF17CC9} + {374B7E56-A815-4F56-A4C2-6094B5A97EE7} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {7DAC3708-4415-490B-AAE8-0DAA74E2EA8B} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {E5A8DB4E-1205-4CE9-B802-3D6ADADF737A} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {2971858E-2CCA-4688-B8CA-84F130AD5AA9} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {2503F67B-63BB-4364-8B31-1DD3049C92B7} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {88BC8170-9123-48C0-A914-11D3CE805196} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {316B1D0A-9CF7-4E5C-A39A-8A389B075A19} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {2F4986D6-3F56-4C05-8A1D-399594F96093} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {E72E105D-B15F-4D69-9A13-CAA49D4889D6} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {40383055-CC50-4600-AD9A-53C14F620D03} diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 000000000..3f0e00340 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index ccab56434..41afb2634 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,38 @@ [中](README.zh-CN.md) | EN +[![codecov](https://codecov.io/gh/masastack/MASA.Contrib/branch/develop/graph/badge.svg?token=87TPNHUHW2)](https://codecov.io/gh/masastack/MASA.Contrib) + # MASA.Contrib -MASA.Contrib is the best practice of MASA.BuildingBlocks +The purpose of MASA.Contrib is based on [MASA.BuildingBlocks](https://github.com/masastack/MASA.BuildingBlocks) to provide open, community driven reusable components for building mesh applications. These components will be used by the [MASA Stack](https://github.com/masastack) and [MASA Labs](https://github.com/masalabs) projects. ## Structure ```c# MASA.Contrib -│──solution items -│ ── nuget.config -│──src +├── solution items +│ ├── nuget.config +├── src +│ ├── BasicAbility +│ │ ├── MASA.Contrib.BasicAbility.Dcc ConfigurationAPI +│ ├── Configuration +│ │ ├── MASA.Contrib.Configuration │ ├── Data -│ │ ├── MASA.Contrib.Data.Uow.EF Unit of work -│ │ └── MASA.Contribs.Data.Contracts.EF Protocol EF version +│ │ ├── MASA.Contrib.Data.UoW.EF Unit of work +│ │ └── MASA.Contrib.Data.Contracts.EF Protocol EF version │ ├── DDD -│ │ ├── MASA.Contribs.DDD.Domain In-process and cross-process support -│ │ └── MASA.Contribs.DDD.Domain.Repository.EF +│ │ ├── MASA.Contrib.DDD.Domain In-process and cross-process support +│ │ └── MASA.Contrib.DDD.Domain.Repository.EF │ ├── Dispatcher -│ │ ├── MASA.Contrib.Dispatcher.Events In-process event +│ │ ├── MASA.Contrib.Dispatcher.Events In-process event │ │ ├── MASA.Contrib.Dispatcher.IntegrationEvents.Dapr -│ │ └── MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF Cross-process event +│ │ └── MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF Cross-process event │ ├── ReadWriteSpliting │ │ └── CQRS -│ │ │ └── MASA.Contrib.ReadWriteSpliting.CQRS CQRS +│ │ │ └── MASA.Contrib.ReadWriteSpliting.CQRS CQRS │ ├── Service -│ │ └── MASA.Contrib.Service.MinimalAPIs Best practices for [MinimalAPI] -│──test +│ │ └── MASA.Contrib.Service.MinimalAPIs Best practices for [MinimalAPI] +├── test │ ├── MASA.Contrib.Dispatcher.Events │ │ ├── MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest │ │ ├── MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests @@ -36,17 +42,17 @@ MASA.Contrib │ │ ├── MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests │ │ ├── MASA.Contrib.Dispatcher.Events.CheckMethodsType.Tests │ │ ├── MASA.Contrib.Dispatcher.Events.Tests -│ ├── MASA.Contrib.Data.Uow.EF.Tests +│ ├── MASA.Contrib.Data.UoW.EF.Tests │ ├── MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests -│ ├── MASA.Contribs.DDD.Domain.Tests -│ ├── MASA.Contribs.DDD.Domain.Repository.EF.Tests +│ ├── MASA.Contrib.DDD.Domain.Tests +│ ├── MASA.Contrib.DDD.Domain.Repository.EF.Tests ``` ## Feature ### 1. MinimalAPI -What is [MinimalAPI](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#introducing-minimal-apis)?[Usage introduction](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/Service/MASA.Contrib.Service.MinimalAPIs/README.md) +What is [MinimalAPI](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#introducing-minimal-apis)?[Usage introduction](/src/Service/MASA.Contrib.Service.MinimalAPIs/README.md) > Advantage: > @@ -54,7 +60,7 @@ What is [MinimalAPI](https://devblogs.microsoft.com/aspnet/asp-net-core-updates- ### 2. EventBus -[Usage introduction](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.md) +[Usage introduction](/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.md) > Advantage: > @@ -73,22 +79,22 @@ What is [MinimalAPI](https://devblogs.microsoft.com/aspnet/asp-net-core-updates- ### 3. CQRS -what is[CQRS](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)?[Usage introduction](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.md) +what is[CQRS](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)?[Usage introduction](/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.md) ### 4. IntegrationEventBus -Realize cross-process events based on Dapr。[Usage introduction](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md) +Realize cross-process events based on Dapr。[Usage introduction](/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md) > Advantage:Use the same transaction to commit the user-defined context and the log to ensure atomicity and consistency ### 5. DomainEventBus -[Usage introduction](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/DDD/MASA.Contribs.DDD.Domain/README.md) +[Usage introduction](/src/DDD/MASA.Contrib.DDD.Domain/README.md) > Advantage: > > 1. CQRS -> 2. Field Service +> 2. Domain Service > 3. Support domain events (in-process), integrated domain events (cross-process) > 4. Support the unified sending of field events after being pushed onto the stack @@ -99,35 +105,40 @@ Realize cross-process events based on Dapr。[Usage introduction](http://gitlab- ### 7. Contracts.EF -Protocol based on EF implementation,[Usage introduction](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/Data/MASA.Contribs.Data.Contracts.EF/README.md) +Protocol based on EF implementation,[Usage introduction](/Data/MASA.Contrib.Data.Contracts.EF/README.md) > Advantage: > > 1. Filter deleted information when querying -> 2. Open transaction after query -> 3. Soft delete +> 2. Soft delete ```C# -Install-Package MASA.Contribs.Data.Contracts.EF +Install-Package MASA.Contrib.Data.Contracts.EF ``` ```C# -builder.Services - .AddUoW(dbOptions => +builder.Services.AddEventBus(options => { + options.UseUoW(dbOptions => { dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"); - dbOptions.UseSoftDelete(builder.Services);//Start soft delete - }) + dbOptions.UseSoftDelete(builder.Services); + }); +}); + ``` > When the entity inherits ISoftware and is deleted, change the delete state to the modified state, and cooperate with the custom Remove operation to achieve soft deletion > Do not query the data marked as soft deleted when querying > When combined with EventBus, the transaction is opened after the first CUD, and the transaction rollback is supported when the entire Handler is abnormal. +### 8. MASA.Contrib.Configuration + +Redefine Configuration, support the management of Local and ConfigurationAPI nodes, combine IOptions and IOptionsMonitor to complete configuration acquisition and configuration update subscription [Local Usage introduction](src/Configuration/MASA.Contrib.Configuration/README.md) 、[Dcc Usage introduction](src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/README.md) + ## Unit testing rules To ensure the reliability of the entire source code, the unit test coverage is at least 90% ## ☀️ License agreement -[![MASA.Contrib](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/LICENSE) +[![MASA.Contrib](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](/LICENSE.txt) diff --git a/README.zh-CN.md b/README.zh-CN.md index 83518f5d6..7f3210bf1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,32 +1,38 @@ 中 | [EN](README.md) +[![codecov](https://codecov.io/gh/masastack/MASA.Contrib/branch/develop/graph/badge.svg?token=87TPNHUHW2)](https://codecov.io/gh/masastack/MASA.Contrib) + # MASA.Contrib -MASA.BuildingBlocks最佳实践 +MASA.Contrib是基于[MASA.BuildingBlocks](https://github.com/masastack/MASA.BuildingBlocks)提供开放, 社区驱动的可重用组件,用于构建网格应用程序。这些组件将被[MASA Stack](https://github.com/masastack)和[MASA Labs](https://github.com/masalabs)等项目使用。 ## 结构 ```c# MASA.Contrib -│──solution items -│ ── nuget.config -│──src +├── solution items +│ ├── nuget.config +├── src +│ ├── BasicAbility +│ │ ├── MASA.Contrib.BasicAbility.Dcc ConfigurationAPI +│ ├── Configuration +│ │ ├── MASA.Contrib.Configuration │ ├── Data -│ │ ├── MASA.Contrib.Data.Uow.EF 工作单元 -│ │ └── MASA.Contribs.Data.Contracts.EF 规约EF版 +│ │ ├── MASA.Contrib.Data.UoW.EF 工作单元 +│ │ └── MASA.Contrib.Data.Contracts.EF 规约EF版 │ ├── DDD -│ │ ├── MASA.Contribs.DDD.Domain 进程内、跨进程都支持 -│ │ └── MASA.Contribs.DDD.Domain.Repository.EF +│ │ ├── MASA.Contrib.DDD.Domain 进程内、跨进程都支持 +│ │ └── MASA.Contrib.DDD.Domain.Repository.EF │ ├── Dispatcher -│ │ ├── MASA.Contrib.Dispatcher.Events 进程内事件 +│ │ ├── MASA.Contrib.Dispatcher.Events 进程内事件 │ │ ├── MASA.Contrib.Dispatcher.IntegrationEvents.Dapr -│ │ └── MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF 跨进程事件 +│ │ └── MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF 跨进程事件 │ ├── ReadWriteSpliting │ │ └── CQRS -│ │ │ └── MASA.Contrib.ReadWriteSpliting.CQRS CQRS +│ │ │ └── MASA.Contrib.ReadWriteSpliting.CQRS CQRS │ ├── Service -│ │ └── MASA.Contrib.Service.MinimalAPIs MinimalAPI最佳实践 -│──test +│ │ └── MASA.Contrib.Service.MinimalAPIs MinimalAPI最佳实践 +├── test │ ├── MASA.Contrib.Dispatcher.Events │ │ ├── MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest │ │ ├── MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests @@ -36,17 +42,17 @@ MASA.Contrib │ │ ├── MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests │ │ ├── MASA.Contrib.Dispatcher.Events.CheckMethodsType.Tests │ │ ├── MASA.Contrib.Dispatcher.Events.Tests -│ ├── MASA.Contrib.Data.Uow.EF.Tests +│ ├── MASA.Contrib.Data.UoW.EF.Tests │ ├── MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests -│ ├── MASA.Contribs.DDD.Domain.Tests -│ ├── MASA.Contribs.DDD.Domain.Repository.EF.Tests +│ ├── MASA.Contrib.DDD.Domain.Tests +│ ├── MASA.Contrib.DDD.Domain.Repository.EF.Tests ``` ## 特性 ### 1. MinimalAPI -什么是[MinimalAPI](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#introducing-minimal-apis)?[用法介绍](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/Service/MASA.Contrib.Service.MinimalAPIs/README.zh-cn.md) +什么是[MinimalAPI](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-4/#introducing-minimal-apis)?[用法介绍](/src/Service/MASA.Contrib.Service.MinimalAPIs/README.zh-CN.md) > 优势: > @@ -54,7 +60,7 @@ MASA.Contrib ### 2. EventBus -[用法介绍](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.zh-cn.md) +[用法介绍](/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.zh-CN.md) > 优势: > @@ -73,23 +79,23 @@ MASA.Contrib ### 3. CQRS -什么是[CQRS](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)?[用法介绍](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.zh-cn.md) +什么是[CQRS](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)?[用法介绍](/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.zh-CN.md) ### 4. IntegrationEventBus -基于Dapr实现跨进程的事件。[用法介绍](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-cn.md) +基于Dapr实现跨进程的事件。[用法介绍](/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-CN.md) > 优势:将用户自定义上下文与日志使用同一事务提交,确保原子性、一致性 ### 5. DomainEventBus -[用法介绍](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/src/DDD/MASA.Contribs.DDD.Domain/README.zh-cn.md) +[用法介绍](/src/DDD/MASA.Contrib.DDD.Domain/README.zh-CN.md) > 优势: > -> 2. CQRS -> 3. 领域服务 -> 4. 支持领域事件(进程内)、集成领域事件(跨进程) +> 1. CQRS +> 2. 领域服务 +> 3. 支持领域事件(进程内)、集成领域事件(跨进程) > 4. 支持对领域事件先压栈后统一发送 ### 6. DDD @@ -99,36 +105,40 @@ MASA.Contrib ### 7. Contracts.EF -基于EF实现的规约,[用法介绍](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/Data/MASA.Contribs.Data.Contracts.EF/README.zh-cn.md) +基于EF实现的规约,[用法介绍](src/Data/MASA.Contrib.Data.Contracts.EF/README.zh-CN.md) > 优势: > > 1. 查询的时候过滤已删除的信息 -> 2. 查询后开启事务 -> 3. 软删除 +> 2. 软删除 ```C# -Install-Package MASA.Contribs.Data.Contracts.EF +Install-Package MASA.Contrib.Data.Contracts.EF ``` ```C# -builder.Services - .AddUoW(dbOptions => +builder.Services.AddEventBus(options => { + options.UseUoW(dbOptions => { dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"); dbOptions.UseSoftDelete(builder.Services);//启动软删除 - }) + }); +}); ``` > 当实体继承ISoftware,且被删除时,将删除状态改为修改状态,并配合自定义Remove操作,实现软删除 > 支持查询的时候不查询被标记软删除的数据 > 与EventBus结合使用时,做到了第一次CUD后开启事务,当整个Handler出现异常后支持事务回滚 +### 8. MASA.Contrib.Configuration + +重定义Configuration,支持Local、ConfigurationAPI节点的管理,结合IOptions、IOptionsMonitor完成配置的获取以及配置的更新订阅 [Local用法介绍](src/Configuration/MASA.Contrib.Configuration/README.zh-CN.md) 、[Dcc用法介绍](src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/README.zh-CN.md) + ## 单元测试规则 为确保整个源码的可靠性,单元测试覆盖率最低为90% ## ☀️ 授权协议 -[![MASA.Contrib](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](http://gitlab-hz.lonsid.cn/MASA-Stack/Contribs/MASA.Contrib/-/tree/develop/LICENSE) +[![MASA.Contrib](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](/LICENSE.txt) diff --git a/docs/LoadEvent.md b/docs/LoadEvent.md new file mode 100644 index 000000000..65d1db9e3 --- /dev/null +++ b/docs/LoadEvent.md @@ -0,0 +1,10 @@ +# LoadEvent + +## Getting "Event" relationship chain failed + +When Event, EventHandler, and the main project are not in the same assembly, there will be a failure to obtain the "Event" relationship chain when publishing Events through EventBus. When we use AddEventBus without a special specified assembly, the assembly under the current domain is used by default. Due to the delayed loading feature of dotnet, the acquisition of the event relationship chain is incomplete. There are the following two solutions : + +1. When using AddEventBus, specify the complete set of application assemblies used by the current project by specifying Assemblies + +2. Before using AddEventBus, by calling Event directly +Any method or class of the assembly where the EventHandler is located, make sure that the application assembly where it is located has been loaded into the current assembly ( AppDomain.CurrentDomain.GetAssemblies() ) diff --git a/nuget.config b/nuget.config deleted file mode 100644 index b3dcc28ac..000000000 --- a/nuget.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/packageIcon.png b/packageIcon.png new file mode 100644 index 000000000..2128805c1 Binary files /dev/null and b/packageIcon.png differ diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/ConfigurationApiClient.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/ConfigurationApiClient.cs new file mode 100644 index 000000000..8ad400b82 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/ConfigurationApiClient.cs @@ -0,0 +1,158 @@ +namespace MASA.Contrib.BasicAbility.Dcc; + +public class ConfigurationApiClient : ConfigurationAPIBase, IConfigurationApiClient +{ + private readonly IServiceProvider _serviceProvider; + private readonly IMemoryCacheClient _client; + private readonly JsonSerializerOptions _jsonSerializerOptions; + + private readonly ConcurrentDictionary>> _taskExpandoObjects = new(); + private readonly ConcurrentDictionary>> _taskJsonObjects = new(); + + public ConfigurationApiClient( + IServiceProvider serviceProvider, + IMemoryCacheClient client, + JsonSerializerOptions jsonSerializerOptions, + DccSectionOptions defaultSectionOption, + List? expandSectionOptions) + : base(defaultSectionOption, expandSectionOptions) + { + _serviceProvider = serviceProvider; + _client = client; + _jsonSerializerOptions = jsonSerializerOptions; + } + + public Task<(string Raw, ConfigurationTypes ConfigurationType)> GetRawAsync(string environment, string cluster, string appId, + string configObject, Action valueChanged) + { + var key = FomartKey(environment, cluster, appId, configObject); + return GetRawByKeyAsync(key, valueChanged); + } + + public async Task GetAsync(string environment, string cluster, string appId, string configObject, Action valueChanged) + { + var key = FomartKey(environment, cluster, appId, configObject); + + var value = await _taskJsonObjects.GetOrAdd(key, (k) => new Lazy>(async () => + { + var options = new JsonSerializerOptions(_jsonSerializerOptions); + options.EnableDynamicTypes(); + + var result = await GetRawByKeyAsync(k, (value) => + { + var result = JsonSerializer.Deserialize(value, options); + + var newValue = new Lazy>(() => Task.FromResult((object)result!)); + _taskJsonObjects.AddOrUpdate(k, newValue, (_, _) => newValue); + valueChanged?.Invoke(result!); + }); + if (typeof(T).GetInterfaces().Any(type => type == typeof(IConvertible))) + { + if (result.ConfigurationType == ConfigurationTypes.Text) + return Convert.ChangeType(result.Raw, typeof(T)); + + throw new FormatException(result.Raw); + } + + return JsonSerializer.Deserialize(result.Raw, options) ?? throw new ArgumentException(nameof(configObject)); + })).Value; + + return (T)value; + } + + public async Task GetDynamicAsync(string environment, string cluster, string appId, string configObject, + Action valueChanged) + { + var key = FomartKey(environment, cluster, appId, configObject); + + var value = _taskExpandoObjects.GetOrAdd(key, (k) => new Lazy>(async () => + { + var options = new JsonSerializerOptions(_jsonSerializerOptions); + options.EnableDynamicTypes(); + + var raw = await GetRawByKeyAsync(k, (value) => + { + var result = JsonSerializer.Deserialize(value, options); + var newValue = new Lazy>(() => Task.FromResult(result)!); + _taskExpandoObjects.AddOrUpdate(k, newValue!, (_, _) => newValue!); + valueChanged?.Invoke(result!); + }); + + return JsonSerializer.Deserialize(raw.Raw, options) ?? throw new ArgumentException(nameof(configObject)); + })).Value; + + return await value; + } + + public async Task GetDynamicAsync(string key) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + var configuration = _serviceProvider.GetRequiredService(); + key = key.Replace(".", ConfigurationPath.KeyDelimiter); + return await Task.FromResult(Format(configuration.GetSection(key))); + } + + private async Task<(string Raw, ConfigurationTypes ConfigurationType)> GetRawByKeyAsync(string key, Action valueChanged) + { + var raw = await _client.GetAsync(key, (value) => + { + var result = FormatRaw(value); + valueChanged?.Invoke(result.Raw); + }); + + return FormatRaw(raw); + } + + private (string Raw, ConfigurationTypes ConfigurationType) FormatRaw(string? raw) + { + if (raw == null) + throw new ArgumentException("configObject invalid"); + + var result = JsonSerializer.Deserialize(raw, _jsonSerializerOptions); + if (result == null || result.ConfigFormat == 0) + throw new ArgumentException("configObject invalid"); + + switch (result.ConfigFormat) + { + case ConfigFormats.Json: + return (result.Content!, ConfigurationTypes.Json); + + case ConfigFormats.Text: + return (result.Content!, ConfigurationTypes.Text); + + case ConfigFormats.Properties: + var properties = PropertyConfigurationParser.Parse(result.Content!, _jsonSerializerOptions); + if (properties == null) + throw new ArgumentException("configObject invalid"); + + return (JsonSerializer.Serialize(properties, _jsonSerializerOptions), ConfigurationTypes.Properties); + + default: + throw new NotSupportedException("Unsupported configuration type"); + } + } + + private string FomartKey(string environment, string cluster, string appId, string configObject) + => $"{GetEnvironment(environment)}-{GetCluster(cluster)}-{GetAppId(appId)}-{GetConfigObject(configObject)}".ToLower(); + + private dynamic Format(IConfigurationSection section) + { + var childrenSections = section.GetChildren(); + if (!section.Exists() || !childrenSections.Any()) + { + return section.Value; + } + else + { + var result = new ExpandoObject(); + var parent = result as IDictionary; + foreach (var child in childrenSections) + { + parent[child.Key] = Format(child); + } + return result; + } + } +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/ConfigurationApiManage.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/ConfigurationApiManage.cs new file mode 100644 index 000000000..f8f5036a9 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/ConfigurationApiManage.cs @@ -0,0 +1,28 @@ +namespace MASA.Contrib.BasicAbility.Dcc; + +public class ConfigurationApiManage : ConfigurationAPIBase, IConfigurationApiManage +{ + private readonly ICallerProvider _callerProvider; + + public ConfigurationApiManage( + ICallerProvider callerProvider, + DccSectionOptions defaultSectionOption, + List? expandSectionOptions) + : base(defaultSectionOption, expandSectionOptions) + { + _callerProvider = callerProvider; + } + + public async Task UpdateAsync(string environment, string cluster, string appId, string configObject, object value) + { + var requestUri = $"open-api/releasing/{GetEnvironment(environment)}/{GetCluster(cluster)}/{GetAppId(appId)}/{GetConfigObject(configObject)}?secret={GetSecret(appId)}"; + var result = await _callerProvider.PutAsync(requestUri, value, default); + + // 299 is the status code when throwing a UserFriendlyException in masa.framework + if ((int)result.StatusCode == 299 || !result.IsSuccessStatusCode) + { + var error = await result.Content.ReadAsStringAsync(); + throw new HttpRequestException(error); + } + } +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/ConfigFormats.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/ConfigFormats.cs new file mode 100644 index 000000000..fbd4b6c6e --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/ConfigFormats.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal; + +internal enum ConfigFormats +{ + Properties = 1, + Text, + Json +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/ConfigurationAPIBase.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/ConfigurationAPIBase.cs new file mode 100644 index 000000000..cbbd6b8c5 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/ConfigurationAPIBase.cs @@ -0,0 +1,37 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal; + +public class ConfigurationAPIBase +{ + private readonly DccSectionOptions _defaultSectionOption; + private readonly List _expandSectionOptions; + + protected ConfigurationAPIBase(DccSectionOptions defaultSectionOption, List? expandSectionOptions) + { + _defaultSectionOption = defaultSectionOption; + _expandSectionOptions = expandSectionOptions ?? new(); + } + + protected string GetSecret(string appId) + { + if (_defaultSectionOption.AppId == GetAppId(appId)) + return _defaultSectionOption.Secret ?? ""; + + var option = _expandSectionOptions.FirstOrDefault(x => x.AppId == appId); + if (option == null) + throw new ArgumentNullException(nameof(appId)); + + return option.Secret ?? ""; + } + + protected string GetEnvironment(string environment) + => !string.IsNullOrEmpty(environment) ? environment : _defaultSectionOption.Environment!; + + protected string GetCluster(string cluster) + => !string.IsNullOrEmpty(cluster) ? cluster : _defaultSectionOption.Cluster!; + + protected string GetAppId(string appId) + => !string.IsNullOrEmpty(appId) ? appId : _defaultSectionOption.AppId!; + + protected string GetConfigObject(string configObject) + => !string.IsNullOrEmpty(configObject) ? configObject : throw new ArgumentNullException(nameof(configObject)); +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Constants.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Constants.cs new file mode 100644 index 000000000..d352c723b --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Constants.cs @@ -0,0 +1,12 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal; + +internal class Constants +{ + internal const string DEFAULT_CLIENT_NAME = "masa.plugins.caching.dcc"; + + internal const string DEFAULT_SUBSCRIBE_KEY_PREFIX = "masa.dcc:"; + + internal const string DEFAULT_ENVIRONMENT_NAME = "ASPNETCORE_ENVIRONMENT"; + + internal const string DATA_DICTIONARY_SECTION_NAME = "DataDictionary"; +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/DccConfigurationRepository.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/DccConfigurationRepository.cs new file mode 100644 index 000000000..40158fc81 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/DccConfigurationRepository.cs @@ -0,0 +1,92 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal; + +internal class DccConfigurationRepository : AbstractConfigurationRepository +{ + private readonly IConfigurationApiClient _client; + + public override SectionTypes SectionType { get; init; } = SectionTypes.ConfigurationAPI; + + private readonly ConcurrentDictionary> _dictionaries = new(); + + private readonly ConcurrentDictionary _configObjectConfigurationTypeRelations = new(); + + public DccConfigurationRepository( + IEnumerable sectionOptions, + IConfigurationApiClient client, + ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _client = client; + foreach (var sectionOption in sectionOptions) + { + Initialize(sectionOption).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + + private async Task Initialize(DccSectionOptions sectionOption) + { + foreach (var configObject in sectionOption.ConfigObjects) + { + string key = $"{sectionOption.Environment!}-{sectionOption.Cluster!}-{sectionOption.AppId}-{configObject}".ToLower(); + var result = await _client.GetRawAsync(sectionOption.Environment!, sectionOption.Cluster!, sectionOption.AppId, configObject, (val) => + { + if (_configObjectConfigurationTypeRelations.TryGetValue(key, out var configurationType)) + { + _dictionaries[key] = FormatRaw(sectionOption.AppId, configObject, val, configurationType); + FireRepositoryChange(SectionType, Load()); + } + }); + + _configObjectConfigurationTypeRelations.TryAdd(key, result.ConfigurationType); + _dictionaries[key] = FormatRaw(sectionOption.AppId, configObject, result.Raw, result.ConfigurationType); + } + } + + private IDictionary FormatRaw(string appId, string configObject, string? raw, ConfigurationTypes configurationType) + { + if (raw == null) + return new Dictionary(); + + switch (configurationType) + { + case ConfigurationTypes.Json: + return SecondaryFormat(appId, configObject, JsonConfigurationParser.Parse(raw)); + case ConfigurationTypes.Properties: + return SecondaryFormat(appId, configObject, JsonSerializer.Deserialize>(raw)!); + case ConfigurationTypes.Text: + return new Dictionary() + { + { $"{appId}{ConfigurationPath.KeyDelimiter}{DATA_DICTIONARY_SECTION_NAME}{ConfigurationPath.KeyDelimiter}{configObject}" , raw ?? "" } + }; + default: + throw new NotSupportedException(nameof(configurationType)); + } + } + + private IDictionary SecondaryFormat( + string appId, + string configObject, + IDictionary data) + { + var dictionary = new Dictionary(); + foreach (var item in data) + { + dictionary[$"{appId}{ConfigurationPath.KeyDelimiter}{configObject}{ConfigurationPath.KeyDelimiter}{item.Key}"] = item.Value; + } + return dictionary; + } + + public override Properties Load() + { + Dictionary properties = new(); + foreach (var item in _dictionaries) + { + foreach (var key in item.Value.Keys) + { + properties[key] = item.Value[key] ?? string.Empty; + } + } + return new Properties(properties); + } + +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/DccFactory.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/DccFactory.cs new file mode 100644 index 000000000..039bee5b4 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/DccFactory.cs @@ -0,0 +1,20 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal; + +internal class DccFactory +{ + public static IConfigurationApiClient CreateClient( + IServiceProvider serviceProvider, + IMemoryCacheClient client, + JsonSerializerOptions jsonSerializerOptions, + DccSectionOptions defaultSectionOption, + List? expandSectionOptions) + { + return new ConfigurationApiClient(serviceProvider, client, jsonSerializerOptions, defaultSectionOption, expandSectionOptions); + } + + public static IConfigurationApiManage CreateManage( + ICallerFactory callerFactory, + DccSectionOptions defaultSectionOption, + List? expandSectionOptions) + => new ConfigurationApiManage(callerFactory.CreateClient(), defaultSectionOption, expandSectionOptions); +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Model/Property.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Model/Property.cs new file mode 100644 index 000000000..760b9cc23 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Model/Property.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal.Model; + +internal class Property +{ + public string Key { get; set; } = default!; + + public string Value { get; set; } = default!; +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Model/PublishRelease.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Model/PublishRelease.cs new file mode 100644 index 000000000..b94b53372 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Model/PublishRelease.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal.Model; + +internal class PublishRelease +{ + public ConfigFormats ConfigFormat { get; set; } + + public string? Content { get; set; } +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Parser/JsonConfigurationParser.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Parser/JsonConfigurationParser.cs new file mode 100644 index 000000000..b23f71761 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Parser/JsonConfigurationParser.cs @@ -0,0 +1,101 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal.Parser; + +/// +/// https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Configuration.Json/src/JsonConfigurationFileParser.cs +/// +internal sealed class JsonConfigurationParser +{ + private JsonConfigurationParser() + { + } + + private readonly Dictionary _data = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Stack _paths = new Stack(); + + public static IDictionary Parse(string json) + => new JsonConfigurationParser().ParseJson(json); + + private IDictionary ParseJson(string json) + { + var jsonDocumentOptions = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + var doc = JsonDocument.Parse(json, jsonDocumentOptions); + + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + throw new FormatException($"[{doc.RootElement.ValueKind}]Invalid top level JsonElement."); + } + + VisitElement(doc.RootElement); + + return _data; + } + + private void VisitElement(JsonElement element) + { + var isEmpty = true; + + foreach (JsonProperty property in element.EnumerateObject()) + { + isEmpty = false; + EnterContext(property.Name); + VisitValue(property.Value); + ExitContext(); + } + + if (isEmpty && _paths.Count > 0) + { + _data[_paths.Peek()] = ""; + } + } + + private void VisitValue(JsonElement value) + { + Debug.Assert(_paths.Count > 0); + + switch (value.ValueKind) + { + case JsonValueKind.Object: + VisitElement(value); + break; + + case JsonValueKind.Array: + int index = 0; + foreach (JsonElement arrayElement in value.EnumerateArray()) + { + EnterContext(index.ToString()); + VisitValue(arrayElement); + ExitContext(); + index++; + } + + break; + + case JsonValueKind.Number: + case JsonValueKind.String: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + string key = _paths.Peek(); + if (_data.ContainsKey(key)) + { + throw new FormatException($"[{key}]key is duplicated."); + } + + _data[key] = value.ToString(); + break; + + default: + throw new FormatException($"[{value.ValueKind}]Unsupported json token."); + } + } + + private void EnterContext(string context) => + _paths.Push(_paths.Count > 0 ? _paths.Peek() + ConfigurationPath.KeyDelimiter + context : context); + + private void ExitContext() => _paths.Pop(); +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Parser/PropertyConfigurationParser.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Parser/PropertyConfigurationParser.cs new file mode 100644 index 000000000..fb05155cd --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Internal/Parser/PropertyConfigurationParser.cs @@ -0,0 +1,7 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Internal.Parser; + +internal class PropertyConfigurationParser +{ + public static IDictionary? Parse(string raw, JsonSerializerOptions serializerOption) + => JsonSerializer.Deserialize>(raw, serializerOption)?.ToDictionary(k => k.Key, v => v.Value); +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/MASA.Contrib.BasicAbility.Dcc.csproj b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/MASA.Contrib.BasicAbility.Dcc.csproj new file mode 100644 index 000000000..7830d62c8 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/MASA.Contrib.BasicAbility.Dcc.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/MasaConfigurationExtensions.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/MasaConfigurationExtensions.cs new file mode 100644 index 000000000..ac6c63bb9 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/MasaConfigurationExtensions.cs @@ -0,0 +1,204 @@ +namespace MASA.Contrib.BasicAbility.Dcc; + +public static class MasaConfigurationExtensions +{ + public static IMasaConfigurationBuilder UseDcc( + this IMasaConfigurationBuilder builder, + IServiceCollection services, + Action? jsonSerializerOptions = null, + Action? callerOptions = null) + => builder.UseDcc(services, "Appsettings", jsonSerializerOptions, callerOptions); + + public static IMasaConfigurationBuilder UseDcc( + this IMasaConfigurationBuilder builder, + IServiceCollection services, + string defaultSectionName, + Action? jsonSerializerOptions = null, + Action? callerOptions = null) + { + if (!builder.GetSectionRelations().TryGetValue(defaultSectionName, out IConfiguration? configuration)) + throw new ArgumentNullException("Failed to obtain Dcc configuration, check whether the current section is configured with Dcc"); + + var configurationSection = configuration.GetSection("DccOptions"); + var dccOptions = configurationSection.Get(); + + List expandSections = new(); + var configurationExpandSection = configuration.GetSection("ExpandSections"); + if (configurationExpandSection.Exists()) + { + configurationExpandSection.Bind(expandSections); + } + + return builder.UseDcc(services, () => dccOptions, option => + { + option.Environment = configuration["Environment"]; + option.Cluster = configuration["Cluster"]; + option.AppId = configuration["AppId"]; + option.ConfigObjects = configuration.GetSection("ConfigObjects").Get>(); + option.Secret = configuration["Sectet"]; + }, option => option.ExpandSections = expandSections, jsonSerializerOptions, callerOptions); + } + + public static IMasaConfigurationBuilder UseDcc( + this IMasaConfigurationBuilder builder, + IServiceCollection services, + Func configureOptions, + Action defaultSectionOptions, + Action? expansionSectionOptions = null, + Action? jsonSerializerOptions = null, + Action? callerOptions = null) + { + if (services.Any(service => service.ImplementationType == typeof(DccConfigurationProvider))) + return builder; + + services.AddSingleton(); + + var config = GetDccConfigurationOption(configureOptions, defaultSectionOptions, expansionSectionOptions); + + var jsonSerializerOption = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }; + jsonSerializerOptions?.Invoke(jsonSerializerOption); + services.AddCaller(options => + { + if (callerOptions == null) + { + options.UseHttpClient(() + => new MasaHttpClientBuilder(DEFAULT_CLIENT_NAME, string.Empty, opt => opt.BaseAddress = new Uri(config.DccConfigurationOption.ManageServiceAddress)) + ); + } + else + { + callerOptions.Invoke(options); + } + }); + + services.AddMasaRedisCache(DEFAULT_CLIENT_NAME, config.DccConfigurationOption.RedisOptions).AddSharedMasaMemoryCache(config.DccConfigurationOption.SubscribeKeyPrefix ?? DEFAULT_SUBSCRIBE_KEY_PREFIX); + + TryAddConfigurationApiClient(services, config.DefaultSectionOption, config.ExpansionSectionOptions, jsonSerializerOption); + TryAddConfigurationApiManage(services, config.DefaultSectionOption, config.ExpansionSectionOptions); + + var sectionOptions = new List() + { + config.DefaultSectionOption + }.Concat(config.ExpansionSectionOptions); + + var configurationApiClient = services.BuildServiceProvider().GetRequiredService(); + var loggerFactory = services.BuildServiceProvider().GetRequiredService(); + builder.AddRepository(new DccConfigurationRepository(sectionOptions, configurationApiClient, loggerFactory)); + return builder; + } + + public static IServiceCollection TryAddConfigurationApiClient(IServiceCollection services, + DccSectionOptions defaultSectionOption, + List expansionSectionOptions, + JsonSerializerOptions jsonSerializerOption) + { + services.TryAddSingleton(serviceProvider => + { + var client = serviceProvider.GetRequiredService() + .CreateClient(DEFAULT_CLIENT_NAME); + + if (client == null) + throw new ArgumentNullException(nameof(client)); + + return DccFactory.CreateClient( + serviceProvider, + client, + jsonSerializerOption, + defaultSectionOption, + expansionSectionOptions); + }); + return services; + } + + public static IServiceCollection TryAddConfigurationApiManage(IServiceCollection services, + DccSectionOptions defaultSectionOption, + List expansionSectionOptions) + { + services.TryAddSingleton(serviceProvider => + { + var callerFactory = serviceProvider.GetRequiredService(); + return DccFactory.CreateManage(callerFactory, defaultSectionOption, expansionSectionOptions); + }); + return services; + } + + private static (DccSectionOptions DefaultSectionOption, List ExpansionSectionOptions, DccConfigurationOptions DccConfigurationOption) GetDccConfigurationOption( + Func configureOptions, + Action defaultSectionOptions, + Action? expansionSectionOptions = null) + { + var dccConfigurationOption = configureOptions(); + if (dccConfigurationOption == null) + throw new ArgumentNullException(nameof(configureOptions)); + + if (string.IsNullOrEmpty(dccConfigurationOption.ManageServiceAddress)) + throw new ArgumentNullException(nameof(dccConfigurationOption.ManageServiceAddress)); + + if (dccConfigurationOption.RedisOptions == null) + throw new ArgumentNullException(nameof(dccConfigurationOption.RedisOptions)); + + if (dccConfigurationOption.RedisOptions.Servers == null || dccConfigurationOption.RedisOptions.Servers.Count == 0 || dccConfigurationOption.RedisOptions.Servers.Any(service => string.IsNullOrEmpty(service.Host) || service.Port <= 0)) + throw new ArgumentNullException(nameof(dccConfigurationOption.RedisOptions.Servers)); + + if (defaultSectionOptions == null) + throw new ArgumentNullException(nameof(defaultSectionOptions)); + + var defaultSectionOption = new DccSectionOptions(); + defaultSectionOptions.Invoke(defaultSectionOption); + + if (string.IsNullOrEmpty(defaultSectionOption.AppId)) + throw new ArgumentNullException("AppId cannot be empty"); + + if (defaultSectionOption.ConfigObjects == null || !defaultSectionOption.ConfigObjects.Any()) + throw new ArgumentNullException("ConfigObjects cannot be empty"); + + if (string.IsNullOrEmpty(defaultSectionOption.Cluster)) + defaultSectionOption.Cluster = "Default"; + if (string.IsNullOrEmpty(defaultSectionOption.Environment)) + defaultSectionOption.Environment = GetDefaultEnvironment(); + + var dccCachingOption = new DccExpandSectionOptions(); + expansionSectionOptions?.Invoke(dccCachingOption); + List expansionOptions = new(); + foreach (var expansionOption in dccCachingOption.ExpandSections ?? new()) + { + if (string.IsNullOrEmpty(expansionOption.Environment)) + expansionOption.Environment = defaultSectionOption.Environment; + if (string.IsNullOrEmpty(expansionOption.Cluster)) + expansionOption.Cluster = defaultSectionOption.Cluster; + + if (expansionOption.ConfigObjects == null || !expansionOption.ConfigObjects.Any()) + throw new ArgumentNullException("ConfigObjects in the extension section cannot be empty"); + + if (expansionOption.AppId == defaultSectionOption.AppId || + expansionOptions.Any(section => section.AppId == expansionOption.AppId)) + throw new ArgumentNullException("The current section already exists, no need to mount repeatedly"); + + expansionOptions.Add(expansionOption); + } + return (defaultSectionOption, expansionOptions, dccConfigurationOption); + } + + private static ICachingBuilder AddSharedMasaMemoryCache(this ICachingBuilder builder, string subscribeKeyPrefix) + { + builder.AddMasaMemoryCache(options => + { + options.SubscribeKeyType = SubscribeKeyTypes.SpecificPrefix; + options.SubscribeKeyPrefix = subscribeKeyPrefix; + }); + + return builder; + } + + private static string GetDefaultEnvironment() + => System.Environment.GetEnvironmentVariable(DEFAULT_ENVIRONMENT_NAME) ?? + throw new ArgumentNullException("Error getting environment information, please make sure the value of ASPNETCORE_ENVIRONMENT has been configured"); + + private class DccConfigurationProvider + { + + } +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccConfigurationOptions.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccConfigurationOptions.cs new file mode 100644 index 000000000..6ee6baf4f --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccConfigurationOptions.cs @@ -0,0 +1,13 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Options; + +public class DccConfigurationOptions +{ + public RedisConfigurationOptions RedisOptions { get; set; } + + public string ManageServiceAddress { get; set; } = default!; + + /// + /// The prefix of Dcc PubSub, it is not recommended to modify + /// + public string? SubscribeKeyPrefix { get; set; } +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccExpandSectionOptions.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccExpandSectionOptions.cs new file mode 100644 index 000000000..e9629637c --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccExpandSectionOptions.cs @@ -0,0 +1,9 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Options; + +public class DccExpandSectionOptions +{ + /// + /// Expansion section information + /// + public List? ExpandSections { get; set; } +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccSectionOptions.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccSectionOptions.cs new file mode 100644 index 000000000..fc905907e --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/Options/DccSectionOptions.cs @@ -0,0 +1,24 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Options; + +public class DccSectionOptions +{ + /// + /// The environment name. + /// Get from the environment variable ASPNETCORE_ENVIRONMENT when Environment is null or empty + /// + public string? Environment { get; set; } = null; + + /// + /// The cluster name. + /// + public string? Cluster { get; set; } + + /// + /// The app id. + /// + public string AppId { get; set; } = default!; + + public List ConfigObjects { get; set; } = default!; + + public string? Secret { get; set; } +} diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/README.md b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/README.md new file mode 100644 index 000000000..d74017c3a --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/README.md @@ -0,0 +1,164 @@ +[中](README.zh-CN.md) | EN + +## MASA.Contrib.BasicAbility.Dcc + +Effect: + +Extend the ability of IConfiguration to manage remote configuration through Dcc. + +```c# +IConfiguration +├── Local Local node (fixed) +├── ConfigurationAPI Remote node (fixed Dcc to expand its capacity) +│ ├── AppId Replace-With-Your-AppId +│ ├── AppId ├── Platforms Custom node +│ ├── AppId ├── Platforms ├── Name Parameter Name +│ ├── AppId ├── DataDictionary Dictionary (fixed) The type of Text in DCC is mounted here +``` + +Example: + +```C# +Install-Package MASA.Contrib.Configuration +Install-Package MASA.Contrib.BasicAbility.Dcc //Provides the ability to remotely configure +``` + +appsettings.json +``` +{ + //Dcc configuration, extended Configuration capabilities, support remote configuration + "DccOptions": { + "ManageServiceAddress": "http://localhost:8890", + "RedisOptions": { + "Servers": [ + { + "Host": "localhost", + "Port": 8889 + } + ], + "DefaultDatabase": 0, + "Password": "" + } + }, + "AppId": "Replace-With-Your-AppId", + "Environment": "Development", + "ConfigObjects": [ "Platforms" ], //The name of the object to be mounted, the Platforms configuration will be mounted here under the ConfigurationAPI: node + "Secret": "", //Dcc App key + "Cluster": "Default" +} + +``` + +```C# +builder.AddMasaConfiguration(configurationBuilder => +{ + configurationBuilder.UseDcc(builder.Services);//Use Dcc + + options.Mapping(SectionTypes.Local, "Appsettings", ""); //Map CustomDccSectionOptions to the Appsettings node under Local +}); + +/// +/// Automatically map node relationships +/// +public class PlatformOptions : MasaConfigurationOptions +{ + public override SectionTypes SectionType { get; init; } = SectionTypes.ConfigurationAPI; + + [JsonIgnore] + public virtual string? ParentSection { get; init; } = "AppId"; + + [JsonIgnore] + public virtual string? Section { get; init; } = "Platforms"; + + public string Name { get; set; } +} + +public class CustomDccSectionOptions +{ + /// + /// The environment name. + /// Get from the environment variable ASPNETCORE_ENVIRONMENT when Environment is null or empty + /// + public string? Environment { get; set; } = null; + + /// + /// The cluster name. + /// + public string? Cluster { get; set; } + + /// + /// The app id. + /// + public string AppId { get; set; } = default!; + + public List ConfigObjects { get; set; } = default!; + + public string? Secret { get; set; } +} +``` + +How to use configuration: + +```c# +var app = builder.Build(); + +app.MapGet("/GetPlatform", ([FromServices] IOptions option) => +{ + //recommend + return System.Text.Json.JsonSerializer.Serialize(option.Value);//Or use IOptionsMonitor to support monitoring changes +}); + +app.MapGet("/GetPlatformByMonitor", ([FromServices] IOptionsMonitor options) => +{ + options.OnChange(option => + { + //TODO Configuration update + }); + return System.Text.Json.JsonSerializer.Serialize(option.CurrentValue); +}); + +app.MapGet("/GetPlatformName", ([FromServices] IConfiguration configuration) => +{ + //Format ConfigurationAPI::: + return configuration["ConfigurationAPI::Platforms:Name"]; +}); + +app.MapPut("/UpdatePlatform", ([FromServices] IConfigurationAPIManage configurationAPIManage, + [FromServices] IOptions configuration, + PlatformOptions newPlatform) => +{ + //Modify Dcc configuration + return configurationAPIManage.UpdateAsync(option.Value.Environment, + option.Value.Cluster, + option.Value.AppId, + "",newPlatform);//Here Replace-With-Your-ConfigObject is Platforms +}); +app.Run(); +``` + +How to update the configuration: + +```c# +var app = builder.Build(); + +app.MapPut("/UpdatePlatform", ([FromServices] IConfigurationAPIManage configurationAPIManage, + [FromServices] IOptions configuration, + PlatformOptions newPlatform) => +{ + //Modify Dcc configuration + return configurationAPIManage.UpdateAsync(option.Value.Environment, + option.Value.Cluster, + option.Value.AppId, + "" + ,newPlatform); + //Here Replace-With-Your-ConfigObject is Platforms +}); + +app.Run(); +``` + +Summarize: + +Dcc provides remote configuration management and viewing capabilities for IConfiguration. For the complete capabilities of IConfiguration, please refer to the [document](../../Configuration/MASA.Contrib.Configuration/README.md) + +Platforms here is remote configuration, which introduces the effect and usage of remote configuration after mounting to IConfiguration. This configuration has nothing to do with Platforms in MASA.Contrib.Configuration. It just shows the use of the same configuration information in two sources. Ways and differences in mapping node relationships \ No newline at end of file diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/README.zh-CN.md b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/README.zh-CN.md new file mode 100644 index 000000000..d55b20ac9 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/README.zh-CN.md @@ -0,0 +1,155 @@ +中 | [EN](README.md) + +## MASA.Contrib.BasicAbility.Dcc + +作用: + +通过Dcc扩展IConfiguration管理远程配置的能力。 + +```c# +IConfiguration +├── Local 本地节点(固定) +├── ConfigurationAPI 远程节点(固定 Dcc扩展其能力) +│ ├── AppId Replace-With-Your-AppId +│ ├── AppId ├── Platforms 自定义节点 +│ ├── AppId ├── Platforms ├── Name 参数 +│ ├── AppId ├── DataDictionary 字典(固定)DCC中类型为Text的挂载到此处 +``` + +用例: + +```C# +Install-Package MASA.Contrib.Configuration +Install-Package MASA.Contrib.BasicAbility.Dcc //提供远程配置的能力 +``` + +appsettings.json +``` +{ + //Dcc配置,扩展Configuration能力,支持远程配置 + "DccOptions": { + "ManageServiceAddress ": "http://localhost:8890", + "RedisOptions": { + "Servers": [ + { + "Host": "localhost", + "Port": 8889 + } + ], + "DefaultDatabase": 0, + "Password": "" + } + }, + "AppId": "Replace-With-Your-AppId", + "Environment": "Development", + "ConfigObjects": [ "Platforms" ], //待挂载的对象名, 此处会将Platforms配置挂载到ConfigurationAPI:节点下 + "Secret": "", //Dcc App 秘钥 + "Cluster": "Default" +} + +``` + +```C# +builder.AddMasaConfiguration(configurationBuilder => +{ + configurationBuilder.UseDcc(builder.Services);//使用Dcc提供远程配置的能力 + + options.Mapping(SectionTypes.Local, "Appsettings", ""); //将CustomDccSectionOptions映射到Local下的Appsettings节点 +}); + +/// +/// 自动映射节点关系 +/// +public class PlatformOptions : MasaConfigurationOptions +{ + public override SectionTypes SectionType { get; init; } = SectionTypes.ConfigurationAPI; + + [JsonIgnore] + public virtual string? ParentSection { get; init; } = "Replace-With-Your-AppId"; + + [JsonIgnore] + public virtual string? Section { get; init; } = "Platforms"; + + public string Name { get; set; } +} + +public class CustomDccSectionOptions +{ + /// + /// The environment name. + /// Get from the environment variable ASPNETCORE_ENVIRONMENT when Environment is null or empty + /// + public string? Environment { get; set; } = null; + + /// + /// The cluster name. + /// + public string? Cluster { get; set; } + + /// + /// The app id. + /// + public string AppId { get; set; } = default!; + + public List ConfigObjects { get; set; } = default!; + + public string? Secret { get; set; } +} +``` + +如何使用配置: + +```c# +var app = builder.Build(); + +app.MapGet("/GetPlatform", ([FromServices] IOptions option) => +{ + //推荐 + return System.Text.Json.JsonSerializer.Serialize(option.Value); +}); + +app.MapGet("/GetPlatformByMonitor", ([FromServices] IOptionsMonitor options) => +{ + options.OnChange(option => + { + //TODO 配置更新 + }); + return System.Text.Json.JsonSerializer.Serialize(option.CurrentValue); +}); + +app.MapGet("/GetPlatformName", ([FromServices] IConfiguration configuration) => +{ + //格式:ConfigurationAPI::: + return configuration["ConfigurationAPI::Platforms:Name"]; +}); + +app.Run(); +``` + +如何更新配置 + + +```c# +var app = builder.Build(); + +app.MapPut("/UpdatePlatform", ([FromServices] IConfigurationAPIManage configurationAPIManage, + [FromServices] IOptions configuration, + PlatformOptions newPlatform) => +{ + //修改Dcc配置 + return configurationAPIManage.UpdateAsync(option.Value.Environment, + option.Value.Cluster, + option.Value.AppId, + "" + ,newPlatform); + //此处Replace-With-Your-ConfigObject是Platforms +}); + +app.Run(); +``` + +总结: + +Dcc为IConfiguration提供了远程配置的管理以及查看能力,IConfiguration完整的能力请查看[文档](../../Configuration/MASA.Contrib.Configuration/README.zh-CN.md) + +此处Platforms为远程配置,介绍的是远程配置挂载到IConfiguration之后的效果以及用法,此配置与MASA.Contrib.Configuration中Platforms的毫无关系,仅仅是展示同一个配置信息在两个源的使用方式以及映射节点关系的差别 \ No newline at end of file diff --git a/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/_Imports.cs b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/_Imports.cs new file mode 100644 index 000000000..a25cf5516 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/_Imports.cs @@ -0,0 +1,24 @@ +global using MASA.BuildingBlocks.Configuration; +global using MASA.Contrib.BasicAbility.Dcc.Internal; +global using MASA.Contrib.BasicAbility.Dcc.Internal.Model; +global using MASA.Contrib.BasicAbility.Dcc.Internal.Parser; +global using MASA.Contrib.BasicAbility.Dcc.Options; +global using MASA.Utils.Caching.Core.DependencyInjection; +global using MASA.Utils.Caching.Core.Models; +global using MASA.Utils.Caching.DistributedMemory.DependencyInjection; +global using MASA.Utils.Caching.DistributedMemory.Interfaces; +global using MASA.Utils.Caching.Redis.DependencyInjection; +global using MASA.Utils.Caching.Redis.Extensions; +global using MASA.Utils.Caching.Redis.Models; +global using MASA.Utils.Caller.Core; +global using MASA.Utils.Caller.HttpClient; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; +global using System.Collections.Concurrent; +global using System.Diagnostics; +global using System.Dynamic; +global using System.Text.Json; +global using static MASA.Contrib.BasicAbility.Dcc.Internal.Constants; + diff --git a/src/BuildingBlocks/MASA.BuildingBlocks b/src/BuildingBlocks/MASA.BuildingBlocks new file mode 160000 index 000000000..af4058aed --- /dev/null +++ b/src/BuildingBlocks/MASA.BuildingBlocks @@ -0,0 +1 @@ +Subproject commit af4058aede124ed29f82d57b903527b6e2ee4ab5 diff --git a/src/Configuration/MASA.Contrib.Configuration/LocalMasaConfigurationRepository.cs b/src/Configuration/MASA.Contrib.Configuration/LocalMasaConfigurationRepository.cs new file mode 100644 index 000000000..8bd2b27a0 --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/LocalMasaConfigurationRepository.cs @@ -0,0 +1,71 @@ +namespace MASA.Contrib.Configuration; + +internal class LocalMasaConfigurationRepository : AbstractConfigurationRepository +{ + public override SectionTypes SectionType { get; init; } + + private ConcurrentDictionary _data = new(); + + public LocalMasaConfigurationRepository( + Dictionary sectionRelation, + ILoggerFactory? loggerFactory) + : base(loggerFactory) + { + this.SectionType = SectionTypes.Local; + foreach (var section in sectionRelation) + { + Initialize(section.Key, section.Value); + + ChangeToken.OnChange(() => section.Value.GetReloadToken(), () => + { + Initialize(section.Key, section.Value); + base.FireRepositoryChange(SectionType, Load()); + }); + } + } + + private void Initialize(string rootSectionName, IConfiguration configuration) + { + Dictionary data = new(); + GetData(rootSectionName, configuration, configuration.GetChildren(), ref data); + var properties = new Properties(data); + _data[rootSectionName] = properties; + } + + private void GetData(string rootSectionName, IConfiguration configuration, IEnumerable configurationSections, ref Dictionary dictionary) + { + foreach (var configurationSection in configurationSections) + { + var section = configuration.GetSection(configurationSection.Path); + + var childrenSections = section.GetChildren(); + + if (!section.Exists() || !childrenSections.Any()) + { + var key = string.IsNullOrEmpty(rootSectionName) ? section.Path : $"{rootSectionName}{ConfigurationPath.KeyDelimiter}{section.Path}"; + if (!dictionary.ContainsKey(key)) + { + dictionary.Add(key, configuration[section.Path]); + } + } + else + { + GetData(rootSectionName, configuration, childrenSections, ref dictionary); + } + } + } + + public override Properties Load() + { + Dictionary localProperties = new(); + foreach (var item in _data) + { + foreach (var key in item.Value.GetPropertyNames()) + { + localProperties[key] = item.Value.GetProperty(key) ?? string.Empty; + } + } + return new Properties(localProperties); + } +} + diff --git a/src/Configuration/MASA.Contrib.Configuration/MASA.Contrib.Configuration.csproj b/src/Configuration/MASA.Contrib.Configuration/MASA.Contrib.Configuration.csproj new file mode 100644 index 000000000..d231705d6 --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/MASA.Contrib.Configuration.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationBuilder.cs b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationBuilder.cs new file mode 100644 index 000000000..fd8294c4a --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationBuilder.cs @@ -0,0 +1,52 @@ +namespace MASA.Contrib.Configuration; + +public class MasaConfigurationBuilder : IMasaConfigurationBuilder +{ + private readonly IConfigurationBuilder _builder; + + public IDictionary Properties => _builder.Properties; + + public IList Sources => _builder.Sources; + + public Dictionary GetSectionRelations() => SectionRelations; + + internal Dictionary SectionRelations { get; } = new(); + + internal List Repositories { get; } = new(); + + internal List Relations { get; } = new(); + + public MasaConfigurationBuilder(IConfigurationBuilder builder) + => _builder = builder; + + /// + /// + /// + /// + /// If section is null, it is mounted to the Local section + public void AddSection(IConfigurationBuilder configurationBuilder, string? sectionName = null) + { + if (configurationBuilder == null) + throw new ArgumentNullException(nameof(configurationBuilder)); + + if (configurationBuilder.Sources.Count == 0) + throw new ArgumentException("Source cannot be empty"); + + sectionName = sectionName ?? ""; + + if (SectionRelations.ContainsKey(sectionName)) + throw new ArgumentException("Section already exists", nameof(sectionName)); + + SectionRelations.Add(sectionName, configurationBuilder.Build()); + } + + public void AddRepository(IConfigurationRepository configurationRepository) + => Repositories.Add(configurationRepository); + + public void AddRelations(params ConfigurationRelationOptions[] relationOptions) + => Relations.AddRange(relationOptions); + + public IConfigurationBuilder Add(IConfigurationSource source) => _builder.Add(source); + + public IConfigurationRoot Build() => _builder.Build(); +} diff --git a/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationExtensions.cs b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationExtensions.cs new file mode 100644 index 000000000..09552cf5c --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationExtensions.cs @@ -0,0 +1,35 @@ +namespace MASA.Contrib.Configuration; + +public static class MasaConfigurationExtensions +{ + public static void UseMasaOptions(this IMasaConfigurationBuilder builder, Action options) + { + var relation = new MasaRelationOptions(); + options.Invoke(relation); + builder.AddRelations(relation.Relations.ToArray()); + } + + internal static void AutoMapping(this MasaConfigurationBuilder builder, params Assembly[] assemblies) + { + var optionTypes = assemblies + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type != typeof(IMasaConfigurationOptions) && type != typeof(MasaConfigurationOptions) && typeof(IMasaConfigurationOptions).IsAssignableFrom(type)) + .ToList(); + optionTypes.ForEach(optionType => + { + var option = (IMasaConfigurationOptions)Activator.CreateInstance(optionType)!; + var sectionName = option.Section ?? optionType.Name; + if (builder.Relations.Any(relation => relation.SectionType == option.SectionType && relation.Section == sectionName)) + { + throw new ArgumentException("The section has been loaded, no need to load repeatedly, check whether there are duplicate sections or inheritance between auto-mapping classes"); + } + builder.AddRelations(new ConfigurationRelationOptions() + { + SectionType = option.SectionType, + ParentSection = option.ParentSection, + Section = sectionName, + ObjectType = optionType + }); + }); + } +} diff --git a/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationOptions.cs b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationOptions.cs new file mode 100644 index 000000000..09e2fa6a7 --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationOptions.cs @@ -0,0 +1,19 @@ +namespace MASA.Contrib.Configuration; + +public abstract class MasaConfigurationOptions : IMasaConfigurationOptions +{ + /// + /// The name of the parent section, if it is empty, it will be mounted under SectionType, otherwise it will be mounted to the specified section under SectionType + /// + [JsonIgnore] + public virtual string? ParentSection { get; init; } = null; + + /// + /// The section null means same as the class name, else load from the specify section + /// + [JsonIgnore] + public virtual string? Section { get; init; } = null; + + [JsonIgnore] + public abstract SectionTypes SectionType { get; init; } +} diff --git a/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationProvider.cs b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationProvider.cs new file mode 100644 index 000000000..f4ba2b5cb --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationProvider.cs @@ -0,0 +1,64 @@ +namespace MASA.Contrib.Configuration; + +public class MasaConfigurationProvider : ConfigurationProvider, IRepositoryChangeListener, IDisposable +{ + private readonly ConcurrentDictionary _data; + private readonly IEnumerable _configurationRepositories; + + public MasaConfigurationProvider(MasaConfigurationSource source) + { + _data = new(); + _configurationRepositories = source.Builder.Repositories; + + foreach (var configurationRepository in _configurationRepositories) + { + configurationRepository.AddChangeListener(this); + } + } + + public override void Load() + { + foreach (var configurationRepository in _configurationRepositories) + { + var properties = configurationRepository.Load(); + _data[configurationRepository.SectionType] = properties; + } + SetData(); + } + + public void OnRepositoryChange(SectionTypes sectionType, Properties newProperties) + { + if (_data[sectionType] == newProperties) + return; + + _data[sectionType] = newProperties; + + SetData(); + + OnReload(); + } + + void SetData() + { + Dictionary data = new(); + + foreach (var configurationType in _data.Keys) + { + var properties = _data[configurationType]; + foreach (var key in properties.GetPropertyNames()) + { + data[$"{configurationType}{ConfigurationPath.KeyDelimiter}{key}"] = properties.GetProperty(key)!; + } + } + + Data = data; + } + + public void Dispose() + { + foreach (var configurationRepository in _configurationRepositories) + { + configurationRepository.RemoveChangeListener(this); + } + } +} diff --git a/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationSource.cs b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationSource.cs new file mode 100644 index 000000000..7f57cf17e --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/MasaConfigurationSource.cs @@ -0,0 +1,17 @@ +namespace MASA.Contrib.Configuration; + +public class MasaConfigurationSource : IConfigurationSource +{ + internal MasaConfigurationBuilder Builder; + + public MasaConfigurationSource(MasaConfigurationBuilder builder) + { + Builder = builder; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new MasaConfigurationProvider(this); + } +} + diff --git a/src/Configuration/MASA.Contrib.Configuration/MasaRelationOptions.cs b/src/Configuration/MASA.Contrib.Configuration/MasaRelationOptions.cs new file mode 100644 index 000000000..b521c4efa --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/MasaRelationOptions.cs @@ -0,0 +1,33 @@ +namespace MASA.Contrib.Configuration; + +public class MasaRelationOptions +{ + internal List Relations { get; } = new(); + + /// + /// Map Section relationship + /// + /// + /// + /// parent section, local section is the name of the locally configured section, and ConfigurationAPI is the name of the Appid where the configuration is located + /// The default is null, which is consistent with the mapping class name + /// + /// + public MasaRelationOptions Mapping(SectionTypes sectionType, string parentSection, string? section = null) + { + if (section == null) + section = typeof(TModel).Name; + + if (Relations.Any(relation => relation.SectionType == sectionType && relation.Section == section)) + throw new ArgumentOutOfRangeException(nameof(section), "The current section already has a configuration"); + + Relations.Add(new ConfigurationRelationOptions() + { + SectionType = sectionType, + ParentSection = parentSection, + Section = section, + ObjectType = typeof(TModel) + }); + return this; + } +} diff --git a/src/Configuration/MASA.Contrib.Configuration/README.md b/src/Configuration/MASA.Contrib.Configuration/README.md new file mode 100644 index 000000000..921dd0058 --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/README.md @@ -0,0 +1,135 @@ +[中](README.zh-CN.md) | EN + +## MASA.Contrib.Configuration + +Structure: + +```c# +IConfiguration +├── Local Local node (fixed) +│ ├── Appsettings Default local node +│ ├── ├── Platforms Custom configuration +│ ├── ├── ├── Name Parameter name +├── ConfigurationAPI Remote node (fixed) +│ ├── AppId Replace-With-Your-AppId +│ ├── AppId ├── Platforms Custom node +│ ├── AppId ├── Platforms ├── Name Parameter name +│ ├── AppId ├── DataDictionary Dictionary (fixed) +``` + +Example: + +```C# +Install-Package MASA.Contrib.Configuration +Install-Package MASA.Contrib.BasicAbility.Dcc //DCC can provide remote configuration capabilities +```json +{ + //Custom configuration + "Platforms": { + "Name": "Masa.Demo" + }, + //Dcc configuration, extended Configuration capabilities, support remote configuration + "DccOptions": { + "ManageServiceAddress": "http://localhost:8890", + "RedisOptions": { + "Servers": [ + { + "Host": "localhost", + "Port": 8889 + } + ], + "DefaultDatabase": 0, + "Password": "" + } + }, + "AppId": "Replace-With-Your-AppId", + "ConfigObjects": [ "Platforms" ], //The name of the object to be mounted. Here, the Platforms configuration will be mounted under the ConfigurationAPI: node + "Secret": "", //Dcc App key + "Cluster": "Default" +} +``` + +Automatically map node relationships: + +```c# +public class PlatformOptions : MasaConfigurationOptions +{ + public override SectionTypes SectionType { get; init; } = SectionTypes.Local; + + [JsonIgnore] + public override string? ParentSection { get; init; } = "Appsettings"; + + [JsonIgnore] + public override string? Section { get; init; } = "Platforms"; + + public string Name { get; set; } +} + +//Use MasaConfiguration to take over Configuration, and mount the current Configuration to Local:Appsettings section by default +builder.AddMasaConfiguration(configurationBuilder => +{ + //configurationBuilder.UseDcc(builder.Services);//Use Dcc to extend Configuration capabilities and support remote configuration +}); +``` + +Or manually map node relationships: + +```C# +builder.AddMasaConfiguration(configurationBuilder => +{ + //configurationBuilder.UseDcc(builder.Services);//Use Dcc to extend Configuration capabilities and support remote configuration + + configurationBuilder.UseMasaOptions(options => + { + options.Mapping(SectionTypes.Local, "Appsettings", "Platforms"); //Map the PlatformOptions binding to the Local:Appsettings:Platforms node + }); +}); +``` + +how to use: + +```c# +var app = builder.Build(); + +app.Map("/GetPlatform", ([FromServices] IOptions option) => +{ + //Recommended (need to automatically or manually map the node relationship before it can be used) + return System.Text.Json.JsonSerializer.Serialize(option.Value); +}); + +app.Map("/GetPlatform", ([FromServices] IOptionsMonitor option) => +{ + //Recommended (need to automatically or manually map the node relationship before it can be used) + options.OnChange(option => + { + //TODO Configuration update service + }); + + return System.Text.Json.JsonSerializer.Serialize(option.CurrentValue); +}); + +app.Map("/GetPlatformName", ([FromServices] IConfiguration configuration) => +{ + //Base + return configuration["Local:Appsettings:Platforms:Name"]; +}); + +app.Run(); +``` + +How to take over more local nodes? + +```c# +builder.AddMasaConfiguration(builder =>{ + + builder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("custom.json", true, true), "Custom");//Mount the custom.json configuration under the Local:Custom node +}); +``` + +Tip: + +Configuration automatically obtains classes that inherit the IMasaConfigurationOptions interface by default, and maps the node relationship to facilitate obtaining configuration information through IOptions, IOptionsSnapshot, and IOptionsMonitor + +The above Platforms is a local configuration, used to demonstrate the effect and usage of the local configuration after being mounted to IConfiguration \ No newline at end of file diff --git a/src/Configuration/MASA.Contrib.Configuration/README.zh-CN.md b/src/Configuration/MASA.Contrib.Configuration/README.zh-CN.md new file mode 100644 index 000000000..0bd1a1830 --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/README.zh-CN.md @@ -0,0 +1,140 @@ +中 | [EN](README.md) + +## MASA.Contrib.Configuration + +结构: + +```c# +IConfiguration +├── Local 本地节点(固定) +│ ├── Appsettings 默认本地节点 +│ ├── ├── Platforms 自定义配置 +│ ├── ├── ├── Name 参数 +├── ConfigurationAPI 远程节点(固定) +│ ├── AppId Replace-With-Your-AppId +│ ├── AppId ├── Platforms 自定义节点 +│ ├── AppId ├── Platforms ├── Name 参数 +│ ├── AppId ├── DataDictionary 字典(固定) +``` + +用例: + +```C# +Install-Package MASA.Contrib.Configuration +Install-Package MASA.Contrib.BasicAbility.Dcc //DCC可提供远程配置的能力 +``` + +appsettings.json +```json +{ + //自定义配置 + "Platforms": { + "Name": "Masa.Demo" + }, + //Dcc配置,扩展Configuration能力,支持远程配置 + "DccOptions": { + "ManageServiceAddress ": "http://localhost:8890", + "RedisOptions": { + "Servers": [ + { + "Host": "localhost", + "Port": 8889 + } + ], + "DefaultDatabase": 0, + "Password": "" + } + }, + "AppId": "Replace-With-Your-AppId", + "ConfigObjects": [ "Platforms" ], // 要挂载的对象名称,此处会将Platforms配置挂载到ConfigurationAPI:节点下 + "Secret": "", //Dcc App 秘钥 + "Cluster": "Default" +} +``` + +自动映射节点关系: + +```c# +/// +/// 自动映射节点关系 +/// +public class PlatformOptions : MasaConfigurationOptions +{ + public override SectionTypes SectionType { get; init; } = SectionTypes.Local; + + [JsonIgnore] + public override string? ParentSection { get; init; } = "Appsettings"; + + [JsonIgnore] + public override string? Section { get; init; } = "Platforms"; + + public string Name { get; set; } +} + +//使用MasaConfiguration接管Configuration,默认会将当前的Configuration挂载到Local:Appsettings节点 +builder.AddMasaConfiguration(configurationBuilder => +{ + //configurationBuilder.UseDcc(builder.Services);//使用Dcc 扩展Configuration能力,支持远程配置 +}); +``` + +或手动添加映射节点关系: + +```C# +builder.AddMasaConfiguration(configurationBuilder => +{ + //configurationBuilder.UseDcc(builder.Services);//使用Dcc 扩展Configuration能力,支持远程配置 + + configurationBuilder.UseMasaOptions(options => + { + options.Mapping(SectionTypes.Local, "Appsettings", "Platforms"); //将PlatformOptions绑定映射到Local:Appsettings:Platforms节点 + }); +}); +``` + +如何使用: + +```c# +var app = builder.Build(); + +app.Map("/GetPlatform", ([FromServices] IOptions option) => +{ + //推荐(需要自动或手动映射节点关系后才能使用) + return System.Text.Json.JsonSerializer.Serialize(option.Value); +}); + +app.Map("/GetPlatform", ([FromServices] IOptionsMonitor option) => +{ + options.OnChange(option => + { + //TODO 配置更新业务 + }); + + return System.Text.Json.JsonSerializer.Serialize(option.CurrentValue); +});//推荐(需要自动或手动映射节点关系后才能使用) + +app.Map("/GetPlatformName", ([FromServices] IConfiguration configuration) => +{ + //基础 + return configuration["Local:Appsettings:Platforms:Name"]; +}); + +app.Run(); +``` + +如何接管更多的本地节点? + +```c# +builder.AddMasaConfiguration(builder =>{ + + builder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("custom.json", true, true), "Custom");//将custom.json配置挂载到Local:Custom节点下 +}); +``` + +提示: + +Configuration默认自动获取继承IMasaConfigurationOptions接口的类,并映射节点关系,方便通过IOptions、IOptionsSnapshot、IOptionsMonitor获取配置信息 + +上文Platforms为本地配置,用于演示本地配置挂载到IConfiguration后的效果以及使用用法 \ No newline at end of file diff --git a/src/Configuration/MASA.Contrib.Configuration/ServiceCollectionExtensions.cs b/src/Configuration/MASA.Contrib.Configuration/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..b06ea09b4 --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/ServiceCollectionExtensions.cs @@ -0,0 +1,138 @@ +namespace MASA.Contrib.Configuration; + +public static class ServiceCollectionExtensions +{ + public static WebApplicationBuilder AddMasaConfiguration( + this WebApplicationBuilder builder, + Action? configureDelegate = null) + => builder.AddMasaConfiguration(configureDelegate, + "Appsettings", + AppDomain.CurrentDomain.GetAssemblies()); + + public static WebApplicationBuilder AddMasaConfiguration( + this WebApplicationBuilder builder, + Action? configureDelegate, + string defaultSectionName = "Appsettings", + params Assembly[] assemblies) + { + var configurationBuilder = GetConfigurationBuilder(builder.Configuration); + + IConfigurationRoot masaConfiguration = builder.Services.CreateMasaConfiguration(configureDelegate, configurationBuilder, defaultSectionName, assemblies); + if (!masaConfiguration.Providers.Any()) + return builder; + + Microsoft.Extensions.Hosting.HostingHostBuilderExtensions.ConfigureAppConfiguration(builder.Host, configBuilder => + { + configBuilder.Sources.Clear(); + }); + builder.Configuration.AddConfiguration(masaConfiguration); + + return builder; + } + + public static IConfigurationRoot CreateMasaConfiguration( + this IServiceCollection services, + Action? configureDelegate, + IConfigurationBuilder? configurationBuilder = null, + string defaultSectionName = "Appsettings", + params Assembly[] assemblies) + { + if (services.Any(service => service.ImplementationType == typeof(MasaConfigurationProvider))) + return new ConfigurationBuilder().Build(); + + services.AddSingleton(); + services.AddOptions(); + + MasaConfigurationBuilder masaConfigurationBuilder = new MasaConfigurationBuilder(new ConfigurationBuilder()); + if (configurationBuilder != null) + { + masaConfigurationBuilder.AddSection(configurationBuilder, defaultSectionName); + } + configureDelegate?.Invoke(masaConfigurationBuilder); + + if (masaConfigurationBuilder.SectionRelations.Count == 0) + throw new Exception("Please add the section to be loaded"); + + var localConfigurationRepository = new LocalMasaConfigurationRepository(masaConfigurationBuilder.SectionRelations, services.BuildServiceProvider().GetService()); + masaConfigurationBuilder.AddRepository(localConfigurationRepository); + + var source = new MasaConfigurationSource(masaConfigurationBuilder); + var configuration = masaConfigurationBuilder.Add(source).Build(); + + masaConfigurationBuilder.AutoMapping(assemblies); + masaConfigurationBuilder.Relations.ForEach(relation => + { + List sectionNames = new List() + { + relation.SectionType.ToString(), + }; + if (!string.IsNullOrEmpty(relation.ParentSection)) + sectionNames.Add(relation.ParentSection); + + if (relation.Section != "") + { + sectionNames.AddRange(relation.Section.Split(ConfigurationPath.KeyDelimiter)); + } + + services.ConfigureOption(configuration, sectionNames, relation.ObjectType); + }); + + return configuration; + } + + private static IConfigurationBuilder GetConfigurationBuilder(ConfigurationManager configuration) + { + var configurationBuilder = new ConfigurationBuilder(); + foreach (var source in ((IConfigurationBuilder)configuration).Sources) + { + configurationBuilder.Add(source); + } + return configurationBuilder; + } + + private static void ClearSource(this WebApplicationBuilder builder) + { + Microsoft.Extensions.Hosting.HostingHostBuilderExtensions.ConfigureAppConfiguration(builder.Host, configBuilder => + { + configBuilder.Sources.Clear(); + }); + } + + internal static void ConfigureOption( + this IServiceCollection services, + IConfiguration configuration, + List sectionNames, Type optionType) + { + IConfigurationSection? configurationSection = null; + foreach (var sectionName in sectionNames) + { + if (configurationSection == null) + configurationSection = configuration.GetSection(sectionName); + else + configurationSection = configurationSection.GetSection(sectionName); + } + if (!configurationSection.Exists()) + { + throw new ArgumentNullException("Section", "Check if the mapping section is correct"); + } + + var configurationChangeTokenSource = + Activator.CreateInstance(typeof(ConfigurationChangeTokenSource<>).MakeGenericType(optionType), string.Empty, + configurationSection)!; + services.TryAdd(new ServiceDescriptor(typeof(IOptionsChangeTokenSource<>).MakeGenericType(optionType), + configurationChangeTokenSource)); + + Action configureBinder = _ => { }; + var configureOptions = + Activator.CreateInstance(typeof(NamedConfigureFromConfigurationOptions<>).MakeGenericType(optionType), + string.Empty, + configurationSection, configureBinder)!; + services.TryAdd(new ServiceDescriptor(typeof(IConfigureOptions<>).MakeGenericType(optionType), + configureOptions)); + } + + private class MasaConfigurationProvider + { + + } +} diff --git a/src/Configuration/MASA.Contrib.Configuration/_Imports.cs b/src/Configuration/MASA.Contrib.Configuration/_Imports.cs new file mode 100644 index 000000000..85d4ce738 --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/_Imports.cs @@ -0,0 +1,15 @@ +global using MASA.BuildingBlocks.Configuration; +global using MASA.BuildingBlocks.Configuration.Options; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using Microsoft.Extensions.Primitives; +global using System; +global using System.Collections.Concurrent; +global using System.Collections.Generic; +global using System.Linq; +global using System.Reflection; +global using System.Text.Json.Serialization; diff --git a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/DispatcherOptionsExtensions.cs b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/DispatcherOptionsExtensions.cs index fe1608028..ee5ae368f 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/DispatcherOptionsExtensions.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/DispatcherOptionsExtensions.cs @@ -13,17 +13,15 @@ public static IDispatcherOptions UseRepository( where TDbContext : DbContext { if (options.Services == null) - { throw new ArgumentNullException(nameof(options.Services)); - } - if (options.Services.Any(service => service.ImplementationType == typeof(RepositoryProvider))) return options; + if (options.Services.Any(service => service.ImplementationType == typeof(RepositoryProvider))) + return options; + options.Services.AddSingleton(); if (options.Services.All(service => service.ServiceType != typeof(IUnitOfWork))) - { throw new Exception("Please add UoW first."); - } options.Services.TryAddRepository(assemblies); return options; diff --git a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Internal/LinqExtensions.cs b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Internal/LinqExtensions.cs new file mode 100644 index 000000000..b47f613b9 --- /dev/null +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Internal/LinqExtensions.cs @@ -0,0 +1,93 @@ +namespace MASA.Contrib.DDD.Domain.Repository.EF.Internal; + +internal static class LinqExtensions +{ + public static IQueryable GetQueryable(this IQueryable query, Dictionary fields) where TEntity : class + { + foreach (var field in fields) + { + query = query.GetQueryable(field.Key, field.Value); + } + return query; + } + + private static IQueryable GetQueryable(this IQueryable query, string field, object val) where TEntity : class + { + Type type = typeof(TEntity); + var parameter = Expression.Parameter(type, "entity"); + + PropertyInfo property = type.GetProperty(field)!; + Expression expProperty = Expression.Property(parameter, property.Name); + + Expression> valueLamda = () => val; + Expression expValue = Expression.Convert(valueLamda.Body, property.PropertyType); + Expression expression = Expression.Equal(expProperty, expValue); + Expression> filter = (Expression>)Expression.Lambda(expression, parameter); + return query.Where(filter); + } + + public static IQueryable OrderBy(this IQueryable query, Dictionary fields) where TEntity : class + { + var index = 0; + foreach (var field in fields) + { + if (index == 0) + query = query.OrderBy(field.Key, field.Value); + else + query = query.ThenBy(field.Key, field.Value); + index++; + } + + return query; + } + + private static IQueryable OrderBy(this IQueryable query, string field, bool desc) where TEntity : class + { + ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntity)); + Expression key = Expression.Property(parameterExpression, field); + var propertyInfo = GetPropertyInfo(typeof(TEntity), field); + var orderExpression = GetOrderExpression(typeof(TEntity), propertyInfo); + if (desc) + { + var method = typeof(Queryable).GetMethods().FirstOrDefault(m => m.Name == "OrderByDescending" && m.GetParameters().Length == 2); + var genericMethod = method!.MakeGenericMethod(typeof(TEntity), propertyInfo.PropertyType); + return (genericMethod.Invoke(null, new object[] { query, orderExpression }) as IQueryable)!; + } + else + { + var method = typeof(Queryable).GetMethods().FirstOrDefault(m => m.Name == "OrderBy" && m.GetParameters().Length == 2); + var genericMethod = method!.MakeGenericMethod(typeof(TEntity), propertyInfo.PropertyType); + return (IQueryable)genericMethod.Invoke(null, new object[] { query, orderExpression })!; + } + } + + private static IQueryable ThenBy(this IQueryable query, string field, bool desc) where T : class + { + ParameterExpression parameterExpression = Expression.Parameter(typeof(T)); + Expression key = Expression.Property(parameterExpression, field); + var propertyInfo = GetPropertyInfo(typeof(T), field); + var orderExpression = GetOrderExpression(typeof(T), propertyInfo); + if (desc) + { + var method = typeof(Queryable).GetMethods().FirstOrDefault(m => m.Name == "ThenByDescending" && m.GetParameters().Length == 2); + var genericMethod = method!.MakeGenericMethod(typeof(T), propertyInfo.PropertyType); + return (genericMethod.Invoke(null, new object[] { query, orderExpression }) as IQueryable)!; + } + else + { + var method = typeof(Queryable).GetMethods().FirstOrDefault(m => m.Name == "ThenBy" && m.GetParameters().Length == 2); + var genericMethod = method!.MakeGenericMethod(typeof(T), propertyInfo.PropertyType); + return (IQueryable)genericMethod.Invoke(null, new object[] { query, orderExpression })!; + } + } + + private static PropertyInfo GetPropertyInfo(Type entityType, string field) + => entityType.GetProperties().FirstOrDefault(p => p.Name.Equals(field, StringComparison.OrdinalIgnoreCase))!; + + private static LambdaExpression GetOrderExpression(Type entityType, PropertyInfo propertyInfo) + { + var parametersExpression = Expression.Parameter(entityType); + var fieldExpression = Expression.PropertyOrField(parametersExpression, propertyInfo.Name); + return Expression.Lambda(fieldExpression, parametersExpression); + } +} diff --git a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Internal/ServiceCollectionRepositoryExtensions.cs b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Internal/ServiceCollectionRepositoryExtensions.cs index 4a344663f..f009c3c8f 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Internal/ServiceCollectionRepositoryExtensions.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Internal/ServiceCollectionRepositoryExtensions.cs @@ -2,6 +2,11 @@ namespace MASA.Contrib.DDD.Domain.Repository.EF.Internal; internal static class ServiceCollectionRepositoryExtensions { + /// + /// The relationship between entity and keys + /// + public static Dictionary Relations = new(); + public static IServiceCollection TryAddRepository( this IServiceCollection services, params Assembly[] assemblies) @@ -9,7 +14,7 @@ public static IServiceCollection TryAddRepository( { if (assemblies == null || assemblies.Length == 0) { - assemblies = AppDomain.CurrentDomain.GetAssemblies(); + throw new ArgumentNullException(nameof(assemblies)); } var allTypes = assemblies.SelectMany(assembly => assembly.GetTypes()); @@ -21,10 +26,53 @@ public static IServiceCollection TryAddRepository( services.TryAddAddDefaultRepository(repositoryInterfaceType, GetRepositoryImplementationType(typeof(TDbContext), entityType)); services.TryAddCustomRepository(repositoryInterfaceType, allTypes.ToArray()); + + var keys = GetKeys(entityType); + CheckKeys(entityType, keys); + Relations.TryAdd(entityType, keys); } + return services; } + private static string[] GetKeys(Type entityType) + { + IAggregateRoot aggregateRoot; + try + { + var constructorInfo = entityType + .GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .FirstOrDefault(con => !con.CustomAttributes.Any()); + + if (constructorInfo == null) + throw new ArgumentNullException("The entity needs to have an empty constructor"); + + aggregateRoot = (IAggregateRoot)Activator.CreateInstance(entityType, constructorInfo.IsPrivate)!; + } + catch (Exception) + { + throw new ArgumentNullException("The entity needs to have an empty constructor"); + } + + var keys = aggregateRoot.GetKeys().Select(k => k.Name).ToArray(); + if (keys.Length != keys.Where(key => !string.IsNullOrEmpty(key)).Distinct().Count()) + throw new ArgumentException("The joint primary key cannot be empty"); + + return keys; + } + + /// + /// Check if the combined primary key is correct + /// + private static void CheckKeys(Type entityType, string[] fields) + { + foreach (var field in fields) + { + if (!entityType.GetProperties().Any(p => p.Name.Equals(field, StringComparison.OrdinalIgnoreCase))!) + throw new ArgumentException("Check if the combined primary key is correct"); + } + } + private static bool IsAggregateRootEntity(this Type type) => type.IsClass && !type.IsGenericType && !type.IsAbstract && type != typeof(AggregateRoot) && type != typeof(Entity) && typeof(IAggregateRoot).IsAssignableFrom(type); diff --git a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/MASA.Contrib.DDD.Domain.Repository.EF.csproj b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/MASA.Contrib.DDD.Domain.Repository.EF.csproj index 2e7fffff8..679608d24 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/MASA.Contrib.DDD.Domain.Repository.EF.csproj +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/MASA.Contrib.DDD.Domain.Repository.EF.csproj @@ -5,11 +5,14 @@ enable enable - + - - - + + - + + + + + diff --git a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.md b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.md index a9829bb4e..54dc8f883 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.md +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.md @@ -1,3 +1,5 @@ +[中](README.zh-CN.md) | EN + ## MASA.Contrib.DDD.Domain.Repository.EF Example: @@ -14,7 +16,7 @@ Install-Package MASA.Contrib.DDD.Domain.Repository.EF builder.Services .AddDomainEventBus(options => { - options.UseRepository();//Use the EF version of Repository to achieve + options.UseRepository();//Use the EF version of Repository to achieve } ``` @@ -51,7 +53,7 @@ public interface IProductRepository : IRepository public class ProductRepository : Repository, IProductRepository { - public Task> ItemsWithNameAsync(string name) + public Task> ItemsWithNameAsync(string name) { //Todo } diff --git a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.zh-cn.md b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.zh-CN.md similarity index 88% rename from src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.zh-cn.md rename to src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.zh-CN.md index 84d376759..3a9cf09c5 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.zh-cn.md +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/README.zh-CN.md @@ -1,3 +1,5 @@ +中 | [EN](README.md) + ## MASA.Contrib.DDD.Domain.Repository.EF 用例: @@ -14,7 +16,7 @@ Install-Package MASA.Contrib.DDD.Domain.Repository.EF builder.Services .AddDomainEventBus(options => { - options.UseRepository();//使用Repository的EF版实现 + options.UseRepository();//使用Repository的EF版实现 } ``` @@ -30,9 +32,9 @@ public class DemoService : ServiceBase { public CatalogService(IServiceCollection services) : base(services) { - + } - + public async Task CreateProduct(ProductItem product,[FromService]IRepository repository) { await repository.AddAsync(product); @@ -51,7 +53,7 @@ public interface IProductRepository : IRepository public class ProductRepository : Repository, IProductRepository { - public Task> ItemsWithNameAsync(string name) + public Task> ItemsWithNameAsync(string name) { //Todo } diff --git a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Repository.cs b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Repository.cs index bcde5dbce..bf1894894 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Repository.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Repository.cs @@ -12,111 +12,202 @@ public Repository(TDbContext context, IUnitOfWork unitOfWork) UnitOfWork = unitOfWork; } + public override bool TransactionHasBegun + => _context.Database.CurrentTransaction != null; + public override DbTransaction Transaction { get { + if (!UseTransaction) + throw new NotSupportedException(nameof(Transaction)); + if (TransactionHasBegun) - { return _context.Database.CurrentTransaction!.GetDbTransaction(); - } + return _context.Database.BeginTransaction().GetDbTransaction(); } + } + + public override bool UseTransaction + { + get => UnitOfWork.UseTransaction; + set => UnitOfWork.UseTransaction = value; + } + + public override IUnitOfWork UnitOfWork { get; } + + public override EntityState EntityState + { + get => UnitOfWork.EntityState; set { - if (_context.Database.CurrentTransaction == null) - { - _context.Database.UseTransaction(value); - } - else - { - throw new NotSupportedException("The transaction is opened"); - } + UnitOfWork.EntityState = value; + if (value == EntityState.Changed) + CheckAndOpenTransaction(); } } - public override IUnitOfWork UnitOfWork { get; set; } - - public override async ValueTask AddAsync(TEntity entity, CancellationToken cancellationToken = default) - => (await _context.AddAsync(entity, cancellationToken).AsTask()).Entity; + public override async ValueTask AddAsync( + TEntity entity, + CancellationToken cancellationToken = default) + { + var response = (await _context.AddAsync(entity, cancellationToken).AsTask()).Entity; + EntityState = EntityState.Changed; + return response; + } - public override Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) - => _context.AddRangeAsync(entities, cancellationToken); + public override async Task AddRangeAsync( + IEnumerable entities, + CancellationToken cancellationToken = default) + { + await _context.AddRangeAsync(entities, cancellationToken); + EntityState = EntityState.Changed; + } public override Task CommitAsync(CancellationToken cancellationToken = default) => UnitOfWork.CommitAsync(cancellationToken); - public override ValueTask DisposeAsync() => ValueTask.CompletedTask; + public override async ValueTask DisposeAsync() => await _context.DisposeAsync(); + + public override void Dispose() => _context.Dispose(); + + public override Task FindAsync( + object?[]? keyValues, + CancellationToken cancellationToken = default) + { + if (keyValues == null) + return Task.FromResult(default(TEntity?)); + + var keys = GetKeys(typeof(TEntity)); + Dictionary fields = new(); + for (var i = 0; i < keys.Length; i++) + { + fields.Add(keys[i], keyValues[i]!); + } - public override ValueTask FindAsync(object?[]? keyValues, CancellationToken cancellationToken) - => _context.Set().FindAsync(keyValues, cancellationToken); + return _context.Set().IgnoreQueryFilters().GetQueryable(fields).FirstOrDefaultAsync(cancellationToken); + } public override Task FindAsync( Expression> predicate, CancellationToken cancellationToken = default) => _context.Set().Where(predicate).FirstOrDefaultAsync(cancellationToken); - public override Task GetCountAsync(CancellationToken cancellationToken) - => _context.Set().LongCountAsync(cancellationToken); + public override async Task GetCountAsync(CancellationToken cancellationToken = default) + => await _context.Set().LongCountAsync(cancellationToken); public override Task GetCountAsync( Expression> predicate, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) => _context.Set().LongCountAsync(predicate, cancellationToken); - public override async Task> GetListAsync(CancellationToken cancellationToken) + public override async Task> GetListAsync(CancellationToken cancellationToken = default) => await _context.Set().ToListAsync(cancellationToken); public override async Task> GetListAsync( Expression> predicate, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) => await _context.Set().Where(predicate).ToListAsync(cancellationToken); - public override Task> GetPaginatedListAsync(int skip, int take, string? sorting, CancellationToken cancellationToken) + /// + /// + /// + /// + /// + /// asc or desc, default asc + /// + /// + public override Task> GetPaginatedListAsync( + int skip, + int take, + Dictionary? sorting, + CancellationToken cancellationToken = default) { - var iQueryable = _context.Set(); - return (string.Equals(sorting ?? "asc", "asc", StringComparison.CurrentCultureIgnoreCase) ? iQueryable.OrderBy(x => x.GetKeys()) : iQueryable.OrderByDescending(x => x.GetKeys())).Skip(skip).Take(take).ToListAsync(cancellationToken); + sorting ??= new Dictionary(GetKeys(typeof(TEntity)).Select(key => new KeyValuePair(key, false))); + + return _context.Set().OrderBy(sorting).Skip(skip).Take(take).ToListAsync(cancellationToken); } - public override Task> GetPaginatedListAsync(Expression> predicate, int skip, int take, string? sorting, CancellationToken cancellationToken) + /// + /// + /// + /// condition + /// + /// + /// asc or desc, default asc + /// + /// + public override Task> GetPaginatedListAsync( + Expression> predicate, + int skip, + int take, + Dictionary? sorting, + CancellationToken cancellationToken = default) { - var iQueryable = _context.Set().Where(predicate); - return (string.Equals(sorting ?? "asc", "asc", StringComparison.CurrentCultureIgnoreCase) ? iQueryable.OrderBy(x => x.GetKeys()) : iQueryable.OrderByDescending(x => x.GetKeys())).Skip(skip).Take(take).ToListAsync(cancellationToken); + sorting ??= new Dictionary(GetKeys(typeof(TEntity)).Select(key => new KeyValuePair(key, false))); + + return _context.Set().Where(predicate).OrderBy(sorting).Skip(skip).Take(take).ToListAsync(cancellationToken); } public override Task RemoveAsync(TEntity entity, CancellationToken cancellationToken = default) { _context.Set().Remove(entity); + EntityState = EntityState.Changed; return Task.FromResult(entity); } public override async Task RemoveAsync(Expression> predicate, CancellationToken cancellationToken = default) { - var entities = await GetListAsync(predicate, cancellationToken)!; + var entities = await GetListAsync(predicate, cancellationToken); + EntityState = EntityState.Changed; _context.Set().RemoveRange(entities); } public override Task RemoveRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) { _context.Set().RemoveRange(entities); + EntityState = EntityState.Changed; return Task.CompletedTask; } public override Task RollbackAsync(CancellationToken cancellationToken = default) => UnitOfWork.RollbackAsync(cancellationToken); - public override Task SaveChangesAsync(CancellationToken cancellationToken = default) - => UnitOfWork.SaveChangesAsync(cancellationToken); + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + await UnitOfWork.SaveChangesAsync(cancellationToken); + } public override Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) { _context.Set().Update(entity); + EntityState = EntityState.Changed; return Task.FromResult(entity); } public override Task UpdateRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) { _context.Set().UpdateRange(entities); + EntityState = EntityState.Changed; return Task.CompletedTask; } + + /// + /// When additions, deletions and changes are made through the Repository and the transaction is currently allowed and the transaction is not opened, the transaction is started + /// + private void CheckAndOpenTransaction() + { + if (!UnitOfWork.UseTransaction) + return; + + if (!UnitOfWork.TransactionHasBegun) + { + _ = UnitOfWork.Transaction; // Open the transaction + } + CommitState = CommitState.UnCommited; + } + + protected string[] GetKeys(Type entityType) + => ServiceCollectionRepositoryExtensions.Relations[entityType]!; } diff --git a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/_Imports.cs b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/_Imports.cs index 14372d0ed..95dee48cd 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/_Imports.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/_Imports.cs @@ -1,4 +1,4 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.DDD.Domain.Entities; global using MASA.BuildingBlocks.DDD.Domain.Repositories; global using MASA.BuildingBlocks.Dispatcher.Events; @@ -7,6 +7,9 @@ global using Microsoft.EntityFrameworkCore.Storage; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; +global using System.Collections.Concurrent; global using System.Data.Common; global using System.Linq.Expressions; global using System.Reflection; +global using EntityState = MASA.BuildingBlocks.Data.UoW.EntityState; diff --git a/src/DDD/MASA.Contrib.DDD.Domain/DomainEventBus.cs b/src/DDD/MASA.Contrib.DDD.Domain/DomainEventBus.cs index f6e6c0822..67fc8ce04 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/DomainEventBus.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/DomainEventBus.cs @@ -7,9 +7,13 @@ public class DomainEventBus : IDomainEventBus private readonly IUnitOfWork _unitOfWork; private readonly DispatcherOptions _options; - private readonly ConcurrentQueue _eventQueue = new ConcurrentQueue(); + private readonly ConcurrentQueue> _eventQueue = new(); - public DomainEventBus(IEventBus eventBus, IIntegrationEventBus integrationEventBus, IUnitOfWork unitOfWork, IOptions options) + public DomainEventBus( + IEventBus eventBus, + IIntegrationEventBus integrationEventBus, + IUnitOfWork unitOfWork, + IOptions options) { _eventBus = eventBus; _integrationEventBus = integrationEventBus; @@ -19,33 +23,62 @@ public DomainEventBus(IEventBus eventBus, IIntegrationEventBus integrationEventB public async Task PublishAsync(TEvent @event) where TEvent : IEvent { + if (@event is IDomainEvent domainEvent && !IsAssignableFromDomainQuery(@event.GetType())) + { + domainEvent.UnitOfWork = _unitOfWork; + } if (@event is IIntegrationEvent integrationEvent) { - if (integrationEvent.UnitOfWork == null) - { - integrationEvent.UnitOfWork = _unitOfWork; - } - await _integrationEventBus.PublishAsync(integrationEvent); + await _integrationEventBus.PublishAsync((TEvent)integrationEvent); } else { await _eventBus.PublishAsync(@event); } + + bool IsAssignableFromDomainQuery(Type? type) + { + if (type == null) + return false; + + if (!type.IsGenericType) + { + return IsAssignableFromDomainQuery(type.BaseType); + } + return type.GetInterfaces().Any(type => type.GetGenericTypeDefinition() == typeof(IDomainQuery<>)); + } } public Task Enqueue(TDomentEvent @event) where TDomentEvent : IDomainEvent { - _eventQueue.Enqueue(@event); + _eventQueue.Enqueue(new KeyValuePair(@event.GetType(), @event)); return Task.CompletedTask; } public async Task PublishQueueAsync() { - while (_eventQueue.TryDequeue(out IDomainEvent? @event)) + while (_eventQueue.TryDequeue(out KeyValuePair @event)) { - await PublishAsync(@event); + await PublishAsync(@event.Key, @event.Value); } } + private async Task PublishAsync(Type type, TEvent @event) where TEvent : IEvent + { + if (@event is IIntegrationEvent integrationEvent) + { + await PublishAsync(integrationEvent); + } + else + { + var parameters = Convert.ChangeType(@event, type); + var invokeDelegate = InvokeBuilder.Build(_eventBus.GetType(), nameof(_eventBus.PublishAsync), type); + await invokeDelegate.Invoke(_eventBus, parameters); + } + } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + => await _unitOfWork.CommitAsync(cancellationToken); + public IEnumerable GetAllEventTypes() => _options.AllEventTypes.Concat(_eventBus.GetAllEventTypes()).Distinct(); } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainCommand.cs b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainCommand.cs index 9fba0e906..24c351c5d 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainCommand.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainCommand.cs @@ -1,19 +1,15 @@ namespace MASA.Contrib.DDD.Domain.Events; -public record DomainCommand : IDomainCommand +public record DomainCommand(Guid Id, DateTime CreationTime) : IDomainCommand { - public Guid Id { get; init; } + [JsonIgnore] + public Guid Id { get; } = Id; - public DateTime CreationTime { get; init; } + [JsonIgnore] + public DateTime CreationTime { get; } = CreationTime; [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } public DomainCommand() : this(Guid.NewGuid(), DateTime.UtcNow) { } - - public DomainCommand(Guid id, DateTime creationTime) - { - this.Id = id; - this.CreationTime = creationTime; - } } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainEvent.cs b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainEvent.cs index af5a97433..2aeb5a3fc 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainEvent.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainEvent.cs @@ -1,19 +1,16 @@ namespace MASA.Contrib.DDD.Domain.Events; -public record DomainEvent : IDomainEvent +public record DomainEvent(Guid Id, DateTime CreationTime) : IDomainEvent { - public Guid Id { get; init; } + [JsonIgnore] + public Guid Id { get; } = Id; - public DateTime CreationTime { get; init; } + [JsonIgnore] + public DateTime CreationTime { get; } = CreationTime; [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } public DomainEvent() : this(Guid.NewGuid(), DateTime.UtcNow) { } - public DomainEvent(Guid id, DateTime creationTime) - { - this.Id = id; - this.CreationTime = creationTime; - } } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainQuery.cs b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainQuery.cs index a91c481e4..e72db94d6 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainQuery.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainQuery.cs @@ -1,22 +1,22 @@ namespace MASA.Contrib.DDD.Domain.Events; -public abstract record DomainQuery : IDomainQuery +public abstract record DomainQuery(Guid Id, DateTime CreationTime) : IDomainQuery where TResult : notnull { - public Guid Id { get; init; } + [JsonIgnore] + public Guid Id { get; } = Id; - public DateTime CreationTime { get; init; } + [JsonIgnore] + public DateTime CreationTime { get; } = CreationTime; [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork + { + get => null; + set => throw new NotSupportedException(nameof(UnitOfWork)); + } public abstract TResult Result { get; set; } public DomainQuery() : this(Guid.NewGuid(), DateTime.UtcNow) { } - - public DomainQuery(Guid id, DateTime creationTime) - { - this.Id = id; - this.CreationTime = creationTime; - } } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Events/IntegrationDomainEvent.cs b/src/DDD/MASA.Contrib.DDD.Domain/Events/IntegrationDomainEvent.cs index a0f916a10..3c027cb0f 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Events/IntegrationDomainEvent.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Events/IntegrationDomainEvent.cs @@ -1,12 +1,9 @@ namespace MASA.Contrib.DDD.Domain.Events; -public abstract record IntegrationDomainEvent : DomainEvent, IIntegrationDomainEvent +public abstract record IntegrationDomainEvent(Guid Id, DateTime CreationTime) : DomainEvent(Id, CreationTime), IIntegrationDomainEvent { + [JsonIgnore] public abstract string Topic { get; set; } public IntegrationDomainEvent() : this(Guid.NewGuid(), DateTime.UtcNow) { } - - public IntegrationDomainEvent(Guid id, DateTime creationTime) : base(id, creationTime) - { - } } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Internal/InvokeBuilder.cs b/src/DDD/MASA.Contrib.DDD.Domain/Internal/InvokeBuilder.cs new file mode 100644 index 000000000..74c3f9690 --- /dev/null +++ b/src/DDD/MASA.Contrib.DDD.Domain/Internal/InvokeBuilder.cs @@ -0,0 +1,21 @@ +namespace MASA.Contrib.DDD.Domain.Internal; + +internal class InvokeBuilder +{ + internal delegate Task TaskInvokeDelegate(object target, params object[] parameters); + + internal static TaskInvokeDelegate Build(Type targetType, string methodName, Type parameterType) + { + var targetParameter = Expression.Parameter(typeof(object), "target"); + var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); + + var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(0)); + var valueCast = Expression.Convert(valueObj, parameterType); + var instanceCast = Expression.Convert(targetParameter, targetType); + var methodCall = Expression.Call(instanceCast, methodName, new[] { parameterType }, valueCast); + + var castMethodCall = Expression.Convert(methodCall, typeof(Task)); + var lambda = Expression.Lambda(castMethodCall, targetParameter, parametersParameter); + return lambda.Compile(); + } +} diff --git a/src/DDD/MASA.Contrib.DDD.Domain/MASA.Contrib.DDD.Domain.csproj b/src/DDD/MASA.Contrib.DDD.Domain/MASA.Contrib.DDD.Domain.csproj index e7e66739f..be122bd18 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/MASA.Contrib.DDD.Domain.csproj +++ b/src/DDD/MASA.Contrib.DDD.Domain/MASA.Contrib.DDD.Domain.csproj @@ -7,9 +7,12 @@ - - - + + + + + + diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Options/DispatcherOptions.cs b/src/DDD/MASA.Contrib.DDD.Domain/Options/DispatcherOptions.cs index 8fdef4264..742ffa1bc 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Options/DispatcherOptions.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Options/DispatcherOptions.cs @@ -22,17 +22,17 @@ public Assembly[] Assemblies } private bool IsAggregateRootEntity(Type type) - => type.IsClass && !type.IsGenericType && !type.IsAbstract && type != typeof(AggregateRoot) && type != typeof(Entity) && typeof(IAggregateRoot).IsAssignableFrom(type); + => type.IsClass && !type.IsGenericType && !type.IsAbstract && type != typeof(AggregateRoot) && typeof(IAggregateRoot).IsAssignableFrom(type); private IEnumerable Types { get; set; } - public List AllEventTypes { get; private set; } + private IEnumerable GetTypes(Type type) => Types.Where(t => t.IsClass && type.IsAssignableFrom(t)); - public List AllDomainServiceTypes { get; private set; } + internal List AllEventTypes { get; private set; } - public List AllAggregateRootTypes { get; private set; } + internal List AllDomainServiceTypes { get; private set; } - private IEnumerable GetTypes(Type type) => Types.Where(t => type.IsAssignableFrom(t) && t.IsClass); + internal List AllAggregateRootTypes { get; private set; } public IServiceCollection Services { get; } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/README.md b/src/DDD/MASA.Contrib.DDD.Domain/README.md index dc44c3e61..842995a1e 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/README.md +++ b/src/DDD/MASA.Contrib.DDD.Domain/README.md @@ -1,3 +1,5 @@ +[中](README.zh-CN.md) | EN + ### DomainEventBus Example: @@ -10,7 +12,7 @@ Install-Package MASA.Contrib.Dispatcher.Events Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.Dapr Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF -Install-Package MASA.Contrib.Data.Uow.EF +Install-Package MASA.Contrib.Data.UoW.EF ``` 1. Add DomainEventBus @@ -20,9 +22,9 @@ builder.Services .AddDomainEventBus(options => { options.UseEventBus()//Use in-process events - .UseUow(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=idientity")) + .UseUoW(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=idientity")) .UseDaprEventBus()///Use cross-process events - .UseEventLog() + .UseEventLog() .UseRepository();//Use the EF version of Repository to achieve }) ``` @@ -36,7 +38,7 @@ public class RegisterUserDomainCommand : DomainCommand public string Password { get; set; } = default!; - public string Mobile { get; set; } = default!; + public string PhoneNumber { get; set; } = default!; } ``` > DomainQuery refers to Query in CQRS @@ -46,8 +48,8 @@ public class RegisterUserDomainCommand : DomainCommand ```C# public class UserHandler { - [EventHandler] - public Task RegisterUserHandlerAsync(RegisterUserDomainCommand command) + [EventHandler] + public Task RegisterUserHandlerAsync(RegisterUserDomainCommand command) { //TODO Registered user business } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/README.zh-cn.md b/src/DDD/MASA.Contrib.DDD.Domain/README.zh-CN.md similarity index 89% rename from src/DDD/MASA.Contrib.DDD.Domain/README.zh-cn.md rename to src/DDD/MASA.Contrib.DDD.Domain/README.zh-CN.md index bcad3c833..a6402cc4d 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/README.zh-cn.md +++ b/src/DDD/MASA.Contrib.DDD.Domain/README.zh-CN.md @@ -1,3 +1,5 @@ +中 | [EN](README.md) + ### DomainEventBus 用例: @@ -10,7 +12,7 @@ Install-Package MASA.Contrib.Dispatcher.Events Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.Dapr Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF -Install-Package MASA.Contrib.Data.Uow.EF +Install-Package MASA.Contrib.Data.UoW.EF ``` 1. 添加DomainEventBus @@ -20,9 +22,9 @@ builder.Services .AddDomainEventBus(options => { options.UseEventBus()//使用进程内事件 - .UseUow(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=idientity")) + .UseUoW(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=idientity")) .UseDaprEventBus()///使用跨进程事件 - .UseEventLog() + .UseEventLog() .UseRepository();//使用Repository的EF版实现 }) ``` @@ -36,7 +38,7 @@ public class RegisterUserDomainCommand : DomainCommand public string Password { get; set; } = default!; - public string Mobile { get; set; } = default!; + public string PhoneNumber { get; set; } = default!; } ``` > DomainQuery参考CQRS中的Query @@ -46,8 +48,8 @@ public class RegisterUserDomainCommand : DomainCommand ```C# public class UserHandler { - [EventHandler] - public Task RegisterUserHandlerAsync(RegisterUserDomainCommand command) + [EventHandler] + public Task RegisterUserHandlerAsync(RegisterUserDomainCommand command) { //TODO 注册用户业务 } @@ -114,4 +116,4 @@ public async Task RegisterUserSucceededHandlerAsync(RegisterUserSucceededIntegra { //todo } -``` \ No newline at end of file +``` diff --git a/src/DDD/MASA.Contrib.DDD.Domain/ServiceCollectionExtensions.cs b/src/DDD/MASA.Contrib.DDD.Domain/ServiceCollectionExtensions.cs index 7211a238c..28c03d33e 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/ServiceCollectionExtensions.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/ServiceCollectionExtensions.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.DependencyInjection.Extensions; - namespace MASA.Contrib.DDD.Domain; public static class ServiceCollectionExtensions @@ -8,7 +6,9 @@ public static IServiceCollection AddDomainEventBus( this IServiceCollection services, Action? options = null) { - if (services.Any(service => service.ImplementationType == typeof(DomainEventBusProvider))) return services; + if (services.Any(service => service.ImplementationType == typeof(DomainEventBusProvider))) + return services; + services.AddSingleton(); var dispatcherOptions = new DispatcherOptions(services); @@ -17,7 +17,7 @@ public static IServiceCollection AddDomainEventBus( { dispatcherOptions.Assemblies = AppDomain.CurrentDomain.GetAssemblies(); } - services.TryAddSingleton(typeof(IOptions), serviceProvider => Microsoft.Extensions.Options.Options.Create(dispatcherOptions)); + services.AddSingleton(typeof(IOptions), serviceProvider => Options.Create(dispatcherOptions)); if (services.All(service => service.ServiceType != typeof(IEventBus))) { @@ -26,7 +26,7 @@ public static IServiceCollection AddDomainEventBus( if (services.All(service => service.ServiceType != typeof(IUnitOfWork))) { - throw new Exception("Please add Uow first."); + throw new Exception("Please add UoW first."); } if (services.All(service => service.ServiceType != typeof(IIntegrationEventBus))) diff --git a/src/DDD/MASA.Contrib.DDD.Domain/_Imports.cs b/src/DDD/MASA.Contrib.DDD.Domain/_Imports.cs index 021c36b81..51c99d1c7 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/_Imports.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/_Imports.cs @@ -1,12 +1,15 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.DDD.Domain.Entities; global using MASA.BuildingBlocks.DDD.Domain.Events; global using MASA.BuildingBlocks.DDD.Domain.Repositories; global using MASA.BuildingBlocks.DDD.Domain.Services; global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents; +global using MASA.Contrib.DDD.Domain.Internal; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; global using Microsoft.Extensions.Options; global using System.Collections.Concurrent; +global using System.Linq.Expressions; global using System.Reflection; global using System.Text.Json.Serialization; diff --git a/src/Data/MASA.Contrib.Data.Contracts.EF/MASA.Contrib.Data.Contracts.EF.csproj b/src/Data/MASA.Contrib.Data.Contracts.EF/MASA.Contrib.Data.Contracts.EF.csproj index c29fc9254..4219b95ea 100644 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/MASA.Contrib.Data.Contracts.EF.csproj +++ b/src/Data/MASA.Contrib.Data.Contracts.EF/MASA.Contrib.Data.Contracts.EF.csproj @@ -7,10 +7,13 @@ - - - - - + + + + + + + + diff --git a/src/Data/MASA.Contrib.Data.Contracts.EF/README.md b/src/Data/MASA.Contrib.Data.Contracts.EF/README.md index 76ba4c345..04282f6fe 100644 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/README.md +++ b/src/Data/MASA.Contrib.Data.Contracts.EF/README.md @@ -1,20 +1,31 @@ +[中](README.zh-CN.md) | EN + ## Contracts.EF Example: ```C# +Install-Package MASA.Contrib.Data.UoW.EF Install-Package MASA.Contrib.Data.Contracts.EF ``` ```C# -builder.Services - .AddUoW(dbOptions => +builder.Services.AddEventBus(options => { + options.UseUoW(dbOptions => { dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"); dbOptions.UseSoftDelete(builder.Services); - }) + }); +}); ``` > When the entity inherits ISoftware and is deleted, change the delete state to the modified state, and cooperate with the custom Remove operation to achieve soft deletion > Do not query the data marked as soft deleted when querying -> When combined with EventBus, the transaction is opened after the first CUD, and the transaction rollback is supported when the entire Handler is abnormal. + +> Frequently Asked Questions: + +- Problem 1: After using UseSoftDelete, there is a problem that the submission cannot be saved + + After using Uow, the transaction will be enabled by default after Add、 Modified、 and Deleted + and the transaction can be saved normally after the transaction is submitted + If the EventBus is used, the transaction will be automatically submitted \ No newline at end of file diff --git a/src/Data/MASA.Contrib.Data.Contracts.EF/README.zh-cn.md b/src/Data/MASA.Contrib.Data.Contracts.EF/README.zh-CN.md similarity index 51% rename from src/Data/MASA.Contrib.Data.Contracts.EF/README.zh-cn.md rename to src/Data/MASA.Contrib.Data.Contracts.EF/README.zh-CN.md index be3fb7afc..5de5f31ae 100644 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/README.zh-cn.md +++ b/src/Data/MASA.Contrib.Data.Contracts.EF/README.zh-CN.md @@ -1,20 +1,31 @@ +中 | [EN](README.md) + ## Contracts.EF 用例: ```C# +Install-Package MASA.Contrib.Data.UoW.EF Install-Package MASA.Contrib.Data.Contracts.EF ``` ```C# -builder.Services - .AddUoW(dbOptions => +builder.Services.AddEventBus(options => { + options.UseUoW(dbOptions => { dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"); dbOptions.UseSoftDelete(builder.Services);//启动软删除 - }) + }); +}); ``` > 当实体继承ISoftware,且被删除时,将删除状态改为修改状态,并配合自定义Remove操作,实现软删除 > 支持查询的时候不查询被标记软删除的数据 -> 与EventBus结合使用时,做到了第一次CUD后开启事务,当整个Handler出现异常后支持事务回滚 + +> 常见问题: + +- 问题1:使用UseSoftDelete后出现提交保存不上的问题 + + 使用Uow后,默认在进行Add、Modified、Deleted后会启用事务 + 需要提交事务之后才能正常保存 + 如果使用EventBus则会自动提交事务 \ No newline at end of file diff --git a/src/Data/MASA.Contrib.Data.Contracts.EF/ServiceCollectionExtensions.cs b/src/Data/MASA.Contrib.Data.Contracts.EF/ServiceCollectionExtensions.cs index 77488ea6e..d15233ea7 100644 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/ServiceCollectionExtensions.cs +++ b/src/Data/MASA.Contrib.Data.Contracts.EF/ServiceCollectionExtensions.cs @@ -12,7 +12,6 @@ public static MasaDbContextOptionsBuilder UseSoftDelete( throw new Exception("Please add UoW first."); optionsBuilder.UseQueryFilterProvider() - .UseSaveChangesFilter() .UseSaveChangesFilter(); return optionsBuilder; diff --git a/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/SoftDeleteSaveChangesFilter.cs b/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/SoftDeleteSaveChangesFilter.cs index b739b1f94..da0023f04 100644 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/SoftDeleteSaveChangesFilter.cs +++ b/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/SoftDeleteSaveChangesFilter.cs @@ -4,11 +4,12 @@ public class SoftDeleteSaveChangesFilter : ISaveChangesFilter { public void OnExecuting(ChangeTracker changeTracker) { - foreach (var entity in changeTracker.Entries().Where(e => e.State == EntityState.Deleted)) + changeTracker.DetectChanges(); + foreach (var entity in changeTracker.Entries().Where(e => e.State == Microsoft.EntityFrameworkCore.EntityState.Deleted)) { if (entity.Entity is ISoftDelete) { - entity.State = EntityState.Modified; + entity.State = Microsoft.EntityFrameworkCore.EntityState.Modified; entity.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true; } } diff --git a/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/TransactionSaveChangesFilter.cs b/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/TransactionSaveChangesFilter.cs deleted file mode 100644 index a6c47eb16..000000000 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/TransactionSaveChangesFilter.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MASA.Contrib.Data.Contracts.EF.SoftDelete -{ - public class TransactionSaveChangesFilter : ISaveChangesFilter - { - private readonly IUnitOfWork _unitOfWork; - - public TransactionSaveChangesFilter(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; - - public void OnExecuting(ChangeTracker changeTracker) - { - changeTracker.DetectChanges(); - if (changeTracker.Entries().Any(e => e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted) && !_unitOfWork.TransactionHasBegun) - { - var transaction = _unitOfWork.Transaction; // Open the transaction - } - } - } -} diff --git a/src/Data/MASA.Contrib.Data.Contracts.EF/_Imports.cs b/src/Data/MASA.Contrib.Data.Contracts.EF/_Imports.cs index c160c452a..d4c3c90c7 100644 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/_Imports.cs +++ b/src/Data/MASA.Contrib.Data.Contracts.EF/_Imports.cs @@ -1,5 +1,5 @@ global using MASA.BuildingBlocks.Data.Contracts; -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.Contrib.Data.Contracts.EF.SoftDelete; global using MASA.Utils.Data.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore; diff --git a/src/Data/MASA.Contrib.Data.Uow.EF/DispatcherOptionsExtensions.cs b/src/Data/MASA.Contrib.Data.UoW.EF/DispatcherOptionsExtensions.cs similarity index 50% rename from src/Data/MASA.Contrib.Data.Uow.EF/DispatcherOptionsExtensions.cs rename to src/Data/MASA.Contrib.Data.UoW.EF/DispatcherOptionsExtensions.cs index c7cb8535e..b549686ef 100644 --- a/src/Data/MASA.Contrib.Data.Uow.EF/DispatcherOptionsExtensions.cs +++ b/src/Data/MASA.Contrib.Data.UoW.EF/DispatcherOptionsExtensions.cs @@ -1,33 +1,41 @@ -namespace MASA.Contrib.Data.Uow.EF; +namespace MASA.Contrib.Data.UoW.EF; public static class DispatcherOptionsExtensions { public static IDispatcherOptions UseUoW( this IDispatcherOptions options, - Action? optionsBuilder = null) + Action? optionsBuilder = null, + bool disableRollbackOnFailure = false, + bool useTransaction = true) where TDbContext : MasaDbContext { if (options.Services == null) - { throw new ArgumentNullException(nameof(options.Services)); - } - if (options.Services.Any(service => service.ImplementationType == typeof(UowProvider))) return options; - options.Services.AddSingleton(); + if (options.Services.Any(service => service.ImplementationType == typeof(UoWProvider))) + return options; - options.Services.AddLogging(); - options.Services.AddScoped>(); - if (options.Services.All(service => service.ServiceType != typeof(MasaDbContextOptions))) + options.Services.AddSingleton(); + + options.Services.AddScoped(serviceProvider => { + var dbContext = serviceProvider.GetRequiredService(); + var logger = serviceProvider.GetService>>(); + return new UnitOfWork(dbContext, logger) + { + DisableRollbackOnFailure = disableRollbackOnFailure, + UseTransaction = useTransaction + }; + }); + if (options.Services.All(service => service.ServiceType != typeof(MasaDbContextOptions))) options.Services.AddMasaDbContext(optionsBuilder); - } options.Services.AddScoped(); return options; } - private class UowProvider + private class UoWProvider { } diff --git a/src/Data/MASA.Contrib.Data.UoW.EF/MASA.Contrib.Data.UoW.EF.csproj b/src/Data/MASA.Contrib.Data.UoW.EF/MASA.Contrib.Data.UoW.EF.csproj new file mode 100644 index 000000000..afd4da368 --- /dev/null +++ b/src/Data/MASA.Contrib.Data.UoW.EF/MASA.Contrib.Data.UoW.EF.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/Data/MASA.Contrib.Data.UoW.EF/README.md b/src/Data/MASA.Contrib.Data.UoW.EF/README.md new file mode 100644 index 000000000..efd89e947 --- /dev/null +++ b/src/Data/MASA.Contrib.Data.UoW.EF/README.md @@ -0,0 +1,20 @@ +[中](README.zh-CN.md) | EN + +## Contracts.EF + +Example: + +```C# +Install-Package MASA.Contrib.Data.UoW.EF +Install-Package MASA.Contrib.Data.Contracts.EF +``` + +```C# +builder.Services.AddEventBus(options => { + options.UseUoW(dbOptions => + { + dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"); + }); +}); +``` + diff --git a/src/Data/MASA.Contrib.Data.UoW.EF/README.zh-CN.md b/src/Data/MASA.Contrib.Data.UoW.EF/README.zh-CN.md new file mode 100644 index 000000000..316b94e90 --- /dev/null +++ b/src/Data/MASA.Contrib.Data.UoW.EF/README.zh-CN.md @@ -0,0 +1,20 @@ +中 | [EN](README.md) + +## Contracts.EF + +用例: + +```C# +Install-Package MASA.Contrib.Data.UoW.EF +Install-Package MASA.Contrib.Data.Contracts.EF +``` + +```C# +builder.Services.AddEventBus(options => { + options.UseUoW(dbOptions => + { + dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"); + }); +}); +``` + diff --git a/src/Data/MASA.Contrib.Data.Uow.EF/Transaction.cs b/src/Data/MASA.Contrib.Data.UoW.EF/Transaction.cs similarity index 52% rename from src/Data/MASA.Contrib.Data.Uow.EF/Transaction.cs rename to src/Data/MASA.Contrib.Data.UoW.EF/Transaction.cs index c4a02136c..f7fdf6f2b 100644 --- a/src/Data/MASA.Contrib.Data.Uow.EF/Transaction.cs +++ b/src/Data/MASA.Contrib.Data.UoW.EF/Transaction.cs @@ -1,11 +1,9 @@ -using System.Text.Json.Serialization; - -namespace MASA.Contrib.Data.Uow.EF; +namespace MASA.Contrib.Data.UoW.EF; public class Transaction : ITransaction { public Transaction(IUnitOfWork unitOfWork) => UnitOfWork = unitOfWork; [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } } diff --git a/src/Data/MASA.Contrib.Data.Uow.EF/UnitOfWork.cs b/src/Data/MASA.Contrib.Data.UoW.EF/UnitOfWork.cs similarity index 53% rename from src/Data/MASA.Contrib.Data.Uow.EF/UnitOfWork.cs rename to src/Data/MASA.Contrib.Data.UoW.EF/UnitOfWork.cs index bb619503f..eaacfefac 100644 --- a/src/Data/MASA.Contrib.Data.Uow.EF/UnitOfWork.cs +++ b/src/Data/MASA.Contrib.Data.UoW.EF/UnitOfWork.cs @@ -1,16 +1,18 @@ -namespace MASA.Contrib.Data.Uow.EF; +namespace MASA.Contrib.Data.UoW.EF; -public class UnitOfWork : IUnitOfWork +public class UnitOfWork : IAsyncDisposable, IUnitOfWork where TDbContext : MasaDbContext { public DbTransaction Transaction { get { + if (!UseTransaction) + throw new NotSupportedException("Doesn't support transaction opening"); + if (TransactionHasBegun) - { return _context.Database.CurrentTransaction!.GetDbTransaction(); - } + return _context.Database.BeginTransaction().GetDbTransaction(); } } @@ -19,11 +21,17 @@ public DbTransaction Transaction public bool DisableRollbackOnFailure { get; set; } + public EntityState EntityState { get; set; } + + public CommitState CommitState { get; set; } + + public bool UseTransaction { get; set; } = true; + private readonly DbContext _context; - private readonly ILogger> _logger; + private readonly ILogger>? _logger; - public UnitOfWork(TDbContext dbContext, ILogger> logger) + public UnitOfWork(TDbContext dbContext, ILogger>? logger = null) { _context = dbContext; _logger = logger; @@ -32,42 +40,27 @@ public UnitOfWork(TDbContext dbContext, ILogger> logger) public async Task SaveChangesAsync(CancellationToken cancellationToken = default) { await _context.SaveChangesAsync(cancellationToken); + EntityState = EntityState.Unchanged; } public async Task CommitAsync(CancellationToken cancellationToken = default) { - if (!TransactionHasBegun) - { - await SaveChangesAsync(); - return; - } + if (!UseTransaction || !TransactionHasBegun) + throw new NotSupportedException("Transaction not opened"); - try - { - await SaveChangesAsync(); - await _context.Database.CommitTransactionAsync(cancellationToken); - } - catch (Exception ex) - { - if (DisableRollbackOnFailure) - { - _logger.LogError(ex, "Failed to commit transaction"); - throw; - } - await _context.Database.RollbackTransactionAsync(cancellationToken); - _logger.LogError(ex, "Failed to commit transaction, rolled back"); - } + await _context.Database.CommitTransactionAsync(cancellationToken); + CommitState = CommitState.Commited; } public async Task RollbackAsync(CancellationToken cancellationToken = default) { - if (!TransactionHasBegun) - throw new NotSupportedException("Transactions are not started and rollback is not supported"); + if (!UseTransaction || !TransactionHasBegun) + throw new NotSupportedException("Transactions are not opened and rollback is not supported"); + await _context.Database.RollbackTransactionAsync(cancellationToken); } - public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } + public ValueTask DisposeAsync() => _context.DisposeAsync(); + + public void Dispose() => _context.Dispose(); } diff --git a/src/Data/MASA.Contrib.Data.Uow.EF/_Imports.cs b/src/Data/MASA.Contrib.Data.UoW.EF/_Imports.cs similarity index 67% rename from src/Data/MASA.Contrib.Data.Uow.EF/_Imports.cs rename to src/Data/MASA.Contrib.Data.UoW.EF/_Imports.cs index 005db3fab..cfd1ca3f9 100644 --- a/src/Data/MASA.Contrib.Data.Uow.EF/_Imports.cs +++ b/src/Data/MASA.Contrib.Data.UoW.EF/_Imports.cs @@ -1,4 +1,4 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.Utils.Data.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore; @@ -6,3 +6,6 @@ global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; global using System.Data.Common; +global using System.Text.Json.Serialization; +global using EntityState = MASA.BuildingBlocks.Data.UoW.EntityState; + diff --git a/src/Data/MASA.Contrib.Data.Uow.EF/MASA.Contrib.Data.Uow.EF.csproj b/src/Data/MASA.Contrib.Data.Uow.EF/MASA.Contrib.Data.Uow.EF.csproj deleted file mode 100644 index 9a41bc05d..000000000 --- a/src/Data/MASA.Contrib.Data.Uow.EF/MASA.Contrib.Data.Uow.EF.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - - - - - diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Event.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Event.cs index bcab2ea47..9431ab1d1 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Event.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Event.cs @@ -1,16 +1,12 @@ namespace MASA.Contrib.Dispatcher.Events; -public record Event : IEvent +public record Event(Guid Id, DateTime CreationTime) : IEvent { - public Guid Id { get; init; } + [JsonIgnore] + public Guid Id { get; } = Id; - public DateTime CreationTime { get; init; } + [JsonIgnore] + public DateTime CreationTime { get; } = CreationTime; public Event() : this(Guid.NewGuid(), DateTime.UtcNow) { } - - public Event(Guid id, DateTime creationTime) - { - this.Id = id; - this.CreationTime = creationTime; - } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/EventBus.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/EventBus.cs index 4b8c5ddd2..1f7cd0cd8 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/EventBus.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/EventBus.cs @@ -10,6 +10,8 @@ public class EventBus : IEventBus private IUnitOfWork? _unitOfWork; + private readonly string LoadEventHelpLink = "https://github.com/masastack/MASA.Contrib/tree/develop/docs/LoadEvent.md"; + public EventBus(IServiceProvider serviceProvider, IOptions options) { _serviceProvider = serviceProvider; @@ -19,14 +21,21 @@ public EventBus(IServiceProvider serviceProvider, IOptions op public async Task PublishAsync(TEvent @event) where TEvent : IEvent { + var eventType = typeof(TEvent); if (@event is null) { - throw new ArgumentNullException(typeof(TEvent).Name); + throw new ArgumentNullException(eventType.Name); } var middlewares = _serviceProvider.GetRequiredService>>(); - if (@event is ITransaction transactionEvent) + if (!_options.UnitOfWorkRelation.ContainsKey(eventType)) { + throw new NotSupportedException($"Getting \"{eventType.Name}\" relationship chain failed, see {LoadEventHelpLink} for details. "); + } + + if (_options.UnitOfWorkRelation[eventType]) + { + ITransaction transactionEvent = (ITransaction) @event; var unitOfWork = _serviceProvider.GetService(); if (unitOfWork != null) { @@ -42,12 +51,17 @@ public async Task PublishAsync(TEvent @event) where TEvent : IEvent } } - EventHandlerDelegate publishEvent = async () => - { - await _dispatcher.PublishEventAsync(_serviceProvider, @event); - }; + EventHandlerDelegate publishEvent = async () => { await _dispatcher.PublishEventAsync(_serviceProvider, @event); }; await middlewares.Reverse().Aggregate(publishEvent, (next, middleware) => () => middleware.HandleAsync(@event, next))(); } - public IEnumerable GetAllEventTypes() => _options.GetAllEventTypes(); + public IEnumerable GetAllEventTypes() => _options.AllEventTypes; + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + if (_unitOfWork is null) + throw new ArgumentNullException("You need to UseUoW when adding services"); + + await _unitOfWork.CommitAsync(cancellationToken); + } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatchRelationNetwork.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatchRelationNetwork.cs index 9c79ebf3d..645c988a3 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatchRelationNetwork.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatchRelationNetwork.cs @@ -8,9 +8,9 @@ internal class DispatchRelationNetwork public Dictionary> CancelRelationNetwork { get; set; } = new(); - private readonly ILogger _logger; + private readonly ILogger? _logger; - public DispatchRelationNetwork(ILogger logger) => _logger = logger; + public DispatchRelationNetwork(ILogger? logger) => _logger = logger; public void Add(Type keyEventType, EventHandlerAttribute handler) { @@ -82,7 +82,7 @@ private void CheckConstraints() { foreach (var cancelRelation in CancelRelationNetwork) { - if (!HandlerRelationNetwork.Any(relation => relation.Key == cancelRelation.Key)) + if (HandlerRelationNetwork.All(relation => relation.Key != cancelRelation.Key)) { throw new NotSupportedException($"{cancelRelation.Key.Name} is only have a cancel handler, it must have an event handler."); } @@ -92,7 +92,7 @@ private void CheckConstraints() if (maxHandlerOrder == null || maxHandlerOrder.IsHandlerMissing(maxCancelOrder)) { var methodName = cancelRelation.Value.Select(x => x.ActionMethodInfo.Name).LastOrDefault(); - _logger.LogWarning($"The {methodName} method is meaningless, because its Order attribute is too large, and no handler corresponding to the Order can be triggered. It is suggested to lower the Order attribute of {methodName} or add a matching handler - {cancelRelation.Value.Select(x => x.InstanceType.FullName).LastOrDefault()}"); + _logger?.LogWarning($"The {methodName} method is meaningless, because its Order attribute is too large, and no handler corresponding to the Order can be triggered. It is suggested to lower the Order attribute of {methodName} or add a matching handler - {cancelRelation.Value.Select(x => x.InstanceType.FullName).LastOrDefault()}"); } } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/Dispatcher.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/Dispatcher.cs index 3e789509b..412d5ab03 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/Dispatcher.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/Dispatcher.cs @@ -2,11 +2,11 @@ namespace MASA.Contrib.Dispatcher.Events.Internal.Dispatch; internal class Dispatcher : DispatcherBase { - public Dispatcher(IServiceCollection services, bool forceInit = false) : base(services, forceInit) { } + public Dispatcher(IServiceCollection services, Assembly[] assemblies, bool forceInit = false) : base(services, assemblies, forceInit) { } - public Dispatcher Build(ServiceLifetime lifetime, params Assembly[] assemblies) + public Dispatcher Build(ServiceLifetime lifetime) { - foreach (var assembly in assemblies) + foreach (var assembly in _assemblies) { AddRelationNetwork(assembly); } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs index 6dd13ab60..7074e5be1 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs @@ -2,39 +2,40 @@ namespace MASA.Contrib.Dispatcher.Events.Internal.Dispatch; internal class DispatcherBase { - protected static DispatchRelationNetwork _sharingRelationNetwork; + protected static DispatchRelationNetwork? _sharingRelationNetwork; protected readonly IServiceCollection _services; - private readonly ILogger _logger; + protected readonly Assembly[] _assemblies; - public DispatcherBase(IServiceCollection services, bool forceInit) + private readonly ILogger? _logger; + + public DispatcherBase(IServiceCollection services, Assembly[] assemblies, bool forceInit) { _services = services; + _assemblies = assemblies; var serviceProvider = services.BuildServiceProvider(); if (_sharingRelationNetwork == null || forceInit) { - _sharingRelationNetwork = new DispatchRelationNetwork(serviceProvider.GetRequiredService>()); + _sharingRelationNetwork = new DispatchRelationNetwork(serviceProvider.GetService>()); } - _logger = serviceProvider.GetRequiredService>(); + _logger = serviceProvider.GetService>(); } public async Task PublishEventAsync(IServiceProvider serviceProvider, TEvent @event) where TEvent : IEvent { var eventType = typeof(TEvent); - if (!_sharingRelationNetwork.RelationNetwork.TryGetValue(eventType, out List? dispatchRelations) || dispatchRelations == null) + if (!_sharingRelationNetwork!.RelationNetwork.TryGetValue(eventType, out List? dispatchRelations)) { - if(@event is IIntegrationEvent) + if (@event is IIntegrationEvent) { - _logger.LogError($"Dispatcher: The current event is an out-of-process event. You should use IIntegrationEventBus or IDomainEventBus to send it"); + _logger?.LogError($"Dispatcher: The current event is an out-of-process event. You should use IIntegrationEventBus or IDomainEventBus to send it"); throw new ArgumentNullException($"The current event is an out-of-process event. You should use IIntegrationEventBus or IDomainEventBus to send it"); } - else - { - _logger.LogError($"Dispatcher: The {eventType.FullName} Handler method was not found. Check to see if the EventHandler feature is added to the method and if the Assembly is specified when using EventBus"); - throw new ArgumentNullException($"The {eventType.FullName} Handler method was not found. Check to see if the EventHandler feature is added to the method and if the Assembly is specified when using EventBus"); - } + + _logger?.LogError($"Dispatcher: The {eventType.FullName} Handler method was not found. Check to see if the EventHandler feature is added to the method and if the Assembly is specified when using EventBus"); + throw new ArgumentNullException($"The {eventType.FullName} Handler method was not found. Check to see if the EventHandler feature is added to the method and if the Assembly is specified when using EventBus"); } await ExecuteEventHandlerAsync(serviceProvider, dispatchRelations, @event); } @@ -57,7 +58,7 @@ private async Task ExecuteEventHandlerAsync(IServiceProvider serviceProv await executionStrategy.ExecuteAsync(strategyOptions, @event, async (@event) => { - _logger.LogDebug("----- Publishing event {@Event}: message id: {messageId} -----", @event, @event.Id); + _logger?.LogDebug("----- Publishing event {@Event}: message id: {messageId} -----", @event, @event.Id); await dispatchHandler.ExcuteAction(serviceProvider, @event); }, async (@event, ex, failureLevels) => { @@ -75,14 +76,14 @@ await executionStrategy.ExecuteAsync(strategyOptions, @event, async (@event) => } else { - _logger.LogWarning("----- Publishing event {@Event} error rollback is ignored: message id: {messageId} -----", @event, @event.Id); + _logger?.LogWarning("----- Publishing event {@Event} error rollback is ignored: message id: {messageId} -----", @event, @event.Id); } }); } } private async Task ExecuteEventCanceledHandlerAsync(IServiceProvider serviceProvider, - ILogger logger, + ILogger? logger, IExecutionStrategy executionStrategy, IEnumerable cancelHandlers, TEvent @event) @@ -92,9 +93,9 @@ private async Task ExecuteEventCanceledHandlerAsync(IServiceProvider ser foreach (var cancelHandler in cancelHandlers) { strategyOptions.SetStrategy(cancelHandler); - await executionStrategy.ExecuteAsync(strategyOptions, @event, async (@event) => + await executionStrategy.ExecuteAsync(strategyOptions, @event, async @event => { - logger.LogDebug("----- Publishing event {@Event} rollback start: message id: {messageId} -----", @event, @event.Id); + logger?.LogDebug("----- Publishing event {@Event} rollback start: message id: {messageId} -----", @event, @event.Id); await cancelHandler.ExcuteAction(serviceProvider, @event); }, (@event, ex, failureLevels) => { @@ -102,7 +103,7 @@ await executionStrategy.ExecuteAsync(strategyOptions, @event, async (@event) => { throw ex; } - logger.LogWarning("----- Publishing event {@Event} rollback error ignored: message id: {messageId} -----", @event, @event.Id); + logger?.LogWarning("----- Publishing event {@Event} rollback error ignored: message id: {messageId} -----", @event, @event.Id); return Task.CompletedTask; }); } @@ -110,16 +111,16 @@ await executionStrategy.ExecuteAsync(strategyOptions, @event, async (@event) => protected void AddRelationNetwork(Type parameterType, EventHandlerAttribute handler) { - _sharingRelationNetwork.Add(parameterType, handler); + _sharingRelationNetwork!.Add(parameterType, handler); } - protected IEnumerable GetAddServiceTypeList() => _sharingRelationNetwork.HandlerRelationNetwork + protected IEnumerable GetAddServiceTypeList() => _sharingRelationNetwork!.HandlerRelationNetwork .Concat(_sharingRelationNetwork.CancelRelationNetwork) .SelectMany(relative => relative.Value) .Where(dispatchHandler => dispatchHandler.InvokeDelegate != null) .Select(dispatchHandler => dispatchHandler.InstanceType).Distinct(); - protected void Build() => _sharingRelationNetwork.Build(); + protected void Build() => _sharingRelationNetwork!.Build(); protected bool IsSagaMode(Type handlerType, MethodInfo method) => typeof(IEventHandler<>).IsGenericInterfaceAssignableFrom(handlerType) && method.Name.Equals(nameof(IEventHandler.HandleAsync)) || diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/SagaDispatcher.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/SagaDispatcher.cs index af24ed1e8..c68170662 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/SagaDispatcher.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/SagaDispatcher.cs @@ -2,18 +2,18 @@ namespace MASA.Contrib.Dispatcher.Events.Internal.Dispatch; internal class SagaDispatcher : DispatcherBase { - public SagaDispatcher(IServiceCollection services, bool forceInit = false) : base(services, forceInit) { } + public SagaDispatcher(IServiceCollection services, Assembly[] assemblies, bool forceInit = false) : base(services, assemblies, forceInit) { } - public SagaDispatcher Build(ServiceLifetime lifetime, params Assembly[] assemblies) + public SagaDispatcher Build(ServiceLifetime lifetime) { - AddSagaDispatchRelation(_services, typeof(IEventHandler<>), lifetime, assemblies); - AddSagaDispatchRelation(_services, typeof(ISagaEventHandler<>), lifetime, assemblies); + AddSagaDispatchRelation(_services, typeof(IEventHandler<>), lifetime); + AddSagaDispatchRelation(_services, typeof(ISagaEventHandler<>), lifetime); return this; } - private IServiceCollection AddSagaDispatchRelation(IServiceCollection services, Type eventBusHandlerType, ServiceLifetime lifetime, params Assembly[] assemblies) + private IServiceCollection AddSagaDispatchRelation(IServiceCollection services, Type eventBusHandlerType, ServiceLifetime lifetime) { - foreach (var item in GetAddSagaServices(eventBusHandlerType, assemblies)) + foreach (var item in GetAddSagaServices(eventBusHandlerType)) { services.Add(item.ServiceType, item.ImplementationType, lifetime); AddSagaRelationNetwork(item.ImplementationType); @@ -80,10 +80,10 @@ private List GetSagaHandlers(Type eventBusHandlerType) return eventHandlers; } - private List<(Type ServiceType, Type ImplementationType)> GetAddSagaServices(Type eventBusHandlerType, params Assembly[] assemblies) + private List<(Type ServiceType, Type ImplementationType)> GetAddSagaServices(Type eventBusHandlerType) { List<(Type ServiceType, Type ImplementationType)> list = new(); - var serviceTypeAndImplementationInfo = GetSagaServiceTypeAndImplementations(eventBusHandlerType, assemblies); + var serviceTypeAndImplementationInfo = GetSagaServiceTypeAndImplementations(eventBusHandlerType); foreach (var serviceType in serviceTypeAndImplementationInfo.ServiceTypeList) { var implementationTypes = serviceTypeAndImplementationInfo.ImplementationType.Where(implementationType => serviceType.IsAssignableFrom(implementationType)).ToList(); @@ -97,11 +97,11 @@ private List GetSagaHandlers(Type eventBusHandlerType) return list; } - private (List ServiceTypeList, List ImplementationType) GetSagaServiceTypeAndImplementations(Type eventBusHandlerType, params Assembly[] assemblies) + private (List ServiceTypeList, List ImplementationType) GetSagaServiceTypeAndImplementations(Type eventBusHandlerType) { var concretions = new List(); var interfaces = new List(); - foreach (var type in assemblies.SelectMany(a => a.DefinedTypes).Where(t => !t.IsGeneric())) + foreach (var type in _assemblies.SelectMany(a => a.DefinedTypes).Where(t => !t.IsGeneric())) { if (type.IsConcrete()) { diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Middleware/TransactionMiddleware.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Middleware/TransactionMiddleware.cs index 91e1fd9de..a1abbcedf 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Middleware/TransactionMiddleware.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Middleware/TransactionMiddleware.cs @@ -3,11 +3,11 @@ namespace MASA.Contrib.Dispatcher.Events.Internal.Middleware; public class TransactionMiddleware : IMiddleware where TEvent : notnull, IEvent { - private readonly ILogger> _logger; + private readonly IUnitOfWork? _unitOfWork; - public TransactionMiddleware(ILogger> logger) + public TransactionMiddleware(IUnitOfWork? unitOfWork = null) { - _logger = logger; + _unitOfWork = unitOfWork; } public async Task HandleAsync(TEvent @event, EventHandlerDelegate next) @@ -16,33 +16,34 @@ public async Task HandleAsync(TEvent @event, EventHandlerDelegate next) { await next(); - if (@event is ITransaction transactionEvent) + if (_unitOfWork is { EntityState: EntityState.Changed }) { - if (transactionEvent.UnitOfWork != null) - { - if (transactionEvent.UnitOfWork.TransactionHasBegun) - { - await transactionEvent.UnitOfWork.CommitAsync(); - } - else - { - await transactionEvent.UnitOfWork.SaveChangesAsync(); - } - } + await _unitOfWork.SaveChangesAsync(); } - } - catch (Exception ex) - { - _logger.LogError(ex, nameof(TransactionMiddleware)); - - if (@event is ITransaction transactionEvent && transactionEvent.UnitOfWork != null && transactionEvent.UnitOfWork.TransactionHasBegun && !transactionEvent.UnitOfWork.DisableRollbackOnFailure) + if (IsUseTransaction(@event, out ITransaction? transaction)) { - await transactionEvent.UnitOfWork.RollbackAsync(); + await transaction!.UnitOfWork!.CommitAsync(); } - else + } + catch (Exception) + { + if (IsUseTransaction(@event, out ITransaction? transaction) && !transaction!.UnitOfWork!.DisableRollbackOnFailure) { - throw; + await transaction.UnitOfWork!.RollbackAsync(); } + throw; } } + + private bool IsUseTransaction(TEvent @event, out ITransaction? transaction) + { + if (@event is ITransaction { UnitOfWork: { UseTransaction: true, TransactionHasBegun: true, CommitState: CommitState.UnCommited } } transactionEvent) + { + transaction = transactionEvent; + return true; + } + + transaction = null; + return false; + } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/MASA.Contrib.Dispatcher.Events.csproj b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/MASA.Contrib.Dispatcher.Events.csproj index a80128258..1e61ad2c1 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/MASA.Contrib.Dispatcher.Events.csproj +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/MASA.Contrib.Dispatcher.Events.csproj @@ -7,13 +7,16 @@ - - - - - - - + + + + + + + + + + diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Options/DispatcherOptions.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Options/DispatcherOptions.cs index 6e7cd6a06..3db90c640 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Options/DispatcherOptions.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Options/DispatcherOptions.cs @@ -2,7 +2,7 @@ namespace MASA.Contrib.Dispatcher.Events.Options; public class DispatcherOptions : IDispatcherOptions { - private Assembly[] _assemblies = new Assembly[0]; + private Assembly[] _assemblies = Array.Empty(); public Assembly[] Assemblies { @@ -14,20 +14,23 @@ public Assembly[] Assemblies { throw new ArgumentNullException(nameof(_assemblies)); } - Types = _assemblies.SelectMany(assembly => assembly.GetTypes()).ToList(); - _allEventTypes = GetTypes(typeof(IEvent)).ToList(); + AllEventTypes = _assemblies + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && typeof(IEvent).IsAssignableFrom(type)) + .ToList(); + + UnitOfWorkRelation = AllEventTypes.ToDictionary(type => type, IsSupportUnitOfWork); } } - private IEnumerable Types { get; set; } + private bool IsSupportUnitOfWork(Type eventType) + => typeof(ITransaction).IsAssignableFrom(eventType) && !typeof(IDomainQuery<>).IsGenericInterfaceAssignableFrom(eventType); - private IEnumerable GetTypes(Type type) => Types.Where(t => type.IsAssignableFrom(t) && t.IsClass); + internal Dictionary UnitOfWorkRelation { get; set; } = new(); - public IEnumerable GetAllEventTypes() => _allEventTypes; + public IEnumerable AllEventTypes { get; private set; } public IServiceCollection Services { get; } - private IEnumerable _allEventTypes; - public DispatcherOptions(IServiceCollection services) => Services = services; } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.md b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.md index 2ca2d1622..0d6177dd5 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.md +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.md @@ -1,3 +1,5 @@ +[中](README.zh-CN.md) | EN + ## EventBus Example: @@ -13,8 +15,8 @@ Install-Package MASA.Contrib.Dispatcher.Events ```c# var builder = WebApplication.CreateBuilder(args); var app = builder.Services - .AddEventBus() - //TODO + .AddEventBus() + //TODO ``` 2. Custom Event @@ -171,3 +173,22 @@ builder.Services 4. Support Transaction > Used in conjunction with Contracts.EF and UnitOfWork, when Event implements ITransaction, the transaction will be automatically opened after the first CUD is executed, and the transaction will be submitted after all Handlers are executed. When an exception occurs in the transaction, the transaction will be automatically rolled back. + +##### Summarize + +IEventBus is the core of the event bus. It can be used with CQRS, Uow, MASA.Contrib.DDD.Domain.Repository.EF to automatically execute SaveChange (enable UoW) and Commit (enable UoW without closing transaction) operations after sending Command, And support to roll back the transaction after an exception occurs + +> Question 1. Publishing events through eventBus, Handler error -> and handler throw exception + + > 1. Check custom events or inherited classes to make sure ITransaction is implemented + > 2. Confirm that UoW is used + > 3. Make sure the UseTransaction property of UnitOfWork is false + > 4. Make sure that the DisableRollbackOnFailure property of UnitOfWork is true + +> Question 2. Under what circumstances will SaveChange be automatically saved -> When auto call SaveChange? + + > Use UoW and MASA.Contrib.DDD.Domain.Repository.EF, and use the Add, Update, Delete operations provided by IRepository, publish events through EventBus, and automatically execute SaveChange after executing EventHandler + +> Question 3. If the SaveChange method of UoW is manually called in EventHandler to save, will the framework also save automatically? + + > If the SaveChange method of UoW is manually called in the EventHandler to save, and the Add, Update, and Delete operations provided by IRepository are not used afterward, the SaveChange operation will not be executed twice after the EventHandler execution ends, but if the UoW is manually called. After the SaveChange method is saved and continue to use the Add, Update, and Delete operations provided by IRepository, the framework will call the SaveChange operation again to ensure that the data is saved successfully. \ No newline at end of file diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.zh-cn.md b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.zh-CN.md similarity index 68% rename from src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.zh-cn.md rename to src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.zh-CN.md index 7d857d060..271e987d9 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.zh-cn.md +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/README.zh-CN.md @@ -1,3 +1,5 @@ +中 | [EN](README.md) + ## EventBus 用例: @@ -13,8 +15,8 @@ Install-Package MASA.Contrib.Dispatcher.Events ```c# var builder = WebApplication.CreateBuilder(args); var app = builder.Services - .AddEventBus() - //TODO + .AddEventBus() + //TODO ``` 2. 自定义Event @@ -170,4 +172,23 @@ builder.Services 4. 支持Transaction -> 配合Contracts.EF、UnitOfWork使用,当Event实现了ITransaction,会在执行第一次CUD后自动开启事务,且在Handler全部执行后提交事务,当事务出现异常后,会自动回滚事务 +> 配合MASA.Contrib.DDD.Domain.Repository.EF.Repository、UnitOfWork使用,当Event实现了ITransaction,会在执行Add、Update、Delete方法时自动开启事务,且在Handler全部执行后提交事务,当事务出现异常后,会自动回滚事务 + +##### 总结 + +IEventBus是事件总线的核心,配合CQRS、Uow、MASA.Contrib.DDD.Domain.Repository.EF使用,可实现自动执行SaveChange(启用UoW)与Commit(启用UoW且无关闭事务)操作,并支持出现异常后,回滚事务 + +> 问题1. 通过eventBus发布事件,Handler出错,但数据依然保存到数据库中,事务并未回滚 + + > 1. 检查自定义事件或继承类,确保已经实现ITransaction + > 2. 确认已使用UoW + > 3. 确认UnitOfWork的UseTransaction属性为false + > 4. 确认UnitOfWork的DisableRollbackOnFailure属性为true + +> 问题2. 什么时候自动调用SaveChanges + + > 使用UoW且使用了MASA.Contrib.DDD.Domain.Repository.EF,并且使用IRepository提供的Add、Update、Delete操作,通过EventBus发布事件,在执行EventHandler后会自动执行SaveChange + +> 问题3. 如果在EventHandler中手动调用UoW的SaveChange方法保存,那框架还会自动保存吗? + + > 如果在EventHandler中手动调用了UoW的SaveChange方法保存,且之后并未再使用IRepository提供的Add、Update、Delete操作,则在EventHandler执行结束后不会二次执行SaveChange操作,但如果在手动调用UoW的SaveChange方法保存后又继续使用IRepository提供的Add、Update、Delete操作,则框架会再次调用SaveChange操作以确保数据保存成功 \ No newline at end of file diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/ServiceCollectionExtensions.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/ServiceCollectionExtensions.cs index 24b55c4e3..a5f32a733 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/ServiceCollectionExtensions.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/ServiceCollectionExtensions.cs @@ -12,11 +12,9 @@ public static IServiceCollection AddEventBus( ServiceLifetime lifetime, Action? options = null) { - if (services.Any(service => service.ImplementationType == typeof (EventBusProvider))) return services; + if (services.Any(service => service.ImplementationType == typeof(EventBusProvider))) return services; services.AddSingleton(); - services.AddLogging(); - DispatcherOptions dispatcherOptions = new DispatcherOptions(services); options?.Invoke(dispatcherOptions); if (dispatcherOptions.Assemblies.Length == 0) @@ -25,8 +23,8 @@ public static IServiceCollection AddEventBus( } services.AddSingleton(typeof(IOptions), serviceProvider => Microsoft.Extensions.Options.Options.Create(dispatcherOptions)); - services.AddSingleton(new SagaDispatcher(services).Build(lifetime, dispatcherOptions.Assemblies)); - services.AddSingleton(new Internal.Dispatch.Dispatcher(services).Build(lifetime, dispatcherOptions.Assemblies)); + services.AddSingleton(new SagaDispatcher(services, dispatcherOptions.Assemblies).Build(lifetime)); + services.AddSingleton(new Internal.Dispatch.Dispatcher(services, dispatcherOptions.Assemblies).Build(lifetime)); services.TryAdd(typeof(IExecutionStrategy), typeof(ExecutionStrategy), ServiceLifetime.Singleton); services.AddTransient(typeof(IMiddleware<>), typeof(TransactionMiddleware<>)); services.AddScoped(typeof(IEventBus), typeof(EventBus)); @@ -36,21 +34,19 @@ public static IServiceCollection AddEventBus( public static IServiceCollection AddTestEventBus(this IServiceCollection services, ServiceLifetime lifetime, Action? options = null) { - if (services.Any(service => service.ImplementationType == typeof (EventBusProvider))) return services; + if (services.Any(service => service.ImplementationType == typeof(EventBusProvider))) return services; services.AddSingleton(); - services.AddLogging(); - DispatcherOptions dispatcherOptions = new DispatcherOptions(services); options?.Invoke(dispatcherOptions); if (dispatcherOptions.Assemblies.Length == 0) { dispatcherOptions.Assemblies = AppDomain.CurrentDomain.GetAssemblies(); } - services.AddSingleton(typeof(IOptions), serviceProvider => Microsoft.Extensions.Options.Options.Create(dispatcherOptions)); - services.AddSingleton(new SagaDispatcher(services, true).Build(lifetime, dispatcherOptions.Assemblies)); - services.AddSingleton(new Internal.Dispatch.Dispatcher(services).Build(lifetime, dispatcherOptions.Assemblies)); + services.AddSingleton(typeof(IOptions), serviceProvider => Microsoft.Extensions.Options.Options.Create(dispatcherOptions)); + services.AddSingleton(new SagaDispatcher(services, dispatcherOptions.Assemblies, true).Build(lifetime)); + services.AddSingleton(new Internal.Dispatch.Dispatcher(services, dispatcherOptions.Assemblies).Build(lifetime)); services.TryAdd(typeof(IExecutionStrategy), typeof(ExecutionStrategy), ServiceLifetime.Singleton); services.AddTransient(typeof(IMiddleware<>), typeof(TransactionMiddleware<>)); services.AddScoped(typeof(IEventBus), typeof(EventBus)); diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Strategies/ExecutionStrategy.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Strategies/ExecutionStrategy.cs index 348c5b3fc..a467f31fe 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Strategies/ExecutionStrategy.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Strategies/ExecutionStrategy.cs @@ -2,9 +2,9 @@ namespace MASA.Contrib.Dispatcher.Events.Strategies; public class ExecutionStrategy : IExecutionStrategy { - private readonly ILogger _logger; + private readonly ILogger? _logger; - public ExecutionStrategy(ILogger logger) => _logger = logger; + public ExecutionStrategy(ILogger? logger = null) => _logger = logger; public async Task ExecuteAsync(StrategyOptions strategyOptions, TEvent @event, Func func, Func cancel) where TEvent : IEvent @@ -18,7 +18,7 @@ public async Task ExecuteAsync(StrategyOptions strategyOptions, TEvent @ { if (retryTimes > 0) { - _logger.LogWarning("----- Error Publishing event {@Event} start: The {retries}th retrying consume a message failed. message id: {messageId} -----", @event, retryTimes, @event.Id); + _logger?.LogWarning("----- Error Publishing event {@Event} start: The {retries}th retrying consume a message failed. message id: {messageId} -----", @event, retryTimes, @event.Id); } await func.Invoke(@event); return; @@ -27,11 +27,11 @@ public async Task ExecuteAsync(StrategyOptions strategyOptions, TEvent @ { if (retryTimes > 0) { - _logger.LogWarning("----- Error Publishing event {@Event} finish: The {retries}th retrying consume a message failed. message id: {messageId} -----", @event, retryTimes, @event.Id); + _logger?.LogWarning("----- Error Publishing event {@Event} finish: The {retries}th retrying consume a message failed. message id: {messageId} -----", @event, retryTimes, @event.Id); } else { - _logger.LogWarning(ex, "----- Error Publishing event {@Event}: after {retries}th executions and we will stop retrying. message id: {messageId} -----", @event, strategyOptions.MaxRetryCount, @event.Id); + _logger?.LogWarning(ex, "----- Error Publishing event {@Event}: after {retries}th executions and we will stop retrying. message id: {messageId} -----", @event, strategyOptions.MaxRetryCount, @event.Id); } exception = ex; retryTimes++; diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/_Imports.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/_Imports.cs index bce7d4024..b3e839e01 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/_Imports.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/_Imports.cs @@ -1,4 +1,5 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; +global using MASA.BuildingBlocks.DDD.Domain.Events; global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents; global using MASA.Contrib.Dispatcher.Events.Enums; @@ -14,3 +15,4 @@ global using Microsoft.Extensions.Options; global using System.Linq.Expressions; global using System.Reflection; +global using System.Text.Json.Serialization; diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IProcessingServer.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IProcessingServer.cs new file mode 100644 index 000000000..b2eb56cff --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IProcessingServer.cs @@ -0,0 +1,6 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr; + +public interface IProcessingServer +{ + Task ExecuteAsync(CancellationToken stoppingToken); +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IProcessor.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IProcessor.cs new file mode 100644 index 000000000..e33755dde --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IProcessor.cs @@ -0,0 +1,13 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr; + +public interface IProcessor +{ + Task ExecuteAsync(CancellationToken stoppingToken); + + /// + /// Easy to switch between background tasks + /// + /// unit: seconds + /// + Task DelayAsync(int delay); +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEvent.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEvent.cs index 604def92e..b0378e427 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEvent.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEvent.cs @@ -1,21 +1,18 @@ namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr; -public abstract record IntegrationEvent : IIntegrationEvent +public abstract record IntegrationEvent(Guid Id, DateTime CreationTime) : IIntegrationEvent { - public Guid Id { get; init; } + [JsonIgnore] + public Guid Id { get; } = Id; - public DateTime CreationTime { get; init; } + [JsonIgnore] + public DateTime CreationTime { get; } = CreationTime; [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } + [JsonIgnore] public abstract string Topic { get; set; } public IntegrationEvent() : this(Guid.NewGuid(), DateTime.UtcNow) { } - - public IntegrationEvent(Guid id, DateTime creationTime) - { - this.Id = id; - this.CreationTime = creationTime; - } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventBus.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventBus.cs index 7621bd6f3..23aa53e9c 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventBus.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventBus.cs @@ -2,11 +2,11 @@ namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr; public class IntegrationEventBus : IIntegrationEventBus { - private readonly DispatcherOptions dispatcherOptions; + private readonly DispatcherOptions _dispatcherOptions; private readonly DaprClient _dapr; - private readonly ILogger _logger; + private readonly ILogger? _logger; private readonly IIntegrationEventLogService _eventLogService; - private readonly IOptionsMonitor _appConfig; + private readonly IOptionsMonitor? _appConfig; private readonly string _daprPubsubName; private readonly IEventBus? _eventBus; private readonly IUnitOfWork? _unitOfWork; @@ -14,12 +14,12 @@ public class IntegrationEventBus : IIntegrationEventBus public IntegrationEventBus(IOptions options, DaprClient dapr, IIntegrationEventLogService eventLogService, - IOptionsMonitor appConfig, - ILogger logger, + IOptionsMonitor? appConfig = null, + ILogger? logger = null, IEventBus? eventBus = null, IUnitOfWork? unitOfWork = null) { - dispatcherOptions = options.Value; + _dispatcherOptions = options.Value; _dapr = dapr; _eventLogService = eventLogService; _appConfig = appConfig; @@ -30,9 +30,9 @@ public IntegrationEventBus(IOptions options, } public IEnumerable GetAllEventTypes() => - _eventBus == null ? - dispatcherOptions.GetAllEventTypes() : - dispatcherOptions.GetAllEventTypes().Concat(_eventBus.GetAllEventTypes()).Distinct(); + _eventBus == null + ? _dispatcherOptions.AllEventTypes + : _dispatcherOptions.AllEventTypes.Concat(_eventBus.GetAllEventTypes()).Distinct(); public async Task PublishAsync(TEvent @event) where TEvent : IEvent @@ -54,32 +54,47 @@ public async Task PublishAsync(TEvent @event) private async Task PublishIntegrationAsync(TEvent @event) where TEvent : IIntegrationEvent { - try + if (@event.UnitOfWork == null && _unitOfWork != null) + @event.UnitOfWork = _unitOfWork; + + var topicName = @event.Topic; + if (@event.UnitOfWork != null && !@event.UnitOfWork.UseTransaction) { - if (@event.UnitOfWork == null && _unitOfWork != null) - { - @event.UnitOfWork = _unitOfWork; - } - if (@event.UnitOfWork != null) + try { - _logger.LogInformation("----- Saving changes and integrationEvent: {IntegrationEventId}", @event.Id); - await _eventLogService.SaveEventAsync(@event, @event.UnitOfWork.Transaction); - } + _logger?.LogDebug("----- Saving changes and integrationEvent: {IntegrationEventId}", @event.Id); + await _eventLogService.SaveEventAsync(@event, @event.UnitOfWork!.Transaction); - _logger.LogInformation("----- Publishing integration event: {IntegrationEventId_published} from {AppId} - ({IntegrationEvent})", @event.Id, _appConfig.CurrentValue.AppId, @event); + _logger?.LogDebug( + "----- Publishing integration event: {IntegrationEventIdPublished} from {AppId} - ({IntegrationEvent})", @event.Id, + _appConfig?.CurrentValue.AppId ?? string.Empty, @event); - await _eventLogService.MarkEventAsInProgressAsync(@event.Id); + await _eventLogService.MarkEventAsInProgressAsync(@event.Id); - var topicName = @event.Topic; - _logger.LogInformation("Publishing event {Event} to {PubsubName}.{TopicName}", @event, _daprPubsubName, topicName); - await _dapr.PublishEventAsync(_daprPubsubName, topicName, (dynamic)@event); + _logger?.LogDebug("Publishing event {Event} to {PubsubName}.{TopicName}", @event, _daprPubsubName, topicName); + await _dapr.PublishEventAsync(_daprPubsubName, topicName, (dynamic)@event); - await _eventLogService.MarkEventAsPublishedAsync(@event.Id); + await _eventLogService.MarkEventAsPublishedAsync(@event.Id); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error Publishing integration event: {IntegrationEventId} from {AppId} - ({IntegrationEvent})", + @event.Id, _appConfig?.CurrentValue.AppId ?? string.Empty, @event); + LocalQueueProcessor.Default.AddJobs(new IntegrationEventLogItem(@event.Id, @event.Topic, @event)); + await _eventLogService.MarkEventAsFailedAsync(@event.Id); + } } - catch (Exception ex) + else { - _logger.LogError(ex, "Error Publishing integration event: {IntegrationEventId} from {AppId} - ({IntegrationEvent})", @event.Id, _appConfig.CurrentValue.AppId, @event); - await _eventLogService.MarkEventAsFailedAsync(@event.Id); + await _dapr.PublishEventAsync(_daprPubsubName, topicName, (dynamic)@event); } } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + if (_unitOfWork is null) + throw new ArgumentNullException(nameof(IUnitOfWork), "You need to UseUoW when adding services"); + + await _unitOfWork.CommitAsync(cancellationToken); + } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventHostedService.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventHostedService.cs new file mode 100644 index 000000000..66a0074c8 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventHostedService.cs @@ -0,0 +1,20 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr; + +public class IntegrationEventHostedService : BackgroundService +{ + private readonly ILogger? _logger; + private readonly IProcessingServer _processingServer; + + public IntegrationEventHostedService(IProcessingServer processingServer, ILogger? logger) + { + _logger = logger; + _processingServer = processingServer; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger?.LogDebug("----- IntegrationEvent background task is starting"); + + return _processingServer.ExecuteAsync(stoppingToken); + } +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Internal/IntegrationEventLogItem.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Internal/IntegrationEventLogItem.cs new file mode 100644 index 000000000..2875d237e --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Internal/IntegrationEventLogItem.cs @@ -0,0 +1,31 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Internal; + +/// +/// Use the local queue to retry sending failed messages +/// +internal class IntegrationEventLogItem +{ + public Guid EventId { get; } + + public string Topic { get; } + + public DateTime CreationTime { get; } + + public int RetryCount { get; private set; } + + public object Event { get; } + + public IntegrationEventLogItem(Guid eventId, string topic, object @event) + { + EventId = eventId; + Topic = topic; + RetryCount = 0; + CreationTime = DateTime.UtcNow; + Event = @event; + } + + public void Retry() + { + this.RetryCount++; + } +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Internal/LocalQueueProcessor.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Internal/LocalQueueProcessor.cs new file mode 100644 index 000000000..3ffe46da8 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Internal/LocalQueueProcessor.cs @@ -0,0 +1,60 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Internal; + +internal class LocalQueueProcessor +{ + private readonly ConcurrentDictionary _retryEventLogs; + + public static ILogger? Logger; + public static readonly LocalQueueProcessor Default = new(); + + public LocalQueueProcessor() => _retryEventLogs = new(); + + public static void SetLogger(IServiceCollection services) + { + Logger = services.BuildServiceProvider().GetService>(); + } + + public void AddJobs(IntegrationEventLogItem items) + => _retryEventLogs.TryAdd(items.EventId, items); + + public void RemoveJobs(Guid eventId) + => _retryEventLogs.TryRemove(eventId, out _); + + public void RetryJobs(Guid eventId) + { + if (_retryEventLogs.TryGetValue(eventId, out IntegrationEventLogItem? item)) + { + item.Retry(); + } + } + + public bool IsExist(Guid eventId) + => _retryEventLogs.ContainsKey(eventId); + + public void Delete(int maxRetryTimes) + { + var eventLogItems = _retryEventLogs.Values.Where(log => log.RetryCount >= maxRetryTimes - 1).ToList(); + eventLogItems.ForEach(item => RemoveJobs(item.EventId)); + } + + public List RetrieveEventLogsFailedToPublishAsync(int maxRetryTimes, int retryBatchSize) + { + try + { + return _retryEventLogs + .Select(item => item.Value) + .Where(log => log.RetryCount < maxRetryTimes) + .OrderBy(log => log.RetryCount) + .ThenBy(log => log.CreationTime) + .Take(retryBatchSize) + .ToList(); + } + catch (Exception ex) + { + Logger?.LogWarning(ex, "... getting local retry queue error"); + + Thread.Sleep(TimeSpan.FromSeconds(2)); + return new List(); + } + } +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj index 3fc9a9d3f..5abe0bb1f 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj @@ -7,12 +7,15 @@ - - - - - - + + + + + + + + + diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Options/DispatcherOptions.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Options/DispatcherOptions.cs index 828cff347..f5905da71 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Options/DispatcherOptions.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Options/DispatcherOptions.cs @@ -11,15 +11,128 @@ public string PubSubName { if (string.IsNullOrWhiteSpace(value)) { - throw new ArgumentNullException(_pubSubName); + throw new ArgumentNullException(nameof(_pubSubName)); } + _pubSubName = value; } } + /// + /// Local queue maximum number of retries + /// + public int LocalRetryTimes { get; set; } = 3; + + /// + /// maximum number of retries + /// Default is 10 + /// + public int MaxRetryTimes { get; set; } = 10; + + private int _failedRetryInterval = 60; + + /// + /// The interval at which db polls for failure messages. + /// Default is 60 seconds. + /// unit: seconds + /// + public int FailedRetryInterval + { + get => _failedRetryInterval; + set + { + if (value <= 0) + throw new ArgumentException("must be greater than or equal to 0", nameof(FailedRetryInterval)); + + _failedRetryInterval = value; + } + } + + /// + /// Minimum execution retry interval + /// Default is 60 seconds. + /// + public int MinimumRetryInterval { get; set; } = 60; + + private int _localFailedRetryInterval = 3; + + /// + /// The interval at which the local queue is polled for failed messages. + /// Local queue does not rebuild after service crash + /// Default is 3 seconds. + /// unit: seconds + /// + public int LocalFailedRetryInterval + { + get => _localFailedRetryInterval; + set + { + if (value <= 0) + throw new ArgumentException("must be greater than or equal to 0", nameof(LocalFailedRetryInterval)); + + _localFailedRetryInterval = value; + } + } + + /// + /// maximum number of retries per retry + /// + public int RetryBatchSize { get; set; } = 100; + + private int _cleaningLocalQueueExpireInterval = 60; + + /// + /// Delete local queue expired event interval + /// Default is 60 seconds + /// unit: seconds + /// + public int CleaningLocalQueueExpireInterval + { + get => _cleaningLocalQueueExpireInterval; + set + { + if (value <= 0) + throw new ArgumentException("must be greater than or equal to 0", nameof(CleaningLocalQueueExpireInterval)); + + _cleaningLocalQueueExpireInterval = value; + } + } + + private int _cleaningExpireInterval = 300; + + /// + /// Delete expired event interval + /// Default is 300 seconds. + /// unit: seconds + /// + public int CleaningExpireInterval + { + get => _cleaningExpireInterval; + set + { + if (value <= 0) + throw new ArgumentException("must be greater than or equal to 0", nameof(CleaningExpireInterval)); + + _cleaningExpireInterval = value; + } + } + + /// + /// Expiration time, when the message status is successful and has expired, it will be deleted by the scheduled task + /// Default: ( 24 * 3600 )s + /// + public long PublishedExpireTime { get; set; } = 24 * 3600; + + /// + /// Bulk delete expired messages + /// + public int DeleteBatchCount { get; set; } = 1000; + + public Func? GetCurrentTime { get; set; } = null; + public IServiceCollection Services { get; } - private Assembly[] _assemblies = new Assembly[0]; + private Assembly[] _assemblies = Array.Empty(); public Assembly[] Assemblies { @@ -31,18 +144,15 @@ public Assembly[] Assemblies { throw new ArgumentNullException(nameof(_assemblies)); } - Types = _assemblies.SelectMany(assembly => assembly.GetTypes()).ToList(); - AllEventTypes = GetTypes(typeof(IEvent)).ToList(); + + AllEventTypes = _assemblies + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && typeof(IEvent).IsAssignableFrom(type)) + .ToList(); } } - private List Types { get; set; } - - private List AllEventTypes { get; set; } - - private IEnumerable GetTypes(Type type) => Types.Where(t => type.IsAssignableFrom(t) && t.IsClass && t != typeof(IntegrationEvent)); - - public IEnumerable GetAllEventTypes() => AllEventTypes; + public List AllEventTypes { get; private set; } public DispatcherOptions(IServiceCollection services) => Services = services; } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/DeleteLocalQueueExpiresProcessor.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/DeleteLocalQueueExpiresProcessor.cs new file mode 100644 index 000000000..f3349ce48 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/DeleteLocalQueueExpiresProcessor.cs @@ -0,0 +1,24 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Processor; + +public class DeleteLocalQueueExpiresProcessor : ProcessorBase +{ + private readonly IOptions _options; + + public DeleteLocalQueueExpiresProcessor(IOptions options) + { + _options = options; + } + + /// + /// Delete expired events + /// + /// + /// + public override Task ExecuteAsync(CancellationToken stoppingToken) + { + LocalQueueProcessor.Default.Delete(_options.Value.LocalRetryTimes); + return Task.CompletedTask; + } + + public override int Delay => _options.Value.CleaningLocalQueueExpireInterval; +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/DeletePublishedExpireEventProcessor.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/DeletePublishedExpireEventProcessor.cs new file mode 100644 index 000000000..cd6d24cf8 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/DeletePublishedExpireEventProcessor.cs @@ -0,0 +1,32 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Processor; + +public class DeletePublishedExpireEventProcessor : ProcessorBase +{ + private readonly IServiceProvider _serviceProvider; + private readonly IOptions _options; + + public DeletePublishedExpireEventProcessor( + IServiceProvider serviceProvider, + IOptions options) + { + _serviceProvider = serviceProvider; + _options = options; + } + + /// + /// Delete expired events + /// + /// + /// + public override async Task ExecuteAsync(CancellationToken stoppingToken) + { + using (var scope = _serviceProvider.CreateScope()) + { + var logService = scope.ServiceProvider.GetRequiredService(); + var expireDate = (_options.Value.GetCurrentTime?.Invoke() ?? DateTime.UtcNow).AddSeconds(-_options.Value.PublishedExpireTime); + await logService.DeleteExpiresAsync(expireDate, _options.Value.DeleteBatchCount, stoppingToken); + } + } + + public override int Delay => _options.Value.CleaningExpireInterval; +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/InfiniteLoopProcessor.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/InfiniteLoopProcessor.cs new file mode 100644 index 000000000..fd032471e --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/InfiniteLoopProcessor.cs @@ -0,0 +1,35 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Processor; + +public class InfiniteLoopProcessor : ProcessorBase +{ + private readonly IProcessor _processor; + private readonly ILogger? _logger; + + public InfiniteLoopProcessor(IProcessor processor, ILogger? logger = null) + { + _processor = processor; + _logger = logger; + } + + public override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + await _processor.ExecuteAsync(stoppingToken); + await DelayAsync(((ProcessorBase)_processor).Delay); + } + catch (OperationCanceledException) + { + //ignore + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "Processor '{ProcessorName}' failed", _processor.ToString()); + + Thread.Sleep(TimeSpan.FromSeconds(2)); + } + } + } +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/ProcessorBase.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/ProcessorBase.cs new file mode 100644 index 000000000..2529ada89 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/ProcessorBase.cs @@ -0,0 +1,19 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Processor; + +public abstract class ProcessorBase : IProcessor +{ + public abstract Task ExecuteAsync(CancellationToken stoppingToken); + + // /// + // /// Easy to switch between background tasks + // /// + /// unit: seconds + // /// + public Task DelayAsync(int delay) + => Task.Delay(TimeSpan.FromSeconds(delay)); + + /// + /// Task delay time, unit: seconds + /// + public virtual int Delay { get; } +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/RetryByDataProcessor.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/RetryByDataProcessor.cs new file mode 100644 index 000000000..fafb1dcb2 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/RetryByDataProcessor.cs @@ -0,0 +1,76 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Processor; + +public class RetryByDataProcessor : ProcessorBase +{ + private readonly IServiceProvider _serviceProvider; + private readonly IOptions _options; + private readonly IOptionsMonitor _appConfig; + private readonly ILogger? _logger; + + public RetryByDataProcessor( + IServiceProvider serviceProvider, + IOptionsMonitor appConfig, + IOptions options, + ILogger? logger = null) + { + _serviceProvider = serviceProvider; + _appConfig = appConfig; + _options = options; + _logger = logger; + } + + public override async Task ExecuteAsync(CancellationToken stoppingToken) + { + using (var scope = _serviceProvider.CreateScope()) + { + var unitOfWork = scope.ServiceProvider.GetService(); + if (unitOfWork != null) + unitOfWork.UseTransaction = false; + + var dapr = _serviceProvider.GetRequiredService(); + var eventLogService = scope.ServiceProvider.GetRequiredService(); + + var retrieveEventLogs = + await eventLogService.RetrieveEventLogsFailedToPublishAsync(_options.Value.RetryBatchSize, _options.Value.MaxRetryTimes, _options.Value.MinimumRetryInterval); + + foreach (var eventLog in retrieveEventLogs) + { + try + { + if (LocalQueueProcessor.Default.IsExist(eventLog.EventId)) + continue; // The local queue is retrying, no need to retry + + await eventLogService.MarkEventAsInProgressAsync(eventLog.EventId); + + _logger?.LogDebug("Publishing integration event {Event} to {PubsubName}.{TopicName}", eventLog, + _options.Value.PubSubName, + eventLog.Event.Topic); + + await dapr.PublishEventAsync(_options.Value.PubSubName, eventLog.Event.Topic, eventLog.Event, stoppingToken); + + LocalQueueProcessor.Default.RemoveJobs(eventLog.EventId); + + await eventLogService.MarkEventAsPublishedAsync(eventLog.EventId); + } + catch (UserFriendlyException) + { + //Update state due to multitasking contention, no processing required + } + catch (Exception ex) + { + _logger?.LogError(ex, + "Error Publishing integration event: {IntegrationEventId} from {AppId} - ({IntegrationEvent})", + eventLog.EventId, _appConfig.CurrentValue.AppId, eventLog); + await eventLogService.MarkEventAsFailedAsync(eventLog.EventId); + } + finally + { + if (unitOfWork != null && unitOfWork.TransactionHasBegun) + await unitOfWork.CommitAsync(stoppingToken); + } + } + } + } + + public override int Delay => _options.Value.FailedRetryInterval; +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/RetryByLocalQueueProcessor.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/RetryByLocalQueueProcessor.cs new file mode 100644 index 000000000..452cb9b33 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Processor/RetryByLocalQueueProcessor.cs @@ -0,0 +1,72 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Processor; + +public class RetryByLocalQueueProcessor : ProcessorBase +{ + private readonly IServiceProvider _serviceProvider; + private readonly IOptionsMonitor _appConfig; + private readonly IOptions _options; + private readonly ILogger? _logger; + + public RetryByLocalQueueProcessor( + IServiceProvider serviceProvider, + IOptionsMonitor appConfig, + IOptions options, + ILogger? logger = null) + { + _serviceProvider = serviceProvider; + _appConfig = appConfig; + _options = options; + _logger = logger; + } + + public override async Task ExecuteAsync(CancellationToken stoppingToken) + { + using (var scope = _serviceProvider.CreateScope()) + { + var unitOfWork = scope.ServiceProvider.GetService(); + if (unitOfWork != null) + unitOfWork.UseTransaction = false; + + var dapr = _serviceProvider.GetRequiredService(); + var eventLogService = scope.ServiceProvider.GetRequiredService(); + + var retrieveEventLogs = LocalQueueProcessor.Default.RetrieveEventLogsFailedToPublishAsync(_options.Value.LocalRetryTimes, _options.Value.RetryBatchSize); + + foreach (var eventLog in retrieveEventLogs) + { + try + { + LocalQueueProcessor.Default.RetryJobs(eventLog.EventId); + + await eventLogService.MarkEventAsInProgressAsync(eventLog.EventId); + + _logger?.LogDebug( + "Publishing integration event {Event} to {PubsubName}.{TopicName}", + eventLog, + _options.Value.PubSubName, + eventLog.Topic); + + await dapr.PublishEventAsync(_options.Value.PubSubName, eventLog.Topic, eventLog.Event, stoppingToken); + + await eventLogService.MarkEventAsPublishedAsync(eventLog.EventId); + + LocalQueueProcessor.Default.RemoveJobs(eventLog.EventId); + } + catch (UserFriendlyException) + { + //Update state due to multitasking contention + LocalQueueProcessor.Default.RemoveJobs(eventLog.EventId); + } + catch (Exception ex) + { + _logger?.LogError(ex, + "Error Publishing integration event: {IntegrationEventId} from {AppId} - ({IntegrationEvent})", + eventLog.EventId, _appConfig.CurrentValue.AppId, eventLog); + await eventLogService.MarkEventAsFailedAsync(eventLog.EventId); + } + } + } + } + + public override int Delay => _options.Value.LocalFailedRetryInterval; +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md index b41714267..4b2017a7b 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md @@ -1,11 +1,13 @@ +[中](README.zh-CN.md) | EN + ## IntegrationEventBus -Example: +Example: ```C# Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.Dapr //Send cross-process messages Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF //Record cross-process message logs -Install-Package MASA.Contrib.Data.Uow.EF //Use UnitOfWork +Install-Package MASA.Contrib.Data.UoW.EF //Use UnitOfWork ``` 1. Add IIntegrationEventBus @@ -14,7 +16,7 @@ Install-Package MASA.Contrib.Data.Uow.EF //Use UnitOfWork builder.Services .AddDaprEventBus(options=> { - options.UseUoW(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity")) + options.UseUoW(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity")) .UseEventLog(); ) }); @@ -62,4 +64,41 @@ public async Task DomeIntegrationEventHandleAsync(DomeIntegrationEvent @event) { //todo } -``` \ No newline at end of file +``` + +### retry policy + +```C# +builder.Services + .AddDaprEventBus(options=> + { + // options.MaxRetryTimes = 50;//Maximum number of retries, default: 50 + // options.RetryBatchSize = 100;//Number of single retry events, used to get retry events from persistent data source, default 100 + // options.FailedRetryInterval = 60;//Persistent data source retry pause interval, default 60s + // options.CleaningExpireInterval = 300;//Clearing expired event pause interval, unit: s, default 300s + // options.ExpireDate = 24 * 3600;//Expiration time, CreationTime + ExpireDate = Expiration time, default 1 day + + // options.LocalFailedRetryInterval = 3;//Local queue retry pause interval, default 3s + // options.CleaningLocalQueueExpireInterval = 60;//Clearing local queue expired event pause interval, unit: s, default 60s + }); +``` + +Retry is divided into local queue retry and retry from persistent data source: + +local queue: + +Features: +- Short retry interval, support second-level retry interval +- Get data from memory, faster +- After the system crashes, the previous local queue will not be rebuilt, and will be automatically demoted to the persistent queue to retry the task + +Persistent data source queue: + +Features: + +- After the system crashes, the retry queue can be obtained from db or other persistent sources to ensure 100% retry of events +- As a downgrade solution for local memory queues, lower pressure on db or other data sources + +In the case of a single copy, the tasks of the two queues will only be executed in a single queue, and there will be no simultaneous execution of the two queues. +In the case of multiple copies, the same task may be executed by multiple copies. Although we have made idempotent, but the delivery guarantee is At Least Once, it is still possible that the event publishing is successful, but the state change fails. +At this point, the event may be re-sent. We recommend that the task executor retry across events. \ No newline at end of file diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-CN.md b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-CN.md new file mode 100644 index 000000000..af9d84520 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-CN.md @@ -0,0 +1,105 @@ +中 | [EN](README.md) + +## IntegrationEventBus + +用例: + +```C# +Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.Dapr //发送跨进程消息 +Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF //记录跨进程消息日志 +Install-Package MASA.Contrib.Data.UoW.EF //使用工作单元 +``` + +1. 添加IIntegrationEventBus + +```C# +builder.Services + .AddDaprEventBus(options=> + { + options.UseUoW(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"))//使用工作单元,推荐使用 + .UseEventLog(); + ) + }); +``` + +> CustomerDbContext 需要继承IntegrationEventLogContext + +2. 自定义 IntegrationEvent + +```C# +public class DemoIntegrationEvent : IntegrationEvent +{ + public override string Topic { get; set; } = nameof(DemoIntegrationEvent);//dapr topic name + + //todo 自定义属性参数 +} +``` + +3. 自定义CustomDbContext + +```C# +public class CustomDbContext : IntegrationEventLogContext +{ + public DbSet Users { get; set; } = null!; + + public CustomDbContext(MasaDbContextOptions options) : base(options) + { + + } +} +``` + +4. 发送 Event + +```C# +IIntegrationEventBus eventBus;//通过DI得到IIntegrationEventBus +await eventBus.PublishAsync(new DemoIntegrationEvent());//发送跨进程事件 +``` + +5. 订阅事件 + +```C# +[Topic("pubsub", nameof(DomeIntegrationEvent))] +public async Task DomeIntegrationEventHandleAsync(DomeIntegrationEvent @event) +{ + //todo +} +``` + +### 重试策略 + +```C# +builder.Services + .AddDaprEventBus(options=> + { + // options.MaxRetryTimes = 50;//最大重试次数, 默认:50 + // options.RetryBatchSize = 100;//单次重试事件数量, 用于从持久化数据源获取待重试事件, 默认100 + // options.FailedRetryInterval = 60;//持久化数据源重试停歇间隔, 默认60s + // options.CleaningExpireInterval = 300;//清除已过期事件停歇间隔,单位:s, 默认 300s + // options.ExpireDate = 24 * 3600;//过期时间,CreationTime + ExpireDate = 过期时间, 默认1天 + + // options.LocalFailedRetryInterval = 3;//本地队列重试停歇间隔, 默认3s + // options.CleaningLocalQueueExpireInterval = 60;//清除本地队列已过期事件停歇间隔,单位:s, 默认 60s + }); +``` + +重试分为本地队列重试以及从持久化数据源重试两种: + +本地队列: + +特点: +- 重试间隔短,支持秒级别重试间隔 +- 从内存获取数据,速度更快 +- 系统崩溃后,之前的本地队列不会重建,自动降级到持久化队列中重试任务 + +持久化数据源队列: + +特点: + +- 系统崩溃后,可以从db或者其他持久化源获取重试队列,确保事件100%重试 +- 作为本地内存队列的降级方案,对db或者其他数据源压力更低 + +在单副本情况下,两种队列的任务仅会在单个队列中执行,不会存在两个队列同时执行的情况。 +在多副本情况下,同一个任务可能会被多个副本所执行,虽然我们有做幂等,但为交付保证是 At Least Once,仍然有可能出现事件发布成功,但状态更改失败的情况, +此时事件可能会重发,我们建议任务执行者做好对跨事件的重试 + diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-cn.md b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-cn.md deleted file mode 100644 index ba1a819e2..000000000 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-cn.md +++ /dev/null @@ -1,65 +0,0 @@ -## IntegrationEventBus - -用例: - -```C# -Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.Dapr //发送跨进程消息 -Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF //记录跨进程消息日志 -Install-Package MASA.Contrib.Data.Uow.EF //使用工作单元 -``` - -1. 添加IIntegrationEventBus - -```C# -builder.Services - .AddDaprEventBus(options=> - { - options.UseUoW(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"))//使用工作单元,推荐使用 - .UseEventLog(); - ) - }); -``` - -> CustomerDbContext 需要继承IntegrationEventLogContext - -2. 自定义 IntegrationEvent - -```C# -public class DemoIntegrationEvent : IntegrationEvent -{ - public override string Topic { get; set; } = nameof(DemoIntegrationEvent);//dapr topic name - - //todo 自定义属性参数 -} -``` - -3. 自定义CustomDbContext - -```C# -public class CustomDbContext : IntegrationEventLogContext -{ - public DbSet Users { get; set; } = null!; - - public CustomDbContext(MasaDbContextOptions options) : base(options) - { - - } -} -``` - -4. 发送 Event - -```C# -IIntegrationEventBus eventBus;//通过DI得到IIntegrationEventBus -await eventBus.PublishAsync(new DemoIntegrationEvent());//发送跨进程事件 -``` - -5. 订阅事件 - -```C# -[Topic("pubsub", nameof(DomeIntegrationEvent))] -public async Task DomeIntegrationEventHandleAsync(DomeIntegrationEvent @event) -{ - //todo -} -``` \ No newline at end of file diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Servers/DefaultHostedService.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Servers/DefaultHostedService.cs new file mode 100644 index 000000000..20a92ec81 --- /dev/null +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Servers/DefaultHostedService.cs @@ -0,0 +1,20 @@ +namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Servers; + +public class DefaultHostedService : IProcessingServer +{ + private readonly IEnumerable _processors; + private readonly ILogger? _logger; + + public DefaultHostedService(IEnumerable processors, ILogger? logger = null) + { + _processors = processors; + _logger = logger; + } + + public Task ExecuteAsync(CancellationToken stoppingToken) + { + var processorTasks = _processors.Select(processor => new InfiniteLoopProcessor(processor, _logger)) + .Select(process => process.ExecuteAsync(stoppingToken)); + return Task.WhenAll(processorTasks); + } +} diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs index 6aed6ff7f..5281efc2c 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs @@ -3,9 +3,9 @@ namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr; public static class ServiceCollectionExtensions { public static IServiceCollection AddDaprEventBus( - this IServiceCollection services, - Action? options = null) - where TIntegrationEventLogService : class, IIntegrationEventLogService + this IServiceCollection services, + Action? options = null) + where TIntegrationEventLogService : class, IIntegrationEventLogService => services.TryAddDaprEventBus(null, options); internal static IServiceCollection TryAddDaprEventBus( @@ -14,28 +14,40 @@ internal static IServiceCollection TryAddDaprEventBus? options = null) where TIntegrationEventLogService : class, IIntegrationEventLogService { - if (services.Any(service => service.ImplementationType == typeof (IntegrationEventBusProvider))) return services; + if (services.Any(service => service.ImplementationType == typeof(IntegrationEventBusProvider))) + return services; + services.AddSingleton(); var dispatcherOptions = new DispatcherOptions(services); options?.Invoke(dispatcherOptions); + if (dispatcherOptions.Assemblies.Length == 0) - { dispatcherOptions.Assemblies = AppDomain.CurrentDomain.GetAssemblies(); - } - services.TryAddSingleton(typeof(IOptions), serviceProvider => Microsoft.Extensions.Options.Options.Create(dispatcherOptions)); - services.AddLogging(); + services.TryAddSingleton(typeof(IOptions), + serviceProvider => Microsoft.Extensions.Options.Options.Create(dispatcherOptions)); + LocalQueueProcessor.SetLogger(services); services.AddDaprClient(builder); services.AddScoped(); services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.TryAddSingleton(); + services.AddHostedService(); + if (services.All(service => service.ServiceType != typeof(IUnitOfWork))) + { + var logger = services.BuildServiceProvider().GetService>(); + logger?.LogWarning("UoW is not enabled, local messages will not be integrated"); + } return services; } private class IntegrationEventBusProvider { - } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/_Imports.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/_Imports.cs index 4a7056617..1c635a31f 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/_Imports.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/_Imports.cs @@ -1,13 +1,19 @@ global using Dapr.Client; -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents.Logs; +global using MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Internal; global using MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Options; +global using MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Processor; +global using MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Servers; global using MASA.Utils.Models.Config; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; +global using System.Collections.Concurrent; global using System.Reflection; global using System.Text.Json.Serialization; + diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/DispatcherOptionsExtensions.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/DispatcherOptionsExtensions.cs index 712aae5d5..7fe638666 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/DispatcherOptionsExtensions.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/DispatcherOptionsExtensions.cs @@ -14,19 +14,15 @@ public static IDispatcherOptions UseEventLog( Action optionsBuilder) { if (options.Services == null) - { throw new ArgumentNullException(nameof(options.Services)); - } - if(optionsBuilder == null) - { + if (optionsBuilder == null) throw new ArgumentNullException(nameof(optionsBuilder)); - } - if (options.Services.Any(service => service.ImplementationType == typeof (EventLogProvider))) return options; + if (options.Services.Any(service => service.ImplementationType == typeof(EventLogProvider))) return options; options.Services.AddSingleton(); - DbContextExtensions.AddCustomMasaDbContext(options.Services, optionsBuilder); + options.Services.AddCustomMasaDbContext(optionsBuilder); return options; } @@ -41,16 +37,13 @@ public static IDispatcherOptions UseEventLog( this IDispatcherOptions options) where TDbContext : IntegrationEventLogContext { if (options.Services == null) - { throw new ArgumentNullException(nameof(options.Services)); - } if (typeof(TDbContext) == typeof(IntegrationEventLogContext)) - { - throw new NotSupportedException($"{typeof(TDbContext).FullName} must be IntegrationEventLogContext derived classes, or using UseEventLog() replace UseEventLog<{typeof(TDbContext).FullName}>()"); - } + throw new NotSupportedException( + $"{typeof(TDbContext).FullName} must be IntegrationEventLogContext derived classes, or using UseEventLog() replace UseEventLog<{typeof(TDbContext).FullName}>()"); - if (options.Services.Any(service => service.ImplementationType == typeof (EventLogProvider))) return options; + if (options.Services.Any(service => service.ImplementationType == typeof(EventLogProvider))) return options; options.Services.AddSingleton(); options.Services.TryAddScoped(serviceProvider => serviceProvider.GetRequiredService()); @@ -59,6 +52,5 @@ public static IDispatcherOptions UseEventLog( private class EventLogProvider { - } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogContext.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogContext.cs index 57defda70..83f53bcf5 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogContext.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogContext.cs @@ -32,13 +32,22 @@ private void ConfigureEventLogEntry(EntityTypeBuilder build builder.Property(e => e.CreationTime) .IsRequired(); + builder.Property(e => e.ModificationTime) + .IsRequired(); + builder.Property(e => e.State) .IsRequired(); builder.Property(e => e.TimesSent) .IsRequired(); + builder.Property(e => e.RowVersion) + .IsRowVersion(); + builder.Property(e => e.EventTypeName) .IsRequired(); + + builder.HasIndex(e => new { e.State, e.ModificationTime },"index_state_modificationtime"); + builder.HasIndex(e => new { e.State, e.TimesSent, e.ModificationTime },"index_state_timessent_modificationtime"); } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogService.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogService.cs index 480d65c0b..29a04682f 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogService.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogService.cs @@ -4,66 +4,155 @@ public class IntegrationEventLogService : IIntegrationEventLogService { private readonly IntegrationEventLogContext _eventLogContext; private readonly IServiceProvider _serviceProvider; - private IEnumerable _eventTypes; + private readonly Logger? _logger; + private IEnumerable? _eventTypes; - public IntegrationEventLogService(IntegrationEventLogContext eventLogContext, IServiceProvider serviceProvider) + public IntegrationEventLogService( + IntegrationEventLogContext eventLogContext, + IServiceProvider serviceProvider, + Logger? logger = null) { _eventLogContext = eventLogContext; _serviceProvider = serviceProvider; + _logger = logger; } - public async Task> RetrieveEventLogsPendingToPublishAsync(Guid transactionId) + /// + /// Get messages to retry + /// + /// maximum number of retries per retry + /// + /// default: 60s + /// + public async Task> RetrieveEventLogsFailedToPublishAsync(int retryBatchSize = 200, int maxRetryTimes = 10, int minimumRetryInterval = 60) { + //todo: Subsequent acquisition of the current time needs to be uniformly replaced with the unified time method provided by the framework, which is convenient for subsequent uniform replacement to UTC time or other urban time. The default setting here is Utc time. + var time = DateTime.UtcNow.AddSeconds(-minimumRetryInterval); var result = await _eventLogContext.EventLogs - .Where(e => e.TransactionId == transactionId && e.State == IntegrationEventStates.NotPublished).ToListAsync(); + .Where(e => (e.State == IntegrationEventStates.PublishedFailed || e.State == IntegrationEventStates.InProgress) && + e.TimesSent <= maxRetryTimes && + e.ModificationTime < time) + .OrderBy(o => o.CreationTime) + .Take(retryBatchSize) + .ToListAsync(); if (result.Any()) { - if (_eventTypes == null) - { - _eventTypes = _serviceProvider.GetRequiredService().GetAllEventTypes().Where(type => typeof(IIntegrationEvent).IsAssignableFrom(type)); - } + _eventTypes ??= _serviceProvider.GetRequiredService().GetAllEventTypes() + .Where(type => typeof(IIntegrationEvent).IsAssignableFrom(type)); + return result.OrderBy(o => o.CreationTime) .Select(e => e.DeserializeJsonContent(_eventTypes.First(t => t.Name == e.EventTypeShortName))); } - return new List(); + return result; } public async Task SaveEventAsync(IIntegrationEvent @event, DbTransaction transaction) { - if (transaction == null) throw new ArgumentNullException(nameof(transaction)); + if (transaction == null) + throw new ArgumentNullException(nameof(transaction)); + if (_eventLogContext.Database.CurrentTransaction == null) - _eventLogContext.Database.UseTransaction(transaction, Guid.NewGuid()); + await _eventLogContext.Database.UseTransactionAsync(transaction, Guid.NewGuid()); + var eventLogEntry = new IntegrationEventLog(@event, _eventLogContext.Database.CurrentTransaction!.TransactionId); await _eventLogContext.EventLogs.AddAsync(eventLogEntry); await _eventLogContext.SaveChangesAsync(); + + CheckAndDetached(eventLogEntry); } public Task MarkEventAsPublishedAsync(Guid eventId) { - return UpdateEventStatus(eventId, IntegrationEventStates.Published); + return UpdateEventStatus(eventId, IntegrationEventStates.Published, eventLog => + { + if (eventLog.State != IntegrationEventStates.InProgress) + { + _logger?.LogWarning( + "Failed to modify the state of the local message table to {OptState}, the current State is {State}, Id: {Id}", + IntegrationEventStates.Published, eventLog.State, eventLog.Id); + throw new UserFriendlyException($"Failed to modify the state of the local message table to {IntegrationEventStates.Published}, the current State is {eventLog.State}, Id: {eventLog.Id}"); + } + }); } public Task MarkEventAsInProgressAsync(Guid eventId) { - return UpdateEventStatus(eventId, IntegrationEventStates.InProgress); + return UpdateEventStatus(eventId, IntegrationEventStates.InProgress, eventLog => + { + if (eventLog.State != IntegrationEventStates.NotPublished && eventLog.State != IntegrationEventStates.PublishedFailed) + { + _logger?.LogWarning( + "Failed to modify the state of the local message table to {OptState}, the current State is {State}, Id: {Id}", + IntegrationEventStates.InProgress, eventLog.State, eventLog.Id); + throw new UserFriendlyException($"Failed to modify the state of the local message table to {IntegrationEventStates.InProgress}, the current State is {eventLog.State}, Id: {eventLog.Id}"); + } + }); } public Task MarkEventAsFailedAsync(Guid eventId) { - return UpdateEventStatus(eventId, IntegrationEventStates.PublishedFailed); + return UpdateEventStatus(eventId, IntegrationEventStates.PublishedFailed, eventLog => + { + if (eventLog.State != IntegrationEventStates.InProgress) + { + _logger?.LogWarning( + "Failed to modify the state of the local message table to {OptState}, the current State is {State}, Id: {Id}", + IntegrationEventStates.PublishedFailed, eventLog.State, eventLog.Id); + throw new UserFriendlyException($"Failed to modify the state of the local message table to {IntegrationEventStates.PublishedFailed}, the current State is {eventLog.State}, Id: {eventLog.Id}"); + } + }); } - private Task UpdateEventStatus(Guid eventId, IntegrationEventStates status) + public async Task DeleteExpiresAsync(DateTime expiresAt, int batchCount = 1000, CancellationToken token = default) { - var eventLogEntry = _eventLogContext.EventLogs.Single(e => e.Id == eventId); - eventLogEntry.State = status; + var eventLogs = _eventLogContext.EventLogs.Where(e => e.ModificationTime < expiresAt && e.State == IntegrationEventStates.Published) + .OrderBy(e => e.CreationTime).Take(batchCount); + _eventLogContext.EventLogs.RemoveRange(eventLogs); + await _eventLogContext.SaveChangesAsync(token); + if (_eventLogContext.ChangeTracker.QueryTrackingBehavior != QueryTrackingBehavior.TrackAll) + { + foreach (var log in eventLogs) + { + _eventLogContext.Entry(log).State = EntityState.Detached; + } + } + } + + private async Task UpdateEventStatus(Guid eventId, IntegrationEventStates status, Action? action = null) + { + var eventLogEntry = _eventLogContext.EventLogs.FirstOrDefault(e => e.EventId == eventId); + if (eventLogEntry == null) + throw new ArgumentException(nameof(eventId)); + + action?.Invoke(eventLogEntry); + + + eventLogEntry.State = status; + eventLogEntry.ModificationTime = eventLogEntry.GetCurrentTime(); if (status == IntegrationEventStates.InProgress) eventLogEntry.TimesSent++; - _eventLogContext.EventLogs.Update(eventLogEntry); - return _eventLogContext.SaveChangesAsync(); + try + { + _eventLogContext.EventLogs.Update(eventLogEntry); + await _eventLogContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException ex) + { + throw new UserFriendlyException(ex.Message); + } + + CheckAndDetached(eventLogEntry); + } + + private void CheckAndDetached(IntegrationEventLog integrationEvent) + { + if (_eventLogContext.ChangeTracker.QueryTrackingBehavior != QueryTrackingBehavior.TrackAll) + { + _eventLogContext.Entry(integrationEvent).State = EntityState.Detached; + } } } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/Internal/QueryFilterProvider.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/Internal/QueryFilterProvider.cs index 4b32ab4ad..c21f782a4 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/Internal/QueryFilterProvider.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/Internal/QueryFilterProvider.cs @@ -1,6 +1,3 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using System.Linq.Expressions; - namespace MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Internal; internal abstract class QueryFilterProvider : IQueryFilterProvider diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.csproj b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.csproj index 2c25ed79e..c4d5d2b96 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.csproj +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.csproj @@ -7,10 +7,14 @@ - - - - + + + + + + + + diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.md b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.md index 86e87c0b1..4ab7a3c7d 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.md +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.md @@ -1,3 +1,5 @@ +[中](README.zh-CN.md) | EN + ## MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF > Provide support for sending IntegrationEvent diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.zh-cn.md b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.zh-CN.md similarity index 94% rename from src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.zh-cn.md rename to src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.zh-CN.md index 5dfeb3634..76784f8b5 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.zh-cn.md +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/README.zh-CN.md @@ -1,3 +1,5 @@ +中 | [EN](README.md) + ## MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF > 为发送IntegrationEvent提供支持 @@ -19,4 +21,4 @@ Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF } ``` -> 提示:CustomDbContext需要继承IntegrationEventLogContext \ No newline at end of file +> 提示:CustomDbContext需要继承IntegrationEventLogContext diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/_Imports.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/_Imports.cs index 18e5a61e2..260da3f76 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/_Imports.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/_Imports.cs @@ -5,11 +5,14 @@ global using MASA.Utils.Data.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.ChangeTracking; +global using Microsoft.EntityFrameworkCore.Metadata; global using Microsoft.EntityFrameworkCore.Metadata.Builders; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; global using System; global using System.Collections.Generic; global using System.Data.Common; global using System.Linq; +global using System.Linq.Expressions; global using System.Threading.Tasks; diff --git a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Commands/Command.cs b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Commands/Command.cs index 2253a8817..0ba4e22b0 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Commands/Command.cs +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Commands/Command.cs @@ -1,16 +1,15 @@ namespace MASA.Contrib.ReadWriteSpliting.CQRS.Commands; -public record Command : ICommand +public record Command(Guid Id, DateTime CreationTime) : ICommand { - public Guid Id { get; init; } + [JsonIgnore] + public Guid Id { get; } = Id; - public DateTime CreationTime { get; init; } + [JsonIgnore] + public DateTime CreationTime { get; } = CreationTime; - public Command() : this(Guid.NewGuid(), DateTime.UtcNow) { } + [JsonIgnore] + public IUnitOfWork? UnitOfWork { get; set; } - public Command(Guid id, DateTime creationTime) - { - this.Id = id; - this.CreationTime = creationTime; - } + public Command() : this(Guid.NewGuid(), DateTime.UtcNow) { } } diff --git a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/MASA.Contrib.ReadWriteSpliting.CQRS.csproj b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/MASA.Contrib.ReadWriteSpliting.CQRS.csproj index 70f79c30a..21b30680e 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/MASA.Contrib.ReadWriteSpliting.CQRS.csproj +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/MASA.Contrib.ReadWriteSpliting.CQRS.csproj @@ -7,10 +7,13 @@ - - - - + + - + + + + + + diff --git a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/Query.cs b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/Query.cs index d7e3c2455..44fac69fd 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/Query.cs +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/Query.cs @@ -1,19 +1,15 @@ namespace MASA.Contrib.ReadWriteSpliting.CQRS.Queries; -public abstract record Query : IQuery +public abstract record Query(Guid Id, DateTime CreationTime) : IQuery where TResult : notnull { - public Guid Id { get; init; } + [JsonIgnore] + public Guid Id { get; } = Id; - public DateTime CreationTime { get; init; } + [JsonIgnore] + public DateTime CreationTime { get; } = CreationTime; public abstract TResult Result { get; set; } public Query() : this(Guid.NewGuid(), DateTime.UtcNow) { } - - public Query(Guid id, DateTime creationTime) - { - this.Id = id; - this.CreationTime = creationTime; - } } diff --git a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/QueryHandler.cs b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/QueryHandler.cs index 5e44c2eba..44256ba62 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/QueryHandler.cs +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/QueryHandler.cs @@ -1,13 +1,8 @@ namespace MASA.Contrib.ReadWriteSpliting.CQRS.Queries; -public abstract class QueryHandler : IQueryHandler, ISagaEventHandler - where TCommand : IQuery +public abstract class QueryHandler : IQueryHandler + where TQuery : IQuery where TResult : notnull { - public abstract Task HandleAsync(TCommand @event); - - public virtual Task CancelAsync(TCommand @event) - { - return Task.CompletedTask; - } + public abstract Task HandleAsync(TQuery @event); } diff --git a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.md b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.md index 9ef98153e..a2cb52a95 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.md +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.md @@ -1,3 +1,5 @@ +[中](README.zh-CN.md) | EN + ## CQRS Example: @@ -18,9 +20,9 @@ Install-Package MASA.Contrib.ReadWriteSpliting.CQRS ```C# public class CatalogItemQuery : Query> { - public string Name { get; set; } = default!; + public string Name { get; set; } = default!; - public override List Result { get; set; } = default!; + public override List Result { get; set; } = default!; } ``` @@ -56,7 +58,7 @@ await eventBus.PublishAsync(new CatalogItemQuery() { Name = "Rolex" }); ```c# public class CreateCatalogItemCommand : Command { - public string Name { get; set; } = default!; + public string Name { get; set; } = default!; //todo } diff --git a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.zh-cn.md b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.zh-CN.md similarity index 89% rename from src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.zh-cn.md rename to src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.zh-CN.md index b58e20354..6b84aebe2 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.zh-cn.md +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/README.zh-CN.md @@ -1,3 +1,5 @@ +中 | [EN](README.md) + ## CQRS 用例: @@ -18,9 +20,9 @@ Install-Package MASA.Contrib.ReadWriteSpliting.CQRS ```C# public class CatalogItemQuery : Query> { - public string Name { get; set; } = default!; + public string Name { get; set; } = default!; - public override List Result { get; set; } = default!; + public override List Result { get; set; } = default!; } ``` @@ -56,7 +58,7 @@ await eventBus.PublishAsync(new CatalogItemQuery() { Name = "Rolex" }); ```c# public class CreateCatalogItemCommand : Command { - public string Name { get; set; } = default!; + public string Name { get; set; } = default!; //todo } @@ -83,4 +85,4 @@ public class CatalogCommandHandler : CommandHandler ```C# IEventBus eventBus;//通过DI得到IEventBus await eventBus.PublishAsync(new CreateCatalogItemCommand()); -``` \ No newline at end of file +``` diff --git a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/_Imports.cs b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/_Imports.cs index c934201ef..79632aa72 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/_Imports.cs +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/_Imports.cs @@ -1,3 +1,6 @@ +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.ReadWriteSpliting.CQRS.Commands; global using MASA.BuildingBlocks.ReadWriteSpliting.CQRS.Queries; +global using System.Text.Json.Serialization; + diff --git a/src/Service/MASA.Contrib.Service.MinimalAPIs/MASA.Contrib.Service.MinimalAPIs.csproj b/src/Service/MASA.Contrib.Service.MinimalAPIs/MASA.Contrib.Service.MinimalAPIs.csproj index b8dd6b327..0c88a1c69 100644 --- a/src/Service/MASA.Contrib.Service.MinimalAPIs/MASA.Contrib.Service.MinimalAPIs.csproj +++ b/src/Service/MASA.Contrib.Service.MinimalAPIs/MASA.Contrib.Service.MinimalAPIs.csproj @@ -6,9 +6,13 @@ - - - + + + + + + + diff --git a/src/Service/MASA.Contrib.Service.MinimalAPIs/README.md b/src/Service/MASA.Contrib.Service.MinimalAPIs/README.md index 0a280b03d..6560eea7f 100644 --- a/src/Service/MASA.Contrib.Service.MinimalAPIs/README.md +++ b/src/Service/MASA.Contrib.Service.MinimalAPIs/README.md @@ -1,3 +1,5 @@ +[中](README.zh-CN.md) | EN + ## MinimalAPI Original usage: @@ -20,7 +22,7 @@ Install-Package MASA.Contrib.Service.MinimalAPIs ```c# var builder = WebApplication.CreateBuilder(args); var app = builder.Services - .AddServices(builder); + .AddServices(builder); ``` 2. Customize Service and inherit ServiceBase @@ -28,7 +30,7 @@ var app = builder.Services ```c# public class IntegrationEventService : ServiceBase { - public IntegrationEventService(IServiceCollection services) : base(services) + public IntegrationEventService(IServiceCollection services) : base(services) { App.MapGet("/api/v1/payment/HelloWorld", HelloWorld); } diff --git a/src/Service/MASA.Contrib.Service.MinimalAPIs/README.zh-cn.md b/src/Service/MASA.Contrib.Service.MinimalAPIs/README.zh-CN.md similarity index 85% rename from src/Service/MASA.Contrib.Service.MinimalAPIs/README.zh-cn.md rename to src/Service/MASA.Contrib.Service.MinimalAPIs/README.zh-CN.md index 9cb242df1..ef8c8bca7 100644 --- a/src/Service/MASA.Contrib.Service.MinimalAPIs/README.zh-cn.md +++ b/src/Service/MASA.Contrib.Service.MinimalAPIs/README.zh-CN.md @@ -1,3 +1,5 @@ +中 | [EN](README.md) + ## MinimalAPI 原始用法: @@ -20,7 +22,7 @@ Install-Package MASA.Contrib.Service.MinimalAPIs ```c# var builder = WebApplication.CreateBuilder(args); var app = builder.Services - .AddServices(builder); + .AddServices(builder); ``` 2. 自定义Service并继承ServiceBase,如: @@ -28,7 +30,7 @@ var app = builder.Services ```c# public class IntegrationEventService : ServiceBase { - public IntegrationEventService(IServiceCollection services) : base(services) + public IntegrationEventService(IServiceCollection services) : base(services) { App.MapGet("/api/v1/payment/HelloWorld", HelloWorld); } diff --git a/src/Service/MASA.Contrib.Service.MinimalAPIs/ServiceBase.cs b/src/Service/MASA.Contrib.Service.MinimalAPIs/ServiceBase.cs index 910cee7a0..2ef353c06 100644 --- a/src/Service/MASA.Contrib.Service.MinimalAPIs/ServiceBase.cs +++ b/src/Service/MASA.Contrib.Service.MinimalAPIs/ServiceBase.cs @@ -1,10 +1,13 @@ namespace MASA.Contrib.Service.MinimalAPIs; -public class ServiceBase : IService + +public abstract class ServiceBase : IService { private ServiceProvider _serviceProvider = default!; public WebApplication App => _serviceProvider.GetRequiredService(); + public string BaseUri { get; } + public IServiceCollection Services { get; protected set; } public ServiceBase(IServiceCollection services) @@ -13,9 +16,104 @@ public ServiceBase(IServiceCollection services) _serviceProvider = services.BuildServiceProvider(); } + public ServiceBase(IServiceCollection services, string baseUri) + { + BaseUri = baseUri; + Services = services; + _serviceProvider = services.BuildServiceProvider(); + } + public TService? GetService() => _serviceProvider.GetService(); public TService GetRequiredService() where TService : notnull => Services.BuildServiceProvider().GetRequiredService(); -} + + #region Map GET,POST,PUT,DELETE + + /// + /// Adds a to the that matches HTTP GET requests + /// for the specified pattern, a combination of and or name. + /// + /// The delegate executed when the endpoint is matched. It's name is a part of pattern if is null. + /// The custom uri. It is a part of pattern if it is not null. + /// Determines whether to remove the string 'Async' at the end. + /// A that can be used to further customize the endpoint. + protected RouteHandlerBuilder MapGet(Delegate handler, string? customUri = null, bool trimEndAsync = true) + { + customUri ??= FormatAction(handler.Method.Name, trimEndAsync); + + var pattern = CombineUris(BaseUri, customUri); + + return App.MapGet(pattern, handler); + } + + /// + /// Adds a to the that matches HTTP POST requests + /// for the specified pattern, a combination of and or name. + /// + /// The delegate executed when the endpoint is matched. It's name is a part of pattern if is null. + /// The custom uri. It is a part of pattern if it is not null. + /// Determines whether to remove the string 'Async' at the end. + /// A that can be used to further customize the endpoint. + protected RouteHandlerBuilder MapPost(Delegate handler, string? customUri = null, bool trimEndAsync = true) + { + customUri ??= FormatAction(handler.Method.Name, trimEndAsync); + + var pattern = CombineUris(BaseUri, customUri); + + return App.MapPost(pattern, handler); + } + + /// + /// Adds a to the that matches HTTP PUT requests + /// for the specified pattern, a combination of and or name. + /// + /// The delegate executed when the endpoint is matched. It's name is a part of pattern if is null. + /// The custom uri. It is a part of pattern if it is not null. + /// Determines whether to remove the string 'Async' at the end. + /// A that can be used to further customize the endpoint. + protected RouteHandlerBuilder MapPut(Delegate handler, string? customUri = null, bool trimEndAsync = true) + { + customUri ??= FormatAction(handler.Method.Name, trimEndAsync); + + var pattern = CombineUris(BaseUri, customUri); + + return App.MapPut(pattern, handler); + } + + /// + /// Adds a to the that matches HTTP DELETE requests + /// for the specified pattern, a combination of and or name. + /// + /// The delegate executed when the endpoint is matched. It's name is a part of pattern if is null. + /// The custom uri. It is a part of pattern if it is not null. + /// Determines whether to remove the string 'Async' at the end. + /// A that can be used to further customize the endpoint. + protected RouteHandlerBuilder MapDelete(Delegate handler, string? customUri = null, bool trimEndAsync = true) + { + customUri ??= FormatAction(handler.Method.Name, trimEndAsync); + + var pattern = CombineUris(BaseUri, customUri); + + return App.MapDelete(pattern, handler); + } + + private static string FormatAction(string action, bool trimEndAsync) + { + if (trimEndAsync && action.EndsWith("Async")) + { + var i = action.LastIndexOf("Async", StringComparison.Ordinal); + action = action[..i]; + } + + return action; + } + + private static string CombineUris(params string[] uris) + { + return string.Join("/", uris.Select(u => u.Trim('/'))); + } + + #endregion +} \ No newline at end of file diff --git a/src/Service/MASA.Contrib.Service.MinimalAPIs/ServiceCollectionExtensions.cs b/src/Service/MASA.Contrib.Service.MinimalAPIs/ServiceCollectionExtensions.cs index b6c816c19..98a21e7a1 100644 --- a/src/Service/MASA.Contrib.Service.MinimalAPIs/ServiceCollectionExtensions.cs +++ b/src/Service/MASA.Contrib.Service.MinimalAPIs/ServiceCollectionExtensions.cs @@ -1,9 +1,16 @@ -using System.Linq; - namespace MASA.Contrib.Service.MinimalAPIs; public static class ServiceCollectionExtensions { + /// + /// Add all classes that inherit from ServiceBase to Microsoft.Extensions.DependencyInjection.IServiceCollection + /// Notice: this method must be last call. + /// + /// The Microsoft.AspNetCore.Builder.WebApplicationBuilder. + /// + public static WebApplication AddServices(this WebApplicationBuilder builder) + => builder.Services.AddServices(builder); + /// /// Add all classes that inherit from ServiceBase to Microsoft.Extensions.DependencyInjection.IServiceCollection /// Notice: this method must be last call. @@ -21,7 +28,7 @@ public static WebApplication AddServices(this IServiceCollection services, WebAp services.AddSingleton(new Lazy(() => builder.Build(), LazyThreadSafetyMode.ExecutionAndPublication)) .AddTransient(serviceProvider => serviceProvider.GetRequiredService>().Value); - services.AddServices(true); + services.AddServices(true, AppDomain.CurrentDomain.GetAssemblies()); } var serviceProvider = services.BuildServiceProvider(); diff --git a/src/Service/MASA.Contrib.Service.MinimalAPIs/_Imports.cs b/src/Service/MASA.Contrib.Service.MinimalAPIs/_Imports.cs index e4101ab7d..a6b33566f 100644 --- a/src/Service/MASA.Contrib.Service.MinimalAPIs/_Imports.cs +++ b/src/Service/MASA.Contrib.Service.MinimalAPIs/_Imports.cs @@ -1,6 +1,8 @@ global using MASA.BuildingBlocks.Service.MinimalAPIs; global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Routing; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.DependencyInjection.Extensions; global using System; +global using System.Linq; global using System.Threading; diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccClientTest.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccClientTest.cs new file mode 100644 index 000000000..a8bc038b9 --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccClientTest.cs @@ -0,0 +1,388 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Tests; + +[TestClass] +public class DccClientTest +{ + private Mock _client; + private IServiceCollection _services; + private IServiceProvider _serviceProvider => _services.BuildServiceProvider(); + private JsonSerializerOptions _jsonSerializerOptions; + private DccSectionOptions _dccSectionOptions; + private CustomTrigger _trigger; + + [TestInitialize] + public void Initialize() + { + _client = new Mock(); + _services = new ServiceCollection(); + _jsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }; + _dccSectionOptions = new DccSectionOptions() + { + Environment = "Test", + Cluster = "Default", + AppId = "DccTest", + ConfigObjects = new List() + { + "Test1" + }, + Secret = "" + }; + _trigger = new CustomTrigger(_jsonSerializerOptions); + } + + [DataTestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task TestGetRawAsync(string environment, string cluster, string appId, string configObject) + { + Action valueChanged = delegate (string? val) { }; + _client.Setup(client => client.GetAsync(It.IsAny(), valueChanged).Result).Returns(() => null).Verifiable(); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + await Assert.ThrowsExceptionAsync(async () + => await client.GetRawAsync(environment, cluster, appId, configObject, valueChanged), "configObject invalid" + ); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => "test").Verifiable(); + client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + await Assert.ThrowsExceptionAsync(async () + => await client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()) + ); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => "{}").Verifiable(); + await Assert.ThrowsExceptionAsync(async () + => await client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), "configObject invalid" + ); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new + { + ConfigFormat = "1", + Content = "" + }.Serialize(_jsonSerializerOptions)).Verifiable(); + await Assert.ThrowsExceptionAsync(async () + => await client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), "configObject invalid" + ); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease + { + ConfigFormat = (ConfigFormats)5, + Content = "" + }.Serialize(_jsonSerializerOptions)).Verifiable(); + await Assert.ThrowsExceptionAsync( + async () => await client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), + "Unsupported configuration type"); + } + + [DataTestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task TestGetRawAsyncByJson(string environment, string cluster, string appId, string configObject) + { + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + + var brand = new Brands("Apple"); + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Json, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Verifiable(); + var ret = await client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsTrue(ret.Raw == brand.Serialize(_jsonSerializerOptions)); + Assert.IsTrue(ret.ConfigurationType == ConfigurationTypes.Json); + } + + [DataTestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task TestGetRawAsyncByText(string environment, string cluster, string appId, string configObject) + { + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Text, + Content = "test" + }.Serialize(_jsonSerializerOptions)).Verifiable(); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + var ret = await client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsTrue(ret.Raw == "test"); + Assert.IsTrue(ret.ConfigurationType == ConfigurationTypes.Text); + } + + [DataTestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task TestGetRawAsyncByProperty(string environment, string cluster, string appId, string configObject) + { + List properties = new List() + { + new() + { + Key = "Brand", + Value = "Microsoft" + } + }; + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Text, + Content = properties.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Verifiable(); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + var ret = await client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsTrue(ret.Raw == properties.Serialize(_jsonSerializerOptions)); + Assert.IsTrue(ret.ConfigurationType == ConfigurationTypes.Text); + } + + [TestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task GetAsyncByJson(string environment, string cluster, string appId, string configObject) + { + var brand = new Brands("Microsoft"); + var newBrand = new Brands("Microsoft2"); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Json, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Callback((string str, Action action) => + { + _trigger.Formats = ConfigFormats.Json; + _trigger.Content = newBrand.Serialize(_jsonSerializerOptions); + _trigger.Action = action; + }); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + var ret = await client.GetAsync(environment, cluster, appId, configObject, (Brands br) => + { + Assert.IsTrue(br.Id == newBrand.Id); + Assert.IsTrue(br.Name == newBrand.Name); + }); + Assert.IsNotNull(ret); + + Assert.IsTrue(ret.Serialize(_jsonSerializerOptions).Equals(brand.Serialize(_jsonSerializerOptions))); + _trigger.Execute(); + + ret = await client.GetAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsNotNull(ret); + + Assert.IsTrue(ret.Id == newBrand.Id && ret.Name == newBrand.Name); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Json, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Callback((string str, Action action) => + { + _trigger.Formats = ConfigFormats.Json; + newBrand.Name = "Masa"; + _trigger.Content = newBrand.Serialize(_jsonSerializerOptions); + _trigger.Action = action; + }); + client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + ret = await client.GetAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsNotNull(ret); + Assert.IsTrue(ret.Id == brand.Id && ret.Name == brand.Name); + _trigger.Execute(); + ret = await client.GetAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsTrue(ret.Id == newBrand.Id && ret.Name == "Masa"); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Json, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)); + client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + ret = await client.GetAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsNotNull(ret); + Assert.IsTrue(ret.Id == brand.Id && ret.Name == brand.Name); + + Initialize(); + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Json, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Verifiable(); + client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + await Assert.ThrowsExceptionAsync(async () => + { + await client.GetAsync(environment, cluster, appId, configObject, It.IsAny>()); + }); + } + + [TestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task GetAsyncByText(string environment, string cluster, string appId, string configObject) + { + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Text, + Content = "test" + }.Serialize(_jsonSerializerOptions)).Verifiable(); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + await Assert.ThrowsExceptionAsync(async () => + { + await client.GetAsync(environment, cluster, appId, configObject, It.IsAny>()); + }); + + Initialize(); + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Text, + Content = "1" + }.Serialize(_jsonSerializerOptions)).Verifiable(); + client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + Assert.IsTrue(await client.GetAsync(environment, cluster, appId, configObject, It.IsAny>()) == 1); + } + + [TestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task GetAsyncByProperty(string environment, string cluster, string appId, string configObject) + { + var brand = new List() + { + new() + { + Key = "Id", + Value = Guid.NewGuid().ToString(), + }, + new() + { + Key = "Name", + Value = "Microsoft" + } + }; + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Properties, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Verifiable(); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + var ret = await client.GetAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsNotNull(ret); + + Assert.IsTrue(ret.Id.ToString() == brand.Where(b => b.Key == "Id").Select(t => t.Value).FirstOrDefault() && + ret.Name == brand.Where(b => b.Key == "Name").Select(t => t.Value).FirstOrDefault()); + } + + [TestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task GetDynamicAsyncByJson(string environment, string cluster, string appId, string configObject) + { + var brand = new Brands("Microsoft"); + var newBrand = new Brands("Microsoft2"); + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Json, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Callback((string str, Action action) => + { + _trigger.Formats = ConfigFormats.Json; + _trigger.Content = newBrand.Serialize(_jsonSerializerOptions); + _trigger.Action = action; + }).Verifiable(); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + var ret = await client.GetDynamicAsync(environment, cluster, appId, configObject, (dynamic obj) => + { + Assert.IsTrue((obj.Id + "") == newBrand.Id.ToString()); + + Assert.IsTrue(obj.Name == newBrand.Name); + }); + Assert.IsNotNull(ret); + + Assert.IsTrue(ret.Id == brand.Id.ToString()); + + Assert.IsTrue(ret.Name == brand.Name); + + _trigger.Execute(); + + ret = await client.GetDynamicAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsNotNull(ret); + Assert.IsTrue(ret.Id == newBrand.Id.ToString() && ret.Name == newBrand.Name); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Json, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Callback((string str, Action action) => + { + _trigger.Formats = ConfigFormats.Json; + _trigger.Content = newBrand.Serialize(_jsonSerializerOptions); + _trigger.Action = action; + }).Verifiable(); + client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + ret = await client.GetDynamicAsync(environment, cluster, appId, configObject, It.IsAny()); + Assert.IsNotNull(ret); + Assert.IsTrue(ret.Id == brand.Id.ToString()); + Assert.IsTrue(ret.Name == brand.Name); + _trigger.Execute(); + + ret = await client.GetDynamicAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsNotNull(ret); + Assert.IsTrue(ret.Id == newBrand.Id.ToString() && ret.Name == newBrand.Name); + + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Json, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)); + client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + ret = await client.GetDynamicAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsNotNull(ret); + Assert.IsTrue(ret.Id == brand.Id.ToString() && ret.Name == brand.Name); + } + + [TestMethod] + [DataRow("DccOptions.ManageServiceAddress", "http://localhost:6379")] + [DataRow("DccOptions.RedisOptions.DefaultDatabase", "0")] + [DataRow("DccOptions.RedisOptions.Password", "")] + public async Task GetDynamicAsync(string key, string value) + { + var configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build(); + _services.AddSingleton(configuration); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + var res = (await client.GetDynamicAsync(key)); + Assert.IsTrue(res + "" == value); + } + + [TestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task GetDynamicAsyncByText(string environment, string cluster, string appId, string configObject) + { + string result = "Test"; + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Text, + Content = result + }.Serialize(_jsonSerializerOptions)).Verifiable(); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + await Assert.ThrowsExceptionAsync(async () => + { + await client.GetDynamicAsync(environment, cluster, appId, configObject, It.IsAny>()); + }); + } + + [TestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task GetDynamicAsyncByProperty(string environment, string cluster, string appId, string configObject) + { + var brand = new List() + { + new() + { + Key = "Id", + Value = Guid.NewGuid().ToString(), + }, + new() + { + Key = "Name", + Value = "Microsoft" + } + }; + _client.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result).Returns(() => new PublishRelease() + { + ConfigFormat = ConfigFormats.Properties, + Content = brand.Serialize(_jsonSerializerOptions) + }.Serialize(_jsonSerializerOptions)).Verifiable(); + var client = new ConfigurationApiClient(_serviceProvider, _client.Object, _jsonSerializerOptions, _dccSectionOptions, null); + var ret = await client.GetDynamicAsync(environment, cluster, appId, configObject, It.IsAny>()); + Assert.IsNotNull(ret); + + Assert.IsTrue(ret.Id == brand.Where(b => b.Key == "Id").Select(b => b.Value).FirstOrDefault()); + Assert.IsTrue(ret.Name == brand.Where(b => b.Key == "Name").Select(b => b.Value).FirstOrDefault()); + } +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccManageTest.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccManageTest.cs new file mode 100644 index 000000000..baecfe2bf --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccManageTest.cs @@ -0,0 +1,162 @@ +using MASA.Contrib.BasicAbility.Dcc.Internal; +using MASA.Utils.Caller.Core; +using System.Net; + +namespace MASA.Contrib.BasicAbility.Dcc.Tests; + +[TestClass] +public class DccManageTest +{ + private DccSectionOptions _dccSectionOptions; + private JsonSerializerOptions _jsonSerializerOptions; + private Mock _callerProvider; + + [TestInitialize] + public void Initialize() + { + _dccSectionOptions = new DccSectionOptions() + { + Environment = "Test", + Cluster = "Default", + AppId = "DccTest", + ConfigObjects = new List() + { + "Test1" + }, + Secret = "Secret" + }; + _jsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }; + _callerProvider = new Mock(); + } + + [DataTestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task TestUpdateAsync(string environment, string cluster, string appId, string configObject) + { + var brand = new Brands("Microsoft"); + _callerProvider.Setup(factory => factory.PutAsync(It.IsAny(), It.IsAny(), default).Result).Returns(() => new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(brand.Serialize(_jsonSerializerOptions)) + }).Verifiable(); + + var manage = new ConfigurationApiManage(_callerProvider.Object, _dccSectionOptions, null); + await manage.UpdateAsync(environment, cluster, appId, configObject, brand); + } + + [DataTestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task TestUpdateAsyncAndError(string environment, string cluster, string appId, string configObject) + { + var brand = new Brands("Microsoft"); + + _callerProvider.Setup(factory => factory.PutAsync(It.IsAny(), It.IsAny(), default).Result).Returns(() => new HttpResponseMessage() + { + StatusCode = HttpStatusCode.ExpectationFailed, + Content = new StringContent("error") + }).Verifiable(); + + var manage = new ConfigurationApiManage(_callerProvider.Object, _dccSectionOptions, null); + await Assert.ThrowsExceptionAsync(async () => await manage.UpdateAsync(environment, cluster, appId, configObject, brand)); + } + + [DataTestMethod] + [DataRow("Test", "Default", "DccTest", "Brand")] + public async Task TestUpdateAsyncAndCustomError(string environment, string cluster, string appId, string configObject) + { + var brand = new Brands("Microsoft"); + _callerProvider.Setup(factory => factory.PutAsync(It.IsAny(), It.IsAny(), default).Result).Returns(() => new HttpResponseMessage() + { + StatusCode = (HttpStatusCode)299, + Content = new StringContent("custom error") + }).Verifiable(); + + var manage = new ConfigurationApiManage(_callerProvider.Object, _dccSectionOptions, null); + await Assert.ThrowsExceptionAsync(async () => await manage.UpdateAsync(environment, cluster, appId, configObject, brand)); + } + + [DataTestMethod] + [DataRow("DccTest", "Secret")] + [DataRow("DccTest2", "Secret2")] + [DataRow("DccTest3", "")] + public void TestGetSecret(string appId, string secret) + { + var api = new CustomConfigurationAPI(_dccSectionOptions, new List() + { + new() + { + Environment = "Test2", + Cluster = "Default2", + AppId = "DccTest2", + ConfigObjects = new List() + { + "Test12" + }, + Secret = "Secret2" + } + }); + if (string.IsNullOrEmpty(secret)) + Assert.ThrowsException(() => api.GetSecret(appId)); + else + Assert.IsTrue(api.GetSecret(appId) == secret); + } + + [DataTestMethod] + [DataRow("Test2", "Test2")] + [DataRow("", "Test")] + public void TestGetEnvironment(string environment, string outEnvironment) + { + var api = new CustomConfigurationAPI(_dccSectionOptions, null); + Assert.IsTrue(api.GetEnvironment(environment) == outEnvironment); + } + + [DataTestMethod] + [DataRow("CustomCluster", "CustomCluster")] + [DataRow("", "Default")] + public void GetCluster(string cluster, string outCluster) + { + var api = new CustomConfigurationAPI(_dccSectionOptions, null); + Assert.IsTrue(api.GetCluster(cluster) == outCluster); + } + + [DataTestMethod] + [DataRow("CustomAppid", "CustomAppid")] + [DataRow("", "DccTest")] + public void GetAppid(string appId, string outAppid) + { + var api = new CustomConfigurationAPI(_dccSectionOptions, null); + Assert.IsTrue(api.GetAppId(appId) == outAppid); + } + + [DataTestMethod] + [DataRow("configObject", "configObject")] + [DataRow("", "")] + public void GetConfigObject(string configObject, string outConfigObject) + { + var api = new CustomConfigurationAPI(_dccSectionOptions, null); + if (string.IsNullOrEmpty(configObject)) + Assert.ThrowsException(() => api.GetConfigObject(configObject)); + else + Assert.IsTrue(api.GetConfigObject(configObject) == outConfigObject); + } +} + +public class CustomConfigurationAPI : ConfigurationAPIBase +{ + public CustomConfigurationAPI(DccSectionOptions defaultSectionOption, List? expandSectionOptions) : base(defaultSectionOption, expandSectionOptions) + { + } + + public new string GetSecret(string appId) => base.GetSecret(appId); + + public new string GetEnvironment(string environment) => base.GetEnvironment(environment); + + public new string GetCluster(string cluster) => base.GetCluster(cluster); + + public new string GetAppId(string appId) => base.GetAppId(appId); + + public new string GetConfigObject(string configObject) => base.GetConfigObject(configObject); +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccTest.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccTest.cs new file mode 100644 index 000000000..8d5394a69 --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccTest.cs @@ -0,0 +1,799 @@ +using MASA.Utils.Caching.Core.Interfaces; +using MASA.Utils.Caching.Core.Models; +using MASA.Utils.Caching.DistributedMemory.Models; +using MASA.Utils.Caller.Core; +using MASA.Utils.Caller.HttpClient; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace MASA.Contrib.BasicAbility.Dcc.Tests; + +[TestClass] +public class DccTest +{ + private string DEFAULT_CLIENT_NAME = "masa.plugins.caching.dcc"; + private Mock _masaConfigurationBuilder; + private JsonSerializerOptions _jsonSerializerOptions; + private IServiceCollection _services; + + private Mock _memoryCacheClientFactory; + private Mock _memoryCache; + private Mock _distributedCacheClient; + private const string DefaultEnvironmentName = "ASPNETCORE_ENVIRONMENT"; + private const string DEFAULT_SUBSCRIBE_KEY_PREFIX = "masa.dcc:"; + + [TestInitialize] + public void Initialize() + { + _masaConfigurationBuilder = new Mock(); + _memoryCacheClientFactory = new Mock(); + _memoryCache = new Mock(); + _distributedCacheClient = new Mock(); + _services = new ServiceCollection(); + _jsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }; + } + + [TestMethod] + public void TestErrorDccSection() + { + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary()).Verifiable(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(new ServiceCollection())); + } + + [TestMethod] + public void TestTryAddConfigurationApiClient() + { + _memoryCacheClientFactory.Setup(factory => factory.CreateClient(DEFAULT_CLIENT_NAME)).Returns(() => null!).Verifiable(); + _services.AddSingleton(serviceProvider => _memoryCacheClientFactory.Object); + MasaConfigurationExtensions.TryAddConfigurationApiClient(_services, new DccSectionOptions(), new List(), null!); + Assert.IsTrue(_services.Count(service => service.ServiceType == typeof(IConfigurationApiClient) && service.Lifetime == ServiceLifetime.Singleton) == 1); + Assert.ThrowsException(() => + { + var clienties = _services.BuildServiceProvider().GetServices(); + }); + + _services = new ServiceCollection(); + _memoryCacheClientFactory + .Setup(factory => factory.CreateClient(DEFAULT_CLIENT_NAME)) + .Returns(() => new MemoryCacheClient(_memoryCache.Object, _distributedCacheClient.Object, SubscribeKeyTypes.ValueTypeFullNameAndKey)) + .Verifiable(); + _services.AddSingleton(serviceProvider => _memoryCacheClientFactory.Object); + MasaConfigurationExtensions.TryAddConfigurationApiClient(_services, new DccSectionOptions(), new List(), new JsonSerializerOptions() + { + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); + + var clienties = _services.BuildServiceProvider().GetServices(); + Assert.IsTrue(clienties.Count() == 1); + + _services = new ServiceCollection(); + _memoryCacheClientFactory + .Setup(factory => factory.CreateClient(DEFAULT_CLIENT_NAME)) + .Returns(() => new MemoryCacheClient(_memoryCache.Object, _distributedCacheClient.Object, Utils.Caching.Core.Models.SubscribeKeyTypes.ValueTypeFullNameAndKey)) + .Verifiable(); + _services.AddSingleton(serviceProvider => _memoryCacheClientFactory.Object); + MasaConfigurationExtensions.TryAddConfigurationApiClient(_services, new DccSectionOptions(), new List(), _jsonSerializerOptions); + MasaConfigurationExtensions.TryAddConfigurationApiClient(_services, new DccSectionOptions(), new List(), _jsonSerializerOptions); + clienties = _services.BuildServiceProvider().GetServices(); + Assert.IsTrue(clienties.Count() == 1); + } + + [TestMethod] + public void TestTryAddConfigurationApiManage() + { + Mock httpClientFactory = new(); + _services.AddSingleton(httpClientFactory.Object); + _services.AddCaller(options => options.UseHttpClient()); + + MasaConfigurationExtensions.TryAddConfigurationApiManage(_services, new DccSectionOptions(), new List()); + MasaConfigurationExtensions.TryAddConfigurationApiManage(_services, new DccSectionOptions(), new List()); + Assert.IsTrue(_services.Count(service => service.ServiceType == typeof(IConfigurationApiManage) && service.Lifetime == ServiceLifetime.Singleton) == 1); + var serviceProvider = _services.BuildServiceProvider(); + Assert.IsTrue(serviceProvider.GetServices().Count() == 1); + } + + [TestMethod] + public void TestUseDCCAndErrorSection() + { + _services.AddCaller(options => options.UseHttpClient()); + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary()).Verifiable(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, "", null, null), "configureOptions"); + } + + [TestMethod] + public void TestUseDCCAndNullDccConfigurationOption() + { + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => null!, option => + { + option.AppId = "Test"; + option.Environment = "Test"; + option.ConfigObjects = new List() { "Te" }; + }, null), "configureOptions"); + } + + [TestMethod] + public void TestCustomCaller() + { + var response = JsonSerializer.Serialize(new PublishRelease() + { + Content = string.Empty, + ConfigFormat = ConfigFormats.Text + }); + Mock memoryCacheClient = new(); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + + var configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + _masaConfigurationBuilder.Object.UseDcc(_services, () => new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }, option => + { + option.AppId = "Test"; + option.Environment = "Test"; + option.ConfigObjects = new List() + { + "Settings" + }; + }, null, jsonSerializerOption => + { + jsonSerializerOption.Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + }, option => + { + option.UseHttpClient(builder => + { + builder.Name = "CustomHttpClient"; + builder.Configure = opt => opt.BaseAddress = new Uri("https://github.com"); + }); + }); + var callerProvider = _services.BuildServiceProvider().GetRequiredService().CreateClient("CustomHttpClient"); + Assert.IsNotNull(callerProvider); + } + + [TestMethod] + public void TestUseDCCAndEmptyDccServiceAddress() + { + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "", + }; + }, null!, null), "DccServiceAddress"); + } + + [TestMethod] + public void TestUseDCCAndErrorDccService() + { + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = null! + } + }; + }, null!, null), "Servers"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions + { + Servers = new List() + } + }; + }, null!, null), "Servers"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions + { + Servers = new List() + { + new() + { + Host="", + Port=8080 + } + } + } + }; + }, null!, null), "Servers"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host="localhost", + Port=-1 + } + } + } + }; + }, null!, null), "Servers"); + } + + [TestMethod] + public void TestUseDCCAndErrorDefaultSectionOption() + { + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, null!, null), "defaultSectionOptions"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = ""; + }, null), "AppId cannot be empty"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = null!; + }, null), "ConfigObjects cannot be empty"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List(); + }, null), "ConfigObjects cannot be empty"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, null), "Error getting environment information, please make sure the value of ASPNETCORE_ENVIRONMENT has been configured"); + } + + [TestMethod] + public void TestUseDCCAndErrorExpansionSectionOptions() + { + System.Environment.SetEnvironmentVariable(DefaultEnvironmentName, "Test"); + + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, option => + { + option.ExpandSections = new List() + { + new() + { + AppId = "Test2", + } + }; + }), "ConfigObjects in the extension section cannot be empty"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, option => + { + option.ExpandSections = new List() + { + new() + { + AppId = "Test2", + ConfigObjects=new List() + } + }; + }), "ConfigObjects in the extension section cannot be empty"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, option => + { + option.ExpandSections = new List() + { + new() + { + AppId = "Test", + ConfigObjects=new List() + { + "Settings" + } + } + }; + }), "The current section already exists, no need to mount repeatedly"); + + _services = new ServiceCollection(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, option => + { + option.ExpandSections = new List() + { + new() + { + AppId = "Test2", + ConfigObjects=new List() + { + "Settings" + } + }, + new() + { + AppId = "Test2", + ConfigObjects=new List() + { + "Settings" + } + } + }; + }), "The current section already exists, no need to mount repeatedly"); + } + + [DataTestMethod] + [DataRow("Development", "Default", "WebApplication1", "Brand")] + public void TestUseDCCAndSuccess(string environment, string cluster, string appId, string configObject) + { + System.Environment.SetEnvironmentVariable(DefaultEnvironmentName, "Test"); + var brand = new Brands("Microsoft"); + var response = JsonSerializer.Serialize(new PublishRelease() + { + Content = System.Text.Json.JsonSerializer.Serialize(brand), + ConfigFormat = ConfigFormats.Json + }); + Mock memoryCacheClient = new(); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + + var configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + + _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, null); + var optionFactory = _services.BuildServiceProvider().GetRequiredService>(); + var option = optionFactory.Create(DEFAULT_CLIENT_NAME); + + Assert.IsTrue(option.SubscribeKeyType == SubscribeKeyTypes.SpecificPrefix); + + Assert.IsTrue(option.SubscribeKeyPrefix == DEFAULT_SUBSCRIBE_KEY_PREFIX); + } + + [DataTestMethod] + [DataRow("Development", "Default", "WebApplication1", "Brand")] + public void TestUseDccAndSingleSection(string environment, string cluster, string appId, string configObject) + { + CustomTrigger trigger = new CustomTrigger(_jsonSerializerOptions); + var brand = new Brands("Microsoft"); + var newBrand = new Brands("Masa"); + + var response = JsonSerializer.Serialize(new PublishRelease() + { + Content = brand.Serialize(_jsonSerializerOptions), + ConfigFormat = ConfigFormats.Text + }); + Mock memoryCacheClient = new(); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + var configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + + var chainedConfiguration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true); + + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary() + { + { "Appsettings",chainedConfiguration.Build() } + }).Verifiable(); + + _masaConfigurationBuilder.Object.UseDcc(_services); + Assert.IsTrue( + configurationApiClient + .GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()) + .GetAwaiter() + .GetResult().Raw == brand.Serialize(_jsonSerializerOptions)); + trigger.Execute(); + + Initialize(); + + Dictionary masaDic = new Dictionary() + { + { "Id", Guid.NewGuid().ToString() }, + { "Name", "Masa" } + }; + response = JsonSerializer.Serialize(new PublishRelease() + { + Content = masaDic.Serialize(_jsonSerializerOptions), + ConfigFormat = ConfigFormats.Json + }); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + + chainedConfiguration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true); + + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary() + { + { "Appsettings",chainedConfiguration.Build() } + }).Verifiable(); + + _masaConfigurationBuilder.Object.UseDcc(_services); + Assert.IsTrue(configurationApiClient.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).Result.Raw == masaDic.Serialize(_jsonSerializerOptions)); + + Initialize(); + + response = JsonSerializer.Serialize(new PublishRelease() + { + Content = "Test", + ConfigFormat = ConfigFormats.Text + }); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + + chainedConfiguration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true); + + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary() + { + { "Appsettings",chainedConfiguration.Build() } + }).Verifiable(); + + _masaConfigurationBuilder.Object.UseDcc(_services); + Assert.IsTrue(configurationApiClient.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).GetAwaiter().GetResult().Raw == "Test"); + + Initialize(); + + response = JsonSerializer.Serialize(new PublishRelease() + { + Content = null, + ConfigFormat = ConfigFormats.Text + }); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + + chainedConfiguration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true); + + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary() + { + { "Appsettings",chainedConfiguration.Build() } + }).Verifiable(); + + _masaConfigurationBuilder.Object.UseDcc(_services); + Assert.IsTrue(configurationApiClient.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).GetAwaiter().GetResult().Raw == null); + + Initialize(); + + response = JsonSerializer.Serialize(new PublishRelease() + { + Content = "Test", + ConfigFormat = (ConfigFormats)4 + }); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + + chainedConfiguration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true); + + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary() + { + { "Appsettings",chainedConfiguration.Build() } + }).Verifiable(); + Assert.ThrowsException(() => _masaConfigurationBuilder.Object.UseDcc(_services)); + } + + [TestMethod] + public void TestUseDccAndExpandSections() + { + var brand = new Brands("Microsoft"); + var response = JsonSerializer.Serialize(new PublishRelease() + { + Content = JsonSerializer.Serialize(brand), + ConfigFormat = ConfigFormats.Json + }); + Mock memoryCacheClient = new(); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + + var configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + + var chainedConfiguration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("expandSections.json", true, true); + + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary() + { + { "Appsettings",chainedConfiguration.Build() } + }).Verifiable(); + + _masaConfigurationBuilder.Object.UseDcc(_services); + + var result = configurationApiClient.GetRawAsync("Test", "Default", "DccTest", "Test1", It.IsAny>()) + .ConfigureAwait(false).GetAwaiter().GetResult(); + Assert.IsTrue(result.Raw == JsonSerializer.Serialize(brand)); + } + + [DataTestMethod] + [DataRow("Development", "Default", "WebApplication1", "Brand")] + public void TestUseMultiDcc(string environment, string cluster, string appId, string configObject) + { + var brand = new Brands("Microsoft"); + var response = JsonSerializer.Serialize(new PublishRelease() + { + Content = JsonSerializer.Serialize(brand), + ConfigFormat = ConfigFormats.Json + }); + Mock memoryCacheClient = new(); + memoryCacheClient.Setup(client => client.GetAsync(It.IsAny(), It.IsAny>()).Result) + .Returns(() => response); + + var configurationApiClient = new ConfigurationApiClient(_services.BuildServiceProvider(), + memoryCacheClient.Object, _jsonSerializerOptions, new Mock().Object, new List()); + _services.AddSingleton(configurationApiClient); + + var chainedConfiguration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true); + + _masaConfigurationBuilder.Setup(builder => builder.GetSectionRelations()).Returns(new Dictionary() + { + { "Appsettings",chainedConfiguration.Build() } + }).Verifiable(); + + _masaConfigurationBuilder.Object.UseDcc(_services).UseDcc(_services); + var result = configurationApiClient.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()) + .ConfigureAwait(false).GetAwaiter().GetResult(); + Assert.IsTrue(result.Raw == JsonSerializer.Serialize(brand)); + + var httpClient = _services.BuildServiceProvider().GetRequiredService().CreateClient(DEFAULT_CLIENT_NAME); + Assert.IsTrue(httpClient.BaseAddress!.ToString() == "http://localhost:6379/"); + } + +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Common/SerializeCommon.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Common/SerializeCommon.cs new file mode 100644 index 000000000..7f5827b59 --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Common/SerializeCommon.cs @@ -0,0 +1,7 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Tests.Internal.Common; + +internal static class SerializeCommon +{ + public static string Serialize(this object obj, JsonSerializerOptions? jsonSerializerOptions) + => JsonSerializer.Serialize(obj, jsonSerializerOptions); +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Config/Property.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Config/Property.cs new file mode 100644 index 000000000..01735427f --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Config/Property.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Tests.Internal.Config; + +internal class Property +{ + public string Key { get; set; } = default!; + + public string Value { get; set; } = default!; +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Config/PublishRelease.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Config/PublishRelease.cs new file mode 100644 index 000000000..5cafa5fe5 --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Config/PublishRelease.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Tests.Internal; + +internal class PublishRelease +{ + public ConfigFormats ConfigFormat { get; set; } + + public string? Content { get; set; } +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/CustomTrigger.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/CustomTrigger.cs new file mode 100644 index 000000000..93088edcc --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/CustomTrigger.cs @@ -0,0 +1,26 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Tests.Internal; + +public class CustomTrigger +{ + private JsonSerializerOptions _jsonSerializerOptions; + + public CustomTrigger(JsonSerializerOptions jsonSerializerOptions) + { + _jsonSerializerOptions = jsonSerializerOptions; + } + + internal ConfigFormats Formats { get; set; } + + internal string Content { get; set; } + + internal Action Action { get; set; } + + internal void Execute() + { + Action?.Invoke(new PublishRelease() + { + ConfigFormat = Formats, + Content = Content + }.Serialize(_jsonSerializerOptions)); + } +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Enum/ConfigFormats.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Enum/ConfigFormats.cs new file mode 100644 index 000000000..15e6e5d3b --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Enum/ConfigFormats.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Tests.Internal.Enum; + +internal enum ConfigFormats +{ + Properties = 1, + Text, + Json +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Model/Brands.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Model/Brands.cs new file mode 100644 index 000000000..d394e9e06 --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/Internal/Model/Brands.cs @@ -0,0 +1,14 @@ +namespace MASA.Contrib.BasicAbility.Dcc.Tests.Internal.Model; + +internal class Brands +{ + public Guid Id { get; init; } + + public string Name { get; set; } + + public Brands() + => this.Id = Guid.NewGuid(); + + public Brands(string Name) : this() + => this.Name = Name; +} diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/MASA.Contrib.BasicAbility.Dcc.Tests.csproj b/test/MASA.Contrib.BasicAbility.Dcc.Tests/MASA.Contrib.BasicAbility.Dcc.Tests.csproj new file mode 100644 index 000000000..8d6f3f437 --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/MASA.Contrib.BasicAbility.Dcc.Tests.csproj @@ -0,0 +1,36 @@ + + + + net6.0 + enable + false + enable + + + + + Always + + + Always + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/_Imports.cs b/test/MASA.Contrib.BasicAbility.Dcc.Tests/_Imports.cs new file mode 100644 index 000000000..8161f76a2 --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/_Imports.cs @@ -0,0 +1,14 @@ +global using MASA.BuildingBlocks.Configuration; +global using MASA.Contrib.BasicAbility.Dcc.Options; +global using MASA.Contrib.BasicAbility.Dcc.Tests.Internal; +global using MASA.Contrib.BasicAbility.Dcc.Tests.Internal.Common; +global using MASA.Contrib.BasicAbility.Dcc.Tests.Internal.Config; +global using MASA.Contrib.BasicAbility.Dcc.Tests.Internal.Enum; +global using MASA.Contrib.BasicAbility.Dcc.Tests.Internal.Model; +global using MASA.Utils.Caching.DistributedMemory; +global using MASA.Utils.Caching.DistributedMemory.Interfaces; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; +global using System.Text.Json; diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/appsettings.json b/test/MASA.Contrib.BasicAbility.Dcc.Tests/appsettings.json new file mode 100644 index 000000000..144652eea --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/appsettings.json @@ -0,0 +1,23 @@ +{ + "DccOptions": { + "ManageServiceAddress": "http://localhost:6379", + "SubscribeKeyPrefix": "masa.dcc:", + "RedisOptions": { + "Servers": [ + { + "Host": "localhost", + "Port": 8888 + } + ], + "DefaultDatabase": 0, + "Password": "" + } + }, + "AppId": "WebApplication1", + "Environment": "Development", + "Cluster": "Default", + "ConfigObjects": [ + "Brand" + ], + "Sectet": "" +} \ No newline at end of file diff --git a/test/MASA.Contrib.BasicAbility.Dcc.Tests/expandSections.json b/test/MASA.Contrib.BasicAbility.Dcc.Tests/expandSections.json new file mode 100644 index 000000000..28e953152 --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/expandSections.json @@ -0,0 +1,32 @@ +{ + "DccOptions": { + "ManageServiceAddress": "http://localhost:6379", + "RedisOptions": { + "Servers": [ + { + "Host": "localhost", + "Port": 8888 + } + ], + "DefaultDatabase": 0, + "Password": "" + } + }, + "AppId": "DccTest", + "Environment": "Test", + "Cluster": "Default", + "ConfigObjects": [ + "Test1" + ], + "Sectet": "", + "ExpandSections": [ + { + "AppId": "DccTest2", + "Environment": "Test2", + "Cluster": "Default", + "ConfigObjects": [ + "Test3" + ] + } + ] +} \ No newline at end of file diff --git a/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/ErrorKafkaOptions.cs b/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/ErrorKafkaOptions.cs new file mode 100644 index 000000000..1ab0da584 --- /dev/null +++ b/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/ErrorKafkaOptions.cs @@ -0,0 +1,12 @@ +namespace MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests; + +public class ErrorKafkaOptions : KafkaOptions +{ + [JsonIgnore] + public override string? ParentSection { get; init; } = "Appsettings"; + + public ErrorKafkaOptions() + { + base.Section = "KafkaOptions"; + } +} diff --git a/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/KafkaOptions.cs b/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/KafkaOptions.cs new file mode 100644 index 000000000..a2f1f13ee --- /dev/null +++ b/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/KafkaOptions.cs @@ -0,0 +1,15 @@ +namespace MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests; + +public class KafkaOptions : MasaConfigurationOptions +{ + public string Servers { get; set; } + + public int ConnectionPoolSize { get; set; } + + public override SectionTypes SectionType { get; init; } = SectionTypes.Local; + + public KafkaOptions() + { + base.ParentSection = ""; + } +} diff --git a/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests.csproj b/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests.csproj new file mode 100644 index 000000000..0ddf5d6a8 --- /dev/null +++ b/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + false + enable + + + + + + + diff --git a/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/_Imports.cs b/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/_Imports.cs new file mode 100644 index 000000000..ddfd0182f --- /dev/null +++ b/test/MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests/_Imports.cs @@ -0,0 +1,2 @@ +global using MASA.BuildingBlocks.Configuration; +global using System.Text.Json.Serialization; diff --git a/test/MASA.Contribs.DDD.Domain.Repository/MASA.Contribs.DDD.Domain.Repository.csproj b/test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests.csproj similarity index 61% rename from test/MASA.Contribs.DDD.Domain.Repository/MASA.Contribs.DDD.Domain.Repository.csproj rename to test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests.csproj index 268cbace4..11cf31006 100644 --- a/test/MASA.Contribs.DDD.Domain.Repository/MASA.Contribs.DDD.Domain.Repository.csproj +++ b/test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests.csproj @@ -2,9 +2,13 @@ net6.0 - enable enable false + enable + + + + diff --git a/test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/MountSectionRedisOptions.cs b/test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/MountSectionRedisOptions.cs new file mode 100644 index 000000000..22de5854b --- /dev/null +++ b/test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/MountSectionRedisOptions.cs @@ -0,0 +1,12 @@ +namespace MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests; + +public class MountSectionRedisOptions : MasaConfigurationOptions +{ + [JsonIgnore] + public override string? ParentSection { get; init; } = "Appsettings"; + + [JsonIgnore] + public override string? Section { get; init; } = null; + + public override SectionTypes SectionType { get; init; } = SectionTypes.ConfigurationAPI; +} diff --git a/test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/_Imports.cs b/test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/_Imports.cs new file mode 100644 index 000000000..ddfd0182f --- /dev/null +++ b/test/MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests/_Imports.cs @@ -0,0 +1,2 @@ +global using MASA.BuildingBlocks.Configuration; +global using System.Text.Json.Serialization; diff --git a/test/MASA.Contrib.Configuration.Tests/Config/RabbitMqOptions.cs b/test/MASA.Contrib.Configuration.Tests/Config/RabbitMqOptions.cs new file mode 100644 index 000000000..3bbcc3a4d --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/Config/RabbitMqOptions.cs @@ -0,0 +1,16 @@ +namespace MASA.Contrib.Configuration.Tests.Config; + +public class RabbitMqOptions : MasaConfigurationOptions +{ + public string HostName { get; set; } + + public string UserName { get; set; } + + public string Password { get; set; } + + public string VirtualHost { get; set; } + + public string Port { get; set; } + + public override SectionTypes SectionType { get; init; } = SectionTypes.Local; +} diff --git a/test/MASA.Contrib.Configuration.Tests/Config/RedisOptions.cs b/test/MASA.Contrib.Configuration.Tests/Config/RedisOptions.cs new file mode 100644 index 000000000..3fe96cb56 --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/Config/RedisOptions.cs @@ -0,0 +1,10 @@ +namespace MASA.Contrib.Configuration.Tests.Config; + +public class RedisOptions +{ + public string Ip { get; set; } + + public string Password { get; set; } + + public int Port { get; set; } +} diff --git a/test/MASA.Contrib.Configuration.Tests/Config/SystemOptions.cs b/test/MASA.Contrib.Configuration.Tests/Config/SystemOptions.cs new file mode 100644 index 000000000..39f32fa84 --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/Config/SystemOptions.cs @@ -0,0 +1,14 @@ +namespace MASA.Contrib.Configuration.Tests.Config; + +public class SystemOptions : MasaConfigurationOptions +{ + [JsonIgnore] + public override string? ParentSection { get; init; } = "Appsettings"; + + [JsonIgnore] + public override string? Section { get; init; } = null; + + public override SectionTypes SectionType { get; init; } = SectionTypes.Local; + + public string? Name { get; set; } +} diff --git a/test/MASA.Contrib.Configuration.Tests/ConfigurationTest.cs b/test/MASA.Contrib.Configuration.Tests/ConfigurationTest.cs new file mode 100644 index 000000000..fe62c475c --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/ConfigurationTest.cs @@ -0,0 +1,266 @@ +namespace MASA.Contrib.Configuration.Tests; + +[TestClass] +public class ConfigurationTest +{ + private IConfigurationBuilder _configurationBuilder; + + [TestInitialize] + public void Initialize() + { + _configurationBuilder = new ConfigurationBuilder(); + } + + [TestMethod] + public void TestAddSection() + { + var masaConfigurationBuilder = new MasaConfigurationBuilder(_configurationBuilder); + Assert.ThrowsException(() => masaConfigurationBuilder.AddSection(null!)); + + Assert.ThrowsException(() => masaConfigurationBuilder.AddSection(new ConfigurationBuilder())); + + masaConfigurationBuilder.AddSection( + new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true), "appsettings" + ); + + Assert.IsTrue(masaConfigurationBuilder.GetSectionRelations().Count == 1); + + masaConfigurationBuilder.AddSection( + new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("redis.json", true, true) + ); + Assert.IsTrue(masaConfigurationBuilder.GetSectionRelations().Count == 2); + + Assert.ThrowsException(() => + { + masaConfigurationBuilder.AddSection( + new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("rabbitMq.json", true, true) + ); + }); + } + + [TestMethod] + public void TestAddCustomSection() + { + var builder = WebApplication.CreateBuilder(); + builder.AddMasaConfiguration(configurationBuilder => + { + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("redis.json", true, true), "RedisOptions"); + + configurationBuilder.AddSection( + new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("rabbitMq.json", true, true), "RabbitMqOptions" + ); + + configurationBuilder.UseMasaOptions(option => + { + option.Mapping(SectionTypes.Local, ""); + }); + }); + var serviceProvider = builder.Services.BuildServiceProvider(); + var configuration = serviceProvider.GetRequiredService(); + var redisOption = serviceProvider.GetRequiredService>(); + + Assert.IsNotNull(configuration); + Assert.IsNotNull(redisOption); + Assert.IsTrue(redisOption.Value.Ip == "localhost"); + } + + [TestMethod] + public void TestAddMasaConfiguration() + { + var builder = WebApplication.CreateBuilder(); + builder.AddMasaConfiguration(configurationBuilder => + { + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("redis.json", true, true) + ); + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("rabbitMq.json", true, true), "RabbitMqOptions" + ); + configurationBuilder.UseMasaOptions(option => + option.Mapping(SectionTypes.Local, "", "") + ); + }); + var serviceProvider = builder.Services.BuildServiceProvider(); + var configuration = serviceProvider.GetRequiredService(); + var redisOption = serviceProvider.GetRequiredService>(); + Assert.IsTrue(configuration["Local:Ip"] == "localhost"); + Assert.IsTrue(redisOption.Value.Ip == "localhost"); + + var rabbitMqOption = serviceProvider.GetRequiredService>(); + Assert.IsTrue(configuration["Local:RabbitMqOptions:UserName"] == "admin"); + Assert.IsTrue(rabbitMqOption.Value.UserName == "admin" && rabbitMqOption.Value.Password == "admin"); + } + + [TestMethod] + public void TestAddMultiMasaConfiguration() + { + var builder = WebApplication.CreateBuilder(); + builder.AddMasaConfiguration(configurationBuilder => + { + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("redis.json", true, true) + ); + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("rabbitMq.json", true, true), "RabbitMqOptions" + ); + configurationBuilder.UseMasaOptions(option => + option.Mapping(SectionTypes.Local, "", "") + ); + }).AddMasaConfiguration(); + var serviceProvider = builder.Services.BuildServiceProvider(); + var configuration = serviceProvider.GetRequiredService(); + var redisOption = serviceProvider.GetRequiredService>(); + Assert.IsTrue(configuration["Local:Ip"] == "localhost"); + Assert.IsTrue(redisOption.Value.Ip == "localhost"); + + var rabbitMqOption = serviceProvider.GetRequiredService>(); + Assert.IsTrue(configuration["Local:RabbitMqOptions:UserName"] == "admin"); + Assert.IsTrue(rabbitMqOption.Value.UserName == "admin" && rabbitMqOption.Value.Password == "admin"); + } + + [TestMethod] + public void TestAutoMapSectionError() + { + var builder = WebApplication.CreateBuilder(); + builder.Host.ConfigureAppConfiguration((context, config) => { config.Sources.Clear(); }); + var chainedConfiguration = new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true); + builder.Configuration.AddConfiguration(chainedConfiguration.Build()); + + Assert.ThrowsException(() => + builder.AddMasaConfiguration(configurationBuilder => + { + }, "Appsettings", typeof(ConfigurationTest).Assembly, typeof(KafkaOptions).Assembly)); + } + + [TestMethod] + public void TestAutoMapAndErrorSection() + { + var builder = WebApplication.CreateBuilder(); + Assert.ThrowsException(() => + { + return builder.AddMasaConfiguration(configurationBuilder => + { + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("redis.json", true, true) + ); //Mount to the Local section + }, "Appsettings", typeof(ConfigurationTest).Assembly, typeof(MountSectionRedisOptions).Assembly); + }); + } + + [TestMethod] + public void TestRepeatMappting() + { + var builder = WebApplication.CreateBuilder(); + Assert.ThrowsException(() => + { + builder.AddMasaConfiguration(configurationBuilder => + { + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("redis.json", true, true) + ); + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(builder.Environment.ContentRootPath) + .AddJsonFile("rabbitMq.json", true, true), "RabbitMqOptions" + ); + configurationBuilder.UseMasaOptions(option => + { + option.Mapping(SectionTypes.Local, "", ""); + option.Mapping(SectionTypes.Local, "", ""); + }); + }); + }); + } + + [TestMethod] + public void TestCreateMasaConfiguration() + { + var services = new ServiceCollection(); + services.CreateMasaConfiguration(configurationBuilder => + { + configurationBuilder.AddSection( + new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("redis.json", true, true) + ); + configurationBuilder.AddSection( + new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("rabbitMq.json", true, true), "RabbitMqOptions" + ); + configurationBuilder.UseMasaOptions(option => + option.Mapping(SectionTypes.Local, "", "") + ); + }, new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true), "Appsettings"); + IServiceProvider serviceProvider = services.BuildServiceProvider(); + var redisOption = serviceProvider.GetRequiredService>(); + Assert.IsTrue(redisOption.Value.Ip == "localhost"); + } + + [TestMethod] + public void TestNullSection() + { + var services = new ServiceCollection(); + var ex = Assert.ThrowsException(() => services.CreateMasaConfiguration(null)); + Assert.IsTrue(ex.Message == "Please add the section to be loaded"); + } + + [TestMethod] + public void TestConfigurationChange() + { + var builder = WebApplication.CreateBuilder(); + + var rootPath = builder.Environment.ContentRootPath; + builder.AddMasaConfiguration(configurationBuilder => + { + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(rootPath) + .AddJsonFile("redis.json", true, true), "RedisOptions"); + + configurationBuilder.AddSection(new ConfigurationBuilder() + .SetBasePath(rootPath) + .AddJsonFile("rabbitMq.json", true, true), "RabbitMqOptions" + ); + + configurationBuilder.UseMasaOptions(option => + { + option.Mapping(SectionTypes.Local, ""); + }); + }, "Appsettings", typeof(ConfigurationTest).Assembly); + var serviceProvider = builder.Services.BuildServiceProvider(); + var configuration = serviceProvider.GetRequiredService(); + var systemOption = serviceProvider.GetRequiredService>(); + + Assert.IsNotNull(configuration); + Assert.IsNotNull(systemOption); + Assert.IsTrue(systemOption.Value.Name == "MASA TEST"); + + var newRedisOption = systemOption.Value; + newRedisOption.Name = null; + + File.WriteAllText(Path.Combine(rootPath, "appsettings.json"), System.Text.Json.JsonSerializer.Serialize(new { SystemOptions = newRedisOption })); + + Thread.Sleep(2000); + var option = serviceProvider.GetRequiredService>(); + Assert.IsTrue(option.CurrentValue.Name == ""); + } +} diff --git a/test/MASA.Contrib.Configuration.Tests/MASA.Contrib.Configuration.Tests.csproj b/test/MASA.Contrib.Configuration.Tests/MASA.Contrib.Configuration.Tests.csproj new file mode 100644 index 000000000..318fc49f0 --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/MASA.Contrib.Configuration.Tests.csproj @@ -0,0 +1,43 @@ + + + + net6.0 + enable + false + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + Always + + + + Always + + + + Always + + + + diff --git a/test/MASA.Contrib.Configuration.Tests/_Imports.cs b/test/MASA.Contrib.Configuration.Tests/_Imports.cs new file mode 100644 index 000000000..12d9cc8ba --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/_Imports.cs @@ -0,0 +1,10 @@ +global using MASA.BuildingBlocks.Configuration; +global using MASA.Contrib.Configuration.ErrorSectionAutoMap.Tests; +global using MASA.Contrib.Configuration.MountErrorSectionAutoMap.Tests; +global using MASA.Contrib.Configuration.Tests.Config; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Options; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using System.Text.Json.Serialization; diff --git a/test/MASA.Contrib.Configuration.Tests/appsettings.json b/test/MASA.Contrib.Configuration.Tests/appsettings.json new file mode 100644 index 000000000..9c1279ff5 --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/appsettings.json @@ -0,0 +1,9 @@ +{ + "KafkaOptions": { + "Servers": "Kafka Server", + "int": 10 + }, + "SystemOptions": { + "Name": "MASA TEST" + } +} \ No newline at end of file diff --git a/test/MASA.Contrib.Configuration.Tests/rabbitMq.json b/test/MASA.Contrib.Configuration.Tests/rabbitMq.json new file mode 100644 index 000000000..cbff2c19a --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/rabbitMq.json @@ -0,0 +1,7 @@ +{ + "HostName": "localhost", + "UserName": "admin", + "Password": "admin", + "VirtualHost": "/", + "Port": 5672 +} \ No newline at end of file diff --git a/test/MASA.Contrib.Configuration.Tests/redis.json b/test/MASA.Contrib.Configuration.Tests/redis.json new file mode 100644 index 000000000..ebce8f626 --- /dev/null +++ b/test/MASA.Contrib.Configuration.Tests/redis.json @@ -0,0 +1,5 @@ +{ + "Ip": "localhost", + "Password": "", + "Port": 6379 +} \ No newline at end of file diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.csproj b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests.csproj similarity index 91% rename from test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.csproj rename to test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests.csproj index 1f9d41356..9dc08646c 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.csproj +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests.csproj @@ -2,8 +2,9 @@ net6.0 - enable enable + false + enable diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests/Students.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests/Students.cs new file mode 100644 index 000000000..d9363e62d --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests/Students.cs @@ -0,0 +1,28 @@ +namespace MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests; + +public class Students : AggregateRoot +{ + public Students() + { + RegisterTime = DateTime.UtcNow; + } + + public string SerialNumber { get; set; } = default!; + + public string Name { get; set; } + + public int Age { get; set; } + + public DateTime RegisterTime { get; private set; } + + /// + /// Test the case of the joint primary key error, no business value + /// + /// + public override IEnumerable<(string Name, object Value)> GetKeys() + => new List<(string Name, object Value)>() + { + ("SerialNumber", SerialNumber), + ("","") + }; +} diff --git a/test/MASA.Contribs.DDD.Domain.Entities/_Imports.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests/_Imports.cs similarity index 100% rename from test/MASA.Contribs.DDD.Domain.Entities/_Imports.cs rename to test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests/_Imports.cs diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/Courses.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/Courses.cs new file mode 100644 index 000000000..45965f525 --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/Courses.cs @@ -0,0 +1,19 @@ +namespace MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFind.Tests; + +public class Courses : AggregateRoot +{ + public Courses() + { + Id = Guid.NewGuid(); + } + + public Guid Id { get; init; } + + public string Name { get; set; } + + public override IEnumerable<(string Name, object Value)> GetKeys() + => new List<(string Name, object Value)>() + { + ("Names",Name)//Demonstrate that a non-existent key is used as a joint primary key + }; +} diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFind.Tests.csproj b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFind.Tests.csproj new file mode 100644 index 000000000..9dc08646c --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFind.Tests.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + false + enable + + + + + + + diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/_Imports.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/_Imports.cs new file mode 100644 index 000000000..d6a2a9e7d --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFindTests/_Imports.cs @@ -0,0 +1 @@ +global using MASA.BuildingBlocks.DDD.Domain.Entities; diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/Entities/User.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/Entities/User.cs similarity index 91% rename from test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/Entities/User.cs rename to test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/Entities/User.cs index d4f5399ee..915381728 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/Entities/User.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/Entities/User.cs @@ -1,4 +1,4 @@ -namespace MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Entities; +namespace MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests.Entities; public class User : AggregateRoot { diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests.csproj b/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests.csproj new file mode 100644 index 000000000..453ed369c --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + enable + false + + + + + + + diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/Repositories/IUserRepository.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/Repositories/IUserRepository.cs similarity index 55% rename from test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/Repositories/IUserRepository.cs rename to test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/Repositories/IUserRepository.cs index ecdd10afe..83e3677da 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/Repositories/IUserRepository.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/Repositories/IUserRepository.cs @@ -1,4 +1,6 @@ -namespace MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Repositories; +using MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests.Entities; + +namespace MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests.Repositories; public interface IUserRepository : IRepository { diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/_Imports.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/_Imports.cs similarity index 58% rename from test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/_Imports.cs rename to test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/_Imports.cs index 2348cb021..5db641dcf 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository/_Imports.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests/_Imports.cs @@ -1,3 +1,2 @@ global using MASA.BuildingBlocks.DDD.Domain.Entities; global using MASA.BuildingBlocks.DDD.Domain.Repositories; -global using MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Entities; diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/Hobbies.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/Hobbies.cs new file mode 100644 index 000000000..f95e10bc4 --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/Hobbies.cs @@ -0,0 +1,16 @@ +namespace MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests; + +public class Hobbies : AggregateRoot +{ + public string Name { get; private set; } + + private Hobbies() + { + Id = Guid.NewGuid(); + } + + public Hobbies(string name) : this() + { + Name = name; + } +} diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests.csproj b/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests.csproj new file mode 100644 index 000000000..9f97c3017 --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + false + enable + + + + + + + diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/_Imports.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/_Imports.cs new file mode 100644 index 000000000..d6a2a9e7d --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests/_Imports.cs @@ -0,0 +1 @@ +global using MASA.BuildingBlocks.DDD.Domain.Entities; diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/BaseRepositoryTest.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/BaseRepositoryTest.cs new file mode 100644 index 000000000..1778f8154 --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/BaseRepositoryTest.cs @@ -0,0 +1,78 @@ +namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests; + +[TestClass] +public class BaseRepositoryTest : TestBase +{ + private IServiceCollection _services = default!; + private Assembly[] _assemblies; + private Mock _uoW; + private Mock _dispatcherOptions = default!; + + [TestInitialize] + public void Initialize() + { + _services = new ServiceCollection(); + _assemblies = new Assembly[1] + { + typeof(BaseRepositoryTest).Assembly + }; + _uoW = new(); + _dispatcherOptions = new(); + _dispatcherOptions.Setup(options => options.Services).Returns(() => _services); + } + + [TestMethod] + public void TestNullServices() + { + Assert.ThrowsException(() => + { + _dispatcherOptions.Setup(options => options.Services).Returns(() => null!); + var options = _dispatcherOptions.Object.UseRepository(); + }); + } + + [TestMethod] + public void TestUseCustomRepositoryAndNotImplementation() + { + Mock uoW = new(); + _services.AddScoped(serviceProvider => uoW.Object); + + Assert.ThrowsException(() + => _dispatcherOptions.Object.UseRepository(typeof(TestBase).Assembly, typeof(IUserRepository).Assembly) + ); + } + + [TestMethod] + public void TestNullUnitOfWork() + { + var ex = Assert.ThrowsException(() => + { + _dispatcherOptions.Object.UseRepository(_assemblies); + }); + Assert.IsTrue(ex.Message == "Please add UoW first."); + } + + [TestMethod] + public void TestNullAssembly() + { + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection)); + + Assert.ThrowsException(() => + { + _dispatcherOptions.Object.UseRepository(null!); + }); + } + + [TestMethod] + public void TestAddMultRepository() + { + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection)); + _dispatcherOptions.Object.UseRepository(_assemblies).UseRepository(); + + var serviceProvider = _services.BuildServiceProvider(); + var repository = serviceProvider.GetServices>(); + Assert.IsTrue(repository.Count() == 1); + } +} diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Entities/Address.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Entities/Address.cs index b40622c72..27f677f52 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Entities/Address.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Entities/Address.cs @@ -1,24 +1,23 @@ -namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests.Domain.Entities +namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests.Domain.Entities; + +public class Address : ValueObject { - public class Address : ValueObject - { - public string Street { get; set; } + public string Street { get; set; } - public string City { get; set; } + public string City { get; set; } - public string State { get; set; } + public string State { get; set; } - public string Country { get; set; } + public string Country { get; set; } - public string ZipCode { get; set; } + public string ZipCode { get; set; } - protected override IEnumerable GetEqualityValues() - { - yield return Street; - yield return City; - yield return State; - yield return Country; - yield return ZipCode; - } + protected override IEnumerable GetEqualityValues() + { + yield return Street; + yield return City; + yield return State; + yield return Country; + yield return ZipCode; } } diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Entities/Orders.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Entities/Orders.cs index 0ed2dada2..09fed6f3c 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Entities/Orders.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Entities/Orders.cs @@ -1,19 +1,40 @@ namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests.Domain.Entities; -public class Orders : AuditAggregateRoot +public class Orders : AuditAggregateRoot { public int OrderNumber { get; set; } - public DateTime OrderDate { get; set; } + public DateTime OrderDate { get; private set; } - public string OrderStatus { get; set; } + public string OrderStatus { get; private set; } - public string Description { get; set; } - - public string BuyerId { get; set; } - - public string BuyerName { get; set; } + public string Description { get; set; } = default!; public List OrderItems { get; set; } + + public Orders() + { + this.OrderDate = DateTime.UtcNow; + this.OrderItems = new(); + this.OrderStatus = "Submitted"; + } + + public Orders(int id) : this() + { + base.Id = id; + } + + /// + /// Joint primary key, when this method does not exist, the primary key is Id + /// + /// + public override IEnumerable<(string Name, object Value)> GetKeys() + { + return new List<(string Name, object value)> + { + ("Id", Id), + ("OrderNumber", OrderNumber) + }; + } } diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Repositories/IOrderRepository.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Repositories/IOrderRepository.cs index 39f509ad2..03d6adba4 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Repositories/IOrderRepository.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Domain/Repositories/IOrderRepository.cs @@ -2,4 +2,5 @@ namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests.Domain.Repositories; public interface IOrderRepository : IRepository { + Task AddAsync(Orders order); } diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/CustomDbContext.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/CustomDbContext.cs new file mode 100644 index 000000000..31e12e8cd --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/CustomDbContext.cs @@ -0,0 +1,23 @@ +namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests.Infrastructure; + +public class CustomDbContext : DbContext +{ + public DbSet Orders { get; set; } + + public DbSet Students { get; set; } + + public DbSet Courses { get; set; } + + public DbSet Hobbies { get; set; } + + public CustomDbContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + entityTypeBuilder => + { + entityTypeBuilder.HasKey("SerialNumber"); + }); + } +} diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/Options/DispatcherOptions.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/Options/DispatcherOptions.cs deleted file mode 100644 index 1aadc3d55..000000000 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/Options/DispatcherOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests.Infrastructure.Options; - -public class DispatcherOptions : IDispatcherOptions -{ - public IServiceCollection Services { get; } - - public DispatcherOptions(IServiceCollection services) => Services = services; -} diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/OrderDbContext.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/OrderDbContext.cs deleted file mode 100644 index 09ca126e4..000000000 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/OrderDbContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests.Infrastructure; - -public class OrderDbContext : DbContext -{ - public OrderDbContext(DbContextOptions options) : base(options) { } - - public DbSet Orders { get; set; } -} diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/Repositories/OrderRepository.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/Repositories/OrderRepository.cs index 498158cc6..717b8527f 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/Repositories/OrderRepository.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/Infrastructure/Repositories/OrderRepository.cs @@ -1,8 +1,23 @@ namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests.Infrastructure.Repositories; -public class OrderRepository : Repository, IOrderRepository +public class OrderRepository : Repository, IOrderRepository { - public OrderRepository(OrderDbContext context, IUnitOfWork unitOfWork) : base(context, unitOfWork) + public OrderRepository(CustomDbContext context, IUnitOfWork unitOfWork) : base(context, unitOfWork) { } + + public async Task AddAsync(Orders order) + { + try + { + var transaction = base.Transaction; + await base.AddAsync(order, default); + await base.SaveChangesAsync(); + await base.CommitAsync(); + } + catch (Exception) + { + await base.RollbackAsync(); + } + } } diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/MASA.Contrib.DDD.Domain.Repository.EF.Tests.csproj b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/MASA.Contrib.DDD.Domain.Repository.EF.Tests.csproj index 2ff0d55c2..9f271c9aa 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/MASA.Contrib.DDD.Domain.Repository.EF.Tests.csproj +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/MASA.Contrib.DDD.Domain.Repository.EF.Tests.csproj @@ -3,22 +3,29 @@ net6.0 enable - false + enable + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + - + + + + diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/RepositoryTest.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/RepositoryTest.cs index 5e431db8f..dba5d0c7e 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/RepositoryTest.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/RepositoryTest.cs @@ -3,80 +3,271 @@ namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests; [TestClass] public class RepositoryTest : TestBase { - private readonly Assembly[] _assemblies; + private IServiceCollection _services = default!; + private Assembly[] _assemblies; + private Mock _uoW; + private Mock _dispatcherOptions = default!; - public RepositoryTest() + [TestInitialize] + public void Initialize() { + _services = new ServiceCollection(); _assemblies = new Assembly[1] { - typeof(RepositoryTest).Assembly + typeof(BaseRepositoryTest).Assembly }; + _uoW = new(); + _uoW.Setup(uoW => uoW.UseTransaction).Returns(true); + _dispatcherOptions = new(); + _dispatcherOptions.Setup(options => options.Services).Returns(() => _services); + } [TestMethod] - public void TestNoServices() + public async Task TestAsync() { - Assert.ThrowsException(() => + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection)); + _dispatcherOptions.Object.UseRepository(_assemblies); + + var serviceProvider = _services.BuildServiceProvider(); + + _uoW.Setup(u => u.SaveChangesAsync(default)).Callback(() => + { + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + dbContext.SaveChanges(); + }); + _uoW.Setup(u => u.CommitAsync(default)).Verifiable(); + var orders = new List() + { + new Orders(1) + { + OrderNumber = 9999999, + Description = "Apple", + }, + new Orders(2) + { + OrderNumber = 9999999, + Description = "Apple2", + } + }; + + var repository = serviceProvider.GetRequiredService>(); + await repository.AddRangeAsync(orders); + await repository.UnitOfWork.SaveChangesAsync(); + + var orderList = await repository.GetListAsync(order => order.OrderNumber == 9999999, default); + Assert.IsNotNull(orderList); + Assert.IsTrue(orderList.Count() == 2); + + Assert.IsTrue((await repository.GetListAsync(order => order.Description == "Apple", default)).Count() == 1); + Assert.IsTrue(await repository.GetCountAsync(order => order.Description == "Apple", default) == 1); + + var huaweiOrder = await repository.FindAsync(order => order.Description == "Apple2"); + huaweiOrder!.Description = "HuaWei"; + huaweiOrder.OrderNumber = 9999998; + await repository.UnitOfWork.SaveChangesAsync(default); + + Assert.IsTrue((await repository.GetListAsync(order => order.Description == "Apple", default)).Count() == 1); + Assert.IsTrue(await repository.GetCountAsync(order => order.Description == "HuaWei", default) == 1); + + await repository.AddAsync(new Orders(3) + { + OrderNumber = 9999997, + Description = "Google" + }); + await repository.AddAsync(new Orders(4) { - var options = new DispatcherOptions(null).UseRepository(); + OrderNumber = 9999996, + Description = "Microsoft" }); + + await repository.RemoveAsync(order => order.Description == "Apple", default); + await repository.UnitOfWork.SaveChangesAsync(default); + + var list = await repository.GetPaginatedListAsync(0, 10, null, default); + + Assert.IsTrue(list.Count == 3); + Assert.IsTrue(list[0].Description == "HuaWei"); + Assert.IsTrue(list[1].Description == "Google"); + Assert.IsTrue(list[2].Description == "Microsoft"); + + list = await repository.GetPaginatedListAsync(1, 10, null, default); + Assert.IsTrue(list.Count == 2); + Assert.IsTrue(list[0].Description == "Google"); + Assert.IsTrue(list[1].Description == "Microsoft"); + + list = await repository.GetPaginatedListAsync(order => order.Description != "Google", 0, 10, null, default); + Assert.IsTrue(list.Count == 2); + Assert.IsTrue(list[0].Description == "HuaWei"); + + var count = await repository.GetCountAsync(default); + Assert.IsTrue(count == 3); + + var huaWei = await repository.FindAsync(huaweiOrder.Id, huaweiOrder.OrderNumber); + await repository.RemoveAsync(huaWei!, default); + + await repository.UnitOfWork.SaveChangesAsync(default); + Assert.IsTrue(await repository.GetCountAsync(default) == 2); + + var remainingOrders = await repository.GetListAsync(default); + await repository.RemoveRangeAsync(remainingOrders); + await repository.UnitOfWork.SaveChangesAsync(default); + + Assert.IsTrue(await repository.GetCountAsync(default) == 0); } [TestMethod] - public void TestUseCustomRepositoryAndNotImplementation() + public async Task TestTranscationFailedAsync() { - var services = new ServiceCollection(); + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection)); + _dispatcherOptions.Object.UseRepository(_assemblies); - Mock uow = new(); - services.AddScoped(serviceProvider => uow.Object); + var serviceProvider = _services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + dbContext.Database.BeginTransaction(); - Assert.ThrowsException(() => new DispatcherOptions(services).UseRepository(typeof(TestBase).Assembly, typeof(IUserRepository).Assembly)); + _uoW.Setup(u => u.SaveChangesAsync(default)).Callback(() => + { + dbContext.SaveChanges(); + }); + _uoW.Setup(u => u.CommitAsync(default)).Callback(() => + { + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.CurrentTransaction!.Commit(); + }); + _uoW.Setup(u => u.RollbackAsync(default)).Callback(() => + { + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.CurrentTransaction!.RollbackAsync(); + }); + var repository = serviceProvider.GetRequiredService(); + + var order = new Orders() + { + OrderNumber = 1, + }; + await repository.AddAsync(order); + Assert.IsTrue(await repository.GetCountAsync(default) == 0); } [TestMethod] - public void TestNoUnitOfWorkAssembly() + public async Task TestTranscationSucceededAsync() { - Assert.ThrowsException(() => + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection)); + _dispatcherOptions.Object.UseRepository(_assemblies); + + var serviceProvider = _services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + + _uoW.Setup(u => u.SaveChangesAsync(default)).Callback(() => + { + dbContext.SaveChanges(); + }); + _uoW.Setup(u => u.CommitAsync(default)).Callback(() => { - var serviceProvider = base.CreateServiceProvider(null, _assemblies); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.CurrentTransaction!.Commit(); }); + _uoW.Setup(u => u.RollbackAsync(default)).Callback(() => + { + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.CurrentTransaction!.RollbackAsync(); + }); + var repository = serviceProvider.GetRequiredService(); + + var order = new Orders(1) + { + OrderNumber = 1, + Description = "Apple" + }; + await repository.AddAsync(order); + Assert.IsTrue(await repository.GetCountAsync(default) == 1); } [TestMethod] - public void TestNullAssembly() + public async Task TestUpdateAsync() { - var serviceProvider = base.CreateDefaultServiceProvider(null)!; - var repository = serviceProvider.GetRequiredService>(); - Assert.IsNotNull(repository); - repository.AddAsync(new Orders() + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); + _dispatcherOptions.Object.UseRepository(_assemblies); + + var serviceProvider = _services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + + _uoW.Setup(u => u.SaveChangesAsync(default)).Callback(() => { - BuyerName = "lisa" + dbContext.SaveChanges(); }); + var repository = serviceProvider.GetRequiredService(); + + var order = new Orders(1) + { + OrderNumber = 1, + Description = "Apple" + }; + await repository.AddAsync(order, default); + await repository.UnitOfWork.SaveChangesAsync(default); + dbContext.Entry(order).State = Microsoft.EntityFrameworkCore.EntityState.Detached; + + order = await repository.FindAsync(order => order.Description == "Apple"); + order!.Description = "Apple Company"; + await repository.UnitOfWork.SaveChangesAsync(); + + order = await repository.FindAsync(order => order.Description == "Apple"); + Assert.IsNotNull(order); + + await repository.UpdateAsync(order, default); + await repository.UnitOfWork.SaveChangesAsync(); + dbContext.Entry(order).State = Microsoft.EntityFrameworkCore.EntityState.Detached; + Assert.IsTrue(await repository.GetCountAsync(default) == 1); + + order = await repository.FindAsync(order => order.Description == "Apple"); + Assert.IsNotNull(order); + + order.Description = "Apple Company"; + await repository.UpdateRangeAsync(new List() { order }, default); + await repository.UnitOfWork.SaveChangesAsync(); + + dbContext.Entry(order).State = Microsoft.EntityFrameworkCore.EntityState.Detached; + + order = await repository.FindAsync(order => order.Description == "Apple"); + Assert.IsNull(order); } [TestMethod] - public void TestCustomRepository() + public void TestCompositeKeys() { - var serviceProvider = base.CreateDefaultServiceProvider(_assemblies)!; - IOrderRepository orderRepository = serviceProvider.GetRequiredService(); - Assert.IsNotNull(orderRepository); - orderRepository.AddAsync(new Orders() + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection)); + Assert.ThrowsException(() => { - BuyerName = "lisa" + _dispatcherOptions.Object.UseRepository(typeof(BaseRepositoryTest).Assembly, typeof(Students).Assembly); }); } [TestMethod] - public void TestAddMultRepository() + public void TestErrorCompositeKeys() + { + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection)); + Assert.ThrowsException(() => + { + _dispatcherOptions.Object.UseRepository(typeof(BaseRepositoryTest).Assembly, typeof(Courses).Assembly); + }); + } + + [TestMethod] + public void TestPrivateEntity() { - var services = new ServiceCollection(); - Mock unitOfWork = new(); - services.AddScoped(typeof(IUnitOfWork), serviceProvider => unitOfWork.Object); - services.AddDbContext(options => options.UseSqlite(_connection)); - new DispatcherOptions(services).UseRepository(_assemblies).UseRepository(_assemblies); - - var serviceProvider = services.BuildServiceProvider(); - var repository = serviceProvider.GetServices>(); - Assert.IsTrue(repository.Count() == 1); + _services.AddScoped(typeof(IUnitOfWork), serviceProvider => _uoW.Object); + _services.AddDbContext(options => options.UseSqlite(_connection)); + _dispatcherOptions.Object.UseRepository(typeof(BaseRepositoryTest).Assembly, typeof(Hobbies).Assembly); } } diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/TestBase.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/TestBase.cs index a99971ce8..574d3fdd8 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/TestBase.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/TestBase.cs @@ -1,6 +1,6 @@ namespace MASA.Contrib.DDD.Domain.Repository.EF.Tests; -public class TestBase +public class TestBase : IDisposable { protected readonly SqliteConnection _connection; @@ -14,24 +14,4 @@ public void Dispose() { _connection.Close(); } - - - protected IServiceProvider CreateDefaultServiceProvider(params Assembly[] assemblies) - { - return CreateServiceProvider(services => - { - Mock unitOfWork = new(); - services.AddScoped(typeof(IUnitOfWork), serviceProvider => unitOfWork.Object); - services.AddDbContext(options => options.UseSqlite(_connection)); - }, assemblies); - } - - protected IServiceProvider CreateServiceProvider(Action? action, params Assembly[] assemblies) - { - var services = new ServiceCollection(); - action?.Invoke(services); - - new DispatcherOptions(services).UseRepository(assemblies); - return services.BuildServiceProvider(); - } } diff --git a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/_Imports.cs b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/_Imports.cs index 763686eef..0d939826d 100644 --- a/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/_Imports.cs +++ b/test/MASA.Contrib.DDD.Domain.Repository.EF.Tests/_Imports.cs @@ -1,14 +1,16 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.DDD.Domain.Entities; global using MASA.BuildingBlocks.DDD.Domain.Entities.Auditing; global using MASA.BuildingBlocks.DDD.Domain.Repositories; global using MASA.BuildingBlocks.DDD.Domain.Values; global using MASA.BuildingBlocks.Dispatcher.Events; -global using MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Repositories; +global using MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeys.Tests; +global using MASA.Contrib.DDD.Domain.Repository.EF.CombinedKeysNoFind.Tests; +global using MASA.Contrib.DDD.Domain.Repository.EF.CustomRepository.Tests.Repositories; +global using MASA.Contrib.DDD.Domain.Repository.EF.Entity.Tests; global using MASA.Contrib.DDD.Domain.Repository.EF.Tests.Domain.Entities; global using MASA.Contrib.DDD.Domain.Repository.EF.Tests.Domain.Repositories; global using MASA.Contrib.DDD.Domain.Repository.EF.Tests.Infrastructure; -global using MASA.Contrib.DDD.Domain.Repository.EF.Tests.Infrastructure.Options; global using Microsoft.Data.Sqlite; global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.DependencyInjection; @@ -18,3 +20,4 @@ global using System.Collections.Generic; global using System.Linq; global using System.Reflection; + diff --git a/test/MASA.Contrib.DDD.Domain.Tests/DomainEventBusTest.cs b/test/MASA.Contrib.DDD.Domain.Tests/DomainEventBusTest.cs index f4b86aeaa..9365bd391 100644 --- a/test/MASA.Contrib.DDD.Domain.Tests/DomainEventBusTest.cs +++ b/test/MASA.Contrib.DDD.Domain.Tests/DomainEventBusTest.cs @@ -1,95 +1,127 @@ namespace MASA.Contrib.DDD.Domain.Tests; [TestClass] -public class DomainEventBusTest : TestBase +public class DomainEventBusTest { + private Assembly[] _defaultAssemblies = default!; + private IServiceCollection _services = default!; + private Mock _eventBus = default!; + private Mock _integrationEventBus = default!; + private Mock _uoW = default!; + private IOptions _dispatcherOptions = default!; + + [TestInitialize] + public void Initialize() + { + _defaultAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + _services = new ServiceCollection(); + _eventBus = new(); + _integrationEventBus = new(); + _uoW = new(); + _dispatcherOptions = Options.Create(new DispatcherOptions(new ServiceCollection())); + } + + [TestMethod] + public void TestGetAllEventTypes() + { + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + var eventTypes = assemblies.SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && typeof(IEvent).IsAssignableFrom(type)); + _eventBus.Setup(eventBus => eventBus.GetAllEventTypes()).Returns(() => eventTypes); + _dispatcherOptions.Value.Assemblies = _defaultAssemblies; + var domainEventBus = new DomainEventBus(_eventBus.Object, _integrationEventBus.Object, _uoW.Object, _dispatcherOptions); + + Assert.IsTrue(domainEventBus.GetAllEventTypes().Count() == eventTypes.Count(), ""); + } + [TestMethod] public async Task TestPublishDomainEventAsync() { - PaymentSucceededDomainEvent @event = new PaymentSucceededDomainEvent() - { - OrderId = new Random().Next(10000, 1000000).ToString() - }; - var serviceProvider = CreateDefaultProvider(); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@event); + var domainEventBus = new DomainEventBus(_eventBus.Object, _integrationEventBus.Object, _uoW.Object, _dispatcherOptions); + _eventBus.Setup(eventBus => eventBus.PublishAsync(It.IsAny())).Verifiable(); + + var domainEvent = new PaymentSucceededDomainEvent(new Random().Next(10000, 1000000).ToString()); + await domainEventBus.PublishAsync(domainEvent); - Assert.IsTrue(eventBus.GetAllEventTypes().Count() == 5); + _eventBus.Verify(eventBus => eventBus.PublishAsync(domainEvent), Times.Once, "PublishAsync is executed multiple times"); + _integrationEventBus.Verify(integrationEventBus => integrationEventBus.PublishAsync(domainEvent), Times.Never, "integrationEventBus should not be executed"); + Assert.IsTrue(domainEvent.UnitOfWork!.Equals(_uoW.Object)); } [TestMethod] public async Task TestPublishIntegrationDomainEventAsync() { - PaymentFailedIntegrationDomainEvent @event = new PaymentFailedIntegrationDomainEvent() + var domainEventBus = new DomainEventBus(_eventBus.Object, _integrationEventBus.Object, _uoW.Object, _dispatcherOptions); + _integrationEventBus.Setup(integrationEventBus => integrationEventBus.PublishAsync(It.IsAny())).Verifiable(); + var integrationDomainEvent = new PaymentFailedIntegrationDomainEvent() { OrderId = new Random().Next(10000, 1000000).ToString() }; - var serviceProvider = CreateDefaultProvider(); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@event); + await domainEventBus.PublishAsync(integrationDomainEvent); + + _eventBus.Verify(eventBus => eventBus.PublishAsync(integrationDomainEvent), Times.Never, "eventBus should not be executed"); + _integrationEventBus.Verify(integrationEventBus => integrationEventBus.PublishAsync(integrationDomainEvent), Times.Once, " PublishAsync is executed multiple times"); } [TestMethod] public async Task TestPublishDomainCommandAsync() { + _uoW.Setup(u => u.CommitAsync(default)).Verifiable(); + var domainEventBus = new DomainEventBus(_eventBus.Object, _integrationEventBus.Object, _uoW.Object, _dispatcherOptions); + _eventBus.Setup(eventBus => eventBus.PublishAsync(It.IsAny())) + .Callback((domainEvent) => + { + Mock> userRepository = new(); + var user = new Users() + { + Name = "Jim" + }; + userRepository.Setup(repository => repository.AddAsync(It.IsAny(), CancellationToken.None)).Verifiable(); + domainEvent.UnitOfWork!.CommitAsync(); + }); + var @command = new CreateProductDomainCommand() { Name = "Phone" }; + await domainEventBus.PublishAsync(@command); - var serviceProvider = CreateDefaultProvider(); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@command); + _eventBus.Verify(eventBus => eventBus.PublishAsync(@command), Times.Once, "PublishAsync is executed multiple times"); + _uoW.Verify(u => u.CommitAsync(default), Times.Once); } [TestMethod] - public async Task TestAddMultDomainEventBusAsync() + public void TestAddMultDomainEventBusAsync() { - var services = new ServiceCollection(); - - services.AddDomainEventBus(options => - { - options.Assemblies = new System.Reflection.Assembly[1] { typeof(TestBase).Assembly }; - Mock eventBus = new(); - eventBus.Setup(e => e.PublishAsync(It.IsAny())).Verifiable(); - services.AddScoped(typeof(IEventBus), serviceProvider => eventBus.Object); - - Mock unitOfWork = new(); - services.AddScoped(typeof(IUnitOfWork), serviceProvider => unitOfWork.Object); + _services.AddScoped(serviceProvider => _eventBus.Object); + _services.AddScoped(serviceProvider => _integrationEventBus.Object); + _services.AddScoped(serviceProvider => _uoW.Object); - Mock integrationEventBus = new(); - integrationEventBus.Setup(e => e.PublishAsync(It.IsAny())).Verifiable(); - services.AddScoped(typeof(IIntegrationEventBus), serviceProvider => integrationEventBus.Object); - }).AddDomainEventBus(); - - var serviceProvider = services.BuildServiceProvider(); + _services.AddDomainEventBus(options => options.Assemblies = new Assembly[1] { typeof(DomainEventBusTest).Assembly }).AddDomainEventBus(); + var serviceProvider = _services.BuildServiceProvider(); Assert.IsTrue(serviceProvider.GetServices().Count() == 1); - - var userDomainService = serviceProvider.GetService(); - Assert.IsNotNull(userDomainService); - - Assert.IsTrue(await userDomainService.RegisterUserSucceededAsync("tom") == "succeed"); + Assert.IsTrue(serviceProvider.GetServices>().Count() == 1); } [TestMethod] public void TestNotUseEventBus() { - var services = new ServiceCollection(); - - var ex = Assert.ThrowsException(() => services.AddDomainEventBus()); + var ex = Assert.ThrowsException(() + => _services.AddDomainEventBus() + ); Assert.IsTrue(ex.Message == "Please add EventBus first."); } [TestMethod] public void TestNotUseUnitOfWork() { - var services = new ServiceCollection(); - var eventBus = new Mock(); - services.AddScoped(serviceProvider => eventBus.Object); + _services.AddScoped(serviceProvider => eventBus.Object); - var ex = Assert.ThrowsException(() => services.AddDomainEventBus()); - Assert.IsTrue(ex.Message == "Please add Uow first."); + var ex = Assert.ThrowsException(() + => _services.AddDomainEventBus(options => options.Assemblies = new Assembly[1] { typeof(DomainEventBusTest).Assembly }) + ); + Assert.IsTrue(ex.Message == "Please add UoW first."); } [TestMethod] @@ -100,28 +132,19 @@ public void TestNotUseIntegrationEventBus() var eventBus = new Mock(); services.AddScoped(serviceProvider => eventBus.Object); - var uow = new Mock(); - services.AddScoped(serviceProvider => uow.Object); + var uoW = new Mock(); + services.AddScoped(serviceProvider => uoW.Object); - var ex = Assert.ThrowsException(() => services.AddDomainEventBus()); + var ex = Assert.ThrowsException(() + => services.AddDomainEventBus(options => options.Assemblies = new Assembly[1] { typeof(DomainEventBusTest).Assembly }) + ); Assert.IsTrue(ex.Message == "Please add IntegrationEventBus first."); } [TestMethod] public void TestNullAssembly() { - var services = new ServiceCollection(); - - var eventBus = new Mock(); - services.AddScoped(serviceProvider => eventBus.Object); - - var uow = new Mock(); - services.AddScoped(serviceProvider => uow.Object); - - var integrationEventBus = new Mock(); - services.AddScoped(serviceProvider => integrationEventBus.Object); - - Assert.ThrowsException(() => services.AddDomainEventBus(options => { options.Assemblies = null; })); + Assert.ThrowsException(() => _dispatcherOptions.Value.Assemblies = null!); } [TestMethod] @@ -132,8 +155,8 @@ public void TestNotRepository() var eventBus = new Mock(); services.AddScoped(serviceProvider => eventBus.Object); - var uow = new Mock(); - services.AddScoped(serviceProvider => uow.Object); + var uoW = new Mock(); + services.AddScoped(serviceProvider => uoW.Object); var integrationEventBus = new Mock(); services.AddScoped(serviceProvider => integrationEventBus.Object); @@ -142,12 +165,11 @@ public void TestNotRepository() { services.AddDomainEventBus(options => { - options.Assemblies = new System.Reflection.Assembly[1] { typeof(User).Assembly }; + options.Assemblies = new Assembly[1] { typeof(Users).Assembly }; }); }); } - [TestMethod] public void TestUserRepository() { @@ -156,92 +178,172 @@ public void TestUserRepository() var eventBus = new Mock(); services.AddScoped(serviceProvider => eventBus.Object); - var uow = new Mock(); - services.AddScoped(serviceProvider => uow.Object); + var uoW = new Mock(); + services.AddScoped(serviceProvider => uoW.Object); var integrationEventBus = new Mock(); services.AddScoped(serviceProvider => integrationEventBus.Object); - services.AddScoped, UserRepository>(); + + Mock> repository = new(); + services.AddScoped(serviceProvider => repository.Object); services.AddDomainEventBus(options => { - options.Assemblies = new System.Reflection.Assembly[2] { typeof(User).Assembly, typeof(UserRepository).Assembly }; + options.Assemblies = new Assembly[2] { typeof(Users).Assembly, typeof(DomainEventBusTest).Assembly }; }); } [TestMethod] public async Task TestPublishQueueAsync() { - var services = new ServiceCollection(); - - //todo: Temporary results, used to show the enqueue and dequeue order - int result = 0; - - Mock eventBus = new(); - eventBus - .Setup(e => e.PublishAsync(It.IsAny())) - .Callback(async cmd => - { - if (result == 0) - { - result = 3; - } - else - { - result = 4; - } - await Task.FromResult(result); - }); - Mock integrationEventBus = new(); - integrationEventBus - .Setup(e => e.PublishAsync(It.IsAny())) - .Callback(async cmd => + var domainEvent = new PaymentSucceededDomainEvent("ef5f84db-76e4-4c79-9815-99a1543b6589"); + var integrationDomainEvent = new PaymentFailedIntegrationDomainEvent { OrderId = "d65c1a0c-6e44-40ce-9737-738fa1dcdab4" }; + + _eventBus + .Setup(eventBus => eventBus.PublishAsync(It.IsAny())) + .Callback(() => + { + _integrationEventBus.Verify(integrationEventBus => integrationEventBus.PublishAsync(integrationDomainEvent), Times.Never, "Sent in the wrong order"); + }); + + _integrationEventBus + .Setup(integrationEventBus => integrationEventBus.PublishAsync(It.IsAny())) + .Callback(() => { - if (result == 3) - { - result = 1; - } - else - { - result = 2; - } - await Task.FromResult(result); + _eventBus.Verify(eventBus => eventBus.PublishAsync((IDomainEvent)domainEvent), Times.Once, "Sent in the wrong order"); }); - var uow = new Mock(); - uow.Setup(u => u.CommitAsync(default)).Verifiable(); + var uoW = new Mock(); + uoW.Setup(u => u.CommitAsync(default)).Verifiable(); - var options = Options.Create(new DispatcherOptions(services) { Assemblies = AppDomain.CurrentDomain.GetAssemblies() }); + var options = Options.Create(new DispatcherOptions(_services) { Assemblies = AppDomain.CurrentDomain.GetAssemblies() }); + + var domainEventBus = new DomainEventBus(_eventBus.Object, _integrationEventBus.Object, uoW.Object, options); - var domainEventBus = new DomainEventBus(eventBus.Object, integrationEventBus.Object, uow.Object, options); + await domainEventBus.Enqueue(domainEvent); + await domainEventBus.Enqueue(integrationDomainEvent); - // todo: It has no practical meaning, just to show the order of entering and leaving the team - await domainEventBus.Enqueue(new PaymentSucceededDomainEvent() { OrderId = "ef5f84db-76e4-4c79-9815-99a1543b6589" }); - await domainEventBus.Enqueue(new PaymentFailedIntegrationDomainEvent() { OrderId = "d65c1a0c-6e44-40ce-9737-738fa1dcdab4" }); await domainEventBus.PublishQueueAsync(); - Assert.IsTrue(result == 1); + + _eventBus.Verify(eventBus => eventBus.PublishAsync((IDomainEvent)domainEvent), Times.Once, "Sent in the wrong order"); + _integrationEventBus.Verify(integrationEventBus => integrationEventBus.PublishAsync(integrationDomainEvent), Times.Never, "Sent in the wrong order"); } [TestMethod] - public async Task TestPublishDomainQuery() + public async Task TestPublishDomainQueryAsync() { var services = new ServiceCollection(); var eventBus = new Mock(); eventBus.Setup(e => e.PublishAsync(It.IsAny())) - .Callback(async query => + .Callback(query => { - query.Result = "apple"; + if (query.ProductId == "2f8d4c3c-1736-4e56-a188-f865da6a63d1") + query.Result = "apple"; }); var integrationEventBus = new Mock(); - var uow = new Mock(); - uow.Setup(u => u.CommitAsync(default)).Verifiable(); + var uoW = new Mock(); + uoW.Setup(u => u.CommitAsync(default)).Verifiable(); var options = Options.Create(new DispatcherOptions(services) { Assemblies = AppDomain.CurrentDomain.GetAssemblies() }); - var domainEventBus = new DomainEventBus(eventBus.Object, integrationEventBus.Object, uow.Object, options); + var domainEventBus = new DomainEventBus(eventBus.Object, integrationEventBus.Object, uoW.Object, options); var query = new ProductItemDomainQuery() { ProductId = "2f8d4c3c-1736-4e56-a188-f865da6a63d1" }; + await domainEventBus.PublishAsync(query); Assert.IsTrue(query.Result == "apple"); } + + [TestMethod] + public async Task TestCommitAsync() + { + var services = new ServiceCollection(); + + _uoW.Setup(uow => uow.CommitAsync(CancellationToken.None)).Verifiable(); + Mock> options = new(); + + var domainEventBus = new DomainEventBus(_eventBus.Object, _integrationEventBus.Object, _uoW.Object, options.Object); + await domainEventBus.CommitAsync(CancellationToken.None); + + _uoW.Verify(u => u.CommitAsync(default), Times.Once, "CommitAsync must be called only once"); + } + + [TestMethod] + public void TestParameterInitialization() + { + var id = Guid.NewGuid(); + var createTime = DateTime.UtcNow; + + var domainCommand = new DomainCommand(); + Assert.IsTrue(domainCommand.Id != default); + Assert.IsTrue(domainCommand.CreationTime != default && domainCommand.CreationTime >= createTime); + + domainCommand = new DomainCommand(id, createTime); + Assert.IsTrue(domainCommand.Id == id); + Assert.IsTrue(domainCommand.CreationTime == createTime); + + var domainEvent = new DomainEvent(); + Assert.IsTrue(domainEvent.Id != default); + Assert.IsTrue(domainEvent.CreationTime != default && domainEvent.CreationTime >= createTime); + + domainEvent = new DomainEvent(id, createTime); + Assert.IsTrue(domainEvent.Id == id); + Assert.IsTrue(domainEvent.CreationTime == createTime); + + var domainQuery = new ProductItemDomainQuery() + { + ProductId = Guid.NewGuid().ToString() + }; + Assert.IsTrue(domainQuery.Id != default); + Assert.IsTrue(domainQuery.CreationTime != default && domainQuery.CreationTime >= createTime); + } + + [TestMethod] + public void TestDomainQueryUnitOfWork() + { + var domainQuery = new ProductItemDomainQuery() + { + ProductId = Guid.NewGuid().ToString() + }; + Assert.ThrowsException(() => + { + domainQuery.UnitOfWork = _uoW.Object; + }); + Assert.IsNull(domainQuery.UnitOfWork); + } + + [TestMethod] + public async Task TestDomainServiceAsync() + { + _integrationEventBus.Setup(integrationEventBus => integrationEventBus.PublishAsync(It.IsAny())).Verifiable(); + + _services.AddDomainEventBus(options => + { + options.Assemblies = new Assembly[1] { typeof(DomainEventBusTest).Assembly }; + options.Services.AddScoped(serviceProvider => _eventBus.Object); + options.Services.AddScoped(serviceProvider => _integrationEventBus.Object); + options.Services.AddScoped(serviceProvider => _uoW.Object); + }); + var serviceProvider = _services.BuildServiceProvider(); + + var userDomainService = serviceProvider.GetRequiredService(); + var domainIntegrationEvent = new RegisterUserSucceededDomainIntegrationEvent() { Account = "Tom" }; + await userDomainService.RegisterUserSucceededAsync(domainIntegrationEvent); + + _integrationEventBus.Verify(integrationEventBus => integrationEventBus.PublishAsync(domainIntegrationEvent), Times.Once); + } + + [TestMethod] + public async Task TestPublishEvent() + { + var domainEventBus = new DomainEventBus(_eventBus.Object, _integrationEventBus.Object, _uoW.Object, _dispatcherOptions); + _eventBus.Setup(eventBus => eventBus.PublishAsync(It.IsAny())).Verifiable(); + + var @event = new ForgetPasswordEvent() + { + Account = "Tom" + }; + await domainEventBus.PublishAsync(@event); + _eventBus.Verify(eventBus => eventBus.PublishAsync(@event), Times.Once); + } } diff --git a/test/MASA.Contrib.DDD.Domain.Tests/DomainIntegrationEventBusTest.cs b/test/MASA.Contrib.DDD.Domain.Tests/DomainIntegrationEventBusTest.cs new file mode 100644 index 000000000..1aeb39d9a --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Tests/DomainIntegrationEventBusTest.cs @@ -0,0 +1,49 @@ +namespace MASA.Contrib.DDD.Domain.Tests; + +[TestClass] +public class DomainIntegrationEventBus +{ + private Assembly[] _defaultAssemblies = default!; + private IServiceCollection _services = default!; + private Mock _integrationEventBus = default!; + private Mock _uoW = default!; + private IOptions _dispatcherOptions = default!; + + [TestInitialize] + public void Initialize() + { + _defaultAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + _services = new ServiceCollection(); + _integrationEventBus = new(); + _integrationEventBus.Setup(eventBus => eventBus.PublishAsync(It.IsAny())).Verifiable(); + _uoW = new(); + _dispatcherOptions = Options.Create(new DispatcherOptions(new ServiceCollection())); + } + + [TestMethod] + public async Task PublishQueueAsync() + { + _services.AddEventBus(opt => + { + opt.Assemblies = _defaultAssemblies; + }); + var serviceProvider = _services.BuildServiceProvider(); + var eventBus = serviceProvider.GetRequiredService(); + var payment = new + { + orderId = Guid.NewGuid(), + money = 100, + payTime = DateTime.UtcNow + }; + var domainEventBus = new DomainEventBus(eventBus, _integrationEventBus.Object, _uoW.Object, _dispatcherOptions); + + var domainEvent = new PaymentSucceededDomainEvent(payment.orderId.ToString()); + await domainEventBus.Enqueue(domainEvent); + + var integraionDomainEvent = new PaymentSucceededIntegraionDomainEvent(payment.orderId.ToString(), payment.money, payment.payTime); + await domainEventBus.Enqueue(integraionDomainEvent); + await domainEventBus.PublishQueueAsync(); + Assert.IsTrue(domainEvent.Result); + _integrationEventBus.Verify(eventBus => eventBus.PublishAsync(It.IsAny()), Times.Once); + } +} diff --git a/test/MASA.Contrib.DDD.Domain.Tests/EventHandlers/PaymentSucceededHandlers.cs b/test/MASA.Contrib.DDD.Domain.Tests/EventHandlers/PaymentSucceededHandlers.cs deleted file mode 100644 index 23a753bc0..000000000 --- a/test/MASA.Contrib.DDD.Domain.Tests/EventHandlers/PaymentSucceededHandlers.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MASA.Contrib.DDD.Domain.Tests.EventHandlers; - -public class PaymentSucceededHandlers : IEventHandler -{ - private readonly ILogger _logger; - - public PaymentSucceededHandlers(ILogger logger) => _logger = logger; - - public Task HandleAsync(PaymentSucceededDomainEvent @event) - { - _logger.LogInformation("Publishing PaymentSucceededDomainEvent {@Event} on {CreationTime}", @event, @event.CreationTime); - return Task.CompletedTask; - } -} diff --git a/test/MASA.Contrib.DDD.Domain.Tests/Events/ForgetPasswordEvent.cs b/test/MASA.Contrib.DDD.Domain.Tests/Events/ForgetPasswordEvent.cs new file mode 100644 index 000000000..7e8bdcf94 --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Tests/Events/ForgetPasswordEvent.cs @@ -0,0 +1,10 @@ +namespace MASA.Contrib.DDD.Domain.Tests.Events; + +public class ForgetPasswordEvent : IEvent +{ + public Guid Id { get; init; } = Guid.NewGuid(); + + public DateTime CreationTime { get; init; } = DateTime.UtcNow; + + public string Account { get; set; } +} diff --git a/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentFailedIntegrationDomainEvent.cs b/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentFailedIntegrationDomainEvent.cs index 4aa55e063..70d3a5098 100644 --- a/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentFailedIntegrationDomainEvent.cs +++ b/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentFailedIntegrationDomainEvent.cs @@ -2,12 +2,7 @@ namespace MASA.Contrib.DDD.Domain.Tests.Events; public record PaymentFailedIntegrationDomainEvent : IntegrationDomainEvent { - public PaymentFailedIntegrationDomainEvent() - { - Topic = typeof(PaymentFailedIntegrationDomainEvent).Name; - } - public string OrderId { get; set; } - public override string Topic { get; set; } + public override string Topic { get; set; } = nameof(PaymentFailedIntegrationDomainEvent); } diff --git a/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentSucceededDomainEvent.cs b/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentSucceededDomainEvent.cs index cdfe314a9..5b38d08b8 100644 --- a/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentSucceededDomainEvent.cs +++ b/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentSucceededDomainEvent.cs @@ -1,7 +1,6 @@ namespace MASA.Contrib.DDD.Domain.Tests.Events; -public record PaymentSucceededDomainEvent : DomainEvent +public record PaymentSucceededDomainEvent(string OrderId) : DomainEvent { - public string OrderId { get; set; } + public bool Result { get; set; } = false; } - diff --git a/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentSucceededIntegraionDomainEvent.cs b/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentSucceededIntegraionDomainEvent.cs new file mode 100644 index 000000000..44719cdbf --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Tests/Events/PaymentSucceededIntegraionDomainEvent.cs @@ -0,0 +1,6 @@ +namespace MASA.Contrib.DDD.Domain.Tests.Events; + +public record PaymentSucceededIntegraionDomainEvent(string OrderId, decimal Money, DateTime PayTime) : IntegrationDomainEvent +{ + public override string Topic { get; set; } = nameof(PaymentSucceededIntegraionDomainEvent); +} diff --git a/test/MASA.Contrib.DDD.Domain.Tests/Handlers/PaymentSucceededDomainEventHandller.cs b/test/MASA.Contrib.DDD.Domain.Tests/Handlers/PaymentSucceededDomainEventHandller.cs new file mode 100644 index 000000000..2127a6ea6 --- /dev/null +++ b/test/MASA.Contrib.DDD.Domain.Tests/Handlers/PaymentSucceededDomainEventHandller.cs @@ -0,0 +1,19 @@ +namespace MASA.Contrib.DDD.Domain.Tests.Handlers; + +public class PaymentSucceededDomainEventHandller +{ + private readonly ILogger? _logger; + + public PaymentSucceededDomainEventHandller(ILogger? logger = null) + { + _logger = logger; + } + + [EventHandler] + public Task PaymentSucceeded(PaymentSucceededDomainEvent domainEvent) + { + _logger?.LogInformation("PaymentSucceeded: OrderId: {OrderId}", domainEvent.OrderId); + domainEvent.Result = true; + return Task.CompletedTask; + } +} diff --git a/test/MASA.Contrib.DDD.Domain.Tests/MASA.Contrib.DDD.Domain.Tests.csproj b/test/MASA.Contrib.DDD.Domain.Tests/MASA.Contrib.DDD.Domain.Tests.csproj index 7d8fc395f..3753ec12d 100644 --- a/test/MASA.Contrib.DDD.Domain.Tests/MASA.Contrib.DDD.Domain.Tests.csproj +++ b/test/MASA.Contrib.DDD.Domain.Tests/MASA.Contrib.DDD.Domain.Tests.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -8,17 +8,22 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + - + + diff --git a/test/MASA.Contrib.DDD.Domain.Tests/Repositories/UserRepository.cs b/test/MASA.Contrib.DDD.Domain.Tests/Repositories/UserRepository.cs deleted file mode 100644 index 010fb49b6..000000000 --- a/test/MASA.Contrib.DDD.Domain.Tests/Repositories/UserRepository.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace MASA.Contrib.DDD.Domain.Tests.Repositories; - -public class UserRepository : IRepository -{ - public IUnitOfWork UnitOfWork => throw new NotImplementedException(); - - public ValueTask AddAsync(User entity, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public ValueTask FindAsync(params object?[]? keyValues) - { - throw new NotImplementedException(); - } - - public ValueTask FindAsync(object?[]? keyValues, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task FindAsync(Expression> predicate, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task GetCountAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task GetCountAsync(Expression> predicate, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> GetListAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> GetListAsync(Expression> predicate, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> GetPaginatedListAsync(int skip, int take, string? sorting, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> GetPaginatedListAsync(Expression> predicate, int skip, int take, string? sorting, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> GetPaginatedListAsync(PaginatedOptions options, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task> GetPaginatedListAsync(Expression> predicate, PaginatedOptions options, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task RemoveAsync(User entity, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task RemoveAsync(Expression> predicate, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task RemoveRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task UpdateAsync(User entity, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public Task UpdateRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } -} diff --git a/test/MASA.Contrib.DDD.Domain.Tests/Services/UserDomainService.cs b/test/MASA.Contrib.DDD.Domain.Tests/Services/UserDomainService.cs index 61a112e95..52065a304 100644 --- a/test/MASA.Contrib.DDD.Domain.Tests/Services/UserDomainService.cs +++ b/test/MASA.Contrib.DDD.Domain.Tests/Services/UserDomainService.cs @@ -6,11 +6,11 @@ public UserDomainService(IDomainEventBus eventBus) : base(eventBus) { } - public async Task RegisterUserSucceededAsync(string account) + public async Task RegisterUserSucceededAsync(RegisterUserSucceededDomainIntegrationEvent domainIntegrationEvent) { // TODO Simulate a successful message for registered users - await EventBus.PublishAsync(new RegisterUserSucceededDomainIntegrationEvent() { Account = account }); + await EventBus.PublishAsync(domainIntegrationEvent); return "succeed"; } } diff --git a/test/MASA.Contrib.DDD.Domain.Tests/TestBase.cs b/test/MASA.Contrib.DDD.Domain.Tests/TestBase.cs deleted file mode 100644 index cd3f308cc..000000000 --- a/test/MASA.Contrib.DDD.Domain.Tests/TestBase.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace MASA.Contrib.DDD.Domain.Tests; - -public class TestBase -{ - protected const string DAPR_PUBSUB_NAME = "pubsub"; - - protected IServiceProvider CreateDefaultProvider() - { - return CreateProvider((services) => - { - Mock integrationEventBus = new(); - integrationEventBus.Setup(e => e.PublishAsync(It.IsAny())).Verifiable(); - services.AddScoped(typeof(IIntegrationEventBus), serviceProvider => integrationEventBus.Object); - }); - } - - protected IServiceProvider CreateProvider(Action? action = null) - { - var services = new ServiceCollection(); - action?.Invoke(services); - services.AddDomainEventBus(options => - { - options.Assemblies = new System.Reflection.Assembly[1] { typeof(TestBase).Assembly }; - Mock eventBus = new(); - eventBus.Setup(e => e.PublishAsync(It.IsAny())).Verifiable(); - services.AddScoped(typeof(IEventBus), serviceProvider => eventBus.Object); - - Mock unitOfWork = new(); - services.AddScoped(typeof(IUnitOfWork), serviceProvider => unitOfWork.Object); - }); - return services.BuildServiceProvider(); - } -} diff --git a/test/MASA.Contrib.DDD.Domain.Tests/_Imports.cs b/test/MASA.Contrib.DDD.Domain.Tests/_Imports.cs index 3415e4957..e1a0ca7d6 100644 --- a/test/MASA.Contrib.DDD.Domain.Tests/_Imports.cs +++ b/test/MASA.Contrib.DDD.Domain.Tests/_Imports.cs @@ -1,16 +1,16 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.DDD.Domain.Events; global using MASA.BuildingBlocks.DDD.Domain.Repositories; global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents; global using MASA.Contrib.DDD.Domain.Events; global using MASA.Contrib.DDD.Domain.Tests.Events; -global using MASA.Contrib.DDD.Domain.Tests.Repositories; global using MASA.Contrib.DDD.Domain.Tests.Services; -global using MASA.Contribs.DDD.Domain.Entities; +global using MASA.Contribs.DDD.Domain.Entities.Tests; global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; global using Microsoft.VisualStudio.TestTools.UnitTesting; global using Moq; -global using System.Linq.Expressions; +global using System.Reflection; +global using MASA.Contrib.Dispatcher.Events; +global using Microsoft.Extensions.Logging; diff --git a/test/MASA.Contrib.Data.Contracts.EF.Tests/Domain/Entities/Courses.cs b/test/MASA.Contrib.Data.Contracts.EF.Tests/Domain/Entities/Courses.cs new file mode 100644 index 000000000..2b6039096 --- /dev/null +++ b/test/MASA.Contrib.Data.Contracts.EF.Tests/Domain/Entities/Courses.cs @@ -0,0 +1,14 @@ +namespace MASA.Contrib.Data.Contracts.EF.Tests.Domain.Entities; + +public class Courses : AggregateRoot +{ + public Courses() + { + Id = Guid.NewGuid(); + IsDeleted = false; + } + + public string Name { get; set; } + + public bool IsDeleted { get; set; } +} diff --git a/test/MASA.Contrib.Data.Contracts.EF.Tests/Domain/Entities/Students.cs b/test/MASA.Contrib.Data.Contracts.EF.Tests/Domain/Entities/Students.cs new file mode 100644 index 000000000..968c5de7c --- /dev/null +++ b/test/MASA.Contrib.Data.Contracts.EF.Tests/Domain/Entities/Students.cs @@ -0,0 +1,16 @@ +namespace MASA.Contrib.Data.Contracts.EF.Tests.Domain.Entities; + +public class Students : AuditAggregateRoot +{ + public Students() + { + Id = Guid.NewGuid(); + RegisterTime = DateTime.UtcNow; + } + + public string Name { get; set; } + + public int Age { get; set; } + + public DateTime RegisterTime { get; private set; } +} diff --git a/test/MASA.Contrib.Data.Contracts.EF.Tests/Infrastructure/CustomDbContext.cs b/test/MASA.Contrib.Data.Contracts.EF.Tests/Infrastructure/CustomDbContext.cs new file mode 100644 index 000000000..46299964b --- /dev/null +++ b/test/MASA.Contrib.Data.Contracts.EF.Tests/Infrastructure/CustomDbContext.cs @@ -0,0 +1,12 @@ +using MASA.Contrib.Data.Contracts.EF.Tests.Domain.Entities; + +namespace MASA.Contrib.Data.Contracts.EF.Tests.Infrastructure; + +public class CustomDbContext : MasaDbContext +{ + public DbSet Students { get; set; } + + public DbSet Courses { get; set; } + + public CustomDbContext(MasaDbContextOptions options) : base(options) { } +} diff --git a/test/MASA.Contrib.Data.Contracts.EF.Tests/MASA.Contrib.Data.Contracts.EF.Tests.csproj b/test/MASA.Contrib.Data.Contracts.EF.Tests/MASA.Contrib.Data.Contracts.EF.Tests.csproj new file mode 100644 index 000000000..02aa8883f --- /dev/null +++ b/test/MASA.Contrib.Data.Contracts.EF.Tests/MASA.Contrib.Data.Contracts.EF.Tests.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + false + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/test/MASA.Contrib.Data.Contracts.EF.Tests/SoftDeleteTest.cs b/test/MASA.Contrib.Data.Contracts.EF.Tests/SoftDeleteTest.cs new file mode 100644 index 000000000..0a427c46e --- /dev/null +++ b/test/MASA.Contrib.Data.Contracts.EF.Tests/SoftDeleteTest.cs @@ -0,0 +1,117 @@ +using MASA.Contrib.Data.Contracts.EF.Tests.Domain.Entities; +using MASA.Contrib.Data.Contracts.EF.Tests.Infrastructure; + +namespace MASA.Contrib.Data.Contracts.EF.Tests; + +[TestClass] +public class SoftDeleteTest : IDisposable +{ + protected readonly SqliteConnection _connection; + + public SoftDeleteTest() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + } + + public void Dispose() + { + _connection.Close(); + } + + [TestMethod] + public void UseNotUseUoW() + { + var services = new ServiceCollection(); + services.AddMasaDbContext(option => + { + option.UseSqlite(_connection); + Assert.ThrowsException(() => option.UseSoftDelete(services), "Please add UoW first."); + }); + } + + [TestMethod] + public void TestUseSoftDelete() + { + Mock uoW = new(); + uoW.Setup(u => u.Transaction).Verifiable(); + var services = new ServiceCollection(); + services.AddScoped(serviceProvider => uoW.Object); + services.AddMasaDbContext(option => + { + option.UseSqlite(_connection); + option.UseSoftDelete(services); + }); + + var serviceProvider = services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + + dbContext.Set().Add(new Students() + { + Name = "Tom", + Age = 18 + }); + dbContext.SaveChanges(); + Assert.IsTrue(dbContext.Students.Count() == 1); + uoW.Verify(u => u.Transaction, Times.Never); + + var student = dbContext.Students.FirstOrDefault(s => s.Name == "Tom"); + Assert.IsNotNull(student); + dbContext.Set().Remove(student); + dbContext.SaveChanges(); + + Assert.IsTrue(!dbContext.Students.Any()); + + student.IsDeleted = false; + dbContext.SaveChanges(); + Assert.IsTrue(dbContext.Students.Count() == 1); + + uoW = new(); + uoW.Setup(u => u.Transaction).Verifiable(); + uoW.Setup(u => u.UseTransaction).Returns(() => true); + services = new ServiceCollection(); + services.AddScoped(serviceProvider => uoW.Object); + services.AddMasaDbContext(option => + { + option.UseSqlite(_connection); + option.UseSoftDelete(services); + }); + + serviceProvider = services.BuildServiceProvider(); + dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + + dbContext.Set().Add(new Courses() + { + Name = "Chinese" + }); + dbContext.SaveChanges(); + Assert.IsTrue(dbContext.Courses.Count() == 1); + uoW.Verify(u => u.Transaction, Times.Never); + + var course = dbContext.Set().FirstOrDefault(c => c.Name == "Chinese"); + Assert.IsNotNull(course); + dbContext.Set().Remove(course); + dbContext.SaveChanges(); + Assert.IsTrue(!dbContext.Courses.Any()); + + course.IsDeleted = false; + dbContext.SaveChanges(); + Assert.IsTrue(!dbContext.Courses.Any()); + } + + [TestMethod] + public void TestUseMultiSoftDelete() + { + Mock uoW = new(); + uoW.Setup(u => u.Transaction).Verifiable(); + var services = new ServiceCollection(); + services.AddScoped(serviceProvider => uoW.Object); + services.AddMasaDbContext(option => + { + option.UseSqlite(_connection); + option.UseSoftDelete(services).UseSoftDelete(services); + }); + } +} diff --git a/test/MASA.Contrib.Data.Contracts.EF.Tests/_Imports.cs b/test/MASA.Contrib.Data.Contracts.EF.Tests/_Imports.cs new file mode 100644 index 000000000..13d3abe9d --- /dev/null +++ b/test/MASA.Contrib.Data.Contracts.EF.Tests/_Imports.cs @@ -0,0 +1,10 @@ +global using MASA.BuildingBlocks.Data.UoW; +global using MASA.BuildingBlocks.DDD.Domain.Entities; +global using MASA.BuildingBlocks.DDD.Domain.Entities.Auditing; +global using MASA.Contrib.Data.Contracts.EF.Tests.Domain.Entities; +global using MASA.Utils.Data.EntityFrameworkCore; +global using Microsoft.Data.Sqlite; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; diff --git a/test/MASA.Contrib.Data.Uow.EF.Tests/CustomerDbContext.cs b/test/MASA.Contrib.Data.UoW.EF.Tests/CustomerDbContext.cs similarity index 73% rename from test/MASA.Contrib.Data.Uow.EF.Tests/CustomerDbContext.cs rename to test/MASA.Contrib.Data.UoW.EF.Tests/CustomerDbContext.cs index 92d31bad0..ce3b81dda 100644 --- a/test/MASA.Contrib.Data.Uow.EF.Tests/CustomerDbContext.cs +++ b/test/MASA.Contrib.Data.UoW.EF.Tests/CustomerDbContext.cs @@ -1,9 +1,12 @@ -using MASA.Utils.Data.EntityFrameworkCore; - -namespace MASA.Contrib.Data.Uow.EF.Tests; +namespace MASA.Contrib.Data.UoW.EF.Tests; public class CustomerDbContext : MasaDbContext { + public CustomerDbContext() + { + + } + public CustomerDbContext(MasaDbContextOptions options) : base(options) { } public DbSet User { get; set; } @@ -30,7 +33,12 @@ void ConfigureUserEntry(EntityTypeBuilder builder) public class Users { - public Guid Id { get; set; } + public Guid Id { get; private set; } + + public string Name { get; set; } = default!; - public string Name { get; set; } + public Users() + { + this.Id = Guid.NewGuid(); + } } diff --git a/test/MASA.Contrib.Data.Uow.EF.Tests/MASA.Contrib.Data.Uow.EF.Tests.csproj b/test/MASA.Contrib.Data.UoW.EF.Tests/MASA.Contrib.Data.UoW.EF.Tests.csproj similarity index 75% rename from test/MASA.Contrib.Data.Uow.EF.Tests/MASA.Contrib.Data.Uow.EF.Tests.csproj rename to test/MASA.Contrib.Data.UoW.EF.Tests/MASA.Contrib.Data.UoW.EF.Tests.csproj index 2f7ff853c..78360345b 100644 --- a/test/MASA.Contrib.Data.Uow.EF.Tests/MASA.Contrib.Data.Uow.EF.Tests.csproj +++ b/test/MASA.Contrib.Data.UoW.EF.Tests/MASA.Contrib.Data.UoW.EF.Tests.csproj @@ -8,8 +8,12 @@ - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,7 +25,7 @@ - + diff --git a/test/MASA.Contrib.Data.UoW.EF.Tests/TestBase.cs b/test/MASA.Contrib.Data.UoW.EF.Tests/TestBase.cs new file mode 100644 index 000000000..878c8982e --- /dev/null +++ b/test/MASA.Contrib.Data.UoW.EF.Tests/TestBase.cs @@ -0,0 +1,17 @@ +namespace MASA.Contrib.Data.UoW.EF.Tests; + +public class TestBase : IDisposable +{ + protected readonly SqliteConnection _connection; + + protected TestBase() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + } + + public void Dispose() + { + _connection.Close(); + } +} diff --git a/test/MASA.Contrib.Data.UoW.EF.Tests/TestUnitOfWork.cs b/test/MASA.Contrib.Data.UoW.EF.Tests/TestUnitOfWork.cs new file mode 100644 index 000000000..be9a58037 --- /dev/null +++ b/test/MASA.Contrib.Data.UoW.EF.Tests/TestUnitOfWork.cs @@ -0,0 +1,171 @@ +namespace MASA.Contrib.Data.UoW.EF.Tests; + +[TestClass] +public class TestUnitOfWork : TestBase +{ + private Mock _options; + + [TestInitialize] + public void Initialize() + { + _options = new(); + _options.Setup(option => option.Services).Returns(new ServiceCollection()).Verifiable(); + } + + [TestMethod] + public void TestAddUoWAndNullServices() + { + var options = new Mock(); + Assert.ThrowsException(() => options.Object.UseUoW()); + } + + [TestMethod] + public void TestAddUoW() + { + _options.Object.UseUoW(); + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + Assert.ThrowsException(() + => serviceProvider.GetRequiredService() + ); + } + + [TestMethod] + public void TestAddUoWAndUseSqlLite() + { + _options.Object.UseUoW(options => options.UseSqlite(_connection)); + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + Assert.IsNotNull(serviceProvider.GetRequiredService()); + } + + [TestMethod] + public void TestAddMultUoW() + { + _options.Object + .UseUoW(options => options.UseSqlite(_connection)) + .UseUoW(options => options.UseSqlite(_connection)); + + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + Assert.IsTrue(serviceProvider.GetServices().Count() == 1); + } + + [TestMethod] + public void TestTransaction() + { + Mock uoW = new(); + Assert.IsTrue(new Transaction(uoW.Object).UnitOfWork!.Equals(uoW.Object)); + } + + [TestMethod] + public async Task TestSaveChangesAsync() + { + _options.Object.UseUoW(options => options.UseSqlite(_connection)); + Mock customerDbContext = new(); + customerDbContext.Setup(dbContext => dbContext.SaveChangesAsync(default)).Verifiable(); + var uoW = new UnitOfWork(customerDbContext.Object, null); + await uoW.SaveChangesAsync(default); + customerDbContext.Verify(dbContext => dbContext.SaveChangesAsync(default), Times.Once); + } + + [TestMethod] + public async Task TestUseTranscationAsync() + { + _options.Object.UseUoW(options => options.UseSqlite(_connection)); + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + var uoW = serviceProvider.GetRequiredService(); + + var transaction = uoW.Transaction; + Users user = new Users() + { + Name = Guid.NewGuid().ToString() + }; + dbContext.Add(user); + await uoW.SaveChangesAsync(); + await uoW.RollbackAsync(); + + Assert.IsTrue(dbContext.User.ToList().Count() == 0); + } + + [TestMethod] + public async Task TestNotUseTranscationAsync() + { + _options.Object.UseUoW(options => options.UseSqlite(_connection)); + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + var uoW = new UnitOfWork(dbContext, null); + + Users user = new Users() + { + Name = Guid.NewGuid().ToString() + }; + dbContext.Add(user); + await uoW.SaveChangesAsync(); + await Assert.ThrowsExceptionAsync(async () => await uoW.RollbackAsync()); + } + + [TestMethod] + public async Task TestNotTransactionCommitAsync() + { + _options.Object.UseUoW(options => options.UseSqlite(_connection)); + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + var uoW = new UnitOfWork(dbContext, null); + await Assert.ThrowsExceptionAsync(async () => await uoW.CommitAsync()); + } + + [TestMethod] + public async Task TestCommitAsync() + { + _options.Object.UseUoW(options => options.UseSqlite(_connection)); + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + var uoW = new UnitOfWork(dbContext, null); + var user = new Users() + { + Name = "Tom" + }; + var transcation = uoW.Transaction; + dbContext.User.Add(user); + await uoW.SaveChangesAsync(); + await uoW.CommitAsync(); + + Assert.IsTrue(dbContext.User.ToList().Count == 1); + } + + [TestMethod] + public async Task TestOpenRollbackAsync() + { + _options.Object.UseUoW(options => options.UseSqlite(_connection)); + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + var uoW = serviceProvider.GetRequiredService(); + var user = new Users(); + var transcation = uoW.Transaction; + dbContext.User.Add(user); + await uoW.CommitAsync(); + + Assert.IsTrue(!await dbContext.User.AnyAsync()); + } + + [TestMethod] + public async Task TestAddLoggerAndOpenRollbackAsync() + { + _options.Object.Services.AddLogging(); + _options.Object.UseUoW(options => options.UseSqlite(_connection)); + var serviceProvider = _options.Object.Services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Database.EnsureCreated(); + var uoW = serviceProvider.GetRequiredService(); + var user = new Users(); + var transcation = uoW.Transaction; + dbContext.User.Add(user); + await uoW.CommitAsync(); + + Assert.IsTrue(!await dbContext.User.AnyAsync()); + } +} diff --git a/test/MASA.Contrib.Data.Uow.EF.Tests/_Imports.cs b/test/MASA.Contrib.Data.UoW.EF.Tests/_Imports.cs similarity index 72% rename from test/MASA.Contrib.Data.Uow.EF.Tests/_Imports.cs rename to test/MASA.Contrib.Data.UoW.EF.Tests/_Imports.cs index dafc00074..1af16327c 100644 --- a/test/MASA.Contrib.Data.Uow.EF.Tests/_Imports.cs +++ b/test/MASA.Contrib.Data.UoW.EF.Tests/_Imports.cs @@ -1,5 +1,7 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.Dispatcher.Events; +global using MASA.Utils.Data.EntityFrameworkCore; +global using Microsoft.Data.Sqlite; global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.Metadata.Builders; global using Microsoft.Extensions.DependencyInjection; diff --git a/test/MASA.Contrib.Data.Uow.EF.Tests/TestBase.cs b/test/MASA.Contrib.Data.Uow.EF.Tests/TestBase.cs deleted file mode 100644 index 20eb11ab3..000000000 --- a/test/MASA.Contrib.Data.Uow.EF.Tests/TestBase.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Data.Sqlite; - -namespace MASA.Contrib.Data.Uow.EF.Tests -{ - public class TestBase : IDisposable - { - protected readonly SqliteConnection _connection; - - protected TestBase() - { - _connection = new SqliteConnection("DataSource=:memory:"); - _connection.Open(); - } - - public void Dispose() - { - _connection.Close(); - } - - private IServiceProvider CreateDefaultProvider() - { - var options = new Mock(); - options.Setup(option => option.Services).Returns(new ServiceCollection()).Verifiable(); - options.Object.UseUoW(options => options.UseSqlite(_connection)); - return options.Object.Services.BuildServiceProvider(); - } - - protected (IServiceProvider serviceProvider, CustomerDbContext dbContext) CreateDefault() - { - var serviceProvider = CreateDefaultProvider(); - var dbContext = serviceProvider.GetRequiredService(); - dbContext.Database.EnsureCreated(); - return (serviceProvider, dbContext); - } - } -} diff --git a/test/MASA.Contrib.Data.Uow.EF.Tests/TestUnitOfWork.cs b/test/MASA.Contrib.Data.Uow.EF.Tests/TestUnitOfWork.cs deleted file mode 100644 index 2a7956258..000000000 --- a/test/MASA.Contrib.Data.Uow.EF.Tests/TestUnitOfWork.cs +++ /dev/null @@ -1,115 +0,0 @@ -namespace MASA.Contrib.Data.Uow.EF.Tests; - -[TestClass] -public class TestUnitOfWork : TestBase -{ - [TestMethod] - public void TestAddUowAndNullServices() - { - var options = new Mock(); - Assert.ThrowsException(() => options.Object.UseUoW()); - } - - [TestMethod] - public void TestAddUow() - { - var options = new Mock(); - options.Setup(option => option.Services).Returns(new ServiceCollection()).Verifiable(); - options.Object.UseUoW(); - var serviceProvider = options.Object.Services.BuildServiceProvider(); - Assert.ThrowsException(() => serviceProvider.GetRequiredService()); - } - - [TestMethod] - public void TestAddUowAndUseSqlLite() - { - var options = new Mock(); - options.Setup(option => option.Services).Returns(new ServiceCollection()).Verifiable(); - options.Object.UseUoW(options => options.UseSqlite(_connection)); - var serviceProvider = options.Object.Services.BuildServiceProvider(); - Assert.IsNotNull(serviceProvider.GetRequiredService()); - } - - [TestMethod] - public void TestAddMultUow() - { - var options = new Mock(); - options.Setup(option => option.Services).Returns(new ServiceCollection()).Verifiable(); - options.Object.UseUoW(options => options.UseSqlite(_connection)).UseUoW(options => options.UseSqlite(_connection)); - var serviceProvider = options.Object.Services.BuildServiceProvider(); - - Assert.IsTrue(serviceProvider.GetServices().Count() == 1); - } - - [TestMethod] - public async Task TestNoTransactionAndCommitAsync() - { - var serviceProviderAndDbContext = base.CreateDefault(); - var serviceProvider = serviceProviderAndDbContext.serviceProvider; - var dbContext = serviceProviderAndDbContext.dbContext; - - await using (var unitOfWork = serviceProvider.GetRequiredService()) - { - var transcation = unitOfWork.Transaction; - Assert.IsTrue(unitOfWork == serviceProvider.GetRequiredService().UnitOfWork); - - Users user = new Users() - { - Id = Guid.NewGuid(), - Name = Guid.NewGuid().ToString() - }; - dbContext.Add(user); - await unitOfWork.CommitAsync(); - - Assert.IsTrue(dbContext.User.Any(user => user.Id == user.Id)); - } - } - - [TestMethod] - public async Task TestUseTransactionAndCommitAsync() - { - var serviceProviderAndDbContext = base.CreateDefault(); - var serviceProvider = serviceProviderAndDbContext.serviceProvider; - var dbContext = serviceProviderAndDbContext.dbContext; - - using (var transcation = await dbContext.Database.BeginTransactionAsync()) - { - var unitOfWork = serviceProvider.GetRequiredService(); - - Users user = new Users() - { - Id = Guid.NewGuid(), - Name = Guid.NewGuid().ToString() - }; - dbContext.Add(user); - await unitOfWork.CommitAsync(); ; - } - } - - [TestMethod] - public async Task TestNoTransactionAsync() - { - var serviceProviderAndDbContext = base.CreateDefault(); - var serviceProvider = serviceProviderAndDbContext.serviceProvider; - var dbContext = serviceProviderAndDbContext.dbContext; - - await using (var unitOfWork = serviceProvider.GetRequiredService()) - { - Users user = new Users() - { - Id = Guid.NewGuid(), - Name = Guid.NewGuid().ToString().Substring(0, 6) - }; - dbContext.Add(user); - - await unitOfWork.SaveChangesAsync(); - - await Assert.ThrowsExceptionAsync(async () => - { - await unitOfWork.RollbackAsync(); - }); - - Assert.IsTrue(dbContext.User.Any(user => user.Id == user.Id)); - } - } -} diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Benchmarks.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Benchmarks.cs similarity index 89% rename from test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Benchmarks.cs rename to test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Benchmarks.cs index b65e87930..383306d65 100644 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Benchmarks.cs +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Benchmarks.cs @@ -1,4 +1,4 @@ -namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest; +namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests; [SimpleJob(RunStrategy.ColdStart, RuntimeMoniker.Net60, targetCount: 100)] [MinColumn, MaxColumn, MeanColumn, MedianColumn] @@ -16,16 +16,16 @@ public void GlobalSetup() services.AddLogging(loggingBuilder => loggingBuilder.ClearProviders()); services.AddEventBus(); _serviceProvider = services.BuildServiceProvider(); - _eventBus = _serviceProvider.GetService(); + _eventBus = _serviceProvider.GetRequiredService(); _userEvent = new RegisterUserEvent() { Name = "tom", - Mobile = "18888888888" + PhoneNumber = "18888888888" }; _forgetPasswordEvent = new ForgetPasswordEvent() { Name = "lisa", - Mobile = "19999999999" + PhoneNumber = "19999999999" }; } diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/EventHandlers/CouponHandler.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/EventHandlers/CouponHandler.cs similarity index 90% rename from test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/EventHandlers/CouponHandler.cs rename to test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/EventHandlers/CouponHandler.cs index 691667eec..2d22773d0 100644 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/EventHandlers/CouponHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/EventHandlers/CouponHandler.cs @@ -1,8 +1,8 @@ -namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.Extensions.EventHandlers; +namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.EventHandlers; public class CouponHandler { - private readonly ILogger _logger; + private readonly ILogger? _logger; public CouponHandler(IServiceProvider serviceProvider) => _logger = serviceProvider.GetService>(); diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/EventHandlers/NoticeHandler.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/EventHandlers/NoticeHandler.cs similarity index 83% rename from test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/EventHandlers/NoticeHandler.cs rename to test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/EventHandlers/NoticeHandler.cs index ea115ab65..001c4d74d 100644 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/EventHandlers/NoticeHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/EventHandlers/NoticeHandler.cs @@ -1,8 +1,10 @@ -namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.Extensions.EventHandlers; +using MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.Events; + +namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.EventHandlers; public class SendCouponHandler : ISagaEventHandler { - private readonly ILogger _logger; + private readonly ILogger? _logger; public SendCouponHandler(IServiceProvider serviceProvider) => _logger = serviceProvider.GetService>(); @@ -22,7 +24,7 @@ public Task CancelAsync(ForgetPasswordEvent @event) public class NoticeSmsHandler : ISagaEventHandler { - private readonly ILogger _logger; + private readonly ILogger? _logger; public NoticeSmsHandler(IServiceProvider serviceProvider) => _logger = serviceProvider.GetService>(); @@ -42,7 +44,7 @@ public Task CancelAsync(ForgetPasswordEvent @event) public class NoticeEmailHandler : ISagaEventHandler { - private readonly ILogger _logger; + private readonly ILogger? _logger; public NoticeEmailHandler(IServiceProvider serviceProvider) => _logger = serviceProvider.GetService>(); @@ -58,4 +60,4 @@ public Task CancelAsync(ForgetPasswordEvent @event) _logger?.LogInformation("------Cancel Email Notice------"); return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Events/ForgetPasswordEvent.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Events/ForgetPasswordEvent.cs new file mode 100644 index 000000000..dc9e2fbb7 --- /dev/null +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Events/ForgetPasswordEvent.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.Events; + +public record ForgetPasswordEvent : Event +{ + public string Name { get; set; } + + public string PhoneNumber { get; set; } +} diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Events/RegisterUserEvent.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Events/RegisterUserEvent.cs new file mode 100644 index 000000000..30ba9d578 --- /dev/null +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Events/RegisterUserEvent.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.Events; + +public record RegisterUserEvent : Event +{ + public string Name { get; set; } + + public string PhoneNumber { get; set; } +} diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Middleware/LoggingMiddleware.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Middleware/LoggingMiddleware.cs new file mode 100644 index 000000000..fd07fde6c --- /dev/null +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Middleware/LoggingMiddleware.cs @@ -0,0 +1,15 @@ +namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.Middleware; + +public class LoggingMiddleware : IMiddleware where TEvent : notnull, IEvent +{ + private readonly ILogger>? _logger; + public LoggingMiddleware(ILogger>? logger = null) => _logger = logger; + + public async Task HandleAsync(TEvent @event, EventHandlerDelegate next) + { + var eventType = @event.GetType(); + _logger?.LogInformation("----- Handling command {CommandName} ({@Command})", eventType.FullName, @event); + + await next(); + } +} diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.csproj b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.csproj similarity index 85% rename from test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.csproj rename to test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.csproj index a0945b095..0dbc15b6a 100644 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.csproj +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.csproj @@ -2,20 +2,21 @@ Exe - net6.0 + net6.0 AnyCPU false enable false + enable - + - + diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Program.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Program.cs similarity index 83% rename from test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Program.cs rename to test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Program.cs index 0ec3ffd4c..0ec5f1cb3 100644 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Program.cs +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Program.cs @@ -1,4 +1,4 @@ -namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest; +namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests; class Program { diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/_Imports.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/_Imports.cs similarity index 73% rename from test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/_Imports.cs rename to test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/_Imports.cs index 4e7ce08eb..5276b15ee 100644 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/_Imports.cs @@ -5,10 +5,11 @@ global using BenchmarkDotNet.Running; global using BenchmarkDotNet.Validators; global using MASA.BuildingBlocks.Dispatcher.Events; -global using MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.Extensions.EventHandlers; -global using MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.Extensions.Events; +global using MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.EventHandlers; +global using MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.Events; global using MASA.Contrib.Dispatcher.Events.Enums; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; global using System; global using System.Threading.Tasks; + diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Events/ForgetPasswordEvent.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Events/ForgetPasswordEvent.cs deleted file mode 100644 index d14180190..000000000 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Events/ForgetPasswordEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.Extensions.Events; - -public record ForgetPasswordEvent : Event -{ - public string Name { get; set; } - - public string Mobile { get; set; } -} diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Events/RegisterUserEvent.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Events/RegisterUserEvent.cs deleted file mode 100644 index 10adbe699..000000000 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Events/RegisterUserEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.Extensions.Events; - -public record RegisterUserEvent : Event -{ - public string Name { get; set; } - - public string Mobile { get; set; } -} diff --git a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Middleware/LoggingMiddleware.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Middleware/LoggingMiddleware.cs deleted file mode 100644 index 35b8394f6..000000000 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Middleware/LoggingMiddleware.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.Extensions.Middleware; - -public class LoggingMiddleware : IMiddleware where TEvent : notnull, IEvent -{ - private readonly ILogger> _logger; - public LoggingMiddleware(ILogger> logger) => _logger = logger; - - public async Task HandleAsync(TEvent @event, EventHandlerDelegate next) - { - var eventType = @event.GetType(); - _logger.LogInformation("----- Handling command {CommandName} ({@Command})", eventType.FullName, @event); - - await next(); - } -} \ No newline at end of file diff --git a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/EventHandlers/AddGoodsHandler.cs b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/EventHandlers/AddGoodsHandler.cs index 178e4a9ba..200e042d0 100644 --- a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/EventHandlers/AddGoodsHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/EventHandlers/AddGoodsHandler.cs @@ -1,12 +1,10 @@ -using MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests.EventHandlers; public class AddGoodsHandler { [EventHandler] - public void AddGoods(AddGoodsEvent @event, ILogger logger) + public void AddGoods(AddGoodsEvent @event, ILogger? logger) { - logger.LogInformation($"add goods log,GoodsId:{@event.GoodsId},GoodsName:{@event.GoodsName},CategoryId:{@event.CategoryId}"); + logger?.LogInformation($"add goods log,GoodsId:{@event.GoodsId},GoodsName:{@event.GoodsName},CategoryId:{@event.CategoryId}"); } } diff --git a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/_Imports.cs index 15d889922..61d74bb3c 100644 --- a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/_Imports.cs @@ -1 +1,2 @@ +global using MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests.Events; global using Microsoft.Extensions.Logging; diff --git a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests/EventHandlers/AddCatalogHandler.cs b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests/EventHandlers/AddCatalogHandler.cs index 17a1bf269..1681085fe 100644 --- a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests/EventHandlers/AddCatalogHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests/EventHandlers/AddCatalogHandler.cs @@ -1,5 +1,3 @@ -using MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests.EventHandlers; public class AddCatalogHandler diff --git a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests/_Imports.cs index e69de29bb..53869939c 100644 --- a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests/_Imports.cs @@ -0,0 +1 @@ +global using MASA.Contrib.Dispatcher.Events.CheckMethodsParameterType.Tests.Events; diff --git a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsType.Tests/EventHandlers/AddBasketHandler.cs b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsType.Tests/EventHandlers/AddBasketHandler.cs index 557f6d378..58b91c70c 100644 --- a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsType.Tests/EventHandlers/AddBasketHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsType.Tests/EventHandlers/AddBasketHandler.cs @@ -2,13 +2,13 @@ namespace MASA.Contrib.Dispatcher.Events.CheckMethodsType.Tests.EventHandlers; public class AddBasketHandler { - private readonly ILogger _logger; - public AddBasketHandler(ILogger logger) => _logger = logger; + private readonly ILogger? _logger; + public AddBasketHandler(ILogger? logger) => _logger = logger; [EventHandler] public Task AddLog(AddBasketEvent @event) { - _logger.LogInformation($"add basket log:GoogdsId:{@event.GoodsId},count:{@event.Count}"); + _logger?.LogInformation($"add basket log:GoogdsId:{@event.GoodsId},count:{@event.Count}"); return Task.FromResult("success"); } } diff --git a/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/EventHandlers/UserEventHandler.cs b/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/EventHandlers/UserEventHandler.cs index b62ed164a..02ae7f52a 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/EventHandlers/UserEventHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/EventHandlers/UserEventHandler.cs @@ -1,11 +1,9 @@ -using MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests.EventHandlers; public class UserEventHandler { [EventHandler(IsCancel = true)] - public void BindMobile(BindMobileEvent @event) + public void BindPhoneNumber(BindPhoneNumberEvent @event) { } diff --git a/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/Events/BindMobileEvent.cs b/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/Events/BindPhoneNumberEvent.cs similarity index 58% rename from test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/Events/BindMobileEvent.cs rename to test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/Events/BindPhoneNumberEvent.cs index e339a5215..a28c44bb8 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/Events/BindMobileEvent.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/Events/BindPhoneNumberEvent.cs @@ -1,8 +1,8 @@ namespace MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests.Events; -public record BindMobileEvent : Event +public record BindPhoneNumberEvent : Event { public string AccountId { get; set; } - public string Mobile { get; set; } + public string PhoneNumber { get; set; } } diff --git a/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests.csproj b/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests.csproj index 91a682f61..65fe8ba17 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests.csproj +++ b/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests.csproj @@ -10,5 +10,5 @@ - + diff --git a/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/_Imports.cs index e69de29bb..fdc78d0c0 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests/_Imports.cs @@ -0,0 +1 @@ +global using MASA.Contrib.Dispatcher.Events.OnlyCancelHandler.Tests.Events; diff --git a/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/EventHandlers/EditCategoryHandler.cs b/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/EventHandlers/EditCategoryHandler.cs index 37677b5bd..1d0dcc2a3 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/EventHandlers/EditCategoryHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/EventHandlers/EditCategoryHandler.cs @@ -1,23 +1,21 @@ -using MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.EventHandlers; public class EditCategoryHandler : ISagaEventHandler { - private readonly ILogger _logger; - public EditCategoryHandler(ILogger logger) => _logger = logger; + private readonly ILogger? _logger; + public EditCategoryHandler(ILogger? logger = null) => _logger = logger; [EventHandler(10)] public Task CancelAsync(EditCategoryEvent @event) { - _logger.LogInformation($"cancel edit category log,CategoryId:{@event.CategoryId},Name:{@event.CategoryName}"); + _logger?.LogInformation($"cancel edit category log,CategoryId:{@event.CategoryId},Name:{@event.CategoryName}"); return Task.CompletedTask; } [EventHandler(20)] public Task HandleAsync(EditCategoryEvent @event) { - _logger.LogInformation($"edit category log,CategoryId:{@event.CategoryId},Name:{@event.CategoryName}"); + _logger?.LogInformation($"edit category log,CategoryId:{@event.CategoryId},Name:{@event.CategoryName}"); return Task.CompletedTask; } } diff --git a/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.csproj b/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.csproj index 91a682f61..65fe8ba17 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.csproj +++ b/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.csproj @@ -10,5 +10,5 @@ - + diff --git a/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/_Imports.cs index 7c7bad467..cecf0b334 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/_Imports.cs @@ -1,2 +1,3 @@ global using MASA.BuildingBlocks.Dispatcher.Events; +global using MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.Events; global using Microsoft.Extensions.Logging; diff --git a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/EventHandlers/OrderStockConfirmedHandler.cs b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/EventHandlers/OrderStockConfirmedHandler.cs index 31565057f..a97d6302a 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/EventHandlers/OrderStockConfirmedHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/EventHandlers/OrderStockConfirmedHandler.cs @@ -1,16 +1,14 @@ -using MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.EventHandlers; public class OrderStockConfirmedHandler { - private readonly ILogger _logger; + private readonly ILogger? _logger; - public OrderStockConfirmedHandler(ILogger logger) => _logger = logger; + public OrderStockConfirmedHandler(ILogger? logger = null) => _logger = logger; [EventHandler(-10)] public void AddLog(OrderStockConfirmedEvent @event) { - _logger.LogInformation($"add order stock confirmed log,orderId:{@event.OrderId}"); + _logger?.LogInformation($"add order stock confirmed log,orderId:{@event.OrderId}"); } } diff --git a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.csproj b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.csproj index 91a682f61..65fe8ba17 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.csproj +++ b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.csproj @@ -10,5 +10,5 @@ - + diff --git a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/_Imports.cs index 15d889922..abbead4e1 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/_Imports.cs @@ -1 +1,2 @@ +global using MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.Events; global using Microsoft.Extensions.Logging; diff --git a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/EventHandlers/EditGoodsHandler.cs b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/EventHandlers/EditGoodsHandler.cs index e4256e6b8..ca832f1f9 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/EventHandlers/EditGoodsHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/EventHandlers/EditGoodsHandler.cs @@ -1,16 +1,14 @@ -using MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests.EventHandlers; public class EditGoodsHandler : IEventHandler { - private readonly ILogger _logger; - public EditGoodsHandler(ILogger logger) => _logger = logger; + private readonly ILogger? _logger; + public EditGoodsHandler(ILogger? logger) => _logger = logger; [EventHandler(-10)] public Task HandleAsync(EditGoodsEvent @event) { - _logger.LogInformation($"edit goods log,GoodsId:{@event.GoodsId},Name:{@event.GoodsName},CategoryId:{@event.CategoryId}"); + _logger?.LogInformation($"edit goods log,GoodsId:{@event.GoodsId},Name:{@event.GoodsName},CategoryId:{@event.CategoryId}"); return Task.CompletedTask; } } diff --git a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/_Imports.cs index 7c7bad467..2fa58832a 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/_Imports.cs @@ -1,2 +1,3 @@ global using MASA.BuildingBlocks.Dispatcher.Events; +global using MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests.Events; global using Microsoft.Extensions.Logging; diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/AssemblyResolutionTests.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/AssemblyResolutionTests.cs index d0a8c6f1a..c9b51ff42 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/AssemblyResolutionTests.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/AssemblyResolutionTests.cs @@ -33,7 +33,7 @@ public void TestAddNullAssembly() services.AddTransient(typeof(IMiddleware<>), typeof(LoggingMiddleware<>)); Assert.ThrowsException(() => { - services.AddEventBus(options => options.Assemblies = null); + services.AddEventBus(options => options.Assemblies = null!); }); } @@ -57,7 +57,7 @@ public void TestEventBusByAddNullAssembly() services.AddTransient(typeof(IMiddleware<>), typeof(LoggingMiddleware<>)); Assert.ThrowsException(() => { - services.AddTestEventBus(ServiceLifetime.Scoped, options => options.Assemblies = null); + services.AddTestEventBus(ServiceLifetime.Scoped, options => options.Assemblies = null!); }); } @@ -116,7 +116,7 @@ public void TestAddMultEventBus() [TestMethod] public void TestUseEventBusAndNullServices() { - var options = new DispatcherOptions(null); + var options = new DispatcherOptions(null!); Assert.ThrowsException(() => options.UseEventBus()); } } diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/ChoreTest.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/ChoreTest.cs index 6ed94ada0..6a28ce658 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/ChoreTest.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/ChoreTest.cs @@ -1,4 +1,4 @@ -namespace MASA.Contrib.Dispatcher.Events.Tests; +namespace MASA.Contrib.Dispatcher.Events.Tests; [TestClass] public class ChoreTest : TestBase @@ -6,7 +6,7 @@ public class ChoreTest : TestBase private readonly IEventBus _eventBus; public ChoreTest() { - _eventBus = _serviceProvider.GetService(); + _eventBus = _serviceProvider.GetRequiredService(); } [DataTestMethod] @@ -99,4 +99,4 @@ public void TestDispatchHandlerConstructor() Assert.IsTrue(dispatchHandler.RetryTimes.Equals(5)); Assert.IsTrue(dispatchHandler.IsCancel.Equals(true)); } -} \ No newline at end of file +} diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/ChangePasswordEventHandler.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/ChangePasswordEventHandler.cs index 429cd79f7..202fdc8bd 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/ChangePasswordEventHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/ChangePasswordEventHandler.cs @@ -2,8 +2,8 @@ namespace MASA.Contrib.Dispatcher.Events.Tests.EventHandlers; public class ChangePasswordEventHandler : ISagaEventHandler { - private readonly ILogger _logger; - public ChangePasswordEventHandler(ILogger logger) => _logger = logger; + private readonly ILogger? _logger; + public ChangePasswordEventHandler(ILogger? logger=null) => _logger = logger; [EventHandler(10, FailureLevels.ThrowAndCancel)] public Task HandleAsync(ChangePasswordEvent @event) @@ -21,7 +21,7 @@ public Task CancelAsync(ChangePasswordEvent @event) { throw new ArgumentException("System error, please try again later"); } - _logger.LogInformation("cancel success"); + _logger?.LogInformation("cancel success"); return Task.CompletedTask; } @@ -33,7 +33,7 @@ public Task AddCancelLogs(ChangePasswordEvent @event) { throw new ArgumentException("System error, please try again later"); } - _logger.LogInformation("cancel success"); + _logger?.LogInformation("cancel success"); return Task.CompletedTask; } } diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/ShipOrderEventHandler.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/ShipOrderEventHandler.cs index 98bb34842..c3400a461 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/ShipOrderEventHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/ShipOrderEventHandler.cs @@ -4,9 +4,9 @@ public class ShipOrderEventHandler : ISagaEventHandler { private int ExecCount { get; set; } - private readonly ILogger _logger; + private readonly ILogger? _logger; - public ShipOrderEventHandler(ILogger logger) + public ShipOrderEventHandler(ILogger? logger = null) { _logger = logger; ExecCount = 0; @@ -21,7 +21,7 @@ public Task HandleAsync(ShipOrderEvent @event) throw new Exception("try again"); } - _logger.LogInformation("update express information"); + _logger?.LogInformation("update express information"); if (@event.OrderId.Length > 8) { @event.Message = "the delivery failure"; @@ -35,21 +35,21 @@ public Task HandleAsync(ShipOrderEvent @event) public Task CancelAsync(ShipOrderEvent @event) { @event.Message = "the delivery failed, rolling back success"; - _logger.LogInformation("the delivery failed, rolling back success"); + _logger?.LogInformation("the delivery failed, rolling back success"); return Task.CompletedTask; } } public class ShipOrderAndNoticeHandler : IEventHandler { - private readonly ILogger _logger; - public ShipOrderAndNoticeHandler(ILogger logger) => _logger = logger; + private readonly ILogger? _logger; + public ShipOrderAndNoticeHandler(ILogger? logger = null) => _logger = logger; [EventHandler(20)] public Task HandleAsync(ShipOrderEvent @event) { @event.Message = "the delivery and notice success"; - _logger.LogInformation("order delivered successfully"); + _logger?.LogInformation("order delivered successfully"); return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/TransferEventHandler.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/TransferEventHandler.cs index 34ed72a71..0b41eb4b8 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/TransferEventHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/EventHandlers/TransferEventHandler.cs @@ -4,10 +4,10 @@ public class TransferEventHandler : ISagaEventHandler { private readonly List _blackAccount = new List() { "roller", "thomas" }; - private readonly ILogger _logger; + private readonly ILogger? _logger; private readonly IEventBus _eventBus; - public TransferEventHandler(ILogger logger, IEventBus eventBus) + public TransferEventHandler(IEventBus eventBus, ILogger? logger = null) { _logger = logger; _eventBus = eventBus; @@ -20,7 +20,7 @@ public Task HandleAsync(TransferEvent @event) { throw new NotSupportedException("System error, please try again later"); } - _logger.LogInformation("deduct account balance {event}", @event.ToString()); + _logger?.LogInformation("deduct account balance {event}", @event.ToString()); return Task.CompletedTask; } @@ -45,7 +45,7 @@ public async Task DeductionMoneyHandler(DeductionMoneyEvent @event) IncreaseMoneyEvent increaseMoneyEvent = new IncreaseMoneyEvent() { Account = @event.PayeeAccount, - TransferAccount=@event.Account, + TransferAccount = @event.Account, Money = @event.Money }; await _eventBus.PublishAsync(increaseMoneyEvent); diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/Events/DeductionMoneyEvent.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/Events/DeductionMoneyEvent.cs index d36975649..9623b3fc3 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/Events/DeductionMoneyEvent.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/Events/DeductionMoneyEvent.cs @@ -2,7 +2,7 @@ namespace MASA.Contrib.Dispatcher.Events.Tests.Events; public record DeductionMoneyEvent : Event, ITransaction { - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } public string Account { get; set; } diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/Events/IncreaseMoneyEvent.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/Events/IncreaseMoneyEvent.cs index 62ed400e9..ecbe79664 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/Events/IncreaseMoneyEvent.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/Events/IncreaseMoneyEvent.cs @@ -2,7 +2,7 @@ namespace MASA.Contrib.Dispatcher.Events.Tests.Events; public record IncreaseMoneyEvent : Event, ITransaction { - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } public string Account { get; set; } diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/Events/OrderPaymentFailedIntegrationEvent.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/Events/OrderPaymentFailedIntegrationEvent.cs index 814545634..c715ad3ce 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/Events/OrderPaymentFailedIntegrationEvent.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/Events/OrderPaymentFailedIntegrationEvent.cs @@ -8,7 +8,7 @@ public class OrderPaymentFailedIntegrationEvent : IIntegrationEvent public string Topic { get; set; } = nameof(OrderPaymentFailedIntegrationEvent); - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } public string OrderId { get; set; } diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/FeaturesTest.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/FeaturesTest.cs index ea3419e56..0a6824ab8 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/FeaturesTest.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/FeaturesTest.cs @@ -1,5 +1,3 @@ -using Moq; - namespace MASA.Contrib.Dispatcher.Events.Tests; [TestClass] @@ -8,7 +6,7 @@ public class FeaturesTest : TestBase private readonly IEventBus _eventBus; public FeaturesTest() : base() { - _eventBus = _serviceProvider.GetService(); + _eventBus = _serviceProvider.GetRequiredService(); } [TestMethod] @@ -79,8 +77,8 @@ public async Task TestCorrectEventBus() [TestMethod] public async Task TestNullEvent() { - AddShoppingCartEvent @event = null; - await Assert.ThrowsExceptionAsync(async () => await _eventBus.PublishAsync(@event)); + AddShoppingCartEvent? @event = null; + await Assert.ThrowsExceptionAsync(async () => await _eventBus.PublishAsync(@event!)); } [DataTestMethod] @@ -100,10 +98,6 @@ public async Task TestMultiHandler(string price, int count, string discountAmoun [TestMethod] public async Task TestNotParameter() { - var @event = new DeleteGoodsEvent() - { - CreationTime = DateTime.Now, - }; await Assert.ThrowsExceptionAsync(async () => { try @@ -148,7 +142,7 @@ public Task TestOrderLessThenZero() { ResetMemoryEventBus(typeof(FeaturesTest).Assembly); } - catch (Exception ex) + catch (Exception) { } @@ -165,7 +159,7 @@ public Task TestOnlyCancelHandler() { try { - ResetMemoryEventBus(typeof(OnlyCancelHandler.Tests.Events.BindMobileEvent).Assembly); + ResetMemoryEventBus(typeof(OnlyCancelHandler.Tests.Events.BindPhoneNumberEvent).Assembly); } catch (NotSupportedException) { @@ -224,10 +218,10 @@ public async Task TestTransferEventAndOpenTransaction() { base.ResetMemoryEventBus(services => { - var unitOfWork = new Mock(); - unitOfWork.Setup(x => x.TransactionHasBegun).Returns(true); - unitOfWork.Setup(e => e.CommitAsync(CancellationToken.None)).Verifiable(); - services.AddScoped(serviceProvider => unitOfWork.Object); + var uoW = new Mock(); + uoW.Setup(x => x.TransactionHasBegun).Returns(true); + uoW.Setup(e => e.CommitAsync(CancellationToken.None)).Verifiable(); + services.AddScoped(serviceProvider => uoW.Object); return services; }, true, typeof(AssemblyResolutionTests).Assembly); var @event = new DeductionMoneyEvent() @@ -240,13 +234,10 @@ public async Task TestTransferEventAndOpenTransaction() } [TestMethod] - public async Task TestTransferEventAndCloseTransaction() + public async Task TestCommitAsync() { base.ResetMemoryEventBus(services => { - var unitOfWork = new Mock(); - unitOfWork.Setup(e => e.SaveChangesAsync(CancellationToken.None)).Verifiable(); - services.AddScoped(serviceProvider => unitOfWork.Object); return services; }, true, typeof(AssemblyResolutionTests).Assembly); var @event = new DeductionMoneyEvent() @@ -255,19 +246,20 @@ public async Task TestTransferEventAndCloseTransaction() PayeeAccount = "Jim", Money = 100 }; - await _services.BuildServiceProvider().GetRequiredService().PublishAsync(@event); + var serviceProvider = _services.BuildServiceProvider(); + var eventBus = serviceProvider.GetRequiredService(); + + await Assert.ThrowsExceptionAsync(async () => await eventBus.CommitAsync(default)); } [TestMethod] - public async Task TestTransferEventAndOpenTransactionRollback() + public async Task TestUseUoWCommitAsync() { + var uoW = new Mock(); base.ResetMemoryEventBus(services => { - var unitOfWork = new Mock(); - unitOfWork.Setup(x => x.TransactionHasBegun).Returns(true); - unitOfWork.Setup(e => e.CommitAsync(CancellationToken.None)).Throws(new ArgumentOutOfRangeException("The Money is error")); - unitOfWork.Setup(e => e.RollbackAsync(CancellationToken.None)).Verifiable(); - services.AddScoped(serviceProvider => unitOfWork.Object); + uoW.Setup(e => e.CommitAsync(CancellationToken.None)).Verifiable(); + services.AddScoped(serviceProvider => uoW.Object); return services; }, true, typeof(AssemblyResolutionTests).Assembly); var @event = new DeductionMoneyEvent() @@ -276,7 +268,11 @@ public async Task TestTransferEventAndOpenTransactionRollback() PayeeAccount = "Jim", Money = 100 }; + var serviceProvider = _services.BuildServiceProvider(); + var eventBus = serviceProvider.GetRequiredService(); + await eventBus.PublishAsync(@event); - await _services.BuildServiceProvider().GetRequiredService().PublishAsync(@event); + await eventBus.CommitAsync(default); + uoW.Verify(u => u.CommitAsync(default), Times.Once); } } diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/MASA.Contrib.Dispatcher.Events.Tests.csproj b/test/MASA.Contrib.Dispatcher.Events.Tests/MASA.Contrib.Dispatcher.Events.Tests.csproj index 38c8d126a..3470232d9 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/MASA.Contrib.Dispatcher.Events.Tests.csproj +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/MASA.Contrib.Dispatcher.Events.Tests.csproj @@ -3,6 +3,7 @@ net6.0 false + enable Full enable @@ -12,13 +13,17 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/Middleware/LoggingMiddleware.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/Middleware/LoggingMiddleware.cs index 454460825..e3088c8ab 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/Middleware/LoggingMiddleware.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/Middleware/LoggingMiddleware.cs @@ -2,13 +2,13 @@ namespace MASA.Contrib.Dispatcher.Events.Tests.Middleware; public class LoggingMiddleware : IMiddleware where TEvent : notnull, IEvent { - private readonly ILogger> _logger; - public LoggingMiddleware(ILogger> logger) => _logger = logger; + private readonly ILogger>? _logger; + public LoggingMiddleware(ILogger>? logger = null) => _logger = logger; public async Task HandleAsync(TEvent @event, EventHandlerDelegate next) { var eventType = @event.GetType(); - _logger.LogInformation("----- Handling command {FullName} ({event})", eventType.FullName, @event); + _logger?.LogInformation("----- Handling command {FullName} ({event})", eventType.FullName, @event); await next(); } } diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/SagaTest.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/SagaTest.cs index 1880990d3..ce8fee8b2 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/SagaTest.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/SagaTest.cs @@ -6,7 +6,7 @@ public class SagaTest : TestBase private readonly IEventBus _eventBus; public SagaTest() : base() { - _eventBus = _serviceProvider.GetService(); + _eventBus = _serviceProvider.GetRequiredService(); } [DataTestMethod] @@ -30,7 +30,7 @@ public async Task TestExecuteAbnormalExit(string orderId, string orderState, str [DataRow("jordan", "change password notcices @", 0)] public async Task TestLastCancelError(string account, string content, int isError) { - ResetMemoryEventBus(false, null); + ResetMemoryEventBus(false, null!); ChangePasswordEvent @event = new ChangePasswordEvent() { Account = account, @@ -89,7 +89,7 @@ await Assert.ThrowsExceptionAsync(async () => [TestMethod] public async Task TestMultiOrderBySaga() { - IEventBus eventBus = null; + IEventBus? eventBus = null; Assert.ThrowsException(() => { ResetMemoryEventBus(false, typeof(SagaTest).Assembly, typeof(EditCategoryEvent).Assembly); @@ -104,13 +104,13 @@ public async Task TestMultiOrderBySaga() { await eventBus.PublishAsync(@event); } - ResetMemoryEventBus(false, null); + ResetMemoryEventBus(false, null!); } [TestMethod] public async Task TestLessThenZeroBySaga() { - IEventBus eventBus = null; + IEventBus? eventBus = null; Assert.ThrowsException(() => { ResetMemoryEventBus(false, typeof(EditGoodsEvent).Assembly); @@ -126,6 +126,6 @@ public async Task TestLessThenZeroBySaga() { await eventBus.PublishAsync(@event); } - ResetMemoryEventBus(false, null); + ResetMemoryEventBus(false, null!); } } diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/TestBase.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/TestBase.cs index 4d013922c..551056db7 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/TestBase.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/TestBase.cs @@ -12,9 +12,9 @@ public TestBase() : this(null) } - public TestBase(Func func = null) => ResetMemoryEventBus(func, false, null); + public TestBase(Func? func = null) => ResetMemoryEventBus(func, false, null); - protected void ResetMemoryEventBus(Func func = null, bool isAddLog = true, params Assembly[] assemblies) + protected void ResetMemoryEventBus(Func? func = null, bool isAddLog = true, params Assembly[]? assemblies) { _services = new ServiceCollection(); if (isAddLog) diff --git a/test/MASA.Contrib.Dispatcher.Events.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.Events.Tests/_Imports.cs index 506c0cf2d..dda486fce 100644 --- a/test/MASA.Contrib.Dispatcher.Events.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.Events.Tests/_Imports.cs @@ -1,4 +1,4 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents; global using MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests.Events; @@ -16,4 +16,5 @@ global using Microsoft.Extensions.DependencyInjection.Extensions; global using Microsoft.Extensions.Logging; global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; global using System.Reflection; diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/Events/ForgetPasswordEvent.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/Events/ForgetPasswordEvent.cs deleted file mode 100644 index b64b74881..000000000 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/Events/ForgetPasswordEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MASA.Contrib.Dispatcher.IntegrationEvents.Tests.Events; - -public record ForgetPasswordEvent : IntegrationEvent -{ - public override string Topic { get; set; } = nameof(ForgetPasswordEvent); - - public string Account { get; set; } -} diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/Events/RegisterUserIntegrationEvent.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/Events/RegisterUserIntegrationEvent.cs index 9ba051162..d1f551f25 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/Events/RegisterUserIntegrationEvent.cs +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/Events/RegisterUserIntegrationEvent.cs @@ -2,6 +2,16 @@ namespace MASA.Contrib.Dispatcher.IntegrationEvents.Tests.Events; public record RegisterUserIntegrationEvent : IntegrationEvent { + public RegisterUserIntegrationEvent() + { + + } + + public RegisterUserIntegrationEvent(Guid id, DateTime creationTime) : base(id, creationTime) + { + + } + public string Account { get; set; } public string Password { get; set; } diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/IntegrationEventBusTest.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/IntegrationEventBusTest.cs index 1376996a7..9bd6b68d4 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/IntegrationEventBusTest.cs +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/IntegrationEventBusTest.cs @@ -1,199 +1,338 @@ -using MASA.Utils.Models.Config; -using Microsoft.Extensions.Options; - namespace MASA.Contrib.Dispatcher.IntegrationEvents.Tests; [TestClass] -public class IntegrationEventBusTest : TestBase +public class IntegrationEventBusTest { - [TestMethod] - public async Task TestPublishSuccessAsync() + private Mock _options; + private Mock> _dispatcherOptions; + private Mock _daprClient; + private Mock> _logger; + private Mock _eventLog; + private Mock> _appConfig; + private Mock _eventBus; + private Mock _uoW; + + [TestInitialize] + public void Initialize() { - var serviceProvider = CreateDefaultProvider("RegisterUser"); - var eventBus = serviceProvider.GetRequiredService(); - RegisterUserIntegrationEvent @event = new RegisterUserIntegrationEvent() + _options = new(); + _options.Setup(option => option.Services).Returns(new ServiceCollection()).Verifiable(); + _dispatcherOptions = new(); + _dispatcherOptions.Setup(option => option.Value).Returns(() => new DispatcherOptions(_options.Object.Services)); + _daprClient = new(); + _logger = new(); + _eventLog = new(); + _eventLog.Setup(eventLog => eventLog.SaveEventAsync(It.IsAny(), null!)).Verifiable(); + _eventLog.Setup(eventLog => eventLog.MarkEventAsInProgressAsync(It.IsAny())).Verifiable(); + _eventLog.Setup(eventLog => eventLog.MarkEventAsPublishedAsync(It.IsAny())).Verifiable(); + _eventLog.Setup(eventLog => eventLog.MarkEventAsFailedAsync(It.IsAny())).Verifiable(); + _appConfig = new(); + _appConfig.Setup(appConfig => appConfig.CurrentValue).Returns(() => new AppConfig() { - Account = "lisa", - Password = "123456" - }; - await eventBus.PublishAsync(@event); + AppId = "Test" + }); + _eventBus = new(); + _uoW = new(); + _uoW.Setup(uoW => uoW.CommitAsync(default)).Verifiable(); + _uoW.Setup(uoW => uoW.Transaction).Returns(() => null!); } [TestMethod] - public void TestAddMultDaprEventBusAsync() + public void TestDispatcherOption() { - var options = new DispatcherOptions(new ServiceCollection()) + var services = new ServiceCollection(); + DispatcherOptions options; + + Assert.ThrowsException(() => { - Assemblies = AppDomain.CurrentDomain.GetAssemblies() + options = new DispatcherOptions(services) + { + Assemblies = null! + }; + }); + Assert.ThrowsException(() => + { + options = new DispatcherOptions(services) + { + Assemblies = new System.Reflection.Assembly[0] + }; + }); + options = new DispatcherOptions(services) + { + Assemblies = new System.Reflection.Assembly[1] { typeof(IntegrationEventBusTest).Assembly } }; - options.UseDaprEventBus() + Assert.IsTrue(options.Services.Equals(services)); + var allEventTypes = new System.Reflection.Assembly[1] { typeof(IntegrationEventBusTest).Assembly } + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && type != typeof(IntegrationEvent) && typeof(IEvent).IsAssignableFrom(type)).ToList(); + Assert.IsTrue(options.AllEventTypes.Count == allEventTypes.Count()); + + } + + [TestMethod] + public void TestAddMultDaprEventBus() + { + _dispatcherOptions.Object.Value.UseDaprEventBus() .UseDaprEventBus(); - var serviceProvider = options.Services.BuildServiceProvider(); + var serviceProvider = _dispatcherOptions.Object.Value.Services.BuildServiceProvider(); Assert.IsTrue(serviceProvider.GetServices().Count() == 1); } [TestMethod] - public void TestAddDaprEventBusAndNullServicesAsync() + public void TestAddDaprEventBus() { - var options = new DispatcherOptions(null); - var ex = Assert.ThrowsException(() => options.UseDaprEventBus()); - Assert.IsTrue(ex.Message == $"Value cannot be null. (Parameter '{nameof(options.Services)}')"); + IServiceCollection services = new ServiceCollection(); + services.AddDaprEventBus(); + var serviceProvider = services.BuildServiceProvider(); + var integrationEventBus = serviceProvider.GetRequiredService(); + Assert.IsNotNull(integrationEventBus); } [TestMethod] - public void TestAddDaprEventBusAndNullAssemblyAsync() + public void TestEmptyPubSub() { - Assert.ThrowsException(() => new DispatcherOptions(new ServiceCollection()) + IServiceCollection services = new ServiceCollection(); + Assert.ThrowsException(() => { - Assemblies = null + services.AddDaprEventBus(option => + { + option.PubSubName = ""; + }); }); } [TestMethod] - public async Task TestPublishAsync() + public void TestAddDaprEventBusAndChangeAssemblies() { - var services = new ServiceCollection(); - Mock unitWork = new(); - Mock dbTransaction = new(); - unitWork.Setup(u => u.Transaction).Returns(dbTransaction.Object); - services.AddScoped((serviceProvider) => unitWork.Object); - services.AddOptions(); - services.AddLogging(); - services.AddDaprEventBus(options => + IServiceCollection services = new ServiceCollection(); + + services.AddDaprEventBus(option => { + option.Assemblies = AppDomain.CurrentDomain.GetAssemblies(); + option.PubSubName = "pubsub"; }); - RegisterUserIntegrationEvent @event = new RegisterUserIntegrationEvent() - { - Account = "lisa", - Password = "123456" - }; var serviceProvider = services.BuildServiceProvider(); var integrationEventBus = serviceProvider.GetRequiredService(); - await integrationEventBus.PublishAsync(@event); + Assert.IsNotNull(integrationEventBus); + } - Assert.IsTrue(integrationEventBus.GetAllEventTypes().Count() == 3); + [TestMethod] + public void TestAddDaprEventBusAndNullServicesAsync() + { + _options.Setup(option => option.Services).Returns(() => null!); + var ex = Assert.ThrowsException(() => _options.Object.UseDaprEventBus()); + Assert.IsTrue(ex.Message == $"Value cannot be null. (Parameter '{nameof(_options.Object.Services)}')"); } [TestMethod] - public async Task TestPublishFailedAsync() + public async Task TestPublishIntegrationEventAsync() { + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + _eventBus.Object, + _uoW.Object); RegisterUserIntegrationEvent @event = new RegisterUserIntegrationEvent() { Account = "lisa", Password = "123456" }; - var serviceProvider = CreateCustomerDaprPubSubProvider(DAPR_PUBSUB_NAME, (services) => - { - Mock daprClient = new(); - daprClient.Setup(e => e.PublishEventAsync(DAPR_PUBSUB_NAME, @event.Topic, It.IsAny(), default)) - .Throws(new Exception("send failure")); - services.AddSingleton(_ => daprClient.Object); - services.AddLogging(loggingBuilder => loggingBuilder.AddConsole()); - }); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@event); + _daprClient.Setup(client => client.PublishEventAsync(_dispatcherOptions.Object.Value.PubSubName, @event.Topic, @event, default)).Verifiable(); + await integrationEventBus.PublishAsync(@event); + + _daprClient.Verify(dapr => dapr.PublishEventAsync(_dispatcherOptions.Object.Value.PubSubName, @event.Topic, @event, default), Times.Once); } [TestMethod] - public async Task TestDbTransactionPublishSuccessAsync() + public async Task TestPublishIntegrationEventAndFailedAsync() { + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + _eventBus.Object, + _uoW.Object); RegisterUserIntegrationEvent @event = new RegisterUserIntegrationEvent() { Account = "lisa", Password = "123456" }; - var serviceProvider = CreateDefaultProvider(@event.Topic); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@event); + _eventLog.Setup(eventLog => eventLog.MarkEventAsPublishedAsync(It.IsAny())).Throws(); + _daprClient.Setup(client => client.PublishEventAsync(_dispatcherOptions.Object.Value.PubSubName, @event.Topic, @event, default)).Verifiable(); + await integrationEventBus.PublishAsync(@event); + + _eventLog.Verify(eventLog => eventLog.MarkEventAsInProgressAsync(@event.Id), Times.Once); + _daprClient.Verify(client => client.PublishEventAsync(_dispatcherOptions.Object.Value.PubSubName, @event.Topic, @event, default), Times.Once); + _eventLog.Verify(eventLog => eventLog.MarkEventAsPublishedAsync(@event.Id), Times.Once); + _eventLog.Verify(eventLog => eventLog.MarkEventAsFailedAsync(@event.Id), Times.Once); } [TestMethod] - public async Task TestDbTransactionPublishFailedAsync() + public async Task TestPublishIntegrationEventAndNotUoWAsync() { + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + _eventBus.Object, + _uoW.Object); RegisterUserIntegrationEvent @event = new RegisterUserIntegrationEvent() { Account = "lisa", - Password = "123456" + Password = "123456", + UnitOfWork = _uoW.Object }; - var serviceProvider = CreateCustomerDaprPubSubProvider(DAPR_PUBSUB_NAME, (services) => - { - Mock daprClient = new(); - daprClient.Setup(e => e.PublishEventAsync(DAPR_PUBSUB_NAME, @event.Topic, It.IsAny(), default)) - .Throws(new Exception("send failure")); - services.AddSingleton(_ => daprClient.Object); - services.AddLogging(loggingBuilder => loggingBuilder.AddConsole()); - }); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@event); + _daprClient.Setup(client => client.PublishEventAsync(_dispatcherOptions.Object.Value.PubSubName, @event.Topic, @event, default)).Verifiable(); + await integrationEventBus.PublishAsync(@event); + + _daprClient.Verify(dapr => dapr.PublishEventAsync(_dispatcherOptions.Object.Value.PubSubName, @event.Topic, @event, default), Times.Once); } [TestMethod] - public async Task CheckCustomerPubSubName() + public async Task TestPublishEventAsync() { - RegisterUserIntegrationEvent @event = new RegisterUserIntegrationEvent() + _eventBus.Setup(eventBus => eventBus.PublishAsync(It.IsAny())).Verifiable(); + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + _eventBus.Object, + _uoW.Object); + CreateUserEvent @event = new CreateUserEvent() { - Account = "lisa", - Password = "123456" + Name = "Tom" }; - var daprPubSubName = "PUBSUB"; - var serviceProvider = CreateCustomerDaprPubSubProvider(daprPubSubName, @event.Topic); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@event); - } + await integrationEventBus.PublishAsync(@event); + _eventBus.Verify(eventBus => eventBus.PublishAsync(It.IsAny()), Times.Once); + } [TestMethod] - public async Task CheckPublishEvent() + public async Task TestPublishEventAndNotEventBusAsync() { - ForgetPasswordEvent @event = new ForgetPasswordEvent() + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + null, + _uoW.Object); + CreateUserEvent @event = new CreateUserEvent() { - Account = "lisa" + Name = "Tom" }; - var daprPubSubName = "PUBSUB"; - var serviceProvider = CreateCustomerDaprPubSubProvider(daprPubSubName, ""); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@event); + await Assert.ThrowsExceptionAsync(async () => + { + await integrationEventBus.PublishAsync(@event); + }); } [TestMethod] - public async Task TestPublishEventAndNotUseEventBusAsync() + public async Task TestCommitAsync() { - IOptions options = Options.Create(new DispatcherOptions(new ServiceCollection())); - Mock client = new(); - Mock eventLogService = new(); - Mock> appConfig = new(); - Mock> logger = new(); + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + _eventBus.Object, + _uoW.Object); - var integrationEventBus = new IntegrationEventBus(options, client.Object, eventLogService.Object, appConfig.Object, logger.Object); - var @event = new CreateUserEvent("tom"); - await Assert.ThrowsExceptionAsync(async () => await integrationEventBus.PublishAsync(@event)); + await integrationEventBus.CommitAsync(default); + _uoW.Verify(uoW => uoW.CommitAsync(default), Times.Once); } [TestMethod] - public async Task TestPublishEventAsync() + public async Task TestNotUseUowCommitAsync() + { + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + _eventBus.Object, + null); + + await Assert.ThrowsExceptionAsync(async () => await integrationEventBus.CommitAsync()); + } + + [TestMethod] + public void TestGetAllEventTypes() { - IOptions options = Options.Create(new DispatcherOptions(new ServiceCollection()) + _dispatcherOptions.Setup(option => option.Value).Returns(() => new DispatcherOptions(_options.Object.Services) { - Assemblies = AppDomain.CurrentDomain.GetAssemblies() + Assemblies = new System.Reflection.Assembly[1] { typeof(IntegrationEventBusTest).Assembly } }); - Mock client = new(); - Mock eventLogService = new(); - Mock> appConfig = new(); - Mock> logger = new(); - - Mock eventBus = new(); - eventBus.Setup(e => e.PublishAsync(It.IsAny())).Verifiable(); - eventBus.Setup(e => e.GetAllEventTypes()).Returns(() => new List() - { - typeof(CreateUserEvent), - typeof(ForgetPasswordEvent), - typeof(RegisterUserIntegrationEvent) + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + null, + null); + + Assert.IsTrue(integrationEventBus.GetAllEventTypes().Count() == _dispatcherOptions.Object.Value.AllEventTypes.Count()); + } + + + [TestMethod] + public void TestUseEventBusGetAllEventTypes() + { + var defaultAssembly = new System.Reflection.Assembly[1] { typeof(IntegrationEventBusTest).Assembly }; + _dispatcherOptions.Setup(option => option.Value).Returns(() => new DispatcherOptions(_options.Object.Services) + { + Assemblies = defaultAssembly }); + var allEventType = defaultAssembly + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsClass && typeof(IEvent).IsAssignableFrom(type)) + .ToList(); + _eventBus.Setup(eventBus => eventBus.GetAllEventTypes()).Returns(() => allEventType).Verifiable(); + var integrationEventBus = new IntegrationEventBus( + _dispatcherOptions.Object, + _daprClient.Object, + _eventLog.Object, + _appConfig.Object, + _logger.Object, + _eventBus.Object, + null); - var integrationEventBus = new IntegrationEventBus(options, client.Object, eventLogService.Object, appConfig.Object, logger.Object, eventBus.Object); - var @event = new CreateUserEvent("tom"); - await integrationEventBus.PublishAsync(@event); + Assert.IsTrue(integrationEventBus.GetAllEventTypes().Count() == _dispatcherOptions.Object.Value.AllEventTypes.Count()); + Assert.IsTrue(integrationEventBus.GetAllEventTypes().Count() == allEventType.Count()); + } + + [TestMethod] + public void TestIntegrationEvent() + { + DateTime date = DateTime.UtcNow; + Guid id = Guid.NewGuid(); + RegisterUserIntegrationEvent @event = new RegisterUserIntegrationEvent() + { + Account = "lisa", + Password = "123456" + }; + Assert.IsTrue(@event.CreationTime > date); + Assert.IsTrue(@event.Id != default(Guid)); - Assert.IsTrue(integrationEventBus.GetAllEventTypes().Count() == 3); + @event = new RegisterUserIntegrationEvent(id, date) + { + Account = "lisa", + Password = "123456" + }; + Assert.IsTrue(@event.CreationTime == date); + Assert.IsTrue(@event.Id == id); } public class IntegrationEventLogService : IIntegrationEventLogService @@ -203,6 +342,11 @@ public Task MarkEventAsFailedAsync(Guid eventId) return Task.CompletedTask; } + public Task DeleteExpiresAsync(DateTime expiresAt, int batchCount = 1000, CancellationToken token = new CancellationToken()) + { + throw new NotImplementedException(); + } + public Task MarkEventAsInProgressAsync(Guid eventId) { return Task.CompletedTask; @@ -213,9 +357,9 @@ public Task MarkEventAsPublishedAsync(Guid eventId) return Task.CompletedTask; } - public async Task> RetrieveEventLogsPendingToPublishAsync(Guid transactionId) + public Task> RetrieveEventLogsFailedToPublishAsync(int retryBatchSize = 200, int maxRetryTimes = 10, int minimumRetryInterval = 60) { - return await Task.FromResult(new List()); + return Task.FromResult(new List().AsEnumerable()); } public Task SaveEventAsync(IIntegrationEvent @event, DbTransaction transaction) diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests.csproj b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests.csproj index fbaf82443..cedee4cdf 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests.csproj +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests.csproj @@ -9,11 +9,15 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/RegisterServicesBusTest.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/RegisterServicesBusTest.cs deleted file mode 100644 index 8925a032c..000000000 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/RegisterServicesBusTest.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MASA.Contrib.Dispatcher.IntegrationEvents.Tests; - -[TestClass] -public class RegisterServicesBusTest : TestBase -{ - [TestMethod] - public void TestEmptyDaprPubSubName() - { - var daprPubSubName = string.Empty; - Assert.ThrowsException(() => - { - var serviceProvider = CreateCustomerDaprPubSubProvider(daprPubSubName, "topic"); - }); - } -} diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/TestBase.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/TestBase.cs deleted file mode 100644 index 8c6a8dd03..000000000 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/TestBase.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace MASA.Contrib.Dispatcher.IntegrationEvents.Tests; - -[TestClass] -public class TestBase -{ - protected const string DAPR_PUBSUB_NAME = "pubsub"; - - protected IServiceProvider CreateDefaultProvider(string topic, Action? action = null) - { - return CreateProvider(services => - { - action?.Invoke(services); - Mock daprClient = new(); - daprClient.Setup(e => e.PublishEventAsync(DAPR_PUBSUB_NAME, topic, It.IsAny(), default)).Verifiable(); - services.AddSingleton(_ => daprClient.Object); - - services.AddDaprEventBus(); - }); - } - - protected IServiceProvider CreateCustomerDaprPubSubProvider(string daprPubSubName, string topic, Action? action = null) - { - return CreateCustomerDaprPubSubProvider(daprPubSubName, (services) => - { - action?.Invoke(services); - Mock daprClient = new(); - daprClient.Setup(e => e.PublishEventAsync(daprPubSubName, topic, It.IsAny(), default)).Verifiable(); - services.AddSingleton(_ => daprClient.Object); - }); - } - - protected IServiceProvider CreateCustomerDaprPubSubProvider(string daprPubSubName, Action? action = null) - { - return CreateProvider(services => - { - action?.Invoke(services); - services.AddDaprEventBus(options => options.PubSubName = daprPubSubName); - }); - } - - private IServiceProvider CreateProvider(Action? action = null) - { - var services = new ServiceCollection(); - services.AddLogging(loggingBuilder => loggingBuilder.AddConsole()); - - Mock eventBus = new(); - eventBus.Setup(e => e.PublishAsync(It.IsAny())).Verifiable(); - services.AddScoped((serviceProvider) => eventBus.Object); - - Mock unitWork = new(); - Mock dbTransaction = new(); - unitWork.Setup(u => u.Transaction).Returns(dbTransaction.Object); - services.AddScoped((serviceProvider) => unitWork.Object); - - action?.Invoke(services); - return services.BuildServiceProvider(); - } -} - -public class IntegrationEventLogService : IIntegrationEventLogService -{ - public Task MarkEventAsFailedAsync(Guid eventId) - { - return Task.CompletedTask; - } - - public Task MarkEventAsInProgressAsync(Guid eventId) - { - return Task.CompletedTask; - } - - public Task MarkEventAsPublishedAsync(Guid eventId) - { - return Task.CompletedTask; - } - - public Task> RetrieveEventLogsPendingToPublishAsync(Guid transactionId) - { - return Task.FromResult(new List().AsEnumerable()); - } - - public Task SaveEventAsync(IIntegrationEvent @event, DbTransaction transaction) - { - return Task.CompletedTask; - } -} diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/_Imports.cs index 0da27f506..d2b2f3b43 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/_Imports.cs @@ -1,5 +1,5 @@ global using Dapr.Client; -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents.Logs; @@ -7,8 +7,10 @@ global using MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Options; global using MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests.Events; global using MASA.Contrib.Dispatcher.IntegrationEvents.Tests.Events; +global using MASA.Utils.Models.Config; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; global using Microsoft.VisualStudio.TestTools.UnitTesting; global using Moq; global using System.Data.Common; diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/Events/IntegrationEvent.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/Events/IntegrationEvent.cs index 32fc849e8..445a38cec 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/Events/IntegrationEvent.cs +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/Events/IntegrationEvent.cs @@ -7,7 +7,7 @@ public abstract record IntegrationEvent : IIntegrationEvent public DateTime CreationTime { get; init; } [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } public abstract string Topic { get; set; } diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/IntegrationEventLogServiceTest.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/IntegrationEventLogServiceTest.cs index f5d6b64a0..9931bd08a 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/IntegrationEventLogServiceTest.cs +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/IntegrationEventLogServiceTest.cs @@ -1,6 +1,3 @@ -using MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.Infrastructure; -using Moq; - namespace MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests; [TestClass] @@ -13,7 +10,7 @@ public async Task TestNullDbTransactionAsync() var @event = new OrderPaymentSucceededIntegrationEvent() { OrderId = "1234567890123", - PaymentTime = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds + PaymentTime = (long) (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds }; var serviceProvider = CreateDefaultProvider(); var dbContext = serviceProvider.GetRequiredService(); @@ -21,57 +18,6 @@ public async Task TestNullDbTransactionAsync() await Assert.ThrowsExceptionAsync(async () => await eventLogService.SaveEventAsync(@event, transaction)); } - [TestMethod] - public async Task TestEventLogServiceAsync() - { - var serviceProvider = CreateDefaultProvider(); - var dbContext = serviceProvider.GetRequiredService(); - dbContext.Database.EnsureCreated(); - var transaction = dbContext.Database.GetDbConnection().BeginTransaction(); - var @event = new OrderPaymentSucceededIntegrationEvent() - { - OrderId = "1234567890123", - PaymentTime = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds - }; - - var eventLogService = serviceProvider.GetRequiredService(); - await eventLogService.SaveEventAsync(@event, transaction); - - var transactionId = dbContext.Database.CurrentTransaction!.TransactionId; - - var eventLog = dbContext.EventLogs.FirstOrDefault(); - Assert.IsNotNull(eventLog); - Assert.IsTrue(eventLog.State == IntegrationEventStates.NotPublished); - Assert.IsTrue(eventLog.Id == @event.Id); - - var eventLogs = await eventLogService.RetrieveEventLogsPendingToPublishAsync(transactionId); - Assert.IsNotNull(eventLogs.Count() == 1); - eventLog = dbContext.EventLogs.FirstOrDefault(); - Assert.IsNotNull(eventLog); - Assert.IsTrue(eventLog.State == IntegrationEventStates.NotPublished); - Assert.IsTrue(eventLog.Id == @event.Id); - - - await eventLogService.MarkEventAsInProgressAsync(eventLog.Id); - eventLog = dbContext.EventLogs.Where(x => x.Id == eventLog.Id).FirstOrDefault(); - Assert.IsNotNull(eventLog); - Assert.IsTrue(eventLog.State == IntegrationEventStates.InProgress); - Assert.IsTrue(eventLog.TimesSent == 1); - - await eventLogService.MarkEventAsPublishedAsync(eventLog.Id); - eventLog = dbContext.EventLogs.Where(x => x.Id == eventLog.Id).FirstOrDefault(); - Assert.IsNotNull(eventLog); - Assert.IsTrue(eventLog.State == IntegrationEventStates.Published); - - await eventLogService.MarkEventAsFailedAsync(eventLog.Id); - eventLog = dbContext.EventLogs.Where(x => x.Id == eventLog.Id).FirstOrDefault(); - Assert.IsNotNull(eventLog); - Assert.IsTrue(eventLog.State == IntegrationEventStates.PublishedFailed); - - eventLogs = await eventLogService.RetrieveEventLogsPendingToPublishAsync(transactionId); - Assert.IsNotNull(eventLogs.Count() == 0); - } - [TestMethod] public void TestMultUseEventLogService() { @@ -85,30 +31,21 @@ public void TestMultUseEventLogService() [TestMethod] public void TestNullServices() { - var options = new DispatcherOptions(null); - Assert.ThrowsException(() => - { - options.UseEventLog(options => - { - options.UseSqlite(base._connection); - }); - }); + var options = new DispatcherOptions(null!); + Assert.ThrowsException(() => { options.UseEventLog(options => { options.UseSqlite(base._connection); }); }); } [TestMethod] public void TestNullDbContextOptionsBuilder() { var options = new DispatcherOptions(new ServiceCollection()); - Assert.ThrowsException(() => - { - options.UseEventLog(null); - }); + Assert.ThrowsException(() => { options.UseEventLog(null!); }); } [TestMethod] public void TestUseCustomDbContextByNullServices() { - var options = new DispatcherOptions(null); + var options = new DispatcherOptions(null!); Assert.IsNull(options.Services); Assert.ThrowsException(() => options.UseEventLog()); } @@ -124,10 +61,13 @@ public void TestGenericEventLog() public async Task TestCustomDbContextAsync() { var options = new DispatcherOptions(new ServiceCollection()); - options.Services.AddMasaDbContext(options => options.UseSqlite(_connection)); + options.Services.AddMasaDbContext(options => + options.UseSqlite(_connection).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); var integrationEventBus = new Mock(); - integrationEventBus.Setup(e => e.GetAllEventTypes()).Returns(() => AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes()).Where(type => typeof(IIntegrationEvent).IsAssignableFrom(type))); + integrationEventBus.Setup(e => e.GetAllEventTypes()).Returns(() => + AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes()) + .Where(type => typeof(IIntegrationEvent).IsAssignableFrom(type))); options.Services.AddScoped(serviceProvider => integrationEventBus.Object); options.Services.AddScoped(); @@ -140,17 +80,18 @@ public async Task TestCustomDbContextAsync() var @event = new OrderPaymentSucceededIntegrationEvent() { OrderId = "1234567890123", - PaymentTime = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds + PaymentTime = (long) (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds }; var dbContext = serviceProvider.GetRequiredService(); - dbContext.Database.EnsureCreated(); - using (var transaction = dbContext.Database.BeginTransaction()) - { - await eventLogService.SaveEventAsync(@event, Microsoft.EntityFrameworkCore.Storage.DbContextTransactionExtensions.GetDbTransaction(transaction)); - - await eventLogService.RetrieveEventLogsPendingToPublishAsync(transaction.TransactionId); - } + await dbContext.Database.EnsureCreatedAsync(); + // using (var transaction = await dbContext.Database.BeginTransactionAsync()) + // { + // await eventLogService.SaveEventAsync(@event, + // Microsoft.EntityFrameworkCore.Storage.DbContextTransactionExtensions.GetDbTransaction(transaction)); + // + // await eventLogService.RetrieveEventLogsPendingToPublishAsync(transaction.TransactionId); + // } } [TestMethod] @@ -160,7 +101,9 @@ public async Task TestAddMultEventLog() options.Services.AddMasaDbContext(options => options.UseSqlite(_connection)); var integrationEventBus = new Mock(); - integrationEventBus.Setup(e => e.GetAllEventTypes()).Returns(() => AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes()).Where(type => typeof(IIntegrationEvent).IsAssignableFrom(type))); + integrationEventBus.Setup(e => e.GetAllEventTypes()).Returns(() => + AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes()) + .Where(type => typeof(IIntegrationEvent).IsAssignableFrom(type))); options.Services.AddScoped(serviceProvider => integrationEventBus.Object); options.Services.AddScoped(); @@ -173,17 +116,18 @@ public async Task TestAddMultEventLog() var @event = new OrderPaymentSucceededIntegrationEvent() { OrderId = "1234567890123", - PaymentTime = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds + PaymentTime = (long) (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds }; var dbContext = serviceProvider.GetRequiredService(); - dbContext.Database.EnsureCreated(); - using (var transaction = dbContext.Database.BeginTransaction()) - { - await eventLogService.SaveEventAsync(@event, Microsoft.EntityFrameworkCore.Storage.DbContextTransactionExtensions.GetDbTransaction(transaction)); - - await eventLogService.RetrieveEventLogsPendingToPublishAsync(transaction.TransactionId); - } + await dbContext.Database.EnsureCreatedAsync(); + // using (var transaction = dbContext.Database.BeginTransaction()) + // { + // await eventLogService.SaveEventAsync(@event, + // Microsoft.EntityFrameworkCore.Storage.DbContextTransactionExtensions.GetDbTransaction(transaction)); + // + // await eventLogService.RetrieveEventLogsPendingToPublishAsync(transaction.TransactionId); + // } } [TestMethod] diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.csproj b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.csproj index 1eaa4b8dc..dd5292367 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.csproj +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.csproj @@ -9,12 +9,16 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - + diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/TestBase.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/TestBase.cs index 012f5741b..57215634b 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/TestBase.cs +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/TestBase.cs @@ -1,7 +1,3 @@ -using MASA.BuildingBlocks.Dispatcher.Events; -using Microsoft.Data.Sqlite; -using Moq; - namespace MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests; public class TestBase diff --git a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/_Imports.cs b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/_Imports.cs index b268ee00a..df7c1433c 100644 --- a/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/_Imports.cs +++ b/test/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/_Imports.cs @@ -1,11 +1,15 @@ -global using MASA.BuildingBlocks.Data.Uow; +global using MASA.BuildingBlocks.Data.UoW; +global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents.Logs; global using MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.Domain.Entities; global using MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.Events; +global using MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests.Infrastructure; global using MASA.Utils.Data.EntityFrameworkCore; +global using Microsoft.Data.Sqlite; global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; global using System.Data.Common; global using System.Text.Json.Serialization; diff --git a/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/Commands/CreateProductionCommand.cs b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/Commands/CreateProductionCommand.cs new file mode 100644 index 000000000..f4c4a22c3 --- /dev/null +++ b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/Commands/CreateProductionCommand.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.ReadWriteSpliting.CQRS.Tests.Commands; + +public record CreateProductionCommand : Command +{ + public string Name { get; set; } + + public int Count { get; set; } +} diff --git a/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/CqrsTest.cs b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/CqrsTest.cs new file mode 100644 index 000000000..aa936c80e --- /dev/null +++ b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/CqrsTest.cs @@ -0,0 +1,51 @@ +namespace MASA.Contrib.ReadWriteSpliting.CQRS.Tests; + +[TestClass] +public class CQRSTest +{ + private IServiceCollection _services; + private IServiceProvider _serviceProvider; + private IEventBus _eventBus; + + [TestInitialize] + public void Initialize() + { + _services = new ServiceCollection(); + _services.AddEventBus(); + _serviceProvider = _services.BuildServiceProvider(); + _eventBus = _serviceProvider.GetRequiredService(); + } + + + [DataTestMethod] + [DataRow("")] + [DataRow("tom")] + public void TestCommand(string name) + { + var command = new CreateProductionCommand() + { + Name = name, + Count = 0 + }; + _eventBus.PublishAsync(command); + if (string.IsNullOrEmpty(name)) + { + Assert.IsTrue(command.Count == 2); + } + else + { + Assert.IsTrue(command.Count == 1); + } + } + + [TestMethod] + public void TestQuery() + { + var query = new ProductionItemQuery() + { + ProductionId = "1" + }; + _eventBus.PublishAsync(query); + Assert.IsTrue(query.Result == "Apple"); + } +} diff --git a/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/CreateProductionCommandHandler.cs b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/CreateProductionCommandHandler.cs new file mode 100644 index 000000000..b87ada809 --- /dev/null +++ b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/CreateProductionCommandHandler.cs @@ -0,0 +1,24 @@ +namespace MASA.Contrib.ReadWriteSpliting.CQRS.Tests; + +public class CreateProductionCommandHandler : CommandHandler +{ + [EventHandler(1, Dispatcher.Events.Enums.FailureLevels.ThrowAndCancel, false)] + public override Task HandleAsync(CreateProductionCommand @event) + { + @event.Count++; + if (string.IsNullOrEmpty(@event.Name)) + throw new ArgumentNullException(nameof(@event)); + + if (@event.Id == default(Guid) || @event.CreationTime > DateTime.UtcNow) + throw new ArgumentNullException(nameof(@event)); + + return Task.CompletedTask; + } + + [EventHandler(1)] + public override Task CancelAsync(CreateProductionCommand @event) + { + @event.Count++; + return base.CancelAsync(@event); + } +} diff --git a/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/MASA.Contrib.ReadWriteSpliting.CQRS.Tests.csproj b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/MASA.Contrib.ReadWriteSpliting.CQRS.Tests.csproj new file mode 100644 index 000000000..16dccc410 --- /dev/null +++ b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/MASA.Contrib.ReadWriteSpliting.CQRS.Tests.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + false + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/ProductionQueryHandler.cs b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/ProductionQueryHandler.cs new file mode 100644 index 000000000..43deb0445 --- /dev/null +++ b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/ProductionQueryHandler.cs @@ -0,0 +1,18 @@ +namespace MASA.Contrib.ReadWriteSpliting.CQRS.Tests; + +public class ProductionQueryHandler : QueryHandler +{ + public override Task HandleAsync(ProductionItemQuery @event) + { + if (string.IsNullOrEmpty(@event.ProductionId)) + throw new ArgumentNullException(nameof(@event)); + + if (@event.Id == default(Guid) || @event.CreationTime > DateTime.UtcNow) + throw new ArgumentNullException(nameof(@event)); + + if (@event.ProductionId == "1") + @event.Result = "Apple"; + + return Task.CompletedTask; + } +} diff --git a/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/Queries/ProductionItemQuery.cs b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/Queries/ProductionItemQuery.cs new file mode 100644 index 000000000..2560231b3 --- /dev/null +++ b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/Queries/ProductionItemQuery.cs @@ -0,0 +1,8 @@ +namespace MASA.Contrib.ReadWriteSpliting.CQRS.Tests.Queries; + +public record ProductionItemQuery : Query +{ + public override string Result { get; set; } + + public string ProductionId { get; set; } +} diff --git a/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/_Imports.cs b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/_Imports.cs new file mode 100644 index 000000000..e01b69ac4 --- /dev/null +++ b/test/MASA.Contrib.ReadWriteSpliting.CQRS.Tests/_Imports.cs @@ -0,0 +1,8 @@ +global using MASA.BuildingBlocks.Dispatcher.Events; +global using MASA.Contrib.Dispatcher.Events; +global using MASA.Contrib.ReadWriteSpliting.CQRS.Commands; +global using MASA.Contrib.ReadWriteSpliting.CQRS.Queries; +global using MASA.Contrib.ReadWriteSpliting.CQRS.Tests.Commands; +global using MASA.Contrib.ReadWriteSpliting.CQRS.Tests.Queries; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/test/MASA.Contrib.Service.MinimalAPIs.Tests/MASA.Contrib.Service.MinimalAPIs.Tests.csproj b/test/MASA.Contrib.Service.MinimalAPIs.Tests/MASA.Contrib.Service.MinimalAPIs.Tests.csproj new file mode 100644 index 000000000..6d6fbd4ec --- /dev/null +++ b/test/MASA.Contrib.Service.MinimalAPIs.Tests/MASA.Contrib.Service.MinimalAPIs.Tests.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + false + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/MASA.Contrib.Service.MinimalAPIs.Tests/MinimalAPITest.cs b/test/MASA.Contrib.Service.MinimalAPIs.Tests/MinimalAPITest.cs new file mode 100644 index 000000000..dacff249c --- /dev/null +++ b/test/MASA.Contrib.Service.MinimalAPIs.Tests/MinimalAPITest.cs @@ -0,0 +1,48 @@ +namespace MASA.Contrib.Service.MinimalAPIs.Tests; + +[TestClass] +public class MinimalAPITest +{ + private WebApplicationBuilder _builder; + + [TestInitialize] + public void Initialize() + { + _builder = WebApplication.CreateBuilder(); + } + + [TestMethod] + public void TestAddMultiServices() + { + _builder.Services.AddServices(_builder); + _builder.Services.AddServices(_builder); + var servicePrvider = _builder.Services.BuildServiceProvider(); + Assert.IsTrue(servicePrvider.GetServices>().Count() == 1); + } + + [TestMethod] + public void AddService() + { + var app = _builder.AddServices(); + Assert.IsTrue(_builder.Services.Any(service => service.ServiceType == typeof(CustomService) && service.Lifetime == ServiceLifetime.Scoped)); + + var servicePrvider = _builder.Services.BuildServiceProvider(); + var customService = servicePrvider.GetService(); + Assert.IsNotNull(customService); + + Assert.ReferenceEquals(customService.App, app); + + Assert.ReferenceEquals(customService.Services, _builder.Services); + + Assert.IsNotNull(customService.GetRequiredService()); + Assert.IsNotNull(customService.GetService()); + + Assert.IsTrue(customService.Test() == 1); + + var newCustomService = servicePrvider.CreateScope().ServiceProvider.GetService(); + Assert.IsNotNull(newCustomService); + + Assert.IsTrue(newCustomService.Test() == 1); + + } +} diff --git a/test/MASA.Contrib.Service.MinimalAPIs.Tests/Services/CustomService.cs b/test/MASA.Contrib.Service.MinimalAPIs.Tests/Services/CustomService.cs new file mode 100644 index 000000000..b0ce6d588 --- /dev/null +++ b/test/MASA.Contrib.Service.MinimalAPIs.Tests/Services/CustomService.cs @@ -0,0 +1,13 @@ +namespace MASA.Contrib.Service.MinimalAPIs.Tests.Services; + +public class CustomService : ServiceBase +{ + private int _times = 0; + + public CustomService(IServiceCollection services) : base(services) + { + _times++; + } + + public int Test() => _times; +} diff --git a/test/MASA.Contrib.Service.MinimalAPIs.Tests/_Imports.cs b/test/MASA.Contrib.Service.MinimalAPIs.Tests/_Imports.cs new file mode 100644 index 000000000..7315683bd --- /dev/null +++ b/test/MASA.Contrib.Service.MinimalAPIs.Tests/_Imports.cs @@ -0,0 +1,4 @@ +global using MASA.Contrib.Service.MinimalAPIs.Tests.Services; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/test/MASA.Contribs.DDD.Domain.Entities/MASA.Contribs.DDD.Domain.Entities.csproj b/test/MASA.Contribs.DDD.Domain.Entities.Tests/MASA.Contribs.DDD.Domain.Entities.Tests.csproj similarity index 100% rename from test/MASA.Contribs.DDD.Domain.Entities/MASA.Contribs.DDD.Domain.Entities.csproj rename to test/MASA.Contribs.DDD.Domain.Entities.Tests/MASA.Contribs.DDD.Domain.Entities.Tests.csproj diff --git a/test/MASA.Contribs.DDD.Domain.Entities.Tests/Users.cs b/test/MASA.Contribs.DDD.Domain.Entities.Tests/Users.cs new file mode 100644 index 000000000..342e042f8 --- /dev/null +++ b/test/MASA.Contribs.DDD.Domain.Entities.Tests/Users.cs @@ -0,0 +1,7 @@ +namespace MASA.Contribs.DDD.Domain.Entities.Tests; + +public class Users : AggregateRoot +{ + public string Name { get; set; } +} + diff --git a/test/MASA.Contribs.DDD.Domain.Entities.Tests/_Imports.cs b/test/MASA.Contribs.DDD.Domain.Entities.Tests/_Imports.cs new file mode 100644 index 000000000..d6a2a9e7d --- /dev/null +++ b/test/MASA.Contribs.DDD.Domain.Entities.Tests/_Imports.cs @@ -0,0 +1 @@ +global using MASA.BuildingBlocks.DDD.Domain.Entities; diff --git a/test/MASA.Contribs.DDD.Domain.Entities/User.cs b/test/MASA.Contribs.DDD.Domain.Entities/User.cs deleted file mode 100644 index 780cc5262..000000000 --- a/test/MASA.Contribs.DDD.Domain.Entities/User.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MASA.Contribs.DDD.Domain.Entities; - -public class User : AggregateRoot -{ - public string Name { get; set; } -} - diff --git a/test/MASA.Contribs.DDD.Domain.Repository/_Imports.cs b/test/MASA.Contribs.DDD.Domain.Repository/_Imports.cs deleted file mode 100644 index e69de29bb..000000000