Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BBT.Aether.Uow;
using Microsoft.Extensions.DependencyInjection;

namespace BBT.Aether.DependencyInjection;

/// <summary>
/// Extension methods for IServiceScopeFactory.
/// </summary>
public static class ServiceScopeFactoryExtensions
{
/// <summary>
/// Executes the given action within a new dependency injection scope and a new unit of work.
/// Manages ambient service provider propagation.
/// </summary>
/// <param name="scopeFactory">The service scope factory.</param>
/// <param name="action">The action to execute, receiving the scoped service provider.</param>
/// <param name="options">Unit of work options. Defaults to a new UoW with default settings.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task ExecuteInNewUnitOfWorkScopeAsync(
this IServiceScopeFactory scopeFactory,
Func<IServiceProvider, Task> action,
UnitOfWorkOptions? options = null,
CancellationToken cancellationToken = default)
{
await using var scope = scopeFactory.CreateAsyncScope();
var sp = scope.ServiceProvider;

// Propagate ambient service provider for the new scope
var previousAmbient = AmbientServiceProvider.Current;
AmbientServiceProvider.Current = sp;

try
{
var uowManager = sp.GetRequiredService<IUnitOfWorkManager>();

// Begin UnitOfWork
await using var uow = await uowManager.BeginAsync(options, cancellationToken);

// Execute action
await action(sp);

// Commit UnitOfWork
await uow.CommitAsync(cancellationToken);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): There is an extra closing brace at the end of the file which will cause a compile error.

The class and namespace are already closed, so the final } leaves the file with unbalanced braces. Remove that last brace at the end of the file.

finally
{
// Restore previous ambient context
AmbientServiceProvider.Current = previousAmbient;
}
}
Comment on lines +23 to +54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method duplicates the logic for creating a scope and managing the AmbientServiceProvider, which is already handled by ExecuteInNewScopeAsync. To improve maintainability and reduce code duplication, you can refactor this method to delegate the scope management to ExecuteInNewScopeAsync and wrap the unit of work logic within it.

    public static Task ExecuteInNewUnitOfWorkScopeAsync(
        this IServiceScopeFactory scopeFactory,
        Func<IServiceProvider, Task> action,
        UnitOfWorkOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        return scopeFactory.ExecuteInNewScopeAsync(async sp =>
        {
            var uowManager = sp.GetRequiredService<IUnitOfWorkManager>();

            // Begin UnitOfWork
            await using var uow = await uowManager.BeginAsync(options, cancellationToken);
            
            // Execute action
            await action(sp);
            
            // Commit UnitOfWork
            await uow.CommitAsync(cancellationToken);
        });
    }


/// <summary>
/// Executes the given function within a new dependency injection scope and a new unit of work, returning a result.
/// Manages ambient service provider propagation.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <param name="scopeFactory">The service scope factory.</param>
/// <param name="func">The function to execute, receiving the scoped service provider.</param>
/// <param name="options">Unit of work options. Defaults to a new UoW with default settings.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result of the function execution.</returns>
public static async Task<T> ExecuteInNewUnitOfWorkScopeAsync<T>(
this IServiceScopeFactory scopeFactory,
Func<IServiceProvider, Task<T>> func,
UnitOfWorkOptions? options = null,
CancellationToken cancellationToken = default)
{
await using var scope = scopeFactory.CreateAsyncScope();
var sp = scope.ServiceProvider;

// Propagate ambient service provider for the new scope
var previousAmbient = AmbientServiceProvider.Current;
AmbientServiceProvider.Current = sp;

try
{
var uowManager = sp.GetRequiredService<IUnitOfWorkManager>();

// Begin UnitOfWork
await using var uow = await uowManager.BeginAsync(options, cancellationToken);

// Execute function
var result = await func(sp);

// Commit UnitOfWork
await uow.CommitAsync(cancellationToken);

return result;
}
finally
{
// Restore previous ambient context
AmbientServiceProvider.Current = previousAmbient;
}
}
Comment on lines +66 to +99

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the non-generic version, this method duplicates the scope creation and AmbientServiceProvider management logic from ExecuteInNewScopeAsync<T>. Refactoring to use ExecuteInNewScopeAsync<T> will make the code more concise and easier to maintain by avoiding redundant code.

    public static Task<T> ExecuteInNewUnitOfWorkScopeAsync<T>(
        this IServiceScopeFactory scopeFactory,
        Func<IServiceProvider, Task<T>> func,
        UnitOfWorkOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        return scopeFactory.ExecuteInNewScopeAsync(async sp =>
        {
            var uowManager = sp.GetRequiredService<IUnitOfWorkManager>();

            // Begin UnitOfWork
            await using var uow = await uowManager.BeginAsync(options, cancellationToken);
            
            // Execute function
            var result = await func(sp);
            
            // Commit UnitOfWork
            await uow.CommitAsync(cancellationToken);

            return result;
        });
    }


/// <summary>
/// Executes the given action within a new dependency injection scope without starting a unit of work.
/// Manages ambient service provider propagation.
/// </summary>
/// <param name="scopeFactory">The service scope factory.</param>
/// <param name="action">The action to execute, receiving the scoped service provider.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public static async Task ExecuteInNewScopeAsync(
this IServiceScopeFactory scopeFactory,
Func<IServiceProvider, Task> action)
{
await using var scope = scopeFactory.CreateAsyncScope();
var sp = scope.ServiceProvider;

// Propagate ambient service provider for the new scope
var previousAmbient = AmbientServiceProvider.Current;
AmbientServiceProvider.Current = sp;

try
{
await action(sp);
}
finally
{
// Restore previous ambient context
AmbientServiceProvider.Current = previousAmbient;
}
}

