MVC插件實現


      本人第一篇隨筆,在園子里逛了這么久,今天也記錄一篇自己的勞動成果,也是給自己以后留個記錄。

    最近領導讓我搞一下插件化,就是實現多個web工程通過配置文件進行組裝。之前由於做過一個簡單的算是有點經驗,當時使用的不是area,后來通過翻看orchard源碼有點啟發,打算使用area改一下。

    實現插件化,需要解決四個問題:

          1、如何發現插件以及加載插件及其所依賴的dll

          2、如何注冊路由,正確調用插件的Controller和Action

          3、如何實現ViewEngine,正確的發現View

          4、頁面中的Url如何自動生成

 以下下我們帶着這四個問題依次分析解決:

 1、如何發現插件以及加載插件及其所依賴的dll

     該問題我完全使用了Nop插件的實現方式,為每個工程定義一個Plugin.txt配置文件,運行時通過注冊[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]這個方法,在Application_Start()之前發現和加載插件。PluginManager負責管理加載插件,通過解析Plugin.txt,識別插件的dll和它所依賴的dll。通過Assembly.Load()方法加載dll並使用BuildManager.AddReferencedAssembly(shadowCopiedAssembly)為web項目動態添加引用。由於web項目存在不同的信任級別,在FullTrust級別可以將這些dll直接拷貝到AppDomain.CurrentDomain.DynamicDirectory文件夾下面。但是在其他信任級別下無法訪問該目錄,Nop通過復制到一個臨時目錄並在web.config中修改 <probingprivatePath="Plugins/bin/" />的值來讓iis自動探索該目錄。

代碼如下:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Framework.Core.Plugins
{
   public class Plugin
    {
        /// <summary>
        /// 插件名稱,唯一標識
        /// </summary>
        public string PluginName { get; set; }

        /// <summary>
        /// 插件顯示名稱
        /// </summary>
        public virtual string PluginFriendlyName { get; set; }

        /// <summary>
        /// 插件主文件(DLL)名稱
        /// </summary>
        public string PluginFileName { get; set; }

        /// <summary>
        /// 插件控制器命名空間
        /// </summary>
        public string ControllerNamespace { get; set; }

        /// <summary>
        /// 插件主文件文件信息
        /// </summary>
        public virtual FileInfo PluginFileInfo { get; internal set; }

        /// <summary>
        /// 插件程序集
        /// </summary>
        public virtual Assembly ReferencedAssembly { get; internal set; }

        /// <summary>
        /// 描述
        /// </summary>
        public virtual string Description { get; set; }


        /// <summary>
        /// 顯示順序
        /// </summary>
        public virtual int DisplayOrder { get; set; }

        /// <summary>
        /// 是否已安裝
        /// </summary>
        public virtual bool Installed { get; set; }
    }
}
View Code
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Web;
using System.Web.Compilation;
using Framework.Core.Plugins;
using Framework.Core.Infrastructure;

[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]
namespace Framework.Core.Plugins
{
    public class PluginManager
    {
        #region Const

        private const string InstalledPluginsFilePath = "~/App_Data/InstalledPlugins.txt";
        private const string PluginsPath = "~/Plugins";
        private const string ShadowCopyPath = "~/Plugins/bin";

        #endregion

        #region Fields

        private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();
        private static DirectoryInfo _shadowCopyFolder;
        private static bool _clearShadowDirectoryOnStartup;

        #endregion

        #region Methods

        public static IEnumerable<Plugin> ReferencedPlugins { get; set; }

        /// <summary>
        /// 初始化插件
        /// </summary>
        public static void Initialize()
        {
            using (new WriteLockDisposable(Locker))
            {
                var pluginFolder = new DirectoryInfo(CommonHelper.MapPath(PluginsPath));
                _shadowCopyFolder = new DirectoryInfo(CommonHelper.MapPath(ShadowCopyPath));
                var referencedPlugins = new List<Plugin>();
              
                _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) &&
                   Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]);

                try
                {
                    //獲取已經加載的插件名稱
                    var installedPluginNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());

                    Debug.WriteLine("創建臨時目錄");
                    Directory.CreateDirectory(pluginFolder.FullName);
                    Directory.CreateDirectory(_shadowCopyFolder.FullName);

                    //獲取臨時目錄中的dll文件
                    var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories);
                    if (_clearShadowDirectoryOnStartup)
                    {
                        //清除臨時目錄中的數據
                        foreach (var f in binFiles)
                        {
                            Debug.WriteLine("刪除文件: " + f.Name);
                            try
                            {
                                File.Delete(f.FullName);
                            }
                            catch (Exception exc)
                            {
                                Debug.WriteLine("刪除文件異常: " + f.Name + ".  異常信息: " + exc);
                            }
                        }
                    }

                    //加載插件
                    foreach (var dfd in GetPluginFilesAndPlugins(pluginFolder))
                    {
                        var pluginFile = dfd.Key;
                        var plugin = dfd.Value;
                        //驗證插件名稱
                        if (String.IsNullOrWhiteSpace(plugin.PluginName))
                            throw new Exception(string.Format("插件:'{0}' 沒有設置名稱. 請設置唯一的PluginName,重新編譯.", pluginFile.FullName));
                        if (referencedPlugins.Contains(plugin))
                            throw new Exception(string.Format("插件名稱:'{0}' 已經被占用,請重新設置唯一的PluginName,重新編譯", plugin.PluginName));

                        //設置是否已經安裝
                        plugin.Installed = installedPluginNames
                            .FirstOrDefault(x => x.Equals(plugin.PluginName, StringComparison.InvariantCultureIgnoreCase)) != null;

                        try
                        {
                            if (pluginFile.Directory == null)
                                throw new Exception(string.Format("'{0}'插件目錄無效,無法解析插件dll文件", pluginFile.Name));

                            //獲取插件中的所有DLL
                            var pluginDLLs = pluginFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories)
                                //just make sure we're not registering shadow copied plugins
                                .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName))
                                .Where(x => IsPackagePluginFolder(x.Directory))
                                .ToList();

                            //獲取主插件文件
                            var mainPluginDLL = pluginDLLs
                                .FirstOrDefault(x => x.Name.Equals(plugin.PluginFileName, StringComparison.InvariantCultureIgnoreCase));
                            plugin.PluginFileInfo = mainPluginDLL;

                            //復制主文件到臨時目錄,並加載主文件
                            plugin.ReferencedAssembly = PerformFileDeploy(mainPluginDLL);

                            //加載其他插件相關dll
                            foreach (var dll in pluginDLLs
                                .Where(x => !x.Name.Equals(mainPluginDLL.Name, StringComparison.InvariantCultureIgnoreCase))
                                .Where(x => !IsAlreadyLoaded(x)))
                                    PerformFileDeploy(dll);
                            referencedPlugins.Add(plugin);
                        }
                        catch (ReflectionTypeLoadException ex)
                        {
                            var msg = string.Format("Plugin '{0}'. ", plugin.PluginFriendlyName);
                            foreach (var e in ex.LoaderExceptions)
                                msg += e.Message + Environment.NewLine;

                            var fail = new Exception(msg, ex);
                            throw fail;
                        }
                        catch (Exception ex)
                        {
                            var msg = string.Format("Plugin '{0}'. {1}", plugin.PluginFriendlyName, ex.Message);
                            var fail = new Exception(msg, ex);
                            throw fail;
                        }
                    }
                }
                catch (Exception ex)
                {
                    var msg = string.Empty;
                    for (var e = ex; e != null; e = e.InnerException)
                        msg += e.Message + Environment.NewLine;

                    var fail = new Exception(msg, ex);
                    throw fail;
                }


                ReferencedPlugins = referencedPlugins;

            }
        }

        /// <summary>
        /// 安裝插件
        /// </summary>
        /// <param name="pluginName">插件名稱</param>
        public static void MarkPluginAsInstalled(string pluginName)
        {
            if (String.IsNullOrEmpty(pluginName))
                throw new ArgumentNullException("pluginName");

            var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
            if (!File.Exists(filePath))
                using (File.Create(filePath))
                {
                  
                }


            var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
            bool alreadyMarkedAsInstalled = installedPluginSystemNames
                                .FirstOrDefault(x => x.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)) != null;
            if (!alreadyMarkedAsInstalled)
                installedPluginSystemNames.Add(pluginName);
            PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath);
        }

        /// <summary>
        /// 卸載插件
        /// </summary>
        /// <param name="pluginName">插件名稱</param>
        public static void MarkPluginAsUninstalled(string pluginName)
        {
            if (String.IsNullOrEmpty(pluginName))
                throw new ArgumentNullException("pluginName");

            var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
            if (!File.Exists(filePath))
                using (File.Create(filePath))
                {
                   
                }

            var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
            bool alreadyMarkedAsInstalled = installedPluginSystemNames
                                .FirstOrDefault(x => x.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)) != null;
            if (alreadyMarkedAsInstalled)
                installedPluginSystemNames.Remove(pluginName);
            PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath);
        }

        /// <summary>
        /// 卸載所有插件
        /// </summary>
        public static void MarkAllPluginsAsUninstalled()
        {
            var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
            if (File.Exists(filePath))
                File.Delete(filePath);
        }

        #endregion

        #region 工具

        /// <summary>
        ///獲取指定目錄下的所有插件文件(Plugin.text)和插件信息(Plugin)
        /// </summary>
        /// <param name="pluginFolder">Plugin目錄</param>
        /// <returns>插件文件和插件</returns>
        private static IEnumerable<KeyValuePair<FileInfo, Plugin>> GetPluginFilesAndPlugins(DirectoryInfo pluginFolder)
        {
            if (pluginFolder == null)
                throw new ArgumentNullException("pluginFolder");

            var result = new List<KeyValuePair<FileInfo, Plugin>>();
            //add display order and path to list
            foreach (var descriptionFile in pluginFolder.GetFiles("Plugin.txt", SearchOption.AllDirectories))
            {
                if (!IsPackagePluginFolder(descriptionFile.Directory))
                    continue;

                //解析插件配置文件
                var plugin = PluginFileParser.ParsePluginFile(descriptionFile.FullName);
                result.Add(new KeyValuePair<FileInfo, Plugin>(descriptionFile, plugin));
            }
            //插件排序,數字越低排名越高
            result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder));
            return result;
        }

        /// <summary>
        /// 判斷程序集是否已經加載
        /// </summary>
        /// <param name="fileInfo">程序集文件</param>
        /// <returns>Result</returns>
        private static bool IsAlreadyLoaded(FileInfo fileInfo)
        {

            try
            {
                string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName);
                if (fileNameWithoutExt == null)
                    throw new Exception(string.Format("無法獲取文件名:{0}", fileInfo.Name));
                foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
                {
                    string assemblyName = a.FullName.Split(new[] { ',' }).FirstOrDefault();
                    if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase))
                        return true;
                }
            }
            catch (Exception exc)
            {
                Debug.WriteLine("無法判斷程序集是否加載。" + exc);
            }
            return false;
        }

        /// <summary>
        ///執行解析文件
        /// </summary>
        /// <param name="plug">插件文件</param>
        /// <returns>Assembly</returns>
        private static Assembly PerformFileDeploy(FileInfo plug)
        {
            if (plug.Directory.Parent == null)
                throw new InvalidOperationException("插件" + plug.Name + ":目錄無效" );

            FileInfo shadowCopiedPlug;

            if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
            {
                //運行在MediumTrust下(在MediumTrust下無法訪問DynamicDirectory,也無法設置ResolveAssembly event)
                //需要將所有插件dll都需要拷貝到~/Plugins/bin/下的臨時目錄,因為web.config中的probingPaths設置的是該目錄
                var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName);
                shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder);
            }
            else
            {
                //運行在FullTrust下,可以直接使用標准的DynamicDirectory文件夾,作為臨時目錄
                var directory = AppDomain.CurrentDomain.DynamicDirectory;
                Debug.WriteLine(plug.FullName + " to " + directory);
                shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory));
            }

            //加載程序集
            var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));

            //添加引用信息到BuildManager
            Debug.WriteLine("添加到BuildManager: '{0}'", shadowCopiedAssembly.FullName);
            BuildManager.AddReferencedAssembly(shadowCopiedAssembly);

            return shadowCopiedAssembly;
        }

        /// <summary>
        /// FullTrust級別下的插件初始化
        /// </summary>
        /// <param name="plug"></param>
        /// <param name="shadowCopyPlugFolder"></param>
        /// <returns></returns>
        private static FileInfo InitializeFullTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder)
        {
            var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));
            try
            {
                File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
            }
            catch (IOException)
            {
                Debug.WriteLine(shadowCopiedPlug.FullName + " 文件已被鎖, 嘗試重命名");
                //可能被 devenv鎖住,可以通過重命名來解鎖
                try
                {
                    var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
                    File.Move(shadowCopiedPlug.FullName, oldFile);
                }
                catch (IOException exc)
                {
                    throw new IOException(shadowCopiedPlug.FullName + " 重命名失敗, 無法初始化插件", exc);
                }
                //重新嘗試復制
                File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
            }
            return shadowCopiedPlug;
        }

        /// <summary>
        ///  MediumTrust級別下的插件初始化
        /// </summary>
        /// <param name="plug"></param>
        /// <param name="shadowCopyPlugFolder"></param>
        /// <returns></returns>
        private static FileInfo InitializeMediumTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder)
        {
            var shouldCopy = true;
            var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));

            //檢查插件是否存在,如果存在,判斷是否需要更新
            if (shadowCopiedPlug.Exists)
            {
                var areFilesIdentical = shadowCopiedPlug.CreationTimeUtc.Ticks >= plug.CreationTimeUtc.Ticks;
                if (areFilesIdentical)
                {
                    Debug.WriteLine("插件已經存在,不需要更新: '{0}'", shadowCopiedPlug.Name);
                    shouldCopy = false;
                }
                else
                {
                    //刪除現有插件
                    Debug.WriteLine("有新插件; 刪除現有插件: '{0}'", shadowCopiedPlug.Name);
                    File.Delete(shadowCopiedPlug.FullName);
                }
            }

            if (shouldCopy)
            {
                try
                {
                    File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
                }
                catch (IOException)
                {
                    Debug.WriteLine(shadowCopiedPlug.FullName + " 文件已被鎖, 嘗試重命名");
                    //可能被 devenv鎖住,可以通過重命名來解鎖
                    try
                    {
                        var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
                        File.Move(shadowCopiedPlug.FullName, oldFile);
                    }
                    catch (IOException exc)
                    {
                        throw new IOException(shadowCopiedPlug.FullName + " 重命名失敗, 無法初始化插件", exc);
                     
                    }
                    //重新嘗試復制
                    File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
                }
            }

            return shadowCopiedPlug;
        }
        
        /// <summary>
        ///判斷文件是否屬於插件目錄下的文件(Plugins下)
        /// </summary>
        /// <param name="folder"></param>
        /// <returns></returns>
        private static bool IsPackagePluginFolder(DirectoryInfo folder)
        {
            if (folder == null) return false;
            if (folder.Parent == null) return false;
            if (!folder.Parent.Name.Equals("Plugins", StringComparison.InvariantCultureIgnoreCase)) return false;
            return true;
        }

        /// <summary>
        /// 獲取InstalledPlugins.txt文件的物理路徑
        /// </summary>
        /// <returns></returns>
        private static string GetInstalledPluginsFilePath()
        { 
            return CommonHelper.MapPath(InstalledPluginsFilePath);
        }

        #endregion
    }
}
View Code

 

