1

I would like to apply a single transformation over a large number of columns in Entity Framework 5 without having to explicitly type them all out. As an example I would like to do the following over 50+ columns (convert PascalCase to UNDERSCORE_CASE).

modelBuilder.Entity<Department>() 
            .Property(t => t.DepartmentName) 
            .HasColumnName("DEPARTMENT_NAME");

I found the Dapper.FluentMap which can provide this functionality but it doesn't appear to work when creating the query.

Is there a way to loop over the list of properties and specify the column name following a pattern? For reference the Dapper Transform is listed as

public PropertyTransformConvention()
{
    Properties()
        .Configure(c => c.Transform(s => Regex.Replace(input: s, pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4")));
}

EDIT: This is similar to this question but it does not work for me. Perhaps this has different requirements for EF5.

Using the answer from @Hopeless I have attempted the following modification but the syntax is not quite right. I am new to EF so am not familiar with how to convert the older syntax to the newer.

modelBuilder.Entity<Job>()
            .Map(m =>
{
    m.Properties<Job>(e => e.HasColumnName(name => RegEx.Replace(name, "(?<=.)(?=[A-Z])", "_").ToUpper()));
});
Community
  • 1
  • 1
McArthey
  • 1,614
  • 30
  • 62

2 Answers2

1

You can use the Properties method of DbModelBuilder. Translate the pascal case pattern to underscore pattern easily like this:

modelBuilder.Properties()
            .Configure(e => e.HasColumnName(Regex.Replace(e.ClrPropertyInfo.Name, "(?<=.)(?=[A-Z])", "_").ToUpper());

The pattern can also be like this (.)([A-Z]) and the replacement should then be $1_$2.

Of course the input name should exactly have form of SomeThing. You can also take the pattern in Dapper (posted in your question), which works more exactly for some other rare cases (even including this format DDos (which will be converted to D_Dos). The point here is it does not translate to uppercase for you.

Edit:

It's a pity that in EF5, modelBuilder does not have Properties() method. So for a specific entity type, you can try this:

//in your OnModelCreating scope
//names of navigation properties defined in Job should be passed 
//in TransformAllColumns method
new CapsUnderscorePropertiesConfig<Job>(modelBuilder).TransformAllColumns();

//a helper class
public class CapsUnderscorePropertiesConfig<T> where T : class
{
    EntityTypeConfiguration<T> _entityConfig;
    Dictionary<Type, MethodInfo> _propertyMethods = new Dictionary<Type,MethodInfo>();
    MethodInfo propertyForStruct;
    MethodInfo propertyForNullableStruct;
    public CapsUnderscorePropertiesConfig(DbModelBuilder modelBuilder)
    {
        _entityConfig = modelBuilder.Entity<T>();               
    }
    void config(PropertyInfo pInfo)
    {
        var p = Expression.Parameter(typeof(T));
        var expType = typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(typeof(T), pInfo.PropertyType));
        MethodInfo mi;
        _propertyMethods.TryGetValue(pInfo.PropertyType, out mi);
        if (mi == null)
        {
            if (pInfo.PropertyType.IsValueType)
            {
                //find the Property method for struct type having argument matching Expression<Func<TEntityType, T?>>
                //note the T? inside Func<...> (there is another overload but with T instead).
                if (propertyForStruct == null)
                {
                    foreach(var prop in _entityConfig.GetType().GetMethods().Where(m => m.Name == "Property" && m.IsGenericMethodDefinition)
                                                     .Select(e => new { genMethodDef = e, genMethod = e.MakeGenericMethod(pInfo.PropertyType) })){    
                        //there should be just 2 generic Property<T> methods filtered inhere.
                        //One is for nullable struct and the other is for struct.
                         var secondFuncArgType = prop.genMethodDef.GetParameters()[0].ParameterType.GetGenericArguments()[0].GetGenericArguments()[1];
                         if (secondFuncArgType.IsGenericType && secondFuncArgType.GetGenericTypeDefinition() == typeof(Nullable<>))
                             propertyForNullableStruct = prop.genMethodDef;
                         else propertyForStruct = prop.genMethodDef;
                    }
                }
                mi = pInfo.PropertyType.IsGenericType && pInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) ? 
                    propertyForNullableStruct.MakeGenericMethod(pInfo.PropertyType) :
                    propertyForStruct.MakeGenericMethod(pInfo.PropertyType);
            }
            else //possible property type is string, byte[] or geo type
            {
                mi = _entityConfig.GetType().GetMethods().Single(m => m.Name == "Property" && !m.IsGenericMethodDefinition 
                                                                 && m.GetParameters()[0].ParameterType == expType);
            }
            _propertyMethods[pInfo.PropertyType] = mi;
        }
        var propConfig = mi.Invoke(_entityConfig, new object[] { Expression.Lambda(Expression.Property(p, pInfo.Name), p) }) as PrimitivePropertyConfiguration;            
        propConfig.HasColumnName(Regex.Replace(pInfo.Name, "(?<=.)(?=[A-Z])", "_").ToUpper());
    }
    //at the time of configuring, the Metadataworkspace is not present
    //So we cannot know which properties are navigation properties
    //Those propertie can be excluded by passing their names in here
    public void TransformAllColumns(params string[] excludedNavProperties)
    {
        foreach (var prop in typeof(T).GetProperties().Where(p => !excludedNavProperties.Contains(p.Name)))
        {
            config(prop);
        }
    }
}

NOTE: The code above works only for direct properties, if you have some complex properties (returning some ComplexType), then it won't work. Technically you need to exclude all properties (returning ComplexType) from the entity's properties, then if possible merge the properties of ComplexType with the entity's direct properties before looping through all and configuring each.

PS: I'm not sure if Dapper.FluentMap supports EF5, but from the code you posted, it can be as easy as appending the ToUpper() method like this:

public PropertyTransformConvention()
{
   Properties()
    .Configure(c => c.Transform(s => Regex.Replace(input: s, pattern: "([A-Z])([A-Z][a-z])|([a-z0-9])([A-Z])", replacement: "$1$3_$2$4").ToUpper()));
}

I've tried visiting the homepage of Dapper.FluentMap and looks like that it has some classes based on Convention (if this is from EF, it is supported only since EF6). So I'm not sure if the Dapper's code works in EF5. If it works you should try the code above for convenience.

Hopeless
  • 4,397
  • 5
  • 37
  • 64
  • Looks great but modelBuilder does not have a Properties() method. I have updated the question to include how I have attempted to use your answer but the syntax is not correct. – McArthey Sep 25 '15 at 16:24
  • Regarding your edit that is precisely what I have in the question. The issue is that the e.HasColumnName throws an error because it is not a method of the Job type. – McArthey Sep 25 '15 at 16:48
  • @McArthey sorry, looks like it's not easy to work with EF5, Technically you have to use Reflection to loop through all the properties of the entity (excluding navigation properties), then pick up the right `Property` method (which is fairly complex) of `EntityTypeConfiguration`, then you need to build an `Expression` (as argument for `Property`) manually from the property Type and its Name... It's not easy at all. Maybe there is some other approach. Let me try finding out or struggling with it. Even when I've done I'll notify you to try it (not sure if it could work). – Hopeless Sep 25 '15 at 17:44
  • Thanks for the effort. At least I see I'm not alone in the struggle. – McArthey Sep 25 '15 at 17:46
  • 1
    Also, your username completes your comment perfectly. ;) – McArthey Sep 25 '15 at 17:46
  • 1
    @McArthey I've just done it. You should try understanding the code yourself (I commented it fairly clearly) and maybe improving it more. The code has not been tested but should work (in fact I've just run it against a simple entity type). That's the only approach I can think of for EF5. You also should consider `Dappter.FluentMap` as I said in my **PS**. I won't try improving the code I posted more, it's your part. – Hopeless Sep 25 '15 at 23:20
  • 1
    Great! I will check this out as soon as I can. Huge props to you for the work and follow through! I greatly appreciate it! Amazing that such a simple operation in EF6 can cause so much trouble in EF5. Thanks again! – McArthey Sep 26 '15 at 13:44
  • @McArthey I think the equivalent code used in EF6 is not like that, it introduced the new class called `Convention` (which can be added to `modelBuilder.Conventions`). I've also tried finding out the source code of `Convention.cs` file to see what exactly it's inside but not found, maybe that's another approach you should follow (I believe it may do something much simpler). – Hopeless Sep 26 '15 at 14:11
  • BTW, the private dictionary is used for cache (to improve perf a little), it could be made static (because it's just some kind of mapping between property type and the corresponding `Property` method to use). But then we need some another static method to dispose everything after the configuration being done. – Hopeless Sep 26 '15 at 14:20
0

That´s how I solved it. My goal was to have a camelCase convention applied just to an specific table/entity ( Table "Client" ).

   modelBuilder.Properties().Configure(p => p.HasColumnName(GetDBName(p, p.ClrPropertyInfo.Name)));


private string GetDBName(ConventionPrimitivePropertyConfiguration p, string name)
{
                var result = name;
                var entityName = p.ClrPropertyInfo.ReflectedType.UnderlyingSystemType.Name;
                if (entityName == "Client")
                    result = Helper.CamelCaseParaSnakeCaseOracle(name);
                return result;
}

static public string CamelCaseParaSnakeCaseOracle(string input)
{
                return Regex.Replace(input,
                  @"(?:\b|(?<=([A-Za-z])))([A-Z][a-z]*)",
                  m => string.Format(@"{0}{1}",
                    (m.Groups[1].Value.Length > 0) ? "_" : "", m.Groups[2].Value.ToUpper()));
}