Files
Umbraco-CMS/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs

815 lines
32 KiB
C#
Raw Normal View History

2021-01-13 15:18:59 +11:00
using System;
2019-06-24 11:58:36 +02:00
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Loader;
2019-06-24 11:58:36 +02:00
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.Logging;
2021-01-13 15:18:59 +11:00
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models.PublishedContent;
Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/AB10314-mb-embedded-dependencies # Conflicts: # src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs # src/Umbraco.Infrastructure/ModelsBuilder/LiveModelsProvider.cs # src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs # src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderDashboard.cs # src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs # src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs # src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs # src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs # src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs # src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs # src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs # src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs # src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj # src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs # src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/UmbracoApplicationTests.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidator.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidatorBase.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/DashboardReport.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/MediaTypeModelValidator.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/MemberTypeModelValidator.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardController.cs # src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderExtensions.cs # src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs # src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs # src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs # src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs # src/Umbraco.Web.UI.NetCore/Startup.cs # src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs
2021-02-22 09:00:33 +01:00
using Umbraco.Cms.Infrastructure.ModelsBuilder;
using Umbraco.Cms.Infrastructure.ModelsBuilder.Building;
using Umbraco.Extensions;
2019-06-24 11:58:36 +02:00
using File = System.IO.File;
Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/AB10314-mb-embedded-dependencies # Conflicts: # src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs # src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs # src/Umbraco.Infrastructure/ModelsBuilder/LiveModelsProvider.cs # src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs # src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderDashboard.cs # src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs # src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs # src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs # src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs # src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs # src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs # src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs # src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs # src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj # src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/BuilderTests.cs # src/Umbraco.Tests.UnitTests/Umbraco.ModelsBuilder.Embedded/UmbracoApplicationTests.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidator.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/ContentTypeModelValidatorBase.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/DashboardReport.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/MediaTypeModelValidator.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/MemberTypeModelValidator.cs # src/Umbraco.Web.BackOffice/ModelsBuilder/ModelsBuilderDashboardController.cs # src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderExtensions.cs # src/Umbraco.Web.Common/ModelsBuilder/ModelsBuilderNotificationHandler.cs # src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs # src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs # src/Umbraco.Web.Common/ModelsBuilder/UmbracoAssemblyLoadContext.cs # src/Umbraco.Web.UI.NetCore/Startup.cs # src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs
2021-02-22 09:00:33 +01:00
namespace Umbraco.Cms.Web.Common.ModelsBuilder
2019-06-24 11:58:36 +02:00
{
internal class InMemoryModelFactory : IAutoPublishedModelFactory, IRegisteredObject
2019-06-24 11:58:36 +02:00
{
private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
private volatile bool _hasModels; // volatile 'cos reading outside lock
private bool _pendingRebuild;
2020-09-15 08:45:40 +02:00
private readonly IProfilingLogger _profilingLogger;
private readonly ILogger<InMemoryModelFactory> _logger;
2019-06-24 11:58:36 +02:00
private readonly FileSystemWatcher _watcher;
2021-01-13 15:18:59 +11:00
private int _ver;
private int _skipver;
2019-06-24 11:58:36 +02:00
private readonly int _debugLevel;
2020-09-02 14:44:01 +02:00
private RoslynCompiler _roslynCompiler;
private UmbracoAssemblyLoadContext _currentAssemblyLoadContext;
private readonly Lazy<UmbracoServices> _umbracoServices; // fixme: this is because of circular refs :(
2021-01-13 15:18:59 +11:00
private static readonly Regex s_assemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled);
private static readonly string[] s_ourFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" };
private readonly ModelsBuilderSettings _config;
2020-09-02 14:44:01 +02:00
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IApplicationShutdownRegistry _hostingLifetime;
private readonly ModelsGenerationError _errors;
2020-09-02 14:44:01 +02:00
private readonly IPublishedValueFallback _publishedValueFallback;
private readonly ApplicationPartManager _applicationPartManager;
2021-01-13 15:18:59 +11:00
private static readonly Regex s_usingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline);
private static readonly Regex s_aattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline);
private readonly Lazy<string> _pureLiveDirectory;
2019-06-24 11:58:36 +02:00
public InMemoryModelFactory(
Lazy<UmbracoServices> umbracoServices,
2020-09-15 08:45:40 +02:00
IProfilingLogger profilingLogger,
ILogger<InMemoryModelFactory> logger,
IOptions<ModelsBuilderSettings> config,
IHostingEnvironment hostingEnvironment,
IApplicationShutdownRegistry hostingLifetime,
IPublishedValueFallback publishedValueFallback,
ApplicationPartManager applicationPartManager)
2019-06-24 11:58:36 +02:00
{
_umbracoServices = umbracoServices;
2020-09-15 08:45:40 +02:00
_profilingLogger = profilingLogger;
2019-06-24 11:58:36 +02:00
_logger = logger;
_config = config.Value;
2020-09-02 14:44:01 +02:00
_hostingEnvironment = hostingEnvironment;
_hostingLifetime = hostingLifetime;
2020-09-02 14:44:01 +02:00
_publishedValueFallback = publishedValueFallback;
_applicationPartManager = applicationPartManager;
2020-09-02 14:44:01 +02:00
_errors = new ModelsGenerationError(config, _hostingEnvironment);
2019-06-24 11:58:36 +02:00
_ver = 1; // zero is for when we had no version
_skipver = -1; // nothing to skip
2021-01-13 15:18:59 +11:00
if (!hostingEnvironment.IsHosted)
{
return;
}
2019-06-24 11:58:36 +02:00
_pureLiveDirectory = new Lazy<string>(PureLiveDirectoryAbsolute);
if (!Directory.Exists(_pureLiveDirectory.Value))
2021-01-13 15:18:59 +11:00
{
Directory.CreateDirectory(_pureLiveDirectory.Value);
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
// BEWARE! if the watcher is not properly released then for some reason the
// BuildManager will start confusing types - using a 'registered object' here
// though we should probably plug into Umbraco's MainDom - which is internal
_hostingLifetime.RegisterObject(this);
_watcher = new FileSystemWatcher(_pureLiveDirectory.Value);
2019-06-24 11:58:36 +02:00
_watcher.Changed += WatcherOnChanged;
_watcher.EnableRaisingEvents = true;
// get it here, this need to be fast
_debugLevel = _config.DebugLevel;
AssemblyLoadContext.Default.Resolving += OnResolvingDefaultAssemblyLoadContext;
2019-06-24 11:58:36 +02:00
}
public event EventHandler ModelsChanged;
2021-01-13 15:18:59 +11:00
private UmbracoServices UmbracoServices => _umbracoServices.Value;
2019-06-24 11:58:36 +02:00
/// <summary>
/// Gets the currently loaded Live models assembly
/// </summary>
/// <remarks>
/// Can be null
/// </remarks>
public Assembly CurrentModelsAssembly { get; private set; }
2019-06-24 11:58:36 +02:00
/// <inheritdoc />
public object SyncRoot { get; } = new object();
/// <summary>
/// Gets the RoslynCompiler
/// </summary>
2021-01-13 15:18:59 +11:00
private RoslynCompiler RoslynCompiler
{
get
{
if (_roslynCompiler != null)
{
return _roslynCompiler;
}
_roslynCompiler = new RoslynCompiler(AssemblyLoadContext.All.SelectMany(x => x.Assemblies));
2021-01-13 15:18:59 +11:00
return _roslynCompiler;
}
}
/// <inheritdoc />
public bool Enabled => _config.ModelsMode == ModelsMode.InMemoryAuto;
2021-01-13 15:18:59 +11:00
/// <summary>
/// Handle the event when a reference cannot be resolved from the default context and return our custom MB assembly reference if we have one
/// </summary>
/// <remarks>
/// This is required because the razor engine will only try to load things from the default context, it doesn't know anything
/// about our context so we need to proxy.
/// </remarks>
private Assembly OnResolvingDefaultAssemblyLoadContext(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
Examine 2.0 integration (#10241) * Init commit for examine 2.0 work, most old umb examine tests working, probably a lot that doesn't * Gets Umbraco Examine tests passing and makes some sense out of them, fixes some underlying issues. * Large refactor, remove TaskHelper, rename Notifications to be consistent, Gets all examine/lucene indexes building and startup ordered in the correct way, removes old files, creates new IUmbracoIndexingHandler for abstracting out all index operations for umbraco data, abstracts out IIndexRebuilder, Fixes Stack overflow with LiveModelsProvider and loading assemblies, ports some changes from v8 for startup handling with cold boots, refactors out LastSyncedFileManager * fix up issues with rebuilding and management dashboard. * removes old files, removes NetworkHelper, fixes LastSyncedFileManager implementation to ensure the machine name is used, fix up logging with cold boot state. * Makes MainDom safer to use and makes PublishedSnapshotService lazily register with MainDom * lazily acquire application id (fix unit tests) * Fixes resource casing and missing test file * Ensures caches when requiring internal services for PublishedSnapshotService, UseNuCache is a separate call, shouldn't be buried in AddWebComponents, was also causing issues in integration tests since nucache was being used for the Id2Key service. * For UmbracoTestServerTestBase enable nucache services * Fixing tests * Fix another test * Fixes tests, use TestHostingEnvironment, make Tests.Common use net5, remove old Lucene.Net.Contrib ref. * Fixes up some review notes * Fixes issue with doubly registering PublishedSnapshotService meanig there could be 2x instances of it * Checks for parseexception when executing the query * Use application root instead of duplicating functionality. * Added Examine project to netcore only solution file * Fixed casing issue with LazyLoad, that is not lowercase. * uses cancellationToken instead of bool flag, fixes always reading lastId from the LastSyncedFileManager, fixes RecurringHostedServiceBase so that there isn't an overlapping thread for the same task type * Fix tests * remove legacy test project from solution file * Fix test Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-05-18 18:31:38 +10:00
=> assemblyName.Name == RoslynCompiler.GeneratedAssemblyName
? _currentAssemblyLoadContext?.LoadFromAssemblyName(assemblyName)
: null;
2019-06-24 11:58:36 +02:00
public IPublishedElement CreateModel(IPublishedElement element)
{
// get models, rebuilding them if needed
2021-01-13 15:18:59 +11:00
Dictionary<string, ModelInfo> infos = EnsureModels()?.ModelInfos;
2019-06-24 11:58:36 +02:00
if (infos == null)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
return element;
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
// be case-insensitive
var contentTypeAlias = element.ContentType.Alias;
// lookup model constructor (else null)
infos.TryGetValue(contentTypeAlias, out var info);
2019-06-24 11:58:36 +02:00
// create model
2020-09-02 14:44:01 +02:00
return info == null ? element : info.Ctor(element, _publishedValueFallback);
2019-06-24 11:58:36 +02:00
}
// this runs only once the factory is ready
// NOT when building models
public Type MapModelType(Type type)
{
2021-01-13 15:18:59 +11:00
Infos infos = EnsureModels();
2019-06-24 11:58:36 +02:00
return ModelType.Map(type, infos.ModelTypeMap);
}
// this runs only once the factory is ready
// NOT when building models
public IList CreateModelList(string alias)
{
2021-01-13 15:18:59 +11:00
Infos infos = EnsureModels();
2019-06-24 11:58:36 +02:00
// fail fast
if (infos == null)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
return new List<IPublishedElement>();
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
2021-01-13 15:18:59 +11:00
if (!infos.ModelInfos.TryGetValue(alias, out ModelInfo modelInfo))
{
2019-06-24 11:58:36 +02:00
return new List<IPublishedElement>();
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
2021-01-13 15:18:59 +11:00
Func<IList> ctor = modelInfo.ListCtor;
if (ctor != null)
{
return ctor();
}
2019-06-24 11:58:36 +02:00
2021-01-13 15:18:59 +11:00
Type listType = typeof(List<>).MakeGenericType(modelInfo.ModelType);
2019-06-24 11:58:36 +02:00
ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor<Func<IList>>(declaring: listType);
return ctor();
}
/// <inheritdoc />
public void Reset()
{
if (Enabled)
2021-01-13 15:18:59 +11:00
{
ResetModels();
2021-01-13 15:18:59 +11:00
}
}
2019-06-24 11:58:36 +02:00
// tells the factory that it should build a new generation of models
private void ResetModels()
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Resetting models.");
2019-06-24 11:58:36 +02:00
try
{
_locker.EnterWriteLock();
_hasModels = false;
_pendingRebuild = true;
if (!Directory.Exists(_pureLiveDirectory.Value))
2021-01-13 15:18:59 +11:00
{
Directory.CreateDirectory(_pureLiveDirectory.Value);
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
// clear stuff
var modelsHashFile = Path.Combine(_pureLiveDirectory.Value, "models.hash");
var dllPathFile = Path.Combine(_pureLiveDirectory.Value, "all.dll.path");
2019-06-24 11:58:36 +02:00
2021-01-13 15:18:59 +11:00
if (File.Exists(dllPathFile))
{
File.Delete(dllPathFile);
}
if (File.Exists(modelsHashFile))
{
File.Delete(modelsHashFile);
}
2019-06-24 11:58:36 +02:00
}
finally
{
if (_locker.IsWriteLockHeld)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
_locker.ExitWriteLock();
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
}
}
// ensure that the factory is running with the lastest generation of models
internal Infos EnsureModels()
{
if (_debugLevel > 0)
2021-01-13 15:18:59 +11:00
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Ensuring models.");
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
// don't use an upgradeable lock here because only 1 thread at a time could enter it
try
{
_locker.EnterReadLock();
if (_hasModels)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
return _infos;
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
}
finally
{
if (_locker.IsReadLockHeld)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
_locker.ExitReadLock();
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
}
try
{
_locker.EnterUpgradeableReadLock();
2021-01-13 15:18:59 +11:00
if (_hasModels)
{
return _infos;
}
2019-06-24 11:58:36 +02:00
_locker.EnterWriteLock();
// we don't have models,
// either they haven't been loaded from the cache yet
// or they have been reseted and are pending a rebuild
using (_profilingLogger.DebugDuration<InMemoryModelFactory>("Get models.", "Got models."))
2019-06-24 11:58:36 +02:00
{
try
{
Assembly assembly = GetModelsAssembly(_pendingRebuild);
CurrentModelsAssembly = assembly;
2019-06-24 11:58:36 +02:00
// Raise the model changing event.
// NOTE: That on first load, if there is content, this will execute before the razor view engine
// has loaded which means it hasn't yet bound to this event so there's no need to worry about if
// it will be eagerly re-generated unecessarily on first render. BUT we should be aware that if we
// change this to use the event aggregator that will no longer be the case.
ModelsChanged?.Invoke(this, new EventArgs());
2021-01-13 15:18:59 +11:00
IEnumerable<Type> types = assembly.ExportedTypes.Where(x => x.Inherits<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
2019-06-24 11:58:36 +02:00
_infos = RegisterModels(types);
_errors.Clear();
2019-06-24 11:58:36 +02:00
}
catch (Exception e)
{
try
{
_logger.LogError(e, "Failed to build models.");
2020-09-15 08:45:40 +02:00
_logger.LogWarning("Running without models."); // be explicit
_errors.Report("Failed to build InMemory models.", e);
2019-06-24 11:58:36 +02:00
}
finally
{
CurrentModelsAssembly = null;
2019-06-24 11:58:36 +02:00
_infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
}
}
// don't even try again
_hasModels = true;
}
return _infos;
}
finally
{
if (_locker.IsWriteLockHeld)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
_locker.ExitWriteLock();
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
if (_locker.IsUpgradeableReadLockHeld)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
_locker.ExitUpgradeableReadLock();
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
}
}
public string PureLiveDirectoryAbsolute() => _hostingEnvironment.MapPathContentRoot("~/umbraco/Data/TEMP/PureLive");
// This is NOT thread safe but it is only called from within a lock
private Assembly ReloadAssembly(string pathToAssembly)
{
// If there's a current AssemblyLoadContext, unload it before creating a new one.
if (!(_currentAssemblyLoadContext is null))
{
_currentAssemblyLoadContext.Unload();
// we need to remove the current part too
ApplicationPart currentPart = _applicationPartManager.ApplicationParts.FirstOrDefault(x => x.Name == RoslynCompiler.GeneratedAssemblyName);
if (currentPart != null)
{
_applicationPartManager.ApplicationParts.Remove(currentPart);
}
}
// We must create a new assembly load context
// as long as theres a reference to the assembly load context we can't delete the assembly it loaded
_currentAssemblyLoadContext = new UmbracoAssemblyLoadContext();
// NOTE: We cannot use in-memory assemblies due to the way the razor engine works which must use
// application parts in order to add references to it's own CSharpCompiler.
// These parts must have real paths since that is how the references are loaded. In that
// case we'll need to work on temp files so that the assembly isn't locked.
// Get a temp file path
// NOTE: We cannot use Path.GetTempFileName(), see this issue:
// https://github.com/dotnet/AspNetCore.Docs/issues/3589 which can cause issues, this is recommended instead
var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
File.Copy(pathToAssembly, tempFile, true);
// Load it in
Assembly assembly = _currentAssemblyLoadContext.LoadFromAssemblyPath(tempFile);
// Add the assembly to the application parts - this is required because this is how
// the razor ReferenceManager resolves what to load, see
// https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs#L53
var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly);
foreach (ApplicationPart applicationPart in partFactory.GetApplicationParts(assembly))
2020-09-07 09:03:09 +02:00
{
_applicationPartManager.ApplicationParts.Add(applicationPart);
}
return assembly;
}
// This is NOT thread safe but it is only called from within a lock
private Assembly GetModelsAssembly(bool forceRebuild)
2019-06-24 11:58:36 +02:00
{
if (!Directory.Exists(_pureLiveDirectory.Value))
2021-01-13 15:18:59 +11:00
{
Directory.CreateDirectory(_pureLiveDirectory.Value);
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
2021-01-13 15:18:59 +11:00
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");
2019-06-24 11:58:36 +02:00
// caching the generated models speeds up booting
// currentHash hashes both the types & the user's partials
if (!forceRebuild)
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Looking for cached models.");
2019-06-24 11:58:36 +02:00
if (File.Exists(modelsHashFile) && File.Exists(projFile))
{
var cachedHash = File.ReadAllText(modelsHashFile);
if (currentHash != cachedHash)
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Found obsolete cached models.");
2019-06-24 11:58:36 +02:00
forceRebuild = true;
}
// else cachedHash matches currentHash, we can try to load an existing dll
}
else
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Could not find cached models.");
2019-06-24 11:58:36 +02:00
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);
2020-09-15 08:45:40 +02:00
_logger.LogDebug($"Cached models dll at {dllPath}.");
2019-06-24 11:58:36 +02:00
2020-09-03 14:18:09 +02:00
if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete"))
2019-06-24 11:58:36 +02:00
{
assembly = ReloadAssembly(dllPath);
2021-01-13 15:18:59 +11:00
ModelsBuilderAssemblyAttribute attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
if (attr != null && attr.IsInMemory && attr.SourceHash == currentHash)
2019-06-24 11:58:36 +02:00
{
// 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;
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Loading cached models (dll).");
2019-06-24 11:58:36 +02:00
return assembly;
}
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Cached models dll cannot be loaded (invalid assembly).");
2019-06-24 11:58:36 +02:00
}
else if (!File.Exists(dllPath))
2021-01-13 15:18:59 +11:00
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Cached models dll does not exist.");
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
else if (File.Exists(dllPath + ".delete"))
2021-01-13 15:18:59 +11:00
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Cached models dll is marked for deletion.");
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
else
2021-01-13 15:18:59 +11:00
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Cached models dll cannot be loaded (why?).");
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
}
// 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);
2021-01-13 15:18:59 +11:00
Match match = s_assemblyVersionRegex.Match(text);
2019-06-24 11:58:36 +02:00
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);
2019-06-24 11:58:36 +02:00
File.WriteAllText(dllPathFile, assembly.Location);
2020-09-08 11:23:27 +02:00
File.WriteAllText(modelsHashFile, currentHash);
TryDeleteUnusedAssemblies(dllPathFile);
2019-06-24 11:58:36 +02:00
}
catch
{
ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile);
throw;
}
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Loading cached models (source).");
2019-06-24 11:58:36 +02:00
return assembly;
}
// need to rebuild
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Rebuilding models.");
2019-06-24 11:58:36 +02:00
// generate code, save
var code = GenerateModelsCode(typeModels);
// add extra attributes,
// IsLive=true helps identifying Assemblies that contain live models
2019-06-24 11:58:36 +02:00
// 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}"")]
2021-01-13 15:18:59 +11:00
[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]";
code = code.Replace("//ASSATTR", mbAssemblyDirective);
2019-06-24 11:58:36 +02:00
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);
2019-06-24 11:58:36 +02:00
File.WriteAllText(modelsHashFile, currentHash);
TryDeleteUnusedAssemblies(dllPathFile);
2019-06-24 11:58:36 +02:00
}
catch
{
ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile);
throw;
}
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Done rebuilding.");
2019-06-24 11:58:36 +02:00
return assembly;
}
private void TryDeleteUnusedAssemblies(string dllPathFile)
2020-09-03 14:18:09 +02:00
{
if (File.Exists(dllPathFile))
{
var dllPath = File.ReadAllText(dllPathFile);
2021-01-13 15:18:59 +11:00
DirectoryInfo dirInfo = new DirectoryInfo(dllPath).Parent;
IEnumerable<FileInfo> files = dirInfo.GetFiles().Where(f => f.FullName != dllPath);
foreach (FileInfo file in files)
{
try
{
File.Delete(file.FullName);
}
2021-01-13 15:18:59 +11:00
catch (UnauthorizedAccessException)
{
// The file is in use, we'll try again next time...
// This shouldn't happen anymore.
}
}
2020-09-03 14:18:09 +02:00
}
}
2020-09-02 15:35:05 +02:00
private string GetOutputAssemblyPath(string currentHash)
{
var dirInfo = new DirectoryInfo(Path.Combine(_pureLiveDirectory.Value, "Compiled"));
2020-09-02 15:35:05 +02:00
if (!dirInfo.Exists)
2021-01-13 15:18:59 +11:00
{
2020-09-03 15:03:02 +02:00
Directory.CreateDirectory(dirInfo.FullName);
2021-01-13 15:18:59 +11:00
}
2020-09-02 15:46:48 +02:00
return Path.Combine(dirInfo.FullName, $"generated.cs{currentHash}.dll");
2020-09-02 15:35:05 +02:00
}
2019-06-24 11:58:36 +02:00
private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile)
{
2020-09-15 08:45:40 +02:00
_logger.LogDebug("Failed to compile.");
2019-06-24 11:58:36 +02:00
// the dll file reference still points to the previous dll, which is obsolete
// now and will be deleted by ASP.NET eventually, so better clear that reference.
// also touch the proj file to force views to recompile - don't delete as it's
// useful to have the source around for debugging.
try
{
2021-01-13 15:18:59 +11:00
if (File.Exists(dllPathFile))
{
File.Delete(dllPathFile);
}
if (File.Exists(modelsHashFile))
{
File.Delete(modelsHashFile);
}
if (File.Exists(projFile))
{
File.SetLastWriteTime(projFile, DateTime.Now);
}
2019-06-24 11:58:36 +02:00
}
catch { /* enough */ }
}
private static Infos RegisterModels(IEnumerable<Type> types)
{
2021-01-13 15:18:59 +11:00
Type[] ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) };
2019-06-24 11:58:36 +02:00
var modelInfos = new Dictionary<string, ModelInfo>(StringComparer.InvariantCultureIgnoreCase);
var map = new Dictionary<string, Type>();
2021-01-13 15:18:59 +11:00
foreach (Type type in types)
2019-06-24 11:58:36 +02:00
{
ConstructorInfo constructor = null;
Type parameterType = null;
2021-01-13 15:18:59 +11:00
foreach (ConstructorInfo ctor in type.GetConstructors())
2019-06-24 11:58:36 +02:00
{
2021-01-13 15:18:59 +11:00
ParameterInfo[] parms = ctor.GetParameters();
2020-09-02 14:44:01 +02:00
if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType))
2019-06-24 11:58:36 +02:00
{
if (constructor != null)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet.");
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
constructor = ctor;
parameterType = parms[0].ParameterType;
}
}
if (constructor == null)
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPropertySet.");
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
2021-01-13 15:18:59 +11:00
PublishedModelAttribute attribute = type.GetCustomAttribute<PublishedModelAttribute>(false);
2019-06-24 11:58:36 +02:00
var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias;
if (modelInfos.TryGetValue(typeName, out var modelInfo))
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\".");
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
2020-07-14 10:08:59 +10:00
// TODO: use Core's ReflectionUtilities.EmitCtor !!
// Yes .. DynamicMethod is uber slow
2020-07-14 10:08:59 +10:00
// TODO: But perhaps https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 is better still?
// See CtorInvokeBenchmarks
var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true);
2021-01-13 15:18:59 +11:00
ILGenerator gen = meth.GetILGenerator();
2019-06-24 11:58:36 +02:00
gen.Emit(OpCodes.Ldarg_0);
2020-09-02 15:18:22 +02:00
gen.Emit(OpCodes.Ldarg_1);
2019-06-24 11:58:36 +02:00
gen.Emit(OpCodes.Newobj, constructor);
gen.Emit(OpCodes.Ret);
2020-09-02 14:44:01 +02:00
var func = (Func<IPublishedElement, IPublishedValueFallback, IPublishedElement>)meth.CreateDelegate(typeof(Func<IPublishedElement, IPublishedValueFallback, IPublishedElement>));
2019-06-24 11:58:36 +02:00
modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type };
map[typeName] = type;
}
return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map };
}
private string GenerateModelsCode(IList<TypeModel> typeModels)
{
if (!Directory.Exists(_pureLiveDirectory.Value))
2021-01-13 15:18:59 +11:00
{
Directory.CreateDirectory(_pureLiveDirectory.Value);
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
foreach (var file in Directory.GetFiles(_pureLiveDirectory.Value, "*.generated.cs"))
2021-01-13 15:18:59 +11:00
{
2019-06-24 11:58:36 +02:00
File.Delete(file);
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
var builder = new TextBuilder(_config, typeModels);
2019-06-24 11:58:36 +02:00
var codeBuilder = new StringBuilder();
builder.Generate(codeBuilder, builder.GetModelsToGenerate());
var code = codeBuilder.ToString();
return code;
}
2019-06-24 11:58:36 +02:00
private static string GenerateModelsProj(IDictionary<string, string> files)
{
// ideally we would generate a CSPROJ file but then we'd need a BuildProvider for csproj
// trying to keep things simple for the time being, just write everything to one big file
// group all 'using' at the top of the file (else fails)
var usings = new List<string>();
2021-01-13 15:18:59 +11:00
foreach (string k in files.Keys.ToList())
{
files[k] = s_usingRegex.Replace(files[k], m =>
2019-06-24 11:58:36 +02:00
{
usings.Add(m.Groups[1].Value);
return string.Empty;
});
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
// group all '[assembly:...]' at the top of the file (else fails)
var aattrs = new List<string>();
2021-01-13 15:18:59 +11:00
foreach (string k in files.Keys.ToList())
{
files[k] = s_aattrRegex.Replace(files[k], m =>
2019-06-24 11:58:36 +02:00
{
aattrs.Add(m.Groups[1].Value);
return string.Empty;
});
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
var text = new StringBuilder();
foreach (var u in usings.Distinct())
{
text.Append("using ");
text.Append(u);
text.Append(";\r\n");
}
2021-01-13 15:18:59 +11:00
2019-06-24 11:58:36 +02:00
foreach (var a in aattrs)
{
text.Append("[assembly:");
text.Append(a);
text.Append("]\r\n");
}
2021-01-13 15:18:59 +11:00
2019-06-24 11:58:36 +02:00
text.Append("\r\n\r\n");
2021-01-13 15:18:59 +11:00
foreach (KeyValuePair<string, string> f in files)
2019-06-24 11:58:36 +02:00
{
text.Append("// FILE: ");
text.Append(f.Key);
text.Append("\r\n\r\n");
text.Append(f.Value);
text.Append("\r\n\r\n\r\n");
}
2021-01-13 15:18:59 +11:00
2019-06-24 11:58:36 +02:00
text.Append("// EOF\r\n");
return text.ToString();
}
private void WatcherOnChanged(object sender, FileSystemEventArgs args)
{
var changed = args.Name;
// don't reset when our files change because we are building!
//
// comment it out, and always ignore our files, because it seems that some
// race conditions can occur on slow Cloud filesystems and then we keep
// rebuilding
//if (_building && OurFiles.Contains(changed))
//{
// //_logger.LogInformation<InMemoryModelFactory>("Ignoring files self-changes.");
2019-06-24 11:58:36 +02:00
// return;
//}
// always ignore our own file changes
2021-01-13 15:18:59 +11:00
if (s_ourFiles.Contains(changed))
{
2019-06-24 11:58:36 +02:00
return;
}
2019-06-24 11:58:36 +02:00
2020-09-15 08:45:40 +02:00
_logger.LogInformation("Detected files changes.");
2019-06-24 11:58:36 +02:00
lock (SyncRoot) // don't reset while being locked
{
2019-06-24 11:58:36 +02:00
ResetModels();
}
2019-06-24 11:58:36 +02:00
}
public void Stop(bool immediate)
{
_watcher.EnableRaisingEvents = false;
_watcher.Dispose();
_locker.Dispose();
_hostingLifetime.UnregisterObject(this);
2019-06-24 11:58:36 +02:00
}
2021-01-13 15:18:59 +11:00
internal class Infos
{
public Dictionary<string, Type> ModelTypeMap { get; set; }
public Dictionary<string, ModelInfo> ModelInfos { get; set; }
}
internal class ModelInfo
{
public Type ParameterType { get; set; }
public Func<IPublishedElement, IPublishedValueFallback, IPublishedElement> Ctor { get; set; }
public Type ModelType { get; set; }
public Func<IList> ListCtor { get; set; }
}
2019-06-24 11:58:36 +02:00
}
}