I found a solution that covers 90% of my needs. I haven't found yet is how to rename the generated files, and how to exclude auto-number from end of ambiguous names.
So, the solution. We need to install T4, following the instructions
https://learn.microsoft.com/en-us/ef/core/managing-schemas/scaffolding/templates
dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates
Next, edit the T4 files by adding the schema name to the table names. And for me it work.
In short - I change:
EntityType.Name
To
EntityType.GetSchema() + "_" + EntityType.Name
In detail, my T4 files for Entity Framework Core 8 are below.
DbContext.t4:
<#@ template hostSpecific="true" #>
<#@ assembly name="Microsoft.EntityFrameworkCore" #>
<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #>
<#@ assembly name="Microsoft.EntityFrameworkCore.Relational" #>
<#@ assembly name="Microsoft.Extensions.DependencyInjection.Abstractions" #>
<#@ parameter name="Model" type="Microsoft.EntityFrameworkCore.Metadata.IModel" #>
<#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #>
<#@ parameter name="NamespaceHint" type="System.String" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="Microsoft.EntityFrameworkCore" #>
<#@ import namespace="Microsoft.EntityFrameworkCore.Design" #>
<#@ import namespace="Microsoft.EntityFrameworkCore.Infrastructure" #>
<#@ import namespace="Microsoft.EntityFrameworkCore.Scaffolding" #>
<#@ import namespace="Microsoft.Extensions.DependencyInjection" #>
<#
if (!ProductInfo.GetVersion().StartsWith("9.0"))
{
Warning("Your templates were created using an older version of Entity Framework. Additional features and bug fixes may be available. See https://aka.ms/efcore-docs-updating-templates for more information.");
}
var services = (IServiceProvider)Host;
var providerCode = services.GetRequiredService<IProviderConfigurationCodeGenerator>();
var annotationCodeGenerator = services.GetRequiredService<IAnnotationCodeGenerator>();
var code = services.GetRequiredService<ICSharpHelper>();
var usings = new List<string>
{
"System",
"System.Collections.Generic",
"Microsoft.EntityFrameworkCore"
};
if (NamespaceHint != Options.ModelNamespace
&& !string.IsNullOrEmpty(Options.ModelNamespace))
{
usings.Add(Options.ModelNamespace);
}
if (!string.IsNullOrEmpty(NamespaceHint))
{
#>
namespace <#= NamespaceHint #>;
<#
}
#>
public partial class <#= Options.ContextName #> : DbContext
{
<#
if (!Options.SuppressOnConfiguring)
{
#>
public <#= Options.ContextName #>()
{
}
<#
}
#>
public <#= Options.ContextName #>(DbContextOptions<<#= Options.ContextName #>> options)
: base(options)
{
}
<#
foreach (var entityType in Model.GetEntityTypes().Where(e => !e.IsSimpleManyToManyJoinEntityType()))
{
#>
public virtual DbSet<<#= FirstUp(entityType.GetSchema() ?? entityType.GetDefaultSchema()) + "_" + entityType.Name #>> <#= FirstUp(entityType.GetSchema() ?? entityType.GetDefaultSchema()) + "_" + entityType.GetDbSetName() #> { get; set; }
<#
}
if (!Options.SuppressOnConfiguring)
{
#>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
<#
if (!Options.SuppressConnectionStringWarning)
{
#>
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
<#
}
var useProviderCall = providerCode.GenerateUseProvider(Options.ConnectionString);
usings.AddRange(useProviderCall.GetRequiredUsings());
#>
=> optionsBuilder<#= code.Fragment(useProviderCall, indent: 3) #>;
<#
}
#>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
<#
var anyConfiguration = false;
var modelFluentApiCalls = Model.GetFluentApiCalls(annotationCodeGenerator);
if (modelFluentApiCalls != null)
{
usings.AddRange(modelFluentApiCalls.GetRequiredUsings());
#>
modelBuilder<#= code.Fragment(modelFluentApiCalls, indent: 3) #>;
<#
anyConfiguration = true;
}
StringBuilder mainEnvironment;
foreach (var entityType in Model.GetEntityTypes().Where(e => !e.IsSimpleManyToManyJoinEntityType()))
{
// Save all previously generated code, and start generating into a new temporary environment
mainEnvironment = GenerationEnvironment;
GenerationEnvironment = new StringBuilder();
if (anyConfiguration)
{
WriteLine("");
}
var anyEntityTypeConfiguration = false;
#>
modelBuilder.Entity<<#= FirstUp(entityType.GetSchema() ?? entityType.GetDefaultSchema()) + "_" + entityType.Name #>>(entity =>
{
<#
var key = entityType.FindPrimaryKey();
if (key != null)
{
var keyFluentApiCalls = key.GetFluentApiCalls(annotationCodeGenerator);
if (keyFluentApiCalls != null
|| (!key.IsHandledByConvention() && !Options.UseDataAnnotations))
{
if (keyFluentApiCalls != null)
{
usings.AddRange(keyFluentApiCalls.GetRequiredUsings());
}
#>
entity.HasKey(<#= code.Lambda(key.Properties, "e") #>)<#= code.Fragment(keyFluentApiCalls, indent: 4) #>;
<#
anyEntityTypeConfiguration = true;
}
}
var entityTypeFluentApiCalls = entityType.GetFluentApiCalls(annotationCodeGenerator)
?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations));
if (entityTypeFluentApiCalls != null)
{
usings.AddRange(entityTypeFluentApiCalls.GetRequiredUsings());
if (anyEntityTypeConfiguration)
{
WriteLine("");
}
#>
entity<#= code.Fragment(entityTypeFluentApiCalls, indent: 4) #>;
<#
anyEntityTypeConfiguration = true;
}
foreach (var index in entityType.GetIndexes()
.Where(i => !(Options.UseDataAnnotations && i.IsHandledByDataAnnotations(annotationCodeGenerator))))
{
if (anyEntityTypeConfiguration)
{
WriteLine("");
}
var indexFluentApiCalls = index.GetFluentApiCalls(annotationCodeGenerator);
if (indexFluentApiCalls != null)
{
usings.AddRange(indexFluentApiCalls.GetRequiredUsings());
}
#>
entity.HasIndex(<#= code.Lambda(index.Properties, "e") #>, <#= code.Literal(index.GetDatabaseName()) #>)<#= code.Fragment(indexFluentApiCalls, indent: 4) #>;
<#
anyEntityTypeConfiguration = true;
}
var firstProperty = true;
foreach (var property in entityType.GetProperties())
{
var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator)
?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations)
&& !(c.Method == "IsRequired" && Options.UseNullableReferenceTypes && !property.ClrType.IsValueType));
if (propertyFluentApiCalls == null)
{
continue;
}
usings.AddRange(propertyFluentApiCalls.GetRequiredUsings());
if (anyEntityTypeConfiguration && firstProperty)
{
WriteLine("");
}
#>
entity.Property(e => e.<#= property.Name #>)<#= code.Fragment(propertyFluentApiCalls, indent: 4) #>;
<#
anyEntityTypeConfiguration = true;
firstProperty = false;
}
foreach (var foreignKey in entityType.GetForeignKeys())
{
var foreignKeyFluentApiCalls = foreignKey.GetFluentApiCalls(annotationCodeGenerator)
?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations));
if (foreignKeyFluentApiCalls == null)
{
continue;
}
usings.AddRange(foreignKeyFluentApiCalls.GetRequiredUsings());
if (anyEntityTypeConfiguration)
{
WriteLine("");
}
#>
entity.HasOne(d => d.<#= foreignKey.DependentToPrincipal.Name #>).<#= foreignKey.IsUnique ? "WithOne" : "WithMany" #>(<#= foreignKey.PrincipalToDependent != null ? $"p => p.{foreignKey.PrincipalToDependent.Name}" : "" #>)<#= code.Fragment(foreignKeyFluentApiCalls, indent: 4) #>;
<#
anyEntityTypeConfiguration = true;
}
foreach (var skipNavigation in entityType.GetSkipNavigations().Where(n => n.IsLeftNavigation()))
{
if (anyEntityTypeConfiguration)
{
WriteLine("");
}
var left = skipNavigation.ForeignKey;
var leftFluentApiCalls = left.GetFluentApiCalls(annotationCodeGenerator, useStrings: true);
var right = skipNavigation.Inverse.ForeignKey;
var rightFluentApiCalls = right.GetFluentApiCalls(annotationCodeGenerator, useStrings: true);
var joinEntityType = skipNavigation.JoinEntityType;
if (leftFluentApiCalls != null)
{
usings.AddRange(leftFluentApiCalls.GetRequiredUsings());
}
if (rightFluentApiCalls != null)
{
usings.AddRange(rightFluentApiCalls.GetRequiredUsings());
}
#>
entity.HasMany(d => d.<#= skipNavigation.Name #>).WithMany(p => p.<#= skipNavigation.Inverse.Name #>)
.UsingEntity<Dictionary<string, object>>(
<#= code.Literal(joinEntityType.Name) #>,
r => r.HasOne<<#= FirstUp(right.PrincipalEntityType.GetSchema() ?? right.PrincipalEntityType.GetDefaultSchema()) + "_" + right.PrincipalEntityType.Name #>>().WithMany()<#= code.Fragment(rightFluentApiCalls, indent: 6) #>,
l => l.HasOne<<#= FirstUp(left.PrincipalEntityType.GetSchema() ?? left.PrincipalEntityType.GetDefaultSchema()) + "_" + left.PrincipalEntityType.Name #>>().WithMany()<#= code.Fragment(leftFluentApiCalls, indent: 6) #>,
j =>
{
<#
var joinKey = joinEntityType.FindPrimaryKey();
var joinKeyFluentApiCalls = joinKey.GetFluentApiCalls(annotationCodeGenerator);
if (joinKeyFluentApiCalls != null)
{
usings.AddRange(joinKeyFluentApiCalls.GetRequiredUsings());
}
#>
j.HasKey(<#= code.Arguments(joinKey.Properties.Select(e => e.Name)) #>)<#= code.Fragment(joinKeyFluentApiCalls, indent: 7) #>;
<#
var joinEntityTypeFluentApiCalls = joinEntityType.GetFluentApiCalls(annotationCodeGenerator);
if (joinEntityTypeFluentApiCalls != null)
{
usings.AddRange(joinEntityTypeFluentApiCalls.GetRequiredUsings());
#>
j<#= code.Fragment(joinEntityTypeFluentApiCalls, indent: 7) #>;
<#
}
foreach (var index in joinEntityType.GetIndexes())
{
var indexFluentApiCalls = index.GetFluentApiCalls(annotationCodeGenerator);
if (indexFluentApiCalls != null)
{
usings.AddRange(indexFluentApiCalls.GetRequiredUsings());
}
#>
j.HasIndex(<#= code.Literal(index.Properties.Select(e => e.Name).ToArray()) #>, <#= code.Literal(index.GetDatabaseName()) #>)<#= code.Fragment(indexFluentApiCalls, indent: 7) #>;
<#
}
foreach (var property in joinEntityType.GetProperties())
{
var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator);
if (propertyFluentApiCalls == null)
{
continue;
}
usings.AddRange(propertyFluentApiCalls.GetRequiredUsings());
#>
j.IndexerProperty<<#= code.Reference(property.ClrType) #>>(<#= code.Literal(property.Name) #>)<#= code.Fragment(propertyFluentApiCalls, indent: 7) #>;
<#
}
#>
});
<#
anyEntityTypeConfiguration = true;
}
#>
});
<#
// If any signicant code was generated, append it to the main environment
if (anyEntityTypeConfiguration)
{
mainEnvironment.Append(GenerationEnvironment);
anyConfiguration = true;
}
// Resume generating code into the main environment
GenerationEnvironment = mainEnvironment;
}
foreach (var sequence in Model.GetSequences())
{
var needsType = sequence.Type != typeof(long);
var needsSchema = !string.IsNullOrEmpty(sequence.Schema) && sequence.Schema != sequence.Model.GetDefaultSchema();
var sequenceFluentApiCalls = sequence.GetFluentApiCalls(annotationCodeGenerator);
#>
modelBuilder.HasSequence<#= needsType ? $"<{code.Reference(sequence.Type)}>" : "" #>(<#= code.Literal(sequence.Name) #><#= needsSchema ? $", {code.Literal(sequence.Schema)}" : "" #>)<#= code.Fragment(sequenceFluentApiCalls, indent: 3) #>;
<#
}
if (anyConfiguration)
{
WriteLine("");
}
#>
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
<#
mainEnvironment = GenerationEnvironment;
GenerationEnvironment = new StringBuilder();
foreach (var ns in usings.Distinct().OrderBy(x => x, new NamespaceComparer()))
{
#>
using <#= ns #>;
<#
}
WriteLine("");
GenerationEnvironment.Append(mainEnvironment);
#>
<#+
private string FirstUp(string name)
{
if (name is null) return name;
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name);
}
#>
EntityType.t4:
<#@ template hostSpecific="true" #>
<#@ assembly name="Microsoft.EntityFrameworkCore" #>
<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #>
<#@ assembly name="Microsoft.EntityFrameworkCore.Relational" #>
<#@ assembly name="Microsoft.Extensions.DependencyInjection.Abstractions" #>
<#@ parameter name="EntityType" type="Microsoft.EntityFrameworkCore.Metadata.IEntityType" #>
<#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #>
<#@ parameter name="NamespaceHint" type="System.String" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.ComponentModel.DataAnnotations" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="Microsoft.EntityFrameworkCore" #>
<#@ import namespace="Microsoft.EntityFrameworkCore.Design" #>
<#@ import namespace="Microsoft.Extensions.DependencyInjection" #>
<#
if (EntityType.IsSimpleManyToManyJoinEntityType())
{
// Don't scaffold these
return "";
}
var services = (IServiceProvider)Host;
var annotationCodeGenerator = services.GetRequiredService<IAnnotationCodeGenerator>();
var code = services.GetRequiredService<ICSharpHelper>();
var usings = new List<string>
{
"System",
"System.Collections.Generic"
};
if (Options.UseDataAnnotations)
{
usings.Add("System.ComponentModel.DataAnnotations");
usings.Add("System.ComponentModel.DataAnnotations.Schema");
usings.Add("Microsoft.EntityFrameworkCore");
}
if (!string.IsNullOrEmpty(NamespaceHint))
{
#>
namespace <#= NamespaceHint #>;
<#
}
if (!string.IsNullOrEmpty(EntityType.GetComment()))
{
#>
/// <summary>
/// <#= code.XmlComment(EntityType.GetComment()) #>
/// </summary>
<#
}
if (Options.UseDataAnnotations)
{
foreach (var dataAnnotation in EntityType.GetDataAnnotations(annotationCodeGenerator))
{
#>
<#= code.Fragment(dataAnnotation) #>
<#
}
}
#>
public partial class <#= FirstUp(EntityType.GetSchema() ?? EntityType.GetDefaultSchema()) + "_" + EntityType.Name #>
{
<#
var firstProperty = true;
foreach (var property in EntityType.GetProperties().OrderBy(p => p.GetColumnOrder() ?? -1))
{
if (!firstProperty)
{
WriteLine("");
}
if (!string.IsNullOrEmpty(property.GetComment()))
{
#>
/// <summary>
/// <#= code.XmlComment(property.GetComment(), indent: 1) #>
/// </summary>
<#
}
if (Options.UseDataAnnotations)
{
var dataAnnotations = property.GetDataAnnotations(annotationCodeGenerator)
.Where(a => !(a.Type == typeof(RequiredAttribute) && Options.UseNullableReferenceTypes && !property.ClrType.IsValueType));
foreach (var dataAnnotation in dataAnnotations)
{
#>
<#= code.Fragment(dataAnnotation) #>
<#
}
}
usings.AddRange(code.GetRequiredUsings(property.ClrType));
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !property.ClrType.IsValueType;
var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !property.ClrType.IsValueType;
#>
public <#= code.Reference(property.ClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
firstProperty = false;
}
foreach (var navigation in EntityType.GetNavigations())
{
WriteLine("");
if (Options.UseDataAnnotations)
{
foreach (var dataAnnotation in navigation.GetDataAnnotations(annotationCodeGenerator))
{
#>
<#= code.Fragment(dataAnnotation) #>
<#
}
}
var targetType = FirstUp(navigation.TargetEntityType.GetSchema() ?? navigation.TargetEntityType.GetDefaultSchema()) + "_" + navigation.TargetEntityType.Name;
if (navigation.IsCollection)
{
#>
public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; set; } = new List<<#= targetType #>>();
<#
}
else
{
var needsNullable = Options.UseNullableReferenceTypes && !(navigation.ForeignKey.IsRequired && navigation.IsOnDependent);
var needsInitializer = Options.UseNullableReferenceTypes && navigation.ForeignKey.IsRequired && navigation.IsOnDependent;
#>
public virtual <#= targetType #><#= needsNullable ? "?" : "" #> <#= navigation.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
}
}
foreach (var skipNavigation in EntityType.GetSkipNavigations())
{
WriteLine("");
if (Options.UseDataAnnotations)
{
foreach (var dataAnnotation in skipNavigation.GetDataAnnotations(annotationCodeGenerator))
{
#>
<#= code.Fragment(dataAnnotation) #>
<#
}
}
//skipNavigation.TargetEntityType.Name = FirstUp(skipNavigation.TargetEntityType.GetSchema() ?? skipNavigation.TargetEntityType.GetDefaultSchema()) + "_" + skipNavigation.TargetEntityType.Name;
#>
public virtual ICollection<<#= FirstUp(skipNavigation.TargetEntityType.GetSchema() ?? skipNavigation.TargetEntityType.GetDefaultSchema()) +"_"+ skipNavigation.TargetEntityType.Name #>> <#= skipNavigation.Name #> { get; set; } = new List<<#=FirstUp(skipNavigation.TargetEntityType.GetSchema() ?? skipNavigation.TargetEntityType.GetDefaultSchema()) +"_"+ skipNavigation.TargetEntityType.Name #>>();
<#
}
#>
}
<#
var previousOutput = GenerationEnvironment;
GenerationEnvironment = new StringBuilder();
foreach (var ns in usings.Distinct().OrderBy(x => x, new NamespaceComparer()))
{
#>
using <#= ns #>;
<#
}
WriteLine("");
GenerationEnvironment.Append(previousOutput);
#>
<#+
private string FirstUp(string name)
{
if (name is null) return name;
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name);
}
#>