From 53857d052f403421c5d68bdc3cd81588c7dcacb1 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Tue, 29 May 2018 12:18:39 -0700 Subject: [PATCH] Add BasicApi and BasicViews apps - #7805 - make initial copy of apps from aspnet/Performance repo - add apps to solution - add Readme for the benchmark apps - update BasicApi app to actually do authentication and authorization - bug in the ported app - refactor `Main` methods and add `CreateWebHostBuilder(...)` methods - change projects to understand `$(BenchmarksTargetFramework)` - use NuGet.org EF packages to avoid changing the Universe build graph - use SQLite instead of LocalDb by default - remove unnecessary appsettings.json files and JSON configuration support - add EF migrations - (greatly) reduce startup times compared to creating / deleting databases - add MySql, PostgreSQL, and SqlServer support - load BasicApi data in a `DbContext.OnModelCreating(...)` override - no longer need seed.sql - generalize migrations to support multiple providers - use negative seeding indices to work around npgsql/Npgsql.EntityFrameworkCore.PostgreSQL#36 - work around Pomelo lack of strong name (PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#603) - use BenchmarksOnly* properties for EF dependencies - see also aspnet/Universe#1224 - drop databases (if SQLite) or migrations (otherwise) in `IApplicationLifetime.ApplicationStopping` handlers - add functional tests - drop SQLite database at end of test run - add benchmarks automation - add anonymous BasicApi action i.e. require no authorization - add non-antiforgery BasicViews actions Address PR comments - remove `AntiforgeryTestHelper` workarounds - use `[ApiController]` - use `ActionResult` - remove unused classes nits: - take VS suggestions in added files - optionally display create and delete SQL scripts for per-database migrations - merge `InsertData(...)` calls for consistency with most supported EF providers - SQLite is the only one that requires separate `INSERT`s and EF does the splitting --- Mvc.sln | 32 +++ benchmarkapps/BasicApi/BasicApi.csproj | 43 +++ .../BasicApi/Controllers/PetController.cs | 139 ++++++++++ .../BasicApi/Controllers/TokenController.cs | 74 ++++++ .../20180609000420_InitialCreate.Designer.cs | 172 ++++++++++++ .../20180609000420_InitialCreate.cs | 218 ++++++++++++++++ .../BasicApiContextModelSnapshot.cs | 170 ++++++++++++ .../BasicApi/Models/BasicApiContext.cs | 190 ++++++++++++++ benchmarkapps/BasicApi/Models/Category.cs | 12 + benchmarkapps/BasicApi/Models/Image.cs | 12 + benchmarkapps/BasicApi/Models/Pet.cs | 31 +++ benchmarkapps/BasicApi/Models/Tag.cs | 12 + benchmarkapps/BasicApi/Startup.cs | 246 ++++++++++++++++++ benchmarkapps/BasicApi/benchmarks.json | 48 ++++ benchmarkapps/BasicApi/getWithToken.lua | 51 ++++ benchmarkapps/BasicApi/postJsonWithToken.lua | 83 ++++++ .../BasicApi/runtimeconfig.template.json | 5 + benchmarkapps/BasicViews/BasicViews.csproj | 42 +++ benchmarkapps/BasicViews/BasicViewsContext.cs | 17 ++ .../BasicViews/Components/CurrentUser.cs | 19 ++ .../BasicViews/Controllers/HomeController.cs | 75 ++++++ .../20180609000611_InitialCreate.Designer.cs | 44 ++++ .../20180609000611_InitialCreate.cs | 39 +++ .../BasicViewsContextModelSnapshot.cs | 42 +++ benchmarkapps/BasicViews/Person.cs | 21 ++ benchmarkapps/BasicViews/Startup.cs | 207 +++++++++++++++ .../BasicViews/Views/Home/HtmlHelpers.cshtml | 23 ++ .../BasicViews/Views/Home/Index.cshtml | 21 ++ .../BasicViews/Views/Shared/_Layout.cshtml | 55 ++++ .../BasicViews/Views/_ViewImports.cshtml | 1 + .../BasicViews/Views/_ViewStart.cshtml | 3 + benchmarkapps/BasicViews/benchmarks.json | 39 +++ benchmarkapps/BasicViews/post.lua | 7 + benchmarkapps/BasicViews/postWithToken.lua | 55 ++++ .../BasicViews/runtimeconfig.template.json | 5 + benchmarkapps/BasicViews/web.config | 9 + benchmarkapps/BasicViews/wwwroot/css/site.css | 6 + .../BasicViews/wwwroot/css/site.min.css | 6 + benchmarkapps/BasicViews/wwwroot/js/site.js | 3 + .../BasicViews/wwwroot/js/site.min.js | 3 + benchmarkapps/README.md | 15 ++ build/dependencies.props | 16 ++ .../BasicApiTest.cs | 223 ++++++++++++++++ .../BasicViewsTest.cs | 83 ++++++ .../Infrastructure/BasicApiFixture.cs | 21 ++ .../Infrastructure/BasicViewsFixture.cs | 21 ++ ...soft.AspNetCore.Mvc.FunctionalTests.csproj | 2 + 47 files changed, 2661 insertions(+) create mode 100644 benchmarkapps/BasicApi/BasicApi.csproj create mode 100644 benchmarkapps/BasicApi/Controllers/PetController.cs create mode 100644 benchmarkapps/BasicApi/Controllers/TokenController.cs create mode 100644 benchmarkapps/BasicApi/Migrations/20180609000420_InitialCreate.Designer.cs create mode 100644 benchmarkapps/BasicApi/Migrations/20180609000420_InitialCreate.cs create mode 100644 benchmarkapps/BasicApi/Migrations/BasicApiContextModelSnapshot.cs create mode 100644 benchmarkapps/BasicApi/Models/BasicApiContext.cs create mode 100644 benchmarkapps/BasicApi/Models/Category.cs create mode 100644 benchmarkapps/BasicApi/Models/Image.cs create mode 100644 benchmarkapps/BasicApi/Models/Pet.cs create mode 100644 benchmarkapps/BasicApi/Models/Tag.cs create mode 100644 benchmarkapps/BasicApi/Startup.cs create mode 100644 benchmarkapps/BasicApi/benchmarks.json create mode 100644 benchmarkapps/BasicApi/getWithToken.lua create mode 100644 benchmarkapps/BasicApi/postJsonWithToken.lua create mode 100644 benchmarkapps/BasicApi/runtimeconfig.template.json create mode 100644 benchmarkapps/BasicViews/BasicViews.csproj create mode 100644 benchmarkapps/BasicViews/BasicViewsContext.cs create mode 100644 benchmarkapps/BasicViews/Components/CurrentUser.cs create mode 100644 benchmarkapps/BasicViews/Controllers/HomeController.cs create mode 100644 benchmarkapps/BasicViews/Migrations/20180609000611_InitialCreate.Designer.cs create mode 100644 benchmarkapps/BasicViews/Migrations/20180609000611_InitialCreate.cs create mode 100644 benchmarkapps/BasicViews/Migrations/BasicViewsContextModelSnapshot.cs create mode 100644 benchmarkapps/BasicViews/Person.cs create mode 100644 benchmarkapps/BasicViews/Startup.cs create mode 100644 benchmarkapps/BasicViews/Views/Home/HtmlHelpers.cshtml create mode 100644 benchmarkapps/BasicViews/Views/Home/Index.cshtml create mode 100644 benchmarkapps/BasicViews/Views/Shared/_Layout.cshtml create mode 100644 benchmarkapps/BasicViews/Views/_ViewImports.cshtml create mode 100644 benchmarkapps/BasicViews/Views/_ViewStart.cshtml create mode 100644 benchmarkapps/BasicViews/benchmarks.json create mode 100644 benchmarkapps/BasicViews/post.lua create mode 100644 benchmarkapps/BasicViews/postWithToken.lua create mode 100644 benchmarkapps/BasicViews/runtimeconfig.template.json create mode 100644 benchmarkapps/BasicViews/web.config create mode 100644 benchmarkapps/BasicViews/wwwroot/css/site.css create mode 100644 benchmarkapps/BasicViews/wwwroot/css/site.min.css create mode 100644 benchmarkapps/BasicViews/wwwroot/js/site.js create mode 100644 benchmarkapps/BasicViews/wwwroot/js/site.min.js create mode 100644 benchmarkapps/README.md create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicApiTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicViewsTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/BasicApiFixture.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/BasicViewsFixture.cs diff --git a/Mvc.sln b/Mvc.sln index f03885947a..538cacced1 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -170,6 +170,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorPagesClassLibrary", "t EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Views.TestCommon", "test\Microsoft.AspNetCore.Mvc.Views.TestCommon\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj", "{51E3E785-A9D1-4196-BAFE-A17FF4304B89}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarkapps", "benchmarkapps", "{2859F266-673A-45A2-9E3C-7B39C6DDD38E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicApi", "benchmarkapps\BasicApi\BasicApi.csproj", "{910F023A-88E3-4CB4-8793-AC4005C7B421}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicViews", "benchmarkapps\BasicViews\BasicViews.csproj", "{E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -894,6 +900,30 @@ Global {51E3E785-A9D1-4196-BAFE-A17FF4304B89}.Release|Mixed Platforms.Build.0 = Release|Any CPU {51E3E785-A9D1-4196-BAFE-A17FF4304B89}.Release|x86.ActiveCfg = Release|Any CPU {51E3E785-A9D1-4196-BAFE-A17FF4304B89}.Release|x86.Build.0 = Release|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Debug|Any CPU.Build.0 = Debug|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Debug|x86.ActiveCfg = Debug|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Debug|x86.Build.0 = Debug|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Release|Any CPU.ActiveCfg = Release|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Release|Any CPU.Build.0 = Release|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Release|x86.ActiveCfg = Release|Any CPU + {910F023A-88E3-4CB4-8793-AC4005C7B421}.Release|x86.Build.0 = Release|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Debug|x86.Build.0 = Debug|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Release|Any CPU.Build.0 = Release|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Release|x86.ActiveCfg = Release|Any CPU + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -963,6 +993,8 @@ Global {E83D3745-9BCF-40E8-8D34-AFBA604C2439} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {17122147-ADFD-41C8-87D9-CCC582CCA8F9} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {51E3E785-A9D1-4196-BAFE-A17FF4304B89} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {910F023A-88E3-4CB4-8793-AC4005C7B421} = {2859F266-673A-45A2-9E3C-7B39C6DDD38E} + {E89EB74D-C1CE-456F-B42D-CCF1575E0CFB} = {2859F266-673A-45A2-9E3C-7B39C6DDD38E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A} diff --git a/benchmarkapps/BasicApi/BasicApi.csproj b/benchmarkapps/BasicApi/BasicApi.csproj new file mode 100644 index 0000000000..ec6e654069 --- /dev/null +++ b/benchmarkapps/BasicApi/BasicApi.csproj @@ -0,0 +1,43 @@ + + + netcoreapp2.2 + $(TargetFrameworks);net461 + $(BenchmarksTargetFramework) + + $(DefineConstants);GENERATE_SQL_SCRIPTS + $(DefineConstants);__RemoveThisBitTo__GENERATE_SQL_SCRIPTS + + CS8002;$(WarningsNotAsErrors) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmarkapps/BasicApi/Controllers/PetController.cs b/benchmarkapps/BasicApi/Controllers/PetController.cs new file mode 100644 index 0000000000..828b445899 --- /dev/null +++ b/benchmarkapps/BasicApi/Controllers/PetController.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using BasicApi.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace BasicApi.Controllers +{ + [ApiController] + [Authorize("pet-store-reader")] + [Route("/pet")] + public class PetController : ControllerBase + { + public PetController(BasicApiContext dbContext) + { + DbContext = dbContext; + } + + public BasicApiContext DbContext { get; } + + [HttpGet("{id}", Name = "FindPetById")] + public async Task> FindById(int id) + { + var pet = await DbContext.Pets + .Include(p => p.Category) + .Include(p => p.Images) + .Include(p => p.Tags) + .FirstOrDefaultAsync(p => p.Id == id); + if (pet == null) + { + return new NotFoundResult(); + } + + return pet; + } + + [AllowAnonymous] + [HttpGet("anonymous/{id}")] + public async Task> FindByIdWithoutToken(int id) + { + var pet = await DbContext.Pets + .Include(p => p.Category) + .Include(p => p.Images) + .Include(p => p.Tags) + .FirstOrDefaultAsync(p => p.Id == id); + if (pet == null) + { + return new NotFoundResult(); + } + + return pet; + } + + [HttpGet("findByCategory/{categoryId}")] + public async Task> FindByCategory(int categoryId) + { + var pet = await DbContext.Pets + .Include(p => p.Category) + .Include(p => p.Images) + .Include(p => p.Tags) + .FirstOrDefaultAsync(p => p.Category != null && p.Category.Id == categoryId); + if (pet == null) + { + return new NotFoundResult(); + } + + return pet; + } + + [HttpGet("findByStatus")] + public async Task> FindByStatus(string status) + { + var pet = await DbContext.Pets + .Include(p => p.Category) + .Include(p => p.Images) + .Include(p => p.Tags) + .FirstOrDefaultAsync(p => p.Status == status); + if (pet == null) + { + return new NotFoundResult(); + } + + return pet; + } + + [HttpGet("findByTags")] + public async Task> FindByTags(string[] tags) + { + var pet = await DbContext.Pets + .Include(p => p.Category) + .Include(p => p.Images) + .Include(p => p.Tags) + .FirstOrDefaultAsync(p => p.Tags.Any(t => tags.Contains(t.Name))); + if (pet == null) + { + return new NotFoundResult(); + } + + return pet; + } + + [Authorize("pet-store-writer")] + [HttpPost] + public async Task AddPet([FromBody] Pet pet) + { + DbContext.Pets.Add(pet); + await DbContext.SaveChangesAsync(); + + return new CreatedAtRouteResult("FindPetById", new { id = pet.Id }, pet); + } + + [Authorize("pet-store-writer")] + [HttpPut] + public IActionResult EditPet(Pet pet) + { + throw new NotImplementedException(); + } + + [Authorize("pet-store-writer")] + [HttpPost("{id}/uploadImage")] + public IActionResult UploadImage(int id, IFormFile file) + { + throw new NotImplementedException(); + } + + [Authorize("pet-store-writer")] + [HttpDelete("{id}")] + public IActionResult DeletePet(int id) + { + throw new NotImplementedException(); + } + } +} diff --git a/benchmarkapps/BasicApi/Controllers/TokenController.cs b/benchmarkapps/BasicApi/Controllers/TokenController.cs new file mode 100644 index 0000000000..624288ddd7 --- /dev/null +++ b/benchmarkapps/BasicApi/Controllers/TokenController.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace BasicApi.Controllers +{ + public class TokenController : ControllerBase + { + private static readonly Dictionary _identities; + + static TokenController() + { + _identities = new Dictionary(StringComparer.Ordinal); + + var reader = new ClaimsIdentity(); + reader.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, "reader@example.com")); + reader.AddClaim(new Claim("scope", "pet-store-reader")); + _identities.Add("reader@example.com", reader); + + var writer = new ClaimsIdentity(); + writer.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, "writer@example.com")); + writer.AddClaim(new Claim("scope", "pet-store-reader")); + writer.AddClaim(new Claim("scope", "pet-store-writer")); + _identities.Add("writer@example.com", writer); + } + + private readonly SigningCredentials _credentials; + private readonly JwtBearerOptions _options; + + public TokenController( + IOptionsSnapshot options, + SigningCredentials credentials) + { + _options = options.Get(JwtBearerDefaults.AuthenticationScheme); + _credentials = credentials; + } + + [HttpGet("/token")] + public IActionResult GetToken(string username) + { + if (username == null || !_identities.TryGetValue(username, out var identity)) + { + return new StatusCodeResult(403); + } + + var handler = _options.SecurityTokenValidators.OfType().First(); + var tokenDescriptor = new SecurityTokenDescriptor() + { + Issuer = _options.TokenValidationParameters.ValidIssuer, + Audience = _options.TokenValidationParameters.ValidAudience, + SigningCredentials = _credentials, + Subject = identity + }; + + var securityToken = handler.CreateJwtSecurityToken( + issuer: _options.TokenValidationParameters.ValidIssuer, + audience: _options.TokenValidationParameters.ValidAudience, + signingCredentials: _credentials, + subject: identity); + + var token = handler.WriteToken(securityToken); + return Content(token); + } + } +} diff --git a/benchmarkapps/BasicApi/Migrations/20180609000420_InitialCreate.Designer.cs b/benchmarkapps/BasicApi/Migrations/20180609000420_InitialCreate.Designer.cs new file mode 100644 index 0000000000..bbcf696281 --- /dev/null +++ b/benchmarkapps/BasicApi/Migrations/20180609000420_InitialCreate.Designer.cs @@ -0,0 +1,172 @@ +// +using BasicApi.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace BasicApi.Migrations +{ + [DbContext(typeof(BasicApiContext))] + [Migration("20180609000420_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("BasicApi.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new { Id = -1, Name = "Dogs" }, + new { Id = -2, Name = "Cats" }, + new { Id = -3, Name = "Rabbits" }, + new { Id = -4, Name = "Lions" } + ); + }); + + modelBuilder.Entity("BasicApi.Models.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("PetId"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.HasIndex("PetId"); + + b.ToTable("Images"); + + b.HasData( + new { Id = -1, PetId = -1, Url = "http://example.com/pets/-1_1.png" }, + new { Id = -2, PetId = -2, Url = "http://example.com/pets/-2_1.png" }, + new { Id = -3, PetId = -3, Url = "http://example.com/pets/-3_1.png" }, + new { Id = -4, PetId = -4, Url = "http://example.com/pets/-4_1.png" }, + new { Id = -5, PetId = -5, Url = "http://example.com/pets/-5_1.png" }, + new { Id = -6, PetId = -6, Url = "http://example.com/pets/-6_1.png" }, + new { Id = -7, PetId = -7, Url = "http://example.com/pets/-7_1.png" }, + new { Id = -8, PetId = -8, Url = "http://example.com/pets/-8_1.png" }, + new { Id = -9, PetId = -9, Url = "http://example.com/pets/-9_1.png" }, + new { Id = -10, PetId = -10, Url = "http://example.com/pets/-10_1.png" }, + new { Id = -11, PetId = -11, Url = "http://example.com/pets/-11_1.png" }, + new { Id = -12, PetId = -12, Url = "http://example.com/pets/-12_1.png" } + ); + }); + + modelBuilder.Entity("BasicApi.Models.Pet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Age"); + + b.Property("CategoryId"); + + b.Property("HasVaccinations"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50); + + b.Property("Status") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Pets"); + + b.HasData( + new { Id = -1, Age = 1, CategoryId = -1, HasVaccinations = true, Name = "Dogs1", Status = "available" }, + new { Id = -2, Age = 1, CategoryId = -1, HasVaccinations = true, Name = "Dogs2", Status = "available" }, + new { Id = -3, Age = 1, CategoryId = -1, HasVaccinations = true, Name = "Dogs3", Status = "available" }, + new { Id = -4, Age = 1, CategoryId = -2, HasVaccinations = true, Name = "Cats1", Status = "available" }, + new { Id = -5, Age = 1, CategoryId = -2, HasVaccinations = true, Name = "Cats2", Status = "available" }, + new { Id = -6, Age = 1, CategoryId = -2, HasVaccinations = true, Name = "Cats3", Status = "available" }, + new { Id = -7, Age = 1, CategoryId = -3, HasVaccinations = true, Name = "Rabbits1", Status = "available" }, + new { Id = -8, Age = 1, CategoryId = -3, HasVaccinations = true, Name = "Rabbits2", Status = "available" }, + new { Id = -9, Age = 1, CategoryId = -3, HasVaccinations = true, Name = "Rabbits3", Status = "available" }, + new { Id = -10, Age = 1, CategoryId = -4, HasVaccinations = true, Name = "Lions1", Status = "available" }, + new { Id = -11, Age = 1, CategoryId = -4, HasVaccinations = true, Name = "Lions2", Status = "available" }, + new { Id = -12, Age = 1, CategoryId = -4, HasVaccinations = true, Name = "Lions3", Status = "available" } + ); + }); + + modelBuilder.Entity("BasicApi.Models.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name"); + + b.Property("PetId"); + + b.HasKey("Id"); + + b.HasIndex("PetId"); + + b.ToTable("Tags"); + + b.HasData( + new { Id = -1, Name = "Tag1", PetId = -1 }, + new { Id = -2, Name = "Tag1", PetId = -2 }, + new { Id = -3, Name = "Tag1", PetId = -3 }, + new { Id = -4, Name = "Tag1", PetId = -4 }, + new { Id = -5, Name = "Tag1", PetId = -5 }, + new { Id = -6, Name = "Tag1", PetId = -6 }, + new { Id = -7, Name = "Tag1", PetId = -7 }, + new { Id = -8, Name = "Tag1", PetId = -8 }, + new { Id = -9, Name = "Tag1", PetId = -9 }, + new { Id = -10, Name = "Tag1", PetId = -10 }, + new { Id = -11, Name = "Tag1", PetId = -11 }, + new { Id = -12, Name = "Tag1", PetId = -12 } + ); + }); + + modelBuilder.Entity("BasicApi.Models.Image", b => + { + b.HasOne("BasicApi.Models.Pet") + .WithMany("Images") + .HasForeignKey("PetId"); + }); + + modelBuilder.Entity("BasicApi.Models.Pet", b => + { + b.HasOne("BasicApi.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + }); + + modelBuilder.Entity("BasicApi.Models.Tag", b => + { + b.HasOne("BasicApi.Models.Pet") + .WithMany("Tags") + .HasForeignKey("PetId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/benchmarkapps/BasicApi/Migrations/20180609000420_InitialCreate.cs b/benchmarkapps/BasicApi/Migrations/20180609000420_InitialCreate.cs new file mode 100644 index 0000000000..2dd749ebbc --- /dev/null +++ b/benchmarkapps/BasicApi/Migrations/20180609000420_InitialCreate.cs @@ -0,0 +1,218 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace BasicApi.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(nullable: false) +#if !NET461 + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) +#endif + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Name = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Pets", + columns: table => new + { + Id = table.Column(nullable: false) +#if !NET461 + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) +#endif + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Age = table.Column(nullable: false), + CategoryId = table.Column(nullable: true), + HasVaccinations = table.Column(nullable: false), + Name = table.Column(maxLength: 50, nullable: false), + Status = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Pets", x => x.Id); + table.ForeignKey( + name: "FK_Pets_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Images", + columns: table => new + { + Id = table.Column(nullable: false) +#if !NET461 + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) +#endif + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Url = table.Column(nullable: true), + PetId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Images", x => x.Id); + table.ForeignKey( + name: "FK_Images_Pets_PetId", + column: x => x.PetId, + principalTable: "Pets", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(nullable: false) +#if !NET461 + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) +#endif + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Name = table.Column(nullable: true), + PetId = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + table.ForeignKey( + name: "FK_Tags_Pets_PetId", + column: x => x.PetId, + principalTable: "Pets", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.InsertData( + table: "Categories", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { -1, "Dogs" }, + { -2, "Cats" }, + { -3, "Rabbits" }, + { -4, "Lions" } + }); + + migrationBuilder.InsertData( + table: "Pets", + columns: new[] { "Id", "Age", "CategoryId", "HasVaccinations", "Name", "Status" }, + values: new object[,] + { + { -1, 1, -1, true, "Dogs1", "available" }, + { -2, 1, -1, true, "Dogs2", "available" }, + { -3, 1, -1, true, "Dogs3", "available" }, + + { -4, 1, -2, true, "Cats1", "available" }, + + { -5, 1, -2, true, "Cats2", "available" }, + + { -6, 1, -2, true, "Cats3", "available" }, + + { -7, 1, -3, true, "Rabbits1", "available" }, + + { -8, 1, -3, true, "Rabbits2", "available" }, + + { -9, 1, -3, true, "Rabbits3", "available" }, + + { -10, 1, -4, true, "Lions1", "available" }, + { -11, 1, -4, true, "Lions2", "available" }, + { -12, 1, -4, true, "Lions3", "available" } + }); + + migrationBuilder.InsertData( + table: "Images", + columns: new[] { "Id", "PetId", "Url" }, + values: new object[,] + { + { -1, -1, "http://example.com/pets/-1_1.png" }, + { -2, -2, "http://example.com/pets/-2_1.png" }, + { -11, -11, "http://example.com/pets/-11_1.png" }, + { -3, -3, "http://example.com/pets/-3_1.png" }, + { -4, -4, "http://example.com/pets/-4_1.png" }, + + { -10, -10, "http://example.com/pets/-10_1.png" }, + + { -5, -5, "http://example.com/pets/-5_1.png" }, + + { -6, -6, "http://example.com/pets/-6_1.png" }, + + { -12, -12, "http://example.com/pets/-12_1.png" }, + + { -7, -7, "http://example.com/pets/-7_1.png" }, + { -9, -9, "http://example.com/pets/-9_1.png" }, + { -8, -8, "http://example.com/pets/-8_1.png" } + }); + + migrationBuilder.InsertData( + table: "Tags", + columns: new[] { "Id", "Name", "PetId" }, + values: new object[,] + { + { -11, "Tag1", -11 }, + { -10, "Tag1", -10 }, + { -9, "Tag1", -9 }, + { -6, "Tag1", -6 }, + { -7, "Tag1", -7 }, + { -5, "Tag1", -5 }, + { -4, "Tag1", -4 }, + { -3, "Tag1", -3 }, + { -2, "Tag1", -2 }, + { -1, "Tag1", -1 }, + { -8, "Tag1", -8 }, + { -12, "Tag1", -12 } + }); + + migrationBuilder.CreateIndex( + name: "IX_Images_PetId", + table: "Images", + column: "PetId"); + + migrationBuilder.CreateIndex( + name: "IX_Pets_CategoryId", + table: "Pets", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_Tags_PetId", + table: "Tags", + column: "PetId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Images"); + + migrationBuilder.DropTable( + name: "Tags"); + + migrationBuilder.DropTable( + name: "Pets"); + + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/benchmarkapps/BasicApi/Migrations/BasicApiContextModelSnapshot.cs b/benchmarkapps/BasicApi/Migrations/BasicApiContextModelSnapshot.cs new file mode 100644 index 0000000000..95a4d584ed --- /dev/null +++ b/benchmarkapps/BasicApi/Migrations/BasicApiContextModelSnapshot.cs @@ -0,0 +1,170 @@ +// +using BasicApi.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace BasicApi.Migrations +{ + [DbContext(typeof(BasicApiContext))] + partial class BasicApiContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("BasicApi.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + + b.HasData( + new { Id = -1, Name = "Dogs" }, + new { Id = -2, Name = "Cats" }, + new { Id = -3, Name = "Rabbits" }, + new { Id = -4, Name = "Lions" } + ); + }); + + modelBuilder.Entity("BasicApi.Models.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("PetId"); + + b.Property("Url"); + + b.HasKey("Id"); + + b.HasIndex("PetId"); + + b.ToTable("Images"); + + b.HasData( + new { Id = -1, PetId = -1, Url = "http://example.com/pets/-1_1.png" }, + new { Id = -2, PetId = -2, Url = "http://example.com/pets/-2_1.png" }, + new { Id = -3, PetId = -3, Url = "http://example.com/pets/-3_1.png" }, + new { Id = -4, PetId = -4, Url = "http://example.com/pets/-4_1.png" }, + new { Id = -5, PetId = -5, Url = "http://example.com/pets/-5_1.png" }, + new { Id = -6, PetId = -6, Url = "http://example.com/pets/-6_1.png" }, + new { Id = -7, PetId = -7, Url = "http://example.com/pets/-7_1.png" }, + new { Id = -8, PetId = -8, Url = "http://example.com/pets/-8_1.png" }, + new { Id = -9, PetId = -9, Url = "http://example.com/pets/-9_1.png" }, + new { Id = -10, PetId = -10, Url = "http://example.com/pets/-10_1.png" }, + new { Id = -11, PetId = -11, Url = "http://example.com/pets/-11_1.png" }, + new { Id = -12, PetId = -12, Url = "http://example.com/pets/-12_1.png" } + ); + }); + + modelBuilder.Entity("BasicApi.Models.Pet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Age"); + + b.Property("CategoryId"); + + b.Property("HasVaccinations"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50); + + b.Property("Status") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Pets"); + + b.HasData( + new { Id = -1, Age = 1, CategoryId = -1, HasVaccinations = true, Name = "Dogs1", Status = "available" }, + new { Id = -2, Age = 1, CategoryId = -1, HasVaccinations = true, Name = "Dogs2", Status = "available" }, + new { Id = -3, Age = 1, CategoryId = -1, HasVaccinations = true, Name = "Dogs3", Status = "available" }, + new { Id = -4, Age = 1, CategoryId = -2, HasVaccinations = true, Name = "Cats1", Status = "available" }, + new { Id = -5, Age = 1, CategoryId = -2, HasVaccinations = true, Name = "Cats2", Status = "available" }, + new { Id = -6, Age = 1, CategoryId = -2, HasVaccinations = true, Name = "Cats3", Status = "available" }, + new { Id = -7, Age = 1, CategoryId = -3, HasVaccinations = true, Name = "Rabbits1", Status = "available" }, + new { Id = -8, Age = 1, CategoryId = -3, HasVaccinations = true, Name = "Rabbits2", Status = "available" }, + new { Id = -9, Age = 1, CategoryId = -3, HasVaccinations = true, Name = "Rabbits3", Status = "available" }, + new { Id = -10, Age = 1, CategoryId = -4, HasVaccinations = true, Name = "Lions1", Status = "available" }, + new { Id = -11, Age = 1, CategoryId = -4, HasVaccinations = true, Name = "Lions2", Status = "available" }, + new { Id = -12, Age = 1, CategoryId = -4, HasVaccinations = true, Name = "Lions3", Status = "available" } + ); + }); + + modelBuilder.Entity("BasicApi.Models.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name"); + + b.Property("PetId"); + + b.HasKey("Id"); + + b.HasIndex("PetId"); + + b.ToTable("Tags"); + + b.HasData( + new { Id = -1, Name = "Tag1", PetId = -1 }, + new { Id = -2, Name = "Tag1", PetId = -2 }, + new { Id = -3, Name = "Tag1", PetId = -3 }, + new { Id = -4, Name = "Tag1", PetId = -4 }, + new { Id = -5, Name = "Tag1", PetId = -5 }, + new { Id = -6, Name = "Tag1", PetId = -6 }, + new { Id = -7, Name = "Tag1", PetId = -7 }, + new { Id = -8, Name = "Tag1", PetId = -8 }, + new { Id = -9, Name = "Tag1", PetId = -9 }, + new { Id = -10, Name = "Tag1", PetId = -10 }, + new { Id = -11, Name = "Tag1", PetId = -11 }, + new { Id = -12, Name = "Tag1", PetId = -12 } + ); + }); + + modelBuilder.Entity("BasicApi.Models.Image", b => + { + b.HasOne("BasicApi.Models.Pet") + .WithMany("Images") + .HasForeignKey("PetId"); + }); + + modelBuilder.Entity("BasicApi.Models.Pet", b => + { + b.HasOne("BasicApi.Models.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId"); + }); + + modelBuilder.Entity("BasicApi.Models.Tag", b => + { + b.HasOne("BasicApi.Models.Pet") + .WithMany("Tags") + .HasForeignKey("PetId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/benchmarkapps/BasicApi/Models/BasicApiContext.cs b/benchmarkapps/BasicApi/Models/BasicApiContext.cs new file mode 100644 index 0000000000..35eb47619f --- /dev/null +++ b/benchmarkapps/BasicApi/Models/BasicApiContext.cs @@ -0,0 +1,190 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore; + +namespace BasicApi.Models +{ + public class BasicApiContext : DbContext + { + public BasicApiContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Categories { get; set; } + + public DbSet Images { get; set; } + + public DbSet Pets { get; set; } + + public DbSet Tags { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + var id = -1; + var categories = new[] + { + new Category { Id = id--, Name = "Dogs" }, + new Category { Id = id--, Name = "Cats" }, + new Category { Id = id--, Name = "Rabbits" }, + new Category { Id = id, Name = "Lions" }, + }; + + id = -1; + var categoryId = -1; + var pets = new[] + { + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id--, + Name = "Dogs1", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id--, + Name = "Dogs2", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId--, + HasVaccinations = true, + Id = id--, + Name = "Dogs3", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id--, + Name = "Cats1", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id--, + Name = "Cats2", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId--, + HasVaccinations = true, + Id = id--, + Name = "Cats3", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id--, + Name = "Rabbits1", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id--, + Name = "Rabbits2", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId--, + HasVaccinations = true, + Id = id--, + Name = "Rabbits3", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id--, + Name = "Lions1", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id--, + Name = "Lions2", + Status = "available", + }, + new + { + Age = 1, + CategoryId = categoryId, + HasVaccinations = true, + Id = id, + Name = "Lions3", + Status = "available", + }, + }; + + id = -1; + var images = new[] + { + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id--}_1.png" }, + new { Id = id, PetId = id, Url = $"http://example.com/pets/{id}_1.png" }, + }; + + id = -1; + var tags = new[] + { + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id--, Name = "Tag1" }, + new { Id = id, PetId = id, Name = "Tag1" }, + }; + + modelBuilder.Entity().HasData(categories); + modelBuilder.Entity().HasData(pets); + modelBuilder.Entity().HasData(images); + modelBuilder.Entity().HasData(tags); + } + } +} diff --git a/benchmarkapps/BasicApi/Models/Category.cs b/benchmarkapps/BasicApi/Models/Category.cs new file mode 100644 index 0000000000..f718410fcf --- /dev/null +++ b/benchmarkapps/BasicApi/Models/Category.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace BasicApi.Models +{ + public class Category + { + public int Id { get; set; } + + public string Name { get; set; } + } +} diff --git a/benchmarkapps/BasicApi/Models/Image.cs b/benchmarkapps/BasicApi/Models/Image.cs new file mode 100644 index 0000000000..a5497539bb --- /dev/null +++ b/benchmarkapps/BasicApi/Models/Image.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace BasicApi.Models +{ + public class Image + { + public int Id { get; set; } + + public string Url { get; set; } + } +} diff --git a/benchmarkapps/BasicApi/Models/Pet.cs b/benchmarkapps/BasicApi/Models/Pet.cs new file mode 100644 index 0000000000..8ca2a6f1ca --- /dev/null +++ b/benchmarkapps/BasicApi/Models/Pet.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace BasicApi.Models +{ + public class Pet + { + public int Id { get; set; } + + [Range(0, 150)] + public int Age { get; set; } + + public Category Category { get; set; } + + public bool HasVaccinations { get; set; } + + [Required] + [StringLength(50, MinimumLength = 2)] + public string Name { get; set; } + + public List Images { get; set; } + + public List Tags { get; set; } + + [Required] + public string Status { get; set; } + } +} diff --git a/benchmarkapps/BasicApi/Models/Tag.cs b/benchmarkapps/BasicApi/Models/Tag.cs new file mode 100644 index 0000000000..1875b57cce --- /dev/null +++ b/benchmarkapps/BasicApi/Models/Tag.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace BasicApi.Models +{ + public class Tag + { + public int Id { get; set; } + + public string Name { get; set; } + } +} diff --git a/benchmarkapps/BasicApi/Startup.cs b/benchmarkapps/BasicApi/Startup.cs new file mode 100644 index 0000000000..8fcc4787ae --- /dev/null +++ b/benchmarkapps/BasicApi/Startup.cs @@ -0,0 +1,246 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +#if GENERATE_SQL_SCRIPTS +using System.Linq; +#endif +using System.Security.Cryptography; +using BasicApi.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Serialization; +using Npgsql; + +namespace BasicApi +{ + public class Startup + { + private bool _isSQLite; + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + var rsa = new RSACryptoServiceProvider(2048); + var key = new RsaSecurityKey(rsa.ExportParameters(true)); + + services.AddSingleton(new SigningCredentials( + key, + SecurityAlgorithms.RsaSha256Signature)); + + services.AddAuthentication().AddJwtBearer(options => + { + options.TokenValidationParameters.IssuerSigningKey = key; + options.TokenValidationParameters.ValidAudience = "Myself"; + options.TokenValidationParameters.ValidIssuer = "BasicApi"; + }); + + var connectionString = Configuration["ConnectionString"]; + var databaseType = Configuration["Database"]; + if (string.IsNullOrEmpty(databaseType)) + { + // Use SQLite when running outside a benchmark test or if benchmarks user specified "None". + // ("None" is not passed to the web application.) + databaseType = "SQLite"; + } + else if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentException("Connection string must be specified for {databaseType}."); + } + + switch (databaseType.ToUpper()) + { +#if !NET461 + case "MYSQL": + services + .AddEntityFrameworkMySql() + .AddDbContext(options => options.UseMySql(connectionString)); + break; +#endif + + case "POSTGRESQL": + var settings = new NpgsqlConnectionStringBuilder(connectionString); + if (!settings.NoResetOnClose) + { + throw new ArgumentException("No Reset On Close=true must be specified for Npgsql."); + } + if (settings.Enlist) + { + throw new ArgumentException("Enlist=false must be specified for Npgsql."); + } + + services + .AddEntityFrameworkNpgsql() + .AddDbContextPool(options => options.UseNpgsql(connectionString)); + break; + + case "SQLITE": + _isSQLite = true; + services + .AddEntityFrameworkSqlite() + .AddDbContextPool(options => options.UseSqlite("Data Source=BasicApi.db")); + break; + + case "SQLSERVER": + services + .AddEntityFrameworkSqlServer() + .AddDbContextPool(options => options.UseSqlServer(connectionString)); + break; + + default: + throw new ArgumentException($"Application does not support database type {databaseType}."); + } + + services.AddAuthorization(options => + { + options.AddPolicy( + "pet-store-reader", + builder => builder + .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) + .RequireAuthenticatedUser() + .RequireClaim("scope", "pet-store-reader")); + + options.AddPolicy( + "pet-store-writer", + builder => builder + .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) + .RequireAuthenticatedUser() + .RequireClaim("scope", "pet-store-writer")); + }); + + services + .AddMvcCore() + .AddAuthorization() + .AddJsonFormatters(json => json.ContractResolver = new CamelCasePropertyNamesContractResolver()) + .AddDataAnnotations(); + } + + public void Configure(IApplicationBuilder app, IApplicationLifetime lifetime) + { + var services = app.ApplicationServices; + CreateDatabaseTables(services); + if (_isSQLite) + { + lifetime.ApplicationStopping.Register(() => DropDatabase(services)); + } + else + { + lifetime.ApplicationStopping.Register(() => DropDatabaseTables(services)); + } + + app.Use(next => async context => + { + try + { + await next(context); + } + catch (Exception ex) + { + Console.WriteLine(ex); + throw; + } + }); + + app.UseAuthentication(); + app.UseMvc(); + } + + private void CreateDatabaseTables(IServiceProvider services) + { + using (var serviceScope = services.GetRequiredService().CreateScope()) + { + using (var dbContext = serviceScope.ServiceProvider.GetRequiredService()) + { +#if GENERATE_SQL_SCRIPTS + var migrator = dbContext.GetService(); + var script = migrator.GenerateScript( + fromMigration: Migration.InitialDatabase, + toMigration: dbContext.Database.GetMigrations().LastOrDefault()); + Console.WriteLine("Create script:"); + Console.WriteLine(script); +#endif + + dbContext.Database.Migrate(); + } + } + } + + // Don't leave SQLite's .db file behind. + public static void DropDatabase(IServiceProvider services) + { + using (var serviceScope = services.GetRequiredService().CreateScope()) + { + using (var dbContext = serviceScope.ServiceProvider.GetRequiredService()) + { +#if GENERATE_SQL_SCRIPTS + var migrator = dbContext.GetService(); + var script = migrator.GenerateScript( + fromMigration: dbContext.Database.GetAppliedMigrations().LastOrDefault(), + toMigration: Migration.InitialDatabase); + Console.WriteLine("Delete script:"); + Console.WriteLine(script); +#endif + + dbContext.Database.EnsureDeleted(); + } + } + } + + private void DropDatabaseTables(IServiceProvider services) + { + using (var serviceScope = services.GetRequiredService().CreateScope()) + { + using (var dbContext = serviceScope.ServiceProvider.GetRequiredService()) + { + var migrator = dbContext.GetService(); +#if GENERATE_SQL_SCRIPTS + var script = migrator.GenerateScript( + fromMigration: dbContext.Database.GetAppliedMigrations().LastOrDefault(), + toMigration: Migration.InitialDatabase); + Console.WriteLine("Delete script:"); + Console.WriteLine(script); +#endif + + migrator.Migrate(Migration.InitialDatabase); + } + } + } + + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args) + .Build(); + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) + { + var configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddCommandLine(args) + .Build(); + + return new WebHostBuilder() + .UseKestrel() + .UseUrls("http://+:5000") + .UseConfiguration(configuration) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup(); + } + } +} diff --git a/benchmarkapps/BasicApi/benchmarks.json b/benchmarkapps/BasicApi/benchmarks.json new file mode 100644 index 0000000000..4a99d0bbd3 --- /dev/null +++ b/benchmarkapps/BasicApi/benchmarks.json @@ -0,0 +1,48 @@ +{ + "Default": { + "Client": "Wrk", + "Headers": { + "Cache-Control": "no-cache" + }, + "PresetHeaders": "Json", + "ReadyStateText": "Application started.", + "Source": { + "BranchOrCommit": "dev", + "Project": "benchmarkapps/BasicApi/BasicApi.csproj", + "Repository": "https://github.com/aspnet/mvc.git" + } + }, + "BasicApi.GetToken": { + "Path": "/token", + "PresetHeaders": "Plaintext", + "Query": "?username=reader@example.com" + }, + "BasicApi.GetUsingQueryString": { + "ClientProperties": { + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicApi/getWithToken.lua" + }, + "Path": "/pet/findByStatus", + "Query": "?status=available" + }, + "BasicApi.GetUsingRouteValue": { + "ClientProperties": { + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicApi/getWithToken.lua" + }, + "Path": "/pet/-1" + }, + "BasicApi.GetUsingRouteValueWithoutAuthorization": { + "ClientProperties": { + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicApi/getWithToken.lua" + }, + "Path": "/pet/anonymous/-1" + }, + "BasicApi.GetUsingRouteValueWithoutToken": { + "Path": "/pet/anonymous/-1" + }, + "BasicApi.Post": { + "ClientProperties": { + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicApi/postJsonWithToken.lua" + }, + "Path": "/pet" + } +} diff --git a/benchmarkapps/BasicApi/getWithToken.lua b/benchmarkapps/BasicApi/getWithToken.lua new file mode 100644 index 0000000000..6bccd6e763 --- /dev/null +++ b/benchmarkapps/BasicApi/getWithToken.lua @@ -0,0 +1,51 @@ +-- script that retrieves an authentication token to send in all future requests +-- keep this file and postJsonWithToken.lua in sync with respect to token handling + +-- use token for at most maxRequests, default throughout test +local counter = 0 +local maxRequests = -1 + +-- request access necessary for both reading and writing by default +local username = "writer@example.com" + +-- marker that we have completed the first request +local token = nil + +function init(args) + if args[1] ~= nil then + maxRequests = args[1] + print("Max requests: " .. maxRequests) + end + if args[2] ~= nil then + username = args[2] + end + + local path = "/token?username=" .. username + + -- initialize first (empty) request + req = wrk.format("GET", path, nil, "") +end + +function request() + return req +end + +function response(status, headers, body) + if not token and status == 200 then + token = body + wrk.headers["Authorization"] = "Bearer " .. token + req = wrk.format() + return + end + + if not token then + print("Failed initial request! status: " .. status) + wrk.thread:stop() + end + + if counter == maxRequests then + wrk.thread:stop() + end + + counter = counter + 1 +end diff --git a/benchmarkapps/BasicApi/postJsonWithToken.lua b/benchmarkapps/BasicApi/postJsonWithToken.lua new file mode 100644 index 0000000000..50ce722414 --- /dev/null +++ b/benchmarkapps/BasicApi/postJsonWithToken.lua @@ -0,0 +1,83 @@ +-- script that retrieves an authentication token to send in all future requests and adds a body for those requests +-- keep this file and getWithToken.lua in sync with respect to token handling + +-- do not use wrk's default request +local req = nil + +-- use token for at most maxRequests, default throughout test +local counter = 0 +local maxRequests = -1 + +-- request access necessary for both reading and writing by default +local username = "writer@example.com" + +-- marker that we have completed the first request +local token = nil + +function init(args) + if args[1] ~= nil then + maxRequests = args[1] + print("Max requests: " .. maxRequests) + end + if args[2] ~= nil then + username = args[2] + end + + local path = "/token?username=" .. username + + -- initialize first (empty) request + req = wrk.format("GET", path, nil, "") +end + +function request() + return req +end + +function response(status, headers, body) + if not token and status == 200 then + token = body + wrk.headers["Authorization"] = "Bearer " .. token + wrk.headers["Content-Type"] = "application/json" + wrk.method = "POST" + wrk.body = [[ +{ + "category": { + "name": "Cats" + }, + "images": [ + { + "url": "http://example.com/images/fluffy1.png" + }, + { + "url": "http://example.com/images/fluffy2.png" + }, + ], + "tags": [ + { + "name": "orange" + }, + { + "name": "kitty" + } + ], + "age": 2, + "hasVaccinations": "true", + "name": "fluffy", + "status": "available" +}]] + + req = wrk.format() + return + end + + if not token then + print("Failed initial request! status: " .. status) + wrk.thread:stop() + end + + if counter == maxRequests then + wrk.thread:stop() + end + + counter = counter + 1 +end diff --git a/benchmarkapps/BasicApi/runtimeconfig.template.json b/benchmarkapps/BasicApi/runtimeconfig.template.json new file mode 100644 index 0000000000..0976c49b2c --- /dev/null +++ b/benchmarkapps/BasicApi/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.GC.Server": true + } +} diff --git a/benchmarkapps/BasicViews/BasicViews.csproj b/benchmarkapps/BasicViews/BasicViews.csproj new file mode 100644 index 0000000000..62dd9a8740 --- /dev/null +++ b/benchmarkapps/BasicViews/BasicViews.csproj @@ -0,0 +1,42 @@ + + + netcoreapp2.2 + $(TargetFrameworks);net461 + $(BenchmarksTargetFramework) + + $(DefineConstants);GENERATE_SQL_SCRIPTS + $(DefineConstants);__RemoveThisBitTo__GENERATE_SQL_SCRIPTS + + CS8002;$(WarningsNotAsErrors) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmarkapps/BasicViews/BasicViewsContext.cs b/benchmarkapps/BasicViews/BasicViewsContext.cs new file mode 100644 index 0000000000..1380f5d7a6 --- /dev/null +++ b/benchmarkapps/BasicViews/BasicViewsContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore; + +namespace BasicViews +{ + public class BasicViewsContext : DbContext + { + public BasicViewsContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet People { get; set; } + } +} diff --git a/benchmarkapps/BasicViews/Components/CurrentUser.cs b/benchmarkapps/BasicViews/Components/CurrentUser.cs new file mode 100644 index 0000000000..2763be9178 --- /dev/null +++ b/benchmarkapps/BasicViews/Components/CurrentUser.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; + +namespace BasicViews.Components +{ + public class CurrentUser : ViewComponent + { + private static readonly string[] Names = { "Curly", "Curly Joe", "Joe", "Larry", "Moe", "Shemp" }; + private static int index = 0; + + public string Invoke() + { + index = index++ / Names.Length; + return Names[index]; + } + } +} diff --git a/benchmarkapps/BasicViews/Controllers/HomeController.cs b/benchmarkapps/BasicViews/Controllers/HomeController.cs new file mode 100644 index 0000000000..afb583a171 --- /dev/null +++ b/benchmarkapps/BasicViews/Controllers/HomeController.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace BasicViews.Controllers +{ + public class HomeController : Controller + { + private readonly BasicViewsContext _context; + + public HomeController(BasicViewsContext context) + { + _context = context; + } + + [HttpGet] + public IActionResult Index() + { + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Index(Person person) + { + if (ModelState.IsValid) + { + _context.Add(person); + await _context.SaveChangesAsync(); + } + + return View(person); + } + + [HttpGet] + public IActionResult IndexWithoutToken() + { + return View(viewName: nameof(Index)); + } + + [HttpPost] + [IgnoreAntiforgeryToken] + public async Task IndexWithoutToken(Person person) + { + if (ModelState.IsValid) + { + _context.Add(person); + await _context.SaveChangesAsync(); + } + + return View(viewName: nameof(Index), model: person); + } + + [HttpGet] + public IActionResult HtmlHelpers() + { + return View(); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task HtmlHelpers(Person person) + { + if (ModelState.IsValid) + { + _context.Add(person); + await _context.SaveChangesAsync(); + } + + return View(person); + } + } +} diff --git a/benchmarkapps/BasicViews/Migrations/20180609000611_InitialCreate.Designer.cs b/benchmarkapps/BasicViews/Migrations/20180609000611_InitialCreate.Designer.cs new file mode 100644 index 0000000000..61d6e7f3c4 --- /dev/null +++ b/benchmarkapps/BasicViews/Migrations/20180609000611_InitialCreate.Designer.cs @@ -0,0 +1,44 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace BasicViews.Migrations +{ + [DbContext(typeof(BasicViewsContext))] + [Migration("20180609000611_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("BasicViews.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Age"); + + b.Property("BirthDate"); + + b.Property("Name") + .HasMaxLength(27); + + b.HasKey("Id"); + + b.ToTable("People"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/benchmarkapps/BasicViews/Migrations/20180609000611_InitialCreate.cs b/benchmarkapps/BasicViews/Migrations/20180609000611_InitialCreate.cs new file mode 100644 index 0000000000..9eba030339 --- /dev/null +++ b/benchmarkapps/BasicViews/Migrations/20180609000611_InitialCreate.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace BasicViews.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "People", + columns: table => new + { + Id = table.Column(nullable: false) +#if !NET461 + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) +#endif + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + Name = table.Column(maxLength: 27, nullable: true), + Age = table.Column(nullable: false), + BirthDate = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_People", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "People"); + } + } +} diff --git a/benchmarkapps/BasicViews/Migrations/BasicViewsContextModelSnapshot.cs b/benchmarkapps/BasicViews/Migrations/BasicViewsContextModelSnapshot.cs new file mode 100644 index 0000000000..4b3deaf71b --- /dev/null +++ b/benchmarkapps/BasicViews/Migrations/BasicViewsContextModelSnapshot.cs @@ -0,0 +1,42 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace BasicViews.Migrations +{ + [DbContext(typeof(BasicViewsContext))] + partial class BasicViewsContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "2.1.0-rtm-30799") + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("BasicViews.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Age"); + + b.Property("BirthDate"); + + b.Property("Name") + .HasMaxLength(27); + + b.HasKey("Id"); + + b.ToTable("People"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/benchmarkapps/BasicViews/Person.cs b/benchmarkapps/BasicViews/Person.cs new file mode 100644 index 0000000000..f2118d36be --- /dev/null +++ b/benchmarkapps/BasicViews/Person.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace BasicViews +{ + public class Person + { + public int Id { get; set; } + + [StringLength(27, MinimumLength = 2)] + public string Name { get; set; } + + [Range(10, 54)] + public int Age { get; set; } + + public DateTimeOffset BirthDate { get; set; } + } +} diff --git a/benchmarkapps/BasicViews/Startup.cs b/benchmarkapps/BasicViews/Startup.cs new file mode 100644 index 0000000000..30ebb341bd --- /dev/null +++ b/benchmarkapps/BasicViews/Startup.cs @@ -0,0 +1,207 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +#if GENERATE_SQL_SCRIPTS +using System.Linq; +#endif +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Npgsql; + +namespace BasicViews +{ + public class Startup + { + private bool _isSQLite; + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + var connectionString = Configuration["ConnectionString"]; + var databaseType = Configuration["Database"]; + if (string.IsNullOrEmpty(databaseType)) + { + // Use SQLite when running outside a benchmark test or if benchmarks user specified "None". + // ("None" is not passed to the web application.) + databaseType = "SQLite"; + } + else if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentException("Connection string must be specified for {databaseType}."); + } + + switch (databaseType.ToUpper()) + { +#if !NET461 + case "MYSQL": + services + .AddEntityFrameworkMySql() + .AddDbContext(options => options.UseMySql(connectionString)); + break; +#endif + + case "POSTGRESQL": + var settings = new NpgsqlConnectionStringBuilder(connectionString); + if (!settings.NoResetOnClose) + { + throw new ArgumentException("No Reset On Close=true must be specified for Npgsql."); + } + if (settings.Enlist) + { + throw new ArgumentException("Enlist=false must be specified for Npgsql."); + } + + services + .AddEntityFrameworkNpgsql() + .AddDbContextPool(options => options.UseNpgsql(connectionString)); + break; + + case "SQLITE": + _isSQLite = true; + services + .AddEntityFrameworkSqlite() + .AddDbContextPool(options => options.UseSqlite("Data Source=BasicViews.db")); + break; + + case "SQLSERVER": + services + .AddEntityFrameworkSqlServer() + .AddDbContextPool(options => options.UseSqlServer(connectionString)); + break; + + default: + throw new ArgumentException($"Application does not support database type {databaseType}."); + } + + services.AddMvc(); + } + + public void Configure(IApplicationBuilder app, IApplicationLifetime lifetime) + { + var services = app.ApplicationServices; + CreateDatabaseTables(services); + if (_isSQLite) + { + lifetime.ApplicationStopping.Register(() => DropDatabase(services)); + } + else + { + lifetime.ApplicationStopping.Register(() => DropDatabaseTables(services)); + } + + app.Use(next => async context => + { + try + { + await next(context); + } + catch (Exception ex) + { + Console.WriteLine(ex); + throw; + } + }); + + app.UseStaticFiles(); + app.UseMvcWithDefaultRoute(); + } + + private void CreateDatabaseTables(IServiceProvider services) + { + using (var serviceScope = services.GetRequiredService().CreateScope()) + { + using (var dbContext = serviceScope.ServiceProvider.GetRequiredService()) + { +#if GENERATE_SQL_SCRIPTS + var migrator = dbContext.GetService(); + var script = migrator.GenerateScript( + fromMigration: Migration.InitialDatabase, + toMigration: dbContext.Database.GetMigrations().LastOrDefault()); + Console.WriteLine("Create script:"); + Console.WriteLine(script); +#endif + + dbContext.Database.Migrate(); + } + } + } + + // Don't leave SQLite's .db file behind. + public static void DropDatabase(IServiceProvider services) + { + using (var serviceScope = services.GetRequiredService().CreateScope()) + { + using (var dbContext = serviceScope.ServiceProvider.GetRequiredService()) + { +#if GENERATE_SQL_SCRIPTS + var migrator = dbContext.GetService(); + var script = migrator.GenerateScript( + fromMigration: dbContext.Database.GetAppliedMigrations().LastOrDefault(), + toMigration: Migration.InitialDatabase); + Console.WriteLine("Delete script:"); + Console.WriteLine(script); +#endif + + dbContext.Database.EnsureDeleted(); + } + } + } + + private void DropDatabaseTables(IServiceProvider services) + { + using (var serviceScope = services.GetRequiredService().CreateScope()) + { + using (var dbContext = serviceScope.ServiceProvider.GetRequiredService()) + { + var migrator = dbContext.GetService(); +#if GENERATE_SQL_SCRIPTS + var script = migrator.GenerateScript( + fromMigration: dbContext.Database.GetAppliedMigrations().LastOrDefault(), + toMigration: Migration.InitialDatabase); + Console.WriteLine("Delete script:"); + Console.WriteLine(script); +#endif + + migrator.Migrate(Migration.InitialDatabase); + } + } + } + + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args) + .Build(); + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) + { + var configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddCommandLine(args) + .Build(); + + return new WebHostBuilder() + .UseKestrel() + .UseUrls("http://+:5000") + .UseConfiguration(configuration) + .UseIISIntegration() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup(); + } + } +} diff --git a/benchmarkapps/BasicViews/Views/Home/HtmlHelpers.cshtml b/benchmarkapps/BasicViews/Views/Home/HtmlHelpers.cshtml new file mode 100644 index 0000000000..971bb99626 --- /dev/null +++ b/benchmarkapps/BasicViews/Views/Home/HtmlHelpers.cshtml @@ -0,0 +1,23 @@ +@using BasicViews +@model Person + +@Html.ValidationSummary() + +@using (Html.BeginForm()) +{ +
+ @Html.LabelFor(p => p.Name) + @Html.EditorFor(p => p.Name) +
+
+ @Html.LabelFor(p => p.Age) + @Html.EditorFor(p => p.Age) +
+
+ @Html.LabelFor(p => p.BirthDate) + @Html.EditorFor(p => p.BirthDate) +
+ + + @Html.AntiForgeryToken() +} \ No newline at end of file diff --git a/benchmarkapps/BasicViews/Views/Home/Index.cshtml b/benchmarkapps/BasicViews/Views/Home/Index.cshtml new file mode 100644 index 0000000000..e0720d852a --- /dev/null +++ b/benchmarkapps/BasicViews/Views/Home/Index.cshtml @@ -0,0 +1,21 @@ +@using BasicViews +@model Person + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
\ No newline at end of file diff --git a/benchmarkapps/BasicViews/Views/Shared/_Layout.cshtml b/benchmarkapps/BasicViews/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000000..34517d4aa0 --- /dev/null +++ b/benchmarkapps/BasicViews/Views/Shared/_Layout.cshtml @@ -0,0 +1,55 @@ + + + + + + MVC with views + + + + + + + + + + +
+

