diff --git a/README.md b/README.md index 19d7f0285..cf389ec18 100644 --- a/README.md +++ b/README.md @@ -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. ->

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. +>

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. >

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

> > Read the planned Roadmap and Milestones for future releases of eShopOnContainers within the Wiki for further info about possible new implementations and provide feedback at the ISSUES section if you'd like to see any specific scenario implemented or improved. Also, feel free to discuss on any current issue. -**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.

-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 roadmap. +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 roadmap.

@@ -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 | | ------------ | ------------| ------------| | | | | -| **Download** (First Edition) | **Download** (First Edition from late 2016) | **Download** (Preview Edition) | +| **Download** (First Edition) | **Download** (First Edition from late 2016) | **Download** (First Edition) | Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com)

@@ -66,7 +66,7 @@ Finally, those microservices are consumed by multiple client web and mobile apps *MVC Application (ASP.NET Core)*: 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.
-*SPA (Single Page Application)*: 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). +*SPA (Single Page Application)*: 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).
*Xamarin Mobile App (For iOS, Android and Windows/UWP)*: 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 on Docker Linux containers 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 on Docker Linux containers running in development machines with "Docker for Windows" and the default Hyper-V Linux VM (MobiLinuxVM) installed by "Docker for Windows". The Windows Containers scenario is currently being implemented/tested yet. 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 diff --git a/docker-compose-windows.override.yml b/docker-compose-windows.override.yml index 45b2db748..738549e7e 100644 --- a/docker-compose-windows.override.yml +++ b/docker-compose-windows.override.yml @@ -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" \ No newline at end of file + - "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" \ No newline at end of file diff --git a/docker-compose-windows.prod.yml b/docker-compose-windows.prod.yml index 7b9c9ab22..4767bba70 100644 --- a/docker-compose-windows.prod.yml +++ b/docker-compose-windows.prod.yml @@ -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 diff --git a/docker-compose-windows.yml b/docker-compose-windows.yml index 2caf209ab..f281d855a 100644 --- a/docker-compose-windows.yml +++ b/docker-compose-windows.yml @@ -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 diff --git a/docker-compose.override.yml b/docker-compose.override.yml index c133de39a..8520197f5 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -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" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b9c46b4c2..634253d8a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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" diff --git a/docker-compose.yml b/docker-compose.yml index 17284bce1..b82785d0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/Enterprise-Application-Patterns-using-XamarinForms.pdf b/docs/Enterprise-Application-Patterns-using-XamarinForms.pdf index 4990eacb1..7184e3042 100644 Binary files a/docs/Enterprise-Application-Patterns-using-XamarinForms.pdf and b/docs/Enterprise-Application-Patterns-using-XamarinForms.pdf differ diff --git a/docs/NET-Microservices-Architecture-for-Containerized-NET-Applications-(Microsoft-eBook).pdf b/docs/NET-Microservices-Architecture-for-Containerized-NET-Applications-(Microsoft-eBook).pdf index 98e530404..7f5d6885d 100644 Binary files a/docs/NET-Microservices-Architecture-for-Containerized-NET-Applications-(Microsoft-eBook).pdf and b/docs/NET-Microservices-Architecture-for-Containerized-NET-Applications-(Microsoft-eBook).pdf differ diff --git a/img/xamarin-enterprise-patterns-ebook-cover-small.png b/img/xamarin-enterprise-patterns-ebook-cover-small.png index 994e21ad5..09ef8334b 100644 Binary files a/img/xamarin-enterprise-patterns-ebook-cover-small.png and b/img/xamarin-enterprise-patterns-ebook-cover-small.png differ diff --git a/src/Mobile/README.md b/src/Mobile/README.md index 4f9262e9b..632593518 100644 --- a/src/Mobile/README.md +++ b/src/Mobile/README.md @@ -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. eShopOnContainers -###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 Migrate to a physical computer with a different processor version ## Copyright and license -* Code and documentation copyright 2017 Microsoft Corp. Code released under the [MIT license](https://opensource.org/licenses/MIT). \ No newline at end of file +* Code and documentation copyright 2017 Microsoft Corp. Code released under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/GlobalSettings.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/GlobalSettings.cs index b62bb7ba3..c55a86315 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/GlobalSettings.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/GlobalSettings.cs @@ -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); diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Token/UserToken.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Token/UserToken.cs new file mode 100644 index 000000000..1e7d2f561 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Token/UserToken.cs @@ -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; } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs index cc3df9a4a..81a2df0da 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IIdentityService.cs @@ -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 GetTokenAsync(string code); } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs index daeab7fec..a85bb5471 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Identity/IdentityService.cs @@ -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(); - 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 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(GlobalSetting.Instance.TokenEndpoint, data, GlobalSetting.Instance.ClientId, GlobalSetting.Instance.ClientSecret); + return token; + } } } diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs index 67b8f8c59..ba7cf8889 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/IRequestProvider.cs @@ -8,6 +8,8 @@ namespace eShopOnContainers.Core.Services.RequestProvider Task PostAsync(string uri, TResult data, string token = "", string header = ""); + Task PostAsync(string uri, string data, string clientId, string clientSecret); + Task DeleteAsync(string uri, string token = ""); } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs index f498532ca..e7ae41698 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/RequestProvider/RequestProvider.cs @@ -61,6 +61,28 @@ namespace eShopOnContainers.Core.Services.RequestProvider return result; } + public async Task PostAsync(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(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) diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs index 95d2c8556..8968a3f86 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/LoginViewModel.cs @@ -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(); diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj index e22341f05..f47c0121b 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj @@ -170,6 +170,7 @@ + @@ -263,6 +264,9 @@ MSBuild:UpdateDesignTimeXaml + + + ..\..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.6\Profile\Profile44\System.ComponentModel.Annotations.dll diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/project.json b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/project.json index bd508be39..7c3248428 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/project.json +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.UnitTests/project.json @@ -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": {} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj index 33fbb39d4..9e027439b 100755 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.iOS/eShopOnContainers.iOS.csproj @@ -13,6 +13,7 @@ eShopOnContainersiOS + true true diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs index 9752b46db..669768abd 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs @@ -70,18 +70,18 @@ { return new List() { - 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 White Mug", Name = "Cup 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 Sheet", Name = "Cup 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 White Mug", Name = "Cup 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 Sheet", Name = "Cup 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 } }; } } diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index a99eeb0c1..1f7a20457 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -108,7 +108,8 @@ namespace Identity.API.Configuration IdentityServerConstants.StandardScopes.OfflineAccess, "orders", "basket", - "locations" + "locations", + "marketing" }, } }; diff --git a/src/Services/Identity/Identity.API/Services/ProfileService.cs b/src/Services/Identity/Identity.API/Services/ProfileService.cs index 3d3826929..71d10a777 100644 --- a/src/Services/Identity/Identity.API/Services/ProfileService.cs +++ b/src/Services/Identity/Identity.API/Services/ProfileService.cs @@ -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)) diff --git a/src/Services/Location/Locations.API/Controllers/LocationsController.cs b/src/Services/Location/Locations.API/Controllers/LocationsController.cs index 71f21a721..a6cb8bdad 100644 --- a/src/Services/Location/Locations.API/Controllers/LocationsController.cs +++ b/src/Services/Location/Locations.API/Controllers/LocationsController.cs @@ -41,7 +41,7 @@ namespace Locations.API.Controllers //GET api/v1/[controller]/1 [Route("{locationId}")] [HttpGet] - public async Task GetLocation(string locationId) + public async Task 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(); diff --git a/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs b/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs index 05acfe0b8..6f519f6b1 100644 --- a/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs +++ b/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs @@ -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()); diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs index 03e51b0f4..e2d7f9ea2 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs @@ -7,7 +7,7 @@ public interface ILocationsRepository { - Task GetAsync(string locationId); + Task GetAsync(int locationId); Task> GetLocationListAsync(); diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs index 764962c8a..47d443718 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs @@ -20,9 +20,9 @@ _context = new LocationsContext(settings); } - public async Task GetAsync(string locationId) + public async Task GetAsync(int locationId) { - var filter = Builders.Filter.Eq("Id", ObjectId.Parse(locationId)); + var filter = Builders.Filter.Eq("LocationId", locationId); return await _context.Locations .Find(filter) .FirstOrDefaultAsync(); diff --git a/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs b/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs index 94c566664..5d55bb25e 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs @@ -7,7 +7,7 @@ public interface ILocationsService { - Task GetLocation(string locationId); + Task GetLocation(int locationId); Task GetUserLocation(string id); diff --git a/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs b/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs index 4b1e3d55d..4b88e1927 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs @@ -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 GetLocation(string locationId) + public async Task GetLocation(int locationId) { return await _locationsRepository.GetAsync(locationId); } - public async Task GetUserLocation(string id) + public async Task 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> GetAllLocation() @@ -38,13 +37,8 @@ return await _locationsRepository.GetLocationListAsync(); } - public async Task AddOrUpdateUserLocation(string id, LocationRequest currentPosition) - { - if (!Guid.TryParse(id, out Guid userId)) - { - throw new ArgumentException("Not valid userId"); - } - + public async Task 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(); - 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 newLocations) + { + var newUserLocations = MapUserLocationDetails(newLocations); + var @event = new UserLocationUpdatedIntegrationEvent(userId, newUserLocations); + _eventBus.Publish(@event); + } + + private List MapUserLocationDetails(List newLocations) + { + var result = new List(); + newLocations.ForEach(location => { + result.Add(new UserLocationDetails() + { + LocationId = location.LocationId, + Code = location.Code, + Description = location.Description + }); + }); + + return result; + } } } diff --git a/src/Services/Location/Locations.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs b/src/Services/Location/Locations.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs new file mode 100644 index 000000000..d4112a54d --- /dev/null +++ b/src/Services/Location/Locations.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs @@ -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 LocationList { get; private set; } + + public UserLocationUpdatedIntegrationEvent(string userId, List locationList) + { + UserId = userId; + LocationList = locationList; + } + } +} diff --git a/src/Services/Location/Locations.API/Locations.API.csproj b/src/Services/Location/Locations.API/Locations.API.csproj index 57548fc03..b9ef671dc 100644 --- a/src/Services/Location/Locations.API/Locations.API.csproj +++ b/src/Services/Location/Locations.API/Locations.API.csproj @@ -10,6 +10,7 @@ + @@ -37,5 +38,9 @@ + + + + diff --git a/src/Services/Location/Locations.API/Model/Locations.cs b/src/Services/Location/Locations.API/Model/Locations.cs index 0944008a7..df521b9b7 100644 --- a/src/Services/Location/Locations.API/Model/Locations.cs +++ b/src/Services/Location/Locations.API/Model/Locations.cs @@ -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; } diff --git a/src/Services/Location/Locations.API/Model/UserLocation.cs b/src/Services/Location/Locations.API/Model/UserLocation.cs index 1b7572426..4686bd006 100644 --- a/src/Services/Location/Locations.API/Model/UserLocation.cs +++ b/src/Services/Location/Locations.API/Model/UserLocation.cs @@ -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; } } } diff --git a/src/Services/Location/Locations.API/Model/UserLocationDetails.cs b/src/Services/Location/Locations.API/Model/UserLocationDetails.cs new file mode 100644 index 000000000..6e152fe1b --- /dev/null +++ b/src/Services/Location/Locations.API/Model/UserLocationDetails.cs @@ -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; } + } +} diff --git a/src/Services/Location/Locations.API/Startup.cs b/src/Services/Location/Locations.API/Startup.cs index d601f26be..f767f227b 100644 --- a/src/Services/Location/Locations.API/Startup.cs +++ b/src/Services/Location/Locations.API/Startup.cs @@ -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(Configuration); - + + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + 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(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + + //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(); + services.AddSingleton(); + } } } diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index f3dde55ef..cbcbb382b 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -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 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 GetCampaignsByUserId(Guid userId, int pageSize = 10, int pageIndex = 0) + { + var marketingData = await _marketingDataRepository.GetAsync(userId.ToString()); + + var campaignDtoList = new List(); + + if (marketingData != null) + { + var locationIdCandidateList = marketingData.Locations.Select(x => x.LocationId); + var userCampaignList = await _context.Rules + .OfType() + .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( + pageIndex, pageSize, totalItems, campaignDtoList); + + return Ok(model); + } private List MapCampaignModelListToDtoList(List 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; + } } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs b/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs index 843e58030..57f652d4c 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs @@ -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] diff --git a/src/Services/Marketing/Marketing.API/Controllers/PicController.cs b/src/Services/Marketing/Marketing.API/Controllers/PicController.cs new file mode 100644 index 000000000..9c2b73c36 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Controllers/PicController.cs @@ -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"); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs index a43dcdbda..829011ce5 100644 --- a/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs +++ b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs @@ -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; } } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/UserLocationDTO.cs b/src/Services/Marketing/Marketing.API/Dto/UserLocationDTO.cs new file mode 100644 index 000000000..aee043f82 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dto/UserLocationDTO.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs index 15be03431..f530fae37 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs @@ -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("RuleTypeId") - .HasValue((int)RuleTypeEnum.UserProfileRule) - .HasValue((int)RuleTypeEnum.PurchaseHistoryRule) - .HasValue((int)RuleTypeEnum.UserLocationRule); + .HasValue(RuleType.UserProfileRule.Id) + .HasValue(RuleType.PurchaseHistoryRule.Id) + .HasValue(RuleType.UserLocationRule.Id); builder.Property(r => r.Description) .HasColumnName("Description") diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs index 5f15a725a..fc8c02b4c 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs @@ -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 { 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 { new UserLocationRule { - Description = "UserLocationRule2", + Description = "Campaign is only for Seattle users.", LocationId = 3 } } diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.Designer.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.Designer.cs similarity index 92% rename from src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.Designer.cs rename to src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.Designer.cs index a9d63ee9b..b4696a8f3 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.Designer.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.Designer.cs @@ -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("From") .HasColumnName("From"); + b.Property("Name") + .IsRequired() + .HasColumnName("Name"); + + b.Property("PictureUri") + .IsRequired() + .HasColumnName("PictureUri"); + b.Property("To") .HasColumnName("To"); - b.Property("Url"); - b.HasKey("Id"); b.ToTable("Campaign"); diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.cs similarity index 93% rename from src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.cs rename to src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.cs index d33fbb3d0..e4e33f060 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170609104915_Initial.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170615163431_Init.cs @@ -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(nullable: false), Description = table.Column(nullable: false), From = table.Column(nullable: false), - To = table.Column(nullable: false), - Url = table.Column(nullable: true) + Name = table.Column(nullable: false), + PictureUri = table.Column(nullable: false), + To = table.Column(nullable: false) }, constraints: table => { diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs index 3cfc5fd66..bcac40659 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs @@ -32,11 +32,17 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Mark b.Property("From") .HasColumnName("From"); + b.Property("Name") + .IsRequired() + .HasColumnName("Name"); + + b.Property("PictureUri") + .IsRequired() + .HasColumnName("PictureUri"); + b.Property("To") .HasColumnName("To"); - b.Property("Url"); - b.HasKey("Id"); b.ToTable("Campaign"); diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingReadDataContext.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingReadDataContext.cs new file mode 100644 index 000000000..5790acf09 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingReadDataContext.cs @@ -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 settings) + { + var client = new MongoClient(settings.Value.MongoConnectionString); + if (client != null) + _database = client.GetDatabase(settings.Value.MongoDatabase); + } + + public IMongoCollection MarketingData + { + get + { + return _database.GetCollection("MarketingReadDataModel"); + } + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/Repositories/IMarketingDataRepository.cs b/src/Services/Marketing/Marketing.API/Infrastructure/Repositories/IMarketingDataRepository.cs new file mode 100644 index 000000000..12a4bf2f8 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/Repositories/IMarketingDataRepository.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Repositories +{ + using Model; + using System.Threading.Tasks; + + public interface IMarketingDataRepository + { + Task GetAsync(string userId); + Task UpdateLocationAsync(MarketingData marketingData); + } +} diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/Repositories/MarketingDataRepository.cs b/src/Services/Marketing/Marketing.API/Infrastructure/Repositories/MarketingDataRepository.cs new file mode 100644 index 000000000..19d264a4e --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/Repositories/MarketingDataRepository.cs @@ -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 settings) + { + _context = new MarketingReadDataContext(settings); + } + + public async Task GetAsync(string userId) + { + var filter = Builders.Filter.Eq("UserId", userId); + return await _context.MarketingData + .Find(filter) + .FirstOrDefaultAsync(); + } + + public async Task UpdateLocationAsync(MarketingData marketingData) + { + var filter = Builders.Filter.Eq("UserId", marketingData.UserId); + var update = Builders.Update + .Set("Locations", marketingData.Locations) + .CurrentDate("UpdateDate"); + + await _context.MarketingData + .UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs b/src/Services/Marketing/Marketing.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs new file mode 100644 index 000000000..a7ab0cafd --- /dev/null +++ b/src/Services/Marketing/Marketing.API/IntegrationEvents/Events/UserLocationUpdatedIntegrationEvent.cs @@ -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 LocationList { get; private set; } + + public UserLocationUpdatedIntegrationEvent(string userId, List locationList) + { + UserId = userId; + LocationList = locationList; + } + } +} diff --git a/src/Services/Marketing/Marketing.API/IntegrationEvents/Handlers/UserLocationUpdatedIntegrationEventHandler.cs b/src/Services/Marketing/Marketing.API/IntegrationEvents/Handlers/UserLocationUpdatedIntegrationEventHandler.cs new file mode 100644 index 000000000..7879c3d96 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/IntegrationEvents/Handlers/UserLocationUpdatedIntegrationEventHandler.cs @@ -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 + { + 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 MapUpdatedUserLocations(List newUserLocations) + { + var result = new List(); + newUserLocations.ForEach(location => { + result.Add(new Location() + { + LocationId = location.LocationId, + Code = location.Code, + Description = location.Description + }); + }); + + return result; + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Marketing.API.csproj b/src/Services/Marketing/Marketing.API/Marketing.API.csproj index 34d594e51..4a6e2d4ea 100644 --- a/src/Services/Marketing/Marketing.API/Marketing.API.csproj +++ b/src/Services/Marketing/Marketing.API/Marketing.API.csproj @@ -12,10 +12,13 @@ - - + + PreserveNewest + + + @@ -39,6 +42,10 @@ + + + + @@ -47,4 +54,16 @@ + + + + + + + Always + + + PreserveNewest + + diff --git a/src/Services/Marketing/Marketing.API/MarketingSettings.cs b/src/Services/Marketing/Marketing.API/MarketingSettings.cs index c6e1dfc40..f83200019 100644 --- a/src/Services/Marketing/Marketing.API/MarketingSettings.cs +++ b/src/Services/Marketing/Marketing.API/MarketingSettings.cs @@ -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; } } } diff --git a/src/Services/Marketing/Marketing.API/Model/Campaign.cs b/src/Services/Marketing/Marketing.API/Model/Campaign.cs index c628b4a72..51a4c017c 100644 --- a/src/Services/Marketing/Marketing.API/Model/Campaign.cs +++ b/src/Services/Marketing/Marketing.API/Model/Campaign.cs @@ -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 Rules { get; set; } diff --git a/src/Services/Marketing/Marketing.API/Model/Location.cs b/src/Services/Marketing/Marketing.API/Model/Location.cs new file mode 100644 index 000000000..0e3e19c1a --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/Location.cs @@ -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; } + } +} diff --git a/src/Services/Marketing/Marketing.API/Model/MarketingData.cs b/src/Services/Marketing/Marketing.API/Model/MarketingData.cs new file mode 100644 index 000000000..9f1f355b8 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/MarketingData.cs @@ -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 Locations { get; set; } + public DateTime UpdateDate { get; set; } + } +} diff --git a/src/Services/Marketing/Marketing.API/Model/RuleType.cs b/src/Services/Marketing/Marketing.API/Model/RuleType.cs new file mode 100644 index 000000000..1fa569cbb --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/RuleType.cs @@ -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 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; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs b/src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs deleted file mode 100644 index c58dbf75c..000000000 --- a/src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Model/UserLocationDetails.cs b/src/Services/Marketing/Marketing.API/Model/UserLocationDetails.cs new file mode 100644 index 000000000..e6d7fe305 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/UserLocationDetails.cs @@ -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; } + } +} diff --git a/src/Services/Marketing/Marketing.API/Pics/1.png b/src/Services/Marketing/Marketing.API/Pics/1.png new file mode 100644 index 000000000..22a6a946f Binary files /dev/null and b/src/Services/Marketing/Marketing.API/Pics/1.png differ diff --git a/src/Services/Marketing/Marketing.API/Pics/2.png b/src/Services/Marketing/Marketing.API/Pics/2.png new file mode 100644 index 000000000..e23063bc4 Binary files /dev/null and b/src/Services/Marketing/Marketing.API/Pics/2.png differ diff --git a/src/Services/Marketing/Marketing.API/Program.cs b/src/Services/Marketing/Marketing.API/Program.cs index 981e797c1..2bf3b3d9c 100644 --- a/src/Services/Marketing/Marketing.API/Program.cs +++ b/src/Services/Marketing/Marketing.API/Program.cs @@ -12,6 +12,7 @@ .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup() + .UseWebRoot("Pics") .Build(); host.Run(); diff --git a/src/Services/Marketing/Marketing.API/Startup.cs b/src/Services/Marketing/Marketing.API/Startup.cs index 9609f903f..a31055d39 100644 --- a/src/Services/Marketing/Marketing.API/Startup.cs +++ b/src/Services/Marketing/Marketing.API/Startup.cs @@ -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(Configuration); + services.AddDbContext(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(sp => + { + var logger = sp.GetRequiredService>(); + + 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(); + + //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(); + services.AddSingleton(); + + services.AddTransient, + UserLocationUpdatedIntegrationEventHandler>(); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + eventBus.Subscribe>(); + } + + 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(). + 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}"); + } + ); + } } } diff --git a/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs b/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs new file mode 100644 index 000000000..73225b4cf --- /dev/null +++ b/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs @@ -0,0 +1,23 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.ViewModel +{ + using System.Collections.Generic; + + public class PaginatedItemsViewModel where TEntity : class + { + public int PageIndex { get; private set; } + + public int PageSize { get; private set; } + + public long Count { get; private set; } + + public IEnumerable Data { get; private set; } + + public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable data) + { + this.PageIndex = pageIndex; + this.PageSize = pageSize; + this.Count = count; + this.Data = data; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/appsettings.json b/src/Services/Marketing/Marketing.API/appsettings.json index 36bdcad2c..a05a01836 100644 --- a/src/Services/Marketing/Marketing.API/appsettings.json +++ b/src/Services/Marketing/Marketing.API/appsettings.json @@ -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" } diff --git a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs index e6f964fed..15036e154 100644 --- a/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs +++ b/src/Services/Ordering/Ordering.API/Application/Queries/OrderQueries.cs @@ -53,8 +53,9 @@ return await connection.QueryAsync(@"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]"); } } diff --git a/src/Web/WebMVC/AppSettings.cs b/src/Web/WebMVC/AppSettings.cs index 84d5c43b7..b15167647 100644 --- a/src/Web/WebMVC/AppSettings.cs +++ b/src/Web/WebMVC/AppSettings.cs @@ -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; } } diff --git a/src/Web/WebMVC/Controllers/CampaignsController.cs b/src/Web/WebMVC/Controllers/CampaignsController.cs new file mode 100644 index 000000000..3030c99db --- /dev/null +++ b/src/Web/WebMVC/Controllers/CampaignsController.cs @@ -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 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 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); + } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Infrastructure/API.cs b/src/Web/WebMVC/Infrastructure/API.cs index fc170ffcc..2dcf555df 100644 --- a/src/Web/WebMVC/Infrastructure/API.cs +++ b/src/Web/WebMVC/Infrastructure/API.cs @@ -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}"; + } + } } -} +} \ No newline at end of file diff --git a/src/Web/WebMVC/Services/CampaignService.cs b/src/Web/WebMVC/Services/CampaignService.cs new file mode 100644 index 000000000..e90be9590 --- /dev/null +++ b/src/Web/WebMVC/Services/CampaignService.cs @@ -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 _settings; + private readonly IHttpClient _apiClient; + private readonly ILogger _logger; + private readonly string _remoteServiceBaseUrl; + private readonly IHttpContextAccessor _httpContextAccesor; + + public CampaignService(IOptionsSnapshot settings, IHttpClient httpClient, + ILogger 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 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(dataString); + + return response; + } + + public async Task 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(dataString); + + return response; + } + + private string GetUserIdentity() + { + return _httpContextAccesor.HttpContext.User.FindFirst("sub").Value; + } + + private async Task GetUserTokenAsync() + { + var context = _httpContextAccesor.HttpContext; + return await context.Authentication.GetTokenAsync("access_token"); + } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Services/ICampaignService.cs b/src/Web/WebMVC/Services/ICampaignService.cs new file mode 100644 index 000000000..ab80e930a --- /dev/null +++ b/src/Web/WebMVC/Services/ICampaignService.cs @@ -0,0 +1,13 @@ +namespace Microsoft.eShopOnContainers.WebMVC.Services +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using ViewModels; + + public interface ICampaignService + { + Task GetCampaigns(int pageSize, int pageIndex); + + Task GetCampaignById(int id); + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index f119e1d8e..8147f6720 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -70,6 +70,7 @@ namespace Microsoft.eShopOnContainers.WebMVC services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient, IdentityParser>(); if (Configuration.GetValue("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. diff --git a/src/Web/WebMVC/ViewModels/Campaign.cs b/src/Web/WebMVC/ViewModels/Campaign.cs new file mode 100644 index 000000000..11841fc3f --- /dev/null +++ b/src/Web/WebMVC/ViewModels/Campaign.cs @@ -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 Data { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/ViewModels/CampaignItem.cs b/src/Web/WebMVC/ViewModels/CampaignItem.cs new file mode 100644 index 000000000..a0bfbaf66 --- /dev/null +++ b/src/Web/WebMVC/ViewModels/CampaignItem.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/ViewModels/CampaignViewModel/CampaignViewModel.cs b/src/Web/WebMVC/ViewModels/CampaignViewModel/CampaignViewModel.cs new file mode 100644 index 000000000..67fdf9cbb --- /dev/null +++ b/src/Web/WebMVC/ViewModels/CampaignViewModel/CampaignViewModel.cs @@ -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 CampaignItems { get; set; } + public PaginationInfo PaginationInfo { get; set; } + } +} \ No newline at end of file diff --git a/src/Web/WebMVC/Views/Campaigns/Details.cshtml b/src/Web/WebMVC/Views/Campaigns/Details.cshtml new file mode 100644 index 000000000..2cb2648b6 --- /dev/null +++ b/src/Web/WebMVC/Views/Campaigns/Details.cshtml @@ -0,0 +1,28 @@ +@{ + ViewData["Title"] = "Campaign details"; + @model CampaignItem +} +

