2021-01-13 12:48:41 +11:00
using System ;
using System.Collections.Generic ;
2020-12-09 22:43:49 +11:00
using System.Linq ;
2021-01-14 23:14:35 +11:00
using Microsoft.AspNetCore.Mvc.Razor.Extensions ;
using Microsoft.AspNetCore.Razor.Language ;
using Microsoft.CodeAnalysis ;
using Microsoft.CodeAnalysis.Razor ;
2020-10-27 10:53:01 +00:00
using Microsoft.Extensions.DependencyInjection ;
2021-01-13 12:48:41 +11:00
using Microsoft.Extensions.Options ;
2019-06-24 11:58:36 +02:00
using Umbraco.Core.Composing ;
2021-01-13 12:48:41 +11:00
using Umbraco.Core.Configuration ;
2020-08-23 15:58:37 +02:00
using Umbraco.Core.Configuration.Models ;
2020-12-01 12:42:14 +00:00
using Umbraco.Core.DependencyInjection ;
2021-01-13 12:48:41 +11:00
using Umbraco.Core.Models.PublishedContent ;
using Umbraco.ModelsBuilder.Embedded.Building ;
2019-06-24 11:58:36 +02:00
2021-01-13 12:48:41 +11:00
namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection
2019-06-24 11:58:36 +02:00
{
2021-01-13 12:48:41 +11:00
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder"/> for the common Umbraco functionality
/// </summary>
public static class UmbracoBuilderExtensions
2019-06-24 11:58:36 +02:00
{
2021-01-13 12:48:41 +11:00
/// <summary>
/// Adds umbraco's embedded model builder support
/// </summary>
public static IUmbracoBuilder AddModelsBuilder ( this IUmbracoBuilder builder )
2019-06-24 11:58:36 +02:00
{
2021-01-14 23:14:35 +11:00
builder . AddRazorProjectEngine ( ) ;
2020-11-18 17:40:23 +00:00
builder . Services . AddSingleton < UmbracoServices > ( ) ;
2021-01-13 12:48:41 +11:00
builder . Services . AddSingleton < ModelsBuilderNotificationHandler > ( ) ;
2020-11-18 17:40:23 +00:00
builder . Services . AddUnique < ModelsGenerator > ( ) ;
builder . Services . AddUnique < LiveModelsProvider > ( ) ;
builder . Services . AddUnique < OutOfDateModelsStatus > ( ) ;
builder . Services . AddUnique < ModelsGenerationError > ( ) ;
2020-09-09 11:15:14 +02:00
2020-11-18 17:40:23 +00:00
builder . Services . AddUnique < PureLiveModelFactory > ( ) ;
builder . Services . AddUnique < IPublishedModelFactory > ( factory = >
2019-06-24 11:58:36 +02:00
{
2021-01-13 12:48:41 +11:00
ModelsBuilderSettings config = factory . GetRequiredService < IOptions < ModelsBuilderSettings > > ( ) . Value ;
2020-09-21 21:20:46 +02:00
if ( config . ModelsMode = = ModelsMode . PureLive )
2020-09-11 21:13:18 +02:00
{
2020-10-27 10:53:01 +00:00
return factory . GetRequiredService < PureLiveModelFactory > ( ) ;
2021-01-13 12:48:41 +11:00
2020-09-11 21:13:18 +02:00
// the following would add @using statement in every view so user's don't
// have to do it - however, then noone understands where the @using statement
// comes from, and it cannot be avoided / removed --- DISABLED
2021-01-13 12:48:41 +11:00
2020-09-11 21:13:18 +02:00
/ *
// no need for @using in views
// note:
// we are NOT using the in-code attribute here, config is required
// because that would require parsing the code... and what if it changes?
// we can AddGlobalImport not sure we can remove one anyways
var modelsNamespace = Configuration . Config . ModelsNamespace ;
if ( string . IsNullOrWhiteSpace ( modelsNamespace ) )
modelsNamespace = Configuration . Config . DefaultModelsNamespace ;
System . Web . WebPages . Razor . WebPageRazorHost . AddGlobalImport ( modelsNamespace ) ;
* /
}
else if ( config . EnableFactory )
{
2021-01-13 12:48:41 +11:00
TypeLoader typeLoader = factory . GetRequiredService < TypeLoader > ( ) ;
IPublishedValueFallback publishedValueFallback = factory . GetRequiredService < IPublishedValueFallback > ( ) ;
IEnumerable < Type > types = typeLoader
2020-09-11 21:13:18 +02:00
. GetTypes < PublishedElementModel > ( ) // element models
. Concat ( typeLoader . GetTypes < PublishedContentModel > ( ) ) ; // content models
return new PublishedModelFactory ( types , publishedValueFallback ) ;
}
return null ;
} ) ;
2019-06-24 11:58:36 +02:00
2021-01-13 12:48:41 +11:00
return builder ;
}
/// <summary>
/// Can be called if using an external models builder to remove the embedded models builder controller features
/// </summary>
public static IUmbracoBuilder DisableModelsBuilderControllers ( this IUmbracoBuilder builder )
{
builder . Services . AddSingleton < DisableModelsBuilderNotificationHandler > ( ) ;
return builder ;
2019-06-24 11:58:36 +02:00
}
2021-01-14 23:14:35 +11:00
private static IUmbracoBuilder AddRazorProjectEngine ( this IUmbracoBuilder builder )
{
// TODO: This is super nasty, but we can at least tinker with this for now
// this pre-builds the container just so we can extract the default RazorProjectEngine
// in order to extract out all features and re-add them to ours
// Since we cannot construct the razor engine like netcore does:
// https://github.com/dotnet/aspnetcore/blob/336e05577cd8bec2000ffcada926189199e4cef0/src/Mvc/Mvc.Razor.RuntimeCompilation/src/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs#L86
// because many things are internal we need to resort to this which is to get the default RazorProjectEngine
// that is nornally created and use that to create our custom one while ensuring all of the razor features
// that we can't really add ourselves are there.
// Pretty much all methods, even thnigs like SetCSharpLanguageVersion are actually adding razor features.
//var internalServicesBuilder = new ServiceCollection();
//internalServicesBuilder.AddControllersWithViews().AddRazorRuntimeCompilation();
//var internalServices = internalServicesBuilder.BuildServiceProvider();
//var defaultRazorProjectEngine = internalServices.GetRequiredService<RazorProjectEngine>();
ServiceProvider internalServices = builder . Services . BuildServiceProvider ( ) ;
RazorProjectEngine defaultRazorProjectEngine = internalServices . GetRequiredService < RazorProjectEngine > ( ) ;
builder . Services . AddSingleton ( s = >
{
RazorProjectFileSystem fileSystem = s . GetRequiredService < RazorProjectFileSystem > ( ) ;
// Create the project engine
var projectEngine = RazorProjectEngine . Create ( RazorConfiguration . Default , fileSystem , builder = >
{
// replace all features with the defaults
builder . Features . Clear ( ) ;
foreach ( IRazorEngineFeature f in defaultRazorProjectEngine . EngineFeatures )
{
builder . Features . Add ( f ) ;
}
foreach ( IRazorProjectEngineFeature f in defaultRazorProjectEngine . ProjectFeatures )
{
builder . Features . Add ( f ) ;
}
// The razor engine only supports one instance of IMetadataReferenceFeature
// so we need to jump through some hoops to allow multiple by using a wrapper.
// so get the current ones, remove them from the list, create a wrapper of them and
// our custom one and then add it back.
var metadataReferenceFeatures = builder . Features . OfType < IMetadataReferenceFeature > ( ) . ToList ( ) ;
foreach ( IMetadataReferenceFeature m in metadataReferenceFeatures )
{
builder . Features . Remove ( m ) ;
}
// add our custom one to the list
metadataReferenceFeatures . Add ( new PureLiveMetadataReferenceFeature ( s . GetRequiredService < PureLiveModelFactory > ( ) ) ) ;
// now add them to our wrapper and back into the features
builder . Features . Add ( new MetadataReferenceFeatureWrapper ( metadataReferenceFeatures ) ) ;
//RazorExtensions.Register(builder);
//// Roslyn + TagHelpers infrastructure
//// TODO: These are internal...
//var referenceManager = s.GetRequiredService<RazorReferenceManager>();
//builder.Features.Add(new LazyMetadataReferenceFeature(referenceManager));
//builder.Features.Add(new CompilationTagHelperFeature());
//// TagHelperDescriptorProviders (actually do tag helper discovery)
//builder.Features.Add(new DefaultTagHelperDescriptorProvider());
//builder.Features.Add(new ViewComponentTagHelperDescriptorProvider());
//builder.SetCSharpLanguageVersion(csharpCompiler.ParseOptions.LanguageVersion);
} ) ;
return projectEngine ;
} ) ;
return builder ;
}
}
/// <summary>
/// Wraps multiple <see cref="IMetadataReferenceFeature"/>
/// </summary>
/// <remarks>
/// <para>
/// This is required because the razor engine only supports a single IMetadataReferenceFeature but their APIs don't state this,
/// this is purely down to them doing this 'First' call: https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Razor/Microsoft.CodeAnalysis.Razor/src/CompilationTagHelperFeature.cs#L37
/// So in order to have multiple, we need to have a wrapper.
/// </para>
/// </remarks>
public class MetadataReferenceFeatureWrapper : IMetadataReferenceFeature
{
private readonly IReadOnlyList < IMetadataReferenceFeature > _metadataReferenceFeatures ;
private RazorEngine _engine ;
/// <summary>
/// Initializes a new instance of the <see cref="MetadataReferenceFeatureWrapper"/> class.
/// </summary>
public MetadataReferenceFeatureWrapper ( IEnumerable < IMetadataReferenceFeature > metadataReferenceFeatures )
= > _metadataReferenceFeatures = metadataReferenceFeatures . ToList ( ) ;
/// <inheritdoc/>
public IReadOnlyList < MetadataReference > References
= > _metadataReferenceFeatures . SelectMany ( x = > x . References ) . ToList ( ) ;
/// <inheritdoc/>
public RazorEngine Engine
{
get = > _engine ;
set
{
_engine = value ;
foreach ( IMetadataReferenceFeature feature in _metadataReferenceFeatures )
{
feature . Engine = value ;
}
}
}
}
/// <summary>
/// A custom <see cref="IMetadataReferenceFeature"/> that will dynamically resolve a reference for razor based on the current PureLive assembly.
/// </summary>
/// <remarks>
/// <para>
/// The default implementation of IMetadataReferenceFeature is https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/LazyMetadataReferenceFeature.cs
/// which uses a ReferenceManager https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs
/// to resolve it's references. This is done using ApplicationParts which would be nice and simple to use if we could, but the Razor engine ONLY works
/// with application part assemblies that have physical files with physical paths. We don't want to load in our PureLive assemblies on physical paths because
/// those files will be locked. Instead we load them in via bytes but this is not supported and we'll get an exception if we add them to application parts.
/// The other problem with LazyMetadataReferenceFeature is that it doesn't support dynamic assemblies, it will just check what in application parts once and
/// that's it which will not work for us in Pure Live.
/// </para>
/// </remarks>
internal class PureLiveMetadataReferenceFeature : IMetadataReferenceFeature
{
// TODO: Even though I was hoping this would work and this does allow you to return a metadata reference dynamically at runtime, it doesn't make any
// difference because the CSharpCompiler for razor only loads in it's references one time based on the initial reference checks:
// https://github.com/dotnet/aspnetcore/blob/100ab02ea0214d49535fa56f33a77acd61fe039c/src/Mvc/Mvc.Razor.RuntimeCompilation/src/CSharpCompiler.cs#L84
// Since ReferenceManager resolves them once lazily and that's it.
private readonly PureLiveModelFactory _pureLiveModelFactory ;
public PureLiveMetadataReferenceFeature ( PureLiveModelFactory pureLiveModelFactory ) = > _pureLiveModelFactory = pureLiveModelFactory ;
/// <inheritdoc/>
public IReadOnlyList < MetadataReference > References
{
get
{
// TODO: This won't really work based on how the CSharp compiler works
//if (_pureLiveModelFactory.CurrentModelsMetadataReference != null)
//{
// //var reference = MetadataReference.CreateFromStream(null);
// //reference.
// return new[] { _pureLiveModelFactory.CurrentModelsMetadataReference };
//}
return Array . Empty < MetadataReference > ( ) ;
}
}
/// <inheritdoc/>
public RazorEngine Engine { get ; set ; }
2019-06-24 11:58:36 +02:00
}
}