Skip to content

Support Ad Hoc Model Bound Settings #933

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

Merged
merged 8 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions asp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Abstractions", "Abstractions", "{7B0FA6C2-47BA-4C34-90E0-B75DF44F2124}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{34A0373F-12C9-44A8-9A1C-5EEE7218C877}"
ProjectSection(SolutionItems) = preProject
src\AspNet\Directory.Build.targets = src\AspNet\Directory.Build.targets
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCore", "AspNetCore", "{EBC9F217-E8BC-4DCE-9C67-F22150959EAF}"
EndProject
Expand Down
2 changes: 1 addition & 1 deletion build/test.targets
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<FluentAssertionsVersion>6.8.0</FluentAssertionsVersion>
<MoqVersion>4.18.2</MoqVersion>
<MoqVersion>4.18.3</MoqVersion>
<XunitRunnerVersion>2.4.5</XunitRunnerVersion>
</PropertyGroup>

Expand Down
7 changes: 7 additions & 0 deletions examples/AspNet/OData/SomeOpenApiODataWebApiExample/Book.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
namespace ApiVersioning.Examples;

using Microsoft.AspNet.OData.Query;

// TODO: Model Bound settings can be performed via attributes if the
// return type is known to the API Explorer or can be explicitly done
// via one or more IModelConfiguration implementations

/// <summary>
/// Represents a book.
/// </summary>
[Filter( "author", "published" )]
public class Book
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public void Configuration( IAppBuilder builder )
.Allow( Skip | Count )
.AllowTop( 100 )
.AllowOrderBy( "title", "published" );

// applies model bound settings implicitly using an ad hoc EDM. alternatively, you can create your own
// IModelConfiguration + IODataQueryOptionsConvention for full control over what goes in the ad hoc EDM.
options.AdHocModelBuilder.ModelConfigurations.Add( new ImplicitModelBoundSettingsConvention() );
} );

