diff --git a/.github/workflows/package_push_github.yml b/.github/workflows/package_push_github.yml new file mode 100644 index 000000000..a2b9c37a2 --- /dev/null +++ b/.github/workflows/package_push_github.yml @@ -0,0 +1,39 @@ +name: Package Push Github +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +jobs: + package-push: + name: package build and push + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: setting dotnet version + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + + - 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=0.0.$GITHUB_RUN_ID + + - name: package push + run: | + dotnet nuget push "**/*.symbols.nupkg" --api-key ${{secrets.PACKAGE_NUGET_GITHUB}} --source https://nuget.pkg.github.com/masastack/index.json diff --git a/.github/workflows/package_push_nuget.org.yml b/.github/workflows/package_push_nuget.org.yml new file mode 100644 index 000000000..32f3e3493 --- /dev/null +++ b/.github/workflows/package_push_nuget.org.yml @@ -0,0 +1,50 @@ +name: Package Push Nuget +on: + create: + ref_type: [ tags ] + # push: + # branches: [ develop ] + # pull_request: + # branches: [ develop ] + workflow_dispatch: + +jobs: + packeg-build: + name: packeg build and push + runs-on: ${{matrix.os}} + strategy: + matrix: +# os: [ubuntu-latest, windows-latest, macOS-latest] + os: [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: 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=0.0.$GITHUB_RUN_ID + + - 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/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..7ce1804cd 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,43 @@ 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -110,22 +136,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 +144,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 +296,142 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -329,10 +451,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 +472,25 @@ 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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {40383055-CC50-4600-AD9A-53C14F620D03} diff --git a/README.md b/README.md index ccab56434..5ee9ed804 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,7 +105,7 @@ 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: > @@ -108,26 +114,32 @@ Protocol based on EF implementation,[Usage introduction](http://gitlab-hz.lons > 3. 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..7021d5fc1 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,7 +105,7 @@ 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) > 优势: > @@ -108,27 +114,32 @@ MASA.Contrib > 3. 软删除 ```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 index b3dcc28ac..6873eb959 100644 --- a/nuget.config +++ b/nuget.config @@ -1,13 +1,6 @@ - - - - - - - - - - + + + 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..8f862567c --- /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..530106401 --- /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..84cc092ca --- /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..709cfc5d2 --- /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..20f81b453 --- /dev/null +++ b/src/BasicAbility/MASA.Contrib.BasicAbility.Dcc/MASA.Contrib.BasicAbility.Dcc.csproj @@ -0,0 +1,20 @@ + + + + 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..f83f41adc --- /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), jsonSerializerOption) + ); + } + 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?.Invoke() ?? null; + 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/Configuration/MASA.Contrib.Configuration/LocalMasaConfigurationRepository.cs b/src/Configuration/MASA.Contrib.Configuration/LocalMasaConfigurationRepository.cs new file mode 100644 index 000000000..667d5bfc1 --- /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..c922b16ec --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/MASA.Contrib.Configuration.csproj @@ -0,0 +1,14 @@ + + + + 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..d1e7e2b83 --- /dev/null +++ b/src/Configuration/MASA.Contrib.Configuration/ServiceCollectionExtensions.cs @@ -0,0 +1,140 @@ +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(); + + if (!services.Any(service => service.ImplementationType == typeof(ILoggerFactory))) + services.AddLogging(); + + 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().GetRequiredService()); + 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..0a5db0ecb 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,11 @@ 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..a56c6be61 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Repository.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain.Repository.EF/Repository.cs @@ -12,30 +12,30 @@ 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(); } - set - { - if (_context.Database.CurrentTransaction == null) - { - _context.Database.UseTransaction(value); - } - else - { - throw new NotSupportedException("The transaction is opened"); - } - } } - public override IUnitOfWork UnitOfWork { get; set; } + public override bool UseTransaction + { + get => UnitOfWork.UseTransaction; + set => UnitOfWork.UseTransaction = value; + } + + public override IUnitOfWork UnitOfWork { get; } public override async ValueTask AddAsync(TEntity entity, CancellationToken cancellationToken = default) => (await _context.AddAsync(entity, cancellationToken).AsTask()).Entity; @@ -46,42 +46,78 @@ public override Task AddRangeAsync(IEnumerable entities, CancellationTo public override Task CommitAsync(CancellationToken cancellationToken = default) => UnitOfWork.CommitAsync(cancellationToken); - public override ValueTask DisposeAsync() => ValueTask.CompletedTask; + public override async ValueTask DisposeAsync() => await ValueTask.CompletedTask; + + 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]!); + } + + return _context.Set().IgnoreQueryFilters().GetQueryable(fields).FirstOrDefaultAsync(cancellationToken); + } - public override ValueTask FindAsync(object?[]? keyValues, CancellationToken cancellationToken) - => _context.Set().FindAsync(keyValues, 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); + if (sorting == null) + sorting = new Dictionary(GetKeys(typeof(TEntity)).Select(x => new KeyValuePair(x, 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); + if (sorting == null) + { + sorting = new Dictionary(GetKeys(typeof(TEntity)).Select(x => new KeyValuePair(x, false))); + } + + return _context.Set().Where(predicate).OrderBy(sorting).Skip(skip).Take(take).ToListAsync(cancellationToken); } public override Task RemoveAsync(TEntity entity, CancellationToken cancellationToken = default) @@ -119,4 +155,7 @@ public override Task UpdateRangeAsync(IEnumerable entities, Cancellatio _context.Set().UpdateRange(entities); return Task.CompletedTask; } + + private 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..2950dfb43 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,8 @@ 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; diff --git a/src/DDD/MASA.Contrib.DDD.Domain/DomainEventBus.cs b/src/DDD/MASA.Contrib.DDD.Domain/DomainEventBus.cs index f6e6c0822..556b00400 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/DomainEventBus.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/DomainEventBus.cs @@ -19,18 +19,30 @@ 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 @@ -47,5 +59,8 @@ public async Task PublishQueueAsync() } } + 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..54e6999e5 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainCommand.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainCommand.cs @@ -2,12 +2,14 @@ namespace MASA.Contrib.DDD.Domain.Events; public record DomainCommand : IDomainCommand { + [JsonIgnore] public Guid Id { get; init; } + [JsonIgnore] public DateTime CreationTime { get; init; } [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } public DomainCommand() : this(Guid.NewGuid(), DateTime.UtcNow) { } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainEvent.cs b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainEvent.cs index af5a97433..f87cdedd7 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainEvent.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainEvent.cs @@ -2,12 +2,14 @@ namespace MASA.Contrib.DDD.Domain.Events; public record DomainEvent : IDomainEvent { + [JsonIgnore] public Guid Id { get; init; } + [JsonIgnore] public DateTime CreationTime { get; init; } [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork { get; set; } public DomainEvent() : this(Guid.NewGuid(), DateTime.UtcNow) { } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainQuery.cs b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainQuery.cs index a91c481e4..2e96e40a2 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainQuery.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Events/DomainQuery.cs @@ -3,12 +3,18 @@ namespace MASA.Contrib.DDD.Domain.Events; public abstract record DomainQuery : IDomainQuery where TResult : notnull { + [JsonIgnore] public Guid Id { get; init; } + [JsonIgnore] public DateTime CreationTime { get; init; } [JsonIgnore] - public IUnitOfWork UnitOfWork { get; set; } + public IUnitOfWork? UnitOfWork + { + get => null; + set => throw new NotSupportedException(nameof(UnitOfWork)); + } public abstract TResult Result { get; set; } diff --git a/src/DDD/MASA.Contrib.DDD.Domain/Events/IntegrationDomainEvent.cs b/src/DDD/MASA.Contrib.DDD.Domain/Events/IntegrationDomainEvent.cs index a0f916a10..eff19f144 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/Events/IntegrationDomainEvent.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/Events/IntegrationDomainEvent.cs @@ -2,6 +2,7 @@ namespace MASA.Contrib.DDD.Domain.Events; public abstract record IntegrationDomainEvent : DomainEvent, IIntegrationDomainEvent { + [JsonIgnore] public abstract string Topic { get; set; } public IntegrationDomainEvent() : this(Guid.NewGuid(), DateTime.UtcNow) { } 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..3c7e81c5c 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,9 @@ - - - + + + 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..62791d406 100644 --- a/src/DDD/MASA.Contrib.DDD.Domain/_Imports.cs +++ b/src/DDD/MASA.Contrib.DDD.Domain/_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.Events; global using MASA.BuildingBlocks.DDD.Domain.Repositories; @@ -6,6 +6,7 @@ global using MASA.BuildingBlocks.Dispatcher.Events; global using MASA.BuildingBlocks.Dispatcher.IntegrationEvents; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; global using Microsoft.Extensions.Options; global using System.Collections.Concurrent; global using System.Reflection; 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..b6b9909a2 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,10 @@ - - - - - + + + + + diff --git a/src/Data/MASA.Contrib.Data.Contracts.EF/README.md b/src/Data/MASA.Contrib.Data.Contracts.EF/README.md index 76ba4c345..350d71970 100644 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/README.md +++ b/src/Data/MASA.Contrib.Data.Contracts.EF/README.md @@ -1,20 +1,32 @@ +[中](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 56% 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..e31e3f3f0 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,32 @@ +中 | [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/SoftDelete/TransactionSaveChangesFilter.cs b/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/TransactionSaveChangesFilter.cs index a6c47eb16..f65b7c942 100644 --- a/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/TransactionSaveChangesFilter.cs +++ b/src/Data/MASA.Contrib.Data.Contracts.EF/SoftDelete/TransactionSaveChangesFilter.cs @@ -1,18 +1,18 @@ -namespace MASA.Contrib.Data.Contracts.EF.SoftDelete +namespace MASA.Contrib.Data.Contracts.EF.SoftDelete; + +public class TransactionSaveChangesFilter : ISaveChangesFilter { - public class TransactionSaveChangesFilter : ISaveChangesFilter - { - private readonly IUnitOfWork _unitOfWork; + private readonly IServiceProvider _serviceProvider; - public TransactionSaveChangesFilter(IUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; + public TransactionSaveChangesFilter(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; - public void OnExecuting(ChangeTracker changeTracker) + public void OnExecuting(ChangeTracker changeTracker) + { + changeTracker.DetectChanges(); + var unitOfWork = _serviceProvider.GetRequiredService(); + if (unitOfWork.UseTransaction && changeTracker.Entries().Any(e => e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted) && !unitOfWork.TransactionHasBegun) { - 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 - } + 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 similarity index 69% rename from src/Data/MASA.Contrib.Data.Uow.EF/MASA.Contrib.Data.Uow.EF.csproj rename to src/Data/MASA.Contrib.Data.UoW.EF/MASA.Contrib.Data.UoW.EF.csproj index 9a41bc05d..7924a7213 100644 --- 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 @@ -7,11 +7,11 @@ - - - - - + + + + + - + 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 62% rename from src/Data/MASA.Contrib.Data.Uow.EF/UnitOfWork.cs rename to src/Data/MASA.Contrib.Data.UoW.EF/UnitOfWork.cs index bb619503f..bf03eaf0e 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,13 @@ public DbTransaction Transaction public bool DisableRollbackOnFailure { 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; @@ -36,38 +40,34 @@ public async Task SaveChangesAsync(CancellationToken cancellationToken = default 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 SaveChangesAsync(cancellationToken); await _context.Database.CommitTransactionAsync(cancellationToken); } catch (Exception ex) { if (DisableRollbackOnFailure) { - _logger.LogError(ex, "Failed to commit transaction"); + _logger?.LogError(ex, "Failed to commit transaction"); throw; } + await _context.Database.RollbackTransactionAsync(cancellationToken); - _logger.LogError(ex, "Failed to commit transaction, rolled back"); + _logger?.LogError(ex, "Failed to commit transaction, rolled back"); } } 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() => ValueTask.CompletedTask; } diff --git a/src/Data/MASA.Contrib.Data.Uow.EF/_Imports.cs b/src/Data/MASA.Contrib.Data.UoW.EF/_Imports.cs similarity index 78% rename from src/Data/MASA.Contrib.Data.Uow.EF/_Imports.cs rename to src/Data/MASA.Contrib.Data.UoW.EF/_Imports.cs index 005db3fab..4a660924a 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,4 @@ global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; global using System.Data.Common; +global using System.Text.Json.Serialization; diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Event.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Event.cs index bcab2ea47..c4d6b516d 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Event.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Event.cs @@ -2,8 +2,10 @@ namespace MASA.Contrib.Dispatcher.Events; public record Event : IEvent { + [JsonIgnore] public Guid Id { get; init; } + [JsonIgnore] public DateTime CreationTime { get; init; } public Event() : this(Guid.NewGuid(), DateTime.UtcNow) { } 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/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..b3e0900af 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Dispatch/DispatcherBase.cs @@ -6,11 +6,14 @@ internal class DispatcherBase protected readonly IServiceCollection _services; + protected readonly Assembly[] _assemblies; + private readonly ILogger _logger; - public DispatcherBase(IServiceCollection services, bool forceInit) + public DispatcherBase(IServiceCollection services, Assembly[] assemblies, bool forceInit) { _services = services; + _assemblies = assemblies; var serviceProvider = services.BuildServiceProvider(); if (_sharingRelationNetwork == null || forceInit) { @@ -25,7 +28,7 @@ public async Task PublishEventAsync(IServiceProvider serviceProvider, TE var eventType = typeof(TEvent); if (!_sharingRelationNetwork.RelationNetwork.TryGetValue(eventType, out List? dispatchRelations) || dispatchRelations == null) { - 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"); throw new ArgumentNullException($"The current event is an out-of-process event. You should use IIntegrationEventBus or IDomainEventBus to send it"); 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..272894574 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Middleware/TransactionMiddleware.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/Internal/Middleware/TransactionMiddleware.cs @@ -3,46 +3,36 @@ namespace MASA.Contrib.Dispatcher.Events.Internal.Middleware; public class TransactionMiddleware : IMiddleware where TEvent : notnull, IEvent { - private readonly ILogger> _logger; - - public TransactionMiddleware(ILogger> logger) - { - _logger = logger; - } - public async Task HandleAsync(TEvent @event, EventHandlerDelegate next) { try { await next(); - if (@event is ITransaction transactionEvent) + if (IsUseTransaction(@event, out ITransaction? transaction)) { - if (transactionEvent.UnitOfWork != null) - { - if (transactionEvent.UnitOfWork.TransactionHasBegun) - { - await transactionEvent.UnitOfWork.CommitAsync(); - } - else - { - await transactionEvent.UnitOfWork.SaveChangesAsync(); - } - } + await transaction!.UnitOfWork!.CommitAsync(); } } - catch (Exception ex) + catch (Exception) { - _logger.LogError(ex, nameof(TransactionMiddleware)); - - if (@event is ITransaction transactionEvent && transactionEvent.UnitOfWork != null && transactionEvent.UnitOfWork.TransactionHasBegun && !transactionEvent.UnitOfWork.DisableRollbackOnFailure) - { - await transactionEvent.UnitOfWork.RollbackAsync(); - } - else + if (IsUseTransaction(@event, out ITransaction? transaction)) { - throw; + await transaction!.UnitOfWork!.RollbackAsync(); } + throw; } } + + private bool IsUseTransaction(TEvent @event, out ITransaction? transaction) + { + if (@event is ITransaction transactionEvent && transactionEvent.UnitOfWork != null && transactionEvent.UnitOfWork.TransactionHasBegun) + { + 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..44194dbaf 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,14 @@ - - - - - - - + + + + + + + + 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..2755b474c 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 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 98% 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..1f53ee1e5 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 diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/ServiceCollectionExtensions.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/ServiceCollectionExtensions.cs index 24b55c4e3..b587c5736 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.Events/ServiceCollectionExtensions.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.Events/ServiceCollectionExtensions.cs @@ -12,11 +12,11 @@ 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 +25,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,7 +36,7 @@ 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(); @@ -49,8 +49,8 @@ public static IServiceCollection AddTestEventBus(this IServiceCollection service } 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(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/_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/IntegrationEvent.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEvent.cs index 604def92e..5a50db571 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEvent.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEvent.cs @@ -2,13 +2,16 @@ namespace MASA.Contrib.Dispatcher.IntegrationEvents.Dapr; public abstract record IntegrationEvent : IIntegrationEvent { + [JsonIgnore] public Guid Id { get; init; } + [JsonIgnore] public DateTime CreationTime { get; init; } [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) { } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventBus.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventBus.cs index 7621bd6f3..78dadd4d1 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventBus.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/IntegrationEventBus.cs @@ -31,8 +31,8 @@ public IntegrationEventBus(IOptions options, public IEnumerable GetAllEventTypes() => _eventBus == null ? - dispatcherOptions.GetAllEventTypes() : - dispatcherOptions.GetAllEventTypes().Concat(_eventBus.GetAllEventTypes()).Distinct(); + dispatcherOptions.AllEventTypes : + dispatcherOptions.AllEventTypes.Concat(_eventBus.GetAllEventTypes()).Distinct(); public async Task PublishAsync(TEvent @event) where TEvent : IEvent @@ -54,32 +54,43 @@ 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); - } + await _eventLogService.SaveEventAsync(@event, @event.UnitOfWork!.Transaction); - _logger.LogInformation("----- Publishing integration event: {IntegrationEventId_published} from {AppId} - ({IntegrationEvent})", @event.Id, _appConfig.CurrentValue.AppId, @event); + _logger.LogInformation("----- Publishing integration event: {IntegrationEventId_published} from {AppId} - ({IntegrationEvent})", @event.Id, _appConfig.CurrentValue.AppId, @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.LogInformation("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, @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/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr.csproj index 3fc9a9d3f..87a18256f 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,11 @@ - - - - - - + + + + + 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..2ccadea4f 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Options/DispatcherOptions.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/Options/DispatcherOptions.cs @@ -11,7 +11,7 @@ public string PubSubName { if (string.IsNullOrWhiteSpace(value)) { - throw new ArgumentNullException(_pubSubName); + throw new ArgumentNullException(nameof(_pubSubName)); } _pubSubName = value; } @@ -31,18 +31,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/README.md b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md index b41714267..447920fd8 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.md @@ -1,3 +1,5 @@ +[中](README.zh-CN.md) | EN + ## IntegrationEventBus Example: @@ -5,7 +7,7 @@ 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(); ) }); 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 similarity index 83% rename from src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-cn.md rename to src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-CN.md index ba1a819e2..79d390f00 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-cn.md +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/README.zh-CN.md @@ -1,3 +1,5 @@ +中 | [EN](README.md) + ## IntegrationEventBus 用例: @@ -5,7 +7,7 @@ ```C# 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. 添加IIntegrationEventBus @@ -14,7 +16,7 @@ Install-Package MASA.Contrib.Data.Uow.EF //使用工作单元 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,4 @@ public async Task DomeIntegrationEventHandleAsync(DomeIntegrationEvent @event) { //todo } -``` \ No newline at end of file +``` diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs index 6aed6ff7f..d5ae19493 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs @@ -14,15 +14,17 @@ 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(); @@ -31,6 +33,12 @@ internal static IServiceCollection TryAddDaprEventBus(); services.AddScoped(); + if (!services.Any(service => service.ServiceType == typeof(IUnitOfWork))) + { + var logger = services.BuildServiceProvider().GetRequiredService>(); + logger.LogWarning("UoW is not enabled, local messages will not be integrated"); + } + return services; } diff --git a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/_Imports.cs b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/_Imports.cs index 4a7056617..2bc91ba12 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/_Imports.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.Dapr/_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; 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..605b4aafe 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogService.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/IntegrationEventLogService.cs @@ -32,12 +32,18 @@ public async Task> RetrieveEventLogsPendingToPu 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) @@ -55,15 +61,28 @@ public Task MarkEventAsFailedAsync(Guid eventId) return UpdateEventStatus(eventId, IntegrationEventStates.PublishedFailed); } - private Task UpdateEventStatus(Guid eventId, IntegrationEventStates status) + private async Task UpdateEventStatus(Guid eventId, IntegrationEventStates status) { - var eventLogEntry = _eventLogContext.EventLogs.Single(e => e.Id == eventId); + var eventLogEntry = _eventLogContext.EventLogs.FirstOrDefault(e => e.EventId == eventId); + if (eventLogEntry == null) + throw new ArgumentException(nameof(eventId)); + eventLogEntry.State = status; if (status == IntegrationEventStates.InProgress) eventLogEntry.TimesSent++; _eventLogContext.EventLogs.Update(eventLogEntry); - return _eventLogContext.SaveChangesAsync(); + await _eventLogContext.SaveChangesAsync(); + + 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..0d2b310a2 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,10 @@ - - - - + + + + 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..c83947ac6 100644 --- a/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/_Imports.cs +++ b/src/Dispatcher/MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF/_Imports.cs @@ -5,6 +5,7 @@ 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; @@ -12,4 +13,5 @@ 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..30e214bd3 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Commands/Command.cs +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Commands/Command.cs @@ -2,8 +2,10 @@ namespace MASA.Contrib.ReadWriteSpliting.CQRS.Commands; public record Command : ICommand { + [JsonIgnore] public Guid Id { get; init; } + [JsonIgnore] public DateTime CreationTime { get; init; } 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..946e6e406 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,10 @@ - - - - + + + + - + 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..270217d44 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/Query.cs +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/Queries/Query.cs @@ -3,8 +3,10 @@ namespace MASA.Contrib.ReadWriteSpliting.CQRS.Queries; public abstract record Query : IQuery where TResult : notnull { + [JsonIgnore] public Guid Id { get; init; } + [JsonIgnore] public DateTime CreationTime { get; init; } public abstract TResult Result { get; set; } 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..978c98f77 100644 --- a/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/_Imports.cs +++ b/src/ReadWriteSpliting/CQRS/MASA.Contrib.ReadWriteSpliting.CQRS/_Imports.cs @@ -1,3 +1,4 @@ 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..a0a1a7b4b 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,10 @@ - - - + + + + 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..811652440 --- /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(); + _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 Property() + { + 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 Property() + { + Key = "Id", + Value = Guid.NewGuid().ToString(), + }, + new Property() + { + 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 Property() + { + Key = "Id", + Value = Guid.NewGuid().ToString(), + }, + new Property() + { + 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..75f9073c7 --- /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(); + } + + [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 DccSectionOptions() + { + 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..27164dd0c --- /dev/null +++ b/test/MASA.Contrib.BasicAbility.Dcc.Tests/DccTest.cs @@ -0,0 +1,746 @@ +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(); + _memoryCacheClientFactory = new(); + _memoryCache = new(); + _distributedCacheClient = new(); + _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"); + + Initialize(); + + 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() + { + Mock configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()).Result).Returns(() => ("", ConfigurationTypes.Text)); + _services.AddSingleton(configurationAPIClient.Object); + _masaConfigurationBuilder.Object.UseDcc(_services, () => new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions + { + Servers = new List() + { + new Utils.Caching.Redis.Models.RedisServerOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, option => + { + option.ExpandSections = new List() + { + new DccSectionOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, option => + { + option.ExpandSections = new List() + { + new DccSectionOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, option => + { + option.ExpandSections = new List() + { + new DccSectionOptions() + { + 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 Utils.Caching.Redis.Models.RedisServerOptions() + { + Host = "localhost", + Port = 6379 + } + } + } + }; + }, option => + { + option.AppId = "Test"; + option.ConfigObjects = new List() + { + "Brand" + }; + }, option => + { + option.ExpandSections = new List() + { + new DccSectionOptions() + { + AppId = "Test2", + ConfigObjects=new List() + { + "Settings" + } + }, + new DccSectionOptions() + { + 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"); + Mock configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).Result).Returns(() + => new(brand.Serialize(_jsonSerializerOptions), ConfigurationTypes.Json) + ).Verifiable(); + _services.AddSingleton(configurationAPIClient.Object); + _masaConfigurationBuilder.Object.UseDcc(_services, () => + { + return new DccConfigurationOptions() + { + ManageServiceAddress = "https://github.com", + RedisOptions = new Utils.Caching.Redis.Models.RedisConfigurationOptions() + { + Servers = new List() + { + new Utils.Caching.Redis.Models.RedisServerOptions() + { + 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"); + Mock configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).Result).Returns(() + => new(brand.Serialize(_jsonSerializerOptions), ConfigurationTypes.Json) + ).Callback((string environment, string cluster, string appId, string configObject, Action action) => + { + trigger.Formats = ConfigFormats.Json; + trigger.Content = newBrand.Serialize(_jsonSerializerOptions); + trigger.Action = action; + }); + _services.AddSingleton(configurationAPIClient.Object); + 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); + configurationAPIClient.Verify(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), Times.Once); + trigger.Execute(); + + Initialize(); + + configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).Result).Returns(() + => new(new Dictionary() + { + { "Id",Guid.NewGuid().ToString()}, + { "Name","Masa"} + }.Serialize(_jsonSerializerOptions), ConfigurationTypes.Properties) + ).Verifiable(); + _services.AddSingleton(configurationAPIClient.Object); + 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); + configurationAPIClient.Verify(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), Times.Once); + + Initialize(); + + configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).Result).Returns(() + => new("Test", ConfigurationTypes.Text) + ).Verifiable(); + _services.AddSingleton(configurationAPIClient.Object); + 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); + configurationAPIClient.Verify(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), Times.Once); + + Initialize(); + + configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).Result).Returns(() + => new(null, ConfigurationTypes.Text) + ).Verifiable(); + _services.AddSingleton(configurationAPIClient.Object); + 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); + configurationAPIClient.Verify(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), Times.Once); + + + Initialize(); + + configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).Result).Returns(() + => new("Test", (ConfigurationTypes)4) + ).Verifiable(); + _services.AddSingleton(configurationAPIClient.Object); + 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), "configurationType"); + configurationAPIClient.Verify(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), Times.Once); + } + + [TestMethod] + public void TestUseDccAndExpandSections() + { + var brand = new Brands("Microsoft"); + Mock configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync("Test", "Default", "DccTest", "Test1", It.IsAny>()).Result).Returns(() + => new(brand.Serialize(_jsonSerializerOptions), ConfigurationTypes.Json) + ).Verifiable(); + configurationAPIClient.Setup(client => client.GetRawAsync("Test2", "Default", "DccTest2", "Test3", It.IsAny>()).Result).Returns(() + => new(brand.Serialize(_jsonSerializerOptions), ConfigurationTypes.Json) + ).Verifiable(); + _services.AddSingleton(configurationAPIClient.Object); + 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); + configurationAPIClient.Verify(client => client.GetRawAsync("Test", "Default", "DccTest", "Test1", It.IsAny>()), Times.Once); + configurationAPIClient.Verify(client => client.GetRawAsync("Test2", "Default", "DccTest2", "Test3", It.IsAny>()), Times.Once); + configurationAPIClient.Verify(client => client.GetRawAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Exactly(2)); + } + + [DataTestMethod] + [DataRow("Development", "Default", "WebApplication1", "Brand")] + public void TestUseMultiDcc(string environment, string cluster, string appId, string configObject) + { + var brand = new Brands("Microsoft"); + Mock configurationAPIClient = new(); + configurationAPIClient.Setup(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()).Result).Returns(() + => new(brand.Serialize(_jsonSerializerOptions), ConfigurationTypes.Json) + ).Verifiable(); + _services.AddSingleton(configurationAPIClient.Object); + 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); + configurationAPIClient.Verify(client => client.GetRawAsync(environment, cluster, appId, configObject, It.IsAny>()), Times.Once); + + 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..cdc56df5a 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 = 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 = 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 = 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..cad4e8dd1 100644 --- a/test/MASA.Contrib.DDD.Domain.Tests/DomainEventBusTest.cs +++ b/test/MASA.Contrib.DDD.Domain.Tests/DomainEventBusTest.cs @@ -1,95 +1,130 @@ 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() + var domainEventBus = new DomainEventBus(_eventBus.Object, _integrationEventBus.Object, _uoW.Object, _dispatcherOptions); + _eventBus.Setup(eventBus => eventBus.PublishAsync(It.IsAny())).Verifiable(); + + var domainEvent = new PaymentSucceededDomainEvent() { OrderId = new Random().Next(10000, 1000000).ToString() }; - var serviceProvider = CreateDefaultProvider(); - var eventBus = serviceProvider.GetRequiredService(); - await eventBus.PublishAsync(@event); + 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 +135,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 +158,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 +168,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 +181,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 => + var domainEvent = new PaymentSucceededDomainEvent() { OrderId = "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 == 0) - { - result = 3; - } - else - { - result = 4; - } - await Task.FromResult(result); - }); - Mock integrationEventBus = new(); - integrationEventBus - .Setup(e => e.PublishAsync(It.IsAny())) - .Callback(async cmd => - { - 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/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/MASA.Contrib.DDD.Domain.Tests.csproj b/test/MASA.Contrib.DDD.Domain.Tests/MASA.Contrib.DDD.Domain.Tests.csproj index 7d8fc395f..811911766 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,21 @@ + + 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..6d9c61865 100644 --- a/test/MASA.Contrib.DDD.Domain.Tests/_Imports.cs +++ b/test/MASA.Contrib.DDD.Domain.Tests/_Imports.cs @@ -1,16 +1,15 @@ -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; + 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..d3376dffd --- /dev/null +++ b/test/MASA.Contrib.Data.Contracts.EF.Tests/MASA.Contrib.Data.Contracts.EF.Tests.csproj @@ -0,0 +1,29 @@ + + + + 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..39f797c4d --- /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.Where(s => s.Name == "Tom").FirstOrDefault(); + Assert.IsNotNull(student); + dbContext.Set().Remove(student); + dbContext.SaveChanges(); + + Assert.IsTrue(dbContext.Students.Count() == 0); + + 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.Once); + + var course = dbContext.Set().FirstOrDefault(c => c.Name == "Chinese"); + Assert.IsNotNull(course); + dbContext.Set().Remove(course); + dbContext.SaveChanges(); + Assert.IsTrue(dbContext.Courses.Count() == 0); + + course.IsDeleted = false; + dbContext.SaveChanges(); + Assert.IsTrue(dbContext.Courses.Count() == 0); + } + + [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..cf5fe487b --- /dev/null +++ b/test/MASA.Contrib.Data.UoW.EF.Tests/TestUnitOfWork.cs @@ -0,0 +1,200 @@ +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 TestCloseRollbackAsync() + { + _options.Object.UseUoW(options => options.UseSqlite(_connection), true); + 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 Assert.ThrowsExceptionAsync(async () => await uoW.CommitAsync()); + } + + [TestMethod] + public async Task TestAddLoggerAndCloseRollbackAsync() + { + _options.Object.Services.AddLogging(); + _options.Object.UseUoW(options => options.UseSqlite(_connection), true); + 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 Assert.ThrowsExceptionAsync(async () => await uoW.CommitAsync()); + } + + [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 92% rename from test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Benchmarks.cs rename to test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Benchmarks.cs index b65e87930..92194fcaa 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] @@ -20,12 +20,12 @@ public void GlobalSetup() _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 89% 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..f02d16684 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,4 +1,6 @@ -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 CouponHandler { 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 91% 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..056ac0809 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,4 +1,6 @@ -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 { 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.BenchmarkDotnetTest/Extensions/Middleware/LoggingMiddleware.cs b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Middleware/LoggingMiddleware.cs similarity index 85% rename from test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Middleware/LoggingMiddleware.cs rename to test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Middleware/LoggingMiddleware.cs index 35b8394f6..f2e28eac5 100644 --- a/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest/Extensions/Middleware/LoggingMiddleware.cs +++ b/test/MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests/Extensions/Middleware/LoggingMiddleware.cs @@ -1,4 +1,4 @@ -namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnetTest.Extensions.Middleware; +namespace MASA.Contrib.Dispatcher.Events.BenchmarkDotnet.Tests.Extensions.Middleware; public class LoggingMiddleware : IMiddleware where TEvent : notnull, IEvent { 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 87% 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..3b730f7ce 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,7 +2,7 @@ Exe - net6.0 + net6.0 AnyCPU false enable @@ -11,11 +11,11 @@ - + - + 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.CheckMethodsParameter.Tests/EventHandlers/AddGoodsHandler.cs b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/EventHandlers/AddGoodsHandler.cs index 178e4a9ba..d4be77a2d 100644 --- a/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/EventHandlers/AddGoodsHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests/EventHandlers/AddGoodsHandler.cs @@ -1,5 +1,3 @@ -using MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.CheckMethodsParameter.Tests.EventHandlers; public class AddGoodsHandler 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.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..4c926fc98 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/EventHandlers/EditCategoryHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests/EventHandlers/EditCategoryHandler.cs @@ -1,5 +1,3 @@ -using MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.OrderEqualBySaga.Tests.EventHandlers; public class EditCategoryHandler : ISagaEventHandler 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..ee9550afe 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/EventHandlers/OrderStockConfirmedHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests/EventHandlers/OrderStockConfirmedHandler.cs @@ -1,5 +1,3 @@ -using MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.OrderLessThanZeroByFeature.Tests.EventHandlers; public class OrderStockConfirmedHandler 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..43e09b08d 100644 --- a/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/EventHandlers/EditGoodsHandler.cs +++ b/test/MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests/EventHandlers/EditGoodsHandler.cs @@ -1,5 +1,3 @@ -using MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests.Events; - namespace MASA.Contrib.Dispatcher.Events.OrderLessThanZeroBySaga.Tests.EventHandlers; public class EditGoodsHandler : IEventHandler 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/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..4a5cc269c 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] @@ -148,7 +146,7 @@ public Task TestOrderLessThenZero() { ResetMemoryEventBus(typeof(FeaturesTest).Assembly); } - catch (Exception ex) + catch (Exception) { } @@ -165,7 +163,7 @@ public Task TestOnlyCancelHandler() { try { - ResetMemoryEventBus(typeof(OnlyCancelHandler.Tests.Events.BindMobileEvent).Assembly); + ResetMemoryEventBus(typeof(OnlyCancelHandler.Tests.Events.BindPhoneNumberEvent).Assembly); } catch (NotSupportedException) { @@ -224,10 +222,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 +238,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 +250,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 +272,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/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..d8d2fd127 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()); + } - Assert.IsTrue(integrationEventBus.GetAllEventTypes().Count() == 3); + [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)); + + @event = new RegisterUserIntegrationEvent(id, date) + { + Account = "lisa", + Password = "123456" + }; + Assert.IsTrue(@event.CreationTime == date); + Assert.IsTrue(@event.Id == id); } public class IntegrationEventLogService : IIntegrationEventLogService 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..fd80f19b6 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] @@ -42,28 +39,34 @@ public async Task TestEventLogServiceAsync() var eventLog = dbContext.EventLogs.FirstOrDefault(); Assert.IsNotNull(eventLog); Assert.IsTrue(eventLog.State == IntegrationEventStates.NotPublished); - Assert.IsTrue(eventLog.Id == @event.Id); + Assert.IsTrue(eventLog.EventId == @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); + Assert.IsTrue(eventLog.EventId == @event.Id); + + + await Assert.ThrowsExceptionAsync(async () => + { + await eventLogService.MarkEventAsInProgressAsync(eventLog.Id); + }); + await eventLogService.MarkEventAsInProgressAsync(eventLog.EventId); - 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); + await eventLogService.MarkEventAsPublishedAsync(eventLog.EventId); eventLog = dbContext.EventLogs.Where(x => x.Id == eventLog.Id).FirstOrDefault(); Assert.IsNotNull(eventLog); Assert.IsTrue(eventLog.State == IntegrationEventStates.Published); - await eventLogService.MarkEventAsFailedAsync(eventLog.Id); + await eventLogService.MarkEventAsFailedAsync(eventLog.EventId); eventLog = dbContext.EventLogs.Where(x => x.Id == eventLog.Id).FirstOrDefault(); Assert.IsNotNull(eventLog); Assert.IsTrue(eventLog.State == IntegrationEventStates.PublishedFailed); @@ -85,7 +88,7 @@ public void TestMultUseEventLogService() [TestMethod] public void TestNullServices() { - var options = new DispatcherOptions(null); + var options = new DispatcherOptions(null!); Assert.ThrowsException(() => { options.UseEventLog(options => @@ -101,14 +104,14 @@ public void TestNullDbContextOptionsBuilder() var options = new DispatcherOptions(new ServiceCollection()); Assert.ThrowsException(() => { - options.UseEventLog(null); + 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,7 +127,7 @@ 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))); 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..43f185bed --- /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