Merge from Dev

This commit is contained in:
Ramón Tomás 2017-06-19 11:22:18 +02:00
commit 94c9b0667f
106 changed files with 1673 additions and 177 deletions

View File

@ -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 | | Architecting & Developing | Containers Lifecycle & CI/CD | App patterns with Xamarin.Forms |
| ------------ | ------------| ------------| | ------------ | ------------| ------------|
| <a href='https://aka.ms/microservicesebook'><img src="img/ebook_arch_dev_microservices_containers_cover.png"> </a> | <a href='https://aka.ms/dockerlifecycleebook'> <img src="img/ebook_containers_lifecycle.png"> </a> | <a href='https://aka.ms/xamarinpatternsebook'> <img src="img/xamarin-enterprise-patterns-ebook-cover-small.png"> </a> | | <a href='https://aka.ms/microservicesebook'><img src="img/ebook_arch_dev_microservices_containers_cover.png"> </a> | <a href='https://aka.ms/dockerlifecycleebook'> <img src="img/ebook_containers_lifecycle.png"> </a> | <a href='https://aka.ms/xamarinpatternsebook'> <img src="img/xamarin-enterprise-patterns-ebook-cover-small.png"> </a> |
| <sup> <a href='https://aka.ms/microservicesebook'>**Download** (First Edition)</a> </sup> | <sup> <a href='https://aka.ms/dockerlifecycleebook'>**Download** (First Edition from late 2016) </a> </sup> | <sup> <a href='https://aka.ms/xamarinpatternsebook'>**Download** (Preview Edition) </a> </sup> | | <sup> <a href='https://aka.ms/microservicesebook'>**Download** (First Edition)</a> </sup> | <sup> <a href='https://aka.ms/dockerlifecycleebook'>**Download** (First Edition from late 2016) </a> </sup> | <sup> <a href='https://aka.ms/xamarinpatternsebook'>**Download** (First Edition) </a> </sup> |
Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com) Send feedback to [dotnet-architecture-ebooks-feedback@service.microsoft.com](dotnet-architecture-ebooks-feedback@service.microsoft.com)
<p> <p>
@ -76,7 +76,7 @@ Finally, those microservices are consumed by multiple client web and mobile apps
## Setting up your development environment for eShopOnContainers ## Setting up your development environment for eShopOnContainers
### Visual Studio 2017 and Windows based ### Visual Studio 2017 and Windows based
This is the more straightforward way to get started: 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 ### 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:

View File

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

View File

@ -76,6 +76,18 @@ services:
ports: ports:
- "5100:80" - "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: sql.data:
environment: environment:
- SA_PASSWORD=Pass@word - SA_PASSWORD=Pass@word

View File

@ -61,6 +61,18 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
depends_on: depends_on:
- nosql.data - 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: sql.data:
image: microsoft/mssql-server-windows image: microsoft/mssql-server-windows

View File

@ -58,6 +58,10 @@ services:
- ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://0.0.0.0:80 - ASPNETCORE_URLS=http://0.0.0.0:80
- ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word - 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. - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105.
ports: ports:
- "5110:80" - "5110:80"
@ -70,6 +74,7 @@ services:
- OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 - 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. - 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 - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103
- MarketingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5110
- CatalogUrlHC=http://catalog.api/hc - CatalogUrlHC=http://catalog.api/hc
- OrderingUrlHC=http://ordering.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. - 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 - CatalogUrl=http://catalog.api
- OrderingUrl=http://ordering.api - OrderingUrl=http://ordering.api
- BasketUrl=http://basket.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. #Remote: Use ${ESHOP_EXTERNAL_DNS_NAME_OR_IP} if using external IP or DNS name from browser.
ports: ports:
- "5100:80" - "5100:80"

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,12 +1,12 @@
#eShopOnContainers # eShopOnContainers
eShopOnContainers is a reference app whose imagined purpose is to serve the mobile workforce of a fictitious company that sells products. The app allow to manage the catalog, view products, manage the basket and the orders. eShopOnContainers is a reference app whose imagined purpose is to serve the mobile workforce of a fictitious company that sells products. The app allow to manage the catalog, view products, manage the basket and the orders.
<img src="Images/eShopOnContainers_Architecture_Diagram.png" alt="eShopOnContainers" Width="800" /> <img src="Images/eShopOnContainers_Architecture_Diagram.png" alt="eShopOnContainers" Width="800" />
###Supported platforms: iOS, Android and Windows ### Supported platforms: iOS, Android and Windows
###The app architecture consists of two parts: ### The app architecture consists of two parts:
1. A Xamarin.Forms mobile app for iOS, Android and Windows. 1. A Xamarin.Forms mobile app for iOS, Android and Windows.
2. Several .NET Web API microservices deployed as Docker containers. 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 * Entity Framework
* Identity Server 4 * Identity Server 4
##Three platforms ## Three platforms
The app targets **three** platforms: The app targets **three** platforms:
* iOS * 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). 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: This project uses some third-party assets with a license that requires attribution:

