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 - } - }