Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions Activout.RestClient.Json/RestClientBuilderJsonExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using static Activout.RestClient.Json.SystemTextJsonDefaults;

namespace Activout.RestClient.Json;

Expand All @@ -12,15 +13,18 @@ public static class RestClientBuilderJsonExtensions
/// </summary>
/// <param name="builder">The REST client builder instance.</param>
/// <param name="jsonSerializerOptions">Optional custom JSON serializer options. If not provided, default options will be used.</param>
/// <param name="supportedMediaTypes">Optional list of content types to support. If not provided, defaults will be used.</param>
/// <param name="mediaTypes">Optional list of content types to support. If not provided, defaults will be used.</param>
/// <returns>The REST client builder instance.</returns>
public static IRestClientBuilder WithSystemTextJson(this IRestClientBuilder builder,
JsonSerializerOptions? jsonSerializerOptions = null,
MediaType[]? supportedMediaTypes = null)
MediaType[]? mediaTypes = null)
{
// Register the serializer and deserializer
builder.With(new SystemTextJsonSerializer(jsonSerializerOptions, supportedMediaTypes));
builder.With(new SystemTextJsonDeserializer(jsonSerializerOptions, supportedMediaTypes));
mediaTypes ??= MediaTypes;

builder.With(new SystemTextJsonSerializer(jsonSerializerOptions, mediaTypes));
builder.With(new SystemTextJsonDeserializer(jsonSerializerOptions, mediaTypes));
builder.Accept(string.Join(", ", mediaTypes.Select(type => type.Value)));
builder.ContentType(mediaTypes.First());

return builder;
}
Expand Down
3 changes: 1 addition & 2 deletions Activout.RestClient.Newtonsoft.Json.Test/RestClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Activout.RestClient.Helpers.Implementation;
using Activout.RestClient.Newtonsoft.Json.Test.MovieReviews;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
Expand Down Expand Up @@ -91,7 +90,7 @@ public void TestErrorSync()
// arrange
_mockHttp
.When(HttpMethod.Get, $"{BaseUri}/movies/{MovieId}/reviews/{ReviewId}")
.Respond(HttpStatusCode.NotFound, request => new StringContent(JsonConvert.SerializeObject(new
.Respond(HttpStatusCode.NotFound, _ => new StringContent(JsonConvert.SerializeObject(new
{
Errors = new object[]
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using static Activout.RestClient.Newtonsoft.Json.NewtonsoftJsonDefaults;

namespace Activout.RestClient.Newtonsoft.Json;

Expand All @@ -7,10 +8,12 @@ public static class RestClientBuilderNewtonsoftJsonExtensions
public static IRestClientBuilder WithNewtonsoftJson(this IRestClientBuilder builder,
JsonSerializerSettings? jsonSerializerSettings = null)
{
var settings = jsonSerializerSettings ?? NewtonsoftJsonDefaults.DefaultJsonSerializerSettings;
var settings = jsonSerializerSettings ?? DefaultJsonSerializerSettings;

builder.With(new NewtonsoftJsonSerializer(settings));
builder.With(new NewtonsoftJsonDeserializer(settings));
builder.Accept(string.Join(", ", SupportedMediaTypes.Select(type => type.Value)));
builder.ContentType(SupportedMediaTypes.First());

return builder;
}
Expand Down
19 changes: 19 additions & 0 deletions Activout.RestClient/Implementation/DummyRequestLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Net.Http;

namespace Activout.RestClient.Implementation;

internal sealed class DummyRequestLogger : IRequestLogger, IDisposable
{
public static IRequestLogger Instance { get; } = new DummyRequestLogger();

public IDisposable TimeOperation(HttpRequestMessage httpRequestMessage)
{
return this;
}

public void Dispose()
{
// Do nothing
}
}
85 changes: 28 additions & 57 deletions Activout.RestClient/Implementation/RestClient.cs
Original file line number Diff line number Diff line change
@@ -1,76 +1,47 @@
#nullable disable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using Activout.RestClient.DomainExceptions;

namespace Activout.RestClient.Implementation
namespace Activout.RestClient.Implementation;

internal class RestClient : DynamicObject
{
internal class RestClient<T> : DynamicObject where T : class
{
private readonly RestClientContext _context;
private readonly Type _type;
private readonly RestClientContext _context;
private readonly Type _type;

private readonly IDictionary<MethodInfo, RequestHandler> _requestHandlers =
new ConcurrentDictionary<MethodInfo, RequestHandler>();
private readonly IDictionary<MethodInfo, RequestHandler> _requestHandlers =
new ConcurrentDictionary<MethodInfo, RequestHandler>();

public RestClient(RestClientContext context)
{
_type = typeof(T);
_context = context;
HandleAttributes();
_context.DefaultSerializer = _context.SerializationManager.GetSerializer(_context.DefaultContentType);
}
public RestClient(Type type, RestClientContext context)
{
_type = type;
_context = context;
}

private void HandleAttributes()
{
var attributes = _type.GetCustomAttributes();
foreach (var attribute in attributes)
switch (attribute)
{
case ContentTypeAttribute contentTypeAttribute:
_context.DefaultContentType = MediaType.ValueOf(contentTypeAttribute.ContentType);
break;
case DomainExceptionAttribute domainExceptionAttribute:
_context.DomainExceptionType = domainExceptionAttribute.Type;
break;
case ErrorResponseAttribute errorResponseAttribute:
_context.ErrorResponseType = errorResponseAttribute.Type;
break;
case HeaderAttribute headerAttribute:
_context.DefaultHeaders.AddOrReplaceHeader(headerAttribute.Name, headerAttribute.Value, headerAttribute.Replace);
break;
case PathAttribute pathAttribute:
_context.BaseTemplate = pathAttribute.Template;
break;
}
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return _type.GetMembers().Select(x => x.Name);
}

public override IEnumerable<string> GetDynamicMemberNames()
public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result)
{
var method = _type.GetMethod(binder.Name);
if (method == null)
{
return _type.GetMembers().Select(x => x.Name);
result = null;
return false;
}
Comment on lines +31 to 36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix overload resolution to avoid AmbiguousMatchException and wrong method dispatch

_type.GetMethod(binder.Name) throws for overloaded methods and ignores argument shapes. Resolve by selecting the best candidate using argument count/types (and allowing a trailing CancellationToken).

Apply this diff to use a resolver:

-        var method = _type.GetMethod(binder.Name);
-        if (method == null)
+        var method = ResolveMethod(binder.Name, args);
+        if (method is null)
         {
             result = null;
             return false;
         }

Add this helper inside the class:

private MethodInfo? ResolveMethod(string name, object?[]? args)
{
    var candidates = _type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
                          .Where(m => m.Name == name)
                          .ToArray();

    if (candidates.Length == 0) return null;

    var argArray = args as object[] ?? Array.Empty<object>();
    var argCount = argArray.Length;

    foreach (var m in candidates)
    {
        var ps = m.GetParameters();
        var hasCt = ps.Length > 0 && ps[^1].ParameterType == typeof(System.Threading.CancellationToken);
        var matchCount = hasCt ? ps.Length - 1 : ps.Length;
        if (matchCount != argCount) continue;

        var ok = true;
        for (var i = 0; i < matchCount; i++)
        {
            var a = argArray[i];
            if (a is null) continue; // null is acceptable for ref/nullable params
            if (!ps[i].ParameterType.IsInstanceOfType(a)) { ok = false; break; }
        }
        if (ok) return m;
    }

    // Fallback: first by name
    return candidates.FirstOrDefault();
}

And add the needed using:

 using System.Reflection;
+using System.Threading;
🤖 Prompt for AI Agents
In Activout.RestClient/Implementation/RestClient.cs around lines 31 to 36, the
current call to _type.GetMethod(binder.Name) fails for overloaded methods and
ignores argument shapes; add a ResolveMethod helper (as described in the review)
inside the class to pick the correct MethodInfo by filtering candidates by name,
parameter count/types and allowing a trailing CancellationToken, replace the
direct GetMethod call with ResolveMethod(binder.Name, args) (or similar) so you
select the best overload, and add the required using for BindingFlags/Reflection
at the top of the file.


public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
if (!_requestHandlers.TryGetValue(method, out var requestHandler))
{
var method = _type.GetMethod(binder.Name);
if (method == null)
{
result = null;
return false;
}

if (!_requestHandlers.TryGetValue(method, out var requestHandler))
{
requestHandler = new RequestHandler(method, _context);
_requestHandlers[method] = requestHandler;
}

result = requestHandler.Send(args);
return true;
requestHandler = new RequestHandler(method, _context);
_requestHandlers[method] = requestHandler;
}

result = requestHandler.Send(args);
return true;
Comment on lines +44 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Null-safe handoff of args to RequestHandler.Send(object[])

TryInvokeMember receives object?[]?; RequestHandler.Send expects object[]. Coalesce to an empty array at the call site.

-        result = requestHandler.Send(args);
+        result = requestHandler.Send((args as object[]) ?? Array.Empty<object>());
         return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
result = requestHandler.Send(args);
return true;
result = requestHandler.Send((args as object[]) ?? Array.Empty<object>());
return true;
🤖 Prompt for AI Agents
In Activout.RestClient/Implementation/RestClient.cs around lines 44-45,
TryInvokeMember receives a nullable object?[]? named args but
RequestHandler.Send expects a non-null object[]; coalesce args to
Array.Empty<object>() (or new object[0]) at the call site so you pass a non-null
empty array when args is null, and then call requestHandler.Send with that
coalesced array.

}
}
Loading