configuration.EnableSwagger(
Expand Down
7 changes: 7 additions & 0 deletions examples/AspNetCore/OData/SomeODataOpenApiExample/Book.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
namespace ApiVersioning.Examples;

using Microsoft.OData.ModelBuilder;

// TODO: Model Bound settings can be performed via attributes if the
// return type is known to the API Explorer or can be explicitly done
// via one or more IModelConfiguration implementations

/// <summary>
/// Represents a book.
/// </summary>
[Filter( "author", "published" )]
public class Book
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0;net472;net452</TargetFrameworks>
<TargetFrameworks>net7.0;net452;net472</TargetFrameworks>
<RootNamespace>Asp.Versioning</RootNamespace>
</PropertyGroup>
<ItemGroup Condition=" ('$(TargetFramework)' == 'net452') OR ('$(TargetFramework)' == 'net472') ">
Expand Down
10 changes: 0 additions & 10 deletions src/AspNet/Directory.Build.targets

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.ApiExplorer;

using Asp.Versioning.Conventions;
using Asp.Versioning.Description;
using Asp.Versioning.OData;
using Microsoft.OData.Edm;
using System.Collections.Generic;
using System.Web.Http;
using System.Web.Http.Description;

internal sealed class AdHocEdmScope : IDisposable
{
private readonly IReadOnlyList<VersionedApiDescription> results;
private bool disposed;

internal AdHocEdmScope(
IReadOnlyList<VersionedApiDescription> apiDescriptions,
VersionedODataModelBuilder builder )
{
var conventions = builder.ModelConfigurations.OfType<IODataQueryOptionsConvention>().ToArray();

results = FilterResults( apiDescriptions, conventions );

if ( results.Count > 0 )
{
ApplyAdHocEdm( builder.GetEdmModels(), results );
}
}

public void Dispose()
{
if ( disposed )
{
return;
}

disposed = true;

for ( var i = 0; i < results.Count; i++ )
{
results[i].SetProperty( default( IEdmModel ) );
}
}

private static IReadOnlyList<VersionedApiDescription> FilterResults(
IReadOnlyList<VersionedApiDescription> apiDescriptions,
IReadOnlyList<IODataQueryOptionsConvention> conventions )
{
if ( conventions.Count == 0 )
{
return Array.Empty<VersionedApiDescription>();
}

var results = default( List<VersionedApiDescription> );

for ( var i = 0; i < apiDescriptions.Count; i++ )
{
var apiDescription = apiDescriptions[i];

if ( apiDescription.EdmModel() != null || !apiDescription.IsODataLike() )
{
continue;
}

results ??= new();
results.Add( apiDescription );

for ( var j = 0; j < conventions.Count; j++ )
{
conventions[j].ApplyTo( apiDescription );
}
}

return results?.ToArray() ?? Array.Empty<VersionedApiDescription>();
}

private static void ApplyAdHocEdm(
IReadOnlyList<IEdmModel> models,
IReadOnlyList<VersionedApiDescription> results )
{
for ( var i = 0; i < models.Count; i++ )
{
var model = models[i];
var version = model.GetApiVersion();

for ( var j = 0; j < results.Count; j++ )
{
var result = results[j];
var metadata = result.ActionDescriptor.GetApiVersionMetadata();

if ( metadata.IsMappedTo( version ) )
{
result.SetProperty( model );
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Asp.Versioning.ApiExplorer;
using Asp.Versioning.OData;
using Asp.Versioning.Routing;
using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Formatter;
using Microsoft.AspNet.OData.Routing;
Expand All @@ -16,6 +17,7 @@ namespace Asp.Versioning.ApiExplorer;
using Microsoft.OData.UriParser;
using System.Collections.ObjectModel;
using System.Net.Http.Formatting;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Web.Http;
using System.Web.Http.Controllers;
Expand Down Expand Up @@ -50,7 +52,11 @@ public ODataApiExplorer( HttpConfiguration configuration )
/// <param name="configuration">The current <see cref="HttpConfiguration">HTTP configuration</see>.</param>
/// <param name="options">The associated <see cref="ODataApiExplorerOptions">API explorer options</see>.</param>
public ODataApiExplorer( HttpConfiguration configuration, ODataApiExplorerOptions options )
: base( configuration, options ) => this.options = options;
: base( configuration, options )
{
this.options = options;
options.AdHocModelBuilder.OnModelCreated += MarkAsAdHoc;
}

/// <summary>
/// Gets the options associated with the API explorer.
Expand Down Expand Up @@ -172,7 +178,20 @@ protected override Collection<VersionedApiDescription> ExploreRouteControllers(
if ( route is not ODataRoute )
{
apiDescriptions = base.ExploreRouteControllers( controllerMappings, route, apiVersion );
return ExploreQueryOptions( route, apiDescriptions );

if ( Options.AdHocModelBuilder.ModelConfigurations.Count == 0 )
{
ExploreQueryOptions( route, apiDescriptions );
}
else if ( apiDescriptions.Count > 0 )
{
using ( new AdHocEdmScope( apiDescriptions, Options.AdHocModelBuilder ) )
{
ExploreQueryOptions( route, apiDescriptions );
}
}

return apiDescriptions;
}

apiDescriptions = new();
Expand All @@ -199,7 +218,8 @@ protected override Collection<VersionedApiDescription> ExploreRouteControllers(
}
}

return ExploreQueryOptions( route, apiDescriptions );
ExploreQueryOptions( route, apiDescriptions );
return apiDescriptions;
}

/// <inheritdoc />
Expand All @@ -210,7 +230,25 @@ protected override Collection<VersionedApiDescription> ExploreDirectRouteControl
ApiVersion apiVersion )
{
var apiDescriptions = base.ExploreDirectRouteControllers( controllerDescriptor, candidateActionDescriptors, route, apiVersion );
return ExploreQueryOptions( route, apiDescriptions );

if ( apiDescriptions.Count == 0 )
{
return apiDescriptions;
}

if ( Options.AdHocModelBuilder.ModelConfigurations.Count == 0 )
{
ExploreQueryOptions( route, apiDescriptions );
}
else if ( apiDescriptions.Count > 0 )
{
using ( new AdHocEdmScope( apiDescriptions, Options.AdHocModelBuilder ) )
{
ExploreQueryOptions( route, apiDescriptions );
}
}

return apiDescriptions;
}

/// <summary>
Expand Down Expand Up @@ -238,20 +276,20 @@ protected virtual void ExploreQueryOptions(
queryOptions.ApplyTo( apiDescriptions, settings );
}

private Collection<VersionedApiDescription> ExploreQueryOptions(
IHttpRoute route,
Collection<VersionedApiDescription> apiDescriptions )
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) =>
model.SetAnnotationValue( model, AdHocAnnotation.Instance );

private void ExploreQueryOptions( IHttpRoute route, Collection<VersionedApiDescription> apiDescriptions )
{
if ( apiDescriptions.Count == 0 )
{
return apiDescriptions;
return;
}

var uriResolver = Configuration.GetODataRootContainer( route ).GetRequiredService<ODataUriResolver>();

ExploreQueryOptions( apiDescriptions, uriResolver );

return apiDescriptions;
}

private ResponseDescription CreateResponseDescriptionWithRoute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public partial class ODataApiExplorerOptions : ApiExplorerOptions
/// Initializes a new instance of the <see cref="ODataApiExplorerOptions"/> class.
/// </summary>
/// <param name="configuration">The current <see cref="HttpConfiguration">configuration</see> associated with the options.</param>
public ODataApiExplorerOptions( HttpConfiguration configuration ) : base( configuration ) { }
public ODataApiExplorerOptions( HttpConfiguration configuration )
: base( configuration ) => AdHocModelBuilder = new( configuration );

/// <summary>
/// Gets or sets a value indicating whether the API explorer settings are honored.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.Conventions;

using Asp.Versioning.OData;
using System.Web.Http.Description;

/// <content>
/// Provides additional implementation specific to ASP.NET Web API.
/// </content>
public partial class ImplicitModelBoundSettingsConvention : IModelConfiguration, IODataQueryOptionsConvention
{
/// <inheritdoc />
public void ApplyTo( ApiDescription apiDescription )
{
var response = apiDescription.ResponseDescription;
var type = response.ResponseType ?? response.DeclaredType;

if ( type == null )
{
return;
}

if ( type.IsEnumerable( out var itemType ) )
{
type = itemType;
}

types.Add( type! );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Asp.Versioning.Conventions;

using Microsoft.AspNet.OData;
using System.Runtime.CompilerServices;
using System.Web.Http.Description;

Expand All @@ -14,20 +13,4 @@ public partial class ODataQueryOptionsConventionBuilder
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static Type GetController( ApiDescription apiDescription ) =>
apiDescription.ActionDescriptor.ControllerDescriptor.ControllerType;

[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static bool IsODataLike( ApiDescription description )
{
var parameters = description.ParameterDescriptions;

for ( var i = 0; i < parameters.Count; i++ )
{
if ( parameters[i].ParameterDescriptor.ParameterType.IsODataQueryOptions() )
{
return true;
}
}

return false;
}
}
Loading