Browse Source

Support completing request units of work before responses start

Support completing request units of work before responses start
pull/26047/head
Halil İbrahim Kalkan 4 days ago
committed by GitHub
parent
commit
220db6ef5a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      framework/Volo.Abp.slnx
  2. 27
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpAspNetCoreUnitOfWorkOptions.cs
  3. 37
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpUnitOfWorkMiddleware.cs
  4. 8
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs
  5. 27
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkExtensions.cs
  6. 34
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/ResponseWritingTestEvent.cs
  7. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/TestUnitOfWorkConfig.cs
  8. 119
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs
  9. 156
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestController.cs
  10. 26
      framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo.Abp.AspNetCore.Uow.Tests.csproj
  11. 61
      framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/AbpAspNetCoreUowTestModule.cs
  12. 15
      framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/Program.cs
  13. 145
      framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UnitOfWorkMiddleware_Relational_Tests.cs
  14. 124
      framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UowVisibilityController.cs
  15. 46
      framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UowVisibilityTestEntity.cs
  16. 37
      modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs
  17. 6
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo.Abp.OpenIddict.AspNetCore.Tests.csproj
  18. 69
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenEndpoint_Integration_Tests.cs
  19. 157
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenIntegrationTestModule.cs
  20. 15
      modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/Program.cs

1
framework/Volo.Abp.slnx

@ -194,6 +194,7 @@
<Project Path="test/Volo.Abp.AspNetCore.Serilog.Tests/Volo.Abp.AspNetCore.Serilog.Tests.csproj" />
<Project Path="test/Volo.Abp.AspNetCore.SignalR.Tests/Volo.Abp.AspNetCore.SignalR.Tests.csproj" />
<Project Path="test/Volo.Abp.AspNetCore.Tests/Volo.Abp.AspNetCore.Tests.csproj" />
<Project Path="test/Volo.Abp.AspNetCore.Uow.Tests/Volo.Abp.AspNetCore.Uow.Tests.csproj" />
<Project Path="test/Volo.Abp.Auditing.Tests/Volo.Abp.Auditing.Tests.csproj" />
<Project Path="test/Volo.Abp.Authorization.Tests/Volo.Abp.Authorization.Tests.csproj" />
<Project Path="test/Volo.Abp.Autofac.Tests/Volo.Abp.Autofac.Tests.csproj" />

27
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpAspNetCoreUnitOfWorkOptions.cs

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Uow;
@ -11,4 +11,29 @@ public class AbpAspNetCoreUnitOfWorkOptions
/// starting with an ignored URL.
/// </summary>
public List<string> IgnoredUrls { get; } = new List<string>();
/// <summary>
/// Completes the request unit of work just before the response starts (on
/// <c>HttpResponse.OnStarting</c>) instead of at the end of the pipeline, so data written during
/// the request is committed before the response is flushed. Disabled by default; enable it here
/// globally or opt-in per endpoint via <see cref="CompleteUnitOfWorkOnResponseStartingUrls"/>.
/// <para>
/// Trade-offs when it applies: an exception after the response starts can no longer roll back the
/// committed data (commit and network response are not atomic); database access after the response
/// starts is outside the request unit of work (unsuitable for streaming responses); unit of work
/// events and completed handlers run before the first response byte (adding to its latency); a
/// nested (requiresNew) unit of work that is current when the response starts, and an active child
/// unit of work scope (begun without requiresNew), are left to their owners and the request unit of
/// work then completes at the end of the pipeline as usual.
/// </para>
/// </summary>
public bool CompleteUnitOfWorkOnResponseStarting { get; set; } = false;
/// <summary>
/// Request path prefixes that opt-in to <see cref="CompleteUnitOfWorkOnResponseStarting"/> even when
/// it is globally disabled. A request whose path starts with one of these values (for example
/// "/connect") is included, matched like <see cref="IgnoredUrls"/>. Use
/// <see cref="CompleteUnitOfWorkOnResponseStarting"/> to enable it for every request handled by the middleware.
/// </summary>
public List<string> CompleteUnitOfWorkOnResponseStartingUrls { get; } = new List<string>();
}

37
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpUnitOfWorkMiddleware.cs

