diff --git a/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs new file mode 100644 index 0000000000..3b5bf7aebb --- /dev/null +++ b/src/Umbraco.Core/DependencyInjection/IScopedServiceProvider.cs @@ -0,0 +1,19 @@ +using System; + +namespace Umbraco.Cms.Core.DependencyInjection +{ + /// + /// Provides access to a request scoped service provider when available for cases where + /// IHttpContextAccessor is not available. e.g. No reference to AspNetCore.Http in core. + /// + public interface IScopedServiceProvider + { + /// + /// Gets a request scoped service provider when available. + /// + /// + /// Can be null. + /// + IServiceProvider ServiceProvider { get; } + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 1ad30c6e8e..6385e3431d 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -166,7 +166,9 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddScoped(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => + new PublishedContentQueryAccessor(sp.GetRequiredService()) + ); builder.Services.AddScoped(factory => { var umbCtx = factory.GetRequiredService(); diff --git a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs index 68a15580e4..00adc5018b 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs @@ -1,7 +1,18 @@ -using Umbraco.Cms.Infrastructure; - namespace Umbraco.Cms.Core { + /// + /// Not intended for use in background threads where you should make use of + /// and instead resolve IPublishedContentQuery from a + /// e.g. using + /// + /// + /// // Background thread example + /// using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext(); + /// using IServiceScope serviceScope = _serviceProvider.CreateScope(); + /// IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>(); + /// + /// + /// public interface IPublishedContentQueryAccessor { bool TryGetValue(out IPublishedContentQuery publishedContentQuery); diff --git a/src/Umbraco.Infrastructure/PublishedContentQueryAccessor.cs b/src/Umbraco.Infrastructure/PublishedContentQueryAccessor.cs index 9f716365a0..d61900df95 100644 --- a/src/Umbraco.Infrastructure/PublishedContentQueryAccessor.cs +++ b/src/Umbraco.Infrastructure/PublishedContentQueryAccessor.cs @@ -1,17 +1,21 @@ using System; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Core { public class PublishedContentQueryAccessor : IPublishedContentQueryAccessor { - private readonly IServiceProvider _serviceProvider; + private readonly IScopedServiceProvider _scopedServiceProvider; - public PublishedContentQueryAccessor(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + [Obsolete("Please use alternative constructor")] + public PublishedContentQueryAccessor(IServiceProvider serviceProvider) => _scopedServiceProvider = serviceProvider.GetRequiredService(); + + public PublishedContentQueryAccessor(IScopedServiceProvider scopedServiceProvider) => _scopedServiceProvider = scopedServiceProvider; public bool TryGetValue(out IPublishedContentQuery publishedContentQuery) { - publishedContentQuery = _serviceProvider.GetRequiredService(); + publishedContentQuery = _scopedServiceProvider.ServiceProvider?.GetService(); return publishedContentQuery is not null; } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ScopedServiceProvider.cs b/src/Umbraco.Web.Common/DependencyInjection/ScopedServiceProvider.cs new file mode 100644 index 0000000000..de660184f1 --- /dev/null +++ b/src/Umbraco.Web.Common/DependencyInjection/ScopedServiceProvider.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Web.Common.DependencyInjection +{ + /// + internal class ScopedServiceProvider : IScopedServiceProvider + { + private readonly IHttpContextAccessor _accessor; + + public ScopedServiceProvider(IHttpContextAccessor accessor) => _accessor = accessor; + + /// + public IServiceProvider ServiceProvider => _accessor.HttpContext?.RequestServices; + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index e3e3462e6a..fda4d7ca32 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -349,6 +349,7 @@ namespace Umbraco.Extensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContentQueryAccessorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContentQueryAccessorTests.cs new file mode 100644 index 0000000000..ec62bb55fa --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContentQueryAccessorTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Tests.Integration.TestServerTest; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core +{ + [TestFixture] + public class PublishedContentQueryAccessorTests : UmbracoTestServerTestBase + { + [Test] + public async Task PublishedContentQueryAccessor_WithRequestScope_WillProvideQuery() + { + HttpResponseMessage result = await Client.GetAsync("/demo-published-content-query-accessor"); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + } + } + + public class PublishedContentQueryAccessorTestController : Controller + { + private readonly IPublishedContentQueryAccessor _accessor; + + public PublishedContentQueryAccessorTestController(IPublishedContentQueryAccessor accessor) + { + _accessor = accessor; + } + + [HttpGet("demo-published-content-query-accessor")] + public IActionResult Test() + { + var success = _accessor.TryGetValue(out IPublishedContentQuery query); + + if (!success || query == null) + { + throw new ApplicationException("It doesn't work"); + } + + return Ok(); + } + } +}