Repository: microsoft/semantic-kernel
Pull request #11892: .Net MEVD: Make sure NpgsqlDataSource gets disposed
Author: login: adamsitnik
Labels: name: .NET
description: Issue or Pull requests regarding .NET code
name: kernel
description: Issues or pull requests impacting the core kernel
name: memory
Created: 2025-05-05T17:06:48Z | Merged: 2025-05-08T14:02:45Z

When the users create `PostgresVectorStore` or `PostgresCollection`, they provide a `NpgsqlDataSource` instance. The data source is disposable, so the users expect the owning types to be also disposable.

I made `PostgresVectorStore` and `PostgresCollection` implement `IDisposable`. The users can ask for no disposal by setting a boolean flag in the option bag. Moreover, since the same data source can be shared between multiple stores (by creating a store and then calling `store.GetCollection`), I added a reference count to ensure there is no premature disposal.

fixes #10969

Linked issue #10969: .Net: PostgresVectorStore / NpgsqlDataSource Dispose
Using this code to register `PostgresVectorStore` - is this the intended approach to dispose `NpgsqlDataSource`, or will `PostgresVectorStore` implement `IDisposable`/`IAsyncDisposable`?

```csharp
builder.Services.AddSingleton<PostgresVectorStore>(
    (IServiceProvider sp) =>
    {
        var connString = sp.GetRequiredService<IConfiguration>()
                           .GetConnectionString(connectionName);
        // ...

        NpgsqlDataSourceBuilder dataSourceBuilder = new(connString);
        dataSourceBuilder.UseVector();
        NpgsqlDataSource dataSource = dataSourceBuilder.Build();

        sp.GetRequiredService<IHostApplicationLifetime>()
            .ApplicationStopping.Register(() => dataSource.DisposeAsync());

        return new PostgresVectorStore(dataSource);
    }
);
```



Linked issue comments: user: login: adamsitnik
created_at: 2025-05-08T14:03:00Z
body: Fixed with https://github.com/microsoft/semantic-kernel/pull/11892
author_association: MEMBER

Diff:
diff --git a/dotnet/samples/Concepts/Memory/VectorStore_VectorSearch_MultiStore_Postgres.cs b/dotnet/samples/Concepts/Memory/VectorStore_VectorSearch_MultiStore_Postgres.cs
index aed7954f1011..8c142b7a2b52 100644
--- a/dotnet/samples/Concepts/Memory/VectorStore_VectorSearch_MultiStore_Postgres.cs
+++ b/dotnet/samples/Concepts/Memory/VectorStore_VectorSearch_MultiStore_Postgres.cs
@@ -6,7 +6,6 @@
 using Microsoft.SemanticKernel;
 using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
 using Microsoft.SemanticKernel.Connectors.PgVector;
-using Npgsql;
 
 namespace Memory;
 
@@ -70,10 +69,7 @@ public async Task ExampleWithoutDIAsync()
 
         // Initialize the Postgres docker container via the fixtures and construct the Postgres VectorStore.
         await PostgresFixture.ManualInitializeAsync();
-        var dataSourceBuilder = new NpgsqlDataSourceBuilder(TestConfiguration.Postgres.ConnectionString);
-        dataSourceBuilder.UseVector();
-        await using var dataSource = dataSourceBuilder.Build();
-        var vectorStore = new PostgresVectorStore(dataSource);
+        using PostgresVectorStore vectorStore = new(TestConfiguration.Postgres.ConnectionString);
 
         // Create the common processor that works for any vector store.
         var processor = new VectorStore_VectorSearch_MultiStore_Common(vectorStore, textEmbeddingGenerationService, this.Output);
diff --git a/dotnet/src/Connectors/Connectors.Memory.PgVector/IPostgresVectorStoreDbClient.cs b/dotnet/src/Connectors/Connectors.Memory.PgVector/IPostgresVectorStoreDbClient.cs
deleted file mode 100644
index 3fc7c89a620a..000000000000
--- a/dotnet/src/Connectors/Connectors.Memory.PgVector/IPostgresVectorStoreDbClient.cs
+++ /dev/null
@@ -1,141 +0,0 @@
-﻿// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.Linq.Expressions;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Extensions.VectorData;
-using Microsoft.Extensions.VectorData.ProviderServices;
-using Npgsql;
-using Pgvector;
-
-namespace Microsoft.SemanticKernel.Connectors.PgVector;
-
-/// <summary>
-/// Internal interface for client managing postgres database operations.
-/// </summary>
-internal interface IPostgresVectorStoreDbClient
-{
-    /// <summary>
-    /// The <see cref="NpgsqlDataSource"/> used to connect to the database.
-    /// </summary>
-    NpgsqlDataSource DataSource { get; }
-
-    /// <summary>
-    /// The name of the database.
-    /// </summary>
-    string? DatabaseName { get; }
-
-    /// <summary>
-    /// Check if a table exists.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns></returns>
-    Task<bool> DoesTableExistsAsync(string tableName, CancellationToken cancellationToken = default);
-
-    /// <summary>
-    /// Get all tables.
-    /// </summary>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns>A group of tables.</returns>
-    IAsyncEnumerable<string> GetTablesAsync(CancellationToken cancellationToken = default);
-    /// <summary>
-    /// Create a table. Also creates an index on vector columns if the table has vector properties defined.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="model">The collection model.</param>
-    /// <param name="ifNotExists">Specifies whether to include IF NOT EXISTS in the command.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns></returns>
-    Task CreateTableAsync(string tableName, CollectionModel model, bool ifNotExists = true, CancellationToken cancellationToken = default);
-
-    /// <summary>
-    /// Drop a table.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    Task DeleteTableAsync(string tableName, CancellationToken cancellationToken = default);
-
-    /// <summary>
-    /// Upsert entry into a table.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="row">The row to upsert into the table.</param>
-    /// <param name="keyColumn">The key column of the table.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns></returns>
-    Task UpsertAsync(string tableName, Dictionary<string, object?> row, string keyColumn, CancellationToken cancellationToken = default);
-
-    /// <summary>
-    /// Upsert multiple entries into a table.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="rows">The rows to upsert into the table.</param>
-    /// <param name="keyColumn">The key column of the table.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns></returns>
-    Task UpsertBatchAsync(string tableName, IEnumerable<Dictionary<string, object?>> rows, string keyColumn, CancellationToken cancellationToken = default);
-
-    /// <summary>
-    /// Get a entry by its key.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="key">The key of the entry to get.</param>
-    /// <param name="model">The collection model.</param>
-    /// <param name="includeVectors">If true, the vectors will be included in the entry.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns>The row if the key is found, otherwise null.</returns>
-    Task<Dictionary<string, object?>?> GetAsync<TKey>(string tableName, TKey key, CollectionModel model, bool includeVectors = false, CancellationToken cancellationToken = default)
-        where TKey : notnull;
-
-    /// <summary>
-    /// Get multiple entries by their keys.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="keys">The keys of the entries to get.</param>
-    /// <param name="model">The collection model.</param>
-    /// <param name="includeVectors">If true, the vectors will be included in the entries.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns>The rows that match the given keys.</returns>
-    IAsyncEnumerable<Dictionary<string, object?>> GetBatchAsync<TKey>(string tableName, IEnumerable<TKey> keys, CollectionModel model, bool includeVectors = false, CancellationToken cancellationToken = default)
-        where TKey : notnull;
-
-    /// <summary>
-    /// Delete a entry by its key.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="keyColumn">The name of the key column.</param>
-    /// <param name="key">The key of the entry to delete.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns></returns>
-    Task DeleteAsync<TKey>(string tableName, string keyColumn, TKey key, CancellationToken cancellationToken = default);
-
-    /// <summary>
-    /// Delete multiple entries by their keys.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="keyColumn">The name of the key column.</param>
-    /// <param name="keys">The keys of the entries to delete.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns></returns>
-    Task DeleteBatchAsync<TKey>(string tableName, string keyColumn, IEnumerable<TKey> keys, CancellationToken cancellationToken = default);
-
-    /// <summary>
-    /// Gets the nearest matches to the <see cref="Vector"/>.
-    /// </summary>
-    /// <param name="tableName">The name assigned to a table of entries.</param>
-    /// <param name="model">The collection model.</param>
-    /// <param name="vectorProperty">The vector property.</param>
-    /// <param name="vectorValue">The <see cref="Vector"/> to compare the table's vector with.</param>
-    /// <param name="limit">The maximum number of similarity results to return.</param>
-    /// <param name="options">The options that control the behavior of the search.</param>
-    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
-    /// <returns>An asynchronous stream of result objects that the nearest matches to the <see cref="Vector"/>.</returns>
-    IAsyncEnumerable<(Dictionary<string, object?> Row, double Distance)> GetNearestMatchesAsync<TRecord>(string tableName, CollectionModel model, VectorPropertyModel vectorProperty, object vectorValue, int limit,
-        RecordSearchOptions<TRecord> options, CancellationToken cancellationToken = default);
-
-    IAsyncEnumerable<Dictionary<string, object?>> GetMatchingRecordsAsync<TRecord>(string tableName, CollectionModel model,
-        Expression<Func<TRecord, bool>> filter, int top, FilteredRecordRetrievalOptions<TRecord> options, CancellationToken cancellationToken = default);
-}
diff --git a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresCollection.cs b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresCollection.cs
index c3222fd2ea48..24717cb8c51f 100644
--- a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresCollection.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresCollection.cs
@@ -20,7 +20,7 @@ namespace Microsoft.SemanticKernel.Connectors.PgVector;
 /// <typeparam name="TKey">The type of the key.</typeparam>
 /// <typeparam name="TRecord">The type of the record.</typeparam>
 #pragma warning disable CA1711 // Identifiers should not have incorrect suffix
