I鈥檓 writing a library based on Entity Framework Core and the Provider-specific Metadata API changes hit me in an upleasant way.
The Microsoft.EntityFrameworkCore.RelationalMetadataExtensions class was deleted in EF Core 3. I understand why the change was made but I think it could have been introduced in a non-breaking way by not deleting public classes and instead marking the classes with the Obsolete attribute.
As a consumer of the library dependent on Entity Framework Core, you will get TypeLoadException when upgrading from version 2 to version 3.
Could not load type 'Microsoft.EntityFrameworkCore.RelationalMetadataExtensions' from assembly 'Microsoft.EntityFrameworkCore.Relational, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'
And as the library author, supporting both EF Core version 2 and 3 becomes a huge burden. Here鈥檚 the code I have to write (using ReflectionMagic for readability) to support both versions because of this breaking change:
/*
* For compatibility with both EF Core 2 and EF Core 3, see https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#provider-specific-metadata-api-changes
* We can't use entityType.Relational() or property.Relational() in EF Core 3 or else we get System.TypeLoadException : Could not load type 'Microsoft.EntityFrameworkCore.RelationalMetadataExtensions' from assembly 'Microsoft.EntityFrameworkCore.Relational, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'.
* We can't use entityType.GetSchema() or property.GetColumnName() since we support EF Core 2 and those methods are new in EF Core 3
* So we have to call them all through reflection.
*/
private static readonly Assembly EfCoreRelationalAssembly = typeof(DbContextExtensions).Assembly.GetReferencedAssemblies().Where(e => e.Name == "Microsoft.EntityFrameworkCore.Relational").Select(Assembly.Load).SingleOrDefault();
// EF Core 2
private static readonly dynamic RelationalMetadataExtensions = EfCoreRelationalAssembly?.GetType("Microsoft.EntityFrameworkCore.RelationalMetadataExtensions")?.AsDynamicType();
// EF Core 3
private static readonly dynamic RelationalEntityTypeExtensions = EfCoreRelationalAssembly?.GetType("Microsoft.EntityFrameworkCore.RelationalEntityTypeExtensions")?.AsDynamicType();
private static readonly dynamic RelationalPropertyExtensions = EfCoreRelationalAssembly?.GetType("Microsoft.EntityFrameworkCore.RelationalPropertyExtensions")?.AsDynamicType();
private static string GetSchema(this IEntityType entityType)
{
if (RelationalEntityTypeExtensions != null)
return RelationalEntityTypeExtensions.GetSchema(entityType);
if (RelationalMetadataExtensions != null)
return RelationalMetadataExtensions.Relational(entityType).Schema;
throw NotSupportedException();
}
private static string GetTableName(this IEntityType entityType)
{
if (RelationalEntityTypeExtensions != null)
return RelationalEntityTypeExtensions.GetTableName(entityType);
if (RelationalMetadataExtensions != null)
return RelationalMetadataExtensions.Relational(entityType).TableName;
throw NotSupportedException();
}
private static string GetColumnName(this Microsoft.EntityFrameworkCore.Metadata.IProperty property)
{
if (RelationalPropertyExtensions != null)
return RelationalPropertyExtensions.GetColumnName(property);
if (RelationalMetadataExtensions != null)
return RelationalMetadataExtensions.Relational(property).ColumnName;
throw NotSupportedException();
}
private static Exception NotSupportedException()
{
if (EfCoreRelationalAssembly == null)
throw new InvalidOperationException($"The 'Microsoft.EntityFrameworkCore.Relational' assembly was not found as a referenced assembly by {typeof(DbContextExtensions).Assembly}.");
return new NotSupportedException($"Found neither 'Microsoft.EntityFrameworkCore.RelationalMetadataExtensions' (expected in EF Core 2) nor 'Microsoft.EntityFrameworkCore.RelationalEntityTypeExtensions' (expected in EF Core 3). Did Microsoft introduce a breaking change in {EfCoreRelationalAssembly.GetName()} ?");
}
Note how I鈥檓 getting the reference to the Microsoft.EntityFrameworkCore.Relational assembly:
Assembly.GetExecutingAssembly().GetReferencedAssemblies().Where(e => e.Name == "Microsoft.EntityFrameworkCore.Relational").Select(Assembly.Load).SingleOrDefault();
I could use a much simpler version with a know type but with what happened between EF Core 2 and 3 I鈥檓 now fearing that, for example, Microsoft.EntityFrameworkCore.RelationalMetadataExtensions might disappear too in a future version:
private static readonly Assembly EfCoreRelationalAssembly = typeof(RelationalQueryableExtensions).Assembly;
So please consider obsoleting classes instead of deleting them in the future, library authors will thank you.
@0xced Thanks for the feedback--this is something we have already discussed as a team. The 3.0 release was fundamentally broken in several ways, mostly around the query changes, and so we made the call to bring in as much of the backlogged breaking cleanup as we could as part of the 3.0 release. It was discussed at the time that this would be the last EF Core release where we would do this. Going forward we will be holding a higher bar, even for major releases, especially when it comes to application-level breaks. (Providers sometimes need to be treated slightly differently.)
In addition, in retrospect, we possibly went too far with these even in the 3.0 release, but that's something we can't change now, and can only hope to learn from going forward.
Thanks again for the feedback.
I think obsoleting behavior in version N.X and then removing it roughly a year later in the next major version (N+1).X is probably a good enough trade-off between agility and backward compatibility.