Merge branch 'master' into keep-string-content-on-validation-exception

This commit is contained in:
Drake Lambert 2020-09-18 10:15:15 -05:00 коммит произвёл GitHub
Родитель c098f9771c 21aac14f62
Коммит f758e8104c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 601 добавлений и 48 удалений

Просмотреть файл

@ -30,7 +30,7 @@
</ItemGroup>
<ItemGroup Condition="'$(IsTestProject)' != 'true'">
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.7.54" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.7.56" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -200,6 +200,7 @@ namespace Refit.Generator
AddInheritedMethods(ret.ClassList);
FixInheritedMethods(ret.ClassList);
MergePartialInterfaces(ret.ClassList);
return ret;
}
@ -323,6 +324,25 @@ namespace Refit.Generator
return outResult;
}
void MergePartialInterfaces(List<ClassTemplateInfo> classList)
{
var partialClasses = classList
.GroupBy(c => c.Namespace + c.InterfaceName + string.Join(".", c.TypeParameters))
.Where(g => g.Count() > 1)
.ToList();
foreach (var partialGroup in partialClasses)
{
var firstClass = partialGroup.First();
foreach (var otherClass in partialGroup.Skip(1))
{
firstClass.MethodList.AddRange(otherClass.MethodList);
classList.Remove(otherClass);
}
}
}
public void GenerateWarnings(List<InterfaceDeclarationSyntax> interfacesToGenerate)
{
var missingAttributeWarnings = interfacesToGenerate

Просмотреть файл

@ -131,6 +131,16 @@ public class MyQueryParams
public string SortOrder { get; set; }
public int Limit { get; set; }
public KindOptions Kind { get; set; }
}
public enum KindOptions
{
Foo,
[EnumMember(Value = "bar")]
Bar
}
@ -143,12 +153,13 @@ Task<List<User>> GroupListWithAttribute([AliasAs("id")] int groupId, [Query(".",
params.SortOrder = "desc";
params.Limit = 10;
params.Kind = KindOptions.Bar;
GroupList(4, params)
>>> "/group/4/users?order=desc&Limit=10"
>>> "/group/4/users?order=desc&Limit=10&Kind=bar"
GroupListWithAttribute(4, params)
>>> "/group/4/users?search.order=desc&search.Limit=10"
>>> "/group/4/users?search.order=desc&search.Limit=10&search.Kind=bar"
```
A similar behavior exists if using a Dictionary, but without the advantages of the `AliasAs` attributes and of course no intellisense and/or type safety.
@ -920,6 +931,17 @@ catch (ApiException exception)
// ...
```
You can also override default exceptions behavior by providing custom exception factory in `RefitSettings`. For example, you can suppress all exceptions with the following:
```csharp
var nullTask = Task.FromResult<Exception>(null);
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
ExceptionFactory = httpResponse => nullTask;
});
```
### MSBuild configuration
- `RefitDisableGenerateRefitStubs`: This property allows for other Roslyn-based source generators to disable the Refit's stubs generation during they own generation phase.

Просмотреть файл

@ -9,7 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\Refit\Refit.csproj" PrivateAssets="Analyzers" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,111 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Refit; // for the code gen
using RichardSzalay.MockHttp;
using Xunit;
namespace Refit.Tests
{
public class ExceptionFactoryTests
{
public interface IMyService
{
[Get("/get-with-result")]
Task<string> GetWithResult();
[Put("/put-without-result")]
Task PutWithoutResult();
}
[Fact]
public async Task ProvideFactoryWhichAlwaysReturnsNull_WithResult()
{
var handler = new MockHttpMessageHandler();
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
ExceptionFactory = _ => Task.FromResult<Exception>(null)
};
handler.Expect(HttpMethod.Get, "http://api/get-with-result")
.Respond(HttpStatusCode.NotFound, new StringContent("error-result"));
var fixture = RestService.For<IMyService>("http://api", settings);
var result = await fixture.GetWithResult();
handler.VerifyNoOutstandingExpectation();
Assert.Equal("error-result", result);
}
[Fact]
public async Task ProvideFactoryWhichAlwaysReturnsNull_WithoutResult()
{
var handler = new MockHttpMessageHandler();
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
ExceptionFactory = _ => Task.FromResult<Exception>(null)
};
handler.Expect(HttpMethod.Put, "http://api/put-without-result")
.Respond(HttpStatusCode.NotFound);
var fixture = RestService.For<IMyService>("http://api", settings);
await fixture.PutWithoutResult();
handler.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task ProvideFactoryWhichAlwaysReturnsException_WithResult()
{
var handler = new MockHttpMessageHandler();
var exception = new Exception("I like to fail");
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
ExceptionFactory = _ => Task.FromResult<Exception>(exception)
};
handler.Expect(HttpMethod.Get, "http://api/get-with-result")
.Respond(HttpStatusCode.OK, new StringContent("success-result"));
var fixture = RestService.For<IMyService>("http://api", settings);
var thrownException = await Assert.ThrowsAsync<Exception>(() => fixture.GetWithResult());
Assert.Equal(exception, thrownException);
handler.VerifyNoOutstandingExpectation();
}
[Fact]
public async Task ProvideFactoryWhichAlwaysReturnsException_WithoutResult()
{
var handler = new MockHttpMessageHandler();
var exception = new Exception("I like to fail");
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
ExceptionFactory = _ => Task.FromResult<Exception>(exception)
};
handler.Expect(HttpMethod.Put, "http://api/put-without-result")
.Respond(HttpStatusCode.OK);
var fixture = RestService.For<IMyService>("http://api", settings);
var thrownException = await Assert.ThrowsAsync<Exception>(() => fixture.PutWithoutResult());
Assert.Equal(exception, thrownException);
handler.VerifyNoOutstandingExpectation();
}
}
}

Просмотреть файл

@ -193,6 +193,30 @@ namespace Refit.Tests
Assert.Null(method);
}
[Fact]
public void GenerateTemplateInfoForPartialInterfaces()
{
var partialFile1 = CSharpSyntaxTree.ParseText(File.ReadAllText(IntegrationTestHelper.GetPath("PartialInterfacesApi.First.cs")));
var partialFile2 = CSharpSyntaxTree.ParseText(File.ReadAllText(IntegrationTestHelper.GetPath("PartialInterfacesApi.Second.cs")));
var fixture = new InterfaceStubGenerator();
var input = new[] { partialFile1, partialFile2 }
.Select(file => file.GetRoot())
.SelectMany(root => root.DescendantNodes())
.OfType<InterfaceDeclarationSyntax>()
.ToList();
var result = fixture.GenerateTemplateInfoForInterfaceList(input);
var classInfo = Assert.Single(result.ClassList);
Assert.Equal(nameof(PartialInterfacesApi), classInfo.InterfaceName);
Assert.Collection(classInfo.MethodList,
m => Assert.Equal(nameof(PartialInterfacesApi.First), m.Name),
m => Assert.Equal(nameof(PartialInterfacesApi.Second), m.Name));
}
[Fact]
public void RetainsAliasesInUsings()
{

Просмотреть файл

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Refit; // InterfaceStubGenerator looks for this
namespace Refit.Tests
{
public partial interface PartialInterfacesApi
{
[Get("/get?result=First")]
Task<string> First();
}
}

Просмотреть файл

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Refit; // InterfaceStubGenerator looks for this
namespace Refit.Tests
{
public partial interface PartialInterfacesApi
{
[Get("/get?result=Second")]
Task<string> Second();
}
}

Просмотреть файл

@ -1887,6 +1887,52 @@ namespace Refit.Tests
}
}
namespace Refit.Tests
{
using global::System;
using global::System.Net;
using global::System.Net.Http;
using global::System.Threading.Tasks;
using global::Refit;
using global::RichardSzalay.MockHttp;
using global::Xunit;
/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[Preserve]
[global::System.Reflection.Obfuscation(Exclude=true)]
partial class AutoGeneratedExceptionFactoryTestsIMyService : ExceptionFactoryTests.IMyService
{
/// <inheritdoc />
public HttpClient Client { get; protected set; }
readonly IRequestBuilder requestBuilder;
/// <inheritdoc />
public AutoGeneratedExceptionFactoryTestsIMyService(HttpClient client, IRequestBuilder requestBuilder)
{
Client = client;
this.requestBuilder = requestBuilder;
}
/// <inheritdoc />
Task<string> ExceptionFactoryTests.IMyService.GetWithResult()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("GetWithResult", new Type[] { });
return (Task<string>)func(Client, arguments);
}
/// <inheritdoc />
Task ExceptionFactoryTests.IMyService.PutWithoutResult()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("PutWithoutResult", new Type[] { });
return (Task)func(Client, arguments);
}
}
}
namespace Refit.Tests
{
using global::System.Threading.Tasks;
@ -3018,5 +3064,46 @@ namespace Refit.Tests
}
}
namespace Refit.Tests
{
using global::System.Threading.Tasks;
using global::Refit;
/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[Preserve]
[global::System.Reflection.Obfuscation(Exclude=true)]
partial class AutoGeneratedPartialInterfacesApi : PartialInterfacesApi
{
/// <inheritdoc />
public HttpClient Client { get; protected set; }
readonly IRequestBuilder requestBuilder;
/// <inheritdoc />
public AutoGeneratedPartialInterfacesApi(HttpClient client, IRequestBuilder requestBuilder)
{
Client = client;
this.requestBuilder = requestBuilder;
}
/// <inheritdoc />
Task<string> PartialInterfacesApi.First()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("First", new Type[] { });
return (Task<string>)func(Client, arguments);
}
/// <inheritdoc />
Task<string> PartialInterfacesApi.Second()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("Second", new Type[] { });
return (Task<string>)func(Client, arguments);
}
}
}
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore CS8669 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Просмотреть файл