+
+ +
+
+ +@Html.Partial("_Header", new List
() { + new Header() { Controller = "Catalog", Text = "Back to catalog" }, + new Header() { Controller = "Campaigns", Text = "Back to Campaigns" } }) + +
+
+ Card image cap +
+

@Model.Name

+

@Model.Description

+

+ + From @Model.From.ToString("MMMM dd, yyyy") until @Model.To.ToString("MMMM dd, yyyy") + +

+
+
+
\ No newline at end of file diff --git a/src/Web/WebMVC/Views/Campaigns/Index.cshtml b/src/Web/WebMVC/Views/Campaigns/Index.cshtml new file mode 100644 index 000000000..d4c8abe48 --- /dev/null +++ b/src/Web/WebMVC/Views/Campaigns/Index.cshtml @@ -0,0 +1,37 @@ +@{ + ViewData["Title"] = "Campaigns"; +@model WebMVC.ViewModels.CampaignViewModel +} + +
+
+ +
+
+ +@Html.Partial("_Header", new List
() { + new Header() { Controller = "Catalog", Text = "Back to catalog" } }) + +
+ @if (Model.CampaignItems != null && Model.CampaignItems.Any()) + { + @Html.Partial("_pagination", Model.PaginationInfo) + +
+ @foreach (var catalogItem in Model.CampaignItems) + { +
+ @Html.Partial("_campaign", catalogItem) +
+ } +
+ + @Html.Partial("_pagination", Model.PaginationInfo) + } + else + { +
+ THERE ARE NO CAMPAIGNS +
+ } +
diff --git a/src/Web/WebMVC/Views/Campaigns/_campaign.cshtml b/src/Web/WebMVC/Views/Campaigns/_campaign.cshtml new file mode 100644 index 000000000..de3b52657 --- /dev/null +++ b/src/Web/WebMVC/Views/Campaigns/_campaign.cshtml @@ -0,0 +1,17 @@ +@model CampaignItem + + + +
+
+

@Model.Name

+ @Model.Name + +
+ +
diff --git a/src/Web/WebMVC/Views/Campaigns/_pagination.cshtml b/src/Web/WebMVC/Views/Campaigns/_pagination.cshtml new file mode 100644 index 000000000..038222e93 --- /dev/null +++ b/src/Web/WebMVC/Views/Campaigns/_pagination.cshtml @@ -0,0 +1,32 @@ +@model Microsoft.eShopOnContainers.WebMVC.ViewModels.Pagination.PaginationInfo + +
+
+
+ +
+
+
+ diff --git a/src/Web/WebMVC/Views/Order/Index.cshtml b/src/Web/WebMVC/Views/Order/Index.cshtml index 16e5a2003..61a871f26 100644 --- a/src/Web/WebMVC/Views/Order/Index.cshtml +++ b/src/Web/WebMVC/Views/Order/Index.cshtml @@ -40,3 +40,8 @@ } + diff --git a/src/Web/WebMVC/Views/Shared/_Layout.cshtml b/src/Web/WebMVC/Views/Shared/_Layout.cshtml index 990018b09..ab1a4e542 100644 --- a/src/Web/WebMVC/Views/Shared/_Layout.cshtml +++ b/src/Web/WebMVC/Views/Shared/_Layout.cshtml @@ -13,6 +13,7 @@ + diff --git a/src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml b/src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml index 1b9cde79d..f3ce6a729 100644 --- a/src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml +++ b/src/Web/WebMVC/Views/Shared/_LoginPartial.cshtml @@ -26,6 +26,14 @@ + + +
Campaigns
+ +
+ diff --git a/src/Web/WebMVC/WebMVC.csproj b/src/Web/WebMVC/WebMVC.csproj index a9507394c..48925ab34 100644 --- a/src/Web/WebMVC/WebMVC.csproj +++ b/src/Web/WebMVC/WebMVC.csproj @@ -9,6 +9,18 @@ ..\..\..\docker-compose.dcproj + + + + + + + + + PreserveNewest + + +