From 9d5cde205727311bd8898530cbf86c4b3914ef5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Tue, 30 May 2017 15:01:58 +0200 Subject: [PATCH 01/36] #2412-Create Location Microservice --- ...irewall-rules-for-sts-auth-thru-docker.ps1 | 4 +- docker-compose.override.yml | 10 ++ docker-compose.vs.debug.yml | 15 ++ docker-compose.vs.release.yml | 10 ++ docker-compose.yml | 9 +- eShopOnContainers-ServicesAndWebApps.sln | 56 +++++++- .../Catalog/Catalog.API/Catalog.API.csproj | 1 + .../Identity.API/Configuration/Config.cs | 3 +- .../Location/Locations.API/.dockerignore | 3 + .../Controllers/HomeController.cs | 19 +++ .../Controllers/ValuesController.cs | 44 ++++++ .../Location/Locations.API/Dockerfile | 6 + .../Infrastructure/LocationsContext.cs | 55 ++++++++ .../Infrastructure/LocationsContextSeed.cs | 131 ++++++++++++++++++ .../20170529174524_Initial.Designer.cs | 87 ++++++++++++ .../Migrations/20170529174524_Initial.cs | 129 +++++++++++++++++ .../LocationsContextModelSnapshot.cs | 86 ++++++++++++ .../Locations.API/Locations.API.csproj | 39 ++++++ .../Locations.API/Model/FrontierPoints.cs | 11 ++ .../Location/Locations.API/Model/Locations.cs | 62 +++++++++ .../Location/Locations.API/Program.cs | 25 ++++ .../Properties/launchSettings.json | 29 ++++ .../Location/Locations.API/Startup.cs | 115 +++++++++++++++ .../appsettings.Development.json | 10 ++ .../Location/Locations.API/appsettings.json | 12 ++ .../FunctionalTests/FunctionalTests.csproj | 4 + .../IntegrationTests/IntegrationTests.csproj | 4 + test/Services/UnitTest/UnitTest.csproj | 4 + 28 files changed, 978 insertions(+), 5 deletions(-) create mode 100644 src/Services/Location/Locations.API/.dockerignore create mode 100644 src/Services/Location/Locations.API/Controllers/HomeController.cs create mode 100644 src/Services/Location/Locations.API/Controllers/ValuesController.cs create mode 100644 src/Services/Location/Locations.API/Dockerfile create mode 100644 src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.Designer.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs create mode 100644 src/Services/Location/Locations.API/Locations.API.csproj create mode 100644 src/Services/Location/Locations.API/Model/FrontierPoints.cs create mode 100644 src/Services/Location/Locations.API/Model/Locations.cs create mode 100644 src/Services/Location/Locations.API/Program.cs create mode 100644 src/Services/Location/Locations.API/Properties/launchSettings.json create mode 100644 src/Services/Location/Locations.API/Startup.cs create mode 100644 src/Services/Location/Locations.API/appsettings.Development.json create mode 100644 src/Services/Location/Locations.API/appsettings.json diff --git a/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 b/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 index be63d8a25..865f24067 100644 --- a/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 +++ b/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 @@ -21,6 +21,6 @@ try { Write-Host "Rule found" } catch [Exception] { - New-NetFirewallRule -DisplayName eShopOnContainers-Inbound -Confirm -Description "eShopOnContainers Inbound Rule for port range 5100-5105" -LocalAddress Any -LocalPort 5100-5105 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Inbound - New-NetFirewallRule -DisplayName eShopOnContainers-Outbound -Confirm -Description "eShopOnContainers Outbound Rule for port range 5100-5105" -LocalAddress Any -LocalPort 5100-5105 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Outbound + New-NetFirewallRule -DisplayName eShopOnContainers-Inbound -Confirm -Description "eShopOnContainers Inbound Rule for port range 5100-5105" -LocalAddress Any -LocalPort 5100-5110 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Inbound + New-NetFirewallRule -DisplayName eShopOnContainers-Outbound -Confirm -Description "eShopOnContainers Outbound Rule for port range 5100-5105" -LocalAddress Any -LocalPort 5100-5110 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Outbound } \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 04d1c4d9c..a96720ba5 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -95,3 +95,13 @@ services: - spa=http://webspa/hc ports: - "5107:80" + + locations.api: + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=http://0.0.0.0:80 + - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.LocationsDb;User Id=sa;Password=Pass@word + - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + - EventBusConnection=rabbitmq + ports: + - "5109:80" \ No newline at end of file diff --git a/docker-compose.vs.debug.yml b/docker-compose.vs.debug.yml index 2e7145637..50ba68a69 100644 --- a/docker-compose.vs.debug.yml +++ b/docker-compose.vs.debug.yml @@ -105,3 +105,18 @@ services: entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + locations.api: + image: eshop/locations.api:dev + build: + args: + source: ${DOCKER_BUILD_SOURCE} + environment: + - DOTNET_USE_POLLING_FILE_WATCHER=1 + volumes: + - ./src/Services/Location/Locations.API:/app + - ~/.nuget/packages:/root/.nuget/packages:ro + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" diff --git a/docker-compose.vs.release.yml b/docker-compose.vs.release.yml index d1ca5b2c6..58c9ffb81 100644 --- a/docker-compose.vs.release.yml +++ b/docker-compose.vs.release.yml @@ -70,3 +70,13 @@ services: entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + locations.api: + build: + args: + source: ${DOCKER_BUILD_SOURCE} + volumes: + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" diff --git a/docker-compose.yml b/docker-compose.yml index 21f3972f2..e698992b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,4 +74,11 @@ services: build: context: ./src/Web/WebStatus dockerfile: Dockerfile - \ No newline at end of file + + locations.api: + image: locations.api + build: + context: ./src/Services/Location/Locations.API + dockerfile: Dockerfile + depends_on: + - sql.data \ No newline at end of file diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index eb0e83e02..699185133 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26403.3 +VisualStudioVersion = 15.0.26430.6 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{932D8224-11F6-4D07-B109-DA28AD288A63}" EndProject @@ -76,6 +76,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Health EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventBus.Tests", "src\BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Location", "Location", "{41139F64-4046-4F16-96B7-D941D96FA9C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Locations.API", "src\Services\Location\Locations.API\Locations.API.csproj", "{E7581357-FC34-474C-B8F5-307EE3CE05EF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1002,6 +1006,54 @@ Global {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x64.Build.0 = Release|Any CPU {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x86.ActiveCfg = Release|Any CPU {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x86.Build.0 = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|ARM.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|iPhone.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|x64.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|x64.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|x86.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.AppStore|x86.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|ARM.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|ARM.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|iPhone.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|x64.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Debug|x86.Build.0 = Debug|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|Any CPU.Build.0 = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|ARM.ActiveCfg = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|ARM.Build.0 = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|iPhone.ActiveCfg = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|iPhone.Build.0 = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|x64.ActiveCfg = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|x64.Build.0 = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|x86.ActiveCfg = Release|Any CPU + {E7581357-FC34-474C-B8F5-307EE3CE05EF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1038,5 +1090,7 @@ Global {22A0F9C1-2D4A-4107-95B7-8459E6688BC5} = {A81ECBC2-6B00-4DCD-8388-469174033379} {4BD76717-3102-4969-8C2C-BAAA3F0263B6} = {A81ECBC2-6B00-4DCD-8388-469174033379} {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} + {41139F64-4046-4F16-96B7-D941D96FA9C6} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} + {E7581357-FC34-474C-B8F5-307EE3CE05EF} = {41139F64-4046-4F16-96B7-D941D96FA9C6} EndGlobalSection EndGlobal diff --git a/src/Services/Catalog/Catalog.API/Catalog.API.csproj b/src/Services/Catalog/Catalog.API/Catalog.API.csproj index 10fe25945..5bea2f22d 100644 --- a/src/Services/Catalog/Catalog.API/Catalog.API.csproj +++ b/src/Services/Catalog/Catalog.API/Catalog.API.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index 260989da4..a07f5d01d 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -12,7 +12,8 @@ namespace Identity.API.Configuration return new List { new ApiResource("orders", "Orders Service"), - new ApiResource("basket", "Basket Service") + new ApiResource("basket", "Basket Service"), + new ApiResource("locations", "Locations Service") }; } diff --git a/src/Services/Location/Locations.API/.dockerignore b/src/Services/Location/Locations.API/.dockerignore new file mode 100644 index 000000000..d8f8175f6 --- /dev/null +++ b/src/Services/Location/Locations.API/.dockerignore @@ -0,0 +1,3 @@ +* +!obj/Docker/publish/* +!obj/Docker/empty/ diff --git a/src/Services/Location/Locations.API/Controllers/HomeController.cs b/src/Services/Location/Locations.API/Controllers/HomeController.cs new file mode 100644 index 000000000..e7cea3cca --- /dev/null +++ b/src/Services/Location/Locations.API/Controllers/HomeController.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +// For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Controllers +{ + public class HomeController : Controller + { + // GET: // + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } + } +} diff --git a/src/Services/Location/Locations.API/Controllers/ValuesController.cs b/src/Services/Location/Locations.API/Controllers/ValuesController.cs new file mode 100644 index 000000000..e711e221d --- /dev/null +++ b/src/Services/Location/Locations.API/Controllers/ValuesController.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Locations.API.Controllers +{ + [Route("api/[controller]")] + public class ValuesController : Controller + { + // GET api/values + [HttpGet] + public IEnumerable Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public string Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody]string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody]string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } +} diff --git a/src/Services/Location/Locations.API/Dockerfile b/src/Services/Location/Locations.API/Dockerfile new file mode 100644 index 000000000..3d00f01a9 --- /dev/null +++ b/src/Services/Location/Locations.API/Dockerfile @@ -0,0 +1,6 @@ +FROM microsoft/aspnetcore:1.1 +ARG source +WORKDIR /app +EXPOSE 80 +COPY ${source:-obj/Docker/publish} . +ENTRYPOINT ["dotnet", "Locations.API.dll"] diff --git a/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs b/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs new file mode 100644 index 000000000..42c3f7900 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs @@ -0,0 +1,55 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure +{ + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + using Microsoft.eShopOnContainers.Services.Locations.API.Model; + + public class LocationsContext : DbContext + { + public LocationsContext(DbContextOptions options) : base(options) + { + } + + public DbSet Locations { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity(ConfigureLocations); + builder.Entity(ConfigureFrontierPoints); + } + + void ConfigureLocations(EntityTypeBuilder builder) + { + builder.ToTable("Locations"); + + builder.HasKey(cl => cl.Id); + + builder.Property(cl => cl.Id) + .ForSqlServerUseSequenceHiLo("locations_hilo") + .IsRequired(); + + builder.Property(cb => cb.Code) + .IsRequired() + .HasColumnName("LocationCode") + .HasMaxLength(15); + + builder.HasMany(f => f.Polygon) + .WithOne(l => l.Location) + .IsRequired(); + + builder.Property(cb => cb.Description) + .HasMaxLength(100); + } + + void ConfigureFrontierPoints(EntityTypeBuilder builder) + { + builder.ToTable("FrontierPoints"); + + builder.HasKey(fp => fp.Id); + + builder.Property(fp => fp.Id) + .ForSqlServerUseSequenceHiLo("frontier_hilo") + .IsRequired(); + } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs b/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs new file mode 100644 index 000000000..67673719b --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs @@ -0,0 +1,131 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure +{ + using Microsoft.AspNetCore.Builder; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.Logging; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.eShopOnContainers.Services.Locations.API.Model; + using System.Text; + + public class LocationsContextSeed + { + public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory) + { + var context = (LocationsContext)applicationBuilder + .ApplicationServices.GetService(typeof(LocationsContext)); + + context.Database.Migrate(); + + if (!context.Locations.Any()) + { + context.Locations.AddRange( + GetPreconfiguredLocations()); + + await context.SaveChangesAsync(); + } + } + + static Locations GetPreconfiguredLocations() + { + var ww = new Locations() { Code = "WW", Description = "WorldWide", Latitude = -1, Longitude = -1 }; + ww.ChildLocations.Add(GetUSLocations()); + return ww; + } + + static Locations GetUSLocations() + { + var us = new Locations() { Code = "US", Description = "United States", Latitude = 41.650455, Longitude = -101.357386, Polygon = GetUSPoligon() }; + us.ChildLocations.Add(GetWashingtonLocations()); + return us; + } + + static Locations GetWashingtonLocations() + { + var wht = new Locations() { Code = "WHT", Description = "Washington", Latitude = 47.223652, Longitude = -119.542781, Polygon = GetWashingtonPoligon() }; + wht.ChildLocations.Add(GetSeattleLocations()); + wht.ChildLocations.Add(GetRedmondLocations()); + return wht; + } + + static Locations GetSeattleLocations() + { + var bcn = new Locations() { Code = "SEAT", Description = "Seattle", Latitude = 47.603111, Longitude = -122.330747, Polygon = GetSeattlePoligon() }; + bcn.ChildLocations.Add(new Locations() { Code = "SEAT-PioneerSqr", Description = "Seattle Pioneer Square Shop" , Latitude = 47.602053, Longitude= -122.333884, Polygon = GetSeattlePioneerSqrPoligon() }); + return bcn; + } + + static Locations GetRedmondLocations() + { + var bcn = new Locations() { Code = "REDM", Description = "Redmond", Latitude = 47.674961, Longitude = -122.122887, Polygon = GetRedmondPoligon() }; + bcn.ChildLocations.Add(new Locations() { Code = "REDM-DWNTWP", Description = "Redmond Downtown Central Park Shop", Latitude = 47.674433, Longitude = -122.125006, Polygon = GetRedmondDowntownParkPoligon() }); + return bcn; + } + + static List GetUSPoligon() + { + var poligon = new List(); + poligon.Add(new FrontierPoints() { Latitude = 48.7985, Longitude = -62.88205 }); + poligon.Add(new FrontierPoints() { Latitude = 48.76513, Longitude = -129.31329 }); + poligon.Add(new FrontierPoints() { Latitude = 30.12256, Longitude = -120.9496 }); + poligon.Add(new FrontierPoints() { Latitude = 30.87114, Longitude = -111.39442 }); + poligon.Add(new FrontierPoints() { Latitude = 24.24979, Longitude = -78.11975 }); + return poligon; + } + + static List GetWashingtonPoligon() + { + var poligon = new List(); + poligon.Add(new FrontierPoints() { Latitude = 48.8943, Longitude = -124.68633 }); + poligon.Add(new FrontierPoints() { Latitude = 45.66613, Longitude = -124.32962 }); + poligon.Add(new FrontierPoints() { Latitude = 45.93384, Longitude = -116.73824 }); + poligon.Add(new FrontierPoints() { Latitude = 49.04282, Longitude = -116.96912 }); + return poligon; + } + + static List GetSeattlePoligon() + { + var poligon = new List(); + poligon.Add(new FrontierPoints() { Latitude = 47.82929, Longitude = -122.36238 }); + poligon.Add(new FrontierPoints() { Latitude = 47.6337, Longitude = -122.42091 }); + poligon.Add(new FrontierPoints() { Latitude = 47.45224, Longitude = -122.37371 }); + poligon.Add(new FrontierPoints() { Latitude = 47.50259, Longitude = -122.20788 }); + poligon.Add(new FrontierPoints() { Latitude = 47.73644, Longitude = -122.26934 }); + return poligon; + } + + static List GetRedmondPoligon() + { + var poligon = new List(); + poligon.Add(new FrontierPoints() { Latitude = 47.73148, Longitude = -122.15432 }); + poligon.Add(new FrontierPoints() { Latitude = 47.72559, Longitude = -122.17673 }); + poligon.Add(new FrontierPoints() { Latitude = 47.67851, Longitude = -122.16904 }); + poligon.Add(new FrontierPoints() { Latitude = 47.65036, Longitude = -122.16136 }); + poligon.Add(new FrontierPoints() { Latitude = 47.62746, Longitude = -122.15604 }); + poligon.Add(new FrontierPoints() { Latitude = 47.63463, Longitude = -122.01562 }); + poligon.Add(new FrontierPoints() { Latitude = 47.74244, Longitude = -122.04961 }); + return poligon; + } + + static List GetSeattlePioneerSqrPoligon() + { + var poligon = new List(); + poligon.Add(new FrontierPoints() { Latitude = 47.60338, Longitude = -122.3327 }); + poligon.Add(new FrontierPoints() { Latitude = 47.60192, Longitude = -122.33665 }); + poligon.Add(new FrontierPoints() { Latitude = 47.60096, Longitude = -122.33456 }); + poligon.Add(new FrontierPoints() { Latitude = 47.60136, Longitude = -122.33186 }); + return poligon; + } + + static List GetRedmondDowntownParkPoligon() + { + var poligon = new List(); + poligon.Add(new FrontierPoints() { Latitude = 47.67595, Longitude = -122.12467 }); + poligon.Add(new FrontierPoints() { Latitude = 47.67449, Longitude = -122.12862 }); + poligon.Add(new FrontierPoints() { Latitude = 47.67353, Longitude = -122.12653 }); + poligon.Add(new FrontierPoints() { Latitude = 47.67368, Longitude = -122.12197 }); + return poligon; + } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.Designer.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.Designer.cs new file mode 100644 index 000000000..0623edc4e --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.Designer.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migrations +{ + [DbContext(typeof(LocationsContext))] + [Migration("20170529174524_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:Sequence:.frontier_hilo", "'frontier_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.locations_hilo", "'locations_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "frontier_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Latitude"); + + b.Property("LocationId") + .IsRequired(); + + b.Property("Longitude"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.ToTable("FrontierPoints"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "locations_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Code") + .IsRequired() + .HasColumnName("LocationCode") + .HasMaxLength(15); + + b.Property("Description") + .HasMaxLength(100); + + b.Property("Latitude"); + + b.Property("Longitude"); + + b.Property("ParentId"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") + .WithMany("FrontierPoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations") + .WithMany("ChildLocations") + .HasForeignKey("ParentId"); + }); + } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.cs new file mode 100644 index 000000000..d2352312f --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using System.Text; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "frontier_hilo", + incrementBy: 10); + + migrationBuilder.CreateSequence( + name: "locations_hilo", + incrementBy: 10); + + migrationBuilder.CreateTable( + name: "Locations", + columns: table => new + { + Id = table.Column(nullable: false), + LocationCode = table.Column(maxLength: 15, nullable: false), + Description = table.Column(maxLength: 100, nullable: true), + Latitude = table.Column(nullable: false), + Longitude = table.Column(nullable: false), + ParentId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Locations", x => x.Id); + table.ForeignKey( + name: "FK_Locations_Locations_ParentId", + column: x => x.ParentId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "FrontierPoints", + columns: table => new + { + Id = table.Column(nullable: false), + Latitude = table.Column(nullable: false), + LocationId = table.Column(nullable: false), + Longitude = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FrontierPoints", x => x.Id); + table.ForeignKey( + name: "FK_FrontierPoints_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_FrontierPoints_LocationId", + table: "FrontierPoints", + column: "LocationId"); + + migrationBuilder.CreateIndex( + name: "IX_Locations_ParentId", + table: "Locations", + column: "ParentId"); + + migrationBuilder.Sql(CreateGetDistanceFunction()); + migrationBuilder.Sql(CreateLocationsNearSP()); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FrontierPoints"); + + migrationBuilder.DropTable( + name: "Locations"); + + migrationBuilder.DropSequence( + name: "frontier_hilo"); + + migrationBuilder.DropSequence( + name: "locations_hilo"); + + migrationBuilder.Sql(@"DROP PROCEDURE IF EXISTS pLocationsNear"); + migrationBuilder.Sql(@"DROP FUNCTION IF EXISTS dbo.GetDistanceFromLocation"); + } + + private string CreateLocationsNearSP() + { + var sb = new StringBuilder(); + sb.AppendLine(@"CREATE PROCEDURE [dbo].[pLocationsNear]"); + sb.AppendLine(@"@latitude float,"); + sb.AppendLine(@"@longitude float,"); + sb.AppendLine(@"@size int = 500"); + sb.AppendLine(@"AS"); + sb.AppendLine(@"BEGIN"); + sb.AppendLine(@"SELECT TOP( @size) location.*"); + sb.AppendLine(@"FROM [dbo].[Locations] AS location"); + sb.AppendLine(@"ORDER BY dbo.[GetDistanceFromLocation](location.Latitude, location.Longitude, @latitude, @longitude)"); + sb.AppendLine(@"END"); + return sb.ToString(); + } + + private string CreateGetDistanceFunction() + { + var sb = new StringBuilder(); + sb.AppendLine(@"CREATE FUNCTION [dbo].[GetDistanceFromLocation]("); + sb.AppendLine(@"@CurrentLatitude float,"); + sb.AppendLine(@"@CurrentLongitude float,"); + sb.AppendLine(@"@latitude float,"); + sb.AppendLine(@"@longitude float)"); + sb.AppendLine(@"RETURNS int"); + sb.AppendLine(@"AS"); + sb.AppendLine(@"BEGIN"); + sb.AppendLine(@"DECLARE @geo1 geography = geography::Point(@CurrentLatitude, @CurrentLongitude, 4268),@geo2 geography = geography::Point(@latitude, @longitude, 4268)"); + sb.AppendLine(@"DECLARE @distance int"); + sb.AppendLine(@"SELECT @distance = @geo1.STDistance(@geo2)"); + sb.AppendLine(@"RETURN @distance"); + sb.AppendLine(@"END"); + return sb.ToString(); + } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs new file mode 100644 index 000000000..02f77cdd0 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migrations +{ + [DbContext(typeof(LocationsContext))] + partial class LocationsContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:Sequence:.frontier_hilo", "'frontier_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.locations_hilo", "'locations_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "frontier_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Latitude"); + + b.Property("LocationId") + .IsRequired(); + + b.Property("Longitude"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.ToTable("FrontierPoints"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "locations_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Code") + .IsRequired() + .HasColumnName("LocationCode") + .HasMaxLength(15); + + b.Property("Description") + .HasMaxLength(100); + + b.Property("Latitude"); + + b.Property("Longitude"); + + b.Property("ParentId"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") + .WithMany("FrontierPoints") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations") + .WithMany("ChildLocations") + .HasForeignKey("ParentId"); + }); + } + } +} diff --git a/src/Services/Location/Locations.API/Locations.API.csproj b/src/Services/Location/Locations.API/Locations.API.csproj new file mode 100644 index 000000000..b00054921 --- /dev/null +++ b/src/Services/Location/Locations.API/Locations.API.csproj @@ -0,0 +1,39 @@ + + + + netcoreapp1.1 + ..\..\..\..\docker-compose.dcproj + Microsoft.eShopOnContainers.Services.Locations.API + aspnet-Locations.API-20161122013619 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Location/Locations.API/Model/FrontierPoints.cs b/src/Services/Location/Locations.API/Model/FrontierPoints.cs new file mode 100644 index 000000000..4a9112e5e --- /dev/null +++ b/src/Services/Location/Locations.API/Model/FrontierPoints.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Model +{ + public class FrontierPoints + { + public int Id { get; set; } + public double Latitude { get; set; } + public double Longitude { get; set; } + + public Locations Location { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/Location/Locations.API/Model/Locations.cs b/src/Services/Location/Locations.API/Model/Locations.cs new file mode 100644 index 000000000..ec21ac34f --- /dev/null +++ b/src/Services/Location/Locations.API/Model/Locations.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Model +{ + public class Locations + { + public int Id { get; set; } + public string Code { get; set; } + public int? ParentId { get; set; } + public string Description { get; set; } + public double Latitude { get; set; } + public double Longitude { get; set; } + public bool IsPointInPolygon(double lat, double lon) => CheckIsPointInPolygon(lat, lon); + + public Locations() + { + ChildLocations = new List(); + } + + public virtual List Polygon { get; set; } + + [ForeignKey("ParentId")] + public virtual List ChildLocations { get; set; } + + + private bool CheckIsPointInPolygon(double lat, double lon) + { + double minX = Polygon[0].Latitude; + double maxX = Polygon[0].Latitude; + double minY = Polygon[0].Longitude; + double maxY = Polygon[0].Longitude; + for (int i = 1; i < Polygon.Count; i++) + { + FrontierPoints q = Polygon[i]; + minX = Math.Min(q.Latitude, minX); + maxX = Math.Max(q.Latitude, maxX); + minY = Math.Min(q.Longitude, minY); + maxY = Math.Max(q.Longitude, maxY); + } + + if (lat < minX || lat > maxX || lon < minY || lon > maxY) + { + return false; + } + + bool inside = false; + for (int i = 0, j = Polygon.Count - 1; i < Polygon.Count; j = i++) + { + if ((Polygon[i].Longitude > lon) != (Polygon[j].Latitude > lat) && + lat < (Polygon[j].Longitude - Polygon[i].Latitude) * (lon - Polygon[i].Longitude) / (Polygon[j].Longitude - Polygon[i].Longitude) + Polygon[i].Latitude) + { + inside = !inside; + } + } + + return inside; + } + } +} diff --git a/src/Services/Location/Locations.API/Program.cs b/src/Services/Location/Locations.API/Program.cs new file mode 100644 index 000000000..345956401 --- /dev/null +++ b/src/Services/Location/Locations.API/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Microsoft.eShopOnContainers.Services.Locations.API +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseApplicationInsights() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Services/Location/Locations.API/Properties/launchSettings.json b/src/Services/Location/Locations.API/Properties/launchSettings.json new file mode 100644 index 000000000..45b637914 --- /dev/null +++ b/src/Services/Location/Locations.API/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3278/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Locations.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:3279" + } + } +} diff --git a/src/Services/Location/Locations.API/Startup.cs b/src/Services/Location/Locations.API/Startup.cs new file mode 100644 index 000000000..1a42679a6 --- /dev/null +++ b/src/Services/Location/Locations.API/Startup.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Reflection; +using System; + +namespace Microsoft.eShopOnContainers.Services.Locations.API +{ + public class Startup + { + public IConfigurationRoot Configuration { get; } + + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); + + if (env.IsDevelopment()) + { + builder.AddUserSecrets(typeof(Startup).GetTypeInfo().Assembly); + } + + builder.AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc(); + + services.AddDbContext(options => + { + options.UseSqlServer(Configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + + // Changing default behavior when client evaluation occurs to throw. + // Default in EF Core would be to log a warning when client evaluation is performed. + options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); + //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval + }); + + // Add framework services. + services.AddSwaggerGen(options => + { + options.DescribeAllEnumsAsStrings(); + options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info + { + Title = "eShopOnContainers - Location HTTP API", + Version = "v1", + Description = "The Location Microservice HTTP API. This is a Data-Driven/CRUD microservice sample", + TermsOfService = "Terms Of Service" + }); + }); + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + //Configure logs + + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + app.UseCors("CorsPolicy"); + + ConfigureAuth(app); + + app.UseMvcWithDefaultRoute(); + + app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + LocationsContextSeed.SeedAsync(app, loggerFactory) + .Wait(); + } + + protected virtual void ConfigureAuth(IApplicationBuilder app) + { + var identityUrl = Configuration.GetValue("IdentityUrl"); + app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions + { + Authority = identityUrl.ToString(), + ApiName = "locations", + RequireHttpsMetadata = false + }); + } + } +} diff --git a/src/Services/Location/Locations.API/appsettings.Development.json b/src/Services/Location/Locations.API/appsettings.Development.json new file mode 100644 index 000000000..fa8ce71a9 --- /dev/null +++ b/src/Services/Location/Locations.API/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Services/Location/Locations.API/appsettings.json b/src/Services/Location/Locations.API/appsettings.json new file mode 100644 index 000000000..812cce207 --- /dev/null +++ b/src/Services/Location/Locations.API/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.LocationsDb;User Id=sa;Password=Pass@word;", + "IdentityUrl": "http://localhost:5105", + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} \ No newline at end of file diff --git a/test/Services/FunctionalTests/FunctionalTests.csproj b/test/Services/FunctionalTests/FunctionalTests.csproj index 2b8ee1f44..54a74beda 100644 --- a/test/Services/FunctionalTests/FunctionalTests.csproj +++ b/test/Services/FunctionalTests/FunctionalTests.csproj @@ -38,4 +38,8 @@ + + + + \ No newline at end of file diff --git a/test/Services/IntegrationTests/IntegrationTests.csproj b/test/Services/IntegrationTests/IntegrationTests.csproj index b71e1d4c4..60911dd4d 100644 --- a/test/Services/IntegrationTests/IntegrationTests.csproj +++ b/test/Services/IntegrationTests/IntegrationTests.csproj @@ -46,4 +46,8 @@ + + + + diff --git a/test/Services/UnitTest/UnitTest.csproj b/test/Services/UnitTest/UnitTest.csproj index 3a80e594b..8955e4a45 100644 --- a/test/Services/UnitTest/UnitTest.csproj +++ b/test/Services/UnitTest/UnitTest.csproj @@ -28,4 +28,8 @@ + + + + From 1ac1dae9d36398f27f7bb61c8eec1ea70b6e412b Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Thu, 1 Jun 2017 10:10:00 +0200 Subject: [PATCH 02/36] Add Marketing.API project --- ...irewall-rules-for-sts-auth-thru-docker.ps1 | 4 +- .../Marketing/Marketing.API/.dockerignore | 3 + .../Controllers/CampaignsController.cs | 36 +++++ .../Controllers/HomeController.cs | 13 ++ .../Marketing/Marketing.API/Dockerfile | 6 + .../Infrastructure/MarketingContext.cs | 25 ++++ .../Infrastructure/MarketingContextSeed.cs | 20 +++ .../Marketing.API/Marketing.API.csproj | 56 ++++++++ .../Marketing.API/MarketingSettings.cs | 7 + .../Marketing/Marketing.API/Model/Campaing.cs | 7 + .../Marketing/Marketing.API/Program.cs | 21 +++ .../Properties/launchSettings.json | 29 ++++ .../Marketing/Marketing.API/Startup.cs | 125 ++++++++++++++++++ .../appsettings.Development.json | 10 ++ .../Marketing/Marketing.API/appsettings.json | 10 ++ .../FunctionalTests/FunctionalTests.csproj | 4 + .../IntegrationTests/IntegrationTests.csproj | 4 + test/Services/UnitTest/UnitTest.csproj | 4 + 18 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 src/Services/Marketing/Marketing.API/.dockerignore create mode 100644 src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs create mode 100644 src/Services/Marketing/Marketing.API/Controllers/HomeController.cs create mode 100644 src/Services/Marketing/Marketing.API/Dockerfile create mode 100644 src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs create mode 100644 src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs create mode 100644 src/Services/Marketing/Marketing.API/Marketing.API.csproj create mode 100644 src/Services/Marketing/Marketing.API/MarketingSettings.cs create mode 100644 src/Services/Marketing/Marketing.API/Model/Campaing.cs create mode 100644 src/Services/Marketing/Marketing.API/Program.cs create mode 100644 src/Services/Marketing/Marketing.API/Properties/launchSettings.json create mode 100644 src/Services/Marketing/Marketing.API/Startup.cs create mode 100644 src/Services/Marketing/Marketing.API/appsettings.Development.json create mode 100644 src/Services/Marketing/Marketing.API/appsettings.json diff --git a/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 b/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 index be63d8a25..e3545c584 100644 --- a/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 +++ b/cli-windows/add-firewall-rules-for-sts-auth-thru-docker.ps1 @@ -21,6 +21,6 @@ try { Write-Host "Rule found" } catch [Exception] { - New-NetFirewallRule -DisplayName eShopOnContainers-Inbound -Confirm -Description "eShopOnContainers Inbound Rule for port range 5100-5105" -LocalAddress Any -LocalPort 5100-5105 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Inbound - New-NetFirewallRule -DisplayName eShopOnContainers-Outbound -Confirm -Description "eShopOnContainers Outbound Rule for port range 5100-5105" -LocalAddress Any -LocalPort 5100-5105 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Outbound + New-NetFirewallRule -DisplayName eShopOnContainers-Inbound -Confirm -Description "eShopOnContainers Inbound Rule for port range 5100-5110" -LocalAddress Any -LocalPort 5100-5110 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Inbound + New-NetFirewallRule -DisplayName eShopOnContainers-Outbound -Confirm -Description "eShopOnContainers Outbound Rule for port range 5100-5110" -LocalAddress Any -LocalPort 5100-5110 -Protocol tcp -RemoteAddress Any -RemotePort Any -Direction Outbound } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/.dockerignore b/src/Services/Marketing/Marketing.API/.dockerignore new file mode 100644 index 000000000..d8f8175f6 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/.dockerignore @@ -0,0 +1,3 @@ +* +!obj/Docker/publish/* +!obj/Docker/empty/ diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs new file mode 100644 index 000000000..e5914b3ca --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -0,0 +1,36 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers +{ + using System.Collections.Generic; + using Microsoft.AspNetCore.Mvc; + + [Route("api/[controller]")] + public class CampaignsController : Controller + { + [HttpGet] + public IEnumerable Get() + { + return new string[] { "value1", "value2" }; + } + + [HttpGet("{id:int}")] + public string Get(int id) + { + return "value"; + } + + [HttpPost] + public void Post([FromBody]string value) + { + } + + [HttpPut("{id:int}")] + public void Put(int id, [FromBody]string value) + { + } + + [HttpDelete("{id:int}")] + public void Delete(int id) + { + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Controllers/HomeController.cs b/src/Services/Marketing/Marketing.API/Controllers/HomeController.cs new file mode 100644 index 000000000..d37386b9a --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Controllers/HomeController.cs @@ -0,0 +1,13 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers +{ + using Microsoft.AspNetCore.Mvc; + + // GET: // + public class HomeController : Controller + { + public IActionResult Index() + { + return new RedirectResult("~/swagger"); + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dockerfile b/src/Services/Marketing/Marketing.API/Dockerfile new file mode 100644 index 000000000..ea0590a9f --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dockerfile @@ -0,0 +1,6 @@ +FROM microsoft/aspnetcore:1.1 +ARG source +WORKDIR /app +EXPOSE 80 +COPY ${source:-obj/Docker/publish} . +ENTRYPOINT ["dotnet", "Marketing.API.dll"] diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs new file mode 100644 index 000000000..dc3a80564 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs @@ -0,0 +1,25 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure +{ + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata.Builders; + using Microsoft.eShopOnContainers.Services.Marketing.API.Model; + + public class MarketingContext : DbContext + { + public MarketingContext(DbContextOptions options) : base(options) + { + } + + public DbSet Campaings { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity(ConfigureCampaings); + } + + void ConfigureCampaings(EntityTypeBuilder builder) + { + + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs new file mode 100644 index 000000000..46d82e3be --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs @@ -0,0 +1,20 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure +{ + using Microsoft.AspNetCore.Builder; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.Logging; + using System.Threading.Tasks; + + public class MarketingContextSeed + { + public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, int? retry = 0) + { + var context = (MarketingContext)applicationBuilder + .ApplicationServices.GetService(typeof(MarketingContext)); + + context.Database.Migrate(); + + //TODO: add model seeding + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Marketing.API.csproj b/src/Services/Marketing/Marketing.API/Marketing.API.csproj new file mode 100644 index 000000000..1e6ec2416 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Marketing.API.csproj @@ -0,0 +1,56 @@ + + + + netcoreapp1.1 + ..\..\..\..\docker-compose.dcproj + Microsoft.eShopOnContainers.Services.Marketing.API + portable-net45+win8 + aspnet-TestApp-ce941b30-19cf-4972-b34f-d03f2e7976ed + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Marketing/Marketing.API/MarketingSettings.cs b/src/Services/Marketing/Marketing.API/MarketingSettings.cs new file mode 100644 index 000000000..c6e1dfc40 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/MarketingSettings.cs @@ -0,0 +1,7 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API +{ + public class MarketingSettings + { + public string ConnectionString { get; set; } + } +} diff --git a/src/Services/Marketing/Marketing.API/Model/Campaing.cs b/src/Services/Marketing/Marketing.API/Model/Campaing.cs new file mode 100644 index 000000000..62dca8483 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/Campaing.cs @@ -0,0 +1,7 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model +{ + public class Campaing + { + //TODO: Add Campaing properties + } +} diff --git a/src/Services/Marketing/Marketing.API/Program.cs b/src/Services/Marketing/Marketing.API/Program.cs new file mode 100644 index 000000000..cbda3e538 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Program.cs @@ -0,0 +1,21 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API +{ + using System.IO; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseApplicationInsights() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Properties/launchSettings.json b/src/Services/Marketing/Marketing.API/Properties/launchSettings.json new file mode 100644 index 000000000..4adb2bd6a --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52058/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Marketing.API": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:52059" + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Startup.cs b/src/Services/Marketing/Marketing.API/Startup.cs new file mode 100644 index 000000000..91afead63 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Startup.cs @@ -0,0 +1,125 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API +{ + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.EntityFrameworkCore; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using System.Reflection; + using System; + + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + + if (env.IsDevelopment()) + { + builder.AddUserSecrets(typeof(Startup).GetTypeInfo().Assembly); + } + + builder.AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + //services.AddHealthChecks(checks => + //{ + // var minutes = 1; + // if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) + // { + // minutes = minutesParsed; + // } + // checks.AddSqlCheck("MarketingDb", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); + //}); + + // Add framework services. + services.AddMvc(); + + services.AddDbContext(options => + { + options.UseSqlServer(Configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + + // Changing default behavior when client evaluation occurs to throw. + // Default in EF Core would be to log a warning when client evaluation is performed. + options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); + //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval + }); + + // Add framework services. + services.AddSwaggerGen(options => + { + options.DescribeAllEnumsAsStrings(); + options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info + { + Title = "Marketing HTTP API", + Version = "v1", + Description = "The Marketing Service HTTP API", + TermsOfService = "Terms Of Service" + }); + }); + + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + app.UseCors("CorsPolicy"); + + ConfigureAuth(app); + + app.UseMvcWithDefaultRoute(); + + app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + //MarketingContextSeed.SeedAsync(app, loggerFactory) + // .Wait(); + } + + + protected virtual void ConfigureAuth(IApplicationBuilder app) + { + var identityUrl = Configuration.GetValue("IdentityUrl"); + app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions + { + Authority = identityUrl.ToString(), + ApiName = "marketing", + RequireHttpsMetadata = false + }); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/appsettings.Development.json b/src/Services/Marketing/Marketing.API/appsettings.Development.json new file mode 100644 index 000000000..fa8ce71a9 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Services/Marketing/Marketing.API/appsettings.json b/src/Services/Marketing/Marketing.API/appsettings.json new file mode 100644 index 000000000..36bdcad2c --- /dev/null +++ b/src/Services/Marketing/Marketing.API/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + }, + "ConnectionString": "127.0.0.1", + "IdentityUrl": "http://localhost:5105" +} diff --git a/test/Services/FunctionalTests/FunctionalTests.csproj b/test/Services/FunctionalTests/FunctionalTests.csproj index 2b8ee1f44..54a74beda 100644 --- a/test/Services/FunctionalTests/FunctionalTests.csproj +++ b/test/Services/FunctionalTests/FunctionalTests.csproj @@ -38,4 +38,8 @@ + + + + \ No newline at end of file diff --git a/test/Services/IntegrationTests/IntegrationTests.csproj b/test/Services/IntegrationTests/IntegrationTests.csproj index b71e1d4c4..60911dd4d 100644 --- a/test/Services/IntegrationTests/IntegrationTests.csproj +++ b/test/Services/IntegrationTests/IntegrationTests.csproj @@ -46,4 +46,8 @@ + + + + diff --git a/test/Services/UnitTest/UnitTest.csproj b/test/Services/UnitTest/UnitTest.csproj index 3a80e594b..8955e4a45 100644 --- a/test/Services/UnitTest/UnitTest.csproj +++ b/test/Services/UnitTest/UnitTest.csproj @@ -28,4 +28,8 @@ + + + + From 6082359f080c856005012e750fe09222549c2943 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Thu, 1 Jun 2017 10:10:20 +0200 Subject: [PATCH 03/36] Add docker support --- docker-compose.override.yml | 9 ++++ docker-compose.prod.yml | 11 ++++- docker-compose.vs.debug.yml | 15 +++++++ docker-compose.vs.release.yml | 10 +++++ docker-compose.yml | 11 ++++- eShopOnContainers-ServicesAndWebApps.sln | 56 +++++++++++++++++++++++- 6 files changed, 109 insertions(+), 3 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 04d1c4d9c..079c86768 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -95,3 +95,12 @@ services: - spa=http://webspa/hc ports: - "5107: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 + - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + ports: + - "5110:80" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index c5d8839ea..b1dc89754 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -99,4 +99,13 @@ services: - spa=http://webspa/hc ports: - - "5107:80" \ No newline at end of file + - "5107: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 + - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. + ports: + - "5110:80" \ No newline at end of file diff --git a/docker-compose.vs.debug.yml b/docker-compose.vs.debug.yml index 2e7145637..ef2ff662b 100644 --- a/docker-compose.vs.debug.yml +++ b/docker-compose.vs.debug.yml @@ -105,3 +105,18 @@ services: entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + marketing.api: + image: eshop/marketing.api:dev + build: + args: + source: ${DOCKER_BUILD_SOURCE} + environment: + - DOTNET_USE_POLLING_FILE_WATCHER=1 + volumes: + - ./src/Marketing/Marketing.API:/app + - ~/.nuget/packages:/root/.nuget/packages:ro + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" diff --git a/docker-compose.vs.release.yml b/docker-compose.vs.release.yml index d1ca5b2c6..8be29c095 100644 --- a/docker-compose.vs.release.yml +++ b/docker-compose.vs.release.yml @@ -70,3 +70,13 @@ services: entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + + marketing.api: + build: + args: + source: ${DOCKER_BUILD_SOURCE} + volumes: + - ~/clrdbg:/clrdbg:ro + entrypoint: tail -f /dev/null + labels: + - "com.microsoft.visualstudio.targetoperatingsystem=linux" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 21f3972f2..9612b38fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,15 @@ services: depends_on: - sql.data + marketing.api: + image: eshop/marketing.api + build: + context: ./src/Services/Marketing/Marketing.API + dockerfile: Dockerfile + depends_on: + - sql.data + - identity.api + webspa: image: eshop/webspa build: @@ -74,4 +83,4 @@ services: build: context: ./src/Web/WebStatus dockerfile: Dockerfile - \ No newline at end of file + diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index eb0e83e02..b0734add6 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26403.3 +VisualStudioVersion = 15.0.26430.6 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{932D8224-11F6-4D07-B109-DA28AD288A63}" EndProject @@ -76,6 +76,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Health EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventBus.Tests", "src\BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Marketing", "Marketing", "{A5260DE0-1FDD-467E-9CC1-A028AB081CEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marketing.API", "src\Services\Marketing\Marketing.API\Marketing.API.csproj", "{DF395F85-B010-465D-857A-7EBCC512C0C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1002,6 +1006,54 @@ Global {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x64.Build.0 = Release|Any CPU {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x86.ActiveCfg = Release|Any CPU {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x86.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|ARM.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|iPhone.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|x64.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|x64.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|x86.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.AppStore|x86.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|ARM.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|ARM.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|iPhone.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|x64.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Debug|x86.Build.0 = Debug|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|Any CPU.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|ARM.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|ARM.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|iPhone.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|iPhone.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|x64.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|x64.Build.0 = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|x86.ActiveCfg = Release|Any CPU + {DF395F85-B010-465D-857A-7EBCC512C0C2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1038,5 +1090,7 @@ Global {22A0F9C1-2D4A-4107-95B7-8459E6688BC5} = {A81ECBC2-6B00-4DCD-8388-469174033379} {4BD76717-3102-4969-8C2C-BAAA3F0263B6} = {A81ECBC2-6B00-4DCD-8388-469174033379} {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} + {A5260DE0-1FDD-467E-9CC1-A028AB081CEE} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} + {DF395F85-B010-465D-857A-7EBCC512C0C2} = {A5260DE0-1FDD-467E-9CC1-A028AB081CEE} EndGlobalSection EndGlobal From 665733c40e00574cf93917c5b8210625838a7ae2 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Thu, 1 Jun 2017 10:10:37 +0200 Subject: [PATCH 04/36] Add marketing resource to identity --- src/Services/Identity/Identity.API/Configuration/Config.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index 260989da4..7de28772e 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -12,7 +12,8 @@ namespace Identity.API.Configuration return new List { new ApiResource("orders", "Orders Service"), - new ApiResource("basket", "Basket Service") + new ApiResource("basket", "Basket Service"), + new ApiResource("marketing", "Marketing Service") }; } From 32247c503d0783375a4e06ff8326451b141f36e0 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Thu, 1 Jun 2017 20:15:21 +0200 Subject: [PATCH 05/36] Add marketing api --- docker-compose.override.yml | 20 ++-- docker-compose.prod.yml | 20 ++-- docker-compose.vs.debug.yml | 26 +++-- docker-compose.vs.release.yml | 10 +- .../Controllers/CampaignsController.cs | 68 +++++++++-- .../Marketing/Marketing.API/Dockerfile | 2 +- .../Infrastructure/MarketingContext.cs | 69 ++++++++++- .../Infrastructure/MarketingContextSeed.cs | 49 +++++++- .../Marketing.API/Marketing.API.csproj | 30 ++--- .../20170601175200_Initial.Designer.cs | 108 ++++++++++++++++++ .../Migrations/20170601175200_Initial.cs | 71 ++++++++++++ .../MarketingContextModelSnapshot.cs | 107 +++++++++++++++++ .../Marketing/Marketing.API/Model/Campaing.cs | 19 ++- .../Marketing/Marketing.API/Model/Location.cs | 9 ++ .../Marketing/Marketing.API/Model/Rule.cs | 29 +++++ .../Marketing/Marketing.API/Program.cs | 1 - .../Properties/launchSettings.json | 6 +- .../Marketing/Marketing.API/Startup.cs | 14 +-- .../appsettings.Development.json | 4 +- 19 files changed, 566 insertions(+), 96 deletions(-) create mode 100644 src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.Designer.cs create mode 100644 src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.cs create mode 100644 src/Services/Marketing/Marketing.API/Migrations/MarketingContextModelSnapshot.cs create mode 100644 src/Services/Marketing/Marketing.API/Model/Location.cs create mode 100644 src/Services/Marketing/Marketing.API/Model/Rule.cs diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 079c86768..1348a6e9b 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -49,6 +49,15 @@ services: ports: - "5102: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 + - 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" + webspa: environment: - ASPNETCORE_ENVIRONMENT=Development @@ -94,13 +103,4 @@ services: - mvc=http://webmvc/hc - spa=http://webspa/hc ports: - - "5107: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 - - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - ports: - - "5110:80" \ No newline at end of file + - "5107:80" \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b1dc89754..b9c46b4c2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -54,6 +54,15 @@ services: ports: - "5102: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 + - 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" + webspa: environment: - ASPNETCORE_ENVIRONMENT=Production @@ -99,13 +108,4 @@ services: - spa=http://webspa/hc ports: - - "5107: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 - - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - ports: - - "5110:80" \ No newline at end of file + - "5107:80" \ No newline at end of file diff --git a/docker-compose.vs.debug.yml b/docker-compose.vs.debug.yml index ef2ff662b..85195a493 100644 --- a/docker-compose.vs.debug.yml +++ b/docker-compose.vs.debug.yml @@ -61,62 +61,64 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" - webspa: - image: eshop/webspa:dev + marketing.api: + image: eshop/marketing.api:dev build: args: source: ${DOCKER_BUILD_SOURCE} environment: - DOTNET_USE_POLLING_FILE_WATCHER=1 volumes: - - ./src/Web/WebSPA:/app + - ./src/Services/Marketing/Marketing.API:/app - ~/.nuget/packages:/root/.nuget/packages:ro - ~/clrdbg:/clrdbg:ro entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" - webmvc: - image: eshop/webmvc:dev + + webspa: + image: eshop/webspa:dev build: args: source: ${DOCKER_BUILD_SOURCE} environment: - DOTNET_USE_POLLING_FILE_WATCHER=1 volumes: - - ./src/Web/WebMVC:/app + - ./src/Web/WebSPA:/app - ~/.nuget/packages:/root/.nuget/packages:ro - ~/clrdbg:/clrdbg:ro entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" - webstatus: - image: eshop/webstatus:dev + webmvc: + image: eshop/webmvc:dev build: args: source: ${DOCKER_BUILD_SOURCE} environment: - DOTNET_USE_POLLING_FILE_WATCHER=1 volumes: - - ./src/Web/WebStatus:/app + - ./src/Web/WebMVC:/app - ~/.nuget/packages:/root/.nuget/packages:ro - ~/clrdbg:/clrdbg:ro entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" - marketing.api: - image: eshop/marketing.api:dev + webstatus: + image: eshop/webstatus:dev build: args: source: ${DOCKER_BUILD_SOURCE} environment: - DOTNET_USE_POLLING_FILE_WATCHER=1 volumes: - - ./src/Marketing/Marketing.API:/app + - ./src/Web/WebStatus:/app - ~/.nuget/packages:/root/.nuget/packages:ro - ~/clrdbg:/clrdbg:ro entrypoint: tail -f /dev/null labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" + diff --git a/docker-compose.vs.release.yml b/docker-compose.vs.release.yml index 8be29c095..f01b21eff 100644 --- a/docker-compose.vs.release.yml +++ b/docker-compose.vs.release.yml @@ -41,7 +41,7 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" - webspa: + marketing.api: build: args: source: ${DOCKER_BUILD_SOURCE} @@ -51,7 +51,7 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" - webmvc: + webspa: build: args: source: ${DOCKER_BUILD_SOURCE} @@ -61,7 +61,7 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" - webstatus: + webmvc: build: args: source: ${DOCKER_BUILD_SOURCE} @@ -71,7 +71,7 @@ services: labels: - "com.microsoft.visualstudio.targetoperatingsystem=linux" - marketing.api: + webstatus: build: args: source: ${DOCKER_BUILD_SOURCE} @@ -79,4 +79,4 @@ services: - ~/clrdbg:/clrdbg:ro entrypoint: tail -f /dev/null labels: - - "com.microsoft.visualstudio.targetoperatingsystem=linux" \ No newline at end of file + - "com.microsoft.visualstudio.targetoperatingsystem=linux" diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index e5914b3ca..b029bfa51 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -1,36 +1,86 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers { - using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + using System.Threading.Tasks; + using Microsoft.eShopOnContainers.Services.Marketing.API.Model; + using Microsoft.EntityFrameworkCore; [Route("api/[controller]")] public class CampaignsController : Controller { + private readonly MarketingContext _context; + + public CampaignsController(MarketingContext context) + { + _context = context; + } + [HttpGet] - public IEnumerable Get() + public async Task GetAllCampaigns() { - return new string[] { "value1", "value2" }; + var campaignList = await _context.Campaigns + .Include(c => c.Rules) + .ToListAsync(); + + return Ok(campaignList); } [HttpGet("{id:int}")] - public string Get(int id) + public async Task GetCampaignById(int id) { - return "value"; + var campaign = await _context.Campaigns + .Include(c => c.Rules) + .SingleAsync(c => c.Id == id); + + if (campaign is null) + { + return NotFound(); + } + + return Ok(campaign); } [HttpPost] - public void Post([FromBody]string value) + public async Task CreateCampaign([FromBody] Campaign campaign) { + await _context.Campaigns.AddAsync(campaign); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetCampaignById), new { id = campaign.Id }, null); } [HttpPut("{id:int}")] - public void Put(int id, [FromBody]string value) + public async Task UpdateCampaign(int id, [FromBody]Campaign campaign) { + var campaignToUpdate = await _context.Campaigns.FindAsync(id); + if (campaign is null) + { + return NotFound(); + } + + campaignToUpdate.Description = campaign.Description; + campaignToUpdate.From = campaign.From; + campaignToUpdate.To = campaign.To; + + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetCampaignById), new { id = campaignToUpdate.Id }, null); } [HttpDelete("{id:int}")] - public void Delete(int id) + public async Task Delete(int id) { + var campaign = await _context.Campaigns.FindAsync(id); + if (campaign is null) + { + return NotFound(); + } + + _context.Campaigns.Remove(campaign); + await _context.SaveChangesAsync(); + + return NoContent(); } } -} +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dockerfile b/src/Services/Marketing/Marketing.API/Dockerfile index ea0590a9f..54a79a5e4 100644 --- a/src/Services/Marketing/Marketing.API/Dockerfile +++ b/src/Services/Marketing/Marketing.API/Dockerfile @@ -1,4 +1,4 @@ -FROM microsoft/aspnetcore:1.1 +FROM microsoft/aspnetcore:1.1.2 ARG source WORKDIR /app EXPOSE 80 diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs index dc3a80564..798d8d152 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs @@ -6,20 +6,77 @@ public class MarketingContext : DbContext { - public MarketingContext(DbContextOptions options) : base(options) - { + public MarketingContext(DbContextOptions options) : base(options) + { } - public DbSet Campaings { get; set; } + public DbSet Campaigns { get; set; } + + public DbSet Rules { get; set; } + public DbSet UserProfileRules { get; set; } + public DbSet PurchaseHistoryRules { get; set; } + public DbSet UserLocationRules { get; set; } protected override void OnModelCreating(ModelBuilder builder) { - builder.Entity(ConfigureCampaings); + builder.Entity(ConfigureCampaigns); + builder.Entity(ConfigureRules); + builder.Entity(ConfigureUserLocationRules); } - void ConfigureCampaings(EntityTypeBuilder builder) + void ConfigureCampaigns(EntityTypeBuilder builder) { + builder.ToTable("Campaign"); + + builder.HasKey(ci => ci.Id); + + builder.Property(ci => ci.Id) + .ForSqlServerUseSequenceHiLo("campaign_hilo") + .IsRequired(); + + builder.Property(m => m.Description) + .HasColumnName("Description") + .IsRequired(); + builder.Property(m => m.From) + .HasColumnName("From") + .IsRequired(); + + builder.Property(m => m.To) + .HasColumnName("To") + .IsRequired(); + + builder.Property(m => m.Description) + .HasColumnName("Description") + .IsRequired(); + + builder.HasMany(m => m.Rules) + .WithOne(r => r.Campaign) + .HasForeignKey(m => m.CampaignId) + .IsRequired(); + } + + void ConfigureRules(EntityTypeBuilder builder) + { + builder.ToTable("Rules"); + + builder.HasKey(r => r.Id); + + builder.HasDiscriminator("RuleTypeId") + .HasValue(1) + .HasValue(2) + .HasValue(3); + + builder.Property(r => r.Description) + .HasColumnName("Description") + .IsRequired(); + } + + void ConfigureUserLocationRules(EntityTypeBuilder builder) + { + builder.Property(r => r.LocationId) + .HasColumnName("LocationId") + .IsRequired(); } } -} +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs index 46d82e3be..9d2bb0157 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs @@ -2,7 +2,11 @@ { using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; + using Microsoft.eShopOnContainers.Services.Marketing.API.Model; using Microsoft.Extensions.Logging; + using System; + using System.Collections.Generic; + using System.Linq; using System.Threading.Tasks; public class MarketingContextSeed @@ -14,7 +18,50 @@ context.Database.Migrate(); - //TODO: add model seeding + if (!context.Campaigns.Any()) + { + context.Campaigns.AddRange( + GetPreconfiguredMarketings()); + + await context.SaveChangesAsync(); + } + } + + static List GetPreconfiguredMarketings() + { + return new List + { + new Campaign + { + Description = "Campaign1", + From = DateTime.Now, + To = DateTime.Now.AddDays(7), + Url = "http://CampaignUrl.test/12f09ed3cef54187123f500ad", + Rules = new List + { + new UserLocationRule + { + Description = "UserLocationRule1", + LocationId = 1 + } + } + }, + new Campaign + { + Description = "Campaign2", + From = DateTime.Now.AddDays(7), + To = DateTime.Now.AddDays(14), + Url = "http://CampaignUrl.test/02a59eda65f241871239000ff", + Rules = new List + { + new UserLocationRule + { + Description = "UserLocationRule2", + LocationId = 1 + } + } + } + }; } } } diff --git a/src/Services/Marketing/Marketing.API/Marketing.API.csproj b/src/Services/Marketing/Marketing.API/Marketing.API.csproj index 1e6ec2416..f65be06bb 100644 --- a/src/Services/Marketing/Marketing.API/Marketing.API.csproj +++ b/src/Services/Marketing/Marketing.API/Marketing.API.csproj @@ -2,20 +2,20 @@ netcoreapp1.1 + 1.1.2 + Exe ..\..\..\..\docker-compose.dcproj Microsoft.eShopOnContainers.Services.Marketing.API portable-net45+win8 - aspnet-TestApp-ce941b30-19cf-4972-b34f-d03f2e7976ed + aspnet-Marketing.API-20161122013619 - - - + @@ -24,23 +24,14 @@ + + + + - - - - - - - - - - - - - - - + + @@ -52,5 +43,4 @@ - diff --git a/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.Designer.cs b/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.Designer.cs new file mode 100644 index 000000000..b0305f171 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.Designer.cs @@ -0,0 +1,108 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations +{ + [DbContext(typeof(MarketingContext))] + [Migration("20170601175200_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:Sequence:.campaign_hilo", "'campaign_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "campaign_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Description") + .IsRequired() + .HasColumnName("Description"); + + b.Property("From") + .HasColumnName("From"); + + b.Property("To") + .HasColumnName("To"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("Campaign"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CampaignId"); + + b.Property("Description") + .IsRequired() + .HasColumnName("Description"); + + b.Property("RuleTypeId"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.ToTable("Rules"); + + b.HasDiscriminator("RuleTypeId"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.PurchaseHistoryRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + + b.ToTable("PurchaseHistoryRule"); + + b.HasDiscriminator().HasValue(2); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.UserLocationRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + b.Property("LocationId") + .HasColumnName("LocationId"); + + b.ToTable("UserLocationRule"); + + b.HasDiscriminator().HasValue(3); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.UserProfileRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + + b.ToTable("UserProfileRule"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", "Campaign") + .WithMany("Rules") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.cs b/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.cs new file mode 100644 index 000000000..dee3aa12f --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateSequence( + name: "campaign_hilo", + incrementBy: 10); + + migrationBuilder.CreateTable( + name: "Campaign", + columns: table => new + { + Id = table.Column(nullable: false), + Description = table.Column(nullable: false), + From = table.Column(nullable: false), + To = table.Column(nullable: false), + Url = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Campaign", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Rules", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + CampaignId = table.Column(nullable: false), + Description = table.Column(nullable: false), + RuleTypeId = table.Column(nullable: false), + LocationId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Rules", x => x.Id); + table.ForeignKey( + name: "FK_Rules_Campaign_CampaignId", + column: x => x.CampaignId, + principalTable: "Campaign", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Rules_CampaignId", + table: "Rules", + column: "CampaignId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Rules"); + + migrationBuilder.DropTable( + name: "Campaign"); + + migrationBuilder.DropSequence( + name: "campaign_hilo"); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Migrations/MarketingContextModelSnapshot.cs b/src/Services/Marketing/Marketing.API/Migrations/MarketingContextModelSnapshot.cs new file mode 100644 index 000000000..3d954edcc --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Migrations/MarketingContextModelSnapshot.cs @@ -0,0 +1,107 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations +{ + [DbContext(typeof(MarketingContext))] + partial class MarketingContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:Sequence:.campaign_hilo", "'campaign_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "campaign_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("Description") + .IsRequired() + .HasColumnName("Description"); + + b.Property("From") + .HasColumnName("From"); + + b.Property("To") + .HasColumnName("To"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.ToTable("Campaign"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CampaignId"); + + b.Property("Description") + .IsRequired() + .HasColumnName("Description"); + + b.Property("RuleTypeId"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.ToTable("Rules"); + + b.HasDiscriminator("RuleTypeId"); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.PurchaseHistoryRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + + b.ToTable("PurchaseHistoryRule"); + + b.HasDiscriminator().HasValue(2); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.UserLocationRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + b.Property("LocationId") + .HasColumnName("LocationId"); + + b.ToTable("UserLocationRule"); + + b.HasDiscriminator().HasValue(3); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.UserProfileRule", b => + { + b.HasBaseType("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule"); + + + b.ToTable("UserProfileRule"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", "Campaign") + .WithMany("Rules") + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Marketing/Marketing.API/Model/Campaing.cs b/src/Services/Marketing/Marketing.API/Model/Campaing.cs index 62dca8483..bbe794b1a 100644 --- a/src/Services/Marketing/Marketing.API/Model/Campaing.cs +++ b/src/Services/Marketing/Marketing.API/Model/Campaing.cs @@ -1,7 +1,20 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model { - public class Campaing + using System; + using System.Collections.Generic; + + public class Campaign { - //TODO: Add Campaing properties + public int Id { get; set; } + + public string Description { get; set; } + + public DateTime From { get; set; } + + public DateTime To { get; set; } + + public string Url { get; set; } + + public ICollection Rules { get; set; } } -} +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Model/Location.cs b/src/Services/Marketing/Marketing.API/Model/Location.cs new file mode 100644 index 000000000..e3973f425 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/Location.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model +{ + public class Location + { + public int Id { get; set; } + } +} diff --git a/src/Services/Marketing/Marketing.API/Model/Rule.cs b/src/Services/Marketing/Marketing.API/Model/Rule.cs new file mode 100644 index 000000000..38a47d3eb --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Model/Rule.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model +{ + public abstract class Rule + { + public int Id { get; set; } + + public int RuleTypeId { get; set; } + + public int CampaignId { get; set; } + + public Campaign Campaign { get; set; } + + public string Description { get; set; } + } + + + public class UserProfileRule : Rule + { } + + public class PurchaseHistoryRule : Rule + { } + + public class UserLocationRule : Rule + { + public int LocationId { get; set; } + } +} diff --git a/src/Services/Marketing/Marketing.API/Program.cs b/src/Services/Marketing/Marketing.API/Program.cs index cbda3e538..981e797c1 100644 --- a/src/Services/Marketing/Marketing.API/Program.cs +++ b/src/Services/Marketing/Marketing.API/Program.cs @@ -12,7 +12,6 @@ .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup() - .UseApplicationInsights() .Build(); host.Run(); diff --git a/src/Services/Marketing/Marketing.API/Properties/launchSettings.json b/src/Services/Marketing/Marketing.API/Properties/launchSettings.json index 4adb2bd6a..ec47d57ce 100644 --- a/src/Services/Marketing/Marketing.API/Properties/launchSettings.json +++ b/src/Services/Marketing/Marketing.API/Properties/launchSettings.json @@ -1,9 +1,9 @@ -{ +{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:52058/", + "applicationUrl": "http://localhost:5110", "sslPort": 0 } }, @@ -26,4 +26,4 @@ "applicationUrl": "http://localhost:52059" } } -} +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Startup.cs b/src/Services/Marketing/Marketing.API/Startup.cs index 91afead63..378ba214c 100644 --- a/src/Services/Marketing/Marketing.API/Startup.cs +++ b/src/Services/Marketing/Marketing.API/Startup.cs @@ -36,16 +36,6 @@ // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - //services.AddHealthChecks(checks => - //{ - // var minutes = 1; - // if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) - // { - // minutes = minutesParsed; - // } - // checks.AddSqlCheck("MarketingDb", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); - //}); - // Add framework services. services.AddMvc(); @@ -106,8 +96,8 @@ c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); - //MarketingContextSeed.SeedAsync(app, loggerFactory) - // .Wait(); + MarketingContextSeed.SeedAsync(app, loggerFactory) + .Wait(); } diff --git a/src/Services/Marketing/Marketing.API/appsettings.Development.json b/src/Services/Marketing/Marketing.API/appsettings.Development.json index fa8ce71a9..5fff67bac 100644 --- a/src/Services/Marketing/Marketing.API/appsettings.Development.json +++ b/src/Services/Marketing/Marketing.API/appsettings.Development.json @@ -2,9 +2,7 @@ "Logging": { "IncludeScopes": false, "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" + "Default": "Warning" } } } From bae10b07b87421e66e5ab5c3aaba92c20803be3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Thu, 1 Jun 2017 20:16:19 +0200 Subject: [PATCH 06/36] Created UserLocationModel Map Position to Locations APi Update current user location --- .../Identity.API/Configuration/Config.cs | 6 +- .../Controllers/LocationsController.cs | 35 +++++++++ .../Controllers/ValuesController.cs | 44 ------------ .../Infrastructure/LocationsContext.cs | 23 +++++- .../Infrastructure/LocationsContextSeed.cs | 72 +++++++++---------- ....cs => 20170601140634_Initial.Designer.cs} | 41 +++++++++-- ...4_Initial.cs => 20170601140634_Initial.cs} | 67 +++++++++++------ .../LocationsContextModelSnapshot.cs | 39 ++++++++-- .../Repositories/ILocationsRepository.cs | 17 +++++ .../Repositories/IRepository.cs | 12 ++++ .../Repositories/IUnitOfWork.cs | 13 ++++ .../Repositories/LocationsRepository.cs | 60 ++++++++++++++++ .../Services/IIdentityService.cs | 12 ++++ .../Services/ILocationsService.cs | 10 +++ .../Services/IdentityService.cs | 23 ++++++ .../Services/LocationsService.cs | 62 ++++++++++++++++ .../Locations.API/Locations.API.csproj | 1 + .../Locations.API/Model/FrontierPoints.cs | 16 ++++- .../Location/Locations.API/Model/Locations.cs | 46 ++++++++++-- .../Locations.API/Model/UserLocation.cs | 24 +++++++ .../Location/Locations.API/Startup.cs | 8 +++ .../ViewModel/LocationRequest.cs | 13 ++++ .../IntegrationTests/IntegrationTests.csproj | 5 ++ .../Locations/LocationsScenarioBase.cs | 36 ++++++++++ .../Services/Locations/LocationsScenarios.cs | 37 ++++++++++ .../Locations/LocationsTestsStartup.cs | 26 +++++++ .../Services/Locations/appsettings.json | 7 ++ 27 files changed, 627 insertions(+), 128 deletions(-) create mode 100644 src/Services/Location/Locations.API/Controllers/LocationsController.cs delete mode 100644 src/Services/Location/Locations.API/Controllers/ValuesController.cs rename src/Services/Location/Locations.API/Infrastructure/Migrations/{20170529174524_Initial.Designer.cs => 20170601140634_Initial.Designer.cs} (66%) rename src/Services/Location/Locations.API/Infrastructure/Migrations/{20170529174524_Initial.cs => 20170601140634_Initial.cs} (72%) create mode 100644 src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Repositories/IRepository.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Repositories/IUnitOfWork.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Services/IIdentityService.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Services/IdentityService.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs create mode 100644 src/Services/Location/Locations.API/Model/UserLocation.cs create mode 100644 src/Services/Location/Locations.API/ViewModel/LocationRequest.cs create mode 100644 test/Services/IntegrationTests/Services/Locations/LocationsScenarioBase.cs create mode 100644 test/Services/IntegrationTests/Services/Locations/LocationsScenarios.cs create mode 100644 test/Services/IntegrationTests/Services/Locations/LocationsTestsStartup.cs create mode 100644 test/Services/IntegrationTests/Services/Locations/appsettings.json diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index a07f5d01d..2b78737a9 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -72,7 +72,8 @@ namespace Identity.API.Configuration IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.OfflineAccess, "orders", - "basket" + "basket", + "locations" }, //Allow requesting refresh tokens for long lived API access AllowOfflineAccess = true @@ -103,7 +104,8 @@ namespace Identity.API.Configuration IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.OfflineAccess, "orders", - "basket" + "basket", + "locations" }, } }; diff --git a/src/Services/Location/Locations.API/Controllers/LocationsController.cs b/src/Services/Location/Locations.API/Controllers/LocationsController.cs new file mode 100644 index 000000000..dc2940b73 --- /dev/null +++ b/src/Services/Location/Locations.API/Controllers/LocationsController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services; +using Microsoft.eShopOnContainers.Services.Locations.API.ViewModel; +using System; +using System.Threading.Tasks; + +namespace Locations.API.Controllers +{ + [Route("api/v1/[controller]")] + [Authorize] + public class LocationsController : ControllerBase + { + private readonly ILocationsService _locationsService; + private readonly IIdentityService _identityService; + + public LocationsController(ILocationsService locationsService, IIdentityService identityService) + { + _locationsService = locationsService ?? throw new ArgumentNullException(nameof(locationsService)); + _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService)); + } + + //POST api/v1/[controller]/ + [Route("")] + [HttpPost] + public async Task UpdateUserLocation([FromBody]LocationRequest newLocReq) + { + var userId = _identityService.GetUserIdentity(); + var result = await _locationsService.AddOrUpdateUserLocation(userId, newLocReq); + return result ? + (IActionResult)Ok() : + (IActionResult)BadRequest(); + } + } +} diff --git a/src/Services/Location/Locations.API/Controllers/ValuesController.cs b/src/Services/Location/Locations.API/Controllers/ValuesController.cs deleted file mode 100644 index e711e221d..000000000 --- a/src/Services/Location/Locations.API/Controllers/ValuesController.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Locations.API.Controllers -{ - [Route("api/[controller]")] - public class ValuesController : Controller - { - // GET api/values - [HttpGet] - public IEnumerable Get() - { - return new string[] { "value1", "value2" }; - } - - // GET api/values/5 - [HttpGet("{id}")] - public string Get(int id) - { - return "value"; - } - - // POST api/values - [HttpPost] - public void Post([FromBody]string value) - { - } - - // PUT api/values/5 - [HttpPut("{id}")] - public void Put(int id, [FromBody]string value) - { - } - - // DELETE api/values/5 - [HttpDelete("{id}")] - public void Delete(int id) - { - } - } -} diff --git a/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs b/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs index 42c3f7900..5767694df 100644 --- a/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs +++ b/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs @@ -2,20 +2,26 @@ { using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; + using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories; using Microsoft.eShopOnContainers.Services.Locations.API.Model; + using System.Threading; + using System.Threading.Tasks; - public class LocationsContext : DbContext + public class LocationsContext : DbContext, IUnitOfWork { public LocationsContext(DbContextOptions options) : base(options) { } public DbSet Locations { get; set; } + public DbSet FrontierPoints { get; set; } + public DbSet UserLocation { get; set; } protected override void OnModelCreating(ModelBuilder builder) { builder.Entity(ConfigureLocations); builder.Entity(ConfigureFrontierPoints); + builder.Entity(ConfigureUserLocation); } void ConfigureLocations(EntityTypeBuilder builder) @@ -25,7 +31,7 @@ builder.HasKey(cl => cl.Id); builder.Property(cl => cl.Id) - .ForSqlServerUseSequenceHiLo("locations_hilo") + .ForSqlServerUseSequenceHiLo("locations_seq") .IsRequired(); builder.Property(cb => cb.Code) @@ -48,8 +54,19 @@ builder.HasKey(fp => fp.Id); builder.Property(fp => fp.Id) - .ForSqlServerUseSequenceHiLo("frontier_hilo") + .ForSqlServerUseSequenceHiLo("frontier_seq") .IsRequired(); + } + + void ConfigureUserLocation(EntityTypeBuilder builder) + { + builder.ToTable("UserLocation"); + + builder.Property(ul => ul.Id) + .ForSqlServerUseSequenceHiLo("UserLocation_seq") + .IsRequired(); + + builder.HasIndex(ul => ul.UserId).IsUnique(); } } } diff --git a/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs b/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs index 67673719b..63404f49d 100644 --- a/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs +++ b/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs @@ -29,21 +29,21 @@ static Locations GetPreconfiguredLocations() { - var ww = new Locations() { Code = "WW", Description = "WorldWide", Latitude = -1, Longitude = -1 }; + var ww = new Locations("WW", "WorldWide", -1, -1); ww.ChildLocations.Add(GetUSLocations()); return ww; } static Locations GetUSLocations() { - var us = new Locations() { Code = "US", Description = "United States", Latitude = 41.650455, Longitude = -101.357386, Polygon = GetUSPoligon() }; + var us = new Locations("US", "United States", 41.650455, -101.357386, GetUSPoligon()); us.ChildLocations.Add(GetWashingtonLocations()); return us; } static Locations GetWashingtonLocations() { - var wht = new Locations() { Code = "WHT", Description = "Washington", Latitude = 47.223652, Longitude = -119.542781, Polygon = GetWashingtonPoligon() }; + var wht = new Locations("WHT", "Washington", 47.223652, -119.542781, GetWashingtonPoligon()); wht.ChildLocations.Add(GetSeattleLocations()); wht.ChildLocations.Add(GetRedmondLocations()); return wht; @@ -51,80 +51,80 @@ static Locations GetSeattleLocations() { - var bcn = new Locations() { Code = "SEAT", Description = "Seattle", Latitude = 47.603111, Longitude = -122.330747, Polygon = GetSeattlePoligon() }; - bcn.ChildLocations.Add(new Locations() { Code = "SEAT-PioneerSqr", Description = "Seattle Pioneer Square Shop" , Latitude = 47.602053, Longitude= -122.333884, Polygon = GetSeattlePioneerSqrPoligon() }); + var bcn = new Locations("SEAT", "Seattle", 47.603111, -122.330747, GetSeattlePoligon()); + bcn.ChildLocations.Add(new Locations("SEAT-PioneerSqr", "Seattle Pioneer Square Shop", 47.602053, -122.333884, GetSeattlePioneerSqrPoligon())); return bcn; } static Locations GetRedmondLocations() { - var bcn = new Locations() { Code = "REDM", Description = "Redmond", Latitude = 47.674961, Longitude = -122.122887, Polygon = GetRedmondPoligon() }; - bcn.ChildLocations.Add(new Locations() { Code = "REDM-DWNTWP", Description = "Redmond Downtown Central Park Shop", Latitude = 47.674433, Longitude = -122.125006, Polygon = GetRedmondDowntownParkPoligon() }); + var bcn = new Locations("REDM", "Redmond", 47.674961, -122.122887, GetRedmondPoligon()); + bcn.ChildLocations.Add(new Locations("REDM-DWNTWP", "Redmond Downtown Central Park Shop", 47.674433, -122.125006, GetRedmondDowntownParkPoligon())); return bcn; } static List GetUSPoligon() { var poligon = new List(); - poligon.Add(new FrontierPoints() { Latitude = 48.7985, Longitude = -62.88205 }); - poligon.Add(new FrontierPoints() { Latitude = 48.76513, Longitude = -129.31329 }); - poligon.Add(new FrontierPoints() { Latitude = 30.12256, Longitude = -120.9496 }); - poligon.Add(new FrontierPoints() { Latitude = 30.87114, Longitude = -111.39442 }); - poligon.Add(new FrontierPoints() { Latitude = 24.24979, Longitude = -78.11975 }); + poligon.Add(new FrontierPoints(48.7985, -62.88205)); + poligon.Add(new FrontierPoints(48.76513, -129.3132)); + poligon.Add(new FrontierPoints(30.12256, -120.9496)); + poligon.Add(new FrontierPoints(30.87114, -111.3944)); + poligon.Add(new FrontierPoints(24.24979, -78.11975)); return poligon; } static List GetWashingtonPoligon() { var poligon = new List(); - poligon.Add(new FrontierPoints() { Latitude = 48.8943, Longitude = -124.68633 }); - poligon.Add(new FrontierPoints() { Latitude = 45.66613, Longitude = -124.32962 }); - poligon.Add(new FrontierPoints() { Latitude = 45.93384, Longitude = -116.73824 }); - poligon.Add(new FrontierPoints() { Latitude = 49.04282, Longitude = -116.96912 }); + poligon.Add(new FrontierPoints(48.8943, -124.68633)); + poligon.Add(new FrontierPoints(45.66613, -124.32962)); + poligon.Add(new FrontierPoints(45.93384, -116.73824)); + poligon.Add(new FrontierPoints(49.04282, -116.96912)); return poligon; } static List GetSeattlePoligon() { var poligon = new List(); - poligon.Add(new FrontierPoints() { Latitude = 47.82929, Longitude = -122.36238 }); - poligon.Add(new FrontierPoints() { Latitude = 47.6337, Longitude = -122.42091 }); - poligon.Add(new FrontierPoints() { Latitude = 47.45224, Longitude = -122.37371 }); - poligon.Add(new FrontierPoints() { Latitude = 47.50259, Longitude = -122.20788 }); - poligon.Add(new FrontierPoints() { Latitude = 47.73644, Longitude = -122.26934 }); + poligon.Add(new FrontierPoints(47.82929, -122.36238)); + poligon.Add(new FrontierPoints(47.6337, -122.42091)); + poligon.Add(new FrontierPoints(47.45224, -122.37371)); + poligon.Add(new FrontierPoints(47.50259, -122.20788)); + poligon.Add(new FrontierPoints(47.73644, -122.26934)); return poligon; } static List GetRedmondPoligon() { var poligon = new List(); - poligon.Add(new FrontierPoints() { Latitude = 47.73148, Longitude = -122.15432 }); - poligon.Add(new FrontierPoints() { Latitude = 47.72559, Longitude = -122.17673 }); - poligon.Add(new FrontierPoints() { Latitude = 47.67851, Longitude = -122.16904 }); - poligon.Add(new FrontierPoints() { Latitude = 47.65036, Longitude = -122.16136 }); - poligon.Add(new FrontierPoints() { Latitude = 47.62746, Longitude = -122.15604 }); - poligon.Add(new FrontierPoints() { Latitude = 47.63463, Longitude = -122.01562 }); - poligon.Add(new FrontierPoints() { Latitude = 47.74244, Longitude = -122.04961 }); + poligon.Add(new FrontierPoints(47.73148, -122.15432)); + poligon.Add(new FrontierPoints(47.72559, -122.17673)); + poligon.Add(new FrontierPoints(47.67851, -122.16904)); + poligon.Add(new FrontierPoints(47.65036, -122.16136)); + poligon.Add(new FrontierPoints(47.62746, -122.15604)); + poligon.Add(new FrontierPoints(47.63463, -122.01562)); + poligon.Add(new FrontierPoints(47.74244, -122.04961)); return poligon; } static List GetSeattlePioneerSqrPoligon() { var poligon = new List(); - poligon.Add(new FrontierPoints() { Latitude = 47.60338, Longitude = -122.3327 }); - poligon.Add(new FrontierPoints() { Latitude = 47.60192, Longitude = -122.33665 }); - poligon.Add(new FrontierPoints() { Latitude = 47.60096, Longitude = -122.33456 }); - poligon.Add(new FrontierPoints() { Latitude = 47.60136, Longitude = -122.33186 }); + poligon.Add(new FrontierPoints(47.60338, -122.3327)); + poligon.Add(new FrontierPoints(47.60192, -122.33665)); + poligon.Add(new FrontierPoints(47.60096, -122.33456)); + poligon.Add(new FrontierPoints(47.60136, -122.33186)); return poligon; } static List GetRedmondDowntownParkPoligon() { var poligon = new List(); - poligon.Add(new FrontierPoints() { Latitude = 47.67595, Longitude = -122.12467 }); - poligon.Add(new FrontierPoints() { Latitude = 47.67449, Longitude = -122.12862 }); - poligon.Add(new FrontierPoints() { Latitude = 47.67353, Longitude = -122.12653 }); - poligon.Add(new FrontierPoints() { Latitude = 47.67368, Longitude = -122.12197 }); + poligon.Add(new FrontierPoints(47.67595, -122.12467)); + poligon.Add(new FrontierPoints(47.67449, -122.12862)); + poligon.Add(new FrontierPoints(47.67353, -122.12653)); + poligon.Add(new FrontierPoints(47.67368, -122.12197)); return poligon; } } diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.Designer.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.Designer.cs similarity index 66% rename from src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.Designer.cs rename to src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.Designer.cs index 0623edc4e..441ec1d82 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.Designer.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.Designer.cs @@ -8,22 +8,23 @@ using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure; namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migrations { [DbContext(typeof(LocationsContext))] - [Migration("20170529174524_Initial")] + [Migration("20170601140634_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { modelBuilder .HasAnnotation("ProductVersion", "1.1.2") - .HasAnnotation("SqlServer:Sequence:.frontier_hilo", "'frontier_hilo', '', '1', '10', '', '', 'Int64', 'False'") - .HasAnnotation("SqlServer:Sequence:.locations_hilo", "'locations_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.frontier_seq", "'frontier_seq', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.locations_seq", "'locations_seq', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.UserLocation_seq", "'UserLocation_seq', '', '1', '10', '', '', 'Int64', 'False'") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "frontier_hilo") + .HasAnnotation("SqlServer:HiLoSequenceName", "frontier_seq") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("Latitude"); @@ -44,7 +45,7 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr { b.Property("Id") .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "locations_hilo") + .HasAnnotation("SqlServer:HiLoSequenceName", "locations_seq") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("Code") @@ -68,10 +69,31 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr b.ToTable("Locations"); }); + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "UserLocation_seq") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("LocationId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserLocation"); + }); + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => { b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") - .WithMany("FrontierPoints") + .WithMany("Polygon") .HasForeignKey("LocationId") .OnDelete(DeleteBehavior.Cascade); }); @@ -82,6 +104,13 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr .WithMany("ChildLocations") .HasForeignKey("ParentId"); }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId"); + }); } } } diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.cs similarity index 72% rename from src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.cs rename to src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.cs index d2352312f..3073ca8ce 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170529174524_Initial.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.cs @@ -10,11 +10,15 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateSequence( - name: "frontier_hilo", + name: "frontier_seq", incrementBy: 10); migrationBuilder.CreateSequence( - name: "locations_hilo", + name: "locations_seq", + incrementBy: 10); + + migrationBuilder.CreateSequence( + name: "UserLocation_seq", incrementBy: 10); migrationBuilder.CreateTable( @@ -59,6 +63,25 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "UserLocation", + columns: table => new + { + Id = table.Column(nullable: false), + LocationId = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserLocation", x => x.Id); + table.ForeignKey( + name: "FK_UserLocation_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + migrationBuilder.CreateIndex( name: "IX_FrontierPoints_LocationId", table: "FrontierPoints", @@ -69,8 +92,18 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr table: "Locations", column: "ParentId"); + migrationBuilder.CreateIndex( + name: "IX_UserLocation_LocationId", + table: "UserLocation", + column: "LocationId"); + + migrationBuilder.CreateIndex( + name: "IX_UserLocation_UserId", + table: "UserLocation", + column: "UserId", + unique: true); + migrationBuilder.Sql(CreateGetDistanceFunction()); - migrationBuilder.Sql(CreateLocationsNearSP()); } protected override void Down(MigrationBuilder migrationBuilder) @@ -78,33 +111,22 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr migrationBuilder.DropTable( name: "FrontierPoints"); + migrationBuilder.DropTable( + name: "UserLocation"); + migrationBuilder.DropTable( name: "Locations"); migrationBuilder.DropSequence( - name: "frontier_hilo"); + name: "frontier_seq"); migrationBuilder.DropSequence( - name: "locations_hilo"); + name: "locations_seq"); - migrationBuilder.Sql(@"DROP PROCEDURE IF EXISTS pLocationsNear"); - migrationBuilder.Sql(@"DROP FUNCTION IF EXISTS dbo.GetDistanceFromLocation"); - } + migrationBuilder.DropSequence( + name: "UserLocation_seq"); - private string CreateLocationsNearSP() - { - var sb = new StringBuilder(); - sb.AppendLine(@"CREATE PROCEDURE [dbo].[pLocationsNear]"); - sb.AppendLine(@"@latitude float,"); - sb.AppendLine(@"@longitude float,"); - sb.AppendLine(@"@size int = 500"); - sb.AppendLine(@"AS"); - sb.AppendLine(@"BEGIN"); - sb.AppendLine(@"SELECT TOP( @size) location.*"); - sb.AppendLine(@"FROM [dbo].[Locations] AS location"); - sb.AppendLine(@"ORDER BY dbo.[GetDistanceFromLocation](location.Latitude, location.Longitude, @latitude, @longitude)"); - sb.AppendLine(@"END"); - return sb.ToString(); + migrationBuilder.Sql(@"DROP FUNCTION IF EXISTS dbo.GetDistanceFromLocation"); } private string CreateGetDistanceFunction() @@ -125,5 +147,6 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr sb.AppendLine(@"END"); return sb.ToString(); } + } } diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs index 02f77cdd0..c01237ab2 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs @@ -14,15 +14,16 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr { modelBuilder .HasAnnotation("ProductVersion", "1.1.2") - .HasAnnotation("SqlServer:Sequence:.frontier_hilo", "'frontier_hilo', '', '1', '10', '', '', 'Int64', 'False'") - .HasAnnotation("SqlServer:Sequence:.locations_hilo", "'locations_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.frontier_seq", "'frontier_seq', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.locations_seq", "'locations_seq', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.UserLocation_seq", "'UserLocation_seq', '', '1', '10', '', '', 'Int64', 'False'") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "frontier_hilo") + .HasAnnotation("SqlServer:HiLoSequenceName", "frontier_seq") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("Latitude"); @@ -43,7 +44,7 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr { b.Property("Id") .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "locations_hilo") + .HasAnnotation("SqlServer:HiLoSequenceName", "locations_seq") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("Code") @@ -67,10 +68,31 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr b.ToTable("Locations"); }); + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "UserLocation_seq") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); + + b.Property("LocationId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserLocation"); + }); + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => { b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") - .WithMany("FrontierPoints") + .WithMany("Polygon") .HasForeignKey("LocationId") .OnDelete(DeleteBehavior.Cascade); }); @@ -81,6 +103,13 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migr .WithMany("ChildLocations") .HasForeignKey("ParentId"); }); + + modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation", b => + { + b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") + .WithMany() + .HasForeignKey("LocationId"); + }); } } } diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs new file mode 100644 index 000000000..bd3b58cb2 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs @@ -0,0 +1,17 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories +{ + using Microsoft.eShopOnContainers.Services.Locations.API.Model; + using System.Collections.Generic; + using System.Threading.Tasks; + + public interface ILocationsRepository : IRepository + { + UserLocation Add(UserLocation order); + + void Update(UserLocation order); + + Task GetAsync(int userId); + + Task> GetNearestLocationListAsync(double lat, double lon); + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/IRepository.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/IRepository.cs new file mode 100644 index 000000000..946cf489c --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Repositories/IRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories +{ + public interface IRepository + { + IUnitOfWork UnitOfWork { get; } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/IUnitOfWork.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/IUnitOfWork.cs new file mode 100644 index 000000000..a81ac95e1 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Repositories/IUnitOfWork.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories +{ + public interface IUnitOfWork : IDisposable + { + Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs new file mode 100644 index 000000000..98cb501b9 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs @@ -0,0 +1,60 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories +{ + using System; + using System.Threading.Tasks; + using Microsoft.eShopOnContainers.Services.Locations.API.Model; + using Microsoft.EntityFrameworkCore; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + + public class LocationsRepository + : ILocationsRepository + { + private readonly LocationsContext _context; + + public IUnitOfWork UnitOfWork + { + get + { + return _context; + } + } + + public LocationsRepository(LocationsContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public UserLocation Add(UserLocation location) + { + return _context.UserLocation.Add(location).Entity; + + } + + public async Task GetAsync(int userId) + { + return await _context.UserLocation + .Where(ul => ul.UserId == userId) + .SingleOrDefaultAsync(); + } + + public async Task> GetNearestLocationListAsync(double lat, double lon) + { + var query = $"SELECT TOP(100) location.* " + + $"FROM[dbo].[Locations] AS location " + + $"ORDER BY [dbo].[GetDistanceFromLocation](location.Latitude, location.Longitude, " + + $"{lat.ToString(CultureInfo.InvariantCulture)}, " + + $"{lon.ToString(CultureInfo.InvariantCulture)})"; + + return await _context.Locations.FromSql(query) + .Include(f => f.Polygon) + .ToListAsync(); + } + + public void Update(UserLocation location) + { + _context.Entry(location).State = EntityState.Modified; + } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Services/IIdentityService.cs b/src/Services/Location/Locations.API/Infrastructure/Services/IIdentityService.cs new file mode 100644 index 000000000..d49abe111 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Services/IIdentityService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services +{ + public interface IIdentityService + { + string GetUserIdentity(); + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs b/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs new file mode 100644 index 000000000..672dd2703 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs @@ -0,0 +1,10 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services +{ + using Microsoft.eShopOnContainers.Services.Locations.API.ViewModel; + using System.Threading.Tasks; + + public interface ILocationsService + { + Task AddOrUpdateUserLocation(string userId, LocationRequest locRequest); + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Services/IdentityService.cs b/src/Services/Location/Locations.API/Infrastructure/Services/IdentityService.cs new file mode 100644 index 000000000..bcb7a84f8 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Services/IdentityService.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services +{ + public class IdentityService : IIdentityService + { + private IHttpContextAccessor _context; + + public IdentityService(IHttpContextAccessor context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public string GetUserIdentity() + { + return _context.HttpContext.User.FindFirst("sub").Value; + } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs b/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs new file mode 100644 index 000000000..d2b721399 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs @@ -0,0 +1,62 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services +{ + using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories; + using Microsoft.eShopOnContainers.Services.Locations.API.ViewModel; + using Microsoft.eShopOnContainers.Services.Locations.API.Model; + using System; + using System.Threading.Tasks; + + public class LocationsService : ILocationsService + { + private ILocationsRepository _locationsRepository; + + public LocationsService(ILocationsRepository locationsRepository) + { + _locationsRepository = locationsRepository ?? throw new ArgumentNullException(nameof(locationsRepository)); + } + + public async Task AddOrUpdateUserLocation(string id, LocationRequest currentPosition) + { + int.TryParse(id, out int userId); + var currentUserLocation = await _locationsRepository.GetAsync(userId); + + // Get the nearest locations ordered by proximity + var nearestLocationList = await _locationsRepository.GetNearestLocationListAsync(currentPosition.Latitude, currentPosition.Longitude); + + // Check out in which region we currently are + foreach(var locCandidate in nearestLocationList) + { + // Check location's tree and retrive user most specific area + var findNewLocationResult = locCandidate.GetUserMostSpecificLocation(currentPosition.Latitude, currentPosition.Longitude); + if (findNewLocationResult.isSuccess) + { + CreateUserLocation(currentUserLocation, findNewLocationResult.location, userId); + UpdateUserLocation(currentUserLocation, findNewLocationResult.location); + break; + } + } + + var result = await _locationsRepository.UnitOfWork.SaveChangesAsync(); + return result > 0; + } + + private void CreateUserLocation(UserLocation currentUserLocation, Locations newLocation, int userId) + { + if (currentUserLocation is null) + { + currentUserLocation = currentUserLocation ?? new UserLocation(userId); + currentUserLocation.Location = newLocation; + _locationsRepository.Add(currentUserLocation); + } + } + + private void UpdateUserLocation(UserLocation currentUserLocation, Locations newLocation) + { + if (currentUserLocation != null) + { + currentUserLocation.Location = newLocation; + _locationsRepository.Update(currentUserLocation); + } + } + } +} diff --git a/src/Services/Location/Locations.API/Locations.API.csproj b/src/Services/Location/Locations.API/Locations.API.csproj index b00054921..34808c5e0 100644 --- a/src/Services/Location/Locations.API/Locations.API.csproj +++ b/src/Services/Location/Locations.API/Locations.API.csproj @@ -7,6 +7,7 @@ aspnet-Locations.API-20161122013619 + diff --git a/src/Services/Location/Locations.API/Model/FrontierPoints.cs b/src/Services/Location/Locations.API/Model/FrontierPoints.cs index 4a9112e5e..e9061a88f 100644 --- a/src/Services/Location/Locations.API/Model/FrontierPoints.cs +++ b/src/Services/Location/Locations.API/Model/FrontierPoints.cs @@ -3,9 +3,19 @@ public class FrontierPoints { public int Id { get; set; } - public double Latitude { get; set; } - public double Longitude { get; set; } + public double Latitude { get; private set; } + public double Longitude { get; private set; } - public Locations Location { get; set; } + public FrontierPoints() + { + } + + public FrontierPoints(double latitude, double longitude) + { + Latitude = latitude; + Longitude = longitude; + } + + public Locations Location { get; private set; } } } \ No newline at end of file diff --git a/src/Services/Location/Locations.API/Model/Locations.cs b/src/Services/Location/Locations.API/Model/Locations.cs index ec21ac34f..71ffb603e 100644 --- a/src/Services/Location/Locations.API/Model/Locations.cs +++ b/src/Services/Location/Locations.API/Model/Locations.cs @@ -7,27 +7,59 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Model { public class Locations { - public int Id { get; set; } - public string Code { get; set; } - public int? ParentId { get; set; } - public string Description { get; set; } - public double Latitude { get; set; } - public double Longitude { get; set; } - public bool IsPointInPolygon(double lat, double lon) => CheckIsPointInPolygon(lat, lon); + public int Id { get; private set; } + public string Code { get; private set; } + public int? ParentId { get; private set; } + public string Description { get; private set; } + public double Latitude { get; private set; } + public double Longitude { get; private set; } + public (Locations location, bool isSuccess) GetUserMostSpecificLocation(double userLatitude, double userLongitude) + => CheckUserMostSpecificLocation(userLatitude, userLongitude); public Locations() { ChildLocations = new List(); } + public Locations(string code, string description, double latitude, double longitude, List polygon = null) : this() + { + Code = code; + Description = description; + Latitude = latitude; + Longitude = longitude; + Polygon = polygon ?? new List(); + } + public virtual List Polygon { get; set; } [ForeignKey("ParentId")] public virtual List ChildLocations { get; set; } + private (Locations location, bool isSuccess) CheckUserMostSpecificLocation(double userLatitude, double userLongitude, Locations location = null) + { + Locations result = this; + var childLocations = location != null ? location.ChildLocations : ChildLocations; + + // Check if user is in location's area, if not then returns false + if (!CheckIsPointInPolygon(userLatitude, userLongitude)) + { + return (this, false); + } + + foreach (var childLocation in childLocations) + { + result = childLocation; + + if (childLocation.ChildLocations.Count == 0){ break; } + + CheckUserMostSpecificLocation(userLatitude, userLongitude, childLocation); + } + return (result, true); + } private bool CheckIsPointInPolygon(double lat, double lon) { + if(Polygon.Count == 0) { return false; }; double minX = Polygon[0].Latitude; double maxX = Polygon[0].Latitude; double minY = Polygon[0].Longitude; diff --git a/src/Services/Location/Locations.API/Model/UserLocation.cs b/src/Services/Location/Locations.API/Model/UserLocation.cs new file mode 100644 index 000000000..f26196ace --- /dev/null +++ b/src/Services/Location/Locations.API/Model/UserLocation.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Model +{ + public class UserLocation + { + public int Id { get; set; } + public int UserId { get; set; } + + public UserLocation() + { + } + + public UserLocation(int userId) : this() + { + UserId = userId; + } + + public Locations Location { get; set; } + } +} diff --git a/src/Services/Location/Locations.API/Startup.cs b/src/Services/Location/Locations.API/Startup.cs index 1a42679a6..c13117b87 100644 --- a/src/Services/Location/Locations.API/Startup.cs +++ b/src/Services/Location/Locations.API/Startup.cs @@ -8,6 +8,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Reflection; using System; +using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services; +using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories; +using Microsoft.AspNetCore.Http; namespace Microsoft.eShopOnContainers.Services.Locations.API { @@ -75,6 +78,11 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API .AllowAnyHeader() .AllowCredentials()); }); + + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Services/Location/Locations.API/ViewModel/LocationRequest.cs b/src/Services/Location/Locations.API/ViewModel/LocationRequest.cs new file mode 100644 index 000000000..8619bfc0f --- /dev/null +++ b/src/Services/Location/Locations.API/ViewModel/LocationRequest.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.ViewModel +{ + public class LocationRequest + { + public double Longitude { get; set; } + public double Latitude { get; set; } + } +} diff --git a/test/Services/IntegrationTests/IntegrationTests.csproj b/test/Services/IntegrationTests/IntegrationTests.csproj index 60911dd4d..0306f2005 100644 --- a/test/Services/IntegrationTests/IntegrationTests.csproj +++ b/test/Services/IntegrationTests/IntegrationTests.csproj @@ -13,6 +13,7 @@ + @@ -20,6 +21,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -28,6 +32,7 @@ + diff --git a/test/Services/IntegrationTests/Services/Locations/LocationsScenarioBase.cs b/test/Services/IntegrationTests/Services/Locations/LocationsScenarioBase.cs new file mode 100644 index 000000000..b9d3af44c --- /dev/null +++ b/test/Services/IntegrationTests/Services/Locations/LocationsScenarioBase.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace IntegrationTests.Services.Locations +{ + public class LocationsScenarioBase + { + public TestServer CreateServer() + { + var webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Locations"); + webHostBuilder.UseStartup(); + + return new TestServer(webHostBuilder); + } + + public static class Get + { + public static string Locations = "api/v1/locations"; + + public static string LocationBy(int id) + { + return $"api/v1/locations/{id}"; + } + } + + public static class Post + { + public static string AddNewLocation = "api/v1/locations/"; + } + } +} diff --git a/test/Services/IntegrationTests/Services/Locations/LocationsScenarios.cs b/test/Services/IntegrationTests/Services/Locations/LocationsScenarios.cs new file mode 100644 index 000000000..0c177d6f4 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Locations/LocationsScenarios.cs @@ -0,0 +1,37 @@ +using Microsoft.eShopOnContainers.Services.Locations.API.ViewModel; +using Newtonsoft.Json; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace IntegrationTests.Services.Locations +{ + public class LocationsScenarios + : LocationsScenarioBase + { + [Fact] + public async Task Set_new_user_location_response_ok_status_code() + { + using (var server = CreateServer()) + { + var content = new StringContent(BuildLocationsRequest(), UTF8Encoding.UTF8, "application/json"); + + var response = await server.CreateClient() + .PostAsync(Post.AddNewLocation, content); + + response.EnsureSuccessStatusCode(); + } + } + + string BuildLocationsRequest() + { + var location = new LocationRequest() + { + Longitude = -122.333875, + Latitude = 47.602050 + }; + return JsonConvert.SerializeObject(location); + } + } +} diff --git a/test/Services/IntegrationTests/Services/Locations/LocationsTestsStartup.cs b/test/Services/IntegrationTests/Services/Locations/LocationsTestsStartup.cs new file mode 100644 index 000000000..0e622d854 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Locations/LocationsTestsStartup.cs @@ -0,0 +1,26 @@ +namespace IntegrationTests.Services.Locations +{ + using IntegrationTests.Middleware; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.eShopOnContainers.Services.Locations.API; + + public class LocationsTestsStartup : Startup + { + public LocationsTestsStartup(IHostingEnvironment env) : base(env) + { + } + + protected override void ConfigureAuth(IApplicationBuilder app) + { + if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) + { + app.UseMiddleware(); + } + else + { + base.ConfigureAuth(app); + } + } + } +} diff --git a/test/Services/IntegrationTests/Services/Locations/appsettings.json b/test/Services/IntegrationTests/Services/Locations/appsettings.json new file mode 100644 index 000000000..c31d6c9f1 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Locations/appsettings.json @@ -0,0 +1,7 @@ +{ + "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.LocationsDb;User Id=sa;Password=Pass@word;", + "ExternalCatalogBaseUrl": "http://localhost:5101", + "IdentityUrl": "http://localhost:5105", + "isTest": "true", + "EventBusConnection": "localhost" +} From f993c8c33dd7201678d417d0f513dfecc8a22ceb Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Thu, 1 Jun 2017 20:26:10 +0200 Subject: [PATCH 07/36] Remove unused class --- src/Services/Marketing/Marketing.API/Model/Location.cs | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/Services/Marketing/Marketing.API/Model/Location.cs diff --git a/src/Services/Marketing/Marketing.API/Model/Location.cs b/src/Services/Marketing/Marketing.API/Model/Location.cs deleted file mode 100644 index e3973f425..000000000 --- a/src/Services/Marketing/Marketing.API/Model/Location.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using System.Collections.Generic; -namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model -{ - public class Location - { - public int Id { get; set; } - } -} From 83b229f4a1c274dedc92dd321d24dfa9ade4c9b2 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Thu, 1 Jun 2017 22:13:19 +0200 Subject: [PATCH 08/36] rename file name --- .../Marketing/Marketing.API/Model/{Campaing.cs => Campaign.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Services/Marketing/Marketing.API/Model/{Campaing.cs => Campaign.cs} (100%) diff --git a/src/Services/Marketing/Marketing.API/Model/Campaing.cs b/src/Services/Marketing/Marketing.API/Model/Campaign.cs similarity index 100% rename from src/Services/Marketing/Marketing.API/Model/Campaing.cs rename to src/Services/Marketing/Marketing.API/Model/Campaign.cs From 8494e776872cca3f66ab634fa5e93129484c1841 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Thu, 1 Jun 2017 22:19:07 +0200 Subject: [PATCH 09/36] add version 1 to Campaigns web api --- .../Marketing/Marketing.API/Controllers/CampaignsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index b029bfa51..a70bc5823 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -6,7 +6,7 @@ using Microsoft.eShopOnContainers.Services.Marketing.API.Model; using Microsoft.EntityFrameworkCore; - [Route("api/[controller]")] + [Route("api/v1/[controller]")] public class CampaignsController : Controller { private readonly MarketingContext _context; From a5aea54cc8e58fb13fb193f08529d7a4d2a92764 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:30:39 +0200 Subject: [PATCH 10/36] Add dtos --- .../Controllers/CampaignsController.cs | 120 ++++++++++++++++-- .../Marketing.API/Dto/CampaignDTO.cs | 25 ++++ .../Marketing/Marketing.API/Dto/RuleDTO.cs | 19 +++ 3 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs create mode 100644 src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index b029bfa51..6259b69ca 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -5,8 +5,11 @@ 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; - [Route("api/[controller]")] + [Route("api/v1/[controller]")] + //[Authorize] public class CampaignsController : Controller { private readonly MarketingContext _context; @@ -16,6 +19,7 @@ _context = context; } + [HttpGet] public async Task GetAllCampaigns() { @@ -23,7 +27,9 @@ .Include(c => c.Rules) .ToListAsync(); - return Ok(campaignList); + var campaignDtoList = CampaignModelListToDtoList(campaignList); + + return Ok(campaignDtoList); } [HttpGet("{id:int}")] @@ -31,30 +37,44 @@ { var campaign = await _context.Campaigns .Include(c => c.Rules) - .SingleAsync(c => c.Id == id); + .SingleOrDefaultAsync(c => c.Id == id); if (campaign is null) { return NotFound(); } - return Ok(campaign); + var campaignDto = CampaignModelToDto(campaign); + + return Ok(campaignDto); } [HttpPost] - public async Task CreateCampaign([FromBody] Campaign campaign) + public async Task CreateCampaign([FromBody] CampaignDTO campaign) { - await _context.Campaigns.AddAsync(campaign); + if (campaign is null) + { + return BadRequest(); + } + + var campaingToCreate = CampaignDtoToModel(campaign); + + await _context.Campaigns.AddAsync(campaingToCreate); await _context.SaveChangesAsync(); - return CreatedAtAction(nameof(GetCampaignById), new { id = campaign.Id }, null); + return CreatedAtAction(nameof(GetCampaignById), new { id = campaingToCreate.Id }, null); } [HttpPut("{id:int}")] - public async Task UpdateCampaign(int id, [FromBody]Campaign campaign) + public async Task UpdateCampaign(int id, [FromBody]CampaignDTO campaign) { + if (id < 1 || campaign is null) + { + return BadRequest(); + } + var campaignToUpdate = await _context.Campaigns.FindAsync(id); - if (campaign is null) + if (campaignToUpdate is null) { return NotFound(); } @@ -71,16 +91,92 @@ [HttpDelete("{id:int}")] public async Task Delete(int id) { - var campaign = await _context.Campaigns.FindAsync(id); - if (campaign is null) + if (id < 1) + { + return BadRequest(); + } + + var campaignToDelete = await _context.Campaigns.FindAsync(id); + if (campaignToDelete is null) { return NotFound(); } - _context.Campaigns.Remove(campaign); + _context.Campaigns.Remove(campaignToDelete); await _context.SaveChangesAsync(); return NoContent(); } + + + + private List CampaignModelListToDtoList(List campaignList) + { + var campaignDtoList = new List(); + + campaignList.ForEach(campaign => campaignDtoList + .Add(CampaignModelToDto(campaign))); + + return campaignDtoList; + } + + private CampaignDTO CampaignModelToDto(Campaign campaign) + { + var campaignDto = new CampaignDTO + { + Description = campaign.Description, + From = campaign.From, + To = campaign.To, + Url = campaign.Url, + }; + + campaign.Rules.ForEach(c => + { + switch ((RuleTypeEnum)c.RuleTypeId) + { + case RuleTypeEnum.UserLocationRule: + var userLocationRule = c as UserLocationRule; + campaignDto.Rules.Add(new RuleDTO + { + LocationId = userLocationRule.LocationId, + RuleTypeId = userLocationRule.RuleTypeId, + Description = userLocationRule.Description + }); + break; + } + }); + + return campaignDto; + } + + private Campaign CampaignDtoToModel(CampaignDTO campaignDto) + { + var campaingModel = new Campaign + { + Description = campaignDto.Description, + From = campaignDto.From, + To = campaignDto.To, + Url = campaignDto.Url + }; + + campaignDto.Rules.ForEach(c => + { + switch (c.RuleType) + { + case RuleTypeEnum.UserLocationRule: + campaingModel.Rules.Add(new UserLocationRule + { + LocationId = c.LocationId.Value, + RuleTypeId = (int)c.RuleType, + Description = c.Description, + Campaign = campaingModel + }); + break; + } + }); + + return campaingModel; + } + } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs new file mode 100644 index 000000000..b5676db63 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs @@ -0,0 +1,25 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto +{ + using System; + using System.Collections.Generic; + + public class CampaignDTO + { + public int Id { get; set; } + + public string Description { get; set; } + + public DateTime From { get; set; } + + public DateTime To { get; set; } + + public string Url { get; set; } + + public List Rules { get; set; } + + public CampaignDTO() + { + Rules = new List(); + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs b/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs new file mode 100644 index 000000000..6a1d9aef6 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs @@ -0,0 +1,19 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto +{ + public class RuleDTO + { + public int Id { get; set; } + + public RuleTypeEnum RuleType => (RuleTypeEnum) RuleTypeId; + + public int RuleTypeId { get; set; } + + public int CampaignId { get; set; } + + public int? LocationId { get; set; } + + public string Description { get; set; } + } + + public enum RuleTypeEnum { UserProfileRule = 1, PurchaseHistoryRule = 2, UserLocationRule = 3 } +} \ No newline at end of file From 1bd13747f47d7ed375b636fbe01d14f4b9f6775a Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:31:03 +0200 Subject: [PATCH 11/36] Add exceptions filters --- .../InternalServerErrorObjectResult.cs | 14 ++++ .../Exceptions/MarketingDomainException.cs | 21 ++++++ .../Filters/HttpGlobalExceptionFilter.cs | 67 +++++++++++++++++++ .../Marketing/Marketing.API/Startup.cs | 6 +- 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/Services/Marketing/Marketing.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs create mode 100644 src/Services/Marketing/Marketing.API/Infrastructure/Exceptions/MarketingDomainException.cs create mode 100644 src/Services/Marketing/Marketing.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs b/src/Services/Marketing/Marketing.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs new file mode 100644 index 000000000..5975d92af --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/ActionResult/InternalServerErrorObjectResult.cs @@ -0,0 +1,14 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.ActionResults +{ + using AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + + public class InternalServerErrorObjectResult : ObjectResult + { + public InternalServerErrorObjectResult(object error) + : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/Exceptions/MarketingDomainException.cs b/src/Services/Marketing/Marketing.API/Infrastructure/Exceptions/MarketingDomainException.cs new file mode 100644 index 000000000..68ccd9e52 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/Exceptions/MarketingDomainException.cs @@ -0,0 +1,21 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions +{ + using System; + + /// + /// Exception type for app exceptions + /// + public class MarketingDomainException : Exception + { + public MarketingDomainException() + { } + + public MarketingDomainException(string message) + : base(message) + { } + + public MarketingDomainException(string message, Exception innerException) + : base(message, innerException) + { } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Marketing/Marketing.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs new file mode 100644 index 000000000..14e79775d --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -0,0 +1,67 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Filters +{ + using AspNetCore.Mvc; + using global::Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc.Filters; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.ActionResults; + using Microsoft.Extensions.Logging; + using System.Net; + + public class HttpGlobalExceptionFilter : IExceptionFilter + { + private readonly IHostingEnvironment env; + private readonly ILogger logger; + + public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger logger) + { + this.env = env; + this.logger = logger; + } + + public void OnException(ExceptionContext context) + { + logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(MarketingDomainException)) + { + var json = new JsonErrorResponse + { + Messages = new[] { context.Exception.Message } + }; + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + //It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new BadRequestObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An error occur.Try it again." } + }; + + if (env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + // It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } + + private class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Startup.cs b/src/Services/Marketing/Marketing.API/Startup.cs index 378ba214c..9609f903f 100644 --- a/src/Services/Marketing/Marketing.API/Startup.cs +++ b/src/Services/Marketing/Marketing.API/Startup.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using System.Reflection; using System; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Filters; public class Startup { @@ -37,7 +38,10 @@ public void ConfigureServices(IServiceCollection services) { // Add framework services. - services.AddMvc(); + services.AddMvc(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + }).AddControllersAsServices(); //Injecting Controllers themselves thru DIFor further info see: http://docs.autofac.org/en/latest/integration/aspnetcore.html#controllers-as-services services.AddDbContext(options => { From c132d944d7bed527b06adc2069b5a2a1eb0b662d Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:31:35 +0200 Subject: [PATCH 12/36] update inital migration --- .../20170602122539_Initial.Designer.cs} | 13 ++++++---- .../20170602122539_Initial.cs} | 25 +++++++++++-------- .../MarketingContextModelSnapshot.cs | 11 +++++--- 3 files changed, 30 insertions(+), 19 deletions(-) rename src/Services/Marketing/Marketing.API/{Migrations/20170601175200_Initial.Designer.cs => Infrastructure/MarketingMigrations/20170602122539_Initial.Designer.cs} (86%) rename src/Services/Marketing/Marketing.API/{Migrations/20170601175200_Initial.cs => Infrastructure/MarketingMigrations/20170602122539_Initial.cs} (77%) rename src/Services/Marketing/Marketing.API/{Migrations => Infrastructure/MarketingMigrations}/MarketingContextModelSnapshot.cs (87%) diff --git a/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.Designer.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.Designer.cs similarity index 86% rename from src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.Designer.cs rename to src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.Designer.cs index b0305f171..5a5a6b66d 100644 --- a/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.Designer.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.Designer.cs @@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; -namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations { [DbContext(typeof(MarketingContext))] - [Migration("20170601175200_Initial")] + [Migration("20170602122539_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -16,6 +16,7 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations modelBuilder .HasAnnotation("ProductVersion", "1.1.2") .HasAnnotation("SqlServer:Sequence:.campaign_hilo", "'campaign_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.rule_hilo", "'rule_hilo', '', '1', '10', '', '', 'Int64', 'False'") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", b => @@ -45,7 +46,9 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "rule_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("CampaignId"); @@ -59,7 +62,7 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations b.HasIndex("CampaignId"); - b.ToTable("Rules"); + b.ToTable("Rule"); b.HasDiscriminator("RuleTypeId"); }); @@ -98,7 +101,7 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => { - b.HasOne("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", "Campaign") + b.HasOne("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign") .WithMany("Rules") .HasForeignKey("CampaignId") .OnDelete(DeleteBehavior.Cascade); diff --git a/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.cs similarity index 77% rename from src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.cs rename to src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.cs index dee3aa12f..d33fbb3d0 100644 --- a/src/Services/Marketing/Marketing.API/Migrations/20170601175200_Initial.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/20170602122539_Initial.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Metadata; -namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations { public partial class Initial : Migration { @@ -13,6 +12,10 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations name: "campaign_hilo", incrementBy: 10); + migrationBuilder.CreateSequence( + name: "rule_hilo", + incrementBy: 10); + migrationBuilder.CreateTable( name: "Campaign", columns: table => new @@ -29,11 +32,10 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations }); migrationBuilder.CreateTable( - name: "Rules", + name: "Rule", columns: table => new { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Id = table.Column(nullable: false), CampaignId = table.Column(nullable: false), Description = table.Column(nullable: false), RuleTypeId = table.Column(nullable: false), @@ -41,9 +43,9 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations }, constraints: table => { - table.PrimaryKey("PK_Rules", x => x.Id); + table.PrimaryKey("PK_Rule", x => x.Id); table.ForeignKey( - name: "FK_Rules_Campaign_CampaignId", + name: "FK_Rule_Campaign_CampaignId", column: x => x.CampaignId, principalTable: "Campaign", principalColumn: "Id", @@ -51,21 +53,24 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations }); migrationBuilder.CreateIndex( - name: "IX_Rules_CampaignId", - table: "Rules", + name: "IX_Rule_CampaignId", + table: "Rule", column: "CampaignId"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "Rules"); + name: "Rule"); migrationBuilder.DropTable( name: "Campaign"); migrationBuilder.DropSequence( name: "campaign_hilo"); + + migrationBuilder.DropSequence( + name: "rule_hilo"); } } } diff --git a/src/Services/Marketing/Marketing.API/Migrations/MarketingContextModelSnapshot.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs similarity index 87% rename from src/Services/Marketing/Marketing.API/Migrations/MarketingContextModelSnapshot.cs rename to src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs index 3d954edcc..865daa028 100644 --- a/src/Services/Marketing/Marketing.API/Migrations/MarketingContextModelSnapshot.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingMigrations/MarketingContextModelSnapshot.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; -namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.MarketingMigrations { [DbContext(typeof(MarketingContext))] partial class MarketingContextModelSnapshot : ModelSnapshot @@ -15,6 +15,7 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations modelBuilder .HasAnnotation("ProductVersion", "1.1.2") .HasAnnotation("SqlServer:Sequence:.campaign_hilo", "'campaign_hilo', '', '1', '10', '', '', 'Int64', 'False'") + .HasAnnotation("SqlServer:Sequence:.rule_hilo", "'rule_hilo', '', '1', '10', '', '', 'Int64', 'False'") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", b => @@ -44,7 +45,9 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => { b.Property("Id") - .ValueGeneratedOnAdd(); + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:HiLoSequenceName", "rule_hilo") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); b.Property("CampaignId"); @@ -58,7 +61,7 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations b.HasIndex("CampaignId"); - b.ToTable("Rules"); + b.ToTable("Rule"); b.HasDiscriminator("RuleTypeId"); }); @@ -97,7 +100,7 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Migrations modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Rule", b => { - b.HasOne("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign", "Campaign") + b.HasOne("Microsoft.eShopOnContainers.Services.Marketing.API.Model.Campaign") .WithMany("Rules") .HasForeignKey("CampaignId") .OnDelete(DeleteBehavior.Cascade); From 3440427da30426a7f669b504781cf204ee2f3626 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:31:48 +0200 Subject: [PATCH 13/36] modify context --- .../Marketing.API/Infrastructure/MarketingContext.cs | 12 ++++++++---- .../Infrastructure/MarketingContextSeed.cs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs index 798d8d152..af6a75cf1 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs @@ -28,9 +28,9 @@ { builder.ToTable("Campaign"); - builder.HasKey(ci => ci.Id); + builder.HasKey(m => m.Id); - builder.Property(ci => ci.Id) + builder.Property(m => m.Id) .ForSqlServerUseSequenceHiLo("campaign_hilo") .IsRequired(); @@ -52,16 +52,20 @@ builder.HasMany(m => m.Rules) .WithOne(r => r.Campaign) - .HasForeignKey(m => m.CampaignId) + .HasForeignKey(r => r.CampaignId) .IsRequired(); } void ConfigureRules(EntityTypeBuilder builder) { - builder.ToTable("Rules"); + builder.ToTable("Rule"); builder.HasKey(r => r.Id); + builder.Property(r => r.Id) + .ForSqlServerUseSequenceHiLo("rule_hilo") + .IsRequired(); + builder.HasDiscriminator("RuleTypeId") .HasValue(1) .HasValue(2) diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs index 9d2bb0157..5f15a725a 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContextSeed.cs @@ -57,7 +57,7 @@ new UserLocationRule { Description = "UserLocationRule2", - LocationId = 1 + LocationId = 3 } } } From 4c59c5ebe4e095cd0221f61c0586d64f1a27d8f8 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:32:24 +0200 Subject: [PATCH 14/36] Add migration folder --- src/Services/Marketing/Marketing.API/Marketing.API.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Services/Marketing/Marketing.API/Marketing.API.csproj b/src/Services/Marketing/Marketing.API/Marketing.API.csproj index f65be06bb..bd8e5a871 100644 --- a/src/Services/Marketing/Marketing.API/Marketing.API.csproj +++ b/src/Services/Marketing/Marketing.API/Marketing.API.csproj @@ -11,6 +11,7 @@ + From 6dcc6f84bca48bb8bf52d681893b600b93106d7c Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:32:50 +0200 Subject: [PATCH 15/36] initialitze the list in constructor --- src/Services/Marketing/Marketing.API/Model/Campaing.cs | 8 +++++++- src/Services/Marketing/Marketing.API/Model/Rule.cs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Services/Marketing/Marketing.API/Model/Campaing.cs b/src/Services/Marketing/Marketing.API/Model/Campaing.cs index bbe794b1a..c628b4a72 100644 --- a/src/Services/Marketing/Marketing.API/Model/Campaing.cs +++ b/src/Services/Marketing/Marketing.API/Model/Campaing.cs @@ -15,6 +15,12 @@ public string Url { get; set; } - public ICollection Rules { get; set; } + public List Rules { get; set; } + + + public Campaign() + { + Rules = new List(); + } } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Model/Rule.cs b/src/Services/Marketing/Marketing.API/Model/Rule.cs index 38a47d3eb..936e00c36 100644 --- a/src/Services/Marketing/Marketing.API/Model/Rule.cs +++ b/src/Services/Marketing/Marketing.API/Model/Rule.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model { @@ -26,4 +26,4 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model { public int LocationId { get; set; } } -} +} \ No newline at end of file From 3e47c840d5bf4966548d7b63f40ee59d981a99d6 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:33:01 +0200 Subject: [PATCH 16/36] Add integration test --- .../IntegrationTests/IntegrationTests.csproj | 4 + .../Marketing/MarketingScenarioBase.cs | 45 ++++++ .../Services/Marketing/MarketingScenarios.cs | 132 ++++++++++++++++++ .../Marketing/MarketingTestsStartup.cs | 26 ++++ .../Services/Marketing/appsettings.json | 5 + 5 files changed, 212 insertions(+) create mode 100644 test/Services/IntegrationTests/Services/Marketing/MarketingScenarioBase.cs create mode 100644 test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs create mode 100644 test/Services/IntegrationTests/Services/Marketing/MarketingTestsStartup.cs create mode 100644 test/Services/IntegrationTests/Services/Marketing/appsettings.json diff --git a/test/Services/IntegrationTests/IntegrationTests.csproj b/test/Services/IntegrationTests/IntegrationTests.csproj index 60911dd4d..a44e57ccb 100644 --- a/test/Services/IntegrationTests/IntegrationTests.csproj +++ b/test/Services/IntegrationTests/IntegrationTests.csproj @@ -20,6 +20,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -28,6 +31,7 @@ + diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingScenarioBase.cs b/test/Services/IntegrationTests/Services/Marketing/MarketingScenarioBase.cs new file mode 100644 index 000000000..01383410b --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/MarketingScenarioBase.cs @@ -0,0 +1,45 @@ +namespace IntegrationTests.Services.Marketing +{ + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.TestHost; + using System.IO; + + public class MarketingScenarioBase + { + private const string _campaignsUrlBase = "api/v1/campaigns"; + + public TestServer CreateServer() + { + var webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Marketing"); + webHostBuilder.UseStartup(); + + return new TestServer(webHostBuilder); + } + + public static class Get + { + public static string Campaigns = _campaignsUrlBase; + + public static string CampaignBy(int id) + => $"{_campaignsUrlBase}/{id}"; + } + + 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}"; + } + } +} \ No newline at end of file diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs b/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs new file mode 100644 index 000000000..931975fb8 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs @@ -0,0 +1,132 @@ +namespace IntegrationTests.Services.Marketing +{ + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using Xunit; + using System; + using Newtonsoft.Json; + using Microsoft.eShopOnContainers.Services.Marketing.API.Model; + using System.Collections.Generic; + using System.Net; + using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; + + public class MarketingScenarios + : MarketingScenarioBase + { + [Fact] + public async Task Get_get_all_campaigns_and_response_ok_status_code() + { + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.Campaigns); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Get_get_campaign_by_id_and_response_ok_status_code() + { + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.CampaignBy(1)); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Get_get_campaign_by_id_and_response_not_found_status_code() + { + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.CampaignBy(9999999)); + + Assert.True(response.StatusCode == HttpStatusCode.NotFound); + } + } + + [Fact] + public async Task Post_add_new_campaign_and_response_ok_status_code() + { + using (var server = CreateServer()) + { + var content = new StringContent(JsonConvert.SerializeObject(FakeCampaignDto), Encoding.UTF8, "application/json"); + var response = await server.CreateClient() + .PostAsync(Post.AddNewCampaign, content); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Delete_delete_campaign_and_response_not_content_status_code() + { + using (var server = CreateServer()) + { + var content = new StringContent(JsonConvert.SerializeObject(FakeCampaignDto), Encoding.UTF8, "application/json"); + + //add campaign + var campaignResponse = await server.CreateClient() + .PostAsync(Post.AddNewCampaign, content); + + if (int.TryParse(campaignResponse.Headers.Location.Segments[4], out int id)) + { + var response = await server.CreateClient() + .DeleteAsync(Delete.CampaignBy(id)); + + Assert.True(response.StatusCode == HttpStatusCode.NoContent); + } + + campaignResponse.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Put_update_campaign_and_response_not_content_status_code() + { + using (var server = CreateServer()) + { + var content = new StringContent(JsonConvert.SerializeObject(FakeCampaignDto), Encoding.UTF8, "application/json"); + + //add campaign + var campaignResponse = await server.CreateClient() + .PostAsync(Post.AddNewCampaign, content); + + if (int.TryParse(campaignResponse.Headers.Location.Segments[4], out int id)) + { + FakeCampaignDto.Description = "FakeCampaignUpdatedDescription"; + content = new StringContent(JsonConvert.SerializeObject(FakeCampaignDto), Encoding.UTF8, "application/json"); + var response = await server.CreateClient() + .PutAsync(Put.CampaignBy(id), content); + + Assert.True(response.StatusCode == HttpStatusCode.Created); + } + + campaignResponse.EnsureSuccessStatusCode(); + } + } + + + private static CampaignDTO FakeCampaignDto = new CampaignDTO + { + Description = "FakeCampaignDescription", + From = DateTime.Now, + To = DateTime.Now.AddDays(7), + Url = "http://CampaignUrl.test/fdaf91ad0cef5419719f50198", + Rules = new List + { + new RuleDTO + { + LocationId = 1, + Description = "testDescription", + RuleTypeId = 3, + } + } + }; + } +} diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingTestsStartup.cs b/test/Services/IntegrationTests/Services/Marketing/MarketingTestsStartup.cs new file mode 100644 index 000000000..b8d337ab2 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/MarketingTestsStartup.cs @@ -0,0 +1,26 @@ +namespace IntegrationTests.Services.Marketing +{ + using Microsoft.eShopOnContainers.Services.Marketing.API; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Builder; + using IntegrationTests.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(); + } + else + { + base.ConfigureAuth(app); + } + } + } +} diff --git a/test/Services/IntegrationTests/Services/Marketing/appsettings.json b/test/Services/IntegrationTests/Services/Marketing/appsettings.json new file mode 100644 index 000000000..8e3e07891 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=Pass@word", + "IdentityUrl": "http://localhost:5105", + "isTest": "true" +} From 82182e3184dd5dd4df4b117151c042d2bdd15b24 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:35:21 +0200 Subject: [PATCH 17/36] cleanup --- src/Services/Marketing/Marketing.API/Model/Rule.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Services/Marketing/Marketing.API/Model/Rule.cs b/src/Services/Marketing/Marketing.API/Model/Rule.cs index 936e00c36..03686bf86 100644 --- a/src/Services/Marketing/Marketing.API/Model/Rule.cs +++ b/src/Services/Marketing/Marketing.API/Model/Rule.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; - -namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Model { public abstract class Rule { From 385f1f094affa720a708f11a7b67130f8ad760e1 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 2 Jun 2017 16:36:16 +0200 Subject: [PATCH 18/36] fix error merge --- .../Marketing/Marketing.API/Controllers/CampaignsController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index 04f0d7ba9..1786bc3b7 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; using System.Collections.Generic; + using Microsoft.AspNetCore.Authorization; [Route("api/v1/[controller]")] [Authorize] From a8178fbbb4aa33c801455453d39ea66d7233ef26 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Sat, 3 Jun 2017 00:59:58 +0200 Subject: [PATCH 19/36] remove unused dbset --- .../Marketing/Marketing.API/Infrastructure/MarketingContext.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs index af6a75cf1..2fb45868b 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs @@ -13,9 +13,6 @@ public DbSet Campaigns { get; set; } public DbSet Rules { get; set; } - public DbSet UserProfileRules { get; set; } - public DbSet PurchaseHistoryRules { get; set; } - public DbSet UserLocationRules { get; set; } protected override void OnModelCreating(ModelBuilder builder) { From b5f90cf7b4abf2f9c3c02bfeece6bfb3a4c0c5e2 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Sat, 3 Jun 2017 01:00:55 +0200 Subject: [PATCH 20/36] Create RuleType class to manage RuleTypeEnum --- .../Controllers/CampaignsController.cs | 23 ++++++++++--------- .../Marketing/Marketing.API/Dto/RuleDTO.cs | 11 +++++---- .../Marketing.API/Dto/RuleTypeEnum.cs | 20 ++++++++++++++++ 3 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 src/Services/Marketing/Marketing.API/Dto/RuleTypeEnum.cs diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index 1786bc3b7..41773a78c 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -8,9 +8,10 @@ using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; + using System.Linq; [Route("api/v1/[controller]")] - [Authorize] + //[Authorize] public class CampaignsController : Controller { private readonly MarketingContext _context; @@ -27,7 +28,7 @@ .Include(c => c.Rules) .ToListAsync(); - var campaignDtoList = CampaignModelListToDtoList(campaignList); + var campaignDtoList = MapCampaignModelListToDtoList(campaignList); return Ok(campaignDtoList); } @@ -44,7 +45,7 @@ return NotFound(); } - var campaignDto = CampaignModelToDto(campaign); + var campaignDto = MapCampaignModelToDto(campaign); return Ok(campaignDto); } @@ -57,7 +58,7 @@ return BadRequest(); } - var campaingToCreate = CampaignDtoToModel(campaign); + var campaingToCreate = MapCampaignDtoToModel(campaign); await _context.Campaigns.AddAsync(campaingToCreate); await _context.SaveChangesAsync(); @@ -110,17 +111,17 @@ - private List CampaignModelListToDtoList(List campaignList) + private List MapCampaignModelListToDtoList(List campaignList) { var campaignDtoList = new List(); campaignList.ForEach(campaign => campaignDtoList - .Add(CampaignModelToDto(campaign))); + .Add(MapCampaignModelToDto(campaign))); return campaignDtoList; } - private CampaignDTO CampaignModelToDto(Campaign campaign) + private CampaignDTO MapCampaignModelToDto(Campaign campaign) { var campaignDto = new CampaignDTO { @@ -132,7 +133,7 @@ campaign.Rules.ForEach(c => { - switch ((RuleTypeEnum)c.RuleTypeId) + switch (RuleType.From(c.RuleTypeId)) { case RuleTypeEnum.UserLocationRule: var userLocationRule = c as UserLocationRule; @@ -149,7 +150,7 @@ return campaignDto; } - private Campaign CampaignDtoToModel(CampaignDTO campaignDto) + private Campaign MapCampaignDtoToModel(CampaignDTO campaignDto) { var campaingModel = new Campaign { @@ -161,13 +162,13 @@ campaignDto.Rules.ForEach(c => { - switch (c.RuleType) + switch (RuleType.From(c.RuleTypeId)) { case RuleTypeEnum.UserLocationRule: campaingModel.Rules.Add(new UserLocationRule { LocationId = c.LocationId.Value, - RuleTypeId = (int)c.RuleType, + RuleTypeId = c.RuleTypeId, Description = c.Description, Campaign = campaingModel }); diff --git a/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs b/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs index 6a1d9aef6..a79756e9c 100644 --- a/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs +++ b/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs @@ -1,11 +1,14 @@ -namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto +using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto { public class RuleDTO { public int Id { get; set; } - public RuleTypeEnum RuleType => (RuleTypeEnum) RuleTypeId; - public int RuleTypeId { get; set; } public int CampaignId { get; set; } @@ -14,6 +17,4 @@ public string Description { get; set; } } - - public enum RuleTypeEnum { UserProfileRule = 1, PurchaseHistoryRule = 2, UserLocationRule = 3 } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/RuleTypeEnum.cs b/src/Services/Marketing/Marketing.API/Dto/RuleTypeEnum.cs new file mode 100644 index 000000000..f00b7fbf9 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dto/RuleTypeEnum.cs @@ -0,0 +1,20 @@ +using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; +using System; + +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto +{ + public enum RuleTypeEnum { UserProfileRule = 1, PurchaseHistoryRule = 2, UserLocationRule = 3 } + + public static class RuleType + { + public static RuleTypeEnum From(int id) + { + if (!Enum.IsDefined(typeof(RuleTypeEnum), id)) + { + throw new MarketingDomainException($"Invalid value for RuleType, RuleTypeId: {id}"); + } + + return (RuleTypeEnum)id; + } + } +} \ No newline at end of file From ea8d893d4ecf61bea238ff62db81c5a43cf8e443 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Mon, 5 Jun 2017 19:36:09 +0200 Subject: [PATCH 21/36] change the FakeCampaignDto private object to an GetFakeCampaignDto funtion --- .../Controllers/CampaignsController.cs | 3 +- .../Services/Marketing/MarketingScenarios.cs | 41 +++++++++++-------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index 41773a78c..a525adec5 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -11,7 +11,7 @@ using System.Linq; [Route("api/v1/[controller]")] - //[Authorize] + [Authorize] public class CampaignsController : Controller { private readonly MarketingContext _context; @@ -178,6 +178,5 @@ return campaingModel; } - } } \ No newline at end of file diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs b/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs index 931975fb8..29dcbebda 100644 --- a/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs +++ b/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs @@ -55,7 +55,8 @@ { using (var server = CreateServer()) { - var content = new StringContent(JsonConvert.SerializeObject(FakeCampaignDto), Encoding.UTF8, "application/json"); + var fakeCampaignDto = GetFakeCampaignDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); var response = await server.CreateClient() .PostAsync(Post.AddNewCampaign, content); @@ -68,7 +69,8 @@ { using (var server = CreateServer()) { - var content = new StringContent(JsonConvert.SerializeObject(FakeCampaignDto), Encoding.UTF8, "application/json"); + var fakeCampaignDto = GetFakeCampaignDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); //add campaign var campaignResponse = await server.CreateClient() @@ -91,7 +93,8 @@ { using (var server = CreateServer()) { - var content = new StringContent(JsonConvert.SerializeObject(FakeCampaignDto), Encoding.UTF8, "application/json"); + var fakeCampaignDto = GetFakeCampaignDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); //add campaign var campaignResponse = await server.CreateClient() @@ -99,8 +102,8 @@ if (int.TryParse(campaignResponse.Headers.Location.Segments[4], out int id)) { - FakeCampaignDto.Description = "FakeCampaignUpdatedDescription"; - content = new StringContent(JsonConvert.SerializeObject(FakeCampaignDto), Encoding.UTF8, "application/json"); + fakeCampaignDto.Description = "FakeCampaignUpdatedDescription"; + content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); var response = await server.CreateClient() .PutAsync(Put.CampaignBy(id), content); @@ -111,22 +114,24 @@ } } - - private static CampaignDTO FakeCampaignDto = new CampaignDTO + private static CampaignDTO GetFakeCampaignDto() { - Description = "FakeCampaignDescription", - From = DateTime.Now, - To = DateTime.Now.AddDays(7), - Url = "http://CampaignUrl.test/fdaf91ad0cef5419719f50198", - Rules = new List + return new CampaignDTO() { - new RuleDTO + Description = "FakeCampaignDescription", + From = DateTime.Now, + To = DateTime.Now.AddDays(7), + Url = "http://CampaignUrl.test/fdaf91ad0cef5419719f50198", + Rules = new List { - LocationId = 1, - Description = "testDescription", - RuleTypeId = 3, + new RuleDTO + { + LocationId = 1, + Description = "testDescription", + RuleTypeId = 3, + } } - } - }; + }; + } } } From 41101164fac9d3158d754eedaa12b1ff8d2c348c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Mon, 5 Jun 2017 21:54:03 +0200 Subject: [PATCH 22/36] Persist identity grant store to db Persist machine Keys to Redis --- k8s/deployments.yaml | 1 + .../Identity.API/Configuration/Config.cs | 1 + .../DataProtectionBuilderExtensions.cs | 96 ++++ .../Extensions/RedisXmlRepository.cs | 214 +++++++ .../Identity/Identity.API/Identity.API.csproj | 1 + ...604151240_Init-persisted-grant.Designer.cs | 52 ++ .../20170604151240_Init-persisted-grant.cs | 40 ++ ...70604151338_Init-configuration.Designer.cs | 539 ++++++++++++++++++ .../20170604151338_Init-configuration.cs | 501 ++++++++++++++++ .../ConfigurationDbContextModelSnapshot.cs | 538 +++++++++++++++++ .../PersistedGrantDbContextModelSnapshot.cs | 51 ++ src/Services/Identity/Identity.API/Startup.cs | 81 ++- .../WebMVC/Controllers/AccountController.cs | 11 +- src/Web/WebMVC/Startup.cs | 4 +- .../FunctionalTests/FunctionalTests.csproj | 4 + .../IntegrationTests/IntegrationTests.csproj | 4 + test/Services/UnitTest/UnitTest.csproj | 4 + 17 files changed, 2121 insertions(+), 21 deletions(-) create mode 100644 src/Services/Identity/Identity.API/Extensions/DataProtectionBuilderExtensions.cs create mode 100644 src/Services/Identity/Identity.API/Extensions/RedisXmlRepository.cs create mode 100644 src/Services/Identity/Identity.API/Migrations/20170604151240_Init-persisted-grant.Designer.cs create mode 100644 src/Services/Identity/Identity.API/Migrations/20170604151240_Init-persisted-grant.cs create mode 100644 src/Services/Identity/Identity.API/Migrations/ConfigurationDb/20170604151338_Init-configuration.Designer.cs create mode 100644 src/Services/Identity/Identity.API/Migrations/ConfigurationDb/20170604151338_Init-configuration.cs create mode 100644 src/Services/Identity/Identity.API/Migrations/ConfigurationDb/ConfigurationDbContextModelSnapshot.cs create mode 100644 src/Services/Identity/Identity.API/Migrations/PersistedGrantDbContextModelSnapshot.cs diff --git a/k8s/deployments.yaml b/k8s/deployments.yaml index 1bb406390..a0b1df135 100644 --- a/k8s/deployments.yaml +++ b/k8s/deployments.yaml @@ -69,6 +69,7 @@ kind: Deployment metadata: name: identity spec: + replicas: 3 paused: true template: metadata: diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index 744d0a0ce..b11b972db 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -80,6 +80,7 @@ namespace Identity.API.Configuration }, ClientUri = $"{clientsUrl["Mvc"]}", // public uri of the client AllowedGrantTypes = GrantTypes.Hybrid, + AllowAccessTokensViaBrowser = false, RequireConsent = false, AllowOfflineAccess = true, RedirectUris = new List diff --git a/src/Services/Identity/Identity.API/Extensions/DataProtectionBuilderExtensions.cs b/src/Services/Identity/Identity.API/Extensions/DataProtectionBuilderExtensions.cs new file mode 100644 index 000000000..11fc8342e --- /dev/null +++ b/src/Services/Identity/Identity.API/Extensions/DataProtectionBuilderExtensions.cs @@ -0,0 +1,96 @@ +namespace DataProtectionExtensions +{ + using System; + using System.Linq; + using System.Security.Cryptography.X509Certificates; + using Microsoft.AspNetCore.DataProtection; + using Microsoft.AspNetCore.DataProtection.Repositories; + using Microsoft.AspNetCore.DataProtection.XmlEncryption; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using System.Net; + + /// + /// Extension methods for for configuring + /// data protection options. + /// + public static class DataProtectionBuilderExtensions + { + /// + /// Sets up data protection to persist session keys in Redis. + /// + /// The used to set up data protection options. + /// The connection string specifying the Redis instance and database for key storage. + /// + /// The for continued configuration. + /// + /// + /// Thrown if or is . + /// + /// + /// Thrown if is empty. + /// + public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, string redisConnectionString) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (redisConnectionString == null) + { + throw new ArgumentNullException(nameof(redisConnectionString)); + } + + if (redisConnectionString.Length == 0) + { + throw new ArgumentException("Redis connection string may not be empty.", nameof(redisConnectionString)); + } + + var ips = Dns.GetHostAddressesAsync(redisConnectionString).Result; + + return builder.Use(ServiceDescriptor.Singleton(services => new RedisXmlRepository(ips.First().ToString(), services.GetRequiredService>()))); + } + + /// + /// Updates an to use the service of + /// a specific type, removing all other services of that type. + /// + /// The that should use the specified service. + /// The with the service the should use. + /// + /// The for continued configuration. + /// + /// + /// Thrown if or is . + /// + public static IDataProtectionBuilder Use(this IDataProtectionBuilder builder, ServiceDescriptor descriptor) + { + // This algorithm of removing all other services of a specific type + // before adding the new/replacement service is how the base ASP.NET + // DataProtection bits work. Due to some of the differences in how + // that base set of bits handles DI, it's better to follow suit + // and work in the same way than to try and debug weird issues. + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + for (int i = builder.Services.Count - 1; i >= 0; i--) + { + if (builder.Services[i]?.ServiceType == descriptor.ServiceType) + { + builder.Services.RemoveAt(i); + } + } + + builder.Services.Add(descriptor); + return builder; + } + } +} diff --git a/src/Services/Identity/Identity.API/Extensions/RedisXmlRepository.cs b/src/Services/Identity/Identity.API/Extensions/RedisXmlRepository.cs new file mode 100644 index 000000000..ebb36c1b7 --- /dev/null +++ b/src/Services/Identity/Identity.API/Extensions/RedisXmlRepository.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using Microsoft.AspNetCore.DataProtection.Repositories; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; + +namespace DataProtectionExtensions +{ + /// + /// Key repository that stores XML encrypted keys in a Redis distributed cache. + /// + /// + /// + /// The values stored in Redis are XML documents that contain encrypted session + /// keys used for the protection of things like session state. The document contents + /// are double-encrypted - first with a changing session key; then by a master key. + /// As such, there's no risk in storing the keys in Redis - even if someone can crack + /// the master key, they still need to also crack the session key. (Other solutions + /// for sharing keys across a farm environment include writing them to files + /// on a file share.) + /// + /// + /// While the repository uses a hash to keep the set of encrypted keys separate, you + /// can further separate these items from other items in Redis by specifying a unique + /// database in the connection string. + /// + /// + /// Consumers of the repository are responsible for caching the XML items as needed. + /// Typically repositories are consumed by things like + /// which generates + /// values that get cached. The mechanism is already optimized for caching so there's + /// no need to create a redundant cache. + /// + /// + /// + /// + public class RedisXmlRepository : IXmlRepository, IDisposable + { + /// + /// The root cache key for XML items stored in Redis + /// + public static readonly string RedisHashKey = "DataProtectionXmlRepository"; + + /// + /// The connection to the Redis backing store. + /// + private IConnectionMultiplexer _connection; + + /// + /// Flag indicating whether the object has been disposed. + /// + private bool _disposed = false; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The Redis connection string. + /// + /// + /// The used to log diagnostic messages. + /// + /// + /// Thrown if or is . + /// + public RedisXmlRepository(string connectionString, ILogger logger) + : this(ConnectionMultiplexer.Connect(connectionString), logger) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The Redis database connection. + /// + /// + /// The used to log diagnostic messages. + /// + /// + /// Thrown if or is . + /// + public RedisXmlRepository(IConnectionMultiplexer connection, ILogger logger) + { + if (connection == null) + { + throw new ArgumentNullException(nameof(connection)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + this._connection = connection; + this.Logger = logger; + + // Mask the password so it doesn't get logged. + var configuration = Regex.Replace(this._connection.Configuration, @"password\s*=\s*[^,]*", "password=****", RegexOptions.IgnoreCase); + this.Logger.LogDebug("Storing data protection keys in Redis: {RedisConfiguration}", configuration); + } + + /// + /// Gets the logger. + /// + /// + /// The used to log diagnostic messages. + /// + public ILogger Logger { get; private set; } + + /// + /// Performs application-defined tasks associated with freeing, releasing, + /// or resetting unmanaged resources. + /// + public void Dispose() + { + this.Dispose(true); + } + + /// + /// Gets all top-level XML elements in the repository. + /// + /// + /// An with the set of elements + /// stored in the repository. + /// + public IReadOnlyCollection GetAllElements() + { + var database = this._connection.GetDatabase(); + var hash = database.HashGetAll(RedisHashKey); + var elements = new List(); + + if (hash == null || hash.Length == 0) + { + return elements.AsReadOnly(); + } + + foreach (var item in hash.ToStringDictionary()) + { + elements.Add(XElement.Parse(item.Value)); + } + + this.Logger.LogDebug("Read {XmlElementCount} XML elements from Redis.", elements.Count); + return elements.AsReadOnly(); + } + + /// + /// Adds a top-level XML element to the repository. + /// + /// The element to add. + /// + /// An optional name to be associated with the XML element. + /// For instance, if this repository stores XML files on disk, the friendly name may + /// be used as part of the file name. Repository implementations are not required to + /// observe this parameter even if it has been provided by the caller. + /// + /// + /// The parameter must be unique if specified. + /// For instance, it could be the ID of the key being stored. + /// + /// + /// Thrown if is . + /// + public void StoreElement(XElement element, string friendlyName) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + if (string.IsNullOrEmpty(friendlyName)) + { + // The framework always passes in a name, but + // the contract indicates this may be null or empty. + friendlyName = Guid.NewGuid().ToString(); + } + + this.Logger.LogDebug("Storing XML element with friendly name {XmlElementFriendlyName}.", friendlyName); + + this._connection.GetDatabase().HashSet(RedisHashKey, friendlyName, element.ToString()); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!this._disposed) + { + if (disposing) + { + if (this._connection != null) + { + this._connection.Close(); + this._connection.Dispose(); + } + } + + this._connection = null; + this._disposed = true; + } + } + } +} \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/Identity.API.csproj b/src/Services/Identity/Identity.API/Identity.API.csproj index 003b5cb30..8874cabbc 100644 --- a/src/Services/Identity/Identity.API/Identity.API.csproj +++ b/src/Services/Identity/Identity.API/Identity.API.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Services/Identity/Identity.API/Migrations/20170604151240_Init-persisted-grant.Designer.cs b/src/Services/Identity/Identity.API/Migrations/20170604151240_Init-persisted-grant.Designer.cs new file mode 100644 index 000000000..a2b93219b --- /dev/null +++ b/src/Services/Identity/Identity.API/Migrations/20170604151240_Init-persisted-grant.Designer.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using IdentityServer4.EntityFramework.DbContexts; + +namespace Identity.API.Migrations +{ + [DbContext(typeof(PersistedGrantDbContext))] + [Migration("20170604151240_Init-persisted-grant")] + partial class Initpersistedgrant + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasMaxLength(200); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200); + + b.Property("CreationTime"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000); + + b.Property("Expiration"); + + b.Property("SubjectId") + .HasMaxLength(200); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50); + + b.HasKey("Key"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.ToTable("PersistedGrants"); + }); + } + } +} diff --git a/src/Services/Identity/Identity.API/Migrations/20170604151240_Init-persisted-grant.cs b/src/Services/Identity/Identity.API/Migrations/20170604151240_Init-persisted-grant.cs new file mode 100644 index 000000000..51c896f5f --- /dev/null +++ b/src/Services/Identity/Identity.API/Migrations/20170604151240_Init-persisted-grant.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Identity.API.Migrations +{ + public partial class Initpersistedgrant : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PersistedGrants", + columns: table => new + { + Key = table.Column(maxLength: 200, nullable: false), + ClientId = table.Column(maxLength: 200, nullable: false), + CreationTime = table.Column(nullable: false), + Data = table.Column(maxLength: 50000, nullable: false), + Expiration = table.Column(nullable: true), + SubjectId = table.Column(maxLength: 200, nullable: true), + Type = table.Column(maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PersistedGrants", x => x.Key); + }); + + migrationBuilder.CreateIndex( + name: "IX_PersistedGrants_SubjectId_ClientId_Type", + table: "PersistedGrants", + columns: new[] { "SubjectId", "ClientId", "Type" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PersistedGrants"); + } + } +} diff --git a/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/20170604151338_Init-configuration.Designer.cs b/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/20170604151338_Init-configuration.Designer.cs new file mode 100644 index 000000000..9e6eb2500 --- /dev/null +++ b/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/20170604151338_Init-configuration.Designer.cs @@ -0,0 +1,539 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using IdentityServer4.EntityFramework.DbContexts; + +namespace Identity.API.Migrations.ConfigurationDb +{ + [DbContext(typeof(ConfigurationDbContext))] + [Migration("20170604151338_Init-configuration")] + partial class Initconfiguration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description") + .HasMaxLength(1000); + + b.Property("DisplayName") + .HasMaxLength(200); + + b.Property("Enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiResourceId") + .IsRequired(); + + b.Property("Type") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.ToTable("ApiClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiResourceId") + .IsRequired(); + + b.Property("Description") + .HasMaxLength(1000); + + b.Property("DisplayName") + .HasMaxLength(200); + + b.Property("Emphasize"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.Property("Required"); + + b.Property("ShowInDiscoveryDocument"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiScopeId") + .IsRequired(); + + b.Property("Type") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("ApiScopeId"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiResourceId") + .IsRequired(); + + b.Property("Description") + .HasMaxLength(1000); + + b.Property("Expiration"); + + b.Property("Type") + .HasMaxLength(250); + + b.Property("Value") + .HasMaxLength(2000); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.ToTable("ApiSecrets"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AbsoluteRefreshTokenLifetime"); + + b.Property("AccessTokenLifetime"); + + b.Property("AccessTokenType"); + + b.Property("AllowAccessTokensViaBrowser"); + + b.Property("AllowOfflineAccess"); + + b.Property("AllowPlainTextPkce"); + + b.Property("AllowRememberConsent"); + + b.Property("AlwaysIncludeUserClaimsInIdToken"); + + b.Property("AlwaysSendClientClaims"); + + b.Property("AuthorizationCodeLifetime"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200); + + b.Property("ClientName") + .HasMaxLength(200); + + b.Property("ClientUri") + .HasMaxLength(2000); + + b.Property("EnableLocalLogin"); + + b.Property("Enabled"); + + b.Property("IdentityTokenLifetime"); + + b.Property("IncludeJwtId"); + + b.Property("LogoUri"); + + b.Property("LogoutSessionRequired"); + + b.Property("LogoutUri"); + + b.Property("PrefixClientClaims"); + + b.Property("ProtocolType") + .IsRequired() + .HasMaxLength(200); + + b.Property("RefreshTokenExpiration"); + + b.Property("RefreshTokenUsage"); + + b.Property("RequireClientSecret"); + + b.Property("RequireConsent"); + + b.Property("RequirePkce"); + + b.Property("SlidingRefreshTokenLifetime"); + + b.Property("UpdateAccessTokenClaimsOnRefresh"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Type") + .IsRequired() + .HasMaxLength(250); + + b.Property("Value") + .IsRequired() + .HasMaxLength(250); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Origin") + .IsRequired() + .HasMaxLength(150); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("GrantType") + .IsRequired() + .HasMaxLength(250); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("PostLogoutRedirectUri") + .IsRequired() + .HasMaxLength(2000); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("RedirectUri") + .IsRequired() + .HasMaxLength(2000); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Description") + .HasMaxLength(2000); + + b.Property("Expiration"); + + b.Property("Type") + .HasMaxLength(250); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("IdentityResourceId") + .IsRequired(); + + b.Property("Type") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description") + .HasMaxLength(1000); + + b.Property("DisplayName") + .HasMaxLength(200); + + b.Property("Emphasize"); + + b.Property("Enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.Property("Required"); + + b.Property("ShowInDiscoveryDocument"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "ApiScope") + .WithMany("UserClaims") + .HasForeignKey("ApiScopeId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/20170604151338_Init-configuration.cs b/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/20170604151338_Init-configuration.cs new file mode 100644 index 000000000..8cf20e865 --- /dev/null +++ b/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/20170604151338_Init-configuration.cs @@ -0,0 +1,501 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Identity.API.Migrations.ConfigurationDb +{ + public partial class Initconfiguration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ApiResources", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Description = table.Column(maxLength: 1000, nullable: true), + DisplayName = table.Column(maxLength: 200, nullable: true), + Enabled = table.Column(nullable: false), + Name = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiResources", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Clients", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + AbsoluteRefreshTokenLifetime = table.Column(nullable: false), + AccessTokenLifetime = table.Column(nullable: false), + AccessTokenType = table.Column(nullable: false), + AllowAccessTokensViaBrowser = table.Column(nullable: false), + AllowOfflineAccess = table.Column(nullable: false), + AllowPlainTextPkce = table.Column(nullable: false), + AllowRememberConsent = table.Column(nullable: false), + AlwaysIncludeUserClaimsInIdToken = table.Column(nullable: false), + AlwaysSendClientClaims = table.Column(nullable: false), + AuthorizationCodeLifetime = table.Column(nullable: false), + ClientId = table.Column(maxLength: 200, nullable: false), + ClientName = table.Column(maxLength: 200, nullable: true), + ClientUri = table.Column(maxLength: 2000, nullable: true), + EnableLocalLogin = table.Column(nullable: false), + Enabled = table.Column(nullable: false), + IdentityTokenLifetime = table.Column(nullable: false), + IncludeJwtId = table.Column(nullable: false), + LogoUri = table.Column(nullable: true), + LogoutSessionRequired = table.Column(nullable: false), + LogoutUri = table.Column(nullable: true), + PrefixClientClaims = table.Column(nullable: false), + ProtocolType = table.Column(maxLength: 200, nullable: false), + RefreshTokenExpiration = table.Column(nullable: false), + RefreshTokenUsage = table.Column(nullable: false), + RequireClientSecret = table.Column(nullable: false), + RequireConsent = table.Column(nullable: false), + RequirePkce = table.Column(nullable: false), + SlidingRefreshTokenLifetime = table.Column(nullable: false), + UpdateAccessTokenClaimsOnRefresh = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Clients", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "IdentityResources", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Description = table.Column(maxLength: 1000, nullable: true), + DisplayName = table.Column(maxLength: 200, nullable: true), + Emphasize = table.Column(nullable: false), + Enabled = table.Column(nullable: false), + Name = table.Column(maxLength: 200, nullable: false), + Required = table.Column(nullable: false), + ShowInDiscoveryDocument = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityResources", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ApiClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ApiResourceId = table.Column(nullable: false), + Type = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiClaims", x => x.Id); + table.ForeignKey( + name: "FK_ApiClaims_ApiResources_ApiResourceId", + column: x => x.ApiResourceId, + principalTable: "ApiResources", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ApiScopes", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ApiResourceId = table.Column(nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + DisplayName = table.Column(maxLength: 200, nullable: true), + Emphasize = table.Column(nullable: false), + Name = table.Column(maxLength: 200, nullable: false), + Required = table.Column(nullable: false), + ShowInDiscoveryDocument = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiScopes", x => x.Id); + table.ForeignKey( + name: "FK_ApiScopes_ApiResources_ApiResourceId", + column: x => x.ApiResourceId, + principalTable: "ApiResources", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ApiSecrets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ApiResourceId = table.Column(nullable: false), + Description = table.Column(maxLength: 1000, nullable: true), + Expiration = table.Column(nullable: true), + Type = table.Column(maxLength: 250, nullable: true), + Value = table.Column(maxLength: 2000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiSecrets", x => x.Id); + table.ForeignKey( + name: "FK_ApiSecrets_ApiResources_ApiResourceId", + column: x => x.ApiResourceId, + principalTable: "ApiResources", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClientClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClientId = table.Column(nullable: false), + Type = table.Column(maxLength: 250, nullable: false), + Value = table.Column(maxLength: 250, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientClaims", x => x.Id); + table.ForeignKey( + name: "FK_ClientClaims_Clients_ClientId", + column: x => x.ClientId, + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClientCorsOrigins", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClientId = table.Column(nullable: false), + Origin = table.Column(maxLength: 150, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientCorsOrigins", x => x.Id); + table.ForeignKey( + name: "FK_ClientCorsOrigins_Clients_ClientId", + column: x => x.ClientId, + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClientGrantTypes", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClientId = table.Column(nullable: false), + GrantType = table.Column(maxLength: 250, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientGrantTypes", x => x.Id); + table.ForeignKey( + name: "FK_ClientGrantTypes_Clients_ClientId", + column: x => x.ClientId, + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClientIdPRestrictions", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClientId = table.Column(nullable: false), + Provider = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientIdPRestrictions", x => x.Id); + table.ForeignKey( + name: "FK_ClientIdPRestrictions_Clients_ClientId", + column: x => x.ClientId, + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClientPostLogoutRedirectUris", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClientId = table.Column(nullable: false), + PostLogoutRedirectUri = table.Column(maxLength: 2000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientPostLogoutRedirectUris", x => x.Id); + table.ForeignKey( + name: "FK_ClientPostLogoutRedirectUris_Clients_ClientId", + column: x => x.ClientId, + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClientRedirectUris", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClientId = table.Column(nullable: false), + RedirectUri = table.Column(maxLength: 2000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientRedirectUris", x => x.Id); + table.ForeignKey( + name: "FK_ClientRedirectUris_Clients_ClientId", + column: x => x.ClientId, + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClientScopes", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClientId = table.Column(nullable: false), + Scope = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientScopes", x => x.Id); + table.ForeignKey( + name: "FK_ClientScopes_Clients_ClientId", + column: x => x.ClientId, + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ClientSecrets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClientId = table.Column(nullable: false), + Description = table.Column(maxLength: 2000, nullable: true), + Expiration = table.Column(nullable: true), + Type = table.Column(maxLength: 250, nullable: true), + Value = table.Column(maxLength: 2000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ClientSecrets", x => x.Id); + table.ForeignKey( + name: "FK_ClientSecrets_Clients_ClientId", + column: x => x.ClientId, + principalTable: "Clients", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "IdentityClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + IdentityResourceId = table.Column(nullable: false), + Type = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_IdentityClaims", x => x.Id); + table.ForeignKey( + name: "FK_IdentityClaims_IdentityResources_IdentityResourceId", + column: x => x.IdentityResourceId, + principalTable: "IdentityResources", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ApiScopeClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ApiScopeId = table.Column(nullable: false), + Type = table.Column(maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiScopeClaims", x => x.Id); + table.ForeignKey( + name: "FK_ApiScopeClaims_ApiScopes_ApiScopeId", + column: x => x.ApiScopeId, + principalTable: "ApiScopes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ApiResources_Name", + table: "ApiResources", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ApiClaims_ApiResourceId", + table: "ApiClaims", + column: "ApiResourceId"); + + migrationBuilder.CreateIndex( + name: "IX_ApiScopes_ApiResourceId", + table: "ApiScopes", + column: "ApiResourceId"); + + migrationBuilder.CreateIndex( + name: "IX_ApiScopes_Name", + table: "ApiScopes", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ApiScopeClaims_ApiScopeId", + table: "ApiScopeClaims", + column: "ApiScopeId"); + + migrationBuilder.CreateIndex( + name: "IX_ApiSecrets_ApiResourceId", + table: "ApiSecrets", + column: "ApiResourceId"); + + migrationBuilder.CreateIndex( + name: "IX_Clients_ClientId", + table: "Clients", + column: "ClientId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ClientClaims_ClientId", + table: "ClientClaims", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_ClientCorsOrigins_ClientId", + table: "ClientCorsOrigins", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_ClientGrantTypes_ClientId", + table: "ClientGrantTypes", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_ClientIdPRestrictions_ClientId", + table: "ClientIdPRestrictions", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_ClientPostLogoutRedirectUris_ClientId", + table: "ClientPostLogoutRedirectUris", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_ClientRedirectUris_ClientId", + table: "ClientRedirectUris", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_ClientScopes_ClientId", + table: "ClientScopes", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_ClientSecrets_ClientId", + table: "ClientSecrets", + column: "ClientId"); + + migrationBuilder.CreateIndex( + name: "IX_IdentityClaims_IdentityResourceId", + table: "IdentityClaims", + column: "IdentityResourceId"); + + migrationBuilder.CreateIndex( + name: "IX_IdentityResources_Name", + table: "IdentityResources", + column: "Name", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ApiClaims"); + + migrationBuilder.DropTable( + name: "ApiScopeClaims"); + + migrationBuilder.DropTable( + name: "ApiSecrets"); + + migrationBuilder.DropTable( + name: "ClientClaims"); + + migrationBuilder.DropTable( + name: "ClientCorsOrigins"); + + migrationBuilder.DropTable( + name: "ClientGrantTypes"); + + migrationBuilder.DropTable( + name: "ClientIdPRestrictions"); + + migrationBuilder.DropTable( + name: "ClientPostLogoutRedirectUris"); + + migrationBuilder.DropTable( + name: "ClientRedirectUris"); + + migrationBuilder.DropTable( + name: "ClientScopes"); + + migrationBuilder.DropTable( + name: "ClientSecrets"); + + migrationBuilder.DropTable( + name: "IdentityClaims"); + + migrationBuilder.DropTable( + name: "ApiScopes"); + + migrationBuilder.DropTable( + name: "Clients"); + + migrationBuilder.DropTable( + name: "IdentityResources"); + + migrationBuilder.DropTable( + name: "ApiResources"); + } + } +} diff --git a/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/ConfigurationDbContextModelSnapshot.cs b/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/ConfigurationDbContextModelSnapshot.cs new file mode 100644 index 000000000..7c725f5ae --- /dev/null +++ b/src/Services/Identity/Identity.API/Migrations/ConfigurationDb/ConfigurationDbContextModelSnapshot.cs @@ -0,0 +1,538 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using IdentityServer4.EntityFramework.DbContexts; + +namespace Identity.API.Migrations.ConfigurationDb +{ + [DbContext(typeof(ConfigurationDbContext))] + partial class ConfigurationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description") + .HasMaxLength(1000); + + b.Property("DisplayName") + .HasMaxLength(200); + + b.Property("Enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ApiResources"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiResourceId") + .IsRequired(); + + b.Property("Type") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.ToTable("ApiClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiResourceId") + .IsRequired(); + + b.Property("Description") + .HasMaxLength(1000); + + b.Property("DisplayName") + .HasMaxLength(200); + + b.Property("Emphasize"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.Property("Required"); + + b.Property("ShowInDiscoveryDocument"); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ApiScopes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiScopeId") + .IsRequired(); + + b.Property("Type") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("ApiScopeId"); + + b.ToTable("ApiScopeClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiResourceId") + .IsRequired(); + + b.Property("Description") + .HasMaxLength(1000); + + b.Property("Expiration"); + + b.Property("Type") + .HasMaxLength(250); + + b.Property("Value") + .HasMaxLength(2000); + + b.HasKey("Id"); + + b.HasIndex("ApiResourceId"); + + b.ToTable("ApiSecrets"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AbsoluteRefreshTokenLifetime"); + + b.Property("AccessTokenLifetime"); + + b.Property("AccessTokenType"); + + b.Property("AllowAccessTokensViaBrowser"); + + b.Property("AllowOfflineAccess"); + + b.Property("AllowPlainTextPkce"); + + b.Property("AllowRememberConsent"); + + b.Property("AlwaysIncludeUserClaimsInIdToken"); + + b.Property("AlwaysSendClientClaims"); + + b.Property("AuthorizationCodeLifetime"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200); + + b.Property("ClientName") + .HasMaxLength(200); + + b.Property("ClientUri") + .HasMaxLength(2000); + + b.Property("EnableLocalLogin"); + + b.Property("Enabled"); + + b.Property("IdentityTokenLifetime"); + + b.Property("IncludeJwtId"); + + b.Property("LogoUri"); + + b.Property("LogoutSessionRequired"); + + b.Property("LogoutUri"); + + b.Property("PrefixClientClaims"); + + b.Property("ProtocolType") + .IsRequired() + .HasMaxLength(200); + + b.Property("RefreshTokenExpiration"); + + b.Property("RefreshTokenUsage"); + + b.Property("RequireClientSecret"); + + b.Property("RequireConsent"); + + b.Property("RequirePkce"); + + b.Property("SlidingRefreshTokenLifetime"); + + b.Property("UpdateAccessTokenClaimsOnRefresh"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("Clients"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Type") + .IsRequired() + .HasMaxLength(250); + + b.Property("Value") + .IsRequired() + .HasMaxLength(250); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Origin") + .IsRequired() + .HasMaxLength(150); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientCorsOrigins"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("GrantType") + .IsRequired() + .HasMaxLength(250); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientGrantTypes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Provider") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientIdPRestrictions"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("PostLogoutRedirectUri") + .IsRequired() + .HasMaxLength(2000); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientPostLogoutRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("RedirectUri") + .IsRequired() + .HasMaxLength(2000); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientRedirectUris"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientScopes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClientId") + .IsRequired(); + + b.Property("Description") + .HasMaxLength(2000); + + b.Property("Expiration"); + + b.Property("Type") + .HasMaxLength(250); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("IdentityResourceId") + .IsRequired(); + + b.Property("Type") + .IsRequired() + .HasMaxLength(200); + + b.HasKey("Id"); + + b.HasIndex("IdentityResourceId"); + + b.ToTable("IdentityClaims"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Description") + .HasMaxLength(1000); + + b.Property("DisplayName") + .HasMaxLength(200); + + b.Property("Emphasize"); + + b.Property("Enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200); + + b.Property("Required"); + + b.Property("ShowInDiscoveryDocument"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("IdentityResources"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiResourceClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany("UserClaims") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScope", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany("Scopes") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiScopeClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiScope", "ApiScope") + .WithMany("UserClaims") + .HasForeignKey("ApiScopeId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ApiSecret", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.ApiResource", "ApiResource") + .WithMany("Secrets") + .HasForeignKey("ApiResourceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("Claims") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientCorsOrigin", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedCorsOrigins") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientGrantType", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedGrantTypes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientIdPRestriction", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("IdentityProviderRestrictions") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientPostLogoutRedirectUri", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("PostLogoutRedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientRedirectUri", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("RedirectUris") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientScope", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("AllowedScopes") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.ClientSecret", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.Client", "Client") + .WithMany("ClientSecrets") + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.IdentityClaim", b => + { + b.HasOne("IdentityServer4.EntityFramework.Entities.IdentityResource", "IdentityResource") + .WithMany("UserClaims") + .HasForeignKey("IdentityResourceId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Services/Identity/Identity.API/Migrations/PersistedGrantDbContextModelSnapshot.cs b/src/Services/Identity/Identity.API/Migrations/PersistedGrantDbContextModelSnapshot.cs new file mode 100644 index 000000000..ffe6bc35a --- /dev/null +++ b/src/Services/Identity/Identity.API/Migrations/PersistedGrantDbContextModelSnapshot.cs @@ -0,0 +1,51 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using IdentityServer4.EntityFramework.DbContexts; + +namespace Identity.API.Migrations +{ + [DbContext(typeof(PersistedGrantDbContext))] + partial class PersistedGrantDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.1.2") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasMaxLength(200); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200); + + b.Property("CreationTime"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000); + + b.Property("Expiration"); + + b.Property("SubjectId") + .HasMaxLength(200); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50); + + b.HasKey("Key"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.ToTable("PersistedGrants"); + }); + } + } +} diff --git a/src/Services/Identity/Identity.API/Startup.cs b/src/Services/Identity/Identity.API/Startup.cs index 981e305c8..98ed8578d 100644 --- a/src/Services/Identity/Identity.API/Startup.cs +++ b/src/Services/Identity/Identity.API/Startup.cs @@ -19,6 +19,11 @@ using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.HealthChecks; using Identity.API.Certificate; +using DataProtectionExtensions; +using System.Reflection; +using IdentityServer4.Test; +using IdentityServer4.EntityFramework.DbContexts; +using IdentityServer4.EntityFramework.Mappers; namespace eShopOnContainers.Identity { @@ -34,7 +39,7 @@ namespace eShopOnContainers.Identity if (env.IsDevelopment()) { // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 - builder.AddUserSecrets(); + //builder.AddUserSecrets(); } builder.AddEnvironmentVariables(); @@ -45,8 +50,7 @@ namespace eShopOnContainers.Identity // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) - { - + { // Add framework services. services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); @@ -57,12 +61,12 @@ namespace eShopOnContainers.Identity services.Configure(Configuration); + services.AddMvc(); + services.AddDataProtection(opts => { opts.ApplicationDiscriminator = "eshop.identity"; - }); - - services.AddMvc(); + });//.PersistKeysToRedis("basket.data"); services.AddHealthChecks(checks => { @@ -79,20 +83,20 @@ namespace eShopOnContainers.Identity services.AddTransient, EFLoginService>(); services.AddTransient(); - //callbacks urls from config: - Dictionary clientUrls = new Dictionary(); - clientUrls.Add("Mvc", Configuration.GetValue("MvcClient")); - clientUrls.Add("Spa", Configuration.GetValue("SpaClient")); - clientUrls.Add("Xamarin", Configuration.GetValue("XamarinCallback")); + var connectionString = Configuration.GetConnectionString("DefaultConnection"); + var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; // Adds IdentityServer services.AddIdentityServer(x => x.IssuerUri = "null") - .AddSigningCredential(Certificate.Get()) - .AddInMemoryApiResources(Config.GetApis()) - .AddInMemoryIdentityResources(Config.GetResources()) - .AddInMemoryClients(Config.GetClients(clientUrls)) + .AddSigningCredential(Certificate.Get()) .AddAspNetIdentity() - .Services.AddTransient(); + .AddConfigurationStore(builder => + builder.UseSqlServer(connectionString, options => + options.MigrationsAssembly(migrationsAssembly))) + .AddOperationalStore(builder => + builder.UseSqlServer(connectionString, options => + options.MigrationsAssembly(migrationsAssembly))) + .Services.AddTransient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -134,10 +138,55 @@ namespace eShopOnContainers.Identity template: "{controller=Home}/{action=Index}/{id?}"); }); + InitializeDatabase(app); + //Seed Data var hasher = new PasswordHasher(); new ApplicationContextSeed(hasher).SeedAsync(app, loggerFactory) .Wait(); } + + private void InitializeDatabase(IApplicationBuilder app) + { + //callbacks urls from config: + Dictionary clientUrls = new Dictionary(); + clientUrls.Add("Mvc", Configuration.GetValue("MvcClient")); + clientUrls.Add("Spa", Configuration.GetValue("SpaClient")); + clientUrls.Add("Xamarin", Configuration.GetValue("XamarinCallback")); + + using (var serviceScope = app.ApplicationServices.GetService().CreateScope()) + { + serviceScope.ServiceProvider.GetRequiredService().Database.Migrate(); + var context = serviceScope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); + + if (!context.Clients.Any()) + { + foreach (var client in Config.GetClients(clientUrls)) + { + context.Clients.Add(client.ToEntity()); + } + context.SaveChanges(); + } + + if (!context.IdentityResources.Any()) + { + foreach (var resource in Config.GetResources()) + { + context.IdentityResources.Add(resource.ToEntity()); + } + context.SaveChanges(); + } + + if (!context.ApiResources.Any()) + { + foreach (var api in Config.GetApis()) + { + context.ApiResources.Add(api.ToEntity()); + } + context.SaveChanges(); + } + } + } } } diff --git a/src/Web/WebMVC/Controllers/AccountController.cs b/src/Web/WebMVC/Controllers/AccountController.cs index 0dee2ccfb..531c6f547 100644 --- a/src/Web/WebMVC/Controllers/AccountController.cs +++ b/src/Web/WebMVC/Controllers/AccountController.cs @@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Microsoft.eShopOnContainers.WebMVC.Services; using Microsoft.AspNetCore.Http.Authentication; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; namespace Microsoft.eShopOnContainers.WebMVC.Controllers { @@ -17,15 +19,16 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers public ActionResult Index() => View(); [Authorize] - public IActionResult SignIn(string returnUrl) + public async Task SignIn(string returnUrl) { var user = User as ClaimsPrincipal; - + var token = await HttpContext.Authentication.GetTokenAsync("access_token"); + //TODO - Not retrieving AccessToken yet - var token = user.FindFirst("access_token"); + //var token = user.FindFirst("access_token"); if (token != null) { - ViewData["access_token"] = token.Value; + ViewData["access_token"] = token; } // "Catalog" because UrlHelper doesn't support nameof() for controllers diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index ba0b1244f..1a6fc240b 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -44,12 +44,14 @@ namespace Microsoft.eShopOnContainers.WebMVC // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddMvc(); + services.AddDataProtection(opts => { opts.ApplicationDiscriminator = "eshop.webmvc"; }); - services.AddMvc(); + services.Configure(Configuration); services.AddHealthChecks(checks => diff --git a/test/Services/FunctionalTests/FunctionalTests.csproj b/test/Services/FunctionalTests/FunctionalTests.csproj index 2b8ee1f44..54a74beda 100644 --- a/test/Services/FunctionalTests/FunctionalTests.csproj +++ b/test/Services/FunctionalTests/FunctionalTests.csproj @@ -38,4 +38,8 @@ + + + + \ No newline at end of file diff --git a/test/Services/IntegrationTests/IntegrationTests.csproj b/test/Services/IntegrationTests/IntegrationTests.csproj index b71e1d4c4..60911dd4d 100644 --- a/test/Services/IntegrationTests/IntegrationTests.csproj +++ b/test/Services/IntegrationTests/IntegrationTests.csproj @@ -46,4 +46,8 @@ + + + + diff --git a/test/Services/UnitTest/UnitTest.csproj b/test/Services/UnitTest/UnitTest.csproj index 3a80e594b..8955e4a45 100644 --- a/test/Services/UnitTest/UnitTest.csproj +++ b/test/Services/UnitTest/UnitTest.csproj @@ -28,4 +28,8 @@ + + + + From 3c811156031676d6f43efda25ed3fbf4cbeb8096 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Mon, 5 Jun 2017 23:20:46 +0200 Subject: [PATCH 23/36] Edit mapper methods --- .../Controllers/CampaignsController.cs | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index a525adec5..ca8e93d8c 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -125,26 +125,32 @@ { var campaignDto = new CampaignDTO { + Id = campaign.Id, Description = campaign.Description, From = campaign.From, To = campaign.To, Url = campaign.Url, }; - campaign.Rules.ForEach(c => + campaign.Rules.ForEach(rule => { - switch (RuleType.From(c.RuleTypeId)) + var ruleDto = new RuleDTO + { + Id = rule.Id, + RuleTypeId = rule.RuleTypeId, + Description = rule.Description, + CampaignId = rule.CampaignId + }; + + switch (RuleType.From(rule.RuleTypeId)) { case RuleTypeEnum.UserLocationRule: - var userLocationRule = c as UserLocationRule; - campaignDto.Rules.Add(new RuleDTO - { - LocationId = userLocationRule.LocationId, - RuleTypeId = userLocationRule.RuleTypeId, - Description = userLocationRule.Description - }); + var userLocationRule = rule as UserLocationRule; + ruleDto.LocationId = userLocationRule.LocationId; break; } + + campaignDto.Rules.Add(ruleDto); }); return campaignDto; @@ -154,22 +160,24 @@ { var campaingModel = new Campaign { + Id = campaignDto.Id, Description = campaignDto.Description, From = campaignDto.From, To = campaignDto.To, Url = campaignDto.Url }; - campaignDto.Rules.ForEach(c => + campaignDto.Rules.ForEach(ruleDto => { - switch (RuleType.From(c.RuleTypeId)) + switch (RuleType.From(ruleDto.RuleTypeId)) { case RuleTypeEnum.UserLocationRule: campaingModel.Rules.Add(new UserLocationRule { - LocationId = c.LocationId.Value, - RuleTypeId = c.RuleTypeId, - Description = c.Description, + Id = ruleDto.Id, + LocationId = ruleDto.LocationId.Value, + RuleTypeId = ruleDto.RuleTypeId, + Description = ruleDto.Description, Campaign = campaingModel }); break; From 56c07aaafac8d15f633a781bcc18095f2f6a9377 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Tue, 6 Jun 2017 10:26:11 +0200 Subject: [PATCH 24/36] Edit campaigns controller --- .../Controllers/CampaignsController.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index ca8e93d8c..0082ee71f 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -8,10 +8,9 @@ using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; - using System.Linq; [Route("api/v1/[controller]")] - [Authorize] + //[Authorize] public class CampaignsController : Controller { private readonly MarketingContext _context; @@ -51,14 +50,14 @@ } [HttpPost] - public async Task CreateCampaign([FromBody] CampaignDTO campaign) + public async Task CreateCampaign([FromBody] CampaignDTO campaignDto) { - if (campaign is null) + if (campaignDto is null) { return BadRequest(); } - var campaingToCreate = MapCampaignDtoToModel(campaign); + var campaingToCreate = MapCampaignDtoToModel(campaignDto); await _context.Campaigns.AddAsync(campaingToCreate); await _context.SaveChangesAsync(); @@ -67,9 +66,9 @@ } [HttpPut("{id:int}")] - public async Task UpdateCampaign(int id, [FromBody]CampaignDTO campaign) + public async Task UpdateCampaign(int id, [FromBody]CampaignDTO campaignDto) { - if (id < 1 || campaign is null) + if (id < 1 || campaignDto is null) { return BadRequest(); } @@ -80,9 +79,10 @@ return NotFound(); } - campaignToUpdate.Description = campaign.Description; - campaignToUpdate.From = campaign.From; - campaignToUpdate.To = campaign.To; + campaignToUpdate.Description = campaignDto.Description; + campaignToUpdate.From = campaignDto.From; + campaignToUpdate.To = campaignDto.To; + campaignToUpdate.Url = campaignDto.Url; await _context.SaveChangesAsync(); From ce56a42bd1d4608e54d959905aefe4d63cc5c713 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Tue, 6 Jun 2017 14:18:46 +0200 Subject: [PATCH 25/36] Redesign marketing campaign api in two different controllers (campaigns/locations) --- .../Controllers/CampaignsController.cs | 63 ++------ .../Controllers/LocationsController.cs | 146 ++++++++++++++++++ .../Marketing.API/Dto/CampaignDTO.cs | 8 - .../Marketing/Marketing.API/Dto/RuleDTO.cs | 20 --- .../Marketing.API/Dto/UserLocationRuleDTO.cs | 11 ++ .../Infrastructure/MarketingContext.cs | 6 +- .../Marketing.API/Marketing.API.csproj | 3 + .../Marketing/Marketing.API/Model/Rule.cs | 14 +- .../{Dto => Model}/RuleTypeEnum.cs | 8 +- 9 files changed, 189 insertions(+), 90 deletions(-) create mode 100644 src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs delete mode 100644 src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs create mode 100644 src/Services/Marketing/Marketing.API/Dto/UserLocationRuleDTO.cs rename src/Services/Marketing/Marketing.API/{Dto => Model}/RuleTypeEnum.cs (71%) diff --git a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs index 0082ee71f..f3dde55ef 100644 --- a/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs +++ b/src/Services/Marketing/Marketing.API/Controllers/CampaignsController.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Authorization; [Route("api/v1/[controller]")] - //[Authorize] + [Authorize] public class CampaignsController : Controller { private readonly MarketingContext _context; @@ -24,9 +24,13 @@ public async Task GetAllCampaigns() { var campaignList = await _context.Campaigns - .Include(c => c.Rules) .ToListAsync(); + if (campaignList is null) + { + return Ok(); + } + var campaignDtoList = MapCampaignModelListToDtoList(campaignList); return Ok(campaignDtoList); @@ -36,7 +40,6 @@ public async Task GetCampaignById(int id) { var campaign = await _context.Campaigns - .Include(c => c.Rules) .SingleOrDefaultAsync(c => c.Id == id); if (campaign is null) @@ -57,16 +60,16 @@ return BadRequest(); } - var campaingToCreate = MapCampaignDtoToModel(campaignDto); + var campaign = MapCampaignDtoToModel(campaignDto); - await _context.Campaigns.AddAsync(campaingToCreate); + await _context.Campaigns.AddAsync(campaign); await _context.SaveChangesAsync(); - return CreatedAtAction(nameof(GetCampaignById), new { id = campaingToCreate.Id }, null); + return CreatedAtAction(nameof(GetCampaignById), new { id = campaign.Id }, null); } [HttpPut("{id:int}")] - public async Task UpdateCampaign(int id, [FromBody]CampaignDTO campaignDto) + public async Task UpdateCampaign(int id, [FromBody] CampaignDTO campaignDto) { if (id < 1 || campaignDto is null) { @@ -123,7 +126,7 @@ private CampaignDTO MapCampaignModelToDto(Campaign campaign) { - var campaignDto = new CampaignDTO + return new CampaignDTO { Id = campaign.Id, Description = campaign.Description, @@ -131,34 +134,11 @@ To = campaign.To, Url = campaign.Url, }; - - campaign.Rules.ForEach(rule => - { - var ruleDto = new RuleDTO - { - Id = rule.Id, - RuleTypeId = rule.RuleTypeId, - Description = rule.Description, - CampaignId = rule.CampaignId - }; - - switch (RuleType.From(rule.RuleTypeId)) - { - case RuleTypeEnum.UserLocationRule: - var userLocationRule = rule as UserLocationRule; - ruleDto.LocationId = userLocationRule.LocationId; - break; - } - - campaignDto.Rules.Add(ruleDto); - }); - - return campaignDto; } private Campaign MapCampaignDtoToModel(CampaignDTO campaignDto) { - var campaingModel = new Campaign + return new Campaign { Id = campaignDto.Id, Description = campaignDto.Description, @@ -166,25 +146,6 @@ To = campaignDto.To, Url = campaignDto.Url }; - - campaignDto.Rules.ForEach(ruleDto => - { - switch (RuleType.From(ruleDto.RuleTypeId)) - { - case RuleTypeEnum.UserLocationRule: - campaingModel.Rules.Add(new UserLocationRule - { - Id = ruleDto.Id, - LocationId = ruleDto.LocationId.Value, - RuleTypeId = ruleDto.RuleTypeId, - Description = ruleDto.Description, - Campaign = campaingModel - }); - break; - } - }); - - return campaingModel; } } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs b/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs new file mode 100644 index 000000000..843e58030 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Controllers/LocationsController.cs @@ -0,0 +1,146 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Controllers +{ + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; + using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure; + using Microsoft.eShopOnContainers.Services.Marketing.API.Model; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + [Authorize] + public class LocationsController : Controller + { + private readonly MarketingContext _context; + + public LocationsController(MarketingContext context) + { + _context = context; + } + + [HttpGet] + [Route("api/v1/campaigns/{campaignId:int}/locations/{userLocationRuleId:int}")] + public IActionResult GetLocationByCampaignAndLocationRuleId(int campaignId, + int userLocationRuleId) + { + if (campaignId < 1 || userLocationRuleId < 1) + { + return BadRequest(); + } + + var location = _context.Rules + .OfType() + .SingleOrDefault(c => c.CampaignId == campaignId && c.Id == userLocationRuleId); + + if (location is null) + { + return NotFound(); + } + + var locationDto = MapUserLocationRuleModelToDto(location); + + return Ok(locationDto); + } + + [HttpGet] + [Route("api/v1/campaigns/{campaignId:int}/locations")] + public IActionResult GetAllLocationsByCampaignId(int campaignId) + { + if (campaignId < 1) + { + return BadRequest(); + } + + var locationList = _context.Rules + .OfType() + .Where(c => c.CampaignId == campaignId) + .ToList(); + + if(locationList is null) + { + return Ok(); + } + + var locationDtoList = MapUserLocationRuleModelListToDtoList(locationList); + + return Ok(locationDtoList); + } + + [HttpPost] + [Route("api/v1/campaigns/{campaignId:int}/locations")] + public async Task CreateLocation(int campaignId, + [FromBody] UserLocationRuleDTO locationRuleDto) + { + if (campaignId < 1 || locationRuleDto is null) + { + return BadRequest(); + } + + var locationRule = MapUserLocationRuleDtoToModel(locationRuleDto); + locationRule.CampaignId = campaignId; + + await _context.Rules.AddAsync(locationRule); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetLocationByCampaignAndLocationRuleId), + new { campaignId = campaignId, locationRuleId = locationRule.Id }, null); + } + + [HttpDelete] + [Route("api/v1/campaigns/{campaignId:int}/locations/{userLocationRuleId:int}")] + public async Task DeleteLocationById(int campaignId, int userLocationRuleId) + { + if (campaignId < 1 || userLocationRuleId < 1) + { + return BadRequest(); + } + + var locationToDelete = _context.Rules + .OfType() + .SingleOrDefault(c => c.CampaignId == campaignId && c.Id == userLocationRuleId); + + if (locationToDelete is null) + { + return NotFound(); + } + + _context.Rules.Remove(locationToDelete); + await _context.SaveChangesAsync(); + + return NoContent(); + } + + + + private List MapUserLocationRuleModelListToDtoList(List userLocationRuleList) + { + var userLocationRuleDtoList = new List(); + + userLocationRuleList.ForEach(userLocationRule => userLocationRuleDtoList + .Add(MapUserLocationRuleModelToDto(userLocationRule))); + + return userLocationRuleDtoList; + } + + private UserLocationRuleDTO MapUserLocationRuleModelToDto(UserLocationRule userLocationRule) + { + return new UserLocationRuleDTO + { + Id = userLocationRule.Id, + Description = userLocationRule.Description, + LocationId = userLocationRule.LocationId + }; + } + + private UserLocationRule MapUserLocationRuleDtoToModel(UserLocationRuleDTO userLocationRuleDto) + { + return new UserLocationRule + { + Id = userLocationRuleDto.Id, + Description = userLocationRuleDto.Description, + LocationId = userLocationRuleDto.LocationId + }; + } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs index b5676db63..a43dcdbda 100644 --- a/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs +++ b/src/Services/Marketing/Marketing.API/Dto/CampaignDTO.cs @@ -1,7 +1,6 @@ namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto { using System; - using System.Collections.Generic; public class CampaignDTO { @@ -14,12 +13,5 @@ public DateTime To { get; set; } public string Url { get; set; } - - public List Rules { get; set; } - - public CampaignDTO() - { - Rules = new List(); - } } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs b/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs deleted file mode 100644 index a79756e9c..000000000 --- a/src/Services/Marketing/Marketing.API/Dto/RuleDTO.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto -{ - public class RuleDTO - { - public int Id { get; set; } - - public int RuleTypeId { get; set; } - - public int CampaignId { get; set; } - - public int? LocationId { get; set; } - - public string Description { get; set; } - } -} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/UserLocationRuleDTO.cs b/src/Services/Marketing/Marketing.API/Dto/UserLocationRuleDTO.cs new file mode 100644 index 000000000..a76eb90a0 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/Dto/UserLocationRuleDTO.cs @@ -0,0 +1,11 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto +{ + public class UserLocationRuleDTO + { + public int Id { get; set; } + + public int LocationId { get; set; } + + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs index 2fb45868b..15be03431 100644 --- a/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs +++ b/src/Services/Marketing/Marketing.API/Infrastructure/MarketingContext.cs @@ -64,9 +64,9 @@ .IsRequired(); builder.HasDiscriminator("RuleTypeId") - .HasValue(1) - .HasValue(2) - .HasValue(3); + .HasValue((int)RuleTypeEnum.UserProfileRule) + .HasValue((int)RuleTypeEnum.PurchaseHistoryRule) + .HasValue((int)RuleTypeEnum.UserLocationRule); builder.Property(r => r.Description) .HasColumnName("Description") diff --git a/src/Services/Marketing/Marketing.API/Marketing.API.csproj b/src/Services/Marketing/Marketing.API/Marketing.API.csproj index bd8e5a871..34d594e51 100644 --- a/src/Services/Marketing/Marketing.API/Marketing.API.csproj +++ b/src/Services/Marketing/Marketing.API/Marketing.API.csproj @@ -19,6 +19,7 @@ + @@ -36,6 +37,8 @@ + + diff --git a/src/Services/Marketing/Marketing.API/Model/Rule.cs b/src/Services/Marketing/Marketing.API/Model/Rule.cs index 03686bf86..e02128fa6 100644 --- a/src/Services/Marketing/Marketing.API/Model/Rule.cs +++ b/src/Services/Marketing/Marketing.API/Model/Rule.cs @@ -4,24 +4,30 @@ { public int Id { get; set; } - public int RuleTypeId { get; set; } - public int CampaignId { get; set; } public Campaign Campaign { get; set; } public string Description { get; set; } + + public abstract int RuleTypeId { get;} } public class UserProfileRule : Rule - { } + { + public override int RuleTypeId => (int)RuleTypeEnum.UserProfileRule; + } public class PurchaseHistoryRule : Rule - { } + { + public override int RuleTypeId => (int)RuleTypeEnum.PurchaseHistoryRule; + } public class UserLocationRule : Rule { + public override int RuleTypeId => (int)RuleTypeEnum.UserLocationRule; + public int LocationId { get; set; } } } \ No newline at end of file diff --git a/src/Services/Marketing/Marketing.API/Dto/RuleTypeEnum.cs b/src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs similarity index 71% rename from src/Services/Marketing/Marketing.API/Dto/RuleTypeEnum.cs rename to src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs index f00b7fbf9..c58dbf75c 100644 --- a/src/Services/Marketing/Marketing.API/Dto/RuleTypeEnum.cs +++ b/src/Services/Marketing/Marketing.API/Model/RuleTypeEnum.cs @@ -1,8 +1,8 @@ -using Microsoft.eShopOnContainers.Services.Marketing.API.Infrastructure.Exceptions; -using System; - -namespace Microsoft.eShopOnContainers.Services.Marketing.API.Dto +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 From 6ff6864f912be6a4434d3e8c9af57ecc7193b3ad Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Tue, 6 Jun 2017 14:19:01 +0200 Subject: [PATCH 26/36] Edit the integration test --- .../Marketing/CampaignScenarioBase.cs | 30 +++++++ ...etingScenarios.cs => CampaignScenarios.cs} | 20 ++--- .../Marketing/MarketingScenarioBase.cs | 45 ----------- .../Marketing/MarketingScenariosBase.cs | 20 +++++ .../Marketing/UserLocationRoleScenarios.cs | 80 +++++++++++++++++++ .../UserLocationRoleScenariosBase.cs | 40 ++++++++++ 6 files changed, 175 insertions(+), 60 deletions(-) create mode 100644 test/Services/IntegrationTests/Services/Marketing/CampaignScenarioBase.cs rename test/Services/IntegrationTests/Services/Marketing/{MarketingScenarios.cs => CampaignScenarios.cs} (88%) delete mode 100644 test/Services/IntegrationTests/Services/Marketing/MarketingScenarioBase.cs create mode 100644 test/Services/IntegrationTests/Services/Marketing/MarketingScenariosBase.cs create mode 100644 test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenarios.cs create mode 100644 test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenariosBase.cs diff --git a/test/Services/IntegrationTests/Services/Marketing/CampaignScenarioBase.cs b/test/Services/IntegrationTests/Services/Marketing/CampaignScenarioBase.cs new file mode 100644 index 000000000..a23fd1677 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/CampaignScenarioBase.cs @@ -0,0 +1,30 @@ +namespace IntegrationTests.Services.Marketing +{ + public class CampaignScenarioBase : MarketingScenarioBase + { + public static class Get + { + public static string Campaigns = CampaignsUrlBase; + + public static string CampaignBy(int id) + => $"{CampaignsUrlBase}/{id}"; + } + + 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}"; + } + } +} \ No newline at end of file diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs b/test/Services/IntegrationTests/Services/Marketing/CampaignScenarios.cs similarity index 88% rename from test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs rename to test/Services/IntegrationTests/Services/Marketing/CampaignScenarios.cs index 29dcbebda..8e27958b9 100644 --- a/test/Services/IntegrationTests/Services/Marketing/MarketingScenarios.cs +++ b/test/Services/IntegrationTests/Services/Marketing/CampaignScenarios.cs @@ -6,13 +6,11 @@ using Xunit; using System; using Newtonsoft.Json; - using Microsoft.eShopOnContainers.Services.Marketing.API.Model; - using System.Collections.Generic; using System.Net; using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; - public class MarketingScenarios - : MarketingScenarioBase + public class CampaignScenarios + : CampaignScenarioBase { [Fact] public async Task Get_get_all_campaigns_and_response_ok_status_code() @@ -29,10 +27,11 @@ [Fact] public async Task Get_get_campaign_by_id_and_response_ok_status_code() { + var campaignId = 1; using (var server = CreateServer()) { var response = await server.CreateClient() - .GetAsync(Get.CampaignBy(1)); + .GetAsync(Get.CampaignBy(campaignId)); response.EnsureSuccessStatusCode(); } @@ -44,7 +43,7 @@ using (var server = CreateServer()) { var response = await server.CreateClient() - .GetAsync(Get.CampaignBy(9999999)); + .GetAsync(Get.CampaignBy(int.MaxValue)); Assert.True(response.StatusCode == HttpStatusCode.NotFound); } @@ -122,15 +121,6 @@ From = DateTime.Now, To = DateTime.Now.AddDays(7), Url = "http://CampaignUrl.test/fdaf91ad0cef5419719f50198", - Rules = new List - { - new RuleDTO - { - LocationId = 1, - Description = "testDescription", - RuleTypeId = 3, - } - } }; } } diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingScenarioBase.cs b/test/Services/IntegrationTests/Services/Marketing/MarketingScenarioBase.cs deleted file mode 100644 index 01383410b..000000000 --- a/test/Services/IntegrationTests/Services/Marketing/MarketingScenarioBase.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace IntegrationTests.Services.Marketing -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.TestHost; - using System.IO; - - public class MarketingScenarioBase - { - private const string _campaignsUrlBase = "api/v1/campaigns"; - - public TestServer CreateServer() - { - var webHostBuilder = new WebHostBuilder(); - webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Marketing"); - webHostBuilder.UseStartup(); - - return new TestServer(webHostBuilder); - } - - public static class Get - { - public static string Campaigns = _campaignsUrlBase; - - public static string CampaignBy(int id) - => $"{_campaignsUrlBase}/{id}"; - } - - 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}"; - } - } -} \ No newline at end of file diff --git a/test/Services/IntegrationTests/Services/Marketing/MarketingScenariosBase.cs b/test/Services/IntegrationTests/Services/Marketing/MarketingScenariosBase.cs new file mode 100644 index 000000000..832fb1b82 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/MarketingScenariosBase.cs @@ -0,0 +1,20 @@ +namespace IntegrationTests.Services.Marketing +{ + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.TestHost; + using System.IO; + + public class MarketingScenarioBase + { + public static string CampaignsUrlBase => "api/v1/campaigns"; + + public TestServer CreateServer() + { + var webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseContentRoot(Directory.GetCurrentDirectory() + "\\Services\\Marketing"); + webHostBuilder.UseStartup(); + + return new TestServer(webHostBuilder); + } + } +} \ No newline at end of file diff --git a/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenarios.cs b/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenarios.cs new file mode 100644 index 000000000..f7fbd6cee --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenarios.cs @@ -0,0 +1,80 @@ +namespace IntegrationTests.Services.Marketing +{ + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using Xunit; + using System; + using Newtonsoft.Json; + using System.Net; + using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; + + public class UserLocationRoleScenarios + : UserLocationRoleScenariosBase + { + [Fact] + public async Task Get_get_all_user_location_rules_by_campaignId_and_response_ok_status_code() + { + var campaignId = 1; + + using (var server = CreateServer()) + { + var response = await server.CreateClient() + .GetAsync(Get.UserLocationRulesByCampaignId(campaignId)); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Post_add_new_user_location_rule_and_response_ok_status_code() + { + var campaignId = 1; + + using (var server = CreateServer()) + { + var fakeCampaignDto = GetFakeUserLocationRuleDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); + var response = await server.CreateClient() + .PostAsync(Post.AddNewuserLocationRule(campaignId), content); + + response.EnsureSuccessStatusCode(); + } + } + + [Fact] + public async Task Delete_delete_user_location_role_and_response_not_content_status_code() + { + var campaignId = 1; + + using (var server = CreateServer()) + { + var fakeCampaignDto = GetFakeUserLocationRuleDto(); + var content = new StringContent(JsonConvert.SerializeObject(fakeCampaignDto), Encoding.UTF8, "application/json"); + + //add user location role + var campaignResponse = await server.CreateClient() + .PostAsync(Post.AddNewuserLocationRule(campaignId), content); + + if (int.TryParse(campaignResponse.Headers.Location.Segments[6], out int userLocationRuleId)) + { + var response = await server.CreateClient() + .DeleteAsync(Delete.UserLocationRoleBy(campaignId, userLocationRuleId)); + + Assert.True(response.StatusCode == HttpStatusCode.NoContent); + } + + campaignResponse.EnsureSuccessStatusCode(); + } + } + + private static UserLocationRuleDTO GetFakeUserLocationRuleDto() + { + return new UserLocationRuleDTO + { + LocationId = 20, + Description = "FakeUserLocationRuleDescription" + }; + } + } +} diff --git a/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenariosBase.cs b/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenariosBase.cs new file mode 100644 index 000000000..cd6fcc9f3 --- /dev/null +++ b/test/Services/IntegrationTests/Services/Marketing/UserLocationRoleScenariosBase.cs @@ -0,0 +1,40 @@ +namespace IntegrationTests.Services.Marketing +{ + public class UserLocationRoleScenariosBase : MarketingScenarioBase + { + 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}"; + } +} \ No newline at end of file From 05bd964f2b8279cc6322458ed40d7f17b9067edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Thu, 8 Jun 2017 17:33:55 +0200 Subject: [PATCH 27/36] Adapt Locations.api to nosql db --- docker-compose.override.yml | 7 +- docker-compose.yml | 5 +- .../Controllers/LocationsController.cs | 18 ++ .../InternalServerErrorObjectResult.cs | 14 ++ .../Exceptions/LocationDomainException.cs | 21 ++ .../Filters/HttpGlobalExceptionFilter.cs | 67 +++++++ .../Infrastructure/LocationsContext.cs | 76 ++------ .../Infrastructure/LocationsContextSeed.cs | 182 +++++++++--------- .../20170601140634_Initial.Designer.cs | 116 ----------- .../Migrations/20170601140634_Initial.cs | 152 --------------- .../LocationsContextModelSnapshot.cs | 115 ----------- .../Repositories/ILocationsRepository.cs | 18 +- .../Repositories/IRepository.cs | 12 -- .../Repositories/IUnitOfWork.cs | 13 -- .../Repositories/LocationsRepository.cs | 67 ++++--- .../Services/ILocationsService.cs | 6 + .../Services/LocationsService.cs | 70 ++++--- .../Locations.API/LocationSettings.cs | 15 ++ .../Locations.API/Locations.API.csproj | 7 +- .../Locations.API/Model/FrontierPoints.cs | 21 -- .../Location/Locations.API/Model/Locations.cs | 107 ++-------- .../Locations.API/Model/UserLocation.cs | 30 ++- .../Location/Locations.API/Startup.cs | 24 +-- .../Location/Locations.API/appsettings.json | 3 +- .../Services/Locations/LocationsScenarios.cs | 26 ++- .../Services/Locations/appsettings.json | 3 +- 26 files changed, 414 insertions(+), 781 deletions(-) create mode 100644 src/Services/Location/Locations.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Exceptions/LocationDomainException.cs create mode 100644 src/Services/Location/Locations.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs delete mode 100644 src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.Designer.cs delete mode 100644 src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.cs delete mode 100644 src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs delete mode 100644 src/Services/Location/Locations.API/Infrastructure/Repositories/IRepository.cs delete mode 100644 src/Services/Location/Locations.API/Infrastructure/Repositories/IUnitOfWork.cs create mode 100644 src/Services/Location/Locations.API/LocationSettings.cs delete mode 100644 src/Services/Location/Locations.API/Model/FrontierPoints.cs diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a96720ba5..f23cd74f1 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -83,6 +83,10 @@ services: ports: - "5433:1433" + nosql.data: + ports: + - "27017:27017" + webstatus: environment: - ASPNETCORE_ENVIRONMENT=Development @@ -100,7 +104,8 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - ConnectionString=Server=sql.data;Database=Microsoft.eShopOnContainers.Services.LocationsDb;User Id=sa;Password=Pass@word + - ConnectionString=mongodb://nosql.data + - Database=LocationsDb - identityUrl=http://identity.api #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - EventBusConnection=rabbitmq ports: diff --git a/docker-compose.yml b/docker-compose.yml index e698992b2..01937cd38 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,6 +59,9 @@ services: sql.data: image: microsoft/mssql-server-linux + nosql.data: + image: mongo + basket.data: image: redis ports: @@ -81,4 +84,4 @@ services: context: ./src/Services/Location/Locations.API dockerfile: Dockerfile depends_on: - - sql.data \ No newline at end of file + - nosql.data \ No newline at end of file diff --git a/src/Services/Location/Locations.API/Controllers/LocationsController.cs b/src/Services/Location/Locations.API/Controllers/LocationsController.cs index dc2940b73..fed584f6b 100644 --- a/src/Services/Location/Locations.API/Controllers/LocationsController.cs +++ b/src/Services/Location/Locations.API/Controllers/LocationsController.cs @@ -20,6 +20,24 @@ namespace Locations.API.Controllers _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService)); } + //GET api/v1/[controller]/1 + [Route("{userId:int}")] + [HttpGet] + public async Task GetUserLocation(int userId) + { + var userLocation = await _locationsService.GetUserLocation(userId); + return Ok(userLocation); + } + + //GET api/v1/[controller]/locations + [Route("locations")] + [HttpGet] + public async Task GetAllLocations() + { + var userLocation = await _locationsService.GetAllLocation(); + return Ok(userLocation); + } + //POST api/v1/[controller]/ [Route("")] [HttpPost] diff --git a/src/Services/Location/Locations.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs b/src/Services/Location/Locations.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs new file mode 100644 index 000000000..772129da9 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/ActionResults/InternalServerErrorObjectResult.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.ActionResults +{ + public class InternalServerErrorObjectResult : ObjectResult + { + public InternalServerErrorObjectResult(object error) + : base(error) + { + StatusCode = StatusCodes.Status500InternalServerError; + } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Exceptions/LocationDomainException.cs b/src/Services/Location/Locations.API/Infrastructure/Exceptions/LocationDomainException.cs new file mode 100644 index 000000000..53016fcb2 --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Exceptions/LocationDomainException.cs @@ -0,0 +1,21 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Exceptions +{ + using System; + + /// + /// Exception type for app exceptions + /// + public class LocationDomainException : Exception + { + public LocationDomainException() + { } + + public LocationDomainException(string message) + : base(message) + { } + + public LocationDomainException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Location/Locations.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs new file mode 100644 index 000000000..30b9df1fa --- /dev/null +++ b/src/Services/Location/Locations.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -0,0 +1,67 @@ +namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Filters +{ + using AspNetCore.Mvc; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc.Filters; + using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.ActionResults; + using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Exceptions; + using Microsoft.Extensions.Logging; + using System.Net; + + public class HttpGlobalExceptionFilter : IExceptionFilter + { + private readonly IHostingEnvironment env; + private readonly ILogger logger; + + public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger logger) + { + this.env = env; + this.logger = logger; + } + + public void OnException(ExceptionContext context) + { + logger.LogError(new EventId(context.Exception.HResult), + context.Exception, + context.Exception.Message); + + if (context.Exception.GetType() == typeof(LocationDomainException)) + { + var json = new JsonErrorResponse + { + Messages = new[] { context.Exception.Message } + }; + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + //It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new BadRequestObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + else + { + var json = new JsonErrorResponse + { + Messages = new[] { "An error occur.Try it again." } + }; + + if (env.IsDevelopment()) + { + json.DeveloperMessage = context.Exception; + } + + // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 + // It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information + context.Result = new InternalServerErrorObjectResult(json); + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + context.ExceptionHandled = true; + } + + private class JsonErrorResponse + { + public string[] Messages { get; set; } + + public object DeveloperMessage { get; set; } + } + } +} diff --git a/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs b/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs index 5767694df..4289ffe68 100644 --- a/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs +++ b/src/Services/Location/Locations.API/Infrastructure/LocationsContext.cs @@ -1,72 +1,34 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure { - using Microsoft.EntityFrameworkCore; - using Microsoft.EntityFrameworkCore.Metadata.Builders; - using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories; using Microsoft.eShopOnContainers.Services.Locations.API.Model; - using System.Threading; - using System.Threading.Tasks; + using Microsoft.Extensions.Options; + using MongoDB.Driver; - public class LocationsContext : DbContext, IUnitOfWork + public class LocationsContext { - public LocationsContext(DbContextOptions options) : base(options) - { - } + private readonly IMongoDatabase _database = null; - public DbSet Locations { get; set; } - public DbSet FrontierPoints { get; set; } - public DbSet UserLocation { get; set; } - - protected override void OnModelCreating(ModelBuilder builder) + public LocationsContext(IOptions settings) { - builder.Entity(ConfigureLocations); - builder.Entity(ConfigureFrontierPoints); - builder.Entity(ConfigureUserLocation); + var client = new MongoClient(settings.Value.ConnectionString); + if (client != null) + _database = client.GetDatabase(settings.Value.Database); } - void ConfigureLocations(EntityTypeBuilder builder) + public IMongoCollection UserLocation { - builder.ToTable("Locations"); - - builder.HasKey(cl => cl.Id); - - builder.Property(cl => cl.Id) - .ForSqlServerUseSequenceHiLo("locations_seq") - .IsRequired(); - - builder.Property(cb => cb.Code) - .IsRequired() - .HasColumnName("LocationCode") - .HasMaxLength(15); - - builder.HasMany(f => f.Polygon) - .WithOne(l => l.Location) - .IsRequired(); - - builder.Property(cb => cb.Description) - .HasMaxLength(100); + get + { + return _database.GetCollection("UserLocation"); + } } - void ConfigureFrontierPoints(EntityTypeBuilder builder) + public IMongoCollection Locations { - builder.ToTable("FrontierPoints"); - - builder.HasKey(fp => fp.Id); - - builder.Property(fp => fp.Id) - .ForSqlServerUseSequenceHiLo("frontier_seq") - .IsRequired(); - } - - void ConfigureUserLocation(EntityTypeBuilder builder) - { - builder.ToTable("UserLocation"); - - builder.Property(ul => ul.Id) - .ForSqlServerUseSequenceHiLo("UserLocation_seq") - .IsRequired(); - - builder.HasIndex(ul => ul.UserId).IsUnique(); - } + get + { + return _database.GetCollection("Locations"); + } + } } } diff --git a/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs b/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs index 63404f49d..950698d97 100644 --- a/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs +++ b/src/Services/Location/Locations.API/Infrastructure/LocationsContextSeed.cs @@ -1,131 +1,123 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure { using Microsoft.AspNetCore.Builder; - using Microsoft.EntityFrameworkCore; + using Microsoft.eShopOnContainers.Services.Locations.API.Model; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using MongoDB.Bson; + using MongoDB.Driver; + using MongoDB.Driver.GeoJsonObjectModel; using System.Collections.Generic; - using System.Linq; using System.Threading.Tasks; - using Microsoft.eShopOnContainers.Services.Locations.API.Model; - using System.Text; public class LocationsContextSeed { + private static LocationsContext ctx; public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory) { - var context = (LocationsContext)applicationBuilder - .ApplicationServices.GetService(typeof(LocationsContext)); + var config = applicationBuilder + .ApplicationServices.GetRequiredService>(); - context.Database.Migrate(); + ctx = new LocationsContext(config); - if (!context.Locations.Any()) + if (!ctx.Locations.Database.GetCollection(nameof(Locations)).AsQueryable().Any()) { - context.Locations.AddRange( - GetPreconfiguredLocations()); - - await context.SaveChangesAsync(); - } - } - - static Locations GetPreconfiguredLocations() - { - var ww = new Locations("WW", "WorldWide", -1, -1); - ww.ChildLocations.Add(GetUSLocations()); - return ww; + await SetIndexes(); + await SetUSLocations(); + } } - static Locations GetUSLocations() + static async Task SetUSLocations() { - var us = new Locations("US", "United States", 41.650455, -101.357386, GetUSPoligon()); - us.ChildLocations.Add(GetWashingtonLocations()); - return us; - } - - static Locations GetWashingtonLocations() - { - var wht = new Locations("WHT", "Washington", 47.223652, -119.542781, GetWashingtonPoligon()); - wht.ChildLocations.Add(GetSeattleLocations()); - wht.ChildLocations.Add(GetRedmondLocations()); - return wht; - } - - static Locations GetSeattleLocations() - { - var bcn = new Locations("SEAT", "Seattle", 47.603111, -122.330747, GetSeattlePoligon()); - bcn.ChildLocations.Add(new Locations("SEAT-PioneerSqr", "Seattle Pioneer Square Shop", 47.602053, -122.333884, GetSeattlePioneerSqrPoligon())); - return bcn; + var us = new Locations() + { + Code = "US", + Description = "United States" + }; + us.SetLocation(-101.357386, 41.650455); + await ctx.Locations.InsertOneAsync(us); + await SetWashingtonLocations(us.Id); } - static Locations GetRedmondLocations() + static async Task SetWashingtonLocations(ObjectId parentId) { - var bcn = new Locations("REDM", "Redmond", 47.674961, -122.122887, GetRedmondPoligon()); - bcn.ChildLocations.Add(new Locations("REDM-DWNTWP", "Redmond Downtown Central Park Shop", 47.674433, -122.125006, GetRedmondDowntownParkPoligon())); - return bcn; - } - - static List GetUSPoligon() - { - var poligon = new List(); - poligon.Add(new FrontierPoints(48.7985, -62.88205)); - poligon.Add(new FrontierPoints(48.76513, -129.3132)); - poligon.Add(new FrontierPoints(30.12256, -120.9496)); - poligon.Add(new FrontierPoints(30.87114, -111.3944)); - poligon.Add(new FrontierPoints(24.24979, -78.11975)); - return poligon; + var wht = new Locations() + { + Parent_Id = parentId, + Code = "WHT", + Description = "Washington" + }; + wht.SetLocation(-119.542781, 47.223652); + await ctx.Locations.InsertOneAsync(wht); + await SetSeattleLocations(wht.Id); + await SetRedmondLocations(wht.Id); } - static List GetWashingtonPoligon() + static async Task SetSeattleLocations(ObjectId parentId) { - var poligon = new List(); - poligon.Add(new FrontierPoints(48.8943, -124.68633)); - poligon.Add(new FrontierPoints(45.66613, -124.32962)); - poligon.Add(new FrontierPoints(45.93384, -116.73824)); - poligon.Add(new FrontierPoints(49.04282, -116.96912)); - return poligon; + var stl = new Locations() + { + Parent_Id = parentId, + Code = "SEAT", + Description = "Seattle", + Polygon = GetSeattlePoligon() + }; + stl.SetLocation(-122.330747, 47.603111); + await ctx.Locations.InsertOneAsync(stl); } - static List GetSeattlePoligon() + static async Task SetRedmondLocations(ObjectId parentId) { - var poligon = new List(); - poligon.Add(new FrontierPoints(47.82929, -122.36238)); - poligon.Add(new FrontierPoints(47.6337, -122.42091)); - poligon.Add(new FrontierPoints(47.45224, -122.37371)); - poligon.Add(new FrontierPoints(47.50259, -122.20788)); - poligon.Add(new FrontierPoints(47.73644, -122.26934)); - return poligon; + var rdm = new Locations() + { + Parent_Id = parentId, + Code = "REDM", + Description = "Redmond", + Polygon = GetRedmondPoligon() + }; + rdm.SetLocation(-122.122887, 47.674961); + await ctx.Locations.InsertOneAsync(rdm); } - static List GetRedmondPoligon() + static async Task SetIndexes() { - var poligon = new List(); - poligon.Add(new FrontierPoints(47.73148, -122.15432)); - poligon.Add(new FrontierPoints(47.72559, -122.17673)); - poligon.Add(new FrontierPoints(47.67851, -122.16904)); - poligon.Add(new FrontierPoints(47.65036, -122.16136)); - poligon.Add(new FrontierPoints(47.62746, -122.15604)); - poligon.Add(new FrontierPoints(47.63463, -122.01562)); - poligon.Add(new FrontierPoints(47.74244, -122.04961)); - return poligon; - } + // Set location indexes + var builder = Builders.IndexKeys; + var keys = builder.Geo2DSphere(prop => prop.Location); + await ctx.Locations.Indexes.CreateOneAsync(keys); + } - static List GetSeattlePioneerSqrPoligon() + static GeoJsonPolygon GetSeattlePoligon() { - var poligon = new List(); - poligon.Add(new FrontierPoints(47.60338, -122.3327)); - poligon.Add(new FrontierPoints(47.60192, -122.33665)); - poligon.Add(new FrontierPoints(47.60096, -122.33456)); - poligon.Add(new FrontierPoints(47.60136, -122.33186)); - return poligon; + return new GeoJsonPolygon(new GeoJsonPolygonCoordinates( + new GeoJsonLinearRingCoordinates( + new List() + { + new GeoJson2DGeographicCoordinates(-122.36238,47.82929), + new GeoJson2DGeographicCoordinates(-122.42091,47.6337), + new GeoJson2DGeographicCoordinates(-122.37371,47.45224), + new GeoJson2DGeographicCoordinates(-122.20788,47.50259), + new GeoJson2DGeographicCoordinates(-122.26934,47.73644), + new GeoJson2DGeographicCoordinates(-122.36238,47.82929) + }))); } - static List GetRedmondDowntownParkPoligon() + static GeoJsonPolygon GetRedmondPoligon() { - var poligon = new List(); - poligon.Add(new FrontierPoints(47.67595, -122.12467)); - poligon.Add(new FrontierPoints(47.67449, -122.12862)); - poligon.Add(new FrontierPoints(47.67353, -122.12653)); - poligon.Add(new FrontierPoints(47.67368, -122.12197)); - return poligon; - } + return new GeoJsonPolygon(new GeoJsonPolygonCoordinates( + new GeoJsonLinearRingCoordinates( + new List() + { + new GeoJson2DGeographicCoordinates(47.73148, -122.15432), + new GeoJson2DGeographicCoordinates(47.72559, -122.17673), + new GeoJson2DGeographicCoordinates(47.67851, -122.16904), + new GeoJson2DGeographicCoordinates(47.65036, -122.16136), + new GeoJson2DGeographicCoordinates(47.62746, -122.15604), + new GeoJson2DGeographicCoordinates(47.63463, -122.01562), + new GeoJson2DGeographicCoordinates(47.74244, -122.04961), + new GeoJson2DGeographicCoordinates(47.73148, -122.15432), + }))); + } } } diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.Designer.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.Designer.cs deleted file mode 100644 index 441ec1d82..000000000 --- a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.Designer.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure; - -namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migrations -{ - [DbContext(typeof(LocationsContext))] - [Migration("20170601140634_Initial")] - partial class Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.2") - .HasAnnotation("SqlServer:Sequence:.frontier_seq", "'frontier_seq', '', '1', '10', '', '', 'Int64', 'False'") - .HasAnnotation("SqlServer:Sequence:.locations_seq", "'locations_seq', '', '1', '10', '', '', 'Int64', 'False'") - .HasAnnotation("SqlServer:Sequence:.UserLocation_seq", "'UserLocation_seq', '', '1', '10', '', '', 'Int64', 'False'") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "frontier_seq") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); - - b.Property("Latitude"); - - b.Property("LocationId") - .IsRequired(); - - b.Property("Longitude"); - - b.HasKey("Id"); - - b.HasIndex("LocationId"); - - b.ToTable("FrontierPoints"); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "locations_seq") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); - - b.Property("Code") - .IsRequired() - .HasColumnName("LocationCode") - .HasMaxLength(15); - - b.Property("Description") - .HasMaxLength(100); - - b.Property("Latitude"); - - b.Property("Longitude"); - - b.Property("ParentId"); - - b.HasKey("Id"); - - b.HasIndex("ParentId"); - - b.ToTable("Locations"); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "UserLocation_seq") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); - - b.Property("LocationId"); - - b.Property("UserId"); - - b.HasKey("Id"); - - b.HasIndex("LocationId"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("UserLocation"); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => - { - b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") - .WithMany("Polygon") - .HasForeignKey("LocationId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", b => - { - b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations") - .WithMany("ChildLocations") - .HasForeignKey("ParentId"); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation", b => - { - b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") - .WithMany() - .HasForeignKey("LocationId"); - }); - } - } -} diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.cs deleted file mode 100644 index 3073ca8ce..000000000 --- a/src/Services/Location/Locations.API/Infrastructure/Migrations/20170601140634_Initial.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; -using System.Text; - -namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migrations -{ - public partial class Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateSequence( - name: "frontier_seq", - incrementBy: 10); - - migrationBuilder.CreateSequence( - name: "locations_seq", - incrementBy: 10); - - migrationBuilder.CreateSequence( - name: "UserLocation_seq", - incrementBy: 10); - - migrationBuilder.CreateTable( - name: "Locations", - columns: table => new - { - Id = table.Column(nullable: false), - LocationCode = table.Column(maxLength: 15, nullable: false), - Description = table.Column(maxLength: 100, nullable: true), - Latitude = table.Column(nullable: false), - Longitude = table.Column(nullable: false), - ParentId = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Locations", x => x.Id); - table.ForeignKey( - name: "FK_Locations_Locations_ParentId", - column: x => x.ParentId, - principalTable: "Locations", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "FrontierPoints", - columns: table => new - { - Id = table.Column(nullable: false), - Latitude = table.Column(nullable: false), - LocationId = table.Column(nullable: false), - Longitude = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_FrontierPoints", x => x.Id); - table.ForeignKey( - name: "FK_FrontierPoints_Locations_LocationId", - column: x => x.LocationId, - principalTable: "Locations", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserLocation", - columns: table => new - { - Id = table.Column(nullable: false), - LocationId = table.Column(nullable: true), - UserId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserLocation", x => x.Id); - table.ForeignKey( - name: "FK_UserLocation_Locations_LocationId", - column: x => x.LocationId, - principalTable: "Locations", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateIndex( - name: "IX_FrontierPoints_LocationId", - table: "FrontierPoints", - column: "LocationId"); - - migrationBuilder.CreateIndex( - name: "IX_Locations_ParentId", - table: "Locations", - column: "ParentId"); - - migrationBuilder.CreateIndex( - name: "IX_UserLocation_LocationId", - table: "UserLocation", - column: "LocationId"); - - migrationBuilder.CreateIndex( - name: "IX_UserLocation_UserId", - table: "UserLocation", - column: "UserId", - unique: true); - - migrationBuilder.Sql(CreateGetDistanceFunction()); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "FrontierPoints"); - - migrationBuilder.DropTable( - name: "UserLocation"); - - migrationBuilder.DropTable( - name: "Locations"); - - migrationBuilder.DropSequence( - name: "frontier_seq"); - - migrationBuilder.DropSequence( - name: "locations_seq"); - - migrationBuilder.DropSequence( - name: "UserLocation_seq"); - - migrationBuilder.Sql(@"DROP FUNCTION IF EXISTS dbo.GetDistanceFromLocation"); - } - - private string CreateGetDistanceFunction() - { - var sb = new StringBuilder(); - sb.AppendLine(@"CREATE FUNCTION [dbo].[GetDistanceFromLocation]("); - sb.AppendLine(@"@CurrentLatitude float,"); - sb.AppendLine(@"@CurrentLongitude float,"); - sb.AppendLine(@"@latitude float,"); - sb.AppendLine(@"@longitude float)"); - sb.AppendLine(@"RETURNS int"); - sb.AppendLine(@"AS"); - sb.AppendLine(@"BEGIN"); - sb.AppendLine(@"DECLARE @geo1 geography = geography::Point(@CurrentLatitude, @CurrentLongitude, 4268),@geo2 geography = geography::Point(@latitude, @longitude, 4268)"); - sb.AppendLine(@"DECLARE @distance int"); - sb.AppendLine(@"SELECT @distance = @geo1.STDistance(@geo2)"); - sb.AppendLine(@"RETURN @distance"); - sb.AppendLine(@"END"); - return sb.ToString(); - } - - } -} diff --git a/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs b/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs deleted file mode 100644 index c01237ab2..000000000 --- a/src/Services/Location/Locations.API/Infrastructure/Migrations/LocationsContextModelSnapshot.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure; - -namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Migrations -{ - [DbContext(typeof(LocationsContext))] - partial class LocationsContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { - modelBuilder - .HasAnnotation("ProductVersion", "1.1.2") - .HasAnnotation("SqlServer:Sequence:.frontier_seq", "'frontier_seq', '', '1', '10', '', '', 'Int64', 'False'") - .HasAnnotation("SqlServer:Sequence:.locations_seq", "'locations_seq', '', '1', '10', '', '', 'Int64', 'False'") - .HasAnnotation("SqlServer:Sequence:.UserLocation_seq", "'UserLocation_seq', '', '1', '10', '', '', 'Int64', 'False'") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "frontier_seq") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); - - b.Property("Latitude"); - - b.Property("LocationId") - .IsRequired(); - - b.Property("Longitude"); - - b.HasKey("Id"); - - b.HasIndex("LocationId"); - - b.ToTable("FrontierPoints"); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "locations_seq") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); - - b.Property("Code") - .IsRequired() - .HasColumnName("LocationCode") - .HasMaxLength(15); - - b.Property("Description") - .HasMaxLength(100); - - b.Property("Latitude"); - - b.Property("Longitude"); - - b.Property("ParentId"); - - b.HasKey("Id"); - - b.HasIndex("ParentId"); - - b.ToTable("Locations"); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:HiLoSequenceName", "UserLocation_seq") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.SequenceHiLo); - - b.Property("LocationId"); - - b.Property("UserId"); - - b.HasKey("Id"); - - b.HasIndex("LocationId"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("UserLocation"); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.FrontierPoints", b => - { - b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") - .WithMany("Polygon") - .HasForeignKey("LocationId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", b => - { - b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations") - .WithMany("ChildLocations") - .HasForeignKey("ParentId"); - }); - - modelBuilder.Entity("Microsoft.eShopOnContainers.Services.Locations.API.Model.UserLocation", b => - { - b.HasOne("Microsoft.eShopOnContainers.Services.Locations.API.Model.Locations", "Location") - .WithMany() - .HasForeignKey("LocationId"); - }); - } - } -} diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs index bd3b58cb2..a0657e35d 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Repositories/ILocationsRepository.cs @@ -1,17 +1,25 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories { using Microsoft.eShopOnContainers.Services.Locations.API.Model; + using MongoDB.Bson; using System.Collections.Generic; using System.Threading.Tasks; - public interface ILocationsRepository : IRepository - { - UserLocation Add(UserLocation order); + public interface ILocationsRepository + { + Task GetAsync(ObjectId locationId); - void Update(UserLocation order); + Task> GetLocationListAsync(); - Task GetAsync(int userId); + Task GetUserLocationAsync(int userId); Task> GetNearestLocationListAsync(double lat, double lon); + + Task GetLocationByCurrentAreaAsync(Locations location); + + Task AddUserLocationAsync(UserLocation location); + + Task UpdateUserLocationAsync(UserLocation userLocation); + } } diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/IRepository.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/IRepository.cs deleted file mode 100644 index 946cf489c..000000000 --- a/src/Services/Location/Locations.API/Infrastructure/Repositories/IRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories -{ - public interface IRepository - { - IUnitOfWork UnitOfWork { get; } - } -} diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/IUnitOfWork.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/IUnitOfWork.cs deleted file mode 100644 index a81ac95e1..000000000 --- a/src/Services/Location/Locations.API/Infrastructure/Repositories/IUnitOfWork.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Repositories -{ - public interface IUnitOfWork : IDisposable - { - Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); - } -} diff --git a/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs b/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs index 98cb501b9..bb0039fd7 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Repositories/LocationsRepository.cs @@ -7,54 +7,67 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; + using Microsoft.Extensions.Options; + using MongoDB.Driver; + using MongoDB.Driver.GeoJsonObjectModel; + using MongoDB.Driver.Builders; + using MongoDB.Bson; public class LocationsRepository : ILocationsRepository { - private readonly LocationsContext _context; + private readonly LocationsContext _context; - public IUnitOfWork UnitOfWork + public LocationsRepository(IOptions settings) { - get - { - return _context; - } + _context = new LocationsContext(settings); + } + + public async Task GetAsync(ObjectId locationId) + { + var filter = Builders.Filter.Eq("Id", locationId); + return await _context.Locations + .Find(filter) + .FirstOrDefaultAsync(); } - public LocationsRepository(LocationsContext context) + public async Task GetUserLocationAsync(int userId) { - _context = context ?? throw new ArgumentNullException(nameof(context)); + var filter = Builders.Filter.Eq("UserId", userId); + return await _context.UserLocation + .Find(filter) + .FirstOrDefaultAsync(); } - public UserLocation Add(UserLocation location) + public async Task> GetLocationListAsync() { - return _context.UserLocation.Add(location).Entity; - + return await _context.Locations.Find(new BsonDocument()).ToListAsync(); } - public async Task GetAsync(int userId) + public async Task> GetNearestLocationListAsync(double lat, double lon) { - return await _context.UserLocation - .Where(ul => ul.UserId == userId) - .SingleOrDefaultAsync(); - } + var point = GeoJson.Point(GeoJson.Geographic(lon, lat)); + var query = new FilterDefinitionBuilder().Near(x => x.Location, point); + return await _context.Locations.Find(query).ToListAsync(); + } - public async Task> GetNearestLocationListAsync(double lat, double lon) + public async Task GetLocationByCurrentAreaAsync(Locations location) { - var query = $"SELECT TOP(100) location.* " + - $"FROM[dbo].[Locations] AS location " + - $"ORDER BY [dbo].[GetDistanceFromLocation](location.Latitude, location.Longitude, " + - $"{lat.ToString(CultureInfo.InvariantCulture)}, " + - $"{lon.ToString(CultureInfo.InvariantCulture)})"; + var query = new FilterDefinitionBuilder().GeoIntersects("Location", location.Polygon); + return await _context.Locations.Find(query).FirstOrDefaultAsync(); + } - return await _context.Locations.FromSql(query) - .Include(f => f.Polygon) - .ToListAsync(); + public async Task AddUserLocationAsync(UserLocation location) + { + await _context.UserLocation.InsertOneAsync(location); } - public void Update(UserLocation location) + public async Task UpdateUserLocationAsync(UserLocation userLocation) { - _context.Entry(location).State = EntityState.Modified; + await _context.UserLocation.ReplaceOneAsync( + doc => doc.UserId == userLocation.UserId, + userLocation, + new UpdateOptions { IsUpsert = true }); } } } diff --git a/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs b/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs index 672dd2703..98f2904e1 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Services/ILocationsService.cs @@ -1,10 +1,16 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Services { + using Microsoft.eShopOnContainers.Services.Locations.API.Model; using Microsoft.eShopOnContainers.Services.Locations.API.ViewModel; + using System.Collections.Generic; using System.Threading.Tasks; public interface ILocationsService { + Task GetUserLocation(int id); + + Task> GetAllLocation(); + Task AddOrUpdateUserLocation(string userId, LocationRequest locRequest); } } diff --git a/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs b/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs index d2b721399..81f89b9d6 100644 --- a/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs +++ b/src/Services/Location/Locations.API/Infrastructure/Services/LocationsService.cs @@ -5,6 +5,9 @@ using Microsoft.eShopOnContainers.Services.Locations.API.Model; using System; using System.Threading.Tasks; + using System.Linq; + using Microsoft.eShopOnContainers.Services.Locations.API.Infrastructure.Exceptions; + using System.Collections.Generic; public class LocationsService : ILocationsService { @@ -15,48 +18,53 @@ _locationsRepository = locationsRepository ?? throw new ArgumentNullException(nameof(locationsRepository)); } + public async Task GetUserLocation(int id) + { + return await _locationsRepository.GetUserLocationAsync(id); + } + + public async Task> GetAllLocation() + { + return await _locationsRepository.GetLocationListAsync(); + } + public async Task AddOrUpdateUserLocation(string id, LocationRequest currentPosition) { - int.TryParse(id, out int userId); - var currentUserLocation = await _locationsRepository.GetAsync(userId); - - // Get the nearest locations ordered by proximity + Locations currentUserAreaLocation = null; + + if (!int.TryParse(id, out int userId)) + { + throw new ArgumentException("Not valid userId"); + } + + // Get the nearest locations ordered var nearestLocationList = await _locationsRepository.GetNearestLocationListAsync(currentPosition.Latitude, currentPosition.Longitude); // Check out in which region we currently are - foreach(var locCandidate in nearestLocationList) + foreach(var locationCandidate in nearestLocationList.Where(x=> x.Polygon != null)) { - // Check location's tree and retrive user most specific area - var findNewLocationResult = locCandidate.GetUserMostSpecificLocation(currentPosition.Latitude, currentPosition.Longitude); - if (findNewLocationResult.isSuccess) - { - CreateUserLocation(currentUserLocation, findNewLocationResult.location, userId); - UpdateUserLocation(currentUserLocation, findNewLocationResult.location); - break; - } + currentUserAreaLocation = await _locationsRepository.GetLocationByCurrentAreaAsync(locationCandidate); + if(currentUserAreaLocation != null) { break; } } - var result = await _locationsRepository.UnitOfWork.SaveChangesAsync(); - return result > 0; - } - - private void CreateUserLocation(UserLocation currentUserLocation, Locations newLocation, int userId) - { - if (currentUserLocation is null) + if(currentUserAreaLocation is null) { - currentUserLocation = currentUserLocation ?? new UserLocation(userId); - currentUserLocation.Location = newLocation; - _locationsRepository.Add(currentUserLocation); - } - } + throw new LocationDomainException("User current area not found"); + } - private void UpdateUserLocation(UserLocation currentUserLocation, Locations newLocation) - { - if (currentUserLocation != null) + // If current area found, then update user location + if(currentUserAreaLocation != null) { - currentUserLocation.Location = newLocation; - _locationsRepository.Update(currentUserLocation); - } + var locationAncestors = new List(); + var userLocation = await _locationsRepository.GetUserLocationAsync(userId); + userLocation = userLocation ?? new UserLocation(); + userLocation.UserId = userId; + userLocation.LocationId = currentUserAreaLocation.Id; + userLocation.UpdateDate = DateTime.UtcNow; + await _locationsRepository.UpdateUserLocationAsync(userLocation); + } + + return true; } } } diff --git a/src/Services/Location/Locations.API/LocationSettings.cs b/src/Services/Location/Locations.API/LocationSettings.cs new file mode 100644 index 000000000..b3972239c --- /dev/null +++ b/src/Services/Location/Locations.API/LocationSettings.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.eShopOnContainers.Services.Locations.API +{ + public class LocationSettings + { + public string ExternalCatalogBaseUrl { get; set; } + public string EventBusConnection { get; set; } + public string ConnectionString { get; set; } + public string Database { get; set; } + } +} diff --git a/src/Services/Location/Locations.API/Locations.API.csproj b/src/Services/Location/Locations.API/Locations.API.csproj index 34808c5e0..57548fc03 100644 --- a/src/Services/Location/Locations.API/Locations.API.csproj +++ b/src/Services/Location/Locations.API/Locations.API.csproj @@ -7,9 +7,6 @@ aspnet-Locations.API-20161122013619 - - - @@ -28,6 +25,10 @@ + + + + diff --git a/src/Services/Location/Locations.API/Model/FrontierPoints.cs b/src/Services/Location/Locations.API/Model/FrontierPoints.cs deleted file mode 100644 index e9061a88f..000000000 --- a/src/Services/Location/Locations.API/Model/FrontierPoints.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Microsoft.eShopOnContainers.Services.Locations.API.Model -{ - public class FrontierPoints - { - public int Id { get; set; } - public double Latitude { get; private set; } - public double Longitude { get; private set; } - - public FrontierPoints() - { - } - - public FrontierPoints(double latitude, double longitude) - { - Latitude = latitude; - Longitude = longitude; - } - - public Locations Location { get; private set; } - } -} \ No newline at end of file diff --git a/src/Services/Location/Locations.API/Model/Locations.cs b/src/Services/Location/Locations.API/Model/Locations.cs index 71ffb603e..e35221bce 100644 --- a/src/Services/Location/Locations.API/Model/Locations.cs +++ b/src/Services/Location/Locations.API/Model/Locations.cs @@ -1,94 +1,27 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - - -namespace Microsoft.eShopOnContainers.Services.Locations.API.Model +namespace Microsoft.eShopOnContainers.Services.Locations.API.Model { + using MongoDB.Bson; + using MongoDB.Driver.GeoJsonObjectModel; + using System.Collections.Generic; + public class Locations { - public int Id { get; private set; } - public string Code { get; private set; } - public int? ParentId { get; private set; } - public string Description { get; private set; } - public double Latitude { get; private set; } - public double Longitude { get; private set; } - public (Locations location, bool isSuccess) GetUserMostSpecificLocation(double userLatitude, double userLongitude) - => CheckUserMostSpecificLocation(userLatitude, userLongitude); - - public Locations() - { - ChildLocations = new List(); - } - - public Locations(string code, string description, double latitude, double longitude, List polygon = null) : this() - { - Code = code; - Description = description; - Latitude = latitude; - Longitude = longitude; - Polygon = polygon ?? new List(); - } - - public virtual List Polygon { get; set; } - - [ForeignKey("ParentId")] - public virtual List ChildLocations { get; set; } - - private (Locations location, bool isSuccess) CheckUserMostSpecificLocation(double userLatitude, double userLongitude, Locations location = null) + public ObjectId Id { get; set; } + public string Code { get; set; } + public ObjectId Parent_Id { get; set; } + public string Description { get; set; } + public double Latitude { get; set; } + public double Longitude { get; set; } + public GeoJsonPoint Location { get; set; } + public GeoJsonPolygon Polygon { get; set; } + public void SetLocation(double lon, double lat) => SetPosition(lon, lat); + + private void SetPosition(double lon, double lat) { - Locations result = this; - var childLocations = location != null ? location.ChildLocations : ChildLocations; - - // Check if user is in location's area, if not then returns false - if (!CheckIsPointInPolygon(userLatitude, userLongitude)) - { - return (this, false); - } - - foreach (var childLocation in childLocations) - { - result = childLocation; - - if (childLocation.ChildLocations.Count == 0){ break; } - - CheckUserMostSpecificLocation(userLatitude, userLongitude, childLocation); - } - return (result, true); - } - - private bool CheckIsPointInPolygon(double lat, double lon) - { - if(Polygon.Count == 0) { return false; }; - double minX = Polygon[0].Latitude; - double maxX = Polygon[0].Latitude; - double minY = Polygon[0].Longitude; - double maxY = Polygon[0].Longitude; - for (int i = 1; i < Polygon.Count; i++) - { - FrontierPoints q = Polygon[i]; - minX = Math.Min(q.Latitude, minX); - maxX = Math.Max(q.Latitude, maxX); - minY = Math.Min(q.Longitude, minY); - maxY = Math.Max(q.Longitude, maxY); - } - - if (lat < minX || lat > maxX || lon < minY || lon > maxY) - { - return false; - } - - bool inside = false; - for (int i = 0, j = Polygon.Count - 1; i < Polygon.Count; j = i++) - { - if ((Polygon[i].Longitude > lon) != (Polygon[j].Latitude > lat) && - lat < (Polygon[j].Longitude - Polygon[i].Latitude) * (lon - Polygon[i].Longitude) / (Polygon[j].Longitude - Polygon[i].Longitude) + Polygon[i].Latitude) - { - inside = !inside; - } - } - - return inside; + Latitude = lat; + Longitude = lon; + Location = new GeoJsonPoint( + new GeoJson2DGeographicCoordinates(lon, lat)); } } } diff --git a/src/Services/Location/Locations.API/Model/UserLocation.cs b/src/Services/Location/Locations.API/Model/UserLocation.cs index f26196ace..ca60661c2 100644 --- a/src/Services/Location/Locations.API/Model/UserLocation.cs +++ b/src/Services/Location/Locations.API/Model/UserLocation.cs @@ -1,24 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Locations.API.Model +namespace Microsoft.eShopOnContainers.Services.Locations.API.Model { + using MongoDB.Bson.Serialization.Attributes; + using MongoDB.Bson; + using System; + using System.Collections.Generic; + public class UserLocation { - public int Id { get; set; } - public int UserId { get; set; } - - public UserLocation() - { - } - - public UserLocation(int userId) : this() - { - UserId = userId; - } - - public Locations Location { get; set; } + [BsonIgnoreIfDefault] + public ObjectId Id { get; set; } + public int UserId { get; set; } = 0; + public ObjectId LocationId { get; set; } + public DateTime UpdateDate { get; set; } } } diff --git a/src/Services/Location/Locations.API/Startup.cs b/src/Services/Location/Locations.API/Startup.cs index c13117b87..d601f26be 100644 --- a/src/Services/Location/Locations.API/Startup.cs +++ b/src/Services/Location/Locations.API/Startup.cs @@ -11,6 +11,7 @@ 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 { @@ -39,24 +40,13 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API public void ConfigureServices(IServiceCollection services) { // Add framework services. - services.AddMvc(); - - services.AddDbContext(options => + services.AddMvc(options => { - options.UseSqlServer(Configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); - //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }); - - // Changing default behavior when client evaluation occurs to throw. - // Default in EF Core would be to log a warning when client evaluation is performed. - options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); - //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval - }); + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + }).AddControllersAsServices(); + services.Configure(Configuration); + // Add framework services. services.AddSwaggerGen(options => { @@ -82,7 +72,7 @@ namespace Microsoft.eShopOnContainers.Services.Locations.API services.AddSingleton(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Services/Location/Locations.API/appsettings.json b/src/Services/Location/Locations.API/appsettings.json index 812cce207..a280150a3 100644 --- a/src/Services/Location/Locations.API/appsettings.json +++ b/src/Services/Location/Locations.API/appsettings.json @@ -1,5 +1,6 @@ { - "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.LocationsDb;User Id=sa;Password=Pass@word;", + "ConnectionString": "mongodb://nosql.data", + "Database": "LocationsDb", "IdentityUrl": "http://localhost:5105", "Logging": { "IncludeScopes": false, diff --git a/test/Services/IntegrationTests/Services/Locations/LocationsScenarios.cs b/test/Services/IntegrationTests/Services/Locations/LocationsScenarios.cs index 0c177d6f4..34b043909 100644 --- a/test/Services/IntegrationTests/Services/Locations/LocationsScenarios.cs +++ b/test/Services/IntegrationTests/Services/Locations/LocationsScenarios.cs @@ -1,4 +1,5 @@ -using Microsoft.eShopOnContainers.Services.Locations.API.ViewModel; +using Microsoft.eShopOnContainers.Services.Locations.API.Model; +using Microsoft.eShopOnContainers.Services.Locations.API.ViewModel; using Newtonsoft.Json; using System.Net.Http; using System.Text; @@ -11,26 +12,37 @@ namespace IntegrationTests.Services.Locations : LocationsScenarioBase { [Fact] - public async Task Set_new_user_location_response_ok_status_code() + public async Task Set_new_user_seattle_location_response_ok_status_code() { using (var server = CreateServer()) { - var content = new StringContent(BuildLocationsRequest(), UTF8Encoding.UTF8, "application/json"); + var userId = 1234; + var content = new StringContent(BuildLocationsRequest(-122.315752, 47.604610), UTF8Encoding.UTF8, "application/json"); var response = await server.CreateClient() .PostAsync(Post.AddNewLocation, content); response.EnsureSuccessStatusCode(); + + var userLocationResponse = await server.CreateClient() + .GetAsync(Get.LocationBy(userId)); + + var responseBody = await userLocationResponse.Content.ReadAsStringAsync(); + var userLocation = JsonConvert.DeserializeObject(responseBody); + + response.EnsureSuccessStatusCode(); + + } } - string BuildLocationsRequest() + string BuildLocationsRequest(double lon, double lat) { var location = new LocationRequest() { - Longitude = -122.333875, - Latitude = 47.602050 - }; + Longitude = lon, + Latitude = lat + }; return JsonConvert.SerializeObject(location); } } diff --git a/test/Services/IntegrationTests/Services/Locations/appsettings.json b/test/Services/IntegrationTests/Services/Locations/appsettings.json index c31d6c9f1..9769fb65a 100644 --- a/test/Services/IntegrationTests/Services/Locations/appsettings.json +++ b/test/Services/IntegrationTests/Services/Locations/appsettings.json @@ -1,5 +1,6 @@ { - "ConnectionString": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.LocationsDb;User Id=sa;Password=Pass@word;", + "ConnectionString": "mongodb://localhost:27017", + "Database": "LocationsDb", "ExternalCatalogBaseUrl": "http://localhost:5101", "IdentityUrl": "http://localhost:5105", "isTest": "true", From 3a10038d717f20e2652f56711eabad407f57102e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Tom=C3=A1s?= Date: Thu, 8 Jun 2017 17:45:07 +0200 Subject: [PATCH 28/36] Fix bug antiforgery decryption issue when deploying in a k8s cluster Changed in-memory grant store in idsrv to persist in db --- eShopOnContainers-ServicesAndWebApps.sln | 56 +++++++++++++++- k8s/deploy.ps1 | 2 +- k8s/deployments.yaml | 13 +++- k8s/keystore-data.yaml | 29 ++++++++ .../DataProtection/DataProtection.csproj | 13 ++++ .../DataProtectionBuilderExtensions.cs | 13 ++-- .../DataProtection}/RedisXmlRepository.cs | 24 +++---- .../Identity/Identity.API/Identity.API.csproj | 7 +- src/Services/Identity/Identity.API/Startup.cs | 66 ++++++++++--------- .../Identity/Identity.API/appsettings.json | 3 +- .../WebMVC/Controllers/AccountController.cs | 2 - src/Web/WebMVC/Startup.cs | 34 +++++----- src/Web/WebMVC/WebMVC.csproj | 1 + src/Web/WebMVC/appsettings.json | 1 + src/Web/WebMVC/wwwroot/css/site.min.css | 2 +- src/Web/WebSPA/Startup.cs | 11 +++- src/Web/WebSPA/WebSPA.csproj | 1 + 17 files changed, 196 insertions(+), 82 deletions(-) create mode 100644 k8s/keystore-data.yaml create mode 100644 src/BuildingBlocks/DataProtection/DataProtection/DataProtection.csproj rename src/{Services/Identity/Identity.API/Extensions => BuildingBlocks/DataProtection/DataProtection}/DataProtectionBuilderExtensions.cs (93%) rename src/{Services/Identity/Identity.API/Extensions => BuildingBlocks/DataProtection/DataProtection}/RedisXmlRepository.cs (95%) diff --git a/eShopOnContainers-ServicesAndWebApps.sln b/eShopOnContainers-ServicesAndWebApps.sln index d9c994954..edd18d523 100644 --- a/eShopOnContainers-ServicesAndWebApps.sln +++ b/eShopOnContainers-ServicesAndWebApps.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.12 +VisualStudioVersion = 15.0.26430.6 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{932D8224-11F6-4D07-B109-DA28AD288A63}" EndProject @@ -76,6 +76,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Health EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventBus.Tests", "src\BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataProtection", "DataProtection", "{88B22DBB-AA8F-4290-A454-2C109352C345}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProtection", "src\BuildingBlocks\DataProtection\DataProtection\DataProtection.csproj", "{23A33F9B-7672-426D-ACF9-FF8436ADC81A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1002,6 +1006,54 @@ Global {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x64.Build.0 = Release|Any CPU {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x86.ActiveCfg = Release|Any CPU {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D}.Release|x86.Build.0 = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|ARM.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|iPhone.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|x64.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|x64.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|x86.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.AppStore|x86.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|ARM.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|ARM.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|iPhone.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|x64.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|x64.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|x86.ActiveCfg = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Debug|x86.Build.0 = Debug|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|Any CPU.Build.0 = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|ARM.ActiveCfg = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|ARM.Build.0 = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|iPhone.ActiveCfg = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|iPhone.Build.0 = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|x64.ActiveCfg = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|x64.Build.0 = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|x86.ActiveCfg = Release|Any CPU + {23A33F9B-7672-426D-ACF9-FF8436ADC81A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1038,5 +1090,7 @@ Global {22A0F9C1-2D4A-4107-95B7-8459E6688BC5} = {A81ECBC2-6B00-4DCD-8388-469174033379} {4BD76717-3102-4969-8C2C-BAAA3F0263B6} = {A81ECBC2-6B00-4DCD-8388-469174033379} {89D80DF1-32E1-4AAF-970F-DA0AA6881F9D} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} + {88B22DBB-AA8F-4290-A454-2C109352C345} = {DB0EFB20-B024-4E5E-A75C-52143C131D25} + {23A33F9B-7672-426D-ACF9-FF8436ADC81A} = {88B22DBB-AA8F-4290-A454-2C109352C345} EndGlobalSection EndGlobal diff --git a/k8s/deploy.ps1 b/k8s/deploy.ps1 index 0619dd8e8..9930474ba 100644 --- a/k8s/deploy.ps1 +++ b/k8s/deploy.ps1 @@ -58,7 +58,7 @@ ExecKube -cmd 'delete configmap urls' # start sql, rabbitmq, frontend deploymentsExecKube -cmd 'delete configmap config-files' ExecKube -cmd 'create configmap config-files --from-file=nginx-conf=nginx.conf' ExecKube -cmd 'label configmap config-files app=eshop' -ExecKube -cmd 'create -f sql-data.yaml -f basket-data.yaml -f rabbitmq.yaml -f services.yaml -f frontend.yaml' +ExecKube -cmd 'create -f sql-data.yaml -f basket-data.yaml -f keystore-data.yaml -f rabbitmq.yaml -f services.yaml -f frontend.yaml' # building and publishing docker images not necessary when deploying through CI VSTS if(-not $deployCI) { diff --git a/k8s/deployments.yaml b/k8s/deployments.yaml index a0b1df135..62352e2ef 100644 --- a/k8s/deployments.yaml +++ b/k8s/deployments.yaml @@ -69,7 +69,6 @@ kind: Deployment metadata: name: identity spec: - replicas: 3 paused: true template: metadata: @@ -86,6 +85,10 @@ spec: value: http://0.0.0.0:80/identity - name: ConnectionStrings__DefaultConnection value: "Server=sql-data;Initial Catalog=Microsoft.eShopOnContainers.Services.IdentityDb;User Id=sa;Password=Pass@word" + - name: DPConnectionString + value: keystore-data + - name: IsClusterEnv + value: 'True' - name: MvcClient valueFrom: configMapKeyRef: @@ -153,6 +156,10 @@ spec: env: - name: ASPNETCORE_URLS value: http://0.0.0.0:80/webmvc + - name: DPConnectionString + value: keystore-data + - name: IsClusterEnv + value: 'True' - name: BasketUrl valueFrom: configMapKeyRef: @@ -256,6 +263,10 @@ spec: env: - name: ASPNETCORE_URLS value: http://0.0.0.0:80 + - name: DPConnectionString + value: keystore-data + - name: IsClusterEnv + value: 'True' - name: BasketUrl valueFrom: configMapKeyRef: diff --git a/k8s/keystore-data.yaml b/k8s/keystore-data.yaml new file mode 100644 index 000000000..3340cce35 --- /dev/null +++ b/k8s/keystore-data.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: eshop + component: keystore-data + name: keystore-data +spec: + ports: + - port: 6379 + selector: + app: eshop + component: keystore-data +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: keystore-data +spec: + template: + metadata: + labels: + app: eshop + component: keystore-data + spec: + containers: + - name: keystore-data + image: redis:3.2-alpine + diff --git a/src/BuildingBlocks/DataProtection/DataProtection/DataProtection.csproj b/src/BuildingBlocks/DataProtection/DataProtection/DataProtection.csproj new file mode 100644 index 000000000..bfe61a85a --- /dev/null +++ b/src/BuildingBlocks/DataProtection/DataProtection/DataProtection.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp1.1 + Microsoft.eShopOnContainers.BuildingBlocks + + + + + + + + \ No newline at end of file diff --git a/src/Services/Identity/Identity.API/Extensions/DataProtectionBuilderExtensions.cs b/src/BuildingBlocks/DataProtection/DataProtection/DataProtectionBuilderExtensions.cs similarity index 93% rename from src/Services/Identity/Identity.API/Extensions/DataProtectionBuilderExtensions.cs rename to src/BuildingBlocks/DataProtection/DataProtection/DataProtectionBuilderExtensions.cs index 11fc8342e..3db776b9a 100644 --- a/src/Services/Identity/Identity.API/Extensions/DataProtectionBuilderExtensions.cs +++ b/src/BuildingBlocks/DataProtection/DataProtection/DataProtectionBuilderExtensions.cs @@ -1,13 +1,11 @@ -namespace DataProtectionExtensions +namespace Microsoft.eShopOnContainers.BuildingBlocks { - using System; - using System.Linq; - using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection.Repositories; - using Microsoft.AspNetCore.DataProtection.XmlEncryption; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; + using System; + using System.Linq; using System.Net; /// @@ -46,10 +44,11 @@ { throw new ArgumentException("Redis connection string may not be empty.", nameof(redisConnectionString)); } - + var ips = Dns.GetHostAddressesAsync(redisConnectionString).Result; - return builder.Use(ServiceDescriptor.Singleton(services => new RedisXmlRepository(ips.First().ToString(), services.GetRequiredService>()))); + return builder.Use(ServiceDescriptor.Singleton(services => + new RedisXmlRepository(ips.First().ToString(), services.GetRequiredService>()))); } /// diff --git a/src/Services/Identity/Identity.API/Extensions/RedisXmlRepository.cs b/src/BuildingBlocks/DataProtection/DataProtection/RedisXmlRepository.cs similarity index 95% rename from src/Services/Identity/Identity.API/Extensions/RedisXmlRepository.cs rename to src/BuildingBlocks/DataProtection/DataProtection/RedisXmlRepository.cs index ebb36c1b7..f5a903b65 100644 --- a/src/Services/Identity/Identity.API/Extensions/RedisXmlRepository.cs +++ b/src/BuildingBlocks/DataProtection/DataProtection/RedisXmlRepository.cs @@ -1,17 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using Microsoft.AspNetCore.DataProtection.Repositories; -using Microsoft.Extensions.Logging; -using StackExchange.Redis; - -namespace DataProtectionExtensions +namespace Microsoft.eShopOnContainers.BuildingBlocks { + using Microsoft.AspNetCore.DataProtection.Repositories; + using Microsoft.Extensions.Logging; + using StackExchange.Redis; + using System; + using System.Collections.Generic; + using System.Text.RegularExpressions; + using System.Xml.Linq; + /// /// Key repository that stores XML encrypted keys in a Redis distributed cache. /// @@ -211,4 +207,4 @@ namespace DataProtectionExtensions } } } -} \ No newline at end of file +} diff --git a/src/Services/Identity/Identity.API/Identity.API.csproj b/src/Services/Identity/Identity.API/Identity.API.csproj index b3e499c6b..c27a51593 100644 --- a/src/Services/Identity/Identity.API/Identity.API.csproj +++ b/src/Services/Identity/Identity.API/Identity.API.csproj @@ -39,7 +39,6 @@ - @@ -59,6 +58,7 @@ + @@ -70,4 +70,9 @@ + + + + + diff --git a/src/Services/Identity/Identity.API/Startup.cs b/src/Services/Identity/Identity.API/Startup.cs index 98ed8578d..c5f72af13 100644 --- a/src/Services/Identity/Identity.API/Startup.cs +++ b/src/Services/Identity/Identity.API/Startup.cs @@ -1,29 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Identity.API.Certificate; +using Identity.API.Configuration; +using Identity.API.Data; +using Identity.API.Models; +using Identity.API.Services; +using IdentityServer4.EntityFramework.DbContexts; +using IdentityServer4.EntityFramework.Mappers; +using IdentityServer4.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.eShopOnContainers.BuildingBlocks; +using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Identity.API.Data; -using Identity.API.Models; -using Identity.API.Services; -using Identity.API.Configuration; -using IdentityServer4.Services; -using System.Threading; -using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.HealthChecks; -using Identity.API.Certificate; -using DataProtectionExtensions; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; -using IdentityServer4.Test; -using IdentityServer4.EntityFramework.DbContexts; -using IdentityServer4.EntityFramework.Mappers; +using System.Threading.Tasks; namespace eShopOnContainers.Identity { @@ -39,7 +37,7 @@ namespace eShopOnContainers.Identity if (env.IsDevelopment()) { // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 - //builder.AddUserSecrets(); + builder.AddUserSecrets(); } builder.AddEnvironmentVariables(); @@ -63,10 +61,14 @@ namespace eShopOnContainers.Identity services.AddMvc(); - services.AddDataProtection(opts => + if (Configuration.GetValue("IsClusterEnv") == bool.TrueString) { - opts.ApplicationDiscriminator = "eshop.identity"; - });//.PersistKeysToRedis("basket.data"); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.identity"; + }) + .PersistKeysToRedis(Configuration["DPConnectionString"]); + } services.AddHealthChecks(checks => { @@ -138,15 +140,15 @@ namespace eShopOnContainers.Identity template: "{controller=Home}/{action=Index}/{id?}"); }); - InitializeDatabase(app); + // Store idsrv grant config into db + InitializeGrantStoreAndConfiguration(app).Wait(); //Seed Data var hasher = new PasswordHasher(); - new ApplicationContextSeed(hasher).SeedAsync(app, loggerFactory) - .Wait(); + new ApplicationContextSeed(hasher).SeedAsync(app, loggerFactory).Wait(); } - private void InitializeDatabase(IApplicationBuilder app) + private async Task InitializeGrantStoreAndConfiguration(IApplicationBuilder app) { //callbacks urls from config: Dictionary clientUrls = new Dictionary(); @@ -164,27 +166,27 @@ namespace eShopOnContainers.Identity { foreach (var client in Config.GetClients(clientUrls)) { - context.Clients.Add(client.ToEntity()); + await context.Clients.AddAsync(client.ToEntity()); } - context.SaveChanges(); + await context.SaveChangesAsync(); } if (!context.IdentityResources.Any()) { foreach (var resource in Config.GetResources()) { - context.IdentityResources.Add(resource.ToEntity()); + await context.IdentityResources.AddAsync(resource.ToEntity()); } - context.SaveChanges(); + await context.SaveChangesAsync(); } if (!context.ApiResources.Any()) { foreach (var api in Config.GetApis()) { - context.ApiResources.Add(api.ToEntity()); + await context.ApiResources.AddAsync(api.ToEntity()); } - context.SaveChanges(); + await context.SaveChangesAsync(); } } } diff --git a/src/Services/Identity/Identity.API/appsettings.json b/src/Services/Identity/Identity.API/appsettings.json index c188cb721..557bac0c3 100644 --- a/src/Services/Identity/Identity.API/appsettings.json +++ b/src/Services/Identity/Identity.API/appsettings.json @@ -2,9 +2,10 @@ "ConnectionStrings": { "DefaultConnection": "Server=tcp:127.0.0.1,5433;Database=Microsoft.eShopOnContainers.Services.IdentityDb;User Id=sa;Password=Pass@word;" }, + "IsClusterEnv": "False", "MvcClient": "http://localhost:5100", "SpaClient": "http://localhost:5104", - "XamarinCallback": "http://localhost:5105/xamarincallback", + "XamarinCallback": "http://localhost:5105/xamarincallback", "Logging": { "IncludeScopes": false, "LogLevel": { diff --git a/src/Web/WebMVC/Controllers/AccountController.cs b/src/Web/WebMVC/Controllers/AccountController.cs index 531c6f547..a38207d58 100644 --- a/src/Web/WebMVC/Controllers/AccountController.cs +++ b/src/Web/WebMVC/Controllers/AccountController.cs @@ -24,8 +24,6 @@ namespace Microsoft.eShopOnContainers.WebMVC.Controllers var user = User as ClaimsPrincipal; var token = await HttpContext.Authentication.GetTokenAsync("access_token"); - //TODO - Not retrieving AccessToken yet - //var token = user.FindFirst("access_token"); if (token != null) { ViewData["access_token"] = token; diff --git a/src/Web/WebMVC/Startup.cs b/src/Web/WebMVC/Startup.cs index 1a6fc240b..f119e1d8e 100644 --- a/src/Web/WebMVC/Startup.cs +++ b/src/Web/WebMVC/Startup.cs @@ -1,22 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.eShopOnContainers.BuildingBlocks; +using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http; +using Microsoft.eShopOnContainers.WebMVC.Infrastructure; +using Microsoft.eShopOnContainers.WebMVC.Services; +using Microsoft.eShopOnContainers.WebMVC.ViewModels; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.HealthChecks; using Microsoft.Extensions.Logging; -using Microsoft.eShopOnContainers.WebMVC.ViewModels; -using Microsoft.eShopOnContainers.WebMVC.Services; +using System; using System.IdentityModel.Tokens.Jwt; -using Microsoft.IdentityModel.Tokens; -using Microsoft.AspNetCore.Http; -using System.Threading; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.HealthChecks; -using Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http; -using Microsoft.eShopOnContainers.WebMVC.Infrastructure; namespace Microsoft.eShopOnContainers.WebMVC { @@ -46,12 +41,15 @@ namespace Microsoft.eShopOnContainers.WebMVC { services.AddMvc(); - services.AddDataProtection(opts => + if (Configuration.GetValue("IsClusterEnv") == bool.TrueString) { - opts.ApplicationDiscriminator = "eshop.webmvc"; - }); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webmvc"; + }) + .PersistKeysToRedis(Configuration["DPConnectionString"]); + } - services.Configure(Configuration); services.AddHealthChecks(checks => diff --git a/src/Web/WebMVC/WebMVC.csproj b/src/Web/WebMVC/WebMVC.csproj index 9686de6f0..a9507394c 100644 --- a/src/Web/WebMVC/WebMVC.csproj +++ b/src/Web/WebMVC/WebMVC.csproj @@ -54,6 +54,7 @@ + diff --git a/src/Web/WebMVC/appsettings.json b/src/Web/WebMVC/appsettings.json index 796bc972f..4cb9ec7b8 100644 --- a/src/Web/WebMVC/appsettings.json +++ b/src/Web/WebMVC/appsettings.json @@ -4,6 +4,7 @@ "BasketUrl": "http://localhost:5103", "IdentityUrl": "http://localhost:5105", "CallBackUrl": "http://localhost:5100/", + "IsClusterEnv": "True", "UseResilientHttp": "True", "Logging": { "IncludeScopes": false, diff --git a/src/Web/WebMVC/wwwroot/css/site.min.css b/src/Web/WebMVC/wwwroot/css/site.min.css index 2a4a53b47..4d03fa783 100644 --- a/src/Web/WebMVC/wwwroot/css/site.min.css +++ b/src/Web/WebMVC/wwwroot/css/site.min.css @@ -1 +1 @@ -.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}.esh-app-footer-text{color:#83d01b;line-height:50px;text-align:right;width:100%}@font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item--navigable{display:inline-block;cursor:pointer}.esh-pager-item--navigable.is-disabled{opacity:0;pointer-events:none}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 4vw}}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:0;min-width:14rem;right:0;overflow:hidden;padding:.5rem;position:absolute;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:7rem;transition:height .35s}.esh-identity-item{cursor:pointer;display:block;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5) !important;line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-header-back:hover{color:#fff !important;transition:color .35s}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders_new{min-height:80vh}.esh-orders_new-header{background-color:#00a69c;height:4rem}.esh-orders_new-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders_new-back:hover{color:#fff;transition:color .35s}.esh-orders_new-section{padding:1rem 0}.esh-orders_new-section--right{text-align:right}.esh-orders_new-placeOrder{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-orders_new-placeOrder:hover{background-color:#4a760f;transition:all .35s}.esh-orders_new-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_new-title{font-size:1.25rem;text-transform:uppercase}.esh-orders_new-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_new-items--border:last-of-type{border-color:transparent}.esh-orders_new-item{font-size:1rem;font-weight:300}.esh-orders_new-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_new-item--middle{line-height:1rem}}.esh-orders_new-item--mark{color:#83d01b}.esh-orders_new-image{height:8rem}.esh-orders_detail{min-height:80vh}.esh-orders_detail-section{padding:1rem 0}.esh-orders_detail-section--right{text-align:right}.esh-orders_detail-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_detail-title{text-transform:uppercase}.esh-orders_detail-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_detail-items--border:last-of-type{border-color:transparent}.esh-orders_detail-item{font-size:1rem;font-weight:300}.esh-orders_detail-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_detail-item--middle{line-height:1rem}}.esh-orders_detail-item--mark{color:#83d01b}.esh-orders_detail-image{height:8rem}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;min-width:140px;-webkit-appearance:none}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-top:.65rem;margin-left:.5rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-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-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;transform:translateY(.5rem);padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{text-align:center;margin-bottom:1.5rem;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:none;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{text-align:center;font-weight:900;font-size:28px}.esh-catalog-price::before{content:'$'}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{border:none;border-radius:0;background-color:#83d01b;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basket-margin12{margin-left:12px}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s} \ No newline at end of file +.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}.esh-app-footer-text{color:#83d01b;line-height:50px;text-align:right;width:100%}@font-face{font-family:Montserrat;font-weight:400;src:url("../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{border:none;border-radius:0;background-color:#83d01b;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basket-margin12{margin-left:12px}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem;min-width:140px;-webkit-appearance:none}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-top:.65rem;margin-left:.5rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-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-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;transform:translateY(.5rem);padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{text-align:center;margin-bottom:1.5rem;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:none;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{text-align:center;font-weight:900;font-size:28px}.esh-catalog-price::before{content:'$'}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders_detail{min-height:80vh}.esh-orders_detail-section{padding:1rem 0}.esh-orders_detail-section--right{text-align:right}.esh-orders_detail-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_detail-title{text-transform:uppercase}.esh-orders_detail-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_detail-items--border:last-of-type{border-color:transparent}.esh-orders_detail-item{font-size:1rem;font-weight:300}.esh-orders_detail-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_detail-item--middle{line-height:1rem}}.esh-orders_detail-item--mark{color:#83d01b}.esh-orders_detail-image{height:8rem}.esh-orders_new{min-height:80vh}.esh-orders_new-header{background-color:#00a69c;height:4rem}.esh-orders_new-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders_new-back:hover{color:#fff;transition:color .35s}.esh-orders_new-section{padding:1rem 0}.esh-orders_new-section--right{text-align:right}.esh-orders_new-placeOrder{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-orders_new-placeOrder:hover{background-color:#4a760f;transition:all .35s}.esh-orders_new-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders_new-title{font-size:1.25rem;text-transform:uppercase}.esh-orders_new-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-orders_new-items--border:last-of-type{border-color:transparent}.esh-orders_new-item{font-size:1rem;font-weight:300}.esh-orders_new-item--middle{line-height:8rem}@media screen and (max-width:768px){.esh-orders_new-item--middle{line-height:1rem}}.esh-orders_new-item--mark{color:#83d01b}.esh-orders_new-image{height:8rem}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5) !important;line-height:4rem;text-transform:uppercase;text-decoration:none;transition:color .35s}.esh-header-back:hover{color:#fff !important;transition:color .35s}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:0;min-width:14rem;right:0;overflow:hidden;padding:.5rem;position:absolute;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:7rem;transition:height .35s}.esh-identity-item{cursor:pointer;display:block;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item--navigable{display:inline-block;cursor:pointer}.esh-pager-item--navigable.is-disabled{opacity:0;pointer-events:none}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 4vw}} \ No newline at end of file diff --git a/src/Web/WebSPA/Startup.cs b/src/Web/WebSPA/Startup.cs index 53476266a..d9e9d093b 100644 --- a/src/Web/WebSPA/Startup.cs +++ b/src/Web/WebSPA/Startup.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.HealthChecks; using Newtonsoft.Json.Serialization; using eShopOnContainers.WebSPA; +using Microsoft.eShopOnContainers.BuildingBlocks; namespace eShopConContainers.WebSPA { @@ -59,10 +60,14 @@ namespace eShopConContainers.WebSPA services.Configure(Configuration); - services.AddDataProtection(opts => + if (Configuration.GetValue("IsClusterEnv") == bool.TrueString) { - opts.ApplicationDiscriminator = "eshop.webspa"; - }); + services.AddDataProtection(opts => + { + opts.ApplicationDiscriminator = "eshop.webspa"; + }) + .PersistKeysToRedis(Configuration["DPConnectionString"]); + } services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); diff --git a/src/Web/WebSPA/WebSPA.csproj b/src/Web/WebSPA/WebSPA.csproj index 6f8678a74..90f2905dd 100644 --- a/src/Web/WebSPA/WebSPA.csproj +++ b/src/Web/WebSPA/WebSPA.csproj @@ -79,6 +79,7 @@ --> + From cdd15117fbbe35d9feb1a6900b80d1d9b13ffe32 Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 9 Jun 2017 11:59:56 +0200 Subject: [PATCH 29/36] Add locationService class --- .../Models/Location/LocationRequest.cs | 8 ++++++ .../Services/Location/ILocationService.cs | 10 +++++++ .../Services/Location/LocationService.cs | 28 +++++++++++++++++++ .../eShopOnContainers.Core.csproj | 5 +++- 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/LocationRequest.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationService.cs create mode 100644 src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/LocationService.cs diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/LocationRequest.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/LocationRequest.cs new file mode 100644 index 000000000..99654914c --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Models/Location/LocationRequest.cs @@ -0,0 +1,8 @@ +namespace eShopOnContainers.Core.Models.Location +{ + public class LocationRequest + { + public double Longitude { get; set; } + public double Latitude { get; set; } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationService.cs new file mode 100644 index 000000000..bc2c59915 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/ILocationService.cs @@ -0,0 +1,10 @@ +namespace eShopOnContainers.Core.Services.Location +{ + using System.Threading.Tasks; + using eShopOnContainers.Core.Models.Location; + + public interface ILocationService + { + Task UpdateUserLocation(LocationRequest newLocReq); + } +} \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/LocationService.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/LocationService.cs new file mode 100644 index 000000000..2149a5118 --- /dev/null +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Services/Location/LocationService.cs @@ -0,0 +1,28 @@ +namespace eShopOnContainers.Core.Services.Location +{ + using eShopOnContainers.Core.Models.Location; + using eShopOnContainers.Core.Services.RequestProvider; + using System; + using System.Threading.Tasks; + + public class LocationService : ILocationService + { + private readonly IRequestProvider _requestProvider; + + public LocationService(IRequestProvider requestProvider) + { + _requestProvider = requestProvider; + } + + public async Task UpdateUserLocation(LocationRequest newLocReq) + { + UriBuilder builder = new UriBuilder(GlobalSetting.Instance.LocationEndpoint); + + builder.Path = "api/v1/locations"; + + string uri = builder.ToString(); + + var result = await _requestProvider.PostAsync(uri, newLocReq); + } + } +} diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj index d0dd19a46..00ba88226 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/eShopOnContainers.Core.csproj @@ -1,4 +1,4 @@ - + @@ -70,6 +70,7 @@ + @@ -91,6 +92,8 @@ + + From b96faa369d57e1d991f16e2d8d9803d05868ce1b Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 9 Jun 2017 12:01:47 +0200 Subject: [PATCH 30/36] Add LocationService to RegisterDependencies --- .../ViewModels/Base/ViewModelLocator.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/Base/ViewModelLocator.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/Base/ViewModelLocator.cs index 90abb8f09..3eda91b42 100755 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/Base/ViewModelLocator.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/Base/ViewModelLocator.cs @@ -11,6 +11,7 @@ using eShopOnContainers.Core.Services.Identity; using eShopOnContainers.Core.Services.Order; using eShopOnContainers.Core.Services.User; using Xamarin.Forms; +using eShopOnContainers.Core.Services.Location; namespace eShopOnContainers.Core.ViewModels.Base { @@ -53,24 +54,25 @@ namespace eShopOnContainers.Core.ViewModels.Base builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As().SingleInstance(); - if (useMockServices) + if (useMockServices) { builder.RegisterInstance(new CatalogMockService()).As(); builder.RegisterInstance(new BasketMockService()).As(); builder.RegisterInstance(new OrderMockService()).As(); builder.RegisterInstance(new UserMockService()).As(); - UseMockService = true; + UseMockService = true; } else { builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); - UseMockService = false; + UseMockService = false; } if (_container != null) From 6a1d37b84f23a24c9563ecc7a224cebb75a0f54e Mon Sep 17 00:00:00 2001 From: Christian Arenas Date: Fri, 9 Jun 2017 12:02:39 +0200 Subject: [PATCH 31/36] Add Location Setting objects to view and ViewModel --- .../ViewModels/SettingsViewModel.cs | 156 ++++++++++++++++-- .../Views/SettingsView.xaml | 77 ++++++++- 2 files changed, 214 insertions(+), 19 deletions(-) diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs index 4596eb25c..8bfe9ed25 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/ViewModels/SettingsViewModel.cs @@ -4,38 +4,51 @@ using Xamarin.Forms; using System.Threading.Tasks; using eShopOnContainers.Core.Helpers; using eShopOnContainers.Core.Models.User; +using System; +using eShopOnContainers.Core.Models.Location; +using eShopOnContainers.Core.Services.Location; namespace eShopOnContainers.Core.ViewModels { public class SettingsViewModel : ViewModelBase { - private string _title; - private string _description; + private string _titleUseAzureServices; + private string _descriptionUseAzureServices; private bool _useAzureServices; + private string _titleUseFakeLocation; + private string _descriptionUseFakeLocation; + private bool _useFakeLocation; private string _endpoint; + private double _latitude; + private double _longitude; + private ILocationService _locationService; - public SettingsViewModel() + public SettingsViewModel(ILocationService locationService) { UseAzureServices = !Settings.UseMocks; + _useFakeLocation = Settings.UseFakeLocation; + _latitude = Settings.FakeLatitude; + _longitude = Settings.FakeLongitude; + _locationService = locationService; } - public string Title + public string TitleUseAzureServices { - get { return _title; } + get { return _titleUseAzureServices; } set { - _title = value; - RaisePropertyChanged(() => Title); + _titleUseAzureServices = value; + RaisePropertyChanged(() => TitleUseAzureServices); } } - public string Description + public string DescriptionUseAzureServices { - get { return _description; } + get { return _descriptionUseAzureServices; } set { - _description = value; - RaisePropertyChanged(() => Description); + _descriptionUseAzureServices = value; + RaisePropertyChanged(() => DescriptionUseAzureServices); } } @@ -52,6 +65,39 @@ namespace eShopOnContainers.Core.ViewModels } } + public string TitleUseFakeLocation + { + get { return _titleUseFakeLocation; } + set + { + _titleUseFakeLocation = value; + RaisePropertyChanged(() => TitleUseFakeLocation); + } + } + + public string DescriptionUseFakeLocation + { + get { return _descriptionUseFakeLocation; } + set + { + _descriptionUseFakeLocation = value; + RaisePropertyChanged(() => DescriptionUseFakeLocation); + } + } + + public bool UseFakeLocation + { + get { return _useFakeLocation; } + set + { + _useFakeLocation = value; + + // Save use fake location services to local storage + Settings.UseFakeLocation = _useFakeLocation; + RaisePropertyChanged(() => UseFakeLocation); + } + } + public string Endpoint { get { return _endpoint; } @@ -68,12 +114,47 @@ namespace eShopOnContainers.Core.ViewModels } } + public double Latitude + { + get { return _latitude; } + set + { + _latitude = value; + + UpdateLatitude(_latitude); + + RaisePropertyChanged(() => Latitude); + } + } + + public double Longitude + { + get { return _longitude; } + set + { + _longitude = value; + + UpdateLongitude(_longitude); + + RaisePropertyChanged(() => Longitude); + } + } + public ICommand ToggleMockServicesCommand => new Command(async () => await ToggleMockServicesAsync()); + public ICommand ToggleFakeLocationCommand => new Command(() => ToggleFakeLocationAsync()); + + public ICommand ToggleSendLocationCommand => new Command(async () => await ToggleSendLocationAsync()); + public override Task InitializeAsync(object navigationData) { UpdateInfo(); + UpdateInfoFakeLocation(); + Endpoint = Settings.UrlBase; + _latitude = Settings.FakeLatitude; + _longitude = Settings.FakeLongitude; + _useFakeLocation = Settings.UseFakeLocation; return base.InitializeAsync(navigationData); } @@ -100,17 +181,48 @@ namespace eShopOnContainers.Core.ViewModels } } - private void UpdateInfo() + private void ToggleFakeLocationAsync() + { + ViewModelLocator.RegisterDependencies(!UseAzureServices); + UpdateInfoFakeLocation(); + } + + private async Task ToggleSendLocationAsync() + { + LocationRequest locationRequest = new LocationRequest + { + Latitude = _latitude, + Longitude = _longitude + }; + + await _locationService.UpdateUserLocation(locationRequest); + } + + private void UpdateInfo() { if (!UseAzureServices) { - Title = "Use Mock Services"; - Description = "Mock Services are simulated objects that mimic the behavior of real services using a controlled approach."; + TitleUseAzureServices = "Use Mock Services"; + DescriptionUseAzureServices = "Mock Services are simulated objects that mimic the behavior of real services using a controlled approach."; } else { - Title = "Use Microservices/Containers from eShopOnContainers"; - Description = "When enabling the use of microservices/containers, the app will attempt to use real services deployed as Docker containers at the specified base endpoint, which will must be reachable through the network."; + TitleUseAzureServices = "Use Microservices/Containers from eShopOnContainers"; + DescriptionUseAzureServices = "When enabling the use of microservices/containers, the app will attempt to use real services deployed as Docker containers at the specified base endpoint, which will must be reachable through the network."; + } + } + + private void UpdateInfoFakeLocation() + { + if (!UseFakeLocation) + { + TitleUseFakeLocation = "Use Fake Location"; + DescriptionUseFakeLocation = "Fake Location are added for marketing campaign testing."; + } + else + { + TitleUseFakeLocation = "Use Real Location"; + DescriptionUseFakeLocation = "When enabling the use of real location, the app will attempt to use real location from the device."; } } @@ -119,5 +231,17 @@ namespace eShopOnContainers.Core.ViewModels // Update remote endpoint (save to local storage) Settings.UrlBase = endpoint; } + + private void UpdateLatitude(double latitude) + { + // Update fake latitude (save to local storage) + Settings.FakeLatitude = latitude; + } + + private void UpdateLongitude(double longitude) + { + // Update fake longitude (save to local storage) + Settings.FakeLongitude = longitude; + } } } \ No newline at end of file diff --git a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Views/SettingsView.xaml b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Views/SettingsView.xaml index 7db37dd66..a8da35daa 100644 --- a/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Views/SettingsView.xaml +++ b/src/Mobile/eShopOnContainers/eShopOnContainers.Core/Views/SettingsView.xaml @@ -108,11 +108,11 @@ Grid.Column="0" Grid.Row="1">