Статья рассказывает как быстро реализовать RESTful API в имеющемся классическом ASP.NET приложении.
Как при этом максимально использовать возможности библиотеки MVC.
Какие инструменты будем использовать
1. System.Web.Routing.RouteTable и IRouteHandler для получения ссылок вида mysite.ru/rest/client/0
2. System.Web.Mvc.DefaultModelBinder чтобы не писать перекладывание данных из запроса в модель
3. System.Web.IHttpHandler для преобразования принимаемых запросов в одну из CRUD операций
Принцип работы
В RouteTable добавляем Route который по шаблону перенаправляет запрос на нужный нам HttpHandler.
Регистрируем два Route — для CRUD операций и для операций поиска по параметрам.
HttpHandler осуществляет выбор нужной операции по методу реквеста и переданным параметрам.
Если это get запрос и присутствует параметр query, то выбирается операция поиска по параметрам.
Если это операция записи (create, update, delete), то используется наследник DefaultModelBinder для создания или загрузки нужной модели и к ней применяются данные полученные из запроса.
Во время операции чтения (read) в случае если передан параметр id, выбирается одна модель, если id не передан, то возвращается вся коллекция моделей.
Последним этапом модель преобразуется в JSON объект.
В ответе настраивается кеширование на 30 сек.
Не стал реализовывать конфигурирование чтобы не загромождать код.
При конфигурировании решения могут возникнуть две сложности:
1. 404 ошибка — лечится отключением проверки на существование файла в IIS
(см. здесь или ниже в настройках web.config)
2. Объект сессии отсутсвует — лечится перерегистрацией Session модуля
(см. здесь или ниже в настройках web.config)
Исходники приложения можно скачать здесь
Пример реализации сервиса для модели Client
Класс ClientRestHttpHandler
Класс ClientModelBinder
Именения в Global.asax
Настройка web.config
Все, модель Client доступна по REST API с нашего сайта.
Исходники базовых классов RestHttpHandler и RestRouteHandler
Надеюсь мой опыт будет Вам полезен.
Как при этом максимально использовать возможности библиотеки MVC.
Какие инструменты будем использовать
1. System.Web.Routing.RouteTable и IRouteHandler для получения ссылок вида mysite.ru/rest/client/0
2. System.Web.Mvc.DefaultModelBinder чтобы не писать перекладывание данных из запроса в модель
3. System.Web.IHttpHandler для преобразования принимаемых запросов в одну из CRUD операций
Принцип работы
В RouteTable добавляем Route который по шаблону перенаправляет запрос на нужный нам HttpHandler.
Регистрируем два Route — для CRUD операций и для операций поиска по параметрам.
HttpHandler осуществляет выбор нужной операции по методу реквеста и переданным параметрам.
Если это get запрос и присутствует параметр query, то выбирается операция поиска по параметрам.
Если это операция записи (create, update, delete), то используется наследник DefaultModelBinder для создания или загрузки нужной модели и к ней применяются данные полученные из запроса.
Во время операции чтения (read) в случае если передан параметр id, выбирается одна модель, если id не передан, то возвращается вся коллекция моделей.
Последним этапом модель преобразуется в JSON объект.
В ответе настраивается кеширование на 30 сек.
Не стал реализовывать конфигурирование чтобы не загромождать код.
При конфигурировании решения могут возникнуть две сложности:
1. 404 ошибка — лечится отключением проверки на существование файла в IIS
(см. здесь или ниже в настройках web.config)
2. Объект сессии отсутсвует — лечится перерегистрацией Session модуля
(см. здесь или ниже в настройках web.config)
Исходники приложения можно скачать здесь
Пример реализации сервиса для модели Client
Класс 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 }; } }
Класс 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); } }
Именения в Global.asax
void Application_Start(object sender, EventArgs e) { RestRouteHandler<ClientRestHttpHandler>.Register("client", "clients"); }
Настройка 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>
Все, модель Client доступна по REST API с нашего сайта.
Исходники базовых классов RestHttpHandler и 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, "" } } } ); } }
Надеюсь мой опыт будет Вам полезен.