Skip to content

Instantly share code, notes, and snippets.

@petcarerx
Created July 26, 2016 16:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save petcarerx/cea77a9bb2c3297ac69b5c79d3e9bf1f to your computer and use it in GitHub Desktop.
Save petcarerx/cea77a9bb2c3297ac69b5c79d3e9bf1f to your computer and use it in GitHub Desktop.
Expose a c# class as a barebones JSON web service on the local machine. Uses JSON.NET.
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;
using Newtonsoft.Json.Linq;
namespace com.hybrid47
{
/// <summary>
/// Expose a class as a barebones JSON web service. GET requests to retrieve properties or API, POST for method calls.
/// POST a JSON body matching the method arguments or empty body/object for a no parameter method.
/// Handles overloaded methods.
/// </summary>
/// <typeparam name="T"></typeparam>
public class Sake<T>:IDisposable where T:class, new()
{
private Thread _serverThread;
private HttpListener _listener;
Func<T> _instanceFactory;
private T _instance;
private int _port;
MethodInfo[] _methods;
MemberInfo[] _fieldsProperties;
/// <summary>
/// Create an instance that uses the default parameterless constructor of T()
/// </summary>
/// <param name="port"></param>
public Sake(int port):this(port, new T()){ }
/// <summary>
/// Creates an instance using a func to create the instance when a new instance of T needs to be created. This Func is evaluated when a DELETE is issued.
/// </summary>
/// <param name="port"></param>
/// <param name="injector"></param>
public Sake(int port, Func<T> instanceFactory) : this(port, instanceFactory.Invoke()) { _instanceFactory = instanceFactory; }
Sake(int port, T instance)
{
_port = port;
_instance = instance;
_methods = typeof(T).GetMethods();
_fieldsProperties = typeof(T).GetFields().Union<MemberInfo>(typeof(T).GetProperties()).ToArray();
}
public void Init()
{
if (_serverThread == null)
{
_serverThread = new Thread(() =>
{
_listener = new HttpListener();
_listener.Prefixes.Add("http://*:" + _port.ToString() + "/");
_listener.Start();
while (true)
{
HttpListenerContext context = _listener.GetContext();
Process(context);
}
});
_serverThread.Start();
}
}
private void Process(HttpListenerContext context)
{
if (context.Request.HttpMethod.Equals("GET", StringComparison.CurrentCultureIgnoreCase))
{
Get(context);
}
else if (context.Request.HttpMethod.Equals("POST", StringComparison.CurrentCultureIgnoreCase))
{
Post(context);
}
else if (context.Request.HttpMethod.Equals("DELETE", StringComparison.CurrentCultureIgnoreCase))
{
if (_instanceFactory == null)
{
_instance = new T();
}
else
{
_instance = _instanceFactory.Invoke();
}
}
}
/// <summary>
/// Retrieve a property value or the classes API details
/// </summary>
/// <param name="context"></param>
private void Get(HttpListenerContext context)
{
try
{
string requestPath = context.Request.Url.AbsolutePath;
if (requestPath.Equals("/"))
{
Respond(context, GetClassAPI(), HttpStatusCode.OK);
return;
}
else
{
requestPath = requestPath.TrimStart('/');
var resultObject = new
{
success = true,
result = (FindMemberInfo(requestPath) as PropertyInfo) != null
? (FindMemberInfo(requestPath) as PropertyInfo).GetValue(_instance)
: (FindMemberInfo(requestPath) as FieldInfo).GetValue(_instance)
};
Respond(context, resultObject, HttpStatusCode.OK);
}
}
catch (Exception ex)
{
Respond(context, new { success = false, result = ex.Message }, HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Get details about the methods and properties of the class exposed
/// </summary>
/// <returns></returns>
object GetClassAPI()
{
return new { success = true, properties = _fieldsProperties.ToDictionary(_ => _.Name, _ => GetUnderlyingType(_).FullName), methods = _methods };
}
static Type GetUnderlyingType(MemberInfo member)
{
switch (member.MemberType)
{
case MemberTypes.Event:
return ((EventInfo)member).EventHandlerType;
case MemberTypes.Field:
return ((FieldInfo)member).FieldType;
case MemberTypes.Method:
return ((MethodInfo)member).ReturnType;
case MemberTypes.Property:
return ((PropertyInfo)member).PropertyType;
default:
throw new ArgumentException("Input MemberInfo must be if type EventInfo, FieldInfo, MethodInfo, or PropertyInfo");
}
}
MemberInfo FindMemberInfo(string name)
{
return _fieldsProperties.Single(_ => _.Name.Equals(name));
}
MethodInfo ExtractMethodFromRequest(string methodName, string[] argumentKeys)
{
MethodInfo m = null;
if (_methods.Count(_ => _.Name.Equals(methodName)) > 1)//method is overloaded
{
foreach (MethodInfo mInfo in _methods.Where(_ => _.Name.Equals(methodName)).OrderByDescending(_ => _.GetParameters().Length))//by # of arguments desc
{
if (mInfo.GetParameters().Length == 0)
{ //if iterated to where we are now at a parameterless method, use this method reference.
m = mInfo;
break;
}
else if (mInfo.GetParameters().Select(_ => _.Name).Except(argumentKeys).Count() == 0) //if the overlap of method parameters and arguments leaves no parameters remaining, use this method reference.
{
m = mInfo;
break;
}
}
}
else
{
m = _methods.FirstOrDefault(_ => _.Name.Equals(methodName)); //single instance of method
}
return m;
}
/// <summary>
/// Execute a method
/// </summary>
/// <param name="context"></param>
private void Post(HttpListenerContext context)
{
MethodInfo m = null;
object resultObject = null;
var methodArguments = GetPostBody(context); //JObject from the POST body
var argumentKeys = methodArguments.Properties().Select(_ => _.Name).ToArray(); //keys from method arguments, keys correspond to method signature.
try
{
m = ExtractMethodFromRequest(context.Request.Url.AbsolutePath.TrimStart('/'), argumentKeys);
//if no mapping found, or parameters do not match exactly:
if (m == null || m.GetParameters().Select(_ => _.Name).Except(argumentKeys).Count() != 0)
{
Respond(context, new { success = false }, HttpStatusCode.BadRequest);
return;
}
object invokeResult = m.Invoke(_instance, m.GetParameters().Select(_ => GetParameter(_, methodArguments)).ToArray());
if (m.ReturnType == typeof(void) || invokeResult == null)
{
resultObject = new { success = true, result = (string)null };
}
else
{
resultObject = new { success = true, result = invokeResult };
}
Respond(context, resultObject, HttpStatusCode.OK);
}
catch (Exception ex)
{
Respond(context, new { success = false, result = ex.Message }, HttpStatusCode.InternalServerError);
}
}
private object GetParameter(ParameterInfo param, JObject methodArguments)
{
var argumentJsonObject = methodArguments[param.Name];
//utilize JSON.NET's serialization/deserialization libraries to map JSON to type properties:
return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(argumentJsonObject), param.ParameterType);
}
Newtonsoft.Json.Linq.JObject GetPostBody(HttpListenerContext context)
{
using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
{
context.Request.InputStream.CopyTo(ms);
string post_body = System.Text.Encoding.UTF8.GetString(ms.ToArray());
if (string.IsNullOrEmpty(post_body)) post_body = "{}";//allow for an empty post body for parameterless method call
return JsonConvert.DeserializeObject(post_body) as Newtonsoft.Json.Linq.JObject;
}
}
void Respond(HttpListenerContext context, object responseObject, HttpStatusCode statusCode)
{
string resultBody = JsonConvert.SerializeObject(responseObject, Formatting.Indented);
context.Response.ContentType = "application/json";
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
context.Response.ContentLength64 = resultBody.Length;
context.Response.AddHeader("Date", DateTime.Now.ToString("r"));
context.Response.AddHeader("Access-Control-Allow-Origin", "*");
context.Response.StatusCode = (int)statusCode;
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(resultBody);
context.Response.OutputStream.Write(responseBytes, 0, responseBytes.Length);
context.Response.OutputStream.Close();
}
public void Dispose()
{
if (_listener.IsListening)
{
_listener.Stop();
}
if (_serverThread.ThreadState == ThreadState.Running)
{
try { _serverThread.Abort(); }
catch { }
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment