diff --git a/README.md b/README.md index ad3de086b..faf758f96 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**ALPHA state** - VS 2017 and CLI environments compatible) +# eShopOnContainers - Microservices Architecture and Containers based Reference Application (**BETA state** - Visual Studio 2017 and CLI environments compatible) Sample .NET Core reference application, powered by Microsoft, based on a simplified microservices architecture and Docker containers.

> ### DISCLAIMER -> **IMPORTANT:** The current state of this sample application is **ALPHA**, consider it version a 0.1 foundational version, therefore, many areas could be improved and change significantly while refactoring current code and implementing new features. **Feedback with improvements and pull requests from the community will be highly appreciated and accepted.** +> **IMPORTANT:** The current state of this sample application is **BETA**, consider it version a 0.1 foundational version, therefore, many areas could be improved and change significantly while refactoring current code and implementing new features. **Feedback with improvements and pull requests from the community will be highly appreciated and accepted.** > -> This reference application proposes a simplified microservice oriented architecture implementation to introduce technologies like .NET Core with Docker containers through a comprehensive application. However, this reference application it is not trying to solve all the problems in a large and mission-critical distributed system, it is just a bootstrap for developers to easily get started in the world of Docker containers and microservices with .NET Core. +> This reference application proposes a simplified microservice oriented architecture implementation to introduce technologies like .NET Core with Docker containers through a comprehensive application. The chosen domain is an eShop/eCommerce but simply because it is a well-know domain by most people/developers. +However, this sample application should not be considered as an "eCommerce reference model", at all. The implemented business domain might not be ideal from an eCommerce business point of view. It is neither trying to solve all the problems in a large, scalable and mission-critical distributed system. It is just a bootstrap for developers to easily get started in the world of Docker containers and microservices with .NET Core. >

For example, the next step (still not covered in eShopOnContainers) after understanding Docker containers and microservices development with .NET Core, is to select a microservice cluster/orchestrator like Docker Swarm, Kubernetes or DC/OS (in Azure Container Service) or Azure Service Fabric which in most of the cases will require additional partial changes to your application's configuration (although the present architecture should work on most orchestrators with small changes). -> Or moving your databases to HA cloud services, or implementing your EventBus with Azure Service Bus or any other production ready Service Bus in the market. +> Additional steps would be to move your databases to HA cloud services, or to implement your EventBus with Azure Service Bus or any other production ready Service Bus in the market. >

In the future we might fork this project and make multiple versions targeting specific microservice cluster/orchestrators plus using additional cloud infrastructure.

> > Read the planned Roadmap and Milestones for future releases of eShopOnContainers within the Wiki for further info about possible new implementations and provide feedback at the ISSUES section if you'd like to see any specific scenario implemented or improved. Also, feel free to discuss on any current issue. @@ -32,7 +33,7 @@ Additional miroservice styles with other frameworks and No-SQL databases will be >

However, in a real production environment it is recommended to have your databases (SQL Server and Redis, in this case) in HA (High Available) services like Azure SQL Database, Redis as a service or any other clustering system. If you want to change to a production configuration, you'll just need to change the connection strings once you have set up the servers in a HA cloud or on-premises. ## Related documentation and guidance -While developing this reference application, we are creating a reference Guide/eBook named "Architecting and Developing Containerized and Microservice based .NET Applications" which explains in detail how to develop this kind of architectural style (microservices, Docker containers, Domain-Driven Design for certain microservices) plus other simpler architectural styles, like monolithic apps that can also live as Docker containers. +While developing this reference application, we've been creating a reference Guide/eBook focusing on architecting and developing containerized and microservice based .NET Applications (download link available below) which explains in detail how to develop this kind of architectural style (microservices, Docker containers, Domain-Driven Design for certain microservices) plus other simpler architectural styles, like monolithic apps that can also live as Docker containers.

There are also additional eBooks focusing on Containers/Docker lifecycle (DevOps, CI/CD, etc.) with Microsoft Tools, already published plus an additional eBook focusing on Enterprise Apps Patterns with Xamarin.Forms. You can download them and start reviewing these Guides/eBooks here: @@ -41,9 +42,9 @@ You can download them and start reviewing these Guides/eBooks here: | Architecting & Developing | Containers Lifecycle & CI/CD | App patterns with Xamarin.Forms | | ------------ | ------------| ------------| | | | | -| **Download** (Early DRAFT, still work in progress) | **Download** (First Edition from late 2016) | **Download** (Early DRAFT, still work in progress) | +| **Download** (Early DRAFT, still work in progress) | **Download** (First Edition from late 2016) | **Download** (Preview Edition) | -Send feedback to [cesardl@microsoft.com](cesardl@microsoft.com) +Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com)

However, we encourage to download and review the "Architecting & Developing eBook" because the architectural styles and architectural patterns and technologies explained in the guidance are using this reference application when explaining many pattern implementations, so you'll understand much better the context, design and decisions taken in the current architecture and internal designs. diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 3457d8b9f..0e752fb0e 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -50,7 +50,6 @@ services: - ASPNETCORE_URLS=http://0.0.0.0:5102 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word - identityUrl=http://identity.api:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - - BasketUrl=http://basket.api:5103 - EventBusConnection=rabbitmq ports: - "5102:5102" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5a88f2c6c..58bfe5f82 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -50,7 +50,6 @@ services: - ASPNETCORE_URLS=http://0.0.0.0:5102 - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word - identityUrl=http://identity.api:5105 #Local: You need to open your host's firewall at range 5100-5105. at range 5100-5105. - - BasketUrl=http://basket.api:5103 - EventBusConnection=rabbitmq ports: - "5102:5102" diff --git a/docs/Enterprise-Application-Patterns-using-XamarinForms.pdf b/docs/Enterprise-Application-Patterns-using-XamarinForms.pdf index dcd8d36d8..4990eacb1 100644 Binary files a/docs/Enterprise-Application-Patterns-using-XamarinForms.pdf and b/docs/Enterprise-Application-Patterns-using-XamarinForms.pdf differ diff --git a/docs/architecting-and-developing-containerized-and-microservice-based-net-applications-ebook-early-draft.pdf b/docs/architecting-and-developing-containerized-and-microservice-based-net-applications-ebook-early-draft.pdf index edbf31a88..5306aac12 100644 Binary files a/docs/architecting-and-developing-containerized-and-microservice-based-net-applications-ebook-early-draft.pdf and b/docs/architecting-and-developing-containerized-and-microservice-based-net-applications-ebook-early-draft.pdf differ diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index d59b891f6..43e6defbe 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -62,12 +62,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationEventLogEF", "sr EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HealthChecks", "HealthChecks", "{A81ECBC2-6B00-4DCD-8388-469174033379}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HealthChecks", "src\BuildingBlocks\HealthChecks\src\Microsoft.AspNetCore.HealthChecks\Microsoft.AspNetCore.HealthChecks.csproj", "{DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.HealthChecks", "src\BuildingBlocks\HealthChecks\src\Microsoft.Extensions.HealthChecks\Microsoft.Extensions.HealthChecks.csproj", "{942ED6E8-0050-495F-A0EA-01E97F63760C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.HealthChecks.Data", "src\BuildingBlocks\HealthChecks\src\Microsoft.Extensions.HealthChecks.Data\Microsoft.Extensions.HealthChecks.Data.csproj", "{7804FC60-23E6-490C-8E08-F9FEF829F184}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebStatus", "src\Web\WebStatus\WebStatus.csproj", "{C0A7918D-B4F2-4E7F-8DE2-1E5279EF079F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resilience", "Resilience", "{FBF43D93-F2E7-4FF8-B4AB-186895949B88}" @@ -82,6 +78,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SagaManager", "SagaManager" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SagaManager", "src\Services\SagaManager\SagaManager\SagaManager.csproj", "{F6E0F0DD-1400-43C3-B5E0-7CC325728C47}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HealthChecks", "src\BuildingBlocks\HealthChecks\src\Microsoft.AspNetCore.HealthChecks\Microsoft.AspNetCore.HealthChecks.csproj", "{22A0F9C1-2D4A-4107-95B7-8459E6688BC5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.HealthChecks.SqlServer", "src\BuildingBlocks\HealthChecks\src\Microsoft.Extensions.HealthChecks.SqlServer\Microsoft.Extensions.HealthChecks.SqlServer.csproj", "{4BD76717-3102-4969-8C2C-BAAA3F0263B6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "src\BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{4A980AC4-7205-46BF-8CCB-09E44D700FD4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -720,54 +722,6 @@ Global {9EE28E45-1533-472B-8267-56C48855BA0E}.Release|x64.Build.0 = Release|Any CPU {9EE28E45-1533-472B-8267-56C48855BA0E}.Release|x86.ActiveCfg = Release|Any CPU {9EE28E45-1533-472B-8267-56C48855BA0E}.Release|x86.Build.0 = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|ARM.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|iPhone.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|x64.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|x64.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|x86.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.AppStore|x86.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|ARM.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|ARM.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|iPhone.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|x64.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|x64.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|x86.ActiveCfg = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Debug|x86.Build.0 = Debug|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|Any CPU.Build.0 = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|ARM.ActiveCfg = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|ARM.Build.0 = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|iPhone.ActiveCfg = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|iPhone.Build.0 = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|x64.ActiveCfg = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|x64.Build.0 = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|x86.ActiveCfg = Release|Any CPU - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E}.Release|x86.Build.0 = Release|Any CPU {942ED6E8-0050-495F-A0EA-01E97F63760C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {942ED6E8-0050-495F-A0EA-01E97F63760C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {942ED6E8-0050-495F-A0EA-01E97F63760C}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU @@ -816,54 +770,6 @@ Global {942ED6E8-0050-495F-A0EA-01E97F63760C}.Release|x64.Build.0 = Release|Any CPU {942ED6E8-0050-495F-A0EA-01E97F63760C}.Release|x86.ActiveCfg = Release|Any CPU {942ED6E8-0050-495F-A0EA-01E97F63760C}.Release|x86.Build.0 = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|x64.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Ad-Hoc|x86.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|ARM.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|ARM.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|iPhone.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|x64.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|x64.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|x86.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.AppStore|x86.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|ARM.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|ARM.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|iPhone.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|x64.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|x64.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|x86.ActiveCfg = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Debug|x86.Build.0 = Debug|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|Any CPU.Build.0 = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|ARM.ActiveCfg = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|ARM.Build.0 = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|iPhone.ActiveCfg = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|iPhone.Build.0 = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|x64.ActiveCfg = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|x64.Build.0 = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|x86.ActiveCfg = Release|Any CPU - {7804FC60-23E6-490C-8E08-F9FEF829F184}.Release|x86.Build.0 = Release|Any CPU {C0A7918D-B4F2-4E7F-8DE2-1E5279EF079F}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {C0A7918D-B4F2-4E7F-8DE2-1E5279EF079F}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {C0A7918D-B4F2-4E7F-8DE2-1E5279EF079F}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU @@ -1008,6 +914,150 @@ Global {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|x64.Build.0 = Release|Any CPU {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|x86.ActiveCfg = Release|Any CPU {1A01AF82-6FCB-464C-B39C-F127AEBD315D}.Release|x86.Build.0 = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|ARM.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|iPhone.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|x64.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|x64.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|x86.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.AppStore|x86.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|ARM.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|ARM.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|iPhone.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|x64.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Debug|x86.Build.0 = Debug|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|Any CPU.Build.0 = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|ARM.ActiveCfg = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|ARM.Build.0 = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|iPhone.ActiveCfg = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|iPhone.Build.0 = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|x64.ActiveCfg = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|x64.Build.0 = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|x86.ActiveCfg = Release|Any CPU + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5}.Release|x86.Build.0 = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|ARM.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|x64.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|x64.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|x86.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.AppStore|x86.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|ARM.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|ARM.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|iPhone.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|x64.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Debug|x86.Build.0 = Debug|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|Any CPU.Build.0 = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|ARM.ActiveCfg = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|ARM.Build.0 = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|iPhone.ActiveCfg = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|iPhone.Build.0 = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|x64.ActiveCfg = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|x64.Build.0 = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|x86.ActiveCfg = Release|Any CPU + {4BD76717-3102-4969-8C2C-BAAA3F0263B6}.Release|x86.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|ARM.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|x64.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|x64.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|x86.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.AppStore|x86.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|ARM.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|ARM.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|iPhone.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|x64.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Debug|x86.Build.0 = Debug|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|Any CPU.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|ARM.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|ARM.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|iPhone.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|iPhone.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|x64.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|x64.Build.0 = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|x86.ActiveCfg = Release|Any CPU + {4A980AC4-7205-46BF-8CCB-09E44D700FD4}.Release|x86.Build.0 = Release|Any CPU {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {F6E0F0DD-1400-43C3-B5E0-7CC325728C47}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU @@ -1085,9 +1135,7 @@ Global {8088F3FC-6787-45FA-A924-816EC81CBFAC} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} {9EE28E45-1533-472B-8267-56C48855BA0E} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} {A81ECBC2-6B00-4DCD-8388-469174033379} = {DB0EFB20-B024-4E5E-A75C-52143C131D25} - {DF8367F8-E6BD-4D07-99D2-E416BF8AB01E} = {A81ECBC2-6B00-4DCD-8388-469174033379} {942ED6E8-0050-495F-A0EA-01E97F63760C} = {A81ECBC2-6B00-4DCD-8388-469174033379} - {7804FC60-23E6-490C-8E08-F9FEF829F184} = {A81ECBC2-6B00-4DCD-8388-469174033379} {C0A7918D-B4F2-4E7F-8DE2-1E5279EF079F} = {E279BF0F-7F66-4F3A-A3AB-2CDA66C1CD04} {FBF43D93-F2E7-4FF8-B4AB-186895949B88} = {DB0EFB20-B024-4E5E-A75C-52143C131D25} {D1C47FF1-91F1-4CAF-9ABB-AD642B821502} = {FBF43D93-F2E7-4FF8-B4AB-186895949B88} @@ -1095,5 +1143,8 @@ Global {1A01AF82-6FCB-464C-B39C-F127AEBD315D} = {022E145D-1593-47EE-9608-8E323D3C63F5} {F38B4FF0-0B49-405A-B1B4-F7A5E3BC4C4E} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} {F6E0F0DD-1400-43C3-B5E0-7CC325728C47} = {F38B4FF0-0B49-405A-B1B4-F7A5E3BC4C4E} + {22A0F9C1-2D4A-4107-95B7-8459E6688BC5} = {A81ECBC2-6B00-4DCD-8388-469174033379} + {4BD76717-3102-4969-8C2C-BAAA3F0263B6} = {A81ECBC2-6B00-4DCD-8388-469174033379} + {4A980AC4-7205-46BF-8CCB-09E44D700FD4} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} EndGlobalSection EndGlobal diff --git a/img/ebook_arch_dev_microservices_containers_cover.png b/img/ebook_arch_dev_microservices_containers_cover.png index 1837354b5..caf16c665 100644 Binary files a/img/ebook_arch_dev_microservices_containers_cover.png and b/img/ebook_arch_dev_microservices_containers_cover.png differ diff --git a/img/ebook_arch_dev_microservices_containers_cover_LARGE.png b/img/ebook_arch_dev_microservices_containers_cover_LARGE.png new file mode 100644 index 000000000..f18ec386f Binary files /dev/null and b/img/ebook_arch_dev_microservices_containers_cover_LARGE.png differ diff --git a/img/ebook_arch_dev_microservices_containers_cover_OLD.png b/img/ebook_arch_dev_microservices_containers_cover_OLD.png new file mode 100644 index 000000000..1837354b5 Binary files /dev/null and b/img/ebook_arch_dev_microservices_containers_cover_OLD.png differ diff --git a/img/xamarin-enterprise-patterns-ebook-cover-small.png b/img/xamarin-enterprise-patterns-ebook-cover-small.png index f645ef596..994e21ad5 100644 Binary files a/img/xamarin-enterprise-patterns-ebook-cover-small.png and b/img/xamarin-enterprise-patterns-ebook-cover-small.png differ diff --git a/src/BuildingBlocks/CommandBus/CommandBus/CommandBus.csproj b/src/BuildingBlocks/CommandBus/CommandBus/CommandBus.csproj new file mode 100644 index 000000000..ae05359a1 --- /dev/null +++ b/src/BuildingBlocks/CommandBus/CommandBus/CommandBus.csproj @@ -0,0 +1,8 @@ + + + + netstandard1.4 + Microsoft.eShopOnContainers.BuildingBlocks.CommandBus + + + \ No newline at end of file diff --git a/src/BuildingBlocks/CommandBus/CommandBus/ICommandBus.cs b/src/BuildingBlocks/CommandBus/CommandBus/ICommandBus.cs new file mode 100644 index 000000000..2092e11b0 --- /dev/null +++ b/src/BuildingBlocks/CommandBus/CommandBus/ICommandBus.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public interface ICommandBus + { + Task SendAsync(T command) where T : IntegrationCommand; + + } +} diff --git a/src/BuildingBlocks/CommandBus/CommandBus/IntegrationCommand.cs b/src/BuildingBlocks/CommandBus/CommandBus/IntegrationCommand.cs new file mode 100644 index 000000000..36f1f0fdc --- /dev/null +++ b/src/BuildingBlocks/CommandBus/CommandBus/IntegrationCommand.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public abstract class IntegrationCommand + { + public Guid Id { get; private set; } + public DateTime Sent { get; private set; } + + protected IntegrationCommand() + { + Id = Guid.NewGuid(); + Sent = DateTime.UtcNow; + } + } +} diff --git a/src/BuildingBlocks/CommandBus/CommandBusRabbitMQ/CommandBusRabbitMQ.csproj b/src/BuildingBlocks/CommandBus/CommandBusRabbitMQ/CommandBusRabbitMQ.csproj new file mode 100644 index 000000000..954020d10 --- /dev/null +++ b/src/BuildingBlocks/CommandBus/CommandBusRabbitMQ/CommandBusRabbitMQ.csproj @@ -0,0 +1,7 @@ + + + + netstandard1.4 + + + \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/CommandBus/CommandBus.csproj b/src/BuildingBlocks/EventBus/CommandBus/CommandBus.csproj new file mode 100644 index 000000000..7c3327057 --- /dev/null +++ b/src/BuildingBlocks/EventBus/CommandBus/CommandBus.csproj @@ -0,0 +1,8 @@ + + + + netstandard1.0 + Microsoft.eShopOnContainers.BuildingBlocks.CommandBus + + + \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/CommandBus/ICommandBus.cs b/src/BuildingBlocks/EventBus/CommandBus/ICommandBus.cs new file mode 100644 index 000000000..813d9406c --- /dev/null +++ b/src/BuildingBlocks/EventBus/CommandBus/ICommandBus.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public interface ICommandBus + { + void Send(string name, T data); + void Handle(string name, IIntegrationCommandHandler handler); + void Handle(string name, IIntegrationCommandHandler handler); + void Handle(TI handler) + where TI : IIntegrationCommandHandler; + } +} diff --git a/src/BuildingBlocks/EventBus/CommandBus/IIntegrationCommandHandler.cs b/src/BuildingBlocks/EventBus/CommandBus/IIntegrationCommandHandler.cs new file mode 100644 index 000000000..07f0c1eea --- /dev/null +++ b/src/BuildingBlocks/EventBus/CommandBus/IIntegrationCommandHandler.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public interface IIntegrationCommandHandler + { + void Handle(IntegrationCommand command); + } + + public interface IIntegrationCommandHandler : IIntegrationCommandHandler + { + void Handle(IntegrationCommand command); + } +} diff --git a/src/BuildingBlocks/EventBus/CommandBus/IntegrationCommand.cs b/src/BuildingBlocks/EventBus/CommandBus/IntegrationCommand.cs new file mode 100644 index 000000000..8df6e5279 --- /dev/null +++ b/src/BuildingBlocks/EventBus/CommandBus/IntegrationCommand.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.CommandBus +{ + public abstract class IntegrationCommand + { + public Guid Id { get; } + public DateTime Sent { get; } + + public abstract object GetDataAsObject(); + + protected IntegrationCommand() + { + Id = Guid.NewGuid(); + Sent = DateTime.UtcNow; + } + + } + + public class IntegrationCommand : IntegrationCommand + { + public T Data { get; } + public string Name { get; } + public override object GetDataAsObject() => Data; + + public IntegrationCommand(string name, T data) : base() + { + Data = data; + Name = name; + } + } + +} diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj b/src/BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj new file mode 100644 index 000000000..1387a74dd --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp1.1 + + + + + + + + + + + + + + + + + + diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs new file mode 100644 index 000000000..dd5f7f5b4 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/InMemory_SubscriptionManager_Tests.cs @@ -0,0 +1,56 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using System; +using System.Linq; +using Xunit; + +namespace EventBus.Tests +{ + public class InMemory_SubscriptionManager_Tests + { + [Fact] + public void After_Creation_Should_Be_Empty() + { + var manager = new InMemoryEventBusSubscriptionsManager(); + Assert.True(manager.IsEmpty); + } + + [Fact] + public void After_One_Event_Subscription_Should_Contain_The_Event() + { + var manager = new InMemoryEventBusSubscriptionsManager(); + manager.AddSubscription(() => new TestIntegrationEventHandler()); + Assert.True(manager.HasSubscriptionsForEvent()); + } + + [Fact] + public void After_All_Subscriptions_Are_Deleted_Event_Should_No_Longer_Exists() + { + var manager = new InMemoryEventBusSubscriptionsManager(); + manager.AddSubscription(() => new TestIntegrationEventHandler()); + manager.RemoveSubscription(); + Assert.False(manager.HasSubscriptionsForEvent()); + } + + [Fact] + public void Deleting_Last_Subscription_Should_Raise_On_Deleted_Event() + { + bool raised = false; + var manager = new InMemoryEventBusSubscriptionsManager(); + manager.OnEventRemoved += (o, e) => raised = true; + manager.AddSubscription(() => new TestIntegrationEventHandler()); + manager.RemoveSubscription(); + Assert.True(raised); + } + + [Fact] + public void Get_Handlers_For_Event_Should_Return_All_Handlers() + { + var manager = new InMemoryEventBusSubscriptionsManager(); + manager.AddSubscription(() => new TestIntegrationEventHandler()); + manager.AddSubscription(() => new TestIntegrationOtherEventHandler()); + var handlers = manager.GetHandlersForEvent(); + Assert.Equal(2, handlers.Count()); + } + + } +} diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEvent.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEvent.cs new file mode 100644 index 000000000..a77f3ef6f --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEvent.cs @@ -0,0 +1,11 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EventBus.Tests +{ + public class TestIntegrationEvent : IntegrationEvent + { + } +} diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEventHandler.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEventHandler.cs new file mode 100644 index 000000000..0b5b793ee --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationEventHandler.cs @@ -0,0 +1,23 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace EventBus.Tests +{ + public class TestIntegrationOtherEventHandler : IIntegrationEventHandler + { + public bool Handled { get; private set; } + + public TestIntegrationOtherEventHandler() + { + Handled = false; + } + + public async Task Handle(TestIntegrationEvent @event) + { + Handled = true; + } + } +} diff --git a/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationOtherEventHandler.cs b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationOtherEventHandler.cs new file mode 100644 index 000000000..72e1ed2cd --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus.Tests/TestIntegrationOtherEventHandler.cs @@ -0,0 +1,23 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace EventBus.Tests +{ + public class TestIntegrationEventHandler : IIntegrationEventHandler + { + public bool Handled { get; private set; } + + public TestIntegrationEventHandler() + { + Handled = false; + } + + public async Task Handle(TestIntegrationEvent @event) + { + Handled = true; + } + } +} diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IDynamicIntegrationEventHandler.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IDynamicIntegrationEventHandler.cs new file mode 100644 index 000000000..55d62ade2 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IDynamicIntegrationEventHandler.cs @@ -0,0 +1,13 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions +{ + public interface IDynamicIntegrationEventHandler + { + Task Handle(dynamic eventData); + } +} diff --git a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs index 63f9f1b99..7dd91541b 100644 --- a/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs +++ b/src/BuildingBlocks/EventBus/EventBus/Abstractions/IEventBus.cs @@ -1,11 +1,23 @@ using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions { public interface IEventBus { - void Subscribe(IIntegrationEventHandler handler) where T: IntegrationEvent; - void Unsubscribe(IIntegrationEventHandler handler) where T : IntegrationEvent; + void Subscribe(Func handler) + where T : IntegrationEvent + where TH : IIntegrationEventHandler; + void SubscribeDynamic(string eventName, Func handler) + where TH : IDynamicIntegrationEventHandler; + + void UnsubscribeDynamic(string eventName) + where TH : IDynamicIntegrationEventHandler; + + void Unsubscribe() + where TH : IIntegrationEventHandler + where T : IntegrationEvent; + void Publish(IntegrationEvent @event); } } diff --git a/src/BuildingBlocks/EventBus/EventBus/EventBus.csproj b/src/BuildingBlocks/EventBus/EventBus/EventBus.csproj index ad6867741..ed5efa621 100644 --- a/src/BuildingBlocks/EventBus/EventBus/EventBus.csproj +++ b/src/BuildingBlocks/EventBus/EventBus/EventBus.csproj @@ -6,10 +6,6 @@ Microsoft.eShopOnContainers.BuildingBlocks.EventBus - - - - diff --git a/src/BuildingBlocks/EventBus/EventBus/IEventBusSubscriptionsManager.cs b/src/BuildingBlocks/EventBus/EventBus/IEventBusSubscriptionsManager.cs new file mode 100644 index 000000000..d46292356 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus/IEventBusSubscriptionsManager.cs @@ -0,0 +1,34 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; +using System.Collections.Generic; +using static Microsoft.eShopOnContainers.BuildingBlocks.EventBus.InMemoryEventBusSubscriptionsManager; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus +{ + public interface IEventBusSubscriptionsManager + { + bool IsEmpty { get; } + event EventHandler OnEventRemoved; + void AddDynamicSubscription(string eventName, Func handler) + where TH : IDynamicIntegrationEventHandler; + + void AddSubscription(Func handler) + where T : IntegrationEvent + where TH : IIntegrationEventHandler; + + void RemoveSubscription() + where TH : IIntegrationEventHandler + where T : IntegrationEvent; + void RemoveDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler; + + bool HasSubscriptionsForEvent() where T : IntegrationEvent; + bool HasSubscriptionsForEvent(string eventName); + Type GetEventTypeByName(string eventName); + void Clear(); + IEnumerable GetHandlersForEvent() where T : IntegrationEvent; + IEnumerable GetHandlersForEvent(string eventName); + string GetEventKey(); + } +} \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs b/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs new file mode 100644 index 000000000..e85ef7064 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus/InMemoryEventBusSubscriptionsManager.cs @@ -0,0 +1,162 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus +{ + public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager + { + + + private readonly Dictionary> _handlers; + private readonly List _eventTypes; + + public event EventHandler OnEventRemoved; + + public InMemoryEventBusSubscriptionsManager() + { + _handlers = new Dictionary>(); + _eventTypes = new List(); + } + + public bool IsEmpty => !_handlers.Keys.Any(); + public void Clear() => _handlers.Clear(); + + public void AddDynamicSubscription(string eventName, Func handler) + where TH : IDynamicIntegrationEventHandler + { + DoAddSubscription(handler, eventName, isDynamic: true); + } + + public void AddSubscription(Func handler) + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = GetEventKey(); + DoAddSubscription(handler, eventName, isDynamic: false); + _eventTypes.Add(typeof(T)); + } + + private void DoAddSubscription(Delegate handler, string eventName, bool isDynamic) + { + if (!HasSubscriptionsForEvent(eventName)) + { + _handlers.Add(eventName, new List()); + } + if (isDynamic) + { + _handlers[eventName].Add(SubscriptionInfo.Dynamic(handler)); + } + else + { + _handlers[eventName].Add(SubscriptionInfo.Typed(handler)); + } + } + + + public void RemoveDynamicSubscription(string eventName) + where TH : IDynamicIntegrationEventHandler + { + var handlerToRemove = FindDynamicSubscriptionToRemove(eventName); + DoRemoveHandler(eventName, handlerToRemove); + } + + + public void RemoveSubscription() + where TH : IIntegrationEventHandler + where T : IntegrationEvent + { + var handlerToRemove = FindSubscriptionToRemove(); + var eventName = GetEventKey(); + DoRemoveHandler(eventName, handlerToRemove); + } + + + private void DoRemoveHandler(string eventName, SubscriptionInfo subsToRemove) + { + if (subsToRemove != null) + { + _handlers[eventName].Remove(subsToRemove); + if (!_handlers[eventName].Any()) + { + _handlers.Remove(eventName); + var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); + if (eventType != null) + { + _eventTypes.Remove(eventType); + } + RaiseOnEventRemoved(eventName); + } + + } + } + + public IEnumerable GetHandlersForEvent() where T : IntegrationEvent + { + var key = GetEventKey(); + return GetHandlersForEvent(key); + } + public IEnumerable GetHandlersForEvent(string eventName) => _handlers[eventName]; + + private void RaiseOnEventRemoved(string eventName) + { + var handler = OnEventRemoved; + if (handler != null) + { + OnEventRemoved(this, eventName); + } + } + + + private SubscriptionInfo FindDynamicSubscriptionToRemove(string eventName) + where TH : IDynamicIntegrationEventHandler + { + return DoFindHandlerToRemove(eventName, typeof(TH)); + } + + + private SubscriptionInfo FindSubscriptionToRemove() + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = GetEventKey(); + return DoFindHandlerToRemove(eventName, typeof(TH)); + } + + private SubscriptionInfo DoFindHandlerToRemove(string eventName, Type handlerType) + { + if (!HasSubscriptionsForEvent(eventName)) + { + return null; + } + foreach (var subscription in _handlers[eventName]) + { + var genericArgs = subscription.Factory.GetType().GetGenericArguments(); + if (genericArgs.SingleOrDefault() == handlerType) + { + return subscription; + } + } + + return null; + } + + public bool HasSubscriptionsForEvent() where T : IntegrationEvent + { + var key = GetEventKey(); + return HasSubscriptionsForEvent(key); + } + public bool HasSubscriptionsForEvent(string eventName) => _handlers.ContainsKey(eventName); + + public Type GetEventTypeByName(string eventName) => _eventTypes.SingleOrDefault(t => t.Name == eventName); + + public string GetEventKey() + { + return typeof(T).Name; + } + } +} diff --git a/src/BuildingBlocks/EventBus/EventBus/SubscriptionInfo.cs b/src/BuildingBlocks/EventBus/EventBus/SubscriptionInfo.cs new file mode 100644 index 000000000..33c0aec26 --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBus/SubscriptionInfo.cs @@ -0,0 +1,28 @@ +using System; + +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBus +{ + public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager + { + public class SubscriptionInfo + { + public bool IsDynamic { get; } + public Delegate Factory { get; } + + private SubscriptionInfo(bool isDynamic, Delegate factory) + { + IsDynamic = isDynamic; + Factory = factory; + } + + public static SubscriptionInfo Dynamic(Delegate factory) + { + return new SubscriptionInfo(true, factory); + } + public static SubscriptionInfo Typed(Delegate factory) + { + return new SubscriptionInfo(false, factory); + } + } + } +} diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/CommandBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/CommandBusRabbitMQ.cs new file mode 100644 index 000000000..27e67d9ca --- /dev/null +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/CommandBusRabbitMQ.cs @@ -0,0 +1,145 @@ +//using Microsoft.eShopOnContainers.BuildingBlocks.CommandBus; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Polly; +using Polly.Retry; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using RabbitMQ.Client.Exceptions; +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +/* +namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ +{ + public class CommandBusRabbitMQ : ICommandBus, IDisposable + { + const string BROKER_NAME = "eshop_command_bus"; + + private readonly IRabbitMQPersistentConnection _persistentConnection; + private readonly ILogger _logger; + + private IModel _consumerChannel; + private string _queueName; + + private readonly Dictionary _handlers; + private readonly Dictionary _typeMappings; + + public CommandBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, + ILogger logger) + { + _logger = logger; + _persistentConnection = persistentConnection; + _handlers = new Dictionary(); + _typeMappings = new Dictionary(); + } + + public void Send(string name, T data) + { + Send(new IntegrationCommand(name, data)); + } + + public void Handle(string name, IIntegrationCommandHandler handler) + { + _handlers.Add(name, handler); + _typeMappings.Add(name, typeof(TC)); + } + + public void Handle(string name, IIntegrationCommandHandler handler) + { + _handlers.Add(name, handler); + } + public void Handle(TI handler) where TI : IIntegrationCommandHandler + { + var name = typeof(TI).Name; + _handlers.Add(name, handler); + _typeMappings.Add(name, typeof(TC)); + } + + private void Send(IntegrationCommand command) + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + var policy = RetryPolicy.Handle() + .Or() + .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) => + { + _logger.LogWarning(ex.ToString()); + }); + + using (var channel = _persistentConnection.CreateModel()) + { + var commandName = command.Name; + channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); + var message = JsonConvert.SerializeObject(command); + var body = Encoding.UTF8.GetBytes(message); + policy.Execute(() => + { + channel.BasicPublish(exchange: BROKER_NAME, + routingKey: commandName, + basicProperties: null, + body: body); + }); + } + } + + private IModel CreateConsumerChannel() + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + var channel = _persistentConnection.CreateModel(); + + channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); + _queueName = channel.QueueDeclare().QueueName; + var consumer = new EventingBasicConsumer(channel); + consumer.Received += async (model, ea) => + { + var commandName = ea.RoutingKey; + var message = Encoding.UTF8.GetString(ea.Body); + await InvokeHandler(commandName, message); + }; + + channel.BasicConsume(queue: _queueName, + noAck: true, + consumer: consumer); + + channel.CallbackException += (sender, ea) => + { + _consumerChannel.Dispose(); + _consumerChannel = CreateConsumerChannel(); + }; + + return channel; + } + + private Task InvokeHandler(string commandName, string message) + { + if (_handlers.ContainsKey(commandName)) + { + + } + + } + + public void Dispose() + { + if (_consumerChannel != null) + { + _consumerChannel.Dispose(); + } + } + + + + } +} +*/ \ No newline at end of file diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs index 894afb4e4..0aafaf90a 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/DefaultRabbitMQPersisterConnection.cs @@ -10,18 +10,18 @@ using System.Net.Sockets; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ { - public class DefaultRabbitMQPersisterConnection - : IRabbitMQPersisterConnection + public class DefaultRabbitMQPersistentConnection + : IRabbitMQPersistentConnection { private readonly IConnectionFactory _connectionFactory; - private readonly ILogger _logger; + private readonly ILogger _logger; IConnection _connection; bool _disposed; object sync_root = new object(); - public DefaultRabbitMQPersisterConnection(IConnectionFactory connectionFactory,ILogger logger) + public DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory,ILogger logger) { _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -87,13 +87,13 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ _connection.CallbackException += OnCallbackException; _connection.ConnectionBlocked += OnConnectionBlocked; - _logger.LogInformation($"RabbitMQ persister connection acquire a connection {_connection.Endpoint.HostName} and is subscribed to failure events"); + _logger.LogInformation($"RabbitMQ persistent connection acquired a connection {_connection.Endpoint.HostName} and is subscribed to failure events"); return true; } else { - _logger.LogCritical("FATAL ERROR: RabbitMQ connections can't be created and opened"); + _logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened"); return false; } diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs index e7a493c10..3d32073a3 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.cs @@ -1,7 +1,9 @@ -using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Polly; using Polly.Retry; using RabbitMQ.Client; @@ -21,32 +23,50 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ { const string BROKER_NAME = "eshop_event_bus"; - private readonly IRabbitMQPersisterConnection _persisterConnection; + private readonly IRabbitMQPersistentConnection _persistentConnection; private readonly ILogger _logger; - - private readonly Dictionary> _handlers - = new Dictionary>(); - - private readonly List _eventTypes - = new List(); + private readonly IEventBusSubscriptionsManager _subsManager; + private IModel _consumerChannel; private string _queueName; - public EventBusRabbitMQ(IRabbitMQPersisterConnection persisterConnection, ILogger logger) + public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger logger, IEventBusSubscriptionsManager subsManager) { - _persisterConnection = persisterConnection ?? throw new ArgumentNullException(nameof(persisterConnection)); + _persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - + _subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager(); _consumerChannel = CreateConsumerChannel(); + + _subsManager.OnEventRemoved += SubsManager_OnEventRemoved; } + private void SubsManager_OnEventRemoved(object sender, string eventName) + { + if (!_persistentConnection.IsConnected) + { + _persistentConnection.TryConnect(); + } + + using (var channel = _persistentConnection.CreateModel()) + { + channel.QueueUnbind(queue: _queueName, + exchange: BROKER_NAME, + routingKey: eventName); + + if (_subsManager.IsEmpty) + { + _queueName = string.Empty; + _consumerChannel.Close(); + } + } + } public void Publish(IntegrationEvent @event) { - if (!_persisterConnection.IsConnected) + if (!_persistentConnection.IsConnected) { - _persisterConnection.TryConnect(); + _persistentConnection.TryConnect(); } var policy = RetryPolicy.Handle() @@ -56,7 +76,7 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ _logger.LogWarning(ex.ToString()); }); - using (var channel = _persisterConnection.CreateModel()) + using (var channel = _persistentConnection.CreateModel()) { var eventName = @event.GetType() .Name; @@ -77,95 +97,73 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ } } - public void Subscribe(IIntegrationEventHandler handler) where T : IntegrationEvent + public void SubscribeDynamic(string eventName, Func handler) + where TH: IDynamicIntegrationEventHandler { - var eventName = typeof(T).Name; + DoInternalSubscription(eventName); + _subsManager.AddDynamicSubscription(eventName,handler); + } - if (_handlers.ContainsKey(eventName)) - { - _handlers[eventName].Add(handler); - } - else + public void Subscribe(Func handler) + where T : IntegrationEvent + where TH : IIntegrationEventHandler + { + var eventName = _subsManager.GetEventKey(); + DoInternalSubscription(eventName); + _subsManager.AddSubscription(handler); + } + + private void DoInternalSubscription(string eventName) + { + var containsKey = _subsManager.HasSubscriptionsForEvent(eventName); + if (!containsKey) { - if (!_persisterConnection.IsConnected) + if (!_persistentConnection.IsConnected) { - _persisterConnection.TryConnect(); + _persistentConnection.TryConnect(); } - using (var channel = _persisterConnection.CreateModel()) + using (var channel = _persistentConnection.CreateModel()) { channel.QueueBind(queue: _queueName, exchange: BROKER_NAME, routingKey: eventName); - - _handlers.Add(eventName, new List()); - _handlers[eventName].Add(handler); - _eventTypes.Add(typeof(T)); } - } - } - public void Unsubscribe(IIntegrationEventHandler handler) where T : IntegrationEvent + public void Unsubscribe() + where TH : IIntegrationEventHandler + where T : IntegrationEvent { - var eventName = typeof(T).Name; - - if (_handlers.ContainsKey(eventName) && _handlers[eventName].Contains(handler)) - { - _handlers[eventName].Remove(handler); - - if (_handlers[eventName].Count == 0) - { - _handlers.Remove(eventName); - - var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName); + _subsManager.RemoveSubscription(); + } - if (eventType != null) - { - _eventTypes.Remove(eventType); - - if (!_persisterConnection.IsConnected) - { - _persisterConnection.TryConnect(); - } - - using (var channel = _persisterConnection.CreateModel()) - { - channel.QueueUnbind(queue: _queueName, - exchange: BROKER_NAME, - routingKey: eventName); - - if (_handlers.Keys.Count == 0) - { - _queueName = string.Empty; - - _consumerChannel.Close(); - } - } - } - } - } + public void UnsubscribeDynamic(string eventName) + where TH: IDynamicIntegrationEventHandler + { + _subsManager.RemoveDynamicSubscription(eventName); } + public void Dispose() { if (_consumerChannel != null) { _consumerChannel.Dispose(); } - - _handlers.Clear(); + + _subsManager.Clear(); } private IModel CreateConsumerChannel() { - if (!_persisterConnection.IsConnected) + if (!_persistentConnection.IsConnected) { - _persisterConnection.TryConnect(); + _persistentConnection.TryConnect(); } - var channel = _persisterConnection.CreateModel(); + var channel = _persistentConnection.CreateModel(); channel.ExchangeDeclare(exchange: BROKER_NAME, type: "direct"); @@ -196,16 +194,27 @@ namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ private async Task ProcessEvent(string eventName, string message) { - if (_handlers.ContainsKey(eventName)) + + if (_subsManager.HasSubscriptionsForEvent(eventName)) { - Type eventType = _eventTypes.Single(t => t.Name == eventName); - var integrationEvent = JsonConvert.DeserializeObject(message, eventType); - var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); - var handlers = _handlers[eventName]; + var subscriptions = _subsManager.GetHandlersForEvent(eventName); - foreach (var handler in handlers) + foreach (var subscription in subscriptions) { - await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); + if (subscription.IsDynamic) + { + var handler = subscription.Factory.DynamicInvoke() as IDynamicIntegrationEventHandler; + dynamic eventData = JObject.Parse(message); + await handler.Handle(eventData); + } + else + { + var eventType = _subsManager.GetEventTypeByName(eventName); + var integrationEvent = JsonConvert.DeserializeObject(message, eventType); + var handler = subscription.Factory.DynamicInvoke(); + var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); + await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); + } } } } diff --git a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs index b9debe743..5893791c5 100644 --- a/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs +++ b/src/BuildingBlocks/EventBus/EventBusRabbitMQ/IRabbitMQPersisterConnection.cs @@ -3,8 +3,7 @@ using System; namespace Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ { - - public interface IRabbitMQPersisterConnection + public interface IRabbitMQPersistentConnection : IDisposable { bool IsConnected { get; } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckMiddleware.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckMiddleware.cs index 64b4f48c2..f8e68c957 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckMiddleware.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckMiddleware.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -11,30 +13,34 @@ namespace Microsoft.AspNetCore.HealthChecks { public class HealthCheckMiddleware { - private RequestDelegate _next; - private string _path; - private int? _port; - private IHealthCheckService _service; + private readonly RequestDelegate _next; + private readonly string _path; + private readonly int? _port; + private readonly IHealthCheckService _service; + private readonly TimeSpan _timeout; - public HealthCheckMiddleware(RequestDelegate next, IHealthCheckService service, int port) + public HealthCheckMiddleware(RequestDelegate next, IHealthCheckService service, int port, TimeSpan timeout) { _port = port; _service = service; _next = next; + _timeout = timeout; } - public HealthCheckMiddleware(RequestDelegate next, IHealthCheckService service, string path) + public HealthCheckMiddleware(RequestDelegate next, IHealthCheckService service, string path, TimeSpan timeout) { _path = path; _service = service; _next = next; + _timeout = timeout; } public async Task Invoke(HttpContext context) { if (IsHealthCheckRequest(context)) { - var result = await _service.CheckHealthAsync(); + var timeoutTokenSource = new CancellationTokenSource(_timeout); + var result = await _service.CheckHealthAsync(timeoutTokenSource.Token); var status = result.CheckStatus; if (status != CheckStatus.Healthy) @@ -60,7 +66,9 @@ namespace Microsoft.AspNetCore.HealthChecks } if (context.Request.Path == _path) + { return true; + } return false; } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckStartupFilter.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckStartupFilter.cs index 4aa91b070..cac4b1188 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckStartupFilter.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckStartupFilter.cs @@ -11,15 +11,18 @@ namespace Microsoft.AspNetCore.HealthChecks { private string _path; private int? _port; + private TimeSpan _timeout; - public HealthCheckStartupFilter(int port) + public HealthCheckStartupFilter(int port, TimeSpan timeout) { _port = port; + _timeout = timeout; } - public HealthCheckStartupFilter(string path) + public HealthCheckStartupFilter(string path, TimeSpan timeout) { _path = path; + _timeout = timeout; } public Action Configure(Action next) @@ -27,9 +30,13 @@ namespace Microsoft.AspNetCore.HealthChecks return app => { if (_port.HasValue) - app.UseMiddleware(_port); + { + app.UseMiddleware(_port, _timeout); + } else - app.UseMiddleware(_path); + { + app.UseMiddleware(_path, _timeout); + } next(app); }; diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckWebHostBuilderExtension.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckWebHostBuilderExtension.cs index 0b806ca06..467293137 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckWebHostBuilderExtension.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.AspNetCore.HealthChecks/HealthCheckWebHostBuilderExtension.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.HealthChecks; using Microsoft.Extensions.DependencyInjection; @@ -8,28 +9,38 @@ namespace Microsoft.AspNetCore.Hosting { public static class HealthCheckWebHostBuilderExtension { + public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, int port) + => UseHealthChecks(builder, port, DefaultTimeout); + + public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, int port, TimeSpan timeout) { - Guard.ArgumentValid(port > 0 && port < 65536, nameof(port), "Port must be a value between 1 and 65535"); + Guard.ArgumentValid(port > 0 && port < 65536, nameof(port), "Port must be a value between 1 and 65535."); + Guard.ArgumentValid(timeout > TimeSpan.Zero, nameof(timeout), "Health check timeout must be a positive time span."); builder.ConfigureServices(services => { var existingUrl = builder.GetSetting(WebHostDefaults.ServerUrlsKey); builder.UseSetting(WebHostDefaults.ServerUrlsKey, $"{existingUrl};http://localhost:{port}"); - services.AddSingleton(new HealthCheckStartupFilter(port)); + services.AddSingleton(new HealthCheckStartupFilter(port, timeout)); }); return builder; } public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, string path) + => UseHealthChecks(builder, path, DefaultTimeout); + + public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, string path, TimeSpan timeout) { Guard.ArgumentNotNull(nameof(path), path); // REVIEW: Is there a better URL path validator somewhere? - Guard.ArgumentValid(!path.Contains("?"), nameof(path), "Path cannot contain query string values"); - Guard.ArgumentValid(path.StartsWith("/"), nameof(path), "Path should start with /"); + Guard.ArgumentValid(!path.Contains("?"), nameof(path), "Path cannot contain query string values."); + Guard.ArgumentValid(path.StartsWith("/"), nameof(path), "Path should start with '/'."); + Guard.ArgumentValid(timeout > TimeSpan.Zero, nameof(timeout), "Health check timeout must be a positive time span."); - builder.ConfigureServices(services => services.AddSingleton(new HealthCheckStartupFilter(path))); + builder.ConfigureServices(services => services.AddSingleton(new HealthCheckStartupFilter(path, timeout))); return builder; } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.Data/HealthCheckBuilderDataExtensions.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs similarity index 94% rename from src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.Data/HealthCheckBuilderDataExtensions.cs rename to src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs index e209dfdaf..4998c91ed 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.Data/HealthCheckBuilderDataExtensions.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/HealthCheckBuilderSqlServerExtensions.cs @@ -7,7 +7,7 @@ using System.Data.SqlClient; namespace Microsoft.Extensions.HealthChecks { - public static class HealthCheckBuilderDataExtensions + public static class HealthCheckBuilderSqlServerExtensions { public static HealthCheckBuilder AddSqlCheck(this HealthCheckBuilder builder, string name, string connectionString) { @@ -33,7 +33,7 @@ namespace Microsoft.Extensions.HealthChecks } } } - catch(Exception ex) + catch (Exception ex) { return HealthCheckResult.Unhealthy($"SqlCheck({name}): Exception during check: {ex.GetType().FullName}"); } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.Data/Microsoft.Extensions.HealthChecks.Data.csproj b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/Microsoft.Extensions.HealthChecks.SqlServer.csproj similarity index 100% rename from src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.Data/Microsoft.Extensions.HealthChecks.Data.csproj rename to src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks.SqlServer/Microsoft.Extensions.HealthChecks.SqlServer.csproj diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CachedHealthCheck.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CachedHealthCheck.cs new file mode 100644 index 000000000..39ed087eb --- /dev/null +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CachedHealthCheck.cs @@ -0,0 +1,109 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.HealthChecks +{ + public abstract class CachedHealthCheck + { + private static readonly TypeInfo HealthCheckTypeInfo = typeof(IHealthCheck).GetTypeInfo(); + + private volatile int _writerCount; + + public CachedHealthCheck(string name, TimeSpan cacheDuration) + { + Guard.ArgumentNotNullOrEmpty(nameof(name), name); + Guard.ArgumentValid(cacheDuration.TotalMilliseconds >= 0, nameof(cacheDuration), "Cache duration must be zero (disabled) or greater than zero."); + + Name = name; + CacheDuration = cacheDuration; + } + + public IHealthCheckResult CachedResult { get; internal set; } + + public TimeSpan CacheDuration { get; } + + public DateTimeOffset CacheExpiration { get; internal set; } + + public string Name { get; } + + protected virtual DateTimeOffset UtcNow => DateTimeOffset.UtcNow; + + protected abstract IHealthCheck Resolve(IServiceProvider serviceProvider); + + public async ValueTask RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default(CancellationToken)) + { + while (CacheExpiration <= UtcNow) + { + // Can't use a standard lock here because of async, so we'll use this flag to determine when we should write a value, + // and the waiters who aren't allowed to write will just spin wait for the new value. + if (Interlocked.Exchange(ref _writerCount, 1) != 0) + { + await Task.Delay(5, cancellationToken).ConfigureAwait(false); + continue; + } + + try + { + var check = Resolve(serviceProvider); + CachedResult = await check.CheckAsync(cancellationToken); + } + catch (OperationCanceledException) + { + CachedResult = HealthCheckResult.Unhealthy("The health check operation timed out"); + } + catch (Exception ex) + { + CachedResult = HealthCheckResult.Unhealthy($"Exception during check: {ex.GetType().FullName}"); + } + + CacheExpiration = UtcNow + CacheDuration; + _writerCount = 0; + break; + } + + return CachedResult; + } + + public static CachedHealthCheck FromHealthCheck(string name, TimeSpan cacheDuration, IHealthCheck healthCheck) + { + Guard.ArgumentNotNull(nameof(healthCheck), healthCheck); + + return new TypeOrHealthCheck_HealthCheck(name, cacheDuration, healthCheck); + } + + public static CachedHealthCheck FromType(string name, TimeSpan cacheDuration, Type healthCheckType) + { + Guard.ArgumentNotNull(nameof(healthCheckType), healthCheckType); + Guard.ArgumentValid(HealthCheckTypeInfo.IsAssignableFrom(healthCheckType.GetTypeInfo()), nameof(healthCheckType), $"Health check must implement '{typeof(IHealthCheck).FullName}'."); + + return new TypeOrHealthCheck_Type(name, cacheDuration, healthCheckType); + } + + class TypeOrHealthCheck_HealthCheck : CachedHealthCheck + { + private readonly IHealthCheck _healthCheck; + + public TypeOrHealthCheck_HealthCheck(string name, TimeSpan cacheDuration, IHealthCheck healthCheck) : base(name, cacheDuration) + => _healthCheck = healthCheck; + + protected override IHealthCheck Resolve(IServiceProvider serviceProvider) => _healthCheck; + } + + class TypeOrHealthCheck_Type : CachedHealthCheck + { + private readonly Type _healthCheckType; + + public TypeOrHealthCheck_Type(string name, TimeSpan cacheDuration, Type healthCheckType) : base(name, cacheDuration) + => _healthCheckType = healthCheckType; + + protected override IHealthCheck Resolve(IServiceProvider serviceProvider) + => (IHealthCheck)serviceProvider.GetRequiredService(_healthCheckType); + } + } +} diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CachedHealthCheckExtensions.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CachedHealthCheckExtensions.cs new file mode 100644 index 000000000..2c3388709 --- /dev/null +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CachedHealthCheckExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.HealthChecks +{ + public static class CachedHealthCheckExtensions + { + public static ValueTask RunAsync(this CachedHealthCheck check, IServiceProvider serviceProvider) + { + Guard.ArgumentNotNull(nameof(check), check); + + return check.RunAsync(serviceProvider, CancellationToken.None); + } + } +} diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/AddCheck.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/AddCheck.cs index cd10fb93d..5b7b49af0 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/AddCheck.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/AddCheck.cs @@ -15,96 +15,102 @@ namespace Microsoft.Extensions.HealthChecks { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromCheck(check, builder.DefaultCacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromCheck(check), builder.DefaultCacheDuration); } public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func check) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromCheck(check, builder.DefaultCacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromCheck(check), builder.DefaultCacheDuration); } public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func check, TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromCheck(check, cacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromCheck(check), cacheDuration); } public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func check, TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromCheck(check, cacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromCheck(check), cacheDuration); } public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func> check) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromTaskCheck(check, builder.DefaultCacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromTaskCheck(check), builder.DefaultCacheDuration); } public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func> check) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromTaskCheck(check, builder.DefaultCacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromTaskCheck(check), builder.DefaultCacheDuration); } public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func> check, TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromTaskCheck(check, cacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromTaskCheck(check), cacheDuration); } public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func> check, TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromTaskCheck(check, cacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromTaskCheck(check), cacheDuration); } public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func> check) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check, builder.DefaultCacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check), builder.DefaultCacheDuration); } public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func> check) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check, builder.DefaultCacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check), builder.DefaultCacheDuration); } public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func> check, TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check, cacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check), cacheDuration); } public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func> check, TimeSpan cacheDuration) { Guard.ArgumentNotNull(nameof(builder), builder); - builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check, cacheDuration)); - return builder; + return builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check), cacheDuration); + } + + // IHealthCheck versions of AddCheck + + public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string checkName, IHealthCheck check) + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return builder.AddCheck(checkName, check, builder.DefaultCacheDuration); + } + + // Type versions of AddCheck + + public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name) where TCheck : class, IHealthCheck + { + Guard.ArgumentNotNull(nameof(builder), builder); + + return builder.AddCheck(name, builder.DefaultCacheDuration); } } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs index cb97eec46..f3c795629 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.HealthChecks where T : IComparable { Guard.ArgumentNotNull(nameof(builder), builder); - Guard.ArgumentNotNullOrWhitespace(nameof(name), name); + Guard.ArgumentNotNullOrEmpty(nameof(name), name); Guard.ArgumentNotNull(nameof(currentValueFunc), currentValueFunc); builder.AddCheck(name, () => @@ -23,7 +23,7 @@ namespace Microsoft.Extensions.HealthChecks var status = currentValue.CompareTo(minValue) >= 0 ? CheckStatus.Healthy : CheckStatus.Unhealthy; return HealthCheckResult.FromStatus( status, - $"{name}: min={minValue}, current={currentValue}", + $"min={minValue}, current={currentValue}", new Dictionary { { "min", minValue }, { "current", currentValue } } ); }); @@ -35,16 +35,16 @@ namespace Microsoft.Extensions.HealthChecks where T : IComparable { Guard.ArgumentNotNull(nameof(builder), builder); - Guard.ArgumentNotNullOrWhitespace(nameof(name), name); + Guard.ArgumentNotNullOrEmpty(nameof(name), name); Guard.ArgumentNotNull(nameof(currentValueFunc), currentValueFunc); - builder.AddCheck($"{name}", () => + builder.AddCheck(name, () => { var currentValue = currentValueFunc(); var status = currentValue.CompareTo(maxValue) <= 0 ? CheckStatus.Healthy : CheckStatus.Unhealthy; return HealthCheckResult.FromStatus( status, - $"{name}: max={maxValue}, current={currentValue}", + $"max={maxValue}, current={currentValue}", new Dictionary { { "max", maxValue }, { "current", currentValue } } ); }); diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs index 6ab393547..d7df58def 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.HealthChecks.Internal; @@ -37,73 +35,12 @@ namespace Microsoft.Extensions.HealthChecks Func> checkFunc) { Guard.ArgumentNotNull(nameof(builder), builder); - Guard.ArgumentNotNullOrWhitespace(nameof(url), url); + Guard.ArgumentNotNullOrEmpty(nameof(url), url); Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); var urlCheck = new UrlChecker(checkFunc, url); builder.AddCheck($"UrlCheck({url})", () => urlCheck.CheckAsync()); return builder; } - - public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable urlItems, string groupName) - => AddUrlChecks(builder, urlItems, groupName, CheckStatus.Warning, response => UrlChecker.DefaultUrlCheck(response)); - - public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable urlItems, string groupName, - Func checkFunc) - { - Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); - - return AddUrlChecks(builder, urlItems, groupName, CheckStatus.Warning, response => new ValueTask(checkFunc(response))); - } - - public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable urlItems, string groupName, - Func> checkFunc) - { - Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); - - return AddUrlChecks(builder, urlItems, groupName, CheckStatus.Warning, response => new ValueTask(checkFunc(response))); - } - - public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable urlItems, string groupName, - Func> checkFunc) - { - Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); - - return AddUrlChecks(builder, urlItems, groupName, CheckStatus.Warning, response => checkFunc(response)); - } - - public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable urlItems, string groupName, - CheckStatus partialSuccessStatus) - => AddUrlChecks(builder, urlItems, groupName, partialSuccessStatus, response => UrlChecker.DefaultUrlCheck(response)); - - public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable urlItems, string groupName, - CheckStatus partialSuccessStatus, Func checkFunc) - { - Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); - - return AddUrlChecks(builder, urlItems, groupName, partialSuccessStatus, response => new ValueTask(checkFunc(response))); - } - - public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable urlItems, string groupName, - CheckStatus partialSuccessStatus, Func> checkFunc) - { - Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); - - return AddUrlChecks(builder, urlItems, groupName, partialSuccessStatus, response => new ValueTask(checkFunc(response))); - } - - public static HealthCheckBuilder AddUrlChecks(this HealthCheckBuilder builder, IEnumerable urlItems, string groupName, - CheckStatus partialSuccessStatus, Func> checkFunc) - { - var urls = urlItems?.ToArray(); - - Guard.ArgumentNotNull(nameof(builder), builder); - Guard.ArgumentNotNullOrEmpty(nameof(urlItems), urls); - Guard.ArgumentNotNullOrWhitespace(nameof(groupName), groupName); - - var urlChecker = new UrlChecker(checkFunc, urls) { PartiallyHealthyStatus = partialSuccessStatus }; - builder.AddCheck($"UrlChecks({groupName})", () => urlChecker.CheckAsync()); - return builder; - } } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CompositeHealthCheckResult.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CompositeHealthCheckResult.cs index 5a3367843..6894ce85f 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CompositeHealthCheckResult.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/CompositeHealthCheckResult.cs @@ -7,7 +7,6 @@ using System.Linq; namespace Microsoft.Extensions.HealthChecks { - // REVIEW: Does this need to be thread safe? ///

/// Represents a composite health check result built from several results. /// @@ -31,17 +30,23 @@ namespace Microsoft.Extensions.HealthChecks { var checkStatuses = new HashSet(_results.Select(x => x.Value.CheckStatus)); if (checkStatuses.Count == 0) + { return _initialStatus; + } if (checkStatuses.Count == 1) + { return checkStatuses.First(); + } if (checkStatuses.Contains(CheckStatus.Healthy)) + { return _partiallyHealthyStatus; + } return CheckStatus.Unhealthy; } } - public string Description => string.Join(Environment.NewLine, _results.Select(r => r.Value.Description)); + public string Description => string.Join(Environment.NewLine, _results.Select(r => $"{r.Key}: {r.Value.Description}")); public IReadOnlyDictionary Data { @@ -58,23 +63,21 @@ namespace Microsoft.Extensions.HealthChecks public IReadOnlyDictionary Results => _results; - // REVIEW: Should description be required? Seems redundant for success checks. - public void Add(string name, CheckStatus status, string description) => Add(name, status, description, null); public void Add(string name, CheckStatus status, string description, Dictionary data) { - Guard.ArgumentNotNullOrWhitespace(nameof(name), name); - Guard.ArgumentValid(status != CheckStatus.Unknown, nameof(status), "Cannot add unknown status to composite health check result"); - Guard.ArgumentNotNullOrWhitespace(nameof(description), description); + Guard.ArgumentNotNullOrEmpty(nameof(name), name); + Guard.ArgumentValid(status != CheckStatus.Unknown, nameof(status), "Cannot add 'Unknown' status to composite health check result."); + Guard.ArgumentNotNullOrEmpty(nameof(description), description); _results.Add(name, HealthCheckResult.FromStatus(status, description, data)); } public void Add(string name, IHealthCheckResult checkResult) { - Guard.ArgumentNotNullOrWhitespace(nameof(name), name); + Guard.ArgumentNotNullOrEmpty(nameof(name), name); Guard.ArgumentNotNull(nameof(checkResult), checkResult); _results.Add(name, checkResult); diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheck.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheck.cs index d068a7d05..5e1caa2ff 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheck.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheck.cs @@ -9,68 +9,34 @@ namespace Microsoft.Extensions.HealthChecks { public class HealthCheck : IHealthCheck { - private DateTimeOffset _cacheExpiration; - private IHealthCheckResult _cachedResult; - private volatile int _writerCount; - - protected HealthCheck(Func> check, TimeSpan cacheDuration) + protected HealthCheck(Func> check) { Guard.ArgumentNotNull(nameof(check), check); - Guard.ArgumentValid(cacheDuration >= TimeSpan.Zero, nameof(cacheDuration), "Cache duration must either be zero (disabled) or a positive value"); Check = check; - CacheDuration = cacheDuration; } - public TimeSpan CacheDuration { get; } - protected Func> Check { get; } - protected virtual DateTimeOffset UtcNow => DateTimeOffset.UtcNow; - - public async ValueTask CheckAsync(CancellationToken cancellationToken) - { - while (_cacheExpiration <= UtcNow) - { - // Can't use a standard lock here because of async, so we'll use this flag to determine when we should write a value, - // and the waiters who aren't allowed to write will just spin wait for the new value. - if (Interlocked.Exchange(ref _writerCount, 1) != 0) - { - await Task.Delay(5, cancellationToken).ConfigureAwait(false); - continue; - } - - try - { - _cachedResult = await Check(cancellationToken).ConfigureAwait(false); - _cacheExpiration = UtcNow + CacheDuration; - break; - } - finally - { - _writerCount = 0; - } - } - - return _cachedResult; - } + public ValueTask CheckAsync(CancellationToken cancellationToken = default(CancellationToken)) + => Check(cancellationToken); - public static HealthCheck FromCheck(Func check, TimeSpan cacheDuration) - => new HealthCheck(token => new ValueTask(check()), cacheDuration); + public static HealthCheck FromCheck(Func check) + => new HealthCheck(token => new ValueTask(check())); - public static HealthCheck FromCheck(Func check, TimeSpan cacheDuration) - => new HealthCheck(token => new ValueTask(check(token)), cacheDuration); + public static HealthCheck FromCheck(Func check) + => new HealthCheck(token => new ValueTask(check(token))); - public static HealthCheck FromTaskCheck(Func> check, TimeSpan cacheDuration) - => new HealthCheck(token => new ValueTask(check()), cacheDuration); + public static HealthCheck FromTaskCheck(Func> check) + => new HealthCheck(token => new ValueTask(check())); - public static HealthCheck FromTaskCheck(Func> check, TimeSpan cacheDuration) - => new HealthCheck(token => new ValueTask(check(token)), cacheDuration); + public static HealthCheck FromTaskCheck(Func> check) + => new HealthCheck(token => new ValueTask(check(token))); - public static HealthCheck FromValueTaskCheck(Func> check, TimeSpan cacheDuration) - => new HealthCheck(token => check(), cacheDuration); + public static HealthCheck FromValueTaskCheck(Func> check) + => new HealthCheck(token => check()); - public static HealthCheck FromValueTaskCheck(Func> check, TimeSpan cacheDuration) - => new HealthCheck(check, cacheDuration); + public static HealthCheck FromValueTaskCheck(Func> check) + => new HealthCheck(check); } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs index f6cc17304..006e4a6ef 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs @@ -8,33 +8,128 @@ namespace Microsoft.Extensions.HealthChecks { public class HealthCheckBuilder { - private readonly Dictionary _checks; + private readonly Dictionary _checksByName; + private readonly HealthCheckGroup _currentGroup; + private readonly Dictionary _groups; public HealthCheckBuilder() { - _checks = new Dictionary(StringComparer.OrdinalIgnoreCase); + _checksByName = new Dictionary(StringComparer.OrdinalIgnoreCase); + _currentGroup = new HealthCheckGroup(string.Empty, CheckStatus.Unhealthy); + _groups = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [string.Empty] = _currentGroup + }; + DefaultCacheDuration = TimeSpan.FromMinutes(5); } - public IReadOnlyDictionary Checks => _checks; + /// + /// This constructor should only be used when creating a grouped health check builder. + /// + public HealthCheckBuilder(HealthCheckBuilder rootBuilder, HealthCheckGroup currentGroup) + { + Guard.ArgumentNotNull(nameof(rootBuilder), rootBuilder); + Guard.ArgumentNotNull(nameof(currentGroup), currentGroup); + + _checksByName = rootBuilder._checksByName; + _currentGroup = currentGroup; + _groups = rootBuilder._groups; + + DefaultCacheDuration = rootBuilder.DefaultCacheDuration; + } + + /// + /// Gets the registered checks, indexed by check name. + /// + public IReadOnlyDictionary ChecksByName => _checksByName; + /// + /// Gets the current default cache duration used when registering checks. + /// public TimeSpan DefaultCacheDuration { get; private set; } - public HealthCheckBuilder AddCheck(string name, IHealthCheck check) + /// + /// Gets the registered groups, indexed by group name. The root group's name is . + /// + public IReadOnlyDictionary Groups => _groups; + + /// + /// Registers a health check type that will later be resolved via dependency + /// injection. + /// + public HealthCheckBuilder AddCheck(string checkName, TimeSpan cacheDuration) where TCheck : class, IHealthCheck { - Guard.ArgumentNotNullOrWhitespace(nameof(name), name); + Guard.ArgumentNotNullOrEmpty(nameof(checkName), checkName); + Guard.ArgumentValid(!_checksByName.ContainsKey(checkName), nameof(checkName), $"A check with name '{checkName}' has already been registered."); + + var namedCheck = CachedHealthCheck.FromType(checkName, cacheDuration, typeof(TCheck)); + + _checksByName.Add(checkName, namedCheck); + _currentGroup.ChecksInternal.Add(namedCheck); + + return this; + } + + /// + /// Registers a concrete health check to the builder. + /// + public HealthCheckBuilder AddCheck(string checkName, IHealthCheck check, TimeSpan cacheDuration) + { + Guard.ArgumentNotNullOrEmpty(nameof(checkName), checkName); Guard.ArgumentNotNull(nameof(check), check); + Guard.ArgumentValid(!_checksByName.ContainsKey(checkName), nameof(checkName), $"A check with name '{checkName}' has already been registered."); + + var namedCheck = CachedHealthCheck.FromHealthCheck(checkName, cacheDuration, check); + + _checksByName.Add(checkName, namedCheck); + _currentGroup.ChecksInternal.Add(namedCheck); + + return this; + } + + /// + /// Creates a new health check group, to which you can add one or more health + /// checks. Uses when the group is + /// partially successful. + /// + public HealthCheckBuilder AddHealthCheckGroup(string groupName, Action groupChecks) + => AddHealthCheckGroup(groupName, groupChecks, CheckStatus.Unhealthy); + + /// + /// Creates a new health check group, to which you can add one or more health + /// checks. + /// + public HealthCheckBuilder AddHealthCheckGroup(string groupName, Action groupChecks, CheckStatus partialSuccessStatus) + { + Guard.ArgumentNotNullOrEmpty(nameof(groupName), groupName); + Guard.ArgumentNotNull(nameof(groupChecks), groupChecks); + Guard.ArgumentValid(partialSuccessStatus != CheckStatus.Unknown, nameof(partialSuccessStatus), "Check status 'Unknown' is not valid for partial success."); + Guard.ArgumentValid(!_groups.ContainsKey(groupName), nameof(groupName), $"A group with name '{groupName}' has already been registered."); + Guard.OperationValid(_currentGroup.GroupName == string.Empty, "Nested groups are not supported by HealthCheckBuilder."); + + var group = new HealthCheckGroup(groupName, partialSuccessStatus); + _groups.Add(groupName, group); + + var innerBuilder = new HealthCheckBuilder(this, group); + groupChecks(innerBuilder); - _checks.Add(name, check); return this; } public HealthCheckBuilder WithDefaultCacheDuration(TimeSpan duration) { - Guard.ArgumentValid(duration >= TimeSpan.Zero, nameof(duration), "Duration must be zero (disabled) or a positive duration"); + Guard.ArgumentValid(duration >= TimeSpan.Zero, nameof(duration), "Duration must be zero (disabled) or a positive duration."); DefaultCacheDuration = duration; return this; } + + public HealthCheckBuilder WithPartialSuccessStatus(CheckStatus partiallyHealthyStatus) + { + _currentGroup.PartiallyHealthyStatus = partiallyHealthyStatus; + + return this; + } } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckExtensions.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckExtensions.cs deleted file mode 100644 index 2669afc77..000000000 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.HealthChecks -{ - public static class HealthCheckExtensions - { - public static ValueTask CheckAsync(this IHealthCheck healthCheck) - { - Guard.ArgumentNotNull(nameof(healthCheck), healthCheck); - - return healthCheck.CheckAsync(CancellationToken.None); - } - } -} diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckGroup.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckGroup.cs new file mode 100644 index 000000000..18c55132b --- /dev/null +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckGroup.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.HealthChecks +{ + public class HealthCheckGroup + { + private CheckStatus _partialSuccessStatus; + + public HealthCheckGroup(string groupName, CheckStatus partialSuccessStatus) + { + Guard.ArgumentNotNull(nameof(groupName), groupName); + + GroupName = groupName; + PartiallyHealthyStatus = partialSuccessStatus; + } + + public IReadOnlyList Checks => ChecksInternal.AsReadOnly(); + + internal List ChecksInternal { get; } = new List(); + + public string GroupName { get; } + + public CheckStatus PartiallyHealthyStatus + { + get => _partialSuccessStatus; + internal set + { + Guard.ArgumentValid(value != CheckStatus.Unknown, nameof(value), "Check status 'Unknown' is not valid for partial success."); + + _partialSuccessStatus = value; + } + } + } +} diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckResult.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckResult.cs index 7ea31b42a..d8ef80dc4 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckResult.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckResult.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Linq; namespace Microsoft.Extensions.HealthChecks { diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckService.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckService.cs index 6e16bed61..1d2934e0e 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckService.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckService.cs @@ -3,52 +3,117 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.HealthChecks { public class HealthCheckService : IHealthCheckService { - public IReadOnlyDictionary _checks; + private readonly HealthCheckBuilder _builder; + private readonly IReadOnlyList _groups; + private readonly HealthCheckGroup _root; + private readonly IServiceProvider _serviceProvider; + private readonly IServiceScopeFactory _serviceScopeFactory; - private ILogger _logger; - - public HealthCheckService(HealthCheckBuilder builder, ILogger logger) + public HealthCheckService(HealthCheckBuilder builder, IServiceProvider serviceProvider, IServiceScopeFactory serviceScopeFactory) { - _checks = builder.Checks; - _logger = logger; + _builder = builder; + _groups = GetGroups().Where(group => group.GroupName != string.Empty).ToList(); + _root = GetGroup(string.Empty); + _serviceProvider = serviceProvider; + _serviceScopeFactory = serviceScopeFactory; } - public async Task CheckHealthAsync(CheckStatus partiallyHealthyStatus, CancellationToken cancellationToken) + public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) { - var logMessage = new StringBuilder(); - var result = new CompositeHealthCheckResult(partiallyHealthyStatus); - - foreach (var check in _checks) + using (var scope = GetServiceScope()) { - try - { - var healthCheckResult = await check.Value.CheckAsync().ConfigureAwait(false); - logMessage.AppendLine($"HealthCheck: {check.Key} : {healthCheckResult.CheckStatus}"); - result.Add(check.Key, healthCheckResult); - } - catch (Exception ex) + var scopeServiceProvider = scope.ServiceProvider; + var groupTasks = _groups.Select(group => new { Group = group, Task = RunGroupAsync(scopeServiceProvider, group, cancellationToken) }).ToList(); + var result = await RunGroupAsync(scopeServiceProvider, _root, cancellationToken).ConfigureAwait(false); + + await Task.WhenAll(groupTasks.Select(x => x.Task)); + + foreach (var groupTask in groupTasks) { - logMessage.AppendLine($"HealthCheck: {check.Key} : Exception {ex.GetType().FullName} thrown"); - result.Add(check.Key, CheckStatus.Unhealthy, $"Exception during check: {ex.GetType().FullName}"); + result.Add($"Group({groupTask.Group.GroupName})", groupTask.Task.Result); } + + return result; } + } + + public IReadOnlyList GetAllChecks() + => _builder.ChecksByName.Values.ToList().AsReadOnly(); - if (logMessage.Length == 0) - logMessage.AppendLine("HealthCheck: No checks have been registered"); + public CachedHealthCheck GetCheck(string checkName) + => _builder.ChecksByName[checkName]; + + public HealthCheckGroup GetGroup(string groupName) + => _builder.Groups[groupName]; + + public IReadOnlyList GetGroups() + => _builder.Groups.Values.ToList().AsReadOnly(); + + private IServiceScope GetServiceScope() + => _serviceScopeFactory == null ? new UnscopedServiceProvider(_serviceProvider) : _serviceScopeFactory.CreateScope(); + + public async ValueTask RunCheckAsync(CachedHealthCheck healthCheck, CancellationToken cancellationToken = default(CancellationToken)) + { + using (var scope = GetServiceScope()) + { + return await RunCheckAsync(scope.ServiceProvider, healthCheck, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Uses the provided service provider and executes the provided check. + /// + public ValueTask RunCheckAsync(IServiceProvider serviceProvider, CachedHealthCheck healthCheck, CancellationToken cancellationToken = default(CancellationToken)) + { + Guard.ArgumentNotNull(nameof(serviceProvider), serviceProvider); + Guard.ArgumentNotNull(nameof(healthCheck), healthCheck); + + return healthCheck.RunAsync(serviceProvider, cancellationToken); + } + + /// + /// Creates a new resolution scope from the default service provider and executes the checks in the given group. + /// + public async Task RunGroupAsync(HealthCheckGroup group, CancellationToken cancellationToken = default(CancellationToken)) + { + using (var scope = GetServiceScope()) + return await RunGroupAsync(scope.ServiceProvider, group, cancellationToken).ConfigureAwait(false); + } + + /// + /// Uses the provided service provider and executes the checks in the given group. + /// + public async Task RunGroupAsync(IServiceProvider serviceProvider, HealthCheckGroup group, CancellationToken cancellationToken = default(CancellationToken)) + { + var result = new CompositeHealthCheckResult(group.PartiallyHealthyStatus); + var checkTasks = group.Checks.Select(check => new { Check = check, Task = check.RunAsync(serviceProvider, cancellationToken).AsTask() }).ToList(); + await Task.WhenAll(checkTasks.Select(checkTask => checkTask.Task)); + + foreach (var checkTask in checkTasks) + { + result.Add(checkTask.Check.Name, checkTask.Task.Result); + } - _logger.Log((result.CheckStatus == CheckStatus.Healthy ? LogLevel.Information : LogLevel.Error), 0, logMessage.ToString(), null, MessageFormatter); return result; } - private static string MessageFormatter(string state, Exception error) => state; + private class UnscopedServiceProvider : IServiceScope + { + public UnscopedServiceProvider(IServiceProvider serviceProvider) + => ServiceProvider = serviceProvider; + + public IServiceProvider ServiceProvider { get; } + + public void Dispose() { } + } } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckServiceCollectionExtensions.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckServiceCollectionExtensions.cs index cdd763d9f..678731737 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckServiceCollectionExtensions.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckServiceCollectionExtensions.cs @@ -2,20 +2,28 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using Microsoft.Extensions.HealthChecks; namespace Microsoft.Extensions.DependencyInjection { public static class HealthCheckServiceCollectionExtensions { - public static IServiceCollection AddHealthChecks(this IServiceCollection services, Action checkupAction) + private static readonly Type HealthCheckServiceInterface = typeof(IHealthCheckService); + + public static IServiceCollection AddHealthChecks(this IServiceCollection services, Action checks) { - var checkupBuilder = new HealthCheckBuilder(); + Guard.OperationValid(!services.Any(descriptor => descriptor.ServiceType == HealthCheckServiceInterface), "AddHealthChecks may only be called once."); + + var builder = new HealthCheckBuilder(); - checkupAction.Invoke(checkupBuilder); + services.AddSingleton(serviceProvider => + { + var serviceScopeFactory = serviceProvider.GetService(); + return new HealthCheckService(builder, serviceProvider, serviceScopeFactory); + }); - services.AddSingleton(checkupBuilder); - services.AddSingleton(); + checks(builder); return services; } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckServiceExtensions.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckServiceExtensions.cs deleted file mode 100644 index 98ab54b70..000000000 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/HealthCheckServiceExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.HealthChecks -{ - public static class HealthCheckServiceExtensions - { - public static Task CheckHealthAsync(this IHealthCheckService service) - { - Guard.ArgumentNotNull(nameof(service), service); - - return service.CheckHealthAsync(CheckStatus.Unhealthy, CancellationToken.None); - } - - public static Task CheckHealthAsync(this IHealthCheckService service, CheckStatus partiallyHealthyStatus) - { - Guard.ArgumentNotNull(nameof(service), service); - - return service.CheckHealthAsync(partiallyHealthyStatus, CancellationToken.None); - } - - public static Task CheckHealthAsync(this IHealthCheckService service, CancellationToken cancellationToken) - { - Guard.ArgumentNotNull(nameof(service), service); - - return service.CheckHealthAsync(CheckStatus.Unhealthy, cancellationToken); - } - public static Task CheckHealthAsync(this IHealthCheckService service, CheckStatus partiallyHealthyStatus, CancellationToken cancellationToken) - { - Guard.ArgumentNotNull(nameof(service), service); - - return service.CheckHealthAsync(partiallyHealthyStatus, cancellationToken); - } - } -} diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/IHealthCheck.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/IHealthCheck.cs index b5b95405f..e4aa45d28 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/IHealthCheck.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/IHealthCheck.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Threading; using System.Threading.Tasks; @@ -9,8 +8,6 @@ namespace Microsoft.Extensions.HealthChecks { public interface IHealthCheck { - TimeSpan CacheDuration { get; } - - ValueTask CheckAsync(CancellationToken cancellationToken); + ValueTask CheckAsync(CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/IHealthCheckService.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/IHealthCheckService.cs index 60e917264..17a49cb00 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/IHealthCheckService.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/IHealthCheckService.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,6 +10,49 @@ namespace Microsoft.Extensions.HealthChecks { public interface IHealthCheckService { - Task CheckHealthAsync(CheckStatus partiallyHealthyStatus, CancellationToken cancellationToken); + /// + /// Runs all registered health checks. + /// + Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Gets all registered health checks as a flat list. + /// + IReadOnlyList GetAllChecks(); + + /// + /// Gets a health check by name. + /// + CachedHealthCheck GetCheck(string checkName); + + /// + /// Gets all health checks in a group. + /// + HealthCheckGroup GetGroup(string groupName); + + /// + /// Gets all the health check groups. + /// + IReadOnlyList GetGroups(); + + /// + /// Creates a new resolution scope from the default service provider and executes the provided check. + /// + ValueTask RunCheckAsync(CachedHealthCheck healthCheck, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Uses the provided service provider and executes the provided check. + /// + ValueTask RunCheckAsync(IServiceProvider serviceProvider, CachedHealthCheck healthCheck, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Creates a new resolution scope from the default service provider and executes the checks in the given group. + /// + Task RunGroupAsync(HealthCheckGroup group, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Uses the provided service provider and executes the checks in the given group. + /// + Task RunGroupAsync(IServiceProvider serviceProvider, HealthCheckGroup group, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Internal/UrlChecker.cs b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Internal/UrlChecker.cs index ad021b149..56800d334 100644 --- a/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Internal/UrlChecker.cs +++ b/src/BuildingBlocks/HealthChecks/src/Microsoft.Extensions.HealthChecks/Internal/UrlChecker.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; @@ -13,55 +12,33 @@ namespace Microsoft.Extensions.HealthChecks.Internal public class UrlChecker { private readonly Func> _checkFunc; - private readonly string[] _urls; + private readonly string _url; - // REVIEW: Cache timeout here? - public UrlChecker(Func> checkFunc, params string[] urls) + public UrlChecker(Func> checkFunc, string url) { Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); - Guard.ArgumentNotNullOrEmpty(nameof(urls), urls); + Guard.ArgumentNotNullOrEmpty(nameof(url), url); _checkFunc = checkFunc; - _urls = urls; + _url = url; } public CheckStatus PartiallyHealthyStatus { get; set; } = CheckStatus.Warning; - public Task CheckAsync() - => _urls.Length == 1 ? CheckSingleAsync() : CheckMultiAsync(); - - public async Task CheckSingleAsync() - { - var httpClient = CreateHttpClient(); - var result = default(IHealthCheckResult); - await CheckUrlAsync(httpClient, _urls[0], (_, checkResult) => result = checkResult).ConfigureAwait(false); - return result; - } - - public async Task CheckMultiAsync() + public async Task CheckAsync() { - var composite = new CompositeHealthCheckResult(PartiallyHealthyStatus); - var httpClient = CreateHttpClient(); - - // REVIEW: Should these be done in parallel? - foreach (var url in _urls) - await CheckUrlAsync(httpClient, url, (name, checkResult) => composite.Add(name, checkResult)).ConfigureAwait(false); - - return composite; - } - - private async Task CheckUrlAsync(HttpClient httpClient, string url, Action adder) - { - var name = $"UrlCheck({url})"; - try - { - var response = await httpClient.GetAsync(url).ConfigureAwait(false); - var result = await _checkFunc(response); - adder(name, result); - } - catch (Exception ex) + using (var httpClient = CreateHttpClient()) { - adder(name, HealthCheckResult.Unhealthy($"Exception during check: {ex.GetType().FullName}")); + try + { + var response = await httpClient.GetAsync(_url).ConfigureAwait(false); + return await _checkFunc(response); + } + catch (Exception ex) + { + var data = new Dictionary { { "url", _url } }; + return HealthCheckResult.Unhealthy($"Exception during check: {ex.GetType().FullName}", data); + } } } @@ -74,8 +51,7 @@ namespace Microsoft.Extensions.HealthChecks.Internal public static async ValueTask DefaultUrlCheck(HttpResponseMessage response) { - // REVIEW: Should this be an explicit 200 check, or just an "is success" check? - var status = response.StatusCode == HttpStatusCode.OK ? CheckStatus.Healthy : CheckStatus.Unhealthy; + var status = response.IsSuccessStatusCode ? CheckStatus.Healthy : CheckStatus.Unhealthy; var data = new Dictionary { { "url", response.RequestMessage.RequestUri.ToString() }, @@ -83,7 +59,7 @@ namespace Microsoft.Extensions.HealthChecks.Internal { "reason", response.ReasonPhrase }, { "body", await response.Content?.ReadAsStringAsync() } }; - return HealthCheckResult.FromStatus(status, $"UrlCheck({response.RequestMessage.RequestUri}): status code {response.StatusCode} ({(int)response.StatusCode})", data); + return HealthCheckResult.FromStatus(status, $"status code {response.StatusCode} ({(int)response.StatusCode})", data); } protected virtual HttpClient GetHttpClient() diff --git a/src/BuildingBlocks/HealthChecks/src/common/Guard.cs b/src/BuildingBlocks/HealthChecks/src/common/Guard.cs index 8ed91054e..9f394be51 100644 --- a/src/BuildingBlocks/HealthChecks/src/common/Guard.cs +++ b/src/BuildingBlocks/HealthChecks/src/common/Guard.cs @@ -9,37 +9,49 @@ static class Guard public static void ArgumentNotNull(string argumentName, object value) { if (value == null) + { throw new ArgumentNullException(argumentName); + } } - public static void ArgumentNotNullOrEmpty(string argumentName, string value) + public static void ArgumentNotNullOrEmpty(string argumentName, string value) { if (value == null) + { throw new ArgumentNullException(argumentName); + } if (string.IsNullOrEmpty(value)) - throw new ArgumentException("Value cannot be an empty string", argumentName); + { + throw new ArgumentException("Value cannot be an empty string.", argumentName); + } } // Use IReadOnlyCollection instead of IEnumerable to discourage double enumeration public static void ArgumentNotNullOrEmpty(string argumentName, IReadOnlyCollection items) { if (items == null) + { throw new ArgumentNullException(argumentName); + } if (items.Count == 0) - throw new ArgumentException("Collection must contain at least one item", argumentName); + { + throw new ArgumentException("Collection must contain at least one item.", argumentName); + } } - public static void ArgumentNotNullOrWhitespace(string argumentName, string value) + public static void ArgumentValid(bool valid, string argumentName, string exceptionMessage) { - if (value == null) - throw new ArgumentNullException(argumentName); - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException("Value must contain a non-whitespace value", argumentName); + if (!valid) + { + throw new ArgumentException(exceptionMessage, argumentName); + } } - public static void ArgumentValid(bool valid, string argumentName, string exceptionMessage) + public static void OperationValid(bool valid, string exceptionMessage) { if (!valid) - throw new ArgumentException(exceptionMessage, argumentName); + { + throw new InvalidOperationException(exceptionMessage); + } } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs index f9c43afc0..70759cc03 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/App.xaml.cs @@ -27,8 +27,7 @@ namespace eShopOnContainers private void InitApp() { UseMockServices = Settings.UseMocks; - ViewModelLocator.Initialize(); - ViewModelLocator.UpdateDependencies(UseMockServices); + ViewModelLocator.RegisterDependencies(UseMockServices); } private Task InitNavigation() diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Behaviors/EventToCommandBehavior.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Behaviors/EventToCommandBehavior.cs index af70fc2d9..ee0aa89e3 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Behaviors/EventToCommandBehavior.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Behaviors/EventToCommandBehavior.cs @@ -31,7 +31,7 @@ namespace eShopOnContainers.Core.Behaviors BindableProperty.CreateAttached("EventArgsConverterParameter", typeof(object), typeof(EventToCommandBehavior), null, BindingMode.OneWay); - private Delegate _handler; + protected Delegate _handler; private EventInfo _eventInfo; public string EventName diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Helpers/Settings.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Helpers/Settings.cs index 97223631e..24da4fd2b 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Helpers/Settings.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Helpers/Settings.cs @@ -1,4 +1,3 @@ -using eShopOnContainers.Core.ViewModels.Base; using Plugin.Settings; using Plugin.Settings.Abstractions; @@ -32,7 +31,6 @@ namespace eShopOnContainers.Core.Helpers #endregion - public static string AuthAccessToken { get @@ -57,7 +55,6 @@ namespace eShopOnContainers.Core.Helpers } } - public static bool UseMocks { get diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/BasketService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/BasketService.cs index ae7617115..c3607c1e3 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/BasketService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Basket/BasketService.cs @@ -16,8 +16,7 @@ namespace eShopOnContainers.Core.Services.Basket } public async Task GetBasketAsync(string guidUser, string token) - { - + { UriBuilder builder = new UriBuilder(GlobalSetting.Instance.BasketEndpoint); builder.Path = guidUser; @@ -30,18 +29,17 @@ namespace eShopOnContainers.Core.Services.Basket ServicesHelper.FixBasketItemPictureUri(basket?.Items); return basket; - } public async Task UpdateBasketAsync(CustomerBasket customerBasket, string token) { - UriBuilder builder = new UriBuilder(GlobalSetting.Instance.BasketEndpoint); + UriBuilder builder = new UriBuilder(GlobalSetting.Instance.BasketEndpoint); - string uri = builder.ToString(); + string uri = builder.ToString(); - var result = await _requestProvider.PostAsync(uri, customerBasket, token); + var result = await _requestProvider.PostAsync(uri, customerBasket, token); - return result; + return result; } public async Task ClearBasketAsync(string guidUser, string token) diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/CatalogMockService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/CatalogMockService.cs index b37226b33..d58b2b698 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/CatalogMockService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/CatalogMockService.cs @@ -61,12 +61,5 @@ namespace eShopOnContainers.Core.Services.Catalog return MockCatalogType; } - - public async Task GetCatalogItemAsync(string id) - { - await Task.Delay(500); - - return MockCatalog.FirstOrDefault(c => c.Id == id); - } } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/CatalogService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/CatalogService.cs index bbed8a9ed..40807b2f5 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/CatalogService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/CatalogService.cs @@ -20,7 +20,6 @@ namespace eShopOnContainers.Core.Services.Catalog public async Task> FilterAsync(int catalogBrandId, int catalogTypeId) { - UriBuilder builder = new UriBuilder(GlobalSetting.Instance.CatalogEndpoint); builder.Path = string.Format("api/v1/catalog/items/type/{0}/brand/{1}", catalogTypeId, catalogBrandId); @@ -34,12 +33,10 @@ namespace eShopOnContainers.Core.Services.Catalog return catalog?.Data.ToObservableCollection(); else return new ObservableCollection(); - } public async Task> GetCatalogAsync() { - UriBuilder builder = new UriBuilder(GlobalSetting.Instance.CatalogEndpoint); builder.Path = "api/v1/catalog/items"; @@ -56,37 +53,28 @@ namespace eShopOnContainers.Core.Services.Catalog return catalog?.Data.ToObservableCollection(); } else - return new ObservableCollection(); - - } - - public Task GetCatalogItemAsync(string id) - { - throw new NotImplementedException(); + return new ObservableCollection(); } public async Task> GetCatalogBrandAsync() { + UriBuilder builder = new UriBuilder(GlobalSetting.Instance.CatalogEndpoint); - UriBuilder builder = new UriBuilder(GlobalSetting.Instance.CatalogEndpoint); - - builder.Path = "api/v1/catalog/catalogbrands"; - - string uri = builder.ToString(); + builder.Path = "api/v1/catalog/catalogbrands"; - IEnumerable brands = - await _requestProvider.GetAsync>(uri); + string uri = builder.ToString(); - if (brands != null) - return brands?.ToObservableCollection(); - else - return new ObservableCollection(); + IEnumerable brands = + await _requestProvider.GetAsync>(uri); + if (brands != null) + return brands?.ToObservableCollection(); + else + return new ObservableCollection(); } public async Task> GetCatalogTypeAsync() { - UriBuilder builder = new UriBuilder(GlobalSetting.Instance.CatalogEndpoint); builder.Path = "api/v1/catalog/catalogtypes"; @@ -99,8 +87,7 @@ namespace eShopOnContainers.Core.Services.Catalog if (types != null) return types.ToObservableCollection(); else - return new ObservableCollection(); - + return new ObservableCollection(); } } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/ICatalogService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/ICatalogService.cs index 508da75d8..74d6aa91b 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/ICatalogService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Catalog/ICatalogService.cs @@ -10,6 +10,5 @@ namespace eShopOnContainers.Core.Services.Catalog Task> FilterAsync(int catalogBrandId, int catalogTypeId); Task> GetCatalogTypeAsync(); Task> GetCatalogAsync(); - Task GetCatalogItemAsync(string id); } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs index 118690002..cc3df9a4a 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs @@ -1,11 +1,8 @@ -using System.Threading.Tasks; - -namespace eShopOnContainers.Core.Services.Identity +namespace eShopOnContainers.Core.Services.Identity { public interface IIdentityService { - string CreateAuthorizeRequest(); + string CreateAuthorizationRequest(); string CreateLogoutRequest(string token); - string DecodeToken(string token); } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs index bfb938b18..de80cec93 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs @@ -1,19 +1,15 @@ using IdentityModel.Client; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace eShopOnContainers.Core.Services.Identity { public class IdentityService : IIdentityService { - public string CreateAuthorizeRequest() + public string CreateAuthorizationRequest() { - // Create URI to authorize endpoint - var authorizeRequest = - new AuthorizeRequest(GlobalSetting.Instance.IdentityEndpoint); + // Create URI to authorization endpoint + var authorizeRequest = new AuthorizeRequest(GlobalSetting.Instance.IdentityEndpoint); // Dictionary with values for the authorize request var dic = new Dictionary(); @@ -29,7 +25,6 @@ namespace eShopOnContainers.Core.Services.Identity dic.Add("state", currentCSRFToken); var authorizeUri = authorizeRequest.Create(dic); - return authorizeUri; } @@ -45,30 +40,5 @@ namespace eShopOnContainers.Core.Services.Identity token, GlobalSetting.Instance.LogoutCallback); } - - public string DecodeToken(string token) - { - var parts = token.Split('.'); - - string partToConvert = parts[1]; - partToConvert = partToConvert.Replace('-', '+'); - partToConvert = partToConvert.Replace('_', '/'); - switch (partToConvert.Length % 4) - { - case 0: - break; - case 2: - partToConvert += "=="; - break; - case 3: - partToConvert += "="; - break; - } - - var partAsBytes = Convert.FromBase64String(partToConvert); - var partAsUTF8String = Encoding.UTF8.GetString(partAsBytes, 0, partAsBytes.Count()); - - return JObject.Parse(partAsUTF8String).ToString(); - } } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs index 4bb899c90..67b8f8c59 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs @@ -6,16 +6,8 @@ namespace eShopOnContainers.Core.Services.RequestProvider { Task GetAsync(string uri, string token = ""); - Task PostAsync(string uri, TResult data, string token = ""); - Task PostAsync(string uri, TResult data, string token = "", string header = ""); - Task PostAsync(string uri, TRequest data, string token = ""); - - Task PutAsync(string uri, TResult data, string token = ""); - - Task PutAsync(string uri, TRequest data, string token = ""); - Task DeleteAsync(string uri, string token = ""); } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs index a63f2096c..f498532ca 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs @@ -5,7 +5,6 @@ using Newtonsoft.Json.Serialization; using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; using System.Threading.Tasks; using System; @@ -23,7 +22,6 @@ namespace eShopOnContainers.Core.Services.RequestProvider DateTimeZoneHandling = DateTimeZoneHandling.Utc, NullValueHandling = NullValueHandling.Ignore }; - _serializerSettings.Converters.Add(new StringEnumConverter()); } @@ -33,7 +31,6 @@ namespace eShopOnContainers.Core.Services.RequestProvider HttpResponseMessage response = await httpClient.GetAsync(uri); await HandleResponse(response); - string serialized = await response.Content.ReadAsStringAsync(); TResult result = await Task.Run(() => @@ -56,7 +53,6 @@ namespace eShopOnContainers.Core.Services.RequestProvider HttpResponseMessage response = await httpClient.PostAsync(uri, content); await HandleResponse(response); - string serialized = await response.Content.ReadAsStringAsync(); TResult result = await Task.Run(() => @@ -65,61 +61,21 @@ namespace eShopOnContainers.Core.Services.RequestProvider return result; } - public Task PostAsync(string uri, TResult data, string token = "") - { - return PostAsync(uri, data, token); - } - - public async Task PostAsync(string uri, TRequest data, string token = "") - { - HttpClient httpClient = CreateHttpClient(token); - string serialized = await Task.Run(() => JsonConvert.SerializeObject(data, _serializerSettings)); - var content = new StringContent(serialized, Encoding.UTF8, "application/json"); - HttpResponseMessage response = await httpClient.PostAsync(uri, content); - - await HandleResponse(response); - - string responseData = await response.Content.ReadAsStringAsync(); - - return await Task.Run(() => JsonConvert.DeserializeObject(responseData, _serializerSettings)); - } - - public Task PutAsync(string uri, TResult data, string token = "") - { - return PutAsync(uri, data, token); - } - - public async Task PutAsync(string uri, TRequest data, string token = "") - { - HttpClient httpClient = CreateHttpClient(token); - string serialized = await Task.Run(() => JsonConvert.SerializeObject(data, _serializerSettings)); - HttpResponseMessage response = await httpClient.PutAsync(uri, new StringContent(serialized, Encoding.UTF8, "application/json")); - - await HandleResponse(response); - - string responseData = await response.Content.ReadAsStringAsync(); - - return await Task.Run(() => JsonConvert.DeserializeObject(responseData, _serializerSettings)); - } - public async Task DeleteAsync(string uri, string token = "") { HttpClient httpClient = CreateHttpClient(token); - await httpClient.DeleteAsync(uri); } private HttpClient CreateHttpClient(string token = "") { var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (!string.IsNullOrEmpty(token)) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); } - return httpClient; } @@ -140,8 +96,8 @@ namespace eShopOnContainers.Core.Services.RequestProvider { var content = await response.Content.ReadAsStringAsync(); - if (response.StatusCode == HttpStatusCode.Forbidden - || response.StatusCode == HttpStatusCode.Unauthorized) + if (response.StatusCode == HttpStatusCode.Forbidden || + response.StatusCode == HttpStatusCode.Unauthorized) { throw new ServiceAuthenticationException(content); } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/Base/ViewModelLocator.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/Base/ViewModelLocator.cs index c9e253a2e..90abb8f09 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/Base/ViewModelLocator.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/Base/ViewModelLocator.cs @@ -1,4 +1,4 @@ -using Microsoft.Practices.Unity; +using Autofac; using eShopOnContainers.Services; using System; using System.Globalization; @@ -16,7 +16,7 @@ namespace eShopOnContainers.Core.ViewModels.Base { public static class ViewModelLocator { - private static readonly IUnityContainer _unityContainer = new UnityContainer(); + private static IContainer _container; public static readonly BindableProperty AutoWireViewModelProperty = BindableProperty.CreateAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocator), default(bool), propertyChanged: OnAutoWireViewModelChanged); @@ -33,56 +33,56 @@ namespace eShopOnContainers.Core.ViewModels.Base public static bool UseMockService { get; set; } - public static void Initialize() + public static void RegisterDependencies(bool useMockServices) { - // Services - _unityContainer.RegisterType(); - _unityContainer.RegisterType(new ContainerControlledLifetimeManager()); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); + var builder = new ContainerBuilder(); // View models - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - _unityContainer.RegisterType(); - } + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + + // Services + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); - public static void UpdateDependencies(bool useMockServices) - { - // Change injected dependencies if (useMockServices) { - _unityContainer.RegisterInstance(new CatalogMockService()); - _unityContainer.RegisterInstance(new BasketMockService()); - _unityContainer.RegisterInstance(new OrderMockService()); - _unityContainer.RegisterInstance(new UserMockService()); + builder.RegisterInstance(new CatalogMockService()).As(); + builder.RegisterInstance(new BasketMockService()).As(); + builder.RegisterInstance(new OrderMockService()).As(); + builder.RegisterInstance(new UserMockService()).As(); UseMockService = true; } else { - var requestProvider = Resolve(); - _unityContainer.RegisterInstance(new CatalogService(requestProvider)); - _unityContainer.RegisterInstance(new BasketService(requestProvider)); - _unityContainer.RegisterInstance(new OrderService(requestProvider)); - _unityContainer.RegisterInstance(new UserService(requestProvider)); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); UseMockService = false; } + + if (_container != null) + { + _container.Dispose(); + } + _container = builder.Build(); } public static T Resolve() { - return _unityContainer.Resolve(); + return _container.Resolve(); } private static void OnAutoWireViewModelChanged(BindableObject bindable, object oldValue, object newValue) @@ -103,7 +103,7 @@ namespace eShopOnContainers.Core.ViewModels.Base { return; } - var viewModel = _unityContainer.Resolve(viewModelType); + var viewModel = _container.Resolve(viewModelType); view.BindingContext = viewModel; } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/CatalogViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/CatalogViewModel.cs index 554c49852..abc7f70ac 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/CatalogViewModel.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/CatalogViewModel.cs @@ -5,8 +5,6 @@ using Xamarin.Forms; using eShopOnContainers.Core.Models.Catalog; using eShopOnContainers.Core.Services.Catalog; using System.Windows.Input; -using eShopOnContainers.Core.Services.Basket; -using eShopOnContainers.Core.Services.User; namespace eShopOnContainers.Core.ViewModels { diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs index 21d6c716b..c174e36c8 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs @@ -188,7 +188,7 @@ namespace eShopOnContainers.Core.ViewModels await Task.Delay(500); - LoginUrl = _identityService.CreateAuthorizeRequest(); + LoginUrl = _identityService.CreateAuthorizationRequest(); IsValid = true; IsLogin = true; @@ -228,7 +228,7 @@ namespace eShopOnContainers.Core.ViewModels Settings.AuthAccessToken = string.Empty; Settings.AuthIdToken = string.Empty; IsLogin = false; - LoginUrl = _identityService.CreateAuthorizeRequest(); + LoginUrl = _identityService.CreateAuthorizationRequest(); } else if (unescapedUrl.Contains(GlobalSetting.Instance.IdentityCallback)) { diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/OrderDetailViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/OrderDetailViewModel.cs index 449c6ea98..9f0d59676 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/OrderDetailViewModel.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/OrderDetailViewModel.cs @@ -1,8 +1,6 @@ using System.Threading.Tasks; using eShopOnContainers.Core.Models.Orders; using eShopOnContainers.Core.ViewModels.Base; -using eShopOnContainers.Core.Services.Catalog; -using eShopOnContainers.Core.Services.Basket; using eShopOnContainers.Core.Services.Order; using System; using eShopOnContainers.Core.Helpers; @@ -11,9 +9,8 @@ namespace eShopOnContainers.Core.ViewModels { public class OrderDetailViewModel : ViewModelBase { - private Order _order; - private IOrderService _ordersService; + private Order _order; public OrderDetailViewModel(IOrderService ordersService) { diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs index c5a58b745..b42e6445a 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs @@ -71,16 +71,14 @@ namespace eShopOnContainers.Core.ViewModels private void MockServices() { - ViewModelLocator.UpdateDependencies(!UseAzureServices); + ViewModelLocator.RegisterDependencies(!UseAzureServices); UpdateInfo(); } public override Task InitializeAsync(object navigationData) { UpdateInfo(); - Endpoint = Settings.UrlBase; - return base.InitializeAsync(navigationData); } @@ -89,12 +87,12 @@ namespace eShopOnContainers.Core.ViewModels if (!UseAzureServices) { Title = "Use Mock Services"; - Description = "Mock Services are simulated objects that mimic the behavior of real services in controlled ways"; + Description = "Mock Services are simulated objects that mimic the behavior of real services using a controlled approach."; } else { Title = "Use Microservices/Containers from eShopOnContainers"; - Description = "When enabling the use of microservices/containers the Xamarin.Forms app will try to use real services deployed as Docker containers in the specified base IP that will need to be reachable through the network"; + Description = "When enabling the use of microservices/containers, the app will attempt to use real services deployed as Docker containers at the specified base endpoint, which will must be reachable through the network."; } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj index 68a795fe8..f5c8c4b13 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj @@ -11,11 +11,11 @@ eShopOnContainers.Core eShopOnContainers.Core v4.5 - Profile259 512 {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile111 true @@ -170,85 +170,7 @@ - - - - - ..\..\..\..\packages\Acr.UserDialogs.6.3.3\lib\portable-win+net45+wp8+win8+wpa81\Acr.UserDialogs.dll - True - - - ..\..\..\..\packages\Acr.UserDialogs.6.3.3\lib\portable-win+net45+wp8+win8+wpa81\Acr.UserDialogs.Interface.dll - True - - - ..\..\..\..\packages\Xamarin.FFImageLoading.2.2.6-pre-256\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.dll - True - - - ..\..\..\..\packages\Xamarin.FFImageLoading.Forms.2.2.6-pre-256\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.Forms.dll - True - - - ..\..\..\..\packages\Xamarin.FFImageLoading.2.2.6-pre-256\lib\portable-net45+win8+wpa81+wp8\FFImageLoading.Platform.dll - True - - - ..\..\..\..\packages\IdentityModel.1.3.1\lib\portable-net45+wp80+win8+wpa81\IdentityModel.Portable.dll - True - - - ..\..\..\..\packages\Unity.4.0.1\lib\portable-net45+wp80+win8+wpa81+MonoAndroid10+MonoTouch10\Microsoft.Practices.Unity.dll - True - - - ..\..\..\..\packages\Newtonsoft.Json.9.0.2-beta1\lib\portable-net45+wp80+win8+wpa81\Newtonsoft.Json.dll - True - - - ..\..\..\..\packages\Xam.Plugins.Settings.2.6.0.12-beta\lib\portable-net45+wp80+win8+wpa81\Plugin.Settings.dll - True - - - ..\..\..\..\packages\Xam.Plugins.Settings.2.6.0.12-beta\lib\portable-net45+wp80+win8+wpa81\Plugin.Settings.Abstractions.dll - True - - - ..\..\..\..\packages\SlideOverKit.2.1.4\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SlideOverKit.dll - True - - - ..\..\..\..\packages\Splat.1.6.2\lib\Portable-net45+win+wpa81+wp80\Splat.dll - True - - - ..\..\..\..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.dll - True - - - ..\..\..\..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.Extensions.dll - True - - - ..\..\..\..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.Primitives.dll - True - - - ..\..\..\..\packages\Xamarin.Forms.2.3.3.175\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Core.dll - True - - - ..\..\..\..\packages\Xamarin.Forms.Pages.2.3.3.175\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10\Xamarin.Forms.Pages.dll - True - - - ..\..\..\..\packages\Xamarin.Forms.2.3.3.175\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Platform.dll - True - - - ..\..\..\..\packages\Xamarin.Forms.2.3.3.175\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Xaml.dll - True - + diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/packages.config b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/packages.config deleted file mode 100644 index 5f01c8896..000000000 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/packages.config +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/project.json b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/project.json new file mode 100644 index 000000000..20f36e083 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/project.json @@ -0,0 +1,25 @@ +{ + "dependencies": { + "Acr.UserDialogs": "6.3.3", + "Autofac": "4.4.0", + "CommonServiceLocator": "1.3", + "IdentityModel": "1.3.1", + "Microsoft.Bcl": "1.1.10", + "Microsoft.Bcl.Build": "1.0.21", + "Microsoft.Net.Http": "2.2.29", + "modernhttpclient": "2.4.2", + "Newtonsoft.Json": "9.0.2-beta1", + "SlideOverKit": "2.1.4", + "Splat": "1.6.2", + "Xam.Plugins.Settings": "2.6.0.12-beta", + "Xamarin.FFImageLoading": "2.2.6-pre-256", + "Xamarin.FFImageLoading.Forms": "2.2.6-pre-256", + "Xamarin.Forms": "2.3.3.175", + "Xamarin.Forms.Pages": "2.3.3.175", + "Xamarin.Forms.Theme.Base": "1.0.0.43-pre1", + "Xamarin.Forms.Theme.Light": "1.0.0.43-pre1" + }, + "frameworks": { + ".NETPortable,Version=v4.5,Profile=Profile111": {} + } +} \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj index 7e9dbb852..0a02480c9 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/eShopOnContainers.Droid.csproj @@ -21,10 +21,11 @@ armeabi,armeabi-v7a,x86 - + 1G + True @@ -37,14 +38,8 @@ True None False - False - False 1G Xamarin - False - False - False - False pdbonly @@ -102,10 +97,6 @@ ..\..\..\..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll True - - ..\..\..\..\packages\Unity.4.0.1\lib\portable-net45+wp80+win8+wpa81+MonoAndroid10+MonoTouch10\Microsoft.Practices.Unity.dll - True - ..\..\..\..\packages\modernhttpclient.2.4.2\lib\MonoAndroid\ModernHttpClient.dll True diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/packages.config b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/packages.config index 2f46c3e39..3d9f0c8a8 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/packages.config +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Droid/packages.config @@ -35,7 +35,6 @@ - diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Droid/MainActivity.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Droid/MainActivity.cs index 3344874f5..843195c33 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Droid/MainActivity.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Droid/MainActivity.cs @@ -14,7 +14,7 @@ namespace eShopOnContainers.TestRunner.Droid AddExecutionAssembly(typeof(ExtensibilityPointFactory).Assembly); // or in any reference assemblies getting the Assembly from any type/class - AddTestAssembly(typeof(UnitTests.DummyTests).Assembly); + AddTestAssembly(typeof(UnitTests.CatalogViewModelTests).Assembly); base.OnCreate(bundle); } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Droid/eShopOnContainers.TestRunner.Droid.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Droid/eShopOnContainers.TestRunner.Droid.csproj index e4132d7fa..be2e82d00 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Droid/eShopOnContainers.TestRunner.Droid.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Droid/eShopOnContainers.TestRunner.Droid.csproj @@ -20,6 +20,7 @@ Properties\AndroidManifest.xml + true diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Windows/App.xaml.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Windows/App.xaml.cs index 9f53c60d4..c21eb84c0 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Windows/App.xaml.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.Windows/App.xaml.cs @@ -12,7 +12,7 @@ namespace eShopOnContainers.TestRunner.Windows { // Otherwise you need to ensure that the test assemblies will // become part of the app bundle - AddTestAssembly(typeof(UnitTests.DummyTests).GetTypeInfo().Assembly); + AddTestAssembly(typeof(UnitTests.CatalogViewModelTests).GetTypeInfo().Assembly); } } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/AppDelegate.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/AppDelegate.cs index 686dea845..67c4a9a36 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/AppDelegate.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/AppDelegate.cs @@ -17,7 +17,7 @@ namespace eShopOnContainers.TestRunner.iOS // Otherwise you need to ensure that the test assemblies will // become part of the app bundle - AddTestAssembly(typeof(UnitTests.DummyTests).Assembly); + AddTestAssembly(typeof(UnitTests.CatalogViewModelTests).Assembly); return base.FinishedLaunching(app, options); } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/eShopOnContainers.TestRunner.iOS.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/eShopOnContainers.TestRunner.iOS.csproj index 5b4c3c548..0122264ae 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/eShopOnContainers.TestRunner.iOS.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/eShopOnContainers.TestRunner.iOS.csproj @@ -155,6 +155,12 @@ ..\..\..\..\packages\xunit.runner.utility.2.2.0-beta4-build3444\lib\netstandard1.1\xunit.runner.utility.dotnet.dll True + + ..\..\..\..\packages\Xam.Plugins.Settings.2.6.0.12-beta\lib\Xamarin.iOS10\Plugin.Settings.Abstractions.dll + + + ..\..\..\..\packages\Xam.Plugins.Settings.2.6.0.12-beta\lib\Xamarin.iOS10\Plugin.Settings.dll + diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/packages.config b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/packages.config index dbdc2d2f5..ab2dec370 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/packages.config +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.TestRunner.iOS/packages.config @@ -1,6 +1,7 @@  + diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Behaviors/EventToCommandBehaviorTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Behaviors/EventToCommandBehaviorTests.cs new file mode 100644 index 000000000..ce6ce8f96 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Behaviors/EventToCommandBehaviorTests.cs @@ -0,0 +1,120 @@ +using Xunit; +using Xamarin.Forms; +using System; +using System.Globalization; + +namespace eShopOnContainers.UnitTests +{ + public class EventToCommandBehaviorTests + { + [Fact] + public void InvalidEventNameShouldThrowArgumentExceptionText() + { + var behavior = new MockEventToCommandBehavior + { + EventName = "OnItemTapped" + }; + var listView = new ListView(); + + Assert.Throws(() => listView.Behaviors.Add(behavior)); + } + + [Fact] + public void CommandExecutedWhenEventFiresText() + { + bool executedCommand = false; + var behavior = new MockEventToCommandBehavior + { + EventName = "ItemTapped", + Command = new Command(() => + { + executedCommand = true; + }) + }; + var listView = new ListView(); + listView.Behaviors.Add(behavior); + + behavior.RaiseEvent(listView, null); + + Assert.True(executedCommand); + } + + [Fact] + public void CommandCanExecuteTest() + { + var behavior = new MockEventToCommandBehavior + { + EventName = "ItemTapped", + Command = new Command(() => Assert.True(false), () => false) + }; + var listView = new ListView(); + listView.Behaviors.Add(behavior); + + behavior.RaiseEvent(listView, null); + } + + [Fact] + public void CommandCanExecuteWithParameterShouldNotExecuteTest() + { + bool shouldExecute = false; + var behavior = new MockEventToCommandBehavior + { + EventName = "ItemTapped", + CommandParameter = shouldExecute, + Command = new Command(o => Assert.True(false), o => o.Equals(true)) + }; + var listView = new ListView(); + listView.Behaviors.Add(behavior); + + behavior.RaiseEvent(listView, null); + } + + [Fact] + public void CommandWithConverterTest() + { + const string item = "ItemProperty"; + bool executedCommand = false; + var behavior = new MockEventToCommandBehavior + { + EventName = "ItemTapped", + EventArgsConverter = new ItemTappedEventArgsConverter(false), + Command = new Command(o => + { + executedCommand = true; + Assert.NotNull(o); + Assert.Equal(item, o); + }) + }; + var listView = new ListView(); + listView.Behaviors.Add(behavior); + + behavior.RaiseEvent(listView, new ItemTappedEventArgs(listView, item)); + + Assert.True(executedCommand); + } + + private class ItemTappedEventArgsConverter : IValueConverter + { + private readonly bool _returnParameter; + + public bool HasConverted { get; private set; } + + public ItemTappedEventArgsConverter(bool returnParameter) + { + _returnParameter = returnParameter; + } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + HasConverted = true; + return _returnParameter ? parameter : (value as ItemTappedEventArgs)?.Item; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + } + +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/DummyTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/DummyTests.cs deleted file mode 100644 index 89257db27..000000000 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/DummyTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Threading.Tasks; -using Xunit; - -namespace eShopOnContainers.UnitTests -{ - public class DummyTests - { - [Fact] - public void ThisShouldPass_Sync() - { - Assert.True(true); - } - - [Fact] - public async Task ThisShouldPass_Async() - { - await Task.Run(() => { Assert.True(true); }); - } - - [Fact] - public async Task ThisShouldFail_Async() - { - await Task.Run(() => { throw new Exception("Oops!"); }); - } - } -} \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Mocks/MockEventToCommandBehavior.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Mocks/MockEventToCommandBehavior.cs new file mode 100644 index 000000000..16ad65044 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Mocks/MockEventToCommandBehavior.cs @@ -0,0 +1,12 @@ +using eShopOnContainers.Core.Behaviors; + +namespace eShopOnContainers.UnitTests +{ + public class MockEventToCommandBehavior : EventToCommandBehavior + { + public void RaiseEvent(params object[] args) + { + _handler.DynamicInvoke(args); + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Mocks/MockViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Mocks/MockViewModel.cs new file mode 100644 index 000000000..64acab458 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Mocks/MockViewModel.cs @@ -0,0 +1,53 @@ +using eShopOnContainers.Core.ViewModels.Base; +using eShopOnContainers.Core.Validations; + +namespace eShopOnContainers.UnitTests +{ + public class MockViewModel : ViewModelBase + { + private ValidatableObject _forename; + private ValidatableObject _surname; + + public ValidatableObject Forename + { + get + { + return _forename; + } + set + { + _forename = value; + RaisePropertyChanged(() => Forename); + } + } + + public ValidatableObject Surname + { + get + { + return _surname; + } + set + { + _surname = value; + RaisePropertyChanged(() => Surname); + } + } + + public MockViewModel() + { + _forename = new ValidatableObject(); + _surname = new ValidatableObject(); + + _forename.Validations.Add(new IsNotNullOrEmptyRule { ValidationMessage = "Forename is required." }); + _surname.Validations.Add(new IsNotNullOrEmptyRule { ValidationMessage = "Surname name is required." }); + } + + public bool Validate() + { + bool isValidForename = _forename.Validate(); + bool isValidSurname = _surname.Validate(); + return isValidForename && isValidSurname; + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/BasketServiceTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Services/BasketServiceTests.cs similarity index 100% rename from src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/BasketServiceTests.cs rename to src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Services/BasketServiceTests.cs diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/CatalogServiceTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Services/CatalogServiceTests.cs similarity index 100% rename from src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/CatalogServiceTests.cs rename to src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Services/CatalogServiceTests.cs diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/OrdersServiceTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Services/OrdersServiceTests.cs similarity index 55% rename from src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/OrdersServiceTests.cs rename to src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Services/OrdersServiceTests.cs index a235234cc..f8411ef6f 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/OrdersServiceTests.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/Services/OrdersServiceTests.cs @@ -1,6 +1,5 @@ using eShopOnContainers.Core; using eShopOnContainers.Core.Services.Order; -using eShopOnContainers.Core.Services.RequestProvider; using System.Threading.Tasks; using Xunit; @@ -8,6 +7,15 @@ namespace eShopOnContainers.UnitTests { public class OrdersServiceTests { + [Fact] + public async Task GetFakeOrderTest() + { + var ordersMockService = new OrderMockService(); + var order = await ordersMockService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken); + + Assert.NotNull(order); + } + [Fact] public async Task GetFakeOrdersTest() { @@ -16,15 +24,5 @@ namespace eShopOnContainers.UnitTests Assert.NotEqual(0, result.Count); } - - [Fact] - public async Task GetOrdersTest() - { - var requestProvider = new RequestProvider(); - var ordersService = new OrderService(requestProvider); - var result = await ordersService.GetOrdersAsync(GlobalSetting.Instance.AuthToken); - - Assert.NotEqual(0, result.Count); - } } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/CatalogViewModelTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/CatalogViewModelTests.cs new file mode 100644 index 000000000..c1d4deaec --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/CatalogViewModelTests.cs @@ -0,0 +1,223 @@ +using Xunit; +using eShopOnContainers.Core.ViewModels; +using eShopOnContainers.Core.ViewModels.Base; +using eShopOnContainers.Core.Services.Catalog; +using eShopOnContainers.Core.Models.Catalog; +using System.Threading.Tasks; +using System.Linq; + +namespace eShopOnContainers.UnitTests +{ + public class CatalogViewModelTests + { + public CatalogViewModelTests() + { + ViewModelLocator.RegisterDependencies(true); + } + + [Fact] + public void AddCatalogItemCommandIsNotNullTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.NotNull(catalogViewModel.AddCatalogItemCommand); + } + + [Fact] + public void FilterCommandIsNotNullTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.NotNull(catalogViewModel.FilterCommand); + } + + [Fact] + public void ClearFilterCommandIsNotNullTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.NotNull(catalogViewModel.ClearFilterCommand); + } + + [Fact] + public void ProductsPropertyIsNullWhenViewModelInstantiatedTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.Null(catalogViewModel.Products); + } + + [Fact] + public void BrandsPropertyuIsNullWhenViewModelInstantiatedTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.Null(catalogViewModel.Brands); + } + + [Fact] + public void BrandPropertyIsNullWhenViewModelInstantiatedTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.Null(catalogViewModel.Brand); + } + + [Fact] + public void TypesPropertyIsNullWhenViewModelInstantiatedTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.Null(catalogViewModel.Types); + } + + [Fact] + public void TypePropertyIsNullWhenViewModelInstantiatedTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.Null(catalogViewModel.Type); + } + + [Fact] + public void IsFilterPropertyIsFalseWhenViewModelInstantiatedTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + Assert.False(catalogViewModel.IsFilter); + } + + [Fact] + public async Task ProductsPropertyIsNotNullAfterViewModelInitializationTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + + await catalogViewModel.InitializeAsync(null); + + Assert.NotNull(catalogViewModel.Products); + } + + [Fact] + public async Task BrandsPropertyIsNotNullAfterViewModelInitializationTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + + await catalogViewModel.InitializeAsync(null); + + Assert.NotNull(catalogViewModel.Brands); + } + + [Fact] + public async Task TypesPropertyIsNotNullAfterViewModelInitializationTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + + await catalogViewModel.InitializeAsync(null); + + Assert.NotNull(catalogViewModel.Types); + } + + [Fact] + public async Task SettingProductsPropertyShouldRaisePropertyChanged() + { + bool invoked = false; + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + + catalogViewModel.PropertyChanged += (sender, e) => + { + if (e.PropertyName.Equals("Products")) + invoked = true; + }; + await catalogViewModel.InitializeAsync(null); + + Assert.True(invoked); + } + + [Fact] + public async Task SettingBrandsPropertyShouldRaisePropertyChanged() + { + bool invoked = false; + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + + catalogViewModel.PropertyChanged += (sender, e) => + { + if (e.PropertyName.Equals("Brands")) + invoked = true; + }; + await catalogViewModel.InitializeAsync(null); + + Assert.True(invoked); + } + + [Fact] + public async Task SettingTypesPropertyShouldRaisePropertyChanged() + { + bool invoked = false; + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + + catalogViewModel.PropertyChanged += (sender, e) => + { + if (e.PropertyName.Equals("Types")) + invoked = true; + }; + await catalogViewModel.InitializeAsync(null); + + Assert.True(invoked); + } + + [Fact] + public void AddCatalogItemCommandSendsAddProductMessageTest() + { + bool messageReceived = false; + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + + Xamarin.Forms.MessagingCenter.Subscribe(this, MessageKeys.AddProduct, (sender, arg) => + { + messageReceived = true; + }); + catalogViewModel.AddCatalogItemCommand.Execute(null); + + Assert.True(messageReceived); + } + + [Fact] + public async Task FilterCommandSendsFilterMessageTest() + { + bool messageReceived = false; + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + await catalogViewModel.InitializeAsync(null); + catalogViewModel.Brand = catalogViewModel.Brands.FirstOrDefault(); + catalogViewModel.Type = catalogViewModel.Types.FirstOrDefault(); + + Xamarin.Forms.MessagingCenter.Subscribe(this, MessageKeys.Filter, (sender) => + { + messageReceived = true; + }); + catalogViewModel.FilterCommand.Execute(null); + + Assert.True(messageReceived); + } + + [Fact] + public async Task ClearFilterCommandResetsPropertiesTest() + { + var catalogService = new CatalogMockService(); + var catalogViewModel = new CatalogViewModel(catalogService); + + await catalogViewModel.InitializeAsync(null); + catalogViewModel.ClearFilterCommand.Execute(null); + + Assert.Null(catalogViewModel.Brand); + Assert.Null(catalogViewModel.Type); + Assert.NotNull(catalogViewModel.Products); + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/MainViewModelTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/MainViewModelTests.cs new file mode 100644 index 000000000..1ccffcd34 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/MainViewModelTests.cs @@ -0,0 +1,54 @@ +using Xunit; +using eShopOnContainers.Core.ViewModels; +using eShopOnContainers.Core.ViewModels.Base; +using eShopOnContainers.Core.Models.Navigation; +using System.Threading.Tasks; + +namespace eShopOnContainers.UnitTests +{ + public class MainViewModelTests + { + public MainViewModelTests() + { + ViewModelLocator.RegisterDependencies(true); + } + + [Fact] + public void SettingsCommandIsNotNullWhenViewModelInstantiatedTest() + { + var mainViewModel = new MainViewModel(); + Assert.NotNull(mainViewModel.SettingsCommand); + } + + [Fact] + public async Task ViewModelInitializationSendsChangeTabMessageTest() + { + bool messageReceived = false; + var mainViewModel = new MainViewModel(); + var tabParam = new TabParameter { TabIndex = 2 }; + + Xamarin.Forms.MessagingCenter.Subscribe(this, MessageKeys.ChangeTab, (sender, arg) => + { + messageReceived = true; + }); + await mainViewModel.InitializeAsync(tabParam); + + Assert.True(messageReceived); + } + + [Fact] + public void IsBusyPropertyIsFalseWhenViewModelInstantiatedTest() + { + var mainViewModel = new MainViewModel(); + Assert.False(mainViewModel.IsBusy); + } + + [Fact] + public async Task IsBusyPropertyIsTrueAfterViewModelInitializationTest() + { + var mainViewModel = new MainViewModel(); + await mainViewModel.InitializeAsync(null); + Assert.True(mainViewModel.IsBusy); + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/MockViewModelTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/MockViewModelTests.cs new file mode 100644 index 000000000..82312bc69 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/MockViewModelTests.cs @@ -0,0 +1,113 @@ +using Xunit; +using eShopOnContainers.Core.ViewModels.Base; + +namespace eShopOnContainers.UnitTests +{ + public class MockViewModelTests + { + public MockViewModelTests() + { + ViewModelLocator.RegisterDependencies(true); + } + + [Fact] + public void CheckValidationFailsWhenPropertiesAreEmptyTest() + { + var mockViewModel = new MockViewModel(); + + bool isValid = mockViewModel.Validate(); + + Assert.False(isValid); + Assert.Null(mockViewModel.Forename.Value); + Assert.Null(mockViewModel.Surname.Value); + Assert.False(mockViewModel.Forename.IsValid); + Assert.False(mockViewModel.Surname.IsValid); + Assert.NotEmpty(mockViewModel.Forename.Errors); + Assert.NotEmpty(mockViewModel.Surname.Errors); + } + + [Fact] + public void CheckValidationFailsWhenOnlyForenameHasDataTest() + { + var mockViewModel = new MockViewModel(); + mockViewModel.Forename.Value = "John"; + + bool isValid = mockViewModel.Validate(); + + Assert.False(isValid); + Assert.NotNull(mockViewModel.Forename.Value); + Assert.Null(mockViewModel.Surname.Value); + Assert.True(mockViewModel.Forename.IsValid); + Assert.False(mockViewModel.Surname.IsValid); + Assert.Empty(mockViewModel.Forename.Errors); + Assert.NotEmpty(mockViewModel.Surname.Errors); + } + + [Fact] + public void CheckValidationPassesWhenOnlySurnameHasDataTest() + { + var mockViewModel = new MockViewModel(); + mockViewModel.Surname.Value = "Smith"; + + bool isValid = mockViewModel.Validate(); + + Assert.False(isValid); + Assert.Null(mockViewModel.Forename.Value); + Assert.NotNull(mockViewModel.Surname.Value); + Assert.False(mockViewModel.Forename.IsValid); + Assert.True(mockViewModel.Surname.IsValid); + Assert.NotEmpty(mockViewModel.Forename.Errors); + Assert.Empty(mockViewModel.Surname.Errors); + } + + [Fact] + public void CheckValidationPassesWhenBothPropertiesHaveDataTest() + { + var mockViewModel = new MockViewModel(); + mockViewModel.Forename.Value = "John"; + mockViewModel.Surname.Value = "Smith"; + + bool isValid = mockViewModel.Validate(); + + Assert.True(isValid); + Assert.NotNull(mockViewModel.Forename.Value); + Assert.NotNull(mockViewModel.Surname.Value); + Assert.True(mockViewModel.Forename.IsValid); + Assert.True(mockViewModel.Surname.IsValid); + Assert.Empty(mockViewModel.Forename.Errors); + Assert.Empty(mockViewModel.Surname.Errors); + } + + [Fact] + public void SettingForenamePropertyShouldRaisePropertyChanged() + { + bool invoked = false; + var mockViewModel = new MockViewModel(); + + mockViewModel.Forename.PropertyChanged += (sender, e) => + { + if (e.PropertyName.Equals("Value")) + invoked = true; + }; + mockViewModel.Forename.Value = "John"; + + Assert.True(invoked); + } + + [Fact] + public void SettingSurnamePropertyShouldRaisePropertyChanged() + { + bool invoked = false; + var mockViewModel = new MockViewModel(); + + mockViewModel.Surname.PropertyChanged += (sender, e) => + { + if (e.PropertyName.Equals("Value")) + invoked = true; + }; + mockViewModel.Surname.Value = "Smith"; + + Assert.True(invoked); + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/OrderViewModelTests.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/OrderViewModelTests.cs new file mode 100644 index 000000000..fbfc6a951 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/ViewModels/OrderViewModelTests.cs @@ -0,0 +1,55 @@ +using Xunit; +using eShopOnContainers.Core; +using eShopOnContainers.Core.ViewModels; +using eShopOnContainers.Core.ViewModels.Base; +using eShopOnContainers.Core.Services.Order; +using System.Threading.Tasks; + +namespace eShopOnContainers.UnitTests +{ + public class OrderViewModelTests + { + public OrderViewModelTests() + { + ViewModelLocator.RegisterDependencies(true); + } + + [Fact] + public void OrderPropertyIsNullWhenViewModelInstantiatedTest() + { + var orderService = new OrderMockService(); + var orderViewModel = new OrderDetailViewModel(orderService); + Assert.Null(orderViewModel.Order); + } + + [Fact] + public async Task OrderPropertyIsNotNullAfterViewModelInitializationTest() + { + var orderService = new OrderMockService(); + var orderViewModel = new OrderDetailViewModel(orderService); + + var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken); + await orderViewModel.InitializeAsync(order); + + Assert.NotNull(orderViewModel.Order); + } + + [Fact] + public async Task SettingOrderPropertyShouldRaisePropertyChanged() + { + bool invoked = false; + var orderService = new OrderMockService(); + var orderViewModel = new OrderDetailViewModel(orderService); + + orderViewModel.PropertyChanged += (sender, e) => + { + if (e.PropertyName.Equals("Order")) + invoked = true; + }; + var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken); + await orderViewModel.InitializeAsync(order); + + Assert.True(invoked); + } + } +} \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/eShopOnContainers.UnitTests.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/eShopOnContainers.UnitTests.csproj index 1225ae872..115aca788 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/eShopOnContainers.UnitTests.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/eShopOnContainers.UnitTests.csproj @@ -34,11 +34,17 @@ 4 - - - - + + + + + + + + + + @@ -57,6 +63,15 @@ ..\..\..\..\packages\xunit.extensibility.execution.2.2.0-beta4-build3444\lib\netstandard1.0\xunit.execution.dotnet.dll True + + ..\..\..\..\packages\Xamarin.Forms.2.3.4.231\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Core.dll + + + ..\..\..\..\packages\Xamarin.Forms.2.3.4.231\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Platform.dll + + + ..\..\..\..\packages\Xamarin.Forms.2.3.4.231\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+Xamarin.iOS10+xamarinmac20\Xamarin.Forms.Xaml.dll + @@ -68,6 +83,12 @@ eShopOnContainers.Core + + + + + + + \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/packages.config b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/packages.config index a26539956..4b0dc784c 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/packages.config +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/packages.config @@ -1,10 +1,11 @@  - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Windows/project.json b/src/Mobile/eShopOnContainers/eShopOnContainers.Windows/project.json index 42f8b9552..b6c67c002 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Windows/project.json +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Windows/project.json @@ -5,7 +5,6 @@ "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0", "Newtonsoft.Json": "9.0.1", "SlideOverKit": "2.1.4", - "Unity": "4.0.1", "Xam.Plugins.Settings": "2.6.0.12-beta", "Xamarin.FFImageLoading": "2.2.6-pre-256", "Xamarin.FFImageLoading.Forms": "2.2.6-pre-256", diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj index 99f033a1d..fa1114dd7 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj @@ -166,10 +166,6 @@ ..\..\..\..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll True - - ..\..\..\..\packages\Unity.4.0.1\lib\portable-net45+wp80+win8+wpa81+MonoAndroid10+MonoTouch10\Microsoft.Practices.Unity.dll - True - ..\..\..\..\packages\modernhttpclient.2.4.2\lib\Xamarin.iOS10\ModernHttpClient.dll True diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/packages.config b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/packages.config index 75be50fe1..de074af61 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/packages.config +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/packages.config @@ -11,7 +11,6 @@ - diff --git a/src/Services/Basket/Basket.API/Basket.API.csproj b/src/Services/Basket/Basket.API/Basket.API.csproj index ce20513cc..b3ba97b10 100644 --- a/src/Services/Basket/Basket.API/Basket.API.csproj +++ b/src/Services/Basket/Basket.API/Basket.API.csproj @@ -43,7 +43,6 @@ - diff --git a/src/Services/Basket/Basket.API/Controllers/BasketController.cs b/src/Services/Basket/Basket.API/Controllers/BasketController.cs index 5bf23509c..16f008509 100644 --- a/src/Services/Basket/Basket.API/Controllers/BasketController.cs +++ b/src/Services/Basket/Basket.API/Controllers/BasketController.cs @@ -5,6 +5,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Basket.API.IntegrationEvents.Events; +using Microsoft.eShopOnContainers.Services.Basket.API.Services; namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers { @@ -16,11 +20,17 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers [Authorize] public class BasketController : Controller { - private IBasketRepository _repository; + private readonly IBasketRepository _repository; + private readonly IIdentityService _identitySvc; + private readonly IEventBus _eventBus; - public BasketController(IBasketRepository repository) + public BasketController(IBasketRepository repository, + IIdentityService identityService, + IEventBus eventBus) { _repository = repository; + _identitySvc = identityService; + _eventBus = eventBus; } // GET api/values/5 [HttpGet("{id}")] @@ -40,11 +50,28 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers return Ok(basket); } + [HttpPost] + public async Task Checkout() + { + var userId = _identitySvc.GetUserIdentity(); + var basket = await _repository.GetBasketAsync(userId); + _eventBus.Publish(new UserCheckoutAccepted(userId, basket)); + if (basket == null) + { + return BadRequest(); + } + + + + return Accepted(); + } + // DELETE api/values/5 [HttpDelete("{id}")] public void Delete(string id) { _repository.DeleteBasketAsync(id); } + } } diff --git a/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAccepted.cs b/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAccepted.cs new file mode 100644 index 000000000..2d3e15ff3 --- /dev/null +++ b/src/Services/Basket/Basket.API/IntegrationEvents/Events/UserCheckoutAccepted.cs @@ -0,0 +1,21 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Microsoft.eShopOnContainers.Services.Basket.API.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Basket.API.IntegrationEvents.Events +{ + public class UserCheckoutAccepted : IntegrationEvent + { + public string UserId {get; } + CustomerBasket Basket { get; } + public UserCheckoutAccepted(string userId, CustomerBasket basket) + { + UserId = userId; + Basket = basket; + } + + } +} diff --git a/src/Services/Basket/Basket.API/Model/Basket.cs b/src/Services/Basket/Basket.API/Model/Basket.cs index d07c710e7..7b9f620d8 100644 --- a/src/Services/Basket/Basket.API/Model/Basket.cs +++ b/src/Services/Basket/Basket.API/Model/Basket.cs @@ -13,7 +13,7 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API.Model public CustomerBasket(string customerId) { BuyerId = customerId; - Items = new List(); + Items = new List(); } } } diff --git a/src/Services/Basket/Basket.API/Services/IIdentityService.cs b/src/Services/Basket/Basket.API/Services/IIdentityService.cs new file mode 100644 index 000000000..8cc7bd848 --- /dev/null +++ b/src/Services/Basket/Basket.API/Services/IIdentityService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Basket.API.Services +{ + public interface IIdentityService + { + string GetUserIdentity(); + } +} diff --git a/src/Services/Basket/Basket.API/Services/IdentityService.cs b/src/Services/Basket/Basket.API/Services/IdentityService.cs new file mode 100644 index 000000000..08d1ffffa --- /dev/null +++ b/src/Services/Basket/Basket.API/Services/IdentityService.cs @@ -0,0 +1,24 @@ + +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Basket.API.Services +{ + public class IdentityService : IIdentityService + { + private IHttpContextAccessor _context; + + public IdentityService(IHttpContextAccessor context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public string GetUserIdentity() + { + return _context.HttpContext.User.FindFirst("sub").Value; + } + } +} diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs index 60fc46de2..855312a65 100644 --- a/src/Services/Basket/Basket.API/Startup.cs +++ b/src/Services/Basket/Basket.API/Startup.cs @@ -3,6 +3,7 @@ using Basket.API.IntegrationEvents.EventHandling; using Basket.API.IntegrationEvents.Events; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; using Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server; @@ -19,6 +20,7 @@ using StackExchange.Redis; using System.Linq; using System.Net; using System.Threading.Tasks; +using System; namespace Microsoft.eShopOnContainers.Services.Basket.API { @@ -68,20 +70,18 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API }); - services.AddSingleton(sp => + services.AddSingleton(sp => { var settings = sp.GetRequiredService>().Value; - var logger = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>(); var factory = new ConnectionFactory() { HostName = settings.EventBusConnection }; - return new DefaultRabbitMQPersisterConnection(factory, logger); + return new DefaultRabbitMQPersistentConnection(factory, logger); }); - services.AddSingleton(); - services.AddSwaggerGen(); services.ConfigureSwaggerGen(options => @@ -108,9 +108,16 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API }); services.AddTransient(); - services.AddTransient, ProductPriceChangedIntegrationEventHandler>(); - services.AddTransient, OrderStartedIntegrationEventHandler>(); + RegisterServiceBus(services); + } + private void RegisterServiceBus(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); } @@ -155,11 +162,13 @@ namespace Microsoft.eShopOnContainers.Services.Basket.API var orderStartedHandler = app.ApplicationServices .GetService>(); - var eventBus = app.ApplicationServices - .GetRequiredService(); + var eventBus = app.ApplicationServices.GetRequiredService(); + + eventBus.Subscribe + (() => app.ApplicationServices.GetRequiredService()); - eventBus.Subscribe(catalogPriceHandler); - eventBus.Subscribe(orderStartedHandler); + eventBus.Subscribe + (() => app.ApplicationServices.GetRequiredService()); } } } diff --git a/src/Services/Catalog/Catalog.API/Catalog.API.csproj b/src/Services/Catalog/Catalog.API/Catalog.API.csproj index d805e06e0..4306d6922 100644 --- a/src/Services/Catalog/Catalog.API/Catalog.API.csproj +++ b/src/Services/Catalog/Catalog.API/Catalog.API.csproj @@ -59,7 +59,7 @@ - + diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index c13ac2d1b..9eb195674 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; @@ -103,18 +104,19 @@ services.AddTransient(); - services.AddSingleton(sp => + services.AddSingleton(sp => { var settings = sp.GetRequiredService>().Value; - var logger = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>(); var factory = new ConnectionFactory() { HostName = settings.EventBusConnection }; - return new DefaultRabbitMQPersisterConnection(factory, logger); + return new DefaultRabbitMQPersistentConnection(factory, logger); }); + services.AddSingleton(); services.AddSingleton(); } diff --git a/src/Services/Identity/Identity.API/Identity.API.csproj b/src/Services/Identity/Identity.API/Identity.API.csproj index f5db7da5e..e99a6efa9 100644 --- a/src/Services/Identity/Identity.API/Identity.API.csproj +++ b/src/Services/Identity/Identity.API/Identity.API.csproj @@ -64,7 +64,7 @@ - + diff --git a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs index 1485a536f..950c4bdc5 100644 --- a/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs +++ b/src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs @@ -61,20 +61,16 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands [DataMember] public IEnumerable OrderItems => _orderItems; - public void AddOrderItem(OrderItemDTO item) - { - _orderItems.Add(item); - } - public CreateOrderCommand() { _orderItems = new List(); } - public CreateOrderCommand(string city, string street, string state, string country, string zipcode, + public CreateOrderCommand(List orderItems, string city, string street, string state, string country, string zipcode, string cardNumber, string cardHolderName, DateTime cardExpiration, string cardSecurityNumber, int cardTypeId, int paymentId, int buyerId) : this() { + _orderItems = orderItems; City = city; Street = street; State = state; diff --git a/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs new file mode 100644 index 000000000..f846f5031 --- /dev/null +++ b/src/Services/Ordering/Ordering.API/Application/IntegrationEvents/EventHandling/UserCheckoutAcceptedIntegrationEventHandler.cs @@ -0,0 +1,16 @@ +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ordering.API.Application.IntegrationEvents.EventHandling +{ + public class UserCheckoutAcceptedIntegrationEventHandler : IDynamicIntegrationEventHandler + { + public async Task Handle(dynamic eventData) + { + int i = 0; + } + } +} diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs index 7d19815b9..69faf8e42 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -41,12 +41,12 @@ { var json = new JsonErrorResponse { - Messages = new[] { "An error ocurr.Try it again." } + Messages = new[] { "An error occur.Try it again." } }; if (env.IsDevelopment()) { - json.DeveloperMeesage = context.Exception; + json.DeveloperMessage = context.Exception; } // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 @@ -61,7 +61,7 @@ { public string[] Messages { get; set; } - public object DeveloperMeesage { get; set; } + public object DeveloperMessage { get; set; } } } } diff --git a/src/Services/Ordering/Ordering.API/Ordering.API.csproj b/src/Services/Ordering/Ordering.API/Ordering.API.csproj index 445f93ad8..d5ef524bb 100644 --- a/src/Services/Ordering/Ordering.API/Ordering.API.csproj +++ b/src/Services/Ordering/Ordering.API/Ordering.API.csproj @@ -13,13 +13,6 @@ ..\..\..\..\docker-compose.dcproj - - - - - - - PreserveNewest @@ -34,7 +27,7 @@ - + diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index d58ddcfd6..c110c63b1 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -1,6 +1,8 @@  using Ordering.API.Application.IntegrationCommands.Commands; +using Ordering.API.Application.IntegrationEvents.EventHandling; +using Ordering.API.Application.Sagas; namespace Microsoft.eShopOnContainers.Services.Ordering.API { @@ -17,6 +19,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; + using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; @@ -111,20 +114,22 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API var serviceProvider = services.BuildServiceProvider(); services.AddTransient(); - services.AddSingleton(sp => + services.AddSingleton(sp => { - var logger = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>(); var factory = new ConnectionFactory() { HostName = Configuration["EventBusConnection"] }; - return new DefaultRabbitMQPersisterConnection(factory, logger); + return new DefaultRabbitMQPersistentConnection(factory, logger); }); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); services.AddOptions(); //configure autofac @@ -138,6 +143,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API return new AutofacServiceProvider(container.Build()); } + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); @@ -148,6 +154,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API app.UseFailingMiddleware(); ConfigureAuth(app); + ConfigureEventBus(app); app.UseMvcWithDefaultRoute(); @@ -162,7 +169,20 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API .Options); integrationEventLogContext.Database.Migrate(); - ConfigureEventBus(app); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.SubscribeDynamic( + "UserCheckoutAccepted", + () => app.ApplicationServices.GetRequiredService()); + + eventBus.Subscribe>( + () => app.ApplicationServices + .GetService>() + ); + } protected virtual void ConfigureAuth(IApplicationBuilder app) @@ -175,16 +195,5 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API RequireHttpsMetadata = false }); } - - protected virtual void ConfigureEventBus(IApplicationBuilder app) - { - var confirmGracePeriodHandler = app.ApplicationServices - .GetService>(); - - var eventBus = app.ApplicationServices - .GetRequiredService(); - - eventBus.Subscribe(confirmGracePeriodHandler); - } } } diff --git a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs index 5b95ee23c..18d66534b 100644 --- a/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs +++ b/src/Services/Ordering/Ordering.Infrastructure/OrderingContext.cs @@ -252,7 +252,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure // After executing this line all the changes (from the Command Handler and Domain Event Handlers) - // performed thought the DbContext will be commited + // performed throught the DbContext will be commited var result = await base.SaveChangesAsync(); return true; diff --git a/src/Services/SagaManager/SagaManager/Program.cs b/src/Services/SagaManager/SagaManager/Program.cs index d76a8a080..55e449e83 100644 --- a/src/Services/SagaManager/SagaManager/Program.cs +++ b/src/Services/SagaManager/SagaManager/Program.cs @@ -1,4 +1,5 @@ -using SagaManager.IntegrationEvents; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using SagaManager.IntegrationEvents; namespace SagaManager { @@ -18,6 +19,27 @@ namespace SagaManager public static IConfigurationRoot Configuration { get; set; } public static void Main(string[] args) + { + StartUp(); + + IServiceCollection services = new ServiceCollection(); + var serviceProvider = ConfigureServices(services); + + var logger = serviceProvider.GetService(); + Configure(logger); + + + var sagaManagerService = serviceProvider + .GetRequiredService(); + + while (true) + { + sagaManagerService.CheckFinishedGracePeriodOrders(); + System.Threading.Thread.Sleep(30000); + } + } + + public static void StartUp() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) @@ -25,41 +47,46 @@ namespace SagaManager .AddEnvironmentVariables(); Configuration = builder.Build(); + } - var serviceProvider = new ServiceCollection() - .AddLogging() + public static IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddLogging() .AddOptions() .Configure(Configuration) .AddSingleton() .AddSingleton() - .AddSingleton(sp => + .AddSingleton() + .AddSingleton() + .AddSingleton(sp => { var settings = sp.GetRequiredService>().Value; - var logger = sp.GetRequiredService>(); + var logger = sp.GetRequiredService>(); var factory = new ConnectionFactory() { HostName = settings.EventBusConnection }; - return new DefaultRabbitMQPersisterConnection(factory, logger); + return new DefaultRabbitMQPersistentConnection(factory, logger); }) - .AddSingleton() - .BuildServiceProvider(); + .AddSingleton(); + + RegisterServiceBus(services); + + return services.BuildServiceProvider(); + } - //configure console logging - serviceProvider - .GetService() + public static void Configure(ILoggerFactory loggerFactory) + { + loggerFactory .AddConsole(Configuration.GetSection("Logging")) .AddConsole(LogLevel.Debug); + } - var sagaManagerService = serviceProvider - .GetRequiredService(); - - while (true) - { - sagaManagerService.CheckFinishedGracePeriodOrders(); - System.Threading.Thread.Sleep(30000); - } + private static void RegisterServiceBus(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); } } } \ No newline at end of file diff --git a/src/Services/SagaManager/SagaManager/Services/SagaManagerService.cs b/src/Services/SagaManager/SagaManager/Services/SagaManagerService.cs index bc9cac1cb..7445ca090 100644 --- a/src/Services/SagaManager/SagaManager/Services/SagaManagerService.cs +++ b/src/Services/SagaManager/SagaManager/Services/SagaManagerService.cs @@ -1,4 +1,7 @@ -namespace SagaManager.Services +using System; +using Microsoft.Extensions.Logging; + +namespace SagaManager.Services { using System.Collections.Generic; using System.Data.SqlClient; @@ -11,12 +14,15 @@ { private readonly SagaManagerSettings _settings; private readonly IConfirmGracePeriodEvent _confirmGracePeriodEvent; + private readonly ILogger _logger; public SagaManagerService(IOptions settings, - IConfirmGracePeriodEvent confirmGracePeriodEvent) + IConfirmGracePeriodEvent confirmGracePeriodEvent, + ILogger logger) { _settings = settings.Value; _confirmGracePeriodEvent = confirmGracePeriodEvent; + _logger = logger; } public void CheckFinishedGracePeriodOrders() @@ -34,12 +40,20 @@ IEnumerable orderIds = new List(); using (var conn = new SqlConnection(_settings.ConnectionString)) { - conn.Open(); - orderIds = conn.Query( - @"SELECT Id FROM [Microsoft.eShopOnContainers.Services.OrderingDb].[ordering].[orders] - WHERE DATEDIFF(hour, [OrderDate], GETDATE()) >= @GracePeriod - AND [OrderStatusId] = 1", - new { GracePeriod = _settings.GracePeriod }); + try + { + conn.Open(); + orderIds = conn.Query( + @"SELECT Id FROM [Microsoft.eShopOnContainers.Services.OrderingDb].[ordering].[orders] + WHERE DATEDIFF(hour, [OrderDate], GETDATE()) >= @GracePeriod + AND [OrderStatusId] = 1", + new { GracePeriod = _settings.GracePeriod }); + } + catch (SqlException exception) + { + _logger.LogError(exception.Message); + } + } return orderIds; diff --git a/src/Web/WebMVC/WebMVC.csproj b/src/Web/WebMVC/WebMVC.csproj index 20a8baca0..3f0f6b84b 100644 --- a/src/Web/WebMVC/WebMVC.csproj +++ b/src/Web/WebMVC/WebMVC.csproj @@ -59,7 +59,6 @@ - diff --git a/src/Web/WebSPA/WebSPA.csproj b/src/Web/WebSPA/WebSPA.csproj index 33412407a..cebe4377d 100644 --- a/src/Web/WebSPA/WebSPA.csproj +++ b/src/Web/WebSPA/WebSPA.csproj @@ -74,7 +74,6 @@ - diff --git a/src/Web/WebStatus/WebStatus.csproj b/src/Web/WebStatus/WebStatus.csproj index 47a3e20f0..e3028722d 100644 --- a/src/Web/WebStatus/WebStatus.csproj +++ b/src/Web/WebStatus/WebStatus.csproj @@ -21,7 +21,6 @@ - diff --git a/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs b/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs index 5f52e1771..5b2424114 100644 --- a/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs +++ b/test/Services/FunctionalTests/Services/Ordering/OrderingScenarios.cs @@ -45,7 +45,19 @@ namespace FunctionalTests.Services.Ordering string BuildOrder() { + List orderItemsList = new List(); + orderItemsList.Add(new OrderItemDTO() + { + ProductId = 1, + Discount = 8M, + UnitPrice = 10, + Units = 1, + ProductName = "Some name" + } + ); + var order = new CreateOrderCommand( + orderItemsList, cardExpiration: DateTime.UtcNow.AddYears(1), cardNumber: "5145-555-5555", cardHolderName: "Jhon Senna", @@ -60,15 +72,6 @@ namespace FunctionalTests.Services.Ordering buyerId: 3 ); - order.AddOrderItem(new OrderItemDTO() - { - ProductId = 1, - Discount = 8M, - UnitPrice = 10, - Units = 1, - ProductName = "Some name" - }); - return JsonConvert.SerializeObject(order); } } diff --git a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs index 3e2350c9d..49f04fa3b 100644 --- a/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs +++ b/test/Services/IntegrationTests/Services/Ordering/OrderingScenarios.cs @@ -9,7 +9,9 @@ using System.Text; using System.Threading.Tasks; using Xunit; + using System.Collections; using static Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands.CreateOrderCommand; + using System.Collections.Generic; public class OrderingScenarios : OrderingScenarioBase @@ -59,7 +61,19 @@ string BuildOrder() { + List orderItemsList = new List(); + orderItemsList.Add(new OrderItemDTO() + { + ProductId = 1, + Discount = 10M, + UnitPrice = 10, + Units = 1, + ProductName = "Some name" + } + ); + var order = new CreateOrderCommand( + orderItemsList, cardExpiration: DateTime.UtcNow.AddYears(1), cardNumber: "5145-555-5555", cardHolderName: "Jhon Senna", @@ -74,20 +88,12 @@ buyerId: 1 ); - order.AddOrderItem(new OrderItemDTO() - { - ProductId = 1, - Discount = 10M, - UnitPrice = 10, - Units = 1, - ProductName = "Some name" - }); - return JsonConvert.SerializeObject(order); } string BuildOrderWithInvalidExperationTime() { var order = new CreateOrderCommand( + null, cardExpiration: DateTime.UtcNow.AddYears(-1), cardNumber: "5145-555-5555", cardHolderName: "Jhon Senna", diff --git a/test/Services/UnitTest/Basket/Application/BasketWebApiTest.cs b/test/Services/UnitTest/Basket/Application/BasketWebApiTest.cs index 6c5f116bd..b6e45425a 100644 --- a/test/Services/UnitTest/Basket/Application/BasketWebApiTest.cs +++ b/test/Services/UnitTest/Basket/Application/BasketWebApiTest.cs @@ -1,20 +1,27 @@ -using Microsoft.AspNetCore.Mvc; +using Basket.API.IntegrationEvents.Events; +using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; using Microsoft.eShopOnContainers.Services.Basket.API.Controllers; using Microsoft.eShopOnContainers.Services.Basket.API.Model; using Moq; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; +using IBasketIdentityService = Microsoft.eShopOnContainers.Services.Basket.API.Services.IIdentityService; namespace UnitTest.Basket.Application { public class BasketWebApiTest { private readonly Mock _basketRepositoryMock; + private readonly Mock _identityServiceMock; + private readonly Mock _serviceBusMock; public BasketWebApiTest() { _basketRepositoryMock = new Mock(); + _identityServiceMock = new Mock(); + _serviceBusMock = new Mock(); } [Fact] @@ -26,9 +33,12 @@ namespace UnitTest.Basket.Application _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) .Returns(Task.FromResult(fakeCustomerBasket)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + _serviceBusMock.Setup(x => x.Publish(It.IsAny())); //Act - var basketController = new BasketController(_basketRepositoryMock.Object); + var basketController = new BasketController( + _basketRepositoryMock.Object, _identityServiceMock.Object, _serviceBusMock.Object); var actionResult = await basketController.Get(fakeCustomerId) as OkObjectResult; //Assert @@ -45,9 +55,12 @@ namespace UnitTest.Basket.Application _basketRepositoryMock.Setup(x => x.UpdateBasketAsync(It.IsAny())) .Returns(Task.FromResult(fakeCustomerBasket)); - + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + _serviceBusMock.Setup(x => x.Publish(It.IsAny())); //Act - var basketController = new BasketController(_basketRepositoryMock.Object); + var basketController = new BasketController( + _basketRepositoryMock.Object, _identityServiceMock.Object, _serviceBusMock.Object); + var actionResult = await basketController.Post(fakeCustomerBasket) as OkObjectResult; //Assert @@ -55,6 +68,38 @@ namespace UnitTest.Basket.Application Assert.Equal(((CustomerBasket)actionResult.Value).BuyerId, fakeCustomerId); } + [Fact] + public async Task Doing_Checkout_Without_Basket_Should_Return_Bad_Request() + { + var fakeCustomerId = "2"; + _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) + .Returns(Task.FromResult((CustomerBasket)null)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + //Act + var basketController = new BasketController( + _basketRepositoryMock.Object, _identityServiceMock.Object, _serviceBusMock.Object); + + var result = await basketController.Checkout() as BadRequestResult; + Assert.NotNull(result); + } + + [Fact] + public async Task Doing_Checkout_Wit_Basket_Should_Publish_UserCheckoutAccepted_Integration_Event() + { + var fakeCustomerId = "1"; + var fakeCustomerBasket = GetCustomerBasketFake(fakeCustomerId); + _basketRepositoryMock.Setup(x => x.GetBasketAsync(It.IsAny())) + .Returns(Task.FromResult(fakeCustomerBasket)); + _identityServiceMock.Setup(x => x.GetUserIdentity()).Returns(fakeCustomerId); + //Act + var basketController = new BasketController( + _basketRepositoryMock.Object, _identityServiceMock.Object, _serviceBusMock.Object); + + var result = await basketController.Checkout() as AcceptedResult; + _serviceBusMock.Verify(mock => mock.Publish(It.IsAny()), Times.Once); + Assert.NotNull(result); + } + private CustomerBasket GetCustomerBasketFake(string fakeCustomerId) { return new CustomerBasket(fakeCustomerId) diff --git a/test/Services/UnitTest/Ordering/Application/IdentifierCommandHandlerTest.cs b/test/Services/UnitTest/Ordering/Application/IdentifierCommandHandlerTest.cs index 66070c497..2a4a356ec 100644 --- a/test/Services/UnitTest/Ordering/Application/IdentifierCommandHandlerTest.cs +++ b/test/Services/UnitTest/Ordering/Application/IdentifierCommandHandlerTest.cs @@ -70,6 +70,7 @@ namespace UnitTest.Ordering.Application private CreateOrderCommand FakeOrderRequest(Dictionary args = null) { return new CreateOrderCommand( + null, city: args != null && args.ContainsKey("city") ? (string)args["city"] : null, street: args != null && args.ContainsKey("street") ? (string)args["street"] : null, state: args != null && args.ContainsKey("state") ? (string)args["state"] : null, diff --git a/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs b/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs index 22760fc7d..9a4a70bf8 100644 --- a/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs +++ b/test/Services/UnitTest/Ordering/Application/NewOrderCommandHandlerTest.cs @@ -72,6 +72,7 @@ namespace UnitTest.Ordering.Application private CreateOrderCommand FakeOrderRequestWithBuyer(Dictionary args = null) { return new CreateOrderCommand( + null, city: args != null && args.ContainsKey("city") ? (string)args["city"] : null, street: args != null && args.ContainsKey("street") ? (string)args["street"] : null, state: args != null && args.ContainsKey("state") ? (string)args["state"] : null, diff --git a/test/Services/UnitTest/UnitTest.csproj b/test/Services/UnitTest/UnitTest.csproj index 63eaf2fcd..ef467c5b6 100644 --- a/test/Services/UnitTest/UnitTest.csproj +++ b/test/Services/UnitTest/UnitTest.csproj @@ -13,6 +13,7 @@ +