@ -37,8 +37,36 @@ public class AbpUnitOfWorkMiddleware : AbpMiddlewareBase, ITransientDependency
using (var uow = _unitOfWorkManager.Reserve(UnitOfWork.UnitOfWorkReservationName))
{
var completionStarted = false;
if (!context.Response.HasStarted && ShouldCompleteOnResponseStarting(context))
{
context.Response.OnStarting(async () =>
{
// Skip if the completion has already been started at the end of the pipeline;
// the response is then being started from inside that completion (e.g. by an
// event handler writing to the response), so completing again would fail.
// A nested (requiresNew) unit of work that is current and an active child
// unit of work scope are left to their owners; the request unit of work then
// completes at the end of the pipeline as usual.
if (!completionStarted &&
_unitOfWorkManager.Current == uow &&
!uow.HasActiveChildUnitOfWorks())
{
// Set before completing so a post-commit failure isn't masked by the completion below.
completionStarted = true;
await uow.CompleteAsync(_cancellationTokenProvider.Token);
}
});
}
await next(context);
await uow.CompleteAsync(_cancellationTokenProvider.Token);
if (!completionStarted)
{
completionStarted = true;
await uow.CompleteAsync(_cancellationTokenProvider.Token);
}
}
}
@ -48,6 +76,13 @@ public class AbpUnitOfWorkMiddleware : AbpMiddlewareBase, ITransientDependency
_options.IgnoredUrls.Any(x => context.Request.Path.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase));
}
private bool ShouldCompleteOnResponseStarting(HttpContext context)
{
return _options.CompleteUnitOfWorkOnResponseStarting ||
(context.Request.Path.Value != null &&
_options.CompleteUnitOfWorkOnResponseStartingUrls.Any(x => context.Request.Path.Value.StartsWith(x, StringComparison.OrdinalIgnoreCase)));
}
protected async override Task<bool> ShouldSkipAsync(HttpContext context, RequestDelegate next)
{
// Blazor components will render concurrently, so we need to skip the middleware for them.

8
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs

@ -30,12 +30,14 @@ internal class ChildUnitOfWork : IUnitOfWork
public Dictionary<string, object> Items => _parent.Items;
private readonly IUnitOfWork _parent;
private bool _isDisposed;
public ChildUnitOfWork([NotNull] IUnitOfWork parent)
{
Check.NotNull(parent, nameof(parent));
_parent = parent;
_parent.IncrementActiveChildUnitOfWorkCount();
_parent.Failed += (sender, args) => { Failed.InvokeSafely(sender!, args); };
_parent.Disposed += (sender, args) => { Disposed.InvokeSafely(sender!, args); };
@ -122,7 +124,13 @@ internal class ChildUnitOfWork : IUnitOfWork
public void Dispose()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
_parent.DecrementActiveChildUnitOfWorkCount();
}
public override string ToString()

27
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkExtensions.cs

@ -7,6 +7,8 @@ namespace Volo.Abp.Uow;
public static class UnitOfWorkExtensions
{
private const string ActiveChildUnitOfWorkCountItemKey = "_AbpActiveChildUnitOfWorkCount";
public static bool IsReservedFor([NotNull] this IUnitOfWork unitOfWork, string reservationName)
{
Check.NotNull(unitOfWork, nameof(unitOfWork));
@ -14,6 +16,31 @@ public static class UnitOfWorkExtensions
return unitOfWork.IsReserved && unitOfWork.ReservationName == reservationName;
}
/// <summary>
/// Checks if there is an active (not yet disposed) child unit of work scope over the given
/// unit of work, i.e. a scope created by <see cref="IUnitOfWorkManager.Begin"/> without
/// requiresNew while this unit of work was current. Such a scope shares this unit of work,
/// so it should not be completed while the scope is still active.
/// </summary>
public static bool HasActiveChildUnitOfWorks([NotNull] this IUnitOfWork unitOfWork)
{
Check.NotNull(unitOfWork, nameof(unitOfWork));
return unitOfWork.Items.GetOrDefault(ActiveChildUnitOfWorkCountItemKey) is int count && count > 0;
}
internal static void IncrementActiveChildUnitOfWorkCount(this IUnitOfWork unitOfWork)
{
var count = unitOfWork.Items.GetOrDefault(ActiveChildUnitOfWorkCountItemKey) as int? ?? 0;
unitOfWork.Items[ActiveChildUnitOfWorkCountItemKey] = count + 1;
}
internal static void DecrementActiveChildUnitOfWorkCount(this IUnitOfWork unitOfWork)
{
var count = unitOfWork.Items.GetOrDefault(ActiveChildUnitOfWorkCountItemKey) as int? ?? 0;
unitOfWork.Items[ActiveChildUnitOfWorkCountItemKey] = Math.Max(0, count - 1);
}
public static void AddItem<TValue>([NotNull] this IUnitOfWork unitOfWork, string key, TValue value)
where TValue : class
{

34
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/ResponseWritingTestEvent.cs

@ -0,0 +1,34 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
namespace Volo.Abp.AspNetCore.Mvc.Uow;
public class ResponseWritingTestEvent
{
}
/// <summary>
/// Writes to the HTTP response from a local event handler. When the event is published inside the
/// request unit of work, this runs during the unit of work completion at the end of the pipeline
/// and starts the response from inside that completion.
/// </summary>
public class ResponseWritingTestEventHandler : ILocalEventHandler<ResponseWritingTestEvent>, ITransientDependency
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ResponseWritingTestEventHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task HandleEventAsync(ResponseWritingTestEvent eventData)
{
var response = _httpContextAccessor.HttpContext?.Response;
if (response != null)
{
await response.WriteAsync("event-written");
}
}
}

2
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/TestUnitOfWorkConfig.cs

@ -7,4 +7,6 @@ public class TestUnitOfWorkConfig : ISingletonDependency
public const string ExceptionOnCompleteMessage = "TestUnitOfWork configured for exception";
public bool ThrowExceptionOnComplete { get; set; }
public bool? UowCompletedAfterResponseFlush { get; set; }
}

119
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs

@ -1,12 +1,19 @@
using System.Net.Http;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Volo.Abp.AspNetCore.Uow;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.Uow;
public class UnitOfWorkMiddleware_Tests : AspNetCoreMvcTestBase
{
private AbpAspNetCoreUnitOfWorkOptions Options =>
ServiceProvider.GetRequiredService<IOptions<AbpAspNetCoreUnitOfWorkOptions>>().Value;
[Fact]
public async Task Get_Actions_Should_Not_Be_Transactional()
{
@ -27,4 +34,114 @@ public class UnitOfWorkMiddleware_Tests : AspNetCoreMvcTestBase
var result = await Client.SendAsync(requestMessage);
result.IsSuccessStatusCode.ShouldBeTrue();
}
[Fact]
public async Task Ambient_Uow_Should_Be_Completed_Before_Response_Is_Flushed_When_Enabled()
{
Options.CompleteUnitOfWorkOnResponseStarting = true;
var result = await GetResponseAsStringAsync("/api/unitofwork-test/CommitBeforeResponseFlush");
result.ShouldBe("first:completed");
}
[Fact]
public async Task Ambient_Uow_Is_Not_Completed_On_Response_Start_By_Default()
{
var result = await GetResponseAsStringAsync("/api/unitofwork-test/CommitBeforeResponseFlush");
result.ShouldBe("first:not-completed");
}
[Fact]
public async Task Ambient_Uow_Is_Already_Completed_When_An_Exception_Is_Raised_After_The_Response_Started()
{
Options.CompleteUnitOfWorkOnResponseStarting = true;
// Once the response has started, an exception can't turn it into an error response (the
// connection is reset). Database-level rollback/commit is covered by the relational tests.
await Should.ThrowAsync<HttpRequestException>(async () =>
{
var response = await Client.GetAsync("/api/unitofwork-test/CommitThenThrowAfterResponseFlush");
await response.Content.ReadAsStringAsync();
});
ServiceProvider.GetRequiredService<TestUnitOfWorkConfig>()
.UowCompletedAfterResponseFlush.ShouldBe(true);
}
[Fact]
public async Task Repository_Access_After_Response_Flush_Runs_Outside_The_Request_Uow()
{
Options.CompleteUnitOfWorkOnResponseStarting = true;
var body = await GetResponseAsStringAsync("/api/unitofwork-test/ReadRepositoryAfterResponseFlush");
body.ShouldBe("before=ok(1);after=ok(1,ambient=null)");
}
[Fact]
public async Task Raw_Database_Provider_After_Response_Flush_Throws()
{
Options.CompleteUnitOfWorkOnResponseStarting = true;
var body = await GetResponseAsStringAsync("/api/unitofwork-test/RawDatabaseProviderAfterResponseFlush");
body.ShouldBe("first:threw-AbpException");
}
[Fact]
public async Task Response_Flush_Inside_Nested_Uow_Should_Not_Complete_The_Nested_Uow()
{
Options.CompleteUnitOfWorkOnResponseStarting = true;
var body = await GetResponseAsStringAsync("/api/unitofwork-test/NestedUowDuringResponseFlush");
body.ShouldBe("first:outer-not-completed:nested-completed-by-owner");
}
[Fact]
public async Task Completing_The_Uow_In_The_Action_Still_Fails_At_End_Of_Pipeline_By_Default()
{
var response = await Client.GetAsync("/api/unitofwork-test/CompleteCurrentUow");
response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError);
}
[Fact]
public async Task Opt_In_Url_Enables_The_Feature_For_A_Matching_Path()
{
Options.CompleteUnitOfWorkOnResponseStartingUrls.Add("/api/unitofwork-test/CommitBeforeResponseFlush");
var result = await GetResponseAsStringAsync("/api/unitofwork-test/CommitBeforeResponseFlush");
result.ShouldBe("first:completed");
}
[Fact]
public async Task Opt_In_Url_With_A_Trailing_Slash_Still_Matches()
{
Options.CompleteUnitOfWorkOnResponseStartingUrls.Add("/api/unitofwork-test/");
var result = await GetResponseAsStringAsync("/api/unitofwork-test/CommitBeforeResponseFlush");
result.ShouldBe("first:completed");
}
[Fact]
public async Task Response_Flush_Inside_A_Child_Uow_Scope_Should_Not_Complete_The_Request_Uow()
{
Options.CompleteUnitOfWorkOnResponseStarting = true;
// A child scope (Begin without requiresNew) shares the request unit of work, so completing
// it on response start would commit under the still-active scope; it is left to the end of
// the pipeline instead, like a nested (requiresNew) unit of work.
var body = await GetResponseAsStringAsync("/api/unitofwork-test/ChildUowDuringResponseFlush");
body.ShouldBe("first:request-not-completed");
}
[Fact]
public async Task An_Event_Handler_Starting_The_Response_During_The_End_Of_Pipeline_Completion_Should_Not_Fail()
{
Options.CompleteUnitOfWorkOnResponseStarting = true;
// The response does not start during the pipeline here, so the middleware completes the
// unit of work at its end; the event handler then starts the response from inside that
// completion. The OnStarting callback must not attempt a second completion (which would
// throw "Completion has already been requested for this unit of work").
var body = await GetResponseAsStringAsync("/api/unitofwork-test/PublishEventThatWritesResponseOnCompletion");
body.ShouldBe("event-written");
}
}

