diff --git a/Hutopy.sln b/Hutopy.sln index 5714f45..2fe5931 100644 --- a/Hutopy.sln +++ b/Hutopy.sln @@ -25,6 +25,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Packages.props = Directory.Packages.props global.json = global.json README.md = README.md + start-infrastructure.sh = start-infrastructure.sh EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{4E4EE20C-F06A-4A1B-851F-C5577796941C}" diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index e03ce4c..b8f43d6 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -15,22 +15,13 @@ namespace Hutopy.Infrastructure; public static class DependencyInjection { - public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, + IConfiguration configuration) { // Replace password in the connection string with env var in local environment. // Prod will use the connectionString stored in the vault with password in it directly. - var connectionString = configuration.GetConnectionString("DefaultConnection") ?? ""; - - var dbPassword = configuration["DB_PASSWORD"] ?? ""; - var dbHost = configuration["DB_HOST"] ?? "localhost"; - - if (dbPassword != string.Empty) - { - connectionString = connectionString.Replace("{DB_PASSWORD}", dbPassword); - connectionString = connectionString.Replace("{DB_HOST}", dbHost); - } - - Guard.Against.Null(connectionString, message: "Connection string 'DefaultConnection' not found."); + var connectionString = configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Missing ConnectionString: DefaultConnection"); services.AddScoped(); services.AddScoped(); @@ -49,15 +40,9 @@ public static class DependencyInjection .AddBearerToken(IdentityConstants.BearerScheme); services.AddAuthorizationBuilder(); - - services.AddIdentityCore(options => - { - options.Password.RequireDigit = false; - options.Password.RequireLowercase = false; - options.Password.RequireUppercase = false; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequiredLength = 8; - }) + + services + .AddIdentityCore() .AddRoles() .AddEntityFrameworkStores() .AddApiEndpoints() diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index 27f1ac1..2a76f33 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -39,25 +39,6 @@ public static class DependencyInjection services.AddEndpointsApiExplorer(); - services.AddOpenApiDocument((configure, sp) => - { - configure.Title = "Hutopy API"; - - // Add JWT - configure.AddSecurity( - "JWT", - [], - new OpenApiSecurityScheme - { - Type = OpenApiSecuritySchemeType.ApiKey, - Name = "Authorization", - In = OpenApiSecurityApiKeyLocation.Header, - Description = "Type into the textbox: Bearer {your JWT token}." - }); - - configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); - }); - return services; } @@ -78,7 +59,8 @@ public static class DependencyInjection public static IServiceCollection AddAuthorizationAndAuthentication(this IServiceCollection services, ConfigurationManager configuration) { - var authenticationBuilder = services.AddAuthentication(options => + var authenticationBuilder = services + .AddAuthentication(options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; @@ -86,8 +68,7 @@ public static class DependencyInjection .AddCookie("Identity.Application", options => { options.LoginPath = "/api/Users/login"; - }) - .AddCookie(); + }); var authJwt = configuration.GetSection("Authentication:Jwt"); if (authJwt.Exists()) diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 781bdc2..ea5b072 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -4,6 +4,8 @@ using Hutopy.Infrastructure.Data; using Hutopy.Web; using Azure.Identity; using Microsoft.AspNetCore.HttpOverrides; +using NSwag; +using NSwag.Generation.Processors.Security; var builder = WebApplication.CreateBuilder(args); @@ -48,6 +50,25 @@ builder.Services.AddWebServices(); builder.Services.AddAuthorizationAndAuthentication(builder.Configuration); builder.Services.AddControllers(); +builder.Services.AddOpenApiDocument((configure, sp) => +{ + configure.Title = "Hutopy API"; + + // Add JWT + configure.AddSecurity( + "JWT", + [], + new OpenApiSecurityScheme + { + Type = OpenApiSecuritySchemeType.ApiKey, + Name = "Authorization", + In = OpenApiSecurityApiKeyLocation.Header, + Description = "Type into the textbox: Bearer {your JWT token}." + }); + + configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); + }); + var app = builder.Build(); app.UseForwardedHeaders( @@ -75,22 +96,18 @@ app.UseHealthChecks("/health"); app.UseHttpsRedirection(); app.UseStaticFiles(); -app.UseSwaggerUi(settings => +if (app.Environment.IsDevelopment()) { - settings.Path = "/api"; - settings.DocumentPath = "/api/specification.json"; -}); + app.UseOpenApi(); + app.UseSwaggerUi(options => options.Path = "/api"); +} app.MapControllerRoute( name: "default", pattern: "{controller}/{action=Index}/{id?}"); -app.MapFallbackToFile("index.html"); - -app.UseExceptionHandler(options => { }); - -app.Map("/", () => Results.Redirect("/api")); - +//TODO: validate the behavior +// app.UseExceptionHandler(); app.MapEndpoints(); app.Run(); diff --git a/src/Web/Properties/launchSettings.json b/src/Web/Properties/launchSettings.json index 6ef4f83..39a9650 100644 --- a/src/Web/Properties/launchSettings.json +++ b/src/Web/Properties/launchSettings.json @@ -8,9 +8,9 @@ } }, "profiles": { - "Hutopy.Web": { + "Hutopy.Web - DEV": { "commandName": "Project", - "launchBrowser": true, + "launchBrowser": false, "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -18,7 +18,7 @@ }, "IIS Express": { "commandName": "IISExpress", - "launchBrowser": true, + "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 9193242..80a1ea9 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -7,46 +7,26 @@ - - + + - - - - - - - - - - + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive - all - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - - OnBuildSuccess - - - - - - - - - - - - diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json index 9dad30e..ec39dd6 100644 --- a/src/Web/appsettings.Development.json +++ b/src/Web/appsettings.Development.json @@ -8,7 +8,7 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Server=localhost,1433;Database=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True" + "DefaultConnection": "Server=localhost,1433;Initial Catalog=Hutopy;User Id=sa;Password=P@ssword123!;MultipleActiveResultSets=true;TrustServerCertificate=True" }, "Authentication": { "Jwt": { diff --git a/src/Web/config.nswag b/src/Web/config.nswag deleted file mode 100644 index 4af18f2..0000000 --- a/src/Web/config.nswag +++ /dev/null @@ -1,63 +0,0 @@ -{ - "runtime": "Net80", - "defaultVariables": null, - "documentGenerator": { - "aspNetCoreToOpenApi": { - "project": "Web.csproj", - "msBuildProjectExtensionsPath": null, - "configuration": null, - "runtime": null, - "targetFramework": null, - "noBuild": true, - "msBuildOutputPath": null, - "verbose": false, - "workingDirectory": null, - "requireParametersWithoutDefault": true, - "apiGroupNames": null, - "defaultPropertyNameHandling": "CamelCase", - "defaultReferenceTypeNullHandling": "Null", - "defaultDictionaryValueReferenceTypeNullHandling": "NotNull", - "defaultResponseReferenceTypeNullHandling": "NotNull", - "generateOriginalParameterNames": true, - "defaultEnumHandling": "Integer", - "flattenInheritanceHierarchy": false, - "generateKnownTypes": true, - "generateEnumMappingDescription": false, - "generateXmlObjects": false, - "generateAbstractProperties": false, - "generateAbstractSchemas": true, - "ignoreObsoleteProperties": false, - "allowReferencesWithProperties": false, - "useXmlDocumentation": true, - "resolveExternalXmlDocumentation": true, - "excludedTypeNames": [], - "serviceHost": null, - "serviceBasePath": null, - "serviceSchemes": [], - "infoTitle": "Hutopy API", - "infoDescription": null, - "infoVersion": "1.0.0", - "documentTemplate": null, - "documentProcessorTypes": [], - "operationProcessorTypes": [], - "typeNameGeneratorType": null, - "schemaNameGeneratorType": null, - "contractResolverType": null, - "serializerSettingsType": null, - "useDocumentProvider": true, - "documentName": "v1", - "aspNetCoreEnvironment": null, - "createWebHostBuilderMethod": null, - "startupType": null, - "allowNullableBodyParameters": true, - "useHttpAttributeNameAsOperationId": false, - "output": "wwwroot/api/specification.json", - "outputType": "OpenApi3", - "newLineBehavior": "Auto", - "assemblyPaths": [], - "assemblyConfig": null, - "referencePaths": [], - "useNuGetCache": false - } - } -} diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json deleted file mode 100644 index bf1f109..0000000 --- a/src/Web/wwwroot/api/specification.json +++ /dev/null @@ -1,869 +0,0 @@ -{ - "x-generator": "NSwag v14.0.3.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))", - "openapi": "3.0.0", - "info": { - "title": "Hutopy API", - "version": "1.0.0" - }, - "paths": { - "/api/GetMyUser": { - "get": { - "tags": [ - "GetMyUser" - ], - "operationId": "GetCurrentUser", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserDto" - } - } - } - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/GetMyUser/profile-picture": { - "get": { - "tags": [ - "GetMyUser" - ], - "operationId": "GetCurrentUserProfilePicture", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Stream" - } - } - } - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/GetMyUser/profile-picture-2": { - "patch": { - "tags": [ - "GetMyUser" - ], - "operationId": "PatchApiGetMyUserProfilePicture2", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Stream" - } - } - } - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/JoinUs": { - "get": { - "tags": [ - "JoinUs" - ], - "operationId": "GetFutureCreators", - "parameters": [ - { - "name": "PageNumber", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - }, - "x-position": 1 - }, - { - "name": "PageSize", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - }, - "x-position": 2 - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaginatedListOfFutureCreatorListDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "JoinUs" - ], - "operationId": "CreateFutureCreator", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateFutureCreatorCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string", - "format": "guid" - } - } - } - } - } - } - }, - "/api/Stripe/confirmTransaction": { - "post": { - "tags": [ - "Stripe" - ], - "operationId": "ConfirmTransaction", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConfirmStripeTransactionCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/Stripe/getMyLastReceipt": { - "get": { - "tags": [ - "Stripe" - ], - "operationId": "GetMyLastReceipt", - "parameters": [ - { - "name": "Email", - "in": "query", - "required": true, - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - }, - { - "name": "CreatorId", - "in": "query", - "required": true, - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 2 - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MyLastReceiptDto" - } - } - } - } - } - } - }, - "/api/Stripe": { - "post": { - "tags": [ - "Stripe" - ], - "operationId": "CreateSessionCheckout", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSessionCheckoutCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/Users": { - "post": { - "tags": [ - "Users" - ], - "operationId": "CreateUser", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateUserCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string", - "format": "guid" - } - } - } - } - } - }, - "get": { - "tags": [ - "Users" - ], - "operationId": "GetMinimalUser", - "parameters": [ - { - "name": "UserId", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - }, - { - "name": "UserName", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 2 - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MinimalUserDto" - } - } - } - } - } - } - }, - "/api/Users/login": { - "post": { - "tags": [ - "Users" - ], - "operationId": "Login", - "requestBody": { - "x-name": "command", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginCommand" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/Users/upload-profile-picture": { - "post": { - "tags": [ - "Users" - ], - "operationId": "UploadProfilePicture", - "requestBody": { - "x-name": "stream", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary", - "nullable": false - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - } - } - } - } - }, - "/api/WeatherForecasts": { - "get": { - "tags": [ - "WeatherForecasts" - ], - "operationId": "GetWeatherForecasts", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WeatherForecast" - } - } - } - } - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/facebook/sign-in": { - "get": { - "tags": [ - "Facebook" - ], - "operationId": "Facebook_SignIn", - "responses": { - "200": { - "description": "" - } - } - } - }, - "/api/google/sign-in": { - "post": { - "tags": [ - "Google" - ], - "operationId": "Google_SignIn", - "requestBody": { - "x-name": "request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GoogleSignInRequest" - } - } - }, - "required": true, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "UserDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "format": "guid" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "userName": { - "type": "string" - }, - "userTransactions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserTransactionDto" - } - }, - "userRoles": { - "type": "array", - "items": { - "type": "string" - } - }, - "totalBalance": { - "type": "number", - "format": "decimal" - } - } - }, - "UserTransactionDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "amount": { - "type": "number", - "format": "decimal" - }, - "currency": { - "type": "string" - }, - "tipMessage": { - "type": "string" - }, - "created": { - "type": "string", - "format": "date-time" - }, - "isConfirmed": { - "type": "boolean" - } - } - }, - "Stream": { - "allOf": [ - { - "$ref": "#/components/schemas/MarshalByRefObject" - }, - { - "type": "object", - "x-abstract": true, - "additionalProperties": false, - "properties": { - "canRead": { - "type": "boolean" - }, - "canWrite": { - "type": "boolean" - }, - "canSeek": { - "type": "boolean" - }, - "canTimeout": { - "type": "boolean" - }, - "length": { - "type": "integer", - "format": "int64" - }, - "position": { - "type": "integer", - "format": "int64" - }, - "readTimeout": { - "type": "integer", - "format": "int32" - }, - "writeTimeout": { - "type": "integer", - "format": "int32" - } - } - } - ] - }, - "MarshalByRefObject": { - "type": "object", - "x-abstract": true, - "additionalProperties": false - }, - "PaginatedListOfFutureCreatorListDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FutureCreatorListDto" - } - }, - "pageNumber": { - "type": "integer", - "format": "int32" - }, - "totalPages": { - "type": "integer", - "format": "int32" - }, - "totalCount": { - "type": "integer", - "format": "int32" - }, - "hasPreviousPage": { - "type": "boolean" - }, - "hasNextPage": { - "type": "boolean" - } - } - }, - "FutureCreatorListDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "format": "guid" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - } - } - }, - "CreateFutureCreatorCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "emailAddress": { - "type": "string" - }, - "phoneNumber": { - "type": "string" - }, - "socialNetworkAccount": { - "type": "string" - }, - "reasonToJoin": { - "type": "string" - } - } - }, - "ConfirmStripeTransactionCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "object": { - "type": "string" - }, - "created": { - "type": "integer", - "format": "int32" - }, - "data": { - "$ref": "#/components/schemas/Data" - }, - "request": { - "$ref": "#/components/schemas/Request" - } - } - }, - "Data": { - "type": "object", - "additionalProperties": false, - "properties": { - "object": { - "$ref": "#/components/schemas/Object" - } - } - }, - "Object": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - }, - "amount": { - "type": "integer", - "format": "int32" - }, - "billing_details": { - "$ref": "#/components/schemas/BillingDetails" - }, - "calculated_statement_descriptor": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "paid": { - "type": "boolean" - }, - "payment_intent": { - "type": "string" - }, - "payment_method": { - "type": "string" - }, - "receipt_url": { - "type": "string" - }, - "status": { - "type": "string" - }, - "failure_message": { - "type": "string" - } - } - }, - "BillingDetails": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phone": { - "type": "string" - } - } - }, - "Request": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "type": "string" - } - } - }, - "MyLastReceiptDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "receiptUrl": { - "type": "string" - } - } - }, - "CreateSessionCheckoutCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "creatorId": { - "type": "string" - }, - "amount": { - "type": "integer", - "format": "int32" - }, - "currency": { - "type": "string" - }, - "tipMessage": { - "type": "string" - } - } - }, - "CreateUserCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "emailAddress": { - "type": "string" - }, - "userName": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "LoginCommand": { - "type": "object", - "additionalProperties": false, - "properties": { - "emailAddress": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "MinimalUserDto": { - "type": "object", - "additionalProperties": false, - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "userName": { - "type": "string" - } - } - }, - "WeatherForecast": { - "type": "object", - "additionalProperties": false, - "properties": { - "date": { - "type": "string", - "format": "date-time" - }, - "temperatureC": { - "type": "integer", - "format": "int32" - }, - "temperatureF": { - "type": "integer", - "format": "int32" - }, - "summary": { - "type": "string", - "nullable": true - } - } - }, - "GoogleSignInRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "accessToken": { - "type": "string" - } - } - } - }, - "securitySchemes": { - "JWT": { - "type": "apiKey", - "description": "Type into the textbox: Bearer {your JWT token}.", - "name": "Authorization", - "in": "header" - } - } - }, - "security": [ - { - "JWT": [] - } - ] -} \ No newline at end of file diff --git a/tests/Application.FunctionalTests/TestcontainersTestDatabase.cs b/tests/Application.FunctionalTests/TestcontainersTestDatabase.cs index eaf57f8..342336f 100644 --- a/tests/Application.FunctionalTests/TestcontainersTestDatabase.cs +++ b/tests/Application.FunctionalTests/TestcontainersTestDatabase.cs @@ -35,11 +35,11 @@ public class TestcontainersTestDatabase : ITestDatabase var context = new ApplicationDbContext(options); - context.Database.Migrate(); + await context.Database.MigrateAsync(); _respawner = await Respawner.CreateAsync(_connectionString, new RespawnerOptions { - TablesToIgnore = new Respawn.Graph.Table[] { "__EFMigrationsHistory" } + TablesToIgnore = ["__EFMigrationsHistory"] }); }