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;
}
}
P.S. Let me know please if something is missing here which could prevent it from being really useful :)