Skip to main content
added 103 characters in body
Source Link
Dmitry Nogin
  • 6.1k
  • 3
  • 20
  • 40

P.S. Let me know please if something is missing here which could prevent it from being really useful :)

P.S. Let me know please if something is missing here which could prevent it from being really useful :)

added 2 characters in body
Source Link
Dmitry Nogin
  • 6.1k
  • 3
  • 20
  • 40

Optional authenticateauthenticate parameter supports custom authentication schemas using ad-hoc HttpClient extension method to setup HttpClient.DefaultRequestHeaders in a way like this:

Optional authenticate parameter supports custom authentication schemas using ad-hoc HttpClient extension method to setup HttpClient.DefaultRequestHeaders in a way like this:

Optional authenticate parameter supports custom authentication schemas using ad-hoc HttpClient extension method to setup HttpClient.DefaultRequestHeaders in a way like this:

Source Link
Dmitry Nogin
  • 6.1k
  • 3
  • 20
  • 40

Testable REST API Client

GitHub and NuGet

Allows to invoke public/private REST API just by defining an interface. 200 lines of C# in total.

Demo

Sample API is publicly available at http://jsonplaceholder.typicode.com:

public interface ITypicode : IDisposable
{
    [Description("GET posts")]
    Task<BlogPost[]> GetAsync();

    [Description("GET posts/{0}")]
    Task<BlogPost> GetAsync(int id);

    [Description("PUT posts/{0} {1}")]
    Task<BlogPost> PutAsync(int id, BlogPost data);
}

public class BlogPost
{
    public int UserId { get; set; }
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}

Now we could test proxy generator:

[TestMethod]
public async Task Call_REST_API()
{
    using (var proxy = Proxy.Create<ITypicode>("http://jsonplaceholder.typicode.com"))
    {
        var posts = await proxy.GetAsync();
        Assert.AreEqual(100, posts.Length);

        var post = await proxy.GetAsync(1);
        Assert.AreEqual(1, post.Id);

        post.Title = "XYZ";
        post = await proxy.PutAsync(1, post);
        Assert.AreEqual("XYZ", post.Title);
    }
}

Optional authenticate parameter supports custom authentication schemas using ad-hoc HttpClient extension method to setup HttpClient.DefaultRequestHeaders in a way like this:

Proxy.Create<ITypicode>(url, (HttpClient client) => client.AuthenticateAsync(...))

Library

Library code contains a unified proxy generator based on Castle.Core:

public static class Proxy
{
    public static T Create<T>(object target) where T : class =>
        Create<T>(call => Dynamic.InvokeMember(
            target, call.Name, call.Args.ToArray()));

    public static T Create<T>(string apiUrl, Func<HttpClient, Task> authenticate = null) where T : class, IDisposable =>
        Create<T>(new Uri(apiUrl, UriKind.Absolute), authenticate);

    public static T Create<T>(Uri apiUrl, Func<HttpClient, Task> authenticate = null) where T : class, IDisposable =>
        RestProxy.Create<T>(apiUrl, authenticate);

    public static T Create<T>(Func<ProxyCall, object> target) where T : class
    {
        var proxyGenerator = new ProxyGenerator();
        return proxyGenerator.CreateInterfaceProxyWithoutTarget<T>(
            ProxyGenerationOptions.Default,
            new Interceptor(target));
    }

    class Interceptor : IInterceptor
    {
        public Interceptor(Func<ProxyCall, object> target) => Target = target;
        Func<ProxyCall, object> Target { get; }
        public void Intercept(IInvocation invocation) =>
            invocation.ReturnValue = Target(
                new ProxyCall(invocation.Method.Name, invocation.Arguments)
                    .Returns(invocation.Method.ReturnType)
                    .Define(invocation.Method.GetCustomAttributes()));
    }
}

Where helpers are:

public class ProxyCall
{
    public ProxyCall(string name, IEnumerable<object> args)
        : this(name, args.ToArray())
    {
    }

    public ProxyCall(string name, params object[] args)
    {
        Name = name;
        Args = args;
    }

    public string Name { get; }
    public IReadOnlyList<object> Args { get; }

    public CallResult Result { get; private set; } = CallResult.None;
    public ProxyCall Returns<T>() => Returns(typeof(T));
    public ProxyCall ReturnsAsync<T>() => Returns(typeof(Task<T>));
    public ProxyCall Returns(Type type) => With(result: new CallResult(type));

    public CallMethod Method { get; private set; } = CallMethod.Undefined;
    public ProxyCall Define(string description) =>
        Define(new DescriptionAttribute(description));
    public ProxyCall Define<TAttribute>() where TAttribute : Attribute, new() =>
        Define(new TAttribute());
    public ProxyCall Define(IEnumerable<Attribute> attributes) =>
        Define(attributes.ToArray());
    public ProxyCall Define(params Attribute[] attributes) => 
        With(method: new CallMethod(attributes.Concat(Method)));
    
    ProxyCall With(CallResult result = null, CallMethod method = null) => 
        new ProxyCall(Name, Args) 
        { 
            Result = result ?? Result,
            Method = method ?? Method
        };

    public bool IsDispose => 
        Name == nameof(IDisposable.Dispose) &&
        Result.Void &&
        !Args.Any();
}

And:

public class CallResult 
{
    public static readonly CallResult None = new CallResult(typeof(void));

    internal CallResult(Type raw) => Raw = raw;
    public Type Raw { get; }
    public bool Sync => !Async;
    public bool Async => typeof(Task).IsAssignableFrom(Raw);
    public bool Void => Raw == typeof(void) || Raw == typeof(Task);
    public Type Type => Async 
        ? (Void ? typeof(void) : Raw.GetGenericArguments()[0])
        : Raw;
}

And:

public class CallMethod : ReadOnlyCollection<Attribute>
{
    public static readonly CallMethod Undefined = new CallMethod();

    internal CallMethod(IEnumerable<Attribute> attributes) 
        : this(attributes.ToArray())
    {
    }

    internal CallMethod(params Attribute[] attributes)
        : base(attributes)
    {
    }

    public bool Contains<TAttribute>() where TAttribute : Attribute =>
        Select<TAttribute>().Any();
    public T Peek<TAttribute, T>(Func<TAttribute, T> selector) where TAttribute : Attribute =>
        Select(selector).FirstOrDefault();
    public IEnumerable<Attribute> Select<TAttribute>() where TAttribute : Attribute =>
        Select((TAttribute a) => a);
    public IEnumerable<T> Select<TAttribute, T>(Func<TAttribute, T> selector) where TAttribute : Attribute =>
        Items.OfType<TAttribute>().Select(selector);

    public string Description => Peek((DescriptionAttribute a) => a.Description);
}

The following class provides all necessary support for REST API calls:

static class RestProxy
{
    public static T Create<T>(Uri apiUrl, Func<HttpClient, Task> authenticate = null) 
        where T : class, IDisposable
    {
        var client = new HttpClient() { BaseAddress = apiUrl };
        return Proxy.Create<T>(Execute);            
        object Execute(ProxyCall call)
        {
            if(call.IsDispose)
            {
                client.Dispose();
                return null;
            }
                            
            return call.Result.Void ? Send() : SendAndReceive();
            object Send() => client.SendAsync(call, authenticate);
            object SendAndReceive() =>
                typeof(RestProxy)
                    .GetMethod(nameof(SendAndReceiveAsync), BindingFlags.Static | BindingFlags.NonPublic)
                    .MakeGenericMethod(call.Result.Type)
                    .Invoke(null, new object[] { client, call, authenticate });
        }
    }        
    
    static async Task<T> SendAndReceiveAsync<T>(
        HttpClient client, ProxyCall call, Func<HttpClient, Task> authenticate)
    {
        var response = await client.SendAsync(call, authenticate);
        var json = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<T>(json);
    }

    static async Task<HttpResponseMessage> SendAsync(
        this HttpClient client, ProxyCall call, Func<HttpClient, Task> authenticate = null)
    {
        var description = call.Method.Description
            .Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

        var request = new HttpRequestMessage(
            new HttpMethod(description[0]),
            new Uri(
                string.Format(description[1], call.Args.ToArray()),
                UriKind.Relative))
            {
                Content = description.Length < 3 ||
                    !int.TryParse(description[2].Trim("{}".ToCharArray()), out var index)
                    ? null
                    : new StringContent(
                        JsonConvert.SerializeObject(call.Args[index]),
                        Encoding.UTF8,
                        "application/json")
            };

        var response = await client.SendAsync(request);
        if (response.StatusCode == HttpStatusCode.Unauthorized)
            if(authenticate == null)
                throw new AuthenticationException();
            else
            {
                await authenticate(client);
                return await SendAsync(client, call);
            }

        response.EnsureSuccessStatusCode();
        return response;            
    }
}