Hello @await Component.InvokeAsync("CurrentUser")!

+ +
+ @RenderBody() +
+
+ + + + + + + + + @RenderSection("scripts", required: false) + + diff --git a/benchmarkapps/BasicViews/Views/_ViewImports.cshtml b/benchmarkapps/BasicViews/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..9018c7897f --- /dev/null +++ b/benchmarkapps/BasicViews/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/benchmarkapps/BasicViews/Views/_ViewStart.cshtml b/benchmarkapps/BasicViews/Views/_ViewStart.cshtml new file mode 100644 index 0000000000..a5f10045db --- /dev/null +++ b/benchmarkapps/BasicViews/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/benchmarkapps/BasicViews/benchmarks.json b/benchmarkapps/BasicViews/benchmarks.json new file mode 100644 index 0000000000..82e0812a21 --- /dev/null +++ b/benchmarkapps/BasicViews/benchmarks.json @@ -0,0 +1,39 @@ +{ + "Default": { + "Client": "Wrk", + "Headers": { + "Cache-Control": "no-cache" + }, + "PresetHeaders": "Html", + "ReadyStateText": "Application started.", + "Source": { + "BranchOrCommit": "dev", + "Project": "benchmarkapps/BasicViews/BasicViews.csproj", + "Repository": "https://github.com/aspnet/mvc.git" + } + }, + "BasicViews.GetHtmlHelpers": { + "Path": "/Home/HtmlHelpers" + }, + "BasicViews.GetTagHelpers": { + "Path": "/Home/Index" + }, + "BasicViews.Post": { + "ClientProperties": { + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicViews/postWithToken.lua" + }, + "Path": "/Home/Index" + }, + "BasicViews.PostIgnoringToken": { + "ClientProperties": { + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicViews/postWithToken.lua" + }, + "Path": "/Home/IndexWithoutToken" + }, + "BasicViews.PostWithoutToken": { + "ClientProperties": { + "Scripts": "https://raw.githubusercontent.com/aspnet/Mvc/dev/benchmarkapps/BasicViews/post.lua" + }, + "Path": "/Home/IndexWithoutToken" + } +} diff --git a/benchmarkapps/BasicViews/post.lua b/benchmarkapps/BasicViews/post.lua new file mode 100644 index 0000000000..9a7640ee6d --- /dev/null +++ b/benchmarkapps/BasicViews/post.lua @@ -0,0 +1,7 @@ +-- script that POSTs body for requests + +function init(args) + wrk.body = "Age=12&BirthDate=2006-03-01T09%3A51%3A43.041-07%3A00&Name=George" + wrk.headers["Content-Type"] = "application/x-www-form-urlencoded" + wrk.method = "POST" +end diff --git a/benchmarkapps/BasicViews/postWithToken.lua b/benchmarkapps/BasicViews/postWithToken.lua new file mode 100644 index 0000000000..a67309c671 --- /dev/null +++ b/benchmarkapps/BasicViews/postWithToken.lua @@ -0,0 +1,55 @@ +-- script that retrieves an antiforgery token to send in all future requests and adds a body for those requests + +-- do not use wrk's default request +local req = nil + +-- use token for at most maxRequests, default throughout test +local counter = 0 +local maxRequests = -1 + +-- marker that we have completed the first request +local token = nil + +function init(args) + -- initialize first (empty) request + req = wrk.format("GET") +end + +function request() + return req +end + +function response(status, headers, body) + if not token and status == 200 then + local cookie = string.gsub(headers["Set-Cookie"], "^([^;]*)(;.*)?$", "%1") + if not cookie or cookie == "" then + print("Unable to find antiforgery cookie in initial response!") + wrk.thread:stop() + end + + token = string.gsub(body, '^.* name="__RequestVerificationToken".* value="([^"]*)"[ >].*$', "%1") + if not token or token == "" then + print("Unable to find antiforgery token in initial response!") + wrk.thread:stop() + end + + wrk.body = "Age=12&BirthDate=2006-03-01T09%3A51%3A43.041-07%3A00&Name=George&__RequestVerificationToken=" .. token + wrk.headers["Content-Type"] = "application/x-www-form-urlencoded" + wrk.headers["Cookie"] = cookie + wrk.method = "POST" + + req = wrk.format() + return + end + + if not token then + print("Failed initial request! status: " .. status) + wrk.thread:stop() + end + + if counter == maxRequests then + wrk.thread:stop() + end + + counter = counter + 1 +end diff --git a/benchmarkapps/BasicViews/runtimeconfig.template.json b/benchmarkapps/BasicViews/runtimeconfig.template.json new file mode 100644 index 0000000000..0976c49b2c --- /dev/null +++ b/benchmarkapps/BasicViews/runtimeconfig.template.json @@ -0,0 +1,5 @@ +{ + "configProperties": { + "System.GC.Server": true + } +} diff --git a/benchmarkapps/BasicViews/web.config b/benchmarkapps/BasicViews/web.config new file mode 100644 index 0000000000..be088b9ef4 --- /dev/null +++ b/benchmarkapps/BasicViews/web.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/benchmarkapps/BasicViews/wwwroot/css/site.css b/benchmarkapps/BasicViews/wwwroot/css/site.css new file mode 100644 index 0000000000..7621c24947 --- /dev/null +++ b/benchmarkapps/BasicViews/wwwroot/css/site.css @@ -0,0 +1,6 @@ +label { + font-size: 1.2em; +} +.test-it { + float: right; +} \ No newline at end of file diff --git a/benchmarkapps/BasicViews/wwwroot/css/site.min.css b/benchmarkapps/BasicViews/wwwroot/css/site.min.css new file mode 100644 index 0000000000..fda38a3c0e --- /dev/null +++ b/benchmarkapps/BasicViews/wwwroot/css/site.min.css @@ -0,0 +1,6 @@ +label { + font-size: 1.3em; +} +.test-it { + float: right; +} \ No newline at end of file diff --git a/benchmarkapps/BasicViews/wwwroot/js/site.js b/benchmarkapps/BasicViews/wwwroot/js/site.js new file mode 100644 index 0000000000..894706a762 --- /dev/null +++ b/benchmarkapps/BasicViews/wwwroot/js/site.js @@ -0,0 +1,3 @@ +console.log("Hello World"); +function test() { +} \ No newline at end of file diff --git a/benchmarkapps/BasicViews/wwwroot/js/site.min.js b/benchmarkapps/BasicViews/wwwroot/js/site.min.js new file mode 100644 index 0000000000..121d23bfa6 --- /dev/null +++ b/benchmarkapps/BasicViews/wwwroot/js/site.min.js @@ -0,0 +1,3 @@ +console.log("Hello Minified World"); +function test() { +} \ No newline at end of file diff --git a/benchmarkapps/README.md b/benchmarkapps/README.md new file mode 100644 index 0000000000..bf86159bcc --- /dev/null +++ b/benchmarkapps/README.md @@ -0,0 +1,15 @@ +## Purpose + +These projects assist in Benchmarking MVC. +They makes it easier to test local changes than having the App in the Benchmarks repo by letting us make changes in MVC branches and use the example commandline below to run the benchmarks against our branches. + +## Usage + +1. Push changes you would like to test to a branch on GitHub +2. Clone aspnet/benchmarks repo to your machine or install the global BenchmarksDriver tool https://www.nuget.org/packages/BenchmarksDriver/ +3. If cloned go to the BenchmarksDriver project +4. Use the following command as a guideline for running a test using your changes + +`benchmarks --server --client -j https://raw.githubusercontent.com/aspnet/MVC/dev/benchmarkaps/BasicApi/BasicApi.json` + +5. For more info/commands see https://github.com/aspnet/benchmarks/blob/dev/src/BenchmarksDriver/README.md diff --git a/build/dependencies.props b/build/dependencies.props index 22e161b819..573fdcc668 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,12 +5,27 @@ 0.9.9 0.10.13 + + + 2.1.0 + 2.1.0 + 2.1.0 + 0.42.1 + 2.1.0 + 2.1.0-rc1-final + 2.2.0-preview1-34492 2.2.0-preview1-17090 + 2.2.0-preview1-34492 2.2.0-preview1-34492 2.2.0-preview1-34492 2.2.0-preview1-34492 2.2.0-preview1-34492 + 2.2.0-preview1-34492 2.2.0-preview1-34492 2.2.0-preview1-34492 2.2.0-preview1-34492 @@ -51,6 +66,7 @@ 1.7.0 2.2.0-preview1-34492 2.2.0-preview1-34492 + 2.2.0-preview1-34492 2.2.0-preview1-34492 2.2.0-preview1-34492 2.2.0-preview1-34492 diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicApiTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicApiTest.cs new file mode 100644 index 0000000000..42d2a5a5ce --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicApiTest.cs @@ -0,0 +1,223 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class BasicApiTest : IClassFixture + { + private static readonly byte[] PetBytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false) + .GetBytes(@"{ + ""category"" : { + ""name"" : ""Cats"" + }, + ""images"": [ + { + ""url"": ""http://example.com/images/fluffy1.png"" + }, + { + ""url"": ""http://example.com/images/fluffy2.png"" + }, + ], + ""tags"": [ + { + ""name"": ""orange"" + }, + { + ""name"": ""kitty"" + } + ], + ""age"": 2, + ""hasVaccinations"": ""true"", + ""name"" : ""fluffy"", + ""status"" : ""available"" +}"); + + public BasicApiTest(BasicApiFixture fixture) + { + Client = fixture.CreateClient(); + } + + public HttpClient Client { get; } + + [Fact] + public async Task Token_WithUnknownUser_ReturnsForbidden() + { + // Arrange & Act + var response = await Client.GetAsync("/token?username=fallguy@example.com"); + + // Assert + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task Token_WithKnownUser_ReturnsOkAndToken() + { + // Arrange & Act + var response = await Client.GetAsync("/token?username=reader@example.com"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType); + + var token = await response.Content.ReadAsStringAsync(); + Assert.NotNull(token); + Assert.NotEmpty(token); + } + + [Fact] + public async Task FindByStatus_WithNoToken_ReturnsUnauthorized() + { + // Arrange & Act + var response = await Client.GetAsync("/pet/findByStatus?status=available"); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [InlineData("reader@example.com")] + [InlineData("writer@example.com")] + public async Task FindByStatus_WithToken_ReturnsOkAndPet(string username) + { + // Arrange & Act 1 + var token = await Client.GetStringAsync($"/token?username={username}"); + + // Assert 1 (guard) + Assert.NotEmpty(token); + + // Arrange 2 + var request = new HttpRequestMessage(HttpMethod.Get, "/pet/findByStatus?status=available"); + request.Headers.Add(HeaderNames.Authorization, $"Bearer {token}"); + + // Act 2 + var response = await Client.SendAsync(request); + + // Assert 2 + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + var json = await response.Content.ReadAsStringAsync(); + Assert.NotNull(json); + Assert.NotEmpty(json); + } + + [Fact] + public async Task FindById_WithInvalidPetId_ReturnsNotFound() + { + // Arrange & Act 1 + var token = await Client.GetStringAsync("/token?username=reader@example.com"); + + // Assert 1 (guard) + Assert.NotEmpty(token); + + // Arrange 2 + var request = new HttpRequestMessage(HttpMethod.Get, "/pet/100"); + request.Headers.Add(HeaderNames.Authorization, $"Bearer {token}"); + + // Act 2 + var response = await Client.SendAsync(request); + + // Assert 2 + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task FindById_WithValidPetId_ReturnsOkAndPet() + { + // Arrange & Act 1 + var token = await Client.GetStringAsync("/token?username=reader@example.com"); + + // Assert 1 (guard) + Assert.NotEmpty(token); + + // Arrange 2 + var request = new HttpRequestMessage(HttpMethod.Get, "/pet/-1"); + request.Headers.Add(HeaderNames.Authorization, $"Bearer {token}"); + + // Act 2 + var response = await Client.SendAsync(request); + + // Assert 2 + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); + + var json = await response.Content.ReadAsStringAsync(); + Assert.NotNull(json); + Assert.NotEmpty(json); + } + + [Fact] + public async Task AddPet_WithInsufficientClaims_ReturnsForbidden() + { + // Arrange & Act 1 + var token = await Client.GetStringAsync("/token?username=reader@example.com"); + + // Assert 1 (guard) + Assert.NotEmpty(token); + + // Arrange 2 + var request = new HttpRequestMessage(HttpMethod.Post, "/pet") + { + Content = new ByteArrayContent(PetBytes) + { + Headers = + { + { "Content-Type", "application/json" }, + }, + }, + Headers = + { + { HeaderNames.Authorization, $"Bearer {token}" }, + }, + }; + + // Act 2 + var response = await Client.SendAsync(request); + + // Assert 2 + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task AddPet_WithValidClaims_ReturnsCreated() + { + // Arrange & Act 1 + var token = await Client.GetStringAsync("/token?username=writer@example.com"); + + // Assert 1 (guard) + Assert.NotEmpty(token); + + // Arrange 2 + var request = new HttpRequestMessage(HttpMethod.Post, "/pet") + { + Content = new ByteArrayContent(PetBytes) + { + Headers = + { + { HeaderNames.ContentType, "application/json" }, + }, + }, + Headers = + { + { HeaderNames.Authorization, $"Bearer {token}" }, + }, + }; + + // Act 2 + var response = await Client.SendAsync(request); + + // Assert 2 + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + var location = response.Headers.Location.ToString(); + Assert.NotNull(location); + Assert.EndsWith("/1", location); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicViewsTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicViewsTest.cs new file mode 100644 index 0000000000..5f9b0bbcd8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicViewsTest.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class BasicViewsTest : IClassFixture + { + public BasicViewsTest(BasicViewsFixture fixture) + { + Client = fixture.CreateClient(); + } + + public HttpClient Client { get; } + + [Theory] + [InlineData("/")] + [InlineData("/Home/HtmlHelpers")] + public async Task Get_ReturnsOkAndAntiforgeryToken(string path) + { + // Arrange & Act + var response = await Client.GetAsync(path); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/html", response.Content.Headers.ContentType.MediaType); + + var html = await response.Content.ReadAsStringAsync(); + Assert.NotNull(html); + Assert.NotEmpty(html); + + var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(html, "/"); + Assert.NotNull(token); + Assert.NotEmpty(token); + } + + [Theory] + [InlineData("/")] + [InlineData("/Home/HtmlHelpers")] + public async Task Post_ReturnsOkAndNewPerson(string path) + { + // Arrange & Act 1 + var html = await Client.GetStringAsync(path); + + // Assert 1 (guard) + Assert.NotEmpty(html); + + // Arrange 2 + var token = AntiforgeryTestHelper.RetrieveAntiforgeryToken(html, "/"); + var name = Guid.NewGuid().ToString(); + name = name.Substring(startIndex: 0, length: name.LastIndexOf('-')); + var form = new Dictionary + { + { "__RequestVerificationToken", token }, + { "Age", "12" }, + { "BirthDate", "2006-03-01T09:51:43.041-07:00" }, + { "Name", name }, + }; + + var content = new FormUrlEncodedContent(form); + var request = new HttpRequestMessage(HttpMethod.Post, path) + { + Content = content, + }; + + // Act 2 + var response = await Client.SendAsync(request); + + // Assert 2 + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.NotNull(body); + Assert.Contains($@"value=""{name}""", body); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/BasicApiFixture.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/BasicApiFixture.cs new file mode 100644 index 0000000000..56294311a7 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/BasicApiFixture.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BasicApi; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class BasicApiFixture : MvcTestFixture + { + // Do not leave .db file behind. Also, ensure added pet gets expected id (1) in subsequent runs. + protected override void Dispose(bool disposing) + { + if (disposing) + { + Startup.DropDatabase(Server.Host.Services); + } + + base.Dispose(disposing); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/BasicViewsFixture.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/BasicViewsFixture.cs new file mode 100644 index 0000000000..3ea0c60835 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Infrastructure/BasicViewsFixture.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using BasicViews; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class BasicViewsFixture : MvcTestFixture + { + // Do not leave .db file behind. + protected override void Dispose(bool disposing) + { + if (disposing) + { + Startup.DropDatabase(Server.Host.Services); + } + + base.Dispose(disposing); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj index b47eec39bf..ac7be75576 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj @@ -24,6 +24,8 @@ + +