Поставщик карт сайта ASP.NET MVC ломается при большой нагрузке

У меня проблема с поставщиком карты сайта ASP.NET MVC, и это вызывает у меня много головной боли. Проблема возникает, когда сервер находится под большой нагрузкой, URL-адрес разрешается неправильно. Я только что обновился до последней версии (3.1.0 RC), где я надеялся, что это будет исправлено, но, к сожалению, это не так.

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

[TestMethod]
public void ForumTopic_Breadcrumb() {
    // Arrange
    var isValid = true;

    for (var i = 0; i < 100; i++) {
        try {
            // Send the request
            var request = Http.WebRequest("http://www.mysite.com/Forum/ViewTopic/38044"); // Http.WebRequest is a utility method to send a request to the server and retrieve the content (Note: now the question has been answered i have remove the reference to the actual site)

            // Parse the html document
            var document = new HtmlDocument(); // HTML Agility Pack
            document.LoadHtml(request.Data);

            // Get the required info
            var forumNode = document.DocumentNode.SelectSingleNode("//div[@id='breadcrumb']/a[@href='/Forum/ViewForum/1']");
            var topicNode = document.DocumentNode.SelectSingleNode("//div[@id='breadcrumb']/span");

            // Test if the info is valid
            if (forumNode == null || topicNode.InnerText != "Test Topic")
                throw new Exception();
        } catch {
            isValid = false;
            break;
        }
    }

    // Asset
    Assert.IsTrue(isValid);
}

Этот тест не проходит, так как часто отображается неправильная навигационная цепочка и/или заголовок.

В моем методе действия ViewTopic есть следующий код:

// Override the parent node title and url
SiteMap.CurrentNode.ParentNode.Title = topic.Forum.ForumName;
SiteMap.CurrentNode.ParentNode.Url = Url.GenerateUrl("ViewForum", new { id = topic.Forum.ForumID });

// Set the meta description
SiteMap.CurrentNode["MetaDescription"] = topic.Subject;

А также применение атрибута SiteMapTitle для изменения заголовка текущего узла на тему темы.

Я был бы очень признателен, если бы вы могли помочь. Спасибо


person nfplee    schedule 05.08.2011    source источник


Ответы (1)


Я получил ответ от разработчика, в котором говорится, что это связано с ограничением ASP.NET. Он надеется удалить это в версии 4, полностью выделив эту функциональность. До тех пор лучшим решением будет переопределить заголовок, передав его как ViewData.

Изменить (ниже приведено временное решение, которое я придумал):

Создайте следующий файл SiteMapAttribute.cs:

public class SiteMapAttribute : ActionFilterAttribute {
    protected string TitlePropertyName { get; set; }
    protected string PageTitlePropertyName { get; set; }
    protected string MetaDescriptionPropertyName { get; set; }
    protected string MetaKeywordsPropertyName { get; set; }
    protected string ParentTitlePropertyName { get; set; }

    public SiteMapAttribute() {
    }

    public SiteMapAttribute(string titlePropertyName) {
        TitlePropertyName = titlePropertyName;
    }

    public SiteMapAttribute(string titlePropertyName, string pageTitlePropertyName, string metaDescriptionPropertyName, string metaKeywordsPropertyName)
        : this(titlePropertyName) {
        PageTitlePropertyName = pageTitlePropertyName;
        MetaDescriptionPropertyName = metaDescriptionPropertyName;
        MetaKeywordsPropertyName = metaKeywordsPropertyName;
    }

    public SiteMapAttribute(string titlePropertyName, string pageTitlePropertyName, string metaDescriptionPropertyName, string metaKeywordsPropertyName, string parentTitlePropertyName)
        : this(titlePropertyName, pageTitlePropertyName, metaDescriptionPropertyName, metaKeywordsPropertyName) {
        ParentTitlePropertyName = parentTitlePropertyName;
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        if (filterContext.Result is ViewResult) {
            var result = (ViewResult)filterContext.Result;

            // Get the current node
            var currentNode = filterContext.Controller.GetCurrentSiteMapNode();

            // Make sure the node is found
            if (currentNode != null) {
                // Set the title and meta information (if applicable)
                if (!result.ViewData.ContainsKey("Title"))
                    result.ViewData["Title"] = Resolve(result, TitlePropertyName) ?? currentNode.Title;

                if (!result.ViewData.ContainsKey("PageTitle"))
                    result.ViewData["PageTitle"] = Resolve(result, PageTitlePropertyName) ?? (currentNode["PageTitle"] ?? currentNode.Title);

                if (!result.ViewData.ContainsKey("MetaDescription"))
                    result.ViewData["MetaDescription"] = Resolve(result, MetaDescriptionPropertyName) ?? currentNode["MetaDescription"];

                if (!result.ViewData.ContainsKey("MetaKeywords"))
                    result.ViewData["MetaKeywords"] = Resolve(result, MetaKeywordsPropertyName) ?? currentNode["MetaKeywords"];

                if (!result.ViewData.ContainsKey("ParentTitle"))
                    result.ViewData["ParentTitle"] = Resolve(result, ParentTitlePropertyName);
            }
        }
    }

    private string Resolve(ViewResult result, string propertyName) {
        if (string.IsNullOrEmpty(propertyName))
            return null;

        var target = ResolveTarget(result.ViewData.Model, propertyName);

        if (target == null)
            target = ResolveTarget(result.ViewData, propertyName);

        return target != null ? target.ToString() : null;
    }

    private object ResolveTarget(object target, string expression) {
        try {
            var parameter = Expression.Parameter(target.GetType(), "target");
            var lambdaExpression = DynamicExpression.ParseLambda(new[] { parameter }, null, "target." + expression);
            return lambdaExpression.Compile().DynamicInvoke(target);
        } catch {
            return null;
        }
    }
}

Затем вам нужно убедиться, что этот атрибут применяется ко всем вашим контроллерам. В ASP.NET MVC 3 это намного проще, так как вы можете зарегистрировать это как глобальный фильтр.

Теперь вам нужно изменить главную страницу и сказать что-то вроде:

<head>
    <title><%= ViewData["PageTitle"] %></title>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <% if (ViewData["MetaDescription"] != null && !string.IsNullOrEmpty(ViewData["MetaDescription"].ToString())) { %>
        <meta name="Description" content="<%= ViewData["MetaDescription"] %>" />
    <% } %>
    <% if (ViewData["MetaKeywords"] != null && !string.IsNullOrEmpty(ViewData["MetaKeywords"].ToString())) { %>
        <meta name="Keywords" content="<%= ViewData["MetaKeywords"] %>" />
    <% } %>
</head>

Теперь, чтобы исправить хлебные крошки (SiteMapPath), мне пришлось немного повозиться. Сначала я создал свой собственный помощник:

/// <summary>
/// MvcSiteMapHtmlHelper extension methods
/// </summary>
public static class SiteMapBreadcrumbExtensions {
    /// <summary>
    /// Source metadata
    /// </summary>
    private static Dictionary<string, object> SourceMetadata = new Dictionary<string, object> { { "HtmlHelper", typeof(SiteMapBreadcrumbExtensions).FullName } };

    /// <summary>
    /// Gets SiteMap path for the current request
    /// </summary>
    /// <param name="helper">MvcSiteMapHtmlHelper instance</param>
    /// <returns>SiteMap path for the current request</returns>
    public static MvcHtmlString SiteMapBreadcrumb(this MvcSiteMapHtmlHelper helper) {
        return SiteMapBreadcrumb(helper, null);
    }

    /// <summary>
    /// Gets SiteMap path for the current request
    /// </summary>
    /// <param name="helper">MvcSiteMapHtmlHelper instance</param>
    /// <param name="templateName">Name of the template.</param>
    /// <returns>SiteMap path for the current request</returns>
    public static MvcHtmlString SiteMapBreadcrumb(this MvcSiteMapHtmlHelper helper, string templateName) {
        var model = BuildModel(helper, helper.Provider.CurrentNode);

        return helper
            .CreateHtmlHelperForModel(model)
            .DisplayFor(m => model, templateName, new { Title = helper.HtmlHelper.ViewData["Title"], ParentTitle = helper.HtmlHelper.ViewData["ParentTitle"], ParentUrl = helper.HtmlHelper.ViewData["ParentUrl"] });
    }

    /// <summary>
    /// Builds the model.
    /// </summary>
    /// <param name="helper">The helper.</param>
    /// <param name="startingNode">The starting node.</param>
    /// <returns>The model.</returns>
    private static SiteMapPathHelperModel BuildModel(MvcSiteMapHtmlHelper helper, SiteMapNode startingNode) {
        // Build model
        var model = new SiteMapPathHelperModel();
        var node = startingNode;

        while (node != null) {
            var mvcNode = node as MvcSiteMapNode;

            // Check visibility
            var nodeVisible = true;

            if (mvcNode != null)
                nodeVisible = mvcNode.VisibilityProvider.IsVisible(node, HttpContext.Current, SourceMetadata);

            // Check ACL
            if (nodeVisible && node.IsAccessibleToUser(HttpContext.Current))
                model.Nodes.Add(SiteMapNodeModelMapper.MapToSiteMapNodeModel(node, mvcNode, SourceMetadata));

            node = node.ParentNode;
        }

        model.Nodes.Reverse();

        return model;
    }
}

Единственная разница между этим и встроенным в том, что он передает ViewData в шаблон. Затем, наконец, я создал 2 DisplayTemplates:

SiteMapNodeModel.axcx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MvcSiteMapProvider.Web.Html.Models.SiteMapNodeModel>" %>
<% if (Model.IsCurrentNode) { %>
    <span class="orange-text"><%= (ViewData["Title"] ?? Model.Title).ToString() %></span>
<% } else if ((bool)ViewData["IsParent"]) { %>
    <a href="<%= (ViewData["ParentUrl"] ?? Model.Url).ToString() %>"><%= (ViewData["ParentTitle"] ?? Model.Title).ToString() %></a>
<% } else { %>
    <a href="<%= Model.Url %>"><%= Model.Title %></a>
<% } %>

И SiteMapPathHelperModel.ascx:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MvcSiteMapProvider.Web.Html.Models.SiteMapPathHelperModel>" %>
<% for (var i = 0; i < Model.Nodes.Count(); i++) { %>
    <%= Html.DisplayFor(m => Model.Nodes[i], new { isParent = Model.Count() - 2 == i }) %>
    <% if (Model.Nodes[i] != Model.Last()) { %>
        &gt;
    <% } %>
<% } %>

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

<%= Html.MvcSiteMap().SiteMapBreadcrumb() %>

Теперь, когда это сделано, вам просто нужно знать, как переопределить мета-информацию/хлебные крошки для конкретного действия. Самый простой способ сделать это — переопределить SiteMapAttribute для определенного действия, например.

[SiteMap("Subject", "Subject", "Subject", "", "Forum.ForumName")]
public ActionResult ViewTopic(int id, [DefaultValue(1)] int page) {
}

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

ViewData["Title"] = "My Title - " + DateTime.UtcNow.ToShortDateString();

Надеюсь это поможет.

person nfplee    schedule 05.08.2011