156
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestController.cs

@ -1,5 +1,14 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Shouldly;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Local;
using Volo.Abp.MemoryDb;
using Volo.Abp.TestApp.MemoryDb;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Uow;
namespace Volo.Abp.AspNetCore.Mvc.Uow;
@ -64,4 +73,149 @@ public class UnitOfWorkTestController : AbpController
_testUnitOfWorkConfig.ThrowExceptionOnComplete = true;
}
[HttpGet]
[Route("CommitBeforeResponseFlush")]
public async Task CommitBeforeResponseFlush()
{
var uow = CurrentUnitOfWork;
uow.ShouldNotBeNull();
// Start the response from inside the pipeline, before the middleware would commit.
await Response.WriteAsync("first");
await Response.Body.FlushAsync();
await Response.WriteAsync(uow.IsCompleted ? ":completed" : ":not-completed");
}
[HttpGet]
[Route("CommitThenThrowAfterResponseFlush")]
public async Task CommitThenThrowAfterResponseFlush()
{
var uow = CurrentUnitOfWork;
await Response.WriteAsync("first");
await Response.Body.FlushAsync();
_testUnitOfWorkConfig.UowCompletedAfterResponseFlush = uow.IsCompleted;
throw new UserFriendlyException("boom after the response was already flushed");
}
[HttpGet]
[Route("ReadRepositoryAfterResponseFlush")]
public async Task ReadRepositoryAfterResponseFlush()
{
var repository = LazyServiceProvider.LazyGetRequiredService<IRepository<Person, Guid>>();
var before = (await repository.GetListAsync()).Count;
await Response.WriteAsync($"before=ok({before})");
await Response.Body.FlushAsync();
string after;
try
{
var count = (await repository.GetListAsync()).Count;
after = $";after=ok({count},ambient={(UnitOfWorkManager.Current == null ? "null" : "present")})";
}
catch (Exception ex)
{
after = $";after=threw:{ex.GetType().Name}";
}
await Response.WriteAsync(after);
}
[HttpGet]
[Route("RawDatabaseProviderAfterResponseFlush")]
public async Task RawDatabaseProviderAfterResponseFlush()
{
var databaseProvider = LazyServiceProvider
.LazyGetRequiredService<IMemoryDatabaseProvider<TestAppMemoryDbContext>>();
await Response.WriteAsync("first");
await Response.Body.FlushAsync();
string outcome;
try
{
await databaseProvider.GetDatabaseAsync();
outcome = ":ok";
}
catch (AbpException)
{
// Raw provider access has no ambient uow once the response started, so it throws.
outcome = ":threw-AbpException";
}
await Response.WriteAsync(outcome);
}
[HttpGet]
[Route("NestedUowDuringResponseFlush")]
public async Task NestedUowDuringResponseFlush()
{
using (var nested = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
await Response.WriteAsync("first");
await Response.Body.FlushAsync();
// The outer request unit of work (nested.Outer) must not have been completed on response
// start while a nested unit of work is current.
await Response.WriteAsync(nested.Outer!.IsCompleted ? ":outer-completed" : ":outer-not-completed");
string outcome;
try
{
await nested.CompleteAsync();
outcome = ":nested-completed-by-owner";
}
catch (AbpException)
{
outcome = ":nested-already-completed";
}
await Response.WriteAsync(outcome);
}
}
[HttpGet]
[Route("CompleteCurrentUow")]
public async Task CompleteCurrentUow()
{
// Complete the request unit of work inside the action, without writing the response yet.
// The middleware must still try to complete it at the end of the pipeline (original behavior).
await CurrentUnitOfWork.CompleteAsync();
}
[HttpGet]
[Route("ChildUowDuringResponseFlush")]
public async Task ChildUowDuringResponseFlush()
{
var requestUow = CurrentUnitOfWork!;
using (UnitOfWorkManager.Begin())
{
await Response.WriteAsync("first");
await Response.Body.FlushAsync();
// The request unit of work must not be completed on response start while a child
// unit of work scope (begun without requiresNew) is still active over it.
await Response.WriteAsync(requestUow.IsCompleted ? ":request-completed" : ":request-not-completed");
}
}
[HttpGet]
[Route("PublishEventThatWritesResponseOnCompletion")]
public async Task<ActionResult> PublishEventThatWritesResponseOnCompletion()
{
// Published inside the request unit of work, so the handler runs while the middleware is
// completing it at the end of the pipeline. The handler writes to the response, which starts
// it mid-completion; the middleware's OnStarting callback must not try to complete again.
await LazyServiceProvider.LazyGetRequiredService<ILocalEventBus>()
.PublishAsync(new ResponseWritingTestEvent());
// Ok() sets 200 without writing the body, so the response does not start inside the pipeline.
return Ok();
}
}

