Программное добавление твитта с изображением на C#

Программирование

Tagged Under : , ,

Понадобилось коллеге выполнить в рамках одной из задач добавление сообщения в twitter вместе с картинкой. Примеров не нашёл, но я после непродолжительного поиска нашёл данный исходный код.
Добавил сюда, чтобы потом не искать больше.

Sport Online: следим за результатами матчей с Windows Phone

Проекты

Tagged Under : ,

Ни для кого не секрет, что количество футбольных болельщиков достаточно велико, но не всегда можно присутствовать на матче или в условиях просмотра теле- и видео трансляции. Тем не менее хочется следить за ходом матча или счётом.

Это послужило идеей для создания приложения для смартфонов с windows phone, которое бы позволяло следить за счётом в матче и ходом событий посредством текстовых и аудио трансляций. В ходе развития идеи добавился функционал добавления напоминаний на предстоящие матчи, для которых будут вестись трансляции.
Причём напоминание устанавливается на время корректное именно для времени установленном на смартфоне, т.е. независимо от часового пояса напоминание сработает в нужное время.

Базой для приложения стал портал livetv.ru

В результате реализации описанных идей появилось приложение Sport Online.

скачать Sport Online для Windows Phone

WCF: метаданные содержат ссылки, которые не могут быть разрешены

Программирование

Tagged Under :

Вчера переносил на новый сервер свои WCF-сервисы и при обновлении прокси-класса столкнулся с ошибкой:

Метаданные содержат ссылки, которые не могут быть разрешены: 'http://example.com/service.svc?wsdl'.
Документ WSDL содержит ссылки, которые невозможно разрешить.
Возникла ошибка при загрузке "http://example.com/service.svc?xsd=xsd0".
Базовое соединение закрыто: Непредвиденная ошибка при приеме.
Не удается прочитать данные из транспортного соединения: Удаленный хост принудительно разорвал существующее подключение.
Удаленный хост принудительно разорвал существующее подключение

или на английском так:

Metadata contains a reference that cannot be resolved: 'http://example.com/service.svc?wsdl'.
The WSDL document contains links that could not be resolved.
There was an error downloading 'http://example.com/service.svc?xsd=xsd0'.
The underlying connection was closed: An unexpected error occurred on a receive.
Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.

Проблема в том, что присутствующие в WSDL’е ссылки вида http://example.com/service.svc?xsd=xsd0 не открываются и сервер сбрасывает соединение.
Как выяснилось ошибка состоит в том, что необходимо для процесса под которым запущен Application Pool сайта, в рамках которого работает WCF-сервис установить права на запись в папку C:\Windows\Temp

С данными правами всё начинает корректно работать.

Источник: WCF Add Service Reference gotcha with Windows Server

ASP.NET MVC: простенькая задачка

Программирование

Tagged Under : ,

Итак, у вас есть Action контроллера, в котором содержится следующий фрагмент кода:

ViewBag.Test = "test message";
ViewBag.Alert = "alert message";
ViewData["Alert"] = "alert message 2";

ViewBag.Func = new Func<string, string>(x => {
    ViewBag.Test = x;
    x = ViewBag.Alert;
    ViewBag.Alert = (string)ViewData["Alert"];
    return x;
});

И представление следующего вида:

@ViewBag.Func("x")
@ViewData["Test"]
@ViewBag.Alert

Какие строки мы увидим на странице и в какой последовательности? Сначала ответьте себе устно, а потом проверьте запустив.

Orchard CMS: интегрируем Uppod

Программирование

Tagged Under : ,

Итак, появилась необходимость добавить для сайта на базе Orchard CMS, модуль для проигрывания mp3-файлов. Выбранный модуль jPlayer, к сожалению, уронил весь сайт. Не решившись более рисковать решил создать свой модуль на базе плеера Uppod (не факт, конечно, что мой не роняет :) ). С ним я работаю давно, поэтому долго не выбирал.

Процедура создания модуля отличается от других примеров создания модулей на данном сайте тем, что в рамках модуля мы будем активно использовать шорткоды, функционал которых добавляется на сайт путём добавления моего модуля kosfiz.Shortcodes, создание которого описано в записи «Orchard CMS: добавляем поддержку shortcodes«.

Итак, прежде всего необходимо создать модуль, и затем открыть проект с ним. Далее, в папку Models добавляем класс UppodSettingsRecord описывающий настройки плеера, распространяющиеся на весь сайт:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace kosfiz.Uppod.Models
{
    public class UppodSettingsRecord
    {
        public virtual int Id { get; set; }
        public virtual string VideoPlayerStyle { get; set; }
        public virtual int VideoPlayerWidth { get; set; }
        public virtual int VideoPlayerHeight { get; set; }
        public virtual string AudioPlayerStyle { get; set; }
        public virtual int AudioPlayerWidth { get; set; }
        public virtual int AudioPlayerHeight { get; set; }
        public virtual string PhotoPlayerStyle { get; set; }
        public virtual int PhotoPlayerWidth { get; set; }
        public virtual int PhotoPlayerHeight { get; set; }
        public virtual string BackgroundColor { get; set; }
    }
}

Данный класс описывает размеры и стили для каждого из видов плееров (видео, аудио, фото), а также цвет фона.
Потом отражаем свойства описанные в классе в базу посредством класса Migrations

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Orchard.Data.Migration;
using System.Data;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Builders;
using Orchard.Core.Contents.Extensions;

namespace kosfiz.Uppod
{
    public class Migrations : DataMigrationImpl
    {
        public int Create()
        {
            SchemaBuilder.CreateTable("UppodSettingsRecord", table => table.Column("Id", DbType.Int32, column => column.PrimaryKey().Identity())
                .Column("VideoPlayerStyle", DbType.String, column => column.Nullable())
                .Column("VideoPlayerWidth", DbType.Int32, column => column.NotNull().WithDefault(400))
                .Column("VideoPlayerHeight", DbType.Int32, column => column.NotNull().WithDefault(300))
                .Column("AudioPlayerStyle", DbType.String, column => column.Nullable())
                .Column("AudioPlayerWidth", DbType.Int32, column => column.NotNull().WithDefault(300))
                .Column("AudioPlayerHeight", DbType.Int32, column => column.NotNull().WithDefault(90))
                .Column("PhotoPlayerStyle", DbType.String, column => column.Nullable())
                .Column("PhotoPlayerWidth", DbType.Int32, column => column.NotNull().WithDefault(400))
                .Column("PhotoPlayerHeight", DbType.Int32, column => column.NotNull().WithDefault(300))
                .Column("BackgroundColor", DbType.String, column => column.NotNull().WithDefault("ffffff"))
                );

            return 1;
        }
    }
}

Указываем имена полей, типы данных, и значения по умолчанию, а также обязательность. Дальше создаём папку Services и в неё добавляем интерфейс IUppodService и класс, реализующий данный интерфейс UppodService. Данный класс содержит методы обновления и получения настроек плеера, а также метод, который будет вызываться на обработку шорткода uppod. Последний и представляет наибольший интерес:

public static string UppodRender(Dictionary<string, object> atts)
{
    string type = "video";
    if (atts.ContainsKey("type"))
        type = (string)atts["type"];

    string file = string.Empty;
    string playlist = string.Empty;
    if (atts.ContainsKey("files"))
    {
        file = (string)atts["files"];
        if (file.Contains(","))
        {
            playlist = "{ 'playlist' : [" + string.Join(", ", file.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(x=> "{" + string.Format(""file":"{0}"", x) + "}").ToList()) + "] }";
            file = string.Empty;
        }
    }

    var settings = GetSettings();

    int width = 0;
    int height = 0;
    string style = string.Empty;
    switch (type)
    {
        case "audio":
            width = settings.AudioPlayerWidth;
            height = settings.AudioPlayerHeight;
            style = settings.AudioPlayerStyle;
            break;
        case "video":
            width = settings.VideoPlayerWidth;
            height = settings.VideoPlayerHeight;
            style = settings.VideoPlayerStyle;
            break;
        case "photo":
            width = settings.PhotoPlayerWidth;
            height = settings.PhotoPlayerHeight;
            style = settings.PhotoPlayerStyle;
            break;
        default:
            break;
    }

    if(atts.ContainsKey("width"))
        width = (int)atts["width"];

   
    if (atts.ContainsKey("height"))
        height = (int)atts["height"];

    string Poster = string.Empty;
    if (atts.ContainsKey("poster"))
        Poster = (string)atts["poster"];

    string Comment = string.Empty;
    if (atts.ContainsKey("comment"))
        Comment = (string)atts["comment"];

    Random rnd = new Random();
    string playerId = "uplayer_" + rnd.Next(0, 10000);

    StringBuilder flashvars = new StringBuilder();
    flashvars.Append("{");
    flashvars.AppendFormat(""m":"{0}","
    + ""uid":"{1}","
    + ""{2}": "{3}""
    + "{4}"
    + "{5}"
    + "{6}", type, playerId, string.IsNullOrEmpty(file) ? "pl" : "file", string.IsNullOrEmpty(file) ? playlist.Replace(""", "'") : file,
    string.IsNullOrEmpty(style) ? "" : ", "st" : "" + style + """, string.IsNullOrEmpty(Poster) ? "" : string.Format(", "poster" : "{0}"", Poster),
    string.IsNullOrEmpty(Comment) ? "" : string.Format(", "comment" : "{0}"", Comment));
    flashvars.Append("}");

    StringBuilder flashparams = new StringBuilder();
    flashparams.Append("{");
    flashparams.AppendFormat("id:"{0}", "
    + "bgcolor:"#{1}", allowFullScreen:"true", allowScriptAccess:"always"", playerId, settings.BackgroundColor);
    flashparams.Append("}");

    StringBuilder htmlparams = new StringBuilder();
    htmlparams.Append("{");
    htmlparams.AppendFormat("m: "{0}", comment: "{1}", uid: "{2}", {3} : "{4}"", type, string.IsNullOrEmpty(Comment) ? "" : Comment,
        playerId, string.IsNullOrEmpty(file) ? "pl" : "file", string.IsNullOrEmpty(file) ? playlist.Replace(""", "'
") : file);
    htmlparams.Append("
}");

    string div = string.Format("
<div id="{0}" style="width:{1}px; height:{2}px;"></div>", playerId, width, height);
    string script = string.Format("
<script type="text/javascript">init("{0}", {1}, {2}, {3}, {4}, {5})</script>", playerId, flashvars, flashparams, htmlparams, width, height);

    return string.Format("
{0}<br/>{1}", div, script);
}

Данный метод добавляет в страницу вместо шорткода div и скрипт вызывающий инициализацию плеера. Сама функция инициализации (и другие) содержится в скрипте и выглядит следующим образом:

function initUppodFlashPlayer(playerId, flashvars, flashparams, width, height) {
    swfobject.embedSWF("/Modules/kosfiz.Uppod/Scripts/uppod.swf", playerId, width, height, "9.0.115", false, flashvars, flashparams);
}

function initHtml5Player(htmlparams) {
    var v = document.createElement('video');
    if (v.canPlayType('video/mp4')) {
        this.vplayer = new Uppod(htmlparams);
    }
    else { }

}
function init(playerId, flashvars, flashparams, htmlparams, width, height) {
    var playerVersion = swfobject.getFlashPlayerVersion();

    if (playerVersion.major >= 9)
        initUppodFlashPlayer(playerId, flashvars, flashparams, width, height);
    else
        initUppodHtml5Player(htmlparams);
}

Другие файлы необходимые для работы плеера добавляем в папку Scripts: uppod.js (html5-версия), uppod.swf (плеер), swfobject (вдруг ещё не добавлен), uppod_init.js (содержимое приведено выше).

Теперь надо добавить скрипты в код страницы и добавить шорткод uppod и метод для его обработки в словарь шорткодов. Для этого добавляем папку Handlers и в него класс UppodHandler:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Orchard.ContentManagement.Handlers;
using kosfiz.Uppod.Models;
using Orchard.Data;
using Orchard;
using Orchard.UI.Resources;
using Orchard.Caching;
using kosfiz.UppodServices;
using kosfiz.Shortcodes.Models;

namespace kosfiz.Uppod.Handlers
{
    public class UppodHandler: ContentHandler
    {
        readonly IWorkContextAccessor _workContextAccessor;
        public UppodHandler(IWorkContextAccessor workContextAccessor, IRepository<UppodSettingsRecord> repository, ICacheManager cacheManager, ISignals signals)
        {
            try
            {
                _workContextAccessor = workContextAccessor;
                var resourceManager = _workContextAccessor.GetContext().Resolve<IResourceManager>();
                var links = resourceManager.GetRegisteredLinks();
                bool uppod = false;
                bool swfobject = false;
                foreach (var link in links)
                {
                    if (link.Href.ToLower().Contains("uppod"))
                        uppod = true;
                    if (link.Href.ToLower().Contains("swfobject"))
                        swfobject = true;
                }

                if (!swfobject)
                    resourceManager.RegisterHeadScript("<script type="text/javascript" src="/Modules/kosfiz.Uppod/Scripts/swfobject-2.2.min.js"></script>");
                if (!uppod)
                {
                    resourceManager.RegisterHeadScript("<script type="text/javascript" src="/Modules/kosfiz.Uppod/Scripts/uppod.js"></script>");
                    resourceManager.RegisterHeadScript("<script type="text/javascript" src="/Modules/kosfiz.Uppod/Scripts/uppod_init.js"></script>");
                }
            }
            finally
            {
                ShortCodeService.AddShortCode("uppod", UppodService.UppodRender); //добавляем шорткод в список обрабатываемых, указывая в какой метод передавать параметры
                UppodService uppodService = new UppodService(repository, cacheManager, signals);
            }
        }
    }
}

На этом программирование основного функционала можно считать законченным, осталось лишь добавить код для внесения настроек модуля в админку.
Добавляем AdminMenu в корень проекта

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Orchard.UI.Navigation;
using Orchard.Localization;

namespace kosfiz.Uppod
{
    public class AdminMenu : INavigationProvider
    {
        public Localizer T { get; set; }

        public AdminMenu()
        {
            T = NullLocalizer.Instance;
        }

        public void GetNavigation(NavigationBuilder builder)
        {
            builder.Add(T("Uppod"), "49", menu => menu.Add(T("Uppod"), "0", item => item.Action("Index", "Admin", new { area = "kosfiz.Uppod" })));
        }

        public string MenuName
        {
            get { return "admin"; }
        }
    }
}

Осталось добавить контроллер и представление, соответственно:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using Orchard.UI.Admin;
using kosfiz.Uppod.ViewModels;
using kosfiz.Uppod.Services;
using Orchard;
using Orchard.Localization;

namespace kosfiz.Uppod.Controllers
{
    [ValidateInput(false), Admin]
    public class AdminController: Controller
    {
        private readonly IUppodService _uppodService;
       
        public IOrchardServices Services { get; set; }
        public Localizer T { get; set; }

        public AdminController(IOrchardServices services, IUppodService uppodService)
        {
            Services = services;
            _uppodService = uppodService;
        }

        [HttpGet]
        public ActionResult Index()
        {
            var m = _uppodService.Get();
            UppodSettingsViewModel model = new UppodSettingsViewModel { AudioPlayerHeight = m.AudioPlayerHeight, AudioPlayerStyle = m.AudioPlayerStyle, AudioPlayerWidth = m.AudioPlayerWidth, BackgroundColor = m.BackgroundColor, VideoPlayerHeight = m.VideoPlayerHeight, VideoPlayerStyle = m.VideoPlayerStyle, VideoPlayerWidth = m.VideoPlayerWidth, PhotoPlayerHeight = m.PhotoPlayerHeight, PhotoPlayerStyle = m.PhotoPlayerStyle, PhotoPlayerWidth = m.PhotoPlayerWidth };
            return View(model);
        }

        [HttpPost]
        public ActionResult Index(UppodSettingsViewModel model)
        {
            if (ModelState.IsValid)
            {
                _uppodService.Set(model.VideoPlayerStyle, model.VideoPlayerWidth, model.VideoPlayerHeight, model.AudioPlayerStyle, model.AudioPlayerWidth, model.AudioPlayerHeight, model.PhotoPlayerStyle, model.PhotoPlayerWidth, model.PhotoPlayerHeight, model.BackgroundColor);
                return RedirectToAction("Index");
            }
           
            return View();
        }
    }
}
@model kosfiz.Uppod.ViewModels.UppodSettingsViewModel

<h1>@Html.TitleForPage(@T("Manage Uppod settings").ToString())</h1>
@using (Html.BeginFormAntiForgeryPost())
{
    <fieldset>
    <table>
        <tr>
            <td>@T("Background color"):</td>
            <td>
                @Html.TextBoxFor(model=>model.BackgroundColor)
            </td>
        </tr>
        <tr>
            <td colspan="3">
                <h2>@T("Video")</h2>
            </td>
        </tr>
        <tr>
            <td>@T("Style"):</td>
            <td>
                @Html.TextBoxFor(model=>model.VideoPlayerStyle)
            </td>
        </tr>
        <tr>
            <td>@T("Width"):</td>
            <td>
                @Html.TextBoxFor(model=>model.VideoPlayerWidth)
            </td>
        </tr>
        <tr>
            <td>@T("Height"):</td>
            <td>
                @Html.TextBoxFor(model=>model.VideoPlayerHeight)
            </td>
        </tr>
        <tr>
            <td colspan="3">
                <h2>@T("Audio")</h2>
            </td>
        </tr>
        <tr>
            <td>@T("Style"):</td>
            <td>
                @Html.TextBoxFor(model=>model.AudioPlayerStyle)
            </td>
        </tr>
        <tr>
            <td>@T("Width"):</td>
            <td>
                @Html.TextBoxFor(model=>model.AudioPlayerWidth)
            </td>
        </tr>
        <tr>
            <td>@T("Height"):</td>
            <td>
                @Html.TextBoxFor(model=>model.AudioPlayerHeight)
            </td>
        </tr>
        <tr>
            <td colspan="3">
                <h2>@T("Photo")</h2>
            </td>
        </tr>
        <tr>
            <td>@T("Style"):</td>
            <td>
                @Html.TextBoxFor(model=>model.PhotoPlayerStyle)
            </td>
        </tr>
        <tr>
            <td>@T("Width"):</td>
            <td>
                @Html.TextBoxFor(model=>model.PhotoPlayerWidth)
            </td>
        </tr>
        <tr>
            <td>@T("Height"):</td>
            <td>
                @Html.TextBoxFor(model=>model.PhotoPlayerHeight)
            </td>
        </tr>

    </table>
    <input type="submit" value="@T("Save")" title="@T("Save")" />
    </fieldset>
}

Остаётся только собрать модуль.

Ссылка на модуль

Оба модуля успешно используются, к примеру, здесь: http://school286.ru/odod-voenno-patrioticheskoe

P.S.: CodeColorer «любезно» во вставках кода подтёр кое-что в тех местах, где идёт работа со строками, так что лучше смотреть код в модуле.

Orchard CMS: добавляем поддержку shortcodes

Программирование

Tagged Under : , ,

Shortcodes должны быть известны пользователям блогов на WordPress: с помощью шорткодов можно добавить свой функционал прямо в текст статьи. Это, на мой взгляд, необходимость.

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

Данной возможности в Orchard CMS по умолчанию не наблюдается: там всё по заранее настроенному порядку, т.е. один тип контента нельзя вставить внутрь другого, нельзя добавить несколько одинаковых типов контента к статье (допустим несколько галерей). Это, естественно, ограничивает разработчиков и, как следствие, конечных пользователей.

Итак, задача, которую нужно прежде всего решить, это программно переопределить Parts.Common.Body.cshtml таким образом, чтобы в ней обрабатывались шорткоды.

Пошагово процесс выглядит следующим образом:

  • Добавить класс, описывающий шорткод
  • Реализовать класс, через который можно будет добавлять шорткоды и методы их обрабатывающие. Реализовать в рамках класса простейший парсер шорткодов и отрисовку
  • Переопределить Parts.Common.Body.cshtml

Создадим модуль и откроем его проект в Visual Studio или другой среде разработки. В папку Modules добавляем класс ShortCodeItem со следующим содержимым:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace kosfiz.Shortcodes.Models
{
    public class ShortCodeItem
    {
        private string codeName = string.Empty;
        public string CodeName
        {
            get
            {
                return codeName;
            }
            set
            {
                codeName = value;
            }
        }

        private int startIndex;
        public int StartIndex
        {
            get
            {
                return startIndex;
            }
            set
            {
                startIndex = value;
            }
        }

        private int endIndex;
        public int EndIndex
        {
            get
            {
                return endIndex;
            }
            set
            {
                endIndex = value;
            }
        }

        public string TargetText
        {
            get;
            set;
        }

        public string SourceText
        {
            get;
            set;
        }
    }
}

На следующем шаге добавляем ShortCodeService, который и будет выполнять основные функции.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;

namespace kosfiz.Shortcodes.Models
{
    public class ShortCodeService
    {
        //здесь будем хранить все шорткоды и методы, их обрабатывающие
        static Dictionary<string, Func<Dictionary<string, object>, string>> Methods = new Dictionary<string, Func<Dictionary<string, object>, string>>();

        //метод добавления шорткода
        public static void AddShortCode(string ShortCodeName, Func<Dictionary<string, object>, string> method)
        {
            if (!Methods.ContainsKey(ShortCodeName)) //если шорткод уже есть, то игнорируем его
                Methods.Add(ShortCodeName, method);
        }

        //выполняем метод соответсвующий имени шорткода и передаём ему параметры, сторонний метод должен принимать на выход словарь строка = объект
        private static string DoMethod(string ShortCodeName, Dictionary<string, object> atts)
        {
            return Methods[ShortCodeName].Invoke(atts);
        }

        //из текста выбирает шорткоды
        private static List<ShortCodeItem> GetShortCodes(string Text)
        {
            List<ShortCodeItem> codes = new List<ShortCodeItem>();

            foreach (var item in Methods)
            {
                try
                {
                    string startTag = string.Format("[{0}", item.Key);
                    string endTag = string.Format("[/{0}]", item.Key);

                    int startIndex = Text.IndexOf(startTag);
                    while (startIndex != -1)
                    {
                        int endIndex = Text.IndexOf(endTag, startIndex);
                        if (endIndex != -1)
                        {
                            int paramsEndIndex = Text.IndexOf("]", startIndex);
                            if (paramsEndIndex != -1)
                            {
                                string source = Text.Substring(startIndex + startTag.Length, paramsEndIndex - startIndex - startTag.Length);
                                Dictionary<string, object> atts = GetValues(source, paramsEndIndex + 1, endIndex - paramsEndIndex - 1, Text);
                                codes.Add(new ShortCodeItem { CodeName = item.Key, StartIndex = startIndex, EndIndex = endIndex, TargetText = DoMethod(item.Key, atts), SourceText = Text.Substring(startIndex, endIndex + endTag.Length - startIndex) });
                            }
                        }
                        startIndex = Text.IndexOf(startTag, startIndex + 1);
                    }
                }
                catch (Exception) { }
            }

            return codes;
        }

        //формируем словарь параметров шорткода
        private static Dictionary<string, object> GetValues(string source, int InnerStart, int InnerEnd, string InnerText)
        {
            Dictionary<string, object> values = new Dictionary<string, object>();

            int paramNameIndex = source.IndexOf("=");
            int lastParamValueIndex = 0;
            while (paramNameIndex != -1)
            {
                string attrName = source.Substring(0, paramNameIndex);
                string attrValue = string.Empty;
                if (source[paramNameIndex + 1] != '"')
                {
                    lastParamValueIndex = source.IndexOf(" ", paramNameIndex + 1);
                    if (lastParamValueIndex == -1)
                        lastParamValueIndex = source.Length - 1;
                    attrValue = source.Substring(paramNameIndex + 1, lastParamValueIndex - paramNameIndex);
                }
                else
                {
                    lastParamValueIndex = source.IndexOf("\"", paramNameIndex + 2);
                    if (lastParamValueIndex == -1)
                        lastParamValueIndex = source.Length - 1;
                    attrValue = source.Substring(paramNameIndex + 2, lastParamValueIndex - paramNameIndex - 2);
                }

                values.Add(attrName.Trim(), attrValue.Trim());
                source = source.Remove(0, lastParamValueIndex);
                paramNameIndex = source.IndexOf("=");
            }
            //InnerHtml предопределённый параметр, содержит содержимое тегов шорткода [shortcodeName]InnerHtml[/shortcodeName]
            values.Add("InnerHtml", InnerText.Substring(InnerStart, InnerEnd).Trim());
            return values;
        }

        //заменяем теги шорткодов на html сгенерированный модулями
        public static MvcHtmlString Render(string Text)
        {
            List<ShortCodeItem> codes = GetShortCodes(Text);
            foreach (var item in codes)
                Text = Text.Replace(item.SourceText, item.TargetText);
            return MvcHtmlString.Create(Text);
        }
    }
}

Собственно, парсер обрабатывает шорткод теги вида:
[shortcodeName attr1=value attr2="value with whitespace or =" attr3=value2]different text[/shortcodeName]

Т.е. необходим закрывающий шорткод тег, значения с пробелом или = заключается в двойные кавычки. Содержимое между открывающим и закрывающим шорткодами передаётся во внешний метод по ключу InnerHtml.

Осталось переопределить Parts.Common.Body.cshtml. Для этого добавляем в папку Views проекта копию оригинального файла и меняем его содержимое на следующее:

@using kosfiz.Shortcodes.Models;
@{
    var body = ShortCodeService.Render(Model.Html.ToString());
}
@body

Далее добавляем в папку Models класс ShortCodeShapeProvider

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Orchard.DisplayManagement.Descriptors;
using Orchard;
using Orchard.ContentManagement;

namespace kosfiz.Shortcodes.Models
{
    public class ShortCodeShapeProvider : IShapeTableProvider
    {
        private readonly IWorkContextAccessor _workContextAccessor;
        public ShortCodeShapeProvider(IWorkContextAccessor workContextAccessor)
        {
            _workContextAccessor = workContextAccessor;
        }

        public void Discover(ShapeTableBuilder builder)
        {
            builder.Describe("Parts_Common_Body").OnDisplaying(displaying =>
            {
                ContentItem item = displaying.Shape.ContentItem;
                if (displaying.ShapeMetadata.DisplayType == "Detail")
                {
                    displaying.ShapeMetadata.Alternates.Add("Parts_Common_Body");
                }
            });
        }
    }
}

Вот собственно и всё. Пример создания модуля использующего шорткоды описан в статье «Ссылка на модуль

MS SQL Full-Text Search vs Sphinx

Программирование

Tagged Under : , , , , , , , ,

Собственно, к сравнению меня подвигла довольно длинная предыстория, если кратко, то в сети достаточно просто найти сравнения различных средств организации полнотекстового поиска, но я так и не увидел в кандидатах подобных «соревнований» полнотекстовый поиск от Microsoft (он же MS SQL Full-Text Search), который я успешно использую для решения задач.

Почему Sphinx? Я давно слышу об этой поисковой системе, различные реализации поиска на базе Sphinx мне многократно приводили в качестве примера того, как должен работать поиск на тех сайтах, в разработке которых я участвовал.

Итак, начнём. Прежде всего поставим задачу: нужно в таблице улиц найти все записи соответствующие запросам. Текст запроса может быть полным названием улицы или же частью названия.

Что имеется в наличии:

  • Машина с ОС Windows 7 Ultimate 64x;
  • ОЗУ: 2Гб, ЦП: Intel Core 2 Duo T5470 1.6ГГц;
  • MS SQL Server 2008 SP1;
  • База (citiesdb) с таблицей улиц (Streets), количество записей – 823276;
  • На таблице висит полнотекстовый индекс на поле Name (размер индекса 23 Мб).

Итак, прежде всего мне потребовалось установить сам Sphinx:

  • Качаем версию 2.0.1-beta (Win32 binaries w/MySQL+PgSQL+libstemmer+id64 support)
  • Распаковываем и содержимое переносим в папку c:sphinx (по крайней мере у меня такая)
  • Настраиваем конфиг
  • source cities
    {
        type            = mssql
        sql_host        = KOSFIZ-PC
        sql_user        = test
        sql_pass        = ******
        sql_db          = citiesdb
        sql_port        = 1433

        mssql_unicode       = 1

        sql_query       =
            SELECT [ID], [Name]
            FROM Streets
    }

    index StreetsIndex2
    {
        source          = cities
        path            = f:indexiesstreets2
        morphology      = stem_enru
        min_word_len        = 2
        charset_type        =  utf-8
        min_prefix_len      = 0
        enable_star     = 1
        html_strip      = 0
    }

    searchd
    {
            address                         = 127.0.0.1
        port                            = 3312
        log                             = c:sphinxlogsearchd.log
        query_log                       = c:sphinxlogquery.log
        read_timeout                    = 5
            max_children                    = 0
            pid_file                        = c:sphinxlogsearchd.pid
    }
  • Строим индекс: indexer.exe –config c:sphinxsphinx.conf.in –all (если сервис searchd уже установлен и запущен добавляем –rotate)
  • Устанавливаем сервис searchd: searchd.exe –install –config c:sphinxsphinx.conf.in –servicename sphinx
  • Запускаем сервис из консоли: net start sphinx

Размер индекса составил 15Мб с небольшим. Время ~ 6 с.

Для тестирования MS SQL Full-Text Search’а таблица была предварительно подготовлена: добавлен полнотекстовый индекс. И хранимая процедура:

CREATE PROCEDURE [dbo].[GetStreets]
    @query nvarchar(400)
AS
BEGIN
    select top 1000 ID, Name from Streets with(nolock)
    where CONTAINS(Name, @query)
END

Отмечу, что при тестировании выборка будет из 1000 строк. FREETEXT я решил не использовать поскольку использование * с ним невозможно, да и к CONTAINS я прикипел всей душой.

Основные методы, используемые в тесте, выглядят следующим образом:

private static void Sphinx(string query)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();

    using (ConnectionBase connection = new PersistentTcpConnection("localhost", 3312))
    {
        SearchQuery searchQuery = new SearchQuery(query);
        searchQuery.MatchMode = MatchMode.Extended2;
        searchQuery.Indexes.Add("StreetsIndex2");
        searchQuery.Limit = 1000;
        SearchCommand searchCommand = new SearchCommand(connection);
        searchCommand.QueryList.Add(searchQuery);
        searchCommand.Execute();
    }

    sw.Stop();
    results.Add(sw.ElapsedMilliseconds);
}

private static void MSFullText(string query)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    DataTable dt = null;

    using (SqlConnection conn = new SqlConnection(connectionString))
    {
        using (SqlCommand cmd = new SqlCommand())
        {
            cmd.Connection = conn;
            cmd.CommandText = "GetStreets";
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.AddWithValue("@query", query);
            SqlDataAdapter adapter = new SqlDataAdapter(cmd);
            dt = new DataTable();
            adapter.Fill(dt);
        }
    }

    sw.Stop();
    results.Add(sw.ElapsedMilliseconds);            
}

private static string PrepareString(string query)
{
    return string.Format("FORMSOF(INFLECTIONAL, {0})", query);
}

private static string PrepareStringWithWildcard(string query)
{
    return string.Format(""{0}"", query);
}

Для работы со Sphinx’ом использовал sphinx-dotnet-client.

Были проведены следующие тесты:

  • Получение результатов по полному названию улицы (для чистоты эксперимента проводилось 10 прогонов). Тестовая строка «ленина». Приведено среднее время за 1 вызов.
    MS SQL (CONTAINS) = 6,4 мс
    Sphinx = 182,4 мс
  • Получение результатов по части названия улицы (для чистоты эксперимента проводилось 10 прогонов). Тестовая строка «ленинск*». Приведено среднее время за 1 вызов.
    MS SQL (CONTAINS) = 4,5 мс
    Sphinx = 189,3 мс
  • Получение результатов по полному названию улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, поток 1. Набор из 10 строк предопределён.
    MS SQL (CONTAINS) = 6,3 мс
    Sphinx = 23,6 мс
  • Получение результатов по части названия улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, поток 1. Набор из 10 строк предопределён.
    MS SQL (CONTAINS) = 7,9 мс
    Sphinx = 23,4 мс
  • Получение результатов по полному названию улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, потоков 10. Набор из 10 строк предопределён.
    MS SQL (CONTAINS) = 43 мс
    Sphinx = 41,5 мс
  • Получение результатов по части названия улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, потоков 10. Набор из 10 строк предопределён.
    MS SQL (CONTAINS) = 30,6 мс
    Sphinx = 40,9 мс
  • Получение результатов по полному названию улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, потоков 10. На каждом вызове строка случайна из набора в 10000 строк, набор состоит из названий улиц, имеющихся в базе.
    MS SQL (CONTAINS) = 41,7 мс
    Sphinx = 44,2 мс
  • Получение результатов по части названия улицы (для чистоты эксперимента проводилось 10 прогонов). Приведено среднее время за 10 вызов, потоков 10. На каждом вызове строка случайна из набора в 10000 строк, набор состоит из названий улиц с добавлением * в конец слова, имеющихся в базе.
    MS SQL (CONTAINS) = 43 мс
    Sphinx = 47 мс

Проведём анализ полученных в ходе тестирования результатов.
В тестах 1 и 2 Sphinx проявил себя крайне плохо, вызвано это тем, что при первом запросе файл индекса помещается в память, т.е. попросту кешируется, но на это требуется значительное время. Хранимая процедура, используемая для Full-Text Search’а, будучи вызванной однажды уже скомпилирована и план запроса закэширован (правда сколько план выполнения пролежит в кэше заранее неизвестно), поэтому на вызов и получение данных затрачено минимальное время.

В тестах 3 и 4 явно видно снижение среднего времени запросов за результатами к Sphinx’у, что объясняется длительным первым запросом и быстрыми остальными. CONTAINS держится на том же временном уровне.

Оставшиеся тесты явно свидетельствуют о том, что при увеличении числа потоков начинает деградировать величина скорость обработки запросов, т.е. поиск начинает подтормаживать. Причём деградация наблюдается и для MS SQL Full-Text Search’а и для Sphinx’а. На коэффициент разный, скорость поиска у первого более сильно зависит от кол-ва потоков, примерно в два раза.

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

И в тот момент, когда я уже начал смотреть в сторону Sphinx’а как в сторону поискового движка будущих проектов мне в голову пришла простая идея: раз полнотекстовый поиск от MS начинает загинаться при больших кол-вах одновременных запросов, но при этом показывает хорошие результаты, выполняя их по очереди, то почему бы не реализовать своеобразную многозадачность по известному сценарию: выполнять запросы от всех потоков по очереди, т.е. сначала один запрос от первого потока, потом от второго, затем третьего и т.д., потом по второму кругу, третьему и до конца.

В итоге такой подход оправдал себя: среднее время на 10 запросов вернулось к значениям 1 – 4 тестов. Добился я этого изменив метод MSFullText следующим образом:

static object locker = new object();
private static void MSFullText(string query)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    DataTable dt = null;

        lock(locker)
    using (SqlConnection conn = new SqlConnection(connectionString))
    {
        using (SqlCommand cmd = new SqlCommand())
        {
            cmd.Connection = conn;
            cmd.CommandText = "GetStreets";
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.AddWithValue("@query", query);
            SqlDataAdapter adapter = new SqlDataAdapter(cmd);
            dt = new DataTable();
            adapter.Fill(dt);
        }
    }

    sw.Stop();
    results.Add(sw.ElapsedMilliseconds);            
}

Поступив аналогично для случая с Sphinx’ом, тоже удастся сократить среднее время поиска и приблизить его к среднему времени выдаваемому полнотекстовым поиском от MS.

Собственно, на этом я прекратил тестирование.

Итак, для чего же я для себя вынес из данного сравнения:

  • Полнотекстовый SQL-поиск от MS при возрастании кол-ва одновременных потоков начинает сильно деградировать в скорости получения результатов, чуть меньше деградирует Sphinx. Но последний хак относительно многопоточности показывает обе технологии поиска в выгодном свете.
  • Для редких одиночных запросов использовать Sphinx нет смысла, ибо он будет уступать полнотекстовому SQL-поиску от MS даже в случае, если план запроса не кеширован (100-140 мс);
  • Использовать поиск от MS удобно тем, что последние изменения сразу же попадают в индекс при включенном Change Tracking’е, а в случае со Sphinx’ом надо делать какое-то расписание или надстройку по отслеживанию изменений в таблице;
  • Sphinx криво работает с *. Например, по строке «лен*» ничего не найдёт, хотя в конфиге явно прописано min_prefix_len = 0. Зато для «ленинск*» отрабатывает корректно;
  • Оба метода поиска требуют предварительной обработки строки поиска, например, Sphinx падает при наличии в строке поиска /. Для учёта морфологии CONTAINS’у нужно прописать FORMSOF и INFLECTIONAL;
  • 6. При 40 потоках полнотекстовый поиск от MS сильно затормозил, Sphinx оборвал соединения и не принимал новые;

  • Sphinx при нагрузке загружает ЦП на 100% при этом много времени начинает отъедать ядро ОС, с MS SQL Full-Text Search ситуация совсем другая, что не может не радовать.


    Первый пик – MS SQL Full-Text Search, второй – Sphinx.
    Для наблюдательных отмечу: да, вы не ошиблись Sphinx пробежался быстрее и среднее время запроса составило 3 мс в то время как MS SQL Full-Text Search отработал в среднем за 5 мс на запрос. Общее кол-во запросов 4000, что соответствует интервалу времени 12-20 секунд.

Простейшая реализация long polling в WCF-сервисах на примере текстовой трансляции

Программирование

Tagged Under : , , , , ,

Собственно, про long polling я узнал от своего коллеги Ильи Кубаря. Long polling весьма простое в реализации решение для осуществления реалтайм обновлений на клиентской стороне, например, в чатах или например в текстовых трансляциях футбольных матчей.
Суть метода состоит в следующем:

  • с клиента посылается на сервер AJAX-запрос;
  • на серверной части если данные обновились, то отправляется ответ, если нет – ответ не отправляется до тех пор пока нет обновлений;
  • на клиенте и на сервере выставляется таймаут.

Long polling по сравнению с polling’ом имеет значительные плюсы:

  • данные действительно обновляются моментально
  • уменьшается кол-во запросов с клиента к серверу (не страшно, что они дольше выполняются)

Итак, рассмотрим в качестве простейшего примера прототип текстовой трансляции совместив на одной странице список сообщений и их добавление (сделаем прототип страницы управления так сказать), а также дополнительно посмотрим как сделать wcf-сервис так сказать RESTful-сервисом.
Создадим WCF Service Application и переименуем дефолтный интерфейс к ILongPollingService со следующим содержанием:

[ServiceContract(SessionMode = SessionMode.NotAllowed)] // сессия нам не нужна
public interface ILongPollingService
{
    [OperationContract]
    [WebGet(ResponseFormat = WebMessageFormat.Json)] //метод доступен при обращении по http
    List<Message> PushData(string msg); //новое сообщение от ведущего трансляции

    [OperationContract]
    [WebGet(ResponseFormat = WebMessageFormat.Json)] //метод доступен при обращении по http
    List<Message> GetData(string ID); // ID - это идентификатор последнего сообщения полученного клиентом
}

Далее опишем класс реализующий интерфейс ILongPollingService:

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] //нужно дя поддержки REST'а
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.PerCall)]
public class Service : ILongPollingService
{
    static List<Message> list = new List<Message>(); //здесь храним сообщения трансляции

    DateTime end; // время когда нужно выдать ответ

    public List<Message> PushData(string msg) //метод добавляющий сообщения
    {
        string id = Guid.NewGuid().ToString(); //создаём идентификатор
        list.Add(new Message { ID = id, MessageBody = msg }); // добавляем сообщение в список
        return list;
    }

    public List<Message> GetData(string ID) //метод, возвращающий текст трансляции
    {
        if (list.Count > 0)
            if (list[list.Count - 1].ID == ID) //если у клиента то же сообщение, что и последнее в списке добавленных, ждём
            {
                end = DateTime.Now.AddMinutes(2); //через две минуты отдаём то, что есть, чтобы если что, очередь запросов двигалась
                Wait(ID); // ждём
            }
        return list;
    }

    private void Wait(string ID)
    {
        while (list[list.Count - 1].ID == ID && DateTime.Now < end)
        {
            System.Threading.Thread.Sleep(1);
        };
    }
}

Принцип я думаю понятен: на каждый запрос создаётся отдельный экземпляр сервиса обрабатывающий запрос и отдающий данные. Список сообщений доступен всем экземплярам поэтому все могут отследить изменения.
Ну, и напоследок, класс сообщений выглядит так:

[DataContract]
public class Message
{
    [DataMember]
    public string ID
    {
        get;
        set;
    }

    [DataMember]
    public string MessageBody
    {
        get;
        set;
    }
}

Для поддержки REST нужно внести кое-какие настройки в кофиг, например:

<system.serviceModel>
    <services>
      <service name="LongPollingService.Service" behaviorConfiguration="LongPollingService.ServiceBehavior">
        <endpoint address="/json" binding="webHttpBinding" contract="LongPollingService.ILongPollingService" behaviorConfiguration="LongPollingServiceJsonBehavior"/>
      </service>
    </services>
    <bindings>
    </bindings>
    <behaviors>
      <endpointBehaviors>
        <behavior name="LongPollingServiceJsonBehavior">
          <webHttp/>
        </behavior>
      </endpointBehaviors>
      <serviceBehaviors>
        <behavior name="LongPollingService.ServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

И в разметку LongPollingService.svc добавить Factory:

<%@ ServiceHost Language="C#" Debug="false" Service="LongPollingService.Service" CodeBehind="LongPollingService.svc.cs" Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory" %>

Этих простых шагов достаточно для добавления поддержки REST.

Ну. а код страницы базируется на паре js-функций и очень прост:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LongPollingService.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="jquery-1.6.4.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        var id = '';

        $(document).ready(function () {
            getmsgs();
        });

        function getmsgs() {
            $.ajax({
                async: true,
                url: '/LongPollingService.svc/json/GetData',
                data: 'ID=' + id,
                type: 'GET',
                dataType: 'json',
                timeout: 130000, //ставим времени больше двух минут, а то будут Aborted-ответы, а нам нужны 200
                success: function (data) {
                    var str = '';
                    for (var index = 0; index < data.length; index++) {
                        str += data[index].MessageBody + '<br/>';
                        id = data[index].ID;
                    }
                    $("#msgs").html(str);
                },
                complete: getmsgs
            });
        }

        function send() {
            $.ajax({
                async: true,
                url: '/LongPollingService.svc/json/PushData',
                data: 'msg=' + $("#txt").val(),
                type: 'GET',
                dataType: 'json',
                success: function (data) {
                    var str = '';
                    for (var index = 0; index < data.length; index++) {
                        str += data[index].MessageBody + '<br/>';
                        id = data[index].ID;
                    }
                    $("#msgs").html(str);
                }
            });
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <div id="msgs">
    </div>
    <input type="text" id="txt" />
    <input type="button" value="send" onclick="send(); return false;" />
    </div>
    </form>
</body>
</html>

CodeBehind абсолютно пуст, поэтому его не привожу.

Исходный код примера можно скачать.

P.S.: возможно в WCF есть встроенная поддержка long polling, но замарачиваться на этот счёт я не стал.

Определение мобильных устройств на ASP.NET-сайтах

Программирование

Tagged Under : , , ,

Некоторое время назад появилась необходимость заменить стандартный механизм определения мобильного устройства на ASP.NET-сайте. Повод был серьёзный, данный механизм отрабатывал весьма медленно и к, сожалению, не определял многие модели мобильных устройств.
В качестве замены была выбрана библиотека FiftyOne.Foundation.

Принцип работы данной библиотеки прост: при запуске приложения, происходит замена стандартного BrowserCapabilitiesProvider на MobileCapabilitiesProvider (делается это через HttpModule). В момент инициализации из файла с устройствами wurfl.xml наполняется коллекция устройств, их свойств и хендлеров им сопоставленных, учитывая, что файл весьма немаленький (~16Мб) весь процесс может занимать значительное время от 5 до 15 секунд (во время данного процесса значительное время отнимают регулярки: очень много вызовов), но в последующем всё отрабатывает очень быстро по сравнению со стандартным механизмом.

Собственно, подключение нового механизма очень хорошо описано на сайте библиотеки, но всё же опишу основные шаги для случая, когда не надо перенаправлять пользователя и отправлять данные о неопределённом устройстве:
1. Добавляем секцию и её описание в web.config

<sectionGroup name="fiftyOne">
  <section name="log" type="FiftyOne.Foundation.Mobile.Configuration.LogSection, FiftyOne.Foundation" requirePermission="false" allowDefinition="Everywhere" restartOnExternalChanges="false" allowExeDefinition="MachineToApplication"/>
  <section name="redirect" type="FiftyOne.Foundation.Mobile.Configuration.RedirectSection, FiftyOne.Foundation" requirePermission="false" allowDefinition="Everywhere" restartOnExternalChanges="false" allowExeDefinition="MachineToApplication"/>
  <section name="wurfl" type="FiftyOne.Foundation.Mobile.Detection.Wurfl.Configuration.WurflSection, FiftyOne.Foundation" requirePermission="false" allowDefinition="Everywhere" restartOnExternalChanges="false" allowExeDefinition="MachineToApplication"/>
</sectionGroup>

<skip />

<fiftyOne>
<log logFile="~/App_Data/Log.txt"
     logLevel="Info"/>
<wurfl wurflFilePath="~/App_Data/wurfl.xml.gz"
       useActualDeviceRoot="false">      
  <wurflPatches>
    <add name="browser_definitions"
           filePath="~/App_Data/web_browsers_patch.xml"
           enabled="true"/>
  </wurflPatches>
</wurfl>
</fiftyOne>

Описание элементов можно посмотреть здесь.
2. Добавляем модуль:

<httpModules>
  <add name="Detector" type="FiftyOne.Foundation.Mobile.Detection.DetectorModule, FiftyOne.Foundation"/>
</httpModules>

<modules>
  <remove name="Detector"/>
  <add name="Detector" type="FiftyOne.Foundation.Mobile.Detection.DetectorModule, FiftyOne.Foundation"/>
</modules>

3. В App_Data кладём wurfl.xml.gz и web_browsers_patch.xml (он нужен если у browser_definitions параметр enabled = true, если нет, то он не понадобится)

4. Кладём сборку FiftyOne.Foundation 4 в bin.

Этого вполне достаточно, чтобы подменить стандартный механизм. После этого мобильные устройства начнут определяться быстрее и точнее, в том числе будут определяться модели устройств, которые не определялись ранее. Например, для user agent’а: Mozilla/5.0 (SymbianOS/9.1; U; [en-us]) AppleWebKit/413 (KHTML, like Gecko) Safari/413 ранее выводилось Unknown, после подключения FiftyOne.Foundation определяется устройство Nokia и модель N73, а также что пользователь зашёл с мобильного устройства.

На последок хочу заметить, что пока заметил только один заметный баг: для user agent’а Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) (что согласно UserAgent Switcher’у соответствует гугл боту) свойство Request.Browser.Crawler возвращает false, что неправильно. Всё возвращается на свои места с отключением browser_definitions (enabled = false) при этом мобильные устройства всё также отлично определяются.

Сравниваем xml-парсеры под .NET

Программирование

Tagged Under : , , , ,

В принципе, скорость работы классов для работы с XML уже сравнивалась и я ничего нового не открою и не покажу. Необходимость же провести данное сравнение возникла в результате оптимизации библиотеки для определения мобильных устройств. Данные об устройствах лежат в xml-файле размеров в 16 с небольшим мегабайт. При разборе xml’я использовался XmlReader, который в принципе должен отрабатывать всегда быстрее, поскольку тот же LINQ2XML это надстройка над XmlReader’ом. Но каким-то загадочным образом (на данный момент непонятно, возможно что-то пропустил) у меня вышло, что последний (LINQ2XML) отрабатывал быстрее на ~100 мс и я решил сделать контрольный замер для следующих парсеров: XmlReader, XmlDocument, XElement (LINQ2XML), VTD-XML (был посоветован как супер быстрый).

Следовательно, в качестве подопытного файла использовался тот же самый wurfl.xml из FiftyOne.Foundation.
Суть состоит в том, что необходимо пробежаться по всему файлу и получить значения аттрибутов для всех устройств и их свойств. Вот методы реализующие данный функционал:

static void LinqToXmlWork(string fileName)
{
    using (StreamReader xml = new StreamReader(fileName))
    {
        XElement root = XElement.Load(xml, LoadOptions.None);
        var devices = root.Descendants("device");
        foreach (XElement device in devices)
        {
            string id = device.Attribute("id").Value;
            string userAgent = device.Attribute("user_agent").Value;
            string fallback = device.Attribute("fall_back").Value;
            if (!device.IsEmpty)
                foreach (XElement capability in device.Descendants("capability"))
                {
                    string name = capability.Attribute("name").Value;
                    string value = capability.Attribute("value").Value;
                }
        }
    }
}

static void XmlReaderWork(string fileName)
{
    using (StreamReader xml = new StreamReader(fileName))
    {
        XmlReader reader = XmlReader.Create(xml);
        while (reader.Read())
        {
            switch (reader.Name)
            {
                case "device":
                    if (reader.IsStartElement())
                    {
                        string id = reader.GetAttribute("id", string.Empty);
                        string userAgent = reader.GetAttribute("user_agent", string.Empty);
                        string fallback = reader.GetAttribute("fall_back", string.Empty);
                    }
                    break;

                case "capability":
                    if (reader.IsStartElement())
                    {
                        string name = reader.GetAttribute("name", string.Empty);
                        string value = reader.GetAttribute("value", string.Empty);
                    }
                    break;
            }
        }
    }
}

static void XmlDocumentWork(string fileName)
{
    using (StreamReader xml = new StreamReader(fileName))
    {
        XmlDocument xmlDocument = new XmlDocument();
        xmlDocument.Load(xml);
        XmlNodeList deviceList = xmlDocument.SelectNodes("/wurfl/devices/device");
        foreach (XmlNode device in deviceList)
        {
            string id = device.Attributes["id"].Value;
            string userAgent = device.Attributes["user_agent"].Value;
            string fallback = device.Attributes["fall_back"].Value;
            XmlNodeList groups = device.ChildNodes;
            foreach (XmlNode group in groups)
            {
                XmlNodeList capabilities = group.ChildNodes;
                foreach (XmlNode capability in capabilities)
                {
                    string name = capability.Attributes["name"].Value;
                    string value = capability.Attributes["value"].Value;
                }
            }
        }
    }
}

static void VTDXmlWork(string fileName)
{
    VTDGen vg = new VTDGen();
    if (vg.parseFile(fileName, true))
    {
        VTDNav vn = vg.getNav();
        AutoPilot ap = new AutoPilot();
        ap.bind(vn);
        ap.selectXPath("/wurfl/devices/device");
        while (ap.evalXPath() != -1)
        {
            AutoPilot attrPilot = new AutoPilot(vn);
            attrPilot.selectAttr("*");
            int i = attrPilot.iterateAttr();
            string id = vn.toString(i+1);
            i = attrPilot.iterateAttr();
            string userAgent = vn.toString(i + 1);
            attrPilot.selectAttr("fall_back");
            i = attrPilot.iterateAttr();
            string fallback = vn.toString(i + 1);
           
            if (vn.toElement(VTDNav.FIRST_CHILD))
            {
                do
                {
                    if (vn.toElement(VTDNav.FIRST_CHILD))
                    {
                        do
                        {
                            AutoPilot attrs = new AutoPilot(vn);
                            attrs.selectAttr("*");
                            i = attrs.iterateAttr();
                            string name = vn.toString(i + 1);
                            i = attrs.iterateAttr();
                            string value = vn.toString(i + 1);
                        } while (vn.toElement(VTDNav.NEXT_SIBLING));
                    }
                    vn.toElement(VTDNav.PARENT);
                } while (vn.toElement(VTDNav.NEXT_SIBLING));
                vn.toElement(VTDNav.PARENT);
            }
        }
    }
}

В результате получилось следующее (тест запускался 10 раз):

XmlReader 497 мс
XElement 1572 мс
XmlDocument 2218 мс
VTD-XML 1100 мс

Отмечу, что в релизе библиотека VTD-XML так и не захотела работать, так что результат взят для debug. Ещё одна особенность в том, что когда идёт непосредственно отладка в студии, то результаты остаются пропорциональны для первых трёх парсеров, т.е. время увеличивается для всех, но места по скорости не поменяются, а вот VTD-XML сдвигается на последнее место с результатов ~2500 мс.

По читабельности на мой взгляд код для XElement, XmlReader и XmlDocument одинаковы, а вот для VTD-XML всё выглядит просто ужасно. По компактности на мой субъективный взгляд получше выглядит код для XElement.

Замеры производились на ноутбуке: с ОС Windows 7 Ultimate SP1 x64, ОЗУ 2Гб, ЦП Core2 DUO T5470 1.6Гц x 2.

Завтра приведённое тестирование проведу на машине, на которой наблюдался странный эффект с XmlReader’ом и XElement’ом.