Closed tonysneed closed 5 years ago
@netbitshift The first thing you need to do is create a class in your project that inherits from HbsCSharpEntityTypeGenerator
and overrides GeneratePropertyDataAnnotations
, customizing the RequiredAttribute
that is inserted by EF Core scaffolding.
Then you will add a ScaffoldingDesignTimeServices
class that implements IDesignTimeServices
in which you call services.AddHandlebarsScaffolding
, followed by the registration of your custom HbsCSharpEntityTypeGenerator
:
services.AddSingleton<ICSharpEntityTypeGenerator, MyHbsCSharpEntityTypeGenerator>();
MyCSharpEntityTypeGenerator
is a bit involved, but not too complicated. Here is the code.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using EntityFrameworkCore.Scaffolding.Handlebars;
using EntityFrameworkCore.Scaffolding.Handlebars.Internal;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
namespace MyHandlebarsScaffoldingExtensions
{
public class MyHbsCSharpEntityTypeGenerator : HbsCSharpEntityTypeGenerator
{
public MyHbsCSharpEntityTypeGenerator(
IEntityTypeTemplateService entityTypeTemplateService,
IEntityTypeTransformationService entityTypeTransformationService,
ICSharpHelper cSharpHelper) : base(entityTypeTemplateService, entityTypeTransformationService, cSharpHelper)
{
}
protected override void GeneratePropertyDataAnnotations(IProperty property)
{
if (property == null) throw new ArgumentNullException(nameof(property));
GenerateKeyAttribute(property);
GenerateRequiredAttribute(property);
GenerateColumnAttribute(property);
GenerateMaxLengthAttribute(property);
}
private void GenerateRequiredAttribute(IProperty property)
{
if (!property.IsNullable
&& property.ClrType.IsNullableType()
&& !property.IsPrimaryKey())
{
// Add RequiredAttribute with parameter
var requiredAttribute = new AttributeWriter(nameof(RequiredAttribute));
requiredAttribute.AddParameter("AllowEmptyStrings=true");
PropertyAnnotationsData.Add(new Dictionary<string, object>
{
{ "property-annotation", requiredAttribute.ToString()},
});
}
}
private void GenerateKeyAttribute(IProperty property)
{
var key = property.AsProperty().PrimaryKey;
if (key?.Properties.Count == 1)
{
if (key is Key concreteKey
&& key.Properties.SequenceEqual(new KeyDiscoveryConvention(null).DiscoverKeyProperties(concreteKey.DeclaringEntityType, concreteKey.DeclaringEntityType.GetProperties().ToList())))
{
return;
}
if (key.Relational().Name != ConstraintNamer.GetDefaultName(key))
{
return;
}
PropertyAnnotationsData.Add(new Dictionary<string, object>
{
{ "property-annotation", new AttributeWriter(nameof(KeyAttribute)) },
});
}
}
private void GenerateColumnAttribute(IProperty property)
{
var columnName = property.Relational().ColumnName;
var columnType = property.GetConfiguredColumnType();
var delimitedColumnName = columnName != null && columnName != property.Name ? CSharpHelper.Literal(columnName) : null;
var delimitedColumnType = columnType != null ? CSharpHelper.Literal(columnType) : null;
if ((delimitedColumnName ?? delimitedColumnType) != null)
{
var columnAttribute = new AttributeWriter(nameof(ColumnAttribute));
if (delimitedColumnName != null)
{
columnAttribute.AddParameter(delimitedColumnName);
}
if (delimitedColumnType != null)
{
columnAttribute.AddParameter($"{nameof(ColumnAttribute.TypeName)} = {delimitedColumnType}");
}
PropertyAnnotationsData.Add(new Dictionary<string, object>
{
{ "property-annotation", columnAttribute },
});
}
}
private void GenerateMaxLengthAttribute(IProperty property)
{
var maxLength = property.GetMaxLength();
if (maxLength.HasValue)
{
var lengthAttribute = new AttributeWriter(
property.ClrType == typeof(string)
? nameof(StringLengthAttribute)
: nameof(MaxLengthAttribute));
lengthAttribute.AddParameter(CSharpHelper.Literal(maxLength.Value));
PropertyAnnotationsData.Add(new Dictionary<string, object>
{
{ "property-annotation", lengthAttribute.ToString() },
});
}
}
private class AttributeWriter
{
private readonly string _attibuteName;
private readonly List<string> _parameters = new List<string>();
public AttributeWriter(string attributeName)
{
_attibuteName = attributeName ?? throw new ArgumentNullException(nameof(attributeName));
}
public void AddParameter(string parameter)
{
if (parameter == null) throw new ArgumentNullException(nameof(parameter));
_parameters.Add(parameter);
}
public override string ToString()
=> "[" + (_parameters.Count == 0
? StripAttribute(_attibuteName)
: StripAttribute(_attibuteName) + "(" + string.Join(", ", _parameters) + ")") + "]";
private static string StripAttribute(string attributeName)
{
if (attributeName == null) throw new ArgumentNullException(nameof(attributeName));
return attributeName.EndsWith("Attribute", StringComparison.Ordinal)
? attributeName.Substring(0, attributeName.Length - 9)
: attributeName;
}
}
}
}
The most interesting part for you would be the GenerateRequiredAttribute
method, in which you add the desired parameter.
The ScaffoldingDesignTimeServices
class looks like this.
public class ScaffoldingDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection services)
{
// Add Handlebars scaffolding templates
services.AddHandlebarsScaffolding();
// Add custom data annotations
services.AddSingleton<ICSharpEntityTypeGenerator, MyHbsCSharpEntityTypeGenerator>();
}
}
The sad part of this story is that, for this to work, you need to perform scaffolding from the command line using dotnet ef context scaffold
, rather than the nice GUI of the EF Core Power Tools by @ErikEJ.
hmmm, well at least its possible, but our workflow needs to stay within the use of the ef powertools ui and/or the handlebar templates
I suggest you raise an issue in the EF Core repository, I have had several PRs to scaffolding improvements accepted recently.
👍for @ErikEJ suggestion to see if the built-in EF Core scaffolding lets you specify the parameter you need.
If not, there are two ways you can use the Handlebars templates. One way is with the EF Core Power Tools, but you will not be able to customize generation at the code level or use Handlebars helpers. The other way is to generate the entities by running dotnet ef context scaffold
from the command-line, which is definitely more tedious than the UI approach, but it's possible.
Yes @ErikEJ - This does seem more like a EF core issue rather than a tool issue and Yes @tonysneed , we were using the command line before we started using powertools.
All this because as is often the case, one or more fields in the Db are marked "not null" but we still want to allow a record to be generated, so those fields where allowed would get an empty string.
It's really sort of a data issue. If you require a given field of a record to be provided to complete a valid record, then NEITHER empty string or null should be allowed, but i digress.
Continuation of this issue.