Skip to content

Commit 002260d

Browse files
authored
Add generic variants of MVC attributes (#47075)
* Add generic variants of MVC attributes * Throw descriptive exception if multiple attributes applied * Add constraint to ModelBinderAttribute
1 parent ec9a2d8 commit 002260d

17 files changed

+200
-17
lines changed

src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2456,7 +2456,7 @@ private void AcceptsProduct_Default([ModelBinder] Product product)
24562456
}
24572457

24582458
// This will show up as source = unknown
2459-
private void AcceptsProduct_Custom([ModelBinder(BinderType = typeof(BodyModelBinder))] Product product)
2459+
private void AcceptsProduct_Custom([ModelBinder<BodyModelBinder>] Product product)
24602460
{
24612461
}
24622462

@@ -2556,7 +2556,7 @@ private void FromModelBinding(int id)
25562556
{
25572557
}
25582558

2559-
private void FromCustom([ModelBinder(typeof(BodyModelBinder))] int id)
2559+
private void FromCustom([ModelBinder<BodyModelBinder>] int id)
25602560
{
25612561
}
25622562

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Mvc;
5+
6+
/// <inheritdoc />
7+
/// <typeparam name="T">A type which configures a middleware pipeline.</typeparam>
8+
public class MiddlewareFilterAttribute<T> : MiddlewareFilterAttribute
9+
{
10+
/// <summary>
11+
/// Instantiates a new instance of <see cref="MiddlewareFilterAttribute"/>.
12+
/// </summary>
13+
public MiddlewareFilterAttribute() : base(typeof(T)) { }
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Mvc.ModelBinding;
5+
6+
namespace Microsoft.AspNetCore.Mvc;
7+
8+
/// <inheritdoc />
9+
/// <typeparam name="TBinder">A <see cref="Type"/> which implements <see cref="IModelBinder"/>.</typeparam>
10+
/// <remarks>
11+
/// This is a derived generic variant of the <see cref="ModelBinderAttribute"/>.
12+
/// Ensure that only one instance of either attribute is provided on the target.
13+
/// </remarks>
14+
public class ModelBinderAttribute<TBinder> : ModelBinderAttribute where TBinder : IModelBinder
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of <see cref="ModelBinderAttribute"/>.
18+
/// </summary>
19+
/// <remarks>
20+
/// Subclass this attribute and set <see cref="BindingSource"/> if <see cref="BindingSource.Custom"/> is not
21+
/// correct for the specified type parameter.
22+
/// </remarks>
23+
public ModelBinderAttribute() : base(typeof(TBinder)) { }
24+
}

