Code templates for scaffolding entity types and DbContext from an existing database
rowanmiller opened this issue Β· 57 comments
We have one seam at the moment where folks can get a hold of the IModel we build from the database and manipulate it. We haven't really done any usability testing on this to see if it is good for changing table name conventions, etc..
We will also want some higher level extensibility points, such as code generation templates.
Where is this IModel manipulation being allowed? I guess some sort of extension over the default ef command (EntityFramework.Commands) need to be implemented, but where? Also: does this "extension" for model customization implies creating a new command assembly or will the default EntityFramework.Commands allow some sort of DI?
Regards
The seam we have at the moment is designed for providers to use... so yes, you probably need to implement at least some parts of the provider to use it. This issue is about creating some high level seams that can be used by the application developer.
Just wanted to check my understanding - as of the present release there is no way for an application developer to customise the code generated via scaffolding (a provider writer can), although in earlier releases there was a razor based template approach which has since been pulled? So essentially to do stuff like pluralisation etc we are waiting for the resolution of this issue - is that correct?
@dazinator yes, that is correct. Post the 7.0.0 release we plan to provide customizable code-gen templates and code based interception points where you can customize the reverse engineered model and how it is written out to code.
One problem with this approach is that the existing model seems to support just one Name for an entity, which is used for both the generated type, and also for the property on the context. This means that if you want an entity type called Customer
but and you want the DbSet<Customer>
on your context to be called Customers
, it doesn't look like you can do it, because the code gen pulls the name of both the type and that property from the IEntitySet.Name
property.
I've worked through an attempt to singularize some names. (I'm working with a database where all the tables have plural names. Leaving this as is produces correct-sounding dbset properties, but wrong-sounding entity types. E.g., new Customers
is a weird way to create a single customer. Singular names produce wrong-sounding dbset props, but I find that slightly less disturbing.)
Having done this, I'm not sure the approach of modifying the model feels right, because the name you want depends on where the name is actually being used - a property referring to a collection of entities of some type typically wants a different name than one referring to a single property, and some people might want yet another convention again for the mapped table name. I'd prefer the ability to supply an interface (e.g., INameMapper
) with methods such as GetSingularNavigationPropertyName
, GetMultipleNavigationPropertyName
, GetEntityTypeName
, and maybe even GetContextDbSetPropertyName
.
There's already a partial implementation of this concept in RelationalScaffoldingModelFactory
, which has GetEntityTypeName
and GetPropertyName
methods you can override. My ideal solution to this would have this fleshed out so I can control everything, and for it to be separated out, so that I'm not obliged to derive from, say, SqlServerScaffoldingModelFactory
just to be able to plug in my logic. (Pluralizing/singularizing of identifiers used in code doesn't feel like it should have a dependency on a database-specific type, but as far as I can tell, I needed to make my own derived SqlServerScaffoldingModelFactory
to be able to use this extensibility point.)
I realize this is going on 2 years old, but was anything changed here that might make customization easier?
π For providing customizable code gen templates (for ex, T4 or handlebars), but doing so in a way that can feed into dotnet ef scaffold
, so that it's cross platform and not tied to Visual Studio.
No progress yet. Our main focus for Reverse Engineering is enable updating the model to incorporate changes made to the database after the initial scaffold. #831 During update, we'll try to preserve any manual changes that have been made to the model (e.g. renames and inheritance) which mitigates some of the reasons you might be asking for this feature. But there are still plenty of other reasons to do this feature. It's just a matter of prioritization.
In #9676 @ajcvickers states you would accept a PR to add virtual methods for extensibility. If that's the case, I'd be happy to submit a PR for this.
@tonysneed Yep. There was a PR (#9908) but it was abandoned. We would want protected virtual methods.
If anyone is interested in poking around the internal components...
class MyDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
=> services
.AddSingleton<ICSharpDbContextGenerator, MyDbContextGenerator>()
.AddSingleton<ICSharpEntityTypeGenerator, MyEntityTypeGenerator>();
}
class MyDbContextGenerator : CSharpDbContextGenerator
{
public MyDbContextGenerator(
IScaffoldingProviderCodeGenerator providerCodeGenerator,
IAnnotationCodeGenerator annotationCodeGenerator,
ICSharpUtilities cSharpUtilities)
: base(providerCodeGenerator, annotationCodeGenerator, cSharpUtilities)
{
}
// TODO: Override DbContext generation
}
class MyEntityTypeGenerator : CSharpEntityTypeGenerator
{
public MyEntityTypeGenerator(ICSharpUtilities cSharpUtilities)
: base(cSharpUtilities)
{
}
// TODO: Override entity type generation
}
I can pick this one up. Already signed a contrib agreement for ASPNET Core.
For some reason adding the user profile dotnet
folder to my path system env variable doesn't seem to work.
dotnet --version
doesn't see it.
However, if I replace the path entirely, it's OK.
Am I doing something obviously wrong? TIA!
P.S. I can just open the VS solution from the command line, so I can get past the issue. Just wondering about setting the global path and what I might be doing wrong there.
That answer outdated. Just add an implementation anywhere in your project. The tools will scan the assembly to find it.
@tonysneed Make sure the userprofile dotnet is at the start of the path so that it is found before the normally installed copy.
Also, don't use %USERPROFILE%
in System variables. It's ...unpredictable. π
@ajcvickers I'm having a hard time controlling the placement of the userprofile dotnet in the path. Adding it at the top ..
Seems to have no effect. It is placed after the system dotnet.
Not a big deal, since I can set the path on the command line and open VS from there. Just curious if I'm missing something obvious.
I have removed the system .net path setting
@tonysneed I think you need to set it as a system environment variable--at least, that's what I did. You can also click on Edit text..,
then it's easy to put it where you want:
OK, mystery solved. I was opening the command prompt from the GitHub Desktop app, and it wasn't picking up the new environment variables -- until I restarted the app.
Locally set env var for win
@bricelam I'm assuming if the implementation were provided in a separate library / package, then you would need to register it in Startup.ConfigureDesignTimeServices
?
So I have a general question. I have a need to generate custom classes based on an EF model. These could either be C# or TypeScript classes. Would the best way be to implement IScaffoldingCodeGenerator
?
My goal would be to have the code generator use a templating engine that is not coupled to Visual Studio and can run cross-platform, such as Handlebars.Net. Is there anything about this approach that might conflict with your plans for the toolchain?
...you would need to register it in Startup.ConfigureDesignTimeServices?
For now, yes. But we probably want to improve this experience as part of the design-time extensibility work.
implement
IScaffoldingCodeGenerator
?
Yes although, our factoring looks like it could use some improvement...
Is there anything about this approach that might conflict with your plans for the toolchain?
No, in fact we had an early implementation that used Razor, but it didn't feel like the right choice for generating C#, so we backed it out and decided to wait for other templating engines to emerge on .NET Core.
Thanks @bricelam .. is the .NET Core Template Engine suitable?
Hmm, I don't think it's the right tool for the job. We need something that can operate on a structured model. E.g. foreach
over a collection of items and read properties from those items.
T4 worked well in EF6. (So well that I prototyped a port to .NET Core when we were considering this originally.)
Yes I came across your port and thought it looked promising, because many folks have experience with T4, and running it from the command-line using .net core provides x-plat capability. But seems T4 may not be so appealing to the non-VS side.
My thought is that it's easy enough to implement something like IScaffoldingCodeGenerator
and plug in whatever templating engine you want. The important thing is to pick one that can provide an example of how to do it, which is something I'd like to take on, because my framework replies on a good code gen story.
I created a package that allows customization of generated classes that are reverse engineered from an existing database using Handlebars templates: EntityFrameworkCore.Scaffolding.Handlebars. The project repo is here: https://github.com/TrackableEntities/EntityFrameworkCore.Scaffolding.Handlebars.
To use it just add the NuGet package to a .NET Core library or app, and add a class that implements IDesignTimeServices
in which you call the AddHandlebarsScaffolding
method.
public class ScaffoldingDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
// There are options to scaffold only the entities or only the context class
var options = ReverseEngineerOptions.DbContextAndEntities;
services.AddHandlebarsScaffolding(options);
}
}
When you run the dotnet ef dbcontext scaffold
command and pass a connection string and other args, the package will add a CodeTemplates folder that contains CSharpDbContext and CSharpEntityType folders with Handlebars templates, and the EF tooling will use them to generate customizable context and entity classes.
For example, there is a Properties.hbs template in CSharpEntityType/Partials that you can customize to use List<T>
instead of ICollection<T>
:
public List<{{nav-property-type}}> {{nav-property-name}} { get; set; }
Likewise, you can also customize other aspects of the class, such as imports or the constructor, or you can inherit from a particular base class.
@bricelam I have some suggestions as a result of going through the process that could make it easier to use packages like this when executing the scaffolding commands. I'll create separate issues for those and link to them from here. See #10154, #10156.
The nice thing about the ASPNET generators is that they pick up razor templates placed in a default location and just use them. Iβm wondering what the drawbacks of using Razor to generate EF models would be?
Even something simple like exists for JSON.Net would be really helpful:
https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Serialization_DefaultContractResolver.htm
public class MyContractResolver : DefaultContractResolver
{
protected override string ResolvePropertyName(string propertyName)
{
//Modify
return propertyName;
}
}
Then have them for like:
Table Name
Column Name
Base Class Name
That covers most reasonable/expected/common cases without having some monolithic feature to support.
Iβm wondering what the drawbacks of using Razor to generate EF models would be?
@tonysneed the consensus was that they resulting syntax was a bit strange even for such simple things as generating multiple consecutive lines of C# code. Perhaps we were not using it right? Here is the last version of what we had:
(was removed on #3393).
Another reason I remember is that we started asking folks about how to achieve certain things it became clear that Razor was not designed to output plain C# and that although it was possible to hack our way through to do so, we would do it at our risk. I.e. there were no plans to do any work on Razor at the time to make sure that the scenario would work well and to support it.
Hello @rowanmiller, @divega , @bricelam,
I'd like to add an extensibility point to the entity file naming.
Currently it's set to:
I would like to open this up by adding another public interface as a public virtual property to the CSharpModelGenerator class. Something along the lines of "IEntityFileNameGenerator" with a single method "GenerateFileName"
Here is an example:
https://gist.github.com/irperez/2ca16d2e9480f040e54e271abcc34c36
namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal { public interface IEntityFileNameGenerator { string GenerateFileName(IEntityType entityType, string FileExtension); } }
Similar to what you are doing here:
https://github.com/aspnet/EntityFrameworkCore/blob/2377e8c2418b5dc9925af567b9d79c63235fbcbb/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs#L28
The idea is that I would create a default implementation that would default to what's being done today such as what's shown above. entityType.DisplayName() + FileExtension.
This gives third party developers the ability to overwrite that behavior and add something like so: entityType.DisplayName() + '.g' + FileExtension // => "TestObject.g.cs"
This would make it easier for developers to utilize partial classes or to customize the naming to their needs.
I believe this will require an addition to this section of code to ensure the default implementation is loaded:
https://github.com/aspnet/EntityFrameworkCore/blob/a603b983d0eca46f664fb1780bec3b6ef3e8c5ac/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs#L48-L87
We'll also need to add the IEntityFileNameGenerator as a parameter to this constructor:
https://github.com/aspnet/EntityFrameworkCore/blob/a603b983d0eca46f664fb1780bec3b6ef3e8c5ac/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs#L34-L38
Did I miss anything?
I can provide the PR for this, but looking for approval/feedback. Also if approved, should this be done on the "dev" branch?
@irperez Sorry I missed completely you comment. I would recommend creating a new issue so that we take a look at it in our triage. It is not super clear to me that you would need a new extensibility point, but I haven't looked at what your scenario in enough detail.
While we're all quietly waiting for something... anything to happen in this space I took the liberty of decompiling the inbuilt CSharp generator and providing a few more extensibility points.
A caveat: I had to drop the column ordering because it was looking for a type in some old version of .Relational. So it no longer orders the column properties by their table column ordinal. Probably fixable but ain't nobody got time for TypeLoadException's.
Could not load type 'Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalFullAnnotationNames' from assembly 'Microsoft.EntityFrameworkCore.Relational, Version=2.2.4.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'.
Certainly something I personally don't care about.
An example of use:
public class SynotiveCSharpEntityTypeGenerator: ExtensibleCSharpEntityTypeGenerator
{
private readonly ICSharpHelper _cSharpHelper;
public SynotiveCSharpEntityTypeGenerator(ICSharpHelper cSharpHelper) : base(cSharpHelper)
{
_cSharpHelper = cSharpHelper;
}
protected override void GenerateProperty(IProperty property, EntityTypeGenerationContext context)
{
var type =
property.ClrType.IsAssignableFrom(typeof(string)) &&
property.Name.Contains("Uri", StringComparison.InvariantCultureIgnoreCase)
? _cSharpHelper.Reference(typeof(Uri))
: _cSharpHelper.Reference(property.ClrType);
context.StringBuilder.AppendLine("public " + type + " " + property.Name +
" { get; set; }");
}
}
The implementation:
public class EntityTypeGenerationContext
{
public EntityTypeGenerationContext(IEntityType entityType, bool useDataAnnotations)
{
EntityType = entityType;
StringBuilder = new IndentedStringBuilder();
UseDataAnnotations = useDataAnnotations;
}
public IEntityType EntityType { get; }
public IndentedStringBuilder StringBuilder { get; }
public bool UseDataAnnotations { get; }
}
public class ExtensibleCSharpEntityTypeGenerator : ICSharpEntityTypeGenerator
{
private readonly ICSharpHelper _code;
public ExtensibleCSharpEntityTypeGenerator(ICSharpHelper cSharpHelper)
{
_code = cSharpHelper;
}
public virtual string WriteCode(
IEntityType entityType,
string @namespace,
bool useDataAnnotations)
{
var context = new EntityTypeGenerationContext(entityType, useDataAnnotations);
GenerateNamespaces(entityType, context);
context.StringBuilder.AppendLine();
context.StringBuilder.AppendLine("namespace " + @namespace);
context.StringBuilder.AppendLine("{");
using (context.StringBuilder.Indent())
GenerateClass(entityType, context);
context.StringBuilder.AppendLine("}");
return context.StringBuilder.ToString();
}
protected virtual void GenerateNamespaces(IEntityType entityType, EntityTypeGenerationContext context)
{
context.StringBuilder.AppendLine("using System;");
context.StringBuilder.AppendLine("using System.Collections.Generic;");
if (context.UseDataAnnotations)
{
context.StringBuilder.AppendLine("using System.ComponentModel.DataAnnotations;");
context.StringBuilder.AppendLine("using System.ComponentModel.DataAnnotations.Schema;");
}
foreach (string str in entityType.GetProperties().SelectMany(p => p.ClrType.GetNamespaces()).Where(ns =>
{
if (ns != "System")
return ns != "System.Collections.Generic";
return false;
}).Distinct().OrderBy(x => x, new NamespaceComparer()))
{
context.StringBuilder.AppendLine("using " + str + ";");
}
}
protected virtual void GenerateClass(IEntityType entityType, EntityTypeGenerationContext context)
{
GenerateEntityTypeDataAnnotations(entityType, context);
context.StringBuilder.AppendLine("public partial class " + entityType.Name);
context.StringBuilder.AppendLine("{");
using (context.StringBuilder.Indent())
{
GenerateConstructor(entityType, context);
GenerateProperties(entityType, context);
GenerateNavigationProperties(entityType, context);
}
context.StringBuilder.AppendLine("}");
}
protected virtual void GenerateEntityTypeDataAnnotations(IEntityType entityType,
EntityTypeGenerationContext context)
{
if (context.UseDataAnnotations)
{
GenerateTableAttribute(entityType, context);
}
}
protected void GenerateTableAttribute(IEntityType entityType, EntityTypeGenerationContext context)
{
var tableName = entityType.Relational().TableName;
var schema = entityType.Relational().Schema;
var defaultSchema = entityType.Model.Relational().DefaultSchema;
var flag = schema != null && schema != defaultSchema;
if ((flag ? 1 : (tableName == null ? 0 : (tableName != entityType.Scaffolding().DbSetName ? 1 : 0))) == 0)
return;
var attributeWriter = new AttributeWriter("TableAttribute");
attributeWriter.AddParameter(_code.Literal(tableName));
if (flag)
attributeWriter.AddParameter("Schema = " + _code.Literal(schema));
context.StringBuilder.AppendLine(attributeWriter.ToString());
}
protected virtual void GenerateConstructor(IEntityType entityType, EntityTypeGenerationContext context)
{
var list = entityType.GetNavigations().Where(n => n.IsCollection()).ToList();
if (list.Count <= 0)
return;
context.StringBuilder.AppendLine("public " + entityType.Name + "()");
context.StringBuilder.AppendLine("{");
using (context.StringBuilder.Indent())
{
foreach (var navigation in list)
{
context.StringBuilder.AppendLine(
navigation.Name + " = new HashSet<" + navigation.GetTargetType().Name + ">();");
}
}
context.StringBuilder.AppendLine("}");
context.StringBuilder.AppendLine();
}
protected virtual void GenerateProperties(IEntityType entityType, EntityTypeGenerationContext context)
{
foreach (var property in entityType.GetProperties())
{
GeneratePropertyDataAnnotations(property, context);
GenerateProperty(property, context);
}
}
protected virtual void GenerateProperty(IProperty property, EntityTypeGenerationContext context)
{
context.StringBuilder.AppendLine("public " + _code.Reference(property.ClrType) + " " + property.Name +
" { get; set; }");
}
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
protected virtual void GeneratePropertyDataAnnotations(IProperty property, EntityTypeGenerationContext context)
{
if (context.UseDataAnnotations)
{
GenerateKeyAttribute(property, context);
GenerateRequiredAttribute(property, context);
GenerateColumnAttribute(property, context);
GenerateMaxLengthAttribute(property, context);
}
}
protected virtual void GenerateKeyAttribute(IProperty property, EntityTypeGenerationContext context)
{
var primaryKey = property.AsProperty(nameof(GenerateKeyAttribute)).PrimaryKey;
if (primaryKey == null || primaryKey.Properties.Count != 1 || primaryKey is Key key &&
primaryKey.Properties.SequenceEqual(
new KeyDiscoveryConvention(null).DiscoverKeyProperties(key.DeclaringEntityType,
key.DeclaringEntityType.GetProperties().ToList())) || primaryKey.Relational().Name !=
ConstraintNamer.GetDefaultName(primaryKey))
return;
context.StringBuilder.AppendLine(new AttributeWriter("KeyAttribute"));
}
protected virtual void GenerateColumnAttribute(IProperty property, EntityTypeGenerationContext context)
{
var columnName = property.Relational().ColumnName;
var configuredColumnType = property.GetConfiguredColumnType();
var parameter = columnName == null || columnName == property.Name ? null : _code.Literal(columnName);
var str = configuredColumnType != null ? _code.Literal(configuredColumnType) : null;
switch (parameter ?? str)
{
case null:
break;
default:
var attributeWriter = new AttributeWriter("ColumnAttribute");
if (parameter != null)
attributeWriter.AddParameter(parameter);
if (str != null)
attributeWriter.AddParameter("TypeName = " + str);
context.StringBuilder.AppendLine(attributeWriter);
break;
}
}
protected virtual void GenerateMaxLengthAttribute(IProperty property, EntityTypeGenerationContext context)
{
var maxLength = property.GetMaxLength();
if (!maxLength.HasValue)
return;
var attributeWriter =
new AttributeWriter(property.ClrType == typeof(string)
? "StringLengthAttribute"
: "MaxLengthAttribute");
attributeWriter.AddParameter(_code.Literal(maxLength.Value));
context.StringBuilder.AppendLine(attributeWriter.ToString());
}
protected virtual void GenerateRequiredAttribute(IProperty property, EntityTypeGenerationContext context)
{
if (property.IsNullable || !IsNullableType(property.ClrType) ||
property.IsPrimaryKey())
return;
context.StringBuilder.AppendLine(new AttributeWriter("RequiredAttribute").ToString());
}
protected static bool IsNullableType(Type type)
{
var typeInfo = type.GetTypeInfo();
if (!typeInfo.IsValueType)
return true;
if (typeInfo.IsGenericType)
return typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>);
return false;
}
protected virtual void GenerateNavigationProperties(IEntityType entityType, EntityTypeGenerationContext context)
{
var source = entityType.GetNavigations().OrderBy(n => !n.IsDependentToPrincipal() ? 1 : 0)
.ThenBy(n => !n.IsCollection() ? 0 : 1);
if (!source.Any())
return;
context.StringBuilder.AppendLine();
foreach (var navigation in source)
{
GenerateNavigationDataAnnotations(navigation, context);
GenerateNavigationProperty(navigation, context);
}
}
protected virtual void GenerateNavigationProperty(INavigation navigation, EntityTypeGenerationContext context)
{
var name = navigation.GetTargetType().Name;
context.StringBuilder.AppendLine((object) ("public virtual " +
(navigation.IsCollection()
? "ICollection<" + name + ">"
: name) + " " + navigation.Name + " { get; set; }"));
}
protected virtual void GenerateNavigationDataAnnotations(INavigation navigation,
EntityTypeGenerationContext context)
{
if (context.UseDataAnnotations)
{
GenerateForeignKeyAttribute(navigation, context);
GenerateInversePropertyAttribute(navigation, context);
}
}
private void GenerateForeignKeyAttribute(INavigation navigation, EntityTypeGenerationContext context)
{
if (!navigation.IsDependentToPrincipal() || !navigation.ForeignKey.PrincipalKey.IsPrimaryKey())
return;
var attributeWriter = new AttributeWriter("ForeignKeyAttribute");
attributeWriter.AddParameter(_code.Literal(string.Join(",",
navigation.ForeignKey.Properties.Select(p => p.Name))));
context.StringBuilder.AppendLine((object) attributeWriter.ToString());
}
private void GenerateInversePropertyAttribute(INavigation navigation, EntityTypeGenerationContext context)
{
if (!navigation.ForeignKey.PrincipalKey.IsPrimaryKey())
return;
var inverse = navigation.FindInverse();
if (inverse == null)
return;
var attributeWriter = new AttributeWriter("InversePropertyAttribute");
attributeWriter.AddParameter(_code.Literal(inverse.Name));
context.StringBuilder.AppendLine((object) attributeWriter.ToString());
}
private class AttributeWriter
{
private readonly List<string> _parameters = new List<string>();
private readonly string _attributeName;
public AttributeWriter(string attributeName)
{
_attributeName = attributeName;
}
public void AddParameter(string parameter)
{
_parameters.Add(parameter);
}
public override string ToString()
{
return "[" + (_parameters.Count == 0
? StripAttribute(_attributeName)
: StripAttribute(_attributeName) + "(" + string.Join(", ", _parameters) + ")") + "]";
}
private static string StripAttribute(string attributeName)
{
if (!attributeName.EndsWith("Attribute", StringComparison.Ordinal))
return attributeName;
return attributeName.Substring(0, attributeName.Length - 9);
}
}
}
I fixed the ordering -> https://gist.github.com/worldspawn/9773700c8581b3b458f74ecf6abb2dcf
worldspawn, Many thanks for the EntityTypeGenerationContext.
I've been trying to use it to change my DBContext, but not having much luck.
To use the example you provided ,would I need to add your example class to the IServiceCollection?
That is, would I need to do this:
public class ScaffoldingDesignTimeServices : IDesignTimeServices { public void ConfigureDesignTimeServices(IServiceCollection services) { services.AddSingleton<ExtensibleCSharpEntityTypeGenerator, SynotiveCSharpEntityTypeGenerator>(); } }
and then run dotnet ef dctontext scaffold.... ?
I'm trying to find an EF Core 2.2 equivalent to the code written for EF 1.x below, but DBContextWriter has gone away.
`public class CustomDbContextWriter : DbContextWriter
{
public CustomDbContextWriter(
ScaffoldingUtilities scaffoldingUtilities,
CSharpUtilities cSharpUtilities)
: base(scaffoldingUtilities, cSharpUtilities)
{ }
public override string WriteCode(ModelConfiguration modelConfiguration)
{
var code = base.WriteCode(modelConfiguration);
foreach (var entityConfig in modelConfiguration.EntityConfigurations)
{
var entityName = entityConfig.EntityType.Name;
//var setName = Inflector.Inflector.Pluralize(entityName) ?? entityName;
var setName = entityName;
code = code.Replace(
$"DbSet<{entityName}> {entityName}",
$"DbSet<{entityName}> {setName}");
}
return code;
}
}`
Yes the old syntax did seem more handy. I dont think this approach can help with the output of the actual context class itself.
You don't need to do anything with your ScaffoldingDesignTimeServices. Just make sure its in the same library as your context and remember to set that same library as the default project, in package manager console have the same library selected as the target. I had a similar problem getting it to work and that was what I had done correctly.
Throwing Console.WriteLines in the code should emit to the package manager console.
An even better solution is to hand code your entities and your context! gasp I'm forced to use this malarky, if you have a choice - dont :D
I put together a sample in bricelam/EFCore.TextTemplating showing how to use T4 templates to customize the code scaffolded by Scaffold-DbContext (and dotnet ef dbcontext scaffold
). We're going to keep evolving it and bring it in as part of the EF Core project. Here are some changes we'd like to make to it as part of that process:
- Make the default templates handle everything that CSharpModelGenerator does today
- But also provide a minimalist template like the one in the sample
- Discover template files in the project (like EF6 does) and process them at runtime (well, during
ef dbcontext scaffold
that is) using Mono.TextTemplating - Make generating IEntityTypeConfiguration classes opt-in (issue #8434)
- Think about ways to help users get their copies of the templates up-to-date (e.g. by providing a link in the templates of where to find the latest versions)
Consider the ability to split out tables and views. See #20264.
When is this going to be implemented?
@janissimsons
This issue is in the Backlog milestone. This means that it is not planned for the next release (EF Core 5.0). We will re-assess the backlog following the this release and consider this item at that time. However, keep in mind that there are many other high priority features with which it will be competing for resources.
Note: consider C# 9 records.
@ajcvickers could you please add some note (maybe in the readme) about the pros/cons/points of attention on using a model entirely written with C# records instead of classes?
The properties are not necessarily read-only as they can be defined explicitly.
Since EF Core heavily use reflection, I just want to be sure not to find potential pitfalls when writinga model with records.
Thanks
Looking forward to an update on this item. Even if it was a simple as a JSON settings file. The ability to fix a bad name even by schema and table name something unique would be great.
@cgountanis Try EF Core Power Tools!
@cgountanis Try EF Core Power Tools!
What about Non-Visual-Studio (VS Code, command line) and Non-Windows users? I see a need there to make it available for them as well. It's an actual impediment in my current team.
@JauernigIT @cgountanis My EF Core Scaffolding tool allows customization of generated entities using Handlebars templates when reverse engineering them from a database β all from the command line with the standard dotnet ef CLI: https://github.com/TrackableEntities/EntityFrameworkCore.Scaffolding.Handlebars
Note that when this feature is implemented (it's in the plan for the 7.0.0 release), it's planned to be fully cross platform and command-line-compatible.
@roji This sounds cool. Where could I find more info on what youβre planning to do with code templates?
There's some info up on this issue, but the general idea would likely be to have customizable T4 templates for scaffolding, which would be used when e.g. the command line is used.
@JauernigIT @cgountanis My EF Core Scaffolding tool allows customization of generated entities using Handlebars templates when reverse engineering them from a database β all from the command line with the standard dotnet ef CLI: https://github.com/TrackableEntities/EntityFrameworkCore.Scaffolding.Handlebars
With a team of developers, kinda need to be standard issued. I do anything/everything possible to stay away from 3rd party extras for obvious setup/support reasons.
Please, make sure that the feature is .NET language agnostic so that https://github.com/efcore/EFCore.FSharp can extend it for F#