Реализация RESTful сервиса в классическом ASP.NET

    Статья рассказывает как быстро реализовать 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
    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, "" } }
                    }
                );
        }
    }
    

    Надеюсь мой опыт будет Вам полезен.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 7
    • 0
      а как вы решали задачи аутентификации и авторизации?
      • 0
        API используется только самим сайтом, авторизируемся используя стандартные механизмы ASP.NET — страничка с Form аутентификацией.

        В конечном счете в ASP.NET все должно сводиться к стандартным механизмам — сессия, IPrincipal и декларативная или императивная модель безопасности кода.
        msdn.microsoft.com/en-us/library/a95batfc.aspx
        • +1
          для реста это далеко не всегда вариант. рекомендую
          • 0
            Да, согласен, нативная авторизация лучше, но там пока тоже не все так просто.
            Поэтому при выборе подхода надо хорошенько подумать.
            Кстати, очень интересная темя для статьи.
      • 0
        А почему WCF не использовали?
        • –1
          Можно и WCF, но если нужен только REST, то все прибамбасы с WCF атрибутами, интерфейсами и его настройкой будут дополнительной нагрузкой.
          А так — минимальные изменения в окружении и REST сервис для нужд сайта готов.
          За 15 минут объяснил подход всем участникам команды.
          С WCF такой трюк боюсь что не прошел бы.
          А команда у нас еще и распределенная.
          • 0
            Понятно :) В общем, посмотрите статью msdn.microsoft.com/en-us/library/dd203052.aspx ничего там сложного нет. Статья большая, ибо расписывается RESTful с ног до головы, а о WCF там пару глав.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.