/// <summary>
/// Executes the given function within a new dependency injection scope without starting a unit of work, returning a result.
/// Manages ambient service provider propagation.
/// </summary>
/// <typeparam name="T">The type of the result.</typeparam>
/// <param name="scopeFactory">The service scope factory.</param>
/// <param name="func">The function to execute, receiving the scoped service provider.</param>
/// <returns>The result of the function execution.</returns>
public static async Task<T> ExecuteInNewScopeAsync<T>(
this IServiceScopeFactory scopeFactory,
Func<IServiceProvider, Task<T>> func)
{
await using var scope = scopeFactory.CreateAsyncScope();
var sp = scope.ServiceProvider;

// Propagate ambient service provider for the new scope
var previousAmbient = AmbientServiceProvider.Current;
AmbientServiceProvider.Current = sp;

try
{
return await func(sp);
}
finally
{
// Restore previous ambient context
AmbientServiceProvider.Current = previousAmbient;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,28 @@ public static Result<T> Tap<T>(this Result<T> result, Action<T> sideEffect)
public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, Error error)
=> result.IsSuccess && !predicate(result.Value!) ? Result<T>.Fail(error) : result;

/// <summary>
/// Ensures a predicate is satisfied, otherwise returns an error (lazy evaluation).
/// </summary>
/// <typeparam name="T">Value type</typeparam>
/// <param name="result">The source result</param>
/// <param name="predicate">Condition that must be true</param>
/// <param name="errorFactory">Function to create error if predicate fails</param>
/// <returns>The original result if predicate passes, otherwise a failure</returns>
public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, Func<Error> errorFactory)
=> result.IsSuccess && !predicate(result.Value!) ? Result<T>.Fail(errorFactory()) : result;

/// <summary>
/// Ensures a predicate is satisfied, otherwise returns an error (lazy evaluation with value access).
/// </summary>
/// <typeparam name="T">Value type</typeparam>
/// <param name="result">The source result</param>
/// <param name="predicate">Condition that must be true</param>
/// <param name="errorFactory">Function to create error based on the value if predicate fails</param>
/// <returns>The original result if predicate passes, otherwise a failure</returns>
public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, Func<T, Error> errorFactory)
=> result.IsSuccess && !predicate(result.Value!) ? Result<T>.Fail(errorFactory(result.Value!)) : result;

/// <summary>
/// Executes a side effect on failure without altering the error.
/// Useful for logging, alerting, or cleanup actions on failure.
Expand Down Expand Up @@ -349,6 +371,34 @@ public static async Task<Result<T>> EnsureAsync<T>(this Result<T> result, Func<T
return await predicate(result.Value!) ? result : Result<T>.Fail(error);
}

/// <summary>
/// Async version of Ensure for sync Result with async predicate (lazy error evaluation).
/// </summary>
/// <typeparam name="T">Value type</typeparam>
/// <param name="result">The source result</param>
/// <param name="predicate">Async predicate that must be true</param>
/// <param name="errorFactory">Function to create error if predicate fails</param>
/// <returns>The original result if predicate passes, otherwise a failure</returns>
public static async Task<Result<T>> EnsureAsync<T>(this Result<T> result, Func<T, Task<bool>> predicate, Func<Error> errorFactory)
{
if (!result.IsSuccess) return result;
return await predicate(result.Value!) ? result : Result<T>.Fail(errorFactory());
}

/// <summary>
/// Async version of Ensure for sync Result with async predicate (lazy error evaluation with value access).
/// </summary>
/// <typeparam name="T">Value type</typeparam>
/// <param name="result">The source result</param>
/// <param name="predicate">Async predicate that must be true</param>
/// <param name="errorFactory">Function to create error based on the value if predicate fails</param>
/// <returns>The original result if predicate passes, otherwise a failure</returns>
public static async Task<Result<T>> EnsureAsync<T>(this Result<T> result, Func<T, Task<bool>> predicate, Func<T, Error> errorFactory)
{
if (!result.IsSuccess) return result;
return await predicate(result.Value!) ? result : Result<T>.Fail(errorFactory(result.Value!));
}

/// <summary>
/// Async version of Map for Task-wrapped results.
/// </summary>
Expand Down Expand Up @@ -458,6 +508,34 @@ public static async Task<Result<T>> EnsureAsync<T>(this Task<Result<T>> task, Fu
return result.Ensure(predicate, error);
}

/// <summary>
/// Async version of Ensure for Task-wrapped results (lazy error evaluation).
/// </summary>
/// <typeparam name="T">Value type</typeparam>
/// <param name="task">The source result task</param>
/// <param name="predicate">Condition that must be true</param>
/// <param name="errorFactory">Function to create error if predicate fails</param>
/// <returns>The original result if predicate passes, otherwise a failure</returns>
public static async Task<Result<T>> EnsureAsync<T>(this Task<Result<T>> task, Func<T, bool> predicate, Func<Error> errorFactory)
{
var result = await task;
return result.Ensure(predicate, errorFactory);
}

/// <summary>
/// Async version of Ensure for Task-wrapped results (lazy error evaluation with value access).
/// </summary>
/// <typeparam name="T">Value type</typeparam>
/// <param name="task">The source result task</param>
/// <param name="predicate">Condition that must be true</param>
/// <param name="errorFactory">Function to create error based on the value if predicate fails</param>
/// <returns>The original result if predicate passes, otherwise a failure</returns>
public static async Task<Result<T>> EnsureAsync<T>(this Task<Result<T>> task, Func<T, bool> predicate, Func<T, Error> errorFactory)
{
var result = await task;
return result.Ensure(predicate, errorFactory);
}

/// <summary>
/// Async version of Match for Task-wrapped results with sync handlers.
/// </summary>
Expand Down