Entity Framework 6: Pregenerate Model and View Cache

Application startup and first query time can be slow when using Entity Framework with code first. You can easily increase performance in both fields by providing the EDM and the view cache.

When you utilize Entity Framework with a code first approach, you may experience a long startup time of your application or a pretty long time required for the first DB access, especially if you have more than a few entities.

In a nutshell this is because Entity Framework has to collect lots of information about how your data model is built up. It creates the entity data model (EDM) and has to generate a set of views to access the database. That process can take up several seconds and is definitely noticeable.

Fortunately we can circumvent that by generating caches for the Entity Framework that get loaded during startup. You can for example just generate those on your build server and deploy them with your application, or you can just have them generated at the first startup, or - as in our case - do a combination of both.

At this point I want to point to a blog of my co-worker David, who has written about EF6 Code First startup performance already about 2 years ago. We’re still using the same concept for loading the caches, just with a few updates.

Generate caches for deployment

For generating the caches we have a few prerequisites. Most importantly:

  • Cache generation mustn’t be a manual process, so having the devs use a tool (EF Power Tools) in Visual Studio is not an option.
  • Caches must be generated based on the objects in an assembly, a database isn’t available.
  • Everything must run on the build server.

For this task we’ve written a little command line application which generates the EDMX and the view cache programmatically. It gets compiled and run during a build on our build server, so cache generation doesn’t affect build times for our local builds.
Let’s just start by showing you the complete implementation of the generator logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void Export(string targetDir)
{
string edmxFile = Path.Combine(targetDir, typeof(FusonicContext).FullName + ".edmx");
string viewFile = Path.Combine(targetDir, typeof(FusonicContext).Name + ".xml");
Database.SetInitializer<FusonicContext>(null);

//Generate EDMX
using(var xmlWriter = XmlWriter.Create(edmxFile, new XmlWriterSettings { Indent = true }))
{
EdmxWriter.WriteEdmx(new FusonicContext(), xmlWriter);
}

//Generate view cache
var cacheFactory = new FileViewCacheFactory(viewFile);
InteractiveViews.SetViewCacheFactory(new FusonicContext(), cacheFactory);
cacheFactory.Create(typeof(FusonicContext).Name, "CodeFirstDatabase");
}

FusonicContext is a class that inherits from EFs DbContext. We use it to apply our own conventions to the DbContext and, as you’ll see later on, for loading the view cache.

EdmxWriter is a class delivered by Entity Framework itself, which lets you export a DbContext or a DbModel to an XML.

For the view cache generation we use an external library called EFInteractiveViews. It is also used to load the generated view cache later. In our three-liner we’re basically just telling it to store the views in a file.

So once you know how to do it, generating the caches isn’t too much of a hassle.

Load EF caches

For loading the EDMX and the view cache during runtime we also have a small prerequisite: the user shouldn’t know about it.
This basically means that if the cache files are outdated, we just create new ones. It’ll affect the performance for the first start only - all succeeding starts will have valid cache files.

And why do we want this? Well, in the optimal case the user has the application installed via a setup and the application doesn’t change, thus not requiring an update to the caches.
But the user can also be a tester just doing “copy & paste deployment” with the new DLLs received from a dev. We don’t want to annoy the tester with errors about some outdated EF caches.
Also, the user can be a developer testing a new fancy module. Forcing the developer to generate a cache for every little change in a new entity would also be a nuisance.

So long story short: if it’s old, throw it away.

Load EDMX

