Adventures in Entity Framework Core 7 Constructor Binding
Entity Framework has supported using custom constructors for several versions now, but in order for it to work you need to follow the convention that EF expects for this to actually work. If you fall foul of those conventions, you'll likely see something like the following:
No suitable constructor was found for entity type 'Test'. The following constructors had parameters that could not be bound to properties of the entity type:
Cannot bind 'did' in 'Test(string title, int did)
Note that only mapped properties can be bound to constructor parameters. Navigations to related entities, including references to owned types, cannot be bound.
This is basically telling you that EF didn't know what to do with that did
parameter in the constructor. If your Test
type looks like this, the cause is fairly obvious:
private class Test
{
public Test(string title, int did)
{
}
public string Title { get; set; }
}
Hint: there's no property/field etc. that the did
parameter could be linked to. But what about this?
public class Test
{
public Test(string defaultHTML, string htmlContent)
{
...
}
public string DefaultHTML { get; set; }
public string HTMLContent { get; set; }
}
Looks perfectly reasonable to me. But if you try and use it, EF will complain loudly:
No suitable constructor was found for entity type 'Test'. The following constructors had parameters that could not be bound to properties of the entity type:
Cannot bind 'htmlContent' in 'Test(string defaultHTML, string htmlContent)'
Note that only mapped properties can be bound to constructor parameters. Navigations to related entities, including references to owned types, cannot be bound.
This failure to bind the htmlContent
constructor parameter could be caused by a few things:
The property is not mapped via something like
entity.Property(e => e.HTMLContent)
in the configuration.Type mismatch between the constructor parameter and the target property to bind to.
The type of the parameter and property match and assuming that the entity configuration is up to scratch as well, the cause of this may leave you scratching your head. You can take a look at the docs for what naming conventions are accepted which includes this tidbit:
The parameter types and names must match property types and names, except that properties can be Pascal-cased while the parameters are camel-cased.
htmlContent
seems camel-cased to me so doesn't really help much. Thankfully, EF is open source, so we can dig into the code to see what's actually going on. There we can see EF is doing the following transformation on our parameter names when looking for matches:
var pascalized = char.ToUpperInvariant(parameterName[0]) + parameterName[1..];
So to match our constructor get accepted by EF, we need to change the parameter names to this:
public Test(string defaultHTML, string hTMLContent)
{ }
Note how the htmlContent
parameter has been renamed to hTMLContent
(capitalised TML, lowercase h). Personally, I find the names which result from this pretty hideous. The fact that "HTML" is in uppercase in defaultHTML
but the h
is lowercase in hTMLContent
is an eyesore.
One option to get around this might be able to simply change the name of the property from HTMLContent
to HtmlContent
, in which case the problem goes away. If the underlying database has the original name, you can preserve it via using HasColumnName.
That disconnect doesn't seem ideal to me - a single call to HasColumnName
tucked away somewhere is going to quickly be forgotten about. Fortunately, you can change this behaviour by swapping out the implementation of IPropertyParameterBindingFactory
that is used during constructor binding to something like this:
public class CaseInsensitivePropertyParameterBindingFactory
: IPropertyParameterBindingFactory
{
public ParameterBinding FindParameter(IEntityType entityType,
Type parameterType,
string parameterName)
{
return entityType.GetProperties()
.Where(p => p.ClrType == parameterType
&& p.Name.Equals(parameterName,
StringComparison.OrdinalIgnoreCase))
.Select(p => new PropertyParameterBinding(p))
.FirstOrDefault();
}
}
To use it, you need to call ReplaceService
when configuring your DbContext
like so:
services.AddDbContext<MyDbContext>(options =>
{
options.ReplaceService<IPropertyParameterBindingFactory,
CaseInsensitivePropertyParameterBindingFactory>();
...
}
With that in place, the example from earlier will bind the htmlContent
constructor to the HTMLContent
property without error.
Word of warning though - these APIs are flagged as internal EF APIs and so may change in future versions.