2019-06-24 11:58:36 +02:00
using System.Collections ;
using System.Reflection ;
using System.Reflection.Emit ;
2021-01-14 23:14:35 +11:00
using System.Runtime.Loader ;
2019-06-24 11:58:36 +02:00
using System.Text ;
using System.Text.RegularExpressions ;
2022-10-07 10:42:32 +02:00
using Microsoft.AspNetCore.Mvc.Razor ;
2020-09-21 09:27:54 +02:00
using Microsoft.Extensions.Logging ;
2021-01-13 15:18:59 +11:00
using Microsoft.Extensions.Options ;
2021-02-18 11:06:02 +01:00
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 ;
2021-02-22 09:00:33 +01:00
using Umbraco.Cms.Infrastructure.ModelsBuilder ;
using Umbraco.Cms.Infrastructure.ModelsBuilder.Building ;
2021-02-18 11:06:02 +01:00
using Umbraco.Extensions ;
2019-06-24 11:58:36 +02:00
using File = System . IO . File ;
2022-10-07 10:42:32 +02:00
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto
2019-06-24 11:58:36 +02:00
{
2021-08-11 19:11:35 +02:00
internal class InMemoryModelFactory : IAutoPublishedModelFactory , IRegisteredObject , IDisposable
2019-06-24 11:58:36 +02:00
{
2022-05-09 09:39:46 +02: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 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" } ;
2019-06-24 11:58:36 +02:00
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim ( ) ;
2020-09-15 08:45:40 +02:00
private readonly IProfilingLogger _profilingLogger ;
2021-05-26 07:36:21 +01:00
private readonly ILogger < InMemoryModelFactory > _logger ;
2022-03-29 13:44:21 +02:00
private readonly FileSystemWatcher ? _watcher ;
2019-10-28 19:08:42 +11:00
private readonly Lazy < UmbracoServices > _umbracoServices ; // fixme: this is because of circular refs :(
2020-09-02 14:44:01 +02:00
private readonly IHostingEnvironment _hostingEnvironment ;
2020-03-26 15:39:20 +11:00
private readonly IApplicationShutdownRegistry _hostingLifetime ;
2019-10-28 19:08:42 +11:00
private readonly ModelsGenerationError _errors ;
2020-09-02 14:44:01 +02:00
private readonly IPublishedValueFallback _publishedValueFallback ;
2022-10-07 10:42:32 +02:00
private readonly InMemoryAssemblyLoadContextManager _loadContextManager ;
private readonly RuntimeCompilationCacheBuster _runtimeCompilationCacheBuster ;
2022-03-29 13:44:21 +02:00
private readonly Lazy < string > _pureLiveDirectory = null ! ;
2022-05-09 09:39:46 +02:00
private readonly int _debugLevel ;
private Infos _infos = new Infos { ModelInfos = null , ModelTypeMap = new Dictionary < string , Type > ( ) } ;
private volatile bool _hasModels ; // volatile 'cos reading outside lock
private bool _pendingRebuild ;
private int _ver ;
private int? _skipver ;
private RoslynCompiler ? _roslynCompiler ;
private ModelsBuilderSettings _config ;
2021-08-11 19:11:35 +02:00
private bool _disposedValue ;
2021-05-17 17:50:58 +10:00
2021-05-26 07:36:21 +01:00
public InMemoryModelFactory (
2020-03-17 16:26:56 +01:00
Lazy < UmbracoServices > umbracoServices ,
2020-09-15 08:45:40 +02:00
IProfilingLogger profilingLogger ,
2021-05-26 07:36:21 +01:00
ILogger < InMemoryModelFactory > logger ,
2021-09-24 09:49:53 +02:00
IOptionsMonitor < ModelsBuilderSettings > config ,
2020-03-17 16:26:56 +01:00
IHostingEnvironment hostingEnvironment ,
2020-03-26 15:39:20 +11:00
IApplicationShutdownRegistry hostingLifetime ,
2021-01-14 23:14:35 +11:00
IPublishedValueFallback publishedValueFallback ,
2022-10-07 10:42:32 +02:00
InMemoryAssemblyLoadContextManager loadContextManager ,
RuntimeCompilationCacheBuster runtimeCompilationCacheBuster )
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 ;
2021-09-24 09:49:53 +02:00
_config = config . CurrentValue ;
2020-09-02 14:44:01 +02:00
_hostingEnvironment = hostingEnvironment ;
2020-03-25 15:06:22 +11:00
_hostingLifetime = hostingLifetime ;
2020-09-02 14:44:01 +02:00
_publishedValueFallback = publishedValueFallback ;
2022-10-07 10:42:32 +02:00
_loadContextManager = loadContextManager ;
_runtimeCompilationCacheBuster = runtimeCompilationCacheBuster ;
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
2022-05-03 19:23:15 +02:00
2021-01-13 15:18:59 +11:00
if ( ! hostingEnvironment . IsHosted )
{
return ;
}
2019-06-24 11:58:36 +02:00
2021-10-04 13:28:57 +02:00
config . OnChange ( x = > _config = x ) ;
2021-05-17 17:50:58 +10:00
_pureLiveDirectory = new Lazy < string > ( PureLiveDirectoryAbsolute ) ;
if ( ! Directory . Exists ( _pureLiveDirectory . Value ) )
2021-01-13 15:18:59 +11:00
{
2021-05-17 17:50:58 +10: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
2020-03-25 15:06:22 +11:00
_hostingLifetime . RegisterObject ( this ) ;
2021-05-17 17:50:58 +10:00
_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 ;
}
2021-01-14 23:14:35 +11:00
/// <summary>
2021-05-26 07:36:21 +01:00
/// Gets the currently loaded Live models assembly
2021-01-14 23:14:35 +11:00
/// </summary>
/// <remarks>
/// Can be null
/// </remarks>
2022-03-29 13:44:21 +02:00
public Assembly ? CurrentModelsAssembly { get ; private set ; }
2021-01-14 23:14:35 +11:00
2019-06-24 11:58:36 +02:00
/// <inheritdoc />
public object SyncRoot { get ; } = new object ( ) ;
2022-05-09 09:39:46 +02:00
private UmbracoServices UmbracoServices = > _umbracoServices . Value ;
2021-01-14 23:14:35 +11:00
/// <summary>
/// Gets the RoslynCompiler
/// </summary>
2021-01-13 15:18:59 +11:00
private RoslynCompiler RoslynCompiler
{
get
{
if ( _roslynCompiler ! = null )
{
return _roslynCompiler ;
}
2021-07-07 09:45:32 -06:00
_roslynCompiler = new RoslynCompiler ( ) ;
2021-01-13 15:18:59 +11:00
return _roslynCompiler ;
}
}
/// <inheritdoc />
2021-05-26 07:36:21 +01:00
public bool Enabled = > _config . ModelsMode = = ModelsMode . InMemoryAuto ;
2021-01-13 15:18:59 +11:00
2019-06-24 11:58:36 +02:00
public IPublishedElement CreateModel ( IPublishedElement element )
{
// get models, rebuilding them if needed
2022-05-09 09:39:46 +02: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)
2019-10-28 21:53:32 +11:00
infos . TryGetValue ( contentTypeAlias , out var info ) ;
2019-06-24 11:58:36 +02:00
// create model
2022-03-29 13:44:21 +02:00
return info is null | | info . Ctor is null ? element : info . Ctor ( element , _publishedValueFallback ) ;
2019-06-24 11:58:36 +02:00
}
2022-05-03 19:23:15 +02:00
/// <inheritdoc />
public Type GetModelType ( string? alias )
{
Infos infos = EnsureModels ( ) ;
// fail fast
2022-05-09 09:39:46 +02:00
if ( alias is null | |
2022-05-03 19:23:15 +02:00
infos . ModelInfos is null | |
! infos . ModelInfos . TryGetValue ( alias , out ModelInfo ? modelInfo ) | |
modelInfo . ModelType is null )
{
return typeof ( IPublishedElement ) ;
}
return modelInfo . ModelType ;
}
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
2022-03-29 13:44:21 +02:00
public IList CreateModelList ( string? alias )
2019-06-24 11:58:36 +02:00
{
2021-01-13 15:18:59 +11:00
Infos infos = EnsureModels ( ) ;
2019-06-24 11:58:36 +02:00
// fail fast
2022-05-09 09:39:46 +02:00
if ( alias is null | | infos . ModelInfos is null | | ! infos . ModelInfos . TryGetValue ( alias , out ModelInfo ? modelInfo ) )
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
2022-03-29 13:44:21 +02:00
Func < IList > ? ctor = modelInfo . ListCtor ;
2021-01-13 15:18:59 +11:00
if ( ctor ! = null )
{
return ctor ( ) ;
}
2019-06-24 11:58:36 +02:00
2022-03-29 13:44:21 +02:00
if ( modelInfo . ModelType is null )
{
return new List < IPublishedElement > ( ) ;
}
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 ) ;
2022-03-29 13:44:21 +02:00
return ctor is null ? new List < IPublishedElement > ( ) : ctor ( ) ;
2019-06-24 11:58:36 +02:00
}
2020-04-20 22:25:05 +10:00
/// <inheritdoc />
public void Reset ( )
{
2021-01-15 18:17:13 +11:00
if ( Enabled )
2021-01-13 15:18:59 +11:00
{
2020-04-20 22:25:05 +10:00
ResetModels ( ) ;
2021-01-13 15:18:59 +11:00
}
2020-04-20 22:25:05 +10: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 ;
2021-05-17 17:50:58 +10:00
if ( ! Directory . Exists ( _pureLiveDirectory . Value ) )
2021-01-13 15:18:59 +11:00
{
2021-05-17 17:50:58 +10:00
Directory . CreateDirectory ( _pureLiveDirectory . Value ) ;
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
// clear stuff
2021-05-17 17:50:58 +10:00
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
2021-05-26 07:36:21 +01:00
using ( _profilingLogger . DebugDuration < InMemoryModelFactory > ( "Get models." , "Got models." ) )
2019-06-24 11:58:36 +02:00
{
try
{
2021-01-19 18:44:19 +11:00
Assembly assembly = GetModelsAssembly ( _pendingRebuild ) ;
2021-01-14 23:14:35 +11:00
CurrentModelsAssembly = assembly ;
2019-06-24 11:58:36 +02:00
2022-10-07 10:42:32 +02:00
/ *
* We used to use an event here , and a RefreshingRazorViewEngine to bust the caches ,
* this worked by essentially completely recreating the entire ViewEngine / ViewCompiler every time we generate models .
* There was this note about first load :
* 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 unnecessarily 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 .
*
* Now we have our own ViewCompiler , and clear the caches more directly , however what the comment mentioned
* is not really a big problem since this will execute before the razor view engine has loaded ,
* which means the cache will be empty already .
* /
_runtimeCompilationCacheBuster . BustCache ( ) ;
2021-01-15 18:17:13 +11:00
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 ) ;
2019-10-28 19:08:42 +11:00
_errors . Clear ( ) ;
2019-06-24 11:58:36 +02:00
}
catch ( Exception e )
{
try
{
2020-10-06 14:21:41 +02:00
_logger . LogError ( e , "Failed to build models." ) ;
2020-09-15 08:45:40 +02:00
_logger . LogWarning ( "Running without models." ) ; // be explicit
2021-05-26 07:36:21 +01:00
_errors . Report ( "Failed to build InMemory models." , e ) ;
2019-06-24 11:58:36 +02:00
}
finally
{
2021-01-14 23:14:35 +11:00
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
}
}
2022-05-09 09:39:46 +02:00
public string PureLiveDirectoryAbsolute ( ) = > _hostingEnvironment . MapPathContentRoot ( Core . Constants . SystemDirectories . TempData + "/InMemoryAuto" ) ;
2021-05-17 17:50:58 +10:00
2021-01-15 18:17:13 +11:00
// This is NOT thread safe but it is only called from within a lock
2021-01-19 18:44:19 +11:00
private Assembly ReloadAssembly ( string pathToAssembly )
2021-01-14 23:14:35 +11:00
{
2022-10-07 10:42:32 +02:00
_loadContextManager . RenewAssemblyLoadContext ( ) ;
2021-01-14 23:14:35 +11:00
2021-01-19 15:46:25 +11:00
// NOTE: We cannot use in-memory assemblies due to the way the razor engine works which must use
2021-01-15 18:17:13 +11:00
// 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
2021-01-19 15:46:25 +11:00
// 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 ( ) ) ;
2021-01-14 23:14:35 +11:00
File . Copy ( pathToAssembly , tempFile , true ) ;
2021-01-15 18:17:13 +11:00
2021-01-14 23:14:35 +11:00
// Load it in
2022-10-07 10:42:32 +02:00
Assembly assembly = _loadContextManager . LoadModelsAssembly ( tempFile ) ;
2021-01-14 23:14:35 +11:00
return assembly ;
2020-09-04 10:38:37 +02:00
}
2021-01-15 18:17:13 +11:00
// This is NOT thread safe but it is only called from within a lock
2021-01-19 18:44:19 +11:00
private Assembly GetModelsAssembly ( bool forceRebuild )
2019-06-24 11:58:36 +02:00
{
2021-05-17 17:50:58 +10:00
if ( ! Directory . Exists ( _pureLiveDirectory . Value ) )
2021-01-13 15:18:59 +11:00
{
2021-05-17 17:50:58 +10: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 ( ) ;
2019-10-28 21:53:32 +11:00
var currentHash = TypeModelHasher . Hash ( typeModels ) ;
2021-05-17 17:50:58 +10:00
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
{
2021-01-19 18:44:19 +11:00
assembly = ReloadAssembly ( dllPath ) ;
2020-09-09 12:46:23 +02:00
2022-03-29 13:44:21 +02:00
ModelsBuilderAssemblyAttribute ? attr = assembly . GetCustomAttribute < ModelsBuilderAssemblyAttribute > ( ) ;
2021-05-26 07:36:21 +01:00
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
2022-03-29 13:44:21 +02:00
_skipver = assembly . GetName ( ) . Version ? . Revision ;
2019-06-24 11:58:36 +02:00
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
{
2020-09-07 11:47:46 +02:00
var assemblyPath = GetOutputAssemblyPath ( currentHash ) ;
2020-11-19 03:07:05 -08:00
RoslynCompiler . CompileToFile ( projFile , assemblyPath ) ;
2021-01-19 18:44:19 +11:00
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 ) ;
2020-09-03 14:53:36 +02:00
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 ) ;
2022-05-09 09:39:46 +02:00
2019-06-24 11:58:36 +02:00
// add extra attributes,
2021-05-26 07:36:21 +01:00
// 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 + + ;
2021-05-26 07:36:21 +01:00
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 >
{
2022-05-09 09:39:46 +02:00
{ "models.generated.cs" , code } ,
2019-06-24 11:58:36 +02:00
} ;
var proj = GenerateModelsProj ( projFiles ) ;
File . WriteAllText ( projFile , proj ) ;
// compile and register
try
{
2020-09-07 11:47:46 +02:00
var assemblyPath = GetOutputAssemblyPath ( currentHash ) ;
2020-11-19 03:07:05 -08:00
RoslynCompiler . CompileToFile ( projFile , assemblyPath ) ;
2021-01-19 18:44:19 +11:00
assembly = ReloadAssembly ( assemblyPath ) ;
2020-09-07 10:19:25 +02:00
File . WriteAllText ( dllPathFile , assemblyPath ) ;
2019-06-24 11:58:36 +02:00
File . WriteAllText ( modelsHashFile , currentHash ) ;
2020-09-03 14:53:36 +02:00
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 ;
}
2020-09-04 10:38:37 +02:00
private void TryDeleteUnusedAssemblies ( string dllPathFile )
2020-09-03 14:18:09 +02:00
{
if ( File . Exists ( dllPathFile ) )
{
var dllPath = File . ReadAllText ( dllPathFile ) ;
2022-03-29 13:44:21 +02:00
DirectoryInfo ? dirInfo = new DirectoryInfo ( dllPath ) . Parent ;
IEnumerable < FileInfo > ? files = dirInfo ? . GetFiles ( ) . Where ( f = > f . FullName ! = dllPath ) ;
if ( files is null )
{
return ;
}
2021-01-13 15:18:59 +11:00
foreach ( FileInfo file in files )
2020-09-03 14:53:36 +02:00
{
try
{
File . Delete ( file . FullName ) ;
}
2021-01-13 15:18:59 +11:00
catch ( UnauthorizedAccessException )
2020-09-03 14:53:36 +02:00
{
// The file is in use, we'll try again next time...
2020-09-07 10:19:25 +02:00
// This shouldn't happen anymore.
2020-09-03 14:53:36 +02:00
}
}
2020-09-03 14:18:09 +02:00
}
}
2020-09-02 15:35:05 +02:00
private string GetOutputAssemblyPath ( string currentHash )
{
2021-05-17 17:50:58 +10:00
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
}
2021-05-26 07:36:21 +01: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
}
2022-05-09 09:39:46 +02:00
catch
{ /* enough */
}
2019-06-24 11:58:36 +02:00
}
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
{
2022-03-29 13:44:21 +02:00
ConstructorInfo ? constructor = null ;
Type ? parameterType = null ;
2019-06-24 11:58:36 +02:00
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
2022-03-29 13:44:21 +02:00
PublishedModelAttribute ? attribute = type . GetCustomAttribute < PublishedModelAttribute > ( false ) ;
2019-06-24 11:58:36 +02:00
var typeName = attribute = = null ? type . Name : attribute . ContentTypeAlias ;
2019-10-28 21:53:32 +11:00
if ( modelInfos . TryGetValue ( typeName , out var modelInfo ) )
2021-01-13 15:18:59 +11:00
{
2022-03-29 13:44:21 +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 !!
2019-10-28 19:08:42 +11:00
// 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
2019-10-28 21:53:32 +11:00
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 )
{
2021-05-17 17:50:58 +10:00
if ( ! Directory . Exists ( _pureLiveDirectory . Value ) )
2021-01-13 15:18:59 +11:00
{
2021-05-17 17:50:58 +10:00
Directory . CreateDirectory ( _pureLiveDirectory . Value ) ;
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
2021-05-17 17:50:58 +10: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
2019-10-28 18:02:52 +11: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 ;
}
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
2022-05-09 09:39:46 +02:00
// if (_building && OurFiles.Contains(changed))
// {
2021-05-26 07:36:21 +01:00
// //_logger.LogInformation<InMemoryModelFactory>("Ignoring files self-changes.");
2019-06-24 11:58:36 +02:00
// return;
2022-05-09 09:39:46 +02:00
// }
2019-06-24 11:58:36 +02:00
// always ignore our own file changes
2021-01-13 15:18:59 +11:00
if ( s_ourFiles . Contains ( changed ) )
2021-01-14 23:14:35 +11:00
{
2019-06-24 11:58:36 +02:00
return ;
2021-01-14 23:14:35 +11:00
}
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
2022-05-09 09:39:46 +02:00
// don't reset while being locked
lock ( SyncRoot )
2021-01-14 23:14:35 +11:00
{
2019-06-24 11:58:36 +02:00
ResetModels ( ) ;
2021-01-14 23:14:35 +11:00
}
2019-06-24 11:58:36 +02:00
}
public void Stop ( bool immediate )
{
2021-08-11 19:11:35 +02:00
Dispose ( ) ;
2020-03-25 15:06:22 +11:00
_hostingLifetime . UnregisterObject ( this ) ;
2019-06-24 11:58:36 +02:00
}
2021-08-11 19:11:35 +02:00
protected virtual void Dispose ( bool disposing )
{
if ( ! _disposedValue )
{
if ( disposing )
{
2022-03-29 13:44:21 +02:00
if ( _watcher is not null )
{
_watcher . EnableRaisingEvents = false ;
_watcher . Dispose ( ) ;
}
2021-08-11 19:11:35 +02:00
_locker . Dispose ( ) ;
}
_disposedValue = true ;
}
}
public void Dispose ( )
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose ( disposing : true ) ;
}
2021-01-13 15:18:59 +11:00
internal class Infos
{
2022-03-29 13:44:21 +02:00
public Dictionary < string , Type > ? ModelTypeMap { get ; set ; }
2021-01-13 15:18:59 +11:00
2022-03-29 13:44:21 +02:00
public Dictionary < string , ModelInfo > ? ModelInfos { get ; set ; }
2021-01-13 15:18:59 +11:00
}
internal class ModelInfo
{
2022-03-29 13:44:21 +02:00
public Type ? ParameterType { get ; set ; }
2021-01-13 15:18:59 +11:00
2022-03-29 13:44:21 +02:00
public Func < IPublishedElement , IPublishedValueFallback , IPublishedElement > ? Ctor { get ; set ; }
2021-01-13 15:18:59 +11:00
2022-03-29 13:44:21 +02:00
public Type ? ModelType { get ; set ; }
2021-01-13 15:18:59 +11:00
2022-03-29 13:44:21 +02:00
public Func < IList > ? ListCtor { get ; set ; }
2021-01-13 15:18:59 +11:00
}
2019-06-24 11:58:36 +02:00
}
}