-public sealed class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRecord>
+public sealed class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TKey, TRecord>, IDisposable
 #pragma warning restore CA1711 // Identifiers should not have incorrect suffix
     where TKey : notnull
     where TRecord : class
@@ -32,7 +32,7 @@ public sealed class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TK
     private readonly VectorStoreCollectionMetadata _collectionMetadata;
 
     /// <summary>Postgres client that is used to interact with the database.</summary>
-    private readonly IPostgresVectorStoreDbClient _client;
+    private readonly PostgresDbClient _client;
 
     /// <summary>The model for this collection.</summary>
     private readonly CollectionModel _model;
@@ -48,9 +48,24 @@ public sealed class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TK
     /// </summary>
     /// <param name="dataSource">The data source to use for connecting to the database.</param>
     /// <param name="name">The name of the collection.</param>
+    /// <param name="ownsDataSource">A value indicating whether the data source should be disposed after the collection is disposed.</param>
     /// <param name="options">Optional configuration options for this class.</param>
-    public PostgresCollection(NpgsqlDataSource dataSource, string name, PostgresCollectionOptions? options = default)
-        : this(new PostgresDbClient(dataSource), name, options)
+    public PostgresCollection(NpgsqlDataSource dataSource, string name, bool ownsDataSource, PostgresCollectionOptions? options = default)
+        : this(new PostgresDbClient(dataSource, options?.Schema, ownsDataSource), name, options)
+    {
+        Verify.NotNull(dataSource);
+    }
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="PostgresCollection{TKey, TRecord}"/> class.
+    /// </summary>
+    /// <param name="connectionString">Postgres database connection string.</param>
+    /// <param name="name">The name of the collection.</param>
+    /// <param name="options">Optional configuration options for this class.</param>
+    public PostgresCollection(string connectionString, string name, PostgresCollectionOptions? options = default)
+#pragma warning disable CA2000 // Dispose objects before losing scope
+        : this(PostgresUtils.CreateDataSource(connectionString), name, ownsDataSource: true, options)
+#pragma warning restore CA2000 // Dispose objects before losing scope
     {
     }
 
@@ -63,7 +78,7 @@ public PostgresCollection(NpgsqlDataSource dataSource, string name, PostgresColl
     /// <remarks>
     /// This constructor is internal. It allows internal code to create an instance of this class with a custom client.
     /// </remarks>
-    internal PostgresCollection(IPostgresVectorStoreDbClient client, string name, PostgresCollectionOptions? options = default)
+    internal PostgresCollection(PostgresDbClient client, string name, PostgresCollectionOptions? options = default)
     {
         // Verify.
         Verify.NotNull(client);
@@ -86,6 +101,9 @@ internal PostgresCollection(IPostgresVectorStoreDbClient client, string name, Po
         };
     }
 
+    /// <inheritdoc/>
+    public void Dispose() => this._client.Dispose();
+
     /// <inheritdoc/>
     public override Task<bool> CollectionExistsAsync(CancellationToken cancellationToken = default)
     {
diff --git a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresDbClient.cs b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresDbClient.cs
index c016096ace68..e6ee0c002dee 100644
--- a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresDbClient.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresDbClient.cs
@@ -21,17 +21,40 @@ namespace Microsoft.SemanticKernel.Connectors.PgVector;
 /// </remarks>
 /// <param name="dataSource">Postgres data source.</param>
 /// <param name="schema">Schema of collection tables.</param>
+/// <param name="ownsDataSource">A value indicating whether the data source should be disposed after the collection is disposed.</param>
 [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "We need to build the full table name using schema and collection, it does not support parameterized passing.")]
-internal class PostgresDbClient(NpgsqlDataSource dataSource, string schema = PostgresConstants.DefaultSchema) : IPostgresVectorStoreDbClient
+internal sealed class PostgresDbClient(NpgsqlDataSource dataSource, string? schema, bool ownsDataSource) : IDisposable
 {
-    private readonly string _schema = schema;
+    private readonly string _schema = schema ?? PostgresVectorStoreOptions.Default.Schema;
+    private int _referenceCount = 1;
 
     private readonly NpgsqlConnectionStringBuilder _connectionStringBuilder = new(dataSource.ConnectionString);
+    private readonly bool _ownsDataSource = ownsDataSource;
 
     public NpgsqlDataSource DataSource { get; } = dataSource;
 
     public string? DatabaseName => this._connectionStringBuilder.Database;
 
+    public void Dispose()
+    {
+        // An instance of PostgresDbClient can be shared between a single store and multiple collections.
+        // The reference count is used to track how many collections are using this instance.
+        // When the number gets to zero, the DataSource is getting disposed.
+        if (Interlocked.Decrement(ref this._referenceCount) == 0)
+        {
+            if (this._ownsDataSource)
+            {
+                this.DataSource.Dispose();
+            }
+        }
+    }
+
+    internal PostgresDbClient IncreaseReferenceCount()
+    {
+        Interlocked.Increment(ref this._referenceCount);
+        return this;
+    }
+
     /// <inheritdoc />
     public async Task<bool> DoesTableExistsAsync(string tableName, CancellationToken cancellationToken = default)
     {
diff --git a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresServiceCollectionExtensions.cs
index 1fd91e4e1c66..0366d68511c7 100644
--- a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresServiceCollectionExtensions.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresServiceCollectionExtensions.cs
@@ -34,7 +34,9 @@ public static IServiceCollection AddPostgresVectorStore(this IServiceCollection
                     EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>()
                 };
 
-                return new PostgresVectorStore(dataSource, options);
+                // The data source has been solved from the DI container, so we do not own it.
+                bool ownsDataSource = false;
+                return new PostgresVectorStore(dataSource, ownsDataSource, options);
             });
 
         return services;
@@ -50,28 +52,16 @@ public static IServiceCollection AddPostgresVectorStore(this IServiceCollection
     /// <returns>The service collection.</returns>
     public static IServiceCollection AddPostgresVectorStore(this IServiceCollection services, string connectionString, PostgresVectorStoreOptions? options = default, string? serviceId = default)
     {
-        string? npgsqlServiceId = serviceId == null ? default : $"{serviceId}_NpgsqlDataSource";
-        // Register NpgsqlDataSource to ensure proper disposal.
-        services.AddKeyedSingleton<NpgsqlDataSource>(
-            npgsqlServiceId,
-            (sp, obj) =>
-            {
-                NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionString);
-                dataSourceBuilder.UseVector();
-                return dataSourceBuilder.Build();
-            });
-
         services.AddKeyedSingleton<VectorStore>(
             serviceId,
             (sp, obj) =>
             {
-                var dataSource = sp.GetRequiredKeyedService<NpgsqlDataSource>(npgsqlServiceId);
                 options ??= sp.GetService<PostgresVectorStoreOptions>() ?? new()
                 {
                     EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>()
                 };
 
-                return new PostgresVectorStore(dataSource, options);
+                return new PostgresVectorStore(connectionString, options);
             });
 
         return services;
@@ -106,7 +96,9 @@ public static IServiceCollection AddPostgresVectorStoreRecordCollection<TKey, TR
                     EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>()
                 };
 
-                return (new PostgresCollection<TKey, TRecord>(dataSource, collectionName, options) as VectorStoreCollection<TKey, TRecord>)!;
+                // The data source has been solved from the DI container, so we do not own it.
+                bool ownsDataSource = false;
+                return (new PostgresCollection<TKey, TRecord>(dataSource, collectionName, ownsDataSource, options) as VectorStoreCollection<TKey, TRecord>)!;
             });
 
         AddVectorizedSearch<TKey, TRecord>(services, serviceId);