src/Mvc/Mvc.Core/src/ModelBinding/Metadata/ModelAttributes.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,25 @@ public static ModelAttributes GetAttributesForParameter(ParameterInfo parameterI
214214

215215
private static Type? GetMetadataType(Type type)
216216
{
217-
return type.GetCustomAttribute<ModelMetadataTypeAttribute>()?.MetadataType;
217+
// GetCustomAttribute will examine the members inheritance chain
218+
// for attributes of a particular type by default. Meaning that
219+
// in the following scenario, the `ModelMetadataType` attribute on
220+
// both the derived _and_ base class will be returned.
221+
// [ModelMetadataType<BaseModel>]
222+
// private class BaseViewModel { }
223+
// [ModelMetadataType<DerivedModel>]
224+
// private class DerivedViewModel : BaseViewModel { }
225+
// To avoid this, we call `GetCustomAttributes` directly
226+
// to avoid examining the inheritance hierarchy.
227+
// See https://source.dot.net/#System.Private.CoreLib/src/System/Attribute.CoreCLR.cs,677
228+
var modelMetadataTypeAttributes = type.GetCustomAttributes<ModelMetadataTypeAttribute>(inherit: false);
229+
try
230+
{
231+
return modelMetadataTypeAttributes?.SingleOrDefault()?.MetadataType;
232+
}
233+
catch (InvalidOperationException e)
234+
{
235+
throw new InvalidOperationException("Only one ModelMetadataType attribute is permitted per type.", e);
236+
}
218237
}
219238
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Mvc;
5+
6+
/// <inheritdoc />
7+
/// <typeparam name="T">The type of metadata class that is associated with a data model class.</typeparam>
8+
/// <remarks>
9+
/// This is a derived generic variant of the <see cref="ModelMetadataTypeAttribute"/>
10+
/// which does not allow multiple instances on a single target.
11+
/// Ensure that only one instance of either attribute is provided on the target.
12+
/// </remarks>
13+
public class ModelMetadataTypeAttribute<T> : ModelMetadataTypeAttribute
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="ModelMetadataTypeAttribute" /> class.
17+
/// </summary>
18+
public ModelMetadataTypeAttribute() : base(typeof(T)) { }
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Mvc;
5+
6+
/// <inheritdoc />
7+
/// <typeparam name="T">The <see cref="Type"/> of object that is going to be written in the response.</typeparam>
8+
/// <remarks>
9+
/// This is a derived generic variant of the <see cref="ProducesAttribute"/>.
10+
/// Ensure that only one instance of either attribute is provided on the target.
11+
/// </remarks>
12+
public class ProducesAttribute<T> : ProducesAttribute
13+
{
14+
/// <summary>
15+
/// Initializes an instance of <see cref="ProducesAttribute"/>.
16+
/// </summary>
17+
public ProducesAttribute() : base(typeof(T)) { }
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Mvc;
5+
6+
/// <inheritdoc />
7+
/// <typeparam name="T">The <see cref="Type"/> of object that is going to be written in the response.</typeparam>
8+
public class ProducesResponseTypeAttribute<T> : ProducesResponseTypeAttribute
9+
{
10+
/// <summary>
11+
/// Initializes an instance of <see cref="ProducesResponseTypeAttribute"/>.
12+
/// </summary>
13+
/// <param name="statusCode">The HTTP response status code.</param>
14+
public ProducesResponseTypeAttribute(int statusCode) : base(typeof(T), statusCode) { }
15+
16+
/// <summary>
17+
/// Initializes an instance of <see cref="ProducesResponseTypeAttribute"/>.
18+
/// </summary>
19+
/// <param name="statusCode">The HTTP response status code.</param>
20+
/// <param name="contentType">The content type associated with the response.</param>
21+
/// <param name="additionalContentTypes">Additional content types supported by the response.</param>
22+
public ProducesResponseTypeAttribute(int statusCode, string contentType, params string[] additionalContentTypes)
23+
: base(typeof(T), statusCode, contentType, additionalContentTypes) { }
24+
}

src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
#nullable enable
22
*REMOVED*static Microsoft.AspNetCore.Routing.ControllerLinkGeneratorExtensions.GetUriByAction(this Microsoft.AspNetCore.Routing.LinkGenerator! generator, string! action, string! controller, object? values, string? scheme, Microsoft.AspNetCore.Http.HostString host, Microsoft.AspNetCore.Http.PathString pathBase = default(Microsoft.AspNetCore.Http.PathString), Microsoft.AspNetCore.Http.FragmentString fragment = default(Microsoft.AspNetCore.Http.FragmentString), Microsoft.AspNetCore.Routing.LinkOptions? options = null) -> string?
3+
Microsoft.AspNetCore.Mvc.MiddlewareFilterAttribute<T>
4+
Microsoft.AspNetCore.Mvc.MiddlewareFilterAttribute<T>.MiddlewareFilterAttribute() -> void
5+
Microsoft.AspNetCore.Mvc.ModelBinderAttribute<TBinder>
6+
Microsoft.AspNetCore.Mvc.ModelBinderAttribute<TBinder>.ModelBinderAttribute() -> void
7+
Microsoft.AspNetCore.Mvc.ModelMetadataTypeAttribute<T>
8+
Microsoft.AspNetCore.Mvc.ModelMetadataTypeAttribute<T>.ModelMetadataTypeAttribute() -> void
9+
Microsoft.AspNetCore.Mvc.ProducesAttribute<T>
10+
Microsoft.AspNetCore.Mvc.ProducesAttribute<T>.ProducesAttribute() -> void
11+
Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute<T>
12+
Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute<T>.ProducesResponseTypeAttribute(int statusCode) -> void
13+
Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute<T>.ProducesResponseTypeAttribute(int statusCode, string! contentType, params string![]! additionalContentTypes) -> void
14+
Microsoft.AspNetCore.Mvc.ServiceFilterAttribute<TFilter>
15+
Microsoft.AspNetCore.Mvc.ServiceFilterAttribute<TFilter>.ServiceFilterAttribute() -> void
16+
Microsoft.AspNetCore.Mvc.TypeFilterAttribute<TFilter>
17+
Microsoft.AspNetCore.Mvc.TypeFilterAttribute<TFilter>.TypeFilterAttribute() -> void
318
Microsoft.AspNetCore.Mvc.ValidationProblemDetails.Errors.set -> void
419
static Microsoft.AspNetCore.Routing.ControllerLinkGeneratorExtensions.GetUriByAction(this Microsoft.AspNetCore.Routing.LinkGenerator! generator, string! action, string! controller, object? values, string! scheme, Microsoft.AspNetCore.Http.HostString host, Microsoft.AspNetCore.Http.PathString pathBase = default(Microsoft.AspNetCore.Http.PathString), Microsoft.AspNetCore.Http.FragmentString fragment = default(Microsoft.AspNetCore.Http.FragmentString), Microsoft.AspNetCore.Routing.LinkOptions? options = null) -> string?
520
Microsoft.AspNetCore.Mvc.CreatedResult.CreatedResult() -> void
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using Microsoft.AspNetCore.Mvc.Filters;
6+
7+
namespace Microsoft.AspNetCore.Mvc;
8+
9+
/// <inheritdoc />
10+
/// <typeparam name="TFilter">The <see cref="Type"/> of filter to find.</typeparam>
11+
[DebuggerDisplay("ServiceFilter: Type={ServiceType} Order={Order}")]
12+
public class ServiceFilterAttribute<TFilter> : ServiceFilterAttribute where TFilter : IFilterMetadata
13+
{
14+
/// <summary>
15+
/// Instantiates a new <see cref="ServiceFilterAttribute"/> instance.
16+
/// </summary>
17+
public ServiceFilterAttribute() : base(typeof(TFilter)) { }
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Mvc.Filters;
5+
6+
namespace Microsoft.AspNetCore.Mvc;
7+
8+
/// <inheritdoc />
9+
/// <typeparam name="TFilter">The <see cref="Type"/> of filter to create.</typeparam>
10+
public class TypeFilterAttribute<TFilter> : TypeFilterAttribute where TFilter : IFilterMetadata
11+
{
12+
/// <summary>
13+
/// Instantiates a new <see cref="TypeFilterAttribute"/> instance.
14+
/// </summary>
15+
public TypeFilterAttribute() : base(typeof(TFilter)) { }
16+
}

0 commit comments

Comments
 (0)