Pull to refresh

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

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

Надеюсь мой опыт будет Вам полезен.
Tags:
Hubs:
+3
Comments 7
Comments Comments 7

Articles