diff --git a/Src/Dnn/ToSic.Sxc.Dnn.Core/Dnn/LookUp/DnnLookUpEngineResolver.cs b/Src/Dnn/ToSic.Sxc.Dnn.Core/Dnn/LookUp/DnnLookUpEngineResolver.cs index f77820cd9..b85906146 100644 --- a/Src/Dnn/ToSic.Sxc.Dnn.Core/Dnn/LookUp/DnnLookUpEngineResolver.cs +++ b/Src/Dnn/ToSic.Sxc.Dnn.Core/Dnn/LookUp/DnnLookUpEngineResolver.cs @@ -68,7 +68,7 @@ private LookUpEngine BuildDnnBasedLookupEngine(PortalSettings portalSettings, in { { KeyId, original.Get(OldDnnModuleId) } }); - additions.Add(new LookUpInLookUps(SourceModule, modAdditional, original)); + additions.Add(new LookUpInLookUps(SourceModule, [modAdditional, original])); } // Create the lookup for "site" based on the "portal" and only give it "id" & "guid" diff --git a/Src/Dnn/ToSic.Sxc.Dnn.Razor/Dnn/Razor/Internal/RoslynBuildManager.cs b/Src/Dnn/ToSic.Sxc.Dnn.Razor/Dnn/Razor/Internal/RoslynBuildManager.cs index 0664e7ab0..432bd1018 100644 --- a/Src/Dnn/ToSic.Sxc.Dnn.Razor/Dnn/Razor/Internal/RoslynBuildManager.cs +++ b/Src/Dnn/ToSic.Sxc.Dnn.Razor/Dnn/Razor/Internal/RoslynBuildManager.cs @@ -263,7 +263,7 @@ private CSharpCodeProvider GetCSharpCodeProvider() { var l = Log.Fn(); // See if in memory cache - if (memoryCacheService.Get(CSharpCodeProviderCacheKey) is CSharpCodeProvider fromCache) + if (memoryCacheService.TryGet(CSharpCodeProviderCacheKey, out var fromCache)) return l.Return(fromCache, "from cached"); var codeProvider = new CSharpCodeProvider(); // TODO: @stv test with latest nuget package for @inherits ; issue diff --git a/Src/Dnn/ToSic.Sxc.Dnn.WebApi/Dnn/WebApi/Internal/Routing/AppApiControllerSelectorService_Cache.cs b/Src/Dnn/ToSic.Sxc.Dnn.WebApi/Dnn/WebApi/Internal/Routing/AppApiControllerSelectorService_Cache.cs index 5480dddfc..d05d4172d 100644 --- a/Src/Dnn/ToSic.Sxc.Dnn.WebApi/Dnn/WebApi/Internal/Routing/AppApiControllerSelectorService_Cache.cs +++ b/Src/Dnn/ToSic.Sxc.Dnn.WebApi/Dnn/WebApi/Internal/Routing/AppApiControllerSelectorService_Cache.cs @@ -15,7 +15,7 @@ private HttpControllerDescriptor Get(string appFolder, string editionPath, strin //SetPathForCompilersInsideController(appFolder, editionPath, controllerTypeName, shared); var descriptorCacheKey = CacheKey(appFolder, controllerTypeName, shared, spec); - if (memoryCacheService.Get(descriptorCacheKey) is HttpControllerDescriptorWithPaths dataWithPaths) + if (memoryCacheService.TryGet(descriptorCacheKey, out var dataWithPaths)) { PreservePathForGetCodeInController(dataWithPaths.Folder, dataWithPaths.FullPath); return dataWithPaths.Descriptor; diff --git a/Src/Dnn/ToSic.Sxc.Dnn/DnnPackageBuilder/ReleaseNotes.txt b/Src/Dnn/ToSic.Sxc.Dnn/DnnPackageBuilder/ReleaseNotes.txt index 90c2ad5ba..858010c12 100644 --- a/Src/Dnn/ToSic.Sxc.Dnn/DnnPackageBuilder/ReleaseNotes.txt +++ b/Src/Dnn/ToSic.Sxc.Dnn/DnnPackageBuilder/ReleaseNotes.txt @@ -1,5 +1,5 @@
-
v17.08.00
+
v17.09.00
Part of module installation is the deletion of unneeded data. In an infrequent case, You could get a timeout exception. This is not a show-stopper. Simply reload the page so DNN can continue deletion until all unnecessary data is deleted and the module is installed.
diff --git a/Src/Dnn/ToSic.Sxc.Dnn/DnnPackageBuilder/ToSic.Sxc.Dnn.dnn b/Src/Dnn/ToSic.Sxc.Dnn/DnnPackageBuilder/ToSic.Sxc.Dnn.dnn index 34501cf90..13c114806 100644 --- a/Src/Dnn/ToSic.Sxc.Dnn/DnnPackageBuilder/ToSic.Sxc.Dnn.dnn +++ b/Src/Dnn/ToSic.Sxc.Dnn/DnnPackageBuilder/ToSic.Sxc.Dnn.dnn @@ -1,6 +1,6 @@  - + Content 2sxc is a DNN Extension to create attractive and designed content. It solves the common problem, allowing the web designer to create designed templates for different content elements, so that the user must only fill in fields and receive a perfectly designed and animated output. icon.png @@ -69,7 +69,7 @@ @@ -120,7 +120,7 @@ ToSic.SexyContent.DnnBusinessController [DESKTOPMODULEID] - 01.00.00,08.11.00,08.12.00,09.00.00,10.00.00,11.00.00,12.00.00,13.00.00,13.01.00,13.04.00,14.00.00,15.00.00,15.02.00,16.00.00,16.07.01,17.00.00,17.08.00 + 01.00.00,08.11.00,08.12.00,09.00.00,10.00.00,11.00.00,12.00.00,13.00.00,13.01.00,13.04.00,14.00.00,15.00.00,15.02.00,16.00.00,16.07.01,17.00.00,17.09.00 @@ -627,7 +627,7 @@ - + App 2sxc App is an extension that allows to install and use a 2sxc app. icon-app.png diff --git a/Src/Oqtane/ToSic.Sxc.Oqt.Package/ToSic.Sxc.Oqtane.Install.nuspec b/Src/Oqtane/ToSic.Sxc.Oqt.Package/ToSic.Sxc.Oqtane.Install.nuspec index 435f0ae64..e5bee25f6 100644 --- a/Src/Oqtane/ToSic.Sxc.Oqt.Package/ToSic.Sxc.Oqtane.Install.nuspec +++ b/Src/Oqtane/ToSic.Sxc.Oqt.Package/ToSic.Sxc.Oqtane.Install.nuspec @@ -2,7 +2,7 @@ ToSic.Sxc.Oqtane.Install - 17.08.00 + 17.09.00 2sic internet solutions GmbH, Switzerland 2sic internet solutions GmbH, Switzerland 2sxc CMS and Meta-Module for Oqtane diff --git a/Src/Oqtane/ToSic.Sxc.Oqt.Server/Controllers/EditUiMiddleware.cs b/Src/Oqtane/ToSic.Sxc.Oqt.Server/Controllers/EditUiMiddleware.cs index 0253dafb9..903a45ab8 100644 --- a/Src/Oqtane/ToSic.Sxc.Oqt.Server/Controllers/EditUiMiddleware.cs +++ b/Src/Oqtane/ToSic.Sxc.Oqt.Server/Controllers/EditUiMiddleware.cs @@ -25,7 +25,7 @@ public static Task PageOutputCached(HttpContext context, IWebHostEnvironment env var key = CacheKey(virtualPath); var memoryCacheService = sp.GetService(); - if (memoryCacheService.Get(key) is not string html) + if (!memoryCacheService.TryGet(key, out var html)) { var path = Path.Combine(env.WebRootPath, virtualPath); if (!File.Exists(path)) throw new FileNotFoundException("File not found: " + path); diff --git a/Src/Sxc.Tests/ToSic.Sxc.Tests/ServicesTests/CacheTests/CacheKeyTests.cs b/Src/Sxc.Tests/ToSic.Sxc.Tests/ServicesTests/CacheTests/CacheKeyTests.cs new file mode 100644 index 000000000..50d159968 --- /dev/null +++ b/Src/Sxc.Tests/ToSic.Sxc.Tests/ServicesTests/CacheTests/CacheKeyTests.cs @@ -0,0 +1,73 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using ToSic.Sxc.Services.Cache; +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using static ToSic.Sxc.Services.Cache.CacheServiceConstants; + +namespace ToSic.Sxc.Tests.ServicesTests.CacheTests; + +[TestClass] +public class CacheKeyTests +{ + internal const string FullDefaultPrefix = $"{DefaultPrefix}{Sep}App:0{Sep}{SegmentPrefix}{DefaultSegment}{Sep}"; + + [DataRow(null)] + [DataRow("")] + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void MainKeyBad(string main) + => AreEqual(FullDefaultPrefix + main, new CacheKeySpecs(0, main).Key); + + [DataRow(Sep + "App:0", 0, "zero")] + [DataRow(Sep + "App:27", 27, "27")] + [DataRow(Sep + "App:42", 42, "42")] + [DataRow("", CacheKeySpecs.NoApp, "no app")] + [TestMethod] + public void MainKeyAppIds(string expectedReplace, int appId, string message) + => AreEqual(FullDefaultPrefix.Replace(Sep + "App:0", expectedReplace) + "Test", new CacheKeySpecs(appId, "Test").Key, message); + + + + [DataRow("TestKey")] + [DataRow("A")] + [DataRow("A\nB")] + [TestMethod] + public void MainKeyOnly(string main) + => AreEqual( FullDefaultPrefix + main, new CacheKeySpecs(0, main).Key); + + private const string FullSegmentPrefix = $"{DefaultPrefix}{Sep}App:0{Sep}{SegmentPrefix}"; + + [DataRow($"{DefaultSegment}{Sep}Main", "Main", null)] + [DataRow($"{DefaultSegment}{Sep}Main", "Main", "")] + [DataRow($"MySegment{Sep}Main", "Main", "MySegment")] + [TestMethod] + public void MainAndSegment(string expected, string main, string segment) + => AreEqual( FullSegmentPrefix + expected, new CacheKeySpecs(0, main, segment).Key); + + + [TestMethod] + public void EnsureDicIsSorted() + { + var expected = $"{Sep}A=AVal{Sep}B=BVal{Sep}C=CVal"; + var dic = new Dictionary + { + { "B", "BVal" }, + { "A", "AVal" }, + { "C", "CVal" } + }; + var resultDic1 = CacheKeySpecs.GetVaryByOfDic(dic); + AreEqual(expected, resultDic1); + + var dic2 = new Dictionary + { + { "C", "CVal" }, + { "A", "AVal" }, + { "B", "BVal" } + }; + var resultDic2 = CacheKeySpecs.GetVaryByOfDic(dic2); + AreEqual(expected, resultDic2); + AreEqual(resultDic1, resultDic2); + + } +} \ No newline at end of file diff --git a/Src/Sxc.Tests/ToSic.Sxc.Tests/ServicesTests/CacheTests/CacheSpecsTests.cs b/Src/Sxc.Tests/ToSic.Sxc.Tests/ServicesTests/CacheTests/CacheSpecsTests.cs new file mode 100644 index 000000000..8b06c14aa --- /dev/null +++ b/Src/Sxc.Tests/ToSic.Sxc.Tests/ServicesTests/CacheTests/CacheSpecsTests.cs @@ -0,0 +1,146 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using ToSic.Sxc.Context.Internal; +using ToSic.Sxc.Services; +using ToSic.Sxc.Services.Cache; +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; +using static ToSic.Sxc.Services.Cache.CacheServiceConstants; + +namespace ToSic.Sxc.Tests.ServicesTests.CacheTests; + +[TestClass] +public class CacheSpecsTests: TestBaseSxcDb +{ + private static readonly string MainPrefix = $"{CacheKeyTests.FullDefaultPrefix.Replace("App:0", "App:-1")}Main{Sep}"; + + private ICacheSpecs GetForMain(string name = "Main") => GetService().CreateSpecs(name); + + [TestMethod] + public void ShareKeyAcrossApps() + { + var expected = $"{CacheKeyTests.FullDefaultPrefix.Replace(Sep + "App:0", "")}Main"; + var svc = GetService(); + var specs = svc.CreateSpecs("Main", shared: true); + AreEqual(expected, specs.Key); + } + + [TestMethod] + public void VaryByCustom1CaseSensitive() + { + var expected = MainPrefix + "VaryByKey1=Value1"; + var svc = GetService(); + var specs = svc.CreateSpecs("Main") + .VaryBy("Key1", "Value1", caseSensitive: true); + AreEqual(expected, specs.Key); + } + + [TestMethod] + public void VaryByCustom1() + { + var expected = MainPrefix + "VaryByKey1=Value1".ToLowerInvariant(); + var specs = GetForMain() + .VaryBy("Key1", "Value1"); + AreEqual(expected, specs.Key); + } + + [TestMethod] + public void VaryByCustom2SameKey() + { + var expected = MainPrefix + "VaryByKey1=Value1".ToLowerInvariant(); + var specs = GetForMain() + .VaryBy("Key1", "valueWhichWillGoAway") + .VaryBy("Key1", "Value1"); + AreEqual(expected, specs.Key); + } + + [TestMethod] + public void VaryByCustom2DiffKey() + { + var expected = MainPrefix + $"VaryByKey1=Val1{Sep}VaryByKey2=Value2".ToLowerInvariant(); + var specs = GetForMain() + .VaryBy("Key1", "Val1") + .VaryBy("Key2", "Value2"); + AreEqual(expected, specs.Key); + } + + // Disabled for now, not sure if this single-value case is needed + //[TestMethod] + //public void VaryByCustom1ValueOnly() + //{ + // var expected = MainPrefix + "VaryByThisIsTheValue=".ToLowerInvariant(); + // var specs = GetForMain() + // .VaryBy("ThisIsTheValue"); + // AreEqual(expected, specs.Key); + //} + + [TestMethod] + public void VaryByParameters() + { + var expected = MainPrefix + "VaryByParameters=".ToLowerInvariant(); + var pars = new Parameters(); + var specs = GetForMain().VaryByParameters(pars); + AreEqual(expected, specs.Key); + } + + [DataRow(null, "no names specified")] + [DataRow("", "empty names specified")] + [DataRow("A,B,C", "too many names")] + [DataRow("A", "names with exact casing")] + [DataRow("a", "names with different casing")] + [TestMethod] + public void VaryByParametersOneNamed(string names, string testName) + { + var expected = MainPrefix + "VaryByParameters=A=AVal".ToLowerInvariant(); + var pars = new Parameters(new() + { + { "A", "AVal" } + }); + var specs = GetForMain().VaryByParameters(pars, names: names); + AreEqual(expected, specs.Key, testName); + } + + + [TestMethod] + public void VaryByParametersWithNamesAll() + { + var expected = MainPrefix + "VaryByParameters=A=AVal&B=BVal&C=CVal".ToLowerInvariant(); + var pars = new Parameters(new() + { + { "A", "AVal" }, + { "B", "BVal" }, + { "C", "CVal" } + }); + var specs = GetForMain().VaryByParameters(pars, names: "A,B,c"); + AreEqual(expected, specs.Key); + } + + [TestMethod] + public void VaryByParametersWithNamesSome() + { + var expected = MainPrefix + "VaryByParameters=A=AVal&C=CVal".ToLowerInvariant(); + var pars = new Parameters(new() + { + { "A", "AVal" }, + { "B", "BVal" }, + { "C", "CVal" } + }); + var specs = GetForMain().VaryByParameters(pars, names: "A,c"); + AreEqual(expected, specs.Key); + } + + [TestMethod] + public void VaryByParametersBlankSameAsNone() + { + var expected = MainPrefix + "VaryByParameters=A=AVal&C=CVal".ToLowerInvariant(); + var pars = new Parameters(new() + { + { "A", "AVal" }, + { "B", "" }, + { "C", "CVal" } + }); + var specsFiltered = GetForMain().VaryByParameters(pars, names: "A,c"); + var specsAll = GetForMain().VaryByParameters(pars); + AreEqual(expected, specsFiltered.Key); + AreEqual(specsFiltered.Key, specsAll.Key); + } + +} \ No newline at end of file diff --git a/Src/Sxc.Tests/ToSic.Sxc.Tests/ToSic.Sxc.Tests.csproj b/Src/Sxc.Tests/ToSic.Sxc.Tests/ToSic.Sxc.Tests.csproj index f67b40e47..dad889170 100644 --- a/Src/Sxc.Tests/ToSic.Sxc.Tests/ToSic.Sxc.Tests.csproj +++ b/Src/Sxc.Tests/ToSic.Sxc.Tests/ToSic.Sxc.Tests.csproj @@ -135,6 +135,8 @@ + + diff --git a/Src/Sxc/ToSic.Sxc.WebApi/Backend/AppStack/AppStackBackend.cs b/Src/Sxc/ToSic.Sxc.WebApi/Backend/AppStack/AppStackBackend.cs index c8f7ef347..ec6be9efe 100644 --- a/Src/Sxc/ToSic.Sxc.WebApi/Backend/AppStack/AppStackBackend.cs +++ b/Src/Sxc/ToSic.Sxc.WebApi/Backend/AppStack/AppStackBackend.cs @@ -12,7 +12,7 @@ public class AppStackBackend( AppDataStackService dataStackService, IZoneCultureResolver zoneCulture, IAppStates appStates, - LazySvc qDefBuilder) + Generator qDefBuilder) : ServiceBase("Sxc.ApiApQ", connect: [dataStackService, zoneCulture, appStates]) { public List GetAll(int appId, string part, string key, Guid? viewGuid, string[] languages) @@ -56,15 +56,16 @@ public List GetStackDump(IAppState appState, string partName, private IEntity GetViewSettingsForMixin(Guid? viewGuid, string[] languages, IAppEntityService appState, string realName) { - IEntity viewStackPart = null; - if (viewGuid != null) - { - var viewEnt = appState.List.One(viewGuid.Value); - if (viewEnt == null) throw new($"Tried to get view but not found. Guid was {viewGuid}"); - var view = new View(viewEnt, languages, Log, qDefBuilder); + if (viewGuid == null) + return null; - viewStackPart = realName == RootNameSettings ? view.Settings : view.Resources; - } + var viewEnt = appState.List.One(viewGuid.Value) + ?? throw new($"Tried to get view but not found. Guid was {viewGuid}"); + + var view = new View(viewEnt, languages, Log, qDefBuilder); + var viewStackPart = realName == RootNameSettings + ? view.Settings + : view.Resources; return viewStackPart; } diff --git a/Src/Sxc/ToSic.Sxc.WebApi/Backend/Views/ViewsExportImport.cs b/Src/Sxc/ToSic.Sxc.WebApi/Backend/Views/ViewsExportImport.cs index 870e6b30a..12ee41224 100644 --- a/Src/Sxc/ToSic.Sxc.WebApi/Backend/Views/ViewsExportImport.cs +++ b/Src/Sxc/ToSic.Sxc.WebApi/Backend/Views/ViewsExportImport.cs @@ -36,7 +36,7 @@ public class ViewsExportImport( AppIconHelpers appIconHelpers, Generator impExpHelpers, IResponseMaker responseMaker, - LazySvc qDefBuilder, + Generator qDefBuilder, IAppPathsMicroSvc appPathSvc) : ServiceBase("Bck.Views", connect: diff --git a/Src/Sxc/ToSic.Sxc/Apps/Internal/Blocks/BlockConfiguration.cs b/Src/Sxc/ToSic.Sxc/Apps/Internal/Blocks/BlockConfiguration.cs index ae6b7f16c..f32411f74 100644 --- a/Src/Sxc/ToSic.Sxc/Apps/Internal/Blocks/BlockConfiguration.cs +++ b/Src/Sxc/ToSic.Sxc/Apps/Internal/Blocks/BlockConfiguration.cs @@ -2,7 +2,6 @@ using ToSic.Eav.Cms.Internal; using ToSic.Eav.DataSource.Internal.Query; using ToSic.Lib.DI; -using ToSic.Sxc.Blocks; using ToSic.Sxc.Blocks.Internal; namespace ToSic.Sxc.Apps.Internal; @@ -13,19 +12,20 @@ public class BlockConfiguration: EntityBasedWithLog, IAppIdentity public int ZoneId { get; } public int AppId { get; } - internal IEntity PreviewTemplate { get; set; } + internal IEntity PreviewViewEntity { get; set; } internal IBlockIdentifier BlockIdentifierOrNull; - private readonly LazySvc _qDefBuilder; + private readonly Generator _qDefBuilder; - public BlockConfiguration(IEntity entity, IAppIdentity cmsRuntime, IEntity previewTemplate, LazySvc qDefBuilder, string languageCode, ILog parentLog): base(entity, languageCode, parentLog, "Blk.Config") + public BlockConfiguration(IEntity entity, IAppIdentity appIdentity, IEntity previewViewEntity, Generator qDefBuilder, string languageCode, ILog parentLog): + base(entity, languageCode, parentLog, "Blk.Config") { Log.A("Entity is " + (entity == null ? "" : "not") + " null"); _qDefBuilder = qDefBuilder; - ZoneId = cmsRuntime.ZoneId; - AppId = cmsRuntime.AppId; - PreviewTemplate = previewTemplate; + ZoneId = appIdentity.ZoneId; + AppId = appIdentity.AppId; + PreviewViewEntity = previewViewEntity; } internal BlockConfiguration WarnIfMissingData() @@ -54,8 +54,8 @@ public IView View if (_view != null) return _view; // if we're previewing another template, look that up - var templateEntity = PreviewTemplate ?? Entity?.Children(ViewParts.ViewFieldInContentBlock).FirstOrDefault(); - return _view = templateEntity == null ? null : new View(templateEntity, LookupLanguages, Log, _qDefBuilder); + var viewEntity = PreviewViewEntity ?? Entity?.Children(ViewParts.ViewFieldInContentBlock).FirstOrDefault(); + return _view = viewEntity == null ? null : new View(viewEntity, LookupLanguages, Log, _qDefBuilder); } } private IView _view; diff --git a/Src/Sxc/ToSic.Sxc/Apps/Internal/Work/WorkBlocks.cs b/Src/Sxc/ToSic.Sxc/Apps/Internal/Work/WorkBlocks.cs index d30573884..04d3bc211 100644 --- a/Src/Sxc/ToSic.Sxc/Apps/Internal/Work/WorkBlocks.cs +++ b/Src/Sxc/ToSic.Sxc/Apps/Internal/Work/WorkBlocks.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using ToSic.Eav.Apps; using ToSic.Eav.Apps.Internal.Work; using ToSic.Eav.Cms.Internal; using ToSic.Eav.Context; @@ -11,17 +12,19 @@ namespace ToSic.Sxc.Apps.Internal.Work; [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public class WorkBlocks( IZoneCultureResolver cultureResolver, - LazySvc qDefBuilder, - GenWorkPlus workEntities) - : WorkUnitBase("SxS.Blocks", connect: [cultureResolver, qDefBuilder, workEntities]) + Generator qDefBuilder, + GenWorkPlus workEntities, + LazySvc workViews) + : WorkUnitBase("SxS.Blocks", connect: [cultureResolver, qDefBuilder, workEntities, workViews]) { public const string BlockTypeName = "2SexyContent-ContentGroup"; - private IImmutableList ContentGroups() => workEntities.New(AppWorkCtx).Get(BlockTypeName).ToImmutableList(); + private IImmutableList GetContentGroups() => workEntities.New(AppWorkCtx).Get(BlockTypeName).ToImmutableList(); public List AllWithView() { - return ContentGroups() + var appIdentity = AppWorkCtx.PureIdentity(); + return GetContentGroups() .Select(b => { var templateGuid = b.Children(ViewParts.ViewFieldInContentBlock) @@ -32,7 +35,7 @@ public List AllWithView() : null; }) .Where(b => b != null) - .Select(e => new BlockConfiguration(e.Entity, AppWorkCtx, null, qDefBuilder, cultureResolver.CurrentCultureCode, Log)) + .Select(s => new BlockConfiguration(s.Entity, appIdentity, null, qDefBuilder, cultureResolver.CurrentCultureCode, Log)) .ToList(); } @@ -43,7 +46,7 @@ public List AllWithView() public BlockConfiguration GetBlockConfig(Guid contentGroupGuid) { var l = Log.Fn($"get CG#{contentGroupGuid}"); - var groupEntity = ContentGroups().One(contentGroupGuid); + var groupEntity = GetContentGroups().One(contentGroupGuid); var found = groupEntity != null; return l.Return(found ? new BlockConfiguration(groupEntity, AppWorkCtx, null, qDefBuilder, cultureResolver.CurrentCultureCode, Log) diff --git a/Src/Sxc/ToSic.Sxc/Apps/Internal/Work/WorkViews.cs b/Src/Sxc/ToSic.Sxc/Apps/Internal/Work/WorkViews.cs index 89df8c807..995352488 100644 --- a/Src/Sxc/ToSic.Sxc/Apps/Internal/Work/WorkViews.cs +++ b/Src/Sxc/ToSic.Sxc/Apps/Internal/Work/WorkViews.cs @@ -21,7 +21,7 @@ public class WorkViews( IZoneCultureResolver cultureResolver, IConvertToEavLight dataToFormatLight, LazySvc appIconHelpers, - LazySvc qDefBuilder) + Generator qDefBuilder) : WorkUnitBase("Cms.ViewRd", connect: [appEntities, valConverterLazy, cultureResolver, dataToFormatLight, appIconHelpers, qDefBuilder]) { @@ -37,18 +37,24 @@ public class WorkViews( public record ViewInfoForPathSelect(IView View, string Name, string UrlIdentifier, bool IsRegex, string MainKey); private List ViewEntities => _viewDs.Get(() => AppWorkCtx.AppState.GetPiggyBackPropExpiring( - () => appEntities.New(AppWorkCtx) - .Get(AppConstants.TemplateContentType) - .ToList() - ).Value); + () => appEntities.New(AppWorkCtx) + .Get(AppConstants.TemplateContentType) + .ToList() + ).Value + ); private readonly GetOnce> _viewDs = new(); + /// + /// Get all the views. + /// + /// + /// + /// Never cache this result in PiggyBack, as it has a service which would expire later on. + /// public IList GetAll() => - _all ??= AppWorkCtx.AppState.GetPiggyBackPropExpiring(() => ViewEntities - .Select(e => ViewOfEntity(e, "")) - .OrderBy(e => e.Name) - .ToList() - ).Value; + _all ??= [.. ViewEntities + .Select(e => ViewOfEntity(e, e.EntityId)) + .OrderBy(e => e.Name)]; private IList _all; @@ -73,23 +79,42 @@ public List GetForViewSwitch() var mainParam = isRegex ? urlIdentifier.Substring(0, urlIdentifier.Length - 3) : urlIdentifier; - return new ViewInfoForPathSelect(v, v.Name, urlIdentifier, isRegex, mainParam.ToLowerInvariant()); + + // Only save the necessary information in the PiggyBack + // Never save the View or the ViewInfoForPathSelect, as that would also preserve an old Service used in the View + return new + { + v.Entity, + v.Name, + urlIdentifier, + isRegex, + MainParam = mainParam.ToLowerInvariant() + }; }) .ToList() ); - return l.Return(views.Value, $"all: {GetAll().Count}; switchable: {views.Value.Count}; wasCached: {views.IsCached}"); + var final = views.Value + .Select(v => new ViewInfoForPathSelect( + ViewOfEntity(v.Entity, v.Entity.EntityId), v.Name, v.urlIdentifier, v.isRegex, v.MainParam) + ) + .ToList(); + + return l.Return(final, $"all: {GetAll().Count}; switchable: {final.Count}; wasCached: {views.IsCached}"); } - public IView Get(int templateId) => ViewOfEntity(ViewEntities.One(templateId), templateId); + public IView Get(int templateId) => ViewOfEntity(ViewEntities.One(templateId), templateId, withServices: true); + + public IView Get(Guid guid) => ViewOfEntity(ViewEntities.One(guid), guid, withServices: true); - public IView Get(Guid guid) => ViewOfEntity(ViewEntities.One(guid), guid); + public IView Recreate(IView originalWithoutServices) => + ViewOfEntity(originalWithoutServices.Entity, originalWithoutServices.Id, withServices: true); - private IView ViewOfEntity(IEntity templateEntity, object templateId) => + private IView ViewOfEntity(IEntity templateEntity, object templateId, bool withServices = true) => templateEntity == null ? throw new("The template with id '" + templateId + "' does not exist.") - : new View(templateEntity, [cultureResolver.CurrentCultureCode], Log, qDefBuilder); + : new View(templateEntity, [cultureResolver.CurrentCultureCode], Log, withServices ? qDefBuilder : null); internal IEnumerable GetCompatibleViews(IApp app, BlockConfiguration blockConfiguration) @@ -127,32 +152,36 @@ internal IEnumerable GetCompatibleViews(IApp app, BlockConfigura /// /// Get templates which match the signature of possible content-items, presentation etc. of the current template /// - /// + /// /// - private IEnumerable GetFullyCompatibleViews(BlockConfiguration blockConfiguration) + private IEnumerable GetFullyCompatibleViews(BlockConfiguration blockConfig) { - var isList = blockConfiguration.Content.Count > 1; + var isList = blockConfig.Content.Count > 1; - var compatibleTemplates = GetAll().Where(t => t.UseForList || !isList); + var compatibleTemplates = GetAll() + .Where(t => t.UseForList || !isList); + + // Compatibility check must verify each config on the view and compare with the current blockConfig compatibleTemplates = compatibleTemplates - .Where(t => blockConfiguration.Content.All(c => c == null) || blockConfiguration.Content.First(e => e != null).Type.NameId == t.ContentType) - .Where(t => blockConfiguration.Presentation.All(c => c == null) || blockConfiguration.Presentation.First(e => e != null).Type.NameId == t.PresentationType) - .Where(t => blockConfiguration.Header.All(c => c == null) || blockConfiguration.Header.First(e => e != null).Type.NameId == t.HeaderType) - .Where(t => blockConfiguration.HeaderPresentation.All(c => c == null) || blockConfiguration.HeaderPresentation.First(e => e != null).Type.NameId == t.HeaderPresentationType); + .Where(t => blockConfig.Content.All(c => c == null) || blockConfig.Content.First(e => e != null).Type.NameId == t.ContentType) + .Where(t => blockConfig.Presentation.All(c => c == null) || blockConfig.Presentation.First(e => e != null).Type.NameId == t.PresentationType) + .Where(t => blockConfig.Header.All(c => c == null) || blockConfig.Header.First(e => e != null).Type.NameId == t.HeaderType) + .Where(t => blockConfig.HeaderPresentation.All(c => c == null) || blockConfig.HeaderPresentation.First(e => e != null).Type.NameId == t.HeaderPresentationType); return compatibleTemplates; } // todo: check if this call could be replaced with the normal ContentTypeController.Get to prevent redundant code - public IEnumerable GetContentTypesWithStatus(string appPath, string appPathShared) + public IList GetContentTypesWithStatus(string appPath, string appPathShared) { var templates = GetAll().ToList(); var visible = templates.Where(t => !t.IsHidden).ToList(); var valConverter = valConverterLazy.Value; - return AppWorkCtx.AppState.ContentTypes.OfScope(Scopes.Default) + var result = AppWorkCtx.AppState.ContentTypes + .OfScope(Scopes.Default) .Where(ct => templates.Any(t => t.ContentType == ct.NameId)) // must exist in at least 1 template .OrderBy(ct => ct.Name) .Select(ct => @@ -169,6 +198,8 @@ public IEnumerable GetContentTypesWithStatus(string appPath, Properties = dataToFormatLight.Convert(details?.Entity), IsDefault = ct.Metadata.HasType(Decorators.IsDefaultDecorator), }; - }); + }) + .ToList(); + return result; } } \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Blocks/Internal/BlockBuilder/BlockBuilder_Render.cs b/Src/Sxc/ToSic.Sxc/Blocks/Internal/BlockBuilder/BlockBuilder_Render.cs index ffe3b4207..282a52596 100644 --- a/Src/Sxc/ToSic.Sxc/Blocks/Internal/BlockBuilder/BlockBuilder_Render.cs +++ b/Src/Sxc/ToSic.Sxc/Blocks/Internal/BlockBuilder/BlockBuilder_Render.cs @@ -32,7 +32,7 @@ public IRenderResult Run(bool topLevel, RenderSpecs specs) ModuleId = Block.ParentId, // Note 2023-10-30 2dm changed the handling of the preview template and checks if it's set. In case caching is too aggressive this can be the problem. Remove early 2024 //CanCache = !isErr && exsOrNull.SafeNone() && (Block.ContentGroupExists || Block.Configuration?.PreviewTemplateId.HasValue == true), - CanCache = !isErr && exsOrNull.SafeNone() && (Block.ContentGroupExists || Block.Configuration?.PreviewTemplate != null), + CanCache = !isErr && exsOrNull.SafeNone() && (Block.ContentGroupExists || Block.Configuration?.PreviewViewEntity != null), }; // case when we do not have an app diff --git a/Src/Sxc/ToSic.Sxc/Blocks/Internal/View/BlockViewLoader.cs b/Src/Sxc/ToSic.Sxc/Blocks/Internal/View/BlockViewLoader.cs index 44271227d..6c5564925 100644 --- a/Src/Sxc/ToSic.Sxc/Blocks/Internal/View/BlockViewLoader.cs +++ b/Src/Sxc/ToSic.Sxc/Blocks/Internal/View/BlockViewLoader.cs @@ -45,8 +45,13 @@ private IView TryGetViewBasedOnUrlParams(IContextOfBlock context, WorkViews view var foundMatch = set.IsRegex ? urlParameterDict.ContainsKey(set.MainKey) // match details/.* : urlParameterDict.ContainsValue(set.MainKey); // match view/details - if (foundMatch) - return l.Return(set.View, "template override: " + set.Name); + + if (!foundMatch) continue; + + //var originalWithoutServices = set.View; + //var finalView = views.Recreate(originalWithoutServices); + var finalView = set.View; + return l.Return(finalView, "template override: " + set.Name); } return l.ReturnNull("template override: none"); diff --git a/Src/Sxc/ToSic.Sxc/Blocks/Internal/View/View.cs b/Src/Sxc/ToSic.Sxc/Blocks/Internal/View/View.cs index e55367064..5c3561eb8 100644 --- a/Src/Sxc/ToSic.Sxc/Blocks/Internal/View/View.cs +++ b/Src/Sxc/ToSic.Sxc/Blocks/Internal/View/View.cs @@ -13,7 +13,7 @@ public class View( IEntity templateEntity, string[] languageCodes, ILog parentLog, - LazySvc qDefBuilder) + Generator qDefBuilder) : EntityBasedWithLog(templateEntity, languageCodes, parentLog, "Sxc.View"), IView { private IEntity GetBestRelationship(string key) => Entity.Children(key).FirstOrDefault(); @@ -75,7 +75,10 @@ internal string GetTypeStaticName(string groupPart) { var queryRaw = GetBestRelationship(FieldPipeline); var query = queryRaw != null - ? qDefBuilder.Value.Create(queryRaw, Entity.AppId) + ? (qDefBuilder?.New().Create(queryRaw, Entity.AppId) + ?? throw new ArgumentException( + @"Query Definition builder is null. View is probably from PiggyBack cache. To use it, you must first Recreate it with the WorkViews", + nameof(qDefBuilder))) : null; return (queryRaw, query); }); diff --git a/Src/Sxc/ToSic.Sxc/Code/Internal/HotBuild/AssemblyCacheManager.cs b/Src/Sxc/ToSic.Sxc/Code/Internal/HotBuild/AssemblyCacheManager.cs index 59b9abcc7..0c8179371 100644 --- a/Src/Sxc/ToSic.Sxc/Code/Internal/HotBuild/AssemblyCacheManager.cs +++ b/Src/Sxc/ToSic.Sxc/Code/Internal/HotBuild/AssemblyCacheManager.cs @@ -23,7 +23,7 @@ public class AssemblyCacheManager(MemoryCacheService memoryCacheService) : Servi public (List assemblyResults, string cacheKey) TryGetDependencies(HotBuildSpec spec) { var cacheKey = KeyDependency(spec); - return (memoryCacheService.Get(cacheKey) as List, cacheKey); + return (memoryCacheService.Get>(cacheKey), cacheKey); } private static string KeyDependency(HotBuildSpec spec) => $"{GlobalCacheRoot}a:{spec.AppId}.e:{spec.Edition}.d:{DependenciesLoader.DependenciesFolder}"; #endregion @@ -32,7 +32,7 @@ public class AssemblyCacheManager(MemoryCacheService memoryCacheService) : Servi internal static string KeyTemplate(string templateFullPath) => $"{GlobalCacheRoot}v:{templateFullPath.ToLowerInvariant()}"; - private AssemblyResult Get(string key) => memoryCacheService.Get(key) as AssemblyResult; + private AssemblyResult Get(string key) => memoryCacheService.Get(key); public AssemblyResult TryGetTemplate(string templateFullPath) => Get(KeyTemplate(templateFullPath)); diff --git a/Src/Sxc/ToSic.Sxc/LookUp/Internal/LookUpEngineResolverBase.cs b/Src/Sxc/ToSic.Sxc/LookUp/Internal/LookUpEngineResolverBase.cs index d6cb58564..9c047c891 100644 --- a/Src/Sxc/ToSic.Sxc/LookUp/Internal/LookUpEngineResolverBase.cs +++ b/Src/Sxc/ToSic.Sxc/LookUp/Internal/LookUpEngineResolverBase.cs @@ -92,7 +92,7 @@ protected List AddHttpAndDiSources(/*LookUpEngine existingList,*/ List< if (!sources.HasSource(LookUpConstants.SourceQuery)) additions .FirstOrDefault(lu => lu.Name.EqualsInsensitive(LookUpConstants.SourceQueryString)) - .DoIfNotNull(qsl => additions.Add(new LookUpInLookUps(LookUpConstants.SourceQuery, qsl))); + .DoIfNotNull(qsl => additions.Add(new LookUpInLookUps(LookUpConstants.SourceQuery, [qsl]))); return l.Return(additions, $"{additions.Count} additions"); } diff --git a/Src/Sxc/ToSic.Sxc/Services/Cache/CacheKeySpecs.cs b/Src/Sxc/ToSic.Sxc/Services/Cache/CacheKeySpecs.cs new file mode 100644 index 000000000..2ca8b22ac --- /dev/null +++ b/Src/Sxc/ToSic.Sxc/Services/Cache/CacheKeySpecs.cs @@ -0,0 +1,52 @@ +using System.Text; +using ToSic.Eav.Plumbing; +using static ToSic.Sxc.Services.Cache.CacheServiceConstants; + +namespace ToSic.Sxc.Services.Cache; + +internal record CacheKeySpecs(int AppId, string Main, string RegionName = default, Dictionary VaryByDic = default) +{ + /// + /// Special marker to say that the cache should not vary by appId + /// + internal const int NoApp = -9876; + + public string Key => _key ??= GetKey(this); + private string _key; + + public override string ToString() => Key; + + private static string GetKey(CacheKeySpecs keySpecs) + { + if (string.IsNullOrWhiteSpace(keySpecs.Main)) + throw new ArgumentException("Key must not be empty", nameof(keySpecs.Main)); + + // Prevent accidental adding of the prefix/segment multiple times + var mainKey = keySpecs.Main.StartsWith(DefaultPrefix) + ? keySpecs.Main + : $"{DefaultPrefix}{(keySpecs.AppId == NoApp ? "" : Sep + "App:" + keySpecs.AppId)}{Sep}{SegmentPrefix}{keySpecs.RegionName.NullIfNoValue() ?? DefaultSegment}{Sep}{keySpecs.Main}"; + + if (keySpecs.VaryByDic == null || keySpecs.VaryByDic.Count == 0) + return mainKey; + + var varyBy = GetVaryByOfDic(keySpecs.VaryByDic); + if (string.IsNullOrWhiteSpace(varyBy) || mainKey.EndsWith(varyBy)) + return mainKey; + + return $"{mainKey}{varyBy}"; + } + + internal static string GetVaryByOfDic(Dictionary dic) + { + // Keys must be ordered A-Z so that they are the same, no mater the order of adding + var ordered = dic + .OrderBy(p => p.Key, comparer: StringComparer.InvariantCultureIgnoreCase) + .ThenBy(p => p.Value, comparer: StringComparer.InvariantCultureIgnoreCase) + .ToList(); + + var sb = new StringBuilder(); + foreach (var pair in ordered) + sb.Append($"{Sep}{pair.Key}={pair.Value}"); + return sb.ToString(); + } +} diff --git a/Src/Sxc/ToSic.Sxc/Services/Cache/CacheService.cs b/Src/Sxc/ToSic.Sxc/Services/Cache/CacheService.cs new file mode 100644 index 000000000..a4468e8ac --- /dev/null +++ b/Src/Sxc/ToSic.Sxc/Services/Cache/CacheService.cs @@ -0,0 +1,92 @@ +using ToSic.Eav.Apps; +using ToSic.Eav.Apps.Integration; +using ToSic.Eav.Caching; +using ToSic.Lib.DI; +using ToSic.Sxc.Services.Internal; + +namespace ToSic.Sxc.Services.Cache; + +/// +/// WIP thoughts... +/// - policy variants +/// - also vary-by variants, eg. vary by user, by culture, by role, by device, by query, by url, by page, by module, by app, by tenant, by site, by domain, by host, by path, by query, by form, by cookie, by header, by session, by cache, by request, by response, by server, by client, by browser, by os, by device, by location, by time, by date, by day, by month, by year, by week, by hour, by minute, by second, by millisecond, by timezone, by language, by currency, by country, by region, by state, by city, by zip, by postal, by address, by phone, by fax, by email, by name, by title, by description, by keyword, by tag, by category, by group, by list, by array, by object, by property, by field, by column, by row, by table, by view, by form, by control, by input, by button, by link, by image, by icon, by logo, by text, by number, by integer, by float, by decimal, by boolean +/// +/// +internal class CacheService(MemoryCacheService cache, LazySvc appStates, Generator appPathsLazy) : ServiceForDynamicCode($"{SxcLogName}.CchSvc", connect: [cache, appStates]), ICacheService +{ + /// + /// AppId to use in key generation, so it won't collide with other apps. + /// + private int AppId => _appId ??= CodeApiSvc?.App.AppId ?? -1; + private int? _appId; + + public ICacheSpecs CreateSpecs(string key, NoParamOrder protector = default, string regionName = default, bool? shared = default) + { + var l = Log.Fn($"Key: {key} / Segment: {regionName}"); + var keySpec = new CacheKeySpecs(shared == true ? CacheKeySpecs.NoApp : AppId, key, regionName); + var specs = new CacheSpecs(Log, _CodeApiSvc, appStates, appPathsLazy, keySpec, cache.NewPolicyMaker()); + return l.Return(specs); + } + + public bool Contains(ICacheSpecs specs) + => cache.Contains(specs.Key); + + public bool Contains(string key) + => cache.Contains(new CacheKeySpecs(AppId, key).Key); + + public bool Contains(ICacheSpecs specs) + => cache.TryGet(specs.Key, out _); + + public bool Contains(string key) + => cache.TryGet(new CacheKeySpecs(AppId, key).Key, out _); + + public T Get(ICacheSpecs specs, NoParamOrder protector = default, T fallback = default) + => cache.Get(specs.Key, fallback); + + public T Get(string key, NoParamOrder protector = default, T fallback = default) + => cache.Get(new CacheKeySpecs(AppId, key).Key, fallback); + + + private ICacheSpecs ProcessSpecs(string key = default, Func tweak = default) + { + if (key == default) + throw new ArgumentException("key must be set"); + + var specs = CreateSpecs(key); + return tweak == null + ? specs + : tweak(specs); + } + + public T GetOrSet(string key, NoParamOrder protector = default, Func generate = default) + => GetOrSet(ProcessSpecs(key), generate: generate); + + public T GetOrSet(ICacheSpecs specs, NoParamOrder protector = default, Func generate = default) + { + if (cache.TryGet(specs.Key, out T value)) return value; + + if (generate == null) return default; + var newValue = generate(); + + cache.SetNew(specs.Key, newValue, _ => specs.PolicyMaker); + return newValue; + } + + public bool TryGet(ICacheSpecs specs, out T value) + => cache.TryGet(specs.Key, out value); + + public bool TryGet(string key, out T value) + => cache.TryGet(new CacheKeySpecs(AppId, key).Key, out value); + + public object Remove(string key) + => cache.Remove(new CacheKeySpecs(AppId, key).Key); + + public object Remove(ICacheSpecs specs) + => cache.Remove(specs.Key); + + public void Set(string key, T value, NoParamOrder protector = default) + => Set(ProcessSpecs(key), value); + + public void Set(ICacheSpecs specs, T value, NoParamOrder protector = default) + => cache.SetNew(specs.Key, value, _ => specs.PolicyMaker); +} \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Services/Cache/CacheServiceConstants.cs b/Src/Sxc/ToSic.Sxc/Services/Cache/CacheServiceConstants.cs new file mode 100644 index 000000000..8f8726f7f --- /dev/null +++ b/Src/Sxc/ToSic.Sxc/Services/Cache/CacheServiceConstants.cs @@ -0,0 +1,9 @@ +namespace ToSic.Sxc.Services.Cache; + +internal class CacheServiceConstants +{ + internal const string Sep = "|"; + internal const string DefaultPrefix = "Sxc-CacheService"; + internal const string SegmentPrefix = "Seg:"; + internal const string DefaultSegment = "Default"; +} \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Services/Cache/CacheSpecs.cs b/Src/Sxc/ToSic.Sxc/Services/Cache/CacheSpecs.cs new file mode 100644 index 000000000..dbabf6e04 --- /dev/null +++ b/Src/Sxc/ToSic.Sxc/Services/Cache/CacheSpecs.cs @@ -0,0 +1,160 @@ +using System.Collections.Specialized; +using ToSic.Eav.Apps; +using ToSic.Eav.Apps.Integration; +using ToSic.Eav.Apps.State; +using ToSic.Eav.Caching; +using ToSic.Eav.Plumbing; +using ToSic.Lib.DI; +using ToSic.Sxc.Code.Internal; +using ToSic.Sxc.Context; +using ToSic.Sxc.Context.Internal; +using ToSic.Sxc.Web.Internal.Url; +using static System.StringComparer; + +namespace ToSic.Sxc.Services.Cache; + +internal class CacheSpecs(ILog parentLog, ICodeApiService codeApiSvc, LazySvc appStates, Generator appPathsLazy, CacheKeySpecs key, IPolicyMaker policyMaker): ICacheSpecs +{ + #region Keys + + /// + /// Generate the final cache key. + /// This services will always add a prefix to the key, to avoid conflicts with other cache keys. + /// + /// This happens automatically, the Key method is only needed if you want to see a key manually, mainly for debugging purposes. + /// + /// + /// + public string Key => key.Key; + + #endregion + + public IPolicyMaker PolicyMaker => policyMaker; + + #region Next(...) calls, for functional API + + private ICacheSpecs Next(IPolicyMaker newPm) => new CacheSpecs(parentLog, codeApiSvc, appStates, appPathsLazy, key, newPm); + + private ICacheSpecs Next(string varyBy, int value) => Next(varyBy, value.ToString()); + + private ICacheSpecs Next(string varyBy, string value, bool caseSensitive = false) + { + varyBy = "VaryBy" + varyBy; + varyBy = caseSensitive ? varyBy : varyBy.ToLowerInvariant(); + value = caseSensitive ? value : value.ToLowerInvariant(); + + var newDic = new Dictionary(key.VaryByDic ?? [], InvariantCultureIgnoreCase) + { + [varyBy] = value + }; + return new CacheSpecs(parentLog, codeApiSvc, appStates, appPathsLazy, key with { VaryByDic = newDic }, policyMaker); + } + + #endregion + + + #region Time Absolute / Sliding + + public ICacheSpecs SetAbsoluteExpiration(DateTimeOffset absoluteExpiration) + => Next(policyMaker.SetAbsoluteExpiration(absoluteExpiration)); + + public ICacheSpecs SetSlidingExpiration(TimeSpan slidingExpiration) + => Next(policyMaker.SetSlidingExpiration(slidingExpiration)); + + #endregion + + public ICacheSpecs WatchFile(string filePath) + => Next(policyMaker.WatchFiles([filePath])); + + public ICacheSpecs WatchFiles(IEnumerable filePaths) + => Next(policyMaker.WatchFiles([..filePaths])); + + public ICacheSpecs WatchFolder(string folderPath, bool watchSubfolders = false) + => Next(policyMaker.WatchFolders(new Dictionary { { folderPath, watchSubfolders } })); + + public ICacheSpecs WatchFolders(IDictionary folderPaths) + => Next(policyMaker.WatchFolders(folderPaths)); + + public ICacheSpecs WatchCacheKeys(IEnumerable cacheKeys) + => Next(policyMaker.WatchCacheKeys(cacheKeys)); + + + public ICacheSpecs WatchAppData(NoParamOrder protector = default) + => Next(policyMaker.WatchApps([appStates.Value.GetCacheState(codeApiSvc.App.AppId) as IAppStateChanges])); + + public ICacheSpecs WatchAppFolder(NoParamOrder protector = default, bool? withSubfolders = true) + { + var appState = appStates.Value.GetReader(codeApiSvc.App.AppId); + var appPath = appPathsLazy.New().Init(((ICodeApiServiceInternal)codeApiSvc)?._Block?.Context.Site, appState); + var mainPath = appPath.PhysicalPath; + return Next(policyMaker.WatchFolders(new Dictionary { { mainPath, withSubfolders ?? true } })); + } + + + //public ICacheTweak AddAppStates(List appStates) + // => new CacheTweak(policyMaker.AddAppStates(appStates), keyAdditions); + + //public ICacheTweak ConnectFeaturesService(IEavFeaturesService featuresService) + // => new CacheTweak(policyMaker.ConnectFeaturesService(featuresService), keyAdditions); + + //public ICacheTweak AddUpdateCallback(CacheEntryUpdateCallback updateCallback) + // => new CacheTweak(policyMaker.AddUpdateCallback(updateCallback), keyAdditions); + + #region Vary By Value + + //public ICacheSpecs VaryBy(string value, NoParamOrder protector = default, bool caseSensitive = false) + // => Next(value, "", caseSensitive: caseSensitive); + + public ICacheSpecs VaryBy(string name, string value, NoParamOrder protector = default, bool caseSensitive = false) + => Next(name, value, caseSensitive: caseSensitive); + + #endregion + + #region Vary-By Custom User, QueryString, etc. + + + public ICacheSpecs VaryByPageParameter(string name, NoParamOrder protector = default, bool caseSensitive = false) + => Next($"PageParameter-{name}", codeApiSvc?.CmsContext.Page.Parameters[name] ?? "", caseSensitive: caseSensitive); + + public ICacheSpecs VaryByParameters(IParameters parameters, NoParamOrder protector = default, string names = default, bool caseSensitive = false) + => VaryByParamsInternal("Parameters", parameters, names, caseSensitive: caseSensitive); + + public ICacheSpecs VaryByPageParameters(string names = default, NoParamOrder protector = default, bool caseSensitive = false) + => VaryByParamsInternal("PageParameters", codeApiSvc?.CmsContext.Page.Parameters ?? new Parameters(), names, caseSensitive: caseSensitive); + + private ICacheSpecs VaryByParamsInternal(string varyByName, IParameters parameters, string names, bool caseSensitive = false) + { + var all = parameters + .Filter(names) + .OrderBy(p => p.Key, comparer: InvariantCultureIgnoreCase) + .ToList(); + + var nvc = all + .Where(pair => pair.Value.HasValue()) + .Aggregate(new NameValueCollection(), + (seed, pair) => + { + seed.Add(pair.Key, pair.Value); + return seed; + }); + + var asUrl = nvc.NvcToString(); + return Next(varyByName, asUrl, caseSensitive: caseSensitive); + } + + #endregion + + #region VaryBy Page, Module, User + + public ICacheSpecs VaryByPage(NoParamOrder protector = default, ICmsPage page = default, int? id = default) + => Next("Page", id ?? page?.Id ?? codeApiSvc?.CmsContext.Page.Id ?? -1); + + public ICacheSpecs VaryByModule(NoParamOrder protector = default, ICmsModule module = default, int? id = default) + => Next("Module", id ?? module?.Id ?? codeApiSvc?.CmsContext.Module.Id ?? -1); + + public ICacheSpecs VaryByUser(NoParamOrder protector = default, ICmsUser user = default, int? id = default) + => Next("User", id ?? user?.Id ?? codeApiSvc?.CmsContext?.User?.Id ?? -1); + + #endregion + +} \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Services/Cache/ICacheService.cs b/Src/Sxc/ToSic.Sxc/Services/Cache/ICacheService.cs new file mode 100644 index 000000000..3bfd90af2 --- /dev/null +++ b/Src/Sxc/ToSic.Sxc/Services/Cache/ICacheService.cs @@ -0,0 +1,148 @@ +using ToSic.Sxc.Services.Cache; + +// ReSharper disable once CheckNamespace +namespace ToSic.Sxc.Services; + +/// +/// Experimental! +/// Cache service to help your code cache data. +/// +/// It does quite a bit of magic, for example: +/// +/// - scope the cache to the current App, so a key "data" will not bleed to other apps +/// - change the key when the data is for the current user only +/// - ...and more. +/// +/// It's not yet fully documented. +/// +/// +/// Basic use will just use a string key. The internal key will be more complex, +/// while advanced use would first create the specs using and then use those specs for all operations. +/// +[InternalApi_DoNotUse_MayChangeWithoutNotice("WIP v17.09")] +[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] +public interface ICacheService +{ + /// + /// Create cache specs for a specific key and optional segment. + /// + /// This is used for complex setups where the same specs will be reused for multiple operations. + /// + /// The main cache key (name) to use. It will be extended internally, to prevent collisions, so it can be fairly short. + /// see [](xref:NetCode.Conventions.NamedParameters) + /// a cache region to segment the cache into multiple regions + /// + /// If set to `true` it will make this key available on other apps which access the data with allApps = `true`. + /// By default, each app has its own region, preventing key collisions between apps. + /// + /// + ICacheSpecs CreateSpecs(string key, NoParamOrder protector = default, string regionName = default, bool? shared = default); + + /// + /// Check if the cache contains data for the given specs. + /// + bool Contains(ICacheSpecs specs); + + /// + /// Check if the cache contains data for the given key. + /// + bool Contains(string key); + + /// + /// Check if the cache contains data of specified type for the given specs. + /// + bool Contains(ICacheSpecs specs); + + /// + /// Check if the cache contains data of specified type for the given key. + /// + bool Contains(string key); + + /// + /// Get data from the cache of the given type for the given specs, with optional fallback. + /// + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// + T Get(ICacheSpecs specs, NoParamOrder protector = default, T fallback = default); + + /// + /// Get data from the cache of the given type for the given key, with optional fallback. + /// + /// + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// + /// + T Get(string key, NoParamOrder protector = default, T fallback = default); + + /// + /// Get or set data in the cache for the given key, with optional generation and specs-tweaking. + /// + /// + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// + /// + T GetOrSet(string key, NoParamOrder protector = default, Func generate = default); + + /// + /// Get or set data in the cache for the given specs, with optional generation. + /// + /// + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// + /// + T GetOrSet(ICacheSpecs specs, NoParamOrder protector = default, Func generate = default); + + /// + /// Try to get data of the specified type from the cache for the given specs. + /// + /// + /// + /// + /// `true` if found, `false` if not found + bool TryGet(ICacheSpecs specs, out T value); + + /// + /// Try to get data of the specified type from the cache for the given key. + /// + /// + /// + /// + /// `true` if found, `false` if not found + bool TryGet(string key, out T value); + + /// + /// Remove a cache entry. + /// + /// + /// The object if it was in the cache, otherwise null. + object Remove(string key); + + /// + /// Remove a cache entry. + /// + /// + /// The object if it was in the cache, otherwise null. + object Remove(ICacheSpecs key); + + /// + /// Set a value in the cache. + /// + /// + /// + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + void Set(string key, T value, NoParamOrder protector = default); + + /// + /// Set a value in the cache. + /// + /// + /// + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + void Set(ICacheSpecs specs, T value, NoParamOrder protector = default); +} \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Services/Cache/ICacheSpecs.cs b/Src/Sxc/ToSic.Sxc/Services/Cache/ICacheSpecs.cs new file mode 100644 index 000000000..63d076917 --- /dev/null +++ b/Src/Sxc/ToSic.Sxc/Services/Cache/ICacheSpecs.cs @@ -0,0 +1,141 @@ +using ToSic.Eav.Caching; +using ToSic.Sxc.Context; + +namespace ToSic.Sxc.Services.Cache; + +/// +/// Experimental! +/// Cache Specs which determine how the key is generated, additional dependencies and expiration policies. +/// +[InternalApi_DoNotUse_MayChangeWithoutNotice("WIP v17.09")] +[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] +public interface ICacheSpecs +{ + [PrivateApi] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + string Key { get; } + + [PrivateApi] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public IPolicyMaker PolicyMaker { get; } + + /// + /// Set absolute expiration, alternative is sliding expiration. + /// If neither are set, a sliding expiration of 1 hour will be used. + /// + /// + /// + /// + /// If neither absolute nor sliding are set, a sliding expiration of 1 hour will be used. + /// Setting both is invalid and will throw an exception. + /// + ICacheSpecs SetAbsoluteExpiration(DateTimeOffset absoluteExpiration); + + /// + /// Set sliding expiration, alternative is absolute expiration. + /// + /// + /// + /// + /// If neither absolute nor sliding are set, a sliding expiration of 1 hour will be used. + /// Setting both is invalid and will throw an exception. + /// + ICacheSpecs SetSlidingExpiration(TimeSpan slidingExpiration); + + /// + /// Depend on the app data, so if any data changes, the cache will be invalidated. + /// + /// + ICacheSpecs WatchAppData(NoParamOrder protector = default); + + /// + /// Depend on the app folder, so if any file in the app folder changes, the cache will be invalidated. WIP! + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// should it also watch subfolders? default is `true` + /// + ICacheSpecs WatchAppFolder(NoParamOrder protector = default, bool? withSubfolders = default); + + ///// + ///// Add files + ///// + ///// + ///// + //ICacheSpecs WatchFiles(IEnumerable filePaths); + + #region VaryBy + + /// + /// Vary the cache by a specific name and value. + /// All cache items where this value is the same, will be considered the same. + /// For example, this could be a category name or something where the data for this category is always the same. + /// + /// + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// + /// + ICacheSpecs VaryBy(string name, string value, NoParamOrder protector = default, bool caseSensitive = false); + + /// + /// Vary the cache by page, so that each page has its own cache. + /// By default, it will take the current page, but you can optionally specify a custom page or ID. + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// optional page id to use instead of the default + /// optional page object to use instead of the default + /// + ICacheSpecs VaryByPage(NoParamOrder protector = default, ICmsPage page = default, int? id = default); + + /// + /// Vary the cache by module, so that each module has its own cache. + /// By default, it will take the current module, but you can optionally specify a custom module or ID. + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// optional page id to use instead of the default + /// optional module object to use instead of the default + ICacheSpecs VaryByModule(NoParamOrder protector = default, ICmsModule module = default, int? id = default); + + /// + /// Vary the cache by user, so that each user has its own cache. + /// By default, it will take the current user, but you can specify a custom user or ID. + /// + /// see [](xref:NetCode.Conventions.NamedParameters) + /// optional page id to use instead of the default + /// optional module object to use instead of the default + /// + ICacheSpecs VaryByUser(NoParamOrder protector = default, ICmsUser user = default, int? id = default); + + /// + /// Vary the cache by a specific page parameter, eg `?category=1`. + /// Using this method will only vary the cache by this specific parameter and ignore the rest. + /// + /// Name of a single parameter + /// see [](xref:NetCode.Conventions.NamedParameters) + /// Determines if the value should be treated case-sensitive, default is `false` + /// + ICacheSpecs VaryByPageParameter(string name, NoParamOrder protector = default, bool caseSensitive = false); + + /// + /// Vary the cache by one or more specific page parameter, eg `?category=1&sort=asc`. + /// Using this method will only vary the cache by the mentioned parameters and ignore the rest. + /// + /// Names of one or more parameters, comma-separated + /// see [](xref:NetCode.Conventions.NamedParameters) + /// Determines if the value should be treated case-sensitive, default is `false` + /// + ICacheSpecs VaryByPageParameters(string names = default, NoParamOrder protector = default, bool caseSensitive = false); + + /// + /// Vary the cache by parameters which may not come from the page. + /// + /// parameters object + /// see [](xref:NetCode.Conventions.NamedParameters) + /// Names of one or more parameters, comma-separated + /// Determines if the value should be treated case-sensitive, default is `false` + /// + ICacheSpecs VaryByParameters(IParameters parameters, NoParamOrder protector = default, string names = default, bool caseSensitive = false); + + #endregion + +} \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Services/ServiceKits/ServiceKit16.cs b/Src/Sxc/ToSic.Sxc/Services/ServiceKits/ServiceKit16.cs index dea488832..4a9242144 100644 --- a/Src/Sxc/ToSic.Sxc/Services/ServiceKits/ServiceKit16.cs +++ b/Src/Sxc/ToSic.Sxc/Services/ServiceKits/ServiceKit16.cs @@ -125,4 +125,9 @@ public class ServiceKit16() : ServiceKit("Sxc.Kit16") [InternalApi_DoNotUse_MayChangeWithoutNotice("Still Beta in v17.08")] public ITemplateService Template => _templates ??= GetKitService(); private ITemplateService _templates; + + [InternalApi_DoNotUse_MayChangeWithoutNotice("Still Beta in v17.09")] + public ICacheService Cache => _cache ??= GetKitService(); + private ICacheService _cache; + } \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Templates/ITemplateEngine.cs b/Src/Sxc/ToSic.Sxc/Services/Template/ITemplateEngine.cs similarity index 96% rename from Src/Sxc/ToSic.Sxc/Templates/ITemplateEngine.cs rename to Src/Sxc/ToSic.Sxc/Services/Template/ITemplateEngine.cs index a28b36c63..c72880a4d 100644 --- a/Src/Sxc/ToSic.Sxc/Templates/ITemplateEngine.cs +++ b/Src/Sxc/ToSic.Sxc/Services/Template/ITemplateEngine.cs @@ -1,6 +1,6 @@ using ToSic.Eav.LookUp; -namespace ToSic.Sxc.Templates; +namespace ToSic.Sxc.Services.Template; /// /// Engine which parses a template containing placeholders and replaces them with values from sources. diff --git a/Src/Sxc/ToSic.Sxc/Services/ITemplateService.cs b/Src/Sxc/ToSic.Sxc/Services/Template/ITemplateService.cs similarity index 91% rename from Src/Sxc/ToSic.Sxc/Services/ITemplateService.cs rename to Src/Sxc/ToSic.Sxc/Services/Template/ITemplateService.cs index 684cd1ec6..293ff599d 100644 --- a/Src/Sxc/ToSic.Sxc/Services/ITemplateService.cs +++ b/Src/Sxc/ToSic.Sxc/Services/Template/ITemplateService.cs @@ -1,7 +1,8 @@ using ToSic.Eav.LookUp; using ToSic.Sxc.Data; -using ToSic.Sxc.Templates; +using ToSic.Sxc.Services.Template; +// ReSharper disable once CheckNamespace namespace ToSic.Sxc.Services; /// @@ -92,4 +93,16 @@ public interface ITemplateService /// /// string Parse(string template, NoParamOrder protector = default, IEnumerable sources = default); + + + /// + /// Merge multiple sources into one. + /// + /// + /// + /// + /// + /// Added v17.09 + /// + ILookUp MergeSources(string name, IEnumerable sources); } \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Templates/LookUpWithFunction.cs b/Src/Sxc/ToSic.Sxc/Services/Template/LookUpWithFunction.cs similarity index 90% rename from Src/Sxc/ToSic.Sxc/Templates/LookUpWithFunction.cs rename to Src/Sxc/ToSic.Sxc/Services/Template/LookUpWithFunction.cs index e78d44282..601cffcac 100644 --- a/Src/Sxc/ToSic.Sxc/Templates/LookUpWithFunction.cs +++ b/Src/Sxc/ToSic.Sxc/Services/Template/LookUpWithFunction.cs @@ -1,7 +1,7 @@ using ToSic.Eav.LookUp; using ToSic.Eav.Plumbing; -namespace ToSic.Sxc.Templates; +namespace ToSic.Sxc.Services.Templates; internal class LookUpWithFunction(string name, Func getter): LookUpBase(name, "LookUp using simple function") { diff --git a/Src/Sxc/ToSic.Sxc/Templates/LookUpWithFunctionAndFormat.cs b/Src/Sxc/ToSic.Sxc/Services/Template/LookUpWithFunctionAndFormat.cs similarity index 87% rename from Src/Sxc/ToSic.Sxc/Templates/LookUpWithFunctionAndFormat.cs rename to Src/Sxc/ToSic.Sxc/Services/Template/LookUpWithFunctionAndFormat.cs index 1f2541fde..7d7048c5d 100644 --- a/Src/Sxc/ToSic.Sxc/Templates/LookUpWithFunctionAndFormat.cs +++ b/Src/Sxc/ToSic.Sxc/Services/Template/LookUpWithFunctionAndFormat.cs @@ -1,6 +1,6 @@ using ToSic.Eav.LookUp; -namespace ToSic.Sxc.Templates; +namespace ToSic.Sxc.Services.Templates; internal class LookUpWithFunctionAndFormat(string name, Func getter): LookUpBase(name, "LookUp using Function which also handles format") { diff --git a/Src/Sxc/ToSic.Sxc/Templates/TemplateEngineTokens.cs b/Src/Sxc/ToSic.Sxc/Services/Template/TemplateEngineTokens.cs similarity index 90% rename from Src/Sxc/ToSic.Sxc/Templates/TemplateEngineTokens.cs rename to Src/Sxc/ToSic.Sxc/Services/Template/TemplateEngineTokens.cs index 521565f52..8fa4e8a40 100644 --- a/Src/Sxc/ToSic.Sxc/Templates/TemplateEngineTokens.cs +++ b/Src/Sxc/ToSic.Sxc/Services/Template/TemplateEngineTokens.cs @@ -1,7 +1,8 @@ using ToSic.Eav.LookUp; using ToSic.Lib.Data; +using ToSic.Sxc.Services.Template; -namespace ToSic.Sxc.Templates; +namespace ToSic.Sxc.Services.Templates; internal class TemplateEngineTokens(ILookUpEngine original): ITemplateEngine, IWrapper { @@ -21,12 +22,12 @@ IEnumerable ITemplateEngine.GetSources(NoParamOrder protector = default // loop through depth to get all underlying sources var current = original; - var sources = current.Sources; //.ToList(); + var sources = current.Sources; for (var i = 0; i < depth; i++) { if (current.Downstream == null) break; current = current.Downstream; - sources = sources.Concat(current.Sources); //.ToList(); + sources = sources.Concat(current.Sources); } return sources.ToList(); } diff --git a/Src/Sxc/ToSic.Sxc/Templates/TemplateService.cs b/Src/Sxc/ToSic.Sxc/Services/Template/TemplateService.cs similarity index 85% rename from Src/Sxc/ToSic.Sxc/Templates/TemplateService.cs rename to Src/Sxc/ToSic.Sxc/Services/Template/TemplateService.cs index f09fdc1c1..7cb976064 100644 --- a/Src/Sxc/ToSic.Sxc/Templates/TemplateService.cs +++ b/Src/Sxc/ToSic.Sxc/Services/Template/TemplateService.cs @@ -1,10 +1,10 @@ using ToSic.Eav.LookUp; using ToSic.Eav.Plumbing; using ToSic.Lib.DI; -using ToSic.Sxc.Services; using ToSic.Sxc.Services.Internal; +using ToSic.Sxc.Services.Template; -namespace ToSic.Sxc.Templates; +namespace ToSic.Sxc.Services.Templates; internal class TemplateService(LazySvc getEngineLazy) : ServiceForDynamicCode($"{SxcLogName}.LUpSvc"), ITemplateService { @@ -71,7 +71,7 @@ public ILookUp CreateSource(string name, IDictionary values) => new LookUpInDictionary(NameOrErrorIfBad(name), values); public ILookUp CreateSource(string name, ILookUp original) - => new LookUpInLookUps(NameOrErrorIfBad(name), original); + => new LookUpInLookUps(NameOrErrorIfBad(name), [original]); public ILookUp CreateSource(string name, Func getter) => new LookUpWithFunction(NameOrErrorIfBad(name), getter); @@ -82,6 +82,17 @@ public ILookUp CreateSource(string name, Func getter) public ILookUp CreateSource(string name, ICanBeEntity item, NoParamOrder protector = default, string[] dimensions = default) => new LookUpInEntity(name, item.Entity, dimensions: dimensions ?? _CodeApiSvc.Cdf.Dimensions); + public ILookUp MergeSources(string name, IEnumerable sources) + { + var sourceList = sources?.ToList() + ?? throw new ArgumentNullException(nameof(sources), @"Sources must not be null"); ; + + if (!sourceList.Any()) + throw new ArgumentException(@"Sources must not be empty", nameof(sources)); + + return new LookUpInLookUps(name, sourceList); + } + private string NameOrErrorIfBad(string name) => string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Name must not be empty", nameof(name)) diff --git a/Src/Sxc/ToSic.Sxc/Startup/RegisterSxcServices_ServicesAndKits.cs b/Src/Sxc/ToSic.Sxc/Startup/RegisterSxcServices_ServicesAndKits.cs index 14c0676cd..d6f0e22e9 100644 --- a/Src/Sxc/ToSic.Sxc/Startup/RegisterSxcServices_ServicesAndKits.cs +++ b/Src/Sxc/ToSic.Sxc/Startup/RegisterSxcServices_ServicesAndKits.cs @@ -9,10 +9,11 @@ using ToSic.Sxc.Edit.Toolbar.Internal; using ToSic.Sxc.Images; using ToSic.Sxc.Services; +using ToSic.Sxc.Services.Cache; using ToSic.Sxc.Services.CmsService; using ToSic.Sxc.Services.DataServices; using ToSic.Sxc.Services.Internal; -using ToSic.Sxc.Templates; +using ToSic.Sxc.Services.Templates; using ToSic.Sxc.Web.Internal.ContentSecurityPolicy; using ToSic.Sxc.Web.Internal.PageService; using CodeDataFactory = ToSic.Sxc.Data.Internal.CodeDataFactory; @@ -96,6 +97,9 @@ public static IServiceCollection AddServicesAndKits(this IServiceCollection serv // Lookup Service - WIP v17 services.TryAddTransient(); + // Cache Service - WIP v17 + services.TryAddTransient(); + return services; } } \ No newline at end of file diff --git a/Src/Sxc/ToSic.Sxc/Web/Internal/DotNet/HttpNetFramework.cs b/Src/Sxc/ToSic.Sxc/Web/Internal/DotNet/HttpNetFramework.cs index 03d0b2ec1..944236371 100644 --- a/Src/Sxc/ToSic.Sxc/Web/Internal/DotNet/HttpNetFramework.cs +++ b/Src/Sxc/ToSic.Sxc/Web/Internal/DotNet/HttpNetFramework.cs @@ -14,16 +14,7 @@ public class HttpNetFramework: HttpAbstractionBase, IHttp #region Request and properties thereof - public override NameValueCollection QueryStringParams - { - get - { - if (_queryStringValues != null) return _queryStringValues; - return _queryStringValues = Request == null - ? [] - : Request.QueryString; - } - } + public override NameValueCollection QueryStringParams => _queryStringValues ??= Request?.QueryString ?? []; private NameValueCollection _queryStringValues; #endregion Request diff --git a/Src/Sxc/ToSic.Sxc/Web/Internal/HybridHtmlString/Url/UrlHelpers.cs b/Src/Sxc/ToSic.Sxc/Web/Internal/HybridHtmlString/Url/UrlHelpers.cs index 4a6a94e8b..31ac0c723 100644 --- a/Src/Sxc/ToSic.Sxc/Web/Internal/HybridHtmlString/Url/UrlHelpers.cs +++ b/Src/Sxc/ToSic.Sxc/Web/Internal/HybridHtmlString/Url/UrlHelpers.cs @@ -18,15 +18,17 @@ public static NameValueCollection ParseQueryString(string query) // Note that this NameValueCollection is different than the one from HttpUtility // but because that one is internal, we cannot create one directly var nvc = new NameValueCollection(); - query = (query ?? "").Trim(); - if (string.IsNullOrWhiteSpace(query)) return nvc; + query = query?.Trim() ?? ""; + if (string.IsNullOrWhiteSpace(query)) + return nvc; // remove anything other than query string from url - if (query.StartsWith("?")) query = query.Substring(1); + if (query.StartsWith("?")) + query = query.Substring(1); foreach (var vp in query.Split('&')) { - if(string.IsNullOrWhiteSpace(vp)) continue; + if (string.IsNullOrWhiteSpace(vp)) continue; var singlePair = vp.Split('='); nvc.Add(singlePair[0], singlePair.Length == 2 ? singlePair[1] : string.Empty); @@ -49,7 +51,6 @@ private class KeyValuePairTemp { public string Key; public string Value; - //public string[] AllValues; } internal static string NvcToString(NameValueCollection nvc, string keyValueSeparator, string pairSeparator, @@ -70,17 +71,17 @@ internal static string NvcToString(NameValueCollection nvc, string keyValueSepar // Key null; 2 options left, values or no values if (key is null) return noValues - ? Array.Empty() + ? [] // If no key, treat the values as standalone keys : values.Select(v => new KeyValuePairTemp { Key = v, Value = null }).ToArray(); // Key not null, no values - if (noValues) return new[] { new KeyValuePairTemp { Key = key, Value = null } }; + if (noValues) return [new() { Key = key, Value = null }]; // Key null, values - two options - give as array or single item return repeatKeyForEachValue ? values.Select(v => new KeyValuePairTemp { Key = key, Value = v.ToString() }) - : new[] { new KeyValuePairTemp { Key = key, Value = string.Join(valueSeparator, values) } }; + : [new() { Key = key, Value = string.Join(valueSeparator, values) }]; }) .Select(set => set.Key + (string.IsNullOrEmpty(set.Value) ? "" : keyValueSeparator + set.Value)) //.SelectMany(set => @@ -106,7 +107,10 @@ internal static string NvcToString(NameValueCollection nvc, string keyValueSepar // return new[] { key + (values.All(string.IsNullOrEmpty) ? "" : keyValueSeparator + string.Join(valueSeparator, values)) }; //}) .ToArray(); - return allPairs.Any() ? string.Join(pairSeparator, allPairs) + terminator : empty; + + return allPairs.Any() + ? string.Join(pairSeparator, allPairs) + terminator + : empty; } @@ -140,7 +144,7 @@ public static string QuickAddUrlParameter(string url, string name, string value) => $"{url}{(url.IndexOf('?') > 0 ? '&' : '?')}{name}={value}"; - public static string AddQueryString(string url, string newParams) => AddQueryString(url, UrlHelpers.ParseQueryString(newParams)); + public static string AddQueryString(string url, string newParams) => AddQueryString(url, ParseQueryString(newParams)); public static string AddQueryString(string url, NameValueCollection newParams) { @@ -151,7 +155,7 @@ public static string AddQueryString(string url, NameValueCollection newParams) var parts = new UrlParts(url); // if the url already has some params we should take that and split it into it's pieces - var queryParams = UrlHelpers.ParseQueryString(parts.Query); + var queryParams = ParseQueryString(parts.Query); // new params would replace existing queryString params or append new param to queryString var finalParams = queryParams.Merge(newParams); @@ -165,7 +169,7 @@ private static string GetUrlWithUpdatedQueryString(UrlParts parts, NameValueColl { var newUrl = parts.ToLink(suffix: false); if (queryString.Count > 0) - newUrl += UrlParts.QuerySeparator + UrlHelpers.NvcToString(queryString); + newUrl += UrlParts.QuerySeparator + NvcToString(queryString); if (!string.IsNullOrWhiteSpace(parts.Fragment)) newUrl += UrlParts.FragmentSeparator + parts.Fragment; diff --git a/Src/Sxc/ToSic.Sxc/Web/Internal/LightSpeed/LightSpeed.cs b/Src/Sxc/ToSic.Sxc/Web/Internal/LightSpeed/LightSpeed.cs index 7ddfe481e..abfc3e245 100644 --- a/Src/Sxc/ToSic.Sxc/Web/Internal/LightSpeed/LightSpeed.cs +++ b/Src/Sxc/ToSic.Sxc/Web/Internal/LightSpeed/LightSpeed.cs @@ -19,7 +19,7 @@ namespace ToSic.Sxc.Web.Internal.LightSpeed; internal class LightSpeed( IEavFeaturesService features, LazySvc appStatesLazy, - LazySvc appPathsLazy, + Generator appPathsLazy, LazySvc cmsContext, LazySvc outputCacheManager ) : ServiceBase(SxcLogName + ".Lights", connect: [features, appStatesLazy, appPathsLazy, cmsContext, outputCacheManager]), IOutputCache @@ -144,7 +144,7 @@ private IList AppPaths(List dependentApps) var paths = new List(); foreach (var appState in dependentApps) { - var appPaths = appPathsLazy.Value.Init(app.Site, appStatesLazy.Value.ToReader(appState, Log)); + var appPaths = appPathsLazy.New().Init(app.Site, appStatesLazy.Value.ToReader(appState, Log)); if (Directory.Exists(appPaths.PhysicalPath)) paths.Add(appPaths.PhysicalPath); if (Directory.Exists(appPaths.PhysicalPathShared)) paths.Add(appPaths.PhysicalPathShared); } @@ -186,7 +186,7 @@ private string GetSuffix() private readonly GetOnce _userId = new(); // Note 2023-10-30 2dm changed the handling of the preview template and checks if it's set. In case caching is too aggressive this can be the problem. Remove early 2024 - private string ViewKey => _viewKey.Get(() => _block.Configuration?.PreviewTemplate != null ? $"{_block.Configuration.AppId}:{_block.Configuration.View?.Id}" : null); + private string ViewKey => _viewKey.Get(() => _block.Configuration?.PreviewViewEntity != null ? $"{_block.Configuration.AppId}:{_block.Configuration.View?.Id}" : null); private readonly GetOnce _viewKey = new(); public OutputCacheItem Existing => _existing.Get(ExistingGenerator); diff --git a/Src/Sxc/ToSic.Sxc/Web/Internal/LightSpeed/OutputCacheManager.cs b/Src/Sxc/ToSic.Sxc/Web/Internal/LightSpeed/OutputCacheManager.cs index a349fe4ba..2f98e247b 100644 --- a/Src/Sxc/ToSic.Sxc/Web/Internal/LightSpeed/OutputCacheManager.cs +++ b/Src/Sxc/ToSic.Sxc/Web/Internal/LightSpeed/OutputCacheManager.cs @@ -45,5 +45,5 @@ public string Add(string cacheKey, OutputCacheItem data, int duration, List memoryCacheService.Get(key) as OutputCacheItem; + public OutputCacheItem Get(string key) => memoryCacheService.Get(key); } \ No newline at end of file