From c6860a26641c1f0c972d5f4ce7dbc7f2be178394 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 15 Jul 2026 13:15:41 -0700 Subject: [PATCH] Update Named Catalogs Interfaces --- .github/copilot-instructions.md | 1 + .../Services/INamedCatalogManager.cs | 35 +------------ .../Services/INamedSourceCatalogManager.cs | 51 +------------------ .../docs/changelog/v1.0.0.md | 1 + .../docs/core/interfaces.md | 2 +- .../ServiceCollectionExtensions.cs | 4 +- .../Services/DefaultAIProfileManager.cs | 16 ++++++ .../Services/NamedCatalogManager.cs | 15 +++++- .../Services/NamedSourceCatalogManager.cs | 49 +++++++----------- .../AIServiceCollectionExtensionsTests.cs | 2 + .../Catalogs/NamedCatalogManagerTests.cs | 11 ++++ .../NamedSourceCatalogManagerTests.cs | 34 +++++++++++++ .../Mvc/IndexProfileTypeRulesTests.cs | 5 ++ ...rchIndexProfileProvisioningServiceTests.cs | 5 ++ 14 files changed, 113 insertions(+), 118 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9275c23c..28cad270 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -102,6 +102,7 @@ Keep the docs focused on `CrestApps.Core`. If you need to mention the Orchard Co - For optional provider integrations in sample hosts, do not eagerly read validated options in UI setup paths when an unconfigured provider should simply appear unavailable rather than crash the page. - For catalog entry models, always provide an authoritative `CatalogEntryHandlerBase` implementation that includes a `PopulateAsync` mapping path for every known property reachable from `JsonNode`/`JsonObject`, uses the shared JSON helper extensions instead of ad-hoc parsing where practical, sets create-time defaults (timestamps and current user/owner values when the model supports them) in `InitializedAsync`/`CreatingAsync`, and validates required fields in `ValidatingAsync`. - For any `INameAwareModel` flow that has an authoritative catalog handler, validate duplicate names in the handler so users see a validation error early, but keep the store-level uniqueness enforcement as the final safeguard instead of moving that responsibility into managers. +- For catalog manager creation APIs, name-aware managers should offer both named and name-later `NewAsync` overloads, but any source-aware creation path must still require `source` up front and should not expose a name-only creation contract. - When handler or service code needs to read typed values from `JsonNode`/`JsonObject`, add or reuse public helpers in `JsonNodeExtensions` rather than introducing new private `TryGetEnum`, `TryGetInt32`, `TryGetDateTime`, or similar parsing helpers in individual classes. - Keep UI-only provider/authentication validation rules in the MVC and Blazor web projects instead of the framework handlers; for AI deployment and AI connection forms, enforce provider/connection/endpoint/API key requirements in the web layer so the handler layer stays focused on shared model concerns. - Always keep exactly one trailing newline at the end of each file, no more and no less. diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalogManager.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalogManager.cs index 938f839c..592c5a4e 100644 --- a/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalogManager.cs +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedCatalogManager.cs @@ -5,21 +5,13 @@ namespace CrestApps.Core.Services; /// /// A catalog manager that supports finding entries by their unique name, -/// extending with name-based lookup for models +/// extending with name-based lookup for models /// that implement . /// /// The type of catalog entry, which must have a property. -public interface INamedCatalogManager : IReadCatalogManager +public interface INamedCatalogManager : ICatalogManager where T : INameAwareModel { - /// - /// Asynchronously deletes the specified model from the catalog. - /// - /// The model to delete. - /// The token to monitor for cancellation requests. - /// if the model was successfully deleted; otherwise, . - ValueTask DeleteAsync(T model, CancellationToken cancellationToken = default); - /// /// Asynchronously creates a new model instance pre-assigned to the specified name, /// optionally populating it from JSON data. @@ -30,29 +22,6 @@ public interface INamedCatalogManager : IReadCatalogManager /// A newly created and initialized model instance assigned to the specified name. ValueTask NewAsync(string name, JsonNode data = null, CancellationToken cancellationToken = default); - /// - /// Asynchronously creates the specified model in the catalog. - /// - /// The model to create. - /// The token to monitor for cancellation requests. - ValueTask CreateAsync(T model, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates the specified model in the catalog, optionally merging changes from JSON data. - /// - /// The model to update. - /// Optional JSON data containing fields to merge into the model. - /// The token to monitor for cancellation requests. - ValueTask UpdateAsync(T model, JsonNode data = null, CancellationToken cancellationToken = default); - - /// - /// Asynchronously validates the specified model and returns the validation result. - /// - /// The model to validate. - /// The token to monitor for cancellation requests. - /// The validation result details indicating success or failure with error messages. - ValueTask ValidateAsync(T model, CancellationToken cancellationToken = default); - /// /// Asynchronously finds a catalog entry by its unique name. /// diff --git a/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalogManager.cs b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalogManager.cs index 7f33ea8f..6105c41b 100644 --- a/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalogManager.cs +++ b/src/Abstractions/CrestApps.Core.Abstractions/Services/INamedSourceCatalogManager.cs @@ -5,21 +5,13 @@ namespace CrestApps.Core.Services; /// /// A catalog manager that supports composite lookup by both name and source, -/// extending +/// extending /// for models that implement both and . /// /// The type of catalog entry. -public interface INamedSourceCatalogManager : IReadCatalogManager +public interface INamedSourceCatalogManager : ISourceCatalogManager where T : INameAwareModel, ISourceAwareModel { - /// - /// Asynchronously deletes the specified model from the catalog. - /// - /// The model to delete. - /// The token to monitor for cancellation requests. - /// if the model was successfully deleted; otherwise, . - ValueTask DeleteAsync(T model, CancellationToken cancellationToken = default); - /// /// Asynchronously creates a new model instance pre-assigned to the specified name and source, /// optionally populating it from JSON data. @@ -31,29 +23,6 @@ public interface INamedSourceCatalogManager : IReadCatalogManager /// A newly created and initialized model instance assigned to the specified name and source. ValueTask NewAsync(string name, string source, JsonNode data = null, CancellationToken cancellationToken = default); - /// - /// Asynchronously creates the specified model in the catalog. - /// - /// The model to create. - /// The token to monitor for cancellation requests. - ValueTask CreateAsync(T model, CancellationToken cancellationToken = default); - - /// - /// Asynchronously updates the specified model in the catalog, optionally merging changes from JSON data. - /// - /// The model to update. - /// Optional JSON data containing fields to merge into the model. - /// The token to monitor for cancellation requests. - ValueTask UpdateAsync(T model, JsonNode data = null, CancellationToken cancellationToken = default); - - /// - /// Asynchronously validates the specified model and returns the validation result. - /// - /// The model to validate. - /// The token to monitor for cancellation requests. - /// The validation result details indicating success or failure with error messages. - ValueTask ValidateAsync(T model, CancellationToken cancellationToken = default); - /// /// Asynchronously finds a catalog entry by its unique name. /// @@ -62,22 +31,6 @@ public interface INamedSourceCatalogManager : IReadCatalogManager /// The matching entry, or if no entry with the specified name exists. ValueTask FindByNameAsync(string name, CancellationToken cancellationToken = default); - /// - /// Asynchronously retrieves all catalog entries belonging to the specified source. - /// - /// The source or provider name to filter by. - /// The token to monitor for cancellation requests. - /// An enumerable of entries matching the specified source. - ValueTask> GetAsync(string source, CancellationToken cancellationToken = default); - - /// - /// Asynchronously finds all catalog entries that belong to the specified source. - /// - /// The source or provider name to search for. - /// The token to monitor for cancellation requests. - /// An enumerable of entries matching the specified source. - ValueTask> FindBySourceAsync(string source, CancellationToken cancellationToken = default); - /// /// Asynchronously retrieves a catalog entry by its unique name and source combination. /// diff --git a/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md index 24ed80b9..1aa63b90 100644 --- a/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md +++ b/src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md @@ -82,6 +82,7 @@ description: Initial standalone release notes for the CrestApps.Core repository. - expands Elasticsearch AI data source authentication beyond Basic by adding Elastic Cloud ID support plus `ApiKey`, `Base64ApiKey`, and `KeyIdAndKey` modes with protected per-source secrets in the MVC and Blazor editors and the shared Elasticsearch client factory - adds `CrestApps.Core.PostgreSQL` and `CrestApps.Core.AI.PostgreSQL` packages providing a lightweight PostgreSQL + pgvector vector search backend as an alternative to Elasticsearch and Azure AI Search, registers the same keyed services (`ISearchIndexManager`, `ISearchDocumentManager`, `IDataSourceContentManager`, `IDataSourceDocumentReader`, `IODataFilterTranslator`) under the `"PostgreSQL"` provider name, supports `AddAIDocuments()`, `AddAIDataSources()`, and `AddAIMemory()` builder extensions, and integrates into both MVC and Blazor sample hosts - fixes hosted document and data-source indexing flows so background workers create a scoped service provider before resolving scoped indexing services, preventing upload-triggered failures and similar nightly alignment lifetime issues +- aligns named catalog manager creation overloads so `INamedCatalogManager` also exposes unnamed `NewAsync(...)` creation, while source-aware managers keep source-required creation paths and no longer advertise name-only manager registrations for source-bound AI templates and deployments - standardizes Azure AI Search configuration on top-level `AuthenticationType`, `ApiKey`, `IdentityClientId`, and `IndexPrefix` settings under `CrestApps:AzureAISearch`, and refreshes the sample host / docs examples to list the full supported option set in one place - adds an explicit Elasticsearch data-source environment selector (`SelfManaged` vs `CloudHosted`) so the MVC and Blazor editors show either `Url` or `CloudId` as appropriate and validation now requires the matching field for the chosen environment - makes `AIDataSource` source-aware through the shared `Source` property, updates the AI data-source stores to expose `ISourceCatalog`, and removes the public `SourceType` model property in favor of `Source` while still reading legacy persisted `SourceType` payloads diff --git a/src/CrestApps.Core.Docs/docs/core/interfaces.md b/src/CrestApps.Core.Docs/docs/core/interfaces.md index ef2d811f..126eeca1 100644 --- a/src/CrestApps.Core.Docs/docs/core/interfaces.md +++ b/src/CrestApps.Core.Docs/docs/core/interfaces.md @@ -25,7 +25,7 @@ Use these contracts when you are building reusable storage or management infrast | --- | --- | | `IReadCatalog`, `ICatalog` | Query and mutate catalog-backed data | | `IReadCatalogManager`, `ICatalogManager` | Validation and lifecycle handling over catalogs | -| `INamedCatalog`, `ISourceCatalog`, related managers | Name-based and source-based lookup patterns | +| `INamedCatalog`, `ISourceCatalog`, related managers | Name-based and source-based lookup patterns, including name-optional creation for named managers and source-required creation for source-aware managers | | `ICatalogEntryHandler` | Hooks for create, update, and delete events | | `INameAwareModel`, `IDisplayTextAwareModel`, `ISourceAwareModel` | Common model markers used across the framework | diff --git a/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs b/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs index 193b4655..d286e5f9 100644 --- a/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs +++ b/src/Primitives/CrestApps.Core.AI/ServiceCollectionExtensions.cs @@ -69,7 +69,6 @@ public static IServiceCollection AddCoreAITemplating( }); services.TryAddScoped(); - services.TryAddScoped>(sp => (INamedCatalogManager)sp.GetRequiredService()); services.TryAddScoped>(sp => (ISourceCatalogManager)sp.GetRequiredService()); services.TryAddScoped>(sp => sp.GetRequiredService()); services.TryAddEnumerable(ServiceDescriptor.Scoped, AIProfileTemplateCatalogHandler>()); @@ -443,10 +442,9 @@ public static IServiceCollection AddCoreAIOrchestration(this IServiceCollection return snapshot.Value.ApplySiteOverrides(settings.CurrentValue); }); - // Register the Framework-level deployment manager. services.TryAddScoped(); - services.TryAddScoped>(sp => (INamedCatalogManager)sp.GetRequiredService()); + services.TryAddScoped(); services.TryAddScoped>(sp => (ISourceCatalogManager)sp.GetRequiredService()); services.TryAddScoped>(sp => sp.GetRequiredService()); diff --git a/src/Primitives/CrestApps.Core.AI/Services/DefaultAIProfileManager.cs b/src/Primitives/CrestApps.Core.AI/Services/DefaultAIProfileManager.cs index cb8847ac..2ed24635 100644 --- a/src/Primitives/CrestApps.Core.AI/Services/DefaultAIProfileManager.cs +++ b/src/Primitives/CrestApps.Core.AI/Services/DefaultAIProfileManager.cs @@ -87,12 +87,28 @@ private async ValueTask> MergeProvidedProfilesAsync( return merged; } + /// + /// Creates a new AI profile instance. + /// + /// The optional initialization data. + /// The cancellation token. + /// A newly created AI profile. + public new async ValueTask NewAsync(JsonNode data = null, CancellationToken cancellationToken = default) + { + var profile = await base.NewAsync(data, cancellationToken); + + EnsureDefaults(profile); + + return profile; + } + /// /// Creates a new AI profile instance. /// /// The profile name. /// The optional initialization data. /// The cancellation token. + /// A newly created AI profile assigned to the specified name. public new async ValueTask NewAsync(string name, JsonNode data = null, CancellationToken cancellationToken = default) { var profile = await base.NewAsync(name, data, cancellationToken); diff --git a/src/Primitives/CrestApps.Core/Services/NamedCatalogManager.cs b/src/Primitives/CrestApps.Core/Services/NamedCatalogManager.cs index c5f06496..5c91f06e 100644 --- a/src/Primitives/CrestApps.Core/Services/NamedCatalogManager.cs +++ b/src/Primitives/CrestApps.Core/Services/NamedCatalogManager.cs @@ -62,11 +62,24 @@ public async ValueTask FindByNameAsync(string name, CancellationToken cancell } /// - /// News the operation. + /// Asynchronously creates a new model instance, optionally populating it from JSON data. + /// + /// Optional JSON data to seed the new model. + /// The cancellation token. + /// A newly created and initialized model instance. + public virtual async ValueTask NewAsync(JsonNode? data = null, CancellationToken cancellationToken = default) + { + return await InitializeNewEntryAsync(new T(), data, cancellationToken); + } + + /// + /// Asynchronously creates a new model instance pre-assigned to the specified name, + /// optionally populating it from JSON data. /// /// The name. /// The data. /// The cancellation token. + /// A newly created and initialized model instance assigned to the specified name. public virtual async ValueTask NewAsync(string name, JsonNode? data = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(name); diff --git a/src/Primitives/CrestApps.Core/Services/NamedSourceCatalogManager.cs b/src/Primitives/CrestApps.Core/Services/NamedSourceCatalogManager.cs index 533be317..9f059340 100644 --- a/src/Primitives/CrestApps.Core/Services/NamedSourceCatalogManager.cs +++ b/src/Primitives/CrestApps.Core/Services/NamedSourceCatalogManager.cs @@ -7,7 +7,7 @@ namespace CrestApps.Core.Services; /// /// Represents the named Source Catalog Manager. /// -public class NamedSourceCatalogManager : CatalogManagerBase, INamedCatalogManager, ISourceCatalogManager, INamedSourceCatalogManager +public class NamedSourceCatalogManager : CatalogManagerBase, INamedSourceCatalogManager where T : CatalogItem, INameAwareModel, ISourceAwareModel, new() { protected readonly INamedSourceCatalog NamedSourceCatalog; @@ -106,15 +106,15 @@ public async ValueTask> FindBySourceAsync(string source, Cancella } /// - /// News the operation. + /// Asynchronously creates a new model instance pre-assigned to the specified source, + /// optionally populating it from JSON data. /// - /// The name. /// The source. /// The data. /// The cancellation token. - public virtual async ValueTask NewAsync(string name, string source, JsonNode? data = null, CancellationToken cancellationToken = default) + /// A newly created and initialized model instance assigned to the specified source. + public virtual async ValueTask NewAsync(string source, JsonNode? data = null, CancellationToken cancellationToken = default) { - ArgumentException.ThrowIfNullOrEmpty(name); ArgumentException.ThrowIfNullOrEmpty(source); var entry = new T @@ -122,49 +122,36 @@ public virtual async ValueTask NewAsync(string name, string source, JsonNode? Source = source, }; - SetName(entry, name); - entry = await InitializeNewEntryAsync(entry, data, cancellationToken); entry.Source = source; - SetName(entry, name); return entry; } - ValueTask INamedCatalogManager.NewAsync(string name, JsonNode data, CancellationToken cancellationToken) + /// + /// Asynchronously creates a new model instance pre-assigned to the specified name and source, + /// optionally populating it from JSON data. + /// + /// The name. + /// The source. + /// The data. + /// The cancellation token. + /// A newly created and initialized model instance assigned to the specified name and source. + public virtual async ValueTask NewAsync(string name, string source, JsonNode? data = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(name); - - var entry = new T(); - SetName(entry, name); - - return InitializeNameOnlyEntryAsync(entry, name, data, cancellationToken); - } - - ValueTask ISourceCatalogManager.NewAsync(string source, JsonNode data, CancellationToken cancellationToken) - { ArgumentException.ThrowIfNullOrEmpty(source); - return InitializeSourceOnlyEntryAsync(source, data, cancellationToken); - } - - private async ValueTask InitializeNameOnlyEntryAsync(T entry, string name, JsonNode? data, CancellationToken cancellationToken) - { - entry = await InitializeNewEntryAsync(entry, data, cancellationToken); - SetName(entry, name); - - return entry; - } - - private async ValueTask InitializeSourceOnlyEntryAsync(string source, JsonNode? data, CancellationToken cancellationToken) - { var entry = new T { Source = source, }; + SetName(entry, name); + entry = await InitializeNewEntryAsync(entry, data, cancellationToken); entry.Source = source; + SetName(entry, name); return entry; } diff --git a/tests/CrestApps.Core.Tests/Core/Services/AIServiceCollectionExtensionsTests.cs b/tests/CrestApps.Core.Tests/Core/Services/AIServiceCollectionExtensionsTests.cs index 92fa0e77..d5045191 100644 --- a/tests/CrestApps.Core.Tests/Core/Services/AIServiceCollectionExtensionsTests.cs +++ b/tests/CrestApps.Core.Tests/Core/Services/AIServiceCollectionExtensionsTests.cs @@ -32,10 +32,12 @@ public void AddCoreAIServices_DoesNotRegisterGenericConnectionOrDeploymentCatalo Assert.Null(scopedServices.GetService>()); Assert.Null(scopedServices.GetService>()); Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); Assert.Null(scopedServices.GetService>()); Assert.Null(scopedServices.GetService>()); Assert.Null(scopedServices.GetService>()); Assert.Null(scopedServices.GetService>()); + Assert.Null(scopedServices.GetService>()); } [Fact] diff --git a/tests/CrestApps.Core.Tests/Core/Services/Catalogs/NamedCatalogManagerTests.cs b/tests/CrestApps.Core.Tests/Core/Services/Catalogs/NamedCatalogManagerTests.cs index 3f566c35..1a712109 100644 --- a/tests/CrestApps.Core.Tests/Core/Services/Catalogs/NamedCatalogManagerTests.cs +++ b/tests/CrestApps.Core.Tests/Core/Services/Catalogs/NamedCatalogManagerTests.cs @@ -55,6 +55,17 @@ public async Task NewAsync_AssignsRequestedName() Assert.False(string.IsNullOrEmpty(entry.ItemId)); } + [Fact] + public async Task NewAsync_WithoutName_CreatesEntryWithoutAssigningName() + { + var manager = CreateManager(); + + var entry = await manager.NewAsync(cancellationToken: CancellationToken); + + Assert.Null(entry.Name); + Assert.False(string.IsNullOrEmpty(entry.ItemId)); + } + [Fact] public async Task NewAsync_RestoresRequestedNameAfterInitialization() { diff --git a/tests/CrestApps.Core.Tests/Core/Services/Catalogs/NamedSourceCatalogManagerTests.cs b/tests/CrestApps.Core.Tests/Core/Services/Catalogs/NamedSourceCatalogManagerTests.cs index c9dd0980..d20a3279 100644 --- a/tests/CrestApps.Core.Tests/Core/Services/Catalogs/NamedSourceCatalogManagerTests.cs +++ b/tests/CrestApps.Core.Tests/Core/Services/Catalogs/NamedSourceCatalogManagerTests.cs @@ -52,6 +52,18 @@ public async Task NewAsync_AssignsRequestedNameAndSource() Assert.False(string.IsNullOrEmpty(entry.ItemId)); } + [Fact] + public async Task NewAsync_WithoutName_AssignsRequestedSource() + { + var manager = CreateManager(); + + var entry = await manager.NewAsync("A", cancellationToken: CancellationToken); + + Assert.Null(entry.Name); + Assert.Equal("A", entry.Source); + Assert.False(string.IsNullOrEmpty(entry.ItemId)); + } + [Fact] public async Task NewAsync_RestoresRequestedNameAndSourceAfterInitialization() { @@ -76,6 +88,28 @@ public async Task NewAsync_RestoresRequestedNameAndSourceAfterInitialization() Assert.Equal("A", entry.Source); } + [Fact] + public async Task NewAsync_WithoutName_RestoresRequestedSourceAfterInitialization() + { + var catalog = InMemoryCatalogFactory.CreateNamedSourceCatalog([]); + var logger = Mock.Of>>(); + var handler = new TestCatalogEntryHandler + { + OnInitializingAsync = ctx => + { + ctx.Model.Source = "Changed"; + + return Task.CompletedTask; + } + }; + + var manager = new NamedSourceCatalogManager(catalog, [handler], logger); + + var entry = await manager.NewAsync("A", cancellationToken: CancellationToken); + + Assert.Equal("A", entry.Source); + } + [Fact] public async Task FindByNameAsync_InvokesLoadedHandler() { diff --git a/tests/CrestApps.Core.Tests/Framework/Mvc/IndexProfileTypeRulesTests.cs b/tests/CrestApps.Core.Tests/Framework/Mvc/IndexProfileTypeRulesTests.cs index 7f40265f..8bd2c063 100644 --- a/tests/CrestApps.Core.Tests/Framework/Mvc/IndexProfileTypeRulesTests.cs +++ b/tests/CrestApps.Core.Tests/Framework/Mvc/IndexProfileTypeRulesTests.cs @@ -373,6 +373,11 @@ public ValueTask NewAsync(string name, JsonNode data = null, return ValueTask.FromResult(new SearchIndexProfile { Name = name }); } + public ValueTask NewAsync(JsonNode data = null, CancellationToken cancellationToken = default) + { + return ValueTask.FromResult(new SearchIndexProfile()); + } + public ValueTask> PageAsync(int page, int pageSize, TQuery context, CancellationToken cancellationToken = default) where TQuery : QueryContext { diff --git a/tests/CrestApps.Core.Tests/Framework/Mvc/SearchIndexProfileProvisioningServiceTests.cs b/tests/CrestApps.Core.Tests/Framework/Mvc/SearchIndexProfileProvisioningServiceTests.cs index 86042fb3..58c57fa3 100644 --- a/tests/CrestApps.Core.Tests/Framework/Mvc/SearchIndexProfileProvisioningServiceTests.cs +++ b/tests/CrestApps.Core.Tests/Framework/Mvc/SearchIndexProfileProvisioningServiceTests.cs @@ -194,6 +194,11 @@ public ValueTask NewAsync(string name, JsonNode data = null, return ValueTask.FromResult(new SearchIndexProfile { Name = name }); } + public ValueTask NewAsync(JsonNode data = null, CancellationToken cancellationToken = default) + { + return ValueTask.FromResult(new SearchIndexProfile()); + } + public ValueTask> PageAsync(int page, int pageSize, TQuery context, CancellationToken cancellationToken = default) where TQuery : QueryContext {