Browse Source

Support HTTP QUERY method

- Treat QUERY as a safe method like GET: excluded from audit logging and non-transactional UOW
- Add HTTP verb constants and Is* helpers to HttpMethodHelper
pull/25797/head
maliming 3 months ago
parent
commit
38c651767e
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 2
      docs/en/framework/api-development/auto-controllers.md
  2. 4
      docs/en/framework/architecture/domain-driven-design/unit-of-work.md
  3. 2
      docs/en/framework/infrastructure/audit-logging.md
  4. 5
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs
  5. 5
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs
  6. 6
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs
  7. 8
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AspNetCoreUnitOfWorkTransactionBehaviourProvider.cs
  8. 1
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs
  9. 1
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs
  10. 62
      framework/src/Volo.Abp.Http/Volo/Abp/Http/HttpMethodHelper.cs
  11. 14
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController_Tests.cs
  12. 11
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs
  13. 11
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Tests.cs
  14. 10
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestController.cs
  15. 8
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestPage.cshtml.cs
  16. 32
      framework/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/Uow/AspNetCoreUnitOfWorkTransactionBehaviourProvider_Tests.cs
  17. 64
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingInterceptor_HttpMethod_Tests.cs
  18. 2
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs
  19. 7
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs
  20. 8
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs
  21. 105
      framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/HttpMethodHelper_Tests.cs

2
docs/en/framework/api-development/auto-controllers.md

@ -62,6 +62,8 @@ ABP uses a naming convention while determining the HTTP method for a service met
If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service.
The naming convention doesn't map the HTTP QUERY method (a safe method that carries its parameters in the request body, useful when a GET request would have too many query string parameters). If you want to expose an action as a QUERY endpoint, use the `[AcceptVerbs("QUERY")]` attribute explicitly. Such an action is treated as a safe method, so it is not audited and doesn't start a transactional unit of work by default, just like a GET request. However, unlike a GET request, a QUERY request still requires the anti-forgery token because it carries a request body. This is consistent with ASP.NET Core, which doesn't treat QUERY as an anti-forgery exempt method.
### Route
Route is calculated based on some conventions:

4
docs/en/framework/architecture/domain-driven-design/unit-of-work.md

@ -38,10 +38,10 @@ All of these are automatically handled by the ABP.
While the section above explains the UOW as it is database transaction, actually a UOW doesn't have to be transactional. By default;
* **HTTP GET** requests don't start a transactional UOW. They still starts a UOW, but **doesn't create a database transaction**.
* **HTTP GET** and **HTTP QUERY** requests don't start a transactional UOW. They still starts a UOW, but **doesn't create a database transaction**.
* All other HTTP request types start a UOW with a database transaction, if database level transactions are supported by the underlying database provider.
This is because an HTTP GET request doesn't (and shouldn't) make any change in the database. You can change this behavior using the options explained below.
This is because they are safe HTTP methods that don't (and shouldn't) make any change in the database. You can change this behavior using the options explained below.
## Default Options

2
docs/en/framework/infrastructure/audit-logging.md

@ -48,7 +48,7 @@ Here, a list of the options you can configure:
* `IsEnabledForAnonymousUsers` (default: `true`): If you want to write audit logs only for the authenticated users, set this to `false`. If you save audit logs for anonymous users, you will see `null` for `UserId` values for these users.
* `AlwaysLogOnException` (default: `true`): If you set to true, it always saves the audit log on an exception/error case without checking other options (except `IsEnabled`, which completely disables the audit logging).
* `IsEnabledForIntegrationService` (default: `false`): Audit Logging is disabled for [integration services](../api-development/integration-services.md) by default. Set this property as `true` to enable it.
* `IsEnabledForGetRequests` (default: `false`): HTTP GET requests should not make any change in the database normally and audit log system doesn't save audit log objects for GET request. Set this to `true` to enable it also for the GET requests.
* `IsEnabledForGetRequests` (default: `false`): Safe HTTP methods (GET, HEAD and QUERY) should not make any change in the database normally and the audit log system doesn't save audit log objects for these requests. Set this to `true` to enable it also for the safe requests.
* `DisableLogActionInfo` (default: `false`):If you set to true, Will no longer log `AuditLogActionInfo`.
* `ApplicationName`: If multiple applications are saving audit logs into a single database, set this property to your application name, so you can distinguish the logs of different applications. If you don't set, it will set from the `IApplicationInfoAccessor.ApplicationName` value, which is the entry assembly name by default.
* `IgnoredTypes`: A list of `Type`s to be ignored for audit logging. If this is an entity type, changes for this type of entities will not be saved. This list is also used while serializing the action parameters.

5
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs

@ -1,5 +1,4 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -7,6 +6,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Filters;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.Threading;
using Volo.Abp.Uow;
@ -81,7 +81,8 @@ public class AbpUowActionFilter : IAsyncActionFilter, IAbpFilter, ITransientDepe
{
var abpUnitOfWorkDefaultOptions = context.GetRequiredService<IOptions<AbpUnitOfWorkDefaultOptions>>().Value;
options.IsTransactional = abpUnitOfWorkDefaultOptions.CalculateIsTransactional(
autoValue: !string.Equals(context.HttpContext.Request.Method, HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase)
autoValue: !(HttpMethodHelper.IsGet(context.HttpContext.Request.Method)
|| HttpMethodHelper.IsQuery(context.HttpContext.Request.Method))
);
}

5
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs

@ -1,5 +1,4 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -7,6 +6,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Filters;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.Threading;
using Volo.Abp.Uow;
@ -86,7 +86,8 @@ public class AbpUowPageFilter : IAsyncPageFilter, IAbpFilter, ITransientDependen
{
var abpUnitOfWorkDefaultOptions = context.GetRequiredService<IOptions<AbpUnitOfWorkDefaultOptions>>().Value;
options.IsTransactional = abpUnitOfWorkDefaultOptions.CalculateIsTransactional(
autoValue: !string.Equals(context.HttpContext.Request.Method, HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase)
autoValue: !(HttpMethodHelper.IsGet(context.HttpContext.Request.Method)
|| HttpMethodHelper.IsQuery(context.HttpContext.Request.Method))
);
}

6
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs

@ -7,6 +7,7 @@ using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Middleware;
using Volo.Abp.Auditing;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.Uow;
using Volo.Abp.Users;
@ -135,8 +136,9 @@ public class AbpAuditingMiddleware : AbpMiddlewareBase, ITransientDependency
}
if (!AuditingOptions.IsEnabledForGetRequests &&
(string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase) ||
string.Equals(httpContext.Request.Method, HttpMethods.Head, StringComparison.OrdinalIgnoreCase)))
(HttpMethodHelper.IsGet(httpContext.Request.Method) ||
HttpMethodHelper.IsHead(httpContext.Request.Method) ||
HttpMethodHelper.IsQuery(httpContext.Request.Method)))
{
return false;
}

8
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AspNetCoreUnitOfWorkTransactionBehaviourProvider.cs

@ -1,8 +1,8 @@
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.Uow;
namespace Volo.Abp.AspNetCore.Uow;
@ -37,10 +37,8 @@ public class AspNetCoreUnitOfWorkTransactionBehaviourProvider : IUnitOfWorkTrans
}
}
return !string.Equals(
httpContext.Request.Method,
HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase
);
var method = httpContext.Request.Method;
return !(HttpMethodHelper.IsGet(method) || HttpMethodHelper.IsQuery(method));
}
}

1
framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs

@ -62,6 +62,7 @@ public class AbpAuditingOptions
//TODO: Move this to asp.net core layer or convert it to a more dynamic strategy?
/// <summary>
/// Default: false.
/// When false, safe methods (GET, HEAD and QUERY) are excluded from audit logging.
/// </summary>
public bool IsEnabledForGetRequests { get; set; }

1
framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs

@ -193,6 +193,7 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
if (!options.IsEnabledForGetRequests &&
(string.Equals(auditLogInfo.HttpMethod, "Get", StringComparison.OrdinalIgnoreCase) ||
string.Equals(auditLogInfo.HttpMethod, "Head", StringComparison.OrdinalIgnoreCase) ||
string.Equals(auditLogInfo.HttpMethod, "Query", StringComparison.OrdinalIgnoreCase) ||
invocation.Method.Name.StartsWith("Get", StringComparison.OrdinalIgnoreCase)))
{
return false;

62
framework/src/Volo.Abp.Http/Volo/Abp/Http/HttpMethodHelper.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
@ -8,15 +8,25 @@ namespace Volo.Abp.Http;
public static class HttpMethodHelper
{
public const string DefaultHttpVerb = "POST";
public const string Get = "GET";
public const string Post = "POST";
public const string Put = "PUT";
public const string Delete = "DELETE";
public const string Patch = "PATCH";
public const string Head = "HEAD";
public const string Options = "OPTIONS";
public const string Trace = "TRACE";
public const string Query = "QUERY";
public const string DefaultHttpVerb = Post;
public static Dictionary<string, string[]> ConventionalPrefixes { get; set; } = new Dictionary<string, string[]>
{
{"GET", new[] {"GetList", "GetAll", "Get"}},
{"PUT", new[] {"Put", "Update"}},
{"DELETE", new[] {"Delete", "Remove"}},
{"POST", new[] {"Create", "Add", "Insert", "Post"}},
{"PATCH", new[] {"Patch"}}
{Get, new[] {"GetList", "GetAll", "Get"}},
{Put, new[] {"Put", "Update"}},
{Delete, new[] {"Delete", "Remove"}},
{Post, new[] {"Create", "Add", "Insert", "Post"}},
{Patch, new[] {"Patch"}}
};
public static string GetConventionalVerbForMethodName(string methodName)
@ -50,24 +60,44 @@ public static class HttpMethodHelper
{
switch (httpMethod?.ToUpperInvariant())
{
case "GET":
case Get:
return HttpMethod.Get;
case "POST":
case Post:
return HttpMethod.Post;
case "PUT":
case Put:
return HttpMethod.Put;
case "DELETE":
case Delete:
return HttpMethod.Delete;
case "OPTIONS":
case Options:
return HttpMethod.Options;
case "TRACE":
case Trace:
return HttpMethod.Trace;
case "HEAD":
case Head:
return HttpMethod.Head;
case "PATCH":
return new HttpMethod("PATCH");
case Patch:
return new HttpMethod(Patch);
case Query:
return new HttpMethod(Query);
default:
throw new AbpException("Unknown HTTP METHOD: " + httpMethod);
}
}
public static bool IsGet(string? httpMethod) => string.Equals(httpMethod, Get, StringComparison.OrdinalIgnoreCase);
public static bool IsPost(string? httpMethod) => string.Equals(httpMethod, Post, StringComparison.OrdinalIgnoreCase);
public static bool IsPut(string? httpMethod) => string.Equals(httpMethod, Put, StringComparison.OrdinalIgnoreCase);
public static bool IsDelete(string? httpMethod) => string.Equals(httpMethod, Delete, StringComparison.OrdinalIgnoreCase);
public static bool IsPatch(string? httpMethod) => string.Equals(httpMethod, Patch, StringComparison.OrdinalIgnoreCase);
public static bool IsHead(string? httpMethod) => string.Equals(httpMethod, Head, StringComparison.OrdinalIgnoreCase);
public static bool IsOptions(string? httpMethod) => string.Equals(httpMethod, Options, StringComparison.OrdinalIgnoreCase);
public static bool IsTrace(string? httpMethod) => string.Equals(httpMethod, Trace, StringComparison.OrdinalIgnoreCase);
public static bool IsQuery(string? httpMethod) => string.Equals(httpMethod, Query, StringComparison.OrdinalIgnoreCase);
}

14
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Auditing/AuditTestController_Tests.cs

@ -58,6 +58,20 @@ public class AuditTestController_Tests : AspNetCoreMvcTestBase
await _auditingStore.Received().DidNotReceive().SaveAsync(Arg.Any<AuditLogInfo>());
}
[Fact]
public async Task Should_Disable_AuditLog_For_Query_Requests()
{
_options.IsEnabledForGetRequests = false;
using (var requestMessage = new HttpRequestMessage(new HttpMethod("QUERY"), "api/audit-test/audit-success"))
{
var response = await Client.SendAsync(requestMessage);
response.StatusCode.ShouldBe(System.Net.HttpStatusCode.OK);
}
await _auditingStore.Received().DidNotReceive().SaveAsync(Arg.Any<AuditLogInfo>());
}
[Fact]
public async Task Should_Trigger_Middleware_And_AuditLog_Success_For_GetRequests()
{

11
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Tests.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Net.Http;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
@ -18,4 +19,12 @@ public class UnitOfWorkMiddleware_Tests : AspNetCoreMvcTestBase
var result = await Client.PostAsync("/api/unitofwork-test/ActionRequiresUowPost", null);
result.IsSuccessStatusCode.ShouldBeTrue();
}
[Fact]
public async Task Query_Actions_Should_Not_Be_Transactional()
{
using var requestMessage = new HttpRequestMessage(new HttpMethod("QUERY"), "/api/unitofwork-test/ActionRequiresUowQuery");
var result = await Client.SendAsync(requestMessage);
result.IsSuccessStatusCode.ShouldBeTrue();
}
}

