fix injectionses

This commit is contained in:
john 2025-05-21 00:19:49 +02:00
parent b93115d787
commit cd078ca643
11 changed files with 119 additions and 55 deletions

View file

@ -2,6 +2,7 @@ using Femto.Api.Controllers.Posts.Dto;
using Femto.Common; using Femto.Common;
using Femto.Modules.Blog.Application; using Femto.Modules.Blog.Application;
using Femto.Modules.Blog.Application.Commands.CreatePost; using Femto.Modules.Blog.Application.Commands.CreatePost;
using Femto.Modules.Blog.Application.Commands.DeletePost;
using Femto.Modules.Blog.Application.Queries.GetPosts; using Femto.Modules.Blog.Application.Queries.GetPosts;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -18,7 +19,7 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var res = await blogModule.PostQuery( var res = await blogModule.Query(
new GetPostsQuery(currentUserContext.CurrentUser?.Id) new GetPostsQuery(currentUserContext.CurrentUser?.Id)
{ {
From = searchParams.From, From = searchParams.From,
@ -48,7 +49,7 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current
CancellationToken cancellationToken CancellationToken cancellationToken
) )
{ {
var guid = await blogModule.PostCommand( var guid = await blogModule.Command(
new CreatePostCommand( new CreatePostCommand(
req.AuthorId, req.AuthorId,
req.Content, req.Content,
@ -70,4 +71,11 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current
return new CreatePostResponse(guid); return new CreatePostResponse(guid);
} }
[HttpDelete("{postId}")]
[Authorize]
public async Task DeletePost(Guid postId, CancellationToken cancellationToken)
{
await blogModule.Command(new DeletePostCommand(postId, currentUserContext.CurrentUser.Id), cancellationToken);
}
} }

View file

@ -4,5 +4,23 @@ namespace Femto.Api;
internal class CurrentUserContext : ICurrentUserContext internal class CurrentUserContext : ICurrentUserContext
{ {
public CurrentUser? CurrentUser { get; set; } private CurrentUser? _currentUser;
public CurrentUser? tryGetUserCurrentUser() => this._currentUser;
public bool HasUser => this._currentUser is not null;
public CurrentUser CurrentUser
{
get
{
if (_currentUser is null)
throw new InvalidOperationException(
"don't access current user if not authenticated"
);
return _currentUser;
}
set => _currentUser = value;
}
} }

View file

@ -21,11 +21,16 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
builder.Logging.ClearProviders(); var loggerFactory = LoggerFactory.Create(b =>
builder.Logging.AddConsole(); {
builder.Logging.AddDebug(); b.SetMinimumLevel(LogLevel.Information)
builder.Logging.SetMinimumLevel(LogLevel.Information); .AddConfiguration(builder.Configuration.GetSection("Logging"))
.AddConsole()
.AddDebug();
});
builder.Services.AddSingleton(loggerFactory);
builder.Services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
var connectionString = builder.Configuration.GetConnectionString("Database"); var connectionString = builder.Configuration.GetConnectionString("Database");
if (connectionString is null) if (connectionString is null)
@ -39,9 +44,9 @@ if (blobStorageRoot is null)
var eventBus = new EventBus(Channel.CreateUnbounded<IEvent>()); var eventBus = new EventBus(Channel.CreateUnbounded<IEvent>());
builder.Services.AddHostedService(_ => eventBus); builder.Services.AddHostedService(_ => eventBus);
builder.Services.InitializeBlogModule(connectionString, eventBus); builder.Services.InitializeBlogModule(connectionString, eventBus, loggerFactory);
builder.Services.InitializeMediaModule(connectionString, blobStorageRoot); builder.Services.InitializeMediaModule(connectionString, blobStorageRoot);
builder.Services.InitializeAuthenticationModule(connectionString, eventBus); builder.Services.InitializeAuthenticationModule(connectionString, eventBus, loggerFactory);
builder.Services.AddScoped<CurrentUserContext, CurrentUserContext>(); builder.Services.AddScoped<CurrentUserContext, CurrentUserContext>();
builder.Services.AddScoped<ICurrentUserContext>(s => s.GetRequiredService<CurrentUserContext>()); builder.Services.AddScoped<ICurrentUserContext>(s => s.GetRequiredService<CurrentUserContext>());

View file

@ -3,7 +3,9 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Femto": "Debug",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*"

View file

@ -2,7 +2,10 @@ namespace Femto.Common;
public interface ICurrentUserContext public interface ICurrentUserContext
{ {
CurrentUser? CurrentUser { get; } CurrentUser? tryGetUserCurrentUser();
bool HasUser { get; }
CurrentUser CurrentUser { get; }
} }
public record CurrentUser(Guid Id, string Username, string SessionId); public record CurrentUser(Guid Id, string Username, string SessionId);

View file

@ -1,16 +1,21 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Femto.Modules.Auth.Application; namespace Femto.Common;
/// <summary> /// <summary>
/// We use this to bind a scope to the request scope in the composition root /// We use this to bind a scope to the request scope in the composition root
/// Any scoped services provided by this subcontainer should be accessed via a ScopeBinding injected in the host /// Any scoped services provided by this subcontainer should be accessed via a ScopeBinding injected in the host
/// </summary> /// </summary>
/// <param name="scope"></param> /// <param name="scope"></param>
public class ScopeBinding(IServiceScope scope) : IDisposable public class ScopeBinding<T>(IServiceScope scope) : IDisposable
where T : notnull
{ {
public T GetService<T>() where T : notnull => scope.ServiceProvider.GetRequiredService<T>(); public T GetService() {
return scope.ServiceProvider.GetRequiredService<T>();
}
public void Dispose() => scope.Dispose(); public void Dispose() {
scope.Dispose();
}
} }

View file

@ -1,3 +1,4 @@
using Femto.Common;
using Femto.Common.Infrastructure; using Femto.Common.Infrastructure;
using Femto.Common.Infrastructure.DbConnection; using Femto.Common.Infrastructure.DbConnection;
using Femto.Common.Infrastructure.Outbox; using Femto.Common.Infrastructure.Outbox;
@ -15,23 +16,36 @@ namespace Femto.Modules.Auth.Application;
public static class AuthStartup public static class AuthStartup
{ {
public static void InitializeAuthenticationModule(this IServiceCollection rootContainer, public static void InitializeAuthenticationModule(
string connectionString, IEventBus eventBus) this IServiceCollection rootContainer,
string connectionString,
IEventBus eventBus,
ILoggerFactory loggerFactory
)
{ {
var hostBuilder = Host.CreateDefaultBuilder(); var hostBuilder = Host.CreateDefaultBuilder();
hostBuilder.ConfigureServices(services => ConfigureServices(services, connectionString, eventBus)); hostBuilder.ConfigureServices(services =>
ConfigureServices(services, connectionString, eventBus, loggerFactory)
);
var host = hostBuilder.Build(); var host = hostBuilder.Build();
rootContainer.AddScoped(_ => new ScopeBinding(host.Services.CreateScope())); rootContainer.AddScoped(_ => new ScopeBinding<IAuthModule>(host.Services.CreateScope()));
rootContainer.AddScoped(services =>
rootContainer.AddScoped<IAuthModule>(services => services.GetRequiredService<ScopeBinding<IAuthModule>>().GetService()
services.GetRequiredService<ScopeBinding>().GetService<IAuthModule>()); );
rootContainer.AddHostedService(services => new AuthApplication(host)); rootContainer.AddHostedService(services => new AuthApplication(host));
eventBus.Subscribe((evt, cancellationToken) => EventSubscriber(evt, host.Services, cancellationToken)); eventBus.Subscribe(
(evt, cancellationToken) => EventSubscriber(evt, host.Services, cancellationToken)
);
} }
private static void ConfigureServices(IServiceCollection services, string connectionString, IEventPublisher publisher) private static void ConfigureServices(
IServiceCollection services,
string connectionString,
IEventPublisher publisher,
ILoggerFactory loggerFactory
)
{ {
services.AddTransient<IDbConnectionFactory>(_ => new DbConnectionFactory(connectionString)); services.AddTransient<IDbConnectionFactory>(_ => new DbConnectionFactory(connectionString));
@ -39,13 +53,15 @@ public static class AuthStartup
{ {
builder.UseNpgsql(connectionString); builder.UseNpgsql(connectionString);
builder.UseSnakeCaseNamingConvention(); builder.UseSnakeCaseNamingConvention();
var loggerFactory = LoggerFactory.Create(b => { });
builder.UseLoggerFactory(loggerFactory); builder.UseLoggerFactory(loggerFactory);
// #if DEBUG // #if DEBUG
// builder.EnableSensitiveDataLogging(); // builder.EnableSensitiveDataLogging();
// #endif // #endif
}); });
services.AddSingleton(loggerFactory);
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
services.AddQuartzHostedService(options => services.AddQuartzHostedService(options =>
{ {
options.WaitForJobsToComplete = true; options.WaitForJobsToComplete = true;

View file

@ -1,37 +1,29 @@
using Femto.Common.Domain; using Femto.Common.Domain;
using MediatR; using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Femto.Modules.Blog.Application; namespace Femto.Modules.Blog.Application;
internal class BlogModule(IHost host) : IBlogModule internal class BlogModule(IMediator mediator) : IBlogModule
{ {
public async Task PostCommand(ICommand command, CancellationToken cancellationToken = default) public async Task Command(ICommand command, CancellationToken cancellationToken = default)
{ {
using var scope = host.Services.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Send(command, cancellationToken); await mediator.Send(command, cancellationToken);
} }
public async Task<TResponse> PostCommand<TResponse>( public async Task<TResponse> Command<TResponse>(
ICommand<TResponse> command, ICommand<TResponse> command,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
using var scope = host.Services.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var response = await mediator.Send(command, cancellationToken); var response = await mediator.Send(command, cancellationToken);
return response; return response;
} }
public async Task<TResponse> PostQuery<TResponse>( public async Task<TResponse> Query<TResponse>(
IQuery<TResponse> query, IQuery<TResponse> query,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
) )
{ {
using var scope = host.Services.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var response = await mediator.Send(query, cancellationToken); var response = await mediator.Send(query, cancellationToken);
return response; return response;
} }

View file

@ -1,4 +1,5 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Femto.Common;
using Femto.Common.Infrastructure; using Femto.Common.Infrastructure;
using Femto.Common.Infrastructure.DbConnection; using Femto.Common.Infrastructure.DbConnection;
using Femto.Common.Infrastructure.Outbox; using Femto.Common.Infrastructure.Outbox;
@ -20,20 +21,24 @@ public static class BlogStartup
public static void InitializeBlogModule( public static void InitializeBlogModule(
this IServiceCollection rootContainer, this IServiceCollection rootContainer,
string connectionString, string connectionString,
IEventBus bus IEventBus bus,
ILoggerFactory loggerFactory
) )
{ {
var hostBuilder = Host.CreateDefaultBuilder(); var hostBuilder = Host.CreateDefaultBuilder();
hostBuilder.ConfigureServices(services => hostBuilder.ConfigureServices(services =>
ConfigureServices(services, connectionString, bus) ConfigureServices(services, connectionString, bus, loggerFactory)
); );
var host = hostBuilder.Build(); var host = hostBuilder.Build();
rootContainer.AddHostedService(services => new BlogApplication(host)); rootContainer.AddHostedService(_ => new BlogApplication(host));
rootContainer.AddScoped<IBlogModule>(_ => new BlogModule(host)); rootContainer.AddScoped(_ => new ScopeBinding<IBlogModule>(host.Services.CreateScope()));
rootContainer.AddScoped(services =>
services.GetRequiredService<ScopeBinding<IBlogModule>>().GetService()
);
bus.Subscribe( bus.Subscribe(
(evt, cancellationToken) => EventSubscriber(evt, host.Services, cancellationToken) (evt, cancellationToken) => EventSubscriber(evt, host.Services, cancellationToken)
@ -43,7 +48,8 @@ public static class BlogStartup
private static void ConfigureServices( private static void ConfigureServices(
this IServiceCollection services, this IServiceCollection services,
string connectionString, string connectionString,
IEventPublisher publisher IEventPublisher publisher,
ILoggerFactory loggerFactory
) )
{ {
services.AddTransient<IDbConnectionFactory>(_ => new DbConnectionFactory(connectionString)); services.AddTransient<IDbConnectionFactory>(_ => new DbConnectionFactory(connectionString));
@ -58,10 +64,12 @@ public static class BlogStartup
} }
); );
builder.UseSnakeCaseNamingConvention(); builder.UseSnakeCaseNamingConvention();
var loggerFactory = LoggerFactory.Create(b => { });
builder.UseLoggerFactory(loggerFactory); builder.UseLoggerFactory(loggerFactory);
}); });
services.AddSingleton(loggerFactory);
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
services.AddOutbox<BlogContext, OutboxMessageHandler>(); services.AddOutbox<BlogContext, OutboxMessageHandler>();
services.AddMediatR(c => services.AddMediatR(c =>
@ -71,6 +79,8 @@ public static class BlogStartup
services.ConfigureDomainServices<BlogContext>(); services.ConfigureDomainServices<BlogContext>();
services.AddScoped<IBlogModule, BlogModule>();
services.AddSingleton(publisher); services.AddSingleton(publisher);
} }

View file

@ -0,0 +1,5 @@
using Femto.Common.Domain;
namespace Femto.Modules.Blog.Application.Commands.DeletePost;
public record DeletePostCommand(Guid PostId, Guid InitiatingUserId) : ICommand;

View file

@ -4,14 +4,14 @@ namespace Femto.Modules.Blog.Application;
public interface IBlogModule public interface IBlogModule
{ {
Task PostCommand(ICommand command, CancellationToken cancellationToken = default); Task Command(ICommand command, CancellationToken cancellationToken = default);
Task<TResponse> PostCommand<TResponse>( Task<TResponse> Command<TResponse>(
ICommand<TResponse> command, ICommand<TResponse> command,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
); );
Task<TResponse> PostQuery<TResponse>( Task<TResponse> Query<TResponse>(
IQuery<TResponse> query, IQuery<TResponse> query,
CancellationToken cancellationToken = default CancellationToken cancellationToken = default
); );