Skip to content
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

TDEAL-8: HttpRequestExtensions and other improvements #229

Merged
merged 17 commits into from
Dec 26, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Lombiq.HelpfulLibraries.Common.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace Microsoft.AspNetCore.Http;

public static class HttpRequestExtensions
{
/// <summary>
/// Returns the query string without the leading <c>?</c> and without the keys in <paramref name="keysToExclude"/>.
/// </summary>
public static string GetQueryWithout(this HttpRequest request, params string[] keysToExclude)
{
var query = HttpUtility.ParseQueryString(request.QueryString.Value ?? string.Empty);
return string.Join('&', query
.AllKeys
.Where(key => key != null && !keysToExclude.Exists(key.EqualsOrdinalIgnoreCase))
.Select(key => $"{HttpUtility.UrlEncode(key)}={HttpUtility.UrlEncode(query[key] ?? string.Empty)}"));
}

/// <summary>
/// Returns the current URL but appends a new query string entry.
/// </summary>
public static string GetLinkWithAdditionalQuery(this HttpRequest request, string queryString, string key, object value)
{
queryString ??= string.Empty;
if (queryString.StartsWith('?')) queryString = queryString[1..];

var pageQuery = string.IsNullOrEmpty(queryString)
? StringHelper.CreateInvariant($"{key}={value}")
: StringHelper.CreateInvariant($"&{key}={value}");
return $"{request.PathBase}{request.Path}?{queryString}{pageQuery}";
}

/// <summary>
/// Returns the current URL but appends a new query string entry.
/// </summary>
public static string GetLinkWithAdditionalQuery(this HttpRequest request, string key, object value) =>
request.GetLinkWithAdditionalQuery(request.QueryString.Value, key, value);

/// <summary>
/// Returns the current URL excluding any existing query string entry with the key <paramref name="key"/>, and with
/// a new <paramref name="key"/>-<paramref name="value"/> entry appended.
/// </summary>
public static string GetLinkWithDifferentQuery(this HttpRequest request, string key, object value) =>
request.GetLinkWithAdditionalQuery(request.GetQueryWithout(key), key, value);

/// <summary>
/// Returns the current URL but with the value of the query string entry of with the key <paramref name="key"/>
/// cycled to the next item in the <paramref name="values"/>. This can be useful for example to generate table
/// header links that cycle ascending-descending-unsorted sorting by that column.
/// </summary>
public static string GetLinkAndCycleQueryValue(this HttpRequest request, string key, params string[] values)
{
var query = HttpUtility.ParseQueryString(request.QueryString.Value ?? string.Empty);

var value = query[key] ?? string.Empty;
var index = ((IList<string>)values).IndexOf(value);
var newValue = values[(index + 1) % values.Length];

return request.GetLinkWithDifferentQuery(key, newValue);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;

namespace Lombiq.HelpfulLibraries.AspNetCore.Extensions;

public static class ServiceCollectionExtensions
{
/// <summary>
/// Configures <see cref="MvcOptions"/> to add the <typeparamref name="TFilter"/> to the list of filters.
/// </summary>
public static void AddAsyncResultFilter<TFilter>(this IServiceCollection services)
where TFilter : IAsyncResultFilter =>
services.Configure<MvcOptions>(options => options.Filters.Add(typeof(TFilter)));
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Lombiq.HelpfulLibraries.OrchardCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.ContentManagement;
using System;
using System.Collections.Generic;
Expand Down Expand Up @@ -76,4 +77,39 @@ public static string ActionTask<TController>(
params (string Key, object Value)[] additionalArguments)
where TController : ControllerBase =>
httpContext.Action(taskActionExpression.StripResult(), additionalArguments);

/// <summary>
/// Returns the text of the MVC route value identified by <paramref name="name"/> (case-insensitive).
/// </summary>
public static string GetRouteValueString(this HttpContext httpContext, string name) =>
httpContext?.Request.RouteValues.GetMaybe(name)?.ToString();

/// <summary>
/// Returns a value indicating whether the current MVC route matches the provided <paramref name="area"/>, <paramref
/// name="controller"/> and <paramref name="action"/>.
/// </summary>
public static bool IsAction(this HttpContext httpContext, string area, string controller, string action) =>
httpContext?.Request.RouteValues is { } routeValues &&
routeValues.GetMaybe(nameof(area))?.ToString() == area &&
routeValues.GetMaybe(nameof(controller))?.ToString() == controller &&
routeValues.GetMaybe(nameof(action))?.ToString() == action;

/// <summary>
/// Returns a value indicating whether the current page is a content item display action.
/// </summary>
public static bool IsContentDisplay(this HttpContext httpContext) =>
httpContext.IsAction("OrchardCore.Contents", "Item", "Display");

/// <summary>
/// Gets the content item from the database by the ID in the <c>contentItemId</c> or <c>id</c> route values.
/// </summary>
public static Task<ContentItem> GetContentItemAsync(this HttpContext httpContext, string jsonPath = null)
{
var id = httpContext.GetRouteValueString(nameof(ContentItem.ContentItemId));
if (string.IsNullOrWhiteSpace(id)) id = httpContext.GetRouteValueString("id");
if (string.IsNullOrWhiteSpace(id)) return Task.FromResult<ContentItem>(null);

var contentManager = httpContext.RequestServices.GetRequiredService<IContentManager>();
return contentManager.GetAsync(id, jsonPath);
}
}
28 changes: 28 additions & 0 deletions Lombiq.HelpfulLibraries.OrchardCore/Contents/TaxonomyHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement;
using OrchardCore.Taxonomies.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Lombiq.HelpfulLibraries.OrchardCore.Contents;
Expand All @@ -20,4 +23,29 @@ await _contentHandleManager.GetContentItemIdAsync($"alias:{alias}") is { } conte
await _contentManager.GetAsync(contentItemId) is { } contentItem
? contentItem.As<TaxonomyPart>()?.Terms?.Find(term => term.ContentItemId == termId)
: null;

/// <summary>
/// Returns all child content items in a taxonomy tree.
/// </summary>
/// <param name="contentItem">The root of the tree to enumerate.</param>
/// <param name="includeSelf">
/// If <see langword="true"/> the <paramref name="contentItem"/> will be the first result so the collection is never
/// empty as long as <paramref name="contentItem"/> isn't <see langword="null"/>.
/// </param>
/// <returns>An unsorted list of all child items.</returns>
public static IList<ContentItem> GetAllChildren(ContentItem contentItem, bool includeSelf = false)
{
var results = new List<ContentItem>();
if (contentItem == null) return results;
if (includeSelf) results.Add(contentItem);

var partTerms = contentItem.As<TaxonomyPart>()?.Terms ?? Enumerable.Empty<ContentItem>();
var itemTerms = (contentItem.Content.Terms as JArray)?.ToObject<List<ContentItem>>() ?? Enumerable.Empty<ContentItem>();
foreach (var child in partTerms.Concat(itemTerms))
{
results.AddRange(GetAllChildren(child, includeSelf: true));
}

return results;
}
}
14 changes: 11 additions & 3 deletions Lombiq.HelpfulLibraries.OrchardCore/Mvc/WidgetFilterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ protected WidgetFilterBase(
/// <summary>
/// Returns the object used as the view-model and passed to the widget shape.
/// </summary>
protected abstract Task<TViewModel> GetViewModelAsync();
protected virtual Task<TViewModel> GetViewModelAsync() =>
throw new NotSupportedException($"Please override either overloads of \"{nameof(GetViewModelAsync)}\"!");

protected virtual Task<TViewModel> GetViewModelAsync(ResultExecutingContext context) =>
GetViewModelAsync();

public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
Expand All @@ -83,13 +87,17 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE
if ((AdminOnly && !isAdmin) ||
(FrontEndOnly && isAdmin) ||
(_requiredPermission != null && !await _authorizationService.AuthorizeAsync(user, _requiredPermission)) ||
await GetViewModelAsync() is not { } viewModel)
await GetViewModelAsync(context) is not { } viewModel)
{
await next();
return;
}

await _layoutAccessor.AddShapeToZoneAsync(ZoneName, await _shapeFactory.CreateAsync(ViewName, viewModel));
await _layoutAccessor.AddShapeToZoneAsync(
ZoneName,
ViewName == null && viewModel is IShape shape
? shape
: await _shapeFactory.CreateAsync(ViewName, viewModel));

await next();
}
Expand Down