@@ -135,29 +127,16 @@ public static IServiceCollection AddPostgresVectorStoreRecordCollection<TKey, TR
         where TKey : notnull
         where TRecord : class
     {
-        string? npgsqlServiceId = serviceId == null ? default : $"{serviceId}_NpgsqlDataSource";
-        // Register NpgsqlDataSource to ensure proper disposal.
-        services.AddKeyedSingleton<NpgsqlDataSource>(
-            npgsqlServiceId,
-            (sp, obj) =>
-            {
-                NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionString);
-                dataSourceBuilder.UseVector();
-                return dataSourceBuilder.Build();
-            });
-
         services.AddKeyedSingleton<VectorStoreCollection<TKey, TRecord>>(
             serviceId,
             (sp, obj) =>
             {
-                var dataSource = sp.GetRequiredKeyedService<NpgsqlDataSource>(npgsqlServiceId);
-
                 options ??= sp.GetService<PostgresCollectionOptions>() ?? new()
                 {
                     EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>()
                 };
 
-                return (new PostgresCollection<TKey, TRecord>(dataSource, collectionName, options) as VectorStoreCollection<TKey, TRecord>)!;
+                return (new PostgresCollection<TKey, TRecord>(connectionString, collectionName, options) as VectorStoreCollection<TKey, TRecord>)!;
             });
 
         AddVectorizedSearch<TKey, TRecord>(services, serviceId);
diff --git a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresUtils.cs b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresUtils.cs
index 93c012642c5d..fbf7dd4309eb 100644
--- a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresUtils.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresUtils.cs
@@ -63,4 +63,13 @@ public static async IAsyncEnumerable<T> WrapAsyncEnumerableAsync<T>(
             yield return item;
         }
     }
+
+    internal static NpgsqlDataSource CreateDataSource(string connectionString)
+    {
+        Verify.NotNullOrWhiteSpace(connectionString);
+
+        NpgsqlDataSourceBuilder sourceBuilder = new(connectionString);
+        sourceBuilder.UseVector();
+        return sourceBuilder.Build();
+    }
 }
diff --git a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresVectorStore.cs b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresVectorStore.cs
index 9812496a275b..bb32909f453c 100644
--- a/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresVectorStore.cs
+++ b/dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresVectorStore.cs
@@ -13,10 +13,9 @@ namespace Microsoft.SemanticKernel.Connectors.PgVector;
 /// <summary>
 /// Represents a vector store implementation using PostgreSQL.
 /// </summary>
