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.