Add support for updateable ActionDescriptorCollection

Fixes #5350
This commit is contained in:
Pranav K 2016-10-06 16:32:18 -07:00
parent 177fb2a6b1
commit c5a5ba1fee
17 changed files with 513 additions and 51 deletions

View File

@ -0,0 +1,21 @@
// 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 Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
/// <summary>
/// Provides a way to signal invalidation of the cached collection of <see cref="Abstractions.ActionDescriptor" /> from an
/// <see cref="IActionDescriptorCollectionProvider"/>.
/// </summary>
public interface IActionDescriptorChangeProvider
{
/// <summary>
/// Gets a <see cref="IChangeToken"/> used to signal invalidation of cached <see cref="Abstractions.ActionDescriptor"/>
/// instances.
/// </summary>
/// <returns>The <see cref="IChangeToken"/>.</returns>
IChangeToken GetChangeToken();
}
}

View File

@ -7,10 +7,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
/// Provides the currently cached collection of <see cref="Abstractions.ActionDescriptor"/>.
/// </summary>
/// <remarks>
/// The default implementation, does not update the cache, it is up to the user
/// to create or use an implementation that can update the available actions in
/// the application. The implementor is also responsible for updating the
/// <see cref="ActionDescriptorCollection.Version"/> in a thread safe way.
/// The default implementation internally caches the collection and uses
/// <see cref="IActionDescriptorChangeProvider"/> to invalidate this cache, incrementing
/// <see cref="ActionDescriptorCollection.Version"/> the collection is reconstructed.
///
/// Default consumers of this service, are aware of the version and will recache
/// data as appropriate, but rely on the version being unique.

View File

@ -1,31 +1,55 @@
// 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.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Internal
{
/// <summary>
/// Default implementation of <see cref="IActionDescriptorCollectionProvider"/>.
/// This implementation caches the results at first call, and is not responsible for updates.
/// </summary>
public class ActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
private readonly IServiceProvider _serviceProvider;
private readonly IActionDescriptorProvider[] _actionDescriptorProviders;
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
private ActionDescriptorCollection _collection;
private int _version = -1;
/// <summary>
/// Initializes a new instance of the <see cref="ActionDescriptorCollectionProvider" /> class.
/// </summary>
/// <param name="serviceProvider">The application IServiceProvider.</param>
public ActionDescriptorCollectionProvider(IServiceProvider serviceProvider)
/// <param name="actionDescriptorProviders">The sequence of <see cref="IActionDescriptorProvider"/>.</param>
/// <param name="actionDescriptorChangeProviders">The sequence of <see cref="IActionDescriptorChangeProvider"/>.</param>
public ActionDescriptorCollectionProvider(
IEnumerable<IActionDescriptorProvider> actionDescriptorProviders,
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders)
{
_serviceProvider = serviceProvider;
_actionDescriptorProviders = actionDescriptorProviders
.OrderBy(p => p.Order)
.ToArray();
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
ChangeToken.OnChange(
GetCompositeChangeToken,
UpdateCollection);
}
private IChangeToken GetCompositeChangeToken()
{
var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];
for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)
{
changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();
}
return new CompositeChangeToken(changeTokens);
}
/// <summary>
@ -37,34 +61,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
if (_collection == null)
{
_collection = GetCollection();
UpdateCollection();
}
return _collection;
}
}
private ActionDescriptorCollection GetCollection()
private void UpdateCollection()
{
var providers =
_serviceProvider.GetServices<IActionDescriptorProvider>()
.OrderBy(p => p.Order)
.ToArray();
var context = new ActionDescriptorProviderContext();
foreach (var provider in providers)
for (var i = 0; i < _actionDescriptorProviders.Length; i++)
{
provider.OnProvidersExecuting(context);
_actionDescriptorProviders[i].OnProvidersExecuting(context);
}
for (var i = providers.Length - 1; i >= 0; i--)
for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
{
providers[i].OnProvidersExecuted(context);
_actionDescriptorProviders[i].OnProvidersExecuted(context);
}
return new ActionDescriptorCollection(
new ReadOnlyCollection<ActionDescriptor>(context.Results), 0);
_collection = new ActionDescriptorCollection(
new ReadOnlyCollection<ActionDescriptor>(context.Results),
Interlocked.Increment(ref _version));
}
}
}

View File

