Skip to content

OData Fixes July 2019 #521

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 5 commits into from
Jul 18, 2019
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
213 changes: 166 additions & 47 deletions src/Common.OData.ApiExplorer/AspNet.OData/Routing/ODataRouteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.OData.Edm;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -94,7 +95,6 @@ void AppendEntitySetOrOperation( IList<string> segments )
#else
var controllerDescriptor = Context.ActionDescriptor;
#endif
var controllerName = controllerDescriptor.ControllerName;

if ( Context.IsAttributeRouted )
{
Expand All @@ -103,32 +103,47 @@ void AppendEntitySetOrOperation( IList<string> segments )
#else
var prefix = controllerDescriptor.ControllerTypeInfo.GetCustomAttributes<ODataRoutePrefixAttribute>().FirstOrDefault()?.Prefix?.Trim( '/' );
#endif
var template = Context.RouteTemplate;
AppendEntitySetOrOperationFromAttributes( segments, prefix );
}
else
{
AppendEntitySetOrOperationFromConvention( segments, controllerDescriptor.ControllerName );
}
}

void AppendEntitySetOrOperationFromAttributes( IList<string> segments, string prefix )
{
var template = Context.RouteTemplate;

if ( IsNullOrEmpty( prefix ) )
if ( Context.IsOperation && Context.RouteTemplateGeneration == Client )
{
template = FixUpArrayParameters( template, Context.Operation );
}

if ( IsNullOrEmpty( prefix ) )
{
segments.Add( template );
}
else
{
if ( IsNullOrEmpty( template ) )
{
segments.Add( prefix );
}
else if ( template[0] == '(' && Context.UrlKeyDelimiter == Parentheses )
{
segments.Add( Context.RouteTemplate );
segments.Add( prefix + template );
}
else
{
if ( IsNullOrEmpty( template ) )
{
segments.Add( prefix );
}
else if ( template[0] == '(' && Context.UrlKeyDelimiter == Parentheses )
{
segments.Add( prefix + template );
}
else
{
segments.Add( prefix );
segments.Add( template );
}
segments.Add( prefix );
segments.Add( template );
}

return;
}
}

