diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 8bd130e8277..88972af8bc4 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -91,6 +91,14 @@ If you are not sure, do not guess, just tell that you don't know or ask clarifyi
- Run tests with project rebuilding enabled (don't use `--no-build`) to ensure code changes are picked up
- After changing public APIs, run `build.cmd` / `build.sh` to refresh the checked-in `*.baseline.json` files
+#### API baselining
+- Public API surface is tracked in checked-in `*.baseline.json` files under `src/`
+- Normal dev loop: make the API change, run EFCore.ApiBaseline.Tests, review the diff, and check in the updated baseline if the change is intentional
+- On CI these tests fail on baseline mismatches
+- API stages (`Stable`, `Experimental`, `Obsolete`) are part of the baseline
+- Pubternal APIs marked with `.Internal` / `[EntityFrameworkInternal]` are not treated as public API
+- When a PR with API changes is merged, a workflow labels the PR with `api-review`, generates ApiChief diffs between the old and new baselines, and posts them as PR comments for review
+
#### Environment Setup
- **ALWAYS** run `restore.cmd` (Windows) or `. ./restore.sh` (Linux/Mac) first to restore dependencies
- **ALWAYS** run `. .\activate.ps1` (PowerShell) or `. ./activate.sh` (Bash) to set up the development environment with correct SDK versions before building or running the tests
diff --git a/.github/workflows/api-review-baselines.yml b/.github/workflows/api-review-baselines.yml
index d248d24536c..521b18774aa 100644
--- a/.github/workflows/api-review-baselines.yml
+++ b/.github/workflows/api-review-baselines.yml
@@ -1,6 +1,5 @@
-# This workflow labels PRs that modify `*.baseline.json` files with `api-review`,
-# computes ApiChief deltas between the base and selected PR baseline files, and posts the
-# results back to the pull request as a comment.
+# This workflow finds changed `*.baseline.json` files in a merged PR,
+# labels the PR for API review, generates ApiChief diffs, and posts them as PR comments.
name: Comment API baseline deltas on PRs
diff --git a/EFCore.slnx b/EFCore.slnx
index 0c850b88709..30a3b70e9da 100644
--- a/EFCore.slnx
+++ b/EFCore.slnx
@@ -56,6 +56,7 @@
+
diff --git a/build.cmd b/build.cmd
index 65388b3e6d3..068028ee3dd 100644
--- a/build.cmd
+++ b/build.cmd
@@ -1,33 +1,3 @@
@echo off
-setlocal DisableDelayedExpansion
-set "__BuildArgs=%*"
-setlocal EnableDelayedExpansion
-
-set "__EFConfiguration=Debug"
-set "__EFCI=false"
-
-:parseArgs
-if "%~1"=="" goto runBuild
-
-if /I "%~1"=="-c" (
- set "__EFConfiguration=%~2"
- shift /1
-) else if /I "%~1"=="-configuration" (
- set "__EFConfiguration=%~2"
- shift /1
-) else if /I "%~1"=="-ci" (
- set "__EFCI=true"
-)
-
-shift /1
-goto parseArgs
-
-:runBuild
-powershell -ExecutionPolicy ByPass -NoProfile -command "& '%~dp0eng\common\build.ps1' -nodeReuse:$false -restore -build %__BuildArgs%"
-if errorlevel 1 exit /b %ErrorLevel%
-
-set "__CiArg="
-if /I "!__EFCI!"=="true" set "__CiArg=-Ci"
-
-pwsh -File "%~dp0tools\MakeApiBaselines.ps1" -Configuration "!__EFConfiguration!" !__CiArg!
+powershell -ExecutionPolicy ByPass -NoProfile -command "& '%~dp0eng\common\build.ps1' -nodeReuse:$false -restore -build %*"
exit /b %ErrorLevel%
diff --git a/build.sh b/build.sh
index 332e28e86f1..eef26afef9f 100755
--- a/build.sh
+++ b/build.sh
@@ -13,35 +13,5 @@ while [[ -h $source ]]; do
done
scriptroot="$( cd -P "$( dirname "$source" )" && pwd )"
-configuration=Debug
-ci=false
-args=("$@")
-while [[ $# -gt 0 ]]; do
- case "$1" in
- -c|--configuration)
- configuration="$2"
- shift 2
- ;;
- --ci)
- ci=true
- shift
- ;;
- *)
- shift
- ;;
- esac
-done
-
-"$scriptroot/eng/common/build.sh" --nodeReuse false --build --restore "${args[@]}"
-build_exit_code=$?
-if [[ $build_exit_code -ne 0 ]]; then
- exit $build_exit_code
-fi
-
-baseline_args=(-Configuration "$configuration")
-if [[ "$ci" == true ]]; then
- baseline_args+=(-Ci)
-fi
-
-pwsh -NoProfile -File "$scriptroot/tools/MakeApiBaselines.ps1" "${baseline_args[@]}"
+"$scriptroot/eng/common/build.sh" --nodeReuse false --build --restore "$@"
diff --git a/eng/Tools/ApiChief/Commands/EmitDelta.cs b/eng/Tools/ApiChief/Commands/EmitDelta.cs
index eb4a031bc82..47fa5ea97dc 100644
--- a/eng/Tools/ApiChief/Commands/EmitDelta.cs
+++ b/eng/Tools/ApiChief/Commands/EmitDelta.cs
@@ -161,8 +161,8 @@ private static string FormatDiffMarkdown(ApiType type)
{
List lines = [];
- var typeAdded = type.Additions != null && type.Removals == null;
- var typeRemoved = type.Removals != null && type.Additions == null;
+ var typeAdded = type.IsNew;
+ var typeRemoved = type.IsRemoved;
if (typeRemoved)
{
@@ -172,10 +172,14 @@ private static string FormatDiffMarkdown(ApiType type)
{
lines.Add($"+ {type.Type}");
}
+ else
+ {
+ lines.Add($" {type.Type}");
+ }
AppendStageDiffLine(lines, type.Removals, '-');
AppendStageDiffLine(lines, type.Additions, '+');
- AppendGroupedDiffMembers(lines, type.Removals, type.Additions);
+ AppendGroupedDiffMembers(lines, type);
return $"### `{type.Type}`{Environment.NewLine}{Environment.NewLine}```diff{Environment.NewLine}{string.Join(Environment.NewLine, lines)}{Environment.NewLine}```{Environment.NewLine}";
}
@@ -188,10 +192,11 @@ private static void AppendStageDiffLine(List lines, ApiType? changeSet,
}
}
- private static void AppendGroupedDiffMembers(List lines, ApiType? removals, ApiType? additions)
+ private static void AppendGroupedDiffMembers(List lines, ApiType type)
{
- var removedEntries = GetDiffEntries(removals, '-');
- var addedEntries = GetDiffEntries(additions, '+');
+ var removedEntries = GetDiffEntries(type.Removals, '-');
+ var addedEntries = GetDiffEntries(type.Additions, '+');
+ var unchangedEntries = GetDiffEntries(type, ' ');
var sharedNames = removedEntries
.Select(static entry => entry.Name)
@@ -220,6 +225,11 @@ private static void AppendGroupedDiffMembers(List lines, ApiType? remova
{
lines.Add(entry.Line);
}
+
+ foreach (var entry in unchangedEntries)
+ {
+ lines.Add(entry.Line);
+ }
}
private static List<(string Name, string Line)> GetDiffEntries(ApiType? changeSet, char prefix)
diff --git a/eng/Tools/ApiChief/Model/ApiMember.cs b/eng/Tools/ApiChief/Model/ApiMember.cs
index bae088fa1c8..b78074cd89b 100644
--- a/eng/Tools/ApiChief/Model/ApiMember.cs
+++ b/eng/Tools/ApiChief/Model/ApiMember.cs
@@ -6,7 +6,7 @@
namespace ApiChief.Model;
-internal sealed class ApiMember : IEquatable
+public sealed class ApiMember : IEquatable
{
[JsonPropertyOrder(0)]
public string Member { get; set; } = string.Empty;
diff --git a/eng/Tools/ApiChief/Model/ApiModel.cs b/eng/Tools/ApiChief/Model/ApiModel.cs
index 15b67a3d6bb..78365dfeec0 100644
--- a/eng/Tools/ApiChief/Model/ApiModel.cs
+++ b/eng/Tools/ApiChief/Model/ApiModel.cs
@@ -13,7 +13,7 @@
namespace ApiChief.Model;
-internal sealed class ApiModel
+public sealed class ApiModel
{
private static readonly JsonSerializerOptions _serializerOptions = new()
{
@@ -62,7 +62,9 @@ private static ISet FindChanges(ApiModel baseline, ApiModel current, bo
var baselineType = baseline.Types.FirstOrDefault(type => type.Equals(currentType));
if (baselineType == null)
{
- result.Add(CreateTypeDelta(currentType, currentType, null, includeSharedMembers: false)!);
+ var delta = CreateTypeDelta(currentType, currentType, null, includeSharedMembers: false)!;
+ delta.IsNew = true;
+ result.Add(delta);
continue;
}
@@ -80,7 +82,9 @@ private static ISet FindChanges(ApiModel baseline, ApiModel current, bo
continue;
}
- result.Add(CreateTypeDelta(baselineType, null, baselineType, includeSharedMembers: false)!);
+ var removedDelta = CreateTypeDelta(baselineType, null, baselineType, includeSharedMembers: false)!;
+ removedDelta.IsRemoved = true;
+ result.Add(removedDelta);
}
return result;
@@ -88,6 +92,12 @@ private static ISet FindChanges(ApiModel baseline, ApiModel current, bo
private static ApiType? CreateTypeDelta(ApiType outputType, ApiType? currentType, ApiType? baselineType, bool includeSharedMembers)
{
+ var stageChanged = currentType != null && baselineType != null && currentType.Stage != baselineType.Stage;
+ if (stageChanged)
+ {
+ includeSharedMembers = true;
+ }
+
var (addedMethods, removedMethods, sharedMethods) = PartitionMembers(currentType?.Methods, baselineType?.Methods, includeSharedMembers);
var (addedFields, removedFields, sharedFields) = PartitionMembers(currentType?.Fields, baselineType?.Fields, includeSharedMembers);
var (addedProperties, removedProperties, sharedProperties) = PartitionMembers(currentType?.Properties, baselineType?.Properties, includeSharedMembers);
diff --git a/eng/Tools/ApiChief/Model/ApiStage.cs b/eng/Tools/ApiChief/Model/ApiStage.cs
index f9c5c240138..0e45afa825d 100644
--- a/eng/Tools/ApiChief/Model/ApiStage.cs
+++ b/eng/Tools/ApiChief/Model/ApiStage.cs
@@ -3,7 +3,7 @@
namespace ApiChief.Model;
-internal enum ApiStage
+public enum ApiStage
{
Stable = 0,
Experimental = 1,
diff --git a/eng/Tools/ApiChief/Model/ApiType.cs b/eng/Tools/ApiChief/Model/ApiType.cs
index 1a5a48c4a55..d469fe44dd1 100644
--- a/eng/Tools/ApiChief/Model/ApiType.cs
+++ b/eng/Tools/ApiChief/Model/ApiType.cs
@@ -7,7 +7,7 @@
namespace ApiChief.Model;
-internal sealed class ApiType : IEquatable
+public sealed class ApiType : IEquatable
{
[JsonIgnore]
public string FullTypeName { get; set; } = string.Empty;
@@ -34,6 +34,12 @@ internal sealed class ApiType : IEquatable
[JsonPropertyOrder(6)]
public ApiType? Removals { get; set; }
+ [JsonIgnore]
+ public bool IsNew { get; set; }
+
+ [JsonIgnore]
+ public bool IsRemoved { get; set; }
+
public bool Equals(ApiType? other) => other != null && Type == other.Type;
public override bool Equals(object? obj) => Equals(obj as ApiType);
public override int GetHashCode() => Type.GetHashCode();
diff --git a/eng/helix.proj b/eng/helix.proj
index 213fd301692..da0da19701a 100644
--- a/eng/helix.proj
+++ b/eng/helix.proj
@@ -49,6 +49,8 @@
+
+
diff --git a/src/EFCore.Cosmos/EFCore.Cosmos.baseline.json b/src/EFCore.Cosmos/EFCore.Cosmos.baseline.json
index e60de5ef881..c257b6e06ef 100644
--- a/src/EFCore.Cosmos/EFCore.Cosmos.baseline.json
+++ b/src/EFCore.Cosmos/EFCore.Cosmos.baseline.json
@@ -141,7 +141,8 @@
"Member": "virtual Microsoft.EntityFrameworkCore.Infrastructure.CosmosDbContextOptionsBuilder Microsoft.EntityFrameworkCore.Infrastructure.CosmosDbContextOptionsBuilder.ConnectionMode(Microsoft.Azure.Cosmos.ConnectionMode connectionMode);"
},
{
- "Member": "virtual Microsoft.EntityFrameworkCore.Infrastructure.CosmosDbContextOptionsBuilder Microsoft.EntityFrameworkCore.Infrastructure.CosmosDbContextOptionsBuilder.ContentResponseOnWriteEnabled(bool enabled = true);"
+ "Member": "virtual Microsoft.EntityFrameworkCore.Infrastructure.CosmosDbContextOptionsBuilder Microsoft.EntityFrameworkCore.Infrastructure.CosmosDbContextOptionsBuilder.ContentResponseOnWriteEnabled(bool enabled = true);",
+ "Stage": "Obsolete"
},
{
"Member": "override bool Microsoft.EntityFrameworkCore.Infrastructure.CosmosDbContextOptionsBuilder.Equals(object? obj);"
diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json
index 550a1b2e69f..0ba0e094a3c 100644
--- a/src/EFCore.Relational/EFCore.Relational.baseline.json
+++ b/src/EFCore.Relational/EFCore.Relational.baseline.json
@@ -8399,6 +8399,19 @@
}
]
},
+ {
+ "Type": "class Microsoft.EntityFrameworkCore.Diagnostics.MigrationVersionEventData : Microsoft.EntityFrameworkCore.Diagnostics.DbContextTypeEventData",
+ "Methods": [
+ {
+ "Member": "Microsoft.EntityFrameworkCore.Diagnostics.MigrationVersionEventData.MigrationVersionEventData(Microsoft.EntityFrameworkCore.Diagnostics.EventDefinitionBase eventDefinition, System.Func messageGenerator, System.Type contextType, string? migrationVersion);"
+ }
+ ],
+ "Properties": [
+ {
+ "Member": "virtual string? Microsoft.EntityFrameworkCore.Diagnostics.MigrationVersionEventData.MigrationVersion { get; }"
+ }
+ ]
+ },
{
"Type": "class Microsoft.EntityFrameworkCore.Diagnostics.MigratorConnectionEventData : Microsoft.EntityFrameworkCore.Diagnostics.MigratorEventData",
"Methods": [
@@ -12792,6 +12805,9 @@
{
"Member": "static readonly Microsoft.Extensions.Logging.EventId Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.NonTransactionalMigrationOperationWarning"
},
+ {
+ "Member": "static readonly Microsoft.Extensions.Logging.EventId Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.OldMigrationVersionWarning"
+ },
{
"Member": "static readonly Microsoft.Extensions.Logging.EventId Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.OptionalDependentWithAllNullPropertiesWarning"
},
@@ -13491,6 +13507,9 @@
{
"Member": "static void Microsoft.EntityFrameworkCore.Diagnostics.RelationalLoggerExtensions.NonTransactionalMigrationOperationWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, Microsoft.EntityFrameworkCore.Migrations.IMigrator migrator, Microsoft.EntityFrameworkCore.Migrations.Migration migration, Microsoft.EntityFrameworkCore.Migrations.MigrationCommand command);"
},
+ {
+ "Member": "static void Microsoft.EntityFrameworkCore.Diagnostics.RelationalLoggerExtensions.OldMigrationVersionWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, System.Type contextType, string? migrationVersion);"
+ },
{
"Member": "static void Microsoft.EntityFrameworkCore.Diagnostics.RelationalLoggerExtensions.OptionalDependentWithAllNullPropertiesWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, Microsoft.EntityFrameworkCore.Update.IUpdateEntry entry);"
},
diff --git a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json
index 8c014f27cad..df149a439a8 100644
--- a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json
+++ b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json
@@ -2630,7 +2630,7 @@
"Member": "static System.Linq.IQueryable> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.FreeTextTable(this Microsoft.EntityFrameworkCore.DbSet source, string freeText, System.Linq.Expressions.Expression>? columnSelector = null, string? languageTerm = null, int? topN = null);"
},
{
- "Member": "static System.Linq.IQueryable> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.VectorSearch(this Microsoft.EntityFrameworkCore.DbSet source, System.Linq.Expressions.Expression> vectorPropertySelector, TVector similarTo, string metric, int topN);",
+ "Member": "static System.Linq.IQueryable> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.VectorSearch(this Microsoft.EntityFrameworkCore.DbSet source, System.Linq.Expressions.Expression> vectorPropertySelector, TVector similarTo, string metric);",
"Stage": "Experimental"
}
]
diff --git a/src/EFCore/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json
index fe4da0650e4..593bc83e529 100644
--- a/src/EFCore/EFCore.baseline.json
+++ b/src/EFCore/EFCore.baseline.json
@@ -17035,7 +17035,7 @@
"Type": "class Microsoft.EntityFrameworkCore.Storage.Json.JsonReaderData",
"Methods": [
{
- "Member": "Microsoft.EntityFrameworkCore.Storage.Json.JsonReaderData.JsonReaderData(byte[] buffer);"
+ "Member": "Microsoft.EntityFrameworkCore.Storage.Json.JsonReaderData.JsonReaderData(System.ReadOnlyMemory buffer);"
},
{
"Member": "Microsoft.EntityFrameworkCore.Storage.Json.JsonReaderData.JsonReaderData(System.IO.Stream stream);"
@@ -17742,10 +17742,6 @@
{
"Member": "override System.Linq.Expressions.Expression Microsoft.EntityFrameworkCore.Query.LiftableConstantProcessor.VisitExtension(System.Linq.Expressions.Expression node);",
"Stage": "Experimental"
- },
- {
- "Member": "override System.Linq.Expressions.Expression Microsoft.EntityFrameworkCore.Query.LiftableConstantProcessor.VisitMember(System.Linq.Expressions.MemberExpression memberExpression);",
- "Stage": "Experimental"
}
],
"Properties": [
diff --git a/src/EFCore/Query/LiftableConstantProcessor.cs b/src/EFCore/Query/LiftableConstantProcessor.cs
index ee465c6dc26..525d460bb1c 100644
--- a/src/EFCore/Query/LiftableConstantProcessor.cs
+++ b/src/EFCore/Query/LiftableConstantProcessor.cs
@@ -250,6 +250,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
#if DEBUG
// TODO: issue #33482 - we should properly deal with NTS types rather than disabling them here
// especially using such a crude method
+ [EntityFrameworkInternal]
protected override Expression VisitMember(MemberExpression memberExpression)
=> memberExpression is { Expression: ConstantExpression, Type.Name: "SqlServerBytesReader" or "GaiaGeoReader" }
? memberExpression
diff --git a/test/EFCore.ApiBaseline.Tests/ApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/ApiBaselineTest.cs
new file mode 100644
index 00000000000..bc306d1cbf9
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/ApiBaselineTest.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Linq;
+using ApiChief.Model;
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public static class ApiBaselineTest
+{
+ private static readonly string RepoRoot = FindRepoRoot();
+
+ private static bool IsCi
+ => Environment.GetEnvironmentVariable("CI") == "true"
+ || Environment.GetEnvironmentVariable("BUILD_BUILDID") != null
+ || Environment.GetEnvironmentVariable("PIPELINE_WORKSPACE") != null
+ || Environment.GetEnvironmentVariable("GITHUB_RUN_ID") != null;
+
+ public static void AssertBaselineMatch(string projectName, string assemblyFileName)
+ {
+ var assemblyPath = Path.Combine(AppContext.BaseDirectory, assemblyFileName);
+ Assert.True(File.Exists(assemblyPath), $"Assembly not found: {assemblyPath}");
+
+ var baselinePath = Path.Combine(RepoRoot, "src", projectName, $"{projectName}.baseline.json");
+
+ var current = ApiModel.LoadFromAssembly(assemblyPath);
+
+ if (!File.Exists(baselinePath))
+ {
+ if (IsCi)
+ {
+ Assert.Fail($"Baseline file not found: {baselinePath}");
+ }
+
+ File.WriteAllText(baselinePath, current.ToString());
+ return;
+ }
+
+ var baseline = ApiModel.LoadFromFile(baselinePath);
+ baseline.EvaluateDelta(current);
+
+ if (current.Types.Count > 0)
+ {
+ if (IsCi)
+ {
+ var additions = current.Types
+ .Where(t => t.Additions != null)
+ .Select(t => t.Type)
+ .ToList();
+
+ var removals = current.Types
+ .Where(t => t.Removals != null)
+ .Select(t => t.Type)
+ .ToList();
+
+ var message =
+ $"API baseline mismatch for {projectName}. "
+ + $"Update the baselines by running the tests locally.{Environment.NewLine}"
+ + (additions.Count > 0
+ ? $" Types with additions: {string.Join(", ", additions)}{Environment.NewLine}"
+ : "")
+ + (removals.Count > 0
+ ? $" Types with removals: {string.Join(", ", removals)}{Environment.NewLine}"
+ : "")
+ + $"{Environment.NewLine}Delta:{Environment.NewLine}{current}";
+
+ Assert.Fail(message);
+ }
+
+ // Running locally — regenerate the baseline from the assembly
+ var updated = ApiModel.LoadFromAssembly(assemblyPath);
+ File.WriteAllText(baselinePath, updated.ToString());
+ }
+ }
+
+ private static string FindRepoRoot()
+ {
+ var dir = new DirectoryInfo(AppContext.BaseDirectory);
+ while (dir != null && !File.Exists(Path.Combine(dir.FullName, "EFCore.slnx")))
+ {
+ dir = dir.Parent;
+ }
+
+ return dir?.FullName ?? throw new InvalidOperationException(
+ "Could not find repository root. Ensure the test is run from within the EF Core repository.");
+ }
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCore.ApiBaseline.Tests.csproj b/test/EFCore.ApiBaseline.Tests/EFCore.ApiBaseline.Tests.csproj
new file mode 100644
index 00000000000..cec5f621c0b
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCore.ApiBaseline.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ $(DefaultNetCoreTargetFramework)
+ Microsoft.EntityFrameworkCore.ApiBaseline.Tests
+ Microsoft.EntityFrameworkCore
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreAbstractionsApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreAbstractionsApiBaselineTest.cs
new file mode 100644
index 00000000000..4624550a473
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreAbstractionsApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreAbstractionsApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.Abstractions", "Microsoft.EntityFrameworkCore.Abstractions.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreApiBaselineTest.cs
new file mode 100644
index 00000000000..98f2f7dbcc9
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore", "Microsoft.EntityFrameworkCore.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreCosmosApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreCosmosApiBaselineTest.cs
new file mode 100644
index 00000000000..11de169ccf5
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreCosmosApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreCosmosApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.Cosmos", "Microsoft.EntityFrameworkCore.Cosmos.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreDesignApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreDesignApiBaselineTest.cs
new file mode 100644
index 00000000000..ecb80cf0f53
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreDesignApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreDesignApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.Design", "Microsoft.EntityFrameworkCore.Design.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreInMemoryApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreInMemoryApiBaselineTest.cs
new file mode 100644
index 00000000000..11583274fcf
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreInMemoryApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreInMemoryApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.InMemory", "Microsoft.EntityFrameworkCore.InMemory.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreProxiesApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreProxiesApiBaselineTest.cs
new file mode 100644
index 00000000000..ffcd35f439b
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreProxiesApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreProxiesApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.Proxies", "Microsoft.EntityFrameworkCore.Proxies.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreRelationalApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreRelationalApiBaselineTest.cs
new file mode 100644
index 00000000000..614c379a234
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreRelationalApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreRelationalApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.Relational", "Microsoft.EntityFrameworkCore.Relational.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerAbstractionsApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerAbstractionsApiBaselineTest.cs
new file mode 100644
index 00000000000..ca474db45ec
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerAbstractionsApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreSqlServerAbstractionsApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.SqlServer.Abstractions", "Microsoft.EntityFrameworkCore.SqlServer.Abstractions.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerApiBaselineTest.cs
new file mode 100644
index 00000000000..15a9fdcb0cf
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreSqlServerApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.SqlServer", "Microsoft.EntityFrameworkCore.SqlServer.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerHierarchyIdApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerHierarchyIdApiBaselineTest.cs
new file mode 100644
index 00000000000..6db7576595c
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerHierarchyIdApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreSqlServerHierarchyIdApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.SqlServer.HierarchyId", "Microsoft.EntityFrameworkCore.SqlServer.HierarchyId.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerNTSApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerNTSApiBaselineTest.cs
new file mode 100644
index 00000000000..d4754c04f5a
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreSqlServerNTSApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreSqlServerNTSApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.SqlServer.NTS", "Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreSqliteCoreApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreSqliteCoreApiBaselineTest.cs
new file mode 100644
index 00000000000..344a85d3423
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreSqliteCoreApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreSqliteCoreApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.Sqlite.Core", "Microsoft.EntityFrameworkCore.Sqlite.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/EFCoreSqliteNTSApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/EFCoreSqliteNTSApiBaselineTest.cs
new file mode 100644
index 00000000000..e3f3662934a
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/EFCoreSqliteNTSApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class EFCoreSqliteNTSApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "EFCore.Sqlite.NTS", "Microsoft.EntityFrameworkCore.Sqlite.NetTopologySuite.dll");
+}
diff --git a/test/EFCore.ApiBaseline.Tests/MicrosoftDataSqliteCoreApiBaselineTest.cs b/test/EFCore.ApiBaseline.Tests/MicrosoftDataSqliteCoreApiBaselineTest.cs
new file mode 100644
index 00000000000..eaa3411e8e5
--- /dev/null
+++ b/test/EFCore.ApiBaseline.Tests/MicrosoftDataSqliteCoreApiBaselineTest.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class MicrosoftDataSqliteCoreApiBaselineTest
+{
+ [Fact]
+ public void ApiSurfaceMatchesBaseline()
+ => ApiBaselineTest.AssertBaselineMatch(
+ "Microsoft.Data.Sqlite.Core", "Microsoft.Data.Sqlite.dll");
+}
diff --git a/tools/MakeApiBaselines.ps1 b/tools/MakeApiBaselines.ps1
deleted file mode 100644
index 46547b5ded3..00000000000
--- a/tools/MakeApiBaselines.ps1
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/usr/bin/env pwsh
-<#
-.DESCRIPTION
- Creates API baseline files representing the current public API surface exposed by this repo.
-.PARAMETER ProjectNamePattern
- Optional wildcard used to filter which source projects are processed.
-.PARAMETER Configuration
- Build configuration to use when locating binaries and building ApiChief.
-.PARAMETER Ci
- Indicates that the script was invoked from a CI build.
-#>
-
-param(
- [string]$ProjectNamePattern = "*",
- [ValidateSet("Debug", "Release")]
- [string]$Configuration = "Debug",
- [switch]$Ci
-)
-
-$apiChiefDeltaNoChangesExitCode = 2
-
-if ($PSVersionTable.PSVersion.Major -lt 6) {
- Write-Host "PowerShell 6.0 or greater is required to run this script. See https://aka.ms/install-powershell."
- Write-Host "Current version:" $PSVersionTable.PSVersion.ToString()
- exit 1
-}
-
-Write-Output "Building ApiChief tool"
-
-$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
-. (Join-Path $repoRoot "eng/common/tools.ps1")
-$dotnetRoot = InitializeDotNetCli -install $true
-$dotnet = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet')
-$project = Join-Path $repoRoot "eng/Tools/ApiChief/ApiChief.csproj"
-$srcFolder = Join-Path $repoRoot "src"
-& $dotnet build $project --configuration $Configuration --nologo --verbosity q
-
-if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
-}
-
-$command = (& $dotnet msbuild $project --getProperty:TargetPath -p:Configuration=$Configuration --nologo).Trim()
-if ([string]::IsNullOrWhiteSpace($command) -or !(Test-Path $command)) {
- Write-Error "Unable to locate the built ApiChief binary."
- exit 1
-}
-
-Write-Output "Creating API baseline files in the src folder"
-
-Get-ChildItem -Path $srcFolder -Recurse -Filter *.csproj |
- Sort-Object FullName |
- Where-Object {
- ($_.BaseName -like $ProjectNamePattern) -and (Select-String -Path $_.FullName -Pattern 'true' -Quiet)
- } |
- ForEach-Object {
- $name = $_.BaseName
- $artifactDir = Join-Path $repoRoot "artifacts/bin/$name/$Configuration"
- $tfm = Get-ChildItem -Path $artifactDir -Directory -ErrorAction SilentlyContinue |
- Where-Object { $_.Name -match '^net\d+\.\d+$' } |
- Sort-Object { [version]($_.Name -replace '^net', '') } -Descending |
- Select-Object -First 1 -ExpandProperty Name
-
- if ($null -eq $tfm) {
- Write-Warning "Skipping $name because no built net* target was found under '$artifactDir'. Build the project first."
- return
- }
-
- $assemblyName = (& $dotnet msbuild $_.FullName --getProperty:AssemblyName -p:Configuration=$Configuration --nologo).Trim()
- if ([string]::IsNullOrWhiteSpace($assemblyName)) {
- $assemblyName = $name
- }
-
- $assemblyPath = Join-Path $artifactDir "$tfm/$assemblyName.dll"
- if (!(Test-Path $assemblyPath)) {
- Write-Warning "Skipping $name because '$assemblyPath' does not exist. Build the project first."
- return
- }
-
- $baselinePath = Join-Path $_.Directory.FullName "$name.baseline.json"
- $previousBaselinePath = Join-Path $_.Directory.FullName "$name.previous.baseline.json"
- $deltaPath = Join-Path $_.Directory.FullName "$name.delta.json"
-
- if (Test-Path $previousBaselinePath) {
- Remove-Item $previousBaselinePath -Force
- }
-
- if (Test-Path $baselinePath) {
- Rename-Item $baselinePath -NewName (Split-Path $previousBaselinePath -Leaf)
- }
-
- if (Test-Path $deltaPath) {
- Remove-Item $deltaPath -Force
- }
-
- Write-Host " Processing $name ($tfm, $Configuration)"
- & $dotnet $command $assemblyPath emit baseline -o $baselinePath
-
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
-
- if (Test-Path $previousBaselinePath) {
- if ($Ci) {
- & $dotnet $command $baselinePath emit delta $previousBaselinePath -o $deltaPath
- $deltaExitCode = $LASTEXITCODE
-
- if ($deltaExitCode -eq 0) {
- Write-Error "API changes were detected for $name and the baselines in the PR need to be updated by running build locally."
- exit 1
- }
- elseif ($deltaExitCode -ne $apiChiefDeltaNoChangesExitCode) {
- exit $deltaExitCode
- }
-
- if (Test-Path $deltaPath) {
- Remove-Item $deltaPath -Force
- }
- }
-
- Remove-Item $previousBaselinePath -Force
- }
- }