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
8 changes: 8 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/api-review-baselines.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions EFCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<Project Path="test/dotnet-ef.Tests/dotnet-ef.Tests.csproj" />
<Project Path="test/ef.Tests/ef.Tests.csproj" />
<Project Path="test/EFCore.Analyzers.Tests/EFCore.Analyzers.Tests.csproj" />
<Project Path="test/EFCore.ApiBaseline.Tests/EFCore.ApiBaseline.Tests.csproj" />
<Project Path="test/EFCore.AspNet.InMemory.FunctionalTests/EFCore.AspNet.InMemory.FunctionalTests.csproj" />
<Project Path="test/EFCore.AspNet.Specification.Tests/EFCore.AspNet.Specification.Tests.csproj" />
<Project Path="test/EFCore.AspNet.Sqlite.FunctionalTests/EFCore.AspNet.Sqlite.FunctionalTests.csproj" />
Expand Down
32 changes: 1 addition & 31 deletions build.cmd
Original file line number Diff line number Diff line change
@@ -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%
32 changes: 1 addition & 31 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$@"
22 changes: 16 additions & 6 deletions eng/Tools/ApiChief/Commands/EmitDelta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ private static string FormatDiffMarkdown(ApiType type)
{
List<string> 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)
{
Expand All @@ -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}";
}
Expand All @@ -188,10 +192,11 @@ private static void AppendStageDiffLine(List<string> lines, ApiType? changeSet,
}
}

private static void AppendGroupedDiffMembers(List<string> lines, ApiType? removals, ApiType? additions)
private static void AppendGroupedDiffMembers(List<string> 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)
Expand Down Expand Up @@ -220,6 +225,11 @@ private static void AppendGroupedDiffMembers(List<string> 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)
Expand Down
2 changes: 1 addition & 1 deletion eng/Tools/ApiChief/Model/ApiMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace ApiChief.Model;

internal sealed class ApiMember : IEquatable<ApiMember>
public sealed class ApiMember : IEquatable<ApiMember>
{
[JsonPropertyOrder(0)]
public string Member { get; set; } = string.Empty;
Expand Down
16 changes: 13 additions & 3 deletions eng/Tools/ApiChief/Model/ApiModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace ApiChief.Model;

internal sealed class ApiModel
public sealed class ApiModel
{
private static readonly JsonSerializerOptions _serializerOptions = new()
{
Expand Down Expand Up @@ -62,7 +62,9 @@ private static ISet<ApiType> 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;
}

Expand All @@ -80,14 +82,22 @@ private static ISet<ApiType> 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;
}

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);
Expand Down
2 changes: 1 addition & 1 deletion eng/Tools/ApiChief/Model/ApiStage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace ApiChief.Model;

internal enum ApiStage
public enum ApiStage
{
Stable = 0,
Experimental = 1,
Expand Down
8 changes: 7 additions & 1 deletion eng/Tools/ApiChief/Model/ApiType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace ApiChief.Model;

internal sealed class ApiType : IEquatable<ApiType>
public sealed class ApiType : IEquatable<ApiType>
{
[JsonIgnore]
public string FullTypeName { get; set; } = string.Empty;
Expand All @@ -34,6 +34,12 @@ internal sealed class ApiType : IEquatable<ApiType>
[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();
Expand Down
2 changes: 2 additions & 0 deletions eng/helix.proj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
<!-- The trimming and NativeAOT test projects are console programs -->
<XUnitProject Remove="$(RepoRoot)/test/EFCore.TrimmingTests/*.csproj"/>
<XUnitProject Remove="$(RepoRoot)/test/EFCore.NativeAotTests/*.csproj"/>
<!-- API baseline tests require source files and cannot run in Helix -->
<XUnitProject Remove="$(RepoRoot)/test/EFCore.ApiBaseline.Tests/*.csproj"/>
</ItemGroup>

<!-- Work-around for https://github.com/dotnet/runtime/issues/70758 -->
Expand Down
3 changes: 2 additions & 1 deletion src/EFCore.Cosmos/EFCore.Cosmos.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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);"
Expand Down
19 changes: 19 additions & 0 deletions src/EFCore.Relational/EFCore.Relational.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<Microsoft.EntityFrameworkCore.Diagnostics.EventDefinitionBase, Microsoft.EntityFrameworkCore.Diagnostics.EventData, string> 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": [
Expand Down Expand Up @@ -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"
},
Expand Down Expand Up @@ -13491,6 +13507,9 @@
{
"Member": "static void Microsoft.EntityFrameworkCore.Diagnostics.RelationalLoggerExtensions.NonTransactionalMigrationOperationWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger<Microsoft.EntityFrameworkCore.DbLoggerCategory.Migrations> 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<Microsoft.EntityFrameworkCore.DbLoggerCategory.Migrations> diagnostics, System.Type contextType, string? migrationVersion);"
},
{
"Member": "static void Microsoft.EntityFrameworkCore.Diagnostics.RelationalLoggerExtensions.OptionalDependentWithAllNullPropertiesWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger<Microsoft.EntityFrameworkCore.DbLoggerCategory.Update> diagnostics, Microsoft.EntityFrameworkCore.Update.IUpdateEntry entry);"
},
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.SqlServer/EFCore.SqlServer.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -2630,7 +2630,7 @@
"Member": "static System.Linq.IQueryable<Microsoft.EntityFrameworkCore.Query.FullTextSearchResult<TKey>> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.FreeTextTable<T, TKey>(this Microsoft.EntityFrameworkCore.DbSet<T> source, string freeText, System.Linq.Expressions.Expression<System.Func<T, object>>? columnSelector = null, string? languageTerm = null, int? topN = null);"
},
{
"Member": "static System.Linq.IQueryable<Microsoft.EntityFrameworkCore.VectorSearchResult<T>> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.VectorSearch<T, TVector>(this Microsoft.EntityFrameworkCore.DbSet<T> source, System.Linq.Expressions.Expression<System.Func<T, TVector>> vectorPropertySelector, TVector similarTo, string metric, int topN);",
"Member": "static System.Linq.IQueryable<Microsoft.EntityFrameworkCore.VectorSearchResult<T>> Microsoft.EntityFrameworkCore.SqlServerQueryableExtensions.VectorSearch<T, TVector>(this Microsoft.EntityFrameworkCore.DbSet<T> source, System.Linq.Expressions.Expression<System.Func<T, TVector>> vectorPropertySelector, TVector similarTo, string metric);",
"Stage": "Experimental"
}
]
Expand Down
6 changes: 1 addition & 5 deletions src/EFCore/EFCore.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> buffer);"
},
{
"Member": "Microsoft.EntityFrameworkCore.Storage.Json.JsonReaderData.JsonReaderData(System.IO.Stream stream);"
Expand Down Expand Up @@ -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": [
Expand Down
1 change: 1 addition & 0 deletions src/EFCore/Query/LiftableConstantProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading