Одна из фич, незаметно попавших в ASP.NET 4.5 – встроенная поддержка минификации контента. Упаковщик живет в пространстве имен System.Web.Optimization.

Упаковка контента включена по умолчанию в шаблонах ASP.NET MVC 4/.NET 4.5 в Visual Stidio 11 Beta, и, надеюсь, останется включенной по умолчанию в релизной версии.

Хоть фича и упоминается как новшество в ASP.NET 4.5, она доступна для проектов на базе .NET 4.0 и ASP.NET MVC 3. Сам по себе упаковщик доступен в виде пререлизного пакета nuget.

После установки пакета придется сделать еще несколько изменений:

1. Добавить вызов EnableDefaultBundles на старте приложения, в global.asax.cs:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    System.Web.Optimization.BundleTable.Bundles.EnableDefaultBundles();
}

2. Заменить явное подключение скриптов и стилей в _Layout.cshtml на

<link href="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Content/css")" rel="stylesheet" type="text/css" />
<script src="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Scripts/js")" type="text/javascript"></script>

После этого все css и все скрипты будут подтянуты двумя файлами:

Untitled

Скриншот выше снят для стандартного шаблона ASP.NET MVC 3. Все скрипты в шаблоне весят достаточно много, отсюда и полумегабайтный размер выходного файла. Достаточно удалить лишние файлы из Scripts, и размер пакета станет более приемлимым :)

Кроме очевидного “собрать все в один большой файл”, System.Web.Optimization делает еще несколько вещей:

  • Игнорирует файлы .intellisense.js, -vsdoc.js, .debug.js.
  • Подхватывает уже минимизированные файлы – min.js, и подключает их “как есть”.
  • Учитывает порядок подключения скриптов для JQuery/UI/Validate, modernizr, dojo, mootools, prototype и ext.js

    Дополнительная плюшка, котороя не слишком заметна в документации – возможность подключения своих трансформаций для выходных файлов, на случай если стандартных JsMinify и CssMinify не хватит. Список уже готовых трансформаций виден в поиске на nuget. И в нем уже есть Less и CoffeeScript.

  • 30. May 2011 · Write a comment · Categories: C#, Misc

    Никогда не стоит преуменьшать опасность гугления кода. Вас может увлечь за толпой леммингов. Например, попытавшись найти пример отправки POST-запроса из C# вы получите кучу примеров использования HttpWebRequest. В 95% случаев ручная работа с WebRequest не оправдана ничем, кроме незнания стандартной библиотеки. WebRequest обычно тянет за собой:

  • 10-20 строк кода на каждый запрос.
  • Ручное выставление Cookie Container для каждого запроса.
  • Неочевидную последовательность using-ов (для тех, кто знает о IDisposable).
  • Ссылку на System.Web, и отказ от клиентcкого профиля.
  • В то же время стандарный класс WebClient умеет практически все, ради чего вы пишете код с WebRequest/Response/Stream и прочим шаманством. Он даже умеет энкодить отправляемые значения без HttpUtility.

    Отправка GET-запросов:

    С WebRequest (без using, мне повезет):

    System.Net.WebRequest reqGET = System.Net.WebRequest.Create("http://site.ru/");
    System.Net.WebResponse resp = reqGET.GetResponse();
    System.IO.Stream stream = resp.GetResponseStream();
    System.IO.StreamReader sr = new System.IO.StreamReader(stream);
    string s = sr.ReadToEnd();
    Console.WriteLine(s);

    С WebClient:

    using (var client = new System.Net.WebClient())
    {
        string s = client.DownloadString("http://site.ru/");
        Console.WriteLine(s);
    }

    Отправка POST-запросов:

    С WebRequest (опять без using):

    System.Net.WebRequest reqPOST = System.Net.WebRequest.Create("http://site.ru/send.php");
    reqPOST.Method = "POST";
    reqPOST.ContentType = "application/x-www-form-urlencoded";
    byte[] sentData = Encoding.Unicode.GetBytes("message=" + System.Web.HttpUtility.UrlEncode("some data"));
    reqPOST.ContentLength = sentData.Length;
    System.IO.Stream sendStream = reqPOST.GetRequestStream();
    sendStream.Write(sentData, 0, sentData.Length);
    sendStream.Close();
    System.Net.WebResponse result = reqPOST.GetResponse();

    С WebClient:

    using (var client = new System.Net.WebClient())
    {
        var values = new System.Collections.Specialized.NameValueCollection();
        values.Add("message", "some data");
        client.UploadValues("http://site.ru/send.php", values);
    }

    Сохранение Cookies между запросами:

    WebClient по умолчанию не сохраняет cookies между запросами. Это легко лечится наследованием от него, и переопределением одного метода:

    public class CookieAwareWebClient : WebClient
    {
        private CookieContainer m_container = new CookieContainer();

        protected override WebRequest GetWebRequest(Uri address)
        {
            WebRequest request = base.GetWebRequest(address);
            if (request is HttpWebRequest)
            {
                (request as HttpWebRequest).CookieContainer = m_container;
            }
            return request;
        }
    }

    (подсмотрено у Yuriy Solodkyy)

    Вместо заключения:

    Тонка и неразличима грань между тупым копипастом и повторным использованием кода.

    Недавно наткнулся на очень забавный и неочевидный баг студии. Суть бага – студия начинает подвисать при сохранении aspx/ascx файлов секунд так на 20-30. Через полминуты все проходит, и можно работать дальше. Воспроизводится стабильно, простым нажатием Ctrl+S. При этом студия не подвисает намертво, просто не отзывается на действия, и вот значок сохранения не пропадает из статус бара:

    CropperCapture[2]

    Запускаем второй экземпляр студии. Снимаем в DebugOptions and Settings чекбокс Enable Just My Code. Цепляемся им к первому экземпляру студии:

    Options

    attach

    Нажимаем Ctrl+S в проблемном экземпляре студии. Видим в Output кучу исключений:

    NotFound

    Включаем в DebugExceptions остановку при бросании System.IO.FileNotFoundException. Сохраняем еще раз, смотрим на детали исключения:

    breakRegion

    В проекте нет явных ссылок на antlr.runtime. Но есть ссылка на DDay.iCal, которая хоть и ссылается на antlr, но не требует ее для нормальной работы. Распространяется DDay, естественно, без проблемной сборки.

    При сохранении aspx-файла студия пытается пересоздать файл aspx.designer.cs. Для этого ей нужно получить типы всех элементов управления, упоминаемых в разметке. Например, студия видит в разметке элемент вроде:

    Region2

    Для получения конкретного типа PostBackTriggerControl студия берет список нейспейсов с префиксом lsf из конфигурации, и пытается найти каждую комбинацию “namespace.type”. А неймпейсов с префиксом lsf у нас оказалось много:

    config

    Студия достаточно быстро находит нужный ей LogicSoftware.EasyProjects.Web.Gui.Code.PostBackTriggerControl в сборке LogicSoftware.EasyProjects.Web.Gui.Code. Но на этом она не останавливается.

    На всякий случай студия предпринимает попытки найти тип PostBackTriggerControl еще и во всех явно или косвенно упоминаемых сборках. Даже если сборка явно задана при регистрации неймспейса. Если тип найден, он запоминается в кэше. Поймав при поиске FileNotFoundException, студия забывает сохранить правильный результат в кэш.

    Бешенное количество попыток – количество элементов на странице * количество регистраций неймспейсов * количество сборок на которые ссылается сайт * количество проблемных сборок -  и дает те самые 210 исключений при одном нажатии Ctrl+S. Исправляется подбрасываеним недостающих сборок в папку с зависимостями. Добавлять ссылки из web-проекта на них или копировать bin – необязательно. Достаточно, чтобы студия увидела их при поиске.

    Недавно напоролись на один древний баг в ASP.NET Membership. Краткий смысл бага – бросание исключения при инициализации провайдера Membership уводит весь Membership в нерабочее состояние.

    Полный смысл бага:

    Вот код статического метода System.Web.Security.Membership.Initialize(), который вызывается при любых обращениях к методам ProviderBase:

    private static void Initialize()
    {
        if (!s_Initialized || !s_InitializedDefaultProvider)
        {
            if (s_InitializeException != null)
            {
                throw s_InitializeException;
            }
            if (HostingEnvironment.IsHosted)
            {
                HttpRuntime.CheckAspNetHostingPermission(AspNetHostingPermissionLevel.Low, "Feature_not_supported_at_this_level");
            }
            lock (s_lock)
            {
                if (!s_Initialized || !s_InitializedDefaultProvider)
                {
                    if (s_InitializeException != null)
                    {
                        throw s_InitializeException;
                    }
                    bool initializeGeneralSettings = !s_Initialized;
                    bool initializeDefaultProvider = !s_InitializedDefaultProvider
                        && (!HostingEnvironment.IsHosted || (BuildManager.PreStartInitStage == PreStartInitStage.AfterPreStartInit));
                    if (initializeDefaultProvider || initializeGeneralSettings)
                    {
                        bool flag3;
                        bool flag4 = false;
                        try
                        {
                            RuntimeConfig appConfig = RuntimeConfig.GetAppConfig();
                            MembershipSection membership = appConfig.Membership;
                            flag3 = InitializeSettings(initializeGeneralSettings, appConfig, membership);
                            flag4 = InitializeDefaultProvider(initializeDefaultProvider, membership);
                        }
                        catch (Exception exception)
                        {
                            s_InitializeException = exception;
                            throw;
                        }
                        if (flag3)
                        {
                            s_Initialized = true;
                        }
                        if (flag4)
                        {
                            s_InitializedDefaultProvider = true;
                        }
                    }
                }
            }
        }
    }

    Во втором ASP.NET баг осложняется еще и тем, что стандартная реализация ProvidersHelper.InstantiateProvider проглатывает оригинальные исключения и оборачивает их в ConfigurationErrorsException, сохраняя лишь текст сообщения

    Не используйте Membership. А если используете – не бросайте исключения при инициализации своих провадеров.

    Пару лет назад наш основной продукт, EasyProjects .NET 6.x, было решено оставить на вялом саппорте команды из 4 человек. И написать на замену новый, свежий, EasyProjects .NET 7 AikiProjects BirdView Projects, с дамами, преферансом, и кучей JavaScript-а. Разработка BirdView, естественно, затянулась. EasyProjects .NET же плавно дописался до версии 7.1, оброс Enterprise-фичами, продался на семизначную сумму за год. Появились толстые клиенты, покупающие по лицензии на 200-300 пользователей. При том, что в начале разработки мы считали что 50 пользователей и 100 проектов – это очень много, и “будет тормозить”.

    С моей колокольни (отдела custom development) было интересно наблюдать, как EP.NET все меньше подходил для управления Software Development Projects. И все больше адаптировался под управление не-IT проектами. Я только догадываюсь, как наш софт использует DNA Lab в FBI, или Enterprise Serviсes в Microsoft. Но они явно не сторонники гибких методологий.

    И вот месяц назад концепция поменялась. EasyProjects .NET больше не умирает. Его ждет бурное развитие, 80% рост продаж (если Sales не врет), и еще более Enterprise-плюшки:

    • Resource Management
    • Multiple Dependencies
    • Financial Reporting module
    • Critical Path
    • Enterprise Edition (куда ж без него)
    • и много-много других мелких изменений

    Смена концепции внезапно забросила меня на роль Product Owner-а. Прокси, естественно. Пытаюсь достичь нирваны общего вижина понять, во что должен превратиться софт хотя бы в течении полугода. Пока понимание очень смутное, но оптимистичное.