diff --git a/CleanAspCore.Api.Tests/CleanAspCore.Api.Tests.csproj b/CleanAspCore.Api.Tests/CleanAspCore.Api.Tests.csproj index 0a508be..568c876 100644 --- a/CleanAspCore.Api.Tests/CleanAspCore.Api.Tests.csproj +++ b/CleanAspCore.Api.Tests/CleanAspCore.Api.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/CleanAspCore.Api.Tests/Features/Departments/DepartmentControllerTests.cs b/CleanAspCore.Api.Tests/Features/Departments/DepartmentControllerTests.cs index b9a2f4f..6e1c7ff 100644 --- a/CleanAspCore.Api.Tests/Features/Departments/DepartmentControllerTests.cs +++ b/CleanAspCore.Api.Tests/Features/Departments/DepartmentControllerTests.cs @@ -1,7 +1,4 @@ -using System.Net.Http.Json; -using CleanAspCore.Api.Tests.Helpers; -using CleanAspCore.Domain.Department; -using FluentAssertions; +using CleanAspCore.Domain.Department; namespace CleanAspCore.Api.Tests.Features.Departments; @@ -11,17 +8,19 @@ public class DepartmentControllerTests : IntegrationTestBase public async Task SearchDepartments_ReturnsExpectedDepartments() { //Arrange - Context.Departments.Add(new Department() + await using var api = CreateApi(); + api.SeedData(context => { - Id = 0, - Name = "Foo", - City = "bar", + context.Departments.Add(new Department() + { + Id = 0, + Name = "Foo", + City = "bar", + }); }); - - await Context.SaveChangesAsync(); //Act - var result = await Client.GetFromJsonAsync("Department"); + var result = await api.CreateClient().GetFromJsonAsync("Department"); //Assert result.Should().BeEquivalentTo(new[] diff --git a/CleanAspCore.Api.Tests/Features/Employees/EmployeeControllerTests.cs b/CleanAspCore.Api.Tests/Features/Employees/EmployeeControllerTests.cs index 71ed0a0..d8ccdfa 100644 --- a/CleanAspCore.Api.Tests/Features/Employees/EmployeeControllerTests.cs +++ b/CleanAspCore.Api.Tests/Features/Employees/EmployeeControllerTests.cs @@ -1,9 +1,6 @@ -using System.Net.Http.Json; -using CleanAspCore.Api.Tests.Helpers; -using CleanAspCore.Domain.Department; +using CleanAspCore.Domain.Department; using CleanAspCore.Domain.Employee; using CleanAspCore.Domain.Job; -using FluentAssertions; namespace CleanAspCore.Api.Tests.Features.Employees; @@ -13,34 +10,36 @@ public class EmployeeControllerTests : IntegrationTestBase public async Task SearchEmployee_ReturnsExpectedJobs() { //Arrange - Context.Employees.Add(new Employee() + await using var api = CreateApi(); + api.SeedData(context => { - Id = 0, - FirstName = "Foo", - LastName = "Bar", - Email = "email", - Gender = "Weird", - DepartmentId = 1, - JobId = 2 - }); + context.Employees.Add(new Employee() + { + Id = 0, + FirstName = "Foo", + LastName = "Bar", + Email = "email", + Gender = "Weird", + DepartmentId = 1, + JobId = 2 + }); - Context.Departments.Add(new Department() - { - Id = 1, - Name = "Foo", - City = "Bar" - }); + context.Departments.Add(new Department() + { + Id = 1, + Name = "Foo", + City = "Bar" + }); - Context.Jobs.Add(new Job - { - Id = 2, - Name = "Foo", + context.Jobs.Add(new Job + { + Id = 2, + Name = "Foo", + }); }); - - await Context.SaveChangesAsync(); //Act - var result = await Client.GetFromJsonAsync("Employee"); + var result = await api.CreateClient().GetFromJsonAsync("Employee"); //Assert result.Should().BeEquivalentTo(new[] @@ -62,21 +61,23 @@ public async Task SearchEmployee_ReturnsExpectedJobs() public async Task AddEmployee_IsAdded() { //Arrange - Context.Departments.Add(new Department() + await using var api = CreateApi(); + api.SeedData(context => { - Id = 1, - Name = "Foo", - City = "Bar" - }); + context.Departments.Add(new Department() + { + Id = 1, + Name = "Foo", + City = "Bar" + }); - Context.Jobs.Add(new Job() - { - Id = 2, - Name = "Foo", + context.Jobs.Add(new Job() + { + Id = 2, + Name = "Foo", + }); }); - await Context.SaveChangesAsync(); - var newEmployee = new EmployeeDto() { Id = 0, @@ -89,33 +90,39 @@ public async Task AddEmployee_IsAdded() }; //Act - var result = await Client.PostAsJsonAsync("Employee", newEmployee); + var result = await api.CreateClient().PostAsJsonAsync("Employee", newEmployee); result.EnsureSuccessStatusCode(); //Assert - Context.Employees.Should().BeEquivalentTo(new[] + api.AssertDatabase(context => { - new Employee() + context.Employees + .Include(x => x.Department) + .Include(x => x.Job) + .Should().BeEquivalentTo(new[] { - Id = 0, - FirstName = "Foo", - LastName = "Bar", - Email = "email", - Gender = "Weird", - DepartmentId = 1, - Department = new Department() + new Employee() { - Id = 1, - Name = "Foo", - City = "Bar" - }, - JobId = 2, - Job = new Job() - { - Id = 2, - Name = "Foo", + Id = 0, + FirstName = "Foo", + LastName = "Bar", + Email = "email", + Gender = "Weird", + DepartmentId = 1, + Department = new Department() + { + Id = 1, + Name = "Foo", + City = "Bar" + }, + JobId = 2, + Job = new Job() + { + Id = 2, + Name = "Foo", + } } - } + }); }); } diff --git a/CleanAspCore.Api.Tests/Features/Import/ImportControllerTests.cs b/CleanAspCore.Api.Tests/Features/Import/ImportControllerTests.cs index f061350..b7ff0de 100644 --- a/CleanAspCore.Api.Tests/Features/Import/ImportControllerTests.cs +++ b/CleanAspCore.Api.Tests/Features/Import/ImportControllerTests.cs @@ -1,26 +1,79 @@ -using System.Text.Json; +using CleanAspCore.Domain.Department; using CleanAspCore.Domain.Employee; +using CleanAspCore.Domain.Job; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.FileProviders; namespace CleanAspCore.Api.Tests.Features.Import; -public class ImportControllerTests +public class ImportControllerTests : IntegrationTestBase { [Fact] - public async Task foo() + public async Task Import_SingleNewEmployee_IsImported() { - var foo = - """ -[ + await using var api = CreateApi().ConfigureServices(services => { - "id": 1, - "firstname": "Neal", - "lastname": "Collopy", - "email": "ncollopy0@slate.com", - "gender": "Male", - "Department": 1, - "Job": 2 - }] -"""; - var result = JsonSerializer.Deserialize(foo); + var fileProviderMock = new Mock() + .SetupJsonFileMock("TestData/Employee.json", new[] + { + new Employee + { + Id = 1, + FirstName = "Foo", + LastName = "Bar", + Email = "email", + Gender = "Weird", + JobId = 2, + DepartmentId = 3 + } + }) + .SetupJsonFileMock("TestData/Job.json", Array.Empty()) + .SetupJsonFileMock("TestData/Department.json", Array.Empty()); + + services.Replace(new ServiceDescriptor(typeof(IFileProvider), fileProviderMock.Object)); + }); + + api.SeedData(context => + { + context.Jobs.Add(new Job + { + Id = 2, + Name = "Foo", + }); + + context.Departments.Add(new Department + { + Id = 3, + Name = "Bar", + City = "Foo" + }); + }); + + //Act + var result = await api.CreateClient().PutAsync("Import", null); + result.EnsureSuccessStatusCode(); + + //Assert + api.AssertDatabase(context => + { + context.Employees.Should().BeEquivalentTo(new [] + { + new Employee + { + Id = 1, + FirstName = "Foo", + LastName = "Bar", + Email = "email", + Gender = "Weird", + JobId = 2, + DepartmentId = 3 + } + }); + }); + } + + public ImportControllerTests(PostgreSqlLifetime fixture) : base(fixture) + { } } \ No newline at end of file diff --git a/CleanAspCore.Api.Tests/Features/Jobs/JobControllerTests.cs b/CleanAspCore.Api.Tests/Features/Jobs/JobControllerTests.cs index f425435..8a894e1 100644 --- a/CleanAspCore.Api.Tests/Features/Jobs/JobControllerTests.cs +++ b/CleanAspCore.Api.Tests/Features/Jobs/JobControllerTests.cs @@ -1,7 +1,4 @@ -using System.Net.Http.Json; -using CleanAspCore.Api.Tests.Helpers; using CleanAspCore.Domain.Job; -using FluentAssertions; namespace CleanAspCore.Api.Tests.Features.Jobs; @@ -11,16 +8,18 @@ public class JobControllerTests : IntegrationTestBase public async Task SearchJobs_ReturnsExpectedJobs() { //Arrange - Context.Jobs.Add(new Job() + await using var api = CreateApi(); + api.SeedData(context => { - Id = 0, - Name = "Foo", + context.Jobs.Add(new Job() + { + Id = 0, + Name = "Foo", + }); }); - await Context.SaveChangesAsync(); - //Act - var result = await Client.GetFromJsonAsync("Job"); + var result = await api.CreateClient().GetFromJsonAsync("Job"); //Assert result.Should().BeEquivalentTo(new[] diff --git a/CleanAspCore.Api.Tests/Helpers/FileProviderMockExtensions.cs b/CleanAspCore.Api.Tests/Helpers/FileProviderMockExtensions.cs new file mode 100644 index 0000000..4e119c1 --- /dev/null +++ b/CleanAspCore.Api.Tests/Helpers/FileProviderMockExtensions.cs @@ -0,0 +1,23 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.FileProviders; + +namespace CleanAspCore.Api.Tests.Helpers; + +public static class FileProviderMockExtensions +{ + public static Mock SetupJsonFileMock(this Mock mock, string path, object obj) + { + mock.SetupFileMock(path, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(obj))); + return mock; + } + + public static Mock SetupFileMock(this Mock mock, string path, byte[] content) + { + var fileInfoMock = new Mock(); + fileInfoMock.Setup(x => x.CreateReadStream()).Returns(new MemoryStream(content)); + + mock.Setup(x => x.GetFileInfo(path)).Returns(fileInfoMock.Object); + return mock; + } +} \ No newline at end of file diff --git a/CleanAspCore.Api.Tests/Helpers/IntegrationTestBase.cs b/CleanAspCore.Api.Tests/Helpers/IntegrationTestBase.cs index 2cad45d..25fee5c 100644 --- a/CleanAspCore.Api.Tests/Helpers/IntegrationTestBase.cs +++ b/CleanAspCore.Api.Tests/Helpers/IntegrationTestBase.cs @@ -1,4 +1,5 @@ using CleanAspCore.Data; +using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; namespace CleanAspCore.Api.Tests.Helpers; @@ -6,13 +7,33 @@ namespace CleanAspCore.Api.Tests.Helpers; [Collection("Database collection")] public abstract class IntegrationTestBase { - public readonly HttpClient Client; - public readonly HrContext Context; + private readonly PostgreSqlLifetime _fixture; public IntegrationTestBase(PostgreSqlLifetime fixture) { - var factory = new TestWebApplicationFactory($"Host=127.0.0.1;Port={fixture.Port};Database={Guid.NewGuid()};Username=postgres;Password=postgres"); - Client = factory.CreateClient(); - Context = factory.Services.CreateScope().ServiceProvider.GetRequiredService(); + _fixture = fixture; + } + + public TestWebApplicationFactory CreateApi() => + new($"Host=127.0.0.1;Port={_fixture.Port};Database={Guid.NewGuid()};Username=postgres;Password=postgres"); +} + +public static class WebApplicationFactoryExtensions +{ + public static IServiceScope CreateScope(this WebApplicationFactory webApplicationFactory) => webApplicationFactory.Services.CreateScope(); + + public static void SeedData(this WebApplicationFactory webApplicationFactory, Action seedAction) + { + using var scope = webApplicationFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + seedAction(context); + context.SaveChanges(); + } + + public static void AssertDatabase(this WebApplicationFactory webApplicationFactory, Action seedAction) + { + using var scope = webApplicationFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + seedAction(context); } } \ No newline at end of file diff --git a/CleanAspCore.Api.Tests/Helpers/TestWebApplicationFactory.cs b/CleanAspCore.Api.Tests/Helpers/TestWebApplicationFactory.cs index 945edd8..35e5b3a 100644 --- a/CleanAspCore.Api.Tests/Helpers/TestWebApplicationFactory.cs +++ b/CleanAspCore.Api.Tests/Helpers/TestWebApplicationFactory.cs @@ -6,17 +6,19 @@ namespace CleanAspCore.Api.Tests.Helpers; -public class TestWebApplicationFactory : WebApplicationFactory where TProgram : class +public class TestWebApplicationFactory : WebApplicationFactory { private readonly string _connectionString; + private Action? _configure; public TestWebApplicationFactory(string connectionString) { _connectionString = connectionString; } - + protected override IHost CreateHost(IHostBuilder builder) { + builder.UseEnvironment(Environments.Production); builder.ConfigureServices(services => { var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); @@ -27,8 +29,24 @@ protected override IHost CreateHost(IHostBuilder builder) } services.AddDbContext(options => options.UseNpgsql(_connectionString)); + + _configure?.Invoke(services); }); - return base.CreateHost(builder); + var app = base.CreateHost(builder); + + using var serviceScope = app.Services.GetRequiredService().CreateScope(); + var context = serviceScope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); + + return app; + } + + + + public TestWebApplicationFactory ConfigureServices(Action configure) + { + _configure = configure; + return this; } } \ No newline at end of file diff --git a/CleanAspCore.Api.Tests/Usings.cs b/CleanAspCore.Api.Tests/Usings.cs index 8c927eb..7976eb5 100644 --- a/CleanAspCore.Api.Tests/Usings.cs +++ b/CleanAspCore.Api.Tests/Usings.cs @@ -1 +1,6 @@ -global using Xunit; \ No newline at end of file +global using Xunit; +global using FluentAssertions; +global using Moq; +global using System.Net.Http.Json; +global using CleanAspCore.Api.Tests.Helpers; +global using Microsoft.EntityFrameworkCore; \ No newline at end of file diff --git a/CleanAspCore/CleanAspCore.csproj b/CleanAspCore/CleanAspCore.csproj index a5315ee..180061b 100644 --- a/CleanAspCore/CleanAspCore.csproj +++ b/CleanAspCore/CleanAspCore.csproj @@ -19,6 +19,7 @@ + diff --git a/CleanAspCore/DbSetExtensions.cs b/CleanAspCore/DbSetExtensions.cs new file mode 100644 index 0000000..dea858b --- /dev/null +++ b/CleanAspCore/DbSetExtensions.cs @@ -0,0 +1,14 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace CleanAspCore; + +public static class DbSetExtensions +{ + public static EntityEntry? AddIfNotExists(this DbSet dbSet, T entity, Expression> predicate = null) where T : class, new() + { + var exists = predicate != null ? dbSet.Any(predicate) : dbSet.Any(); + return !exists ? dbSet.Add(entity) : null; + } +} \ No newline at end of file diff --git a/CleanAspCore/Features/Import/HrDataReader.cs b/CleanAspCore/Features/Import/HrDataReader.cs deleted file mode 100644 index 8b7693c..0000000 --- a/CleanAspCore/Features/Import/HrDataReader.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Text.Json; -using CleanAspCore.Domain.Department; -using CleanAspCore.Domain.Employee; -using CleanAspCore.Domain.Job; - -namespace CleanAspCore.Features.Import; - -public class HrDataReader -{ - private static readonly JsonSerializerOptions Options = new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = true - }; - - public async Task GetEmployees() => await JsonSerializer.DeserializeAsync(File.OpenRead("TestData/Employee.json"), Options); - public async Task GetJobs() => await JsonSerializer.DeserializeAsync(File.OpenRead("TestData/Job.json"), Options); - public async Task GetDepartments() => await JsonSerializer.DeserializeAsync(File.OpenRead("TestData/Department.json"), Options); -} diff --git a/CleanAspCore/Features/Import/ImportController.cs b/CleanAspCore/Features/Import/ImportController.cs deleted file mode 100644 index f8313e1..0000000 --- a/CleanAspCore/Features/Import/ImportController.cs +++ /dev/null @@ -1,65 +0,0 @@ -using CleanAspCore.Domain.Department; -using CleanAspCore.Domain.Job; -using CleanAspCore.Features.Departments; -using CleanAspCore.Features.Employees; -using CleanAspCore.Features.Jobs; - -namespace CleanAspCore.Features.Import; - -[ApiController] -[Route("[controller]")] -[Produces("application/json")] -public class ImportController : Controller -{ - private readonly HrDataReader _hrDataReader; - private readonly ISender _sender; - - public ImportController(HrDataReader hrDataReader, ISender sender) - { - _hrDataReader = hrDataReader; - _sender = sender; - } - - [HttpPut] - public async Task Put() - { - await ImportJobs(); - await ImportDepartments(); - await ImportEmployees(); - - return Ok(); - } - - private async Task ImportDepartments() - { - var newEmployees = await _hrDataReader.GetDepartments(); - var existingDepartments = await _sender.Send(new GetDepartments.Request()); - var employeeToImport = newEmployees - .Where(x => existingDepartments.All(y => y.Id != x.Id)); - - await _sender.Send(new AddDepartments.Request(employeeToImport.Select(x => x.ToDomain()).ToList())); - } - - private async Task ImportJobs() - { - var newJobs = await _hrDataReader.GetJobs(); - var existingJobs = await _sender.Send(new GetJobs.Request()); - var employeeToImport = newJobs - .Where(x => existingJobs.All(y => y.Id != x.Id)); - - await _sender.Send(new AddJobs.Request(employeeToImport.Select(x => x.ToDomain()).ToList())); - } - - private async Task ImportEmployees() - { - var newEmployees = await _hrDataReader.GetEmployees(); - var existingEmployees = await _sender.Send(new GetEmployees.Request()); - var employeeToImport = newEmployees - .Where(x => existingEmployees.All(y => y.Id != x.Id)); - - foreach (var employeeDto in employeeToImport) - { - await _sender.Send(new AddEmployee.Request(employeeDto)); - } - } -} \ No newline at end of file diff --git a/CleanAspCore/Features/Import/ImportTestData.cs b/CleanAspCore/Features/Import/ImportTestData.cs new file mode 100644 index 0000000..2914885 --- /dev/null +++ b/CleanAspCore/Features/Import/ImportTestData.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using CleanAspCore.Data; +using CleanAspCore.Domain.Department; +using CleanAspCore.Domain.Employee; +using CleanAspCore.Domain.Job; +using Microsoft.Extensions.FileProviders; + +namespace CleanAspCore.Features.Import; + +public static class ImportTestData +{ + public static void MapPutImport(this IEndpointRouteBuilder endpoints) => endpoints.MapPut("Import", + async (ISender sender) => + TypedResults.Json(await sender.Send(new Request()).ToHttpResultAsync())) + .WithTags("Import"); + + public record Request : IRequest>; + + public record Handler : IRequestHandler> + { + private readonly HrContext _context; + private readonly IFileProvider _fileProvider; + + public Handler(HrContext context, IFileProvider fileProvider) + { + _context = context; + _fileProvider = fileProvider; + } + + public async ValueTask> Handle(Request request, CancellationToken cancellationToken) + { + var newJobs = await GetJobs(); + foreach (var newJob in newJobs.Select(x => x.ToDomain())) + { + _context.Jobs.AddIfNotExists(newJob); + } + + var newDepartments = await GetDepartments(); + foreach (var newDepartment in newDepartments.Select(x => x.ToDomain())) + { + _context.Departments.AddIfNotExists(newDepartment); + } + + var newEmployees = await GetEmployees(); + foreach (var newEmployee in newEmployees.Select(x => x.ToDomain())) + { + _context.Employees.AddIfNotExists(newEmployee); + } + + await _context.SaveChangesAsync(cancellationToken); + + return new Success(); + } + + + private static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true + }; + + private async Task GetEmployees() => await JsonSerializer.DeserializeAsync(_fileProvider.GetFileInfo("TestData/Employee.json").CreateReadStream(), Options); + private async Task GetJobs() => await JsonSerializer.DeserializeAsync(_fileProvider.GetFileInfo("TestData/Job.json").CreateReadStream(), Options); + private async Task GetDepartments() => await JsonSerializer.DeserializeAsync(_fileProvider.GetFileInfo("TestData/Department.json").CreateReadStream(), Options); + } +} \ No newline at end of file diff --git a/CleanAspCore/Features/Import/Registrations.cs b/CleanAspCore/Features/Import/Registrations.cs deleted file mode 100644 index 282fce4..0000000 --- a/CleanAspCore/Features/Import/Registrations.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CleanAspCore.Features.Import; - -public static class Registrations -{ - public static void AddImport(this IServiceCollection services) - { - services.AddTransient(); - } -} \ No newline at end of file diff --git a/CleanAspCore/OneOfExtensions.cs b/CleanAspCore/OneOfExtensions.cs index a1654d3..2e79025 100644 --- a/CleanAspCore/OneOfExtensions.cs +++ b/CleanAspCore/OneOfExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http.HttpResults; +using System.Runtime.InteropServices.JavaScript; +using Microsoft.AspNetCore.Http.HttpResults; using NotFound = OneOf.Types.NotFound; namespace CleanAspCore; @@ -12,7 +13,7 @@ public static class OneOfExtensions public static async ValueTask> ToHttpResultAsync(this ValueTask> oneOf) => (await oneOf).ToHttpResult(); - + public static Results ToHttpResult(this OneOf oneOf) => oneOf.Match>( success => TypedResults.Ok(), diff --git a/CleanAspCore/Program.cs b/CleanAspCore/Program.cs index 9c30aad..a14c4b2 100644 --- a/CleanAspCore/Program.cs +++ b/CleanAspCore/Program.cs @@ -5,6 +5,7 @@ using CleanAspCore.Features.Import; using CleanAspCore.Features.Jobs; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.FileProviders; var builder = WebApplication.CreateBuilder(args); @@ -16,7 +17,6 @@ builder.Services.AddSwaggerGen(); builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); -builder.Services.AddImport(); builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); builder.Services.AddMediator(options => @@ -24,11 +24,17 @@ options.ServiceLifetime = ServiceLifetime.Transient; }); +IFileProvider physicalProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory()); +builder.Services.AddSingleton(physicalProvider); + var app = builder.Build(); -using var serviceScope = app.Services.GetRequiredService().CreateScope(); -var context = serviceScope.ServiceProvider.GetRequiredService(); -context.Database.Migrate(); +if (app.Environment.IsDevelopment()) +{ + using var serviceScope = app.Services.GetRequiredService().CreateScope(); + var context = serviceScope.ServiceProvider.GetRequiredService(); + context.Database.Migrate(); +} // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) @@ -47,6 +53,7 @@ app.MapAddEmployee(); app.MapUpdateEmployeeById(); app.MapDeleteEmployeeById(); +app.MapPutImport(); app.Run();