11
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Tests.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Net.Http;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
@ -18,4 +19,12 @@ public class UnitOfWorkPageFilter_Tests : AspNetCoreMvcTestBase
var result = await Client.PostAsync("/Uow/UnitOfWorkTestPage?handler=RequiresUow", null);
result.IsSuccessStatusCode.ShouldBeTrue();
}
[Fact]
public async Task Query_Actions_Should_Not_Be_Transactional()
{
using var requestMessage = new HttpRequestMessage(new HttpMethod("QUERY"), "/Uow/UnitOfWorkTestPage?handler=RequiresUow");
var result = await Client.SendAsync(requestMessage);
result.IsSuccessStatusCode.ShouldBeTrue();
}
}

10
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestController.cs

@ -34,6 +34,16 @@ public class UnitOfWorkTestController : AbpController
return Content("OK");
}
[AcceptVerbs("QUERY")]
[Route("ActionRequiresUowQuery")]
public ActionResult ActionRequiresUowQuery()
{
CurrentUnitOfWork.ShouldNotBeNull();
CurrentUnitOfWork.Options.IsTransactional.ShouldBeFalse();
return Content("OK");
}
[HttpGet]
[Route("HandledException")]
[UnitOfWork(isTransactional: true)]

8
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkTestPage.cshtml.cs

@ -31,6 +31,14 @@ public class UnitOfWorkTestPage : AbpPageModel
return Content("OK");
}
public IActionResult OnQueryRequiresUow()
{
CurrentUnitOfWork.ShouldNotBeNull();
CurrentUnitOfWork.Options.IsTransactional.ShouldBeFalse();
return Content("OK");
}
[UnitOfWork(isTransactional: true)]
public ObjectResult OnGetHandledException()
{

32
framework/test/Volo.Abp.AspNetCore.Tests/Volo/Abp/AspNetCore/Uow/AspNetCoreUnitOfWorkTransactionBehaviourProvider_Tests.cs

@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
namespace Volo.Abp.AspNetCore.Uow;
public class AspNetCoreUnitOfWorkTransactionBehaviourProvider_Tests
{
private static AspNetCoreUnitOfWorkTransactionBehaviourProvider CreateProvider(string method)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = method;
return new AspNetCoreUnitOfWorkTransactionBehaviourProvider(
new HttpContextAccessor { HttpContext = httpContext },
Microsoft.Extensions.Options.Options.Create(new AspNetCoreUnitOfWorkTransactionBehaviourProviderOptions()));
}
[Theory]
[InlineData("GET", false)]
[InlineData("QUERY", false)]
[InlineData("query", false)]
[InlineData("HEAD", true)]
[InlineData("POST", true)]
[InlineData("PUT", true)]
[InlineData("DELETE", true)]
public void IsTransactional_Should_Treat_Get_And_Query_As_Non_Transactional(string method, bool expected)
{
CreateProvider(method).IsTransactional.ShouldBe(expected);
}
}

