public override void WriteToStream(Type type, object value, Stream writeStream, System.Text.Encoding content) { using (var writer = new StreamWriter(writeStream)) { // First, create a package to hold the results var pkg = new ICTMediaType(); // Then, get some information about the request // This depends upon the use of the default route template; in WebApiConfig.cs... // routeTemplate: "api/{controller}/{id}" // Also, we must remove any trailing slashes var request = HttpContext.Current.Request; var requestMethod = request.HttpMethod; var requestUri = request.Path.TrimEnd('/'); var requestController = request.Url.Segments[2].TrimEnd('/'); var requestId = (request.Url.Segments.Count() > 3) ? request.Url.Segments[3].TrimEnd('/') : ""; // Next, discover what is in the response by inspecting the type name; it could be // 1. If error - return a package that holds the error message // 2. If collection - return a package that holds a collection // 3. If object - return a package that holds an object // Setup for switch-case statement string responseDataType = ""; if (type.Name == "HttpError") { responseDataType = "error"; } else if (type.Namespace == "System.Collections.Generic") { responseDataType = "collection"; } else { responseDataType = "object"; } // This is the package generating code... switch (responseDataType) { case "error": { // Link relation for "self" pkg.links.Add(new link() { rel = "self", href = requestUri, methods = requestMethod }); pkg.count = 0; pkg.data.Add(value); } break; case "collection": { // How many items? pkg.count = (value as ICollection).Count; // Transform the data into an enumerable collection IEnumerable collection = (IEnumerable)value; foreach (var item in collection) { // Create an object that can expand at runtime // by dynamically and programmatically adding new properties IDictionary <string, object> newItem = new ExpandoObject(); // N O T I C E // ########### // We must do special processing for the Chinook database // Each entity class has an identifier with a composite name, // "Entity" name plus "Id" (e.g. CustomerId) // We want to locate this "Entity" plus "Id" property name // so that we can get the integer identifier for the item in the collection // Algorithm... // 1. For each property (except "Id"), add it to newItem // 2. While looking at each property, // check whether its name matches the "Entity" plus "Id" pattern // 3. If yes, save its value as the item's identifier bool shouldContinueLooking = true; // Id value as an integer int idValueInt = 0; // Go through the all the properties in an item // and add them to the expando object foreach (PropertyInfo prop in item.GetType().GetProperties()) { // Add the property newItem.Add(ConfigurePropertyName(prop), prop.GetValue(item)); if (prop.Name == "Id") { // Save/remember the Id value idValueInt = (int)prop.GetValue(item); // Stop looking, because we have found the identifier shouldContinueLooking = false; } // Should we continue looking for the identifier? if (shouldContinueLooking) { // Setup the entity name var entityName = ""; // Get the controller name // Remove the plural "s", if present var possibleBaseType = requestController.TrimEnd('s'); // Compare the result against a number of rules/checks if (prop.Name.Length > 2 && prop.Name.EndsWith("Id") && prop.Name.StartsWith(possibleBaseType, true, null) && prop.GetValue(item) is Int32) { // Boom, we have located the identifier entityName = possibleBaseType; } // Now do the comparison, if a match, add an "Id" property entityName = entityName + "Id"; if (prop.Name.ToLower() == entityName.ToLower()) { // We have found the identifier idValueInt = (int)prop.GetValue(item); } } } // Add the links (below) dynamic o = item; // ################################################################################ // Get the supported resource URIs for the item... var allApiDescriptionsForItem = ApiExplorerService.GetApiDescriptionsForUri(requestController, idValueInt.ToString()); // Setup a collection to hold the links var itemLinks = new List <link>(); // For each supported resource URI, generate and compose the link foreach (var apiDescription in allApiDescriptionsForItem) { // Fix the URI string var itemUri = apiDescription.RelativePath.Replace("{id}", idValueInt.ToString()); // Create and initially configure the link var relValue = "self"; if (apiDescription.HttpMethod.Method == "PUT" || apiDescription.HttpMethod.Method == "DELETE") { relValue = "edit"; } var newLink = new link() { rel = relValue, href = itemUri, methods = apiDescription.HttpMethod.Method }; newLink.title = apiDescription.Documentation; // Get the ActionDescriptor property var actionDescriptor = apiDescription.ActionDescriptor as System.Web.Http.Controllers.ReflectedHttpActionDescriptor; // Look for a "from body" parameter - we need that to render the field list var parBind = actionDescriptor.ActionBinding.ParameterBindings.SingleOrDefault(pb => pb.WillReadBody); // Generate the field list, if we have a parameter binding if (parBind != null) { // Get its data type Type parType = parBind.Descriptor.ParameterType; // Setup a fields collection var fields = new List <field>(); // Generate the field list (different procedure for strings) if (parType != null) { if (parType.Name == "String") { // Generate our own field fields.Add(new field { name = "(none)", type = "string" }); } else { fields = GenerateFields(parType); } } newLink.fields = fields; } // Add the new link to the collection of links itemLinks.Add(newLink); } // Add the collection of links to the new item newItem.Add("links", itemLinks); // Add the new item to the package's "data" property pkg.data.Add(newItem); } // ################################################################################ // Generate links for the collection URI var allApiDescriptionsForColl = ApiExplorerService .GetApiDescriptionsForUri(requestController, null); // Setup a collection to hold the links var pkgLinks = new List <link>(); // For each supported resource URI, generate and compose the link foreach (var apiDescription in allApiDescriptionsForColl) { // Fix the URI string var itemUri = apiDescription.RelativePath.Replace("{id}", 0.ToString()); // Create and initially configure the link var relValue = "self"; if (apiDescription.HttpMethod.Method == "POST") { relValue = "edit"; } var newLink = new link() { rel = relValue, href = itemUri, methods = apiDescription.HttpMethod.Method }; newLink.title = apiDescription.Documentation; // Get the ActionDescriptor property var actionDescriptor = apiDescription.ActionDescriptor as System.Web.Http.Controllers.ReflectedHttpActionDescriptor; Type parType = null; // Look for a "from body" parameter - we need that to render the field list var parBind = actionDescriptor.ActionBinding.ParameterBindings.SingleOrDefault(pb => pb.WillReadBody); if (parBind != null) { parType = parBind.Descriptor.ParameterType; } // Alternatively, look for a "from URI" parameter, as we can use that too if (apiDescription.ParameterDescriptions.Count == 1) { var pd = apiDescription.ParameterDescriptions[0]; if (pd.Source == System.Web.Http.Description.ApiParameterSource.FromUri) { // Yay, can use the binding and the type parBind = actionDescriptor.ActionBinding.ParameterBindings[0]; parType = pd.ParameterDescriptor.ParameterType; } } // Generate the field list, if we have a parameter binding if (parBind != null) { // Get its data type //Type parType = parBind.Descriptor.ParameterType; // Setup a fields collection var fields = new List <field>(); // Generate the field list (different procedure for strings) if (parType != null) { if (parType.Name == "String") { // Generate our own field fields.Add(new field { name = "(none)", type = "string" }); } else { fields = GenerateFields(parType); } } newLink.fields = fields; } // Add the new link to the collection of links pkgLinks.Add(newLink); } // Add the collection of links to the package pkg.links = pkgLinks; } break; case "object": { // No, NOT a collection IDictionary <string, object> newItem = new ExpandoObject(); // Go through the all the properties in an item foreach (PropertyInfo prop in value.GetType().GetProperties()) { newItem.Add(ConfigurePropertyName(prop), prop.GetValue(value)); } pkg.count = 1; pkg.data.Add(newItem); // ################################################################################ // Get the supported resource URIs for the item... var allApiDescriptionsForItem = ApiExplorerService.GetApiDescriptionsForUri(requestController, requestId.ToString()); // Setup a collection to hold the links var itemLinks = new List <link>(); // For each supported resource URI, generate and compose the link foreach (var apiDescription in allApiDescriptionsForItem) { // Fix the URI string var itemUri = apiDescription.RelativePath.Replace("{id}", requestId.ToString()); // Create and initially configure the link var relValue = "self"; if (apiDescription.HttpMethod.Method == "PUT" || apiDescription.HttpMethod.Method == "DELETE") { relValue = "edit"; } var newLink = new link() { rel = relValue, href = itemUri, methods = apiDescription.HttpMethod.Method }; newLink.title = apiDescription.Documentation; // Get the ActionDescriptor property var actionDescriptor = apiDescription.ActionDescriptor as System.Web.Http.Controllers.ReflectedHttpActionDescriptor; // Look for a "from body" parameter - we need that to render the field list var parBind = actionDescriptor.ActionBinding.ParameterBindings.SingleOrDefault(pb => pb.WillReadBody); // Generate the field list, if we have a parameter binding if (parBind != null) { // Get its data type Type parType = parBind.Descriptor.ParameterType; // Setup a fields collection var fields = new List <field>(); // Generate the field list (different procedure for strings) if (parType != null) { if (parType.Name == "String") { // Generate our own field fields.Add(new field { name = "(none)", type = "string" }); } else { fields = GenerateFields(parType); } } newLink.fields = fields; } // Add the new link to the collection of links itemLinks.Add(newLink); } pkg.links = itemLinks; // ################################################################################ // Generate links for the collection URI var allApiDescriptionsForColl = ApiExplorerService .GetApiDescriptionsForUri(requestController, null); // Setup a collection to hold the links var pkgLinks = new List <link>(); // For each supported resource URI, generate and compose the link foreach (var apiDescription in allApiDescriptionsForColl) { // Fix the URI string //var itemUri = apiDescription.RelativePath.Replace("{id}", idValueInt.ToString()); var itemUri = apiDescription.RelativePath.Replace("{id}", 0.ToString()); // Create and initially configure the link var relValue = "collection"; if (apiDescription.HttpMethod.Method == "POST") { relValue = "edit"; } var newLink = new link() { rel = relValue, href = itemUri, methods = apiDescription.HttpMethod.Method }; newLink.title = apiDescription.Documentation; // Get the ActionDescriptor property var actionDescriptor = apiDescription.ActionDescriptor as System.Web.Http.Controllers.ReflectedHttpActionDescriptor; Type parType = null; // Look for a "from body" parameter - we need that to render the field list var parBind = actionDescriptor.ActionBinding.ParameterBindings.SingleOrDefault(pb => pb.WillReadBody); if (parBind != null) { parType = parBind.Descriptor.ParameterType; } // Alternatively, look for a "from URI" parameter, as we can use that too if (apiDescription.ParameterDescriptions.Count == 1) { var pd = apiDescription.ParameterDescriptions[0]; if (pd.Source == System.Web.Http.Description.ApiParameterSource.FromUri) { // Yay, can use the binding and the type parBind = actionDescriptor.ActionBinding.ParameterBindings[0]; parType = pd.ParameterDescriptor.ParameterType; } } // Generate the field list, if we have a parameter binding if (parBind != null) { // Get its data type //Type parType = parBind.Descriptor.ParameterType; // Setup a fields collection var fields = new List <field>(); // Generate the field list (different procedure for strings) if (parType != null) { if (parType.Name == "String") { // Generate our own field fields.Add(new field { name = "(none)", type = "string" }); } else { fields = GenerateFields(parType); } } newLink.fields = fields; } // Add the new link to the collection of links pkgLinks.Add(newLink); } // Add the collection of links to the package pkg.links.AddRange(pkgLinks); } break; default: break; } // Deliver the package... string json = JsonConvert.SerializeObject(pkg, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); var buffer = Encoding.Default.GetBytes(json); writeStream.Write(buffer, 0, buffer.Length); writeStream.Flush(); writeStream.Close(); } }
public override void WriteToStream(Type type, object value, Stream writeStream, System.Text.Encoding content) { using (var writer = new StreamWriter(writeStream)) { // First, create a package to hold the results var pkg = new ICTMediaType(); if (value != null) { // Determine the pattern by gathering query characteristics // How many segments, after the fixed "/api/ segments // ================================================== // Will always have something in it var segments = HttpContext.Current.Request.Url.Segments; // Remove the first two segments, / and api/ // This will leave only the controller name and whatever follows that var segmentsCount = segments.Length - 2; // Do we have a query string? // ========================== var query = HttpContext.Current.Request.QueryString; // If there's no query string, it does not blow up var queryCount = query.Count; // If there's no query string, this value is zero // Get the route data, look for integer "id" property // ================================================== HttpRequestMessage hrm = HttpContext.Current.Items["MS_HttpRequestMessage"] as HttpRequestMessage; var routeData = hrm.GetRouteData(); // Has route template as a string // Also has or shows "id" as a "parameter" var rdItem = routeData.Values.SingleOrDefault(r => r.Key == "id"); // We'll get back a kvp with the data, or with nulls for key and value var idKeyValue = Convert.ToInt32(rdItem.Value); // If this is zero, then "id" is not in the route data // If non-zero, "id" is in the route data, and we have the value // Do we have an integer identifier? var intId = (!string.IsNullOrEmpty(rdItem.Key)) ? true : false; // How many items are in the response? // =================================== //var isCollection = // value.GetType().GetInterfaces() // .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); var isCollection = type.Namespace == "System.Collections.Generic" ? true : false; var itemCount = 1; if (isCollection) { var items = (ICollection)value; itemCount = items.Count; } // Continue... var absolutePath = HttpContext.Current.Request.Url.AbsolutePath; string[] u = absolutePath.Split(new char[] { '/' }); // We will use this later (soon)... var pattern = 0; if (isCollection) { // Will have zero or more items // Will either be a get-all // Or a get-some-filtered // Come back and fix this later... pattern = -1; if (segmentsCount == 1 && queryCount == 0 && idKeyValue == 0) { // Get all // Zero or more items in "value" // Exactly 1 segment after "/api/", which suggests a collection // Does not have a query string // Does NOT have the "id" parameter in the route pattern = 1; } if (segmentsCount == 1 && queryCount > 0 && idKeyValue == 0) { // Get some filtered // Zero or more items in "value" // Exactly 1 segment after "/api/", which suggests a collection // Has a query string // Does NOT have the "id" parameter in the route pattern = 2; } IEnumerable collection = (IEnumerable)value; int count = 0; foreach (var item in collection) { count++; IDictionary <string, object> newItem = new ExpandoObject(); // Name and value of the field that holds the identifier var baseClassName = ""; var idValue = 0; // Go through the all the properties in an item foreach (PropertyInfo prop in item.GetType().GetProperties()) { // N O T I C E // ########### // Special processing for the Chinook database // Each entity class has an identifier with a composite name // Entity plus Id (e.g. CustomerId) // Algorithm... // 1. For each property (except "Id"), add it to newItem // 2. While looking at each property, // check whether its name matches the "Entity" plus "Id" pattern // 3. If yes, create a property named "Id" with the same value // Safety check, which rejects any property named "Id" if (!(prop.Name == "Id")) { newItem.Add(prop.Name, prop.GetValue(item)); } // New algorithm... var objName = ""; // Get the all-but-last character of the URI segment for the "controller" var possibleBaseType = u[2]; // Remove the plural "s", if present if (u[2].EndsWith("s", false, null)) { possibleBaseType = u[2].TrimEnd('s'); } // Compare the result against a number of rules/checks if (prop.Name.Length > 2 && prop.Name.EndsWith("Id") && prop.Name.StartsWith(possibleBaseType, true, null) && prop.GetValue(item) is Int32) { // Boom, we have located the identifier objName = possibleBaseType; } // We now have the name of the base class // (but do we need it for anything?) baseClassName = objName; // Now do the comparison, if a match, add an "Id" property objName = objName + "Id"; if (prop.Name.ToLower() == objName.ToLower()) { newItem.Add("Id", prop.GetValue(item)); // Save the value of this identifier to make the link next/below idValue = (int)prop.GetValue(item); } } // Add the links (below) dynamic o = item; // Get the supported HTTP methods for the item... // Setup... object idValueObject; int idValueInt = 0; if (newItem.TryGetValue("Id", out idValueObject)) { idValueInt = (int)idValueObject; } // Get the item methods and add a link var itemMethods = string.Join(",", ApiExplorerService.GetSupportedMethods(u[2], idValueInt.ToString())); newItem.Add("Link", new link() { rel = "item", href = string.Format("{0}/{1}", absolutePath, idValueInt), methods = itemMethods }); pkg.data.Add(newItem); } // Add a link relation for 'self' pkg.links.Add(new link() { rel = "self", href = absolutePath, methods = "GET" }); // Link relation for 'create', if supported // Hard-coded for now - we want to make this discoverable // TODO - make this discoverable ! ! ! if (ApiExplorerService.GetSupportedMethods(u[2], null).Contains("POST")) { var postLink = new link() { rel = "create", href = absolutePath, methods = "POST" }; postLink.fields = new List <field>(); postLink.fields.Add(new field { name = "FirstName", type = "string" }); postLink.fields.Add(new field { name = "LastName", type = "string" }); postLink.fields.Add(new field { name = "Age", type = "int" }); pkg.links.Add(postLink); } pkg.count = count; } else { // No, NOT a collection // Set pattern (come back to this later and fix) pattern = -1; IDictionary <string, object> newItem = new ExpandoObject(); // Go through the all the properties in an item foreach (PropertyInfo prop in value.GetType().GetProperties()) { newItem.Add(prop.Name, prop.GetValue(value)); } var itemMethods = string.Join(",", ApiExplorerService.GetSupportedMethods(u[2], u[3])); newItem.Add("Link", new link() { rel = "self", href = absolutePath, methods = itemMethods }); // Link relation for 'self' pkg.links.Add(new link() { rel = "self", href = absolutePath, methods = itemMethods }); var controllerMethods = string.Join(",", ApiExplorerService.GetSupportedMethods(u[2], null)); // Link relation for 'collection' pkg.links.Add(new link() { rel = "collection", href = string.Format("/{0}", u[1]), methods = controllerMethods }); pkg.count = 1; pkg.data.Add(newItem); } } string json = JsonConvert.SerializeObject(pkg, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore }); var buffer = Encoding.Default.GetBytes(json); writeStream.Write(buffer, 0, buffer.Length); writeStream.Flush(); writeStream.Close(); } }