Browse Source

Merge from Dev

pull/809/head
Ramón Tomás 7 years ago
parent
commit
94c9b0667f
106 changed files with 1673 additions and 177 deletions
  1. +13
    -13
      README.md
  2. +13
    -1
      docker-compose-windows.override.yml
  3. +13
    -1
      docker-compose-windows.prod.yml
  4. +13
    -1
      docker-compose-windows.yml
  5. +7
    -1
      docker-compose.override.yml
  6. +3
    -0
      docker-compose.prod.yml
  7. +4
    -0
      docker-compose.yml
  8. BIN
      docs/Enterprise-Application-Patterns-using-XamarinForms.pdf
  9. BIN
      docs/NET-Microservices-Architecture-for-Containerized-NET-Applications-(Microsoft-eBook).pdf
  10. BIN
      img/xamarin-enterprise-patterns-ebook-cover-small.png
  11. +6
    -6
      src/Mobile/README.md
  12. +7
    -1
      src/Mobile/eShopOnContainers/eShopOnContainers.Core/GlobalSettings.cs
  13. +22
    -0
      src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Token/UserToken.cs
  14. +5
    -1
      src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs
  15. +22
    -5
      src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs
  16. +2
    -0
      src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs
  17. +33
    -0
      src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs
  18. +8
    -7
      src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs
  19. +4
    -0
      src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj
  20. +2
    -1
      src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/project.json
  21. +1
    -0
      src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj
  22. +12
    -12
      src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs
  23. +2
    -1
      src/Services/Identity/Identity.API/Configuration/Config.cs
  24. +1
    -1
      src/Services/Identity/Identity.API/Services/ProfileService.cs
  25. +2
    -1
      src/Services/Location/Locations.API/Controllers/LocationsController.cs
  26. +8
    -4
      src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs
  27. +1
    -1
      src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs
  28. +2
    -2
      src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs
  29. +1
    -1
      src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs
  30. +39
    -19
      src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs
  31. +18
    -0
      src/Services/Location/Locations.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs
  32. +5
    -0
      src/Services/Location/Locations.API/Locations.API.csproj
  33. +2
    -0
      src/Services/Location/Locations.API/Model/Locations.cs
  34. +2
    -3
      src/Services/Location/Locations.API/Model/UserLocation.cs
  35. +14
    -0
      src/Services/Location/Locations.API/Model/UserLocationDetails.cs
  36. +40
    -10
      src/Services/Location/Locations.API/Startup.cs
  37. +71
    -11
      src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs
  38. +2
    -2
      src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs
  39. +28
    -0
      src/Services/Marketing/Marketing.API/Controllers/PicController.cs
  40. +3
    -1
      src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs
  41. +12
    -0
      src/Services/Marketing/Marketing.API/Dto/UserLocationDTO.cs
  42. +9
    -5
      src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs
  43. +10
    -8
      src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs
  44. +10
    -4
      src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.Designer.cs
  45. +4
    -3
      src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.cs
  46. +8
    -2
      src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs
  47. +26
    -0
      src/Services/Marketing/Marketing.API/Infrastructure/MarketingReadDataContext.cs
  48. +11
    -0
      src/Services/Marketing/Marketing.API/Infrastructure/Repositories/IMarketingDataRepository.cs
  49. +41
    -0
      src/Services/Marketing/Marketing.API/Infrastructure/Repositories/MarketingDataRepository.cs
  50. +18
    -0
      src/Services/Marketing/Marketing.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs
  51. +46
    -0
      src/Services/Marketing/Marketing.API/IntegrationEvents/Handlers/UserLocationUpdatedIntegrationEventHandler.cs
  52. +21
    -2
      src/Services/Marketing/Marketing.API/Marketing.API.csproj
  53. +3
    -0
      src/Services/Marketing/Marketing.API/MarketingSettings.cs
  54. +3
    -1
      src/Services/Marketing/Marketing.API/Model/Campaign.cs
  55. +16
    -0
      src/Services/Marketing/Marketing.API/Model/Location.cs
  56. +19
    -0
      src/Services/Marketing/Marketing.API/Model/MarketingData.cs
  57. +51
    -0
      src/Services/Marketing/Marketing.API/Model/RuleType.cs
  58. +0
    -20
      src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs
  59. +9
    -0
      src/Services/Marketing/Marketing.API/Model/UserLocationDetails.cs
  60. BIN
      src/Services/Marketing/Marketing.API/Pics/1.png
  61. BIN
      src/Services/Marketing/Marketing.API/Pics/2.png
  62. +1
    -0
      src/Services/Marketing/Marketing.API/Program.cs
  63. +83
    -4
      src/Services/Marketing/Marketing.API/Startup.cs
  64. +23
    -0
      src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs
  65. +4
    -1
      src/Services/Marketing/Marketing.API/appsettings.json
  66. +3
    -2
      src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs
  67. +1
    -0
      src/Web/WebMVC/AppSettings.cs
  68. +64
    -0
      src/Web/WebMVC/Controllers/CampaignsController.cs
  69. +17
    -2
      src/Web/WebMVC/Infrastructure/API.cs
  70. +70
    -0
      src/Web/WebMVC/Services/CampaignService.cs
  71. +13
    -0
      src/Web/WebMVC/Services/ICampaignService.cs
  72. +2
    -1
      src/Web/WebMVC/Startup.cs
  73. +12
    -0
      src/Web/WebMVC/ViewModels/Campaign.cs
  74. +19
    -0
      src/Web/WebMVC/ViewModels/CampaignItem.cs
  75. +12
    -0
      src/Web/WebMVC/ViewModels/CampaignViewModel/CampaignViewModel.cs
  76. +28
    -0
      src/Web/WebMVC/Views/Campaigns/Details.cshtml
  77. +37
    -0
      src/Web/WebMVC/Views/Campaigns/Index.cshtml
  78. +17
    -0
      src/Web/WebMVC/Views/Campaigns/_campaign.cshtml
  79. +32
    -0
      src/Web/WebMVC/Views/Campaigns/_pagination.cshtml
  80. +5
    -0
      src/Web/WebMVC/Views/Order/Index.cshtml
  81. +1
    -0
      src/Web/WebMVC/Views/Shared/_Layout.cshtml
  82. +8
    -0
      src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml
  83. +16
    -0
      src/Web/WebMVC/WebMVC.csproj
  84. +1
    -0
      src/Web/WebMVC/appsettings.json
  85. +98
    -0
      src/Web/WebMVC/wwwroot/css/campaigns/campaigns.component.css
  86. +2
    -2
      src/Web/WebMVC/wwwroot/css/shared/components/identity/identity.css
  87. +10
    -0
      src/Web/WebSPA/Client/modules/basket/basket.service.ts
  88. +14
    -0
      src/Web/WebSPA/Client/modules/orders/orders.component.ts
  89. +4
    -0
      src/Web/WebSPA/WebSPA.csproj
  90. +35
    -0
      test/Services/FunctionalTests/FunctionalTests.csproj
  91. +1
    -1
      test/Services/FunctionalTests/Middleware/AutoAuthorizeMiddleware.cs
  92. +39
    -0
      test/Services/FunctionalTests/Services/Location/LocationsScenariosBase.cs
  93. +45
    -0
      test/Services/FunctionalTests/Services/Location/LocationsTestsStartup.cs
  94. +8
    -0
      test/Services/FunctionalTests/Services/Location/appsettings.json
  95. +35
    -0
      test/Services/FunctionalTests/Services/Marketing/CampaignScenariosBase.cs
  96. +58
    -0
      test/Services/FunctionalTests/Services/Marketing/MarketingScenarios.cs
  97. +20
    -0
      test/Services/FunctionalTests/Services/Marketing/MarketingScenariosBase.cs
  98. +26
    -0
      test/Services/FunctionalTests/Services/Marketing/MarketingTestsStartup.cs
  99. +40
    -0
      test/Services/FunctionalTests/Services/Marketing/UserLocationRoleScenariosBase.cs
  100. +8
    -0
      test/Services/FunctionalTests/Services/Marketing/appsettings.json

+ 13
- 13
README.md View File

@ -3,20 +3,20 @@ Sample .NET Core reference application, powered by Microsoft, based on a simplif
**Note for Pull Requests**: We accept pull request from the community. When doing it, please do it onto the DEV branch which is the consolidated work-in-progress branch. Do not request it onto Master, if possible.
> ### DISCLAIMER
> **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.**
> **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. 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.
> <p>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).
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.
> <p>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).
> 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.
> <p> In the future we might fork this project and make multiple versions targeting specific microservice cluster/orchestrators plus using additional cloud infrastructure. <p>
> <img src="img/exploring-to-production-ready.png">
> Read the planned <a href='https://github.com/dotnet/eShopOnContainers/wiki/01.-Roadmap-and-Milestones-for-future-releases'>Roadmap and Milestones for future releases of eShopOnContainers</a> within the Wiki for further info about possible new implementations and provide feedback at the <a href='https://github.com/dotnet/eShopOnContainers/issues'>ISSUES section</a> if you'd like to see any specific scenario implemented or improved. Also, feel free to discuss on any current issue.
**Architecture overview**: This reference application is cross-platform either at the server and client side, thanks to .NET Core services capable of running on Linux or Windows containers depending on your Docker host, and to Xamarin for mobile apps running on Android, iOS or Windows/UWP plus any browser for the client web apps.
The architecture proposes a simplified microservice oriented architecture implementation with multiple autonomous microservices (each one owning its own data/db) and implementing different approaches within each microservice (simple CRUD vs. DDD/CQRS patterns) using Http as the current communication protocol.
**Architecture overview**: This reference application is cross-platform either at the server and client side, thanks to .NET Core services capable of running on Linux or Windows containers depending on your Docker host, and to Xamarin for mobile apps running on Android, iOS or Windows/UWP plus any browser for the client web apps.
The architecture proposes a simplified microservice oriented architecture implementation with multiple autonomous microservices (each one owning its own data/db) and implementing different approaches within each microservice (simple CRUD vs. DDD/CQRS patterns) using Http as the current communication protocol.
<p>
It also supports asynchronous communication for data updates propagation across multiple services based on Integration Events and an Event Bus plus other features defined at the <a href='https://github.com/dotnet/eShopOnContainers/wiki/01.-Roadmap-and-Milestones-for-future-releases'>roadmap</a>.
It also supports asynchronous communication for data updates propagation across multiple services based on Integration Events and an Event Bus plus other features defined at the <a href='https://github.com/dotnet/eShopOnContainers/wiki/01.-Roadmap-and-Milestones-for-future-releases'>roadmap</a>.
<p>
<img src="img/eshop_logo.png">
<img src="img/eShopOnContainers_Architecture_Diagram.png">
@ -43,7 +43,7 @@ You can download them and start reviewing these Guides/eBooks here:
| Architecting & Developing | Containers Lifecycle & CI/CD | App patterns with Xamarin.Forms |
| ------------ | ------------| ------------|
| <a href='https://aka.ms/microservicesebook'><img src="img/ebook_arch_dev_microservices_containers_cover.png"> </a> | <a href='https://aka.ms/dockerlifecycleebook'> <img src="img/ebook_containers_lifecycle.png"> </a> | <a href='https://aka.ms/xamarinpatternsebook'> <img src="img/xamarin-enterprise-patterns-ebook-cover-small.png"> </a> |
| <sup> <a href='https://aka.ms/microservicesebook'>**Download** (First Edition)</a> </sup> | <sup> <a href='https://aka.ms/dockerlifecycleebook'>**Download** (First Edition from late 2016) </a> </sup> | <sup> <a href='https://aka.ms/xamarinpatternsebook'>**Download** (Preview Edition) </a> </sup> |
| <sup> <a href='https://aka.ms/microservicesebook'>**Download** (First Edition)</a> </sup> | <sup> <a href='https://aka.ms/dockerlifecycleebook'>**Download** (First Edition from late 2016) </a> </sup> | <sup> <a href='https://aka.ms/xamarinpatternsebook'>**Download** (First Edition) </a> </sup> |
Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com)
<p>
@ -66,7 +66,7 @@ Finally, those microservices are consumed by multiple client web and mobile apps
<b>*MVC Application (ASP.NET Core)*</b>: Its an MVC application where you can find interesting scenarios on how to consume HTTP-based microservices from C# running in the server side, as it is a typical ASP.NET Core MVC application. Since it is a server-side application, access to other containers/microservices is done within the internal Docker Host network with its internal name resolution.
<img src="img/eshop-webmvc-app-screenshot.png">
<br>
<b>*SPA (Single Page Application)*</b>: Providing similar "eShop business functionality" but developed with Angular 2, Typescript and slightly using ASP.NET Core MVC. This is another approach for client web applications to be used when you want to have a more modern client behavior which is not behaving with the typical browser round-trip on every action but behaving like a Single-Page-Application which is more similar to a desktop app usage experience. The consumption of the HTTP-based microservices is done from TypeScript/JavaScript in the client browser, so the client calls to the microservices come from out of the Docker Host internal network (Like from your network or even from the Internet).
<b>*SPA (Single Page Application)*</b>: Providing similar "eShop business functionality" but developed with Angular 2, Typescript and slightly using ASP.NET Core MVC. This is another approach for client web applications to be used when you want to have a more modern client behavior which is not behaving with the typical browser round-trip on every action but behaving like a Single-Page-Application which is more similar to a desktop app usage experience. The consumption of the HTTP-based microservices is done from TypeScript/JavaScript in the client browser, so the client calls to the microservices come from out of the Docker Host internal network (Like from your network or even from the Internet).
<img src="img/eshop-webspa-app-screenshot.png">
<br>
<b>*Xamarin Mobile App (For iOS, Android and Windows/UWP)*</b>: It is a client mobile app supporting the most common mobile OS platforms (iOS, Android and Windows/UWP). In this case, the consumption of the microservices is done from C# but running on the client devices, so out of the Docker Host internal network (Like from your network or even the Internet).
@ -76,19 +76,19 @@ Finally, those microservices are consumed by multiple client web and mobile apps
## Setting up your development environment for eShopOnContainers
### Visual Studio 2017 and Windows based
This is the more straightforward way to get started:
https://github.com/dotnet/eShopOnContainers/wiki/02.-Setting-eShopOnContainer-solution-up-in-a-Visual-Studio-2017-environment
https://github.com/dotnet-architecture/eShopOnContainers/wiki/02.-Setting-eShopOnContainers-in-a-Visual-Studio-2017-environment
### CLI and Windows based
For those who prefer the CLI on Windows, using dotnet CLI, docker CLI and VS Code for Windows:
For those who prefer the CLI on Windows, using dotnet CLI, docker CLI and VS Code for Windows:
https://github.com/dotnet/eShopOnContainers/wiki/03.-Setting-the-eShopOnContainers-solution-up-in-a-Windows-CLI-environment-(dotnet-CLI,-Docker-CLI-and-VS-Code)
### CLI and Mac based
For those who prefer the CLI on a Mac, using dotnet CLI, docker CLI and VS Code for Mac
For those who prefer the CLI on a Mac, using dotnet CLI, docker CLI and VS Code for Mac
(Instructions still TBD, but similar to Windows CLI):
https://github.com/dotnet/eShopOnContainers/wiki/04.-Setting-eShopOnContainer-solution-up-in-a-Mac,-VS-Code-and-CLI-environment--(dotnet-CLI,-Docker-CLI-and-VS-Code)
> ### Note on tested Docker Containers/Images
> Most of the development and testing of this project was (as of early March 2017) done <b> on Docker Linux containers</b> running in development machines with "Docker for Windows" and the default Hyper-V Linux VM (MobiLinuxVM) installed by "Docker for Windows".
> Most of the development and testing of this project was (as of early March 2017) done <b> on Docker Linux containers</b> running in development machines with "Docker for Windows" and the default Hyper-V Linux VM (MobiLinuxVM) installed by "Docker for Windows".
The <b>Windows Containers scenario is currently being implemented/tested yet</b>. The application should be able to run on Windows Nano Containers based on different Docker base images, as well, as the .NET Core services have also been tested running on plain Windows (with no Docker).
The app was also partially tested on "Docker for Mac" using a development MacOS machine with .NET Core and VS Code installed, which is still a scenario using Linux containers running on the VM setup in the Mac by the "Docker for Windows" setup. But further testing and feedback on Mac environments and Windows Containers, from the community, will be appreciated.
@ -118,4 +118,4 @@ You can create new issues at the issues section, do pull requests and/or send em
## Questions
[QUESTION] Answer +1 if the solution is working for you (Through VS2017 or CLI environment):
https://github.com/dotnet/eShopOnContainers/issues/107
https://github.com/dotnet/eShopOnContainers/issues/107

+ 13
- 1
docker-compose-windows.override.yml View File

@ -95,4 +95,16 @@ services:
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
- EventBusConnection=rabbitmq
ports:
- "5109:80"
- "5109:80"
marketing.api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word
- EventBusConnection=rabbitmq
- MongoConnectionString=mongodb://nosql.data
- MongoDatabase=MarketingDb
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
ports:
- "5110:80"

+ 13
- 1
docker-compose-windows.prod.yml View File

@ -75,7 +75,19 @@ services:
- BasketUrl=http://basket.api
ports:
- "5100:80"
marketing.api:
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word
- EventBusConnection=rabbitmq
- MongoConnectionString=mongodb://nosql.data
- MongoDatabase=MarketingDb
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
ports:
- "5110:80"
sql.data:
environment:
- SA_PASSWORD=Pass@word


+ 13
- 1
docker-compose-windows.yml View File

@ -61,7 +61,19 @@ services:
dockerfile: Dockerfile
depends_on:
- nosql.data
- rabbitmq
marketing.api:
image: eshop/marketing.api
build:
context: ./src/Services/Marketing/Marketing.API
dockerfile: Dockerfile
depends_on:
- sql.data
- nosql.data
- identity.api
- rabbitmq
sql.data:
image: microsoft/mssql-server-windows


+ 7
- 1
docker-compose.override.yml View File

@ -58,6 +58,10 @@ services:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word
- MongoConnectionString=mongodb://nosql.data
- MongoDatabase=MarketingDb
- EventBusConnection=rabbitmq
- ExternalCatalogBaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5110 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
ports:
- "5110:80"
@ -70,6 +74,7 @@ services:
- OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102
- IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
- BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103
- MarketingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5110
- CatalogUrlHC=http://catalog.api/hc
- OrderingUrlHC=http://ordering.api/hc
- IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser.
@ -84,7 +89,8 @@ services:
- CatalogUrl=http://catalog.api
- OrderingUrl=http://ordering.api
- BasketUrl=http://basket.api
- IdentityUrl=http://10.0.75.1:5105 #Local: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser.
- IdentityUrl=http://10.0.75.1:5105
- MarketingUrl=http://marketing.api #Local: Use 10.0.75.1 in a "Docker for Windows" environment, if using "localhost" from browser.
#Remote: Use ${ESHOP_EXTERNAL_DNS_NAME_OR_IP} if using external IP or DNS name from browser.
ports:
- "5100:80"


+ 3
- 0
docker-compose.prod.yml View File

@ -59,6 +59,9 @@ services:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word
- MongoConnectionString=mongodb://nosql.data
- MongoDatabase=MarketingDb
- EventBusConnection=rabbitmq
- identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
ports:
- "5110:80"


+ 4
- 0
docker-compose.yml View File

@ -53,7 +53,9 @@ services:
dockerfile: Dockerfile
depends_on:
- sql.data
- nosql.data
- identity.api
- rabbitmq
webspa:
image: eshop/webspa
@ -74,6 +76,7 @@ services:
- ordering.api
- identity.api
- basket.api
- marketing.api
sql.data:
image: microsoft/mssql-server-linux
@ -112,3 +115,4 @@ services:
dockerfile: Dockerfile
depends_on:
- nosql.data
- rabbitmq

BIN
docs/Enterprise-Application-Patterns-using-XamarinForms.pdf View File


BIN
docs/NET-Microservices-Architecture-for-Containerized-NET-Applications-(Microsoft-eBook).pdf View File


BIN
img/xamarin-enterprise-patterns-ebook-cover-small.png View File

Before After
Width: 260  |  Height: 336  |  Size: 25 KiB Width: 260  |  Height: 336  |  Size: 38 KiB

+ 6
- 6
src/Mobile/README.md View File

@ -1,12 +1,12 @@
#eShopOnContainers
# eShopOnContainers
eShopOnContainers is a reference app whose imagined purpose is to serve the mobile workforce of a fictitious company that sells products. The app allow to manage the catalog, view products, manage the basket and the orders.
<img src="Images/eShopOnContainers_Architecture_Diagram.png" alt="eShopOnContainers" Width="800" />
###Supported platforms: iOS, Android and Windows
### Supported platforms: iOS, Android and Windows
###The app architecture consists of two parts:
### The app architecture consists of two parts:
1. A Xamarin.Forms mobile app for iOS, Android and Windows.
2. Several .NET Web API microservices deployed as Docker containers.
@ -34,7 +34,7 @@ This project exercises the following platforms, frameworks or features:
* Entity Framework
* Identity Server 4
##Three platforms
## Three platforms
The app targets **three** platforms:
* iOS
@ -45,7 +45,7 @@ The app targets **three** platforms:
As of 07/03/2017, eShopOnContainers features **89.2% code share** (7.2% iOS / 16.7% Android / 8.7% Windows).
##Licenses
## Licenses
This project uses some third-party assets with a license that requires attribution:
@ -155,4 +155,4 @@ In the configuration window of the machine, go to the Compatibility section and
<img src="Images/set-compatibility-vs-sml.png" alt="Migrate to a physical computer with a different processor version" Width="600" />
## Copyright and license
* Code and documentation copyright 2017 Microsoft Corp. Code released under the [MIT license](https://opensource.org/licenses/MIT).
* Code and documentation copyright 2017 Microsoft Corp. Code released under the [MIT license](https://opensource.org/licenses/MIT).

+ 7
- 1
src/Mobile/eShopOnContainers/eShopOnContainers.Core/GlobalSettings.cs View File

@ -6,7 +6,6 @@
public const string MockTag = "Mock";
public const string DefaultEndpoint = "http://13.88.8.119";
private string _baseEndpoint;
private static readonly GlobalSetting _instance = new GlobalSetting();
@ -31,6 +30,10 @@
}
}
public string ClientId { get { return "xamarin"; }}
public string ClientSecret { get { return "secret"; }}
public string AuthToken { get; set; }
public string RegisterWebsite { get; set; }
@ -47,6 +50,8 @@
public string UserInfoEndpoint { get; set; }
public string TokenEndpoint { get; set; }
public string LogoutEndpoint { get; set; }
public string IdentityCallback { get; set; }
@ -61,6 +66,7 @@
BasketEndpoint = string.Format("{0}:5103", baseEndpoint);
IdentityEndpoint = string.Format("{0}:5105/connect/authorize", baseEndpoint);
UserInfoEndpoint = string.Format("{0}:5105/connect/userinfo", baseEndpoint);
TokenEndpoint = string.Format("{0}:5105/connect/token", baseEndpoint);
LogoutEndpoint = string.Format("{0}:5105/connect/endsession", baseEndpoint);
IdentityCallback = string.Format("{0}:5105/xamarincallback", baseEndpoint);
LogoutCallback = string.Format("{0}:5105/Account/Redirecting", baseEndpoint);


+ 22
- 0
src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Token/UserToken.cs View File

@ -0,0 +1,22 @@
using Newtonsoft.Json;
namespace eShopOnContainers.Core.Models.Token
{
public class UserToken
{
[JsonProperty("id_token")]
public string IdToken { get; set; }
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
}
}

+ 5
- 1
src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs View File

@ -1,8 +1,12 @@
namespace eShopOnContainers.Core.Services.Identity
using eShopOnContainers.Core.Models.Token;
using System.Threading.Tasks;
namespace eShopOnContainers.Core.Services.Identity
{
public interface IIdentityService
{
string CreateAuthorizationRequest();
string CreateLogoutRequest(string token);
Task<UserToken> GetTokenAsync(string code);
}
}

+ 22
- 5
src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs View File

@ -1,11 +1,22 @@
using IdentityModel.Client;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using eShopOnContainers.Core.Services.RequestProvider;
using eShopOnContainers.Core.Models.Token;
namespace eShopOnContainers.Core.Services.Identity
{
public class IdentityService : IIdentityService
{
private readonly IRequestProvider _requestProvider;
public IdentityService(IRequestProvider requestProvider)
{
_requestProvider = requestProvider;
}
public string CreateAuthorizationRequest()
{
// Create URI to authorization endpoint
@ -13,11 +24,10 @@ namespace eShopOnContainers.Core.Services.Identity
// Dictionary with values for the authorize request
var dic = new Dictionary<string, string>();
dic.Add("client_id", "xamarin");
dic.Add("client_secret", "secret");
dic.Add("response_type", "code id_token token");
dic.Add("client_id", GlobalSetting.Instance.ClientId);
dic.Add("client_secret", GlobalSetting.Instance.ClientSecret);
dic.Add("response_type", "code id_token");
dic.Add("scope", "openid profile basket orders locations offline_access");
dic.Add("redirect_uri", GlobalSetting.Instance.IdentityCallback);
dic.Add("nonce", Guid.NewGuid().ToString("N"));
@ -31,7 +41,7 @@ namespace eShopOnContainers.Core.Services.Identity
public string CreateLogoutRequest(string token)
{
if(string.IsNullOrEmpty(token))
if (string.IsNullOrEmpty(token))
{
return string.Empty;
}
@ -41,5 +51,12 @@ namespace eShopOnContainers.Core.Services.Identity
token,
GlobalSetting.Instance.LogoutCallback);
}
public async Task<UserToken> GetTokenAsync(string code)
{
string data = string.Format("grant_type=authorization_code&code={0}&redirect_uri={1}", code, WebUtility.UrlEncode(GlobalSetting.Instance.IdentityCallback));
var token = await _requestProvider.PostAsync<UserToken>(GlobalSetting.Instance.TokenEndpoint, data, GlobalSetting.Instance.ClientId, GlobalSetting.Instance.ClientSecret);
return token;
}
}
}

+ 2
- 0
src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs View File

@ -8,6 +8,8 @@ namespace eShopOnContainers.Core.Services.RequestProvider
Task<TResult> PostAsync<TResult>(string uri, TResult data, string token = "", string header = "");
Task<TResult> PostAsync<TResult>(string uri, string data, string clientId, string clientSecret);
Task DeleteAsync(string uri, string token = "");
}
}

+ 33
- 0
src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs View File

@ -61,6 +61,28 @@ namespace eShopOnContainers.Core.Services.RequestProvider
return result;
}
public async Task<TResult> PostAsync<TResult>(string uri, string data, string clientId, string clientSecret)
{
HttpClient httpClient = CreateHttpClient(string.Empty);
if (!string.IsNullOrWhiteSpace(clientId) && !string.IsNullOrWhiteSpace(clientSecret))
{
AddBasicAuthenticationHeader(httpClient, clientId, clientSecret);
}
var content = new StringContent(data);
content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
HttpResponseMessage response = await httpClient.PostAsync(uri, content);
await HandleResponse(response);
string serialized = await response.Content.ReadAsStringAsync();
TResult result = await Task.Run(() =>
JsonConvert.DeserializeObject<TResult>(serialized, _serializerSettings));
return result;
}
public async Task DeleteAsync(string uri, string token = "")
{
HttpClient httpClient = CreateHttpClient(token);
@ -90,6 +112,17 @@ namespace eShopOnContainers.Core.Services.RequestProvider
httpClient.DefaultRequestHeaders.Add(parameter, Guid.NewGuid().ToString());
}
private void AddBasicAuthenticationHeader(HttpClient httpClient, string clientId, string clientSecret)
{
if (httpClient == null)
return;
if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
return;
httpClient.DefaultRequestHeaders.Authorization = new BasicAuthenticationHeaderValue(clientId, clientSecret);
}
private async Task HandleResponse(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)


+ 8
- 7
src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs View File

@ -203,16 +203,15 @@ namespace eShopOnContainers.Core.ViewModels
private void Logout()
{
var authIdToken = Settings.AuthIdToken;
var logoutRequest = _identityService.CreateLogoutRequest(authIdToken);
if(!string.IsNullOrEmpty(logoutRequest))
if (!string.IsNullOrEmpty(logoutRequest))
{
// Logout
LoginUrl = logoutRequest;
}
if(Settings.UseMocks)
if (Settings.UseMocks)
{
Settings.AuthAccessToken = string.Empty;
Settings.AuthIdToken = string.Empty;
@ -235,12 +234,14 @@ namespace eShopOnContainers.Core.ViewModels
else if (unescapedUrl.Contains(GlobalSetting.Instance.IdentityCallback))
{
var authResponse = new AuthorizeResponse(url);
if (!string.IsNullOrWhiteSpace(authResponse.AccessToken))
if (!string.IsNullOrWhiteSpace(authResponse.Code))
{
if (authResponse.AccessToken != null)
var userToken = await _identityService.GetTokenAsync(authResponse.Code);
string accessToken = userToken.AccessToken;
if (!string.IsNullOrWhiteSpace(accessToken))
{
Settings.AuthAccessToken = authResponse.AccessToken;
Settings.AuthAccessToken = accessToken;
Settings.AuthIdToken = authResponse.IdentityToken;
await NavigationService.NavigateToAsync<MainViewModel>();


+ 4
- 0
src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj View File

@ -170,6 +170,7 @@
<Compile Include="Converters\FirstValidationErrorConverter.cs" />
<Compile Include="Effects\EntryLineColorEffect.cs" />
<Compile Include="Behaviors\LineColorBehavior.cs" />
<Compile Include="Models\Token\UserToken.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
@ -263,6 +264,9 @@
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Models\Token\" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.ComponentModel.Annotations">
<HintPath>..\..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.6\Profile\Profile44\System.ComponentModel.Annotations.dll</HintPath>


+ 2
- 1
src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/project.json View File

@ -1,7 +1,8 @@
{
"dependencies": {
"Xamarin.Forms": "2.3.4.231",
"xunit": "2.2.0"
"xunit": "2.2.0",
"xunit.runner.console": "2.2.0"
},
"frameworks": {
".NETPortable,Version=v4.5,Profile=Profile111": {}


+ 1
- 0
src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj View File

@ -13,6 +13,7 @@
<AssemblyName>eShopOnContainersiOS</AssemblyName>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<SkipValidatePackageReferences>true</SkipValidatePackageReferences>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' ">
<DebugSymbols>true</DebugSymbols>


+ 12
- 12
src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs View File

@ -70,18 +70,18 @@
{
return new List<CatalogItem>()
{
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/1", AvailableStock = 1},
new CatalogItem() { CatalogTypeId=1,CatalogBrandId=2, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/2", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/3", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/4", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=5, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/5", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/6", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/7", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/8", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/9", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/10", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11", AvailableStock = 1 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/12", AvailableStock = 1 }
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/1", AvailableStock = 100},
new CatalogItem() { CatalogTypeId=1,CatalogBrandId=2, Description = ".NET Black & White Mug", Name = ".NET Black & White Mug", Price= 8.50M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/2", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/3", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Foundation T-shirt", Name = ".NET Foundation T-shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/4", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=5, Description = "Roslyn Red Sheet", Name = "Roslyn Red Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/5", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Blue Hoodie", Name = ".NET Blue Hoodie", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/6", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/7", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Kudu Purple Hoodie", Name = "Kudu Purple Hoodie", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/8", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/9", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = ".NET Foundation Sheet", Name = ".NET Foundation Sheet", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/10", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11", AvailableStock = 100 },
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=5, Description = "Prism White TShirt", Name = "Prism White TShirt", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/12", AvailableStock = 100 }
};
}
}


+ 2
- 1
src/Services/Identity/Identity.API/Configuration/Config.cs View File

@ -108,7 +108,8 @@ namespace Identity.API.Configuration
IdentityServerConstants.StandardScopes.OfflineAccess,
"orders",
"basket",
"locations"
"locations",
"marketing"
},
}
};


+ 1
- 1
src/Services/Identity/Identity.API/Services/ProfileService.cs View File

@ -74,7 +74,7 @@ namespace Identity.API.Services
if (!string.IsNullOrWhiteSpace(user.Name))
claims.Add(new Claim("name", user.Name));
if (!string.IsNullOrWhiteSpace(user.Name))
if (!string.IsNullOrWhiteSpace(user.LastName))
claims.Add(new Claim("last_name", user.LastName));
if (!string.IsNullOrWhiteSpace(user.CardNumber))


+ 2
- 1
src/Services/Location/Locations.API/Controllers/LocationsController.cs View File

@ -41,7 +41,7 @@ namespace Locations.API.Controllers
//GET api/v1/[controller]/1
[Route("{locationId}")]
[HttpGet]
public async Task<IActionResult> GetLocation(string locationId)
public async Task<IActionResult> GetLocation(int locationId)
{
var location = await _locationsService.GetLocation(locationId);
return Ok(location);
@ -54,6 +54,7 @@ namespace Locations.API.Controllers
{
var userId = _identityService.GetUserIdentity();
var result = await _locationsService.AddOrUpdateUserLocation(userId, newLocReq);
return result ?
(IActionResult)Ok() :
(IActionResult)BadRequest();


+ 8
- 4
src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs View File

@ -32,7 +32,8 @@
var us = new Locations()
{
Code = "US",
Description = "United States"
Description = "United States",
LocationId = 1
};
us.SetLocation(-101.357386, 41.650455);
us.SetArea(GetUSPoligon());
@ -46,7 +47,8 @@
{
Parent_Id = parentId,
Code = "WHT",
Description = "Washington"
Description = "Washington",
LocationId = 2
};
wht.SetLocation(-119.542781, 47.223652);
wht.SetArea(GetWashingtonPoligon());
@ -61,7 +63,8 @@
{
Parent_Id = parentId,
Code = "SEAT",
Description = "Seattle"
Description = "Seattle",
LocationId = 3
};
stl.SetArea(GetSeattlePoligon());
stl.SetLocation(-122.330747, 47.603111);
@ -74,7 +77,8 @@
{
Parent_Id = parentId,
Code = "REDM",
Description = "Redmond"
Description = "Redmond",
LocationId = 4
};
rdm.SetLocation(-122.122887, 47.674961);
rdm.SetArea(GetRedmondPoligon());


+ 1
- 1
src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs View File

@ -7,7 +7,7 @@
public interface ILocationsRepository
{
Task<Locations> GetAsync(string locationId);
Task<Locations> GetAsync(int locationId);
Task<List<Locations>> GetLocationListAsync();


+ 2
- 2
src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs View File

@ -20,9 +20,9 @@
_context = new LocationsContext(settings);
}
public async Task<Locations> GetAsync(string locationId)
public async Task<Locations> GetAsync(int locationId)
{
var filter = Builders<Locations>.Filter.Eq("Id", ObjectId.Parse(locationId));
var filter = Builders<Locations>.Filter.Eq("LocationId", locationId);
return await _context.Locations
.Find(filter)
.FirstOrDefaultAsync();


+ 1
- 1
src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs View File

@ -7,7 +7,7 @@
public interface ILocationsService
{
Task<Locations> GetLocation(string locationId);
Task<Locations> GetLocation(int locationId);
Task<UserLocation> GetUserLocation(string id);


+ 39
- 19
src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs View File

@ -8,29 +8,28 @@
using System.Linq;
using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Exceptions;
using System.Collections.Generic;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.Services.Locations.API.IntegrationEvents.Events;
public class LocationsService : ILocationsService
{
private ILocationsRepository _locationsRepository;
private readonly ILocationsRepository _locationsRepository;
private readonly IEventBus _eventBus;
public LocationsService(ILocationsRepository locationsRepository)
public LocationsService(ILocationsRepository locationsRepository, IEventBus eventBus)
{
_locationsRepository = locationsRepository ?? throw new ArgumentNullException(nameof(locationsRepository));
_eventBus = eventBus ?? throw new ArgumentNullException(nameof(eventBus));
}
public async Task<Locations> GetLocation(string locationId)
public async Task<Locations> GetLocation(int locationId)
{
return await _locationsRepository.GetAsync(locationId);
}
public async Task<UserLocation> GetUserLocation(string id)
public async Task<UserLocation> GetUserLocation(string userId)
{
if (!Guid.TryParse(id, out Guid userId))
{
throw new ArgumentException("Not valid userId");
}
return await _locationsRepository.GetUserLocationAsync(userId.ToString());
return await _locationsRepository.GetUserLocationAsync(userId);
}
public async Task<List<Locations>> GetAllLocation()
@ -38,13 +37,8 @@
return await _locationsRepository.GetLocationListAsync();
}
public async Task<bool> AddOrUpdateUserLocation(string id, LocationRequest currentPosition)
{
if (!Guid.TryParse(id, out Guid userId))
{
throw new ArgumentException("Not valid userId");
}
public async Task<bool> AddOrUpdateUserLocation(string userId, LocationRequest currentPosition)
{
// Get the list of ordered regions the user currently is within
var currentUserAreaLocationList = await _locationsRepository.GetCurrentUserRegionsListAsync(currentPosition);
@ -55,14 +49,40 @@
// If current area found, then update user location
var locationAncestors = new List<string>();
var userLocation = await _locationsRepository.GetUserLocationAsync(userId.ToString());
var userLocation = await _locationsRepository.GetUserLocationAsync(userId);
userLocation = userLocation ?? new UserLocation();
userLocation.UserId = userId;
userLocation.LocationId = currentUserAreaLocationList[0].Id;
userLocation.LocationId = currentUserAreaLocationList[0].LocationId;
userLocation.UpdateDate = DateTime.UtcNow;
await _locationsRepository.UpdateUserLocationAsync(userLocation);
// Publish integration event to update marketing read data model
// with the new locations updated
PublishNewUserLocationPositionIntegrationEvent(userId, currentUserAreaLocationList);
return true;
}
private void PublishNewUserLocationPositionIntegrationEvent(string userId, List<Locations> newLocations)
{
var newUserLocations = MapUserLocationDetails(newLocations);
var @event = new UserLocationUpdatedIntegrationEvent(userId, newUserLocations);
_eventBus.Publish(@event);
}
private List<UserLocationDetails> MapUserLocationDetails(List<Locations> newLocations)
{
var result = new List<UserLocationDetails>();
newLocations.ForEach(location => {
result.Add(new UserLocationDetails()
{
LocationId = location.LocationId,
Code = location.Code,
Description = location.Description
});
});
return result;
}
}
}

+ 18
- 0
src/Services/Location/Locations.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs View File

@ -0,0 +1,18 @@
namespace Microsoft.eShopOnContainers.Services.Locations.API.IntegrationEvents.Events
{
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events;
using Microsoft.eShopOnContainers.Services.Locations.API.Model;
using System.Collections.Generic;
public class UserLocationUpdatedIntegrationEvent : IntegrationEvent
{
public string UserId { get; private set; }
public List<UserLocationDetails> LocationList { get; private set; }
public UserLocationUpdatedIntegrationEvent(string userId, List<UserLocationDetails> locationList)
{
UserId = userId;
LocationList = locationList;
}
}
}

+ 5
- 0
src/Services/Location/Locations.API/Locations.API.csproj View File

@ -10,6 +10,7 @@
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.1.0" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.2.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
@ -37,5 +38,9 @@
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" />
</ItemGroup>
</Project>

+ 2
- 0
src/Services/Location/Locations.API/Model/Locations.cs View File

@ -7,8 +7,10 @@
public class Locations
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public int LocationId { get; set; }
public string Code { get; set; }
[BsonRepresentation(BsonType.ObjectId)]
public string Parent_Id { get; set; }


+ 2
- 3
src/Services/Location/Locations.API/Model/UserLocation.cs View File

@ -9,9 +9,8 @@
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public Guid UserId { get; set; }
[BsonRepresentation(BsonType.ObjectId)]
public string LocationId { get; set; }
public string UserId { get; set; }
public int LocationId { get; set; }
public DateTime UpdateDate { get; set; }
}
}

+ 14
- 0
src/Services/Location/Locations.API/Model/UserLocationDetails.cs View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Locations.API.Model
{
public class UserLocationDetails
{
public int LocationId { get; set; }
public string Code { get; set; }
public string Description { get; set; }
}
}

+ 40
- 10
src/Services/Location/Locations.API/Startup.cs View File

@ -1,17 +1,21 @@
using Microsoft.AspNetCore.Builder;
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure;
using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Filters;
using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories;
using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using System.Reflection;
using System;
using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services;
using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Filters;
namespace Microsoft.eShopOnContainers.Services.Locations.API
{
@ -37,7 +41,7 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc(options =>
@ -46,7 +50,21 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API
}).AddControllersAsServices();
services.Configure<LocationSettings>(Configuration);
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new ConnectionFactory()
{
HostName = Configuration["EventBusConnection"]
};
return new DefaultRabbitMQPersistentConnection(factory, logger);
});
RegisterServiceBus(services);
// Add framework services.
services.AddSwaggerGen(options =>
{
@ -72,7 +90,13 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IIdentityService, IdentityService>();
services.AddTransient<ILocationsService, LocationsService>();
services.AddTransient<ILocationsRepository, LocationsRepository>();
services.AddTransient<ILocationsRepository, LocationsRepository>();
//configure autofac
var container = new ContainerBuilder();
container.Populate(services);
return new AutofacServiceProvider(container.Build());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -109,5 +133,11 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API
RequireHttpsMetadata = false
});
}
private void RegisterServiceBus(IServiceCollection services)
{
services.AddSingleton<IEventBus, EventBusRabbitMQ>();
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
}
}
}

+ 71
- 11
src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs View File

@ -1,23 +1,34 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers
{
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure;
using System.Threading.Tasks;
using Microsoft.eShopOnContainers.Services.Marketing.API.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopOnContainers.Services.Marketing.API.Dto;
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Infrastructure.Repositories;
using AspNetCore.Mvc;
using Infrastructure;
using System.Threading.Tasks;
using Model;
using EntityFrameworkCore;
using Dto;
using AspNetCore.Authorization;
using Extensions.Options;
using Microsoft.eShopOnContainers.Services.Marketing.API.ViewModel;
[Route("api/v1/[controller]")]
[Authorize]
public class CampaignsController : Controller
{
private readonly MarketingContext _context;
private readonly MarketingSettings _settings;
private readonly IMarketingDataRepository _marketingDataRepository;
public CampaignsController(MarketingContext context)
public CampaignsController(MarketingContext context,
IMarketingDataRepository marketingDataRepository,
IOptionsSnapshot<MarketingSettings> settings)
{
_context = context;
_marketingDataRepository = marketingDataRepository;
_settings = settings.Value;
}
[HttpGet]
@ -82,10 +93,11 @@
return NotFound();
}
campaignToUpdate.Name = campaignDto.Name;
campaignToUpdate.Description = campaignDto.Description;
campaignToUpdate.From = campaignDto.From;
campaignToUpdate.To = campaignDto.To;
campaignToUpdate.Url = campaignDto.Url;
campaignToUpdate.PictureUri = campaignDto.PictureUri;
await _context.SaveChangesAsync();
@ -112,6 +124,43 @@
return NoContent();
}
[HttpGet("user/{userId:guid}")]
public async Task<IActionResult> GetCampaignsByUserId(Guid userId, int pageSize = 10, int pageIndex = 0)
{
var marketingData = await _marketingDataRepository.GetAsync(userId.ToString());
var campaignDtoList = new List<CampaignDTO>();
if (marketingData != null)
{
var locationIdCandidateList = marketingData.Locations.Select(x => x.LocationId);
var userCampaignList = await _context.Rules
.OfType<UserLocationRule>()
.Include(c => c.Campaign)
.Where(c => c.Campaign.From <= DateTime.Now
&& c.Campaign.To >= DateTime.Now
&& locationIdCandidateList.Contains(c.LocationId))
.Select(c => c.Campaign)
.ToListAsync();
if (userCampaignList != null && userCampaignList.Any())
{
var userCampaignDtoList = MapCampaignModelListToDtoList(userCampaignList);
campaignDtoList.AddRange(userCampaignDtoList);
}
}
var totalItems = campaignDtoList.Count();
campaignDtoList = campaignDtoList
.Skip(pageSize * pageIndex)
.Take(pageSize).ToList();
var model = new PaginatedItemsViewModel<CampaignDTO>(
pageIndex, pageSize, totalItems, campaignDtoList);
return Ok(model);
}
private List<CampaignDTO> MapCampaignModelListToDtoList(List<Campaign> campaignList)
@ -129,10 +178,11 @@
return new CampaignDTO
{
Id = campaign.Id,
Name = campaign.Name,
Description = campaign.Description,
From = campaign.From,
To = campaign.To,
Url = campaign.Url,
PictureUri = GetUriPlaceholder(campaign.PictureUri)
};
}
@ -141,11 +191,21 @@
return new Campaign
{
Id = campaignDto.Id,
Name = campaignDto.Name,
Description = campaignDto.Description,
From = campaignDto.From,
To = campaignDto.To,
Url = campaignDto.Url
PictureUri = campaignDto.PictureUri
};
}
private string GetUriPlaceholder(string campaignUri)
{
var baseUri = _settings.ExternalCatalogBaseUrl;
campaignUri = campaignUri.Replace("http://externalcatalogbaseurltobereplaced", baseUri);
return campaignUri;
}
}
}

+ 2
- 2
src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs View File

@ -83,8 +83,8 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers
await _context.Rules.AddAsync(locationRule);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetLocationByCampaignAndLocationRuleId),
new { campaignId = campaignId, locationRuleId = locationRule.Id }, null);
return CreatedAtAction(nameof(GetLocationByCampaignAndLocationRuleId),
new { campaignId = campaignId, userLocationRuleId = locationRule.Id }, null);
}
[HttpDelete]


+ 28
- 0
src/Services/Marketing/Marketing.API/Controllers/PicController.cs View File

@ -0,0 +1,28 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers
{
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using System.IO;
public class PicController : Controller
{
private readonly IHostingEnvironment _env;
public PicController(IHostingEnvironment env)
{
_env = env;
}
[HttpGet]
[Route("api/v1/campaigns/{campaignId:int}/pic")]
public IActionResult GetImage(int campaignId)
{
var webRoot = _env.WebRootPath;
var path = Path.Combine(webRoot, campaignId + ".png");
var buffer = System.IO.File.ReadAllBytes(path);
return File(buffer, "image/png");
}
}
}

+ 3
- 1
src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs View File

@ -6,12 +6,14 @@
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
public string Url { get; set; }
public string PictureUri { get; set; }
}
}

+ 12
- 0
src/Services/Marketing/Marketing.API/Dto/UserLocationDTO.cs View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto
{
public class UserLocationDTO
{
public string Id { get; set; }
public Guid UserId { get; set; }
public int LocationId { get; set; }
public DateTime UpdateDate { get; set; }
}
}

+ 9
- 5
src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs View File

@ -31,8 +31,8 @@
.ForSqlServerUseSequenceHiLo("campaign_hilo")
.IsRequired();
builder.Property(m => m.Description)
.HasColumnName("Description")
builder.Property(m => m.Name)
.HasColumnName("Name")
.IsRequired();
builder.Property(m => m.From)
@ -47,6 +47,10 @@
.HasColumnName("Description")
.IsRequired();
builder.Property(m => m.PictureUri)
.HasColumnName("PictureUri")
.IsRequired();
builder.HasMany(m => m.Rules)
.WithOne(r => r.Campaign)
.HasForeignKey(r => r.CampaignId)
@ -64,9 +68,9 @@
.IsRequired();
builder.HasDiscriminator<int>("RuleTypeId")
.HasValue<UserProfileRule>((int)RuleTypeEnum.UserProfileRule)
.HasValue<PurchaseHistoryRule>((int)RuleTypeEnum.PurchaseHistoryRule)
.HasValue<UserLocationRule>((int)RuleTypeEnum.UserLocationRule);
.HasValue<UserProfileRule>(RuleType.UserProfileRule.Id)
.HasValue<PurchaseHistoryRule>(RuleType.PurchaseHistoryRule.Id)
.HasValue<UserLocationRule>(RuleType.UserLocationRule.Id);
builder.Property(r => r.Description)
.HasColumnName("Description")


+ 10
- 8
src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs View File

@ -9,7 +9,7 @@
using System.Linq;
using System.Threading.Tasks;
public class MarketingContextSeed
public static class MarketingContextSeed
{
public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, int? retry = 0)
{
@ -33,30 +33,32 @@
{
new Campaign
{
Description = "Campaign1",
Name = ".NET Bot Black Hoodie 50% OFF",
Description = "Campaign Description 1",
From = DateTime.Now,
To = DateTime.Now.AddDays(7),
Url = "http://CampaignUrl.test/12f09ed3cef54187123f500ad",
PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/campaigns/1/pic",
Rules = new List<Rule>
{
new UserLocationRule
{
Description = "UserLocationRule1",
Description = "Campaign is only for United States users.",
LocationId = 1
}
}
},
new Campaign
{
Description = "Campaign2",
From = DateTime.Now.AddDays(7),
Name = "Roslyn Red T-Shirt 3x2",
Description = "Campaign Description 2",
From = DateTime.Now.AddDays(-7),
To = DateTime.Now.AddDays(14),
Url = "http://CampaignUrl.test/02a59eda65f241871239000ff",
PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/campaigns/2/pic",
Rules = new List<Rule>
{
new UserLocationRule
{
Description = "UserLocationRule2",
Description = "Campaign is only for Seattle users.",
LocationId = 3
}
}


src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.Designer.cs → src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.Designer.cs View File

@ -8,8 +8,8 @@ using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure;
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations
{
[DbContext(typeof(MarketingContext))]
[Migration("20170609104915_Initial")]
partial class Initial
[Migration("20170615163431_Init")]
partial class Init
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
@ -33,11 +33,17 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Mark
b.Property<DateTime>("From")
.HasColumnName("From");
b.Property<string>("Name")
.IsRequired()
.HasColumnName("Name");
b.Property<string>("PictureUri")
.IsRequired()
.HasColumnName("PictureUri");
b.Property<DateTime>("To")
.HasColumnName("To");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("Campaign");

src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.cs → src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.cs View File

@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations
{
public partial class Initial : Migration
public partial class Init : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
@ -23,8 +23,9 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Mark
Id = table.Column<int>(nullable: false),
Description = table.Column<string>(nullable: false),
From = table.Column<DateTime>(nullable: false),
To = table.Column<DateTime>(nullable: false),
Url = table.Column<string>(nullable: true)
Name = table.Column<string>(nullable: false),
PictureUri = table.Column<string>(nullable: false),
To = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{

+ 8
- 2
src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs View File

@ -32,11 +32,17 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Mark
b.Property<DateTime>("From")
.HasColumnName("From");
b.Property<string>("Name")
.IsRequired()
.HasColumnName("Name");
b.Property<string>("PictureUri")
.IsRequired()
.HasColumnName("PictureUri");
b.Property<DateTime>("To")
.HasColumnName("To");
b.Property<string>("Url");
b.HasKey("Id");
b.ToTable("Campaign");


+ 26
- 0
src/Services/Marketing/Marketing.API/Infrastructure/MarketingReadDataContext.cs View File

@ -0,0 +1,26 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure
{
using Microsoft.eShopOnContainers.Services.Marketing.API.Model;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
public class MarketingReadDataContext
{
private readonly IMongoDatabase _database = null;
public MarketingReadDataContext(IOptions<MarketingSettings> settings)
{
var client = new MongoClient(settings.Value.MongoConnectionString);
if (client != null)
_database = client.GetDatabase(settings.Value.MongoDatabase);
}
public IMongoCollection<MarketingData> MarketingData
{
get
{
return _database.GetCollection<MarketingData>("MarketingReadDataModel");
}
}
}
}

+ 11
- 0
src/Services/Marketing/Marketing.API/Infrastructure/Repositories/IMarketingDataRepository.cs View File

@ -0,0 +1,11 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Repositories
{
using Model;
using System.Threading.Tasks;
public interface IMarketingDataRepository
{
Task<MarketingData> GetAsync(string userId);
Task UpdateLocationAsync(MarketingData marketingData);
}
}

+ 41
- 0
src/Services/Marketing/Marketing.API/Infrastructure/Repositories/MarketingDataRepository.cs View File

@ -0,0 +1,41 @@
using Microsoft.eShopOnContainers.Services.Marketing.API.Model;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Repositories
{
public class MarketingDataRepository
: IMarketingDataRepository
{
private readonly MarketingReadDataContext _context;
public MarketingDataRepository(IOptions<MarketingSettings> settings)
{
_context = new MarketingReadDataContext(settings);
}
public async Task<MarketingData> GetAsync(string userId)
{
var filter = Builders<MarketingData>.Filter.Eq("UserId", userId);
return await _context.MarketingData
.Find(filter)
.FirstOrDefaultAsync();
}
public async Task UpdateLocationAsync(MarketingData marketingData)
{
var filter = Builders<MarketingData>.Filter.Eq("UserId", marketingData.UserId);
var update = Builders<MarketingData>.Update
.Set("Locations", marketingData.Locations)
.CurrentDate("UpdateDate");
await _context.MarketingData
.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true });
}
}
}

+ 18
- 0
src/Services/Marketing/Marketing.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs View File

@ -0,0 +1,18 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.IntegrationEvents.Events
{
using Model;
using System.Collections.Generic;
using BuildingBlocks.EventBus.Events;
public class UserLocationUpdatedIntegrationEvent : IntegrationEvent
{
public string UserId { get; private set; }
public List<UserLocationDetails> LocationList { get; private set; }
public UserLocationUpdatedIntegrationEvent(string userId, List<UserLocationDetails> locationList)
{
UserId = userId;
LocationList = locationList;
}
}
}

+ 46
- 0
src/Services/Marketing/Marketing.API/IntegrationEvents/Handlers/UserLocationUpdatedIntegrationEventHandler.cs View File

@ -0,0 +1,46 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.IntegrationEvents.Handlers
{
using BuildingBlocks.EventBus.Abstractions;
using System.Threading.Tasks;
using Events;
using System;
using Infrastructure.Repositories;
using Model;
using System.Collections.Generic;
public class UserLocationUpdatedIntegrationEventHandler
: IIntegrationEventHandler<UserLocationUpdatedIntegrationEvent>
{
private readonly IMarketingDataRepository _marketingDataRepository;
public UserLocationUpdatedIntegrationEventHandler(IMarketingDataRepository repository)
{
_marketingDataRepository = repository ?? throw new ArgumentNullException(nameof(repository));
}
public async Task Handle(UserLocationUpdatedIntegrationEvent @event)
{
var userMarketingData = await _marketingDataRepository.GetAsync(@event.UserId);
userMarketingData = userMarketingData ??
new MarketingData() { UserId = @event.UserId };
userMarketingData.Locations = MapUpdatedUserLocations(@event.LocationList);
await _marketingDataRepository.UpdateLocationAsync(userMarketingData);
}
private List<Location> MapUpdatedUserLocations(List<UserLocationDetails> newUserLocations)
{
var result = new List<Location>();
newUserLocations.ForEach(location => {
result.Add(new Location()
{
LocationId = location.LocationId,
Code = location.Code,
Description = location.Description
});
});
return result;
}
}
}

+ 21
- 2
src/Services/Marketing/Marketing.API/Marketing.API.csproj View File

@ -12,10 +12,13 @@
<ItemGroup>
<Folder Include="Infrastructure\MarketingMigrations\" />
<Folder Include="IntegrationEvents\EventHandling\" />
<Folder Include="IntegrationEvents\Events\" />
<Content Include="Pics\**\*;">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Folder Include="Infrastructure\MarketingMigrations\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.1.0" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.2.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" />
@ -39,6 +42,10 @@
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.2" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="1.1.1" />
<PackageReference Include="mongocsharpdriver" Version="2.4.3" />
<PackageReference Include="MongoDB.Bson" Version="2.4.3" />
<PackageReference Include="MongoDB.Driver" Version="2.4.3" />
<PackageReference Include="MongoDB.Driver.Core" Version="2.4.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
@ -47,4 +54,16 @@
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Dockerfile">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Pics\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

+ 3
- 0
src/Services/Marketing/Marketing.API/MarketingSettings.cs View File

@ -3,5 +3,8 @@
public class MarketingSettings
{
public string ConnectionString { get; set; }
public string MongoConnectionString { get; set; }
public string MongoDatabase { get; set; }
public string ExternalCatalogBaseUrl { get; set; }
}
}

+ 3
- 1
src/Services/Marketing/Marketing.API/Model/Campaign.cs View File

@ -7,13 +7,15 @@
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
public string Url { get; set; }
public string PictureUri { get; set; }
public List<Rule> Rules { get; set; }


+ 16
- 0
src/Services/Marketing/Marketing.API/Model/Location.cs View File

@ -0,0 +1,16 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model
{
public class Location
{
public int LocationId { get; set; }
public string Code { get; set; }
public string Description { get; set; }
}
}

+ 19
- 0
src/Services/Marketing/Marketing.API/Model/MarketingData.cs View File

@ -0,0 +1,19 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model
{
public class MarketingData
{
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string UserId { get; set; }
public List<Location> Locations { get; set; }
public DateTime UpdateDate { get; set; }
}
}

+ 51
- 0
src/Services/Marketing/Marketing.API/Model/RuleType.cs View File

@ -0,0 +1,51 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model
{
using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
public sealed class RuleType
{
public static readonly RuleType UserProfileRule = new RuleType(1, nameof(UserProfileRule));
public static readonly RuleType PurchaseHistoryRule = new RuleType(2, nameof(UserProfileRule));
public static readonly RuleType UserLocationRule = new RuleType(3, nameof(UserProfileRule));
public readonly int Id;
public readonly string Name;
private RuleType(int id, string name)
{
Id = id;
Name = name;
}
public static IEnumerable<RuleType> List() =>
new[] { UserProfileRule, PurchaseHistoryRule, UserLocationRule };
public static RuleType FromName(string name)
{
var state = List()
.SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (state == null)
{
throw new MarketingDomainException($"Possible values for RuleType: {String.Join(",", List().Select(s => s.Name))}");
}
return state;
}
public static RuleType From(int id)
{
var state = List().SingleOrDefault(s => s.Id == id);
if (state == null)
{
throw new MarketingDomainException($"Possible values for RuleType: {String.Join(",", List().Select(s => s.Name))}");
}
return state;
}
}
}

+ 0
- 20
src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs View File

@ -1,20 +0,0 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model
{
using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions;
using System;
public enum RuleTypeEnum { UserProfileRule = 1, PurchaseHistoryRule = 2, UserLocationRule = 3 }
public static class RuleType
{
public static RuleTypeEnum From(int id)
{
if (!Enum.IsDefined(typeof(RuleTypeEnum), id))
{
throw new MarketingDomainException($"Invalid value for RuleType, RuleTypeId: {id}");
}
return (RuleTypeEnum)id;
}
}
}

+ 9
- 0
src/Services/Marketing/Marketing.API/Model/UserLocationDetails.cs View File

@ -0,0 +1,9 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model
{
public class UserLocationDetails
{
public int LocationId { get; set; }
public string Code { get; set; }
public string Description { get; set; }
}
}

BIN
src/Services/Marketing/Marketing.API/Pics/1.png View File

Before After
Width: 370  |  Height: 241  |  Size: 136 KiB

BIN
src/Services/Marketing/Marketing.API/Pics/2.png View File

Before After
Width: 372  |  Height: 243  |  Size: 166 KiB

+ 1
- 0
src/Services/Marketing/Marketing.API/Program.cs View File

@ -12,6 +12,7 @@
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.UseWebRoot("Pics")
.Build();
host.Run();


+ 83
- 4
src/Services/Marketing/Marketing.API/Startup.cs View File

@ -11,6 +11,18 @@
using System.Reflection;
using System;
using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Filters;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ;
using RabbitMQ.Client;
using BuildingBlocks.EventBus.Abstractions;
using BuildingBlocks.EventBus;
using IntegrationEvents.Events;
using IntegrationEvents.Handlers;
using Infrastructure.Repositories;
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Polly;
using System.Threading.Tasks;
using System.Data.SqlClient;
public class Startup
{
@ -35,7 +47,7 @@
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc(options =>
@ -43,6 +55,8 @@
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
}).AddControllersAsServices(); //Injecting Controllers themselves thru DIFor further info see: http://docs.autofac.org/en/latest/integration/aspnetcore.html#controllers-as-services
services.Configure<MarketingSettings>(Configuration);
services.AddDbContext<MarketingContext>(options =>
{
options.UseSqlServer(Configuration["ConnectionString"],
@ -59,6 +73,20 @@
//Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval
});
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new ConnectionFactory()
{
HostName = Configuration["EventBusConnection"]
};
return new DefaultRabbitMQPersistentConnection(factory, logger);
});
RegisterServiceBus(services);
// Add framework services.
services.AddSwaggerGen(options =>
{
@ -80,6 +108,14 @@
.AllowAnyHeader()
.AllowCredentials());
});
services.AddTransient<IMarketingDataRepository, MarketingDataRepository>();
//configure autofac
var container = new ContainerBuilder();
container.Populate(services);
return new AutofacServiceProvider(container.Build());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -100,10 +136,13 @@
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
MarketingContextSeed.SeedAsync(app, loggerFactory)
.Wait();
}
var context = (MarketingContext)app
.ApplicationServices.GetService(typeof(MarketingContext));
WaitForSqlAvailabilityAsync(context, loggerFactory, app).Wait();
ConfigureEventBus(app);
}
protected virtual void ConfigureAuth(IApplicationBuilder app)
{
@ -115,5 +154,45 @@
RequireHttpsMetadata = false
});
}
private void RegisterServiceBus(IServiceCollection services)
{
services.AddSingleton<IEventBus, EventBusRabbitMQ>();
services.AddSingleton<IEventBusSubscriptionsManager,
InMemoryEventBusSubscriptionsManager>();
services.AddTransient<IIntegrationEventHandler<UserLocationUpdatedIntegrationEvent>,
UserLocationUpdatedIntegrationEventHandler>();
}
private void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<UserLocationUpdatedIntegrationEvent,
IIntegrationEventHandler<UserLocationUpdatedIntegrationEvent>>();
}
private async Task WaitForSqlAvailabilityAsync(MarketingContext ctx, ILoggerFactory loggerFactory, IApplicationBuilder app, int retries = 0)
{
var logger = loggerFactory.CreateLogger(nameof(Startup));
var policy = CreatePolicy(retries, logger, nameof(WaitForSqlAvailabilityAsync));
await policy.ExecuteAsync(async () =>
{
await MarketingContextSeed.SeedAsync(app, loggerFactory);
});
}
private Policy CreatePolicy(int retries, ILogger logger, string prefix)
{
return Policy.Handle<SqlException>().
WaitAndRetryAsync(
retryCount: retries,
sleepDurationProvider: retry => TimeSpan.FromSeconds(5),
onRetry: (exception, timeSpan, retry, ctx) =>
{
logger.LogTrace($"[{prefix}] Exception {exception.GetType().Name} with message ${exception.Message} detected on attempt {retry} of {retries}");
}
);
}
}
}

+ 23
- 0
src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs View File

@ -0,0 +1,23 @@
namespace Microsoft.eShopOnContainers.Services.Marketing.API.ViewModel
{
using System.Collections.Generic;
public class PaginatedItemsViewModel<TEntity> where TEntity : class
{
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public long Count { get; private set; }
public IEnumerable<TEntity> Data { get; private set; }
public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable<TEntity> data)
{
this.PageIndex = pageIndex;
this.PageSize = pageSize;
this.Count = count;
this.Data = data;
}
}
}

+ 4
- 1
src/Services/Marketing/Marketing.API/appsettings.json View File

@ -6,5 +6,8 @@
}
},
"ConnectionString": "127.0.0.1",
"IdentityUrl": "http://localhost:5105"
"MongoConnectionString": "mongodb://nosql.data",
"MongoDatabase": "MarketingDb",
"IdentityUrl": "http://localhost:5105",
"ExternalCatalogBaseUrl": "http://localhost:5110"
}

+ 3
- 2
src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs View File

@ -53,8 +53,9 @@
return await connection.QueryAsync<dynamic>(@"SELECT o.[Id] as ordernumber,o.[OrderDate] as [date],os.[Name] as [status],SUM(oi.units*oi.unitprice) as total
FROM [ordering].[Orders] o
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
GROUP BY o.[Id], o.[OrderDate], os.[Name]");
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
GROUP BY o.[Id], o.[OrderDate], os.[Name]
ORDER BY o.[Id]");
}
}


+ 1
- 0
src/Web/WebMVC/AppSettings.cs View File

@ -11,6 +11,7 @@ namespace Microsoft.eShopOnContainers.WebMVC
public string CatalogUrl { get; set; }
public string OrderingUrl { get; set; }
public string BasketUrl { get; set; }
public string MarketingUrl { get; set; }
public Logging Logging { get; set; }
}


+ 64
- 0
src/Web/WebMVC/Controllers/CampaignsController.cs View File

@ -0,0 +1,64 @@
namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
using AspNetCore.Authorization;
using AspNetCore.Mvc;
using Services;
using ViewModels;
using System.Threading.Tasks;
using System;
using ViewModels.Pagination;
using global::WebMVC.ViewModels;
[Authorize]
public class CampaignsController : Controller
{
private readonly ICampaignService _campaignService;
public CampaignsController(ICampaignService campaignService) =>
_campaignService = campaignService;
public async Task<IActionResult> Index(int page = 0, int pageSize = 10)
{
var campaignList = await _campaignService.GetCampaigns(pageSize, page);
var vm = new CampaignViewModel()
{
CampaignItems = campaignList.Data,
PaginationInfo = new PaginationInfo()
{
ActualPage = page,
ItemsPerPage = pageSize,
TotalItems = campaignList.Count,
TotalPages = (int)Math.Ceiling(((decimal)campaignList.Count / pageSize))
}
};
vm.PaginationInfo.Next = (vm.PaginationInfo.ActualPage == vm.PaginationInfo.TotalPages - 1) ? "is-disabled" : "";
vm.PaginationInfo.Previous = (vm.PaginationInfo.ActualPage == 0) ? "is-disabled" : "";
return View(vm);
}
public async Task<IActionResult> Details(int id)
{
var campaignDto = await _campaignService.GetCampaignById(id);
if (campaignDto is null)
{
return NotFound();
}
var campaign = new CampaignItem
{
Id = campaignDto.Id,
Name = campaignDto.Name,
Description = campaignDto.Description,
From = campaignDto.From,
To = campaignDto.To,
PictureUri = campaignDto.PictureUri
};
return View(campaign);
}
}
}

+ 17
- 2
src/Web/WebMVC/Infrastructure/API.cs View File

@ -1,4 +1,6 @@
namespace WebMVC.Infrastructure
using System;
namespace WebMVC.Infrastructure
{
public static class API
{
@ -79,5 +81,18 @@
return $"{baseUri}catalogTypes";
}
}
public static class Marketing
{
public static string GetAllCampaigns(string baseUri, string userId, int take, int page)
{
return $"{baseUri}user/{userId}?pageSize={take}&pageIndex={page}";
}
public static string GetAllCampaignById(string baseUri, int id)
{
return $"{baseUri}{id}";
}
}
}
}
}

+ 70
- 0
src/Web/WebMVC/Services/CampaignService.cs View File

@ -0,0 +1,70 @@
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
using global::WebMVC.Infrastructure;
using AspNetCore.Authentication;
using AspNetCore.Http;
using BuildingBlocks.Resilience.Http;
using ViewModels;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;
public class CampaignService : ICampaignService
{
private readonly IOptionsSnapshot<AppSettings> _settings;
private readonly IHttpClient _apiClient;
private readonly ILogger<CampaignService> _logger;
private readonly string _remoteServiceBaseUrl;
private readonly IHttpContextAccessor _httpContextAccesor;
public CampaignService(IOptionsSnapshot<AppSettings> settings, IHttpClient httpClient,
ILogger<CampaignService> logger, IHttpContextAccessor httpContextAccesor)
{
_settings = settings;
_apiClient = httpClient;
_logger = logger;
_remoteServiceBaseUrl = $"{_settings.Value.MarketingUrl}/api/v1/campaigns/";
_httpContextAccesor = httpContextAccesor ?? throw new ArgumentNullException(nameof(httpContextAccesor));
}
public async Task<Campaign> GetCampaigns(int pageSize, int pageIndex)
{
var userId = GetUserIdentity();
var allCampaignItemsUri = API.Marketing.GetAllCampaigns(_remoteServiceBaseUrl,
userId, pageSize, pageIndex);
var authorizationToken = await GetUserTokenAsync();
var dataString = await _apiClient.GetStringAsync(allCampaignItemsUri, authorizationToken);
var response = JsonConvert.DeserializeObject<Campaign>(dataString);
return response;
}
public async Task<CampaignItem> GetCampaignById(int id)
{
var campaignByIdItemUri = API.Marketing.GetAllCampaignById(_remoteServiceBaseUrl, id);
var authorizationToken = await GetUserTokenAsync();
var dataString = await _apiClient.GetStringAsync(campaignByIdItemUri, authorizationToken);
var response = JsonConvert.DeserializeObject<CampaignItem>(dataString);
return response;
}
private string GetUserIdentity()
{
return _httpContextAccesor.HttpContext.User.FindFirst("sub").Value;
}
private async Task<string> GetUserTokenAsync()
{
var context = _httpContextAccesor.HttpContext;
return await context.Authentication.GetTokenAsync("access_token");
}
}
}

+ 13
- 0
src/Web/WebMVC/Services/ICampaignService.cs View File

@ -0,0 +1,13 @@
namespace Microsoft.eShopOnContainers.WebMVC.Services
{
using System.Collections.Generic;
using System.Threading.Tasks;
using ViewModels;
public interface ICampaignService
{
Task<Campaign> GetCampaigns(int pageSize, int pageIndex);
Task<CampaignItem> GetCampaignById(int id);
}
}

+ 2
- 1
src/Web/WebMVC/Startup.cs View File

@ -70,6 +70,7 @@ namespace Microsoft.eShopOnContainers.WebMVC
services.AddTransient<ICatalogService, CatalogService>();
services.AddTransient<IOrderingService, OrderingService>();
services.AddTransient<IBasketService, BasketService>();
services.AddTransient<ICampaignService, CampaignService>();
services.AddTransient<IIdentityParser<ApplicationUser>, IdentityParser>();
if (Configuration.GetValue<string>("UseResilientHttp") == bool.TrueString)
@ -125,7 +126,7 @@ namespace Microsoft.eShopOnContainers.WebMVC
SaveTokens = true,
GetClaimsFromUserInfoEndpoint = true,
RequireHttpsMetadata = false,
Scope = { "openid", "profile", "orders", "basket" }
Scope = { "openid", "profile", "orders", "basket", "marketing" }
};
//Wait untill identity service is ready on compose.


+ 12
- 0
src/Web/WebMVC/ViewModels/Campaign.cs View File

@ -0,0 +1,12 @@
namespace Microsoft.eShopOnContainers.WebMVC.ViewModels
{
using System.Collections.Generic;
public class Campaign
{
public int PageIndex { get; set; }
public int PageSize { get; set; }
public int Count { get; set; }
public List<CampaignItem> Data { get; set; }
}
}

+ 19
- 0
src/Web/WebMVC/ViewModels/CampaignItem.cs View File

@ -0,0 +1,19 @@
namespace Microsoft.eShopOnContainers.WebMVC.ViewModels
{
using System;
public class CampaignItem
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
public string PictureUri { get; set; }
}
}

+ 12
- 0
src/Web/WebMVC/ViewModels/CampaignViewModel/CampaignViewModel.cs View File

@ -0,0 +1,12 @@
namespace WebMVC.ViewModels
{
using System.Collections.Generic;
using Microsoft.eShopOnContainers.WebMVC.ViewModels;
using Microsoft.eShopOnContainers.WebMVC.ViewModels.Pagination;
public class CampaignViewModel
{
public IEnumerable<CampaignItem> CampaignItems { get; set; }
public PaginationInfo PaginationInfo { get; set; }
}
}

+ 28
- 0
src/Web/WebMVC/Views/Campaigns/Details.cshtml View File

@ -0,0 +1,28 @@
@{
ViewData["Title"] = "Campaign details";
@model CampaignItem
}
<section class="esh-campaigns-hero">
<div class="container">
<img class="esh-campaigns-title" src="~/images/main_banner_text.png" />
</div>
</section>
@Html.Partial("_Header", new List<Header>() {
new Header() { Controller = "Catalog", Text = "Back to catalog" },
new Header() { Controller = "Campaigns", Text = "Back to Campaigns" } })
<div class="container">
<div class="card esh-campaigns-items">
<img class="card-img-top" src="@Model.PictureUri" alt="Card image cap">
<div class="card-block">
<h4 class="card-title">@Model.Name</h4>
<p class="card-text">@Model.Description</p>
<p class="card-text">
<small class="text-muted">
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
</small>
</p>
</div>
</div>
</div>

+ 37
- 0
src/Web/WebMVC/Views/Campaigns/Index.cshtml View File

@ -0,0 +1,37 @@
@{
ViewData["Title"] = "Campaigns";
@model WebMVC.ViewModels.CampaignViewModel
}
<section class="esh-campaigns-hero">
<div class="container">
<img class="esh-campaigns-title" src="~/images/main_banner_text.png" />
</div>
</section>
@Html.Partial("_Header", new List<Header>() {
new Header() { Controller = "Catalog", Text = "Back to catalog" } })
<div class="container">
@if (Model.CampaignItems != null && Model.CampaignItems.Any())
{
@Html.Partial("_pagination", Model.PaginationInfo)
<div class="card-group esh-campaigns-items row">
@foreach (var catalogItem in Model.CampaignItems)
{
<div class="esh-campaigns-item col-md-4">
@Html.Partial("_campaign", catalogItem)
</div>
}
</div>
@Html.Partial("_pagination", Model.PaginationInfo)
}
else
{
<div class="esh-campaigns-items row">
THERE ARE NO CAMPAIGNS
</div>
}
</div>

+ 17
- 0
src/Web/WebMVC/Views/Campaigns/_campaign.cshtml View File

@ -0,0 +1,17 @@
@model CampaignItem
<form asp-controller="Campaigns" asp-action="Details" asp-route-id="@Model.Id">
<div class="card-block">
<h4 class="card-title esh-campaigns-name">@Model.Name</h4>
<img class="card-img-top esh-campaigns-thumbnail" src="@Model.PictureUri" alt="@Model.Name">
<input class="esh-campaigns-button" type="submit" value="More details">
</div>
<div class="card-footer">
<small class="text-muted">
From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy")
</small>
</div>
</form>

+ 32
- 0
src/Web/WebMVC/Views/Campaigns/_pagination.cshtml View File

@ -0,0 +1,32 @@
@model Microsoft.eShopOnContainers.WebMVC.ViewModels.Pagination.PaginationInfo
<div class="esh-pager">
<div class="container">
<article class="esh-pager-wrapper row">
<nav>
<a class="esh-pager-item esh-pager-item--navigable @Model.Previous"
id="Previous"
asp-controller="Campaigns"
asp-action="Index"
asp-route-page="@(Model.ActualPage -1)"
aria-label="Previous">
Previous
</a>
<span class="esh-pager-item">
Showing @Model.ItemsPerPage of @Model.TotalItems products - Page @(Model.ActualPage + 1) - @Model.TotalPages
</span>
<a class="esh-pager-item esh-pager-item--navigable @Model.Next"
id="Next"
asp-controller="Campaigns"
asp-action="Index"
asp-route-page="@(Model.ActualPage + 1)"
aria-label="Next">
Next
</a>
</nav>
</article>
</div>
</div>

+ 5
- 0
src/Web/WebMVC/Views/Order/Index.cshtml View File

@ -40,3 +40,8 @@
}
</div>
</div>
<script>
setTimeout(function () {
window.location.reload(true);
}, 5000);
</script>

+ 1
- 0
src/Web/WebMVC/Views/Shared/_Layout.cshtml View File

@ -13,6 +13,7 @@
<link rel="stylesheet" href="~/css/shared/components/identity/identity.css" />
<link rel="stylesheet" href="~/css/shared/components/pager/pager.css" />
<link rel="stylesheet" href="~/css/basket/basket.component.css" />
<link rel="stylesheet" href="~/css/campaigns/campaigns.component.css" />
<link rel="stylesheet" href="~/css/basket/basket-status/basket-status.component.css" />
<link rel="stylesheet" href="~/css/catalog/catalog.component.css" />
<link rel="stylesheet" href="~/css/orders/orders.component.css" />


+ 8
- 0
src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml View File

@ -26,6 +26,14 @@
<img class="esh-identity-image" src="~/images/my_orders.png">
</a>
<a class="esh-identity-item"
asp-controller="Campaigns"
asp-action="Index">
<div class="esh-identity-name esh-identity-name--upper">Campaigns</div>
<img class="esh-identity-image" src="~/images/my_orders.png">
</a>
<a class="esh-identity-item"
href="javascript:document.getElementById('logoutForm').submit()">


+ 16
- 0
src/Web/WebMVC/WebMVC.csproj View File

@ -9,6 +9,18 @@
<DockerComposeProjectPath>..\..\..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<Content Remove="wwwroot/css\campaigns\catalog.component.css" />
</ItemGroup>
<ItemGroup>
<Content Include="wwwroot\css\campaigns\campaigns.component.css" />
<Content Include="wwwroot\css\campaigns\orders.component.css" />
<Content Include="wwwroot\css\catalog\orders.component.css">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<!--<ItemGroup>
<Compile Remove="wwwroot\lib\bootstrap\**" />
<Content Remove="wwwroot\lib\bootstrap\**" />
@ -70,4 +82,8 @@
<Folder Include="wwwroot\lib\" />
</ItemGroup>
<ItemGroup>
<None Include="ViewModels\CampaignItem.cs" />
</ItemGroup>
</Project>

+ 1
- 0
src/Web/WebMVC/appsettings.json View File

@ -2,6 +2,7 @@
"CatalogUrl": "http://localhost:5101",
"OrderingUrl": "http://localhost:5102",
"BasketUrl": "http://localhost:5103",
"MarketingUrl": "http://localhost:5110",
"IdentityUrl": "http://localhost:5105",
"CallBackUrl": "http://localhost:5100/",
"IsClusterEnv": "False",


+ 98
- 0
src/Web/WebMVC/wwwroot/css/campaigns/campaigns.component.css View File

@ -0,0 +1,98 @@
.esh-campaigns-hero {
background-image: url("../../images/main_banner.png");
background-size: cover;
height: 260px;
width: 100%;
}
.esh-campaigns-title {
position: relative;
top: 74.28571px;
}
.esh-campaigns-label::before {
color: rgba(255, 255, 255, 0.5);
content: attr(data-title);
font-size: 0.65rem;
margin-top: 0.65rem;
margin-left: 0.5rem;
position: absolute;
text-transform: uppercase;
z-index: 1;
}
.esh-campaigns-label::after {
background-image: url("../../images/arrow-down.png");
height: 7px;
content: '';
position: absolute;
right: 1.5rem;
top: 2.5rem;
width: 10px;
z-index: 1;
}
.esh-campaigns-items {
margin-top: 1rem;
}
.esh-campaigns-item {
text-align: center;
margin-bottom: 1.5rem;
width: 33%;
display: inline-block;
float: none !important;
}
@media screen and (max-width: 1024px) {
.esh-campaigns-item {
width: 50%;
}
}
@media screen and (max-width: 768px) {
.esh-campaigns-item {
width: 100%;
}
}
.esh-campaigns-thumbnail {
max-width: 370px;
width: 100%;
}
.esh-campaigns-button {
background-color: #83D01B;
border: none;
color: #FFFFFF;
cursor: pointer;
font-size: 1rem;
height: 3rem;
margin-top: 1rem;
transition: all 0.35s;
width: 80%;
}
.esh-campaigns-button.is-disabled {
opacity: .5;
pointer-events: none;
}
.esh-campaigns-button:hover {
background-color: #4a760f;
transition: all 0.35s;
}
.esh-campaigns-name {
font-size: 1rem;
font-weight: 300;
margin-top: .5rem;
text-align: center;
text-transform: uppercase;
}
.esh-campaigns-description {
text-align: center;
font-weight: 300;
font-size: 14px;
}

+ 2
- 2
src/Web/WebMVC/wwwroot/css/shared/components/identity/identity.css View File

@ -29,7 +29,7 @@
.esh-identity-drop {
background: #FFFFFF;
height: 0;
height: 0rem;
min-width: 14rem;
right: 0;
overflow: hidden;
@ -41,7 +41,7 @@
.esh-identity:hover .esh-identity-drop {
border: 1px solid #EEEEEE;
height: 7rem;
height: 9.5rem;
transition: height 0.35s;
}


+ 10
- 0
src/Web/WebSPA/Client/modules/basket/basket.service.ts View File

@ -51,6 +51,10 @@ export class BasketService {
}
}
}
this.basketEvents.orderCreated$.subscribe(x => {
this.dropBasket();
});
}
addItemToBasket(item): Observable<boolean> {
@ -67,6 +71,7 @@ export class BasketService {
setBasketCheckout(basketCheckout): Observable<boolean> {
return this.service.postWithId(this.basketUrl + '/checkout', basketCheckout).map((response: Response) => {
this.basketEvents.orderCreated();
return true;
});
}
@ -100,6 +105,11 @@ export class BasketService {
return basketCheckout;
}
dropBasket() {
this.basket.items = [];
this.basketDropedSource.next();
}
private loadData() {
this.getBasket().subscribe(basket => {
if (basket != null)


+ 14
- 0
src/Web/WebSPA/Client/modules/orders/orders.component.ts View File

@ -9,6 +9,9 @@ import { ConfigurationService } from '../shared/services/configuration.service';
templateUrl: './orders.component.html'
})
export class OrdersComponent implements OnInit {
private oldOrders: IOrder[];
private interval = null;
orders: IOrder[];
constructor(private service: OrdersService, private configurationService: ConfigurationService) { }
@ -21,12 +24,23 @@ export class OrdersComponent implements OnInit {
this.getOrders();
});
}
// call orders until new order is retrieved
this.interval = setTimeout(() => {
this.service.getOrders().subscribe(orders => {
this.orders = orders;
if (this.orders.length != this.oldOrders.length) {
clearInterval(this.interval);
}
});
}, 1000);
}
getOrders() {
this.service.getOrders().subscribe(orders => {
this.orders = orders;
this.oldOrders = this.orders;
console.log('orders items retrieved: ' + orders.length);
});
}


+ 4
- 0
src/Web/WebSPA/WebSPA.csproj View File

@ -90,4 +90,8 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\assets\" />
</ItemGroup>
</Project>

+ 35
- 0
test/Services/FunctionalTests/FunctionalTests.csproj View File

@ -2,16 +2,46 @@
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<PackageTargetFallback>$(PackageTargetFallback);netstandard1.6.1;dnxcore50;portable-net451+win8</PackageTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Services\Location\**" />
<Compile Remove="Services\Marketing\**" />
<EmbeddedResource Remove="Services\Location\**" />
<EmbeddedResource Remove="Services\Marketing\**" />
<None Remove="Services\Location\**" />
<None Remove="Services\Marketing\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Services\Catalog\settings.json" />
</ItemGroup>
<ItemGroup>
<Compile Include="Services\Location\LocationsScenariosBase.cs" />
<Compile Include="Services\Location\LocationsTestsStartup.cs" />
<Compile Include="Services\Marketing\CampaignScenariosBase.cs" />
<Compile Include="Services\Marketing\UserLocationRoleScenariosBase.cs" />
<Compile Include="Services\Marketing\MarketingScenarios.cs" />
<Compile Include="Services\Marketing\MarketingScenariosBase.cs" />
<Compile Include="Services\Marketing\MarketingTestsStartup.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Services\Catalog\settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Services\Location\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Services\Marketing\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
@ -25,6 +55,8 @@
<ItemGroup>
<ProjectReference Include="..\..\..\src\Services\Basket\Basket.API\Basket.API.csproj" />
<ProjectReference Include="..\..\..\src\Services\Catalog\Catalog.API\Catalog.API.csproj" />
<ProjectReference Include="..\..\..\src\Services\Location\Locations.API\Locations.API.csproj" />
<ProjectReference Include="..\..\..\src\Services\Marketing\Marketing.API\Marketing.API.csproj" />
<ProjectReference Include="..\..\..\src\Services\Ordering\Ordering.API\Ordering.API.csproj" />
<ProjectReference Include="..\..\..\src\Web\WebMVC\WebMVC.csproj" />
</ItemGroup>
@ -33,6 +65,9 @@
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Services\Locations\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Services\Ordering\settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>


+ 1
- 1
test/Services/FunctionalTests/Middleware/AutoAuthorizeMiddleware.cs View File

@ -18,7 +18,7 @@ namespace FunctionalTests.Middleware
public async Task Invoke(HttpContext httpContext)
{
var identity = new ClaimsIdentity("cookies");
identity.AddClaim(new Claim("sub", "1234"));
identity.AddClaim(new Claim("sub", "9e3163b9-1ae6-4652-9dc6-7898ab7b7a00"));
httpContext.User.AddIdentity(identity);
await _next.Invoke(httpContext);
}


+ 39
- 0
test/Services/FunctionalTests/Services/Location/LocationsScenariosBase.cs View File

@ -0,0 +1,39 @@
namespace FunctionalTests.Services.Locations
{
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using System;
using System.IO;
public class LocationsScenariosBase
{
public TestServer CreateServer()
{
var webHostBuilder = new WebHostBuilder();
webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Location");
webHostBuilder.UseStartup<LocationsTestsStartup>();
return new TestServer(webHostBuilder);
}
public static class Get
{
public static string Locations = "api/v1/locations";
public static string LocationBy(string id)
{
return $"api/v1/locations/{id}";
}
public static string UserLocationBy(Guid id)
{
return $"api/v1/locations/user/{id}";
}
}
public static class Post
{
public static string AddNewLocation = "api/v1/locations/";
}
}
}

+ 45
- 0
test/Services/FunctionalTests/Services/Location/LocationsTestsStartup.cs View File

@ -0,0 +1,45 @@
namespace FunctionalTests.Services.Locations
{
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.eShopOnContainers.Services.Locations.API;
using System.Security.Claims;
using System.Threading.Tasks;
public class LocationsTestsStartup : Startup
{
public LocationsTestsStartup(IHostingEnvironment env) : base(env)
{
}
protected override void ConfigureAuth(IApplicationBuilder app)
{
if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant())
{
app.UseMiddleware<LocationAuthorizeMiddleware>();
}
else
{
base.ConfigureAuth(app);
}
}
class LocationAuthorizeMiddleware
{
private readonly RequestDelegate _next;
public LocationAuthorizeMiddleware(RequestDelegate rd)
{
_next = rd;
}
public async Task Invoke(HttpContext httpContext)
{
var identity = new ClaimsIdentity("cookies");
identity.AddClaim(new Claim("sub", "4611ce3f-380d-4db5-8d76-87a8689058ed"));
httpContext.User.AddIdentity(identity);
await _next.Invoke(httpContext);
}
}
}
}

+ 8
- 0
test/Services/FunctionalTests/Services/Location/appsettings.json View File

@ -0,0 +1,8 @@
{
"ConnectionString": "mongodb://localhost:27017",
"Database": "LocationsDb",
"ExternalCatalogBaseUrl": "http://localhost:5101",
"IdentityUrl": "http://localhost:5105",
"isTest": "true",
"EventBusConnection": "localhost"
}

+ 35
- 0
test/Services/FunctionalTests/Services/Marketing/CampaignScenariosBase.cs View File

@ -0,0 +1,35 @@
namespace FunctionalTests.Services.Marketing
{
using System;
public class CampaignScenariosBase : MarketingScenariosBase
{
public static class Get
{
public static string Campaigns = CampaignsUrlBase;
public static string CampaignBy(int id)
=> $"{CampaignsUrlBase}/{id}";
public static string UserCampaignsByUserId(Guid userId)
=> $"{CampaignsUrlBase}/user/{userId}";
}
public static class Post
{
public static string AddNewCampaign = CampaignsUrlBase;
}
public static class Put
{
public static string CampaignBy(int id)
=> $"{CampaignsUrlBase}/{id}";
}
public static class Delete
{
public static string CampaignBy(int id)
=> $"{CampaignsUrlBase}/{id}";
}
}
}

+ 58
- 0
test/Services/FunctionalTests/Services/Marketing/MarketingScenarios.cs View File

@ -0,0 +1,58 @@
namespace FunctionalTests.Services.Marketing
{
using UserLocation = Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation;
using LocationRequest = Microsoft.eShopOnContainers.Services.Locations.API.ViewModel.LocationRequest;
using FunctionalTests.Services.Locations;
using Newtonsoft.Json;
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using System.Collections.Generic;
using Microsoft.eShopOnContainers.Services.Marketing.API.Dto;
public class MarketingScenarios : MarketingScenariosBase
{
[Fact]
public async Task Set_new_user_location_and_get_location_campaign_by_user_id()
{
using (var locationsServer = new LocationsScenariosBase().CreateServer())
using (var marketingServer = new MarketingScenariosBase().CreateServer())
{
var location = new LocationRequest
{
Longitude = -122.315752,
Latitude = 47.604610
};
var content = new StringContent(JsonConvert.SerializeObject(location),
Encoding.UTF8, "application/json");
var userId = new Guid("4611ce3f-380d-4db5-8d76-87a8689058ed");
// GIVEN a new location of user is created
var response = await locationsServer.CreateClient()
.PostAsync(LocationsScenariosBase.Post.AddNewLocation, content);
//Get location user from Location.API
var userLocationResponse = await locationsServer.CreateClient()
.GetAsync(LocationsScenariosBase.Get.UserLocationBy(userId));
var responseBody = await userLocationResponse.Content.ReadAsStringAsync();
var userLocation = JsonConvert.DeserializeObject<UserLocation>(responseBody);
await Task.Delay(300);
//Get campaing from Marketing.API given a userId
var UserLocationCampaignResponse = await marketingServer.CreateClient()
.GetAsync(CampaignScenariosBase.Get.UserCampaignsByUserId(userId));
responseBody = await UserLocationCampaignResponse.Content.ReadAsStringAsync();
var userLocationCampaigns = JsonConvert.DeserializeObject<List<CampaignDTO>>(responseBody);
Assert.True(userLocationCampaigns.Count > 0);
}
}
}
}

+ 20
- 0
test/Services/FunctionalTests/Services/Marketing/MarketingScenariosBase.cs View File

@ -0,0 +1,20 @@
namespace FunctionalTests.Services.Marketing
{
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using System.IO;
public class MarketingScenariosBase
{
public static string CampaignsUrlBase => "api/v1/campaigns";
public TestServer CreateServer()
{
var webHostBuilder = new WebHostBuilder();
webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Marketing");
webHostBuilder.UseStartup<MarketingTestsStartup>();
return new TestServer(webHostBuilder);
}
}
}

+ 26
- 0
test/Services/FunctionalTests/Services/Marketing/MarketingTestsStartup.cs View File

@ -0,0 +1,26 @@
namespace FunctionalTests.Services.Marketing
{
using Microsoft.eShopOnContainers.Services.Marketing.API;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using FunctionalTests.Middleware;
public class MarketingTestsStartup : Startup
{
public MarketingTestsStartup(IHostingEnvironment env) : base(env)
{
}
protected override void ConfigureAuth(IApplicationBuilder app)
{
if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant())
{
app.UseMiddleware<AutoAuthorizeMiddleware>();
}
else
{
base.ConfigureAuth(app);
}
}
}
}

+ 40
- 0
test/Services/FunctionalTests/Services/Marketing/UserLocationRoleScenariosBase.cs View File

@ -0,0 +1,40 @@
namespace FunctionalTests.Services.Marketing
{
public class UserLocationRoleScenariosBase : MarketingScenariosBase
{
private const string EndpointLocationName = "locations";
public static class Get
{
public static string UserLocationRulesByCampaignId(int campaignId)
=> GetUserLocationRolesUrlBase(campaignId);
public static string UserLocationRuleByCampaignAndUserLocationRuleId(int campaignId,
int userLocationRuleId)
=> $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}";
}
public static class Post
{
public static string AddNewuserLocationRule(int campaignId)
=> GetUserLocationRolesUrlBase(campaignId);
}
public static class Put
{
public static string UserLocationRoleBy(int campaignId,
int userLocationRuleId)
=> $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}";
}
public static class Delete
{
public static string UserLocationRoleBy(int campaignId,
int userLocationRuleId)
=> $"{GetUserLocationRolesUrlBase(campaignId)}/{userLocationRuleId}";
}
private static string GetUserLocationRolesUrlBase(int campaignId)
=> $"{CampaignsUrlBase}/{campaignId}/{EndpointLocationName}";
}
}

+ 8
- 0
test/Services/FunctionalTests/Services/Marketing/appsettings.json View File

@ -0,0 +1,8 @@
{
"ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word",
"MongoConnectionString": "mongodb://localhost:27017",
"MongoDatabase": "MarketingDb",
"IdentityUrl": "http://localhost:5105",
"isTest": "true",
"EventBusConnection": "localhost"
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save