Skip to content

Commit 7e0a1af

Browse files
committed
Added Authenticator to RestRequest
Made Options immutable to make the client thread-safe
1 parent 19c30a8 commit 7e0a1af

File tree

17 files changed

+308
-479
lines changed

17 files changed

+308
-479
lines changed

RestSharp.sln

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Serializers.CsvHe
3535
EndProject
3636
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestSharp.Tests.Serializers.Csv", "test\RestSharp.Tests.Serializers.Csv\RestSharp.Tests.Serializers.Csv.csproj", "{E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}"
3737
EndProject
38+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGen", "SourceGen", "{55B8F371-B2BA-4DEE-AB98-5BAB8A21B1C2}"
39+
EndProject
40+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerator", "gen\SourceGenerator\SourceGenerator.csproj", "{FE778406-ADCF-45A1-B775-A054B55BFC50}"
41+
EndProject
3842
Global
3943
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4044
Debug.Appveyor|Any CPU = Debug.Appveyor|Any CPU
@@ -444,6 +448,36 @@ Global
444448
{E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|x64.Build.0 = Release|Any CPU
445449
{E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|x86.ActiveCfg = Release|Any CPU
446450
{E6D94FFD-7811-40BE-ABC4-6D6AB41F0060}.Release|x86.Build.0 = Release|Any CPU
451+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|Any CPU.ActiveCfg = Debug|Any CPU
452+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|Any CPU.Build.0 = Debug|Any CPU
453+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|ARM.ActiveCfg = Debug|Any CPU
454+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|ARM.Build.0 = Debug|Any CPU
455+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|Mixed Platforms.ActiveCfg = Debug|Any CPU
456+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|Mixed Platforms.Build.0 = Debug|Any CPU
457+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|x64.ActiveCfg = Debug|Any CPU
458+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|x64.Build.0 = Debug|Any CPU
459+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|x86.ActiveCfg = Debug|Any CPU
460+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug.Appveyor|x86.Build.0 = Debug|Any CPU
461+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
462+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|Any CPU.Build.0 = Debug|Any CPU
463+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|ARM.ActiveCfg = Debug|Any CPU
464+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|ARM.Build.0 = Debug|Any CPU
465+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
466+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
467+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|x64.ActiveCfg = Debug|Any CPU
468+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|x64.Build.0 = Debug|Any CPU
469+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|x86.ActiveCfg = Debug|Any CPU
470+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Debug|x86.Build.0 = Debug|Any CPU
471+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|Any CPU.ActiveCfg = Release|Any CPU
472+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|Any CPU.Build.0 = Release|Any CPU
473+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|ARM.ActiveCfg = Release|Any CPU
474+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|ARM.Build.0 = Release|Any CPU
475+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
476+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|Mixed Platforms.Build.0 = Release|Any CPU
477+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x64.ActiveCfg = Release|Any CPU
478+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x64.Build.0 = Release|Any CPU
479+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x86.ActiveCfg = Release|Any CPU
480+
{FE778406-ADCF-45A1-B775-A054B55BFC50}.Release|x86.Build.0 = Release|Any CPU
447481
EndGlobalSection
448482
GlobalSection(SolutionProperties) = preSolution
449483
HideSolutionNode = FALSE
@@ -461,6 +495,7 @@ Global
461495
{5A8A5BBE-28DA-4C89-B393-BE39A96E8DC0} = {9051DDA0-E563-45D5-9504-085EBAACF469}
462496
{2150E333-8FDC-42A3-9474-1A3956D46DE8} = {8C7B43EB-2F93-483C-B433-E28F9386AD67}
463497
{E6D94FFD-7811-40BE-ABC4-6D6AB41F0060} = {9051DDA0-E563-45D5-9504-085EBAACF469}
498+
{FE778406-ADCF-45A1-B775-A054B55BFC50} = {55B8F371-B2BA-4DEE-AB98-5BAB8A21B1C2}
464499
EndGlobalSection
465500
GlobalSection(ExtensibilityGlobals) = postSolution
466501
SolutionGuid = {77FF357B-03FA-4FA5-A68F-BFBE5800FEBA}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) .NET Foundation and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
using System.Text;
17+
using Microsoft.CodeAnalysis;
18+
using Microsoft.CodeAnalysis.CSharp;
19+
using Microsoft.CodeAnalysis.CSharp.Syntax;
20+
using Microsoft.CodeAnalysis.Text;
21+
22+
namespace SourceGenerator;
23+
24+
[Generator]
25+
public class ImmutableGenerator : ISourceGenerator {
26+
public void Initialize(GeneratorInitializationContext context) { }
27+
28+
public void Execute(GeneratorExecutionContext context) {
29+
var compilation = context.Compilation;
30+
31+
var mutableClasses = compilation.SyntaxTrees
32+
.Select(tree => compilation.GetSemanticModel(tree))
33+
.SelectMany(model => model.SyntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>())
34+
.Where(syntax => syntax.AttributeLists.Any(list => list.Attributes.Any(attr => attr.Name.ToString() == "GenerateImmutable")));
35+
36+
foreach (var mutableClass in mutableClasses) {
37+
var immutableClass = GenerateImmutableClass(mutableClass, compilation);
38+
context.AddSource($"ReadOnly{mutableClass.Identifier.Text}.cs", SourceText.From(immutableClass, Encoding.UTF8));
39+
}
40+
}
41+
42+
static string GenerateImmutableClass(TypeDeclarationSyntax mutableClass, Compilation compilation) {
43+
var containingNamespace = compilation.GetSemanticModel(mutableClass.SyntaxTree).GetDeclaredSymbol(mutableClass)!.ContainingNamespace;
44+
45+
var namespaceName = containingNamespace.ToDisplayString();
46+
47+
var className = mutableClass.Identifier.Text;
48+
49+
var usings = mutableClass.SyntaxTree.GetCompilationUnitRoot().Usings.Select(u => u.ToString());
50+
51+
var properties = GetDefinitions(SyntaxKind.SetKeyword)
52+
.Select(prop => $" public {prop.Type} {prop.Identifier.Text} {{ get; }}")
53+
.ToArray();
54+
55+
var props = GetDefinitions(SyntaxKind.SetKeyword).ToArray();
56+
57+
const string argName = "inner";
58+
var mutableProperties = props
59+
.Select(prop => $" {prop.Identifier.Text} = {argName}.{prop.Identifier.Text};");
60+
61+
var constructor = $@" public ReadOnly{className}({className} {argName}) {{
62+
{string.Join("\n", mutableProperties)}
63+
}}";
64+
65+
const string template = @"{Usings}
66+
67+
namespace {Namespace};
68+
69+
public class ReadOnly{ClassName} {
70+
{Constructor}
71+
72+
{Properties}
73+
}";
74+
75+
var code = template
76+
.Replace("{Usings}", string.Join("\n", usings))
77+
.Replace("{Namespace}", namespaceName)
78+
.Replace("{ClassName}", className)
79+
.Replace("{Constructor}", constructor)
80+
.Replace("{Properties}", string.Join("\n", properties));
81+
82+
return code;
83+
84+
IEnumerable<PropertyDeclarationSyntax> GetDefinitions(SyntaxKind kind)
85+
=> mutableClass.Members
86+
.OfType<PropertyDeclarationSyntax>()
87+
.Where(
88+
prop =>
89+
prop.AccessorList!.Accessors.Any(accessor => accessor.Keyword.IsKind(kind))
90+
);
91+
}
92+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>11</LangVersion>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>disable</Nullable>
8+
<IncludeBuildOutput>false</IncludeBuildOutput>
9+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="All" />
14+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="All" />
15+
<!-- <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" PrivateAssets="All" />-->
16+
<!-- <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.4.0" PrivateAssets="All"/>-->
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
21+
</ItemGroup>
22+
</Project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) .NET Foundation and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
namespace RestSharp.Extensions;
17+
18+
[AttributeUsage(AttributeTargets.Class)]
19+
public class GenerateImmutableAttribute : Attribute { }
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) .NET Foundation and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
using System.Linq.Expressions;
17+
using System.Reflection;
18+
19+
namespace RestSharp.Extensions;
20+
21+
static class ImmutableGenerator {
22+
static readonly MethodInfo SetPropertyMethod =
23+
typeof(ImmutableGenerator).GetMethod(nameof(SetProperty), BindingFlags.Static | BindingFlags.NonPublic)!;
24+
25+
static void SetProperty<T>(T obj, PropertyInfo property, object value) => property.SetValue(obj, (T)value);
26+
27+
public static Func<TMutable, TImmutable> CreateImmutableFunc<TMutable, TImmutable>()
28+
where TMutable : class
29+
where TImmutable : class, new()
30+
{
31+
var mutableType = typeof(TMutable);
32+
var immutableType = typeof(TImmutable);
33+
34+
var parameter = Expression.Parameter(mutableType, "mutable");
35+
36+
var bindings = mutableType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
37+
.Select(property =>
38+
Expression.Bind(
39+
immutableType.GetProperty(property.Name)!,
40+
Expression.Property(parameter, property)));
41+
42+
var body = Expression.MemberInit(Expression.New(immutableType), bindings);
43+
44+
var lambda = Expression.Lambda<Func<TMutable, TImmutable>>(body, parameter);
45+
46+
return lambda.Compile();
47+
}
48+
}

