diff --git a/appveyor.yml b/appveyor.yml index 1f4e400..b6281f1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: '0.17.1+{build}' +version: '0.17.2+{build}' configuration: Release image: Visual Studio 2017 diff --git a/src/AspNetCore.Client.BlazorJson/BlazorSimpleJsonSerializer.cs b/src/AspNetCore.Client.BlazorJson/BlazorSimpleJsonSerializer.cs index a444449..ae2d513 100644 --- a/src/AspNetCore.Client.BlazorJson/BlazorSimpleJsonSerializer.cs +++ b/src/AspNetCore.Client.BlazorJson/BlazorSimpleJsonSerializer.cs @@ -14,7 +14,8 @@ namespace AspNetCore.Client.Serializers internal class BlazorSimpleJsonSerializer : IHttpContentSerializer { internal static readonly string CONTENT_TYPE = "application/json"; - public string ContentType => CONTENT_TYPE; + internal static readonly string PROBLEM_TYPE = "application/problem+json"; + public string[] ContentTypes => new string[] { CONTENT_TYPE, PROBLEM_TYPE }; private static readonly IDictionary> _knownJsonPrimitives = new Dictionary> { @@ -67,7 +68,7 @@ public HttpContent Serialize(T request) //Can't use the same stream writing as AspNetCore.Client.Serializers.JsonHttpSerializer because Blazor's json doesn't expose those AFAIK var json = Json.Serialize(request); - return new StringContent(json, Encoding.UTF8, ContentType); + return new StringContent(json, Encoding.UTF8, CONTENT_TYPE); } } } diff --git a/src/AspNetCore.Client.Generator.Framework/AspNetCoreHttp/AspNetCoreHttpEndpoint.cs b/src/AspNetCore.Client.Generator.Framework/AspNetCoreHttp/AspNetCoreHttpEndpoint.cs index dd66116..417f4f7 100644 --- a/src/AspNetCore.Client.Generator.Framework/AspNetCoreHttp/AspNetCoreHttpEndpoint.cs +++ b/src/AspNetCore.Client.Generator.Framework/AspNetCoreHttp/AspNetCoreHttpEndpoint.cs @@ -198,7 +198,34 @@ public IEnumerable
GetHeaders() /// public IEnumerable GetResponseTypes() { - return GetChildren().OfType().OrderBy(x => x.SortOrder); + var responseTypes = GetChildren().OfType(); + + var groupedResponses = responseTypes.GroupBy(x => x.Status); + + List prioritizedResponseTypes = new List(); + + foreach (var responseGroup in groupedResponses) + { + if (responseGroup.Count() > 1) + { + if (ResponseTypes.Count(x => x.Status == responseGroup.Key) == 1) + { + var endpointResponse = ResponseTypes.SingleOrDefault(x => x.Status == responseGroup.Key); + prioritizedResponseTypes.Add(endpointResponse); + } + else + { + //This will fail, we will let it. + prioritizedResponseTypes.AddRange(responseGroup); + } + } + else + { + prioritizedResponseTypes.Add(responseGroup.Single()); + } + } + + return prioritizedResponseTypes.OrderBy(x => x.SortOrder); } /// diff --git a/src/AspNetCore.Client.Generator/Helpers.cs b/src/AspNetCore.Client.Generator/Helpers.cs index e3b50b1..987a216 100644 --- a/src/AspNetCore.Client.Generator/Helpers.cs +++ b/src/AspNetCore.Client.Generator/Helpers.cs @@ -9,7 +9,10 @@ using System.Threading; using System.Threading.Tasks; using AspNetCore.Client.Generator.CSharp.AspNetCoreHttp; +using AspNetCore.Client.Generator.Framework; +using AspNetCore.Client.Generator.Framework.AspNetCoreHttp.ResponseTypes; using AspNetCore.Client.Generator.Framework.AspNetCoreHttp.Routes; +using AspNetCore.Client.Generator.Framework.Navigation; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing.Template; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -37,6 +40,38 @@ internal static class Helpers return source.Where(x => !typeof(K).IsAssignableFrom(x.GetType())); } + public static IEnumerable FilterResponseTypes(this IEnumerable source, IEnumerable priorityResponseTypes) + { + var responseTypes = source.OfType(); + + var groupedResponses = responseTypes.GroupBy(x => x.Status); + + List prioritizedResponseTypes = new List(); + + foreach (var responseGroup in groupedResponses) + { + if (responseGroup.Count() > 1) + { + if (priorityResponseTypes.Count(x => x.Status == responseGroup.Key) == 1) + { + var endpointResponse = priorityResponseTypes.SingleOrDefault(x => x.Status == responseGroup.Key); + prioritizedResponseTypes.Add(endpointResponse); + } + else + { + //This will fail, we will let it. + prioritizedResponseTypes.AddRange(responseGroup); + } + } + else + { + prioritizedResponseTypes.Add(responseGroup.Single()); + } + } + + return source.NotOfType().Union(prioritizedResponseTypes).OrderBy(x => x.DefaultValue == null ? 0 : 1).ThenBy(x => x.SortOrder); + } + public static HttpMethod HttpMethodFromEnum(HttpAttributeType type) { switch (type) diff --git a/src/AspNetCore.Client.Generator/Output/ClassWriter.cs b/src/AspNetCore.Client.Generator/Output/ClassWriter.cs index 1a348c2..117bdd1 100644 --- a/src/AspNetCore.Client.Generator/Output/ClassWriter.cs +++ b/src/AspNetCore.Client.Generator/Output/ClassWriter.cs @@ -617,7 +617,7 @@ public static string WriteEndpointInterface(AspNetCoreHttpEndpoint endpoint) {SharedWriter.GetObsolete(endpoint)} {SharedWriter.GetInterfaceReturnType(endpoint.ReturnType, false)} {endpoint.Name} ( -{string.Join($",{Environment.NewLine}", endpoint.GetParameters().Select(SharedWriter.GetParameter).NotNull())} +{string.Join($",{Environment.NewLine}", endpoint.GetParameters().FilterResponseTypes(endpoint.ResponseTypes).Select(SharedWriter.GetParameter).NotNull())} ); {SharedWriter.GetObsolete(endpoint)} @@ -629,7 +629,7 @@ public static string WriteEndpointInterface(AspNetCoreHttpEndpoint endpoint) {SharedWriter.GetObsolete(endpoint)} {SharedWriter.GetInterfaceReturnType(endpoint.ReturnType, true)} {endpoint.Name}Async ( -{string.Join($",{Environment.NewLine}", endpoint.GetParameters().Select(SharedWriter.GetParameter).NotNull())} +{string.Join($",{Environment.NewLine}", endpoint.GetParameters().FilterResponseTypes(endpoint.ResponseTypes).Select(SharedWriter.GetParameter).NotNull())} ); {SharedWriter.GetObsolete(endpoint)} @@ -649,7 +649,7 @@ public static string WriteEndpointImplementation(AspNetCoreHttpController contro {SharedWriter.GetObsolete(endpoint)} public {SharedWriter.GetImplementationReturnType(endpoint.ReturnType, false)} {endpoint.Name} ( -{string.Join($",{Environment.NewLine}", endpoint.GetParameters().Select(SharedWriter.GetParameter).NotNull())} +{string.Join($",{Environment.NewLine}", endpoint.GetParameters().FilterResponseTypes(endpoint.ResponseTypes).Select(SharedWriter.GetParameter).NotNull())} ) {{ {GetMethodDetails(controller, endpoint, false, false)} @@ -667,7 +667,7 @@ public static string WriteEndpointImplementation(AspNetCoreHttpController contro {SharedWriter.GetObsolete(endpoint)} public {SharedWriter.GetImplementationReturnType(endpoint.ReturnType, true)} {endpoint.Name}Async ( -{string.Join($",{Environment.NewLine}", endpoint.GetParameters().Select(SharedWriter.GetParameter).NotNull())} +{string.Join($",{Environment.NewLine}", endpoint.GetParameters().FilterResponseTypes(endpoint.ResponseTypes).Select(SharedWriter.GetParameter).NotNull())} ) {{ {GetMethodDetails(controller, endpoint, true, false)} diff --git a/src/AspNetCore.Client.JSInterop/JSInteropJsonSerializer.cs b/src/AspNetCore.Client.JSInterop/JSInteropJsonSerializer.cs index e45835e..c26a679 100644 --- a/src/AspNetCore.Client.JSInterop/JSInteropJsonSerializer.cs +++ b/src/AspNetCore.Client.JSInterop/JSInteropJsonSerializer.cs @@ -14,7 +14,8 @@ namespace AspNetCore.Client.Serializers internal class JSInteropJsonSerializer : IHttpContentSerializer { internal static readonly string CONTENT_TYPE = "application/json"; - public string ContentType => CONTENT_TYPE; + internal static readonly string PROBLEM_TYPE = "application/problem+json"; + public string[] ContentTypes => new string[] { CONTENT_TYPE, PROBLEM_TYPE }; private static readonly IDictionary> _knownJsonPrimitives = new Dictionary> { @@ -67,7 +68,7 @@ public HttpContent Serialize(T request) //Can't use the same stream writing as AspNetCore.Client.Serializers.JsonHttpSerializer because Blazor's json doesn't expose those AFAIK var json = Json.Serialize(request); - return new StringContent(json, Encoding.UTF8, ContentType); + return new StringContent(json, Encoding.UTF8, CONTENT_TYPE); } } } diff --git a/src/AspNetCore.Client.MessagePack/MessagePackSerializer.cs b/src/AspNetCore.Client.MessagePack/MessagePackSerializer.cs index f02eedc..af16e47 100644 --- a/src/AspNetCore.Client.MessagePack/MessagePackSerializer.cs +++ b/src/AspNetCore.Client.MessagePack/MessagePackSerializer.cs @@ -13,7 +13,7 @@ namespace AspNetCore.Client.Serializers internal class MessagePackSerializer : IHttpContentSerializer { internal static readonly string CONTENT_TYPE = "application/x-msgpack"; - public string ContentType => CONTENT_TYPE; + public string[] ContentTypes => new string[] { CONTENT_TYPE }; /// /// Deserializes the request content which is assumed to be MessagePack into a object of @@ -36,7 +36,7 @@ public HttpContent Serialize(T request) { var data = MessagePack.MessagePackSerializer.Serialize(request, ContractlessStandardResolver.Instance); var content = new ByteArrayContent(data); - content.Headers.ContentType = new MediaTypeHeaderValue(ContentType); + content.Headers.ContentType = new MediaTypeHeaderValue(CONTENT_TYPE); content.Headers.ContentLength = data.Length; return content; diff --git a/src/AspNetCore.Client.Protobuf/ProtobufSerializer.cs b/src/AspNetCore.Client.Protobuf/ProtobufSerializer.cs index 61d1ae1..0c07c70 100644 --- a/src/AspNetCore.Client.Protobuf/ProtobufSerializer.cs +++ b/src/AspNetCore.Client.Protobuf/ProtobufSerializer.cs @@ -13,7 +13,7 @@ namespace AspNetCore.Client.Serializers internal class ProtobufSerializer : IHttpContentSerializer { internal static readonly string CONTENT_TYPE = "application/x-protobuf"; - public string ContentType => CONTENT_TYPE; + public string[] ContentTypes => new string[] { CONTENT_TYPE }; /// /// Deserializes the request content which is assumed to be protobuf into a object of @@ -37,7 +37,7 @@ public HttpContent Serialize(T request) var stream = new MemoryStream(); Serializer.Serialize(stream, request); var content = new StreamContent(stream); - content.Headers.ContentType = new MediaTypeHeaderValue(ContentType); + content.Headers.ContentType = new MediaTypeHeaderValue(CONTENT_TYPE); return content; } } diff --git a/src/AspNetCore.Client/Serializers/HttpSerializer.cs b/src/AspNetCore.Client/Serializers/HttpSerializer.cs index 17d92e9..8bcc717 100644 --- a/src/AspNetCore.Client/Serializers/HttpSerializer.cs +++ b/src/AspNetCore.Client/Serializers/HttpSerializer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -27,13 +28,23 @@ public HttpSerializer(IServiceProvider provider, ClientConfiguration config) _provider = provider; _config = config; - Serializer = (IHttpContentSerializer)_provider.GetService(config.Serializer); + Serializer = (IHttpContentSerializer)_provider.GetService(_config.Serializer); Deserializers = new Dictionary(); foreach (var serType in _config.Deserializers) { var ser = (IHttpContentSerializer)_provider.GetService(serType); - Deserializers.Add(ser.ContentType, ser); + foreach (var contentType in ser.ContentTypes ?? Enumerable.Empty()) + { + if (Deserializers.ContainsKey(contentType)) + { + Deserializers[contentType] = ser; + } + else + { + Deserializers.Add(contentType, ser); + } + } } } diff --git a/src/AspNetCore.Client/Serializers/IHttpContentSerializer.cs b/src/AspNetCore.Client/Serializers/IHttpContentSerializer.cs index e075b92..7fa5880 100644 --- a/src/AspNetCore.Client/Serializers/IHttpContentSerializer.cs +++ b/src/AspNetCore.Client/Serializers/IHttpContentSerializer.cs @@ -10,9 +10,9 @@ namespace AspNetCore.Client.Serializers public interface IHttpContentSerializer { /// - /// Content-Type that this Serializer can parse + /// Content-Types that this can deserialize /// - string ContentType { get; } + string[] ContentTypes { get; } /// /// Deserializes the content of the http response into the type provided diff --git a/src/AspNetCore.Client/Serializers/JsonHttpSerializer.cs b/src/AspNetCore.Client/Serializers/JsonHttpSerializer.cs index 09e6e09..031fd54 100644 --- a/src/AspNetCore.Client/Serializers/JsonHttpSerializer.cs +++ b/src/AspNetCore.Client/Serializers/JsonHttpSerializer.cs @@ -15,7 +15,8 @@ namespace AspNetCore.Client.Serializers internal class JsonHttpSerializer : IHttpContentSerializer { internal static readonly string CONTENT_TYPE = "application/json"; - public string ContentType => CONTENT_TYPE; + internal static readonly string PROBLEM_TYPE = "application/problem+json"; + public string[] ContentTypes => new string[] { CONTENT_TYPE, PROBLEM_TYPE }; private static readonly IDictionary> _knownJsonPrimitives = new Dictionary> @@ -69,7 +70,7 @@ public async Task Deserialize(HttpContent content) public HttpContent Serialize(T request) { var json = JsonConvert.SerializeObject(request); - return new StringContent(json, Encoding.UTF8, ContentType); + return new StringContent(json, Encoding.UTF8, CONTENT_TYPE); } } } diff --git a/src/AspNetCore.Client/Serializers/TextHttpSerializer.cs b/src/AspNetCore.Client/Serializers/TextHttpSerializer.cs index 0947d56..7d866b2 100644 --- a/src/AspNetCore.Client/Serializers/TextHttpSerializer.cs +++ b/src/AspNetCore.Client/Serializers/TextHttpSerializer.cs @@ -14,7 +14,7 @@ namespace AspNetCore.Client.Serializers internal class TextHttpSerializer : IHttpContentSerializer { internal static readonly string CONTENT_TYPE = "text/plain"; - public string ContentType => CONTENT_TYPE; + public string[] ContentTypes => new string[] { CONTENT_TYPE }; /// diff --git a/test/TestWebApp.Clients/Clients.cs b/test/TestWebApp.Clients/Clients.cs index db34863..adb7e11 100644 --- a/test/TestWebApp.Clients/Clients.cs +++ b/test/TestWebApp.Clients/Clients.cs @@ -18,6 +18,8 @@ using Flurl.Http; using Microsoft.AspNetCore.Http.Connections.Client; using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection; @@ -1855,6 +1857,14 @@ public interface IValuesClient : ITestWebAppClient HttpResponseMessage DuplicateMethodReturnAndResponseRaw(int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); ValueTask DuplicateMethodReturnAndResponseAsync(int ControllerHeader = 0, Action BadRequestCallback = null, Action InternalServerErrorCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); ValueTask DuplicateMethodReturnAndResponseRawAsync(int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + void ProblemDetailsRequest(RequiredDto dto, int ControllerHeader = 0, Action BadRequestCallback = null, Action InternalServerErrorCallback = null, Action OKCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + HttpResponseMessage ProblemDetailsRequestRaw(RequiredDto dto, int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + Task ProblemDetailsRequestAsync(RequiredDto dto, int ControllerHeader = 0, Action BadRequestCallback = null, Action InternalServerErrorCallback = null, Action OKCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + ValueTask ProblemDetailsRequestRawAsync(RequiredDto dto, int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + void ModelStateBadRequest(int ControllerHeader = 0, Action>> BadRequestCallback = null, Action InternalServerErrorCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + HttpResponseMessage ModelStateBadRequestRaw(int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + Task ModelStateBadRequestAsync(int ControllerHeader = 0, Action>> BadRequestCallback = null, Action InternalServerErrorCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); + ValueTask ModelStateBadRequestRawAsync(int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default); } internal class ValuesClient : IValuesClient @@ -11828,6 +11838,530 @@ public async ValueTask DuplicateMethodReturnAndResponseRawA return response; } + + public void ProblemDetailsRequest(RequiredDto dto, int ControllerHeader = 0, Action BadRequestCallback = null, Action InternalServerErrorCallback = null, Action OKCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var controller = "Values"; + var action = "ProblemDetailsRequest"; + string url = $@"api/{controller}/{action}"; + HttpResponseMessage response = null; + response = HttpOverride.GetResponseAsync(HttpMethod.Post, url, null, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + bool responseHandled = response != null; + if (response == null) + { + try + { + response = Client.ClientWrapper.Request(url).WithHeader("Test", "EXTRA").WithHeader("ControllerHeader", ControllerHeader).WithRequestModifiers(Modifier).WithCookies(cookies).WithHeaders(headers).WithTimeout(timeout ?? Client.Timeout).AllowAnyHttpStatus().PostAsync(Serializer.Serialize(dto), cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (FlurlHttpException fhex) + { + if (ExceptionCallback != null && ExceptionCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ExceptionCallback are not supported.As they will run out of the scope of this call."); + } + + if (ExceptionCallback != null) + { + responseHandled = true; + ExceptionCallback?.Invoke(fhex); + } + else + { + throw fhex; + } + + return; + } + + HttpOverride.OnNonOverridedResponseAsync(HttpMethod.Post, url, dto, response, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + if (BadRequestCallback != null && BadRequestCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for BadRequestCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + if (BadRequestCallback != null) + { + responseHandled = true; + BadRequestCallback.Invoke(Serializer.Deserialize(response.Content).ConfigureAwait(false).GetAwaiter().GetResult()); + } + } + + if (InternalServerErrorCallback != null && InternalServerErrorCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for InternalServerErrorCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + { + if (InternalServerErrorCallback != null) + { + responseHandled = true; + InternalServerErrorCallback.Invoke(); + } + } + + if (OKCallback != null && OKCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for OKCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + if (OKCallback != null) + { + responseHandled = true; + OKCallback.Invoke(); + } + } + + if (ResponseCallback != null && ResponseCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ResponseCallback are not supported.As they will run out of the scope of this call."); + } + + if (ResponseCallback != null) + { + responseHandled = true; + ResponseCallback.Invoke(response); + } + + if (!responseHandled) + { + throw new System.InvalidOperationException($"Response Status of {response.StatusCode} was not handled properly."); + } + + return; + } + + public HttpResponseMessage ProblemDetailsRequestRaw(RequiredDto dto, int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var controller = "Values"; + var action = "ProblemDetailsRequest"; + string url = $@"api/{controller}/{action}"; + HttpResponseMessage response = null; + response = HttpOverride.GetResponseAsync(HttpMethod.Post, url, null, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + bool responseHandled = response != null; + if (response == null) + { + try + { + response = Client.ClientWrapper.Request(url).WithHeader("Test", "EXTRA").WithHeader("ControllerHeader", ControllerHeader).WithRequestModifiers(Modifier).WithCookies(cookies).WithHeaders(headers).WithTimeout(timeout ?? Client.Timeout).AllowAnyHttpStatus().PostAsync(Serializer.Serialize(dto), cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (FlurlHttpException fhex) + { + if (ExceptionCallback != null && ExceptionCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ExceptionCallback are not supported.As they will run out of the scope of this call."); + } + + if (ExceptionCallback != null) + { + responseHandled = true; + ExceptionCallback?.Invoke(fhex); + } + else + { + throw fhex; + } + + return null; + } + + HttpOverride.OnNonOverridedResponseAsync(HttpMethod.Post, url, dto, response, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + return response; + } + + public async Task ProblemDetailsRequestAsync(RequiredDto dto, int ControllerHeader = 0, Action BadRequestCallback = null, Action InternalServerErrorCallback = null, Action OKCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var controller = "Values"; + var action = "ProblemDetailsRequest"; + string url = $@"api/{controller}/{action}"; + HttpResponseMessage response = null; + response = await HttpOverride.GetResponseAsync(HttpMethod.Post, url, null, cancellationToken).ConfigureAwait(false); + bool responseHandled = response != null; + if (response == null) + { + try + { + response = await Client.ClientWrapper.Request(url).WithHeader("Test", "EXTRA").WithHeader("ControllerHeader", ControllerHeader).WithRequestModifiers(Modifier).WithCookies(cookies).WithHeaders(headers).WithTimeout(timeout ?? Client.Timeout).AllowAnyHttpStatus().PostAsync(Serializer.Serialize(dto), cancellationToken).ConfigureAwait(false); + } + catch (FlurlHttpException fhex) + { + if (ExceptionCallback != null && ExceptionCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ExceptionCallback are not supported.As they will run out of the scope of this call."); + } + + if (ExceptionCallback != null) + { + responseHandled = true; + ExceptionCallback?.Invoke(fhex); + } + else + { + throw fhex; + } + + return; + } + + await HttpOverride.OnNonOverridedResponseAsync(HttpMethod.Post, url, dto, response, cancellationToken).ConfigureAwait(false); + } + + if (BadRequestCallback != null && BadRequestCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for BadRequestCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + if (BadRequestCallback != null) + { + responseHandled = true; + BadRequestCallback.Invoke(await Serializer.Deserialize(response.Content).ConfigureAwait(false)); + } + } + + if (InternalServerErrorCallback != null && InternalServerErrorCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for InternalServerErrorCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + { + if (InternalServerErrorCallback != null) + { + responseHandled = true; + InternalServerErrorCallback.Invoke(); + } + } + + if (OKCallback != null && OKCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for OKCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + if (OKCallback != null) + { + responseHandled = true; + OKCallback.Invoke(); + } + } + + if (ResponseCallback != null && ResponseCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ResponseCallback are not supported.As they will run out of the scope of this call."); + } + + if (ResponseCallback != null) + { + responseHandled = true; + ResponseCallback.Invoke(response); + } + + if (!responseHandled) + { + throw new System.InvalidOperationException($"Response Status of {response.StatusCode} was not handled properly."); + } + + return; + } + + public async ValueTask ProblemDetailsRequestRawAsync(RequiredDto dto, int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var controller = "Values"; + var action = "ProblemDetailsRequest"; + string url = $@"api/{controller}/{action}"; + HttpResponseMessage response = null; + response = await HttpOverride.GetResponseAsync(HttpMethod.Post, url, null, cancellationToken).ConfigureAwait(false); + bool responseHandled = response != null; + if (response == null) + { + try + { + response = await Client.ClientWrapper.Request(url).WithHeader("Test", "EXTRA").WithHeader("ControllerHeader", ControllerHeader).WithRequestModifiers(Modifier).WithCookies(cookies).WithHeaders(headers).WithTimeout(timeout ?? Client.Timeout).AllowAnyHttpStatus().PostAsync(Serializer.Serialize(dto), cancellationToken).ConfigureAwait(false); + } + catch (FlurlHttpException fhex) + { + if (ExceptionCallback != null && ExceptionCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ExceptionCallback are not supported.As they will run out of the scope of this call."); + } + + if (ExceptionCallback != null) + { + responseHandled = true; + ExceptionCallback?.Invoke(fhex); + } + else + { + throw fhex; + } + + return null; + } + + await HttpOverride.OnNonOverridedResponseAsync(HttpMethod.Post, url, dto, response, cancellationToken).ConfigureAwait(false); + } + + return response; + } + + public void ModelStateBadRequest(int ControllerHeader = 0, Action>> BadRequestCallback = null, Action InternalServerErrorCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var controller = "Values"; + var action = "ModelStateBadRequest"; + string url = $@"api/{controller}/{action}"; + HttpResponseMessage response = null; + response = HttpOverride.GetResponseAsync(HttpMethod.Get, url, null, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + bool responseHandled = response != null; + if (response == null) + { + try + { + response = Client.ClientWrapper.Request(url).WithHeader("Test", "EXTRA").WithHeader("ControllerHeader", ControllerHeader).WithRequestModifiers(Modifier).WithCookies(cookies).WithHeaders(headers).WithTimeout(timeout ?? Client.Timeout).AllowAnyHttpStatus().GetAsync(cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (FlurlHttpException fhex) + { + if (ExceptionCallback != null && ExceptionCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ExceptionCallback are not supported.As they will run out of the scope of this call."); + } + + if (ExceptionCallback != null) + { + responseHandled = true; + ExceptionCallback?.Invoke(fhex); + } + else + { + throw fhex; + } + + return; + } + + HttpOverride.OnNonOverridedResponseAsync(HttpMethod.Get, url, null, response, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + if (BadRequestCallback != null && BadRequestCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for BadRequestCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + if (BadRequestCallback != null) + { + responseHandled = true; + BadRequestCallback.Invoke(Serializer.Deserialize>>(response.Content).ConfigureAwait(false).GetAwaiter().GetResult()); + } + } + + if (InternalServerErrorCallback != null && InternalServerErrorCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for InternalServerErrorCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + { + if (InternalServerErrorCallback != null) + { + responseHandled = true; + InternalServerErrorCallback.Invoke(); + } + } + + if (ResponseCallback != null && ResponseCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ResponseCallback are not supported.As they will run out of the scope of this call."); + } + + if (ResponseCallback != null) + { + responseHandled = true; + ResponseCallback.Invoke(response); + } + + if (!responseHandled) + { + throw new System.InvalidOperationException($"Response Status of {response.StatusCode} was not handled properly."); + } + + return; + } + + public HttpResponseMessage ModelStateBadRequestRaw(int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var controller = "Values"; + var action = "ModelStateBadRequest"; + string url = $@"api/{controller}/{action}"; + HttpResponseMessage response = null; + response = HttpOverride.GetResponseAsync(HttpMethod.Get, url, null, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + bool responseHandled = response != null; + if (response == null) + { + try + { + response = Client.ClientWrapper.Request(url).WithHeader("Test", "EXTRA").WithHeader("ControllerHeader", ControllerHeader).WithRequestModifiers(Modifier).WithCookies(cookies).WithHeaders(headers).WithTimeout(timeout ?? Client.Timeout).AllowAnyHttpStatus().GetAsync(cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (FlurlHttpException fhex) + { + if (ExceptionCallback != null && ExceptionCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ExceptionCallback are not supported.As they will run out of the scope of this call."); + } + + if (ExceptionCallback != null) + { + responseHandled = true; + ExceptionCallback?.Invoke(fhex); + } + else + { + throw fhex; + } + + return null; + } + + HttpOverride.OnNonOverridedResponseAsync(HttpMethod.Get, url, null, response, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + return response; + } + + public async Task ModelStateBadRequestAsync(int ControllerHeader = 0, Action>> BadRequestCallback = null, Action InternalServerErrorCallback = null, Action ResponseCallback = null, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var controller = "Values"; + var action = "ModelStateBadRequest"; + string url = $@"api/{controller}/{action}"; + HttpResponseMessage response = null; + response = await HttpOverride.GetResponseAsync(HttpMethod.Get, url, null, cancellationToken).ConfigureAwait(false); + bool responseHandled = response != null; + if (response == null) + { + try + { + response = await Client.ClientWrapper.Request(url).WithHeader("Test", "EXTRA").WithHeader("ControllerHeader", ControllerHeader).WithRequestModifiers(Modifier).WithCookies(cookies).WithHeaders(headers).WithTimeout(timeout ?? Client.Timeout).AllowAnyHttpStatus().GetAsync(cancellationToken).ConfigureAwait(false); + } + catch (FlurlHttpException fhex) + { + if (ExceptionCallback != null && ExceptionCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ExceptionCallback are not supported.As they will run out of the scope of this call."); + } + + if (ExceptionCallback != null) + { + responseHandled = true; + ExceptionCallback?.Invoke(fhex); + } + else + { + throw fhex; + } + + return; + } + + await HttpOverride.OnNonOverridedResponseAsync(HttpMethod.Get, url, null, response, cancellationToken).ConfigureAwait(false); + } + + if (BadRequestCallback != null && BadRequestCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for BadRequestCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + if (BadRequestCallback != null) + { + responseHandled = true; + BadRequestCallback.Invoke(await Serializer.Deserialize>>(response.Content).ConfigureAwait(false)); + } + } + + if (InternalServerErrorCallback != null && InternalServerErrorCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for InternalServerErrorCallback are not supported.As they will run out of the scope of this call."); + } + + if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + { + if (InternalServerErrorCallback != null) + { + responseHandled = true; + InternalServerErrorCallback.Invoke(); + } + } + + if (ResponseCallback != null && ResponseCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ResponseCallback are not supported.As they will run out of the scope of this call."); + } + + if (ResponseCallback != null) + { + responseHandled = true; + ResponseCallback.Invoke(response); + } + + if (!responseHandled) + { + throw new System.InvalidOperationException($"Response Status of {response.StatusCode} was not handled properly."); + } + + return; + } + + public async ValueTask ModelStateBadRequestRawAsync(int ControllerHeader = 0, Action ExceptionCallback = null, IDictionary headers = null, IEnumerable cookies = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + var controller = "Values"; + var action = "ModelStateBadRequest"; + string url = $@"api/{controller}/{action}"; + HttpResponseMessage response = null; + response = await HttpOverride.GetResponseAsync(HttpMethod.Get, url, null, cancellationToken).ConfigureAwait(false); + bool responseHandled = response != null; + if (response == null) + { + try + { + response = await Client.ClientWrapper.Request(url).WithHeader("Test", "EXTRA").WithHeader("ControllerHeader", ControllerHeader).WithRequestModifiers(Modifier).WithCookies(cookies).WithHeaders(headers).WithTimeout(timeout ?? Client.Timeout).AllowAnyHttpStatus().GetAsync(cancellationToken).ConfigureAwait(false); + } + catch (FlurlHttpException fhex) + { + if (ExceptionCallback != null && ExceptionCallback.Method.IsDefined(typeof(AsyncStateMachineAttribute), true)) + { + throw new NotSupportedException("Async void action delegates for ExceptionCallback are not supported.As they will run out of the scope of this call."); + } + + if (ExceptionCallback != null) + { + responseHandled = true; + ExceptionCallback?.Invoke(fhex); + } + else + { + throw fhex; + } + + return null; + } + + await HttpOverride.OnNonOverridedResponseAsync(HttpMethod.Get, url, null, response, cancellationToken).ConfigureAwait(false); + } + + return response; + } } } diff --git a/test/TestWebApp.Clients/TestWebApp.Clients.csproj b/test/TestWebApp.Clients/TestWebApp.Clients.csproj index cb46084..9d95303 100644 --- a/test/TestWebApp.Clients/TestWebApp.Clients.csproj +++ b/test/TestWebApp.Clients/TestWebApp.Clients.csproj @@ -23,12 +23,14 @@ false TestWebApp.Clients TestWebApp.Hubs - $(AllowedNamespaces);TestWebApp.Contracts.*; + $(AllowedNamespaces);TestWebApp.Contracts.*;Microsoft.AspNetCore.Mvc*; + + diff --git a/test/TestWebApp.Contracts/RequiredDto.cs b/test/TestWebApp.Contracts/RequiredDto.cs new file mode 100644 index 0000000..2674dee --- /dev/null +++ b/test/TestWebApp.Contracts/RequiredDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text; + +namespace TestWebApp.Contracts +{ + public class RequiredDto + { + public int Id { get; set; } + [Required] + public string Field1 { get; set; } + } +} diff --git a/test/TestWebApp.Contracts/TestWebApp.Contracts.csproj b/test/TestWebApp.Contracts/TestWebApp.Contracts.csproj index bc7d79e..a24c238 100644 --- a/test/TestWebApp.Contracts/TestWebApp.Contracts.csproj +++ b/test/TestWebApp.Contracts/TestWebApp.Contracts.csproj @@ -13,6 +13,7 @@ + diff --git a/test/TestWebApp.Tests/JsonClientTest.cs b/test/TestWebApp.Tests/JsonClientTest.cs index b7ab63e..355cf75 100644 --- a/test/TestWebApp.Tests/JsonClientTest.cs +++ b/test/TestWebApp.Tests/JsonClientTest.cs @@ -14,6 +14,9 @@ using System.IO; using AspNetCore.Client.Authorization; using Newtonsoft.Json; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc; +using System.Linq; namespace TestWebApp.Tests { @@ -490,6 +493,61 @@ public void DuplicateMethodReturnAndResponseTypeAttributeTest() } } + [Test] + public void ProblemDetailsRequestTest() + { + using (var endpoint = new JsonServerInfo()) + { + var client = endpoint.Provider.GetService(); + + ValidationProblemDetails errors1 = null; + + var dto = new RequiredDto() + { + Id = 1 + }; + + client.ProblemDetailsRequest(dto, BadRequestCallback: _ => + { + errors1 = _; + }); + + var dto2 = new RequiredDto() + { + Id = 1, + Field1 = "Hello" + }; + + client.ProblemDetailsRequest(dto2, + OKCallback: () => + { + + } + ); + } + } + + [Test] + public void ModelStateBadRequestTest() + { + using (var endpoint = new JsonServerInfo()) + { + var client = endpoint.Provider.GetService(); + + IReadOnlyDictionary> errors = null; + + client.ModelStateBadRequest(BadRequestCallback: _ => + { + errors = _; + }); + + var str = errors["Test"]; + + Assert.AreEqual("Something is right!", str.Single()); + + } + } + /// /// Microsoft.AspNetCore.TestHost.ClientHandler does not respect the CancellationToken and will always complete a request. Their unit test around it ClientCancellationAbortsRequest has a "hack" that cancels in TestServer when the token is canceled. /// When the HttpClient has the default HttpMessageHandler, the SendAsync will cancel approriately, until they match this functionality, this test will be disabled diff --git a/test/TestWebApp.Tests/MessagePackClientTest.cs b/test/TestWebApp.Tests/MessagePackClientTest.cs index 3a776df..14d83b0 100644 --- a/test/TestWebApp.Tests/MessagePackClientTest.cs +++ b/test/TestWebApp.Tests/MessagePackClientTest.cs @@ -12,6 +12,7 @@ using TestWebApp.Contracts; using Microsoft.AspNetCore.TestHost; using AspNetCore.Client; +using Microsoft.AspNetCore.Mvc; namespace TestWebApp.Tests { @@ -111,6 +112,42 @@ public void RequestAndResponseChecks() } } + + [Test] + public void ProblemDetailsRequestTest() + { + //Doesn't seem like we need a application/problem+x-msgpack content-type + using (var endpoint = new MessagePackServerInfo()) + { + var client = endpoint.Provider.GetService(); + + ValidationProblemDetails errors1 = null; + + var dto = new RequiredDto() + { + Id = 1 + }; + + client.ProblemDetailsRequest(dto, BadRequestCallback: _ => + { + errors1 = _; + }); + + var dto2 = new RequiredDto() + { + Id = 1, + Field1 = "Hello" + }; + + client.ProblemDetailsRequest(dto2, + OKCallback: () => + { + + } + ); + } + } + /// /// Microsoft.AspNetCore.TestHost.ClientHandler does not respect the CancellationToken and will always complete a request. Their unit test around it ClientCancellationAbortsRequest has a "hack" that cancels in TestServer when the token is canceled. /// When the HttpClient has the default HttpMessageHandler, the SendAsync will cancel approriately, until they match this functionality, this test will be disabled diff --git a/test/TestWebApp/Controllers/ValuesController.cs b/test/TestWebApp/Controllers/ValuesController.cs index 6debe8e..1157258 100644 --- a/test/TestWebApp/Controllers/ValuesController.cs +++ b/test/TestWebApp/Controllers/ValuesController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using System; using System.Collections.Generic; using System.IO; @@ -347,5 +348,21 @@ public ActionResult DuplicateMethodReturnAndResponse() }; } + [HttpPost("[action]")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] + public IActionResult ProblemDetailsRequest(RequiredDto dto) + { + return Ok(); + } + + [HttpGet("[action]")] + [ProducesResponseType(typeof(IReadOnlyDictionary>), StatusCodes.Status400BadRequest)] + public IActionResult ModelStateBadRequest() + { + ModelState.AddModelError("Test", "Something is right!"); + return BadRequest(ModelState); + } + } }