Closed asaleh-lab closed 2 months ago
Hi, can you post your integration test with web application factory and how it is all setup?
Hi, can you post your integration test with web application factory and how it is all setup?
Sure!
this is my base calss, it doesn't have anything related to MultiTenent as I've changed the store and the strategy to InMemory & Static... I'll post the program.cs as well
using Server.Api;
using Server.Application.Abstractions.Data;
using Server.Infrastructure;
using Server.Infrastructure.Data;
using Server.Infrastructure.Interceptors;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Testcontainers.PostgreSql;
namespace Server.Application.IntegrationTests.Infrastructure;
public class IntegrationTestWebAppFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
.WithImage("postgres:latest")
.WithDatabase("server")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
}
public new async Task DisposeAsync()
{
await _dbContainer.StopAsync();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
string connectionString = SetApplicationDbContext(services);
services.RemoveAll(typeof(ISqlConnectionFactory));
services.AddSingleton<ISqlConnectionFactory>(_ =>
new SqlConnectionFactory(connectionString));
});
}
private string SetApplicationDbContext(IServiceCollection services)
{
services.RemoveAll(typeof(DbContextOptions<ApplicationDbContext>));
string connectionString = $"{_dbContainer.GetConnectionString()};Pooling=False";
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString).UseSnakeCaseNamingConvention()
.AddInterceptors(new SoftDeleteInterceptor()));
return connectionString;
}
}
One driven calss
using Server.Application.IntegrationTests.Infrastructure;
using Server.Application.Listings.AddListing;
using Server.Domain.Abstractions;
using FluentAssertions;
namespace Server.Application.IntegrationTests.Listings;
public class AddListingTests: BaseIntegrationTest
{
public AddListingTests(IntegrationTestWebAppFactory factory)
: base(factory)
{
}
[Fact]
public async Task AddListing_ShouldReturnSuccess_WhenListingIsCreated()
{
// Arrange
AddListingCommand command = ListingData.CreateAddListingCommand();
// Act
Result result = await Sender.Send(command);
// Assert
result.IsSuccess.Should().BeTrue();
}
}
Program.cs
using Asp.Versioning.ApiExplorer;
using Server.Api;
using Server.Api.Extensions;
using Server.Api.OpenApi;
using Server.Application;
using Server.Application.Listings.AddListing;
using Server.Domain.Abstractions;
using Server.Infrastructure;
using Finbuckle.MultiTenant;
using HealthChecks.UI.Client;
using MediatR;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Serilog;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((context, loggerConfig) =>
loggerConfig.ReadFrom.Configuration(context.Configuration));
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// builder.Services.AddMultiTenant<TenantInfo>()
#pragma warning disable S125
// .WithStaticStrategy("tenant-1");
#pragma warning restore S125
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration); //MultiTenant is configured here
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
builder.Services.AddAutoMapperMaps();
WebApplication app = builder.Build();
app.UseMultiTenant();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
foreach (ApiVersionDescription description in app.DescribeApiVersions())
{
string url = $"/swagger/{description.GroupName}/swagger.json";
string name = description.GroupName.ToUpperInvariant();
options.SwaggerEndpoint(url, name);
}
});
}
app.UseHttpsRedirection();
app.UseRequestContextLogging();
app.UseSerilogRequestLogging();
app.UseCustomExceptionHandler();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapGet("/api/tenant", (TenantInfo tenantInfo) => Results.Ok(new
{
tenantInfo.Id, tenantInfo.Name, tenantInfo.Identifier, tenantInfo.ConnectionString
}));
app.MapGet("/api/test", async (ISender sender) =>
{
AddListingCommand command = new(
new Name("Pasta", "ar-EG"),
new Barcode("1234567890"),
new ListingUnit(Guid.Parse("F7AE4D5C-B1B7-47F0-B0A0-D98DAC374FE8"),
new Barcode("1234567890"),
10, "EGP",
20, "EGP",
30, "EGP",
40, "EGP",
50, "EGP")
);
Result<AddListingCommandResponse> result = await sender.Send(command, CancellationToken.None);
return result.IsSuccess ? Results.Ok(result) : Results.BadRequest(result.Error);
});
await app.RunAsync();
namespace Server.Api
{
public class Program;
}
Infrastructure including MultiTenant configuration
using Server.Application.Abstractions.Clock;
using Server.Application.Abstractions.Data;
using Server.Application.Abstractions.Email;
using Server.Domain.Abstractions;
using Server.Domain.Listings;
using Server.Domain.Units;
using Server.Domain.Users;
using Server.Infrastructure.Clock;
using Server.Infrastructure.Data;
using Server.Infrastructure.Email;
using Server.Infrastructure.Interceptors;
using Server.Infrastructure.Repositories;
using Dapper;
using Finbuckle.MultiTenant;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Server.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddTransient<IDateTimeProvider, DateTimeProvider>();
services.AddTransient<IEmailService, EmailService>();
AddMultiTenant(services);
AddPersistence(services, configuration);
return services;
}
private static void AddPersistence(IServiceCollection services, IConfiguration configuration)
{
string connectionString = configuration.GetConnectionString("Database") ??
throw new ArgumentNullException(nameof(configuration));
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString).UseSnakeCaseNamingConvention()
.AddInterceptors(new SoftDeleteInterceptor()));
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IListingRepository, ListingRepository>();
services.AddScoped<IUnitRepository, UnitRepository>();
services.AddScoped<IUnitOfWork>(sp => sp.GetRequiredService<ApplicationDbContext>());
services.AddSingleton<ISqlConnectionFactory>(_ =>
new SqlConnectionFactory(connectionString));
SqlMapper.AddTypeHandler(new DateOnlyTypeHandler());
}
private static void AddMultiTenant(IServiceCollection services)
{
services.AddMultiTenant<TenantInfo>()
.WithStaticStrategy("tenant-1")
.WithInMemoryStore(config=>
config.Tenants.Add(new TenantInfo
{
Id = "tenant-1",Identifier = "tenant-1", Name = "tenant-1",
}))
;
}
}
Please let me know if you need further info Thanks!
Hm I don’t see anything obvious. If you put a breakpoint in the multitenant middleware can you tell if the tenant is being correctly resolved at that point?
Only the breakpoint in constructor is being hit but unfortunately it doesn't hit the invoke method at all, it jumps after the constructor directly to the call in following method where i get the NullPointerException
public async Task<Unit?> GetByIdAsync(UnitId id, CancellationToken cancellationToken = default)
{
return await _dbContext.Units
.FirstOrDefaultAsync(unit => unit.Id == id, cancellationToken);
}
Is there anything initiating a query to the db context before your controller is hit? Can you tell if the exception is occurring before the /api/test
delegate is entered?
Hi,
sorry.... It was my mistake.
I test the calling of the APIs in another functional test, while the integration test starts from command/query down to the repositories which means there was no requests at all and accordingly the middleware was not triggered
Adding a singleton of type ITenantinfo satisfies the AppDbContext and i'm not geeting the NullReferenceExcption anymore
services.Where(x => x.ToString().Contains("tenant", StringComparison.CurrentCultureIgnoreCase))
.ToList()
.ForEach(x => services.Remove(x));
var tenantInfo = new TenantInfo
{
Id = "tenant-1", Identifier = "tenant-1", Name = "tenant-1",
};
services.AddMultiTenant<TenantInfo>()
.WithStaticStrategy("tenant-1")
.WithInMemoryStore(config =>
config.Tenants.Add(tenantInfo)
);
services.AddSingleton<ITenantInfo>(tenantInfo);
Thanks and have a nice day!
Ok I’m glad you got it working!
Hi,
when I query a table with MuiltiTenant feature I get NullReferenceException
Query
On this object
Circumstances:
Configuration To minimize the dependencies and isolate the problem I've configured MultiTenant as the following:
I appreciate your advice what to change or where to inspect further Thanks!