Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/OData/AspNetCoreOData into …
Browse files Browse the repository at this point in the history
…ODataRoutingSample.Tests
  • Loading branch information
[email protected] committed Aug 15, 2022
2 parents af22290 + 9e91f05 commit dbcc511
Show file tree
Hide file tree
Showing 66 changed files with 5,969 additions and 162 deletions.
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,7 @@ That's it.

### 3.1 Building and Testing in Visual Studio

~~Visual Studio 2019 Preview is necessary to build the project.~~

Since the project introducts the .NET 6 targe framework to support `DateOnly` and `TimeOnly`, Visual Studio 2022 is required to build source project. (Edit at 2/10/2022)
Visual Studio 2022 is required to build the source project in order to support the `DateOnly` and `TimeOnly` types, which were introduced in .NET 6.

### 3.2 One-click build and test script in command line
Coming soon.
Expand All @@ -98,11 +96,11 @@ Coming soon.

The symbol package is uploaded to nuget symbol server.

It supports source link debug. Remember to make `Enable Source Link support` checked if you debug using Visual Studio.
It supports source link debug. Remember to check `Enable Source Link support` if you debug using Visual Studio.

### 3.4 Nightly Builds

The nightly build process will upload a NuGet packages for ASP.NET Core OData to:
The nightly build process will upload NuGet packages for ASP.NET Core OData to:

* https://www.myget.org/gallery/webapinetcore

Expand All @@ -124,7 +122,7 @@ To connect to webapinightly feed, use this feed URL:

### 5.1 Contribution

Any contribution, feature request, bug, issue are welcome.
Any contributions, feature requests, bugs and issues are welcome.

### 5.2 Support

Expand Down
3 changes: 2 additions & 1 deletion sample/ODataRoutingSample/Controllers/PeopleController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class PeopleController : ControllerBase
new Person
{
FirstName = "Goods",
LastName = "Zhangg",
LastName = "Zha/ngg",
},
new Person
{
Expand All @@ -42,6 +42,7 @@ public IActionResult Get(CancellationToken token)
return Ok(_persons);
}

// People(FirstName='Goods',LastName='Zha%2Fngg')
[HttpGet]
[EnableQuery]
public IActionResult Get(string keyFirstName, string keyLastName)
Expand Down
4 changes: 4 additions & 0 deletions sample/ODataRoutingSample/Models/EdmModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public static IEdmModel GetEdmModel()
builder.EntitySet<Product>("Products");
builder.EntitySet<Person>("People").EntityType.HasKey(c => new { c.FirstName, c.LastName });

// use the following codes to set the order and change the route template.
// builder.EntityType<Person>().Property(c => c.FirstName).Order = 2;
// builder.EntityType<Person>().Property(c => c.LastName).Order = 1;

// function with optional parameters
var functionWithOptional = builder.EntityType<Product>().Collection.Function("GetWholeSalary").ReturnsCollectionFromEntitySet<Order>("Orders");
functionWithOptional.Parameter<int>("minSalary");
Expand Down
5 changes: 5 additions & 0 deletions sample/ODataRoutingSample/Models/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
// </copyright>
//------------------------------------------------------------------------------

using System.ComponentModel.DataAnnotations.Schema;

namespace ODataRoutingSample.Models
{
public class Person
{
// [Column(Order = 1)] // This attribute can be used with [Key] convention model building
// It is ignored if the property is added explicitly.
public string FirstName { get; set; }

// [Column(Order = 2)]
public string LastName { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static IMvcBuilder AddODataNewtonsoftJson(this IMvcBuilder builder,
/// <returns>The <see cref="IMvcCoreBuilder"/>.</returns>
public static IMvcCoreBuilder AddODataNewtonsoftJson(this IMvcCoreBuilder builder)
{
return builder.AddNewtonsoftJson(null);
return builder.AddODataNewtonsoftJson(null);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ private static HttpContext CreateHttpContext(HttpContext originalContext)
kvp.Key == typeof(IODataFeature) ||
kvp.Key == typeof(IItemsFeature) ||
kvp.Key == typeof(IHttpRequestFeature) ||
kvp.Key == typeof(IHttpResponseFeature))
kvp.Key == typeof(IHttpResponseFeature) ||
kvp.Key == typeof(IQueryFeature)) // Noted: we should not pass the QueryFeature from Main request to the sub request
{
continue;
}
Expand Down
8 changes: 8 additions & 0 deletions src/Microsoft.AspNetCore.OData/Common/Error.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,5 +229,13 @@ internal static NotSupportedException NotSupported(string messageFormat, params
{
return new NotSupportedException(Error.Format(messageFormat, messageArgs));
}

internal static void ValidateErrorCode(string expectedErrorCode, Microsoft.OData.ODataError error)
{
if (expectedErrorCode != error.ErrorCode)
{
throw Argument(error.ErrorCode, "Invalid error code");
}
}
}
}
21 changes: 14 additions & 7 deletions src/Microsoft.AspNetCore.OData/Extensions/ActionModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Routing;
Expand Down Expand Up @@ -173,7 +174,13 @@ public static void AddSelector(this ActionModel action, string httpMethods, stri
// let's always create new selector model for action.
// Since the new created selector model is absolute attribute route, the controller attribute route doesn't apply to this selector model.
bool hasAttributeRouteOnController = action.Controller.Selectors.Any(s => s.AttributeRouteModel != null);


// Check if CORS attribute is specified on action. New selectors need to be registered with CORS support.
bool acceptPreflight = action.Controller.Attributes.OfType<IDisableCorsAttribute>().Any() ||
action.Controller.Attributes.OfType<IEnableCorsAttribute>().Any() ||
action.Attributes.OfType<IDisableCorsAttribute>().Any() ||
action.Attributes.OfType<IEnableCorsAttribute>().Any();

// If the methods have different case sensitive, for example, "get", "Get", in the ASP.NET Core 3.1,
// It will throw "An item with the same key has already been added. Key: GET", in
// HttpMethodMatcherPolicy.BuildJumpTable(Int32 exitDestination, IReadOnlyList`1 edges)
Expand All @@ -189,13 +196,13 @@ public static void AddSelector(this ActionModel action, string httpMethods, stri
if (hasAttributeRouteOnController || selectorModel == null)
{
// Create a new selector model.
selectorModel = CreateSelectorModel(action, methods);
selectorModel = CreateSelectorModel(action, methods, acceptPreflight);
action.Selectors.Add(selectorModel);
}
else
{
// Update the existing non attribute routing selector model.
selectorModel = UpdateSelectorModel(selectorModel, methods);
selectorModel = UpdateSelectorModel(selectorModel, methods, acceptPreflight);
}

ODataRoutingMetadata odataMetadata = new ODataRoutingMetadata(prefix, model, path);
Expand All @@ -216,7 +223,7 @@ public static void AddSelector(this ActionModel action, string httpMethods, stri
}
}

internal static SelectorModel UpdateSelectorModel(SelectorModel selectorModel, string[] httpMethods)
internal static SelectorModel UpdateSelectorModel(SelectorModel selectorModel, string[] httpMethods, bool acceptPreflight)
{
Contract.Assert(selectorModel != null);

Expand Down Expand Up @@ -257,15 +264,15 @@ internal static SelectorModel UpdateSelectorModel(SelectorModel selectorModel, s
// append the http method metadata.
Contract.Assert(httpMethods.Length >= 1);
selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods));
selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods));
selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods, acceptPreflight));

// append controller attributes to action selector model? -- NO
// Be noted: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs#L74-L75

return selectorModel;
}

internal static SelectorModel CreateSelectorModel(ActionModel actionModel, string[] httpMethods)
internal static SelectorModel CreateSelectorModel(ActionModel actionModel, string[] httpMethods, bool acceptPreflight)
{
Contract.Assert(actionModel != null);

Expand Down Expand Up @@ -309,7 +316,7 @@ internal static SelectorModel CreateSelectorModel(ActionModel actionModel, strin

Contract.Assert(httpMethods.Length >= 1);
selectorModel.ActionConstraints.Add(new HttpMethodActionConstraint(httpMethods));
selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods));
selectorModel.EndpointMetadata.Add(new HttpMethodMetadata(httpMethods, acceptPreflight));

