📜 ⬆️ ⬇️

Implementing a RESTful service in classic ASP.NET

The article tells how to quickly implement a RESTful API in an existing classic ASP.NET application.
How to maximize the capabilities of the MVC library.

What tools will we use

1. System.Web.Routing. RouteTable and IRouteHandler for receiving links of the form mysite.ru/rest/client/0
2. System.Web.Mvc. DefaultModelBinder to not write data transfer from the request to the model
3. System.Web. IHttpHandler to convert received requests to one of the CRUD operations

Principle of operation
')
In RouteTable we add Route which by template redirects the request to the HttpHandler we need.
We register two Route - for CRUD operations and for search operations by parameters.
HttpHandler selects the desired operation using the request method and the passed parameters.
If this is a get query and the query parameter is present, then the search operation by parameters is selected.
If this is a write operation (create, update, delete), then the successor of DefaultModelBinder is used to create or load the required model and the data obtained from the request is applied to it.
During a read operation, if the id parameter is passed, one model is selected, if id is not passed, then the entire collection of models is returned.
The final stage of the model is converted to a JSON object.

In response, caching is configured for 30 seconds.
I did not implement the configuration in order not to clutter up the code.

When configuring a solution, two difficulties may arise:
1. 404 error - is treated by disabling the check for the existence of a file in IIS
(see here or below in web.config settings)
2. The session object is missing - is being treated by re-registering the Session module
(see here or below in web.config settings)

Sources of the application can be downloaded here.

Service implementation example for Client model

Class ClientRestHttpHandler
public class ClientRestHttpHandler : RestHttpHandler<Client, ClientModelBinder> { protected override IEnumerable<Client> GetAll() { return ClientService.GetAll(); } protected override Client GetBy(int id) { return ClientService.GetById(id); } protected override IEnumerable<Client> GetBy(NameValueCollection query) { var result = ClientService.GetAll(); var contains = query["contains"]; if (contains != null) { result = from item in result where item.FirstName.Contains(contains) || item.LastName.Contains(contains) select item; } return result; } protected override void Create(Client entity) { ClientService.Create(entity); } protected override void Update(Client entity) { ClientService.Update(entity); } protected override void Delete(Client entity) { ClientService.Delete(entity); } protected override object ToJson(Client entity) { return new { entity.Id, entity.FirstName, entity.LastName }; } } 


Class ClientModelBinder
 public class ClientModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType) { var value = bindingContext.ValueProvider.GetValue("id"); if (value == null) return ClientService.New(); var result = (int)value.ConvertTo(typeof(int)); return ClientService.GetById(result); } } 


Names in Global.asax
  void Application_Start(object sender, EventArgs e) { RestRouteHandler<ClientRestHttpHandler>.Register("client", "clients"); } 


Configure web.config
 <configuration> <system.webServer> <modules runAllManagedModulesForAllRequests="true"> <!-- fix for empty session on RESTful requests. see http://stackoverflow.com/questions/218057/httpcontext-current-session-is-null-when-routing-requests --> <remove name="Session" /> <add name="Session" type="System.Web.SessionState.SessionStateModule"/> </modules> <handlers> <add name="WildCard" path="*" verb="*" resourceType="Unspecified" /> </handlers> </system.webServer> </configuration> 


Everything, Client model is available on REST API from our site.

Sources of base classes RestHttpHandler and RestRouteHandler

 public abstract class RestHttpHandler : IHttpHandler, IReadOnlySessionState { public const string ParamKeyId = "id"; public const string ParamKeyQuery = "query"; /// <summary> /// RouteData property gives an access to request data provided by the router /// It has a setter to simplify instantiation from the RestRouteHandler class /// </summary> public RouteData RouteData { get; set; } protected bool HasId { get { return this.RouteData.Values[ParamKeyId] != null; } } protected bool HasQuery { get { return this.RouteData.Values[ParamKeyQuery] != null; } } protected int ParseId() { return int.Parse(this.RouteData.Values[ParamKeyId].ToString()); } protected NameValueCollection ParseQuery() { var regex = new Regex("(?<key>[a-zA-Z\\-]+)($|/)(?<value>[^/]+)?"); var matches = regex.Matches(this.RouteData.Values[ParamKeyQuery].ToString()); var result = new NameValueCollection(); foreach (Match match in matches) { result.Add(match.Groups["key"].Value, match.Groups["value"].Value); } return result; } public bool IsReusable { get { return false; } } public abstract void ProcessRequest(HttpContext context); } 


 public abstract class RestHttpHandler<T, TBinder> : RestHttpHandler where T : class where TBinder : DefaultModelBinder, new() { /// <summary> /// ProcessRequest actually does request mapping to one of CRUD actions /// </summary> public override void ProcessRequest(HttpContext context) { var @params = new NameValueCollection { context.Request.Form, context.Request.QueryString }; foreach (var value in this.RouteData.Values) { @params.Add(value.Key, value.Value.ToString()); } RenderHeader(context); if (context.Request.HttpMethod == "GET") { if (this.HasQuery) { @params.Add(this.ParseQuery()); this.Render(context, this.GetBy(@params)); } else { if (this.HasId) { this.Render(context, this.GetBy(this.ParseId())); } else { this.Render(context, this.GetAll()); } } } else { var entity = BindModel(@params); switch (context.Request.HttpMethod) { case "POST": this.Create(entity); break; case "PUT": this.Update(entity); break; case "DELETE": this.Delete(entity); break; default: throw new NotSupportedException(); } this.Render(context, entity); } } protected abstract T GetBy(int id); protected abstract IEnumerable<T> GetBy(NameValueCollection query); protected abstract IEnumerable<T> GetAll(); protected abstract void Create(T entity); protected abstract void Update(T entity); protected abstract void Delete(T entity); protected abstract object ToJson(T entity); private object ToJson(IEnumerable<T> entities) { return ( from entity in entities select this.ToJson(entity)).ToArray(); } private static T BindModel(NameValueCollection @params) { return new TBinder().BindModel( new ControllerContext(), new ModelBindingContext { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(T)), ValueProvider = new NameValueCollectionValueProvider( @params, CultureInfo.InvariantCulture ) } ) as T; } private static void RenderHeader(HttpContext context) { context.Response.ClearHeaders(); context.Response.ClearContent(); context.Response.ContentType = "application/json"; context.Response.ContentEncoding = Encoding.UTF8; var cachePolicy = context.Response.Cache; cachePolicy.SetCacheability(HttpCacheability.Public); cachePolicy.SetMaxAge(TimeSpan.FromSeconds(30.0)); } private void Render(HttpContext context, IEnumerable<T> entities) { Render(context, RuntimeHelpers.GetObjectValue(this.ToJson(entities))); } private void Render(HttpContext context, T entity) { Render(context, RuntimeHelpers.GetObjectValue(this.ToJson(entity))); } private static void Render(HttpContext context, object result) { context.Response.Write( new JavaScriptSerializer().Serialize( RuntimeHelpers.GetObjectValue(result) ) ); } } 


 public class RestRouteHandler<T> : IRouteHandler where T : RestHttpHandler, new() { IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return new T() { RouteData = requestContext.RouteData }; } public static void Register(string name, string pluralName) { RouteTable.Routes.Add( name, new Route( string.Format( "rest/{0}/{{{1}}}", name, RestHttpHandler.ParamKeyId ), new RestRouteHandler<T>() ) { Defaults = new RouteValueDictionary { { RestHttpHandler.ParamKeyId, null } }, Constraints = new RouteValueDictionary { { RestHttpHandler.ParamKeyId, "\\d*" } } } ); RouteTable.Routes.Add( pluralName, new Route( string.Format( "rest/{0}/{{*{1}}}", pluralName, RestHttpHandler.ParamKeyQuery ), new RestRouteHandler<T>()) { Defaults = new RouteValueDictionary { { RestHttpHandler.ParamKeyQuery, "" } } } ); } } 

I hope my experience will be useful to you.

Source: https://habr.com/ru/post/127811/


All Articles