aboutsummaryrefslogtreecommitdiff
path: root/ServiceStack
diff options
context:
space:
mode:
Diffstat (limited to 'ServiceStack')
-rw-r--r--ServiceStack/FilterAttributeCache.cs27
-rw-r--r--ServiceStack/Host/ActionContext.cs27
-rw-r--r--ServiceStack/Host/ContentTypes.cs77
-rw-r--r--ServiceStack/Host/HttpResponseStreamWrapper.cs95
-rw-r--r--ServiceStack/Host/RestHandler.cs200
-rw-r--r--ServiceStack/Host/RestPath.cs443
-rw-r--r--ServiceStack/Host/ServiceController.cs220
-rw-r--r--ServiceStack/Host/ServiceExec.cs156
-rw-r--r--ServiceStack/Host/ServiceMetadata.cs27
-rw-r--r--ServiceStack/HttpHandlerFactory.cs27
-rw-r--r--ServiceStack/HttpRequestExtensions.cs127
-rw-r--r--ServiceStack/HttpResponseExtensionsInternal.cs237
-rw-r--r--ServiceStack/HttpResult.cs250
-rw-r--r--ServiceStack/HttpUtils.cs34
-rw-r--r--ServiceStack/Properties/AssemblyInfo.cs25
-rw-r--r--ServiceStack/ReflectionExtensions.cs270
-rw-r--r--ServiceStack/ServiceStack.csproj131
-rw-r--r--ServiceStack/ServiceStack.nuget.targets6
-rw-r--r--ServiceStack/ServiceStack.xproj19
-rw-r--r--ServiceStack/ServiceStackHost.Runtime.cs74
-rw-r--r--ServiceStack/ServiceStackHost.cs104
-rw-r--r--ServiceStack/StringMapTypeDeserializer.cs126
-rw-r--r--ServiceStack/UrlExtensions.cs33
-rw-r--r--ServiceStack/packages.config3
-rw-r--r--ServiceStack/project.json17
25 files changed, 2755 insertions, 0 deletions
diff --git a/ServiceStack/FilterAttributeCache.cs b/ServiceStack/FilterAttributeCache.cs
new file mode 100644
index 000000000..378433add
--- /dev/null
+++ b/ServiceStack/FilterAttributeCache.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using ServiceStack;
+
+namespace ServiceStack.Support.WebHost
+{
+ public static class FilterAttributeCache
+ {
+ public static MediaBrowser.Model.Services.IHasRequestFilter[] GetRequestFilterAttributes(Type requestDtoType)
+ {
+ var attributes = requestDtoType.AllAttributes().OfType<MediaBrowser.Model.Services.IHasRequestFilter>().ToList();
+
+ var serviceType = ServiceStackHost.Instance.Metadata.GetServiceTypeByRequest(requestDtoType);
+ if (serviceType != null)
+ {
+ attributes.AddRange(serviceType.AllAttributes().OfType<MediaBrowser.Model.Services.IHasRequestFilter>());
+ }
+
+ attributes.Sort((x,y) => x.Priority - y.Priority);
+
+ return attributes.ToArray();
+ }
+ }
+}
diff --git a/ServiceStack/Host/ActionContext.cs b/ServiceStack/Host/ActionContext.cs
new file mode 100644
index 000000000..9f165cff1
--- /dev/null
+++ b/ServiceStack/Host/ActionContext.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace ServiceStack.Host
+{
+ /// <summary>
+ /// Context to capture IService action
+ /// </summary>
+ public class ActionContext
+ {
+ public const string AnyAction = "ANY";
+
+ public string Id { get; set; }
+
+ public ActionInvokerFn ServiceAction { get; set; }
+ public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; }
+
+ public static string Key(Type serviceType, string method, string requestDtoName)
+ {
+ return serviceType.FullName + " " + method.ToUpper() + " " + requestDtoName;
+ }
+
+ public static string AnyKey(Type serviceType, string requestDtoName)
+ {
+ return Key(serviceType, AnyAction, requestDtoName);
+ }
+ }
+} \ No newline at end of file
diff --git a/ServiceStack/Host/ContentTypes.cs b/ServiceStack/Host/ContentTypes.cs
new file mode 100644
index 000000000..22fdc3e50
--- /dev/null
+++ b/ServiceStack/Host/ContentTypes.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MediaBrowser.Model.Services;
+
+namespace ServiceStack.Host
+{
+ public class ContentTypes
+ {
+ public static ContentTypes Instance = new ContentTypes();
+
+ public void SerializeToStream(IRequest req, object response, Stream responseStream)
+ {
+ var contentType = req.ResponseContentType;
+ var serializer = GetResponseSerializer(contentType);
+ if (serializer == null)
+ throw new NotSupportedException("ContentType not supported: " + contentType);
+
+ var httpRes = new HttpResponseStreamWrapper(responseStream, req)
+ {
+ Dto = req.Response.Dto
+ };
+ serializer(req, response, httpRes);
+ }
+
+ public Action<IRequest, object, IResponse> GetResponseSerializer(string contentType)
+ {
+ var serializer = GetStreamSerializer(contentType);
+ if (serializer == null) return null;
+
+ return (httpReq, dto, httpRes) => serializer(httpReq, dto, httpRes.OutputStream);
+ }
+
+ public Action<IRequest, object, Stream> GetStreamSerializer(string contentType)
+ {
+ switch (GetRealContentType(contentType))
+ {
+ case "application/xml":
+ case "text/xml":
+ case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
+ return (r, o, s) => ServiceStackHost.Instance.SerializeToXml(o, s);
+
+ case "application/json":
+ case "text/json":
+ return (r, o, s) => ServiceStackHost.Instance.SerializeToJson(o, s);
+ }
+
+ return null;
+ }
+
+ public Func<Type, Stream, object> GetStreamDeserializer(string contentType)
+ {
+ switch (GetRealContentType(contentType))
+ {
+ case "application/xml":
+ case "text/xml":
+ case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
+ return ServiceStackHost.Instance.DeserializeXml;
+
+ case "application/json":
+ case "text/json":
+ return ServiceStackHost.Instance.DeserializeJson;
+ }
+
+ return null;
+ }
+
+ private static string GetRealContentType(string contentType)
+ {
+ return contentType == null
+ ? null
+ : contentType.Split(';')[0].ToLower().Trim();
+ }
+
+ }
+} \ No newline at end of file
diff --git a/ServiceStack/Host/HttpResponseStreamWrapper.cs b/ServiceStack/Host/HttpResponseStreamWrapper.cs
new file mode 100644
index 000000000..33038da72
--- /dev/null
+++ b/ServiceStack/Host/HttpResponseStreamWrapper.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using MediaBrowser.Model.Services;
+
+namespace ServiceStack.Host
+{
+ public class HttpResponseStreamWrapper : IHttpResponse
+ {
+ private static readonly UTF8Encoding UTF8EncodingWithoutBom = new UTF8Encoding(false);
+
+ public HttpResponseStreamWrapper(Stream stream, IRequest request)
+ {
+ this.OutputStream = stream;
+ this.Request = request;
+ this.Headers = new Dictionary<string, string>();
+ this.Items = new Dictionary<string, object>();
+ }
+
+ public Dictionary<string, string> Headers { get; set; }
+
+ public object OriginalResponse
+ {
+ get { return null; }
+ }
+
+ public IRequest Request { get; private set; }
+
+ public int StatusCode { set; get; }
+ public string StatusDescription { set; get; }
+ public string ContentType { get; set; }
+
+ public void AddHeader(string name, string value)
+ {
+ this.Headers[name] = value;
+ }
+
+ public string GetHeader(string name)
+ {
+ return this.Headers[name];
+ }
+
+ public void Redirect(string url)
+ {
+ this.Headers["Location"] = url;
+ }
+
+ public Stream OutputStream { get; private set; }
+
+ public object Dto { get; set; }
+
+ public void Write(string text)
+ {
+ var bytes = UTF8EncodingWithoutBom.GetBytes(text);
+ OutputStream.Write(bytes, 0, bytes.Length);
+ }
+
+ public bool UseBufferedStream { get; set; }
+
+ public void Close()
+ {
+ if (IsClosed) return;
+
+ OutputStream.Dispose();
+ IsClosed = true;
+ }
+
+ public void End()
+ {
+ Close();
+ }
+
+ public void Flush()
+ {
+ OutputStream.Flush();
+ }
+
+ public bool IsClosed { get; private set; }
+
+ public void SetContentLength(long contentLength) {}
+
+ public bool KeepAlive { get; set; }
+
+ public Dictionary<string, object> Items { get; private set; }
+
+ public void SetCookie(Cookie cookie)
+ {
+ }
+
+ public void ClearCookies()
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/ServiceStack/Host/RestHandler.cs b/ServiceStack/Host/RestHandler.cs
new file mode 100644
index 000000000..5c360d150
--- /dev/null
+++ b/ServiceStack/Host/RestHandler.cs
@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+
+namespace ServiceStack.Host
+{
+ public class RestHandler
+ {
+ public string RequestName { get; set; }
+
+ public async Task<object> HandleResponseAsync(object response)
+ {
+ var taskResponse = response as Task;
+
+ if (taskResponse == null)
+ {
+ return response;
+ }
+
+ await taskResponse.ConfigureAwait(false);
+
+ var taskResult = ServiceStackHost.Instance.GetTaskResult(taskResponse, RequestName);
+
+ var taskResults = taskResult as Task[];
+
+ if (taskResults == null)
+ {
+ var subTask = taskResult as Task;
+ if (subTask != null)
+ taskResult = ServiceStackHost.Instance.GetTaskResult(subTask, RequestName);
+
+ return taskResult;
+ }
+
+ if (taskResults.Length == 0)
+ {
+ return new object[] { };
+ }
+
+ var firstResponse = ServiceStackHost.Instance.GetTaskResult(taskResults[0], RequestName);
+ var batchedResponses = firstResponse != null
+ ? (object[])Array.CreateInstance(firstResponse.GetType(), taskResults.Length)
+ : new object[taskResults.Length];
+ batchedResponses[0] = firstResponse;
+ for (var i = 1; i < taskResults.Length; i++)
+ {
+ batchedResponses[i] = ServiceStackHost.Instance.GetTaskResult(taskResults[i], RequestName);
+ }
+ return batchedResponses;
+ }
+
+ protected static object CreateContentTypeRequest(IRequest httpReq, Type requestType, string contentType)
+ {
+ if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
+ {
+ var deserializer = ContentTypes.Instance.GetStreamDeserializer(contentType);
+ if (deserializer != null)
+ {
+ return deserializer(requestType, httpReq.InputStream);
+ }
+ }
+ return ServiceStackHost.Instance.CreateInstance(requestType); //Return an empty DTO, even for empty request bodies
+ }
+
+ protected static object GetCustomRequestFromBinder(IRequest httpReq, Type requestType)
+ {
+ Func<IRequest, object> requestFactoryFn;
+ ServiceStackHost.Instance.ServiceController.RequestTypeFactoryMap.TryGetValue(
+ requestType, out requestFactoryFn);
+
+ return requestFactoryFn != null ? requestFactoryFn(httpReq) : null;
+ }
+
+ public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType)
+ {
+ pathInfo = GetSanitizedPathInfo(pathInfo, out contentType);
+
+ return ServiceStackHost.Instance.ServiceController.GetRestPathForRequest(httpMethod, pathInfo);
+ }
+
+ public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
+ {
+ contentType = null;
+ var pos = pathInfo.LastIndexOf('.');
+ if (pos >= 0)
+ {
+ var format = pathInfo.Substring(pos + 1);
+ contentType = GetFormatContentType(format);
+ if (contentType != null)
+ {
+ pathInfo = pathInfo.Substring(0, pos);
+ }
+ }
+ return pathInfo;
+ }
+
+ private static string GetFormatContentType(string format)
+ {
+ //built-in formats
+ if (format == "json")
+ return "application/json";
+ if (format == "xml")
+ return "application/xml";
+
+ return null;
+ }
+
+ public RestPath GetRestPath(string httpMethod, string pathInfo)
+ {
+ if (this.RestPath == null)
+ {
+ string contentType;
+ this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, out contentType);
+
+ if (contentType != null)
+ ResponseContentType = contentType;
+ }
+ return this.RestPath;
+ }
+
+ public RestPath RestPath { get; set; }
+
+ // Set from SSHHF.GetHandlerForPathInfo()
+ public string ResponseContentType { get; set; }
+
+ public async Task ProcessRequestAsync(IRequest httpReq, IResponse httpRes, string operationName)
+ {
+ var appHost = ServiceStackHost.Instance;
+
+ var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo);
+ if (restPath == null)
+ {
+ throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo);
+ }
+ httpReq.SetRoute(restPath);
+
+ if (ResponseContentType != null)
+ httpReq.ResponseContentType = ResponseContentType;
+
+ var request = httpReq.Dto = CreateRequest(httpReq, restPath);
+
+ if (appHost.ApplyRequestFilters(httpReq, httpRes, request))
+ return;
+
+ var rawResponse = await ServiceStackHost.Instance.ServiceController.Execute(request, httpReq).ConfigureAwait(false);
+
+ if (httpRes.IsClosed)
+ return;
+
+ var response = await HandleResponseAsync(rawResponse).ConfigureAwait(false);
+
+ if (appHost.ApplyResponseFilters(httpReq, httpRes, response))
+ return;
+
+ await httpRes.WriteToResponse(httpReq, response).ConfigureAwait(false);
+ }
+
+ public static object CreateRequest(IRequest httpReq, RestPath restPath)
+ {
+ var dtoFromBinder = GetCustomRequestFromBinder(httpReq, restPath.RequestType);
+ if (dtoFromBinder != null)
+ return dtoFromBinder;
+
+ var requestParams = httpReq.GetFlattenedRequestParams();
+ return CreateRequest(httpReq, restPath, requestParams);
+ }
+
+ public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams)
+ {
+ var requestDto = CreateContentTypeRequest(httpReq, restPath.RequestType, httpReq.ContentType);
+
+ return CreateRequest(httpReq, restPath, requestParams, requestDto);
+ }
+
+ public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto)
+ {
+ string contentType;
+ var pathInfo = !restPath.IsWildCardPath
+ ? GetSanitizedPathInfo(httpReq.PathInfo, out contentType)
+ : httpReq.PathInfo;
+
+ return restPath.CreateRequest(pathInfo, requestParams, requestDto);
+ }
+
+ /// <summary>
+ /// Used in Unit tests
+ /// </summary>
+ /// <returns></returns>
+ public object CreateRequest(IRequest httpReq, string operationName)
+ {
+ if (this.RestPath == null)
+ throw new ArgumentNullException("No RestPath found");
+
+ return CreateRequest(httpReq, this.RestPath);
+ }
+ }
+
+}
diff --git a/ServiceStack/Host/RestPath.cs b/ServiceStack/Host/RestPath.cs
new file mode 100644
index 000000000..7222578a9
--- /dev/null
+++ b/ServiceStack/Host/RestPath.cs
@@ -0,0 +1,443 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using ServiceStack.Serialization;
+
+namespace ServiceStack.Host
+{
+ public class RestPath
+ {
+ private const string WildCard = "*";
+ private const char WildCardChar = '*';
+ private const string PathSeperator = "/";
+ private const char PathSeperatorChar = '/';
+ private const char ComponentSeperator = '.';
+ private const string VariablePrefix = "{";
+
+ readonly bool[] componentsWithSeparators;
+
+ private readonly string restPath;
+ private readonly string allowedVerbs;
+ private readonly bool allowsAllVerbs;
+ public bool IsWildCardPath { get; private set; }
+
+ private readonly string[] literalsToMatch;
+
+ private readonly string[] variablesNames;
+
+ private readonly bool[] isWildcard;
+ private readonly int wildcardCount = 0;
+
+ public int VariableArgsCount { get; set; }
+
+ /// <summary>
+ /// The number of segments separated by '/' determinable by path.Split('/').Length
+ /// e.g. /path/to/here.ext == 3
+ /// </summary>
+ public int PathComponentsCount { get; set; }
+
+ /// <summary>
+ /// The total number of segments after subparts have been exploded ('.')
+ /// e.g. /path/to/here.ext == 4
+ /// </summary>
+ public int TotalComponentsCount { get; set; }
+
+ public string[] Verbs
+ {
+ get
+ {
+ return allowsAllVerbs
+ ? new[] { ActionContext.AnyAction }
+ : AllowedVerbs.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+ }
+
+ public Type RequestType { get; private set; }
+
+ public string Path { get { return this.restPath; } }
+
+ public string Summary { get; private set; }
+
+ public string Notes { get; private set; }
+
+ public bool AllowsAllVerbs { get { return this.allowsAllVerbs; } }
+
+ public string AllowedVerbs { get { return this.allowedVerbs; } }
+
+ public int Priority { get; set; } //passed back to RouteAttribute
+
+ public static string[] GetPathPartsForMatching(string pathInfo)
+ {
+ var parts = pathInfo.ToLower().Split(PathSeperatorChar)
+ .Where(x => !string.IsNullOrEmpty(x)).ToArray();
+ return parts;
+ }
+
+ public static IEnumerable<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
+ {
+ var hashPrefix = pathPartsForMatching.Length + PathSeperator;
+ return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
+ }
+
+ public static IEnumerable<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
+ {
+ const string hashPrefix = WildCard + PathSeperator;
+ return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
+ }
+
+ private static IEnumerable<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
+ {
+ foreach (var part in pathPartsForMatching)
+ {
+ yield return hashPrefix + part;
+ var subParts = part.Split(ComponentSeperator);
+ if (subParts.Length == 1) continue;
+
+ foreach (var subPart in subParts)
+ {
+ yield return hashPrefix + subPart;
+ }
+ }
+ }
+
+ public RestPath(Type requestType, string path, string verbs, string summary = null, string notes = null)
+ {
+ this.RequestType = requestType;
+ this.Summary = summary;
+ this.Notes = notes;
+ this.restPath = path;
+
+ this.allowsAllVerbs = verbs == null || verbs == WildCard;
+ if (!this.allowsAllVerbs)
+ {
+ this.allowedVerbs = verbs.ToUpper();
+ }
+
+ var componentsList = new List<string>();
+
+ //We only split on '.' if the restPath has them. Allows for /{action}.{type}
+ var hasSeparators = new List<bool>();
+ foreach (var component in this.restPath.Split(PathSeperatorChar))
+ {
+ if (string.IsNullOrEmpty(component)) continue;
+
+ if (component.Contains(VariablePrefix)
+ && component.IndexOf(ComponentSeperator) != -1)
+ {
+ hasSeparators.Add(true);
+ componentsList.AddRange(component.Split(ComponentSeperator));
+ }
+ else
+ {
+ hasSeparators.Add(false);
+ componentsList.Add(component);
+ }
+ }
+
+ var components = componentsList.ToArray();
+ this.TotalComponentsCount = components.Length;
+
+ this.literalsToMatch = new string[this.TotalComponentsCount];
+ this.variablesNames = new string[this.TotalComponentsCount];
+ this.isWildcard = new bool[this.TotalComponentsCount];
+ this.componentsWithSeparators = hasSeparators.ToArray();
+ this.PathComponentsCount = this.componentsWithSeparators.Length;
+ string firstLiteralMatch = null;
+
+ var sbHashKey = new StringBuilder();
+ for (var i = 0; i < components.Length; i++)
+ {
+ var component = components[i];
+
+ if (component.StartsWith(VariablePrefix))
+ {
+ var variableName = component.Substring(1, component.Length - 2);
+ if (variableName[variableName.Length - 1] == WildCardChar)
+ {
+ this.isWildcard[i] = true;
+ variableName = variableName.Substring(0, variableName.Length - 1);
+ }
+ this.variablesNames[i] = variableName;
+ this.VariableArgsCount++;
+ }
+ else
+ {
+ this.literalsToMatch[i] = component.ToLower();
+ sbHashKey.Append(i + PathSeperatorChar.ToString() + this.literalsToMatch);
+
+ if (firstLiteralMatch == null)
+ {
+ firstLiteralMatch = this.literalsToMatch[i];
+ }
+ }
+ }
+
+ for (var i = 0; i < components.Length - 1; i++)
+ {
+ if (!this.isWildcard[i]) continue;
+ if (this.literalsToMatch[i + 1] == null)
+ {
+ throw new ArgumentException(
+ "A wildcard path component must be at the end of the path or followed by a literal path component.");
+ }
+ }
+
+ this.wildcardCount = this.isWildcard.Count(x => x);
+ this.IsWildCardPath = this.wildcardCount > 0;
+
+ this.FirstMatchHashKey = !this.IsWildCardPath
+ ? this.PathComponentsCount + PathSeperator + firstLiteralMatch
+ : WildCardChar + PathSeperator + firstLiteralMatch;
+
+ this.IsValid = sbHashKey.Length > 0;
+ this.UniqueMatchHashKey = sbHashKey.ToString();
+
+ this.typeDeserializer = new StringMapTypeDeserializer(this.RequestType);
+ RegisterCaseInsenstivePropertyNameMappings();
+ }
+
+ private void RegisterCaseInsenstivePropertyNameMappings()
+ {
+ foreach (var propertyInfo in RequestType.GetSerializableProperties())
+ {
+ var propertyName = propertyInfo.Name;
+ propertyNamesMap.Add(propertyName.ToLower(), propertyName);
+ }
+ }
+
+ public bool IsValid { get; set; }
+
+ /// <summary>
+ /// Provide for quick lookups based on hashes that can be determined from a request url
+ /// </summary>
+ public string FirstMatchHashKey { get; private set; }
+
+ public string UniqueMatchHashKey { get; private set; }
+
+ private readonly StringMapTypeDeserializer typeDeserializer;
+
+ private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>();
+
+ public static Func<RestPath, string, string[], int> CalculateMatchScore { get; set; }
+
+ public int MatchScore(string httpMethod, string[] withPathInfoParts)
+ {
+ if (CalculateMatchScore != null)
+ return CalculateMatchScore(this, httpMethod, withPathInfoParts);
+
+ int wildcardMatchCount;
+ var isMatch = IsMatch(httpMethod, withPathInfoParts, out wildcardMatchCount);
+ if (!isMatch) return -1;
+
+ var score = 0;
+
+ //Routes with least wildcard matches get the highest score
+ score += Math.Max((100 - wildcardMatchCount), 1) * 1000;
+
+ //Routes with less variable (and more literal) matches
+ score += Math.Max((10 - VariableArgsCount), 1) * 100;
+
+ //Exact verb match is better than ANY
+ var exactVerb = httpMethod == AllowedVerbs;
+ score += exactVerb ? 10 : 1;
+
+ return score;
+ }
+
+ /// <summary>
+ /// For performance withPathInfoParts should already be a lower case string
+ /// to minimize redundant matching operations.
+ /// </summary>
+ /// <param name="httpMethod"></param>
+ /// <param name="withPathInfoParts"></param>
+ /// <param name="wildcardMatchCount"></param>
+ /// <returns></returns>
+ public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount)
+ {
+ wildcardMatchCount = 0;
+
+ if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath) return false;
+ if (!this.allowsAllVerbs && !this.allowedVerbs.Contains(httpMethod.ToUpper())) return false;
+
+ if (!ExplodeComponents(ref withPathInfoParts)) return false;
+ if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath) return false;
+
+ int pathIx = 0;
+ for (var i = 0; i < this.TotalComponentsCount; i++)
+ {
+ if (this.isWildcard[i])
+ {
+ if (i < this.TotalComponentsCount - 1)
+ {
+ // Continue to consume up until a match with the next literal
+ while (pathIx < withPathInfoParts.Length && withPathInfoParts[pathIx] != this.literalsToMatch[i + 1])
+ {
+ pathIx++;
+ wildcardMatchCount++;
+ }
+
+ // Ensure there are still enough parts left to match the remainder
+ if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ // A wildcard at the end matches the remainder of path
+ wildcardMatchCount += withPathInfoParts.Length - pathIx;
+ pathIx = withPathInfoParts.Length;
+ }
+ }
+ else
+ {
+ var literalToMatch = this.literalsToMatch[i];
+ if (literalToMatch == null)
+ {
+ // Matching an ordinary (non-wildcard) variable consumes a single part
+ pathIx++;
+ continue;
+ }
+
+ if (withPathInfoParts.Length <= pathIx || withPathInfoParts[pathIx] != literalToMatch) return false;
+ pathIx++;
+ }
+ }
+
+ return pathIx == withPathInfoParts.Length;
+ }
+
+ private bool ExplodeComponents(ref string[] withPathInfoParts)
+ {
+ var totalComponents = new List<string>();
+ for (var i = 0; i < withPathInfoParts.Length; i++)
+ {
+ var component = withPathInfoParts[i];
+ if (string.IsNullOrEmpty(component)) continue;
+
+ if (this.PathComponentsCount != this.TotalComponentsCount
+ && this.componentsWithSeparators[i])
+ {
+ var subComponents = component.Split(ComponentSeperator);
+ if (subComponents.Length < 2) return false;
+ totalComponents.AddRange(subComponents);
+ }
+ else
+ {
+ totalComponents.Add(component);
+ }
+ }
+
+ withPathInfoParts = totalComponents.ToArray();
+ return true;
+ }
+
+ public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
+ {
+ var requestComponents = pathInfo.Split(PathSeperatorChar)
+ .Where(x => !string.IsNullOrEmpty(x)).ToArray();
+
+ ExplodeComponents(ref requestComponents);
+
+ if (requestComponents.Length != this.TotalComponentsCount)
+ {
+ var isValidWildCardPath = this.IsWildCardPath
+ && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
+
+ if (!isValidWildCardPath)
+ throw new ArgumentException(string.Format(
+ "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
+ pathInfo, this.restPath));
+ }
+
+ var requestKeyValuesMap = new Dictionary<string, string>();
+ var pathIx = 0;
+ for (var i = 0; i < this.TotalComponentsCount; i++)
+ {
+ var variableName = this.variablesNames[i];
+ if (variableName == null)
+ {
+ pathIx++;
+ continue;
+ }
+
+ string propertyNameOnRequest;
+ if (!this.propertyNamesMap.TryGetValue(variableName.ToLower(), out propertyNameOnRequest))
+ {
+ if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
+ {
+ pathIx++;
+ continue;
+ }
+
+ throw new ArgumentException("Could not find property "
+ + variableName + " on " + RequestType.GetOperationName());
+ }
+
+ var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; //wildcard has arg mismatch
+ if (value != null && this.isWildcard[i])
+ {
+ if (i == this.TotalComponentsCount - 1)
+ {
+ // Wildcard at end of path definition consumes all the rest
+ var sb = new StringBuilder();
+ sb.Append(value);
+ for (var j = pathIx + 1; j < requestComponents.Length; j++)
+ {
+ sb.Append(PathSeperatorChar + requestComponents[j]);
+ }
+ value = sb.ToString();
+ }
+ else
+ {
+ // Wildcard in middle of path definition consumes up until it
+ // hits a match for the next element in the definition (which must be a literal)
+ // It may consume 0 or more path parts
+ var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
+ if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
+ {
+ var sb = new StringBuilder();
+ sb.Append(value);
+ pathIx++;
+ while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
+ {
+ sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
+ }
+ value = sb.ToString();
+ }
+ else
+ {
+ value = null;
+ }
+ }
+ }
+ else
+ {
+ // Variable consumes single path item
+ pathIx++;
+ }
+
+ requestKeyValuesMap[propertyNameOnRequest] = value;
+ }
+
+ if (queryStringAndFormData != null)
+ {
+ //Query String and form data can override variable path matches
+ //path variables < query string < form data
+ foreach (var name in queryStringAndFormData)
+ {
+ requestKeyValuesMap[name.Key] = name.Value;
+ }
+ }
+
+ return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
+ }
+
+ public override int GetHashCode()
+ {
+ return UniqueMatchHashKey.GetHashCode();
+ }
+ }
+} \ No newline at end of file
diff --git a/ServiceStack/Host/ServiceController.cs b/ServiceStack/Host/ServiceController.cs
new file mode 100644
index 000000000..703f06365
--- /dev/null
+++ b/ServiceStack/Host/ServiceController.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+
+namespace ServiceStack.Host
+{
+ public delegate Task<object> InstanceExecFn(IRequest requestContext, object intance, object request);
+ public delegate object ActionInvokerFn(object intance, object request);
+ public delegate void VoidActionInvokerFn(object intance, object request);
+
+ public class ServiceController
+ {
+ private readonly Func<IEnumerable<Type>> _resolveServicesFn;
+
+ public ServiceController(Func<IEnumerable<Type>> resolveServicesFn)
+ {
+ _resolveServicesFn = resolveServicesFn;
+ this.RequestTypeFactoryMap = new Dictionary<Type, Func<IRequest, object>>();
+ }
+
+ public Dictionary<Type, Func<IRequest, object>> RequestTypeFactoryMap { get; set; }
+
+ public void Init()
+ {
+ foreach (var serviceType in _resolveServicesFn())
+ {
+ RegisterService(serviceType);
+ }
+ }
+
+ private Type[] GetGenericArguments(Type type)
+ {
+ return type.GetTypeInfo().IsGenericTypeDefinition
+ ? type.GetTypeInfo().GenericTypeParameters
+ : type.GetTypeInfo().GenericTypeArguments;
+ }
+
+ public void RegisterService(Type serviceType)
+ {
+ var processedReqs = new HashSet<Type>();
+
+ var actions = ServiceExecGeneral.Reset(serviceType);
+
+ var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo();
+
+ var appHost = ServiceStackHost.Instance;
+ foreach (var mi in serviceType.GetActions())
+ {
+ var requestType = mi.GetParameters()[0].ParameterType;
+ if (processedReqs.Contains(requestType)) continue;
+ processedReqs.Add(requestType);
+
+ ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
+
+ var returnMarker = requestType.GetTypeWithGenericTypeDefinitionOf(typeof(IReturn<>));
+ var responseType = returnMarker != null ?
+ GetGenericArguments(returnMarker)[0]
+ : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ?
+ mi.ReturnType
+ : Type.GetType(requestType.FullName + "Response");
+
+ RegisterRestPaths(requestType);
+
+ appHost.Metadata.Add(serviceType, requestType, responseType);
+
+ if (requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo()))
+ {
+ this.RequestTypeFactoryMap[requestType] = req =>
+ {
+ var restPath = req.GetRoute();
+ var request = RestHandler.CreateRequest(req, restPath, req.GetRequestParams(), ServiceStackHost.Instance.CreateInstance(requestType));
+
+ var rawReq = (IRequiresRequestStream)request;
+ rawReq.RequestStream = req.InputStream;
+ return rawReq;
+ };
+ }
+ }
+ }
+
+ public readonly Dictionary<string, List<RestPath>> RestPathMap = new Dictionary<string, List<RestPath>>();
+
+ public void RegisterRestPaths(Type requestType)
+ {
+ var appHost = ServiceStackHost.Instance;
+ var attrs = appHost.GetRouteAttributes(requestType);
+ foreach (MediaBrowser.Model.Services.RouteAttribute attr in attrs)
+ {
+ var restPath = new RestPath(requestType, attr.Path, attr.Verbs, attr.Summary, attr.Notes);
+
+ if (!restPath.IsValid)
+ throw new NotSupportedException(string.Format(
+ "RestPath '{0}' on Type '{1}' is not Valid", attr.Path, requestType.GetOperationName()));
+
+ RegisterRestPath(restPath);
+ }
+ }
+
+ private static readonly char[] InvalidRouteChars = new[] { '?', '&' };
+
+ public void RegisterRestPath(RestPath restPath)
+ {
+ if (!restPath.Path.StartsWith("/"))
+ throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetOperationName()));
+ if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
+ throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. " +
+ "See https://github.com/ServiceStack/ServiceStack/wiki/Routing for info on valid routes.", restPath.Path, restPath.RequestType.GetOperationName()));
+
+ List<RestPath> pathsAtFirstMatch;
+ if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out pathsAtFirstMatch))
+ {
+ pathsAtFirstMatch = new List<RestPath>();
+ RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch;
+ }
+ pathsAtFirstMatch.Add(restPath);
+ }
+
+ public void AfterInit()
+ {
+ var appHost = ServiceStackHost.Instance;
+
+ //Register any routes configured on Metadata.Routes
+ foreach (var restPath in appHost.RestPaths)
+ {
+ RegisterRestPath(restPath);
+ }
+
+ //Sync the RestPaths collections
+ appHost.RestPaths.Clear();
+ appHost.RestPaths.AddRange(RestPathMap.Values.SelectMany(x => x));
+ }
+
+ public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
+ {
+ var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo);
+
+ List<RestPath> firstMatches;
+
+ var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts);
+ foreach (var potentialHashMatch in yieldedHashMatches)
+ {
+ if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue;
+
+ var bestScore = -1;
+ foreach (var restPath in firstMatches)
+ {
+ var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
+ if (score > bestScore) bestScore = score;
+ }
+ if (bestScore > 0)
+ {
+ foreach (var restPath in firstMatches)
+ {
+ if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts))
+ return restPath;
+ }
+ }
+ }
+
+ var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts);
+ foreach (var potentialHashMatch in yieldedWildcardMatches)
+ {
+ if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue;
+
+ var bestScore = -1;
+ foreach (var restPath in firstMatches)
+ {
+ var score = restPath.MatchScore(httpMethod, matchUsingPathParts);
+ if (score > bestScore) bestScore = score;
+ }
+ if (bestScore > 0)
+ {
+ foreach (var restPath in firstMatches)
+ {
+ if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts))
+ return restPath;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public async Task<object> Execute(object requestDto, IRequest req)
+ {
+ req.Dto = requestDto;
+ var requestType = requestDto.GetType();
+ req.OperationName = requestType.Name;
+
+ var serviceType = ServiceStackHost.Instance.Metadata.GetServiceTypeByRequest(requestType);
+
+ var service = ServiceStackHost.Instance.CreateInstance(serviceType);
+
+ //var service = typeFactory.CreateInstance(serviceType);
+
+ var serviceRequiresContext = service as IRequiresRequest;
+ if (serviceRequiresContext != null)
+ {
+ serviceRequiresContext.Request = req;
+ }
+
+ if (req.Dto == null) // Don't override existing batched DTO[]
+ req.Dto = requestDto;
+
+ //Executes the service and returns the result
+ var response = await ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetOperationName()).ConfigureAwait(false);
+
+ if (req.Response.Dto == null)
+ req.Response.Dto = response;
+
+ return response;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/ServiceStack/Host/ServiceExec.cs b/ServiceStack/Host/ServiceExec.cs
new file mode 100644
index 000000000..cb501a3ad
--- /dev/null
+++ b/ServiceStack/Host/ServiceExec.cs
@@ -0,0 +1,156 @@
+//Copyright (c) Service Stack LLC. All Rights Reserved.
+//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+
+namespace ServiceStack.Host
+{
+ public static class ServiceExecExtensions
+ {
+ public static IEnumerable<MethodInfo> GetActions(this Type serviceType)
+ {
+ foreach (var mi in serviceType.GetRuntimeMethods().Where(i => i.IsPublic && !i.IsStatic))
+ {
+ if (mi.GetParameters().Length != 1)
+ continue;
+
+ var actionName = mi.Name.ToUpper();
+ if (!HttpMethods.AllVerbs.Contains(actionName) && actionName != ActionContext.AnyAction)
+ continue;
+
+ yield return mi;
+ }
+ }
+ }
+
+ internal static class ServiceExecGeneral
+ {
+ public static Dictionary<string, ActionContext> execMap = new Dictionary<string, ActionContext>();
+
+ public static void CreateServiceRunnersFor(Type requestType, List<ActionContext> actions)
+ {
+ foreach (var actionCtx in actions)
+ {
+ if (execMap.ContainsKey(actionCtx.Id)) continue;
+
+ execMap[actionCtx.Id] = actionCtx;
+ }
+ }
+
+ public static async Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName)
+ {
+ var actionName = request.Verb
+ ?? HttpMethods.Post; //MQ Services
+
+ ActionContext actionContext;
+ if (ServiceExecGeneral.execMap.TryGetValue(ActionContext.Key(serviceType, actionName, requestName), out actionContext)
+ || ServiceExecGeneral.execMap.TryGetValue(ActionContext.AnyKey(serviceType, requestName), out actionContext))
+ {
+ if (actionContext.RequestFilters != null)
+ {
+ foreach (var requestFilter in actionContext.RequestFilters)
+ {
+ requestFilter.RequestFilter(request, request.Response, requestDto);
+ if (request.Response.IsClosed) return null;
+ }
+ }
+
+ var response = actionContext.ServiceAction(instance, requestDto);
+
+ var taskResponse = response as Task;
+ if (taskResponse != null)
+ {
+ await taskResponse.ConfigureAwait(false);
+ response = ServiceStackHost.Instance.GetTaskResult(taskResponse, requestName);
+ }
+
+ return response;
+ }
+
+ var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLower();
+ throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetOperationName(), expectedMethodName, serviceType.GetOperationName()));
+ }
+
+ public static List<ActionContext> Reset(Type serviceType)
+ {
+ var actions = new List<ActionContext>();
+
+ foreach (var mi in serviceType.GetActions())
+ {
+ var actionName = mi.Name.ToUpper();
+ var args = mi.GetParameters();
+
+ var requestType = args[0].ParameterType;
+ var actionCtx = new ActionContext
+ {
+ Id = ActionContext.Key(serviceType, actionName, requestType.GetOperationName())
+ };
+
+ try
+ {
+ actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi);
+ }
+ catch
+ {
+ //Potential problems with MONO, using reflection for fallback
+ actionCtx.ServiceAction = (service, request) =>
+ mi.Invoke(service, new[] { request });
+ }
+
+ var reqFilters = new List<IHasRequestFilter>();
+
+ foreach (var attr in mi.GetCustomAttributes(true))
+ {
+ var hasReqFilter = attr as IHasRequestFilter;
+
+ if (hasReqFilter != null)
+ reqFilters.Add(hasReqFilter);
+ }
+
+ if (reqFilters.Count > 0)
+ actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray();
+
+ actions.Add(actionCtx);
+ }
+
+ return actions;
+ }
+
+ private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi)
+ {
+ var serviceParam = Expression.Parameter(typeof(object), "serviceObj");
+ var serviceStrong = Expression.Convert(serviceParam, serviceType);
+
+ var requestDtoParam = Expression.Parameter(typeof(object), "requestDto");
+ var requestDtoStrong = Expression.Convert(requestDtoParam, requestType);
+
+ Expression callExecute = Expression.Call(
+ serviceStrong, mi, requestDtoStrong);
+
+ if (mi.ReturnType != typeof(void))
+ {
+ var executeFunc = Expression.Lambda<ActionInvokerFn>
+ (callExecute, serviceParam, requestDtoParam).Compile();
+
+ return executeFunc;
+ }
+ else
+ {
+ var executeFunc = Expression.Lambda<VoidActionInvokerFn>
+ (callExecute, serviceParam, requestDtoParam).Compile();
+
+ return (service, request) =>
+ {
+ executeFunc(service, request);
+ return null;
+ };
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/ServiceStack/Host/ServiceMetadata.cs b/ServiceStack/Host/ServiceMetadata.cs
new file mode 100644
index 000000000..240e6f32d
--- /dev/null
+++ b/ServiceStack/Host/ServiceMetadata.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+
+namespace ServiceStack.Host
+{
+ public class ServiceMetadata
+ {
+ public ServiceMetadata()
+ {
+ this.OperationsMap = new Dictionary<Type, Type>();
+ }
+
+ public Dictionary<Type, Type> OperationsMap { get; protected set; }
+
+ public void Add(Type serviceType, Type requestType, Type responseType)
+ {
+ this.OperationsMap[requestType] = serviceType;
+ }
+
+ public Type GetServiceTypeByRequest(Type requestType)
+ {
+ Type serviceType;
+ OperationsMap.TryGetValue(requestType, out serviceType);
+ return serviceType;
+ }
+ }
+}
diff --git a/ServiceStack/HttpHandlerFactory.cs b/ServiceStack/HttpHandlerFactory.cs
new file mode 100644
index 000000000..d48bfeb5f
--- /dev/null
+++ b/ServiceStack/HttpHandlerFactory.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Services;
+using ServiceStack.Host;
+
+namespace ServiceStack
+{
+ public class HttpHandlerFactory
+ {
+ // Entry point for HttpListener
+ public static RestHandler GetHandler(IHttpRequest httpReq)
+ {
+ var pathInfo = httpReq.PathInfo;
+
+ var pathParts = pathInfo.TrimStart('/').Split('/');
+ if (pathParts.Length == 0) return null;
+
+ string contentType;
+ var restPath = RestHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out contentType);
+ if (restPath != null)
+ return new RestHandler { RestPath = restPath, RequestName = restPath.RequestType.GetOperationName(), ResponseContentType = contentType };
+
+ return null;
+ }
+ }
+} \ No newline at end of file
diff --git a/ServiceStack/HttpRequestExtensions.cs b/ServiceStack/HttpRequestExtensions.cs
new file mode 100644
index 000000000..c34d62601
--- /dev/null
+++ b/ServiceStack/HttpRequestExtensions.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Model.Services;
+using ServiceStack.Host;
+
+namespace ServiceStack
+{
+ public static class HttpRequestExtensions
+ {
+ /**
+ *
+ Input: http://localhost:96/Cambia3/Temp/Test.aspx/path/info?q=item#fragment
+
+ Some HttpRequest path and URL properties:
+ Request.ApplicationPath: /Cambia3
+ Request.CurrentExecutionFilePath: /Cambia3/Temp/Test.aspx
+ Request.FilePath: /Cambia3/Temp/Test.aspx
+ Request.Path: /Cambia3/Temp/Test.aspx/path/info
+ Request.PathInfo: /path/info
+ Request.PhysicalApplicationPath: D:\Inetpub\wwwroot\CambiaWeb\Cambia3\
+ Request.QueryString: /Cambia3/Temp/Test.aspx/path/info?query=arg
+ Request.Url.AbsolutePath: /Cambia3/Temp/Test.aspx/path/info
+ Request.Url.AbsoluteUri: http://localhost:96/Cambia3/Temp/Test.aspx/path/info?query=arg
+ Request.Url.Fragment:
+ Request.Url.Host: localhost
+ Request.Url.LocalPath: /Cambia3/Temp/Test.aspx/path/info
+ Request.Url.PathAndQuery: /Cambia3/Temp/Test.aspx/path/info?query=arg
+ Request.Url.Port: 96
+ Request.Url.Query: ?query=arg
+ Request.Url.Scheme: http
+ Request.Url.Segments: /
+ Cambia3/
+ Temp/
+ Test.aspx/
+ path/
+ info
+ * */
+
+ /// <summary>
+ /// Duplicate Params are given a unique key by appending a #1 suffix
+ /// </summary>
+ public static Dictionary<string, string> GetRequestParams(this IRequest request)
+ {
+ var map = new Dictionary<string, string>();
+
+ foreach (var name in request.QueryString.Keys)
+ {
+ if (name == null) continue; //thank you ASP.NET
+
+ var values = request.QueryString.GetValues(name);
+ if (values.Length == 1)
+ {
+ map[name] = values[0];
+ }
+ else
+ {
+ for (var i = 0; i < values.Length; i++)
+ {
+ map[name + (i == 0 ? "" : "#" + i)] = values[i];
+ }
+ }
+ }
+
+ if ((request.Verb == HttpMethods.Post || request.Verb == HttpMethods.Put)
+ && request.FormData != null)
+ {
+ foreach (var name in request.FormData.Keys)
+ {
+ if (name == null) continue; //thank you ASP.NET
+
+ var values = request.FormData.GetValues(name);
+ if (values.Length == 1)
+ {
+ map[name] = values[0];
+ }
+ else
+ {
+ for (var i = 0; i < values.Length; i++)
+ {
+ map[name + (i == 0 ? "" : "#" + i)] = values[i];
+ }
+ }
+ }
+ }
+
+ return map;
+ }
+
+ /// <summary>
+ /// Duplicate params have their values joined together in a comma-delimited string
+ /// </summary>
+ public static Dictionary<string, string> GetFlattenedRequestParams(this IRequest request)
+ {
+ var map = new Dictionary<string, string>();
+
+ foreach (var name in request.QueryString.Keys)
+ {
+ if (name == null) continue; //thank you ASP.NET
+ map[name] = request.QueryString[name];
+ }
+
+ if ((request.Verb == HttpMethods.Post || request.Verb == HttpMethods.Put)
+ && request.FormData != null)
+ {
+ foreach (var name in request.FormData.Keys)
+ {
+ if (name == null) continue; //thank you ASP.NET
+ map[name] = request.FormData[name];
+ }
+ }
+
+ return map;
+ }
+
+ public static void SetRoute(this IRequest req, RestPath route)
+ {
+ req.Items["__route"] = route;
+ }
+
+ public static RestPath GetRoute(this IRequest req)
+ {
+ object route;
+ req.Items.TryGetValue("__route", out route);
+ return route as RestPath;
+ }
+ }
+} \ No newline at end of file
diff --git a/ServiceStack/HttpResponseExtensionsInternal.cs b/ServiceStack/HttpResponseExtensionsInternal.cs
new file mode 100644
index 000000000..1195f63ca
--- /dev/null
+++ b/ServiceStack/HttpResponseExtensionsInternal.cs
@@ -0,0 +1,237 @@
+//Copyright (c) Service Stack LLC. All Rights Reserved.
+//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
+
+using System;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Threading;
+using MediaBrowser.Model.Services;
+using ServiceStack.Host;
+
+namespace ServiceStack
+{
+ public static class HttpResponseExtensionsInternal
+ {
+ public static async Task<bool> WriteToOutputStream(IResponse response, object result)
+ {
+ var asyncStreamWriter = result as IAsyncStreamWriter;
+ if (asyncStreamWriter != null)
+ {
+ await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false);
+ return true;
+ }
+
+ var streamWriter = result as IStreamWriter;
+ if (streamWriter != null)
+ {
+ streamWriter.WriteTo(response.OutputStream);
+ return true;
+ }
+
+ var stream = result as Stream;
+ if (stream != null)
+ {
+ WriteTo(stream, response.OutputStream);
+ return true;
+ }
+
+ var bytes = result as byte[];
+ if (bytes != null)
+ {
+ response.ContentType = "application/octet-stream";
+ response.SetContentLength(bytes.Length);
+
+ response.OutputStream.Write(bytes, 0, bytes.Length);
+ return true;
+ }
+
+ return false;
+ }
+
+ public static long WriteTo(Stream inStream, Stream outStream)
+ {
+ var memoryStream = inStream as MemoryStream;
+ if (memoryStream != null)
+ {
+ memoryStream.WriteTo(outStream);
+ return memoryStream.Position;
+ }
+
+ var data = new byte[4096];
+ long total = 0;
+ int bytesRead;
+
+ while ((bytesRead = inStream.Read(data, 0, data.Length)) > 0)
+ {
+ outStream.Write(data, 0, bytesRead);
+ total += bytesRead;
+ }
+
+ return total;
+ }
+
+ /// <summary>
+ /// End a ServiceStack Request with no content
+ /// </summary>
+ public static void EndRequestWithNoContent(this IResponse httpRes)
+ {
+ if (httpRes.StatusCode == (int)HttpStatusCode.OK)
+ {
+ httpRes.StatusCode = (int)HttpStatusCode.NoContent;
+ }
+
+ httpRes.SetContentLength(0);
+ }
+
+ public static Task WriteToResponse(this IResponse httpRes, MediaBrowser.Model.Services.IRequest httpReq, object result)
+ {
+ if (result == null)
+ {
+ httpRes.EndRequestWithNoContent();
+ return Task.FromResult(true);
+ }
+
+ var httpResult = result as IHttpResult;
+ if (httpResult != null)
+ {
+ httpResult.RequestContext = httpReq;
+ httpReq.ResponseContentType = httpResult.ContentType ?? httpReq.ResponseContentType;
+ var httpResSerializer = ContentTypes.Instance.GetResponseSerializer(httpReq.ResponseContentType);
+ return httpRes.WriteToResponse(httpResult, httpResSerializer, httpReq);
+ }
+
+ var serializer = ContentTypes.Instance.GetResponseSerializer(httpReq.ResponseContentType);
+ return httpRes.WriteToResponse(result, serializer, httpReq);
+ }
+
+ private static object GetDto(object response)
+ {
+ if (response == null) return null;
+ var httpResult = response as IHttpResult;
+ return httpResult != null ? httpResult.Response : response;
+ }
+
+ /// <summary>
+ /// Writes to response.
+ /// Response headers are customizable by implementing IHasHeaders an returning Dictionary of Http headers.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ /// <param name="result">Whether or not it was implicity handled by ServiceStack's built-in handlers.</param>
+ /// <param name="defaultAction">The default action.</param>
+ /// <param name="request">The serialization context.</param>
+ /// <returns></returns>
+ public static async Task WriteToResponse(this IResponse response, object result, Action<IRequest, object, IResponse> defaultAction, MediaBrowser.Model.Services.IRequest request)
+ {
+ var defaultContentType = request.ResponseContentType;
+ if (result == null)
+ {
+ response.EndRequestWithNoContent();
+ return;
+ }
+
+ var httpResult = result as IHttpResult;
+ if (httpResult != null)
+ {
+ if (httpResult.RequestContext == null)
+ httpResult.RequestContext = request;
+
+ response.Dto = response.Dto ?? GetDto(httpResult);
+
+ response.StatusCode = httpResult.Status;
+ response.StatusDescription = httpResult.StatusDescription ?? httpResult.StatusCode.ToString();
+ if (string.IsNullOrEmpty(httpResult.ContentType))
+ {
+ httpResult.ContentType = defaultContentType;
+ }
+ response.ContentType = httpResult.ContentType;
+
+ if (httpResult.Cookies != null)
+ {
+ var httpRes = response as IHttpResponse;
+ if (httpRes != null)
+ {
+ foreach (var cookie in httpResult.Cookies)
+ {
+ httpRes.SetCookie(cookie);
+ }
+ }
+ }
+ }
+ else
+ {
+ response.Dto = result;
+ }
+
+ /* Mono Error: Exception: Method not found: 'System.Web.HttpResponse.get_Headers' */
+ var responseOptions = result as IHasHeaders;
+ if (responseOptions != null)
+ {
+ //Reserving options with keys in the format 'xx.xxx' (No Http headers contain a '.' so its a safe restriction)
+ const string reservedOptions = ".";
+
+ foreach (var responseHeaders in responseOptions.Headers)
+ {
+ if (responseHeaders.Key.Contains(reservedOptions)) continue;
+ if (responseHeaders.Key == "Content-Length")
+ {
+ response.SetContentLength(long.Parse(responseHeaders.Value));
+ continue;
+ }
+
+ response.AddHeader(responseHeaders.Key, responseHeaders.Value);
+ }
+ }
+
+ //ContentType='text/html' is the default for a HttpResponse
+ //Do not override if another has been set
+ if (response.ContentType == null || response.ContentType == "text/html")
+ {
+ response.ContentType = defaultContentType;
+ }
+
+ if (new HashSet<string> { "application/json", }.Contains(response.ContentType))
+ {
+ response.ContentType += "; charset=utf-8";
+ }
+
+ var disposableResult = result as IDisposable;
+ var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false);
+ if (writeToOutputStreamResult)
+ {
+ response.Flush(); //required for Compression
+ if (disposableResult != null) disposableResult.Dispose();
+ return;
+ }
+
+ if (httpResult != null)
+ result = httpResult.Response;
+
+ var responseText = result as string;
+ if (responseText != null)
+ {
+ if (response.ContentType == null || response.ContentType == "text/html")
+ response.ContentType = defaultContentType;
+ response.Write(responseText);
+
+ return;
+ }
+
+ if (defaultAction == null)
+ {
+ throw new ArgumentNullException("defaultAction", String.Format(
+ "As result '{0}' is not a supported responseType, a defaultAction must be supplied",
+ (result != null ? result.GetType().GetOperationName() : "")));
+ }
+
+
+ if (result != null)
+ defaultAction(request, result, response);
+
+ if (disposableResult != null)
+ disposableResult.Dispose();
+ }
+
+ }
+}
diff --git a/ServiceStack/HttpResult.cs b/ServiceStack/HttpResult.cs
new file mode 100644
index 000000000..23a5cdffb
--- /dev/null
+++ b/ServiceStack/HttpResult.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Services;
+using ServiceStack.Host;
+
+namespace ServiceStack
+{
+ public class HttpResult
+ : IHttpResult, IAsyncStreamWriter
+ {
+ public HttpResult()
+ : this((object)null, null)
+ {
+ }
+
+ public HttpResult(object response)
+ : this(response, null)
+ {
+ }
+
+ public HttpResult(object response, string contentType)
+ : this(response, contentType, HttpStatusCode.OK)
+ {
+ }
+
+ public HttpResult(HttpStatusCode statusCode, string statusDescription)
+ : this()
+ {
+ StatusCode = statusCode;
+ StatusDescription = statusDescription;
+ }
+
+ public HttpResult(object response, HttpStatusCode statusCode)
+ : this(response, null, statusCode)
+ { }
+
+ public HttpResult(object response, string contentType, HttpStatusCode statusCode)
+ {
+ this.Headers = new Dictionary<string, string>();
+ this.Cookies = new List<Cookie>();
+
+ this.Response = response;
+ this.ContentType = contentType;
+ this.StatusCode = statusCode;
+ }
+
+ public HttpResult(Stream responseStream, string contentType)
+ : this(null, contentType, HttpStatusCode.OK)
+ {
+ this.ResponseStream = responseStream;
+ }
+
+ public HttpResult(string responseText, string contentType)
+ : this(null, contentType, HttpStatusCode.OK)
+ {
+ this.ResponseText = responseText;
+ }
+
+ public HttpResult(byte[] responseBytes, string contentType)
+ : this(null, contentType, HttpStatusCode.OK)
+ {
+ this.ResponseStream = new MemoryStream(responseBytes);
+ }
+
+ public string ResponseText { get; private set; }
+
+ public Stream ResponseStream { get; private set; }
+
+ public string ContentType { get; set; }
+
+ public IDictionary<string, string> Headers { get; private set; }
+
+ public List<Cookie> Cookies { get; private set; }
+
+ public string ETag { get; set; }
+
+ public TimeSpan? Age { get; set; }
+
+ public TimeSpan? MaxAge { get; set; }
+
+ public DateTime? Expires { get; set; }
+
+ public DateTime? LastModified { get; set; }
+
+ public Func<IDisposable> ResultScope { get; set; }
+
+ public string Location
+ {
+ set
+ {
+ if (StatusCode == HttpStatusCode.OK)
+ StatusCode = HttpStatusCode.Redirect;
+
+ this.Headers["Location"] = value;
+ }
+ }
+
+ public void SetPermanentCookie(string name, string value)
+ {
+ SetCookie(name, value, DateTime.UtcNow.AddYears(20), null);
+ }
+
+ public void SetPermanentCookie(string name, string value, string path)
+ {
+ SetCookie(name, value, DateTime.UtcNow.AddYears(20), path);
+ }
+
+ public void SetSessionCookie(string name, string value)
+ {
+ SetSessionCookie(name, value, null);
+ }
+
+ public void SetSessionCookie(string name, string value, string path)
+ {
+ path = path ?? "/";
+ this.Headers["Set-Cookie"] = string.Format("{0}={1};path=" + path, name, value);
+ }
+
+ public void SetCookie(string name, string value, TimeSpan expiresIn, string path)
+ {
+ var expiresAt = DateTime.UtcNow.Add(expiresIn);
+ SetCookie(name, value, expiresAt, path);
+ }
+
+ public void SetCookie(string name, string value, DateTime expiresAt, string path, bool secure = false, bool httpOnly = false)
+ {
+ path = path ?? "/";
+ var cookie = string.Format("{0}={1};expires={2};path={3}", name, value, expiresAt.ToString("R"), path);
+ if (secure)
+ cookie += ";Secure";
+ if (httpOnly)
+ cookie += ";HttpOnly";
+
+ this.Headers["Set-Cookie"] = cookie;
+ }
+
+ public void DeleteCookie(string name)
+ {
+ var cookie = string.Format("{0}=;expires={1};path=/", name, DateTime.UtcNow.AddDays(-1).ToString("R"));
+ this.Headers["Set-Cookie"] = cookie;
+ }
+
+ public int Status { get; set; }
+
+ public HttpStatusCode StatusCode
+ {
+ get { return (HttpStatusCode)Status; }
+ set { Status = (int)value; }
+ }
+
+ public string StatusDescription { get; set; }
+
+ public object Response { get; set; }
+
+ public MediaBrowser.Model.Services.IRequest RequestContext { get; set; }
+
+ public string View { get; set; }
+
+ public string Template { get; set; }
+
+ public int PaddingLength { get; set; }
+
+ public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await WriteToInternalAsync(responseStream, cancellationToken).ConfigureAwait(false);
+ responseStream.Flush();
+ }
+ finally
+ {
+ DisposeStream();
+ }
+ }
+
+ public static Task WriteTo(Stream inStream, Stream outStream, CancellationToken cancellationToken)
+ {
+ var memoryStream = inStream as MemoryStream;
+ if (memoryStream != null)
+ {
+ memoryStream.WriteTo(outStream);
+ return Task.FromResult(true);
+ }
+
+ return inStream.CopyToAsync(outStream, 81920, cancellationToken);
+ }
+
+ public async Task WriteToInternalAsync(Stream responseStream, CancellationToken cancellationToken)
+ {
+ var response = RequestContext != null ? RequestContext.Response : null;
+
+ if (this.ResponseStream != null)
+ {
+ if (response != null)
+ {
+ var ms = ResponseStream as MemoryStream;
+ if (ms != null)
+ {
+ response.SetContentLength(ms.Length);
+
+ await ms.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+ }
+
+ await WriteTo(this.ResponseStream, responseStream, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ if (this.ResponseText != null)
+ {
+ var bytes = Encoding.UTF8.GetBytes(this.ResponseText);
+ if (response != null)
+ response.SetContentLength(bytes.Length);
+
+ await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+ return;
+ }
+
+ var bytesResponse = this.Response as byte[];
+ if (bytesResponse != null)
+ {
+ if (response != null)
+ response.SetContentLength(bytesResponse.Length);
+
+ await responseStream.WriteAsync(bytesResponse, 0, bytesResponse.Length).ConfigureAwait(false);
+ return;
+ }
+
+ ContentTypes.Instance.SerializeToStream(this.RequestContext, this.Response, responseStream);
+ }
+
+ private void DisposeStream()
+ {
+ try
+ {
+ if (ResponseStream != null)
+ {
+ this.ResponseStream.Dispose();
+ }
+ }
+ catch { /*ignore*/ }
+ }
+ }
+}
diff --git a/ServiceStack/HttpUtils.cs b/ServiceStack/HttpUtils.cs
new file mode 100644
index 000000000..41d191d61
--- /dev/null
+++ b/ServiceStack/HttpUtils.cs
@@ -0,0 +1,34 @@
+//Copyright (c) Service Stack LLC. All Rights Reserved.
+//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
+
+using System;
+using System.Collections.Generic;
+
+namespace ServiceStack
+{
+ internal static class HttpMethods
+ {
+ static readonly string[] allVerbs = new[] {
+ "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616
+ "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518
+ "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT",
+ "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253
+ "ORDERPATCH", // RFC 3648
+ "ACL", // RFC 3744
+ "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/
+ "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/
+ "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY",
+ "POLL", "SUBSCRIBE", "UNSUBSCRIBE" //MS Exchange WebDav: http://msdn.microsoft.com/en-us/library/aa142917.aspx
+ };
+
+ public static HashSet<string> AllVerbs = new HashSet<string>(allVerbs);
+
+ public const string Get = "GET";
+ public const string Put = "PUT";
+ public const string Post = "POST";
+ public const string Delete = "DELETE";
+ public const string Options = "OPTIONS";
+ public const string Head = "HEAD";
+ public const string Patch = "PATCH";
+ }
+}
diff --git a/ServiceStack/Properties/AssemblyInfo.cs b/ServiceStack/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..6073dc0b4
--- /dev/null
+++ b/ServiceStack/Properties/AssemblyInfo.cs
@@ -0,0 +1,25 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("ServiceStack")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Service Stack LLC")]
+[assembly: AssemblyProduct("ServiceStack")]
+[assembly: AssemblyCopyright("Copyright (c) ServiceStack 2016")]
+[assembly: AssemblyTrademark("Service Stack")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("06704d66-af8e-411f-8260-8d05de5ce6ad")]
+
+[assembly: AssemblyVersion("4.0.0.0")]
+[assembly: AssemblyFileVersion("4.0.0.0")]
diff --git a/ServiceStack/ReflectionExtensions.cs b/ServiceStack/ReflectionExtensions.cs
new file mode 100644
index 000000000..bbabd0dd7
--- /dev/null
+++ b/ServiceStack/ReflectionExtensions.cs
@@ -0,0 +1,270 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ServiceStack
+{
+ public static class ReflectionExtensions
+ {
+ public static bool IsInstanceOf(this Type type, Type thisOrBaseType)
+ {
+ while (type != null)
+ {
+ if (type == thisOrBaseType)
+ return true;
+
+ type = type.BaseType();
+ }
+ return false;
+ }
+
+ public static Type FirstGenericType(this Type type)
+ {
+ while (type != null)
+ {
+ if (type.IsGeneric())
+ return type;
+
+ type = type.BaseType();
+ }
+ return null;
+ }
+
+ public static Type GetTypeWithGenericTypeDefinitionOf(this Type type, Type genericTypeDefinition)
+ {
+ foreach (var t in type.GetTypeInterfaces())
+ {
+ if (t.IsGeneric() && t.GetGenericTypeDefinition() == genericTypeDefinition)
+ {
+ return t;
+ }
+ }
+
+ var genericType = type.FirstGenericType();
+ if (genericType != null && genericType.GetGenericTypeDefinition() == genericTypeDefinition)
+ {
+ return genericType;
+ }
+
+ return null;
+ }
+
+ public static PropertyInfo[] GetAllProperties(this Type type)
+ {
+ if (type.IsInterface())
+ {
+ var propertyInfos = new List<PropertyInfo>();
+
+ var considered = new List<Type>();
+ var queue = new Queue<Type>();
+ considered.Add(type);
+ queue.Enqueue(type);
+
+ while (queue.Count > 0)
+ {
+ var subType = queue.Dequeue();
+ foreach (var subInterface in subType.GetTypeInterfaces())
+ {
+ if (considered.Contains(subInterface)) continue;
+
+ considered.Add(subInterface);
+ queue.Enqueue(subInterface);
+ }
+
+ var typeProperties = subType.GetTypesProperties();
+
+ var newPropertyInfos = typeProperties
+ .Where(x => !propertyInfos.Contains(x));
+
+ propertyInfos.InsertRange(0, newPropertyInfos);
+ }
+
+ return propertyInfos.ToArray();
+ }
+
+ return type.GetTypesProperties()
+ .Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties
+ .ToArray();
+ }
+
+ public static PropertyInfo[] GetPublicProperties(this Type type)
+ {
+ if (type.IsInterface())
+ {
+ var propertyInfos = new List<PropertyInfo>();
+
+ var considered = new List<Type>();
+ var queue = new Queue<Type>();
+ considered.Add(type);
+ queue.Enqueue(type);
+
+ while (queue.Count > 0)
+ {
+ var subType = queue.Dequeue();
+ foreach (var subInterface in subType.GetTypeInterfaces())
+ {
+ if (considered.Contains(subInterface)) continue;
+
+ considered.Add(subInterface);
+ queue.Enqueue(subInterface);
+ }
+
+ var typeProperties = subType.GetTypesPublicProperties();
+
+ var newPropertyInfos = typeProperties
+ .Where(x => !propertyInfos.Contains(x));
+
+ propertyInfos.InsertRange(0, newPropertyInfos);
+ }
+
+ return propertyInfos.ToArray();
+ }
+
+ return type.GetTypesPublicProperties()
+ .Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties
+ .ToArray();
+ }
+
+ public const string DataMember = "DataMemberAttribute";
+
+ internal static string[] IgnoreAttributesNamed = new[] {
+ "IgnoreDataMemberAttribute",
+ "JsonIgnoreAttribute"
+ };
+
+ public static PropertyInfo[] GetSerializableProperties(this Type type)
+ {
+ var properties = type.IsDto()
+ ? type.GetAllProperties()
+ : type.GetPublicProperties();
+ return properties.OnlySerializableProperties(type);
+ }
+
+
+ private static List<Type> _excludeTypes = new List<Type> { typeof(Stream) };
+
+ public static PropertyInfo[] OnlySerializableProperties(this PropertyInfo[] properties, Type type = null)
+ {
+ var isDto = type.IsDto();
+ var readableProperties = properties.Where(x => x.PropertyGetMethod(nonPublic: isDto) != null);
+
+ if (isDto)
+ {
+ return readableProperties.Where(attr =>
+ attr.HasAttribute<DataMemberAttribute>()).ToArray();
+ }
+
+ // else return those properties that are not decorated with IgnoreDataMember
+ return readableProperties
+ .Where(prop => prop.AllAttributes()
+ .All(attr =>
+ {
+ var name = attr.GetType().Name;
+ return !IgnoreAttributesNamed.Contains(name);
+ }))
+ .Where(prop => !_excludeTypes.Contains(prop.PropertyType))
+ .ToArray();
+ }
+ }
+
+ public static class PlatformExtensions //Because WinRT is a POS
+ {
+ public static bool IsInterface(this Type type)
+ {
+ return type.GetTypeInfo().IsInterface;
+ }
+
+ public static bool IsGeneric(this Type type)
+ {
+ return type.GetTypeInfo().IsGenericType;
+ }
+
+ public static Type BaseType(this Type type)
+ {
+ return type.GetTypeInfo().BaseType;
+ }
+
+ public static Type[] GetTypeInterfaces(this Type type)
+ {
+ return type.GetTypeInfo().ImplementedInterfaces.ToArray();
+ }
+
+ internal static PropertyInfo[] GetTypesPublicProperties(this Type subType)
+ {
+ var pis = new List<PropertyInfo>();
+ foreach (var pi in subType.GetRuntimeProperties())
+ {
+ var mi = pi.GetMethod ?? pi.SetMethod;
+ if (mi != null && mi.IsStatic) continue;
+ pis.Add(pi);
+ }
+ return pis.ToArray();
+ }
+
+ internal static PropertyInfo[] GetTypesProperties(this Type subType)
+ {
+ var pis = new List<PropertyInfo>();
+ foreach (var pi in subType.GetRuntimeProperties())
+ {
+ var mi = pi.GetMethod ?? pi.SetMethod;
+ if (mi != null && mi.IsStatic) continue;
+ pis.Add(pi);
+ }
+ return pis.ToArray();
+ }
+
+ public static bool HasAttribute<T>(this Type type)
+ {
+ return type.AllAttributes().Any(x => x.GetType() == typeof(T));
+ }
+
+ public static bool HasAttribute<T>(this PropertyInfo pi)
+ {
+ return pi.AllAttributes().Any(x => x.GetType() == typeof(T));
+ }
+
+ public static bool IsDto(this Type type)
+ {
+ if (type == null)
+ return false;
+
+ return type.HasAttribute<DataContractAttribute>();
+ }
+
+ public static MethodInfo PropertyGetMethod(this PropertyInfo pi, bool nonPublic = false)
+ {
+ return pi.GetMethod;
+ }
+
+ public static object[] AllAttributes(this PropertyInfo propertyInfo)
+ {
+ return propertyInfo.GetCustomAttributes(true).ToArray();
+ }
+
+ public static object[] AllAttributes(this PropertyInfo propertyInfo, Type attrType)
+ {
+ return propertyInfo.GetCustomAttributes(true).Where(x => attrType.IsInstanceOf(x.GetType())).ToArray();
+ }
+
+ public static object[] AllAttributes(this Type type)
+ {
+ return type.GetTypeInfo().GetCustomAttributes(true).ToArray();
+ }
+
+ public static TAttr[] AllAttributes<TAttr>(this PropertyInfo pi)
+ {
+ return pi.AllAttributes(typeof(TAttr)).Cast<TAttr>().ToArray();
+ }
+
+ public static TAttr[] AllAttributes<TAttr>(this Type type)
+ where TAttr : Attribute
+ {
+ return type.GetTypeInfo().GetCustomAttributes<TAttr>(true).ToArray();
+ }
+ }
+}
diff --git a/ServiceStack/ServiceStack.csproj b/ServiceStack/ServiceStack.csproj
new file mode 100644
index 000000000..3402339a6
--- /dev/null
+++ b/ServiceStack/ServiceStack.csproj
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{680A1709-25EB-4D52-A87F-EE03FFD94BAA}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>ServiceStack</RootNamespace>
+ <AssemblyName>ServiceStack</AssemblyName>
+ <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <FileUpgradeFlags>
+ </FileUpgradeFlags>
+ <OldToolsVersion>3.5</OldToolsVersion>
+ <UpgradeBackupLocation />
+ <PublishUrl>publish\</PublishUrl>
+ <Install>true</Install>
+ <InstallFrom>Disk</InstallFrom>
+ <UpdateEnabled>false</UpdateEnabled>
+ <UpdateMode>Foreground</UpdateMode>
+ <UpdateInterval>7</UpdateInterval>
+ <UpdateIntervalUnits>Days</UpdateIntervalUnits>
+ <UpdatePeriodically>false</UpdatePeriodically>
+ <UpdateRequired>false</UpdateRequired>
+ <MapFileExtensions>true</MapFileExtensions>
+ <ApplicationRevision>0</ApplicationRevision>
+ <ApplicationVersion>1.0.0.%2a</ApplicationVersion>
+ <IsWebBootstrapper>false</IsWebBootstrapper>
+ <UseApplicationTrust>false</UseApplicationTrust>
+ <BootstrapperEnabled>true</BootstrapperEnabled>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>True</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>False</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG;MONO</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
+ <Prefer32Bit>false</Prefer32Bit>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>True</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
+ <DocumentationFile>
+ </DocumentationFile>
+ <Prefer32Bit>false</Prefer32Bit>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Signed|AnyCPU'">
+ <OutputPath>bin\Signed\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <DocumentationFile>bin\Release\ServiceStack.XML</DocumentationFile>
+ <Optimize>true</Optimize>
+ <DebugType>pdbonly</DebugType>
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <ErrorReport>prompt</ErrorReport>
+ <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
+ <Prefer32Bit>false</Prefer32Bit>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="HttpUtils.cs" />
+ <Compile Include="Host\ContentTypes.cs" />
+ <Compile Include="ReflectionExtensions.cs" />
+ <Compile Include="StringMapTypeDeserializer.cs" />
+ <Compile Include="Host\HttpResponseStreamWrapper.cs" />
+ <Compile Include="HttpResult.cs" />
+ <Compile Include="ServiceStackHost.cs" />
+ <Compile Include="ServiceStackHost.Runtime.cs" />
+ <Compile Include="Host\ServiceExec.cs" />
+ <Compile Include="UrlExtensions.cs" />
+ <Compile Include="Host\ActionContext.cs" />
+ <Compile Include="HttpRequestExtensions.cs" />
+ <Compile Include="Host\RestPath.cs" />
+ <Compile Include="Host\ServiceController.cs" />
+ <Compile Include="Host\ServiceMetadata.cs" />
+ <Compile Include="Host\RestHandler.cs" />
+ <Compile Include="HttpResponseExtensionsInternal.cs" />
+ <Compile Include="HttpHandlerFactory.cs" />
+ <Compile Include="FilterAttributeCache.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <ItemGroup>
+ <BootstrapperPackage Include="Microsoft.Net.Client.3.5">
+ <Visible>False</Visible>
+ <ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
+ <Install>false</Install>
+ </BootstrapperPackage>
+ <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
+ <Visible>False</Visible>
+ <ProductName>.NET Framework 3.5 SP1</ProductName>
+ <Install>true</Install>
+ </BootstrapperPackage>
+ <BootstrapperPackage Include="Microsoft.Windows.Installer.3.1">
+ <Visible>False</Visible>
+ <ProductName>Windows Installer 3.1</ProductName>
+ <Install>true</Install>
+ </BootstrapperPackage>
+ </ItemGroup>
+ <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/ServiceStack/ServiceStack.nuget.targets b/ServiceStack/ServiceStack.nuget.targets
new file mode 100644
index 000000000..e69ce0e64
--- /dev/null
+++ b/ServiceStack/ServiceStack.nuget.targets
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Target Name="EmitMSBuildWarning" BeforeTargets="Build">
+ <Warning Text="Packages containing MSBuild targets and props files cannot be fully installed in projects targeting multiple frameworks. The MSBuild targets and props files have been ignored." />
+ </Target>
+</Project> \ No newline at end of file
diff --git a/ServiceStack/ServiceStack.xproj b/ServiceStack/ServiceStack.xproj
new file mode 100644
index 000000000..ba8f8b8f2
--- /dev/null
+++ b/ServiceStack/ServiceStack.xproj
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion>
+ <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+ </PropertyGroup>
+ <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
+ <PropertyGroup Label="Globals">
+ <ProjectGuid>b2d733ab-620e-4c53-88a4-4b6638ab6a7a</ProjectGuid>
+ <RootNamespace>ServiceStack</RootNamespace>
+ <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
+ <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <SchemaVersion>2.0</SchemaVersion>
+ </PropertyGroup>
+ <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
+</Project> \ No newline at end of file
diff --git a/ServiceStack/ServiceStackHost.Runtime.cs b/ServiceStack/ServiceStackHost.Runtime.cs
new file mode 100644
index 000000000..1a1656a0e
--- /dev/null
+++ b/ServiceStack/ServiceStackHost.Runtime.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Service Stack LLC. All Rights Reserved.
+// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
+
+
+using MediaBrowser.Model.Services;
+using ServiceStack.Support.WebHost;
+
+namespace ServiceStack
+{
+ public abstract partial class ServiceStackHost
+ {
+ /// <summary>
+ /// Applies the request filters. Returns whether or not the request has been handled
+ /// and no more processing should be done.
+ /// </summary>
+ /// <returns></returns>
+ public virtual bool ApplyRequestFilters(IRequest req, IResponse res, object requestDto)
+ {
+ if (res.IsClosed) return res.IsClosed;
+
+ //Exec all RequestFilter attributes with Priority < 0
+ var attributes = FilterAttributeCache.GetRequestFilterAttributes(requestDto.GetType());
+ var i = 0;
+ for (; i < attributes.Length && attributes[i].Priority < 0; i++)
+ {
+ var attribute = attributes[i];
+ attribute.RequestFilter(req, res, requestDto);
+ if (res.IsClosed) return res.IsClosed;
+ }
+
+ if (res.IsClosed) return res.IsClosed;
+
+ //Exec global filters
+ foreach (var requestFilter in GlobalRequestFilters)
+ {
+ requestFilter(req, res, requestDto);
+ if (res.IsClosed) return res.IsClosed;
+ }
+
+ //Exec remaining RequestFilter attributes with Priority >= 0
+ for (; i < attributes.Length && attributes[i].Priority >= 0; i++)
+ {
+ var attribute = attributes[i];
+ attribute.RequestFilter(req, res, requestDto);
+ if (res.IsClosed) return res.IsClosed;
+ }
+
+ return res.IsClosed;
+ }
+
+ /// <summary>
+ /// Applies the response filters. Returns whether or not the request has been handled
+ /// and no more processing should be done.
+ /// </summary>
+ /// <returns></returns>
+ public virtual bool ApplyResponseFilters(IRequest req, IResponse res, object response)
+ {
+ if (response != null)
+ {
+ if (res.IsClosed) return res.IsClosed;
+ }
+
+ //Exec global filters
+ foreach (var responseFilter in GlobalResponseFilters)
+ {
+ responseFilter(req, res, response);
+ if (res.IsClosed) return res.IsClosed;
+ }
+
+ return res.IsClosed;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/ServiceStack/ServiceStackHost.cs b/ServiceStack/ServiceStackHost.cs
new file mode 100644
index 000000000..8a1db25e4
--- /dev/null
+++ b/ServiceStack/ServiceStackHost.cs
@@ -0,0 +1,104 @@
+// Copyright (c) Service Stack LLC. All Rights Reserved.
+// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
+
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Services;
+using ServiceStack.Host;
+
+namespace ServiceStack
+{
+ public abstract partial class ServiceStackHost : IDisposable
+ {
+ public static ServiceStackHost Instance { get; protected set; }
+
+ protected ServiceStackHost(string serviceName)
+ {
+ ServiceName = serviceName;
+ ServiceController = CreateServiceController();
+
+ RestPaths = new List<RestPath>();
+ Metadata = new ServiceMetadata();
+ GlobalRequestFilters = new List<Action<IRequest, IResponse, object>>();
+ GlobalResponseFilters = new List<Action<IRequest, IResponse, object>>();
+ }
+
+ public abstract void Configure();
+
+ public abstract object CreateInstance(Type type);
+
+ protected abstract ServiceController CreateServiceController();
+
+ public virtual ServiceStackHost Init()
+ {
+ Instance = this;
+
+ ServiceController.Init();
+ Configure();
+
+ ServiceController.AfterInit();
+
+ return this;
+ }
+
+ public virtual ServiceStackHost Start(string urlBase)
+ {
+ throw new NotImplementedException("Start(listeningAtUrlBase) is not supported by this AppHost");
+ }
+
+ public string ServiceName { get; set; }
+
+ public ServiceMetadata Metadata { get; set; }
+
+ public ServiceController ServiceController { get; set; }
+
+ public List<RestPath> RestPaths = new List<RestPath>();
+
+ public List<Action<IRequest, IResponse, object>> GlobalRequestFilters { get; set; }
+
+ public List<Action<IRequest, IResponse, object>> GlobalResponseFilters { get; set; }
+
+ public abstract T TryResolve<T>();
+ public abstract T Resolve<T>();
+
+ public virtual MediaBrowser.Model.Services.RouteAttribute[] GetRouteAttributes(Type requestType)
+ {
+ return requestType.AllAttributes<MediaBrowser.Model.Services.RouteAttribute>();
+ }
+
+ public abstract object GetTaskResult(Task task, string requestName);
+
+ public abstract Func<string, object> GetParseFn(Type propertyType);
+
+ public abstract void SerializeToJson(object o, Stream stream);
+ public abstract void SerializeToXml(object o, Stream stream);
+ public abstract object DeserializeXml(Type type, Stream stream);
+ public abstract object DeserializeJson(Type type, Stream stream);
+
+ public virtual void Dispose()
+ {
+ //JsConfig.Reset(); //Clears Runtime Attributes
+
+ Instance = null;
+ }
+
+ protected abstract ILogger Logger
+ {
+ get;
+ }
+
+ public void OnLogError(Type type, string message)
+ {
+ Logger.Error(message);
+ }
+
+ public void OnLogError(Type type, string message, Exception ex)
+ {
+ Logger.ErrorException(message, ex);
+ }
+ }
+}
diff --git a/ServiceStack/StringMapTypeDeserializer.cs b/ServiceStack/StringMapTypeDeserializer.cs
new file mode 100644
index 000000000..762e8aaff
--- /dev/null
+++ b/ServiceStack/StringMapTypeDeserializer.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Linq;
+using System.Reflection;
+
+namespace ServiceStack.Serialization
+{
+ /// <summary>
+ /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls)
+ /// </summary>
+ public class StringMapTypeDeserializer
+ {
+ internal class PropertySerializerEntry
+ {
+ public PropertySerializerEntry(Action<object,object> propertySetFn, Func<string, object> propertyParseStringFn)
+ {
+ PropertySetFn = propertySetFn;
+ PropertyParseStringFn = propertyParseStringFn;
+ }
+
+ public Action<object, object> PropertySetFn;
+ public Func<string,object> PropertyParseStringFn;
+ public Type PropertyType;
+ }
+
+ private readonly Type type;
+ private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap
+ = new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase);
+
+ public Func<string, object> GetParseFn(Type propertyType)
+ {
+ //Don't JSV-decode string values for string properties
+ if (propertyType == typeof(string))
+ return s => s;
+
+ return ServiceStackHost.Instance.GetParseFn(propertyType);
+ }
+
+ public StringMapTypeDeserializer(Type type)
+ {
+ this.type = type;
+
+ foreach (var propertyInfo in type.GetSerializableProperties())
+ {
+ var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
+ var propertyType = propertyInfo.PropertyType;
+ var propertyParseStringFn = GetParseFn(propertyType);
+ var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType };
+
+ var attr = propertyInfo.AllAttributes<DataMemberAttribute>().FirstOrDefault();
+ if (attr != null && attr.Name != null)
+ {
+ propertySetterMap[attr.Name] = propertySerializer;
+ }
+ propertySetterMap[propertyInfo.Name] = propertySerializer;
+ }
+ }
+
+ public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs)
+ {
+ string propertyName = null;
+ string propertyTextValue = null;
+ PropertySerializerEntry propertySerializerEntry = null;
+
+ if (instance == null)
+ instance = ServiceStackHost.Instance.CreateInstance(type);
+
+ foreach (var pair in keyValuePairs.Where(x => !string.IsNullOrEmpty(x.Value)))
+ {
+ propertyName = pair.Key;
+ propertyTextValue = pair.Value;
+
+ if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry))
+ {
+ if (propertyName == "v")
+ {
+ continue;
+ }
+
+ continue;
+ }
+
+ if (propertySerializerEntry.PropertySetFn == null)
+ {
+ continue;
+ }
+
+ if (propertySerializerEntry.PropertyType == typeof(bool))
+ {
+ //InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
+ propertyTextValue = LeftPart(propertyTextValue, ',');
+ }
+
+ var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
+ if (value == null)
+ {
+ continue;
+ }
+ propertySerializerEntry.PropertySetFn(instance, value);
+ }
+
+ return instance;
+ }
+
+ public static string LeftPart(string strVal, char needle)
+ {
+ if (strVal == null) return null;
+ var pos = strVal.IndexOf(needle);
+ return pos == -1
+ ? strVal
+ : strVal.Substring(0, pos);
+ }
+ }
+
+ internal class TypeAccessor
+ {
+ public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo)
+ {
+ if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Any()) return null;
+
+ var setMethodInfo = propertyInfo.SetMethod;
+ return (instance, value) => setMethodInfo.Invoke(instance, new[] { value });
+ }
+ }
+}
diff --git a/ServiceStack/UrlExtensions.cs b/ServiceStack/UrlExtensions.cs
new file mode 100644
index 000000000..7b5a50ef1
--- /dev/null
+++ b/ServiceStack/UrlExtensions.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace ServiceStack
+{
+ /// <summary>
+ /// Donated by Ivan Korneliuk from his post:
+ /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html
+ ///
+ /// Modified to only allow using routes matching the supplied HTTP Verb
+ /// </summary>
+ public static class UrlExtensions
+ {
+ public static string GetOperationName(this Type type)
+ {
+ var typeName = type.FullName != null //can be null, e.g. generic types
+ ? LeftPart(type.FullName, "[[") //Generic Fullname
+ .Replace(type.Namespace + ".", "") //Trim Namespaces
+ .Replace("+", ".") //Convert nested into normal type
+ : type.Name;
+
+ return type.IsGenericParameter ? "'" + typeName : typeName;
+ }
+
+ public static string LeftPart(string strVal, string needle)
+ {
+ if (strVal == null) return null;
+ var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase);
+ return pos == -1
+ ? strVal
+ : strVal.Substring(0, pos);
+ }
+ }
+} \ No newline at end of file
diff --git a/ServiceStack/packages.config b/ServiceStack/packages.config
new file mode 100644
index 000000000..6b8deb9c9
--- /dev/null
+++ b/ServiceStack/packages.config
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+</packages> \ No newline at end of file
diff --git a/ServiceStack/project.json b/ServiceStack/project.json
new file mode 100644
index 000000000..fbbe9eaf3
--- /dev/null
+++ b/ServiceStack/project.json
@@ -0,0 +1,17 @@
+{
+ "frameworks":{
+ "netstandard1.6":{
+ "dependencies":{
+ "NETStandard.Library":"1.6.0",
+ }
+ },
+ ".NETPortable,Version=v4.5,Profile=Profile7":{
+ "buildOptions": {
+ "define": [ ]
+ },
+ "frameworkAssemblies":{
+
+ }
+ }
+ }
+} \ No newline at end of file