In order to tell Entity Framework to use the EDMX we previously generated, we use the code-based configuration that is available since EF6 (also see http://go.microsoft.com/fwlink/?LinkId=260883).

Basically our target is to feed the DbConfiguration with a DbModelStore that uses the correct directory to get the EDMX from. The DefaultDbModelStore uses the location you set in the constructor, the filename is expected to be the FullName of the context + “.edmx”, like it was generated.

Now here is a little issue: the DbModelStore does not come with the official EntityFramework. We use a fork of EF with a few changes. You can check it out here on GitHub.

1
2
3
4
5
6
7
8
9
10
public class FusonicContextConfiguration : DbConfiguration
{
public FusonicContextConfiguration()
{
string edmxLocation = Path.GetDirectoryName(FusonicDbCacheHelper.GetEdmxCacheFilePath(typeof(FusonicContext)));
var dbModelStore = new DefaultDbModelStore(edmxLocation);
IDbDependencyResolver dependencyResolver = new SingletonDependencyResolver<DbModelStore>(dbModelStore);
AddDependencyResolver(dependencyResolver);
}
}

As a last step, during startup you have to tell EF to actually use the configuration: DbConfiguration.SetConfiguration(new FusonicContextConfiguration());

The FusonicDbCacheHelper returns the paths for the EDMX and the view cache file. It returns paths for the deployed files if they exist and they are not outdated, otherwise a path in AppData gets returned.
Full code listing of the FusonicDbCacheHelper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static class FusonicDbCacheHelper
{
//your entities might be in another assembly than the db context. The assembly containing your entities
//should be used for checking against obsoletion
private static readonly Assembly domainAssembly = typeof(DevDiaryEntity).Assembly;
private static string edmxCacheFile;
private static string viewCacheFile;

public static string EdmxCacheFile => edmxCacheFile ?? (edmxCacheFile = GetPath(typeof(FusonicContext).FullName + ".edmx"));
public static string ViewCacheFile => viewCacheFile ?? (viewCacheFile = GetPath(typeof(FusonicContext).Name + ".xml"));

private static string GetPath(string filename)
{
//try to find file that was deployed in the same folder as the assembly. Shouldn't be outdated though.
string deployedFile = Path.Combine(Path.GetDirectoryName(domainAssembly.Location), filename);
if (File.Exists(deployedFile) && !IsObsolete(deployedFile))
return deployedFile;

//no up do date file deployed -> use default path (AppData).
string version = domainAssembly.GetName().Version.ToString();
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
$@"Fusonic\DevDiary\EFCache\{version}",
filename);

//Delete file it if it is obsolete. It will be recreated then.
if (File.Exists(path) && IsObsolete(path))
File.Delete(path);

return path;
}

private static bool IsObsolete(string path)
{
DateTime cacheWriteTime = File.GetLastWriteTimeUtc(path);
DateTime assemblyWriteTime = File.GetLastWriteTimeUtc(domainAssembly.Location);
return assemblyWriteTime > cacheWriteTime;
}
}

Load view cache

Loading the view cache is done by utilizing EFInteractiveViews and setting the view cache factory for the context. We have our own FusonicContext inheriting from DbContext, as we apply several custom settings and conventions when creating a DbContext, but this is no requirement. If you want, you can just directly apply the view cache factory to a “standard” DbContext when creating it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class FusonicContext : DbContext
{
private static DbMappingViewCacheFactory viewCacheFactory;

private static DbMappingViewCacheFactory ViewCacheFactory
{
get
{
if (viewCacheFactory == null)
{
string path = FusonicDbCacheHelper.GetEdmxCacheFilePath(typeof(FusonicContext));
Directory.CreateDirectory(Path.GetDirectoryName(path)); //ensure that directory exists
viewCacheFactory = new FileViewCacheFactory(path);
}
return viewCacheFactory;
}
}

public FusonicContext()
{}

public FusonicContext(string connectionString) : base(connectionString)
{
InteractiveViews.SetViewCacheFactory(this, ViewCacheFactory);
}
}

Startup performance impact & conclusion

The application I was testing this in is a quiet database-heavy program. Currently it has over 250 different entities.

It takes the application about 8 seconds to run the first query when the EDMX and the view cache are not provided. With both provided, the process speeds up by 6 seconds down to only 2 seconds for the first query.

After all, providing the EDMX and the view cache is a pretty good idea in code first environments, especially if you have more than a few entities. Even if you don’t deploy it with your application, having the EDMX and the view cache stored on the first startup can give you a huge gain on future startups. Definitely something to think about.