src/RestSharp/IRestClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public interface IRestClient : IDisposable {
2121
/// <summary>
2222
/// Client options that aren't used for configuring HttpClient
2323
/// </summary>
24-
IRestClientOptions Options { get; }
24+
ReadOnlyRestClientOptions Options { get; }
2525

2626
/// <summary>
2727
/// Client-level serializers

src/RestSharp/Options/RestClientOptions.cs

Lines changed: 3 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -21,89 +21,12 @@
2121
using System.Text;
2222
using RestSharp.Authenticators;
2323
using RestSharp.Extensions;
24+
using SourceGenerator;
2425

2526
namespace RestSharp;
2627

27-
public interface IRestClientOptions {
28-
/// <summary>
29-
/// Explicit Host header value to use in requests independent from the request URI.
30-
/// If null, default host value extracted from URI is used.
31-
/// </summary>
32-
Uri? BaseUrl { get; }
33-
34-
/// <summary>
35-
/// Function to calculate the response status. By default, the status will be Completed if it was successful, or NotFound.
36-
/// </summary>
37-
CalculateResponseStatus CalculateResponseStatus { get; }
38-
39-
/// <summary>
40-
/// Authenticator that will be used to populate request with necessary authentication data
41-
/// </summary>
42-
IAuthenticator? Authenticator { get; set; }
43-
44-
/// <summary>
45-
/// Set to true if you need the Content-Type not to have the charset
46-
/// </summary>
47-
bool DisableCharset { get; }
48-
49-
/// <summary>
50-
/// Sets the cache control header for all the requests made by the client
51-
/// </summary>
52-
CacheControlHeaderValue? CachePolicy { get; }
53-
54-
Encoding Encoding { get; }
55-
56-
/// <summary>
57-
/// Modifies the default behavior of RestSharp to swallow exceptions.
58-
/// When set to <code>true</code>, a <see cref="DeserializationException"/> will be thrown
59-
/// in case RestSharp fails to deserialize the response.
60-
/// </summary>
61-
bool ThrowOnDeserializationError { get; }
62-
63-
/// <summary>
64-
/// Modifies the default behavior of RestSharp to swallow exceptions.
65-
/// When set to <code>true</code>, RestSharp will consider the request as unsuccessful
66-
/// in case it fails to deserialize the response.
67-
/// </summary>
68-
bool FailOnDeserializationError { get; }
69-
70-
/// <summary>
71-
/// Modifies the default behavior of RestSharp to swallow exceptions.
72-
/// When set to <code>true</code>, exceptions will be re-thrown.
73-
/// </summary>
74-
bool ThrowOnAnyError { get; }
75-
76-
/// <summary>
77-
/// Sets the base host header for all the requests made by the client
78-
/// </summary>
79-
string? BaseHost { get; }
80-
81-
/// <summary>
82-
/// By default, RestSharp doesn't allow multiple parameters to have the same name.
83-
/// This properly allows to override the default behavior.
84-
/// </summary>
85-
bool AllowMultipleDefaultParametersWithSameName { get; }
86-
87-
/// <summary>
88-
/// Function used to encode parameters
89-
/// </summary>
90-
Func<string, string> Encode {
91-
get;
92-
[Obsolete("Don't change this options at runtime")]
93-
set;
94-
}
95-
96-
/// <summary>
97-
/// Function used to encode query parameters
98-
/// </summary>
99-
Func<string, Encoding, string> EncodeQuery {
100-
get;
101-
[Obsolete("Don't change this options at runtime")]
102-
set;
103-
}
104-
}
105-
106-
public class RestClientOptions : IRestClientOptions {
28+
[GenerateImmutable]
29+
public class RestClientOptions {
10730
static readonly Version Version = new AssemblyName(typeof(RestClientOptions).Assembly.FullName!).Version!;
10831

10932
static readonly string DefaultUserAgent = $"RestSharp/{Version}";

src/RestSharp/Request/RestRequest.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System.Net;
16+
using RestSharp.Authenticators;
1617
using RestSharp.Extensions;
1718

1819
// ReSharper disable ReplaceSubstringWithRangeIndexer
@@ -100,6 +101,11 @@ public RestRequest(Uri resource, Method method = Method.Get)
100101
/// </summary>
101102
public CookieContainer? CookieContainer { get; set; }
102103

104+
/// <summary>
105+
/// Request-level authenticator. It will be used if set, otherwise RestClient.Authenticator will be used.
106+
/// </summary>
107+
public IAuthenticator? Authenticator { get; set; }
108+
103109
/// <summary>
104110
/// Container of all the files to be uploaded with the request.
105111
/// </summary>

src/RestSharp/RestClient.Async.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task<RestResponse> ExecuteAsync(RestRequest request, CancellationTo
2929
internalResponse.ResponseMessage!,
3030
request,
3131
Options.Encoding,
32-
request.CookieContainer!.GetCookies(internalResponse.Url),
32+
request.CookieContainer?.GetCookies(internalResponse.Url),
3333
Options.CalculateResponseStatus,
3434
cancellationToken
3535
)
@@ -87,7 +87,8 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
8787

8888
using var requestContent = new RequestContent(this, request);
8989

90-
if (Options.Authenticator != null) await Options.Authenticator.Authenticate(this, request).ConfigureAwait(false);
90+
var authenticator = request.Authenticator ?? Options.Authenticator;
91+
if (authenticator != null) await authenticator.Authenticate(this, request).ConfigureAwait(false);
9192

9293
var httpMethod = AsHttpMethod(request.Method);
9394
var url = BuildUri(request);

0 commit comments

Comments
 (0)