-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Decouple Modular system from Asp.Net MVC #530
Conversation
It should use How did you do to have custom MVC/Razor configuration done from a module?
I will do some local perf tests to ensure the raw perf has not been impacted (theorically it should have no impact) and the multi-tenant scenario is still good (I expect some impact on this one as you told me the MVC pipeline is different now for each pipeline). Maybe @jersiovic should try it too as the way MVC is setup might impact his branch. |
Typo - good catch! - Will answer the other questions on my way home. |
@@ -67,6 +69,7 @@ public static IMvcCoreBuilder AddExtensionsApplicationParts(this IMvcCoreBuilder | |||
var availableExtensions = extensionManager.GetExtensions(); | |||
using (logger.BeginScope("Loading extensions")) | |||
{ | |||
ConcurrentBag<Assembly> bagOfAssemblies = new ConcurrentBag<Assembly>(); | |||
Parallel.ForEach(availableExtensions, new ParallelOptions { MaxDegreeOfParallelism = 4 }, ae => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why parallel here, it's just enumerating a collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure in this context but if we still load extensions as we currently do. In the loop LoadExtensionAsync()
is called which triggers dyn compilation and / or loading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you saying GetExtensions()
is not actually loading them, but only when we enumerate its result?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, GetExtensions() only grab extensions infos based on the search paths options.
_loadedAssemblies | ||
.Values | ||
.Select(x => x.Value.Location)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've seen you now pass metadata references to the razor view engine as MetadataReferenceFeature
through ApplicationPartManager.FeatureProviders
. Wow, did you try it, does it work? If so, cool.
So, what i can say is that normally here we exclude assemblies which are ambient (part of the app). This by using an hashset which is initialized with ApplicationAssemblyNames
(see below), not to add them but to exclude them.
@@ -18,7 +21,8 @@ public IFeatureInfo GetFeatureForDependency(Type dependency) | |||
return feature; | |||
} | |||
|
|||
throw new InvalidOperationException($"Could not resolve feature for type {dependency.Name}"); | |||
return CoreFeature; | |||
//throw new InvalidOperationException($"Could not resolve feature for type {dependency.Name}"); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We removed this since this commit which adds all registered services by modules to ITypeFeatureProvider.
Maybe in this context you need it. Or maybe because you use a service defined in the core but which would need to be registered through a module, as we do for some services through the Orchard.Commons
module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
@@ -27,7 +27,7 @@ public class ShellContext : IDisposable | |||
/// </summary> | |||
public IServiceScope CreateServiceScope() | |||
{ | |||
return ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope(); | |||
return ServiceProvider.CreateScope(); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be a new extension method from 1.1 that does exactly what I wrote. So I assume you can remove this method altogether.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, but maybe we still need the method to use the ServiceProvider
of a given ShellContext
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes you're right, we could remove it, or keep it as an helper just to write shellContext.CreateScope()
in place of shellContext.ServiceProvider.CreateScope()
.
{ | ||
using (var stream = File.OpenRead(path)) | ||
{ | ||
var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This not to read the entire image into memory 👍
internalServices.AddOptions(); | ||
internalServices.AddLocalization(); | ||
internalServices.AddHostCore(); | ||
internalServices.AddExtensionManagerHost("app_data", "dependencies"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
App_Data
((List<MetadataReference>)options.AdditionalCompilationReferences).AddRange(extensionLibraryService.MetadataReferences()); | ||
var extensionLibraryService = serviceProvider.GetRequiredService<IExtensionLibraryService>(); | ||
apm.FeatureProviders.Add( | ||
new ExtensionMetadataReferenceFeatureProvider(extensionLibraryService.MetadataPaths.ToArray())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway here there is a pending PR on DesignTimeMvcBuilderConfiguration
to be able to resolve shape tag helpers and where we configure services through orchard ... So, we will see ;)
tenantServiceCollection.Add(startupServices); | ||
tenantServiceCollection = new ServiceCollection().Add(tenantServiceCollection.Distinct()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure? E.g about featureServiceCollection
which has been added to featureServiceCollections
it seems that here it stays empty. And featureServiceCollections
is used later to register feature services in ITypeFeatureProvider
.
Then, startupServices.Add(featureServiceCollection)
seems to add an empty collection.
Then, maybe you need startupServices.Add(tenantServiceCollection)
for other reasons.
When i was saying that for That said, as i remember, we need Best. |
Yo JT - could do with your help on the compilation side of things.
I'm trying to stay clear of preservecompilationcontext as it places certain constraints on the environment you roll out to.
…Sent from my iPhone
On 28 Jan 2017, at 00:36, jtkech ***@***.***> wrote:
preservecompilationcontext
|
No problem i will search on this but i would need more infos on the constraints it places. |
@Jetski5822 just done some quick tests but only through
Best. |
@jtkech thanks man for the feedback. The site loads when I add builder.AddTagHelpersAsServices(); - but you end up with it being in a wierd state. I've removed the Parallel.ForEach(), and will add it to the extension manager in a later PR when I perf test it. I have also fixed the app_data casing issue. If you pull latest you will only see the last error you got, which is to do with missing tag helpers.... I am not sure what is going on to be fair. Even with Compilation Context set to true we would still end up with this issue. I was trying to avoid preserveCompilationContext as we provide the assemblies at run time. preserveCompilationContext is used for the compilation of razor view, but by providing the assemblies at runtime I have worked around that. Im just not understanding why the tag helper stuff is being such a pain in the arse at the moment... I am wondering if I am providing something that overrides some other implementation somewhere... I just dont know. What do you think? |
preserveCompilationContext
Test Orchard.Cms.Web
ExtensionLibraryService
Best. |
@jtkech thanks man, pushed some updates. I have added a ModularApplicationPart, this should provide all Paths of all assemblies at run time. This is scoped to the request, so that we know we are not serving up modules that are not enabled, which is good. I have cleaned up the ServiceCollectionExtensions, and added the metadata stuff back in there. That however is no scoped so will need to think about this. ExtensionLibraryService: reverted the metadata to how you were doing it. Still hitting that dam TagManager thing. I might have to debug the TagManager stuff.... I think its a bug in their stuff as controllers work.... |
I think it might be their code... https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor.Runtime/Runtime/TagHelpers/TagHelperTypeResolver.cs#L72 This means that it will try to resolve a reference from the Host and not from the module... Ill override it |
@@ -136,34 +138,12 @@ private static IList<Assembly> GetAssemblies(HashSet<string> assemblyNames, Asse | |||
return locations; | |||
} | |||
|
|||
public static IEnumerable<Assembly> GetModularAssemblies(IServiceProvider services) | |||
public static IEnumerable<TypeInfo> GetModularAssemblies(IServiceProvider services) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename the method?
Totally!
…Sent from my iPhone
On 2 Feb 2017, at 21:03, Sébastien Ros ***@***.***> wrote:
@sebastienros commented on this pull request.
In src/Microsoft.AspNetCore.Mvc.Modules/Mvc/ModularApplicationPart.cs:
> @@ -136,34 +138,12 @@ private static IList<Assembly> GetAssemblies(HashSet<string> assemblyNames, Asse
return locations;
}
- public static IEnumerable<Assembly> GetModularAssemblies(IServiceProvider services)
+ public static IEnumerable<TypeInfo> GetModularAssemblies(IServiceProvider services)
Maybe rename the method?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
services.AddScoped<IFilterMetadata, AdminZoneFilter>(); | ||
services.AddScoped<IFilterMetadata, AdminFilter>(); | ||
services.AddScoped<IFilterMetadata, AdminMenuFilter>(); | ||
services.Configure<MvcOptions>((options) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beautiful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merci!
{ | ||
public class ModuleViewLocationExpanderProvider : IViewLocationExpanderProvider | ||
public class ModulerViewLocationExpanderProvider : IViewLocationExpanderProvider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moduler or modular ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whoops :)
Lots of changes today.
Next up..
|
@jtkech check it out and give me your thoughts. |
@Jetski5822 Sorry for the delay because of other works, just done some tests and here my findings. Awesome work, i like the idea of the modular app part which is a types and references provider. So, because you add mvc services through a module, we need to provide more types and references which are related to the main app, this through app parts or razor options for references. ModularApplicationPart.Types Called many times, each call takes 10ms, 20000 types are returned. In fact, here we can 1st consider only extensions but we also need to add dependencies we now load ourselves. So, i've exposed our loaded assemblies and by only doing the following it works, each call takes 1ms and 9000 types are returned.
ModularApplicationPart.GetReferencePaths() Called only once and when done here we don't need to do it through razor options. But we can't do it through a regular app part because it would use the dependency context on dynamic loaded modules. I've seen you use So, here we can still provide our
Menu The menu shape implemented through a UPDATE: But here modifying the priority breaks the setup, will investigate. Little things Unused methods: Dynamic compilation / loading / storing Here, just to be aware of what we would need because, beyond dynamic compilation, we are very tied to what dynamic loading and storing do specifically. See all assemblies in You've done an amazing work and, while we could reconsider what we really need about all the dynamic stuff, you are defining part of the framework dynamically through a module. Just a little afraid :) Best. |
hey @jtkech did you pull latest when you took a look? |
No, sorry forgot to mention that i was still on the This because i mainly worked on the ModularApplicationPart Types and GetReferencePaths() and i thought that you are using the same here. So, it works but i'm trying to well understand what happens here and then just add what was missing before. E.g for references, just also add those related to the main app and by using the dependency context to grab compilation assemblies and to be consistent with runtime compilation. For Types i just re-use the assemblies already loaded when modules was loaded ... For the menu it's about the main menu on the front end where you have the Home link and which is not displayed. Here we are dependent on the order of module executions. But maybe this now works for you. |
Oops, I was running out the door.
That doesn't work as MVC doesn't have the required types to compile.
Will look when I get back, but yeah... it's a bit dumb
…Sent from my iPhone
On 13 Feb 2017, at 17:21, Sébastien Ros ***@***.***> wrote:
I don't think you get my point, you are still loading all the types of the assemblies.
We already talked about it on skype, i thought it was clear. An extension has an Assembly, but can convey multiple features. Each feature can be enabled independently. MVC should only register the types from the enabled features. Hence you can't return all the types of the assembly when for instance a single feature of this extension is enabled.
The ShellBluePrint contains a collection of types already filtered with only the types associated to the enabled features.
Here is something that should do it:
public IEnumerable<TypeInfo> Types
{
get
{
return ShellBlueprint
.Dependencies
.SelectMany(dep => dep.Type);
}
}
As for the other property (GetReferencePath) I don't know what it represent.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
maybe you need another ApplicationPArt with something else, and it worked because you just returned everything with this one. What are the missing types? Also you might keep the same current code for GetReferencePaths (this is for RAzor I think) |
Here, just for infos the last time i tried nick's branch (not with the latest commits). Types Before, by using So, i still use References Here we need the ambient framework compilation assemblies because So right now, i use And, for the non ambient libraries, here also we can't use the dependency context, so right now i use our Best. |
When re-trying my branch, just seen that again the main menu was not always rendered. Not sure it's the case with your branch but i've seen that now the 2 LoadFeaturesAsync() no longer return ordered features. So, because of the blueprint features order, even Just for infos i could fix it with this temporary workaround. Best. |
@jtkech hang on dude, I'm breaking things at the moment. Use the code before the commit WIP
…Sent from my iPhone
On 14 Feb 2017, at 06:18, jtkech ***@***.***> wrote:
When re-trying my branch, just seen that again the main menu was not always rendered. Not sure it's the case with your branch but i've seen that now the 2 LoadFeaturesAsync() no longer return ordered features.
So, because of the blueprint features order, even Orchard.Mvc has a negative priority, its startup is not always executed first, so shape tag helpers may not be registered ...
Just for infos i could fix it with this temporary workaround.
Best.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
// Results are cached so that there is no mismatch when loading an assembly twice. | ||
// Otherwise the same types would not match. | ||
return _extensions.GetOrAdd(extensionInfo.Id, (key) => | ||
if (!_isInitialized) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initialize()
is already checking for reentrancy so you don't need it here, just call Initialize()
or rename it EnsureInitialized()
@@ -238,32 +237,43 @@ public IEnumerable<IFeatureInfo> GetDependentFeatures(string featureId) | |||
|
|||
public Task<ExtensionEntry> LoadExtensionAsync(IExtensionInfo extensionInfo) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically this is not doing a Load, it's doing a Get. A Get should optionally do a Load, and cache it (memoize), and prevent concurrency. The Load might be private and not care about concurrent calls.
} | ||
|
||
public async Task<IEnumerable<FeatureEntry>> LoadFeaturesAsync() | ||
public Task<IEnumerable<FeatureEntry>> LoadFeaturesAsync(string[] featureIdsToLoad) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be called GetFeaturesAsync()
.
@@ -44,8 +44,7 @@ public class ModularTenantRouterMiddleware | |||
// Because IIS or another middleware might have already set it, we just append the tenant prefix value. | |||
if (!string.IsNullOrEmpty(shellSettings.RequestUrlPrefix)) | |||
{ | |||
string requestPrefix = "/" + shellSettings.RequestUrlPrefix; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's wrong here?
/// Initalizes a new <see cref="AssemblyPart"/> instance. | ||
/// </summary> | ||
/// <param name="assembly"></param> | ||
public SatalliteApplicationPart(IHttpContextAccessor httpContextAccessor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the ShellBluePrint directly, or your don't know when it will be resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cant, the part gets initialized within a Startup class that doesnt have access to the ShellBlueprint.
/// <summary> | ||
/// An <see cref="ApplicationPart"/> backed by an <see cref="Assembly"/>. | ||
/// </summary> | ||
public class SatalliteApplicationPart : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
"Microsoft.AspNetCore.Mvc.ViewFeatures" | ||
}; | ||
|
||
private bool isInitialized = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Private members before constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
} | ||
|
||
var shellAssemblies = ShellBlueprint.Dependencies.Keys.Select(type => type.GetTypeInfo().Assembly).ToList(); | ||
var discoveredAssemblies = DefaultAssemblyDiscoveryProvider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this code just says that if an assemble has a reference on any of the named assembly from the list, then they are a candidate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the code was adapted from the MVC project.
* Removing dynamic compilation * Removing ModularAssemblyProvider * Work on staticmodular (#580) * Work on staticmodular. * Usings * Moving tag helpers to modules * Caching ShellFeatureApplicationPart ReferencePaths * Resolving only ShellBlueprint types for application parts * Fixing views for precompilation * Sharing ICompilerCache across tenants * Supporting precompiled views * Removing dynamic shape tag helpers * Some cleanups. * Cleanup SharedCompilerCacheProvider. * Formatting
Just for infos, after all the updates i also had an issue due to bg tasks running during a fresh setup. If you get the same, see here #583 how i fixed it on my side. Have a good harvest. |
What!!! @jtkech you not gunna be there?
…Sent from my iPhone
On 18 Feb 2017, at 17:38, jtkech ***@***.***> wrote:
@Jetski5822
Just for infos, after all the updates i also had an issue due to bg tasks running during a fresh setup. If you get the same, see here #583 how i fixed it on my side.
Have a good harvest.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.
|
No, sorry. Promised, next year ;) |
* Fixes building and setup. * Fix the workaround. * A better fix i think for the setup issue.
@sebastienros Can we now merge this? |
I'm ok with the current implementation, i was too polarized on assemblies deps and types and then not on those related to shell features. So, here only 2 remarks.
That said, if it is still possible to have some missing types as we seen with shape helpers, i have another suggestion which maybe will provide a more complete list of types (without custom code). The strategy is to 1st grab all mvc types by using the mvc Here, how app mvc types could be grabbed and cached. Then the same list is used by all tenants.
Then, for a given tenant, something like this.
I will do a PR just as a suggestion. I hope your harvest presentation was great and that there will be a video. |
# Conflicts: # src/Orchard.Environment.Shell.Abstractions/Builders/Extensions/ServiceProviderExtensions.cs
ASP.Net Modules
Modules provides a mechinism to have a self contained modular system where you can opt in to a specific application framework and not have the design of you application be dictated to by such.
This directly allows you to decouple the Hosting application from the independent modules. So no more redeploying the whole application for one simple change, instead you can just deploy a single module and recycle you application.
Getting started
First, create a brand new web application.
Within this new application we are initially going to focus on two files,
project.json
andStartup.cs
. If you dont have etiher of these... start again!Okay so first lets open up
project.json
.Within the ConfigureServices method add these lines
Next, at the end of the Configure method, add these lines
Thats it. Erm, wait, what? Okay so right now you must be thinking, well what the hell does this do? good question.
AddModuleServices will add the container middleware to you application pipeline, this means in short that It will look in to a folder called Modules within you application for all folders that contain the mainfest file
Module.txt
. IF you looked on the file system it would look like this:Once it has found that manifest file, and said file is valid, it will then look for all classes that implement
IStartup
, instansiate them and then call the methods on here. An example of one is using the abstract class off of IStartupBy doing this you allow your modules to be self contained, completely decoupled from the Hosting applicaiton.
Add Extra Locations
By default module discovery is linked to the
Modules
folder. This however can be extended.Within the
Startup.cs
file in your host, within theConfigureServices
method, addAdd Extra Manifest filenames
By default the module manifest file is
Module.txt
. This however can be extended.Within the
Startup.cs
file in your host, within theConfigureServices
method, addAdditional framework
You can add your favourite application framework to the pipeline, easily. The below implementations are designed to work side by side, so if you want Asp.Net Mvc and Nancy within your pipeline, just add both.
The modular framework wrappers below are designed to work directly with the modular application framework, so avoid just adding the raw framework and expect it to just work.
Asp.Net Mvc
Within your hosting application add a reference to
Microsoft.AspNetCore.Mvc.Modules
Next, within
Startup.cs
modify the methodAddModuleServices
to look like thisThats it, done. Asp.Net Mvc is now part of your pipeline
NancyFx
Within your hosting application add a reference to
Microsoft.AspNetCore.Nancy.Modules
Next, within
Startup.cs
modify the methodConfigure
to look like thisThats it, done. NancyFx is now part of your pipeline. What this means is that Nancy modules will be automatically discovered.
Change Notes for Orchard Team
I have moved asp.net mvc out of the Host project Orchard.Cms.Web, and MVC is now a dependency of the Startup project and is also inside of the recipe.
Created a NancyFx implementation, thus allowing us to have Nancy in a module or referenced from the host, similar implementation to MVC.
Removed MVC from the core Orchard startup pipeline