-public sealed class PostgresVectorStore : VectorStore
+public sealed class PostgresVectorStore : VectorStore, IDisposable
 {
-    private readonly IPostgresVectorStoreDbClient _postgresClient;
-    private readonly NpgsqlDataSource? _dataSource;
+    private readonly PostgresDbClient _client;
 
     /// <summary>Metadata about vector store.</summary>
     private readonly VectorStoreMetadata _metadata;
@@ -33,48 +32,43 @@ public sealed class PostgresVectorStore : VectorStore
     /// Initializes a new instance of the <see cref="PostgresVectorStore"/> class.
     /// </summary>
     /// <param name="dataSource">Postgres data source.</param>
+    /// <param name="ownsDataSource">A value indicating whether the data source should be disposed after the vector store is disposed.</param>
     /// <param name="options">Optional configuration options for this class</param>
-    public PostgresVectorStore(NpgsqlDataSource dataSource, PostgresVectorStoreOptions? options = default)
+    public PostgresVectorStore(NpgsqlDataSource dataSource, bool ownsDataSource, PostgresVectorStoreOptions? options = default)
     {
-        this._dataSource = dataSource;
+        Verify.NotNull(dataSource);
 
-        options ??= new();
-        this._schema = options.Schema;
-        this._embeddingGenerator = options.EmbeddingGenerator;
-        this._postgresClient = new PostgresDbClient(this._dataSource, this._schema);
+        this._schema = options?.Schema ?? PostgresVectorStoreOptions.Default.Schema;
+        this._embeddingGenerator = options?.EmbeddingGenerator;
+        this._client = new PostgresDbClient(dataSource, this._schema, ownsDataSource);
 
         this._metadata = new()
         {
             VectorStoreSystemName = PostgresConstants.VectorStoreSystemName,
-            VectorStoreName = this._postgresClient.DatabaseName
+            VectorStoreName = this._client.DatabaseName
         };
     }
 
     /// <summary>
     /// Initializes a new instance of the <see cref="PostgresVectorStore"/> class.
     /// </summary>
-    /// <param name="postgresDbClient">An instance of <see cref="IPostgresVectorStoreDbClient"/>.</param>
-    /// <param name="options">Optional configuration options for this class</param>
-    internal PostgresVectorStore(IPostgresVectorStoreDbClient postgresDbClient, PostgresVectorStoreOptions? options = default)
+    /// <param name="connectionString">Postgres database connection string.</param>
+    /// <param name="options">Optional configuration options for this class.</param>
+    public PostgresVectorStore(string connectionString, PostgresVectorStoreOptions? options = default)
+#pragma warning disable CA2000 // Dispose objects before losing scope
+        : this(PostgresUtils.CreateDataSource(connectionString), ownsDataSource: true, options)
+#pragma warning restore CA2000 // Dispose objects before losing scope
     {
-        this._postgresClient = postgresDbClient;
-
-        options ??= PostgresVectorStoreOptions.Default;
-        this._schema = options.Schema;
-        this._embeddingGenerator = options.EmbeddingGenerator;
-
-        this._metadata = new()
-        {
-            VectorStoreSystemName = PostgresConstants.VectorStoreSystemName,
-            VectorStoreName = this._postgresClient.DatabaseName
-        };
     }
 
+    /// <inheritdoc/>
+    public void Dispose() => this._client.Dispose();
+
     /// <inheritdoc />
     public override IAsyncEnumerable<string> ListCollectionNamesAsync(CancellationToken cancellationToken = default)
     {
         return PostgresUtils.WrapAsyncEnumerableAsync(
-            this._postgresClient.GetTablesAsync(cancellationToken),
+            this._client.GetTablesAsync(cancellationToken),
             "ListCollectionNames",
             this._metadata
         );
@@ -88,7 +82,7 @@ public override PostgresCollection<TKey, TRecord> GetCollection<TKey, TRecord>(s
     public override VectorStoreCollection<TKey, TRecord> GetCollection<TKey, TRecord>(string name, VectorStoreRecordDefinition? vectorStoreRecordDefinition = null)
 #endif
         => new PostgresCollection<TKey, TRecord>(
-            this._postgresClient,
+            this._client.IncreaseReferenceCount(),
             name,
             new PostgresCollectionOptions()
             {
@@ -121,7 +115,7 @@ public override Task DeleteCollectionAsync(string name, CancellationToken cancel
         return
             serviceKey is not null ? null :
             serviceType == typeof(VectorStoreMetadata) ? this._metadata :
-            serviceType == typeof(NpgsqlDataSource) ? this._dataSource :
+            serviceType == typeof(NpgsqlDataSource) ? this._client.DataSource :
             serviceType.IsInstanceOfType(this) ? this :
             null;
     }
diff --git a/dotnet/src/Connectors/Connectors.PgVector.UnitTests/PostgresCollectionTests.cs b/dotnet/src/Connectors/Connectors.PgVector.UnitTests/PostgresCollectionTests.cs
index 0c002c727500..f4d9157d4b8e 100644
--- a/dotnet/src/Connectors/Connectors.PgVector.UnitTests/PostgresCollectionTests.cs
+++ b/dotnet/src/Connectors/Connectors.PgVector.UnitTests/PostgresCollectionTests.cs
@@ -2,12 +2,8 @@
 
 using System;
 using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
 using Microsoft.Extensions.VectorData;
 using Microsoft.SemanticKernel.Connectors.PgVector;
-using Moq;
-using Pgvector;
 using Xunit;
 
 namespace SemanticKernel.Connectors.PgVector.UnitTests;
@@ -16,46 +12,6 @@ public class PostgresCollectionTests
 {
     private const string TestCollectionName = "testcollection";
 
-    private readonly Mock<IPostgresVectorStoreDbClient> _postgresClientMock;
-    private readonly CancellationToken _testCancellationToken = new(false);
-
-    public PostgresCollectionTests()
-    {
-        this._postgresClientMock = new Mock<IPostgresVectorStoreDbClient>(MockBehavior.Strict);
-        this._postgresClientMock.Setup(l => l.DatabaseName).Returns("TestDatabase");
-    }
-
-    [Fact]
-    public async Task CreatesCollectionForGenericModelAsync()
-    {
-        // Arrange
-        var recordDefinition = new VectorStoreRecordDefinition
-        {
-            Properties = [
-                new VectorStoreKeyProperty("HotelId", typeof(int)),
-                new VectorStoreDataProperty("HotelName", typeof(string)) { IsIndexed = true, IsFullTextIndexed = true },
-                new VectorStoreDataProperty("HotelCode", typeof(int)) { IsIndexed = true },
-                new VectorStoreDataProperty("ParkingIncluded", typeof(bool)) { IsIndexed = true, StoragePropertyName = "parking_is_included" },
-                new VectorStoreDataProperty("HotelRating", typeof(float)) { IsIndexed = true },
-                new VectorStoreDataProperty("Tags", typeof(List<string>)),
-                new VectorStoreDataProperty("Description", typeof(string)),
-                new VectorStoreVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory<float>?), 100) { DistanceFunction = DistanceFunction.ManhattanDistance }
-            ]
-        };
-        var options = new PostgresCollectionOptions()
-        {
-            VectorStoreRecordDefinition = recordDefinition
-        };
-        var sut = new PostgresCollection<object, Dictionary<string, object?>>(this._postgresClientMock.Object, TestCollectionName, options);
-        this._postgresClientMock.Setup(x => x.DoesTableExistsAsync(TestCollectionName, this._testCancellationToken)).ReturnsAsync(false);
-
-        // Act
-        var exists = await sut.CollectionExistsAsync();
-
-        // Assert.
-        Assert.False(exists);
-    }
-
     [Fact]
     public void ThrowsForUnsupportedType()
     {
@@ -73,136 +29,6 @@ public void ThrowsForUnsupportedType()
         };
 
         // Act & Assert
-        Assert.Throws<NotSupportedException>(() => new PostgresCollection<object, Dictionary<string, object?>>(this._postgresClientMock.Object, TestCollectionName, options));
-    }
-
-    [Fact]
-    public async Task UpsertRecordAsyncProducesExpectedClientCallAsync()
-    {
-        // Arrange
-        Dictionary<string, object?>? capturedArguments = null;
-
-        var sut = new PostgresCollection<int, PostgresHotel<int>>(this._postgresClientMock.Object, TestCollectionName);
-        var record = new PostgresHotel<int>
-        {
-            HotelId = 1,
-            HotelName = "Hotel 1",
-            HotelCode = 1,
-            HotelRating = 4.5f,
-            ParkingIncluded = true,
-            Tags = ["tag1", "tag2"],
-            Description = "A hotel",
-            DescriptionEmbedding = new ReadOnlyMemory<float>([1.0f, 2.0f, 3.0f, 4.0f])
-        };
-
-        this._postgresClientMock.Setup(x => x.UpsertAsync(
-            TestCollectionName,
-            It.IsAny<Dictionary<string, object?>>(),
-            "HotelId",
-            this._testCancellationToken))
-            .Callback<string, Dictionary<string, object?>, string, CancellationToken>((collectionName, args, key, ct) => capturedArguments = args)
-            .Returns(Task.CompletedTask);
-
-        // Act
-        await sut.UpsertAsync(record, cancellationToken: this._testCancellationToken);
-
-        // Assert
-        Assert.NotNull(capturedArguments);
-        Assert.Equal(1, (int)(capturedArguments["HotelId"] ?? 0));
-        Assert.Equal("Hotel 1", (string)(capturedArguments["HotelName"] ?? ""));
-        Assert.Equal(1, (int)(capturedArguments["HotelCode"] ?? 0));
-        Assert.Equal(4.5f, (float)(capturedArguments["HotelRating"] ?? 0.0f));
-        Assert.True((bool)(capturedArguments["parking_is_included"] ?? false));
-        Assert.True(capturedArguments["Tags"] is List<string>);
-        var tags = capturedArguments["Tags"] as List<string>;
-        Assert.Equal(2, tags!.Count);
-        Assert.Equal("tag1", tags[0]);
-        Assert.Equal("tag2", tags[1]);
-        Assert.Equal("A hotel", (string)(capturedArguments["Description"] ?? ""));
-        Assert.NotNull(capturedArguments["DescriptionEmbedding"]);
-        Assert.IsType<Vector>(capturedArguments["DescriptionEmbedding"]);
-        var embedding = ((Vector)capturedArguments["DescriptionEmbedding"]!).ToArray();
-        Assert.Equal(1.0f, embedding[0]);
-        Assert.Equal(2.0f, embedding[1]);
-        Assert.Equal(3.0f, embedding[2]);
-        Assert.Equal(4.0f, embedding[3]);
-    }
-
-    [Fact]
-    public async Task CollectionExistsReturnsValidResultAsync()
-    {
-        // Arrange
-        const string TableName = "CollectionExists";
-
-        this._postgresClientMock.Setup(x => x.DoesTableExistsAsync(TableName, this._testCancellationToken)).ReturnsAsync(true);
-
-        var sut = new PostgresCollection<int, TestRecord<int>>(this._postgresClientMock.Object, TableName);
-
-        // Act
-        var result = await sut.CollectionExistsAsync();
-
-        Assert.True(result);
-    }
-
-    [Fact]
-    public async Task DeleteCollectionCallsClientDeleteAsync()
-    {
-        // Arrange
-        const string TableName = "DeleteCollection";
-
-        this._postgresClientMock.Setup(x => x.DeleteTableAsync(TableName, this._testCancellationToken)).Returns(Task.CompletedTask);
-
-        var sut = new PostgresCollection<int, TestRecord<int>>(this._postgresClientMock.Object, TableName);
-
-        // Act
-        await sut.DeleteCollectionAsync();
-
-        // Assert
-        this._postgresClientMock.Verify(x => x.DeleteTableAsync(TableName, this._testCancellationToken), Times.Once);
-    }
-
-    #region private
-
-    private static void AssertRecord<TKey>(TestRecord<TKey> expectedRecord, TestRecord<TKey>? actualRecord, bool includeVectors)
-    {
-        Assert.NotNull(actualRecord);
-
-        Assert.Equal(expectedRecord.Key, actualRecord.Key);
-        Assert.Equal(expectedRecord.Data, actualRecord.Data);
-
-        if (includeVectors)
-        {
-            Assert.NotNull(actualRecord.Vector);
-            Assert.Equal(expectedRecord.Vector!.Value.ToArray(), actualRecord.Vector.Value.Span.ToArray());
-        }
-        else
-        {
-            Assert.Null(actualRecord.Vector);
-        }
+        Assert.Throws<NotSupportedException>(() => new PostgresCollection<object, Dictionary<string, object?>>("Host=localhost;Database=test;", TestCollectionName, options));
     }
-
-#pragma warning disable CA1812
-    private sealed class TestRecord<TKey>
-    {
-        [VectorStoreKey]
-        public TKey? Key { get; set; }
-
-        [VectorStoreData]
-        public string? Data { get; set; }
-
-        [VectorStoreVector(Dimensions: 4, DistanceFunction = DistanceFunction.CosineDistance)]
-        public ReadOnlyMemory<float>? Vector { get; set; }
-    }
-
-    private sealed class TestRecordWithoutVectorProperty<TKey>
-    {
-        [VectorStoreKey]
-        public TKey? Key { get; set; }
-
-        [VectorStoreData]
-        public string? Data { get; set; }
-    }
-#pragma warning restore CA1812
-
-    #endregion
 }
diff --git a/dotnet/src/Connectors/Connectors.PgVector.UnitTests/PostgresVectorStoreTests.cs b/dotnet/src/Connectors/Connectors.PgVector.UnitTests/PostgresVectorStoreTests.cs
deleted file mode 100644
index a21d88e71d0b..000000000000
--- a/dotnet/src/Connectors/Connectors.PgVector.UnitTests/PostgresVectorStoreTests.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-﻿// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Extensions.VectorData;
-using Microsoft.SemanticKernel.Connectors.PgVector;
-using Moq;
-using Npgsql;
-using Xunit;
-
-namespace SemanticKernel.Connectors.PgVector.UnitTests;
-
-/// <summary>
-/// Contains tests for the <see cref="PostgresVectorStore"/> class.
-/// </summary>
-public class PostgresVectorStoreTests
-{
-    private const string TestCollectionName = "testcollection";
-
-    private readonly Mock<IPostgresVectorStoreDbClient> _postgresClientMock;
-    private readonly CancellationToken _testCancellationToken = new(false);
-
-    public PostgresVectorStoreTests()
-    {
-        this._postgresClientMock = new Mock<IPostgresVectorStoreDbClient>(MockBehavior.Strict);
-        this._postgresClientMock.Setup(l => l.DatabaseName).Returns("TestDatabase");
-    }
-
-    [Fact]
-    public void GetCollectionReturnsCollection()
-    {
-        // Arrange.
-        var sut = new PostgresVectorStore(this._postgresClientMock.Object);
-
-        // Act.
-        var actual = sut.GetCollection<long, SinglePropsModel<long>>(TestCollectionName);
-
-        // Assert.
-        Assert.NotNull(actual);
-        Assert.IsType<PostgresCollection<long, SinglePropsModel<long>>>(actual);
-    }
-
-    [Fact]
-    public void GetCollectionThrowsForInvalidKeyType()
-    {
-        // Arrange.
-        var sut = new PostgresVectorStore(this._postgresClientMock.Object);
-
-        // Act and Assert.
-        Assert.Throws<NotSupportedException>(() => sut.GetCollection<ulong, SinglePropsModel<ulong>>(TestCollectionName));
-    }
-
-    [Fact]
-    public async Task ListCollectionNamesCallsSDKAsync()
-    {
-        // Arrange
-        var expectedCollections = new List<string> { "fake-collection-1", "fake-collection-2", "fake-collection-3" };
-
-        this._postgresClientMock
-            .Setup(client => client.GetTablesAsync(CancellationToken.None))
-            .Returns(expectedCollections.ToAsyncEnumerable());
-
-        var sut = new PostgresVectorStore(this._postgresClientMock.Object);
-
-        // Act.
-        var actual = sut.ListCollectionNamesAsync(this._testCancellationToken);
-
-        // Assert
-        Assert.NotNull(actual);
-        var actualList = await actual.ToListAsync();
-        Assert.Equal(expectedCollections, actualList);
-    }
-
-    [Fact]
-    public async Task ListCollectionNamesThrowsCorrectExceptionAsync()
-    {
-        // Arrange
-        var expectedCollections = new List<string> { "fake-collection-1", "fake-collection-2", "fake-collection-3" };
-
-        this._postgresClientMock
-            .Setup(client => client.GetTablesAsync(CancellationToken.None))
-            .Returns(this.ThrowingAsyncEnumerableAsync);
-
-        var sut = new PostgresVectorStore(this._postgresClientMock.Object);
-
-        // Act.
-        var actual = sut.ListCollectionNamesAsync(this._testCancellationToken);
-
-        // Assert
-        Assert.NotNull(actual);
-        await Assert.ThrowsAsync<VectorStoreException>(async () => await actual.ToListAsync());
-    }
-
-    private async IAsyncEnumerable<string> ThrowingAsyncEnumerableAsync()
-    {
-        int itemIndex = 0;
-        await foreach (var item in new List<string> { "item1", "item2", "item3" }.ToAsyncEnumerable())
-        {
-            if (itemIndex == 1)
-            {
-                throw new NpgsqlException("Test exception");
-            }
-            yield return item;
-            itemIndex++;
-        }
-    }
-
-    public sealed class SinglePropsModel<TKey>
-    {
-        [VectorStoreKey]
-        public required TKey Key { get; set; }
-
-        [VectorStoreData]
-        public string Data { get; set; } = string.Empty;
-
-        [VectorStoreVector(4)]
-        public ReadOnlyMemory<float>? Vector { get; set; }
-
-        public string? NotAnnotated { get; set; }
-    }
-}
diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/PgVector/CommonPostgresVectorStoreRecordCollectionTests.cs b/dotnet/src/IntegrationTests/Connectors/Memory/PgVector/CommonPostgresVectorStoreRecordCollectionTests.cs
index 921f07730e77..689763084d88 100644
--- a/dotnet/src/IntegrationTests/Connectors/Memory/PgVector/CommonPostgresVectorStoreRecordCollectionTests.cs
+++ b/dotnet/src/IntegrationTests/Connectors/Memory/PgVector/CommonPostgresVectorStoreRecordCollectionTests.cs
@@ -21,7 +21,7 @@ public class CommonPostgresVectorStoreRecordCollectionTests(PostgresVectorStoreF
 
     protected override VectorStoreCollection<string, TRecord> GetTargetRecordCollection<TRecord>(string recordCollectionName, VectorStoreRecordDefinition? vectorStoreRecordDefinition)
     {
-        return new PostgresCollection<string, TRecord>(fixture.DataSource!, recordCollectionName, new()
+        return new PostgresCollection<string, TRecord>(fixture.DataSource!, recordCollectionName, ownsDataSource: false, new()
         {
             VectorStoreRecordDefinition = vectorStoreRecordDefinition
         });
diff --git a/dotnet/src/IntegrationTests/Connectors/Memory/PgVector/PostgresVectorStoreFixture.cs b/dotnet/src/IntegrationTests/Connectors/Memory/PgVector/PostgresVectorStoreFixture.cs
index 3fb8aebc5fad..92bddf5bf3b3 100644
--- a/dotnet/src/IntegrationTests/Connectors/Memory/PgVector/PostgresVectorStoreFixture.cs
+++ b/dotnet/src/IntegrationTests/Connectors/Memory/PgVector/PostgresVectorStoreFixture.cs
@@ -42,7 +42,7 @@ public PostgresVectorStoreFixture()
     /// <summary>
     /// Gets a vector store to use for tests.
     /// </summary>
-    public VectorStore VectorStore => new PostgresVectorStore(this.DataSource!);
+    public VectorStore VectorStore => new PostgresVectorStore(this.DataSource!, ownsDataSource: false);
 
     /// <summary>
     /// Get a database connection
diff --git a/dotnet/src/VectorDataIntegrationTests/PgVectorIntegrationTests/Support/PostgresTestStore.cs b/dotnet/src/VectorDataIntegrationTests/PgVectorIntegrationTests/Support/PostgresTestStore.cs
index a2c6409f8605..8abd37437e30 100644
--- a/dotnet/src/VectorDataIntegrationTests/PgVectorIntegrationTests/Support/PostgresTestStore.cs
+++ b/dotnet/src/VectorDataIntegrationTests/PgVectorIntegrationTests/Support/PostgresTestStore.cs
@@ -8,7 +8,9 @@
 
 namespace PgVectorIntegrationTests.Support;
 
+#pragma warning disable CA1001 // Types that own disposable fields should be disposable
 internal sealed class PostgresTestStore : TestStore
+#pragma warning restore CA1001 // Types that own disposable fields should be disposable
 {
     public static PostgresTestStore Instance { get; } = new();
 
@@ -24,7 +26,11 @@ internal sealed class PostgresTestStore : TestStore
     public override VectorStore DefaultVectorStore => this._defaultVectorStore ?? throw new InvalidOperationException("Not initialized");
 
     public PostgresVectorStore GetVectorStore(PostgresVectorStoreOptions options)
-        => new(this.DataSource, options);
+    {
+        // The DataSource is shared with the static instance, we don't want any of the tests to dispose it.
+        bool ownsDataSource = false;
+        return new(this.DataSource, ownsDataSource, options);
+    }
 
     private PostgresTestStore()
     {
@@ -56,11 +62,14 @@ protected override async Task StartAsync()
         await command.ExecuteNonQueryAsync();
         await connection.ReloadTypesAsync();
 
-        this._defaultVectorStore = new(this._dataSource);
+        // It's a shared static instance, we don't want any of the tests to dispose it.
+        bool ownsDataSource = false;
+        this._defaultVectorStore = new(this._dataSource, ownsDataSource);
     }
 
     protected override async Task StopAsync()
     {
+        this._defaultVectorStore!.Dispose();
         await this._dataSource!.DisposeAsync();
         await s_container.StopAsync();
     }


Reviews:
user: login: adamsitnik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T17:08:08Z
user: login: adamsitnik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T17:09:48Z
user: login: adamsitnik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T17:11:30Z
user: login: roji
body: I'm a little bit worried by having a concrete implementation (PostgresVectorStore) being IDisposable without the abstraction (VectorStore) being IDisposable as well... This means that if the user creates a PostgresVectorStore but then interacts with it via the abstraction (because they need to support multiple databases, or just for testing with fakes/mocks), they can no longer dispose it without explicitly checking if it implements IDisposable etc. (they also lose provider warnings on undisposed stuff).

Because of this, one part of me wants to leave PostgresVectorStore non-disposable, and say that it's the responsibility of the caller to dispose the NpgsqlDataSource - or at the very least, default to OwnsDataSource being false... This way the ownership becomes opt-in, and users are much less likely to be unaware/bitten by this by accident (we'd document it very clearly).

It's interesting to compare with SslStream, which also has a [constructor](https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream.-ctor?view=net-9.0#system-net-security-sslstream-ctor(system-io-stream-system-boolean)) that allows specifying whether to close the inner stream when the SslStream is closed. The default behavior ([the constructor without this parameter](https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream.-ctor?view=net-9.0#system-net-security-sslstream-ctor(system-io-stream))) is indeed to close the inner (so to "own"), but then, Stream implement IDisposable, whereas the MEVD abstractions do not. Of course, we could decide to VectorStore and VectorStoreCollection implement IDisposable, but that would force users of all providers to dispose though it's needed for almost none of them.

So I'm leaning towards non-owned as the default, with an opt-in - but I want to hear what you think about it. Introducing even opt-in ownership would allow the following nice things:

* We can add convenience constructors to PostgresVectorStore (and PostgresCollection) which simply accept a connection string, and internally create an (owned) NpgsqlDataSource.
* Note that there's a DI AddPostgresVectorStore (and collection) which accepts a connection string; it internally creates a data source, and then registers it as singleton in the DI container so that it gets disposed (this isn't great since the user didn't ask for an NpgsqlDataSource to get registered, and this might actually conflict with another DI-registered one). With ownership, the PostgresVectorStore (or collection) can own the data source and it would get disposed without needing to register the data source in DI any moer.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T20:11:52Z
user: login: adamsitnik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T11:31:14Z
user: login: adamsitnik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T11:32:19Z
user: login: adamsitnik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T11:45:38Z
user: login: roji
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T19:58:23Z
user: login: adamsitnik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T09:45:53Z
user: login: roji
body: LGTM!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T13:55:19Z
user: login: adamsitnik
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T14:01:31Z

Review comments:
diff_hunk: @@ -110,7 +103,6 @@ public override Task DeleteCollectionAsync(string name, CancellationToken cancel
         return
             serviceKey is not null ? null :
             serviceType == typeof(VectorStoreMetadata) ? this._metadata :
-            serviceType == typeof(NpgsqlDataSource) ? this._dataSource :
path: dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStore.cs
user: login: adamsitnik
body: I believe it was a bug, we should not be exposing the data source by `GetService`
created_at: 2025-05-05T17:08:07Z
author_association: MEMBER
diff_hunk: @@ -16,46 +13,6 @@ public class PostgresCollectionTests
 {
     private const string TestCollectionName = "testcollection";
 
-    private readonly Mock<IPostgresVectorStoreDbClient> _postgresClientMock;
-    private readonly CancellationToken _testCancellationToken = new(false);
-
-    public PostgresCollectionTests()
-    {
-        this._postgresClientMock = new Mock<IPostgresVectorStoreDbClient>(MockBehavior.Strict);
-        this._postgresClientMock.Setup(l => l.DatabaseName).Returns("TestDatabase");
-    }
-
-    [Fact]
-    public async Task CreatesCollectionForGenericModelAsync()
-    {
-        // Arrange
-        var recordDefinition = new VectorStoreRecordDefinition
-        {
-            Properties = [
-                new VectorStoreKeyProperty("HotelId", typeof(int)),
-                new VectorStoreDataProperty("HotelName", typeof(string)) { IsIndexed = true, IsFullTextIndexed = true },
-                new VectorStoreDataProperty("HotelCode", typeof(int)) { IsIndexed = true },
-                new VectorStoreDataProperty("ParkingIncluded", typeof(bool)) { IsIndexed = true, StoragePropertyName = "parking_is_included" },
-                new VectorStoreDataProperty("HotelRating", typeof(float)) { IsIndexed = true },
-                new VectorStoreDataProperty("Tags", typeof(List<string>)),
-                new VectorStoreDataProperty("Description", typeof(string)),
-                new VectorStoreVectorProperty("DescriptionEmbedding", typeof(ReadOnlyMemory<float>?), 100) { DistanceFunction = DistanceFunction.ManhattanDistance }
-            ]
-        };
-        var options = new PostgresCollectionOptions()
-        {
-            VectorStoreRecordDefinition = recordDefinition
-        };
-        var sut = new PostgresCollection<object, Dictionary<string, object?>>(this._postgresClientMock.Object, TestCollectionName, options);
-        this._postgresClientMock.Setup(x => x.DoesTableExistsAsync(TestCollectionName, this._testCancellationToken)).ReturnsAsync(false);
-
-        // Act
-        var exists = await sut.CollectionExistsAsync();
path: dotnet/src/Connectors/Connectors.Postgres.UnitTests/PostgresCollectionTests.cs
user: login: adamsitnik
body: this is covered by the integration tests, I don't see a reason to have internal abstraction just to verify that the public type returns what mock has been configured with
created_at: 2025-05-05T17:09:47Z
author_association: MEMBER
diff_hunk: @@ -25,14 +26,16 @@ public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGene
         [
             services => services
                 .AddSingleton(PostgresTestStore.Instance.DataSource)
-                .AddPostgresVectorStore()
+                .AddPostgresVectorStore(new PostgresVectorStoreOptions() { OwnsDataSource = false })
path: dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/PostgresEmbeddingGenerationTests.cs
user: login: adamsitnik
body: When the DI container is being disposed, it disposes all the `IDisposable` instances that it contains. Since we are using a static data source shared by all the integration tests, it's not desired to dispose it when other tests are running.
created_at: 2025-05-05T17:11:30Z
author_association: MEMBER
diff_hunk: @@ -110,7 +103,6 @@ public override Task DeleteCollectionAsync(string name, CancellationToken cancel
         return
             serviceKey is not null ? null :
             serviceType == typeof(VectorStoreMetadata) ? this._metadata :
-            serviceType == typeof(NpgsqlDataSource) ? this._dataSource :
path: dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStore.cs
user: login: roji
body: I think this was by design; part of the idea behind GetService() is to allow accessing the low-level SDK services that MEVD abstraction wrap, in other providers we expose stuff like the Azure AI client or whatever. For example, say in some context the user needs to do something in the SQL that can't be done via MEVD. With this mechanism, they can extract the data source and execute any custom SQL against it; without it, they have to pass around the NpgsqlDataSource in addition to the PostgresCollection just for this purpose.

Do you think there's an issue with this approach? If the concern is with the disposability introduced in this PR, I think it should be compatible (but I might be missing something)... The contract is quite clear: if the collection owns the data source, the data source gets disposed as part of the collection disposal, and if the user extracted the data source via GetService(), it's their responsibility to make sure they don't use it after the collection is disposed etc.
created_at: 2025-05-05T19:29:32Z
author_association: MEMBER
diff_hunk: @@ -25,14 +26,16 @@ public override VectorStore CreateVectorStore(IEmbeddingGenerator? embeddingGene
         [
             services => services
                 .AddSingleton(PostgresTestStore.Instance.DataSource)
-                .AddPostgresVectorStore()
+                .AddPostgresVectorStore(new PostgresVectorStoreOptions() { OwnsDataSource = false })
path: dotnet/src/VectorDataIntegrationTests/PostgresIntegrationTests/PostgresEmbeddingGenerationTests.cs
user: login: roji
body: Makes sense, maybe paste this as a comment in the tests for context.
created_at: 2025-05-05T19:32:52Z
author_association: MEMBER
diff_hunk: @@ -50,28 +52,16 @@ public static IServiceCollection AddPostgresVectorStore(this IServiceCollection
     /// <returns>The service collection.</returns>
     public static IServiceCollection AddPostgresVectorStore(this IServiceCollection services, string connectionString, PostgresVectorStoreOptions? options = default, string? serviceId = default)
     {
-        string? npgsqlServiceId = serviceId == null ? default : $"{serviceId}_NpgsqlDataSource";
-        // Register NpgsqlDataSource to ensure proper disposal.
path: dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresServiceCollectionExtensions.cs
user: login: adamsitnik
body: with a ctor that takes the connection string, there is no need to register data source anymore
created_at: 2025-05-06T11:31:13Z
author_association: MEMBER
diff_hunk: @@ -29,4 +29,9 @@ public sealed class PostgresCollectionOptions
     /// Gets or sets the default embedding generator for vector properties in this collection.
     /// </summary>
     public IEmbeddingGenerator? EmbeddingGenerator { get; init; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether the data source should be disposed after the collection is disposed.
+    /// </summary>
+    internal bool OwnsDataSource { get; set; }
path: dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresCollectionOptions.cs
user: login: adamsitnik
body: Please keep in mind it's internal as it's not set via the option bag by the user (it's set by the user in the store/collection ctor and or when we pass it around)
created_at: 2025-05-06T11:32:19Z
author_association: MEMBER
diff_hunk: @@ -110,7 +103,6 @@ public override Task DeleteCollectionAsync(string name, CancellationToken cancel
         return
             serviceKey is not null ? null :
             serviceType == typeof(VectorStoreMetadata) ? this._metadata :
-            serviceType == typeof(NpgsqlDataSource) ? this._dataSource :
path: dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresVectorStore.cs
user: login: adamsitnik
body: >  With this mechanism, they can extract the data source and execute any custom SQL against it; without it, they have to pass around the NpgsqlDataSource in addition to the PostgresCollection just for this purpose.

I believe it should not be the responsibility of the vector store but rather the DI container. Just register the data source and the vector store and resolve data source when you need it. Also it's breaking the encapsulation without clear boundary (what are the things we are not going to expose and why?).

Anyway I don't want to die fighting for it, so I am simply going to revert this line.
created_at: 2025-05-06T11:45:38Z
author_association: MEMBER
diff_hunk: @@ -34,7 +34,9 @@ public static IServiceCollection AddPostgresVectorStore(this IServiceCollection
                     EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>()
                 };
 
-                return new PostgresVectorStore(dataSource, options);
+                // The data source has been solved from the DI container, so we do not own it.
+                bool ownsDataSource = false;
path: dotnet/src/Connectors/Connectors.Memory.Postgres/PostgresServiceCollectionExtensions.cs
user: login: roji
body: nit: just inline this?
created_at: 2025-05-06T13:06:17Z
author_association: MEMBER
diff_hunk: @@ -48,9 +48,24 @@ public sealed class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TK
     /// </summary>
     /// <param name="dataSource">The data source to use for connecting to the database.</param>
     /// <param name="name">The name of the collection.</param>
+    /// <param name="ownsDataSource">A value indicating whether the data source should be disposed after the collection is disposed.</param>
     /// <param name="options">Optional configuration options for this class.</param>
-    public PostgresCollection(NpgsqlDataSource dataSource, string name, PostgresCollectionOptions? options = default)
-        : this(new PostgresDbClient(dataSource), name, options)
+    public PostgresCollection(NpgsqlDataSource dataSource, string name, bool ownsDataSource, PostgresCollectionOptions? options = default)
+        : this(new PostgresDbClient(dataSource, options?.Schema, ownsDataSource), name, options)
path: dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresCollection.cs
user: login: adamsitnik
body: By accident I've found and fixed a bug: the schema provided in the option bag was not being passed to the client ctor (so "public" schema was always used for collections created via public ctor)

```diff
- new PostgresDbClient(dataSource)
+ new PostgresDbClient(dataSource, options?.Schema, ownsDataSource)
```
created_at: 2025-05-08T09:45:52Z
author_association: MEMBER
diff_hunk: @@ -70,10 +69,7 @@ public async Task ExampleWithoutDIAsync()
 
         // Initialize the Postgres docker container via the fixtures and construct the Postgres VectorStore.
         await PostgresFixture.ManualInitializeAsync();
-        var dataSourceBuilder = new NpgsqlDataSourceBuilder(TestConfiguration.Postgres.ConnectionString);
-        dataSourceBuilder.UseVector();
-        await using var dataSource = dataSourceBuilder.Build();
-        var vectorStore = new PostgresVectorStore(dataSource);
+        using PostgresVectorStore vectorStore = new(TestConfiguration.Postgres.ConnectionString);
path: dotnet/samples/Concepts/Memory/VectorStore_VectorSearch_MultiStore_Postgres.cs
user: login: roji
body: ❤️  Looks so much nicer!
created_at: 2025-05-08T13:46:27Z
author_association: MEMBER
diff_hunk: @@ -21,17 +21,40 @@ namespace Microsoft.SemanticKernel.Connectors.PgVector;
 /// </remarks>
 /// <param name="dataSource">Postgres data source.</param>
 /// <param name="schema">Schema of collection tables.</param>
+/// <param name="ownsDataSource">A value indicating whether the data source should be disposed after the collection is disposed.</param>
 [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "We need to build the full table name using schema and collection, it does not support parameterized passing.")]
-internal class PostgresDbClient(NpgsqlDataSource dataSource, string schema = PostgresConstants.DefaultSchema) : IPostgresVectorStoreDbClient
+internal sealed class PostgresDbClient(NpgsqlDataSource dataSource, string? schema, bool ownsDataSource) : IDisposable
 {
-    private readonly string _schema = schema;
+    private readonly string _schema = schema ?? PostgresVectorStoreOptions.Default.Schema;
+    private int _referenceCount = 1;
 
     private readonly NpgsqlConnectionStringBuilder _connectionStringBuilder = new(dataSource.ConnectionString);
+    private readonly bool _ownsDataSource = ownsDataSource;
 
     public NpgsqlDataSource DataSource { get; } = dataSource;
 
     public string? DatabaseName => this._connectionStringBuilder.Database;
 
+    public void Dispose()
+    {
+        // An instance of PostgresDbClient can be shared between a single store and multiple collections.
+        // The reference count is used to track how many collections are using this instance.
+        // When the number gets to zero, the DataSource is getting disposed.
+        if (Interlocked.Decrement(ref this._referenceCount) == 0)
+        {
+            if (this._ownsDataSource)
path: dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresDbClient.cs
user: login: roji
body: Invert the condition ordering? Do we even need to track the reference count when `_ownsDataSource` is false?
created_at: 2025-05-08T13:49:39Z
author_association: MEMBER
diff_hunk: @@ -48,9 +48,24 @@ public sealed class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TK
     /// </summary>
     /// <param name="dataSource">The data source to use for connecting to the database.</param>
     /// <param name="name">The name of the collection.</param>
+    /// <param name="ownsDataSource">A value indicating whether the data source should be disposed after the collection is disposed.</param>
     /// <param name="options">Optional configuration options for this class.</param>
-    public PostgresCollection(NpgsqlDataSource dataSource, string name, PostgresCollectionOptions? options = default)
-        : this(new PostgresDbClient(dataSource), name, options)
+    public PostgresCollection(NpgsqlDataSource dataSource, string name, bool ownsDataSource, PostgresCollectionOptions? options = default)
path: dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresCollection.cs
user: login: roji
body: Don't we want to have a constructor overload that doesn't force the user to specify `ownsDataSource` explicitly?
created_at: 2025-05-08T13:50:42Z
author_association: MEMBER
diff_hunk: @@ -34,7 +34,9 @@ public static IServiceCollection AddPostgresVectorStore(this IServiceCollection
                     EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>()
                 };
 
-                return new PostgresVectorStore(dataSource, options);
+                // The data source has been solved from the DI container, so we do not own it.
+                bool ownsDataSource = false;
path: dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresServiceCollectionExtensions.cs
user: login: roji
body: nit: inline the `false`? Doesn't look like we need the variable?
created_at: 2025-05-08T13:52:23Z
author_association: MEMBER
diff_hunk: @@ -106,7 +96,9 @@ public static IServiceCollection AddPostgresVectorStoreRecordCollection<TKey, TR
                     EmbeddingGenerator = sp.GetService<IEmbeddingGenerator>()
                 };
 
-                return (new PostgresCollection<TKey, TRecord>(dataSource, collectionName, options) as VectorStoreCollection<TKey, TRecord>)!;
+                // The data source has been solved from the DI container, so we do not own it.
+                bool ownsDataSource = false;
path: dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresServiceCollectionExtensions.cs
user: login: roji
body: nit: inline?
created_at: 2025-05-08T13:52:49Z
author_association: MEMBER
diff_hunk: @@ -56,11 +62,14 @@ protected override async Task StartAsync()
         await command.ExecuteNonQueryAsync();
         await connection.ReloadTypesAsync();
 
-        this._defaultVectorStore = new(this._dataSource);
+        // It's a shared static instance, we don't want any of the tests to dispose it.
+        bool ownsDataSource = false;
path: dotnet/src/VectorDataIntegrationTests/PgVectorIntegrationTests/Support/PostgresTestStore.cs
user: login: roji
body: nit: inline? Also above
created_at: 2025-05-08T13:54:51Z
author_association: MEMBER
diff_hunk: @@ -48,9 +48,24 @@ public sealed class PostgresCollection<TKey, TRecord> : VectorStoreCollection<TK
     /// </summary>
     /// <param name="dataSource">The data source to use for connecting to the database.</param>
     /// <param name="name">The name of the collection.</param>
+    /// <param name="ownsDataSource">A value indicating whether the data source should be disposed after the collection is disposed.</param>
     /// <param name="options">Optional configuration options for this class.</param>
-    public PostgresCollection(NpgsqlDataSource dataSource, string name, PostgresCollectionOptions? options = default)
-        : this(new PostgresDbClient(dataSource), name, options)
+    public PostgresCollection(NpgsqlDataSource dataSource, string name, bool ownsDataSource, PostgresCollectionOptions? options = default)
path: dotnet/src/Connectors/Connectors.Memory.PgVector/PostgresCollection.cs
user: login: adamsitnik
body: > Don't we want to have a constructor overload that doesn't force the user to specify ownsDataSource explicitly?

I wanted to force the user to decide. So there are no surprises that something does or does not get disposed. (context: we were not sure which value should be the default)
created_at: 2025-05-08T14:01:31Z
author_association: MEMBER

PR comments:
user: login: adamsitnik
created_at: 2025-05-06T09:25:33Z
body: > We can add convenience constructors to PostgresVectorStore (and PostgresCollection) which simply accept a connection string, and internally create an (owned) NpgsqlDataSource.

I like the idea of having a `string` ctor (for both types). It's then obvious that we own it.

When it comes to the ctor accepting `NpgsqlDataSource` , we could force the users to think about it by making `bool ownsDataSource` a mandatory argument (if not, I would prefer it to be set to true by default since all the .NET types that provide such flags do that).

And then the `store.GetCollection` would simply pass the flag it has to the collection ctor.

> I'm a little bit worried by having a concrete implementation (PostgresVectorStore) being IDisposable without the abstraction (VectorStore) being IDisposable as well

I see your point. We have exact same problem in `Microsoft.Extensions.Configuration`: https://github.com/dotnet/runtime/issues/86456

>  they can no longer dispose it without explicitly checking if it implements IDisposable etc. (they also lose provider warnings on undisposed stuff).

Making the concrete types implement `IDisposable` helps, but does not solve the problem for all the scenarios. Unless `NpgsqlDataSource` or the resources that it owns implement a finalizer? 

Since only few implementations of the abstraction own disposable resources, I am not convinced that we should make the abstractions derive from `IDisposable`

@bartonjs @stephentoub what is our guidance for scenario where an implementation of an abstraction owns disposable resources? Should both the abstraction and implementation implement `IDisposable` or just the implementation?




author_association: MEMBER
user: login: roji
created_at: 2025-05-06T10:00:26Z
body: > When it comes to the ctor accepting NpgsqlDataSource , we could force the users to think about it by making bool ownsDataSource a mandatory argument (if not, I would prefer it to be set to true by default since all the .NET types that provide such flags do that).

We could - that would make PostgresVectorStore (and PostgresCollection) similar to SslStream indeed.

> (if not, I would prefer it to be set to true by default since all the .NET types that provide such flags do that)

At least with the SslStream example which I brought above, the difference is that Stream (the abstraction) implements IDisposable while MEVD's VectorStore doesn't (at least currently). That might be a reason to default to not owning. But I think I can can see it the other way too...

BTW an alternative to the reference-counting logic you're proposing in this PR is to simply say that the NpgsqlDataSource is (optionally) owned by the wrapper you pass it to (PostgresVectorStore or PostgresCollection). So instead of tracking how many PostgresCollections have been instantiated from the same PostgresVectorStore, we could say that if you initialize a PostgresVectorStore with an NpgsqlDataSource, that PVS owns the NDS and the latter is disposed when the former is. I think it may be easier/clearer to understand, and it's the user's responsibility to not dispose the PVS before they're done with PostgresCollections that have been retrieved from it.
author_association: MEMBER
user: login: adamsitnik
created_at: 2025-05-06T10:23:16Z
body: >  I think it may be easier/clearer to understand, and it's the user's responsibility to not dispose the PVS before they're done with PostgresCollections that have been retrieved from it.

I believe it's very simple with what I've proposed: you just dispose things when you stop using them. The alternative you have described is a little bit more tricky: make sure you don't dispose the store before you stop using the collections you have used to create it.

However, I don't have a strong opinion about it and I can remove the reference counting if you want me to.


author_association: MEMBER
user: login: adamsitnik
created_at: 2025-05-06T16:35:14Z
body: I am back with some data: out of 13 connectors, 3 wrap a resource that implements `IDisposable`, one of which is `HttpClient`

`AzureAISearchCollection`: wraps `SearchClient` and `SearchIndexClient` that don't implement `IDisposable`
`CosmosMongoCollection` and `MongoCollection`: wrap `IMongoDatabase` and `IMongoCollection` that don't implement `IDisposable`
`CosmosNoSqlCollection`: wraps `Database` that doesn't implement `IDisposable`
`InMemoryCollection`: wraps `ConcurrentDictionary`
`PineconeCollection`: wraps `PineconeClient` and `IndexClient` that don't implement `IDisposable`
`PostgresCollection`: wraps `NpgsqlDataSource` which **implements** `IDisposable`
`QdrantCollection`: wraps `QdrantClient` which **implements** `IDisposable`
`RedisHashSetCollection` and `RedisJsonCollection`: wrap `IDatabase` that doesn't implement `IDisposable`
`SqlServerCollection` and `SqliteCollection`: don't wrap any `IDisposable` resource (create a dedicated connection for every operation)
`WeaviateCollection`: wraps `HttpClient` which **implements** `IDisposable`
author_association: MEMBER
user: login: bartonjs
created_at: 2025-05-06T16:55:42Z
body: > @bartonjs @stephentoub what is our guidance for scenario where an implementation of an abstraction owns disposable resources? Should both the abstraction and implementation implement IDisposable or just the implementation?

At the end of the day, it's a judgment call, which is captured in

> CONSIDER implementing the Basic Dispose Pattern on classes that
themselves don’t hold unmanaged resources or disposable objects, but
are likely to have subtypes that do.
>
> A great example of this is the System.IO.Stream class. Although it is
an abstract base class that doesn’t hold any resources, most of its
subclasses do—and because of this, it implements this pattern.

If it's going to be common, then the base class should be IDisposable.  If it's going to be rare, then it probably shouldn't.

There's also a question of the suitability of being IDisposable in the first place.  If you take a connection string, you opened the connection and clearly own it, so you should be IDisposable.  But if you were passed one you get into the great `leaveOpen` debate... were you given that disposable thing to access, or was ownership transferred?  A class that takes a Stream and a TimeSpan and just sits in the background and calls Flush() at intervals obviously doesn't own the Stream, because someone else should still be writing to it.  So while there's an implied "any fields being IDisposable means the class should be", it's not quite cut and dry.

The store and collections both needing the connection/pool/whatever makes it even harder, because you need to think about what the dispose semantics are.  This is really going to be one of those things where you just have to sit down, think about what a real application is going to do, and then make sense of it all.  You can't just look at one type in isolation.
author_association: MEMBER
user: login: roji
created_at: 2025-05-06T19:58:09Z
body: Thanks for doing the legwork to check the different providers @adamsitnik! To add a bit more info, both Sqlite and SQL Server should in the future also support the DbDataSource abstraction, like Npgsql has NpgsqlDataSource; at that point they'd (at least optionally) wrap a disposable data source as well.

@adamsitnik @westey-m what does this picture do to your feeling on what we should do? I'm personally leaning towards having the abstraction types implement IDisposable... Sure, having IDisposable when it's not needed adds a tiny bit of extra ceremony: users get warnings, need to do `using` or whatever (though DI users wouldn't notice the difference in any case). On the other hand, not having IDisposable creates what seem to be greater risks and problem (undisposed connection pool).

Am noting that even if we make the abstractions disposable, we probably still want to expose config to control ownership - just like SslStream does; in the majority of cases, the MEVD abstractions do own the NpgsqlDataSource, but in some cases the user may also want to use it outside of MEVD.
author_association: MEMBER