2、如何注冊路由,正確調用插件的Controller和Action

    路由我通過擴展現Mvc的RouteCollection的MapRoute方法,將插件名稱作為area強行插入到DataToken中,這樣在ViewEngine中可以使用area規則來發現視圖。然后重寫RegisterRoutes方法,通過遍歷所有插件集合,添加指定的路由,並將所有插件的Controller的命名空間寫入到插件匹配模式中,這樣可以解決不同插件之間Controller重名的問題。

  public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces,string area)
        {
            if (routes == null)
            {
                throw new ArgumentNullException("routes");
            }
            if (url == null)
            {
                throw new ArgumentNullException("url");
            }

            Route route = new Route(url, new MvcRouteHandler())
            {
                Defaults = new RouteValueDictionary(defaults),
                Constraints = new RouteValueDictionary(constraints),
                DataTokens = new RouteValueDictionary()
            };

            if ((namespaces != null) && (namespaces.Length > 0))
            {
                route.DataTokens["Namespaces"] = namespaces;
            }

            if (!string.IsNullOrEmpty(area))
            {
                route.DataTokens["area"] = area;
            }

            routes.Add(name, route);

            return route;
        }
View Code
        public static void RegisterPluginRoutes(RouteCollection routes)
        {

            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            foreach (var plugin in PluginManager.ReferencedPlugins)
            {
    
                routes.MapRoute(plugin.PluginName,
                    string.Concat(plugin.PluginName, "/{controller}/{action}/{id}"),
                    new { area= plugin.PluginName, controller = "Home", action = "Index", id = UrlParameter.Optional },
                   new string[]{ plugin.ControllerNamespace}, plugin.PluginName);
            }

            routes.MapRoute(
                 name: "Default",
                 url: "{controller}/{action}/{id}",
                 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                 namespaces:new string[] { "GWT.Framework.Web.Controllers" }
              );
    

        }
