-
Notifications
You must be signed in to change notification settings - Fork 10k
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
Clear some caches from MVC on hot reload event #33386
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,13 +12,15 @@ | |
using Microsoft.AspNetCore.Mvc.ApplicationParts; | ||
using Microsoft.AspNetCore.Mvc.Controllers; | ||
using Microsoft.AspNetCore.Mvc.Filters; | ||
using Microsoft.AspNetCore.Mvc.HotReload; | ||
using Microsoft.AspNetCore.Mvc.Infrastructure; | ||
using Microsoft.AspNetCore.Mvc.ModelBinding; | ||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; | ||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; | ||
using Microsoft.AspNetCore.Mvc.Routing; | ||
using Microsoft.AspNetCore.Routing; | ||
using Microsoft.Extensions.DependencyInjection.Extensions; | ||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.Extensions.DependencyInjection | ||
|
@@ -51,13 +53,20 @@ public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services) | |
throw new ArgumentNullException(nameof(services)); | ||
} | ||
|
||
var partManager = GetApplicationPartManager(services); | ||
var environment = GetServiceFromCollection<IWebHostEnvironment>(services); | ||
var partManager = GetApplicationPartManager(services, environment); | ||
services.TryAddSingleton(partManager); | ||
|
||
ConfigureDefaultFeatureProviders(partManager); | ||
ConfigureDefaultServices(services); | ||
AddMvcCoreServices(services); | ||
|
||
if (environment?.IsDevelopment() ?? false) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI @Tratcher. I hear this makes you sad. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the workaround for someone using a custom environment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we investigate using some other marker here, e.g. a specific env var that's set by the Hot Reload manager? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes please. #28937 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blazor WASM currently uses Debugging.IsSupported to determine this - https://github.com/dotnet/aspnetcore/blob/main/src/Components/Shared/src/HotReloadFeature.cs#L6-L16. But the experience is nicer there. The trimmer yanks out all of that code during publish, so it entirely no-ops. We don't have the benefit here. Also, aspnetcore's deployment is slightly centered around environments rather than build / publish. @stephentoub, do you have suggestions? Would dotnet/runtime#51159 solve this problem? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With dotnet-watch and the Ctrl-F5 scenarios in VS, a debugger isn't attached (and once a hot reload delta is applied, the runtime prevents attaching a debugger). The runtime currently requires setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think this would be the best way though I would say something ENC for a module is enabled on launch by the debugger on module load callbacks. These env vars are only needed if you want ENC when the debugger attaches. If we want this global property part of the runtime, it probably should be renamed to something without "HotReload". We have been avoiding that term in the public APIs (see AssemblyExtensions.ApplyUpdate or AssemblyExtensions.GetApplyUpdateCapabilities). The runtime itself couldn't do any better with some kind of QCall because it doesn't have a concept of a global "is ENC'able or hotreloadable" flag. It uses these env vars (or a call to a debugging API) to create special ENC per-module data structures. The runtime could return that an assembly/module is ENC'able/hotreloadable but something global is needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a minor difference in behavior with Mono: right now we always require There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But actually, checking the environment variable is a necessary but not sufficient condition for whether we actually have hot reload at runtime - with the ongoing componentization of Mono, workloads can decide whether to bundle hot reload support in some configurations or builds of the user's app. So framework or library authors really need a separate feature switch if they want to affect runtime behavior. (I think we're all aware of the open issue for that, but just in case it's dotnet/runtime#51159) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I filed #33477 to track updating this code once we've had a chance to figure out what the right semantics should be for this flag. Given solving this is largely a hygiene issue, I'm not going to block on resolving if we're able to get support for custom attributes working end-to-end in preview6. |
||
{ | ||
services.TryAddEnumerable( | ||
ServiceDescriptor.Singleton<IActionDescriptorChangeProvider, HotReloadService>()); | ||
} | ||
|
||
var builder = new MvcCoreBuilder(services, partManager); | ||
|
||
return builder; | ||
|
@@ -71,14 +80,13 @@ private static void ConfigureDefaultFeatureProviders(ApplicationPartManager mana | |
} | ||
} | ||
|
||
private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services) | ||
private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services, IWebHostEnvironment? environment) | ||
{ | ||
var manager = GetServiceFromCollection<ApplicationPartManager>(services); | ||
if (manager == null) | ||
{ | ||
manager = new ApplicationPartManager(); | ||
|
||
var environment = GetServiceFromCollection<IWebHostEnvironment>(services); | ||
var entryAssemblyName = environment?.ApplicationName; | ||
if (string.IsNullOrEmpty(entryAssemblyName)) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Reflection.Metadata; | ||
using System.Threading; | ||
using Microsoft.AspNetCore.Mvc.Controllers; | ||
using Microsoft.AspNetCore.Mvc.Infrastructure; | ||
using Microsoft.AspNetCore.Mvc.ModelBinding; | ||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
[assembly: MetadataUpdateHandler(typeof(Microsoft.AspNetCore.Mvc.HotReload.HotReloadService))] | ||
|
||
namespace Microsoft.AspNetCore.Mvc.HotReload | ||
{ | ||
internal sealed class HotReloadService : IActionDescriptorChangeProvider, IDisposable | ||
{ | ||
private readonly DefaultModelMetadataProvider? _modelMetadataProvider; | ||
private readonly DefaultControllerPropertyActivator? _controllerPropertyActivator; | ||
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These two were the only two I found that were keyed off Type (aside from PropertyHelper). The rest are keyed off things like action descriptors or ModelMetadata - which should auto-invalidate. There is the slight issue that those caches will grow indefinitely, but we could take them on if there's a legitimate issue. |
||
private CancellationTokenSource _tokenSource = new(); | ||
|
||
public HotReloadService( | ||
IModelMetadataProvider modelMetadataProvider, | ||
IControllerPropertyActivator controllerPropertyActivator) | ||
{ | ||
ClearCacheEvent += NotifyClearCache; | ||
if (modelMetadataProvider.GetType() == typeof(DefaultModelMetadataProvider)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type isn't sealed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not. I've seen a couple users implement it. It feels weird to delete the caches under them. |
||
{ | ||
_modelMetadataProvider = (DefaultModelMetadataProvider)modelMetadataProvider; | ||
} | ||
|
||
if (controllerPropertyActivator is DefaultControllerPropertyActivator defaultControllerPropertyActivator) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type is sealed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. internal sealed. An alternative to doing this is to put the handler in the type itself, but I wanted to try and contain all of the clearing bits in one place particularly because there's indirection involved turning a static method call in to clearing per-instance caches. |
||
{ | ||
_controllerPropertyActivator = defaultControllerPropertyActivator; | ||
} | ||
} | ||
|
||
public static event Action? ClearCacheEvent; | ||
|
||
public static void ClearCache(Type[]? _) | ||
{ | ||
ClearCacheEvent?.Invoke(); | ||
} | ||
|
||
IChangeToken IActionDescriptorChangeProvider.GetChangeToken() => new CancellationChangeToken(_tokenSource.Token); | ||
|
||
private void NotifyClearCache() | ||
{ | ||
// Trigger the ActionDescriptorChangeProvider | ||
var current = Interlocked.Exchange(ref _tokenSource, new CancellationTokenSource()); | ||
current.Cancel(); | ||
|
||
// Clear individual caches | ||
_modelMetadataProvider?.ClearCache(); | ||
_controllerPropertyActivator?.ClearCache(); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
ClearCacheEvent -= NotifyClearCache; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata | |
/// </summary> | ||
public class DefaultModelMetadataProvider : ModelMetadataProvider | ||
{ | ||
private readonly ModelMetadataCache _modelMetadataCache = new ModelMetadataCache(); | ||
private readonly ConcurrentDictionary<ModelMetadataIdentity, ModelMetadataCacheEntry> _modelMetadataCache = new(); | ||
private readonly Func<ModelMetadataIdentity, ModelMetadataCacheEntry> _cacheEntryFactory; | ||
private readonly ModelMetadataCacheEntry _metadataCacheEntryForObjectType; | ||
|
||
|
@@ -71,6 +71,8 @@ private DefaultModelMetadataProvider( | |
/// <value>Same as <see cref="MvcOptions.ModelBindingMessageProvider"/> in all production scenarios.</value> | ||
protected DefaultModelBindingMessageProvider ModelBindingMessageProvider { get; } | ||
|
||
internal void ClearCache() => _modelMetadataCache.Clear(); | ||
|
||
/// <inheritdoc /> | ||
public override IEnumerable<ModelMetadata> GetMetadataForProperties(Type modelType) | ||
{ | ||
|
@@ -442,10 +444,6 @@ protected virtual DefaultMetadataDetails CreateParameterDetails(ModelMetadataIde | |
ModelAttributes.GetAttributesForParameter(key.ParameterInfo!, key.ModelType)); | ||
} | ||
|
||
private class ModelMetadataCache : ConcurrentDictionary<ModelMetadataIdentity, ModelMetadataCacheEntry> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't even want to ask why this was there |
||
{ | ||
} | ||
|
||
private readonly struct ModelMetadataCacheEntry | ||
{ | ||
public ModelMetadataCacheEntry(ModelMetadata metadata, DefaultMetadataDetails details) | ||
|
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
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 is just silly.