Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using CrestApps.Core.AI.Models;
using CrestApps.Core.Services;

namespace CrestApps.Core.AI.Connections;

/// <summary>
/// Provides persisted storage for AI provider connections while preserving the standard
/// named-and-sourced catalog operations used by connection managers and editors.
/// </summary>
public interface IAIProviderConnectionStore : INamedSourceCatalog<AIProviderConnection>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace CrestApps.Core.Services;

/// <summary>
/// Represents a read-only binding source of catalog entries for models that are identified
/// by name. Each source is ordered by <see cref="Order"/> (lower values have higher priority).
/// </summary>
/// <typeparam name="T">The type of catalog entry.</typeparam>
public interface INamedCatalogSource<T>
where T : INameAwareModel
{
/// <summary>
/// Gets the priority order of this source. Lower values indicate higher priority.
/// When entries with the same name exist in multiple sources, the source with the
/// lower order value wins.
/// </summary>
int Order { get; }

/// <summary>
/// Asynchronously retrieves all entries provided by this source.
/// </summary>
/// <param name="knownEntries">
/// Entries already collected from higher-priority sources, allowing this source
/// to skip entries whose names conflict with existing ones.
/// </param>
/// <returns>A read-only collection of entries from this source.</returns>
ValueTask<IReadOnlyCollection<T>> GetEntriesAsync(IReadOnlyCollection<T> knownEntries);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace CrestApps.Core.Services;

/// <summary>
/// Represents a read-only binding source of catalog entries for models that are identified
/// by both name and source. Extends <see cref="INamedCatalogSource{T}"/> with the additional
/// <see cref="ISourceAwareModel"/> constraint. Each source is ordered by <see cref="Order"/>
/// (lower values have higher priority).
/// </summary>
/// <typeparam name="T">The type of catalog entry.</typeparam>
public interface INamedSourceCatalogSource<T> : INamedCatalogSource<T>
where T : INameAwareModel, ISourceAwareModel
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace CrestApps.Core.Services;

/// <summary>
/// Extends <see cref="INamedCatalogSource{T}"/> with write operations
/// (create, update, delete), allowing the multi-source catalog to delegate
/// mutations to a persistent source.
/// </summary>
/// <typeparam name="T">The type of catalog entry.</typeparam>
public interface IWritableNamedCatalogSource<T> : INamedCatalogSource<T>
where T : INameAwareModel
{
/// <summary>
/// Asynchronously deletes the specified entry from this source.
/// </summary>
/// <param name="entry">The entry to delete.</param>
/// <returns><see langword="true"/> if the entry was successfully deleted; otherwise, <see langword="false"/>.</returns>
ValueTask<bool> DeleteAsync(T entry);

/// <summary>
/// Asynchronously creates the specified entry in this source.
/// </summary>
/// <param name="entry">The entry to create.</param>
ValueTask CreateAsync(T entry);

/// <summary>
/// Asynchronously updates the specified entry in this source.
/// </summary>
/// <param name="entry">The entry to update.</param>
ValueTask UpdateAsync(T entry);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace CrestApps.Core.Services;

/// <summary>
/// Extends <see cref="INamedSourceCatalogSource{T}"/> and <see cref="IWritableNamedCatalogSource{T}"/>
/// with write operations for models that have both name and source. Allows the multi-source
/// catalog to delegate mutations to a persistent source.
/// </summary>
/// <typeparam name="T">The type of catalog entry.</typeparam>
public interface IWritableNamedSourceCatalogSource<T> : INamedSourceCatalogSource<T>, IWritableNamedCatalogSource<T>
where T : INameAwareModel, ISourceAwareModel
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace CrestApps.Core.Services;

/// <summary>
/// Wraps an existing <see cref="INamedSourceCatalog{T}"/> as a writable multi-source
/// binding source. Used by persistence layers (YesSql, EntityCore) to expose their
/// DB-backed catalogs as sources for the multi-source store.
/// </summary>
/// <typeparam name="T">The type of catalog entry.</typeparam>
public class WritableCatalogBindingSource<T> : IWritableNamedSourceCatalogSource<T>
where T : INameAwareModel, ISourceAwareModel
{
private readonly INamedSourceCatalog<T> _inner;

public WritableCatalogBindingSource(INamedSourceCatalog<T> inner)
{
_inner = inner;
}

/// <summary>
/// Gets the priority order. DB-backed sources use 0 (highest priority).
/// </summary>
public int Order => 0;

public ValueTask<IReadOnlyCollection<T>> GetEntriesAsync(IReadOnlyCollection<T> knownEntries)
=> _inner.GetAllAsync();

public ValueTask<bool> DeleteAsync(T entry)
=> _inner.DeleteAsync(entry);

public ValueTask CreateAsync(T entry)
=> _inner.CreateAsync(entry);

public ValueTask UpdateAsync(T entry)
=> _inner.UpdateAsync(entry);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace CrestApps.Core.Services;

/// <summary>
/// Wraps an existing <see cref="INamedCatalog{T}"/> as a writable multi-source
/// binding source. Used by persistence layers to expose their DB-backed catalogs
/// as sources for the multi-source store when the model has no source property.
/// </summary>
/// <typeparam name="T">The type of catalog entry.</typeparam>
public class WritableNamedCatalogBindingSource<T> : IWritableNamedCatalogSource<T>
where T : INameAwareModel
{
private readonly INamedCatalog<T> _inner;

public WritableNamedCatalogBindingSource(INamedCatalog<T> inner)
{
_inner = inner;
}

/// <summary>
/// Gets the priority order. DB-backed sources use 0 (highest priority).
/// </summary>
public int Order => 0;

public ValueTask<IReadOnlyCollection<T>> GetEntriesAsync(IReadOnlyCollection<T> knownEntries)
=> _inner.GetAllAsync();

public ValueTask<bool> DeleteAsync(T entry)
=> _inner.DeleteAsync(entry);

public ValueTask CreateAsync(T entry)
=> _inner.CreateAsync(entry);

public ValueTask UpdateAsync(T entry)
=> _inner.UpdateAsync(entry);
}
1 change: 1 addition & 0 deletions src/CrestApps.Core.Docs/docs/changelog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ This section tracks `CrestApps.Core` releases and notable repository-level chang

| Version | Highlights |
| --- | --- |
| [1.1.0](v1.1.0) | Multi-source binding pattern for AI stores, generic YesSql/EntityCore binding source extensions, connection loading fix |
| [1.0.0](v1.0.0) | Initial standalone release plus merged configuration catalogs, clearer quick-start guidance, and deployment configuration diagnostics |
56 changes: 56 additions & 0 deletions src/CrestApps.Core.Docs/docs/changelog/v1.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
sidebar_label: 1.1.0 Release Notes
sidebar_position: 3
title: "Version 1.1.0 Release Notes"
description: Multi-source binding pattern, generic store extensions, and connection loading fix.
---

# Version 1.1.0 Release Notes

**Package version**: `1.1.0`

## Highlights

- introduces the **multi-source binding pattern** for AI deployments and connections, replacing the previous decorator-based store pattern with an aggregation model that merges entries from multiple binding sources
- ships `INamedCatalogSource<T>` and `INamedSourceCatalogSource<T>` interfaces with writable counterparts, ordered-priority merging base classes (`MultiSourceNamedCatalog<T>` and `MultiSourceNamedSourceCatalog<T>`), and `WritableCatalogBindingSource<T>` / `WritableNamedCatalogBindingSource<T>` adapters
- registers `DefaultAIDeploymentStore` and `DefaultAIProviderConnectionStore` as the built-in multi-source stores in `AddCoreAIServices()`, with configuration-backed binding sources at Order 100 so that `appsettings.json` entries are always available without a persistence package
- adds generic YesSql extensions (`AddYesSqlNamedSourceBindingSource<TModel, TIndex>()`, `AddYesSqlNamedBindingSource<TModel, TIndex>()`) and EntityCore extensions (`AddEntityCoreNamedSourceBindingSource<TModel>()`, `AddEntityCoreNamedBindingSource<TModel>()`) for registering DB-backed binding sources without manual adapter wiring
- fixes a bug where AI provider connections from `appsettings.json` were not visible in the MVC sample because `AddYesSqlNamedSourceDocumentCatalog<AIProviderConnection>()` replaced the multi-source store forwarding
- evaluates all configured connection and provider sections from `AIProviderConnectionCatalogOptions` so that connections from both `ConnectionSections` and `ProviderSections` are discoverable
- reduces unnecessary allocations by removing `.ToArray()` calls on `Dictionary.Values` in configuration sources and materializing filtered collections once in `PageAsync`

## Breaking Changes

- `ConfigurationAIDeploymentStore` and `ConfigurationAIProviderConnectionCatalog` have been removed; their responsibilities are now split between the multi-source stores (`DefaultAIDeploymentStore`, `DefaultAIProviderConnectionStore`) and the configuration binding sources (`ConfigurationAIDeploymentSource`, `ConfigurationAIProviderConnectionSource`)
- hosts that previously registered `ConfigurationAIDeploymentStore` or `ConfigurationAIProviderConnectionCatalog` directly should remove those registrations — `AddCoreAIServices()` handles everything automatically
- the `WritableCatalogSourceAdapter<T>` class has been renamed to `WritableCatalogBindingSource<T>`

## Migration Guide

### Minimal change

If your host calls `AddCoreAIServices()` (or `AddAISuite()`), no changes are needed for the deployment and connection stores — they are registered automatically with appsettings-backed binding sources.

### Adding database-backed storage

To persist deployments and connections in a database alongside the appsettings entries, register the appropriate binding source:

**YesSql:**

```csharp
services.AddYesSqlNamedSourceBindingSource<AIDeployment, AIDeploymentIndex>();
services.AddYesSqlNamedSourceBindingSource<AIProviderConnection, AIProviderConnectionIndex>();
```

**Entity Framework Core:**

```csharp
services.AddEntityCoreNamedSourceBindingSource<AIDeployment>();
services.AddEntityCoreNamedSourceBindingSource<AIProviderConnection>();
```

Or call `AddEntityCoreStores()` which registers both automatically.

### Custom binding sources

To supply entries from an additional source (remote API, embedded resources, etc.), implement `INamedSourceCatalogSource<T>` and register with `TryAddEnumerable`. See the [Data Storage — Multi-Source Binding Pattern](../core/data-storage.md#multi-source-binding-pattern) documentation for full examples.
6 changes: 5 additions & 1 deletion src/CrestApps.Core.Docs/docs/core/ai-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@ A **provider connection** stores credentials and endpoint information for a spec
| `IAIClientFactory` | `DefaultAIClientFactory` | Scoped | Creates typed AI clients |
| `IAICompletionService` | `DefaultAICompletionService` | Scoped | Deployment-aware completion |
| `IAICompletionContextBuilder` | `DefaultAICompletionContextBuilder` | Scoped | Builds context with handler pipeline |
| `IAIDeploymentStore` | `DefaultAIDeploymentStore` | Scoped | Multi-source deployment store (merges DB + config entries) |
| `IAIProviderConnectionStore` | `DefaultAIProviderConnectionStore` | Scoped | Multi-source connection store (merges DB + config entries) |
| `INamedSourceCatalogSource<AIDeployment>` | `ConfigurationAIDeploymentSource` | Scoped | Reads deployments from `appsettings.json` (Order 100) |
| `INamedSourceCatalogSource<AIProviderConnection>` | `ConfigurationAIProviderConnectionSource` | Scoped | Reads connections from `appsettings.json` (Order 100) |
| `ITemplateService` | *(from AddCoreAITemplating)* | Scoped | Template rendering |

It also chains `AddCoreAITemplating()` and `AddCoreServices()` automatically.
It also chains `AddCoreAITemplating()` and `AddCoreServices()` automatically, and forwards `IAIDeploymentStore` to `INamedSourceCatalog<AIDeployment>`, `INamedCatalog<AIDeployment>`, `ISourceCatalog<AIDeployment>`, and `ICatalog<AIDeployment>` (and the same forwarding for `AIProviderConnection`). This means you can inject any of these catalog interfaces and they will resolve through the multi-source store.

Optional format-specific packages stay opt-in. For example, Markdown-aware normalization lives in `CrestApps.Core.AI.Markdown`, so hosts that want Markdig-backed RAG normalization should register `AddCoreAIMarkdown()` explicitly instead of expecting `AddCoreAIServices()` to pull it in automatically.

Expand Down
Loading
Loading