نحوه ایجاد یک نقشه‌ی سایت پویا با استفاده از قابلیت Reflection
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

طبق این استاندارد قالب نقشه‌ی سایت به فرم زیر می‌باشد:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
   <url>
      <loc>http://www.example.com/</loc>
      <lastmod>2005-01-01</lastmod>
      <changefreq>monthly</changefreq>
      <priority>0.8</priority>
   </url>
</urlset>

که یک فایل XML متشکل از یک تگ urlset  است و این تگ نیز حاوی یک یا چند تگ url می‌باشد. با توجه به تعاریف بالا به یک چنین کلاسی خواهیم رسید: 

public enum ChangeFreq
        {
            Always,
            Hourly,
            Daily,
            Weekly,
            Monthly,
            Yearly,
            Never
        }

        [XmlElement("loc")]
        public string Url { get; set; }

        [XmlElement("lastmod")]
        public DateTime? LastModified { get; set; }
        public bool ShouldSerializeLastModified()
        {
            return LastModified.HasValue;
        }

        [XmlElement("changefreq")]
        public ChangeFreq? ChangeFrequency { get; set; }
        public bool ShouldSerializeChangeFrequency()
        {
            return ChangeFrequency.HasValue;
        }
        [XmlElement("priority")]
        public float? Priority { get; set; }
        public bool ShouldSerializePriority()
        {
            return Priority.HasValue;
        }
    }

دقت داشته باشید که چون پروپرتی‌های LastModified ، ChangeFrequency و Priority از نوع Nullable تعریف شده‌اند، پس باید کاری کنیم در صورتیکه این پروپرتی‌ها نال بودند سریالیز نشوند. بدین منظور از تابع ShouldSerialize[MemberName] استفاده می‌شود. این تابع  جزئی از دات نت است. کافی است بعد از ShouldSerialize نام پروپرتی را ذکر کنید. حال به کلاس دیگری نیاز داریم تا لیستی از کلاس فوق را دربر داشته باشد. 

[XmlRoot("urlset",Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9")]
    public class SiteMp
    {
        private readonly List<Location> _locations;

        public SiteMp()
        {
            _locations = new List<Location>();
        }

        [XmlElement("url")]
        public List<Location> Locations
        {
            get { return _locations; }
            set
            {
                foreach (var location in value)
                {
                    Add(location);
                }
            } 
            
        }

        public void Add(Location location)
        {
            _locations.Add(location);
        }
    }

حال برای پردازش کلاس بالا لازم است ActionResultی را طراحی کنیم تا خروجی Response را به فرمت XML پردازش کند:

public class XmlResult : ActionResult
    {
        private readonly object _objectToSerialize;

        public XmlResult(object objectToSerialize)
        {
            _objectToSerialize = objectToSerialize;
        }
        public override void ExecuteResult(ControllerContext context)
        {
            if (_objectToSerialize == null)
               return;
             context.HttpContext.Response.Clear();
             var xmlSerializer = new XmlSerializer(_objectToSerialize.GetType());
             context.HttpContext.Response.ContentType = "text/xml";
             xmlSerializer.Serialize(context.HttpContext.Response.Output, _objectToSerialize);            
        }
    }

و در آخر یک کنترلر ساخته و به صورت زیر از آن استفاده می‌کنیم: 

public class SiteMapController : Controller
    {
        // GET: SiteMap
        public ActionResult Index()
        {
            SiteMp siteMap = new SiteMp();
            siteMap.Add(new Location
            {
                Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/Index"
            });
            siteMap.Add(new Location
            {
                Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/NewRequest",
                ChangeFrequency = Location.ChangeFreq.Always,
                LastModified = DateTime.UtcNow,
                Priority = 0.5f
            });
            siteMap.Add(new Location
            {
                Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/FindRequest",
                ChangeFrequency = Location.ChangeFreq.Always,
                LastModified = DateTime.UtcNow,
                Priority = 0.5f
            });
            siteMap.Add(new Location
            {
                Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/ContactUs/Index",
                ChangeFrequency = Location.ChangeFreq.Daily,
                LastModified = DateTime.UtcNow,
                Priority = 0.5f
            });
            return new XmlResult(siteMap);
        }

اگر دقت کنید لینک‌های ثابت باید به صورت دستی اضافه شوند. سناریویی را تصور کنید که لینک‌ها زیاد باشند(جدای از لینک هایی که از دیتابیس لود می‌شوند) این کار کمی ناجور به نظر می‌رسد. در اینجا میخواهیم از طریق امکانات ،Reflection عمل اضافه کردن لینک به صورت خودکار انجام شود. 

public class ControllerScanner
    {
       public static List<string> ScanAllControllers(HttpRequestBase requestBase)
        {
            Assembly asm = Assembly.GetAssembly(typeof(MvcApplication));

            var controllerActionlist = asm.GetTypes()
                .Where(type => typeof (Controller).IsAssignableFrom(type))
                .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
                .Where((returnType => returnType.ReturnType == (typeof(ViewResult)) || returnType.ReturnType==(typeof(ActionResult))))
                .Select(
                    x =>
                        new
                        {
                            Controller = x.DeclaringType.Name,
                            Action = x.Name,
                            ReturnType = x.ReturnType.Name

                        })
                .OrderBy(x => x.Controller).ThenBy(x => x.Action).Distinct().ToList();

            if (requestBase.Url == null)
                return null;

            var url = requestBase.Url.GetLeftPart(UriPartial.Authority);
            return controllerActionlist.Select(controller => $"{url}/{controller.Controller}/{controller.Action}").ToList();
        }
    }

حال از کلاس بالا در کنترلر SiteMap به صورت زیر استفاده می‌کنیم :

public class SiteMapController : Controller
    {
        // GET: SiteMap
        public ActionResult Index()
        {
            var siteMap = new SiteMap();
            var controllers = ControllerScanner.ScanAllControllers(Request);
            foreach (var controller in controllers)
            {
                siteMap.Add(new Location
                {
                    Url = controller,
                    ChangeFrequency = Location.ChangeFreq.Always,
                    LastModified = DateTime.UtcNow,
                    Priority = 0.5f
                });
            }
            return new XmlResult(siteMap);
        }        
    }

در آخر نیز سطر زیر را به سیستم مسیریابی اضافه نمایید تا در صورت درخواست فایل sitemap.xml  اکشن Index از کنترلر SiteMap فراخوانی شود.

 routes.MapRoute(
                "SiteMap", // Route name
                "sitemap.xml", // URL with parameters
                new { controller = "Sitemap", action = "Index", name = UrlParameter.Optional, area = "" }
            );


  • #
    ‫۸ سال و ۹ ماه قبل، شنبه ۲۸ آذر ۱۳۹۴، ساعت ۰۲:۳۲
    سلام وخسته نباشید ، لطفا این قسمت کد را بدین صورت اصلاح کنید تا خروجی آدرس حاوی کلمه Controller نباشد .
     
     new
                            {
                                Controller = x.DeclaringType.Name.Replace("Controller",""),
                                ...
    
                            })
    • #
      ‫۸ سال و ۹ ماه قبل، شنبه ۲۸ آذر ۱۳۹۴، ساعت ۰۳:۱۸
      سلام. با تشکر . بله این قسمت از کد باید به این صورت اصلاح شود.
  • #
    ‫۷ سال و ۵ ماه قبل، دوشنبه ۲۱ فروردین ۱۳۹۶، ساعت ۰۴:۲۲
    ضمن تشکر بابت نشر این مطلب، من با قسمت route به مشکل برخورد کردم. یعنی، اگر از هر نوع پسوندی برای آدرس استفاده کنم، با خطای ۴۰۴ مواجه می‌شوم (البته به طرز عجیبی بجز aspx). برای تنظیمات مسیریابی به شکل زیر عمل می‌کنم:
    routes.MapRoute(
        "SiteMap",
        "sitemap.xml",
        new { controller = "SiteMap", action = "Index", name = UrlParameter.Optional }
    );
    که البته با همان خطای ۴۰۴ روبرو می‌شود. در نوشتاری در خصوص مقدار RouteExistingFiles توضیح داده شده‌است. پس وضعیت تنظیم سیستم مسیریابی را به شکل زیر اصلاح کردم:
      routes.RouteExistingFiles = true; 
    routes.MapRoute(
        "SiteMap",
        "sitemap.xml",
        new { controller = "SiteMap", action = "Index", name = UrlParameter.Optional }
    );
    نکته اینکه این کار، طبق توصیه، پیش از مسیریابی پیش‌فرض انجام شده است. باز هم خطای ۴۰۴! به آموزش دیگری مراجعه می‌کنم. شگفتا که در این حالت هم باز خطای ۴۰۴! بدون انجام هر تغییری، پسوند xml را با aspx جایگزین می‌کنم. نتیجه:

    با پسوند html هم آزمایش می‌کنم. متاسفانه خطای ۴۰۴!

    هنگامیکه کلا از هیچ پسوندی استفاده نمی‌کنم، نتایج sitemap با فرمت xml مشاهده می‌شود (مطابق انتظار). حقیقتا نمی‌دانم که چه چیزی را از قلم انداخته یا به اشتباه انجام داده‌ام. صرفنظر از اینکه صفحه‌ی sitemap فاقد پسوند باشد یا چه آدرسی داشته باشد ، اینکه هنگام استفاده از پسوند xml به نتیجه نمی‌رسم، آزار دهنده است. درصورت امکان، راهنمایی بفرمایید.

    /*----------------------*/

    متدی که برای اسکن کردن تمام controllerها تدارک دیده شده است، در صورت استفاده از T4MVC ، تمام controllerهای تهیه شده توسط T4MVC را هم به عنوان controller منعکس می‌کند. به عنوان نمونه، هر دو کنترلر زیر در خروجی xml وجود دارند:

    <url>
    <loc>http://localhost:3989/T4MVC_Blog/Index</loc>
    <lastmod>2017-04-09T19:07:41.5751733Z</lastmod>
    <changefreq>Always</changefreq>
    <priority>0.5</priority>
    </url>
    
    <url>
    <loc>http://localhost:3989/Blog/Index</loc>
    <lastmod>2017-04-09T19:07:41.5751733Z</lastmod>
    <changefreq>Always</changefreq>
    <priority>0.5</priority>
    </url>

    آیا امکان تغییر رفتار متد ScanAllControllers وجود دارد؟

    • #
      ‫۷ سال و ۵ ماه قبل، دوشنبه ۲۱ فروردین ۱۳۹۶، ساعت ۰۴:۵۰
      آیا تنظیم زیر را در فایل وب کانفیگ دارید؟
        <system.webServer>
          <modules runAllManagedModulesForAllRequests="true">
          </modules>
       
      • #
        ‫۷ سال و ۵ ماه قبل، دوشنبه ۲۱ فروردین ۱۳۹۶، ساعت ۰۴:۵۶
        نه متاسفانه. اما با انجام تغییر گفته شده، با پسوند xml، نتیجه کاملا رضایت بخش است. از شما سپاسگزارم.
    • #
      ‫۷ سال و ۵ ماه قبل، دوشنبه ۲۱ فروردین ۱۳۹۶، ساعت ۱۷:۳۹
      بله این امکان وجود دارد. کد زیر از تمام کنترلرهای T4Mvc و کنترلرهایی که با خصوصیت Authorized مزین شده اند صرفنظر میکند.
      public static List<string> ScanAllControllers(HttpRequestBase requestBase)
              {
                  if (requestBase.Url == null)
                      return null;
      
                  Assembly asm = Assembly.GetAssembly(typeof(MvcApplication));
                  var securedControllers =
                      asm.GetTypes()
                          .Where(
                              type =>
                                  typeof(IController).IsAssignableFrom(type) &&
                                  Attribute.IsDefined(type, typeof(AuthorizeAttribute)) &&
                                 !type.Name.StartsWith("T4MVC"));
      
                  var allControllers = asm.GetTypes()
                      .Where(type => typeof(Controller).IsAssignableFrom(type));
      
                  var controllers = allControllers.Except(securedControllers);
      
                  var actionsWithAuthorizeAndAjaxAttribute = allControllers.SelectMany(t => t.GetMethods(BindingFlags.Instance
                                                    | BindingFlags.DeclaredOnly
                                                    | BindingFlags.Public))
                      .Where(m => m.GetCustomAttributes(typeof(AuthorizeAttribute), true)
                      .Any() | m.GetCustomAttributes(typeof(AjaxRequestAttribute), true)
                      .Any());
      
                  var controllersList = controllers.SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
                                                   .Except(actionsWithAuthorizeAndAjaxAttribute)
                                                   .Where((returnType => returnType.ReturnType == (typeof(ViewResult)) || returnType.ReturnType == (typeof(ActionResult))))
                                                   .Where(returntype => !returntype.DeclaringType.Name.StartsWith("Site"))
                                                   .Where(returnType => !returnType.DeclaringType.Name.StartsWith("Admin"))
                                                   .Select(
                                                      x =>
                                                          new
                                                          {
                                                              Controller = x.DeclaringType.Name.Replace("Controller", string.Empty),
                                                              Action = x.Name,
                                                              ReturnType = x.ReturnType.Name
      
                                                          })
                                                   .OrderBy(x => x.Controller).ThenBy(x => x.Action).Distinct().ToList();
      
      
      
                  var url = requestBase.Url.GetLeftPart(UriPartial.Authority);
                  return controllersList.Select(controller => $"{url}/{controller.Controller}/{controller.Action}").ToList();
              }