@ -0,0 +1,85 @@
// 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.Collections.Generic;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Internal
{
internal class CompositeChangeToken : IChangeToken
{
public CompositeChangeToken(IList<IChangeToken> changeTokens)
{
if (changeTokens == null)
{
throw new ArgumentNullException(nameof(changeTokens));
}
ChangeTokens = changeTokens;
}
public IList<IChangeToken> ChangeTokens { get; }
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
var disposables = new IDisposable[ChangeTokens.Count];
for (var i = 0; i < ChangeTokens.Count; i++)
{
var disposable = ChangeTokens[i].RegisterChangeCallback(callback, state);
disposables[i] = disposable;
}
return new CompositeDisposable(disposables);
}
public bool HasChanged
{
get
{
for (var i = 0; i < ChangeTokens.Count; i++)
{
if (ChangeTokens[i].HasChanged)
{
return true;
}
}
return false;
}
}
public bool ActiveChangeCallbacks
{
get
{
for (var i = 0; i < ChangeTokens.Count; i++)
{
if (ChangeTokens[i].ActiveChangeCallbacks)
{
return true;
}
}
return false;
}
}
private class CompositeDisposable : IDisposable
{
private readonly IDisposable[] _disposables;
public CompositeDisposable(IDisposable[] disposables)
{
_disposables = disposables;
}
public void Dispose()
{
for (var i = 0; i < _disposables.Length; i++)
{
_disposables[i].Dispose();
}
}
}
}
}

View File

@ -657,17 +657,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
private ControllerActionDescriptor InvokeActionSelector(RouteContext context)
{
var actionDescriptorProvider = GetActionDescriptorProvider();
var serviceContainer = new ServiceCollection();
var list = new List<IActionDescriptorProvider>()
{
actionDescriptorProvider,
};
serviceContainer.AddSingleton(typeof(IEnumerable<IActionDescriptorProvider>), list);
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
serviceContainer.BuildServiceProvider());
new[] { actionDescriptorProvider },
Enumerable.Empty<IActionDescriptorChangeProvider>());
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionDescriptorCollectionProvider);
var actionConstraintProviders = new[]
@ -827,8 +819,9 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
private static ActionConstraintCache GetActionConstraintCache(IActionConstraintProvider[] actionConstraintProviders = null)
{
var services = new ServiceCollection().BuildServiceProvider();
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
var descriptorProvider = new ActionDescriptorCollectionProvider(
Enumerable.Empty<IActionDescriptorProvider>(),
Enumerable.Empty<IActionDescriptorChangeProvider>());
return new ActionConstraintCache(descriptorProvider, actionConstraintProviders.AsEnumerable() ?? new List<IActionConstraintProvider>());
}

View File

@ -2,9 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@ -156,8 +159,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static ActionConstraintCache CreateCache(params IActionConstraintProvider[] providers)
{
var services = CreateServices();
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
var descriptorProvider = new ActionDescriptorCollectionProvider(
Enumerable.Empty<IActionDescriptorProvider>(),
Enumerable.Empty<IActionDescriptorChangeProvider>());
return new ActionConstraintCache(descriptorProvider, providers);
}
}

View File