View Code

3、如何實現ViewEngine,正確的發現View

   關於這個問題我發現Nop和Orchard中好多地方都是硬編碼,通過VIEW(~/Plugin/XXX/views/XXX/XX.csthml)的方式來發現視圖。不知他們是何用意,我覺這樣耦合度過高。此處我通過前面路由中插入的area並配合實現一個繼承自RazorViewEngine的視圖引擎,將所有的插件請求定位到~/Plugins/{area}/Views/{controller}/{action}.cshtml。同時替換掉原有的視圖引擎。代碼如下:

 

    public class PluginViewEngine : RazorViewEngine
    {
        public PluginViewEngine()
        {


            AreaViewLocationFormats = new[] {
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Plugins/{2}/Views/{1}/{0}.cshtml",
                "~/Plugins/{2}/Views/Shared/{0}.cshtml"
            };
            AreaMasterLocationFormats = new[] {
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                 "~/Plugins/{2}/Views/{1}/{0}.cshtml",
                "~/Plugins/{2}/Views/Shared/{0}.cshtml"
            };
            AreaPartialViewLocationFormats = new[] {
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Plugins/{2}/Views/{1}/{0}.cshtml",
                "~/Plugins/{2}/Views/Shared/{0}.cshtml"
            };

            FileExtensions = new[] { "cshtml" };
        }
    }
View Code
 protected void Application_Start()
        {
            ViewEngines.Engines.Clear();
            ViewEngines.Engines.Add(new PluginViewEngine());

            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            ApplicationStartup.RegisterPluginRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
View Code

 4、頁面中的Url如何自動生成

   我們知道頁面中的url可以使用硬編碼方式比如/Home/Index,也可以使用Html.ActionLink(“Index”,“Home”)或者Url.Action方式實現。前者硬編碼的方式已經不適用於插件化,因為開發者不知道是否會被用作插件,如果強行寫入/Pluin1/Home/Index,勢必導致本地無法運行。在插件系統中應該使用后兩者,因為他們都是用過路由系統輸出URL的。MVC框架會基於當前的Controller到路由系統中找到匹配的路徑返回給前台頁面。

   對於URL我們可以使用Html和Url幫助器生成,但是對於Script和css等內容文件MVC框架就無能為力了。為了解決內容文件的加載,我擴展了UrlHelper幫助器,根據當前的請求中是否有area來生成相對路徑。代碼如下

        public static string PluginContent(this UrlHelper urlHelper, string url)
        {
            if (urlHelper.RequestContext.RouteData.Values.Keys.Contains("area"))
            {
                var area = urlHelper.RequestContext.RouteData.Values["area"].ToString();
                if (!string.IsNullOrEmpty(area))
                {
                    url = url.Substring(url.IndexOf("/") + 1);
                    return string.Format("~/Plugins/{0}/{1}", area, url);
                }
            }
            return url;


        }
View Code

在頁面中可以如下調用: @Url.PluginContent("/Views/Shared/_Layout.cshtml")

 

參考文檔:

https://shazwazza.com/post/Developing-a-plugin-framework-in-ASPNET-with-medium-trust.aspx

http://www.cnblogs.com/longyunshiye/p/5786446.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM