четверг, 5 ноября 2009 г.

RouteHandler для IHttpHandler

Сегодня я увидел ошибку в Connect в которой некто предлагает PageRouteHandler’у (новинка в ASP.NET 4) обрабатывать IHttpHandler так же как и Page. Вообще, я не согласен с этим предложением, потому что Page является IHttpHandler, а не наоборот. Что если человек действительно хочет новый обработчик? Дадим ему такое языковыворачивающее название IHttpHandlerRouteHandler.
К сожалению, уже совсем поздно добавлять такое в ASP.NET 4, но оно оказывается тривиально простым для написания. По сути вот оно:

 public class HttpHandlerRouteHandler<THandler>   
   : IRouteHandler where THandler : IHttpHandler, new()   
 {  
      public IHttpHandler GetHttpHandler(RequestContext requestContext)   
      {  
        return new THandler();  
      }  
 }  

Конечно, оно само по себе не особо полезно. Нам нужен метод-расширитель (“extension method”) для того чтобы действительно было просто регистрировать маршруты для HttpHandler’ов.Я написал целый ряд их, но только два примера тут в моем блоге. Остальные вы можете получить скачав проект внизу этого поста.

 public static class HttpHandlerExtensions   
 {  
      public static void MapHttpHandler<THandler>(this RouteCollection routes, string url) where THandler : IHttpHandler, new()   
      {  
           routes.MapHttpHandler<THandler>(null, url, null, null);  
      }  
  //...  
      public static void MapHttpHandler<THandler>(this RouteCollection routes, string name, string url, object defaults, object constraints) where THandler : IHttpHandler, new()   
      {   
           var route = new Route(url, new HttpHandlerRouteHandler<THandler>());  
           route.Defaults = new RouteValueDictionary(defaults);  
           route.Constraints = new RouteValueDictionary(constraints);  
           routes.Add(name, route);  
      }  
 }  

Это дает нам очень простую возможность регистрировать маршрут, который обрабатывается IHttpHandler'ом. В этом случае, я регистрирую маршрут, использующий мой SimpleHttpHandler для обработки любого двухсегментного URL.

 public static void RegisterRoutes(RouteCollection routes)  
 {  
      routes.MapHttpHandler<SampleHttpHandler>("{foo}/{bar}");  
 }  

И здесь код SampleHttpHandler для завершения. Все что он делает, так это выводит значения маршрута.

 public class SampleHttpHandler : IHttpHandler   
 {  
      public bool IsReusable   
      {  
           get { return false; }  
      }  
      public void ProcessRequest(HttpContext context)   
      {  
           var routeValues = context.Request.RequestContext.RouteData.Values;  
           string message = "I saw foo='{0}' and bar='{1}'";  
           message = string.Format(message, routeValues["foo"], routeValues["bar"]);  
           context.Response.Write(message);  
      }  
 }  

Затем я делаю запрос для /testing/yo и вижу сообщение
“I saw foo='testing' and bar='yo'”
в моем браузере.
Здесь одно ограничение – мой http handler должен иметь конструктор без параметров. Это впринципе неплохое ограничение, так как ранее регистрируя маршрут необходимо было вводить проверку, что обработчик имеет пустой конструктор.
Вольный перевод статьи Phil Haack
Там же пример для Visual Studio 2010.

вторник, 3 ноября 2009 г.

Использование C# dynamic для упрощения доступа к ADO.NET данным

Сценарий таков, что вы не используете какой-либо ORM такие как LINQ to SQL или Entity Framework, но вы напрямую используете ADO.NET для выполнения SQL-команд. Это вообще не принятый мною способ, но много народа предпочитает его более высокому уровня доступа к данным.
Давайте посмотрим на пример, который мы собираемся улучшить. Возьмем пример из MSDN о SqlCommand:

 string commandText = "SELECT OrderID, CustomerID FROM dbo.Orders;";  
 using (var connection = new SqlConnection(Settings.Default.NorthwindConnectionString)) {  
   using (var command = new SqlCommand(commandText, connection)) {  
     connection.Open();  
     using (SqlDataReader reader = command.ExecuteReader()) {  
       while (reader.Read()) {  
         Console.WriteLine(String.Format("{0}, {1}", reader[0], reader[1]));  
       }  
     }  
   }  
 }  

Давайте сейчас предположим, что мы мы заинтересованы только в одном select-запросе, который даст возможность нам несколько абстрагироваться от деталей в SQL. Написав несколько небольших хелперов, который используют dynamic, мы сможем написать довольно простой код:

 string commandText = "SELECT OrderID, CustomerID FROM dbo.Orders;";  
 foreach (var row in SimpleQuery.Execute(Settings.Default.NorthwindConnectionString, commandText)) {  
   Console.WriteLine(String.Format("{0}, {1}", row.OrderID, row.CustomerID));  
 }  

Некоторые примечания:
Нам достаточно сделать единственный вызов и прямо получить объекты с которыми мыможем работать. Сравните это с использованием SqlConnection, SqlCommand и SqlDataReader.
Мы используем стандартный шаблон перечисления, в котором SqlDataReader делает вызов reader.Read() на каждой итерации, что выглядит ужасно.
И еще одно важное: мы получаем прямой доступ к свойствам объекта строки благодаря динамическому объекту! Например мы можем написать row.OrderID вместо reader[0](или reader[“OrderID”])
Как же это работает? Первое, давайте посмотрим на SimpleQuery.Execute метод:

 public static IEnumerable<dynamic> Execute(string connString, string commandText) {  
   using (var connection = new SqlConnection(connString)) {  
     using (var command = new SqlCommand(commandText, connection)) {  
       connection.Open();  
       using (SqlDataReader reader = command.ExecuteReader()) {  
         while (reader.Read()) {  
           yield return new DataRecordDynamicWrapper(reader);  
         }  
       }  
     }  
   }  
 }  

Это собственно тоже самое что и в MSDN коде за исключением что оно оборачивает возвращаемый reader в DataRecordDynamicWrapper, который делает всю волшебную динамическую работу. Также заметьте что метод возвращает IEnumerable благодаря которому собственно мы и можем писать 'var row' в тестовом коде (который по-моему выглядит лучше чем 'dynamic row').
Так что сейчас все что осталось так это взглянуть на DataRecordDynamicWrapper, который невероятно прост:
 public class DataRecordDynamicWrapper : DynamicObject {  
   private IDataRecord _dataRecord;  
   public DataRecordDynamicWrapper(IDataRecord dataRecord) { _dataRecord = dataRecord; }  
   public override bool TryGetMember(GetMemberBinder binder, out object result) {  
     result = _dataRecord[binder.Name];  
     return result != null;  
   }  
 }  

Все что он делает, так это получает данные по индексу в _dataRecord для заданного имени свойства.
Единственная последняя вещь, которая ничего не стоит для того чтобы сделать реальной, мы должны добавить поддержку SQL параметров, которые дадут простоту написания SQL кода, не подверженного SQL-инъекциям. Это может быть просто сделано передавая параметры в SimpleQuery.Execute.
Вольный перевод статьи David Ebbo (Исходный код там же)