26
framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo.Abp.AspNetCore.Uow.Tests.csproj

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.test.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>Volo.Abp.AspNetCore.Uow.Tests</AssemblyName>
<PackageId>Volo.Abp.AspNetCore.Uow.Tests</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<PreserveCompilationReferences>true</PreserveCompilationReferences>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc\Volo.Abp.AspNetCore.Mvc.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.TestBase\Volo.Abp.AspNetCore.TestBase.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.EntityFrameworkCore.Sqlite\Volo.Abp.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
</ItemGroup>
</Project>

61
framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/AbpAspNetCoreUowTestModule.cs

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.Autofac;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.Modularity;
namespace Volo.Abp.AspNetCore.Uow;
[DependsOn(
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpEntityFrameworkCoreSqliteModule),
typeof(AbpAutofacModule)
)]
public class AbpAspNetCoreUowTestModule : AbpModule
{
private readonly AbpUnitTestSqliteDatabase _database = new AbpUnitTestSqliteDatabase();
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<UowVisibilityTestDbContext>(options =>
{
options.AddDefaultRepositories(includeAllEntities: true);
});
Configure<AbpDbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = _database.ConnectionString;
});
Configure<AbpDbContextOptions>(options =>
{
options.Configure(dbContext => dbContext.UseSqlite().AddAbpDbContextOptionsExtension());
});
_database.CreateTables(new UowVisibilityTestDbContext(
new DbContextOptionsBuilder<UowVisibilityTestDbContext>()
.UseSqlite(_database.ConnectionString)
.AddAbpDbContextOptionsExtension()
.Options));
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.UseRouting();
app.UseUnitOfWork();
app.UseConfiguredEndpoints();
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
_database.Dispose();
}
}

15
framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/Program.cs

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.AspNetCore.Uow;
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = Environments.Staging
});
await builder.RunAbpModuleAsync<AbpAspNetCoreUowTestModule>();
public partial class Program
{
}

145
framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UnitOfWorkMiddleware_Relational_Tests.cs

@ -0,0 +1,145 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Volo.Abp.AspNetCore.TestBase;
using Xunit;
namespace Volo.Abp.AspNetCore.Uow;
public class UnitOfWorkMiddleware_Relational_Tests : AbpWebApplicationFactoryIntegratedTest<Program>
{
private void EnableCompleteOnResponseStarting()
{
ServiceProvider.GetRequiredService<IOptions<AbpAspNetCoreUnitOfWorkOptions>>()
.Value.CompleteUnitOfWorkOnResponseStarting = true;
}
private async Task<int> CountAsync(string name)
{
var response = await Client.GetAsync("/api/uow-visibility/count?name=" + name);
response.EnsureSuccessStatusCode();
return int.Parse(await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task Row_Written_During_Request_Is_Visible_From_An_Independent_Connection_On_Response_Start()
{
EnableCompleteOnResponseStarting();
var response = await Client.GetAsync("/api/uow-visibility/insert-then-read");
response.EnsureSuccessStatusCode();
(await response.Content.ReadAsStringAsync()).ShouldBe("inserted:visible");
}
[Fact]
public async Task Row_Written_During_Request_Is_Still_Committed_When_The_Feature_Is_Disabled()
{
var name = Guid.NewGuid().ToString("N");
var insert = await Client.GetAsync("/api/uow-visibility/insert?name=" + name);
insert.EnsureSuccessStatusCode();
(await CountAsync(name)).ShouldBe(1);
}
[Fact]
public async Task Exception_Before_Response_Rolls_Back_The_Written_Row()
{
EnableCompleteOnResponseStarting();
var name = Guid.NewGuid().ToString("N");
var insert = await Client.GetAsync("/api/uow-visibility/insert-then-throw?name=" + name);
insert.IsSuccessStatusCode.ShouldBeFalse();
(await CountAsync(name)).ShouldBe(0);
}
[Fact]
public async Task Committed_Row_Survives_An_Exception_Raised_After_The_Response_Started()
{
EnableCompleteOnResponseStarting();
var name = Guid.NewGuid().ToString("N");
await Should.ThrowAsync<HttpRequestException>(async () =>
{
var response = await Client.GetAsync("/api/uow-visibility/insert-flush-then-throw?name=" + name);
await response.Content.ReadAsStringAsync();
});
(await CountAsync(name)).ShouldBe(1);
}
[Fact]
public async Task Committed_Row_Survives_A_Completed_Handler_Failing_On_Response_Start()
{
EnableCompleteOnResponseStarting();
var name = Guid.NewGuid().ToString("N");
// The handler's error must surface as-is (not masked by a second "already requested" completion);
// the row is committed regardless, since the handler runs after commit.
Exception surfaced = null;
try
{
var response = await Client.GetAsync("/api/uow-visibility/insert-flush-throwing-completed-handler?name=" + name);
await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
surfaced = ex;
}
surfaced.ShouldNotBeNull();
surfaced.ToString().ShouldContain("boom in a completed handler");
surfaced.ToString().ShouldNotContain("already");
(await CountAsync(name)).ShouldBe(1);
}
[Fact]
public async Task A_Failing_Commit_On_Response_Start_Does_Not_Persist_Data()
{
EnableCompleteOnResponseStarting();
var name = Guid.NewGuid().ToString("N");
Exception surfaced = null;
try
{
var response = await Client.GetAsync("/api/uow-visibility/insert-then-fail-commit?name=" + name);
await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
surfaced = ex;
}
// The commit fails on response start, so the request must surface an error and persist nothing.
surfaced.ShouldNotBeNull();
(await CountAsync(name)).ShouldBe(0);
}
[Fact]
public async Task Result_Serialization_Failure_Rolls_Back_And_Does_Not_Commit_On_The_Error_Response()
{
EnableCompleteOnResponseStarting();
var name = Guid.NewGuid().ToString("N");
HttpResponseMessage response = null;
Exception surfaced = null;
try
{
response = await Client.GetAsync("/api/uow-visibility/insert-then-throw-in-serialization?name=" + name);
await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
surfaced = ex;
}
// The action ran and saved the row, then serializing the result failed. The request must therefore
// fail with a server error (not a 404 or a success), and the error response, written by the upstream
// exception middleware after the request unit of work is disposed, must not commit the failed request.
(surfaced != null || (response != null && (int)response.StatusCode >= 500)).ShouldBeTrue();
(await CountAsync(name)).ShouldBe(0);
}
}

124
framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UowVisibilityController.cs

@ -0,0 +1,124 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Uow;
namespace Volo.Abp.AspNetCore.Uow;
[Route("api/uow-visibility")]
public class UowVisibilityController : AbpController
{
private readonly IRepository<UowVisibilityTestEntity, Guid> _repository;
public UowVisibilityController(IRepository<UowVisibilityTestEntity, Guid> repository)
{
_repository = repository;
}
[HttpGet]
[Route("insert-then-read")]
[UnitOfWork(isTransactional: true)]
public async Task InsertThenRead()
{
var name = Guid.NewGuid().ToString("N");
await _repository.InsertAsync(new UowVisibilityTestEntity(Guid.NewGuid(), name));
await Response.WriteAsync("inserted");
await Response.Body.FlushAsync();
int count;
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
count = await _repository.CountAsync(x => x.Name == name);
await uow.CompleteAsync();
}
await Response.WriteAsync(count == 1 ? ":visible" : ":not-visible");
}
[HttpGet]
[Route("insert")]
[UnitOfWork(isTransactional: true)]
public async Task Insert(string name)
{
await _repository.InsertAsync(new UowVisibilityTestEntity(Guid.NewGuid(), name));
await Response.WriteAsync("inserted");
}
[HttpGet]
[Route("count")]
public async Task Count(string name)
{
var count = await _repository.CountAsync(x => x.Name == name);
await Response.WriteAsync(count.ToString());
}
// Insert (autoSave sends the INSERT to the transaction) then throw before the response: must roll back.
[HttpGet]
[Route("insert-then-throw")]
[UnitOfWork(isTransactional: true)]
public async Task InsertThenThrow(string name)
{
await _repository.InsertAsync(new UowVisibilityTestEntity(Guid.NewGuid(), name), autoSave: true);
throw new AbpException("boom before the response started");
}
// Insert, flush the response (committed here when enabled), then throw: the committed row survives.
[HttpGet]
[Route("insert-flush-then-throw")]
[UnitOfWork(isTransactional: true)]
public async Task InsertFlushThenThrow(string name)
{
await _repository.InsertAsync(new UowVisibilityTestEntity(Guid.NewGuid(), name));
await Response.WriteAsync("inserted");
await Response.Body.FlushAsync();
throw new AbpException("boom after the response started");
}
// Insert, register a completed handler that throws (runs after commit), then flush.
[HttpGet]
[Route("insert-flush-throwing-completed-handler")]
[UnitOfWork(isTransactional: true)]
public async Task InsertFlushWithThrowingCompletedHandler(string name)
{
await _repository.InsertAsync(new UowVisibilityTestEntity(Guid.NewGuid(), name));
CurrentUnitOfWork.OnCompleted(() => throw new AbpException("boom in a completed handler"));
await Response.WriteAsync("inserted");
await Response.Body.FlushAsync();
}
// Insert a valid row plus an invalid one (Name is required): the commit at response start fails.
[HttpGet]
[Route("insert-then-fail-commit")]
[UnitOfWork(isTransactional: true)]
public async Task InsertThenFailCommit(string name)
{
await _repository.InsertAsync(new UowVisibilityTestEntity(Guid.NewGuid(), name));
await _repository.InsertAsync(new UowVisibilityTestEntity(Guid.NewGuid(), null));
await Response.WriteAsync("inserted");
await Response.Body.FlushAsync();
}
// The action succeeds (so the action filter saves changes), then serializing the object result throws
// before the response starts. The upstream exception middleware writes the error response after the
// request unit of work is disposed, so response-start completion must not commit the failed request.
[HttpGet]
[Route("insert-then-throw-in-serialization")]
[UnitOfWork(isTransactional: true)]
public async Task<IActionResult> InsertThenThrowInSerialization(string name)
{
await _repository.InsertAsync(new UowVisibilityTestEntity(Guid.NewGuid(), name));
return Ok(new ThrowingOnSerializeDto());
}
public class ThrowingOnSerializeDto
{
public string Value => throw new AbpException("boom while serializing the object result");
}
}

46
framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UowVisibilityTestEntity.cs

@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Modeling;
namespace Volo.Abp.AspNetCore.Uow;
public class UowVisibilityTestEntity : AggregateRoot<Guid>
{
public string Name { get; set; }
protected UowVisibilityTestEntity()
{
}
public UowVisibilityTestEntity(Guid id, string name)
: base(id)
{
Name = name;
}
}
[ConnectionStringName("Default")]
public class UowVisibilityTestDbContext : AbpDbContext<UowVisibilityTestDbContext>
{
public DbSet<UowVisibilityTestEntity> UowVisibilityTestEntities { get; set; }
public UowVisibilityTestDbContext(DbContextOptions<UowVisibilityTestDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UowVisibilityTestEntity>(b =>
{
b.ToTable("UowVisibilityTestEntities");
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired();
});
}
}

37
modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs

@ -1,10 +1,14 @@
using Microsoft.AspNetCore.Identity;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AspNetCore.Uow;
using Volo.Abp.Modularity;
using Volo.Abp.OpenIddict.Globalization;
using Volo.Abp.OpenIddict.Scopes;
@ -45,6 +49,37 @@ public class AbpOpenIddictAspNetCoreModule : AbpModule
{
options.RemoveClientIdClaim();
});
// Complete data written while processing OpenIddict requests before the response starts.
// Derived from the configured server endpoint paths so remapped endpoints are covered too.
context.Services.AddOptions<AbpAspNetCoreUnitOfWorkOptions>()
.Configure<IOptions<OpenIddictServerOptions>>((uowOptions, serverOptions) =>
{
foreach (var path in GetServerEndpointPaths(serverOptions.Value))
{
uowOptions.CompleteUnitOfWorkOnResponseStartingUrls.AddIfNotContains(path);
}
});
}
private static IEnumerable<string> GetServerEndpointPaths(OpenIddictServerOptions serverOptions)
{
var endpoints = serverOptions.TokenEndpointUris
.Concat(serverOptions.AuthorizationEndpointUris)
.Concat(serverOptions.DeviceAuthorizationEndpointUris)
.Concat(serverOptions.PushedAuthorizationEndpointUris)
.Concat(serverOptions.EndSessionEndpointUris)
.Concat(serverOptions.RevocationEndpointUris)
.Concat(serverOptions.EndUserVerificationEndpointUris);
foreach (var uri in endpoints)
{
var path = uri.IsAbsoluteUri ? uri.AbsolutePath : "/" + uri.OriginalString.TrimStart('/');
if (path.Length > 1)
{
yield return path;
}
}
}
private void AddOpenIddictServer(IServiceCollection services)

