Разработка динамического sitemap

Что такое sitemap, наверное знают все, кто не знает - может почитать тут или тут. Итак начнем, для начала создадим пару тестов, для вспомогательных методов:
[Fact]
public void XDocumentEquals() {
XDocument expected = XDocument.Parse(@"<someTag>A</someTag>");
XDocument actual = XDocument.Parse(@"<someTag>A</someTag>");
Equal(expected, actual);
}


[Fact]
public void XDocumentNotEquals() {
XDocument expected = XDocument.Parse(@"<someTag>A</someTag>");
XDocument actual = XDocument.Parse(@"<someTag>B</someTag>");
Assert.Throws<EqualException>(() => Equal(expected, actual));
}


И затем реализуем этот метод, он нам понадобиться для сравнения экземпляров класса XDocument.
public static void Equal(XObject expected, XObject actual) {
Assert.Equal(expected.ToString(), actual.ToString());
}


При форматировании XDocument в строку, по умолчанию стоит кодировка “utf-16”, а нам бы хотелось “utf-8”, поэтому напишем тест для функции-расширения XDocument.ToXml():
public class XDocumentExtensionsFacts {
[Fact]
public void ToXml() {
var expected = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<root />";
var xdoc = new XDocument(new XElement("root"));
var actual = xdoc.ToXml();
Assert.Equal(expected, actual, StringComparer.Create(CultureInfo.CurrentCulture, true));
}
}


И получим примерно такую реализацию:
public class EncodedStringWriter : StringWriter {
private readonly Encoding _encoding;

public EncodedStringWriter(Encoding encoding) {
_encoding = encoding;
}

public EncodedStringWriter() : this(Encoding.UTF8) {
}

public override Encoding Encoding {
get { return _encoding; }
}
}

public static class XDocumentExtension {
public static string ToXml(this XDocument doc) {
using(var writer = new EncodedStringWriter()) {
doc.Save(writer);
return writer.ToString();
}
}
}


Создадим класс SiteUrl, который будет хранить информацию о url’ах сайта, и уметь сериализоваться в XElement, для этого напишем несколько тестов:
public class SiteUrlFacts {
[Fact]
public void Constructor() {
var expected = new Uri("http://somesite.com");
var target = new SiteUrl(expected);
Uri actual = target.Location;
Assert.Equal(expected, actual);
}

[Fact]
public void PriorityLessThenZeroThrowsException() {
var target = new SiteUrl("http://somesite.com");
int illegalPriority = new Random().Next(int.MinValue, 0);
Assert.True(illegalPriority < 0);
Assert.Throws<NotSupportedException>(() => target.Priority = illegalPriority);
}

[Fact]
public void PriorityMoreThenOneThrowsException() {
var target = new SiteUrl("http://somesite.com");
int illegalPriority = new Random().Next(1, int.MaxValue);
Assert.True(illegalPriority > 1);
Assert.Throws<NotSupportedException>(() => target.Priority = illegalPriority);
}

[Fact]
public void PrioritySetNormal() {
var target = new SiteUrl("http://somesite.com");
var normalPriority = (float)(new Random().NextDouble());
Assert.True(normalPriority >= 0);
Assert.True(normalPriority <= 1);
target.Priority = normalPriority;
}

[Fact]
public void SerializeHasAllArguments() {
XElement expected = XElement.Parse(
@"<url xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">
<loc>http://www.example.com/</loc>
<lastmod>2005-01-01T00:00:00 05:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>");
var siteUrl = new SiteUrl("http://www.example.com/") {
LastModified = new DateTimeOffset(new DateTime(2005, 01, 01)),
ChangeFrequency = ChangeFrequency.Monthly,
Priority = 0.8f,
};
XElement actual = siteUrl.Serialize();
XObjectAssert.Equal(expected, actual);
}
}



Ну и соответственно реализация:
public enum ChangeFrequency {
Always,
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
Never,
}

public class SiteUrl {
private float? _priority;

public SiteUrl(string location)
: this(new Uri(location)) {
}

public SiteUrl(Uri location) {
Location = location;
}

public Uri Location { get; private set; }
public DateTimeOffset? LastModified { get; set; }
public ChangeFrequency? ChangeFrequency { get; set; }

public float? Priority {
get { return _priority; }
set {
if(value < 0 || value > 1) {
throw new NotSupportedException("Priority must be between 0 and 1");
}
_priority = value;
}
}

public virtual XElement Serialize() {
return new XElement(SiteMapBuilder._sitemap "url",
new XElement(SiteMapBuilder._sitemap "loc", Location),
LastModified.HasValue
? new XElement(SiteMapBuilder._sitemap "lastmod",
LastModified.Value.ToString("yyyy-MM-ddTHH:mm:ss%K"))
: null,
ChangeFrequency.HasValue
? new XElement(SiteMapBuilder._sitemap "changefreq",
ChangeFrequency.Value.ToString().ToLower())
: null,
Priority.HasValue
? new XElement(SiteMapBuilder._sitemap "priority",
Priority.Value)
: null);
}
}


Дальше необходимо написать класс, который будет уметь сериализовать в XML коллекцию SiteMap’ов. напишем соответствующие тесты:
public class SiteMapBuilderFacts {
[Fact]
public void CreateEmptySitemap() {
XDocument expected = XDocument.Parse(
@"<?xml version=""1.0"" encoding=""UTF-8""?>
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"" />");
XDocument actual = new SiteMapBuilder(new List<SiteUrl>()).Build();
XObjectAssert.Equal(expected, actual);
}

[Fact]
public void CreateSitemapWithOneElement() {
XDocument expected =
XDocument.Parse(
@"<?xml version=""1.0"" encoding=""UTF-8""?>
<urlset xmlns=""http://www.sitemaps.org/schemas/sitemap/0.9"">
<url>
<loc>http://www.example.com/</loc>
</url>
</urlset>");
var urlset = new List<SiteUrl> { new SiteUrl("http://www.example.com/") };
XDocument actual = new SiteMapBuilder(urlset).Build();
XObjectAssert.Equal(expected, actual);
}
}


И реализуем:
public class SiteMapBuilder {
public static readonly XNamespace _sitemap = "http://www.sitemaps.org/schemas/sitemap/0.9";
private readonly IEnumerable<SiteUrl> _urlset;

public SiteMapBuilder(IEnumerable<SiteUrl> urlset) {
_urlset = urlset;
}

public XDocument Build() {
return new XDocument(new XElement(_sitemap + "urlset", _urlset.Select(x => x.Serialize())));
}
}


И теперь осталось самое главное - как прикрутить это к ASP.NET? можно так:
<%@ WebHandler Language="C#" Class="SiteMapHandler" %>
using System;
using System.Collections.Generic;
using System.Web;
using Hazzik.SiteMap;

public class SiteMapHandler : IHttpHandler {
#region IHttpHandler Members

public void ProcessRequest(HttpContext context) {
var urlset = new List<SiteUrl> { new SiteUrl("http://localhost/") };
var xml = new SiteMapBuilder(urlset).Build().ToXml();
HttpResponse response = context.Response;
response.Clear();
response.ContentType = "text/xml";
response.Write(xml);
response.End();
}

public bool IsReusable {
get { return true; }
}

#endregion
}


Для ASP.NET MVC можно написать примерно следующую реализацию ActionResult:
public class SiteMapResult : ActionResult {
private readonly IEnumerable<SiteUrl> _map;

public SiteMapResult(IEnumerable<SiteUrl> map) {
_map = map;
}

public IEnumerable<SiteUrl> Map {
get { return _map; }
}

public override void ExecuteResult(ControllerContext context) {
string xml = new SiteMapBuilder(Map).Build().ToXml();
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = "text/xml";
response.Write(xml);
}
}


Ну а дальше я думаю разберетесь, удачи:)
Строго не серчайте, это моя первая запись на техническую тему, да и русским литературным я не очень хорошо владею:) Замечания и комментарии приветствуются:)
comments powered by Disqus