Skip to content

Commit

Permalink
Merge pull request #1 from mariusz96/feature/v3
Browse files Browse the repository at this point in the history
Feature/v3
  • Loading branch information
mariusz96 authored Dec 29, 2023
2 parents 97a272d + d7a84af commit 98c2470
Show file tree
Hide file tree
Showing 33 changed files with 796 additions and 602 deletions.
38 changes: 29 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,26 @@ string? uri = _uriGenerator.GetUriByExpression<InvoicesController>(
## Features:
- Extracts action name, controller name, and route values from expression
- Delegates URL generation to LinkGenerator
- Supports ActionName, Area, NonAction, NonController, FromBody, FromForm, FromHeader, FromServices and FromKeyedServices attributes
- Supports IFormFile, IFormFileCollection, IEnumerable&lt;IFormFile&gt;, CancellationToken, and IFormCollection types
- Supports ASP.NET Core model metadata and application model conventions
- Supports simple types and collections of simple types only
- Supports specifying an endpoint name
- Supports Controller and Async suffixes
- Supports LinkOptions
- Supports bypassable caching
- Invalidates HttpContext's ambient route values

## Binding source filter:
You can specify a predicate which can determine whether an action parameter should be included based on its binding source.

The default one is:
```C#
Func<BindingSource?, bool> bindingSourceFilter = bindingSource =>
bindingSource == null
|| bindingSource.CanAcceptDataFrom(BindingSource.Query)
|| bindingSource.CanAcceptDataFrom(BindingSource.Path);
```
You pass null or default(T) to excluded action parameters when calling IUriGenerator.GetUriByExpression<TController> or a similar method.

For more information on binding sources, see ASP.NET Core documentation.

## Specifying an endpoint name:
If you use named attribute routes:
Expand Down Expand Up @@ -54,9 +68,9 @@ For more information on endpoint names, see ASP.NET Core documentation.
## Performance:
Extracting values from expression trees does introduce some overhead. To partially work around this problem, UriGeneration uses ASP.NET's CachedExpressionCompiler, so that equivalent route values' values' expression trees only have to be compiled once.

Additionally, it uses its internal Microsoft.Extensions.Caching.Memory.MemoryCache instance to cache extracted controller names, action names, and route values' keys within the scope of the application lifetime.
Additionally, it uses its internal Microsoft.Extensions.Caching.Memory.MemoryCache instance to cache extracted action methods' metadata.

This means that, for example, on 2017 Surface Book 2 you are able to generate 200 000 URLs in a second using a template like this: https://localhost:44339/api/invoices/{id}.
This means that, for example, on 2017 Surface Book 2 you are able to generate 150 000 URLs in a second using a template like this: https://localhost:44339/api/invoices/{id}.

## Setup:
- Install UriGeneration via NuGet Package Manager, Package Manager Console or dotnet CLI:
Expand All @@ -66,15 +80,21 @@ Install-Package UriGeneration
```
dotnet add package UriGeneration
```
- Register UriGeneration in a service container (each cache entry will have a size of 1):
- Register UriGeneration in a service container (each method cache entry will have a size of 1):
```C#
builder.Services.AddUriGeneration();
```
```C#
builder.Services.AddUriGeneration(o =>
builder.Services.AddUriGeneration(options =>
{
o.SizeLimit = 500;
o.CompactionPercentage = 0.5;
options.MethodCacheSizeLimit = 500;
options.MethodCacheCompactionPercentage = 0.5;
options.BypassMethodCache = false;
options.BypassCachedExpressionCompiler = false;
options.BindingSourceFilter = bindingSource =>
bindingSource == null
|| bindingSource.CanAcceptDataFrom(BindingSource.Query)
|| bindingSource.CanAcceptDataFrom(BindingSource.Path);
});
```
- Request an instance of IUriGenerator singleton service from any constructor in your app:
Expand Down
12 changes: 4 additions & 8 deletions src/UriGeneration/IUriGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ namespace UriGeneration
{
public interface IUriGenerator
{
string? GetPathByExpression<TController>(Expression<Action<TController>> action, string? endpointName = null, PathString pathBase = default, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetPathByExpression<TController>(Expression<Func<TController, object?>> action, string? endpointName = null, PathString pathBase = default, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetPathByExpression<TController>(HttpContext httpContext, Expression<Action<TController>> action, string? endpointName = null, PathString? pathBase = null, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetPathByExpression<TController>(HttpContext httpContext, Expression<Func<TController, object?>> action, string? endpointName = null, PathString? pathBase = null, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetUriByExpression<TController>(Expression<Action<TController>> action, string? endpointName, string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetUriByExpression<TController>(Expression<Func<TController, object?>> action, string? endpointName, string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetUriByExpression<TController>(HttpContext httpContext, Expression<Action<TController>> action, string? endpointName = null, string? scheme = null, HostString? host = null, PathString? pathBase = null, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetUriByExpression<TController>(HttpContext httpContext, Expression<Func<TController, object?>> action, string? endpointName = null, string? scheme = null, HostString? host = null, PathString? pathBase = null, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetPathByExpression<TController>(Expression<Action<TController>> expression, string? endpointName = null, PathString pathBase = default, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetPathByExpression<TController>(HttpContext httpContext, Expression<Action<TController>> expression, string? endpointName = null, PathString? pathBase = null, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetUriByExpression<TController>(Expression<Action<TController>> expression, string? endpointName, string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, UriOptions? options = null) where TController : class;
string? GetUriByExpression<TController>(HttpContext httpContext, Expression<Action<TController>> expression, string? endpointName = null, string? scheme = null, HostString? host = null, PathString? pathBase = null, FragmentString fragment = default, UriOptions? options = null) where TController : class;
}
}
5 changes: 3 additions & 2 deletions src/UriGeneration/Internal.Abstractions/IValuesExtractor.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;

namespace UriGeneration.Internal.Abstractions
{
internal interface IValuesExtractor
{
bool TryExtractValues<TController>(LambdaExpression action, [NotNullWhen(true)] out Values? values, UriOptions? options = null) where TController : class;
bool TryExtractValues<TController>(HttpContext? httpContext, Expression<Action<TController>> expression, UriOptions? options, [NotNullWhen(true)] out Values? values) where TController : class;
}
}
43 changes: 20 additions & 23 deletions src/UriGeneration/Internal/LoggerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using System.Linq.Expressions;
using System.Reflection;

namespace UriGeneration.Internal
{
Expand All @@ -17,8 +20,8 @@ internal static partial class LoggerExtensions
[LoggerMessage(1004, LogLevel.Debug, "Successfully extracted Type of TController.", EventName = nameof(ControllerExtracted))]
public static partial void ControllerExtracted(this ILogger logger);

[LoggerMessage(1005, LogLevel.Debug, "Successfully retrieved valid cache entry with values: {MethodName}, {ControllerName} and {AreaKey}, {ControllerAreaName}.", EventName = nameof(ValidCacheEntryRetrieved))]
public static partial void ValidCacheEntryRetrieved(this ILogger logger, string methodName, string controllerName, string areaKey, string controllerAreaName);
[LoggerMessage(1005, LogLevel.Debug, "Successfully retrieved valid cache entry with value: {ActionDescriptor}.", EventName = nameof(ValidCacheEntryRetrieved))]
public static partial void ValidCacheEntryRetrieved(this ILogger logger, ActionDescriptor actionDescriptor);

[LoggerMessage(1006, LogLevel.Debug, "Successfully retrieved invalid cache entry: no values will be extracted (see previous log messages for further details).", EventName = nameof(InvalidCacheEntryRetrieved))]
public static partial void InvalidCacheEntryRetrieved(this ILogger logger);
Expand All @@ -28,35 +31,29 @@ internal static partial class LoggerExtensions

[LoggerMessage(1008, LogLevel.Debug, "Expression must point to the method with the same declaring type as TController.", EventName = nameof(MethodDeclaringType))]
public static partial void MethodDeclaringType(this ILogger logger);

[LoggerMessage(1009, LogLevel.Debug, "Excluded method's parameter with position {MethodParameterPosition}: name cannot be null.", EventName = nameof(MethodParameterExcludedName))]
public static partial void MethodParameterExcludedName(this ILogger logger, int methodParameterPosition);

[LoggerMessage(1010, LogLevel.Debug, "Excluded method's parameter: {MethodParameterName} cannot be of Type IFormFile, IEnumerable<IFormFile>, CancellationToken or IFormCollection.", EventName = nameof(MethodParameterExcludedType))]
public static partial void MethodParameterExcludedType(this ILogger logger, string? methodParameterName);
[LoggerMessage(1009, LogLevel.Debug, "Successfully extracted MethodInfo's ActionDescriptor.", EventName = nameof(ActionDescriptorExtracted))]
public static partial void ActionDescriptorExtracted(this ILogger logger);

[LoggerMessage(1011, LogLevel.Debug, "Excluded method's parameter: {MethodParameterName} cannot have FromBody, FromForm, FromHeader, FromServices or FromKeyedServices attribute specified.", EventName = nameof(MethodParameterExcludedAttribute))]
public static partial void MethodParameterExcludedAttribute(this ILogger logger, string? methodParameterName);
[LoggerMessage(1010, LogLevel.Debug, "Controller cannot have bound properties.", EventName = nameof(BoundProperties))]
public static partial void BoundProperties(this ILogger logger);

[LoggerMessage(1012, LogLevel.Debug, "Successfully extracted method's name: {MethodName}.", EventName = nameof(MethodNameExtracted))]
public static partial void MethodNameExtracted(this ILogger logger, string methodName);

[LoggerMessage(1013, LogLevel.Debug, "{MethodName} cannot have NonActionAttribute specified.", EventName = nameof(MethodNameNotExtracted))]
public static partial void MethodNameNotExtracted(this ILogger logger, string methodName);
[LoggerMessage(1011, LogLevel.Debug, "No ActionDescriptor with MethodInfo: {MethodInfo} found in ActionDescriptorCollection.", EventName = nameof(NoActionDescriptorFound))]
public static partial void NoActionDescriptorFound(this ILogger logger, MethodInfo methodInfo);

[LoggerMessage(1014, LogLevel.Debug, "Succesfuly extracted controller's name: {ControllerName}.", EventName = nameof(ControllerNameExtracted))]
public static partial void ControllerNameExtracted(this ILogger logger, string controllerName);
[LoggerMessage(1015, LogLevel.Debug, "{ControllerName} cannot have NonControllerAttribute specified.", EventName = nameof(ControllerNameNotExtracted))]
public static partial void ControllerNameNotExtracted(this ILogger logger, string controllerName);
[LoggerMessage(1012, LogLevel.Debug, "Binding not allowed for parameter: {ParameterName}.", EventName = nameof(BindingNotAllowed))]
public static partial void BindingNotAllowed(this ILogger logger, string parameterName);

[LoggerMessage(1013, LogLevel.Debug, "Parameter: {ParameterName} cannot have binding source: {BindingSource}.", EventName = nameof(DisallowedBindingSource))]
public static partial void DisallowedBindingSource(this ILogger logger, string parameterName, BindingSource? bindingSource);

[LoggerMessage(1016, LogLevel.Debug, "Successfully extracted route value: {Key}, {Value}.", EventName = nameof(RouteValueExtracted))]
[LoggerMessage(1014, LogLevel.Debug, "Successfully extracted route value: {Key}, {Value}.", EventName = nameof(RouteValueExtracted))]
public static partial void RouteValueExtracted(this ILogger logger, string? key, object? value);

[LoggerMessage(1017, LogLevel.Debug, "Successfully extracted all values from expression.", EventName = nameof(ValuesExtracted))]
[LoggerMessage(1015, LogLevel.Debug, "Successfully extracted all values from expression.", EventName = nameof(ValuesExtracted))]
public static partial void ValuesExtracted(this ILogger logger);

[LoggerMessage(1018, LogLevel.Debug, "Failed to extract one or more values from expression {Expression}: an exception occurred.", EventName = nameof(ValuesNotExtracted))]
[LoggerMessage(1016, LogLevel.Debug, "Failed to extract one or more values from expression {Expression}: an exception occurred.", EventName = nameof(ValuesNotExtracted))]
public static partial void ValuesNotExtracted(this ILogger logger, LambdaExpression expression, Exception exception);
}
}
12 changes: 6 additions & 6 deletions src/UriGeneration/Internal/MethodCacheAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ internal sealed class MethodCacheAccessor : IMethodCacheAccessor, IDisposable
{
public IMemoryCache Cache { get; }

public MethodCacheAccessor(IOptions<MethodCacheOptions> optionsAccessor)
public MethodCacheAccessor(IOptions<UriGenerationOptions> globalOptionsAccessor)
{
if (optionsAccessor == null)
if (globalOptionsAccessor == null)
{
throw new ArgumentNullException(nameof(optionsAccessor));
throw new ArgumentNullException(nameof(globalOptionsAccessor));
}

var options = optionsAccessor.Value;
var globalOptions = globalOptionsAccessor.Value;

Cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = options.SizeLimit,
CompactionPercentage = options.CompactionPercentage
SizeLimit = globalOptions.MethodCacheSizeLimit ?? 500,
CompactionPercentage = globalOptions.MethodCacheCompactionPercentage ?? 0.5
});
}

Expand Down
16 changes: 3 additions & 13 deletions src/UriGeneration/Internal/MethodCacheEntry.Factory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Controllers;

namespace UriGeneration.Internal
{
Expand All @@ -8,18 +8,8 @@ internal partial class MethodCacheEntry
new(isValid: false);

public static MethodCacheEntry Valid(
string methodName,
string controllerName,
ParameterInfo[] includedMethodParameters,
string controllerAreaName)
{
return new(
isValid: true,
methodName,
controllerName,
includedMethodParameters,
controllerAreaName);
}
ControllerActionDescriptor actionDescriptor) =>
new(isValid: true, actionDescriptor);

public static MethodCacheEntry Invalid() => InvalidInstance;
}
Expand Down
24 changes: 6 additions & 18 deletions src/UriGeneration/Internal/MethodCacheEntry.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Diagnostics.CodeAnalysis;

namespace UriGeneration.Internal
{
internal partial class MethodCacheEntry
{
[MemberNotNullWhen(
true,
nameof(MethodName),
nameof(ControllerName),
nameof(IncludedMethodParameters),
nameof(ControllerAreaName))]
nameof(ActionDescriptor))]
public bool IsValid { get; }
public string? MethodName { get; }
public string? ControllerName { get; }
public ParameterInfo[]? IncludedMethodParameters { get; }
public string? ControllerAreaName { get; }
public ControllerActionDescriptor? ActionDescriptor { get; }

private MethodCacheEntry(bool isValid)
{
Expand All @@ -24,16 +18,10 @@ private MethodCacheEntry(bool isValid)

private MethodCacheEntry(
bool isValid,
string methodName,
string controllerName,
ParameterInfo[] includedMethodParameters,
string controllerAreaName)
ControllerActionDescriptor actionDescriptor)
{
IsValid = isValid;
MethodName = methodName;
ControllerName = controllerName;
IncludedMethodParameters = includedMethodParameters;
ControllerAreaName = controllerAreaName;
ActionDescriptor = actionDescriptor;
}
}
}
12 changes: 1 addition & 11 deletions src/UriGeneration/Internal/MethodCacheKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,5 @@

namespace UriGeneration.Internal
{
internal record MethodCacheKey
{
public MethodInfo Method { get; }
public Type Controller { get; }

public MethodCacheKey(MethodInfo method, Type controller)
{
Method = method;
Controller = controller;
}
}
internal record MethodCacheKey(MethodInfo Method, Type Controller, int ActionDescriptorsVersion);
}
16 changes: 0 additions & 16 deletions src/UriGeneration/Internal/StringExtensions.cs

This file was deleted.

Loading

0 comments on commit 98c2470

Please sign in to comment.