6
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo.Abp.OpenIddict.AspNetCore.Tests.csproj

@ -5,6 +5,8 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<PreserveCompilationReferences>true</PreserveCompilationReferences>
</PropertyGroup>
<ItemGroup>
@ -16,6 +18,10 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.OpenIddict.AspNetCore\Volo.Abp.OpenIddict.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.OpenIddict.EntityFrameworkCore\Volo.Abp.OpenIddict.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.EntityFrameworkCore.Sqlite\Volo.Abp.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.TestBase\Volo.Abp.AspNetCore.TestBase.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
</ItemGroup>
</Project>

69
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenEndpoint_Integration_Tests.cs

@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Shouldly;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.AspNetCore.Uow;
using Xunit;
namespace Volo.Abp.OpenIddict.Integration;
// A real "/connect/token" (client_credentials) request through the OpenIddict server. A probe registered
// outside UseUnitOfWork reads the token count from an independent connection at response start.
public class OpenIddictTokenEndpoint_Integration_Tests : AbpWebApplicationFactoryIntegratedTest<Program>
{
private AbpAspNetCoreUnitOfWorkOptions Options =>
ServiceProvider.GetRequiredService<IOptions<AbpAspNetCoreUnitOfWorkOptions>>().Value;
private long? TokenCountAtResponseStart =>
ServiceProvider.GetRequiredService<TokenVisibilityRecorder>().TokenCountAtResponseStart;
private Task<HttpResponseMessage> RequestTokenAsync()
{
return Client.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = "test-client",
["client_secret"] = "test-secret"
}));
}
[Fact]
public async Task Token_Row_Is_Committed_Before_The_Connect_Token_Response_Is_Sent()
{
// The OpenIddict module opts its configured endpoint paths (including "/connect/token") in by default.
var response = await RequestTokenAsync();
response.StatusCode.ShouldBe(HttpStatusCode.OK);
(await response.Content.ReadAsStringAsync()).ShouldContain("access_token");
TokenCountAtResponseStart.ShouldBe(1);
}
[Fact]
public async Task Without_The_Opt_In_The_Token_Is_Not_Committed_When_The_Response_Starts()
{
// Negative control: without the opt-in the token is committed only at the end of the pipeline,
// so the probe reads 0. This proves the positive case genuinely observes response-start timing.
Options.CompleteUnitOfWorkOnResponseStartingUrls.Clear();
Options.CompleteUnitOfWorkOnResponseStarting = false;
var response = await RequestTokenAsync();
response.StatusCode.ShouldBe(HttpStatusCode.OK);
TokenCountAtResponseStart.ShouldBe(0);
}
[Fact]
public void The_Configured_OpenIddict_Endpoint_Paths_Are_Opted_In()
{
// The opt-in list is derived from the configured server endpoints, so a non-"/connect" endpoint
// like "/device" is covered, and the custom "/my-custom/token" endpoint the test host registered
// is followed too - a hardcoded "/connect" prefix would miss both.
Options.CompleteUnitOfWorkOnResponseStartingUrls.ShouldContain("/connect/token");
Options.CompleteUnitOfWorkOnResponseStartingUrls.ShouldContain("/device");
Options.CompleteUnitOfWorkOnResponseStartingUrls.ShouldContain("/my-custom/token");
}
}

157
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenIntegrationTestModule.cs

@ -0,0 +1,157 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.AspNetCore.Uow;
using Volo.Abp.Data;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.Modularity;
using Volo.Abp.OpenIddict.Applications;
using Volo.Abp.OpenIddict.EntityFrameworkCore;
using Volo.Abp.OpenIddict.Tokens;
using Volo.Abp.Uow;
using Volo.Abp.Autofac;
namespace Volo.Abp.OpenIddict.Integration;
public class TokenVisibilityRecorder
{
public long? TokenCountAtResponseStart { get; set; }
}
[DependsOn(
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpOpenIddictAspNetCoreModule),
typeof(AbpOpenIddictEntityFrameworkCoreModule),
typeof(AbpEntityFrameworkCoreSqliteModule),
typeof(AbpAutofacModule)
)]
public class OpenIddictTokenIntegrationTestModule : AbpModule
{
// File-based SQLite (not shared-cache in-memory) so an independent connection can read committed
// state while another holds an open write transaction, without the shared-cache single-writer deadlock.
private readonly string _databasePath = Path.Combine(Path.GetTempPath(), $"abp-oidc-uow-{Guid.NewGuid():N}.db");
private string ConnectionString => $"Data Source={_databasePath};Pooling=False";
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
{
options.AddDevelopmentEncryptionAndSigningCertificate = false;
});
PreConfigure<OpenIddictServerBuilder>(builder =>
{
builder.AddEphemeralEncryptionKey();
builder.AddEphemeralSigningKey();
builder.UseAspNetCore().DisableTransportSecurityRequirement();
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton<TokenVisibilityRecorder>();
// A remapped token endpoint, so the tests can prove the opt-in list is derived from the configured
// server endpoints (custom endpoints are followed) rather than a hardcoded "/connect" prefix.
Configure<OpenIddictServerOptions>(options =>
{
options.TokenEndpointUris.Add(new Uri("my-custom/token", UriKind.Relative));
});
// The OpenIddict controllers (including the token endpoint) live in a referenced assembly.
context.Services.GetSingletonInstance<ApplicationPartManager>()
.ApplicationParts.AddIfNotContains(typeof(AbpOpenIddictAspNetCoreModule).Assembly);
using (var dbContext = new OpenIddictDbContext(
new DbContextOptionsBuilder<OpenIddictDbContext>().UseSqlite(ConnectionString).Options))
{
dbContext.Database.EnsureCreated();
}
Configure<AbpDbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = ConnectionString;
});
Configure<AbpDbContextOptions>(options =>
{
options.Configure(c => c.UseSqlite());
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
SeedClientAsync(context.ServiceProvider).GetAwaiter().GetResult();
var app = context.GetApplicationBuilder();
app.UseRouting();
// Registered before UseUnitOfWork so its OnStarting runs after the unit of work commits
// (callbacks run in reverse order): reads the token count from an independent connection.
app.Use(async (ctx, next) =>
{
if (ctx.Request.Path.StartsWithSegments("/connect/token"))
{
ctx.Response.OnStarting(async () =>
{
var recorder = ctx.RequestServices.GetRequiredService<TokenVisibilityRecorder>();
var uowManager = ctx.RequestServices.GetRequiredService<IUnitOfWorkManager>();
using var uow = uowManager.Begin(requiresNew: true, isTransactional: false);
var repository = ctx.RequestServices.GetRequiredService<IRepository<OpenIddictToken, Guid>>();
recorder.TokenCountAtResponseStart = await repository.GetCountAsync();
await uow.CompleteAsync();
});
}
await next();
});
app.UseAuthentication();
app.UseUnitOfWork();
app.UseAuthorization();
app.UseConfiguredEndpoints();
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
if (File.Exists(_databasePath))
{
File.Delete(_databasePath);
}
}
private static async Task SeedClientAsync(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
using var uow = uowManager.Begin();
var applicationManager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
if (await applicationManager.FindByClientIdAsync("test-client") == null)
{
await applicationManager.CreateAsync(new AbpApplicationDescriptor
{
ClientId = "test-client",
ClientSecret = "test-secret",
DisplayName = "Test Client",
ClientType = OpenIddictConstants.ClientTypes.Confidential,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials
}
});
}
await uow.CompleteAsync();
}
}

15
modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/Program.cs

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.OpenIddict.Integration;
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = Environments.Staging
});
await builder.RunAbpModuleAsync<OpenIddictTokenIntegrationTestModule>();
public partial class Program
{
}
Loading…
Cancel
Save