Created
July 26, 2016 16:10
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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