@ -1887,6 +1887,52 @@ namespace Refit.Tests
}
}
namespace Refit.Tests
{
using global::System;
using global::System.Net;
using global::System.Net.Http;
using global::System.Threading.Tasks;
using global::Refit;
using global::RichardSzalay.MockHttp;
using global::Xunit;
/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[Preserve]
[global::System.Reflection.Obfuscation(Exclude=true)]
partial class AutoGeneratedExceptionFactoryTestsIMyService : ExceptionFactoryTests.IMyService
{
/// <inheritdoc />
public HttpClient Client { get; protected set; }
readonly IRequestBuilder requestBuilder;
/// <inheritdoc />
public AutoGeneratedExceptionFactoryTestsIMyService(HttpClient client, IRequestBuilder requestBuilder)
{
Client = client;
this.requestBuilder = requestBuilder;
}
/// <inheritdoc />
Task<string> ExceptionFactoryTests.IMyService.GetWithResult()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("GetWithResult", new Type[] { });
return (Task<string>)func(Client, arguments);
}
/// <inheritdoc />
Task ExceptionFactoryTests.IMyService.PutWithoutResult()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("PutWithoutResult", new Type[] { });
return (Task)func(Client, arguments);
}
}
}
namespace Refit.Tests
{
using global::System.Threading.Tasks;
@ -3018,5 +3064,46 @@ namespace Refit.Tests
}
}
namespace Refit.Tests
{
using global::System.Threading.Tasks;
using global::Refit;
/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[Preserve]
[global::System.Reflection.Obfuscation(Exclude=true)]
partial class AutoGeneratedPartialInterfacesApi : PartialInterfacesApi
{
/// <inheritdoc />
public HttpClient Client { get; protected set; }
readonly IRequestBuilder requestBuilder;
/// <inheritdoc />
public AutoGeneratedPartialInterfacesApi(HttpClient client, IRequestBuilder requestBuilder)
{
Client = client;
this.requestBuilder = requestBuilder;
}
/// <inheritdoc />
Task<string> PartialInterfacesApi.First()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("First", new Type[] { });
return (Task<string>)func(Client, arguments);
}
/// <inheritdoc />
Task<string> PartialInterfacesApi.Second()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("Second", new Type[] { });
return (Task<string>)func(Client, arguments);
}
}
}
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore CS8669 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Просмотреть файл

@ -1887,6 +1887,52 @@ namespace Refit.Tests
}
}
namespace Refit.Tests
{
using global::System;
using global::System.Net;
using global::System.Net.Http;
using global::System.Threading.Tasks;
using global::Refit;
using global::RichardSzalay.MockHttp;
using global::Xunit;
/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[Preserve]
[global::System.Reflection.Obfuscation(Exclude=true)]
partial class AutoGeneratedExceptionFactoryTestsIMyService : ExceptionFactoryTests.IMyService
{
/// <inheritdoc />
public HttpClient Client { get; protected set; }
readonly IRequestBuilder requestBuilder;
/// <inheritdoc />
public AutoGeneratedExceptionFactoryTestsIMyService(HttpClient client, IRequestBuilder requestBuilder)
{
Client = client;
this.requestBuilder = requestBuilder;
}
/// <inheritdoc />
Task<string> ExceptionFactoryTests.IMyService.GetWithResult()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("GetWithResult", new Type[] { });
return (Task<string>)func(Client, arguments);
}
/// <inheritdoc />
Task ExceptionFactoryTests.IMyService.PutWithoutResult()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("PutWithoutResult", new Type[] { });
return (Task)func(Client, arguments);
}
}
}
namespace Refit.Tests
{
using global::System.Threading.Tasks;
@ -3018,5 +3064,46 @@ namespace Refit.Tests
}
}
namespace Refit.Tests
{
using global::System.Threading.Tasks;
using global::Refit;
/// <inheritdoc />
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.Diagnostics.DebuggerNonUserCode]
[Preserve]
[global::System.Reflection.Obfuscation(Exclude=true)]
partial class AutoGeneratedPartialInterfacesApi : PartialInterfacesApi
{
/// <inheritdoc />
public HttpClient Client { get; protected set; }
readonly IRequestBuilder requestBuilder;
/// <inheritdoc />
public AutoGeneratedPartialInterfacesApi(HttpClient client, IRequestBuilder requestBuilder)
{
Client = client;
this.requestBuilder = requestBuilder;
}
/// <inheritdoc />
Task<string> PartialInterfacesApi.First()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("First", new Type[] { });
return (Task<string>)func(Client, arguments);
}
/// <inheritdoc />
Task<string> PartialInterfacesApi.Second()
{
var arguments = new object[] { };
var func = requestBuilder.BuildRestResultFuncForMethod("Second", new Type[] { });
return (Task<string>)func(Client, arguments);
}
}
}
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore CS8669 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. Auto-generated code requires an explicit '#nullable' directive in source.

Просмотреть файл

@ -695,6 +695,9 @@ namespace Refit.Tests
[Get("/foo/bar/{id}")]
Task<string> FetchSomeStuffWithCustomHeader(int id, [Header("X-Emoji")] string custom);
[Get("/foo/bar/{id}")]
Task<string> FetchSomeStuffWithPathMemberInCustomHeader([Header("X-PathMember")]int id, [Header("X-Emoji")] string custom);
[Post("/foo/bar/{id}")]
Task<string> PostSomeStuffWithCustomHeader(int id, [Body] object body, [Header("X-Emoji")] string emoji);
@ -783,6 +786,9 @@ namespace Refit.Tests
[Get("/query")]
Task QueryWithEnum(FooWithEnumMember foo);
[Get("/query")]
Task QueryWithTypeWithEnum(TypeFooWithEnumMember foo);
[Get("/api/{id}")]
Task QueryWithOptionalParameters(int id, [Query]string text = null, [Query]int? optionalId = null, [Query(CollectionFormat = CollectionFormat.Multi)]string[] filters = null);
@ -845,6 +851,12 @@ namespace Refit.Tests
B
}
public class TypeFooWithEnumMember
{
[AliasAs("foo")]
public FooWithEnumMember Foo { get; set; }
}
public class SomeRequestData
{
[AliasAs("rpn")]
@ -1344,6 +1356,17 @@ namespace Refit.Tests
Assert.Null(output.Headers.Authorization);//, "Headers include Authorization header");
}
[Fact]
public void PathMemberAsCustomDynamicHeaderShouldBeInHeaders()
{
var fixture = new RequestBuilderImplementation<IDummyHttpApi>();
var factory = fixture.BuildRequestFactoryForMethod("FetchSomeStuffWithPathMemberInCustomHeader");
var output = factory(new object[] { 6, ":joy_cat:" });
Assert.True(output.Headers.Contains("X-PathMember"), "Headers include X-PathMember header");
Assert.Equal("6", output.Headers.GetValues("X-PathMember").First());
}
[Fact]
public void AddCustomHeadersToRequestHeadersOnly()
{
@ -1653,6 +1676,20 @@ namespace Refit.Tests
Assert.Equal(expectedQuery, uri.PathAndQuery);
}
[Theory]
[InlineData(FooWithEnumMember.A, "/query?foo=A")]
[InlineData(FooWithEnumMember.B, "/query?foo=b")]
public void QueryStringUsesEnumMemberAttributeInTypeWithEnum(FooWithEnumMember queryParameter, string expectedQuery)
{
var fixture = new RequestBuilderImplementation<IDummyHttpApi>();
var factory = fixture.BuildRequestFactoryForMethod("QueryWithTypeWithEnum");
var output = factory(new object[] { new TypeFooWithEnumMember { Foo = queryParameter } });
var uri = new Uri(new Uri("http://api"), output.RequestUri);
Assert.Equal(expectedQuery, uri.PathAndQuery);
}
[Theory]
[InlineData("/api/123?text=title&optionalId=999&filters=A&filters=B")]
public void TestNullableQueryStringParams(string expectedQuery)

Просмотреть файл

@ -22,7 +22,12 @@ namespace Refit
public RefitSettings RefitSettings { get; set; }
protected ApiException(HttpRequestMessage message, HttpMethod httpMethod, HttpStatusCode statusCode, string reasonPhrase, HttpResponseHeaders headers, RefitSettings refitSettings = null) :
base(CreateMessage(statusCode, reasonPhrase))
this(CreateMessage(statusCode, reasonPhrase), message, httpMethod, statusCode, reasonPhrase, headers, refitSettings)
{
}
protected ApiException(string exceptionMessage, HttpRequestMessage message, HttpMethod httpMethod, HttpStatusCode statusCode, string reasonPhrase, HttpResponseHeaders headers, RefitSettings refitSettings = null) :
base(exceptionMessage)
{
RequestMessage = message;
HttpMethod = httpMethod;
@ -40,10 +45,18 @@ namespace Refit
default;
#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods
public static async Task<ApiException> Create(HttpRequestMessage message, HttpMethod httpMethod, HttpResponseMessage response, RefitSettings refitSettings = null)
public static Task<ApiException> Create(HttpRequestMessage message, HttpMethod httpMethod, HttpResponseMessage response, RefitSettings refitSettings = null)
#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods
{
var exception = new ApiException(message, httpMethod, response.StatusCode, response.ReasonPhrase, response.Headers, refitSettings);
var exceptionMessage = CreateMessage(response.StatusCode, response.ReasonPhrase);
return Create(exceptionMessage, message, httpMethod, response, refitSettings);
}
#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods
public static async Task<ApiException> Create(string exceptionMessage, HttpRequestMessage message, HttpMethod httpMethod, HttpResponseMessage response, RefitSettings refitSettings = null)
#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods
{
var exception = new ApiException(exceptionMessage, message, httpMethod, response.StatusCode, response.ReasonPhrase, response.Headers, refitSettings);
if (response.Content == null)
{

Просмотреть файл

@ -6,6 +6,7 @@ using System.Net.Http;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Refit
@ -22,6 +23,7 @@ namespace Refit
ContentSerializer = new NewtonsoftJsonContentSerializer();
UrlParameterFormatter = new DefaultUrlParameterFormatter();
FormUrlEncodedParameterFormatter = new DefaultFormUrlEncodedParameterFormatter();
ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync;
}
/// <summary>
@ -38,6 +40,7 @@ namespace Refit
ContentSerializer = contentSerializer ?? throw new ArgumentNullException(nameof(contentSerializer), "The content serializer can't be null");
UrlParameterFormatter = urlParameterFormatter ?? new DefaultUrlParameterFormatter();
FormUrlEncodedParameterFormatter = formUrlEncodedParameterFormatter ?? new DefaultFormUrlEncodedParameterFormatter();
ExceptionFactory = new DefaultApiExceptionFactory(this).CreateAsync;
}
/// <summary>
@ -55,6 +58,12 @@ namespace Refit
/// </summary>
public Func<HttpMessageHandler> HttpMessageHandlerFactory { get; set; }
/// <summary>
/// Supply a function to provide <see cref="Exception"/> based on <see cref="HttpResponseMessage"/>.
/// If function returns null - no exception is thrown.
/// </summary>
public Func<HttpResponseMessage, Task<Exception>> ExceptionFactory { get; set; }
[Obsolete("Set RefitSettings.ContentSerializer = new NewtonsoftJsonContentSerializer(JsonSerializerSettings) instead.", false)]
public JsonSerializerSettings JsonSerializerSettings
{
@ -104,10 +113,14 @@ namespace Refit
.FirstOrDefault()?.Format;
EnumMemberAttribute enummember = null;
if (parameterValue != null && type.GetTypeInfo().IsEnum)
if (parameterValue != null)
{
var cached = EnumMemberCache.GetOrAdd(type, t => new ConcurrentDictionary<string, EnumMemberAttribute>());
enummember = cached.GetOrAdd(parameterValue.ToString(), val => type.GetMember(val).First().GetCustomAttribute<EnumMemberAttribute>());
var parameterType = parameterValue.GetType();
if (parameterType.IsEnum)
{
var cached = EnumMemberCache.GetOrAdd(parameterType, t => new ConcurrentDictionary<string, EnumMemberAttribute>());
enummember = cached.GetOrAdd(parameterValue.ToString(), val => parameterType.GetMember(val).First().GetCustomAttribute<EnumMemberAttribute>());
}
}
return parameterValue == null
@ -146,4 +159,36 @@ namespace Refit
enummember?.Value ?? parameterValue);
}
}
public class DefaultApiExceptionFactory
{
static readonly Task<Exception> NullTask = Task.FromResult<Exception>(null);
readonly RefitSettings refitSettings;
public DefaultApiExceptionFactory(RefitSettings refitSettings)
{
this.refitSettings = refitSettings;
}
public Task<Exception> CreateAsync(HttpResponseMessage responseMessage)
{
if (!responseMessage.IsSuccessStatusCode)
{
return CreateExceptionAsync(responseMessage, refitSettings);
}
else
{
return NullTask;
}
}
static async Task<Exception> CreateExceptionAsync(HttpResponseMessage responseMessage, RefitSettings refitSettings)
{
var requestMessage = responseMessage.RequestMessage;
var method = requestMessage.Method;
return await ApiException.Create(requestMessage, method, responseMessage, refitSettings).ConfigureAwait(false);
}
}
}

Просмотреть файл

@ -155,7 +155,7 @@ namespace Refit
{
// NB: This jacked up reflection code is here because it's
// difficult to upcast Task<object> to an arbitrary T, especially
// if you need to AOT everything, so we need to reflectively
// if you need to AOT everything, so we need to reflectively
// invoke buildTaskFuncForMethod.
var taskFuncMi = typeof(RequestBuilderImplementation).GetMethod(nameof(BuildTaskFuncForMethod), BindingFlags.NonPublic | BindingFlags.Instance);
var taskFunc = (MulticastDelegate)(taskFuncMi.MakeGenericMethod(restMethod.ReturnResultType, restMethod.DeserializedResultType)).Invoke(this, new[] { restMethod });
@ -244,22 +244,26 @@ namespace Refit
{
resp = await client.SendAsync(rq, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
content = resp.Content ?? new StringContent(string.Empty);
ApiException e = null;
Exception e = null;
disposeResponse = restMethod.ShouldDisposeResponse;
if (!resp.IsSuccessStatusCode && typeof(T) != typeof(HttpResponseMessage))
if (typeof(T) != typeof(HttpResponseMessage))
{
disposeResponse = false; // caller has to dispose
e = await ApiException.Create(rq, restMethod.HttpMethod, resp, restMethod.RefitSettings).ConfigureAwait(false);
e = await settings.ExceptionFactory(resp).ConfigureAwait(false);
}
if (restMethod.IsApiResponse)
{
var body = await DeserializeContentAsync<TBody>(resp, content);
return ApiResponse.Create<T, TBody>(resp, body, e);
// Only attempt to deserialize content if no error present for backward-compatibility
var body = e == null
? await DeserializeContentAsync<TBody>(resp, content)
: default;
return ApiResponse.Create<T, TBody>(resp, body, e as ApiException);
}
else if (e != null)
{
disposeResponse = false; // caller has to dispose
throw e;
}
else
@ -289,10 +293,6 @@ namespace Refit
// and would be A Bad Idea if we hadn't already vetted the return type.
result = (T)(object)resp;
}
else if (!resp.IsSuccessStatusCode)
{
result = default;
}
else if (typeof(T) == typeof(HttpContent))
{
result = (T)(object)content;
@ -458,6 +458,7 @@ namespace Refit
for (var i = 0; i < paramList.Length; i++)
{
var isParameterMappedToRequest = false;
var param = paramList[i];
// if part of REST resource URL, substitute it in
if (restMethod.ParameterMap.ContainsKey(i))
@ -509,7 +510,7 @@ namespace Refit
replacement,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
continue;
isParameterMappedToRequest = true;
}
}
@ -567,18 +568,18 @@ namespace Refit
}
}
continue;
isParameterMappedToRequest = true;
}
// if header, add to request headers
if (restMethod.HeaderParameterMap.ContainsKey(i))
{
headersToAdd[restMethod.HeaderParameterMap[i]] = param?.ToString();
continue;
isParameterMappedToRequest = true;
}
// ignore nulls
if (param == null) continue;
// ignore nulls and already processed parameters
if (isParameterMappedToRequest || param == null) continue;
// for anything that fell through to here, if this is not a multipart method add the parameter to the query string
// or if is an object bound to the path add any non-path bound properties to query string
@ -656,8 +657,8 @@ namespace Refit
}
}
// NB: The URI methods in .NET are dumb. Also, we do this
// UriBuilder business so that we preserve any hardcoded query
// NB: The URI methods in .NET are dumb. Also, we do this
// UriBuilder business so that we preserve any hardcoded query
// parameters as well as add the parameterized ones.
var uri = new UriBuilder(new Uri(new Uri("http://api"), urlTarget));
var query = HttpUtility.ParseQueryString(uri.Query ?? "");
@ -786,9 +787,11 @@ namespace Refit
}
using var resp = await client.SendAsync(rq, ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode)
var exception = await settings.ExceptionFactory(resp).ConfigureAwait(false);
if (exception != null)
{
throw await ApiException.Create(rq, restMethod.HttpMethod, resp, settings).ConfigureAwait(false);
throw exception;
}
};
}
@ -832,11 +835,11 @@ namespace Refit
static void SetHeader(HttpRequestMessage request, string name, string value)
{
// Clear any existing version of this header that might be set, because
// we want to allow removal/redefinition of headers.
// we want to allow removal/redefinition of headers.
// We also don't want to double up content headers which may have been
// set for us automatically.
// NB: We have to enumerate the header names to check existence because
// NB: We have to enumerate the header names to check existence because
// Contains throws if it's the wrong header type for the collection.
if (request.Headers.Any(x => x.Key == name))
{

Просмотреть файл

@ -133,20 +133,7 @@ namespace Refit
return;
if (!relativePath.StartsWith("/"))
{
goto bogusPath;
}
var parts = relativePath.Split('/');
if (parts.Length == 0)
{
goto bogusPath;
}
return;
bogusPath:
throw new ArgumentException($"URL path {relativePath} must be of the form '/foo/bar/baz'");
throw new ArgumentException($"URL path {relativePath} must start with '/' and be of the form '/foo/bar/baz'");
}
Dictionary<int, RestMethodParameterInfo> BuildParameterMap(string relativePath, List<ParameterInfo> parameterInfo)

Просмотреть файл

@ -39,7 +39,12 @@ namespace Refit
public async Task<T> DeserializeAsync<T>(HttpContent content)
{
var xmlSerializer = serializerCache.GetOrAdd(typeof(T), t => new XmlSerializer(t, settings.XmlAttributeOverrides));
var xmlSerializer = serializerCache.GetOrAdd(typeof(T), t => new XmlSerializer(
t,
settings.XmlAttributeOverrides,
Array.Empty<Type>(),
null,
settings.XmlDefaultNamespace));
using var input = new StringReader(await content.ReadAsStringAsync().ConfigureAwait(false));
using var reader = XmlReader.Create(input, settings.XmlReaderWriterSettings.ReaderSettings);
@ -106,6 +111,7 @@ namespace Refit
{
public XmlContentSerializerSettings()
{
XmlDefaultNamespace = null;
XmlReaderWriterSettings = new XmlReaderWriterSettings();
XmlNamespaces = new XmlSerializerNamespaces(
new[]
@ -116,6 +122,8 @@ namespace Refit
XmlAttributeOverrides = new XmlAttributeOverrides();
}
public string XmlDefaultNamespace { get; set; }
public XmlReaderWriterSettings XmlReaderWriterSettings { get; set; }
public XmlSerializerNamespaces XmlNamespaces { get; set; }

Просмотреть файл

@ -1,5 +1,5 @@
{
"version": "5.1",
"version": "5.2",
"publicReleaseRefSpec": [
"^refs/heads/master$", // we release out of master
"^refs/heads/rel/v\\d+\\.\\d+" // we also release branches starting with vN.N