Search code examples
c#.netasp.net-core.net-7.0

XML File produced in a .NET web app works locally, but the content is empty in production


I have a .NET 7 web app, where I have a controller that results in a sitemap.xml file. When I run the application locally, I get an XML file as a result with this content:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>

And it looks like this:

enter image description here

However, when this is pushed to production (everything is hosted as a web app on Azure), the same endpoint returns nothing. It does recognize the endpoint and looks like this:

enter image description here

My code to generate this is shown below:

    [Route("/sitemap.xml")]
    public async Task SitemapXml()
    {
        var countries = await _countryService.GetBySpecificationAsync(new CountrySpecification()
        {
            Take = int.MaxValue
        });
        
        Response.ContentType = "application/xml";

        using (var xml = XmlWriter.Create(Response.Body, new XmlWriterSettings { Indent = true }))
        {
            xml.WriteStartDocument();
            xml.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
            xml.WriteEndElement();
        }
    }

My question: I am completely lost. At first, I thought it was because I didn't add support for static files and this is considered a static file, but I do have:

app.UseStaticFiles();

In the Program.cs.

Any hints on where I should be starting?


Solution

  • I spent some time this week wanting to answer this question, and I have time now.

    The main issue with your attempt is you are not returning XML results. To do so I suggest using IActionResult interface.

    Now time to create sitemap.xml. IMO there are 2 ways to go from here, either using a library OR writing your own sitemap method.

    I will start with a library. For instance, there is a very simple library (NuGet) called SimpleMvcSitemap.Core. Install it in your project, and in your controller insert the following code:

    [Route("/sitemap.xml")]
    public async Task<IActionResult> SitemapXml()
    {
    
        // your await call etc
    
        List<SitemapNode> nodes = new List<SitemapNode>
        {
            new SitemapNode(Url.Action("Index","Home")),
            new SitemapNode(Url.Action("About","Home")),
            //other nodes
        };
    
        return new SitemapProvider().CreateSitemap(new SitemapModel(nodes));
    }
    

    Btw for this test, I created an asp.net MVC .net 7 project.

    I have deployed the solution to azure and it works both on local development and on azure. Here is the result:

    enter image description here

    If you do want to do it manually, you can do following

    var listUrls = new List<string>
    {
        Url.Action("Index", "Home"),
        Url.Action("About", "Home")
    };
    
    return new SitemapResult(listUrls);
    

    And here is the implementation:

    public class SitemapResult : ActionResult
    {
        private readonly IEnumerable<string> _urls;
    
        public SitemapResult(IEnumerable<string> urls)
        {
            _urls = urls;
        }
    
        public override async Task ExecuteResultAsync(ActionContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            var response = context.HttpContext.Response;
            response.ContentType = "application/xml; charset=utf-8";
    
            var settings = new XmlWriterSettings() { Async = true, Encoding = Encoding.UTF8, Indent = false };
            using (var writer = XmlWriter.Create(response.Body, settings))
            {
                WriteToXML(writer);
                await writer.FlushAsync();
            }
        }
    
        private void WriteToXML(XmlWriter writer)
        {
            writer.WriteStartDocument();
            // Write the urlset.
            writer.WriteStartElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
            // url element
            foreach (var item in _urls)
            {
                writer.WriteStartElement("url");
                // loc
                writer.WriteStartElement("loc");
                writer.WriteValue(item);
                writer.WriteEndElement();
    
                writer.WriteEndElement();
            }
            writer.WriteEndElement();
            writer.WriteEndDocument();
        }
    }
    

    The manual way is also deployed on azure and works, but in the manual way you need to do a lot of work that is already done in a library. To be fair both above outcome is inspired form the question How to dynamically create a sitemap.xml in .NET core 2?.