diff --git a/PxWeb.UnitTests/Data/VariablePlacementTests.cs b/PxWeb.UnitTests/Data/VariablePlacementTests.cs new file mode 100644 index 00000000..45652980 --- /dev/null +++ b/PxWeb.UnitTests/Data/VariablePlacementTests.cs @@ -0,0 +1,319 @@ +namespace PxWeb.UnitTests.Data +{ + + [TestClass] + public class VariablePlacementTests + { + [TestMethod] + public void ShouldReturnNoPreferredPlacementIfPlacemntIsNull() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + variablesSelection.Palcement = null; + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNull(problem); + Assert.IsNull(placement); + + } + + [TestMethod] + public void ShouldReturnNoPreferredPlacementIfPlacemntIsNull2() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + variablesSelection.Palcement = new VariablePlacementType(); + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNull(problem); + Assert.IsNull(placement); + + } + + [TestMethod] + public void ShouldReturnNoPreferredPlacementIfPlacemntIsEmpty() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + + variablesSelection.Palcement = new VariablePlacementType(); + variablesSelection.Palcement.Heading = new List(); + variablesSelection.Palcement.Stub = new List(); + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNull(problem); + Assert.IsNull(placement); + + } + + [TestMethod] + public void ShouldReturnNoPreferredPlacementIfPlacemntVariableDoesNotExist() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + + variablesSelection.Palcement = new VariablePlacementType(); + variablesSelection.Palcement.Heading = new List(); + variablesSelection.Palcement.Heading.Add("Age"); + variablesSelection.Palcement.Stub = new List(); + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNotNull(problem); + Assert.IsNull(placement); + } + + [TestMethod] + public void ShouldReturnNoPreferredPlacementIfPlacemntVariableIsDouplicated() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + + variablesSelection.Palcement = new VariablePlacementType(); + variablesSelection.Palcement.Heading = new List(); + variablesSelection.Palcement.Heading.Add("PointOfTime"); + variablesSelection.Palcement.Heading.Add("MEASURE"); + variablesSelection.Palcement.Stub = new List(); + variablesSelection.Palcement.Stub.Add("PointOfTime"); + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNotNull(problem); + Assert.IsNull(placement); + } + + [TestMethod] + public void ShouldNotReturnPreferredPlacementWhenUsingTime() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + + variablesSelection.Palcement = new VariablePlacementType(); + variablesSelection.Palcement.Heading = new List(); + variablesSelection.Palcement.Heading.Add("TIME"); + variablesSelection.Palcement.Heading.Add("MEASURE"); + variablesSelection.Palcement.Stub = new List(); + variablesSelection.Palcement.Stub.Add("PointOfTime"); + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNotNull(problem); + Assert.IsNull(placement); + } + + [TestMethod] + public void ShouldNotReturnPreferredPlacementWhenSpecifyingTooManyVariables() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + + variablesSelection.Palcement = new VariablePlacementType(); + variablesSelection.Palcement.Heading = new List(); + variablesSelection.Palcement.Heading.Add("TIME"); + variablesSelection.Palcement.Heading.Add("MEASURE"); + variablesSelection.Palcement.Stub = new List(); + variablesSelection.Palcement.Stub.Add("Measure"); + variablesSelection.Palcement.Stub.Add("X"); + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNotNull(problem); + Assert.IsNull(placement); + } + + + [TestMethod] + public void ShouldReturnPreferredPlacementWhenUsingTime() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + + variablesSelection.Palcement = new VariablePlacementType(); + variablesSelection.Palcement.Heading = new List(); + variablesSelection.Palcement.Heading.Add("TIME"); + variablesSelection.Palcement.Heading.Add("MEASURE"); + variablesSelection.Palcement.Stub = new List(); + variablesSelection.Palcement.Stub.Add("GENDER"); + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNull(problem); + Assert.IsNotNull(placement); + Assert.AreEqual(2, placement.Heading.Count); + } + + [TestMethod] + public void ShouldReturnPreferredPlacementWhenOnlySpecifyingHeading() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + + variablesSelection.Palcement = new VariablePlacementType(); + variablesSelection.Palcement.Heading = new List(); + variablesSelection.Palcement.Heading.Add("MEASURE"); + variablesSelection.Palcement.Stub = new List(); + Selection[]? selection = GetSelectionForAllVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNull(problem); + Assert.IsNotNull(placement); + Assert.AreEqual(2, placement.Stub.Count); + } + + [TestMethod] + public void ShouldReturnPreferredPlacementWhenOnlySpecifyingHeadingWithElimination() + { + // Arrange + IPlacementHandler placementHandler = new PlacementHandler(); + VariablesSelection variablesSelection = new VariablesSelection(); + + variablesSelection.Palcement = new VariablePlacementType(); + variablesSelection.Palcement.Heading = new List(); + variablesSelection.Palcement.Heading.Add("MEASURE"); + variablesSelection.Palcement.Stub = new List(); + Selection[]? selection = GetSelectionForMandantoryVariables(); + PXMeta meta = ModelStore.CreateModelA().Meta; + + Problem? problem; + + // Act + var placement = placementHandler.GetPlacment(variablesSelection, + selection, + meta, + out problem); + + // Assert + Assert.IsNull(problem); + Assert.IsNotNull(placement); + Assert.AreEqual(1, placement.Stub.Count); + } + + private static Selection[] GetSelectionForAllVariables() + { + var selections = new List(); + var selection = new Selection("PointOfTime"); + selection.ValueCodes.Add("2000"); + selection.ValueCodes.Add("2001"); + selection.ValueCodes.Add("2002"); + selections.Add(selection); + selection = new Selection("Measure"); + selection.ValueCodes.Add("M1"); + selections.Add(selection); + selection = new Selection("GENDER"); + selection.ValueCodes.Add("M"); + selections.Add(selection); + return selections.ToArray(); + + } + + + private static Selection[] GetSelectionForMandantoryVariables() + { + var selections = new List(); + var selection = new Selection("PointOfTime"); + selection.ValueCodes.Add("2000"); + selection.ValueCodes.Add("2001"); + selection.ValueCodes.Add("2002"); + selections.Add(selection); + selection = new Selection("Measure"); + selection.ValueCodes.Add("M1"); + selections.Add(selection); + selection = new Selection("GENDER"); + selections.Add(selection); + return selections.ToArray(); + } + + + } +} diff --git a/PxWeb.UnitTests/ModelStore.cs b/PxWeb.UnitTests/ModelStore.cs index 71b08b5c..71bd70a3 100644 --- a/PxWeb.UnitTests/ModelStore.cs +++ b/PxWeb.UnitTests/ModelStore.cs @@ -2,6 +2,49 @@ { internal static class ModelStore { + + public static PXModel CreateModelA() + { + + PXModel model = new PXModel(); + PXMeta meta = new PXMeta(); + + // Create time variable + var name = "PointOfTime"; + Variable variable = new Variable(name, PlacementType.Heading); + + for (int i = 1968; i < 2025; i++) + { + variable.Values.Add(CreateValue($"{i}")); + } + variable.TimeValue = $"TLIST(A, \"1968\"-\"2025\")"; + variable.IsTime = true; + meta.AddVariable(variable); + + // Create content variable + name = $"MEASURE"; + variable = new Variable(name, PlacementType.Heading); + variable.Values.Add(CreateValue($"M1")); + variable.Values.Add(CreateValue($"M2")); + variable.Elimination = false; + variable.IsContentVariable = true; + meta.Variables.Add(variable); + + //Create classification variable gender + + name = "GENDER"; + + variable = new Variable(name, PlacementType.Stub); + variable.Values.Add(CreateValue($"M")); + variable.Values.Add(CreateValue($"F")); + variable.Elimination = true; + meta.Variables.Add(variable); + + model.Meta = meta; + return model; + } + + public static PXModel GetModelWithOnlyOneVariable(int numberOfValues) { diff --git a/PxWeb/Code/Api2/DataSelection/IPlacementHandler.cs b/PxWeb/Code/Api2/DataSelection/IPlacementHandler.cs new file mode 100644 index 00000000..dc27b235 --- /dev/null +++ b/PxWeb/Code/Api2/DataSelection/IPlacementHandler.cs @@ -0,0 +1,11 @@ +using PCAxis.Paxiom; + +using PxWeb.Api2.Server.Models; + +namespace PxWeb.Code.Api2.DataSelection +{ + public interface IPlacementHandler + { + VariablePlacementType? GetPlacment(VariablesSelection variablesSelection, Selection[] selection, PXMeta meta, out Problem? problem); + } +} diff --git a/PxWeb/Code/Api2/DataSelection/PlacementHandler.cs b/PxWeb/Code/Api2/DataSelection/PlacementHandler.cs new file mode 100644 index 00000000..7f6ca768 --- /dev/null +++ b/PxWeb/Code/Api2/DataSelection/PlacementHandler.cs @@ -0,0 +1,114 @@ +using System.Linq; + +using PCAxis.Paxiom; + +using PxWeb.Api2.Server.Models; +using PxWeb.Helper.Api2; + +namespace PxWeb.Code.Api2.DataSelection +{ + public class PlacementHandler : IPlacementHandler + { + VariablePlacementType? IPlacementHandler.GetPlacment(VariablesSelection variablesSelection, Selection[] selection, PXMeta meta, out Problem? problem) + { + var p = variablesSelection.Palcement; + + //No placement is specified + if (p is null) + { + problem = null; + return null; + } + + //If not placement is specified, return null + if (p.Heading is null || p.Stub is null) + { + problem = null; + return null; + } + + //If not placement is specified, return null + if (p.Heading.Count == 0 && p.Stub.Count == 0) + { + problem = null; + return null; + } + + //Replace the text TIME with tid in list + ReplaceTimeConstant(meta, p); + + var selectedVariablesCode = selection.Where(x => x.ValueCodes.Count > 0).Select(x => x.VariableCode).ToList(); + + //Check if all variables are in the model + if (!AllVariablesExistsInSelection(p, selectedVariablesCode)) + { + problem = ProblemUtility.IllegalPlacementSelection(); + return null; + } + + + //Check if all variables have a placement + if (p.Heading.Count + p.Stub.Count == selectedVariablesCode.Count) + { + //Check for duplicates + if (p.Heading.Intersect(p.Stub).Any()) + { + problem = ProblemUtility.IllegalPlacementSelection(); + return null; + } + + problem = null; + return p; + } + + //Check if user have specified stub or heading + if (OnlyHeadOrStubIsSpecified(p)) + { + + List usedVariables = new List(); + usedVariables.AddRange(p.Heading); + usedVariables.AddRange(p.Stub); + var unusedVariables = selectedVariablesCode.Except(usedVariables, StringComparer.OrdinalIgnoreCase).ToList(); + + if (p.Heading.Count == 0) + { + problem = null; + return new VariablePlacementType() { Heading = unusedVariables, Stub = p.Stub }; + } + else + { + problem = null; + return new VariablePlacementType() { Heading = p.Heading, Stub = unusedVariables }; + } + } + + problem = ProblemUtility.IllegalPlacementSelection(); + return null; + + } + + private static bool OnlyHeadOrStubIsSpecified(VariablePlacementType p) + { + return (p.Heading.Count > 0 && p.Stub.Count == 0) || + (p.Heading.Count == 0 && p.Stub.Count > 0); + } + + private static bool AllVariablesExistsInSelection(VariablePlacementType p, List selectedVariablesCode) + { + return (p.Stub.TrueForAll(variableCode => selectedVariablesCode.Exists(code => + code.Equals(variableCode, StringComparison.OrdinalIgnoreCase))) && + p.Heading.TrueForAll(variableCode => selectedVariablesCode.Exists(code => + code.Equals(variableCode, StringComparison.OrdinalIgnoreCase)))); + } + + private static void ReplaceTimeConstant(PXMeta meta, VariablePlacementType p) + { + var time = meta.Variables.Find(x => x.IsTime); + if (time != null) + { + p.Stub = p.Stub.Select(x => x.Equals("TIME", System.StringComparison.OrdinalIgnoreCase) ? time.Code : x).ToList(); + p.Heading = p.Heading.Select(x => x.Equals("TIME", System.StringComparison.OrdinalIgnoreCase) ? time.Code : x).ToList(); + } + } + } +} diff --git a/PxWeb/Code/Api2/ModelBinder/CommaSeparatedStringToListOfStrings.cs b/PxWeb/Code/Api2/ModelBinder/CommaSeparatedStringToListOfStrings.cs new file mode 100644 index 00000000..3799ab25 --- /dev/null +++ b/PxWeb/Code/Api2/ModelBinder/CommaSeparatedStringToListOfStrings.cs @@ -0,0 +1,57 @@ +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace PxWeb.Code.Api2.ModelBinder +{ + public class CommaSeparatedStringToListOfStrings : IModelBinder + { + + public Task BindModelAsync(ModelBindingContext bindingContext) + { + + if (bindingContext == null) + throw new ArgumentNullException(nameof(bindingContext)); + + var modelName = bindingContext.ModelName; + + var result = new List(); + + var keys = bindingContext.HttpContext.Request.Query.Keys.Where(x => x.Equals(modelName, StringComparison.InvariantCultureIgnoreCase)); + + foreach (var key in keys) + { + string? q = bindingContext.HttpContext.Request.Query[key]; + if (q != null) + { + var items = Regex.Split(q, ",(?=[^\\]]*(?:\\[|$))", RegexOptions.None, + TimeSpan.FromMilliseconds(100)); + + + foreach (var item in items) + { + result.Add(CleanValue(item)); + } + } + } + + + bindingContext.Result = ModelBindingResult.Success(result); + + return Task.CompletedTask; + } + + private static string CleanValue(string value) + { + var item2 = value.Trim(); + if (item2.StartsWith('[') && item2.EndsWith(']')) + { + return value.Substring(1, item2.Length - 2).Trim(); + } + return item2; + + } + } +} diff --git a/PxWeb/Controllers/Api2/TableApiController.cs b/PxWeb/Controllers/Api2/TableApiController.cs index e6c1ec22..b152c00f 100644 --- a/PxWeb/Controllers/Api2/TableApiController.cs +++ b/PxWeb/Controllers/Api2/TableApiController.cs @@ -24,6 +24,7 @@ using PxWeb.Api2.Server.Models; using PxWeb.Code.Api2.DataSelection; +using PxWeb.Code.Api2.ModelBinder; using PxWeb.Code.Api2.Serialization; using PxWeb.Helper.Api2; using PxWeb.Mappers; @@ -47,9 +48,10 @@ public class TableApiController : PxWeb.Api2.Server.Controllers.TableApiControll private readonly ISerializeManager _serializeManager; private readonly PxApiConfigurationOptions _configOptions; private readonly ISelectionHandler _selectionHandler; + private readonly IPlacementHandler _placementHandler; private readonly ISelectionResponseMapper _selectionResponseMapper; - public TableApiController(IDataSource dataSource, ILanguageHelper languageHelper, ITableMetadataResponseMapper responseMapper, IDatasetMapper datasetMapper, ISearchBackend backend, IOptions configOptions, ITablesResponseMapper tablesResponseMapper, ITableResponseMapper tableResponseMapper, ICodelistResponseMapper codelistResponseMapper, ISelectionResponseMapper selectionResponseMapper, ISerializeManager serializeManager, ISelectionHandler selectionHandler) + public TableApiController(IDataSource dataSource, ILanguageHelper languageHelper, ITableMetadataResponseMapper responseMapper, IDatasetMapper datasetMapper, ISearchBackend backend, IOptions configOptions, ITablesResponseMapper tablesResponseMapper, ITableResponseMapper tableResponseMapper, ICodelistResponseMapper codelistResponseMapper, ISelectionResponseMapper selectionResponseMapper, ISerializeManager serializeManager, ISelectionHandler selectionHandler, IPlacementHandler placementHandler) { _dataSource = dataSource; _languageHelper = languageHelper; @@ -63,6 +65,7 @@ public TableApiController(IDataSource dataSource, ILanguageHelper languageHelper _serializeManager = serializeManager; _selectionHandler = selectionHandler; _selectionResponseMapper = selectionResponseMapper; + _placementHandler = placementHandler; } public override IActionResult GetMetadataById([FromRoute(Name = "id"), Required] string id, [FromQuery(Name = "lang")] string? lang, [FromQuery(Name = "outputFormat")] MetadataOutputFormatType? outputFormat) @@ -156,7 +159,7 @@ public override IActionResult ListAllTables([FromQuery(Name = "lang")] string? l } - public override IActionResult GetTableData([FromRoute(Name = "id"), Required] string id, [FromQuery(Name = "lang")] string? lang, [FromQuery(Name = "valuecodes")] Dictionary>? valuecodes, [FromQuery(Name = "codelist")] Dictionary? codelist, [FromQuery(Name = "outputvalues")] Dictionary? outputvalues, [FromQuery(Name = "outputFormat")] string? outputFormat, [FromQuery(Name = "heading")] List? heading, [FromQuery(Name = "stub")] List? stub) + public override IActionResult GetTableData([FromRoute(Name = "id"), Required] string id, [FromQuery(Name = "lang")] string? lang, [FromQuery(Name = "valuecodes"), ModelBinder(typeof(QueryStringToDictionaryOfStrings))] Dictionary>? valuecodes, [FromQuery(Name = "codelist")] Dictionary? codelist, [FromQuery(Name = "outputvalues")] Dictionary? outputvalues, [FromQuery(Name = "outputFormat")] string? outputFormat, [FromQuery(Name = "heading"), ModelBinder(typeof(CommaSeparatedStringToListOfStrings))] List? heading, [FromQuery(Name = "stub"), ModelBinder(typeof(CommaSeparatedStringToListOfStrings))] List? stub) { VariablesSelection variablesSelection = MapDataParameters(valuecodes, codelist, outputvalues, heading, stub); return GetData(id, lang, variablesSelection, outputFormat); @@ -200,10 +203,11 @@ private IActionResult GetData(string id, string? lang, VariablesSelection? varia { selection = _selectionHandler.GetSelection(builder, variablesSelection, out problem); - if (problem is not null) + if (problem is null && selection is not null) { //Check if we should pivot the table - placment = GetPlacment(variablesSelection, builder, out problem); + placment = _placementHandler.GetPlacment(variablesSelection, selection, builder.Model.Meta, out problem); + //GetPlacment(variablesSelection, selection, builder, out problem); } } } @@ -223,7 +227,7 @@ private IActionResult GetData(string id, string? lang, VariablesSelection? varia descriptions.AddRange(placment.Heading.Select(h => new PivotDescription() { - VariableName = model.Meta.Variables.First(v => v.Code == h).Name, + VariableName = model.Meta.Variables.First(v => v.Code.Equals(h, StringComparison.OrdinalIgnoreCase)).Name, VariablePlacement = PlacementType.Heading })); @@ -253,50 +257,6 @@ private IActionResult GetData(string id, string? lang, VariablesSelection? varia } - private VariablePlacementType? GetPlacment(VariablesSelection variablesSelection, IPXModelBuilder builder, out Problem? problem) - { - VariablePlacementType? placment = null; - var p = variablesSelection.Palcement; - //Check if user have specified stub or heading - if (p is not null && (p.Heading.Count > 0 || p.Stub.Count > 0)) - { - if ((p.Heading.Count + p.Stub.Count) != builder.Model.Meta.Variables.Count) - { - //Check if only one variable is selected - if (p.Heading.Count == 0 || p.Stub.Count == 0) - { - List usedVariables = new List(); - usedVariables.AddRange(p.Heading); - usedVariables.AddRange(p.Stub); - - var unusedVariables = builder.Model.Meta.Variables.Select(v => v.Code).Except(usedVariables).ToList(); - - if (p.Heading.Count == 0) - { - placment = new VariablePlacementType() { Heading = unusedVariables, Stub = p.Stub }; - } - else - { - placment = new VariablePlacementType() { Heading = p.Heading, Stub = unusedVariables }; - } - } - else - { - //Not all variables are specified in placment - problem = ProblemUtility.IllegalSelection(); - return null; - } - } - else - { - placment = p; - } - } - - problem = null; - return placment; - } - /// /// Map querystring parameters to VariablesSelection object @@ -304,6 +264,8 @@ private IActionResult GetData(string id, string? lang, VariablesSelection? varia /// /// /// + /// + /// /// private VariablesSelection MapDataParameters(Dictionary>? valuecodes, Dictionary? codelist, Dictionary? outputvalues, List? heading, List? stub) { diff --git a/PxWeb/Helper/Api2/ProblemUtility.cs b/PxWeb/Helper/Api2/ProblemUtility.cs index 7ebfd26d..981ded51 100644 --- a/PxWeb/Helper/Api2/ProblemUtility.cs +++ b/PxWeb/Helper/Api2/ProblemUtility.cs @@ -57,6 +57,15 @@ public static Problem IllegalSelectionExpression() return p; } + public static Problem IllegalPlacementSelection() + { + Problem p = new Problem(); + p.Type = "Parameter error"; + p.Status = 400; + p.Title = "Illegal placment"; + return p; + } + public static Problem TooManyCellsSelected() { Problem p = new Problem(); diff --git a/PxWeb/Program.cs b/PxWeb/Program.cs index d7527172..36718750 100644 --- a/PxWeb/Program.cs +++ b/PxWeb/Program.cs @@ -68,6 +68,7 @@ public static void Main(string[] args) builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddPxDataSource(builder);