@ -0,0 +1,206 @@
// 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.Linq;
using System.Threading;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ActionDescriptorCollectionProviderTest
{
[Fact]
public void ActionDescriptors_ReadsDescriptorsFromActionDescriptorProviders()
{
// Arrange
var expected1 = new ActionDescriptor();
var actionDescriptorProvider1 = GetActionDescriptorProvider(expected1);
var expected2 = new ActionDescriptor();
var expected3 = new ActionDescriptor();
var actionDescriptorProvider2 = GetActionDescriptorProvider(expected2, expected3);
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
new[] { actionDescriptorProvider1, actionDescriptorProvider2 },
Enumerable.Empty<IActionDescriptorChangeProvider>());
// Act
var collection = actionDescriptorCollectionProvider.ActionDescriptors;
// Assert
Assert.Equal(0, collection.Version);
Assert.Collection(
collection.Items,
descriptor => Assert.Same(expected1, descriptor),
descriptor => Assert.Same(expected2, descriptor),
descriptor => Assert.Same(expected3, descriptor));
}
[Fact]
public void ActionDescriptors_CachesValuesByDefault()
{
// Arrange
var actionDescriptorProvider = GetActionDescriptorProvider(new ActionDescriptor());
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
new[] { actionDescriptorProvider },
Enumerable.Empty<IActionDescriptorChangeProvider>());
// Act - 1
var collection1 = actionDescriptorCollectionProvider.ActionDescriptors;
// Assert - 1
Assert.Equal(0, collection1.Version);
// Act - 2
var collection2 = actionDescriptorCollectionProvider.ActionDescriptors;
// Assert - 2
Assert.Same(collection1, collection2);
Mock.Get(actionDescriptorProvider)
.Verify(v => v.OnProvidersExecuting(It.IsAny<ActionDescriptorProviderContext>()), Times.Once());
}
[Fact]
public void ActionDescriptors_UpdateWhenChangeTokenProviderChanges()
{
// Arrange
var actionDescriptorProvider = new Mock<IActionDescriptorProvider>();
var expected1 = new ActionDescriptor();
var expected2 = new ActionDescriptor();
var invocations = 0;
actionDescriptorProvider
.Setup(p => p.OnProvidersExecuting(It.IsAny<ActionDescriptorProviderContext>()))
.Callback((ActionDescriptorProviderContext context) =>
{
if (invocations == 0)
{
context.Results.Add(expected1);
}
else
{
context.Results.Add(expected2);
}
invocations++;
});
var changeProvider = new TestChangeProvider();
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
new[] { actionDescriptorProvider.Object },
new[] { changeProvider });
// Act - 1
var collection1 = actionDescriptorCollectionProvider.ActionDescriptors;
// Assert - 1
Assert.Equal(0, collection1.Version);
Assert.Collection(collection1.Items,
item => Assert.Same(expected1, item));
// Act - 2
changeProvider.TokenSource.Cancel();
var collection2 = actionDescriptorCollectionProvider.ActionDescriptors;
// Assert - 2
Assert.NotSame(collection1, collection2);
Assert.Equal(1, collection2.Version);
Assert.Collection(collection2.Items,
item => Assert.Same(expected2, item));
}
[Fact]
public void ActionDescriptors_SubscribesToNewChangeNotificationsAfterInvalidating()
{
// Arrange
var actionDescriptorProvider = new Mock<IActionDescriptorProvider>();
var expected1 = new ActionDescriptor();
var expected2 = new ActionDescriptor();
var expected3 = new ActionDescriptor();
var invocations = 0;
actionDescriptorProvider
.Setup(p => p.OnProvidersExecuting(It.IsAny<ActionDescriptorProviderContext>()))
.Callback((ActionDescriptorProviderContext context) =>
{
if (invocations == 0)
{
context.Results.Add(expected1);
}
else if (invocations == 1)
{
context.Results.Add(expected2);
}
else
{
context.Results.Add(expected3);
}
invocations++;
});
var changeProvider = new TestChangeProvider();
var actionDescriptorCollectionProvider = new ActionDescriptorCollectionProvider(
new[] { actionDescriptorProvider.Object },
new[] { changeProvider });
// Act - 1
var collection1 = actionDescriptorCollectionProvider.ActionDescriptors;
// Assert - 1
Assert.Equal(0, collection1.Version);
Assert.Collection(collection1.Items,
item => Assert.Same(expected1, item));
// Act - 2
changeProvider.TokenSource.Cancel();
var collection2 = actionDescriptorCollectionProvider.ActionDescriptors;
// Assert - 2
Assert.NotSame(collection1, collection2);
Assert.Equal(1, collection2.Version);
Assert.Collection(collection2.Items,
item => Assert.Same(expected2, item));
// Act - 3
changeProvider.TokenSource.Cancel();
var collection3 = actionDescriptorCollectionProvider.ActionDescriptors;
// Assert - 3
Assert.NotSame(collection2, collection3);
Assert.Equal(2, collection3.Version);
Assert.Collection(collection3.Items,
item => Assert.Same(expected3, item));
}
private static IActionDescriptorProvider GetActionDescriptorProvider(params ActionDescriptor[] values)
{
var actionDescriptorProvider = new Mock<IActionDescriptorProvider>();
actionDescriptorProvider
.Setup(p => p.OnProvidersExecuting(It.IsAny<ActionDescriptorProviderContext>()))
.Callback((ActionDescriptorProviderContext context) =>
{
foreach (var value in values)
{
context.Results.Add(value);
}
});
return actionDescriptorProvider.Object;
}
private class TestChangeProvider : IActionDescriptorChangeProvider
{
public CancellationTokenSource TokenSource { get; private set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}
}

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
@ -3300,8 +3301,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
{
var services = new ServiceCollection().BuildServiceProvider();
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
var descriptorProvider = new ActionDescriptorCollectionProvider(
Enumerable.Empty<IActionDescriptorProvider>(),
Enumerable.Empty<IActionDescriptorChangeProvider>());
return new ControllerActionInvokerCache(descriptorProvider, filterProviders.AsEnumerable() ?? new List<IFilterProvider>());
}

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@ -400,8 +401,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static ControllerActionInvokerCache CreateFilterCache(IFilterProvider[] filterProviders = null)
{
var services = new ServiceCollection().BuildServiceProvider();
var descriptorProvider = new ActionDescriptorCollectionProvider(services);
var descriptorProvider = new ActionDescriptorCollectionProvider(
Enumerable.Empty<IActionDescriptorProvider>(),
Enumerable.Empty<IActionDescriptorChangeProvider>());
return new ControllerActionInvokerCache(
descriptorProvider,
filterProviders.AsEnumerable() ?? new List<IFilterProvider>());

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
@ -172,14 +173,14 @@ namespace Microsoft.AspNetCore.Mvc.Routing
.Setup(p => p.OnProvidersExecuted(It.IsAny<ActionDescriptorProviderContext>()))
.Verifiable();
var descriptorCollectionProvider = new ActionDescriptorCollectionProvider(
new[] { actionProvider.Object },
Enumerable.Empty<IActionDescriptorChangeProvider>());
var context = new Mock<HttpContext>();
context.Setup(o => o.RequestServices
.GetService(typeof(IEnumerable<IActionDescriptorProvider>)))
.Returns(new[] { actionProvider.Object });
context.Setup(o => o.RequestServices
.GetService(typeof(IActionDescriptorCollectionProvider)))
.Returns(new ActionDescriptorCollectionProvider(context.Object.RequestServices));
.GetService(typeof(IActionDescriptorCollectionProvider)))
.Returns(descriptorCollectionProvider);
return context.Object;
}

View File

@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Testing.xunit;
using Newtonsoft.Json;
using Xunit;
using Microsoft.AspNetCore.Http;
using System.Net;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
@ -976,6 +978,40 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(typeof(string).FullName, feedback.Type);
}
[Fact]
public async Task ApiExplorer_Updates_WhenActionDescriptorCollectionIsUpdated()
{
// Act - 1
var body = await Client.GetStringAsync("ApiExplorerReload/Index");
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert - 1
var description = Assert.Single(result);
Assert.Empty(description.ParameterDescriptions);
Assert.Equal("ApiExplorerReload/Index", description.RelativePath);
// Act - 2
var response = await Client.GetAsync("ApiExplorerReload/Reload");
// Assert - 2
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Act - 3
response = await Client.GetAsync("ApiExplorerReload/Index");
// Assert - 3
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
// Act - 4
body = await Client.GetStringAsync("ApiExplorerReload/NewIndex");
result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert - 4
description = Assert.Single(result);
Assert.Empty(description.ParameterDescriptions);
Assert.Equal("ApiExplorerReload/NewIndex", description.RelativePath);
}
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
{
return apiResponseType.ResponseFormats

View File

@ -0,0 +1,28 @@
// 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.Threading;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace ApiExplorerWebSite
{
public class ActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
private ActionDescriptorChangeProvider()
{
}
public static ActionDescriptorChangeProvider Instance { get; } = new ActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}

View File

@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
namespace ApiExplorerWebSite
@ -26,6 +28,12 @@ namespace ApiExplorerWebSite
public void OnResourceExecuting(ResourceExecutingContext context)
{
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
if (controllerActionDescriptor != null && controllerActionDescriptor.MethodInfo.IsDefined(typeof(PassThruAttribute)))
{
return;
}
var descriptions = new List<ApiExplorerData>();
foreach (var group in _descriptionProvider.ApiDescriptionGroups.Items)
{
@ -43,7 +51,6 @@ namespace ApiExplorerWebSite
public void OnResourceExecuted(ResourceExecutedContext context)
{
throw new NotImplementedException();
}
private ApiExplorerData CreateSerializableData(ApiDescription description)

View File

@ -0,0 +1,45 @@
// 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 Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace ApiExplorerWebSite
{
[Route("ApiExplorerReload")]
public class ApiExplorerReloadableController : Controller
{
[ApiExplorerRouteChangeConvention]
[Route("Index")]
public string Index() => "Hello world";
[Route("Reload")]
[PassThru]
public IActionResult Reload()
{
ActionDescriptorChangeProvider.Instance.HasChanged = true;
ActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
return Ok();
}
public class ApiExplorerRouteChangeConventionAttribute : Attribute, IActionModelConvention
{
public void Apply(ActionModel action)
{
if (ActionDescriptorChangeProvider.Instance.HasChanged)
{
action.ActionName = "NewIndex";
action.Selectors.Clear();
action.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "NewIndex"
}
});
}
}
}
}
}

View File

@ -0,0 +1,12 @@
// 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;
namespace ApiExplorerWebSite
{
[AttributeUsage(AttributeTargets.Method)]
public class PassThruAttribute : Attribute
{
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -33,9 +34,10 @@ namespace ApiExplorerWebSite
});
services.AddSingleton<ApiExplorerDataFilter>();
services.AddSingleton<IActionDescriptorChangeProvider>(ActionDescriptorChangeProvider.Instance);
services.AddSingleton(ActionDescriptorChangeProvider.Instance);
}
public void Configure(IApplicationBuilder app)
{
app.UseCultureReplacer();

View File

@ -4,7 +4,6 @@
using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace ApplicationModelWebSite
{
public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention