Skip to content

Commit

Permalink
Merge pull request #41 from Cratis:paging-sorting-fixes
Browse files Browse the repository at this point in the history
Paging and sorting fixes
  • Loading branch information
einari authored Jul 18, 2024
2 parents a2344cf + 9f44583 commit 5e93d9f
Show file tree
Hide file tree
Showing 19 changed files with 334 additions and 49 deletions.
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Cratis.Fundamentals" Version="5.2.7" />
<PackageVersion Include="Cratis.Fundamentals" Version="5.3.1" />
<PackageVersion Include="Cratis.Applications.ProxyGenerator.Build" Version="1.0.0" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.9.5" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.9.5" />
Expand Down
50 changes: 49 additions & 1 deletion Samples/eCommerce/Basic/Web/API/Products/AllProducts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,76 @@
*--------------------------------------------------------------------------------------------*/

// eslint-disable-next-line header/header
import { QueryFor, QueryResultWithState, SortingForQuery, Paging } from '@cratis/applications/queries';
import { QueryFor, QueryResultWithState, SortingActions, SortingActionsForQuery, Paging } from '@cratis/applications/queries';
import { useQuery, useQueryWithPaging, PerformQuery, SetSorting, SetPage } from '@cratis/applications.react/queries';
import { Product } from './Product';
import Handlebars from 'handlebars';

const routeTemplate = Handlebars.compile('/api/products/catalog');

class AllProductsSortBy {
private _id: SortingActionsForQuery<Product[]>;
private _name: SortingActionsForQuery<Product[]>;
private _isRegistered: SortingActionsForQuery<Product[]>;

constructor(readonly query: AllProducts) {
this._id = new SortingActionsForQuery<Product[]>('id', query);
this._name = new SortingActionsForQuery<Product[]>('name', query);
this._isRegistered = new SortingActionsForQuery<Product[]>('isRegistered', query);
}

get id(): SortingActionsForQuery<Product[]> {
return this._id;
}
get name(): SortingActionsForQuery<Product[]> {
return this._name;
}
get isRegistered(): SortingActionsForQuery<Product[]> {
return this._isRegistered;
}
}

class AllProductsSortByWithoutQuery {
private _id: SortingActions = new SortingActions('id');
private _name: SortingActions = new SortingActions('name');
private _isRegistered: SortingActions = new SortingActions('isRegistered');

get id(): SortingActions {
return this._id;
}
get name(): SortingActions {
return this._name;
}
get isRegistered(): SortingActions {
return this._isRegistered;
}
}


export class AllProducts extends QueryFor<Product[]> {
readonly route: string = '/api/products/catalog';
readonly routeTemplate: Handlebars.TemplateDelegate = routeTemplate;
readonly defaultValue: Product[] = [];
private readonly _sortBy: AllProductsSortBy;
private static readonly _sortBy: AllProductsSortByWithoutQuery = new AllProductsSortByWithoutQuery();

constructor() {
super(Product, true);
this._sortBy = new AllProductsSortBy(this);
}

get requestArguments(): string[] {
return [
];
}

get sortBy(): AllProductsSortBy {
return this._sortBy;
}

static get sortBy(): AllProductsSortByWithoutQuery {
return this._sortBy;
}

static use(): [QueryResultWithState<Product[]>, PerformQuery, SetSorting] {
return useQuery<Product[], AllProducts>(AllProducts);
Expand Down
14 changes: 14 additions & 0 deletions Samples/eCommerce/Basic/Web/Catalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useState } from 'react';

export const Catalog = withViewModel(CatalogViewModel, ({ viewModel }) => {
const [products, currentPage, perform, setSorting, setPage] = AllProducts.useWithPaging(10);
const [descending, setDescending] = useState(false);

return (
<div>
Expand All @@ -23,6 +24,19 @@ export const Catalog = withViewModel(CatalogViewModel, ({ viewModel }) => {
const page = currentPage + 1;
setPage(page);
}}>Next page</button>
<br/>

<button onClick={() => {
if (descending) {
setSorting(AllProducts.sortBy.id.ascending);
setPage(0);
setDescending(false);
} else {
setSorting(AllProducts.sortBy.id.descending);
setPage(0);
setDescending(true);
}
}}>Change sorting</button>
</div>
);
});
6 changes: 3 additions & 3 deletions Samples/eCommerce/Basic/Web/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export default defineConfig({
open: false,
proxy: {
'/api': {
target: 'http://localhost:5001',
target: 'http://localhost:5500',
ws: true
},
'/swagger': {
target: 'http://localhost:5001'
target: 'http://localhost:5500'
},
'/.cratis': {
target: 'http://localhost:5001'
target: 'http://localhost:5500'
}
}
},
Expand Down
24 changes: 24 additions & 0 deletions Source/DotNET/Applications/Commands/CommandMethodExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Reflection;
using Cratis.Reflection;
using Microsoft.AspNetCore.Mvc;

namespace Cratis.Applications.Commands;

/// <summary>
/// Extension methods for methods representing commands.
/// </summary>
public static class CommandMethodExtensions
{
/// <summary>
/// Check if a method is a command.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/> to check.</param>
/// <returns>True if it is a command, false if not.</returns>
public static bool IsCommand(this MethodInfo methodInfo) =>
methodInfo.HasAttribute<HttpPostAttribute>() &&
!methodInfo.HasAttribute<AspNetResultAttribute>() &&
(!methodInfo.DeclaringType?.HasAttribute<AspNetResultAttribute>() ?? false);
}
22 changes: 15 additions & 7 deletions Source/DotNET/Applications/Commands/CommandResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@

namespace Cratis.Applications.Commands;

#pragma warning disable SA1402 // File may only contain a single type

/// <summary>
/// Represents the result coming from executing a command.
/// </summary>
/// <typeparam name="T">Type of the data returned.</typeparam>
public class CommandResult<T>
public class CommandResult
{
/// <summary>
/// Represents a successful command result.
/// </summary>
public static readonly CommandResult<T> Success = new();

/// <summary>
/// Gets the <see cref="CorrelationId"/> associated with the command.
/// </summary>
Expand Down Expand Up @@ -56,6 +52,18 @@ public class CommandResult<T>
/// Gets the stack trace if there was an exception.
/// </summary>
public string ExceptionStackTrace { get; init; } = string.Empty;
}

/// <summary>
/// Represents the result coming from executing a command with a response.
/// </summary>
/// <typeparam name="T">Type of the data returned.</typeparam>
public class CommandResult<T> : CommandResult
{
/// <summary>
/// Represents a successful command result.
/// </summary>
public static readonly CommandResult<T> Success = new();

/// <summary>
/// Optional response object. Controller actions representing a command can optionally return a response as any type, this is where it would be.
Expand Down
10 changes: 6 additions & 4 deletions Source/DotNET/Applications/Queries/QueryActionFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,13 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE
void EstablishQueryContext(ActionExecutingContext context)
{
var sorting = Sorting.None;
var paging = Paging.NotPaged;

if (context.HttpContext.Request.Query.ContainsKey(SortByQueryStringKey) &&
context.HttpContext.Request.Query.ContainsKey(SortDirectionQueryStringKey))
{
sorting = new Sorting(
context.HttpContext.Request.Query[SortByQueryStringKey].ToString()!,
context.HttpContext.Request.Query[SortByQueryStringKey].ToString()!.ToPascalCase(),
context.HttpContext.Request.Query[SortDirectionQueryStringKey].ToString()! == "desc" ? SortDirection.Descending : SortDirection.Ascending);
}

Expand All @@ -152,10 +154,10 @@ void EstablishQueryContext(ActionExecutingContext context)
{
var page = int.Parse(context.HttpContext.Request.Query[PageQueryStringKey].ToString()!);
var pageSize = int.Parse(context.HttpContext.Request.Query[PageSizeQueryStringKey].ToString()!);

// TODO: Now it seems like query context can only be set if paging is set, but does not support only sorting.
queryContextManager.Set(new(context.HttpContext.GetCorrelationId(), new(page, pageSize, true), sorting));
paging = new(page, pageSize, true);
}

queryContextManager.Set(new(context.HttpContext.GetCorrelationId(), paging, sorting));
}

IClientEnumerableObservable CreateClientEnumerableObservableFrom(ObjectResult objectResult)
Expand Down
27 changes: 27 additions & 0 deletions Source/DotNET/Applications/Queries/QueryMethodExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Reflection;
using Cratis.Reflection;
using Microsoft.AspNetCore.Mvc;

namespace Cratis.Applications.Queries;

/// <summary>
/// Extension methods for methods representing commands.
/// </summary>
public static class QueryMethodExtensions
{
/// <summary>
/// Check if a method is a command.
/// </summary>
/// <param name="methodInfo">The <see cref="MethodInfo"/> to check.</param>
/// <returns>True if it is a command, false if not.</returns>
public static bool IsQuery(this MethodInfo methodInfo) =>
methodInfo.HasAttribute<HttpGetAttribute>() &&
methodInfo.ReturnType != typeof(void) &&
methodInfo.ReturnType != typeof(Task) &&
methodInfo.ReturnType != typeof(ValueTask) &&
!methodInfo.HasAttribute<AspNetResultAttribute>() &&
(!methodInfo.DeclaringType?.HasAttribute<AspNetResultAttribute>() ?? false);
}
13 changes: 6 additions & 7 deletions Source/DotNET/Applications/Queries/QueryableQueryProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ public class QueryableQueryProvider : IQueryProviderFor<IQueryable>
/// <inheritdoc/>
public QueryProviderResult Execute(IQueryable query, QueryContext queryContext)
{
// TODO: Do you want to count here, possibly iterating the whole queryable?
var totalItems = query.Count();

if (queryContext.Sorting != Sorting.None)
{
query = query.OrderBy(queryContext.Sorting.Field, queryContext.Sorting.Direction);
}

if (queryContext.Paging.IsPaged)
{
query = query.Skip(queryContext.Paging.Page * queryContext.Paging.Size)
.Take(queryContext.Paging.Size);

// TODO: Only supports sorting when paging is enabled, I think that we can support both independently.
if (queryContext.Sorting != Sorting.None)
{
query = query.OrderBy(queryContext.Sorting.Field, queryContext.Sorting.Direction);
}
}

return new(totalItems, query);
Expand Down
50 changes: 50 additions & 0 deletions Source/DotNET/Swagger/CommandResultOperationFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Applications.Commands;
using Cratis.Concepts;
using Cratis.Reflection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Cratis.Applications.Swagger;

/// <summary>
/// Represents an implementation of <see cref="IOperationFilter"/> that adds the command result to the operation for command methods.
/// </summary>
/// <param name="schemaGenerator">The <see cref="ISchemaGenerator"/> to use.</param>
public class CommandResultOperationFilter(ISchemaGenerator schemaGenerator) : IOperationFilter
{
/// <inheritdoc/>
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (!context.MethodInfo.IsCommand()) return;

var returnType = context.MethodInfo.GetActualReturnType();

Type? commandResultType = null;

if (returnType == typeof(Task) || returnType == typeof(ValueTask) || returnType == typeof(void))
{
returnType = typeof(object);
commandResultType = typeof(CommandResult);
}
else if (returnType.IsConcept())
{
returnType = returnType.GetConceptValueType();
}

commandResultType ??= typeof(CommandResult<>).MakeGenericType(returnType);

var schema = schemaGenerator.GenerateSchema(commandResultType, context.SchemaRepository);
var response = operation.Responses.First().Value;
if (response.Content.ContainsKey("application/json"))
{
operation.Responses.First().Value.Content["application/json"].Schema = schema;
}
else
{
response.Content.Add(new("application/json", new() { Schema = schema }));
}
}
}
3 changes: 3 additions & 0 deletions Source/DotNET/Swagger/ConceptSchemaFilter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Concepts;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models;
Expand Down
12 changes: 10 additions & 2 deletions Source/DotNET/Swagger/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.SwaggerGen;

Expand All @@ -12,5 +15,10 @@ public static class Extensions
/// Adds the <see cref="ConceptSchemaFilter"/> <see cref="ISchemaFilter"/>.
/// </summary>
/// <param name="options">The <see cref="SwaggerGenOptions"/>.</param>
public static void AddConcepts(this SwaggerGenOptions options) => options.SchemaFilter<ConceptSchemaFilter>();
}
public static void AddConcepts(this SwaggerGenOptions options)
{
options.SchemaFilter<ConceptSchemaFilter>();
options.OperationFilter<CommandResultOperationFilter>();
options.OperationFilter<QueryResultOperationFilter>();
}
}
Loading

0 comments on commit 5e93d9f

Please sign in to comment.