// append controller attributes to action selector model? -- NO
// Be noted: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ActionAttributeRouteModel.cs#L74-L75
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Edm;
using Microsoft.AspNetCore.OData.Extensions;
using Microsoft.AspNetCore.OData.Formatter.Value;
using Microsoft.AspNetCore.OData.Formatter.Wrapper;
using Microsoft.AspNetCore.OData.Routing;
using Microsoft.AspNetCore.OData.Routing.Parser;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
Expand All @@ -33,6 +36,8 @@ namespace Microsoft.AspNetCore.OData.Formatter.Deserialization
/// </summary>
public class ODataResourceDeserializer : ODataEdmTypeDeserializer
{
private static readonly Regex ContentIdReferencePattern = new Regex(@"\$\d", RegexOptions.Compiled);

/// <summary>
/// Initializes a new instance of the <see cref="ODataResourceDeserializer"/> class.
/// </summary>
Expand Down Expand Up @@ -376,7 +381,7 @@ public virtual void ApplyNestedProperty(object resource, ODataNestedResourceInfo
}

IList<ODataItemWrapper> nestedItems;
var referenceLinks = resourceInfoWrapper.NestedItems.OfType<ODataEntityReferenceLinkWrapper>().ToArray();
ODataEntityReferenceLinkWrapper[] referenceLinks = resourceInfoWrapper.NestedItems.OfType<ODataEntityReferenceLinkWrapper>().ToArray();
if (referenceLinks.Length > 0)
{
// Be noted:
Expand Down Expand Up @@ -578,7 +583,7 @@ private object ReadNestedResourceInline(ODataResourceWrapper resourceWrapper, IE

IEdmStructuredTypeReference structuredType = edmType.AsStructured();

var nestedReadContext = new ODataDeserializerContext
ODataDeserializerContext nestedReadContext = new ODataDeserializerContext
{
Path = readContext.Path,
Model = readContext.Model,
Expand Down Expand Up @@ -797,7 +802,7 @@ private static ODataResourceWrapper CreateResourceWrapper(IEdmTypeReference edmP
resource.Properties = CreateKeyProperties(refLink.EntityReferenceLink.Url, readContext) ?? Array.Empty<ODataProperty>();
ODataResourceWrapper resourceWrapper = new ODataResourceWrapper(resource);

foreach (var instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations)
foreach (ODataInstanceAnnotation instanceAnnotation in refLink.EntityReferenceLink.InstanceAnnotations)
{
resource.InstanceAnnotations.Add(instanceAnnotation);
}
Expand Down Expand Up @@ -835,7 +840,7 @@ private ODataResourceWrapper UpdateResourceWrapper(ODataResourceWrapper resource
else
{
IDictionary<string, ODataProperty> newPropertiesDic = resourceWrapper.Resource.Properties.ToDictionary(p => p.Name, p => p);
foreach (var key in keys)
foreach (ODataProperty key in keys)
{
// Logic: if we have the key property, try to keep the key property and get rid of the key value from ID.
// Need to double confirm whether it is the right logic?
Expand Down Expand Up @@ -870,26 +875,35 @@ private static IList<ODataProperty> CreateKeyProperties(Uri id, ODataDeserialize

try
{
Uri serviceRootUri = null;
if (id.IsAbsoluteUri)
IEdmModel model = readContext.Model;
HttpRequest request = readContext.Request;
IServiceProvider requestContainer = request.GetRouteServices();
Uri resolvedId = id;

string idOriginalString = id.OriginalString;
if (ContentIdReferencePattern.IsMatch(idOriginalString))
{
string serviceRoot = readContext.Request.CreateODataLink();
serviceRootUri = new Uri(serviceRoot, UriKind.Absolute);
// We can expect request.ODataBatchFeature() to not be null
string resolvedUri = ContentIdHelpers.ResolveContentId(
idOriginalString,
request.ODataBatchFeature().ContentIdMapping);
resolvedId = new Uri(resolvedUri, UriKind.RelativeOrAbsolute);
}

var request = readContext.Request;
IEdmModel model = readContext.Model;

// TODO: shall we use the DI to inject the path parser?
DefaultODataPathParser pathParser = new DefaultODataPathParser();
Uri serviceRootUri = new Uri(request.CreateODataLink());
IODataPathParser pathParser = requestContainer?.GetService<IODataPathParser>();
if (pathParser == null) // Seems like IODataPathParser is NOT injected into DI container by default
{
pathParser = new DefaultODataPathParser();
}

IList<ODataProperty> properties = null;
var path = pathParser.Parse(model, serviceRootUri, id, request.GetRouteServices());
ODataPath path = pathParser.Parse(model, serviceRootUri, resolvedId, requestContainer);
KeySegment keySegment = path.OfType<KeySegment>().LastOrDefault();
if (keySegment != null)
{
properties = new List<ODataProperty>();
foreach (var key in keySegment.Keys)
foreach (KeyValuePair<string, object> key in keySegment.Keys)
{
properties.Add(new ODataProperty
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ public override void WriteResponseHeaders(OutputFormatterWriteContext context)
// Set the character set.
MediaTypeHeaderValue currentContentType = GetContentType(response.Headers[HeaderNames.ContentType].FirstOrDefault());
RequestHeaders requestHeader = request.GetTypedHeaders();
if (requestHeader != null && requestHeader.AcceptCharset != null)
// Starting from ASP .NET Core 3.0 AcceptCharset returns an empty collection instead of null.
if (requestHeader?.AcceptCharset.Count > 0)
{
IEnumerable<string> acceptCharsetValues = requestHeader.AcceptCharset.Select(cs => cs.Value.Value);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ internal static async Task WriteToStreamAsync(
IODataSerializer serializer = GetSerializer(type, value, request, serializerProvider);

ODataPath path = request.ODataFeature().Path;
IEdmNavigationSource targetNavigationSource = GetTargetNavigationSource(path, model);
IEdmNavigationSource targetNavigationSource = path.GetNavigationSource();
HttpResponse response = request.HttpContext.Response;

// serialize a response
Expand Down Expand Up @@ -196,30 +196,6 @@ internal static IODataSerializer GetSerializer(Type type, object value, HttpRequ
return serializer;
}

private static IEdmNavigationSource GetTargetNavigationSource(ODataPath path, IEdmModel model)
{
if (path == null)
{
return null;
}

Contract.Assert(model != null);

OperationSegment operationSegment = path.LastSegment as OperationSegment;
if (operationSegment != null)
{
// OData model builder uses an annotation to save the function returned entity set.
// TODO: we need to refactor it later.
ReturnedEntitySetAnnotation entitySetAnnotation = model.GetAnnotationValue<ReturnedEntitySetAnnotation>(operationSegment.Operations.Single());
if (entitySetAnnotation != null)
{
return model.EntityContainer.FindEntitySet(entitySetAnnotation.EntitySetName);
}
}

return path.GetNavigationSource();
}

private static string GetRootElementName(ODataPath path)
{
if (path != null)
Expand Down
Loading

0 comments on commit dbcc511

Please sign in to comment.