Merge branch 'master' into keep-string-content-on-validation-exception
This commit is contained in:
Коммит
f758e8104c
|
@ -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
|
||||
|
|
26
README.md
26
README.md
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче