diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index f703c2944..e178d9bbc 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -1,73 +1,121 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.eShopOnContainers.Services.Catalog.API.Model; - + namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers { - [Route("/")] + using Microsoft.AspNetCore.Mvc; + using Microsoft.EntityFrameworkCore; + using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; + using System; + using System.Linq; + using System.Threading.Tasks; + using ViewModel; + + [Route("api/v1/[controller]")] public class CatalogController : ControllerBase { - private CatalogContext _context; + private readonly CatalogContext _context; public CatalogController(CatalogContext context) { _context = context; } - // GET api/values + // GET api/v1/[controller]/items/[?pageSize=3&pageIndex=10] + [HttpGet] - public IEnumerable Get() + [Route("[action]")] + public async Task Items(int pageSize = 10, int pageIndex = 0) { - return _context.CatalogItems.ToList(); + var totalItems = await _context.CatalogItems + .LongCountAsync(); + + var itemsOnPage = await _context.CatalogItems + .Skip(pageSize * pageIndex) + .Take(pageSize) + .ToListAsync(); + + var model = new PaginatedItemsViewModel( + pageIndex, pageSize, totalItems, itemsOnPage); + + return Ok(model); } - // GET api/values/5 - [HttpGet("{id}")] - public IActionResult Get(Guid id) + // GET api/v1/[controller]/items/withname/samplename + + [HttpGet] + [Route("[action]/withname/{name:minlength(1)}")] + public async Task Items(string name, int pageSize = 10, int pageIndex = 0) { - var item = _context.CatalogItems.FirstOrDefault(x=> x.Id == id); - if(item == null) - { - return NotFound(); - } + var totalItems = await _context.CatalogItems + .Where(c => c.Name.StartsWith(name)) + .LongCountAsync(); - return new OkObjectResult(item); + var itemsOnPage = await _context.CatalogItems + .Where(c => c.Name.StartsWith(name)) + .Skip(pageSize * pageIndex) + .Take(pageSize) + .ToListAsync(); + + var model = new PaginatedItemsViewModel( + pageIndex, pageSize, totalItems, itemsOnPage); + + return Ok(model); } - // POST api/values - [HttpPost] - public IActionResult Post([FromBody]CatalogItem item) + // GET api/v1/[controller]/items/type/1/brand/null + + [HttpGet] + [Route("[action]/type/{catalogTypeId}/brand/{catalogBrandId}")] + public async Task Items(int? catalogTypeId, int? catalogBrandId, int pageSize = 10, int pageIndex = 0) { - try + var root = (IQueryable)_context.CatalogItems; + + if (catalogTypeId.HasValue) { - _context.CatalogItems.Add(item); - _context.SaveChanges(); - return Ok(); + root = root.Where(ci => ci.CatalogTypeId == catalogTypeId); } - catch + + if (catalogBrandId.HasValue) { - return StatusCode(500, "Unable to add new catalog item"); + root = root.Where(ci => ci.CatalogBrandId == catalogBrandId); } + + var totalItems = await root + .LongCountAsync(); + + var itemsOnPage = await root + .Skip(pageSize * pageIndex) + .Take(pageSize) + .ToListAsync(); + + var model = new PaginatedItemsViewModel( + pageIndex, pageSize, totalItems, itemsOnPage); + + return Ok(model); } - // PUT api/values/5 - [HttpPut("{id}")] - public IActionResult Put(int id, [FromBody]CatalogItem item) + // GET api/v1/[controller]/CatalogTypes + + [HttpGet] + [Route("[action]")] + public async Task CatalogTypes() { - _context.CatalogItems.Update(item); - _context.SaveChanges(); - return Ok(); + var items = await _context.CatalogTypes + .ToListAsync(); + + return Ok(items); } - // DELETE api/values/5 - [HttpDelete("{id}")] - public IActionResult Delete(Guid id) + // GET api/v1/[controller]/CatalogBrands + + [HttpGet] + [Route("[action]")] + public async Task CatalogBrands() { - return Ok(); + var items = await _context.CatalogBrands + .ToListAsync(); + + return Ok(items); } } } diff --git a/src/Services/Catalog/Catalog.API/Dockerfile b/src/Services/Catalog/Catalog.API/Dockerfile index 99490a7c3..3e3ef42e8 100644 --- a/src/Services/Catalog/Catalog.API/Dockerfile +++ b/src/Services/Catalog/Catalog.API/Dockerfile @@ -1,4 +1,4 @@ -FROM microsoft/aspnetcore +FROM microsoft/aspnetcore:latest WORKDIR /app EXPOSE 80 ADD . /app diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogBrand.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogBrand.cs new file mode 100644 index 000000000..a8b3cc642 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogBrand.cs @@ -0,0 +1,15 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + + public class CatalogBrand + { + public int Id { get; set; } + + public string Brand { get; set; } + } +} diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs new file mode 100644 index 000000000..c2ebe40af --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContext.cs @@ -0,0 +1,95 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure +{ + using EntityFrameworkCore.Metadata.Builders; + using Microsoft.EntityFrameworkCore; + using Npgsql.EntityFrameworkCore.PostgreSQL; + + public class CatalogContext : DbContext + { + public CatalogContext(DbContextOptions options) : base(options) + { + } + + public DbSet CatalogItems { get; set; } + + public DbSet CatalogBrands { get; set; } + + public DbSet CatalogTypes { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.HasSequence("idseqcatalog") + .StartsAt(1) + .IncrementsBy(1); + + builder.HasSequence("idseqcatalogbrand") + .StartsAt(1) + .IncrementsBy(1); + + builder.HasSequence("idseqcatalogtype") + .StartsAt(1) + .IncrementsBy(1); + + builder.Entity(ConfigureCatalogBrand); + builder.Entity(ConfigureCatalogType); + builder.Entity(ConfigureCatalogItem); + + + builder.HasPostgresExtension("uuid-ossp"); + } + + void ConfigureCatalogItem(EntityTypeBuilder builder) + { + builder.ForNpgsqlToTable("catalog"); + + builder.Property(ci => ci.Id) + .HasDefaultValueSql("nextval('idseqcatalog')") + .IsRequired(); + + builder.Property(ci => ci.Name) + .IsRequired(true) + .HasMaxLength(50); + + builder.Property(ci => ci.Price) + .IsRequired(true); + + builder.Property(ci => ci.PictureUri) + .IsRequired(false); + + builder.HasOne(ci => ci.CatalogBrand) + .WithMany() + .HasForeignKey(ci => ci.CatalogBrandId); + + builder.HasOne(ci => ci.CatalogType) + .WithMany() + .HasForeignKey(ci => ci.CatalogTypeId); + + } + + void ConfigureCatalogBrand(EntityTypeBuilder builder) + { + builder.ForNpgsqlToTable("catalogbrand"); + + builder.Property(cb => cb.Id) + .HasDefaultValueSql("nextval('idseqcatalogbrand')") + .IsRequired(); + + builder.Property(cb => cb.Brand) + .IsRequired() + .HasMaxLength(100); + } + + void ConfigureCatalogType(EntityTypeBuilder builder) + { + builder.ForNpgsqlToTable("catalogtype"); + + builder.Property(cb => cb.Id) + .HasDefaultValueSql("nextval('idseqcatalogtype')") + .IsRequired(); + + builder.Property(cb => cb.Type) + .IsRequired() + .HasMaxLength(100); + } + } +} diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs new file mode 100644 index 000000000..a5852b331 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogContextSeed.cs @@ -0,0 +1,77 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure +{ + using Microsoft.AspNetCore.Builder; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + public class CatalogContextSeed + { + public static async Task SeedAsync(IApplicationBuilder applicationBuilder) + { + var context = (CatalogContext)applicationBuilder + .ApplicationServices.GetService(typeof(CatalogContext)); + + using (context) + { + context.Database.EnsureDeleted(); + + context.Database.EnsureCreated(); + + if (!context.CatalogBrands.Any()) + { + context.CatalogBrands.AddRange( + GetPreconfiguredCatalogBrands()); + + await context.SaveChangesAsync(); + } + + if (!context.CatalogTypes.Any()) + { + context.CatalogTypes.AddRange( + GetPreconfiguredCatalogTypes()); + + await context.SaveChangesAsync(); + } + + if (!context.CatalogItems.Any()) + { + context.CatalogItems.AddRange( + GetPreconfiguredItems()); + + await context.SaveChangesAsync(); + } + } + } + + static IEnumerable GetPreconfiguredCatalogBrands() + { + return new List() + { + new CatalogBrand() { Brand="Azure"}, + new CatalogBrand() { Brand = "Visual Studio" } + }; + } + + static IEnumerable GetPreconfiguredCatalogTypes() + { + return new List() + { + new CatalogType() { Type="Mug"}, + new CatalogType() { Type = "T-Shirt" } + }; + } + + static IEnumerable GetPreconfiguredItems() + { + return new List() + { + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = "Roslyn Red T-Shirt", Name = "Roslyn Red T-Shirt", Price = 12, PictureUri = "https://fakeimg.pl/370x240/EEEEEE/000/?text=RoslynRedT-Shirt" }, + new CatalogItem() { CatalogTypeId=1,CatalogBrandId=2, Description = "Cupt Black & White Mug", Name = "Cupt Black & White Mug", Price= 17, PictureUri = "https://fakeimg.pl/370x240/EEEEEE/000/?text=CuptBlack&WhiteMug" }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=2, Description = "Prism White T-Shirt", Name = "Prism White T-Shirt", Price = 12, PictureUri = "http://fakeimg.pl/370x240/EEEEEE/000/?text=.PrismWhiteT-Shirt" }, + new CatalogItem() { CatalogTypeId=2,CatalogBrandId=1, Description = ".NET Bot Black Sweatshirt", Name = ".NET Bot Black Sweatshirt", Price = 19.5M, PictureUri = "http://fakeimg.pl/370x240/EEEEEE/000/?text=.NETBotBlack" }, + }; + } + } +} diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogItem.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogItem.cs new file mode 100644 index 000000000..0c01486e1 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogItem.cs @@ -0,0 +1,27 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure +{ + using System; + + public class CatalogItem + { + public int Id { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public decimal Price { get; set; } + + public string PictureUri { get; set; } + + public int CatalogTypeId { get; set; } + + public CatalogType CatalogType { get; set; } + + public int CatalogBrandId { get; set; } + + public CatalogBrand CatalogBrand { get; set; } + + public CatalogItem() { } + } +} diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/CatalogType.cs b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogType.cs new file mode 100644 index 000000000..5577427fa --- /dev/null +++ b/src/Services/Catalog/Catalog.API/Infrastructure/CatalogType.cs @@ -0,0 +1,14 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + public class CatalogType + { + public int Id { get; set; } + + public string Type { get; set; } + } +} diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogContext.cs b/src/Services/Catalog/Catalog.API/Model/CatalogContext.cs deleted file mode 100644 index c8fa53cf6..000000000 --- a/src/Services/Catalog/Catalog.API/Model/CatalogContext.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Npgsql.EntityFrameworkCore.PostgreSQL; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model -{ - public class CatalogContext : DbContext - { - public CatalogContext(DbContextOptions options): base(options) - { - } - - public DbSet CatalogItems { get; set; } - - protected override void OnModelCreating(ModelBuilder builder) - { - builder.HasPostgresExtension("uuid-ossp"); - } - } -} diff --git a/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs b/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs deleted file mode 100644 index abc3f91af..000000000 --- a/src/Services/Catalog/Catalog.API/Model/CatalogItem.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopOnContainers.Services.Catalog.API.Model -{ - public class CatalogItem - { - public CatalogItem() - { - } - - public Guid Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public decimal Price { get; set; } - public int ImageCount { get; set; } - } -} diff --git a/src/Services/Catalog/Catalog.API/Program.cs b/src/Services/Catalog/Catalog.API/Program.cs index 82ca9383e..1eaf35596 100644 --- a/src/Services/Catalog/Catalog.API/Program.cs +++ b/src/Services/Catalog/Catalog.API/Program.cs @@ -14,6 +14,7 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API { var host = new WebHostBuilder() .UseKestrel() + .UseUrls(Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? String.Empty) .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup() .Build(); diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs index 80485de5d..9507678a2 100644 --- a/src/Services/Catalog/Catalog.API/Startup.cs +++ b/src/Services/Catalog/Catalog.API/Startup.cs @@ -1,48 +1,81 @@ -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.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.eShopOnContainers.Services.Catalog.API.Model; -using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Serialization; +using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.Services.Catalog.API { public class Startup { + public IConfigurationRoot Configuration { get; } + public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) - .AddJsonFile("settings.json") + .AddJsonFile($"settings.{env.EnvironmentName}.json",optional:false) .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.AddDbContext(c => { + services.AddSingleton(Configuration); + + services.AddDbContext(c => + { c.UseNpgsql(Configuration["ConnectionString"]); + c.ConfigureWarnings(wb => + { + wb.Throw(RelationalEventId.QueryClientEvaluationWarning); + }); }); // Add framework services. + + services.AddCors(); + services.AddMvcCore() - .AddJsonFormatters(); + .AddJsonFormatters(settings=> + { + settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); } // 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 + + if(env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); + //Seed Data + + CatalogContextSeed.SeedAsync(app) + .Wait(); + + // Use frameworks + app.UseCors(policyBuilder=>policyBuilder.AllowAnyOrigin()); + app.UseMvc(); } } diff --git a/src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs b/src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs new file mode 100644 index 000000000..844f38e2b --- /dev/null +++ b/src/Services/Catalog/Catalog.API/ViewModel/PaginatedItemsViewModel.cs @@ -0,0 +1,24 @@ +namespace Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel +{ + using System.Collections.Generic; + + + public class PaginatedItemsViewModel where TEntity : class + { + public int PageIndex { get; private set; } + + public int PageSize { get; private set; } + + public long Count { get; private set; } + + public IEnumerable Data { get; private set; } + + public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable data) + { + this.PageIndex = pageIndex; + this.PageSize = pageSize; + this.Count = count; + this.Data = data; + } + } +} diff --git a/src/Services/Catalog/Catalog.API/project.json b/src/Services/Catalog/Catalog.API/project.json index 563e4d136..50733a295 100644 --- a/src/Services/Catalog/Catalog.API/project.json +++ b/src/Services/Catalog/Catalog.API/project.json @@ -1,21 +1,25 @@ { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0", + "version": "1.0.1", "type": "platform" - }, - "Microsoft.AspNetCore.Mvc": "1.0.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", - "Microsoft.EntityFrameworkCore": "1.0.0", + }, + "Microsoft.AspNetCore.Mvc": "1.0.1", + "Microsoft.AspNetCore.Diagnostics": "1.0.0", + "Microsoft.AspNetCore.Diagnostics.Abstractions": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", + "Microsoft.EntityFrameworkCore": "1.0.1", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0", "Microsoft.Extensions.Logging": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.Logging.Debug": "1.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", - "Npgsql.EntityFrameworkCore.PostgreSQL": "1.0.1" + "Npgsql.EntityFrameworkCore.PostgreSQL": "1.0.2" + }, + "tools": { + }, - "tools": {}, "frameworks": { "netcoreapp1.0": { "imports": [ @@ -39,7 +43,7 @@ "wwwroot", "Views", "Areas/**/Views", - "settings.json", + "settings.Production.json", "web.config", "project.json", "Dockerfile" diff --git a/src/Services/Catalog/Catalog.API/settings.json b/src/Services/Catalog/Catalog.API/settings.development.json similarity index 61% rename from src/Services/Catalog/Catalog.API/settings.json rename to src/Services/Catalog/Catalog.API/settings.development.json index e4f7496f5..a562552b2 100644 --- a/src/Services/Catalog/Catalog.API/settings.json +++ b/src/Services/Catalog/Catalog.API/settings.development.json @@ -1,5 +1,5 @@ { - "ConnectionString": "Server=127.0.0.1;Port=5432;Database=postgres;username=postgres", + "ConnectionString": "Server=127.0.0.1;Port=5432;Database=CatalogDB;username=postgres;password=postgres", "Logging": { "IncludeScopes": false, "LogLevel": { diff --git a/src/Services/Catalog/Catalog.API/settings.production.json b/src/Services/Catalog/Catalog.API/settings.production.json new file mode 100644 index 000000000..a562552b2 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/settings.production.json @@ -0,0 +1,11 @@ +{ + "ConnectionString": "Server=127.0.0.1;Port=5432;Database=CatalogDB;username=postgres;password=postgres", + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +}