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
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@ This project follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [1.0.0-preview.3] — 2026-11-02

The third prerelease. It adds what auditing needs from the library: `ISettingWriteParticipant`,
which commits its work in the same transaction as a settings write, and `SettingWrite.GroupType`.

**Two breaking changes from `1.0.0-preview.2`.** The two APIs deprecated in preview.2 are
removed, as that release announced. And a clear now reports what it deleted, so an observer that
read an empty `Changes` as "this was a clear" must check `Kind` instead. Both entries below carry
the step to take. Still not frozen: pin an exact version rather than a floating range.

### Removed
- **`ISettingDbContext` and `ApplySettingsConfiguration`, as BREAKING in prerelease.** Both were deprecated in `1.0.0-preview.2` for removal in this release. Delete `: ISettingDbContext` and its `DbSet` properties from your context, and rename `ApplySettingsConfiguration` to `ApplySettings`. Neither changes the model, so there is no migration to run.

### Added
- `SettingWrite.GroupType`: the settings class a write was made to. An observer that needs the group's attributes no longer has to scan assemblies for a class whose resolved name matches `GroupName`. It is an init property, so code that constructs a `SettingWrite` keeps compiling.
- The library's first metrics, on a `DotNetBoost.Settings` meter: `dotnetboost.settings.write_observer.failures` and `dotnetboost.settings.change_handler.failures`, each tagged with the implementation's class name, the group and `error.type`. Observer and handler exceptions are still logged and swallowed; until now the log line was the only trace of them. The meter is created through the container's `IMeterFactory` when there is one.
- `ISettingWriteParticipant` and `.UseWriteParticipant<T>()`: a hook that runs inside every write, before the store saves, for work that must commit with the write or not at all — an audit entry. A participant enlists entities through `EfCoreSettingWriteScope.Add`, and the Entity Framework Core store's own save commits them with the rows. An exception aborts the write: the store is not called, the cache keeps its entry, and what was enlisted is discarded. Participants and observers receive the same `SettingWrite` instance, so they share a correlation id. See [Write notifications](docs/write-notifications.md#isettingwriteparticipant).
- `IEnlistingSettingStore`, the store capability participants require, and a `SettingBuilderGuard.EnsureProviderNotConfigured` overload through which a provider declares it. Only Entity Framework Core does. `Build()` refuses a participant for Dapper or MongoDB, and a write refuses one at run time for a store that cannot enlist, rather than run it with no atomicity.

### Changed
- **A clear now reports what it deleted.** A `Cleared` `SettingWrite` used to carry an empty `Changes`; it now lists one `SettingChange` per deleted row, with the stored value as `OldValue` and a null `NewValue`, redacted for `[Sensitive]` properties and for any encrypted row. The manager reads the group before deleting to do it, but only when a participant or observer is registered. An observer that relied on `Changes` being empty for a clear should check `Kind` instead.
- **A clear now reports what it deleted, as BREAKING in prerelease.** A `Cleared` `SettingWrite` used to carry an empty `Changes`; it now lists one `SettingChange` per deleted row, with the stored value as `OldValue` and a null `NewValue`, redacted for `[Sensitive]` properties and for any encrypted row. The manager reads the group before deleting to do it, but only when a participant or observer is registered. An observer that relied on `Changes` being empty for a clear should check `Kind` instead.
- `EfCoreSettingStore.DeleteGroupAsync`, when a participant has enlisted something, runs the delete and a `SaveChangesAsync` in one transaction: the caller's if there is one, otherwise its own inside the context's execution strategy, so it works under `EnableRetryOnFailure`. With nothing enlisted it is the single `ExecuteDeleteAsync` it was.
- Aspire moved from 13.5.2 to 13.6.4, and Microsoft.Extensions.* and EF Core packages from 10.0.11 to 10.0.12 — the floor Aspire 13.6 requires.
- Microsoft.Data.SqlClient (integration tests and sample only) moved from 6.1.6 to 6.1.8, which takes System.Security.Cryptography.Pkcs 9.0.21 and clears CVE-2026-70162 and CVE-2026-84008.
Expand Down
5 changes: 3 additions & 2 deletions docs/storage-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ protected override void OnModelCreating(ModelBuilder mb)
// Options: SqlServer | PostgreSql | Sqlite
```

> **Upgrading from `1.0.0-preview.1`?** `ISettingDbContext` is obsolete and does nothing.
> Delete `: ISettingDbContext` and the two `DbSet` properties; nothing else changes.
> **Upgrading from `1.0.0-preview.1`?** `ISettingDbContext` is gone. Delete
> `: ISettingDbContext` and the two `DbSet` properties; nothing else changes. If you call
> `ApplySettingsConfiguration`, rename it to `ApplySettings`.
> `UseEntityFrameworkCore<TContext>()` registers nothing against `DbContext` itself, so the
> library never claims a framework type it does not own.

Expand Down
46 changes: 0 additions & 46 deletions src/DotNetBoost.Settings.EntityFrameworkCore/EfCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using DotNetBoost.Settings.Core.Interfaces;
using DotNetBoost.Settings.Core.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -21,34 +20,6 @@ public enum DatabaseProvider
Sqlite
}

/// <summary>
/// No longer required. The EF Core store reads its entities through
/// <see cref="DbContext.Set{TEntity}()"/>, so a context needs neither this interface nor the
/// <c>DbSet</c> properties it declares — <c>ApplySettings</c> in <c>OnModelCreating</c> is the
/// whole registration.
/// </summary>
/// <remarks>
/// Kept for one release so existing contexts keep compiling. Delete the
/// <c>: ISettingDbContext</c> and the <c>DbSet</c> property; nothing else changes.
/// </remarks>
[Obsolete("ISettingDbContext is no longer used. Remove it and the DbSet properties from your " +
"DbContext; UseEntityFrameworkCore<TContext>() only needs a DbContext. " +
"This interface will be removed in the next release.")]
public interface ISettingDbContext
{
/// <summary>The persisted settings rows.</summary>
DbSet<Setting> Settings { get; }

/// <summary>Persists pending changes.</summary>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Change-tracking entry for an entity. Needed to state the concurrency token a write is
/// conditional on. A <see cref="DbContext"/> satisfies this member implicitly.
/// </summary>
EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
}

/// <summary>Entity mapping for <see cref="Setting"/>, tuned per engine.</summary>
/// <param name="provider">Selects the value column type and concurrency-token style.</param>
public sealed class SettingConfiguration(DatabaseProvider provider)
Expand Down Expand Up @@ -121,23 +92,6 @@ public static ModelBuilder ApplySettings(this ModelBuilder mb, DbContext context
return mb.ApplySettings(ResolveProvider(context.Database.ProviderName));
}

/// <summary>
/// Renamed to <see cref="ApplySettings(ModelBuilder, DatabaseProvider)"/>.
/// </summary>
/// <remarks>
/// Kept for one release so existing contexts keep compiling, the same way
/// <see cref="ISettingDbContext"/> is. The old name described the mechanism rather than
/// the library, which read as meaningful next to the <c>ApplyMedia()</c> and
/// <c>ApplyMultiLingual()</c> calls it usually sits beside. It was not.
/// </remarks>
[Obsolete(RenamedMessage)]
public static ModelBuilder ApplySettingsConfiguration(this ModelBuilder mb, DatabaseProvider provider)
=> mb.ApplySettings(provider);

private const string RenamedMessage =
"Renamed to ApplySettings, to match ApplyMedia and ApplyMultiLingual. " +
"This alias will be removed in the next release.";

/// <summary>
/// Maps an EF Core provider assembly name onto the engine whose column types and
/// concurrency-token style the settings tables need.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,6 @@ public void InferringOverload_OnAnUnmappedProvider_SaysWhatToDoInstead()
Assert.Contains("Microsoft.EntityFrameworkCore.InMemory", Flatten(ex), StringComparison.Ordinal);
}

/// <summary>
/// The whole purpose of the obsolete alias: a context written against the old name keeps
/// compiling and keeps building the same model. Deprecation warning, not a break.
/// </summary>
[Theory]
[InlineData(DatabaseProvider.SqlServer, "nvarchar(max)")]
[InlineData(DatabaseProvider.PostgreSql, "text")]
[InlineData(DatabaseProvider.Sqlite, "TEXT")]
public void TheObsoleteName_StillBuildsTheSameModel(DatabaseProvider provider, string expectedColumnType)
{
Assert.Equal(expectedColumnType, ValueColumnTypeOf(ObsoleteContextFor(provider)));

// And the same as the new name produces, rather than merely something plausible.
Assert.Equal(ValueColumnTypeOf(ContextFor(provider)), ValueColumnTypeOf(ObsoleteContextFor(provider)));
}

/// <summary>EF wraps a throw from OnModelCreating, so assert over the whole chain.</summary>
private static string Flatten(Exception ex)
{
Expand Down Expand Up @@ -117,35 +101,6 @@ protected override void OnModelCreating(ModelBuilder mb)
=> mb.ApplySettings(DatabaseProvider.Sqlite);
}

// One context type per provider, as above, each calling the obsolete alias. CS0618 is
// suppressed here and nowhere else: these exist precisely to exercise the deprecated name.
private static DbContext ObsoleteContextFor(DatabaseProvider provider) => provider switch
{
DatabaseProvider.SqlServer => new ObsoleteSqlServerContext(),
DatabaseProvider.PostgreSql => new ObsoletePostgresContext(),
_ => new ObsoleteSqliteContext()
};

#pragma warning disable CS0618 // Deliberately calling the obsolete alias.
private sealed class ObsoleteSqlServerContext : SqliteBackedContext
{
protected override void OnModelCreating(ModelBuilder mb)
=> mb.ApplySettingsConfiguration(DatabaseProvider.SqlServer);
}

private sealed class ObsoletePostgresContext : SqliteBackedContext
{
protected override void OnModelCreating(ModelBuilder mb)
=> mb.ApplySettingsConfiguration(DatabaseProvider.PostgreSql);
}

private sealed class ObsoleteSqliteContext : SqliteBackedContext
{
protected override void OnModelCreating(ModelBuilder mb)
=> mb.ApplySettingsConfiguration(DatabaseProvider.Sqlite);
}
#pragma warning restore CS0618

private sealed class InMemoryContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder options)
Expand Down
Loading