Pull to refresh

Приемы разработки ASMX веб-сервисов

Reading time 99 min
Views 119K
В этой статье я расскажу о различных приемах разработки SOAP веб-сервисов по технологии ASMX, а также об этой технологии в целом. Кроме SOAP, также будет рассмотрена реализация AJAX. Статья будет полезна как тем, кто уже знаком с ней, так и тем, кто только собирается создать свой первый веб-сервис.



Содержание


Историческая справка
ASMX и WCF
Введение
  1. Простейшая конструкция
  2. Рекомендуемая конструкция
  3. Прокси-класс с помощью wsdl.exe
  4. Серверный класс по данному wsdl
  5. ajax
  6. Метаданные запроса
  7. Обращение к файлам
  8. web.config
  9. Множественные asmx файлы
  10. Замена веб-страницы
  11. Замена расширения
  12. Скрытие wsdl
  13. Исключения
  14. soap:Header
  15. Кэширование
  16. SoapExtension
  17. Дебаггинг x64 в Visual Studio
  18. Деплой (публикация)
  19. Пулы приложений IIS
  20. Инструменты разработки


Историческая справка


С самого начала корпорация 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:
  • Легкость в разработке
  • Легкость в изучении
  • Нет «ада» конфигурирования

Плюсы WCF:
  • Очень разнообразные и гибкие возможности транспорта
  • Актуальная и развивающаяся технология
  • Различные варианты хостинга
  • Возможность реализации большого множества стандартов 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, чтобы это был одиночный файл.

FinReport.asmx
<%@ Class="FinReportWebService.FinReportService" %>

FinReportService.cs
using System;
using System.Web.Services;
 
namespace FinReportWebService{
 
    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 { getset; }
        public DateTime Date { getset; }
        public string Info { getset; }
    }
}

Нажмите F5 для запуска веб-сервера и откройте в браузере FinReport.asmx, вы должны увидеть



Готово. Теперь разберем по порядку. Веб-сервис представлен одним обычным классом с одной лишь обязательной особенностью – некоторые его методы помечены специальным атрибутом [WebMethod]. Такие методы класса становятся веб-методами веб-сервиса с соответствующей сигнатурой вызова. Этот класс должен обладать конструктором по умолчанию. При каждом новом запросе IIS его инстанциирует дефолтным конструктором и вызывает соответствующий метод.

Вторая обязательная часть минимальной конструкции – это файл с расширением asmx, внутри которого необходимо указать этот класс.

Интересно сравнить этот вручную созданный asmx файл с тем, который создаст Visual Studio. Предположим, что мы хотим сделать еще один веб-сервис, который возвращает курс обмена валют. Добавьте через меню Add New Item файл ExchangeRate.asmx с типом Web Service.



Нажав один-два раза на F7, можно увидеть следующее:
<%@ WebService Language="C#" CodeBehind="ExchangeRate.asmx.cs"  Class="FinReportWebService.ExchangeRate" %>

Оператор 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)
                }
            };
        }
 
        private string getReportInfo(int reportID){
            return "ReportID = " + reportID;
        }
    }
 
 
//    [Serializable]
//    [XmlType(Namespace = FinReportService.XmlNS)]
    public class FinReport {
        public int ReportID { getset; }
        public DateTime Date { getset; }
        public string Info { getset; }
    }
 
    public class GetReportIdArrayArg {
        public DateTime DateBegin { getset; }
        public DateTime DateEnd { getset; }
    }
 
    public class GetReportIdArrayResult {
        public int[] ReportIdArray { getset; }
    }
 
    public class GetReportArg {
        public int ReportID { getset; }
    }
 
    public class GetReportResult {
        public FinReport Report { getset; }
    }
}

Разберем изменения.
Атрибут [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 SDKs\Windows, более того, она там представлена в разных версиях, в зависимости от версии .net, разрядности, версии windows и visual studio.



Примеры использования
wsdl http://192.168.1.101:8080/SomeDir/SomeService?wsdl
wsdl HabraService.wsdl


Давайте сделаем клиента для FinReportWebService. В текущем или новом солюшене создайте новый Windows Forms проект FinReportWebServiceClient. Добавьте в нем папку ProxyClass, скопируйте в нее утилиту wsdl.exe и создайте в ней батник GenProxyClass.bat:
wsdl /n:FinReportWebServiceClient.ProxyClass http://localhost:3500/FinReport.asmx?wsdl
pause

С помощью аргумента /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 схемы, все эти схемы необходимо перечислить в команде:
wsdl /n:MyNamespace HabraService.wsdl Data.xsd Common.xsd Schema.xsd

Однако кроме ручного создания прокси-класса Visual Studio позволяет его создать автоматически. Пункт «Add Service Reference» позволяет создать прокси-класс по технологии WCF, и там же в «Advanced» есть кнопка «Add Web Reference», которая создает его уже по технологии ASMX.

4. Серверный класс по данному wsdl


Как известно, wsdl описание веб-сервиса в технологии ASMX генерируется автоматически. Однако иногда возникает обратная задача: по данному wsdl файлу разработать соответствующий ему веб-сервис. Решается она с помощью той же утилиты wsdl.exe. Она может создать необходимый скелет из классов и вам останется только реализовать программную логику веб-методов.

Для примера возьмем wsdl нашего веб-сервиса. Сохраните его из браузера как файл FinReport.wsdl либо скопируйте отсюда:
FinReport.wsdl
<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://asmx.habrahabr.ru/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" targetNamespace="http://asmx.habrahabr.ru/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
  <wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">Фин. отчеты</wsdl:documentation>
  <wsdl:types>
    <s:schema elementFormDefault="qualified" targetNamespace="http://asmx.habrahabr.ru/">
      <s:element name="GetReportIdArray">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="arg" type="tns:GetReportIdArrayArg" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:complexType name="GetReportIdArrayArg">
        <s:sequence>
          <s:element minOccurs="1" maxOccurs="1" name="DateBegin" type="s:dateTime" />
          <s:element minOccurs="1" maxOccurs="1" name="DateEnd" type="s:dateTime" />
        </s:sequence>
      </s:complexType>
      <s:element name="GetReportIdArrayResponse">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="GetReportIdArrayResult" type="tns:GetReportIdArrayResult" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:complexType name="GetReportIdArrayResult">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="ReportIdArray" type="tns:ArrayOfInt" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="ArrayOfInt">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="unbounded" name="int" type="s:int" />
        </s:sequence>
      </s:complexType>
      <s:element name="GetReport">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="arg" type="tns:GetReportArg" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:complexType name="GetReportArg">
        <s:sequence>
          <s:element minOccurs="1" maxOccurs="1" name="ReportID" type="s:int" />
        </s:sequence>
      </s:complexType>
      <s:element name="GetReportResponse">
        <s:complexType>
          <s:sequence>
            <s:element minOccurs="0" maxOccurs="1" name="GetReportResult" type="tns:GetReportResult" />
          </s:sequence>
        </s:complexType>
      </s:element>
      <s:complexType name="GetReportResult">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="Report" type="tns:FinReport" />
        </s:sequence>
      </s:complexType>
      <s:complexType name="FinReport">
        <s:sequence>
          <s:element minOccurs="1" maxOccurs="1" name="ReportID" type="s:int" />
          <s:element minOccurs="1" maxOccurs="1" name="Date" type="s:dateTime" />
          <s:element minOccurs="0" maxOccurs="1" name="Info" type="s:string" />
        </s:sequence>
      </s:complexType>
    </s:schema>
  </wsdl:types>
  <wsdl:message name="GetReportIdArraySoapIn">
    <wsdl:part name="parameters" element="tns:GetReportIdArray" />
  </wsdl:message>
  <wsdl:message name="GetReportIdArraySoapOut">
    <wsdl:part name="parameters" element="tns:GetReportIdArrayResponse" />
  </wsdl:message>
  <wsdl:message name="GetReportSoapIn">
    <wsdl:part name="parameters" element="tns:GetReport" />
  </wsdl:message>
  <wsdl:message name="GetReportSoapOut">
    <wsdl:part name="parameters" element="tns:GetReportResponse" />
  </wsdl:message>
  <wsdl:portType name="FinReportServiceSoap">
    <wsdl:operation name="GetReportIdArray">
      <wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">Получение списка ID отчетов по периоду</wsdl:documentation>
      <wsdl:input message="tns:GetReportIdArraySoapIn" />
      <wsdl:output message="tns:GetReportIdArraySoapOut" />
    </wsdl:operation>
    <wsdl:operation name="GetReport">
      <wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">Получение отчета по ID</wsdl:documentation>
      <wsdl:input message="tns:GetReportSoapIn" />
      <wsdl:output message="tns:GetReportSoapOut" />
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="FinReportServiceSoap" type="tns:FinReportServiceSoap">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" />
    <wsdl:operation name="GetReportIdArray">
      <soap:operation soapAction="http://asmx.habrahabr.ru/GetReportIdArray" style="document" />
      <wsdl:input>
        <soap:body use="literal" />
      </wsdl:input>
      <wsdl:output>
        <soap:body use="literal" />
      </wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="GetReport">
      <soap:operation soapAction="http://asmx.habrahabr.ru/GetReport" style="document" />
      <wsdl:input>
        <soap:body use="literal" />
      </wsdl:input>
      <wsdl:output>
        <soap:body use="literal" />
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:binding name="FinReportServiceSoap12" type="tns:FinReportServiceSoap">
    <soap12:binding transport="http://schemas.xmlsoap.org/soap/http" />
    <wsdl:operation name="GetReportIdArray">
      <soap12:operation soapAction="http://asmx.habrahabr.ru/GetReportIdArray" style="document" />
      <wsdl:input>
        <soap12:body use="literal" />
      </wsdl:input>
      <wsdl:output>
        <soap12:body use="literal" />
      </wsdl:output>
    </wsdl:operation>
    <wsdl:operation name="GetReport">
      <soap12:operation soapAction="http://asmx.habrahabr.ru/GetReport" style="document" />
      <wsdl:input>
        <soap12:body use="literal" />
      </wsdl:input>
      <wsdl:output>
        <soap12:body use="literal" />
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="FinReportService">
    <wsdl:documentation xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">Фин. отчеты</wsdl:documentation>
    <wsdl:port name="FinReportServiceSoap" binding="tns:FinReportServiceSoap">
      <soap:address location="http://localhost:3500/FinReport.asmx" />
    </wsdl:port>
    <wsdl:port name="FinReportServiceSoap12" binding="tns:FinReportServiceSoap12">
      <soap12:address location="http://localhost:3500/FinReport.asmx" />
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

Создайте в солюшене новый пустой web-проект с именем FinReportWebServiceByWsdl. В него добавьте папку ServerClass, в которую скопируйте файлы FinReport.wsdl и wsdl.exe. Создайте в ней батник GenServerClass.bat:
wsdl /server /n:FinReportWebServiceByWsdl.ServerClass FinReport.wsdl
pause

Запустив его, вы должны получить файл 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 {
            
            [WebMethod]
            [SoapDocumentMethod("http://asmx.habrahabr.ru/GetReportIdArray"
                RequestNamespace = "http://asmx.habrahabr.ru/",
                ResponseNamespace = "http://asmx.habrahabr.ru/"
                Use = SoapBindingUse.Literal, 
                ParameterStyle = SoapParameterStyle.Wrapped)]
 
            public GetReportIdArrayResult GetReportIdArray(GetReportIdArrayArg arg) {
                return new GetReportIdArrayResult();
            }
            
 
            [WebMethod]
            [SoapDocumentMethod("http://asmx.habrahabr.ru/GetReport"
                RequestNamespace = "http://asmx.habrahabr.ru/"
                ResponseNamespace = "http://asmx.habrahabr.ru/"
                Use = SoapBindingUse.Literal, 
                ParameterStyle = SoapParameterStyle.Wrapped)]
            
            public GetReportResult GetReport(GetReportArg arg) {
                return new GetReportResult() {
                    Report = new FinReport {
                        ReportID = arg.ReportID,
                        Date = new DateTime(2015, 03, 15),
                        Info = "ByWSDL"
                    }
                };
            }
        }
}

В этом коде утилита явно описала с помощью атрибутов те параметры веб-сервиса, которые неявно определялись по умолчанию. Остается только добавить файл FinReportByWsdl.asmx, который будет указывать на этот новый класс:
<%@ Class="FinReportWebServiceByWsdl.FinReportService" %>


5. ajax


ASMX веб-сервис может принимать и возвращать данные в формате JSON, что позволяет реализовать технику ajax. Для работы примера в вашем веб-проекте должны быть следующие три файла:

FinReport.asmx – такой же, что и в первых примерах, всего 1 строка
<%@ Class="FinReportWebService.FinReportService" %>

FinReportService.cs – код меняется на следующий
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 GetReportResult Method_1_POST_Objects(GetReportArg arg) {
            return getFinReportResult(arg.ReportID, "Method_1_POST_Objects");
        }
 
 
        [ScriptMethod(ResponseFormat = ResponseFormat.Json, UseHttpGet = true)]
        [WebMethod]
        public string Method_2_GET(int id){
            var result = getFinReportResult(id, "Method_2_GET");
            string text =  JsonConvert.SerializeObject(result);
            return text;
        }
 
 
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        [WebMethod]
        public string Method_3_POST(int id) {
            var result = getFinReportResult(id, "Method_3_POST");
            JavaScriptSerializer js = new JavaScriptSerializer();
            return js.Serialize(result);
        }
 
 
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        [WebMethod]
        public string Method_4_POST_ComplexArg(string json) {
            var arg = JsonConvert.DeserializeObject<GetReportArg>(json);
            var result = getFinReportResult(arg.ReportID, arg.Token + Получен.");
            return JsonConvert.SerializeObject(result);
        }
 
 
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
        [WebMethod]
        public DateTime Method_5_TransformDate(DateTime dateTime){
            return dateTime.AddYears(-3).AddDays(-5).AddHours(-2).AddMinutes(6);
        }
 
 
        [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);
 
            Context.Response.Clear();
            Context.Response.ContentType = "application/json; charset=utf-8";
            Context.Response.AddHeader("content-length", data.Length.ToString());
            Context.Response.BinaryWrite(data);
            Context.Response.Flush();
        }
 
 
        private GetReportResult getFinReportResult(int id, string info) {
            return new GetReportResult() {
                Report = new FinReport() {
                    ReportID = id,
                    Info = info,
                    Date = new DateTime(2015, 03, 15),
                }
            };
        }
    }
 
 
    public class FinReport {
        public DateTime Date { getset; }
        public string Info { getset; }
        public int ReportID { getset; }
    }
 
    public class GetReportArg {
        public int ReportID { getset; }
        public string Token { getset; }
    }
 
    public class GetReportResult {
        public FinReport Report { getset; }
    }
}

Page.htm – собственно веб-страница
<!doctype html>
<html>
<head>
 
<meta charset=utf-8>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
                
<script>
    $(document).ready(function () {
        $("#btn1").click(function () {
            var arg = { arg: { ReportID: 1} };
 
            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: "/FinReport.asmx/Method_1_POST_Objects",
                data: JSON.stringify(arg),
                dataType: "json",
                success: function (data, status) {
                    $("#div1").html(data.d.Report.Info);
                },
                error: function (request, status, error) { alert("Error"); }
            });
        });
 
        $("#btn2").click(function () {
            $.ajax({
                type: "GET",
                contentType: "application/json; charset=utf-8",
                url: "/FinReport.asmx/Method_2_GET",
                data: { id: 2 },
                dataType: "json",
                success: function (data, status) {
                    $("#div2").html(JSON.parse(data.d).Report.Info);
                },
                error: function (request, status, error) { alert("Error"); }
            });
        });
 
        $("#btn3").click(function () {
            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: "/FinReport.asmx/Method_3_POST",
                data: '{"id":3}',
                dataType: "json",
                success: function (data, status) {
                    $("#div3").html(JSON.parse(data.d).Report.Info);
                },
                error: function (request, status, error) { alert("Error"); }
            });
        });
 
 
        $("#btn4").click(function () {
            var arg = { ReportID: 4, Token: "Токен метода 4." };
            var argObj = { json: JSON.stringify(arg) };
 
            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: "/FinReport.asmx/Method_4_POST_ComplexArg",
                data: JSON.stringify(argObj),
                dataType: "json",
                success: function (data, status) {
                    $("#div4").html(JSON.parse(data.d).Report.Info);
                },
                error: function (request, status, error) { alert("Error"); }
            });
        });
 
        $("#btn5").click(function () {
            var now = new Date();
            var arg = { dateTime: now };
 
            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: "/FinReport.asmx/Method_5_TransformDate",
                data: JSON.stringify(arg),
                dataType: "json",
                success: function (data, status) {
                    var date = new Date(parseInt(data.d.replace("/Date(""").replace(")/"""), 10));
                    $("#div5").html(date.toString());
                },
                error: function (request, status, error) { alert("Error"); }
            });
        });
 
        $("#btn6").click(function () {
            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: "/FinReport.asmx/Method_6_POST_NonStandard",
                data: '{"id":6}',
                dataType: "json",
                success: function (data, status) {
                    $("#div6").html(data.Report.Info);
                },
                error: function (request, status, error) { alert("Error"); }
            });
        });
 
 
    });
</script>
                
</head>
<body>
    
    <div id="div1">Div 1</div>
    <div id="div2">Div 2</div>
    <div id="div3">Div 3</div>
    <div id="div4">Div 4</div>
    <div id="div5">Div 5</div>
    <div id="div6">Div 6</div>
    <br/>
    <button id="btn1">Method 1</button>
    <br/>
    <button id="btn2">Method 2</button>
    <br/>
    <button id="btn3">Method 3</button>
    <br/>
    <button id="btn4">Method 4</button>
    <br/>
    <button id="btn5">Method 5</button>
    <br/>
    <button id="btn6">Method 6</button>
 
</body>
</html>

Также в примере используется библиотека Json.NET aka Newtonsoft.Json.

Чтобы веб-сервис мог работать с JSON, нужно применить 2 новых атрибута:
[ScriptService] – без свойств, и [ScriptMethod], в котором свойство ResponseFormat отвечает за формат ответа — JSON или XML, а UseHttpGet – за тип запроса – GET или POST.

В этом веб-сервисе 6 методов, которые демонстрируют различные способы реализации ajax.

Метод 1. Как и во 2 примере он принимает и возвращает объекты классов GetReportArg и GetReportResult. В отформатированном виде запрос и ответ выглядят следующим образом:
{
   "arg": {
      "ReportID": 1
   }
}

{
   "d": {
      "__type": "FinReportWebService.GetReportResult",
      "Report": {
         "ReportID": 1,
         "Date": "/Date(1426356000000)/",
         "Info": "Method_1_POST_Objects"
      }
   }
}
Если с аргументом все понятно, то ответ нужно прокомментировать. Все 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.

Метод возвращает ответ вида
{
   "d": "{\"Report\":{\"ReportID\":2,\"Date\":\"2015-03-15T00:00:00\",\"Info\":\"Method_2_GET\"}}"
}
Как видим, нужно произвести парсинг JSON.parse(data.d), чтобы получить исходный объект вида:
{
   "Report": {
      "ReportID": 2,
      "Date": "2015-03-15T00:00:00",
      "Info": "Method_2_GET"
   }
}
Обратите внимание, что дата благодаря библиотеке Json.NET тут представлена стандартно. Также нужно отметить обязательность заголовка contentType: «application/json; charset=utf-8», несмотря на то, что это GET. Сделано это для защиты от CSRF. По этой причине попытка открыть URL запроса в браузере приведет к исключению. В целом использовать GET не рекомендуется.

Метод 3. Аналогичен предыдущему методу, но используются тип запроса POST и для разнообразия нативный сериалайзер. Запрос выглядит так:
{
   "id": 3
}
Ответ отличается неудобным форматом даты:
{
   "d": "{\"Report\":{\"ReportID\":3,\"Date\":\"\\/Date(1426356000000)\\/\",\"Info\":\"Method_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. Это нестандартный способ формирования ответа, и в некоторых случаях он может быть единственной возможностью получить необходимый результат. Как можно видеть, код сам устанавливает заголовки ответа и на бинарном уровне определяет контент.
{
   "Report": {
      "Date": "2015-03-15T00:00:00",
      "Info": "Method_6_GET_NonStandard, Мой текст",
      "ReportID": 6
   }
}
Обратите внимание, что здесь нет корневого узла «d». Также можно использовать GET. Более того, таким способом можно вернуть и что-то отличное от «application/json;».

maxJsonLength

Для возврата тяжелых ответов необходимо увеличить значение maxJsonLength в web.config:
<?xml version="1.0"?>
<configuration>
  
    <system.web>
        <compilation debug="true" targetFramework="4.0" />
    </system.web>
 
  <system.web.extensions>
    <scripting>
      <webServices>
        <jsonSerialization maxJsonLength="1073741824"/>
      </webServices>
    </scripting>
  </system.web.extensions>
 
</configuration>


6. Метаданные запроса


Кроме самих данных запроса, часто имеет смысл логировать его метаданные, такие как исходящий IP-адрес и запрошенный URL. Это позволяет определять кто, когда и через какую сеть делал запросы. Кроме этих двух главных параметров, вы можете сохранять и любые заголовки. И даже попробовать получить DNS-имя, хотя это не всегда возможно и требует время. Измените код метод getReportInfo() на следующий
private string getReportInfo() {
    var request = this.Context.Request;
//    var request = HttpContext.Current.Request;
 
    StringBuilder sb = new StringBuilder();
    sb.Append("IP = ").AppendLine(request.UserHostAddress);
    sb.Append("URL = ").AppendLine(request.Url.OriginalString);
    sb.Append("Header 'Connection' = ").AppendLine(request.Headers["Connection"]);
 
    DateTime dnsDate = DateTime.Now;
    TimeSpan dnsSpan;
 
    try {
//        throw new Exception("Закомментируй меня.");
        var entry = Dns.GetHostEntry(request.UserHostAddress);
        dnsSpan = DateTime.Now.Subtract(dnsDate);
        sb.Append("HostName = ").AppendLine(entry.HostName);
 
    } catch (Exception ex) {
        dnsSpan = DateTime.Now.Subtract(dnsDate);
        sb.AppendLine(ex.Message);
    }
 
    sb.Append("dnsSpan = ").AppendLine(dnsSpan.ToString());
    return sb.ToString();
}


7. Обращение к файлам


Иногда есть необходимость обратиться к файлам по пути, который относителен данного расположения веб-сервиса. Для примера измените метод getReportInfo на следующий код:
private string getReportInfo(int reportID) {
    string dirRoot = HttpContext.Current.Server.MapPath("~");
 
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("dirRoot = " + dirRoot);
  
    string fileCars = HttpContext.Current.Server.MapPath("~/bin/MyFiles/Cars.txt");
    sb.AppendLine("fileCars = " + fileCars);
 
    try {
        sb.AppendLine("Line 1 = " + File.ReadAllLines(fileCars)[0]);
        File.AppendAllText(fileCars, Environment.NewLine + DateTime.Now + " ReportID = " + reportID);
 
    } catch (Exception ex) {
        sb.AppendLine(ex.Message);
    }
 
    string dirFiles = Path.Combine((new DirectoryInfo(dirRoot)).Parent.FullName, "FinReportWebService_Files");
    sb.AppendLine("dirFiles = " + dirFiles);
 
    try{
        Directory.CreateDirectory(dirFiles);
        string newFile = Path.Combine(dirFiles, Guid.NewGuid() + ".txt");
        File.WriteAllText(newFile, "ReportID = " + reportID);
        sb.AppendLine(newFile);
 
    } catch (Exception ex) {
        sb.AppendLine(ex.Message);
    }
 
 
    return sb.ToString();
}
В проекте создайте папку MyFiles и в ней текстовый файл Cars.txt с параметрами Build Action: None / Copy always и какой-либо первой строкой:



Таким образом при компиляции в папке bin будет автоматически создаваться папка MyFiles с указанным файлом, к которому мы будем обращаться. Также этот код демонстрирует обращение к вышестоящей папке FinReportWebService_Files, которая создается автоматически.

Итак, метод getReportInfo возвращает текст, который содержит следующую информацию:

Расположение самого веб-сервиса
Путь до вложенного файла Cars.txt
Первую строчку этого файла
Текст возможной ошибки его чтения или модификации
Путь до вышестоящей папки FinReportWebService_Files
Путь до успешно созданного в ней файла
Текст возможной ошибки создания папки или файла

После публикации веб-сервиса в IIS, где его учетной записи разрешено только читать файлы мы получим следующий текст с ошибками:
dirRoot = C:\CommonFolder\publish_FinReport
fileCars = C:\CommonFolder\publish_FinReport\bin\MyFiles\Cars.txt
Line 1 = Audi
Access to the path 'C:\CommonFolder\publish_FinReport\bin\MyFiles\Cars.txt' is denied.
dirFiles = C:\CommonFolder\FinReportWebService_Files
Access to the path 'C:\CommonFolder\FinReportWebService_Files' is denied.
В таком случае необходимо учетной записи дать разрешение на создание файлов. Как это сделать описано в приеме 19. Пулы приложений IIS.

8. web.config


Файл web.config является главным механизмом конфигурирования любых ASP.NET приложений. С помощью него можно сделать как большое количество общих ASP.NET настроек, так и некоторые специфичные для asmx веб-сервиса. В этом примере будет рассмотрена только общая техника работы с вашими кастомными настройками. Все, что описано, применимо для любого типа ASP.NET приложения.

web.config

Измените содержимое web.config на следующее:
<?xml version="1.0"?>
<configuration>
 
  <appSettings>
    <add key="ReportType" value="8"/>
    <add key="ReportSubject" value="Средний бизнес"/>
  </appSettings>
 
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
  </system.web>
 
</configuration>
Здесь мы видим две кастомные настройки и стандартного вида 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); } }
 
        //==========================================================
 
        private static string getTextValue(string name, bool getDefaultOnNotFound = false) {
            string value = ConfigurationManager.AppSettings[name];
 
            if (value == null && !getDefaultOnNotFound) {
                throw new KeyNotFoundException("В файле web.config не найдена настройка '" + name + "'");
            }
 
            return value;
        }
 
 
        private static T getStructureValue<T>(string name, bool getDefaultOnNotFound = falsewhere T : struct {
            string textValue = getTextValue(name, getDefaultOnNotFound);
 
            if (textValue == null) {
                return default(T);
            }
 
            try {
                T value = (T) Convert.ChangeType(textValue, typeof (T));
                return value;
 
            } catch (Exception ex) {
                string message = "В файле web.config настройку '{0}' со значением '{1}' не удалось распарсить как '{2}'";
                message = string.Format(message, name, textValue, typeof (T).Name);
                throw new InvalidCastException(message, ex);
            }
        }
 
    }
}

И наконец, в файле FinReportService.cs метод getReportInfo замените на этот код:
private static string getReportInfo(int reportID) {
    StringBuilder sb = new StringBuilder();
    try {
        sb.Append("ReportType = ").AppendLine(WebConfig.ReportType.ToString());
        sb.Append("ReportSubject = ").AppendLine(WebConfig.ReportSubject);
        sb.Append("DbLogin = ").AppendLine(WebConfig.DbLogin);
        sb.Append("DbPass = ").AppendLine(WebConfig.DbPass);
 
    } catch (Exception ex) {
        sb.AppendLine(ex.Message);
    }
            
    return sb.ToString();
}
Теперь вы можете протестировать чтение конфигурации.

web_alpha.config

Идем дальше, весьма полезной является возможность размещать часть настроек в дополнительном файле. Например, когда тестовое и боевое окружение различаются строкой подключения к БД. Это также позволяет хранить секретную часть настроек отдельно. В таком случае повторяющиеся настройки переопределяются этим файлом и также в нем могут быть объявлены новые.

Добавьте в проект файл web_alpha.config:
<?xml version="1.0"?>
<appSettings>
  <add key="ReportSubject" value="Малый бизнес"/>
  <add key="DbLogin" value="reader"/>
  <add key="DbPass" value="uYE4_wn7xc5Sp"/>
</appSettings>
И измените сам web.config на:
<?xml version="1.0"?>
<configuration>
 
  <appSettings file ="web_alpha.config">
    <add key="ReportType" value="8"/>
    <add key="ReportSubject" value="Средний бизнес"/>
  </appSettings>
 
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
  </system.web>
 
</configuration>

web_beta.config

Сторонний файл может располагаться и в вышестоящей папке. Создайте в родительской папке новый файл web_beta.config:
<?xml version="1.0"?>
<appSettings>
  <add key="ReportSubject" value="Крупный бизнес"/>
  <add key="DbLogin" value="admin"/>
  <add key="DbPass" value="guXu4awewr$w"/>
</appSettings>
Измените соответственно web.config на:
<?xml version="1.0"?>
<configuration>
 
  <appSettings file ="../web_beta.config">
    <add key="ReportType" value="8"/>
    <add key="ReportSubject" value="Средний бизнес"/>
  </appSettings>
 
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
  </system.web>
 
</configuration>

web_gamma.config

Существует альтернативный способ размещения настроек в дополнительном файле. Измените web.config на:
<?xml version="1.0"?>
<configuration>
 
  <appSettings configSource ="web_gamma.config"/>
 
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
  </system.web>
 
</configuration>
И создайте файл web_gamma.config:
<?xml version="1.0"?>
<appSettings>
  <add key="ReportType" value="9"/>
  <add key="ReportSubject" value="Некоммерческие"/>
  <add key="DbLogin" value="writer"/>
  <add key="DbPass" value="hQ5zGPPSrkqqfsb"/>
</appSettings>
Этот подход менее гибкий, так как имеет ряд серьезных отличий от предыдущего:
  • Все значения секции настроек определяются только в указанном стороннем файле. Никакого переопределения 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 при публикации в зависимости от текущего типа билда. Измените содержимое этих файлов на следующее:
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
 
  <appSettings>
    <add key="ReportSubject" value="Дебаг"  xdt:Transform="Replace" xdt:Locator="Match(key)"/>
  </appSettings>
 
</configuration>
 

<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
 
  <system.web>
    <compilation xdt:Transform="RemoveAttributes(debug)" />
  </system.web>
  
</configuration>

Синтаксис преобразования описан на MSDN.

Но даже не зная синтаксиса, можно понять, что в случае 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 минут исполнения запроса.
<?xml version="1.0"?>
<configuration>
 
  <appSettings >
    <add key="ReportType" value="8"/> 
    <add key="ReportSubject" value="Средний бизнес"/>
  </appSettings>
 
  
  <system.web>
    <!--100 мб и 30 минут (действует только при compilation.debug = false)-->
    <httpRuntime maxRequestLength="102400" executionTimeout="1800" />
 
    <compilation targetFramework="4.0">
    </compilation>
  </system.web>
 
  <system.webServer>
    <security>
      <requestFiltering>
        <!--100 мб-->
        <requestLimits maxAllowedContentLength="104857600" />
      </requestFiltering>
    </security>
  </system.webServer>
 
</configuration>


Их дефолтные значения равны 4096 КБ и 30000000 байт, то есть 4 МБ и 28.61 МБ

Иерархия Web.config

На самом деле данный web.config находится в самом низу иерархии конфиг-файлов.

Верхний уровень конфигурации это файл machine.config. Его настройки действуют глобально на все веб-приложения, только если не переопределены конфиг-файлами более низкого уровня. Для пулов .net 4 он находится в папке %windir%\Microsoft.NET\Framework\v4.0.30319\Config или %windir%\Microsoft.NET\Framework64\v4.0.30319\Config – в зависимости от разрядности.

Следующий уровень тоже глобальный – это файл web.config, находящийся там же.

Третий уровень – уровень веб-сайта, по умолчанию это папка \inetpub\wwwroot, причем изначально файла web.config там даже и нет.

Четвертый – уровень веб-приложения, стандартный уровень, описанный здесь.

Существует еще и пятый уровень – когда настройки web.config применяются к подпапкам веб-приложения, но это актуально для других типов веб-приложений.

Более подробно вы можете почитать на MSDN

9. Множественные asmx файлы


В одном веб-приложении может быть не один, а несколько 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();
        }
 
        [WebMethod(Description = "Введите идентификитор отчета.")]
        public GetReportResult GetReport(int reportID) {
            GetReportArg arg = new GetReportArg();
            arg.ReportID = reportID;
            return _service.GetReport(arg);
        }
 
        [WebMethod(Description = "Периодянварь 2015 года.")]
        public GetReportIdArrayResult GetReportIdArray() {
            GetReportIdArrayArg arg = new GetReportIdArrayArg();
            arg.DateBegin = new DateTime(2015, 01, 01);
            arg.DateEnd = new DateTime(2015, 02, 01);
            return _service.GetReportIdArray(arg);
        }
    }
 
 
 
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [WebService(Description = "Финотчеты", Namespace = XmlNS)]
    public class FinReportService : WebService {
        public const string XmlNS = "http://asmx.habrahabr.ru/";
        private int _version;
 
        public FinReportService() {
            _version = 1;
        }
 
        public FinReportService(int version) {
            _version = version;
        }
 
        [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 = "Phone: " + ConfigurationManager.AppSettings["phone"] + " Version: " + _version
                }
            };
        }
    }
 
 
    public class FinReport {
        public int ReportID { getset; }
        public DateTime Date { getset; }
        public string Info { getset; }
    }
 
    public class GetReportIdArrayArg {
        public DateTime DateBegin { getset; }
        public DateTime DateEnd { getset; }
    }
 
    public class GetReportIdArrayResult {
        public int[] ReportIdArray { getset; }
    }
 
    public class GetReportArg {
        public int ReportID { getset; }
    }
 
    public class GetReportResult {
        public FinReport Report { getset; }
    }
 
}
Также в проекте должны быть следующие 3 asmx файла:
FinReport.asmx – без изменений:
<%@ Class="FinReportWebService.FinReportService" %>
FinReport_v2.asmx – новый:
<%@ Class="FinReportWebService.FinReportService_v2" %>
FinReport_CHECK.asmx – новый:
<%@ Class="FinReportWebService.FinReportService_Check" %>
А web.config измените на:
<?xml version="1.0"?>
<configuration>
 
  <system.web>
      <compilation debug="true" targetFramework="4.0" />
  </system.web>
 
  <appSettings>
    <add key="phone" value="nokia"/>
  </appSettings>
  
 
  <location path="FinReport.asmx">
    <appSettings>
      <add key="phone" value="samsung"/>
    </appSettings>
  </location>
 
 
  <location path="FinReport_v2.asmx">
    <appSettings>
      <add key="phone" value="htc"/>
    </appSettings>
  </location>
 
  
  <location path="FinReport_CHECK.asmx">
    <appSettings>
      <add key="phone" value="apple"/>
    </appSettings>
 
    <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.NET\Framework64\v4.0.30319\Config, рекомендую с ней ознакомиться.

Однако с помощью web.config легко указать собственный aspx файл. Добавьте в проект файл FinReportPage.aspx:
<!doctype html>
<html>
    <head>
        <meta charset=utf-8>
        <title>Welcome</title>
    </head>
    <body>
        <p>Добро пожаловать!</p>
    </body>
</html>
И укажите в web.config
<?xml version="1.0"?>
<configuration>
    <system.web>
 
      <webServices>
        <wsdlHelpGenerator href="FinReportPage.aspx" />
      </webServices>
 
      <compilation debug="true" targetFramework="4.0" />
 
    </system.web>
</configuration>


11. Замена расширения


С помощью web.config можно легко изменить расширение asmx на любое другое. Добавьте в проект файл с расширением habr, например, FinReportClone.habr, с таким же содержимым что и у FinReport.asmx. А конфиг измените на следующий:
<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" >
      <buildProviders>
        <remove extension=".habr"/>
        <add extension=".habr" type="System.Web.Compilation.WebServiceBuildProvider" />
      </buildProviders>
    </compilation>
  </system.web>
 
  <system.webServer>
    <handlers>
      <add name="HabraHandler" verb="*" path="*.habr" type="System.Web.Services.Protocols.WebServiceHandlerFactory" />
    </handlers>
  </system.webServer>
  
</configuration>
Отмечу, что при запуске из 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>";
 
            bool wsdlRequested = (context.Request.QueryString.ToString().ToLower() == "wsdl");
 
            if (wsdlRequested) {
                response = string.Format(response, "Обратитесь к администратору за wsdl.");
            } else {
                response = string.Format(response, "Веб-сервис финансовой отчетности.");
            }
 
            context.Response.ContentType = "text/html; charset=utf-8";
            context.Response.Write(response);
 
//            string filePage = HttpContext.Current.Server.MapPath("~/FinReportPage.htm");
//            context.Response.WriteFile(filePage);
        }
 
        public bool IsReusable {
            get { return false; }
        }
    }
}
И измените web.config:
<?xml version="1.0"?>
<configuration>
  
  <system.web>
    <compilation debug="true" targetFramework="4.0" >
    </compilation>
  </system.web>
 
  <system.webServer>
    <handlers>
      <add name="FinReportGetHandler" verb="GET" path="FinReport.asmx" type="FinReportWebService.FinReportGetHandler" />
    </handlers>
  </system.webServer>
  
</configuration>

Так же, как и в предыдущем приеме, обработчик сработает только в IIS. Кстати, веб-страницу лучше не хардкодить, а читать из файла.

13. Исключения


Обработка исключений в веб-сервисе всегда имеет очень большое значение. Построение удачного механизма идентификации и возврата ошибок сэкономит много времени и нервов разработчикам с обеих сторон. Ниже приводятся два разных способа информирования клиента веб-сервиса о том, что что-то пошло не так.

<soap:Fault>

Элемент <soap:Fault> является стандартным для SOAP способом возврата ошибки. Состоит из четырех субэлементов:
  • <faultcode> — код ошибки
  • <faultstring> — человеко-читаемое описание ошибки
  • <faultactor> — источник ошибки
  • <detail> — произвольная XML структура для детальных данны

<faultcode> и <faultstring> являются обязательными, остальные два не обязательными.
Для его демонстрации измените метод getReportInfo на следующий код:
        private string getReportInfo(int reportID) {
            throwException_1();
//            throwException_2();
//            throwException_3();
            return "ReportID = " + reportID;
        }
 
 
        private void throwException_1() {
            int x = 0;
            x = 1 / x;
        }
 
 
        private void throwException_2() {
            throw new SoapException("Некорректный ReportID"SoapException.ClientFaultCode, Context.Request.Url.AbsoluteUri);    
        }
 
 
        private void throwException_3() {
            XmlDocument xmlDoc = new XmlDocument();
            XmlNode rootNode = xmlDoc.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace);
 
            XmlNode descNode = xmlDoc.CreateNode(XmlNodeType.Element, "Description", XmlNS);
 
            XmlNode descTypeNode = xmlDoc.CreateNode(XmlNodeType.Element, "Type", XmlNS);
            descTypeNode.InnerText = "DbConnection";
            descNode.AppendChild(descTypeNode);
 
            XmlNode descMessageNode = xmlDoc.CreateNode(XmlNodeType.Element, "Message", XmlNS);
            descMessageNode.InnerText = "Хост не найден.";
            descNode.AppendChild(descMessageNode);
 
            XmlNode habraNode = xmlDoc.CreateNode(XmlNodeType.Element, "HabraInfo", XmlNS);
            XmlAttribute habraNodeAttribute = xmlDoc.CreateAttribute("User");
            habraNodeAttribute.Value = "capslocky";
            habraNode.Attributes.Append(habraNodeAttribute);
 
            rootNode.AppendChild(descNode);
            rootNode.AppendChild(habraNode);
 
            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. ---&gt; Attempted to divide by zero.</faultstring>
      <detail />
    </soap:Fault>
  </soap:Body>
</soap:Envelope>
Необработанные исключения всегда дают soap:Server. При этом отображение стека исключения зависит от настройки customErrors:
 
<?xml version="1.0"?>
<configuration>
  
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <customErrors mode="On"/>
  </system.web>
  
</configuration>
У атрибута mode всего три возможных значения:
  • On – Никогда не показывать стек
  • Off – Всегда показывать
  • RemoteOnly – Показывать только для localhost запросов

С помощью ручного вбрасывания SoapException вы можете сами полностью определить структуру <soap:Fault>. Вызовите метод throwException_2. Этот код приведет к следующему контенту ответа
<?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:Client</faultcode>
      <faultstring>Некорректный ReportID</faultstring>
      <faultactor>http://win2012/custom_folder/FinReport.asmx</faultactor>
      <detail />
    </soap:Fault>
  </soap:Body>
</soap:Envelope>
Здесь мы изменили код ошибки, написали свой текст-пояснение, а также указали , в котором обычно указывают URL запроса.
Вообще существует четыре стандартных fault-кода, которые представлены статическими полями в классе SoapException:
  • Server – проблема в самом веб-сервисе
  • Client – клиент отправил некорректный запрос
  • MustUnderstand – не удалость обработать обязательный для обработки soap:Header
  • VersionMismatch – некорректная версия SOAP

Метод throwException_3() демонстрирует формирование собственных <detail> и кода ошибки:
<?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 xmlns:q0="http://asmx.habrahabr.ru/">q0:TempError</faultcode>
      <faultstring>Временная ошибка</faultstring>
      <faultactor>http://win2012/custom_folder/FinReport.asmx</faultactor>
      <detail>
        <Description xmlns="http://asmx.habrahabr.ru/">
          <Type>DbConnection</Type>
          <Message>Хост не найден.</Message>
        </Description>
        <HabraInfo xmlns="http://asmx.habrahabr.ru/" User="capslocky" />
      </detail>
    </soap:Fault>
  </soap:Body>
</soap:Envelope>
Однако рекомендуется вообще не допускать в веб-методах саму возможность необработанных исключений. То есть в методе всегда должен быть глобальный try-catch, который словит любые необработанные или вручную вброшенные исключения, и вернет их клиенту в заранее оговоренном формате, который может быть представлен как <soap:Fault>, так и способом, описанным ниже.

enum

Идея этого способа заключается в использовании перечислений для сообщения клиенту об успешной или неуспешной обработке его запроса. Важно, что все значения перечислений отражаются в wsdl, поэтому они автоматически присутствуют в клиентском прокси-классе.

Добавьте в проект файл FinReport_GetReport.cs:
using System;
 
namespace FinReportWebService {
 
    public class WebServiceError {
        public ErrorType Type { getset; }
        public string Message { getset; }
    }
 
 
    public enum ResultType {
        Error,
        FoundBasicData,
        FoundFullData
    }
 
 
    public enum ErrorType {
        Undefined,
        DbConnection,
        InvalidArgument,
        Forbidden
    }
 
 
    public class WebServiceErrorException : Exception {
        public WebServiceError Error { getset; }
    }
 
 
    public class FinReport_GetReport {
        private GetReportArg _arg;
        private GetReportResult _result;
 
        public GetReportResult GetReport(GetReportArg arg) {
            _arg = arg;
            initializeResultWithError();
 
            try {
                checkArg();
                fillResult();
 
            } catch (WebServiceErrorException ex) {
                setResultType(ResultType.Error);
                _result.Error = ex.Error;
 
            } catch (Exception ex) {
                setResultType(ResultType.Error);
                _result.Error.Type = ErrorType.Undefined;
                _result.Error.Message = ex.GetType() + ": " +  ex.Message;
            }
 
            if (_result.ResultType == ResultType.Error) {
                _result.Report = null;
 
            } else {
                _result.Error = null;
            }
 
            return _result;
        }
 
        
        private void setResultType(ResultType type) {
            _result.ResultType = type;
        }
 
 
        private void initializeResultWithError() {
            _result = new GetReportResult();
            setResultType(ResultType.Error);
 
            _result.Error = new WebServiceError();
            _result.Error.Type = ErrorType.Undefined;
            _result.Error.Message = "Не указан результат";
        }
 
 
        private void checkArg() {
            if (_arg.ReportID <= 0) {
                throw new WebServiceErrorException() {
                    Error = new WebServiceError() {
                        Type = ErrorType.InvalidArgument,
                        Message = "Некорректный идентификатор отчета: " + _arg.ReportID
                    }
                };
            }
        }
 
 
        private void fillResult() {
            _result.Report = new FinReport();
            _result.Report.ReportID = _arg.ReportID;
            _result.Report.Date = new DateTime(2015, 03, 15);
 
//            throw new WebServiceErrorException() {
//                Error = new WebServiceError() {
//                    Type = ErrorType.DbConnection,
//                    Message = "Хост разорвал соединение"
//                }
//            };
 
//            throw new InvalidOperationException("Упс");
 
            _result.Report.Info = "Some info";
            setResultType(ResultType.FoundFullData);
        }
 
    }
}
Веб-метод GetReport исправьте на
        [WebMethod(Description = "Получение отчета по ID")]
        public GetReportResult GetReport(GetReportArg arg) {
            return new FinReport_GetReport().GetReport(arg);
        }
И расширьте определение класса GetReportResult
    public class GetReportResult {
        public ResultType ResultType { getset; }
        public WebServiceError Error { getset; }
        public FinReport Report { getset; }
    }
Итак, с помощью перечисления ResultType мы сообщаем, что произошла ошибка или что веб-метод успешно отработал с некоторым типом результата. В случае ошибки через специальную структуру мы указываем тип ошибки и некоторый текст. Также пример демонстрирует технику глобального перехвата любых исключений. Таким образом клиент всегда получает в ответ «200 OK» и структуру GetReportResult.

Образцы ответов:
<?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>
    <GetReportResponse xmlns="http://asmx.habrahabr.ru/">
      <GetReportResult>
        <ResultType>FoundFullData</ResultType>
        <Report>
          <ReportID>45</ReportID>
          <Date>2015-03-15T00:00:00</Date>
          <Info>Some info</Info>
        </Report>
      </GetReportResult>
    </GetReportResponse>
  </soap:Body>
</soap:Envelope>
 
 
<?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>
    <GetReportResponse xmlns="http://asmx.habrahabr.ru/">
      <GetReportResult>
        <ResultType>Error</ResultType>
        <Error>
          <Type>DbConnection</Type>
          <Message>Хост разорвал соединение</Message>
        </Error>
      </GetReportResult>
    </GetReportResponse>
  </soap:Body>
</soap:Envelope>
 
 
<?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>
    <GetReportResponse xmlns="http://asmx.habrahabr.ru/">
      <GetReportResult>
        <ResultType>Error</ResultType>
        <Error>
          <Type>Undefined</Type>
          <Message>System.InvalidOperationException: Упс</Message>
        </Error>
      </GetReportResult>
    </GetReportResponse>
  </soap:Body>
</soap:Envelope>
 
 
<?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>
    <GetReportResponse xmlns="http://asmx.habrahabr.ru/">
      <GetReportResult>
        <ResultType>Error</ResultType>
        <Error>
          <Type>InvalidArgument</Type>
          <Message>Некорректный идентификатор отчета: -245</Message>
        </Error>
      </GetReportResult>
    </GetReportResponse>
  </soap:Body>
</soap:Envelope>
Хочу обратить внимание, что если 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 { getset; }
        public ResultTimeSoapHeader ResultTimeHeader { getset; }
        public SoapUnknownHeader[] UnknownHeaders { getset; }
 
        
        [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)
                }
            };
        }
 
 
        private string getReportInfo(int reportID) {
            StringBuilder sb = new StringBuilder();
            sb.Append("ReportID = ").Append(reportID).AppendLine();
 
            if (HabraHeader != null) {
                sb.Append("Login = ").Append(HabraHeader.Login).AppendLine();
                sb.Append("Password = ").Append(HabraHeader.Password).AppendLine();    
            }
 
            foreach (var header in UnknownHeaders) {
                sb.Append("Обнаружен SoapHeader = ").Append(header.Element.Name).AppendLine();
                sb.Append("MustUnderstand = ").Append(header.MustUnderstand).AppendLine();
 
//                if (header.Element.Name == "HabraSoapHeader") {
//                    sb.AppendLine("HabraSoapHeader распознан");
//                    sb.Append("Login = ").Append(header.Element["Login"].InnerText).AppendLine();
//                    sb.Append("Password = ").Append(header.Element["Password"].InnerText).AppendLine();
//                    header.DidUnderstand = true;
//                }
            }
 
            ResultTimeHeader = new ResultTimeSoapHeader();
            ResultTimeHeader.ResultTime = DateTime.Now;
            return sb.ToString();
        }
    }
    
 
    public class HabraSoapHeader : SoapHeader {
        public string Login { getset; }
        public string Password { getset; }
    }
 
 
    public class ResultTimeSoapHeader : SoapHeader {
        public DateTime ResultTime { getset; }
    }
 
 
    public class FinReport {
        public int ReportID { getset; }
        public DateTime Date { getset; }
        public string Info { getset; }
    }
 
 
    public class GetReportArg {
        public int ReportID { getset; }
    }
 
 
    public class GetReportResult {
        public FinReport Report { getset; }
    }
}
Затем перезапустите веб-сервер и перегенерируйте в клиентском проекте прокси-класс, так как объявленные заголовки отразятся в wsdl. Метод вызова сделайте следующим:
        private void webMethodTest_GetReport() {
            var service = GetFinReportService();
            var arg = new GetReportArg();
            arg.ReportID = 45;
 
            service.HabraSoapHeaderValue = new HabraSoapHeader();
            service.HabraSoapHeaderValue.Login = "neo";
            service.HabraSoapHeaderValue.Password = "3Ku2kcQfNLOW";
            service.HabraSoapHeaderValue.MustUnderstand = true;
 
            var result = service.GetReport(arg);
            var resultTimeSoapHeader = service.ResultTimeSoapHeaderValue;
 
            if (resultTimeSoapHeader != null) {
                MessageBox.Show("ResultTimeSoapHeader = " + resultTimeSoapHeader.ResultTime);
            }
 
            MessageBox.Show(result.Report.Info);
        }

Итак, чтобы добавить в asmx веб-сервис заголовок soap, нужно сделать три вещи:
  • Объявить унаследованный от SoapHeader класс заголовка
  • Объявить свойство для этого класса
  • Применить к веб-методу атрибут [SoapHeader], указывающий на это свойство

В этом примере веб-сервисом объявлены два заголовка – HabraSoapHeader и ResultTimeSoapHeader, причем первый ожидается в запросе, а второй возвращается в ответе.

Сами конверты будут выглядеть следующим образом:
<?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:Header>
    <HabraSoapHeader xmlns="http://asmx.habrahabr.ru/" soap:mustUnderstand="1">
      <Login>neo</Login>
      <Password>3Ku2kcQfNLOW</Password>
    </HabraSoapHeader>
  </soap:Header>
  <soap:Body>
    <GetReport xmlns="http://asmx.habrahabr.ru/">
      <arg>
        <ReportID>45</ReportID>
      </arg>
    </GetReport>
  </soap:Body>
</soap:Envelope>

<?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:Header>
    <ResultTimeSoapHeader xmlns="http://asmx.habrahabr.ru/">
      <ResultTime>2015-03-24T11:37:00.6135717+06:00</ResultTime>
    </ResultTimeSoapHeader>
  </soap:Header>
  <soap:Body>
    <GetReportResponse xmlns="http://asmx.habrahabr.ru/">
      <GetReportResult>
        <Report>
          <ReportID>45</ReportID>
          <Date>2015-03-15T00:00:00</Date>
          <Info>
            ReportID = 45
            Login = neo
            Password = 3Ku2kcQfNLOW
          </Info>
        </Report>
      </GetReportResult>
    </GetReportResponse>
  </soap:Body>
</soap:Envelope>
 
Обратите внимание на атрибут soap:mustUnderstand=«1», который означает, что веб-сервис обязательно должен понять этот заголовок. У класса SoapHeader есть свойство public bool DidUnderstand { set; get; }, которое определяет, был ли понят заголовок. Для известных заголовков оно изначально равно true, тогда как для неизвестных – false. И в случае, когда заголовок имел mustUnderstand=«1», а DidUnderstand оказался false, веб-сервер возвратит soap:Fault. Закомментируйте атрибут [SoapHeader(«HabraHeader»)], тогда тот же запрос приведет к:
<?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:MustUnderstand</faultcode>
      <faultstring>Заголовок SOAP HabraSoapHeader не распознан.</faultstring>
    </soap:Fault>
  </soap:Body>
</soap:Envelope>
Теперь этот заголовок попадает в массив 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)
        }
    };
}
 
 
private string getReportInfo(int reportID){
    string key = "getReportInfo_" + reportID;
    string cachedInfo = Context.Cache.Get(key) as string;
 
    if (cachedInfo != null){
        return cachedInfo;
    }
 
    string info = DateTime.Now + " reportID = " + reportID;
    this.Context.Cache.Add(key, info, nullDateTime.Now.AddSeconds(10), TimeSpan.Zero, CacheItemPriority.High, null);
    return info;
}
Атрибутный способ кэширования держит в памяти в течение заданного количества секунд все входящие и исходящие soap сообщения. И в случае полного совпадения входящего сообщения с находящимся в памяти, веб-метод не исполняется, а возвращается закэшированный результат.

Второй способ более гибкий и функциональный, в нем мы сами определяем объекты и ключи кэширования и другие его особенности.

В этом примере если клиент будет посылать запрос раз в секунду, то поле Date будет меняться раз в 5 секунд, а поле Info – раз в 10 секунд.

Напомню, что кэширование должно применяться обдуманно, так как ведет к большому потреблению памяти и риску возврата неактуальных данных.

16. SoapExtension


SoapExtension – это мощная техника, которая позволяет вручную видоизменять запросы и ответы, причем как со стороны веб-сервиса, так и со стороны клиента. К сожалению, полноценное ее описание с различными примерами тянет на отдельную статью, поэтому я опишу ее только в общем и дам ссылки на материалы.

Возможности SoapExtension:
  • Чтение контента запроса или ответа (просмотр soap конвертов в виде MemoryStream)
  • Модификация контента запроса или ответа
  • Чтение и обработка заголовков (soap header)
  • 1 способ привязки: к веб-методу (веб-сервиса или клиента) через кастомный атрибут
  • 2 способ привязки: к веб-сервису (или клиенту) с помощью web.config (app.config) без перекомпиляции!
  • Взаимодействие с самим классом веб-сервиса или прокси-классом
  • Создание цепочек из различных SoapExtension

Что можно сделать с помощью SoapExtension
  • полное низкоуровневое логирование
  • аутентификация
  • наложение и проверка ЭЦП
  • шифрование
  • сжатие
  • обработка soap header
  • любое преобразование контента

Ссылки


17. Дебаггинг x64 в Visual Studio


Дебаггинг веб-приложений в Visual Studio осложняется отсутствием 64-х разрядного режима во встроенном веб-сервере. Если вы укажите Platform target: x64, то запуск приложения приведет к ошибке «Не удалось загрузить файл или сборку «FinReportWebService» либо одну из их зависимостей. Была сделана попытка загрузить программу, имеющую неверный формат».

Есть три разных способа решения данной проблемы.

1 способ. В настройках веб-проекта выбрать «Use Local IIS Web server», то есть использовать локальный IIS



2 способ. Опубликовать веб-приложение на локальный IIS и подключиться к его процессу Debug -> Attach to Process.

3 способ. При отсутствии IIS можно физически заменить стандартный ASP.NET Development Server на альтернативный веб-сервер CassiniDev. Исходники CassiniDev скачайте отсюда: https://cassinidev.codeplex.com/SourceControl/latest#ReadMe.htm

Распаковав, откройте и скомпилируйте \trunk\src\CassiniDev.sln, возможно придется обновить референсы из trunk\src\packages. Теперь в свойствах солюшена измените платформу проектов на x64 и перекомпилируйте:



Стандартный веб-сервер находится в папке C:\Program Files (x86)\Common Files\microsoft shared\DevServer. Сделав его копию, замените его из папки \trunk\src\CassiniDev\bin\x64\Debug



18. Деплой (публикация)


С помощью команды Publish и метода File System Visual Studio создает в указанной папке набор файлов, который необходимо предоставить для IIS



wwwroot

В простейшем случае эта папка веб-сервиса размещается в специальной папке C:\inetpub\wwwroot, что позволяет легко сконвертировать ее в веб-приложение. Напомню, что IIS можно вызвать командой inetmgr.





HTTP Error 404.3 — Not Found

Если возникла ошибка HTTP Error 404.3 — Not Found, то необходимо добавить следующие компоненты IIS



И выполнить команду
%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_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 AppPool\FinReportPool, или IIS AppPool\DefaultAppPool

И в случае кастомной папки в ее свойствах безопасности необходимо будет добавить учетную запись с правами чтения, исполнения и просмотра содержимого



А также переключить удостоверение анонимного пользователя



Также отмечу, что вместо учетной записи можно указать группу <Имя компьютера>\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%\System32\inetsrv\appcmd list wp
%systemroot%\System32\inetsrv\appcmd list sites
%systemroot%\System32\inetsrv\appcmd list app
%systemroot%\System32\inetsrv\appcmd list appPools
pause



Если вы не видите процесса, то чтобы его запустить, достаточно открыть в браузере страницу веб-приложения. Более подробно об утилите AppCmd.exe можно почитать здесь.

20. Инструменты разработки


Если вы только начинаете работать с веб-сервисами, то вам понадобятся дополнительные инструменты, которые значительно облегчат процесс разработки.
  • Fiddler. Универсальный HTTP-дебаггер. Бесплатный и очень функциональный. Описан на Хабре тут. Must have.
  • SoapUI. Мощный инструмент анализа и тестирования веб-сервисов. Существует в различных редакциях, есть бесплатная.
  • Oxygen XML Editor. Очень удобный инструмент работы с XML. Умеет работать с WSDL и SOAP.
  • www.wsdl-analyzer.com. Анализ и сравнение WSDL.
  • www.freeformatter.com. Множество различных инструментов для форматирования, валидации и преобразования.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+16
Comments 71
Comments Comments 71

Articles