В этой статье я расскажу о различных приемах разработки SOAP веб-сервисов по технологии ASMX, а также об этой технологии в целом. Кроме SOAP, также будет рассмотрена реализация AJAX. Статья будет полезна как тем, кто уже знаком с ней, так и тем, кто только собирается создать свой первый веб-сервис.
С самого начала корпорация Microsoft была одним из основных разработчиков стандарта SOAP. В 2002 году в составе самой первой версии ASP.NET 1.0 она представила технологию ASMX (Active Server Method Extended), которая позволила разработчикам в новейшей Visual Studio 2002 легко создавать и потреблять SOAP веб-сервисы. Отмечу, что эта технология официально на MSDN имеет название «XML Web Services». В те годы SOAP только делал первые серьезные шаги в мире веб-разработки. Консорциум W3C одобрил SOAP 1.1 в 2000 году, SOAP 1.2 в 2003 году (дополнен в 2007 году). Поэтому было очень важно сделать для нового стандарта легкую в освоении и применении технологию. И эта цель была достигнута – чтобы работать с веб-сервисами, разработчику даже не обязательно было знать XML, SOAP и WSDL.
В последующие годы технология ASMX получила очень широкое распространение и признание. Также с самого начала Microsoft поставляла к ней аддон Web Services Enhancements (WSE), который позволял реализовывать различные спецификации безопасности WS-* такие, как WS-Security, WS-Policy, WS-ReliableMessaging. Последняя версия — WSE 3.0 вышла в 2005 году. А в 2007 году в составе .NET 3.0 была представлена технология Windows Communication Foundation (WCF), которая стала официальной заменой ASMX. Несмотря на то, что технология ASMX уже давно не развивается, она продолжает широко использоваться и поддерживается новейшими версиями .NET Framework.
ASMX и WCF
Интересно сравнить, сколько веб-сервисов обоих типов видит Google: 314 000 ASMX и 6 280 WCF
Почему же технология ASMX все еще так популярна? Все очень просто: она легка в применении и прекрасно решает задачу в большинстве случаев. Преимущество WCF проявляется, например, в тех случаях, когда вам нужна высокая скорость транспорта, дуплекс, потоковая передача, соблюдение современных стандартов безопасности, REST. Кстати, если вам нужен только REST, то вместо WCF стоит использовать технологию ASP.NET Web API.
Перечислим конкретно плюсы каждой технологии: Плюсы ASMX:
Возможность реализации большого множества стандартов WS-*
Итак, WCF – это «швейцарский нож» в области транспорта данных, а ASMX – «добротная отвертка». И лучше всего, конечно, уметь пользоваться обоими инструментами. Поскольку приемы разработки WCF в интернете описаны более полно и актуально, я решил, что нужно написать статью про ASMX, которая пригодится тем, кому приходится поддерживать старые веб-сервисы, и тем, кто продолжает применять эту технологию для создания новых.
Введение
В статье описаны 20 различных практических приемов, которые можно применить при разработке веб-сервисов по данной технологии. Сценарий для примеров будет следующий. Имеется регулярно пополняемая база данных финансовых отчетов. Необходимо разработать универсальный механизм, с помощью которого у различных клиентов всегда будут актуальные данные по этим отчетам. Решение: пишем SOAP веб-сервис с двумя методами:
Первый метод принимает период во времени и возвращает идентификаторы всех отчетов, которые появились в этом периоде
Второй метод принимает идентификатор отчета и возвращает сами данные по отчету
Потребители веб-сервиса регулярно шлют запросы к первому методу, указывая период с момента их последнего запроса, и при наличии в ответе идентификаторов, запрашивают данные через второй метод.
Примеры демонстрируются на основе кода из «Рекомендуемой конструкции», и чтобы их протестировать достаточно вызвать веб-метод GetReportInfo как показано в примере «Прокси-класс».
1. Простейшая конструкция
Начнем с описания простейшей конструкции веб-сервиса. Внимание, пример носит исключительно теоретический характер! Хоть он и рабочий, никогда так не делайте на практике. Это только демонстрация простоты самой технологии ASMX.
Создайте в Visual Studio новый проект “ASP.NET Empty Web Application” или “ASP.NET Web Service Application” с именем FinReportWebService. Добавьте в него два файла: FinReport.asmx и FinReportService.cs, причем FinReport.asmx добавьте как Text File, а не Web Service, чтобы это был одиночный файл.
public class FinReportService {
[WebMethod]
public int[] GetReportIdArray(DateTime dateBegin, DateTime dateEnd){
int[] array = new int[] {357, 358, 360, 361};
return array;
}
[WebMethod]
public FinReport GetReport(int reportID){
FinReport finReport = new FinReport(){
ReportID = reportID,
Date = new DateTime(2015, 03, 15),
Info = "Some info"
};
return finReport;
}
}
public class FinReport {
public int ReportID { get; set; }
public DateTime Date { get; set; }
public string Info { get; set; }
}
}
Нажмите F5 для запуска веб-сервера и откройте в браузере FinReport.asmx, вы должны увидеть
Готово. Теперь разберем по порядку. Веб-сервис представлен одним обычным классом с одной лишь обязательной особенностью – некоторые его методы помечены специальным атрибутом [WebMethod]. Такие методы класса становятся веб-методами веб-сервиса с соответствующей сигнатурой вызова. Этот класс должен обладать конструктором по умолчанию. При каждом новом запросе IIS его инстанциирует дефолтным конструктором и вызывает соответствующий метод.
Вторая обязательная часть минимальной конструкции – это файл с расширением asmx, внутри которого необходимо указать этот класс.
Интересно сравнить этот вручную созданный asmx файл с тем, который создаст Visual Studio. Предположим, что мы хотим сделать еще один веб-сервис, который возвращает курс обмена валют. Добавьте через меню Add New Item файл ExchangeRate.asmx с типом Web Service.
Нажав один-два раза на F7, можно увидеть следующее:
Оператор Language=«C#» является рудиментарным, и нужен только если вы будете писать исходный код непосредственно внутри asmx файла. Такой код будет компилироваться динамически. Но я считаю, что в целом динамическая компиляция веб-сервиса — не очень хорошая практика, и в частности, не рекомендую использование специальной папки App_Code. А оператор CodeBehind=«ExchangeRate.asmx.cs» просто связывает два файла на уровне Visual Studio.
2. Рекомендуемая конструкция
В этом примере тот же самый веб-сервис реализован более корректным образом. Хотя это и более правильный код, он также служит только для демонстрации. Например, здесь пропущены такие важные стандартные вещи как авторизация, обработка исключений, логирование. Также этот пример будет основой, на которой будут демонстрироваться другие приемы этой статьи. В файле FinReportService.cs содержимое замените на следующий исходный код:
FinReportService.cs
using System;
using System.Web.Services;
using System.Xml.Serialization;
namespace FinReportWebService{
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[WebService(Description = "Фин. отчеты", Namespace = XmlNS)]
public class FinReportService : WebService{
public const string XmlNS = "http://asmx.habrahabr.ru/";
[WebMethod(Description = "Получение списка ID отчетов по периоду")]
public GetReportIdArrayResult GetReportIdArray(GetReportIdArrayArg arg){
return new GetReportIdArrayResult(){
ReportIdArray = new int[] {357, 358, 360, 361}
};
}
[WebMethod(Description = "Получение отчета по ID")]
public GetReportResult GetReport(GetReportArg arg){
return new GetReportResult(){
Report = new FinReport{
ReportID = arg.ReportID,
Date = new DateTime(2015, 03, 15),
Info = getReportInfo(arg.ReportID)
}
};
}
// [Serializable]
// [XmlType(Namespace = FinReportService.XmlNS)]
public class FinReport {
public int ReportID { get; set; }
public DateTime Date { get; set; }
public string Info { get; set; }
}
public class GetReportIdArrayArg {
public DateTime DateBegin { get; set; }
public DateTime DateEnd { get; set; }
}
public class GetReportIdArrayResult {
public int[] ReportIdArray { get; set; }
}
public class GetReportArg {
public int ReportID { get; set; }
}
public class GetReportResult {
public FinReport Report { get; set; }
}
}
Разберем изменения.
Атрибут [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] означает, что веб-сервис проверяется на соответствие спецификации WSI Basic Profile 1.1. Например, согласно ней запрещена перегрузка имени операции, или применение атрибута [SoapRpcMethod]. Такие нарушения будут приводить к ошибке веб-сервиса «Служба „FinReportWebService.FinReportService“ не отвечает спецификации Simple SOAP Binding Profile Version 1.0.». При отсутствии этого атрибута нарушения будут приводить только к предупреждению «Эта веб-служба не отвечает требованиям WS-I Basic Profile v1.1.». В общем случае рекомендуется добавлять этот атрибут, что обеспечивает большую интероперабельность.
Атрибут [WebService(Description = «Фин. отчеты», Namespace = XmlNS)] имеет всего три свойства:
Namespace – дефолтный ХМЛ нэймспейс – указывать обязательно
Description – описание веб-сервиса, отображаемое в браузере
Name – имя веб-сервиса (по дефолту берется имя класса)
Наследование от класса WebService дает доступ к объектам HttpContext, HttpSessionState и некоторым другим, что в некоторых случаях может быть полезно.
В атрибуте [WebMethod(Description = «Получение отчета по ID»)] как правило указывают только Description, который описывает веб-метод в браузере, другие свойства используются редко.
Входящие параметры и возвращаемые значения я лично рекомендую инкапсулировать в специальные классы. Например, я их называю, добавляя суффиксы -Arg и -Result к названию метода, что означает аргумент и результат. В этом примере для упрощения они все находятся в одном файле FinReportService.cs, но в реальных проектах каждый из них я размещаю в отдельном файле в специальной папке типа FinReportServiceTypes. Также их удобно наследовать от общих классов.
По идее, ко всем собственным классам в веб-методах необходимо указывать атрибуты [Serializable] и [XmlType(Namespace = FinReportService.XmlNS)]. Однако в данном случае это не обязательно. Ведь если производится только XML-сериализация, то атрибут [Serializable] не нужен, а XML нэймспейс и так по умолчанию берется из атрибута [WebService]. Отмечу, что в отличие от WCF в ASMX используется обычный XmlSerializer, что позволяет широко управлять сериализацией с помощью таких стандартных атрибутов как [XmlType], [XmlElement], [XmlIgnore] и т.д.
3. Прокси-класс с помощью wsdl.exe
Утилита wsdl.exe является соответствующей для asmx техникой потребления SOAP веб-сервисов. По wsdl файлу или ссылке она генерирует прокси-класс – специальной класс, максимально упрощающий обращение к данному веб-сервису. Разумеется, не важно на какой технологии реализован сам веб-сервис, это может быть что угодно — ASMX, WCF, JAX-WS или NuSOAP. Кстати, у WCF аналогичная утилита называется SvcUtil.exe.
Утилита расположена в папке C:Program Files (x86)Microsoft SDKsWindows, более того, она там представлена в разных версиях, в зависимости от версии .net, разрядности, версии windows и visual studio.
Давайте сделаем клиента для FinReportWebService. В текущем или новом солюшене создайте новый Windows Forms проект FinReportWebServiceClient. Добавьте в нем папку ProxyClass, скопируйте в нее утилиту wsdl.exe и создайте в ней батник GenProxyClass.bat:
С помощью аргумента /n:FinReportWebServiceClient.ProxyClass мы указываем нэймспейс для класса. Запустив его, вы должны получить файл FinReportService.cs. Через Solution Explorer – Show All Files, включите все три файла в солюшен.
На форме добавьте кнопку, а в исходный код формы следующие три метода:
public static FinReportService GetFinReportService(){
var service = new FinReportService();
service.Url = "http://localhost:3500/FinReport.asmx";
service.Timeout = 100 * 1000;
return service;
}
private void webMethodTest_GetReportIdArray() {
var service = GetFinReportService();
var arg = new GetReportIdArrayArg();
arg.DateBegin = new DateTime(2015, 03, 01);
arg.DateEnd = new DateTime(2015, 03, 02);
var result = service.GetReportIdArray(arg);
MessageBox.Show("result.ReportIdArray.Length = " + result.ReportIdArray.Length);
}
private void webMethodTest_GetReport() {
var service = GetFinReportService();
var arg = new GetReportArg();
arg.ReportID = 45;
var result = service.GetReport(arg);
MessageBox.Show(result.Report.Info);
}
Самыми важными свойствами прокси-класса являются Url и Timeout, причем таймаут указывается в миллисекундах и 100 секунд это его дефолтное значение. Теперь с помощью них вы можете протестировать работу веб-сервиса. Демонстрация работы дальнейших приемов будет показана через вызов метода GetReport и заполнение поля result.Report.Info.
В случае создания прокси-класса по wsdl файлу, который ссылается на внешние xsd схемы, все эти схемы необходимо перечислить в команде:
Однако кроме ручного создания прокси-класса Visual Studio позволяет его создать автоматически. Пункт «Add Service Reference» позволяет создать прокси-класс по технологии WCF, и там же в «Advanced» есть кнопка «Add Web Reference», которая создает его уже по технологии ASMX.
4. Серверный класс по данному wsdl
Как известно, wsdl описание веб-сервиса в технологии ASMX генерируется автоматически. Однако иногда возникает обратная задача: по данному wsdl файлу разработать соответствующий ему веб-сервис. Решается она с помощью той же утилиты wsdl.exe. Она может создать необходимый скелет из классов и вам останется только реализовать программную логику веб-методов.
Для примера возьмем wsdl нашего веб-сервиса. Сохраните его из браузера как файл FinReport.wsdl либо скопируйте отсюда:
Создайте в солюшене новый пустой web-проект с именем FinReportWebServiceByWsdl. В него добавьте папку ServerClass, в которую скопируйте файлы FinReport.wsdl и wsdl.exe. Создайте в ней батник GenServerClass.bat:
Запустив его, вы должны получить файл FinReportService.cs. Все четыре файла включите в солюшен.
Итак, как видим, единственное отличие от генерации прокси-класса – это атрибут server. При этом создается абстрактный класс наследованный от WebService с абстрактно описанными веб-методами. Можно от него наследоваться, но при этом все равно придется копировать все атрибуты, поэтому предлагаю сделать следующим образом. Скопировать определение класса в новый файл и пространство имен, убрать слово abstract и написать реализацию методов. После форматирования кода у меня получился следующий файл
using System;
using System.Web.Services;
using System.Web.Services.Description;
using System.Web.Services.Protocols;
using FinReportWebServiceByWsdl.ServerClass;
namespace FinReportWebServiceByWsdl {
[WebService(Namespace="http://asmx.habrahabr.ru/")]
[WebServiceBinding(Name="FinReportServiceSoap", Namespace="http://asmx.habrahabr.ru/")]
public class FinReportService : WebService {
public GetReportResult GetReport(GetReportArg arg) {
return new GetReportResult() {
Report = new FinReport {
ReportID = arg.ReportID,
Date = new DateTime(2015, 03, 15),
Info = "ByWSDL"
}
};
}
}
}
В этом коде утилита явно описала с помощью атрибутов те параметры веб-сервиса, которые неявно определялись по умолчанию. Остается только добавить файл FinReportByWsdl.asmx, который будет указывать на этот новый класс:
ASMX веб-сервис может принимать и возвращать данные в формате JSON, что позволяет реализовать технику ajax. Для работы примера в вашем веб-проекте должны быть следующие три файла:
FinReport.asmx – такой же, что и в первых примерах, всего 1 строка
using System;
using System.Text;
using System.Web.Script.Serialization;
using System.Web.Script.Services;
using System.Web.Services;
using Newtonsoft.Json;
namespace FinReportWebService{
[ScriptService]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[WebService(Description = "Фин. отчеты", Namespace = "http://asmx.habrahabr.ru/")]
public class FinReportService : WebService{
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
[WebMethod]
public void Method_6_POST_NonStandard(int id) {
var result = getFinReportResult(id, "Method_6_POST_NonStandard, Мой текст");
string text = JsonConvert.SerializeObject(result);
byte[] data = Encoding.UTF8.GetBytes(text);
Также в примере используется библиотека Json.NET aka Newtonsoft.Json.
Чтобы веб-сервис мог работать с JSON, нужно применить 2 новых атрибута:
[ScriptService] – без свойств, и [ScriptMethod], в котором свойство ResponseFormat отвечает за формат ответа — JSON или XML, а UseHttpGet – за тип запроса – GET или POST.
В этом веб-сервисе 6 методов, которые демонстрируют различные способы реализации ajax.
Метод 1. Как и во 2 примере он принимает и возвращает объекты классов GetReportArg и GetReportResult. В отформатированном виде запрос и ответ выглядят следующим образом:
Если с аргументом все понятно, то ответ нужно прокомментировать. Все JSON-ответы веб-сервис из соображений безопасности заворачивает в узел «d». Также можем видеть название класса "__type": «FinReportWebService.GetReportResult». А вот формат даты "/Date(1426356000000)/" является проблемой. Что делать с таким форматом, описано в 5 методе, кроме того, подобного формата легко избежать, как показано дальше.
Метод 2. Принципиально иной способ реализации. Используется тип запроса GET, принимает число, возвращает json-строку, а не сам объект и применяется сторонний сериалайзер. В скрипте аргумент задается как data: { id: 2 }, что дополняет URL запроса до вида http://localhost:3500/FinReport.asmx/Method_2_GET?id=2, однако можно сразу указать этот конечный URL.
Обратите внимание, что дата благодаря библиотеке Json.NET тут представлена стандартно. Также нужно отметить обязательность заголовка contentType: «application/json; charset=utf-8», несмотря на то, что это GET. Сделано это для защиты от CSRF. По этой причине попытка открыть URL запроса в браузере приведет к исключению. В целом использовать GET не рекомендуется.
Метод 3. Аналогичен предыдущему методу, но используются тип запроса POST и для разнообразия нативный сериалайзер. Запрос выглядит так:
Метод 4. Усложненный вариант предыдущего, передает сложный аргумент var arg = { ReportID: 4, Token: "Токен метода 4." }; в сериализованном виде:
{
"json": "{"ReportID":4,"Token":"Токен метода 4"}"
}
Ответ аналогичен.
Метод 5. Теперь опишем работу с датой при использовании нативного сериалайзера на примере метода, который принимает и возвращает дату. В методе 1 веб сервис вернул дату как «Date»: "/Date(1426356000000)/". Число в скобках – это количество миллисекунд, прошедших с полуночи 1 января 1970 года UTC (UNIX epoch). При этом вспомним, что у типа Date есть соответствующий конструктор new Date(milliseconds), то есть достаточно выделить это число и использовать в конструкторе даты:
var date = new Date(parseInt(data.d.replace("/Date(", "").replace(")/", ""), 10));
При этом сам веб-сервис корректно понимает нормальный формат даты в аргументе:
{
"dateTime": "2015-03-25T05:49:13.604Z"
}
Метод 6. Это нестандартный способ формирования ответа, и в некоторых случаях он может быть единственной возможностью получить необходимый результат. Как можно видеть, код сам устанавливает заголовки ответа и на бинарном уровне определяет контент.
Обратите внимание, что здесь нет корневого узла «d». Также можно использовать GET. Более того, таким способом можно вернуть и что-то отличное от «application/json;».
maxJsonLength
Для возврата тяжелых ответов необходимо увеличить значение maxJsonLength в web.config:
Кроме самих данных запроса, часто имеет смысл логировать его метаданные, такие как исходящий IP-адрес и запрошенный URL. Это позволяет определять кто, когда и через какую сеть делал запросы. Кроме этих двух главных параметров, вы можете сохранять и любые заголовки. И даже попробовать получить DNS-имя, хотя это не всегда возможно и требует время. Измените код метод getReportInfo() на следующий
private string getReportInfo() {
var request = this.Context.Request;
// var request = HttpContext.Current.Request;
Иногда есть необходимость обратиться к файлам по пути, который относителен данного расположения веб-сервиса. Для примера измените метод getReportInfo на следующий код:
В проекте создайте папку MyFiles и в ней текстовый файл Cars.txt с параметрами Build Action: None / Copy always и какой-либо первой строкой:
Таким образом при компиляции в папке bin будет автоматически создаваться папка MyFiles с указанным файлом, к которому мы будем обращаться. Также этот код демонстрирует обращение к вышестоящей папке FinReportWebService_Files, которая создается автоматически.
Итак, метод getReportInfo возвращает текст, который содержит следующую информацию:
Расположение самого веб-сервиса
Путь до вложенного файла Cars.txt
Первую строчку этого файла
Текст возможной ошибки его чтения или модификации
Путь до вышестоящей папки FinReportWebService_Files
Путь до успешно созданного в ней файла
Текст возможной ошибки создания папки или файла
После публикации веб-сервиса в IIS, где его учетной записи разрешено только читать файлы мы получим следующий текст с ошибками:
dirRoot = C:CommonFolderpublish_FinReport
fileCars = C:CommonFolderpublish_FinReportbinMyFilesCars.txt
Line 1 = Audi
Access to the path 'C:CommonFolderpublish_FinReportbinMyFilesCars.txt' is denied.
dirFiles = C:CommonFolderFinReportWebService_Files
Access to the path 'C:CommonFolderFinReportWebService_Files' is denied.
В таком случае необходимо учетной записи дать разрешение на создание файлов. Как это сделать описано в приеме 19. Пулы приложений IIS.
8. web.config
Файл web.config является главным механизмом конфигурирования любых ASP.NET приложений. С помощью него можно сделать как большое количество общих ASP.NET настроек, так и некоторые специфичные для asmx веб-сервиса. В этом примере будет рассмотрена только общая техника работы с вашими кастомными настройками. Все, что описано, применимо для любого типа ASP.NET приложения.
Здесь мы видим две кастомные настройки и стандартного вида compilation.
Обращение к значениям кастомных настроек рекомендуется производить через специальный класс, который будет возвращать их типизированные значения и осуществлять прочую низкоуровневую логику. Вот пример такого класса, добавьте его в проект:
using System;
using System.Collections.Generic;
using System.Configuration;
namespace FinReportWebService {
internal static class WebConfig {
public static int ReportType { get { return getStructureValue<int>("ReportType"); } }
public static string ReportSubject { get { return getTextValue("ReportSubject"); } }
public static string DbLogin { get { return getTextValue("DbLogin", true); } }
public static string DbPass { get { return getTextValue("DbPass", true); } }
Теперь вы можете протестировать чтение конфигурации.
web_alpha.config
Идем дальше, весьма полезной является возможность размещать часть настроек в дополнительном файле. Например, когда тестовое и боевое окружение различаются строкой подключения к БД. Это также позволяет хранить секретную часть настроек отдельно. В таком случае повторяющиеся настройки переопределяются этим файлом и также в нем могут быть объявлены новые.
Этот подход менее гибкий, так как имеет ряд серьезных отличий от предыдущего:
Все значения секции настроек определяются только в указанном стороннем файле. Никакого переопределения web.config.
Файл web_gamma.config обязательно должен существовать, в случае с web_alpha.config – не обязательно.
Его модификация приведет к рестарту пула, в случае с web_alpha.config – не приведет.
Применим к другим секциям, не только к <appSettings>
web.Debug.config
Теперь опишем еще одну полезную технику работы с web.config, которая появилась в Visual Studio 2010. Речь идет о файлах web.Debug.config и web.Release.config. Эти файлы производят трансформацию web.config при публикации в зависимости от текущего типа билда. Измените содержимое этих файлов на следующее:
Но даже не зная синтаксиса, можно понять, что в случае Debug производится замена значения настройки ReportSubject, а в случае Release происходит удаление атрибута debug=«true». Кстати, не забывайте в продакшене удалять атрибут debug=«true» или выставлять его в false, это улучшает производительность и безопасность.
Кроме того, вы можете создать и собственное преобразование web.Habr.config через меню Build -> Configuration Manager, и контекстное меню Add Config Transoforms файла web.config.
МaxRequestLength
Из общих для ASP.NET настроек хочу выделить ограничение на максимальный размер входящего запроса. Оно равно меньшему из двух параметров. Причем maxRequestLength указывается в килобайтах, а maxAllowedContentLength в байтах. Вот пример для установления ограничения в 100 мегабайт, а также 30 минут исполнения запроса.
<system.web>
<!--100 мб и 30 минут (действует только при compilation.debug = false)-->
<httpRuntime maxRequestLength="102400" executionTimeout="1800" />
Их дефолтные значения равны 4096 КБ и 30000000 байт, то есть 4 МБ и 28.61 МБ
Иерархия Web.config
На самом деле данный web.config находится в самом низу иерархии конфиг-файлов.
Верхний уровень конфигурации это файл machine.config. Его настройки действуют глобально на все веб-приложения, только если не переопределены конфиг-файлами более низкого уровня. Для пулов .net 4 он находится в папке %windir%Microsoft.NETFrameworkv4.0.30319Config или %windir%Microsoft.NETFramework64v4.0.30319Config – в зависимости от разрядности.
Следующий уровень тоже глобальный – это файл web.config, находящийся там же.
Третий уровень – уровень веб-сайта, по умолчанию это папка inetpubwwwroot, причем изначально файла web.config там даже и нет.
Четвертый – уровень веб-приложения, стандартный уровень, описанный здесь.
Существует еще и пятый уровень – когда настройки web.config применяются к подпапкам веб-приложения, но это актуально для других типов веб-приложений.
В одном веб-приложении может быть не один, а несколько asmx файлов. Очевидно, что таким образом можно разрабатывать несколько различных веб-сервисов, у которых будет некоторый общий исходный код. Однако этот прием можно использовать и для одного веб-сервиса, пример ниже показывает зачем.
В файле FinReportService.cs измените код на следующий:
using System;
using System.Configuration;
using System.Web.Services;
namespace FinReportWebService {
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[WebService(Description = "Фин. отчеты v.2", Namespace = XmlNS)]
public class FinReportService_v2 : FinReportService {
public FinReportService_v2() : base(2) {
}
}
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[WebService(Description = "CHECK: Фин. отчеты", Namespace = FinReportService.XmlNS)]
public class FinReportService_Check{
private FinReportService _service;
public FinReportService_Check(){
_service = new FinReportService();
}
<system.web>
<webServices>
<protocols>
<clear/>
<add name ="Documentation"/>
<add name ="HttpPostLocalhost"/>
</protocols>
</webServices>
</system.web>
</location>
</configuration>
Как видим, два новых asmx файла ссылаются на два новых класса. Сначала я объясню назначение класса FinReportService_v2. Его единственное функциональное отличие от базового класса – это то, что его дефолтный конструктор инициализирует поле int _version значением 2, а не 1. Таким образом, у веб-сервиса появляется клон, у которого точно такой же контракт, но имеются различия в обработке запросов. Например, этот клон может быть предназначен для тестирования или действительно представлять новую версию.
Класс FinReportService_Check имеет совершенно другое назначение. Как известно, если веб-сервис открыть в браузере с localhost, то для веб-методов с примитивными типами аргументов можно произвести запрос и увидеть ответ непосредственно в самом браузере. Это позволяет вам, админу и всем, кто имеет доступ на сервер, легко проверить, что он работает корректно.
Теперь прокомментирую конфиг файл. С помощью конструкции можно переопределить любые настройки в отношении конкретного файла. В данном случае у разных asmx файлов будет разное значение кастомной настройки «phone».
С помощью секции <protocols> мы сначала очищаем все способы взаимодействия с FinReport_CHECK.asmx, а потом добавляем просмотр и вызов с localhost. Это делает удаленное обращение к нему невозможным.
10. Замена веб-страницы
По умолчанию веб-страница asmx веб-сервиса, которую вы видите в браузере, создается веб-формой DefaultWsdlHelpGenerator.aspx, которая для х64 находится в папке %windir%Microsoft.NETFramework64v4.0.30319Config, рекомендую с ней ознакомиться.
Однако с помощью web.config легко указать собственный aspx файл. Добавьте в проект файл FinReportPage.aspx:
С помощью web.config можно легко изменить расширение asmx на любое другое. Добавьте в проект файл с расширением habr, например, FinReportClone.habr, с таким же содержимым что и у FinReport.asmx. А конфиг измените на следующий:
Отмечу, что при запуске из Visual Studio, FinReportClone.habr работать не будет, для этого надо сделать публикацию в IIS. Кстати, с помощью этой техники можно заменить веб-сервис ASMX веб-сервисом WCF с сохранением исходного URL.
12. Скрытие wsdl
По умолчанию wsdl описание любого SOAP веб-сервиса доступно путем добавления ?wsdl к его адресу URL. Это означает, что любой, кто знает и видит этот адрес, может легко вызвать его веб-методы. И если у него нет механизма авторизации, это может быть весьма небезопасно. Но даже если такой механизм есть, показывать контракт вашего веб-сервиса в общем случае нежелательно.
1 способ. Добавьте в web.config следующую настройку:
<system.web>
<webServices>
<protocols>
<remove name ="Documentation"/>
</protocols>
</webServices>
</system.web>
Это стандартный способ скрытия информации о веб-сервисе. По сути он просто запрещает GET запросы. Попытка открыть адрес в браузере будет приводить к исключению, тогда как POST запросы на веб-методы работают как обычно. Некритичный минус этого способа – это то, что браузер говорит об ошибке.
2 способ. GET запросы можно перехватить с помощью кастомного HTTP обработчика. Добавьте в проект следующий класс:
using System.Web;
namespace FinReportWebService {
public class FinReportGetHandler : IHttpHandler {
public void ProcessRequest(HttpContext context) {
string response =
@"<!doctype html>
<html>
<head>
<meta charset=utf-8>
</head>
<body>
<p>{0}</p>
</body>
</html>";
Так же, как и в предыдущем приеме, обработчик сработает только в IIS. Кстати, веб-страницу лучше не хардкодить, а читать из файла.
13. Исключения
Обработка исключений в веб-сервисе всегда имеет очень большое значение. Построение удачного механизма идентификации и возврата ошибок сэкономит много времени и нервов разработчикам с обеих сторон. Ниже приводятся два разных способа информирования клиента веб-сервиса о том, что что-то пошло не так.
<soap:Fault>
Элемент <soap:Fault> является стандартным для SOAP способом возврата ошибки. Состоит из четырех субэлементов:
<faultcode> — код ошибки
<faultstring> — человеко-читаемое описание ошибки
<faultactor> — источник ошибки
<detail> — произвольная XML структура для детальных данны
<faultcode> и <faultstring> являются обязательными, остальные два не обязательными.
Для его демонстрации измените метод getReportInfo на следующий код:
XmlQualifiedName faultCode = new XmlQualifiedName("TempError", XmlNS);
throw new SoapException("Временная ошибка", faultCode, Context.Request.Url.AbsoluteUri, rootNode);
}
Вызовите веб-метод GetReport с throwException_1(), в котором сгенерируется необработанная ошибка деления на ноль. ASP.NET DevServer (или IIS) в таком случае вернет http код «500 Internal Server Error» вместо «200 OK» и следующий контент, который для удобства отформатирован:
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>Server was unable to process request. ---> Attempted to divide by zero.</faultstring>
<detail />
</soap:Fault>
</soap:Body>
</soap:Envelope>
Необработанные исключения всегда дают soap:Server. При этом отображение стека исключения зависит от настройки customErrors:
RemoteOnly – Показывать только для localhost запросов
С помощью ручного вбрасывания SoapException вы можете сами полностью определить структуру <soap:Fault>. Вызовите метод throwException_2. Этот код приведет к следующему контенту ответа
Здесь мы изменили код ошибки, написали свой текст-пояснение, а также указали , в котором обычно указывают URL запроса.
Вообще существует четыре стандартных fault-кода, которые представлены статическими полями в классе SoapException:
Server – проблема в самом веб-сервисе
Client – клиент отправил некорректный запрос
MustUnderstand – не удалость обработать обязательный для обработки soap:Header
VersionMismatch – некорректная версия SOAP
Метод throwException_3() демонстрирует формирование собственных <detail> и кода ошибки:
Однако рекомендуется вообще не допускать в веб-методах саму возможность необработанных исключений. То есть в методе всегда должен быть глобальный try-catch, который словит любые необработанные или вручную вброшенные исключения, и вернет их клиенту в заранее оговоренном формате, который может быть представлен как <soap:Fault>, так и способом, описанным ниже.
enum
Идея этого способа заключается в использовании перечислений для сообщения клиенту об успешной или неуспешной обработке его запроса. Важно, что все значения перечислений отражаются в wsdl, поэтому они автоматически присутствуют в клиентском прокси-классе.
Добавьте в проект файл FinReport_GetReport.cs:
using System;
namespace FinReportWebService {
public class WebServiceError {
public ErrorType Type { get; set; }
public string Message { get; set; }
}
public enum ResultType {
Error,
FoundBasicData,
FoundFullData
}
public enum ErrorType {
Undefined,
DbConnection,
InvalidArgument,
Forbidden
}
public class WebServiceErrorException : Exception {
public WebServiceError Error { get; set; }
}
public class FinReport_GetReport {
private GetReportArg _arg;
private GetReportResult _result;
public GetReportResult GetReport(GetReportArg arg) {
_arg = arg;
initializeResultWithError();
[WebMethod(Description = "Получение отчета по ID")]
public GetReportResult GetReport(GetReportArg arg) {
return new FinReport_GetReport().GetReport(arg);
}
И расширьте определение класса GetReportResult
public class GetReportResult {
public ResultType ResultType { get; set; }
public WebServiceError Error { get; set; }
public FinReport Report { get; set; }
}
Итак, с помощью перечисления ResultType мы сообщаем, что произошла ошибка или что веб-метод успешно отработал с некоторым типом результата. В случае ошибки через специальную структуру мы указываем тип ошибки и некоторый текст. Также пример демонстрирует технику глобального перехвата любых исключений. Таким образом клиент всегда получает в ответ «200 OK» и структуру GetReportResult.
Хочу обратить внимание, что если http-запрос имеет некорректную структуру и его не удалось успешно десериализовать в аргумент веб-метода, то веб-метод не вызывается, а клиенту возвращается soap:Fault с описанием возникшей проблемы.
14. soap:Header
Заголовки являются опциональной частью SOAP конверта и несут различные вспомогательные по отношению к его телу данные. С помощью них можно решить вопросы аутентификации, группировки и упорядочивания конвертов, роутинга, унификации метаданных для различных веб-сервисов и так далее.
Измените код веб-сервиса на следующий:
using System;
using System.Text;
using System.Web.Services;
using System.Web.Services.Protocols;
namespace FinReportWebService{
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[WebService(Description = "Фин. отчеты", Namespace = XmlNS)]
public class FinReportService : WebService{
public const string XmlNS = "http://asmx.habrahabr.ru/";
public HabraSoapHeader HabraHeader { get; set; }
public ResultTimeSoapHeader ResultTimeHeader { get; set; }
public SoapUnknownHeader[] UnknownHeaders { get; set; }
[SoapHeader("HabraHeader")] //закомментируйте эту строку
[SoapHeader("ResultTimeHeader", Direction = SoapHeaderDirection.Out)]
[SoapHeader("UnknownHeaders")]
[WebMethod(Description = "Получение отчета по ID")]
public GetReportResult GetReport(GetReportArg arg) {
// throw new SoapHeaderException("Ошибка", SoapException.ClientFaultCode);
return new GetReportResult() {
Report = new FinReport {
ReportID = arg.ReportID,
Date = new DateTime(2015, 03, 15),
Info = getReportInfo(arg.ReportID)
}
};
}
public class HabraSoapHeader : SoapHeader {
public string Login { get; set; }
public string Password { get; set; }
}
public class ResultTimeSoapHeader : SoapHeader {
public DateTime ResultTime { get; set; }
}
public class FinReport {
public int ReportID { get; set; }
public DateTime Date { get; set; }
public string Info { get; set; }
}
public class GetReportArg {
public int ReportID { get; set; }
}
public class GetReportResult {
public FinReport Report { get; set; }
}
}
Затем перезапустите веб-сервер и перегенерируйте в клиентском проекте прокси-класс, так как объявленные заголовки отразятся в wsdl. Метод вызова сделайте следующим:
private void webMethodTest_GetReport() {
var service = GetFinReportService();
var arg = new GetReportArg();
arg.ReportID = 45;
Итак, чтобы добавить в asmx веб-сервис заголовок soap, нужно сделать три вещи:
Объявить унаследованный от SoapHeader класс заголовка
Объявить свойство для этого класса
Применить к веб-методу атрибут [SoapHeader], указывающий на это свойство
В этом примере веб-сервисом объявлены два заголовка – HabraSoapHeader и ResultTimeSoapHeader, причем первый ожидается в запросе, а второй возвращается в ответе.
Обратите внимание на атрибут soap:mustUnderstand=«1», который означает, что веб-сервис обязательно должен понять этот заголовок. У класса SoapHeader есть свойство public bool DidUnderstand { set; get; }, которое определяет, был ли понят заголовок. Для известных заголовков оно изначально равно true, тогда как для неизвестных – false. И в случае, когда заголовок имел mustUnderstand=«1», а DidUnderstand оказался false, веб-сервер возвратит soap:Fault. Закомментируйте атрибут [SoapHeader(«HabraHeader»)], тогда тот же запрос приведет к:
Теперь этот заголовок попадает в массив public SoapUnknownHeader[] UnknownHeaders { get; set; }, и чтобы его успешно обработать раскомментируйте код внутри foreach.
Работать с заголовками также можно с помощью расширений SoapExtension. Это позволяет отделить логику заголовков от логики веб-методов. Они могут сами обрабатывать входящие заголовки или добавлять их в ответ. При этом они могут как обмениваться через интерфейс данными с классом веб-сервиса (или прокси-классом), так и быть совершенно независимы от них.
Также отмечу, что для исключений, связанных с заголовками, существует специальный класс SoapHeaderException, отнаследованный от SoapException. Интересно, что при его вбрасывании на сервере, клиент также получит именно этот тип исключения, а не SoapException, хотя они оба передаются как soap:Fault. Попробуйте понять как это получается.
15. Кэширование
Для asmx веб-сервисов существует два стандартных способов кэширования. Первый заключается в установке свойства CacheDuration в атрибуте веб-метода, а второй в использовании HttpContext.Cache. Оба способа продемонстрированы в следующем коде:
[WebMethod(Description = "Получение отчета по ID", CacheDuration = 5)]
public GetReportResult GetReport(GetReportArg arg){
return new GetReportResult(){
Report = new FinReport{
ReportID = arg.ReportID,
Date = DateTime.Now,
Info = getReportInfo(arg.ReportID)
}
};
}
Атрибутный способ кэширования держит в памяти в течение заданного количества секунд все входящие и исходящие soap сообщения. И в случае полного совпадения входящего сообщения с находящимся в памяти, веб-метод не исполняется, а возвращается закэшированный результат.
Второй способ более гибкий и функциональный, в нем мы сами определяем объекты и ключи кэширования и другие его особенности.
В этом примере если клиент будет посылать запрос раз в секунду, то поле Date будет меняться раз в 5 секунд, а поле Info – раз в 10 секунд.
Напомню, что кэширование должно применяться обдуманно, так как ведет к большому потреблению памяти и риску возврата неактуальных данных.
16. SoapExtension
SoapExtension – это мощная техника, которая позволяет вручную видоизменять запросы и ответы, причем как со стороны веб-сервиса, так и со стороны клиента. К сожалению, полноценное ее описание с различными примерами тянет на отдельную статью, поэтому я опишу ее только в общем и дам ссылки на материалы.
Возможности SoapExtension:
Чтение контента запроса или ответа (просмотр soap конвертов в виде MemoryStream)
Модификация контента запроса или ответа
Чтение и обработка заголовков (soap header)
1 способ привязки: к веб-методу (веб-сервиса или клиента) через кастомный атрибут
2 способ привязки: к веб-сервису (или клиенту) с помощью web.config (app.config) без перекомпиляции!
Взаимодействие с самим классом веб-сервиса или прокси-классом
Дебаггинг веб-приложений в Visual Studio осложняется отсутствием 64-х разрядного режима во встроенном веб-сервере. Если вы укажите Platform target: x64, то запуск приложения приведет к ошибке «Не удалось загрузить файл или сборку «FinReportWebService» либо одну из их зависимостей. Была сделана попытка загрузить программу, имеющую неверный формат».
Есть три разных способа решения данной проблемы.
1 способ. В настройках веб-проекта выбрать «Use Local IIS Web server», то есть использовать локальный IIS
2 способ. Опубликовать веб-приложение на локальный IIS и подключиться к его процессу Debug -> Attach to Process.
Распаковав, откройте и скомпилируйте trunksrcCassiniDev.sln, возможно придется обновить референсы из trunksrcpackages. Теперь в свойствах солюшена измените платформу проектов на x64 и перекомпилируйте:
Стандартный веб-сервер находится в папке C:Program Files (x86)Common Filesmicrosoft sharedDevServer. Сделав его копию, замените его из папки trunksrcCassiniDevbinx64Debug
18. Деплой (публикация)
С помощью команды Publish и метода File System Visual Studio создает в указанной папке набор файлов, который необходимо предоставить для IIS
wwwroot
В простейшем случае эта папка веб-сервиса размещается в специальной папке C:inetpubwwwroot, что позволяет легко сконвертировать ее в веб-приложение. Напомню, что IIS можно вызвать командой inetmgr.
HTTP Error 404.3 — Not Found
Если возникла ошибка HTTP Error 404.3 — Not Found, то необходимо добавить следующие компоненты IIS
И выполнить команду
%windir%Microsoft.NETFramework64v4.0.30319aspnet_regiis.exe –ir
Кастомная папка
Веб-сервис также можно расположить в кастомной папке
Но в таком случае могут возникнуть ошибки из-за отсутствия у IIS разрешений на ее чтение:
HTTP Error 500.19 — Internal Server Error.
The requested page cannot be accessed because the related configuration data for the page is invalid.
или
Access is denied.
Error message 401.3: You do not have permission to view this directory or page using the credentials you supplied
Как их исправить описано в следующем приеме.
19. Пулы приложений IIS
Веб-приложения рекомендуется держать каждое в своем специально созданном пуле. Это позволяет их изолировать и индивидуально настраивать. Создайте новый пул FinReportPool и переключите на него веб-сервис.
Настройка разрешений
Учетная запись пула с удостоверением ApplicationPoolIdentity имеет системное имя вида: IIS AppPool<имя пула>
Например, IIS AppPoolFinReportPool, или IIS AppPoolDefaultAppPool
И в случае кастомной папки в ее свойствах безопасности необходимо будет добавить учетную запись с правами чтения, исполнения и просмотра содержимого
А также переключить удостоверение анонимного пользователя
Также отмечу, что вместо учетной записи можно указать группу <Имя компьютера>IIS_IUSRS
В дополнительных параметрах пула много интересных настроек.
В частности, ошибка конфликта разрядности x86/x64
Не удалось загрузить файл или сборку «FinReportWebService» либо одну из их зависимостей. Была сделана попытка загрузить программу, имеющую неверный формат.
Could not load file or assembly 'FinReportWebService' or one of its dependencies. An attempt was made to load a program with an incorrect format.
может возникнуть из-за неверного значения параметра Enable 32-Bit Applications.
А с помощью параметра Identity можно сменить учетную запись под которой работает веб-приложение.
AppCmd.exe
Каждый пул когда работает порождает отдельный процесс w3wp.exe. Увидеть соответствие между ними можно с помощью Диспетчера задач или следующего батника:
AppcmdList.bat
%systemroot%System32inetsrvappcmd list wp
%systemroot%System32inetsrvappcmd list sites
%systemroot%System32inetsrvappcmd list app
%systemroot%System32inetsrvappcmd list appPools
pause
Если вы не видите процесса, то чтобы его запустить, достаточно открыть в браузере страницу веб-приложения. Более подробно об утилите AppCmd.exe можно почитать здесь.
20. Инструменты разработки
Если вы только начинаете работать с веб-сервисами, то вам понадобятся дополнительные инструменты, которые значительно облегчат процесс разработки.
Fiddler. Универсальный HTTP-дебаггер. Бесплатный и очень функциональный. Описан на Хабре тут. Must have.
SoapUI. Мощный инструмент анализа и тестирования веб-сервисов. Существует в различных редакциях, есть бесплатная.
Oxygen XML Editor. Очень удобный инструмент работы с XML. Умеет работать с WSDL и SOAP.