void AppendEntitySetOrOperationFromConvention( IList<string> segments, string controllerName )
{
var builder = new StringBuilder();

switch ( Context.ActionType )
Expand Down Expand Up @@ -244,33 +259,23 @@ void AppendParametersFromConvention( StringBuilder builder, IEdmOperation operat
var actionParameters = Context.ParameterDescriptions.ToDictionary( p => p.Name, StringComparer.OrdinalIgnoreCase );
var parameter = parameters.Current;
var name = parameter.Name;
#if WEBAPI
var routeParameterName = actionParameters[name].ParameterDescriptor.ParameterName;
#elif API_EXPLORER
var routeParameterName = actionParameters[name].ParameterDescriptor.Name;
#else
var routeParameterName = actionParameters[name].Name;
#endif
var routeParameterName = GetRouteParameterName( actionParameters, name );

builder.Append( '(' );
builder.Append( name );
builder.Append( '=' );

ExpandParameterTemplate( builder, parameter, routeParameterName );

while ( parameters.MoveNext() )
{
parameter = parameters.Current;
name = parameter.Name;
#if WEBAPI
routeParameterName = actionParameters[name].ParameterDescriptor.ParameterName;
#elif API_EXPLORER
routeParameterName = actionParameters[name].ParameterDescriptor.Name;
#else
routeParameterName = actionParameters[name].Name;
#endif
routeParameterName = GetRouteParameterName( actionParameters, name );
builder.Append( ',' );
builder.Append( name );
builder.Append( '=' );

ExpandParameterTemplate( builder, parameter, routeParameterName );
}

Expand Down Expand Up @@ -305,29 +310,128 @@ void ExpandParameterTemplate( StringBuilder template, IEdmTypeReference typeRefe
return;
}

if ( typeDef.TypeKind == EdmTypeKind.Enum )
switch ( typeDef.TypeKind )
{
case EdmTypeKind.Collection:
template.Insert( offset, '[' );
template.Append( ']' );
break;
case EdmTypeKind.Enum:
var fullName = typeReference.FullName();

if ( !Context.AllowUnqualifiedEnum )
{
template.Insert( offset, fullName );
offset += fullName.Length;
}

template.Insert( offset, '\'' );
template.Append( '\'' );
break;
default:
var type = typeDef.GetClrType( Context.EdmModel );

if ( quotedTypes.TryGetValue( type, out var prefix ) )
{
template.Insert( offset, prefix );
offset += prefix.Length;
template.Insert( offset, '\'' );
template.Append( '\'' );
}

break;
}
}

string FixUpArrayParameters( string template, IEdmOperation operation )
{
Contract.Requires( !IsNullOrEmpty( template ) );
Contract.Requires( operation != null );

if ( !operation.IsFunction() )
{
return template;
}

int IndexOfToken( StringBuilder builder, string token )
{
var fullName = typeReference.FullName();
var index = -1;

if ( !Context.AllowUnqualifiedEnum )
for ( var i = 0; i < builder.Length; i++ )
{
template.Insert( offset, fullName );
offset += fullName.Length;
if ( builder[i] != '{' )
{
continue;
}

index = i;
++i;

var matched = true;

for ( var j = 0; j < token.Length; i++, j++ )
{
if ( builder[i] != token[j] )
{
matched = false;
break;
}
}

if ( matched )
{
break;
}

while ( builder[i] != '}' )
{
++i;
}
}

template.Insert( offset, '\'' );
template.Append( '\'' );
return;
return index;
}

void InsertBrackets( StringBuilder builder, string token )
{
var index = IndexOfToken( builder, token );

if ( index >= 0 )
{
builder.Insert( index, '[' ).Insert( index + token.Length + 3, ']' );
}
}

var type = typeDef.GetClrType( Context.EdmModel );
var collectionParameters = from param in operation.Parameters
where param.Type.TypeKind() == EdmTypeKind.Collection &&
param.Name != "bindingParameter"
select param;

if ( quotedTypes.TryGetValue( type, out var prefix ) )
using ( var parameters = collectionParameters.GetEnumerator() )
{
template.Insert( offset, prefix );
offset += prefix.Length;
template.Insert( offset, '\'' );
template.Append( '\'' );
if ( !parameters.MoveNext() )
{
return template;
}

var buffer = new StringBuilder( template );
var actionParameters = Context.ParameterDescriptions.ToDictionary( p => p.Name, StringComparer.OrdinalIgnoreCase );
var parameter = parameters.Current;
var name = parameter.Name;
var routeParameterName = GetRouteParameterName( actionParameters, name );

InsertBrackets( buffer, routeParameterName );

while ( parameters.MoveNext() )
{
parameter = parameters.Current;
name = parameter.Name;
routeParameterName = GetRouteParameterName( actionParameters, name );

InsertBrackets( buffer, routeParameterName );
}

return buffer.ToString();
}
}

Expand Down Expand Up @@ -416,6 +520,21 @@ IList<ApiParameterDescription> GetQueryParameters( IList<ApiParameterDescription
return queryParameters;
}

static string GetRouteParameterName( IReadOnlyDictionary<string, ApiParameterDescription> actionParameters, string name )
{
if ( !actionParameters.TryGetValue( name, out var parameter ) )
{
return name;
}
#if WEBAPI
return parameter.ParameterDescriptor.ParameterName;
#elif API_EXPLORER
return parameter.ParameterDescriptor.Name;
#else
return parameter.Name;
#endif
}

static bool IsBuiltInParameter( Type parameterType ) => ODataQueryOptionsType.IsAssignableFrom( parameterType ) || ODataActionParametersType.IsAssignableFrom( parameterType );

static bool IsKey( IReadOnlyList<IEdmStructuralProperty> keys, ApiParameterDescription parameter )
Expand Down
22 changes: 21 additions & 1 deletion src/Common.OData.ApiExplorer/AspNet.OData/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,27 @@ internal static bool IsEnumerable( this Type type, out Type itemType )

static bool IsSingleResult( this Type type ) => type.Is( SingleResultOfT );

static bool IsODataValue( this Type type ) => type.Is( ODataValueOfT );
static bool IsODataValue( this Type type )
{
while ( type != null )
{
if ( !type.IsGenericType )
{
return false;
}

var typeDef = type.GetGenericTypeDefinition();

if ( typeDef.Equals( ODataValueOfT ) )
{
return true;
}

type = type.BaseType;
}

return false;
}

static bool Is( this Type type, Type typeDefinition ) => type.IsGenericType && type.GetGenericTypeDefinition().Equals( typeDefinition );

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<VersionPrefix>3.1.0</VersionPrefix>
<VersionPrefix>3.1.1</VersionPrefix>
<AssemblyVersion>3.1.0.0</AssemblyVersion>
<TargetFramework>net45</TargetFramework>
<AssemblyTitle>Microsoft ASP.NET Web API Versioned API Explorer for OData v4.0</AssemblyTitle>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
Generate navigation property templates by convention (#463)
Fix overriding global query settings (#488)
Add appropriate separators for composite keys (#500)
Fix client vs server route template generation (#502)
UseQualifiedNames option replaces UseQualifiedOperationNames
Support array URL syntax for collection parameters (#496)
Support DataMemberAttribute (#511)
Support inherited ODataValue<T> (#513)
Preserve attributes during model substitution (#520)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<VersionPrefix>3.1.3</VersionPrefix>
<VersionPrefix>3.1.4</VersionPrefix>
<AssemblyVersion>3.1.0.0</AssemblyVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<NETStandardImplicitPackageVersion>2.0.0-*</NETStandardImplicitPackageVersion>
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.AspNetCore.Mvc.Versioning/ReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Add API version parameter to Content-Type (#484)
Ensure implicitly versioned ActionModel has associated ControllerModel (#519)
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ public virtual void OnProvidersExecuted( ApplicationModelProviderContext context
controllers = ControllerFilter.Apply( controllers );
}

foreach ( var controller in controllers )
for ( var i = 0; i < controllers.Count; i++ )
{
var controller = controllers[i];

if ( !conventionBuilder.ApplyTo( controller ) )
{
ApplyAttributeOrImplicitConventions( controller, implicitVersionModel );
Expand All @@ -75,8 +77,10 @@ static bool IsDecoratedWithAttributes( ControllerModel controller )
{
Contract.Requires( controller != null );

foreach ( var attribute in controller.Attributes )
for ( var i = 0; i < controller.Attributes.Count; i++ )
{
var attribute = controller.Attributes[i];

if ( attribute is IApiVersionProvider || attribute is IApiVersionNeutral )
{
return true;
Expand All @@ -91,8 +95,10 @@ static void ApplyImplicitConventions( ControllerModel controller, ApiVersionMode
Contract.Requires( controller != null );
Contract.Requires( implicitVersionModel != null );

foreach ( var action in controller.Actions )
for ( var i = 0; i < controller.Actions.Count; i++ )
{
var action = controller.Actions[i];
action.SetProperty( controller );
action.SetProperty( implicitVersionModel );
}
}
Expand Down
Loading