/// <summary> /// Handles when a model binding error occurs /// </summary> public void Handle(ModelBindingErrorNotification notification) { ModelsBuilderAssemblyAttribute sourceAttr = notification.SourceType.Assembly.GetCustomAttribute <ModelsBuilderAssemblyAttribute>(); ModelsBuilderAssemblyAttribute modelAttr = notification.ModelType.Assembly.GetCustomAttribute <ModelsBuilderAssemblyAttribute>(); // if source or model is not a ModelsBuider type... if (sourceAttr == null || modelAttr == null) { // if neither are ModelsBuilder types, give up entirely if (sourceAttr == null && modelAttr == null) { return; } // else report, but better not restart (loops?) notification.Message.Append(" The "); notification.Message.Append(sourceAttr == null ? "view model" : "source"); notification.Message.Append(" is a ModelsBuilder type, but the "); notification.Message.Append(sourceAttr != null ? "view model" : "source"); notification.Message.Append(" is not. The application is in an unstable state and should be restarted."); return; } // both are ModelsBuilder types var pureSource = sourceAttr.IsInMemory; var pureModel = modelAttr.IsInMemory; if (sourceAttr.IsInMemory || modelAttr.IsInMemory) { if (pureSource == false || pureModel == false) { // only one is pure - report, but better not restart (loops?) notification.Message.Append(pureSource ? " The content model is in memory generated, but the view model is not." : " The view model is in memory generated, but the content model is not."); notification.Message.Append(" The application is in an unstable state and should be restarted."); } else { // both are pure - report, and if different versions, restart // if same version... makes no sense... and better not restart (loops?) Version sourceVersion = notification.SourceType.Assembly.GetName().Version; Version modelVersion = notification.ModelType.Assembly.GetName().Version; notification.Message.Append(" Both view and content models are in memory generated, with "); notification.Message.Append(sourceVersion == modelVersion ? "same version. The application is in an unstable state and should be restarted." : "different versions. The application is in an unstable state and should be restarted."); } } }
// This is NOT thread safe but it is only called from within a lock private Assembly GetModelsAssembly(bool forceRebuild) { if (!Directory.Exists(_pureLiveDirectory.Value)) { Directory.CreateDirectory(_pureLiveDirectory.Value); } IList <TypeModel> typeModels = UmbracoServices.GetAllTypes(); var currentHash = TypeModelHasher.Hash(typeModels); var modelsHashFile = Path.Combine(_pureLiveDirectory.Value, "models.hash"); var modelsSrcFile = Path.Combine(_pureLiveDirectory.Value, "models.generated.cs"); var projFile = Path.Combine(_pureLiveDirectory.Value, "all.generated.cs"); var dllPathFile = Path.Combine(_pureLiveDirectory.Value, "all.dll.path"); // caching the generated models speeds up booting // currentHash hashes both the types & the user's partials if (!forceRebuild) { _logger.LogDebug("Looking for cached models."); if (File.Exists(modelsHashFile) && File.Exists(projFile)) { var cachedHash = File.ReadAllText(modelsHashFile); if (currentHash != cachedHash) { _logger.LogDebug("Found obsolete cached models."); forceRebuild = true; } // else cachedHash matches currentHash, we can try to load an existing dll } else { _logger.LogDebug("Could not find cached models."); forceRebuild = true; } } Assembly assembly; if (!forceRebuild) { // try to load the dll directly (avoid rebuilding) // // ensure that the .dll file does not have a corresponding .dll.delete file // as that would mean the the .dll file is going to be deleted and should not // be re-used - that should not happen in theory, but better be safe if (File.Exists(dllPathFile)) { var dllPath = File.ReadAllText(dllPathFile); _logger.LogDebug($"Cached models dll at {dllPath}."); if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete")) { assembly = ReloadAssembly(dllPath); ModelsBuilderAssemblyAttribute attr = assembly.GetCustomAttribute <ModelsBuilderAssemblyAttribute>(); if (attr != null && attr.IsInMemory && attr.SourceHash == currentHash) { // if we were to resume at that revision, then _ver would keep increasing // and that is probably a bad idea - so, we'll always rebuild starting at // ver 1, but we remember we want to skip that one - so we never end up // with the "same but different" version of the assembly in memory _skipver = assembly.GetName().Version.Revision; _logger.LogDebug("Loading cached models (dll)."); return(assembly); } _logger.LogDebug("Cached models dll cannot be loaded (invalid assembly)."); } else if (!File.Exists(dllPath)) { _logger.LogDebug("Cached models dll does not exist."); } else if (File.Exists(dllPath + ".delete")) { _logger.LogDebug("Cached models dll is marked for deletion."); } else { _logger.LogDebug("Cached models dll cannot be loaded (why?)."); } } // must reset the version in the file else it would keep growing // loading cached modules only happens when the app restarts var text = File.ReadAllText(projFile); Match match = s_assemblyVersionRegex.Match(text); if (match.Success) { text = text.Replace(match.Value, "AssemblyVersion(\"0.0.0." + _ver + "\")"); File.WriteAllText(projFile, text); } _ver++; try { var assemblyPath = GetOutputAssemblyPath(currentHash); RoslynCompiler.CompileToFile(projFile, assemblyPath); assembly = ReloadAssembly(assemblyPath); File.WriteAllText(dllPathFile, assembly.Location); File.WriteAllText(modelsHashFile, currentHash); TryDeleteUnusedAssemblies(dllPathFile); } catch { ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile); throw; } _logger.LogDebug("Loading cached models (source)."); return(assembly); } // need to rebuild _logger.LogDebug("Rebuilding models."); // generate code, save var code = GenerateModelsCode(typeModels); // add extra attributes, // IsLive=true helps identifying Assemblies that contain live models // AssemblyVersion is so that we have a different version for each rebuild var ver = _ver == _skipver ? ++_ver : _ver; _ver++; string mbAssemblyDirective = $@"[assembly:ModelsBuilderAssembly(IsInMemory = true, SourceHash = ""{currentHash}"")] [assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"; code = code.Replace("//ASSATTR", mbAssemblyDirective); File.WriteAllText(modelsSrcFile, code); // generate proj, save var projFiles = new Dictionary <string, string> { { "models.generated.cs", code } }; var proj = GenerateModelsProj(projFiles); File.WriteAllText(projFile, proj); // compile and register try { var assemblyPath = GetOutputAssemblyPath(currentHash); RoslynCompiler.CompileToFile(projFile, assemblyPath); assembly = ReloadAssembly(assemblyPath); File.WriteAllText(dllPathFile, assemblyPath); File.WriteAllText(modelsHashFile, currentHash); TryDeleteUnusedAssemblies(dllPathFile); } catch { ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile); throw; } _logger.LogDebug("Done rebuilding."); return(assembly); }