64
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AuditingInterceptor_HttpMethod_Tests.cs

@ -0,0 +1,64 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
using Xunit;
namespace Volo.Abp.Auditing;
public class AuditingInterceptor_HttpMethod_Tests : AbpAuditingTestBase
{
protected IAuditingStore AuditingStore;
private string? _httpMethod;
protected override void AfterAddApplication(IServiceCollection services)
{
AuditingStore = Substitute.For<IAuditingStore>();
services.Replace(ServiceDescriptor.Singleton(AuditingStore));
services.Configure<AbpAuditingOptions>(options =>
{
options.IsEnabledForGetRequests = false;
options.Contributors.Add(new TestHttpMethodAuditContributor(() => _httpMethod));
});
}
[Fact]
public async Task Should_Not_Write_AuditLog_For_Query_Http_Method_Without_Explicit_Scope()
{
_httpMethod = "QUERY";
var auditedObject = GetRequiredService<Auditing_Tests.MyAuditedObject1>();
await auditedObject.DoItAsync(new Auditing_Tests.InputObject { Value1 = "x", Value2 = 1 });
await AuditingStore.DidNotReceive().SaveAsync(Arg.Any<AuditLogInfo>());
}
[Fact]
public async Task Should_Write_AuditLog_For_Post_Http_Method_Without_Explicit_Scope()
{
_httpMethod = "POST";
var auditedObject = GetRequiredService<Auditing_Tests.MyAuditedObject1>();
await auditedObject.DoItAsync(new Auditing_Tests.InputObject { Value1 = "x", Value2 = 1 });
await AuditingStore.Received().SaveAsync(Arg.Any<AuditLogInfo>());
}
public class TestHttpMethodAuditContributor : AuditLogContributor
{
private readonly Func<string?> _httpMethodFactory;
public TestHttpMethodAuditContributor(Func<string?> httpMethodFactory)
{
_httpMethodFactory = httpMethodFactory;
}
public override void PreContribute(AuditLogContributionContext context)
{
context.AuditInfo.HttpMethod = _httpMethodFactory();
}
}
}

2
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs

@ -22,6 +22,8 @@ public interface IRegularTestController
Task<Car> PostObjectWithQueryAsync(Car bodyValue);
Task<Car> QueryObjectWithBodyAsync(Car bodyValue);
Task<Car> GetObjectWithUrlAsync(Car bodyValue);
Task<Car> GetObjectandIdAsync(int id, Car bodyValue);

7
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs

@ -71,6 +71,13 @@ public class RegularTestController : AbpController, IRegularTestController
return Task.FromResult(bodyValue);
}
[AcceptVerbs("QUERY")]
[Route("query-object-with-body")]
public Task<Car> QueryObjectWithBodyAsync([FromBody] Car bodyValue)
{
return Task.FromResult(bodyValue);
}
[HttpGet]
[Route("post-object-with-url/bodyValue")]
public Task<Car> GetObjectWithUrlAsync(Car bodyValue)

8
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs

@ -97,6 +97,14 @@ public class RegularTestControllerClientProxy_Tests : AbpHttpClientTestBase
result.Model.ShouldBe("Ford");
}
[Fact]
public async Task QueryObjectWithBodyAsync()
{
var result = await _controller.QueryObjectWithBodyAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
[Fact]
public async Task PostObjectWithQueryAsync_With_Different_Culture()
{

105
framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/HttpMethodHelper_Tests.cs

@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using Shouldly;
using Xunit;
namespace Volo.Abp.Http;
public class HttpMethodHelper_Tests
{
private static readonly Dictionary<string, Func<string?, bool>> Predicates = new()
{
[HttpMethodHelper.Get] = HttpMethodHelper.IsGet,
[HttpMethodHelper.Post] = HttpMethodHelper.IsPost,
[HttpMethodHelper.Put] = HttpMethodHelper.IsPut,
[HttpMethodHelper.Delete] = HttpMethodHelper.IsDelete,
[HttpMethodHelper.Patch] = HttpMethodHelper.IsPatch,
[HttpMethodHelper.Head] = HttpMethodHelper.IsHead,
[HttpMethodHelper.Options] = HttpMethodHelper.IsOptions,
[HttpMethodHelper.Trace] = HttpMethodHelper.IsTrace,
[HttpMethodHelper.Query] = HttpMethodHelper.IsQuery
};
[Theory]
[InlineData(HttpMethodHelper.Get)]
[InlineData(HttpMethodHelper.Post)]
[InlineData(HttpMethodHelper.Put)]
[InlineData(HttpMethodHelper.Delete)]
[InlineData(HttpMethodHelper.Patch)]
[InlineData(HttpMethodHelper.Head)]
[InlineData(HttpMethodHelper.Options)]
[InlineData(HttpMethodHelper.Trace)]
[InlineData(HttpMethodHelper.Query)]
public void Is_Predicates_Should_Match_Only_Their_Own_Verb_Ignoring_Case(string verb)
{
foreach (var (name, predicate) in Predicates)
{
var shouldMatch = name == verb;
predicate(verb).ShouldBe(shouldMatch);
predicate(verb.ToLowerInvariant()).ShouldBe(shouldMatch);
}
}
[Fact]
public void Is_Predicates_Should_Return_False_For_Null()
{
foreach (var predicate in Predicates.Values)
{
predicate(null).ShouldBeFalse();
}
}
[Theory]
[InlineData("QUERY", true)]
[InlineData("query", true)]
[InlineData("Query", true)]
[InlineData("GET", false)]
[InlineData("POST", false)]
[InlineData("", false)]
[InlineData(null, false)]
public void IsQuery_Should_Match_Query_Method_Ignoring_Case(string? httpMethod, bool expected)
{
HttpMethodHelper.IsQuery(httpMethod).ShouldBe(expected);
}
[Fact]
public void ConvertToHttpMethod_Should_Support_Query()
{
HttpMethodHelper.ConvertToHttpMethod("QUERY").Method.ShouldBe("QUERY");
HttpMethodHelper.ConvertToHttpMethod("query").Method.ShouldBe("QUERY");
}
[Theory]
[InlineData("GET")]
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("DELETE")]
[InlineData("PATCH")]
[InlineData("QUERY")]
public void ConvertToHttpMethod_Should_Not_Throw_For_Known_Methods(string httpMethod)
{
Should.NotThrow(() => HttpMethodHelper.ConvertToHttpMethod(httpMethod));
}
[Fact]
public void ConvertToHttpMethod_Should_Throw_For_Unknown_Method()
{
Should.Throw<AbpException>(() => HttpMethodHelper.ConvertToHttpMethod("UNKNOWN"));
}
[Theory]
[InlineData("GetFooAsync", "GET")]
[InlineData("GetListAsync", "GET")]
[InlineData("CreateFooAsync", "POST")]
[InlineData("UpdateFooAsync", "PUT")]
[InlineData("DeleteFooAsync", "DELETE")]
[InlineData("PatchFooAsync", "PATCH")]
[InlineData("DoSomethingAsync", "POST")]
// QUERY is intentionally NOT a naming convention: an action must opt in explicitly
// with [AcceptVerbs("QUERY")]. A method named Query* still maps to the default verb.
[InlineData("QueryFooAsync", "POST")]
public void GetConventionalVerbForMethodName_Should_Not_Map_Query_By_Name(string methodName, string expectedVerb)
{
HttpMethodHelper.GetConventionalVerbForMethodName(methodName).ShouldBe(expectedVerb);
}
}
Loading…
Cancel
Save