neozhu / CleanArchitectureWithBlazorServer

This is a repository for creating a Blazor Server dashboard application following the principles of Clean Architecture
https://architecture.blazorserver.com/
MIT License
812 stars 219 forks source link

[Question] ApplicationDbContext as IDisposable Transient Service #699

Closed m7x333 closed 1 month ago

m7x333 commented 3 months ago

Hello!

From src\Infrastructure\DependencyInjection.cs :

services.AddScoped<IDbContextFactory<ApplicationDbContext>, BlazorContextFactory<ApplicationDbContext>>();
services.AddTransient<IApplicationDbContext>(provider =>
    provider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());

Why you invoke CreateDbContext() directly within service registration? If my assumption is correct this call shall register ApplicationDbContext in DI and because ApplicationDbContext is IDisposable it's lifetime be lifetime of user circuit, if not created from custom scope. So you get new ApplicationDbContext on every mediator handler invocation and it not disposed until user connection is alive.

Can you clarify this?

neozhu commented 3 months ago

I understand your concern. I have tried registering IApplicationDbContext with a Scoped lifetime, but I encountered issues because Blazor Server applications are different from MVC applications. Therefore, I can only use Transient registration.

m7x333 commented 3 months ago

Yes, Blazor server scope bound to user circuit but your solution solves this by spawning new DbContext on every mediator handler call and doesn't dispose them for as long as user connection is alive. You could check this as solution:

  1. https://github.com/dotnet/blazor-samples/tree/main/8.0/BlazorWebAppEFCore. Register DbContextFactory and inject IDbContextFactory directly in component. As you use mediator this seems not a good solution for you.
  2. https://github.com/jbogard/MediatR/issues/607#issuecomment-1312616013. You can create wrapper around Mediator.Send with costume scope. This should work as scope disposed after every Send() invocation.
m7x333 commented 3 months ago

What do you think about this as drop-in replacement for Mediator?

using MediatR;

namespace Infrastructure.MediatorWrapper;

public interface IScopedMediator : IMediator
{
}
using System.Runtime.CompilerServices;
using MediatR;

namespace Infrastructure.MediatorWrapper;

public class ScopedMediator : IScopedMediator
{
    private readonly IServiceScopeFactory _scopeFactory;

    public ScopedMediator(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task<TResponse> Send<TResponse>(
        IRequest<TResponse> request,
        CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            TResponse response = await mediator.Send(request, cancellationToken);

            return response;
        }
    }

    public async Task Send<TRequest>(TRequest request, CancellationToken cancellationToken = default)
        where TRequest : IRequest
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            await mediator.Send(request, cancellationToken);
        }
    }

    public async Task<object?> Send(object request, CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            object? response = await mediator.Send(request, cancellationToken);

            return response;
        }
    }

    public async Task Publish<TNotification>(
        TNotification notification,
        CancellationToken cancellationToken = default)
        where TNotification : INotification
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            await mediator.Publish(notification, cancellationToken);
        }
    }

    public async Task Publish(object notification, CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            await mediator.Publish(notification, cancellationToken);
        }
    }

    public async IAsyncEnumerable<TResponse> CreateStream<TResponse>(
        IStreamRequest<TResponse> request,
        [EnumeratorCancellation]
        CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            IAsyncEnumerable<TResponse> items = mediator.CreateStream(request, cancellationToken);

            await foreach (TResponse item in items)
            {
                yield return item;
            }
        }
    }

    public async IAsyncEnumerable<object?> CreateStream(
        object request,
        [EnumeratorCancellation]
        CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            IAsyncEnumerable<object?> items = mediator.CreateStream(request, cancellationToken);

            await foreach (object? item in items)
            {
                yield return item;
            }
        }
    }
}
ibr-ayg commented 1 month ago

What do you think about this as drop-in replacement for Mediator?

using MediatR;

namespace Infrastructure.MediatorWrapper;

public interface IScopedMediator : IMediator
{
}
using System.Runtime.CompilerServices;
using MediatR;

namespace Infrastructure.MediatorWrapper;

public class ScopedMediator : IScopedMediator
{
    private readonly IServiceScopeFactory _scopeFactory;

    public ScopedMediator(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task<TResponse> Send<TResponse>(
        IRequest<TResponse> request,
        CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            TResponse response = await mediator.Send(request, cancellationToken);

            return response;
        }
    }

    public async Task Send<TRequest>(TRequest request, CancellationToken cancellationToken = default)
        where TRequest : IRequest
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            await mediator.Send(request, cancellationToken);
        }
    }

    public async Task<object?> Send(object request, CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            object? response = await mediator.Send(request, cancellationToken);

            return response;
        }
    }

    public async Task Publish<TNotification>(
        TNotification notification,
        CancellationToken cancellationToken = default)
        where TNotification : INotification
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            await mediator.Publish(notification, cancellationToken);
        }
    }

    public async Task Publish(object notification, CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            await mediator.Publish(notification, cancellationToken);
        }
    }

    public async IAsyncEnumerable<TResponse> CreateStream<TResponse>(
        IStreamRequest<TResponse> request,
        [EnumeratorCancellation]
        CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            IAsyncEnumerable<TResponse> items = mediator.CreateStream(request, cancellationToken);

            await foreach (TResponse item in items)
            {
                yield return item;
            }
        }
    }

    public async IAsyncEnumerable<object?> CreateStream(
        object request,
        [EnumeratorCancellation]
        CancellationToken cancellationToken = default)
    {
        AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
        await using (scope.ConfigureAwait(false))
        {
            IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();

            IAsyncEnumerable<object?> items = mediator.CreateStream(request, cancellationToken);

            await foreach (object? item in items)
            {
                yield return item;
            }
        }
    }
}

This definitely works very well. Thank you

neozhu commented 1 month ago

done