U ovom postu biće prikazano kako može da se napravi custom ActionResult koji vraća feed kao odgovor na zahteva browser-a.

ActionResult

Svaka akcija kontrolera vraća objekat koji proizilazi iz apstraktne ActionResult klase. ASP.NET MVC sadrži nekoliko klasa koje nasleđuju ActionResult:

  • ContentResult
  • EmptyResult
  • FileResult (apstraktna)
    • FileContentResult
    • FilePathResult
    • FileStreamResult
  • HttpUnauthorizedResult
  • JavaScriptResult
  • JsonResult
  • RedirectResult
  • RedirectToRouteResult
  • ViewResultBase (apstraktna)
    • PartialViewResult
    • ViewResult

ActionResult klasa sadrži samo jednu metodu, tako da ako pravimo custom ActionResult potrebno je da preklopimo samo ExecuteResult metodu. Iako, u našem primeru možemo direktno da nasledimo ActionResult klasu, mi ćemo naslediti FileResult.

S obzirom da je MVC open source projekat, evo i source-a ove dve klase:

public abstract class ActionResult {
    public abstract void ExecuteResult(ControllerContext context);
}
 
 public abstract class FileResult : ActionResult {
 
    protected FileResult(string contentType) {
        if (String.IsNullOrEmpty(contentType)) {
            throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentType");
        }
 
        ContentType = contentType;
    }
 
    private string _fileDownloadName;
 
    public string ContentType {
        get;
        private set;
    }
 
    public string FileDownloadName {
        get {
            return _fileDownloadName ?? String.Empty;
        }
        set {
            _fileDownloadName = value;
        }
    }
 
    public override void ExecuteResult(ControllerContext context) {
        if (context == null) {
            throw new ArgumentNullException("context");
        }
 
        HttpResponseBase response = context.HttpContext.Response;
        response.ContentType = ContentType;
 
        if (!String.IsNullOrEmpty(FileDownloadName)) {
            // From RFC 2183, Sec. 2.3:
            // The sender may want to suggest a filename to be used if the entity is
            // detached and stored in a separate file. If the receiving MUA writes
            // the entity to a file, the suggested filename should be used as a
            // basis for the actual filename, where possible.
            ContentDisposition disposition = new ContentDisposition() { FileName = FileDownloadName };
            string headerValue = disposition.ToString();
            context.HttpContext.Response.AddHeader("Content-Disposition", headerValue);
        }
 
        WriteFile(response);
    }
 
    protected abstract void WriteFile(HttpResponseBase response);
 
}

Kako bi browser shvatio da se radi o feed-u, potrebno je da u response-u dodamo header Content-Disposition sa vrednošću application/rss+xml. Ovo je ujedno i razlog zašto ćemo naslediti FileResult umesto ActionResult.

Atom i RSS

Atom i Rss predstavljaju dijalekte XML-a, ali na svu sreću nije potrebno da se bavimo parsiranjem XML-a. U .NET-u postoji skup klasa koje su namenjen za rad sa strukturama podataka kakve su Atom i Rss feed-ovi. U prostoru imena System.ServiceModel.Syndication nalazi se nekoliko klasa:

  • SyndicationFeed
  • SyndicationItem
  • SyndicationContent
  • itd.

Implementacija

Uzećemo za primer blog aplikaciju, odnosno Post klasu kao model za koji ćemo generisati feed:

public class Post
{
    public int PostId { get; set }
    public string Title { get; set; }
    public string Body { get; set; }
    public string Permalink { get; set; }
    public DateTime Posted { get; set; }
}

FeedController je krajnje jednostavan i mislim da nema potrebe detaljnije objašnjavati šta radi:

public class FeedController : Controller
{
    private BlogContext db = new BlogContext();
     
    public ActionResult Atom()
    {
        var posts = db.Posts.All();
        return FeedResult(posts, FeedFormat.Atom10);
    }
     
    public ActionResult Rss()
    {
        var posts = db.Posts.All();
        return FeedResult(posts, FeedFormat.Rss20);
    }   
}

FeedResult izgledaće ovako:

public class FeedResult : FileResult
{
    private IEnumerable items;
    private FeedFormat feedFormat;
    private Uri currentUrl;
    public SyndicationFeed Feed { get; set; }
 
    public FeedResult(IEnumerable items, FeedFormat format)
        : base("application/rss+xml")
    {
        this.items = items;
        this.feedFormat = format;
    }
 
    public override void ExecuteResult(ControllerContext context)
    {
        this.currentUrl = context.RequestContext.HttpContext.Request.Url;
        base.ExecuteResult(context);
    }
 
    protected override void WriteFile(HttpResponseBase response)
    {
        Feed = new SyndicationFeed("Feed title", "Feed description", this.currentUrl, GetSyndicationItems());            
        Feed.Authors.Add(new SyndicationPerson("my@email.com", "My Name", "MyBlogUrl"));            
        Save(XmlWriter.Create(response.Output));
    }
 
    private void Save(XmlWriter writer)
    {
        switch (this.feedFormat)
        {
            case FeedFormat.Atom10:
                Feed.SaveAsAtom10(writer);
                break;
            case FeedFormat.Rss20:
                Feed.SaveAsRss20(writer);
                break;
            default:
                Feed.SaveAsRss20(writer);
                break;
        }
        writer.Close();
    }
 
    private List GetSyndicationItems()
    {
        var items = new List();
        foreach (var post in this.items)
        {
            var item = new SyndicationItem
            {
                Title = SyndicationContent.CreatePlaintextContent(post.Title),
                Content = SyndicationContent.CreateHtmlContent(post.Body),
                Id = post.PostId.ToString(),
                PublishDate = post.Posted,
                LastUpdatedTime = post.Posted
            };
            item.AddPermalink(new Uri(post.Permalink));
            items.Add(item);
        }
        return items;
    }
}
 
public enum FeedFormat
{
    Atom10,
    Rss20
}

Mislim da kod FeedResult-a dovoljno sam sebe objašnjava, treba samo obratiti pažnju na dve metode koje su preklopljne: WriteFile i ExecuteResult. ExecuteResult je metoda nasleđena od ActionResult-a i nju okida ControllerActionInvoker. WriteFile je nasleđena on FileResult klase i ona je odgovorna za pisanje sadržaja fajla u response. U slučaju nekog fajla to bi bio niz bajtova, a u našem slučaju to je sadržaj XML fajla (Atom ili RSS feed).