There’s a part 2 of this story.
The next version of Entity Framework, version 6, has a nice new feature Custom Code First Conventions. In short you can create your own conventions and using these together with the default ones (these were there before). Does you primary key column/property always ends up _PK? You can create convention for that and completely remove bunch of HasKey() lines. But that’s not what I’m going to talk about, if you want to know more follow the link above.
Firebird as other databases adhering to SQL standard in naming and quotations treats unquoted column/table/… as upper case. A lot of ORMs normally quote everything, just to be safe and because it’s easier than to hunt all the exceptions and places where it might collide with reserved keywords. I’m doing that too in provider for Entity Framework for Firebird. That means, that if you create some Code First model, the SQL statements will be quoted and using default naming convention – that’s, simply speaking, same as property/class/… name. Not good. You have to write a lot of explicit HasColumnName.
But with custom conventions today, you can save your typing. Let’s say, that my naming convention is like this. Property SomeValue goes to column SOME_VALUE. So it’s upper case, words separated by underscores. In fact that’s very close to what majority of Firebird users use.
public static string CreateName(string s)
{
return s.Aggregate(string.Empty, (acc, c) => acc + (char.IsUpper(c) && !string.IsNullOrEmpty(acc) ? "_" + c : char.ToUpperInvariant(c).ToString()), _ => _);
}
Yes, it’s a little over-LINQ-ed, but I wanted to try to write it like this.
The convention itself needs to implement IConfigurationConvention interface with proper mix of two generic parameters. Let’s name our columns and tables.
class FirebirdNamingConvention :
IConfigurationConvention<PropertyInfo, PrimitivePropertyConfiguration>,
IConfigurationConvention<Type, EntityTypeConfiguration>
{
public static string CreateName(string s)
{
return s.Aggregate(string.Empty, (acc, c) => acc + (char.IsUpper(c) && !string.IsNullOrEmpty(acc) ? "_" + c : char.ToUpperInvariant(c).ToString()), _ => _);
}
public void Apply(PropertyInfo memberInfo, Func<PrimitivePropertyConfiguration> configuration)
{
var conf = configuration();
conf.ColumnName = CreateName(memberInfo.Name);
}
public void Apply(Type memberInfo, Func<EntityTypeConfiguration> configuration)
{
var conf = configuration();
conf.ToTable(CreateName(memberInfo.Name), null);
}
}
Simple, isn’t it? Now we need to just register this convention.
class FirebirdContext : DbContext
{
public FirebirdContext()
: base(new FbConnection(@"database=localhost:test;user=sysdba;password=masterkey"), true)
{ }
public IDbSet<TestEntity> TestEntities { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(new FirebirdNamingConvention());
//modelBuilder.Properties().Configure(c => c.HasColumnName(FirebirdNamingConvention.CreateName(c.ClrPropertyInfo.Name)));
//modelBuilder.Entities().Configure(c => c.ToTable(FirebirdNamingConvention.CreateName(c.ClrType.Name)));
}
}
I have also included other way to write the conventions, directly in OnModelCreating using so-called lightweight conventions.
Simple code to test.
Database.SetInitializer<FirebirdContext>(null);
using (var ctx = new FirebirdContext())
{
Console.WriteLine(ctx.TestEntities.Where(x => x.MyInteger == 0).ToString());
}
class TestEntity
{
public int Id { get; set; }
public string SomeBoringColumn { get; set; }
public int MyInteger { get; set; }
public DateTime DateTime { get; set; }
public DateTime Timestamp { get; set; }
}
And the result.
SELECT "B"."ID" AS "ID", "B"."SOME_BORING_COLUMN" AS "SOME_BORING_COLUMN", "B"."MY_INTEGER" AS "MY_INTEGER", "B"."DATE_TIME" AS "DATE_TIME", "B"."TIMESTAMP" AS "TIMESTAMP" FROM "TEST_ENTITY" AS "B" WHERE 0 = "B"."MY_INTEGER"
Few lines of code and could save you maybe hundreds of lines of code you’d have to write otherwise.
Note: This code uses custom build FirebirdClient (with current stable one it will not work), because Entity Framework 6 contains some breaking changes for provider writers. I’m working on it and the test builds will be available soon.
goes to column
Cool stuff. Is the reason for using “traditional” conventions over lightweight conventions so that you can provide a convention class that can easily be used by others? Or do you prefer the traditional API?
Also, does this work for columns that are generated by Code First, such as the columns in a many-to-many join table or discriminator columns in a TPH mapping? You might want to consider a model convention for such cases, although that will probably require quite a bit more work than the configuration convention above.
I was just trying both. I started with traditional, because I like my stuff separated and then I tried the lightweight. The method in class to create a name for column was just nice coincidence.
Currently I don’t have preference, but as I liked more configuration classes in previous versions I’ll probably stick with that for configurations as well.
It does not work for columns/properties that are not in entities (i.e. if the FK column/property is in entity, then it works) and are generated. You have to configure these manually using old school configurations. I’ll try to make the model convention work. Now, just one minute try, failed.
Pingback: Firebird News » Custom conventions in Entity Framework 6 helping Firebird – part 2
Pingback: Entity Framework Links #4 - ADO.NET Blog - Site Home - MSDN Blogs
Nice, but what about a real query from a real database? (with a join) crashing since firebird doesn’t support OUTER APPLY? =)
You answered your question. If you create a query that results in CROSS APPLY you’re out of luck. Firebird doesn’t support that. You have to rewrite it.
The problem is that is simply not possiblem because the .Take() always generates an APPLY and for paging you need the .Take() And I wonder which kind of application you can write without a decent paging of the data: I wonder if it would one day possible to internally interpretate the “bad” instruction in the firebird driver and convert it to a normal join… (M$ has a certain talent to create lock-ins… isn’t it?)
That’s not true. .Take() does not generate CROSS APPLY (always), in fact it’s not the take that matters in query. It’s the way you’re querying the graph.
For instance with the structure above, the
ctx.TestEntities.OrderBy(x => x.MyInteger).Take(10).ToString()will result in