Introduction
This project is to illustrate how to make use of the ASP.NET Web API wisely to simulate the traditional Web Services Description Language (WSDL) service binding.
This project uses a single Web API Data Structure Controller that monitors the Model namespace to generate a data structure class file upon request. It provides
developers with up to-date class definitions. Developers are able to plug and play the generated file with their needs without rewriting the same class definitions.
Background
The intended users for this document are users with familiarity of ASP.NET MVC, Web API, and WPF technologies. And users who want faster development on Web API services.
Benefits of using the Data Structure Controller
Eliminate the need for rewriting the data structure classes.
Eliminate the need for implementing WebClient
to access Web API services:
Generated sample file
Enhancements
Implemented user defined sample method interface for programmers to override or inject more useful sample methods for the Web API consuming programmers to use.
Sample method's code.txt file sample:
Generated class file with injected sample method code:
Using the code
Host this DataStructureController
as a Web API Service, call it through:
- url/Api/DataStructure/
- url/Api/DataStructure/?Observable=true
- url/Api/DataStructure/?IncludeSample=true
- url/Api/DataStructure/?Observable=true&IncludeSample=true
After downloading the class file, download the JsonConvert library referred on top of the generated file,
put the downloaded file in your app project, and start using it immediately. Be sure to run the
TestDB.edmx.sql file under the Models folder to create the predefined database. MVC 4.0 installation is required to run the solution.
Data Structure Controller Class Definition
using System;
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using TestProject.Models;
using System.Text;
using System.Data.Objects.DataClasses;
using TestProject.Properties;
using System.Reflection;
using System.Data;
using System.Net.Http.Headers;
using System.IO;
using System.Web;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace TestProject.Controllers.Api
{
public class DataStructureController : ApiController
{
public HttpResponseMessage Get(bool observable=false)
{
var text = GenerateClass(observable: observable);
var response = CreateFileResponse(text);
return response;
}
public HttpResponseMessage Get(string name, bool observable = false)
{
var text = GenerateClass(name, observable);
var response = CreateFileResponse(text);
return response;
}
private static HttpResponseMessage CreateFileResponse(string text)
{
var data = Encoding.UTF8.GetBytes(text);
var response = new HttpResponseMessage();
var ms = new MemoryStream(data);
response.Content = new StreamContent(ms);
response.Content.Headers.ContentType =
new MediaTypeHeaderValue("application/octet-stream");
response.Content.Headers.ContentDisposition =
new ContentDispositionHeaderValue("attachment");
response.Content.Headers.ContentDisposition.FileName =
string.Format("{0}.cs", Settings.Default.ModelNamespace);
return response;
}
public static string GenerateClass(string className = null, bool observable = false)
{
var types = typeof(TestDBEntities).Assembly
.GetTypes()
.Where(x => x.Namespace == Settings.Default.ModelNamespace &&
x.BaseType == typeof(EntityObject));
if (!string.IsNullOrWhiteSpace(className))
types = types.Where(x => x.Name == className);
var builder = new StringBuilder();
var localJsonLibrary = string.Format("{0}/{1}",
DataStructureHelper.MachineHostUrl, Settings.Default.LocalJsonLibrary);
builder.AppendFormat("//{1} or download it from this server: {2}{0}",
Environment.NewLine, Settings.Default.JsonConverterLibrary, localJsonLibrary);
builder.AppendLine(Settings.Default.UsingNamespace);
builder.AppendFormat("namespace {0}{1}{{{1}",
Settings.Default.ModelNamespace, Environment.NewLine);
var classExtend = string.Empty;
var classNotifier = string.Empty;
if (observable)
{
classExtend = ": INotifyPropertyChanged";
classNotifier = Settings.Default.PropertyChangeNotifier;
}
builder.AppendFormat("{0}public static class ApiServiceUrlHelper {1}{0}{{{1}",
GetTabs(1), Environment.NewLine);
builder.AppendFormat("{0}public const string ApiHostUrl = \"{2}\";{1}",
GetTabs(2), Environment.NewLine, DataStructureHelper.MachineHostUrl);
builder.AppendFormat("{0}public const string ApiServiceUrl =
ApiHostUrl + \"/Api/\";{1}", GetTabs(2), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(1), Environment.NewLine);
foreach (var type in types)
{
builder.AppendFormat("{0}public partial class {2}{3}{1}\t{{{1}",
GetTabs(1), Environment.NewLine, type.Name, classExtend);
builder.AppendFormat("{0}public const string ApiServiceUrl =
ApiServiceUrlHelper.ApiServiceUrl + \"{2}/\";{1}",
GetTabs(2), Environment.NewLine, type.Name);
var properties = type.GetProperties();
GetClassDefinition(observable, builder, properties);
builder.AppendLine(classNotifier);
builder.AppendLine(GetClassUsage(type, observable));
builder.AppendFormat("{0}}}{1}", GetTabs(1), Environment.NewLine);
}
builder.Append("}");
return builder.ToString();
}
private static void GetClassDefinition(bool observable,
StringBuilder builder, PropertyInfo[] properties)
{
foreach (var property in properties)
{
if (IsBuiltInType(property.PropertyType))
continue;
builder.Append(GetProperty(property, observable));
}
}
private static string GetClassUsage(Type type, bool observable)
{
var className = type.Name;
var builder = new StringBuilder();
var helper = new DataStructureHelper(observable, 2, className);
var generatedGetUsage = false;
var generatedPosUsage = false;
var generatedPutUsage = false;
var generatedDeleteUsage = false;
if (type.GetInterface(typeof(IDataStructure).Name) != null)
{
var obj = (IDataStructure)Activator.CreateInstance(type);
generatedGetUsage = GenerateMethodSample(obj.GenerateGetMethodSample, helper, builder);
generatedPosUsage = GenerateMethodSample(obj.GeneratePosMethodSample, helper, builder);
generatedPutUsage = GenerateMethodSample(obj.GeneratePutMethodSample, helper, builder);
generatedDeleteUsage = GenerateMethodSample(obj.GenerateDeleteMethodSample, helper, builder);
var fileName = obj.GenerateMethodSamplesFromFile();
GenerateOtherMethodSamples(fileName, helper, builder);
}
if(!generatedGetUsage)
builder.Append(GetClassGetUsage(className, observable));
if(!generatedPosUsage)
builder.Append(GetClassPostUsage(className));
if(!generatedPutUsage)
builder.Append(GetClassPutUsage(className));
if (!generatedDeleteUsage)
builder.Append(GetClassDeleteUsage(className));
return builder.ToString();
}
private static void GenerateOtherMethodSamples(string fileName,
DataStructureHelper helper, StringBuilder builder)
{
var regex = new Regex(@"@(\w+)", RegexOptions.Compiled);
var args = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{"@Observable", helper.Observable.ToString()},
{"@HostUrl", "ApiServiceUrlHelper.ApiServiceUrl"},
{"@CurrentServiceUrl", "ApiServiceUrl"},
{"@CurrentService", helper.CurrentService},
{"@Tabs", helper.Tabs.ToString()},
};
using (var reader = File.OpenText(fileName))
{
var text = reader.ReadToEnd();
string output = regex.Replace(text, match =>
{ if (args.Any(x => x.Key == match.Value))
return args[match.Value];
return match.Value;
});
builder.AppendLine(output);
}
}
private static bool GenerateMethodSample(Func<DataStructureHelper,
string> func, DataStructureHelper helper, StringBuilder builder)
{
var text = func(helper);
var result = text != null;
if (result)
builder.Append(text);
return result;
}
private static string GetClassGetUsage(string className, bool observable)
{
var builder = new StringBuilder();
var type = observable ? "ObservableCollection" : "Collection";
var returnType = string.Format("{0}<{1}>", type, className);
builder.AppendFormat("{0}public static {2} GetAll(){1}{0}{{{1}",
GetTabs(2), Environment.NewLine, returnType);
builder.AppendFormat("{0}using (WebClient client =
new WebClient()){1}{0}{{{1}", GetTabs(3), Environment.NewLine, className);
builder.AppendFormat("{0}var dataString =
client.DownloadString(ApiServiceUrl);{1}", GetTabs(4), Environment.NewLine);
builder.AppendFormat("{0}var data =
JsonConvert.DeserializeObject<{2}>(dataString);{1}",
GetTabs(4), Environment.NewLine, returnType);
builder.AppendFormat("{0}return data;{1}", GetTabs(4), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(3), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(2), Environment.NewLine);
return builder.ToString();
}
private static string GetClassPostUsage(string className)
{
var builder = new StringBuilder();
builder.AppendFormat("{0}public static {2} Add({2} obj){1}{0}{{{1}",
GetTabs(2), Environment.NewLine, className);
builder.AppendFormat("{0}using (WebClient client =
new WebClient()){1}{0}{{{1}", GetTabs(3), Environment.NewLine, className);
builder.AppendFormat("{0}client.Headers[HttpRequestHeader.ContentType] =
\"application/json\";{1}", GetTabs(4), Environment.NewLine);
builder.AppendFormat("{0}var jsonObj =
JsonConvert.SerializeObject(obj);{1}", GetTabs(4), Environment.NewLine, className);
builder.AppendFormat("{0}var dataString =
client.UploadString(ApiServiceUrl, jsonObj);{1}", GetTabs(4), Environment.NewLine);
builder.AppendFormat("{0}var data =
JsonConvert.DeserializeObject<{2}>(dataString);{1}", GetTabs(4), Environment.NewLine, className);
builder.AppendFormat("{0}return data;{1}", GetTabs(4), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(3), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(2), Environment.NewLine);
return builder.ToString();
}
private static string GetClassPutUsage(string className)
{
var builder = new StringBuilder();
builder.AppendFormat("{0}public void Update(){1}{0}{{{1}",
GetTabs(2), Environment.NewLine, className);
builder.AppendFormat("{0}using (WebClient client =
new WebClient()){1}{0}{{{1}", GetTabs(3), Environment.NewLine, className);
builder.AppendFormat("{0}client.Headers[HttpRequestHeader.ContentType] =
\"application/json\";{1}", GetTabs(4), Environment.NewLine);
builder.AppendFormat("{0}var jsonObj =
JsonConvert.SerializeObject(this);{1}", GetTabs(4), Environment.NewLine, className);
builder.AppendFormat("{0}var dataString =
client.UploadString(ApiServiceUrl+this.ID, \"put\",
jsonObj);{1}", GetTabs(4), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(3), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(2), Environment.NewLine);
return builder.ToString();
}
private static string GetClassDeleteUsage(string className)
{
var builder = new StringBuilder();
builder.AppendFormat("{0}public void Delete(){1}{0}{{{1}",
GetTabs(2), Environment.NewLine, className);
builder.AppendFormat("{0}using (WebClient client =
new WebClient()){1}{0}{{{1}", GetTabs(3), Environment.NewLine, className);
builder.AppendFormat("{0}var dataString = client.UploadString(
ApiServiceUrl+this.ID, \"delete\", \"\");{1}", GetTabs(4), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(3), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(2), Environment.NewLine);
return builder.ToString();
}
private static string GetProperty(PropertyInfo property, bool observable)
{
var type = GetTypeName(property.PropertyType, observable);
if (!observable)
return string.Format("{0}public {2} {3} {{ get; set; }}{1}",
GetTabs(2), Environment.NewLine, type, property.Name);
var builder = new StringBuilder();
var privateField = string.Format("_{0}", property.Name);
builder.AppendFormat("{0}private {2} {3};{1}", GetTabs(2),
Environment.NewLine, type, privateField);
builder.AppendFormat("{0}public {2} {3} {1}{0}{{{1}",
GetTabs(2), Environment.NewLine, type, property.Name);
builder.AppendFormat("{0}get{1}{0}{{{1}", GetTabs(3), Environment.NewLine);
builder.AppendFormat("{0}return {2};{1}", GetTabs(4), Environment.NewLine, privateField);
builder.AppendFormat("{0}}}{1}", GetTabs(3), Environment.NewLine);
builder.AppendFormat("{0}set{1}{0}{{{1}", GetTabs(3), Environment.NewLine);
builder.AppendFormat("{0}{2} = value;{1}{0}RaisePropertyChangedEvent(\"{3}\");{1}",
GetTabs(4), Environment.NewLine, privateField, property.Name);
builder.AppendFormat("{0}}}{1}", GetTabs(3), Environment.NewLine);
builder.AppendFormat("{0}}}{1}", GetTabs(2), Environment.NewLine);
return builder.ToString();
}
private static string GetTabs(int num)
{
var builder = new StringBuilder();
for (var i = 0; i < num; i++)
builder.Append("\t");
return builder.ToString();
}
private static string GetTypeName(Type type, bool observable)
{
if (!type.IsGenericType)
return type.Name;
var genericType = type.GetGenericArguments()[0].Name;
if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
return string.Format("{0}?", genericType);
if (type.GetGenericTypeDefinition() == typeof(EntityCollection<>))
{
var collection = observable ? "ObservableCollection" : "Collection";
return string.Format("{0}<{1}>", collection, genericType);
}
return type.Name;
}
private static bool IsBuiltInType(Type type)
{
return type == typeof(EntityState) ||
type == typeof(EntityKey) ||
type == typeof(EntityReference) ||
type.BaseType == typeof(EntityReference);
}
}
}
Points of Interest
I learned that we don't have to wait for Microsoft to do all the things for us when we can do them ourselves. The handler will definitely save 50% or more of your development time;
feel free to enhance the features you want.
History
- Version 1.0. Created the basic functionality of the Data Structure Handler.
- Version 1.1:
- Enhanced the class generation of Web Service consumption to use a centralized URL string.
- Implemented user defined class sample method interface to allow us to inject some useful sample methods or properties to the generated class file.