As part of our continuous integration efforts we've created a custom deployment application for handling Entity Framework 6.0 Code First database migrations. We take our target DLLs and compare those against the currently deployed DLLs to determine the migration path whether that path be upwards or downwards. In order to ensure we're loading the correct version of the data context's DLL we're listening on the AppDomain.CurrentDomain.AssemblyResolve (see: Loading Dependent Assemblies Manually).
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
public enum MigrationsSource
{
Target = 1,
Deployed= 2
}
public class AssemblyLoader
{
private readonly ConcurrentDictionary<string, MigrationsSource> m_Sources = new ConcurrentDictionary<string, MigrationsSource>();
private string m_DeployedPath;
private string m_TargetPath;
public AssemblyLoader()
{
AppDomain.CurrentDomain.AssemblyResolve += ResolveDependentAssembly;
}
~AssemblyLoader()
{
AppDomain.CurrentDomain.AssemblyResolve -= ResolveDependentAssembly;
}
private Assembly ResolveDependentAssembly(object sender, ResolveEventArgs args)
{
MigrationsSource source;
if (m_Sources.TryGetValue(BuildAssemblyId(args.RequestingAssembly), out source))
{
var assemblyName = new AssemblyName(args.Name);
string targetPath = Path.Combine(
source == MigrationsSource.Deployed ? m_DeployedPath : m_TargetPath, string.Format("{0}.dll", assemblyName.Name));
assemblyName.CodeBase = targetPath;
//We have to use LoadFile here, otherwise we won't load a differing
//version, regardless of the codebase because only LoadFile
//will actually load a *new* assembly if it's at a different path
//See: http://msdn.microsoft.com/en-us/library/b61s44e8(v=vs.110).aspx
var dependentAssembly = Assembly.LoadFile(assemblyName.CodeBase);
m_Sources.TryAdd(BuildAssemblyId(dependentAssembly), source);
return dependentAssembly;
}
return null;
}
private string BuildAssemblyId(Assembly assembly)
{
return
Convert.ToBase64String(
HashAlgorithm.Create("SHA1")
.ComputeHash(UTF8Encoding.UTF8.GetBytes(
string.Format("{0}|{1}", assembly.FullName, assembly.Location))));
}
}
This works fine when deploying a simple data context (a single table seeded with a single row) to a server running SQL Server 2012 (running Windows Server 2012), but the following error occurs when deploying the same simple data context to a server running SQL Server 2008 R2 (running Windows Server 2008 Standard):
Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.
at EntityFramework.AssemblyLoader.BuildAssemblyId(Assembly assembly) in c:\EntityFramework\AssemblyLoader.cs:line 103
at EntityFramework.AssemblyLoader.ResolveDependentAssembly(Object sender, ResolveEventArgs args) in c:\EntityFramework\AssemblyLoader.cs:line 42
at System.AppDomain.OnAssemblyResolveEvent(RuntimeAssembly assembly, String assemblyFullName)
at System.RuntimeTypeHandle.GetTypeByName(String name, Boolean throwOnError, Boolean ignoreCase, Boolean reflectionOnly, StackCrawlMarkHandle stackMark, IntPtr pPrivHostBinder, Boolean loadTypeFromPartialName, ObjectHandleOnStack type)
at System.RuntimeTypeHandle.GetTypeByName(String name, Boolean throwOnError, Boolean ignoreCase, Boolean reflectionOnly, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean loadTypeFromPartialName)
at System.RuntimeType.GetType(String typeName, Boolean throwOnError, Boolean ignoreCase, Boolean reflectionOnly, StackCrawlMark& stackMark)
at System.Type.GetType(String typeName, Boolean throwOnError)
at System.Data.Entity.Infrastructure.DependencyResolution.ClrTypeAnnotationSerializer.Deserialize(String name, String value)
at System.Data.Entity.Core.SchemaObjectModel.SchemaElement.CreateMetadataPropertyFromXmlAttribute(String xmlNamespaceUri, String attributeName, String value)
at System.Data.Entity.Core.SchemaObjectModel.SchemaElement.AddOtherContent(XmlReader reader)
at System.Data.Entity.Core.SchemaObjectModel.SchemaElement.ParseAttribute(XmlReader reader)
at System.Data.Entity.Core.SchemaObjectModel.SchemaElement.Parse(XmlReader reader)
at System.Data.Entity.Core.SchemaObjectModel.Schema.HandleEntityTypeElement(XmlReader reader)
at System.Data.Entity.Core.SchemaObjectModel.Schema.HandleElement(XmlReader reader)
at System.Data.Entity.Core.SchemaObjectModel.SchemaElement.ParseElement(XmlReader reader)
at System.Data.Entity.Core.SchemaObjectModel.SchemaElement.Parse(XmlReader reader)
at System.Data.Entity.Core.SchemaObjectModel.Schema.HandleTopLevelSchemaElement(XmlReader reader)
at System.Data.Entity.Core.SchemaObjectModel.Schema.InternalParse(XmlReader sourceReader, String sourceLocation)
at System.Data.Entity.Core.SchemaObjectModel.Schema.Parse(XmlReader sourceReader, String sourceLocation)
at System.Data.Entity.Core.SchemaObjectModel.SchemaManager.ParseAndValidate(IEnumerable`1 xmlReaders, IEnumerable`1 sourceFilePaths, SchemaDataModelOption dataModel, AttributeValueNotification providerNotification, AttributeValueNotification providerManifestTokenNotification, ProviderManifestNeeded providerManifestNeeded, IList`1& schemaCollection)
at System.Data.Entity.Core.SchemaObjectModel.SchemaManager.ParseAndValidate(IEnumerable`1 xmlReaders, IEnumerable`1 sourceFilePaths, SchemaDataModelOption dataModel, DbProviderManifest providerManifest, IList`1& schemaCollection)
at System.Data.Entity.Core.Metadata.Edm.EdmItemCollection.LoadItems(IEnumerable`1 xmlReaders, IEnumerable`1 sourceFilePaths, SchemaDataModelOption dataModelOption, DbProviderManifest providerManifest, ItemCollection itemCollection, Boolean throwOnError)
at System.Data.Entity.Core.Metadata.Edm.EdmItemCollection.Init(IEnumerable`1 xmlReaders, IEnumerable`1 filePaths, Boolean throwOnError)
at System.Data.Entity.Core.Metadata.Edm.EdmItemCollection..ctor(IEnumerable`1 xmlReaders)
at System.Data.Entity.Utilities.XDocumentExtensions.GetStorageMappingItemCollection(XDocument model, DbProviderInfo&providerInfo)
at System.Data.Entity.Migrations.Infrastructure.EdmModelDiffer.Diff(XDocument sourceModel, XDocument targetModel, Lazy`1 modificationCommandTreeGenerator, MigrationSqlGenerator migrationSqlGenerator, String sourceModelVersion, String targetModelVersion)
at System.Data.Entity.Migrations.DbMigrator.IsModelOutOfDate(XDocument model, DbMigration lastMigration)
at System.Data.Entity.Migrations.DbMigrator.Upgrade(IEnumerable`1 pendingMigrations, String targetMigrationId, String lastMigrationId)
at System.Data.Entity.Migrations.DbMigrator.UpdateInternal(String targetMigration)
at System.Data.Entity.Migrations.DbMigrator.<>c__DisplayClassc.<Update>b__b()
at System.Data.Entity.Migrations.DbMigrator.EnsureDatabaseExists(Action mustSucceedToKeepDatabase)
at System.Data.Entity.Migrations.DbMigrator.Update(String targetMigration)
at System.Data.Entity.Internal.DatabaseCreator.CreateDatabase(InternalContext internalContext, Func`3 createMigrator, ObjectContext objectContext)
at System.Data.Entity.Internal.InternalContext.CreateDatabase(ObjectContext objectContext, DatabaseExistenceState existenceState)
at System.Data.Entity.Database.Create(DatabaseExistenceState existenceState)
at System.Data.Entity.CreateDatabaseIfNotExists`1.InitializeDatabase(TContext context)
at System.Data.Entity.Internal.InternalContext.<>c__DisplayClassf`1.<CreateInitializationAction>b__e()
at System.Data.Entity.Internal.InternalContext.PerformInitializationAction(Action action)
at System.Data.Entity.Internal.InternalContext.PerformDatabaseInitialization()
at System.Data.Entity.Internal.LazyInternalContext.<InitializeDatabase>b__4(InternalContext c)
at System.Data.Entity.Internal.RetryAction`1.PerformAction(TInput input)
at System.Data.Entity.Internal.LazyInternalContext.InitializeDatabaseAction(Action`1 action)
at System.Data.Entity.Internal.LazyInternalContext.InitializeDatabase()
at EntityFramework.DbDeploymentManager.HandleDatabaseInitialization(DatabaseEndpoint endpoint) in c:\EntityFramework\DbDeploymentManager.cs:line 185
at EntityFramework.DbDeploymentManager.Deploy() in c:\EntityFramework\DbDeploymentManager.cs:line 67
at EntityFramework.Deployer.Program.Main(String[] args) in c:\EntityFramework.Deployer\Program.cs:line 23
Some logging output revealed that BuildAssemblyId
is throwing the NullReferenceException
because the args.RequestingAssembly
that's being passed in is null
. The value in args.Name
is the name of the DLL that contains our data context. This gets thrown between the creation of the context and the data seeding as the table is created, but empty.
The application was initial running on each machine independently. We've ruled out .NET Framework discrepancy by update each machine to .NET 4.5.1. Also, we ran the deployment application on the same machine; we used the machine that had SQL Server 2012 installed. Finally, both the deployment application and simple data context are referencing the same version of EntityFramework.
EDIT
It was suggested that the attribute on the table object could be the root of the problem.
[Table("dbo.tblCustomer")]
public class Customer
{
public Guid Id { get; set; }
public string Name { get; set; }
}
We removed the TableAttribute
and while it still failed on SQL Server 2008 R2 the error became reproducible on SQL Server 2012.
The adhesive bandage we've applied is to unregister our ResolveDependentAssembly
event after the determination of target and deployed DLLs and before the database is initialized. This will work fine in our current situation because we're only ever deploying a data context to one environment.
Our long term fix is going to be creating separate app domains for the target and each deployment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With