Статья рассказывает как быстро реализовать 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, "" } }
}
);
}
}
Надеюсь мой опыт будет Вам полезен.