View File

@ -6,7 +6,6 @@
public const string MockTag = "Mock"; public const string MockTag = "Mock";
public const string DefaultEndpoint = "http://13.88.8.119"; public const string DefaultEndpoint = "http://13.88.8.119";
private string _baseEndpoint; private string _baseEndpoint;
private static readonly GlobalSetting _instance = new GlobalSetting(); 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 AuthToken { get; set; }
public string RegisterWebsite { get; set; } public string RegisterWebsite { get; set; }
@ -47,6 +50,8 @@
public string UserInfoEndpoint { get; set; } public string UserInfoEndpoint { get; set; }
public string TokenEndpoint { get; set; }
public string LogoutEndpoint { get; set; } public string LogoutEndpoint { get; set; }
public string IdentityCallback { get; set; } public string IdentityCallback { get; set; }
@ -61,6 +66,7 @@
BasketEndpoint = string.Format("{0}:5103", baseEndpoint); BasketEndpoint = string.Format("{0}:5103", baseEndpoint);
IdentityEndpoint = string.Format("{0}:5105/connect/authorize", baseEndpoint); IdentityEndpoint = string.Format("{0}:5105/connect/authorize", baseEndpoint);
UserInfoEndpoint = string.Format("{0}:5105/connect/userinfo", 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); LogoutEndpoint = string.Format("{0}:5105/connect/endsession", baseEndpoint);
IdentityCallback = string.Format("{0}:5105/xamarincallback", baseEndpoint); IdentityCallback = string.Format("{0}:5105/xamarincallback", baseEndpoint);
LogoutCallback = string.Format("{0}:5105/Account/Redirecting", baseEndpoint); LogoutCallback = string.Format("{0}:5105/Account/Redirecting", baseEndpoint);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,18 +70,18 @@
{ {
return new List<CatalogItem>() return new List<CatalogItem>()
{ {
new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = ".NET Bot Black Hoodie", Name = ".NET Bot Black Hoodie", Price = 19.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/1", AvailableStock = 1}, new CatalogItem() { CatalogTypeId=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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 100 },
new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/9", AvailableStock = 1 }, new CatalogItem() { CatalogTypeId=1,CatalogBrandId=5, Description = "Cup<T> White Mug", Name = "Cup<T> White Mug", Price = 12, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/9", AvailableStock = 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 = 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 = 100 },
new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11", AvailableStock = 1 }, new CatalogItem() { CatalogTypeId=3,CatalogBrandId=2, Description = "Cup<T> Sheet", Name = "Cup<T> Sheet", Price = 8.5M, PictureUri = "http://externalcatalogbaseurltobereplaced/api/v1/pic/11", AvailableStock = 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 = 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 = 100 }
}; };
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,21 @@
using Microsoft.AspNetCore.Builder; using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; 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;
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.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using System.Reflection; using System.Reflection;
using System; 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 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. // 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. // Add framework services.
services.AddMvc(options => services.AddMvc(options =>
@ -47,6 +51,20 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API
services.Configure<LocationSettings>(Configuration); services.Configure<LocationSettings>(Configuration);
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new ConnectionFactory()
{
HostName = Configuration["EventBusConnection"]
};
return new DefaultRabbitMQPersistentConnection(factory, logger);
});
RegisterServiceBus(services);
// Add framework services. // Add framework services.
services.AddSwaggerGen(options => services.AddSwaggerGen(options =>
{ {
@ -73,6 +91,12 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API
services.AddTransient<IIdentityService, IdentityService>(); services.AddTransient<IIdentityService, IdentityService>();
services.AddTransient<ILocationsService, LocationsService>(); services.AddTransient<ILocationsService, LocationsService>();
services.AddTransient<ILocationsRepository, LocationsRepository>(); services.AddTransient<ILocationsRepository, LocationsRepository>();
//configure autofac
var container = new ContainerBuilder();
container.Populate(services);
return new AutofacServiceProvider(container.Build());
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // 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 RequireHttpsMetadata = false
}); });
} }
private void RegisterServiceBus(IServiceCollection services)
{
services.AddSingleton<IEventBus, EventBusRabbitMQ>();
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -54,7 +54,8 @@
FROM [ordering].[Orders] o FROM [ordering].[Orders] o
LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid LEFT JOIN[ordering].[orderitems] oi ON o.Id = oi.orderid
LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id
GROUP BY o.[Id], o.[OrderDate], os.[Name]"); GROUP BY o.[Id], o.[OrderDate], os.[Name]
ORDER BY o.[Id]");
} }
} }

View File

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

View File

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

View File

@ -1,4 +1,6 @@
namespace WebMVC.Infrastructure using System;
namespace WebMVC.Infrastructure
{ {
public static class API public static class API
{ {
@ -79,5 +81,18 @@
return $"{baseUri}catalogTypes"; 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}";
}
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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