Forráskód Böngészése

Diverse Änderungen und Anfügungen.

Arne Diekmann 8 éve
szülő
commit
bc5e74c77f
59 módosított fájl, 3452 hozzáadás és 67 törlés
  1. 305 0
      GreenTree.Nachtragsmanagement.Core/AppDomainTypeFinder.cs
  2. 22 0
      GreenTree.Nachtragsmanagement.Core/AppendixVersion.cs
  3. 35 0
      GreenTree.Nachtragsmanagement.Core/ComponentModel/WriteLockDisposable.cs
  4. 46 1
      GreenTree.Nachtragsmanagement.Core/GreenTree.Nachtragsmanagement.Core.csproj
  5. 26 0
      GreenTree.Nachtragsmanagement.Core/ITypeFinder.cs
  6. 33 0
      GreenTree.Nachtragsmanagement.Core/Plugins/BasePlugin.cs
  7. 30 0
      GreenTree.Nachtragsmanagement.Core/Plugins/IPlugin.cs
  8. 75 0
      GreenTree.Nachtragsmanagement.Core/Plugins/IPluginFinder.cs
  9. 27 0
      GreenTree.Nachtragsmanagement.Core/Plugins/LoadPluginsMode.cs
  10. 137 0
      GreenTree.Nachtragsmanagement.Core/Plugins/PluginDescriptor.cs
  11. 168 0
      GreenTree.Nachtragsmanagement.Core/Plugins/PluginExtensions.cs
  12. 191 0
      GreenTree.Nachtragsmanagement.Core/Plugins/PluginFileParser.cs
  13. 176 0
      GreenTree.Nachtragsmanagement.Core/Plugins/PluginFinder.cs
  14. 476 0
      GreenTree.Nachtragsmanagement.Core/Plugins/PluginManager.cs
  15. 1 1
      GreenTree.Nachtragsmanagement.Core/Singleton.cs
  16. 80 0
      GreenTree.Nachtragsmanagement.Core/WebAppTypeFinder.cs
  17. 1 1
      GreenTree.Nachtragsmanagement.Core/WebHelper.cs
  18. 9 0
      GreenTree.Nachtragsmanagement.Core/packages.config
  19. 27 0
      GreenTree.Nachtragsmanagement.Plugin.Test/Controllers/TestController.cs
  20. 8 0
      GreenTree.Nachtragsmanagement.Plugin.Test/Description.txt
  21. 118 0
      GreenTree.Nachtragsmanagement.Plugin.Test/GreenTree.Nachtragsmanagement.Plugin.Test.csproj
  22. 36 0
      GreenTree.Nachtragsmanagement.Plugin.Test/Properties/AssemblyInfo.cs
  23. 36 0
      GreenTree.Nachtragsmanagement.Plugin.Test/RouteProvider.cs
  24. 32 0
      GreenTree.Nachtragsmanagement.Plugin.Test/TestPlugin.cs
  25. 12 0
      GreenTree.Nachtragsmanagement.Plugin.Test/Views/Home/Index.cshtml
  26. 21 0
      GreenTree.Nachtragsmanagement.Plugin.Test/app.config
  27. BIN
      GreenTree.Nachtragsmanagement.Plugin.Test/logo.jpg
  28. 8 0
      GreenTree.Nachtragsmanagement.Plugin.Test/packages.config
  29. 64 2
      GreenTree.Nachtragsmanagement.Services/Appendix/AppendixService.cs
  30. 39 0
      GreenTree.Nachtragsmanagement.Services/Appendix/IAppendixService.cs
  31. 283 0
      GreenTree.Nachtragsmanagement.Services/Deviation/DeviationService.cs
  32. 170 0
      GreenTree.Nachtragsmanagement.Services/Deviation/IDeviationService.cs
  33. 5 2
      GreenTree.Nachtragsmanagement.Services/GreenTree.Nachtragsmanagement.Services.csproj
  34. 126 0
      GreenTree.Nachtragsmanagement.Services/Misc/IMiscService.cs
  35. 212 0
      GreenTree.Nachtragsmanagement.Services/Misc/MiscService.cs
  36. 0 21
      GreenTree.Nachtragsmanagement.Services/Notification/INotificationLogic.cs
  37. 0 15
      GreenTree.Nachtragsmanagement.Services/Notification/INotificationService.cs
  38. 11 0
      GreenTree.Nachtragsmanagement.Services/app.config
  39. 34 0
      GreenTree.Nachtragsmanagement.Web.Framework/ApplicationContext.cs
  40. 4 1
      GreenTree.Nachtragsmanagement.Web.Framework/GreenTree.Nachtragsmanagement.Web.Framework.csproj
  41. 37 0
      GreenTree.Nachtragsmanagement.Web.Framework/Mvc/Routes/GuidConstraint.cs
  42. 16 0
      GreenTree.Nachtragsmanagement.Web.Framework/Mvc/Routes/IRouteProvider.cs
  43. 21 0
      GreenTree.Nachtragsmanagement.Web.Framework/Mvc/Routes/IRoutePublisher.cs
  44. 69 0
      GreenTree.Nachtragsmanagement.Web.Framework/Mvc/Routes/RoutePublisher.cs
  45. 0 0
      GreenTree.Nachtragsmanagement.Web/App_Data/InstalledPlugins.txt
  46. 74 2
      GreenTree.Nachtragsmanagement.Web/Controllers/HomeController.cs
  47. 4 0
      GreenTree.Nachtragsmanagement.Web/Global.asax.cs
  48. 4 0
      GreenTree.Nachtragsmanagement.Web/GreenTree.Nachtragsmanagement.Web.csproj
  49. 12 0
      GreenTree.Nachtragsmanagement.Web/Models/Test/PluginModel.cs
  50. 8 0
      GreenTree.Nachtragsmanagement.Web/Plugins/Misc.Test/Description.txt
  51. BIN
      GreenTree.Nachtragsmanagement.Web/Plugins/Misc.Test/GreenTree.Nachtragsmanagement.Plugin.Test.dll
  52. 21 0
      GreenTree.Nachtragsmanagement.Web/Plugins/Misc.Test/GreenTree.Nachtragsmanagement.Plugin.Test.dll.config
  53. BIN
      GreenTree.Nachtragsmanagement.Web/Plugins/Misc.Test/logo.jpg
  54. 2 21
      GreenTree.Nachtragsmanagement.Web/Views/Home/Index.cshtml
  55. 55 0
      GreenTree.Nachtragsmanagement.Web/Views/Home/Plugins.cshtml
  56. 29 0
      GreenTree.Nachtragsmanagement.Web/Views/Home/Relations.cshtml
  57. 6 0
      GreenTree.Nachtragsmanagement.Web/Views/Shared/_HeaderNavBar.cshtml
  58. 3 0
      GreenTree.Nachtragsmanagement.Web/packages.config
  59. 7 0
      GreenTree.Nachtragsmanagement.sln

+ 305 - 0
GreenTree.Nachtragsmanagement.Core/AppDomainTypeFinder.cs

@@ -0,0 +1,305 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core
+{
+    /// <summary>
+    /// A class that finds types needed by Appenix by looping assemblies in the 
+    /// currently executing AppDomain. Only assemblies whose names matches
+    /// certain patterns are investigated and an optional list of assemblies
+    /// referenced by <see cref="AssemblyNames"/> are always investigated.
+    /// </summary>
+    public class AppDomainTypeFinder : ITypeFinder
+    {
+        #region Fields
+
+        private bool ignoreReflectionErrors = true;
+        private bool loadAppDomainAssemblies = true;
+        private string assemblySkipLoadingPattern = "^System|^mscorlib|^Microsoft|^AjaxControlToolkit|^Antlr3|^Autofac|^AutoMapper|^Castle|^ComponentArt|^CppCodeProvider|^DotNetOpenAuth|^EntityFramework|^EPPlus|^FluentValidation|^ImageResizer|^itextsharp|^log4net|^MaxMind|^MbUnit|^MiniProfiler|^Mono.Math|^MvcContrib|^Newtonsoft|^NHibernate|^nunit|^Org.Mentalis|^PerlRegex|^QuickGraph|^Recaptcha|^Remotion|^RestSharp|^Rhino|^Telerik|^Iesi|^TestDriven|^TestFu|^UserAgentStringLibrary|^VJSharpCodeProvider|^WebActivator|^WebDev|^WebGrease";
+        private string assemblyRestrictToLoadingPattern = ".*";
+        private IList<string> assemblyNames = new List<string>();
+
+        #endregion
+
+        #region Properties
+
+        /// <summary>The app domain to look for types in.</summary>
+        public virtual AppDomain App
+        {
+            get { return AppDomain.CurrentDomain; }
+        }
+
+        /// <summary>Gets or sets wether Nop should iterate assemblies in the app domain when loading Nop types. Loading patterns are applied when loading these assemblies.</summary>
+        public bool LoadAppDomainAssemblies
+        {
+            get { return loadAppDomainAssemblies; }
+            set { loadAppDomainAssemblies = value; }
+        }
+
+        /// <summary>Gets or sets assemblies loaded a startup in addition to those loaded in the AppDomain.</summary>
+        public IList<string> AssemblyNames
+        {
+            get { return assemblyNames; }
+            set { assemblyNames = value; }
+        }
+
+        /// <summary>Gets the pattern for dlls that we know don't need to be investigated.</summary>
+        public string AssemblySkipLoadingPattern
+        {
+            get { return assemblySkipLoadingPattern; }
+            set { assemblySkipLoadingPattern = value; }
+        }
+
+        /// <summary>Gets or sets the pattern for dll that will be investigated. For ease of use this defaults to match all but to increase performance you might want to configure a pattern that includes assemblies and your own.</summary>
+        public string AssemblyRestrictToLoadingPattern
+        {
+            get { return assemblyRestrictToLoadingPattern; }
+            set { assemblyRestrictToLoadingPattern = value; }
+        }
+
+        #endregion
+
+        #region Methods
+
+        public IEnumerable<Type> FindClassesOfType<T>(bool onlyConcreteClasses = true)
+        {
+            return FindClassesOfType(typeof(T), onlyConcreteClasses);
+        }
+
+        public IEnumerable<Type> FindClassesOfType(Type assignTypeFrom, bool onlyConcreteClasses = true)
+        {
+            return FindClassesOfType(assignTypeFrom, GetAssemblies(), onlyConcreteClasses);
+        }
+
+        public IEnumerable<Type> FindClassesOfType<T>(IEnumerable<Assembly> assemblies, bool onlyConcreteClasses = true)
+        {
+            return FindClassesOfType(typeof(T), assemblies, onlyConcreteClasses);
+        }
+
+        public IEnumerable<Type> FindClassesOfType(Type assignTypeFrom, IEnumerable<Assembly> assemblies, bool onlyConcreteClasses = true)
+        {
+            var result = new List<Type>();
+            try
+            {
+                foreach (var a in assemblies)
+                {
+                    Type[] types = null;
+                    try
+                    {
+                        types = a.GetTypes();
+                    }
+                    catch
+                    {
+                        //Entity Framework 6 doesn't allow getting types (throws an exception)
+                        if (!ignoreReflectionErrors)
+                        {
+                            throw;
+                        }
+                    }
+                    if (types != null)
+                    {
+                        foreach (var t in types)
+                        {
+                            if (assignTypeFrom.IsAssignableFrom(t) || (assignTypeFrom.IsGenericTypeDefinition && DoesTypeImplementOpenGeneric(t, assignTypeFrom)))
+                            {
+                                if (!t.IsInterface)
+                                {
+                                    if (onlyConcreteClasses)
+                                    {
+                                        if (t.IsClass && !t.IsAbstract)
+                                        {
+                                            result.Add(t);
+                                        }
+                                    }
+                                    else
+                                    {
+                                        result.Add(t);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            catch (ReflectionTypeLoadException ex)
+            {
+                var msg = string.Empty;
+                foreach (var e in ex.LoaderExceptions)
+                    msg += e.Message + Environment.NewLine;
+
+                var fail = new Exception(msg, ex);
+                Debug.WriteLine(fail.Message, fail);
+
+                throw fail;
+            }
+            return result;
+        }
+
+        /// <summary>Gets the assemblies related to the current implementation.</summary>
+        /// <returns>A list of assemblies that should be loaded.</returns>
+        public virtual IList<Assembly> GetAssemblies()
+        {
+            var addedAssemblyNames = new List<string>();
+            var assemblies = new List<Assembly>();
+
+            if (LoadAppDomainAssemblies)
+                AddAssembliesInAppDomain(addedAssemblyNames, assemblies);
+            AddConfiguredAssemblies(addedAssemblyNames, assemblies);
+
+            return assemblies;
+        }
+
+        #endregion
+
+        #region Utilities
+
+        /// <summary>
+        /// Iterates all assemblies in the AppDomain and if it's name matches the configured patterns add it to our list.
+        /// </summary>
+        /// <param name="addedAssemblyNames"></param>
+        /// <param name="assemblies"></param>
+        private void AddAssembliesInAppDomain(List<string> addedAssemblyNames, List<Assembly> assemblies)
+        {
+            foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
+            {
+                if (Matches(assembly.FullName))
+                {
+                    if (!addedAssemblyNames.Contains(assembly.FullName))
+                    {
+                        assemblies.Add(assembly);
+                        addedAssemblyNames.Add(assembly.FullName);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Adds specificly configured assemblies.
+        /// </summary>
+        /// <param name="addedAssemblyNames"></param>
+        /// <param name="assemblies"></param>
+        protected virtual void AddConfiguredAssemblies(List<string> addedAssemblyNames, List<Assembly> assemblies)
+        {
+            foreach (string assemblyName in AssemblyNames)
+            {
+                Assembly assembly = Assembly.Load(assemblyName);
+                if (!addedAssemblyNames.Contains(assembly.FullName))
+                {
+                    assemblies.Add(assembly);
+                    addedAssemblyNames.Add(assembly.FullName);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Check if a dll is one of the shipped dlls that we know don't need to be investigated.
+        /// </summary>
+        /// <param name="assemblyFullName">
+        /// The name of the assembly to check.
+        /// </param>
+        /// <returns>
+        /// True if the assembly should be loaded into Nop.
+        /// </returns>
+        public virtual bool Matches(string assemblyFullName)
+        {
+            return !Matches(assemblyFullName, AssemblySkipLoadingPattern)
+                   && Matches(assemblyFullName, AssemblyRestrictToLoadingPattern);
+        }
+
+        /// <summary>
+        /// Check if a dll is one of the shipped dlls that we know don't need to be investigated.
+        /// </summary>
+        /// <param name="assemblyFullName">
+        /// The assembly name to match.
+        /// </param>
+        /// <param name="pattern">
+        /// The regular expression pattern to match against the assembly name.
+        /// </param>
+        /// <returns>
+        /// True if the pattern matches the assembly name.
+        /// </returns>
+        protected virtual bool Matches(string assemblyFullName, string pattern)
+        {
+            return Regex.IsMatch(assemblyFullName, pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
+        }
+
+        /// <summary>
+        /// Makes sure matching assemblies in the supplied folder are loaded in the app domain.
+        /// </summary>
+        /// <param name="directoryPath">
+        /// The physical path to a directory containing dlls to load in the app domain.
+        /// </param>
+        protected virtual void LoadMatchingAssemblies(string directoryPath)
+        {
+            var loadedAssemblyNames = new List<string>();
+            foreach (Assembly a in GetAssemblies())
+            {
+                loadedAssemblyNames.Add(a.FullName);
+            }
+
+            if (!Directory.Exists(directoryPath))
+            {
+                return;
+            }
+
+            foreach (string dllPath in Directory.GetFiles(directoryPath, "*.dll"))
+            {
+                try
+                {
+                    var an = AssemblyName.GetAssemblyName(dllPath);
+                    if (Matches(an.FullName) && !loadedAssemblyNames.Contains(an.FullName))
+                    {
+                        App.Load(an);
+                    }
+
+                    //old loading stuff
+                    //Assembly a = Assembly.ReflectionOnlyLoadFrom(dllPath);
+                    //if (Matches(a.FullName) && !loadedAssemblyNames.Contains(a.FullName))
+                    //{
+                    //    App.Load(a.FullName);
+                    //}
+                }
+                catch (BadImageFormatException ex)
+                {
+                    Trace.TraceError(ex.ToString());
+                }
+            }
+        }
+
+        /// <summary>
+        /// Does type implement generic?
+        /// </summary>
+        /// <param name="type"></param>
+        /// <param name="openGeneric"></param>
+        /// <returns></returns>
+        protected virtual bool DoesTypeImplementOpenGeneric(Type type, Type openGeneric)
+        {
+            try
+            {
+                var genericTypeDefinition = openGeneric.GetGenericTypeDefinition();
+                foreach (var implementedInterface in type.FindInterfaces((objType, objCriteria) => true, null))
+                {
+                    if (!implementedInterface.IsGenericType)
+                        continue;
+
+                    var isMatch = genericTypeDefinition.IsAssignableFrom(implementedInterface.GetGenericTypeDefinition());
+                    return isMatch;
+                }
+                return false;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        #endregion
+    }
+}

+ 22 - 0
GreenTree.Nachtragsmanagement.Core/AppendixVersion.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core
+{
+    public static class AppendixVersion
+    {
+        /// <summary>
+        /// Gets or sets the store version
+        /// </summary>
+        public static string CurrentVersion
+        {
+            get
+            {
+                return "1.0.0";
+            }
+        }
+    }
+}

+ 35 - 0
GreenTree.Nachtragsmanagement.Core/ComponentModel/WriteLockDisposable.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core.ComponentModel
+{
+    /// <summary>
+    /// Provides a convenience methodology for implementing locked access to resources. 
+    /// </summary>
+    /// <remarks>
+    /// Intended as an infrastructure class.
+    /// </remarks>
+    public class WriteLockDisposable : IDisposable
+    {
+        private readonly ReaderWriterLockSlim _rwLock;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="WriteLockDisposable"/> class.
+        /// </summary>
+        /// <param name="rwLock">The rw lock.</param>
+        public WriteLockDisposable(ReaderWriterLockSlim rwLock)
+        {
+            _rwLock = rwLock;
+            _rwLock.EnterWriteLock();
+        }
+
+        void IDisposable.Dispose()
+        {
+            _rwLock.ExitWriteLock();
+        }
+    }
+}

+ 46 - 1
GreenTree.Nachtragsmanagement.Core/GreenTree.Nachtragsmanagement.Core.csproj

@@ -30,11 +30,39 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="Autofac, Version=4.0.1.0, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
+      <HintPath>..\packages\Autofac.4.0.1\lib\net45\Autofac.dll</HintPath>
+    </Reference>
+    <Reference Include="Autofac.Integration.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
+      <HintPath>..\packages\Autofac.Mvc5.4.0.2\lib\net45\Autofac.Integration.Mvc.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.configuration" />
     <Reference Include="System.Core" />
     <Reference Include="System.Data.Entity" />
     <Reference Include="System.Web" />
+    <Reference Include="System.Web.Helpers, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.1.0\lib\net45\System.Web.Helpers.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Mvc, Version=5.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.Mvc.5.1.0\lib\net45\System.Web.Mvc.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.Razor.3.1.0\lib\net45\System.Web.Razor.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.1.0\lib\net45\System.Web.WebPages.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages.Deployment, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.1.0\lib\net45\System.Web.WebPages.Deployment.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.1.0\lib\net45\System.Web.WebPages.Razor.dll</HintPath>
+    </Reference>
     <Reference Include="System.Xml.Linq" />
     <Reference Include="System.Data.DataSetExtensions" />
     <Reference Include="Microsoft.CSharp" />
@@ -43,9 +71,12 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="AppDomainTypeFinder.cs" />
+    <Compile Include="AppendixVersion.cs" />
     <Compile Include="BaseEntity.cs" />
     <Compile Include="CommonHelper.cs" />
     <Compile Include="ComponentModel\GenericListTypeConverter.cs" />
+    <Compile Include="ComponentModel\WriteLockDisposable.cs" />
     <Compile Include="Configuration\AppendixConfigurationSection.cs" />
     <Compile Include="Configuration\MailServerElement.cs" />
     <Compile Include="Data\IRepository.cs" />
@@ -63,10 +94,24 @@
     <Compile Include="Domain\User\Function.cs" />
     <Compile Include="Domain\User\Role.cs" />
     <Compile Include="Domain\User\User.cs" />
+    <Compile Include="ITypeFinder.cs" />
     <Compile Include="IWebHelper.cs" />
+    <Compile Include="Plugins\BasePlugin.cs" />
+    <Compile Include="Plugins\IPlugin.cs" />
+    <Compile Include="Plugins\IPluginFinder.cs" />
+    <Compile Include="Plugins\LoadPluginsMode.cs" />
+    <Compile Include="Plugins\PluginDescriptor.cs" />
+    <Compile Include="Plugins\PluginExtensions.cs" />
+    <Compile Include="Plugins\PluginFileParser.cs" />
+    <Compile Include="Plugins\PluginFinder.cs" />
+    <Compile Include="Plugins\PluginManager.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Singleton.cs" />
+    <Compile Include="WebAppTypeFinder.cs" />
     <Compile Include="WebHelper.cs" />
   </ItemGroup>
-  <ItemGroup />
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 26 - 0
GreenTree.Nachtragsmanagement.Core/ITypeFinder.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core
+{
+    /// <summary>
+    /// Classes implementing this interface provide information about types 
+    /// to various services in the Nop engine.
+    /// </summary>
+    public interface ITypeFinder
+    {
+        IList<Assembly> GetAssemblies();
+
+        IEnumerable<Type> FindClassesOfType(Type assignTypeFrom, bool onlyConcreteClasses = true);
+
+        IEnumerable<Type> FindClassesOfType(Type assignTypeFrom, IEnumerable<Assembly> assemblies, bool onlyConcreteClasses = true);
+
+        IEnumerable<Type> FindClassesOfType<T>(bool onlyConcreteClasses = true);
+
+        IEnumerable<Type> FindClassesOfType<T>(IEnumerable<Assembly> assemblies, bool onlyConcreteClasses = true);
+    }
+}

+ 33 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/BasePlugin.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core.Plugins
+{
+    public abstract class BasePlugin : IPlugin
+    {
+        /// <summary>
+        /// Gets or sets the plugin descriptor
+        /// </summary>
+        public virtual PluginDescriptor PluginDescriptor { get; set; }
+
+        /// <summary>
+        /// Install plugin
+        /// </summary>
+        public virtual void Install()
+        {
+            PluginManager.MarkPluginAsInstalled(this.PluginDescriptor.SystemName);
+        }
+
+        /// <summary>
+        /// Uninstall plugin
+        /// </summary>
+        public virtual void Uninstall()
+        {
+            PluginManager.MarkPluginAsUninstalled(this.PluginDescriptor.SystemName);
+        }
+
+    }
+}

+ 30 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/IPlugin.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core.Plugins
+{
+    /// <summary>
+    /// Interface denoting plug-in attributes that are displayed throughout 
+    /// the editing interface.
+    /// </summary>
+    public interface IPlugin
+    {
+        /// <summary>
+        /// Gets or sets the plugin descriptor
+        /// </summary>
+        PluginDescriptor PluginDescriptor { get; set; }
+
+        /// <summary>
+        /// Install plugin
+        /// </summary>
+        void Install();
+
+        /// <summary>
+        /// Uninstall plugin
+        /// </summary>
+        void Uninstall();
+    }
+}

+ 75 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/IPluginFinder.cs

@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core.Plugins
+{
+    /// <summary>
+    /// Plugin finder
+    /// </summary>
+    public interface IPluginFinder
+    {
+        /// <summary>
+        /// Gets plugin groups
+        /// </summary>
+        /// <returns>Plugins groups</returns>
+        IEnumerable<string> GetPluginGroups();
+
+        /// <summary>
+        /// Gets plugins
+        /// </summary>
+        /// <typeparam name="T">The type of plugins to get.</typeparam>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
+        /// <param name="group">Filter by plugin group; pass null to load all records</param>
+        /// <returns>Plugins</returns>
+        IEnumerable<T> GetPlugins<T>(LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly,
+            int storeId = 0, string group = null) where T : class, IPlugin;
+
+        /// <summary>
+        /// Get plugin descriptors
+        /// </summary>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
+        /// <param name="group">Filter by plugin group; pass null to load all records</param>
+        /// <returns>Plugin descriptors</returns>
+        IEnumerable<PluginDescriptor> GetPluginDescriptors(LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly,
+            int storeId = 0, string group = null);
+
+        /// <summary>
+        /// Get plugin descriptors
+        /// </summary>
+        /// <typeparam name="T">The type of plugin to get.</typeparam>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
+        /// <param name="group">Filter by plugin group; pass null to load all records</param>
+        /// <returns>Plugin descriptors</returns>
+        IEnumerable<PluginDescriptor> GetPluginDescriptors<T>(LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly,
+            int storeId = 0, string group = null) where T : class, IPlugin;
+
+        /// <summary>
+        /// Get a plugin descriptor by its system name
+        /// </summary>
+        /// <param name="systemName">Plugin system name</param>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <returns>>Plugin descriptor</returns>
+        PluginDescriptor GetPluginDescriptorBySystemName(string systemName, LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly);
+
+        /// <summary>
+        /// Get a plugin descriptor by its system name
+        /// </summary>
+        /// <typeparam name="T">The type of plugin to get.</typeparam>
+        /// <param name="systemName">Plugin system name</param>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <returns>>Plugin descriptor</returns>
+        PluginDescriptor GetPluginDescriptorBySystemName<T>(string systemName, LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly)
+            where T : class, IPlugin;
+
+        /// <summary>
+        /// Reload plugins
+        /// </summary>
+        void ReloadPlugins();
+    }
+}

+ 27 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/LoadPluginsMode.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core.Plugins
+{
+    /// <summary>
+    /// Represents a mode to load plugins
+    /// </summary>
+    public enum LoadPluginsMode
+    {
+        /// <summary>
+        /// All (Installed & Not installed)
+        /// </summary>
+        All = 0,
+        /// <summary>
+        /// Installed only
+        /// </summary>
+        InstalledOnly = 10,
+        /// <summary>
+        /// Not installed only
+        /// </summary>
+        NotInstalledOnly = 20
+    }
+}

+ 137 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/PluginDescriptor.cs

@@ -0,0 +1,137 @@
+using Autofac;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core.Plugins
+{
+    public class PluginDescriptor : IComparable<PluginDescriptor>
+    {
+        public PluginDescriptor()
+        {
+            this.SupportedVersions = new List<string>();
+            this.LimitedToStores = new List<int>();
+        }
+
+        public PluginDescriptor(Assembly referencedAssembly, FileInfo originalAssemblyFile,
+            Type pluginType)
+            : this()
+        {
+            this.ReferencedAssembly = referencedAssembly;
+            this.OriginalAssemblyFile = originalAssemblyFile;
+            this.PluginType = pluginType;
+        }
+        /// <summary>
+        /// Plugin type
+        /// </summary>
+        public virtual string PluginFileName { get; set; }
+
+        /// <summary>
+        /// Plugin type
+        /// </summary>
+        public virtual Type PluginType { get; set; }
+
+        /// <summary>
+        /// The assembly that has been shadow copied that is active in the application
+        /// </summary>
+        public virtual Assembly ReferencedAssembly { get; internal set; }
+
+        /// <summary>
+        /// The original assembly file that a shadow copy was made from it
+        /// </summary>
+        public virtual FileInfo OriginalAssemblyFile { get; internal set; }
+
+        /// <summary>
+        /// Gets or sets the plugin group
+        /// </summary>
+        public virtual string Group { get; set; }
+
+        /// <summary>
+        /// Gets or sets the friendly name
+        /// </summary>
+        public virtual string FriendlyName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the system name
+        /// </summary>
+        public virtual string SystemName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the version
+        /// </summary>
+        public virtual string Version { get; set; }
+
+        /// <summary>
+        /// Gets or sets the supported versions of nopCommerce
+        /// </summary>
+        public virtual IList<string> SupportedVersions { get; set; }
+
+        /// <summary>
+        /// Gets or sets the author
+        /// </summary>
+        public virtual string Author { get; set; }
+
+        /// <summary>
+        /// Gets or sets the display order
+        /// </summary>
+        public virtual int DisplayOrder { get; set; }
+
+        /// <summary>
+        /// Gets or sets the list of store identifiers in which this plugin is available. If empty, then this plugin is available in all stores
+        /// </summary>
+        public virtual IList<int> LimitedToStores { get; set; }
+
+        /// <summary>
+        /// Gets or sets the value indicating whether plugin is installed
+        /// </summary>
+        public virtual bool Installed { get; set; }
+
+        public virtual T Instance<T>() where T : class, IPlugin
+        {
+            object instance;
+            if (!Singleton<IContainer>.Instance.TryResolve(PluginType, out instance))
+            {
+                instance = Singleton<IContainer>.Instance.ResolveUnregistered(PluginType);
+            }
+            var typedInstance = instance as T;
+            if (typedInstance != null)
+                typedInstance.PluginDescriptor = this;
+            return typedInstance;
+        }
+
+        public IPlugin Instance()
+        {
+            return Instance<IPlugin>();
+        }
+
+        public int CompareTo(PluginDescriptor other)
+        {
+            if (DisplayOrder != other.DisplayOrder)
+                return DisplayOrder.CompareTo(other.DisplayOrder);
+
+            return FriendlyName.CompareTo(other.FriendlyName);
+        }
+
+        public override string ToString()
+        {
+            return FriendlyName;
+        }
+
+        public override bool Equals(object obj)
+        {
+            var other = obj as PluginDescriptor;
+            return other != null &&
+                SystemName != null &&
+                SystemName.Equals(other.SystemName);
+        }
+
+        public override int GetHashCode()
+        {
+            return SystemName.GetHashCode();
+        }
+    }
+}

+ 168 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/PluginExtensions.cs

@@ -0,0 +1,168 @@
+using Autofac;
+using Autofac.Core.Lifetime;
+using Autofac.Integration.Mvc;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+
+namespace GreenTree.Nachtragsmanagement.Core.Plugins
+{
+    public static class PluginExtensions
+    {
+        public static string GetLogoUrl(this PluginDescriptor pluginDescriptor, IWebHelper webHelper)
+        {
+            if (pluginDescriptor == null)
+                throw new ArgumentNullException("pluginDescriptor");
+
+            if (webHelper == null)
+                throw new ArgumentNullException("webHelper");
+
+            if (pluginDescriptor.OriginalAssemblyFile == null || pluginDescriptor.OriginalAssemblyFile.Directory == null)
+            {
+                return null;
+            }
+
+            var pluginDirectory = pluginDescriptor.OriginalAssemblyFile.Directory;
+            var logoLocalPath = Path.Combine(pluginDirectory.FullName, "logo.jpg");
+            if (!File.Exists(logoLocalPath))
+            {
+                return null;
+            }
+
+            string logoUrl = string.Format("~/plugins/{1}/logo.jpg", pluginDirectory.Name);
+            return logoUrl;
+        }
+
+        #region Container
+
+        public static T Resolve<T>(this IContainer container, string key = "", ILifetimeScope scope = null) where T : class
+        {
+            if (scope == null)
+            {
+                //no scope specified
+                scope = Scope(container);
+            }
+            if (string.IsNullOrEmpty(key))
+            {
+                return scope.Resolve<T>();
+            }
+            return scope.ResolveKeyed<T>(key);
+        }
+
+        public static object Resolve(this Type type, IContainer container, ILifetimeScope scope = null)
+        {
+            if (scope == null)
+            {
+                //no scope specified
+                scope = Scope(container);
+            }
+            return scope.Resolve(type);
+        }
+
+        public static T[] ResolveAll<T>(this IContainer container, string key = "", ILifetimeScope scope = null)
+        {
+            if (scope == null)
+            {
+                //no scope specified
+                scope = Scope(container);
+            }
+            if (string.IsNullOrEmpty(key))
+            {
+                return scope.Resolve<IEnumerable<T>>().ToArray();
+            }
+            return scope.ResolveKeyed<IEnumerable<T>>(key).ToArray();
+        }
+
+        public static T ResolveUnregistered<T>(this IContainer container, ILifetimeScope scope = null) where T : class
+        {
+            return ResolveUnregistered(container, typeof(T), scope) as T;
+        }
+
+        public static object ResolveUnregistered(this IContainer container, Type type, ILifetimeScope scope = null)
+        {
+            if (scope == null)
+            {
+                //no scope specified
+                scope = Scope(container);
+            }
+            var constructors = type.GetConstructors();
+            foreach (var constructor in constructors)
+            {
+                try
+                {
+                    var parameters = constructor.GetParameters();
+                    var parameterInstances = new List<object>();
+                    foreach (var parameter in parameters)
+                    {
+                        var service = Resolve(parameter.ParameterType, container, scope);
+                        if (service == null) throw new Exception("Unkown dependency");
+                        parameterInstances.Add(service);
+                    }
+                    return Activator.CreateInstance(type, parameterInstances.ToArray());
+                }
+                catch (Exception)
+                {
+
+                }
+            }
+            throw new Exception("No contructor was found that had all the dependencies satisfied.");
+        }
+
+        public static bool TryResolve(this IContainer container, Type serviceType, ILifetimeScope scope, out object instance)
+        {
+            if (scope == null)
+            {
+                //no scope specified
+                scope = Scope(container);
+            }
+            return scope.TryResolve(serviceType, out instance);
+        }
+
+        public static bool IsRegistered(this IContainer container, Type serviceType, ILifetimeScope scope = null)
+        {
+            if (scope == null)
+            {
+                //no scope specified
+                scope = Scope(container);
+            }
+            return scope.IsRegistered(serviceType);
+        }
+
+        public static object ResolveOptional(this IContainer container, Type serviceType, ILifetimeScope scope = null)
+        {
+            if (scope == null)
+            {
+                //no scope specified
+                scope = Scope(container);
+            }
+            return scope.ResolveOptional(serviceType);
+        }
+
+        public static ILifetimeScope Scope(this IContainer container)
+        {
+            try
+            {
+                if (HttpContext.Current != null)
+                    return AutofacDependencyResolver.Current.RequestLifetimeScope;
+
+                //when such lifetime scope is returned, you should be sure that it'll be disposed once used (e.g. in schedule tasks)
+                return container.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag);
+            }
+            catch (Exception)
+            {
+                //we can get an exception here if RequestLifetimeScope is already disposed
+                //for example, requested in or after "Application_EndRequest" handler
+                //but note that usually it should never happen
+
+                //when such lifetime scope is returned, you should be sure that it'll be disposed once used (e.g. in schedule tasks)
+                return container.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag);
+            }
+        }
+
+        #endregion
+    }
+}

+ 191 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/PluginFileParser.cs

@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core.Plugins
+{
+    /// <summary>
+    /// Plugin files parser
+    /// </summary>
+    public static class PluginFileParser
+    {
+        public static IList<string> ParseInstalledPluginsFile(string filePath)
+        {
+            //read and parse the file
+            if (!File.Exists(filePath))
+                return new List<string>();
+
+            var text = File.ReadAllText(filePath);
+            if (String.IsNullOrEmpty(text))
+                return new List<string>();
+
+            //Old way of file reading. This leads to unexpected behavior when a user's FTP program transfers these files as ASCII (\r\n becomes \n).
+            //var lines = text.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+
+            var lines = new List<string>();
+            using (var reader = new StringReader(text))
+            {
+                string str;
+                while ((str = reader.ReadLine()) != null)
+                {
+                    if (String.IsNullOrWhiteSpace(str))
+                        continue;
+                    lines.Add(str.Trim());
+                }
+            }
+            return lines;
+        }
+
+        public static void SaveInstalledPluginsFile(IList<String> pluginSystemNames, string filePath)
+        {
+            string result = "";
+            foreach (var sn in pluginSystemNames)
+                result += string.Format("{0}{1}", sn, Environment.NewLine);
+
+            File.WriteAllText(filePath, result);
+        }
+
+        public static PluginDescriptor ParsePluginDescriptionFile(string filePath)
+        {
+            var descriptor = new PluginDescriptor();
+            var text = File.ReadAllText(filePath);
+            if (String.IsNullOrEmpty(text))
+                return descriptor;
+
+            var settings = new List<string>();
+            using (var reader = new StringReader(text))
+            {
+                string str;
+                while ((str = reader.ReadLine()) != null)
+                {
+                    if (String.IsNullOrWhiteSpace(str))
+                        continue;
+                    settings.Add(str.Trim());
+                }
+            }
+
+            //Old way of file reading. This leads to unexpected behavior when a user's FTP program transfers these files as ASCII (\r\n becomes \n).
+            //var settings = text.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+
+            foreach (var setting in settings)
+            {
+                var separatorIndex = setting.IndexOf(':');
+                if (separatorIndex == -1)
+                {
+                    continue;
+                }
+                string key = setting.Substring(0, separatorIndex).Trim();
+                string value = setting.Substring(separatorIndex + 1).Trim();
+
+                switch (key)
+                {
+                    case "Group":
+                        descriptor.Group = value;
+                        break;
+                    case "FriendlyName":
+                        descriptor.FriendlyName = value;
+                        break;
+                    case "SystemName":
+                        descriptor.SystemName = value;
+                        break;
+                    case "Version":
+                        descriptor.Version = value;
+                        break;
+                    case "SupportedVersions":
+                        {
+                            //parse supported versions
+                            descriptor.SupportedVersions = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                                .Select(x => x.Trim())
+                                .ToList();
+                        }
+                        break;
+                    case "Author":
+                        descriptor.Author = value;
+                        break;
+                    case "DisplayOrder":
+                        {
+                            int displayOrder;
+                            int.TryParse(value, out displayOrder);
+                            descriptor.DisplayOrder = displayOrder;
+                        }
+                        break;
+                    case "FileName":
+                        descriptor.PluginFileName = value;
+                        break;
+                    case "LimitedToStores":
+                        {
+                            //parse list of store IDs
+                            foreach (var str1 in value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                                                      .Select(x => x.Trim()))
+                            {
+                                int storeId;
+                                if (int.TryParse(str1, out storeId))
+                                {
+                                    descriptor.LimitedToStores.Add(storeId);
+                                }
+                            }
+                        }
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            //nopCommerce 2.00 didn't have 'SupportedVersions' parameter
+            //so let's set it to "2.00"
+            if (descriptor.SupportedVersions.Count == 0)
+                descriptor.SupportedVersions.Add("2.00");
+
+            return descriptor;
+        }
+
+        public static void SavePluginDescriptionFile(PluginDescriptor plugin)
+        {
+            if (plugin == null)
+                throw new ArgumentException("plugin");
+
+            //get the Description.txt file path
+            if (plugin.OriginalAssemblyFile == null)
+                throw new Exception(string.Format("Cannot load original assembly path for {0} plugin.", plugin.SystemName));
+            var filePath = Path.Combine(plugin.OriginalAssemblyFile.Directory.FullName, "Description.txt");
+            if (!File.Exists(filePath))
+                throw new Exception(string.Format("Description file for {0} plugin does not exist. {1}", plugin.SystemName, filePath));
+
+            var keyValues = new List<KeyValuePair<string, string>>();
+            keyValues.Add(new KeyValuePair<string, string>("Group", plugin.Group));
+            keyValues.Add(new KeyValuePair<string, string>("FriendlyName", plugin.FriendlyName));
+            keyValues.Add(new KeyValuePair<string, string>("SystemName", plugin.SystemName));
+            keyValues.Add(new KeyValuePair<string, string>("Version", plugin.Version));
+            keyValues.Add(new KeyValuePair<string, string>("SupportedVersions", string.Join(",", plugin.SupportedVersions)));
+            keyValues.Add(new KeyValuePair<string, string>("Author", plugin.Author));
+            keyValues.Add(new KeyValuePair<string, string>("DisplayOrder", plugin.DisplayOrder.ToString()));
+            keyValues.Add(new KeyValuePair<string, string>("FileName", plugin.PluginFileName));
+            if (plugin.LimitedToStores.Count > 0)
+            {
+                var storeList = "";
+                for (int i = 0; i < plugin.LimitedToStores.Count; i++)
+                {
+                    storeList += plugin.LimitedToStores[i];
+                    if (i != plugin.LimitedToStores.Count - 1)
+                        storeList += ",";
+                }
+                keyValues.Add(new KeyValuePair<string, string>("LimitedToStores", storeList));
+            }
+
+            var sb = new StringBuilder();
+            for (int i = 0; i < keyValues.Count; i++)
+            {
+                var key = keyValues[i].Key;
+                var value = keyValues[i].Value;
+                sb.AppendFormat("{0}: {1}", key, value);
+                if (i != keyValues.Count - 1)
+                    sb.Append(Environment.NewLine);
+            }
+            //save the file
+            File.WriteAllText(filePath, sb.ToString());
+        }
+    }
+}

+ 176 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/PluginFinder.cs

@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace GreenTree.Nachtragsmanagement.Core.Plugins
+{
+    /// <summary>
+    /// Plugin finder
+    /// </summary>
+    public class PluginFinder : IPluginFinder
+    {
+        #region Fields
+
+        private IList<PluginDescriptor> _plugins;
+        private bool _arePluginsLoaded;
+
+        #endregion
+
+        #region Utilities
+
+        /// <summary>
+        /// Ensure plugins are loaded
+        /// </summary>
+        protected virtual void EnsurePluginsAreLoaded()
+        {
+            if (!_arePluginsLoaded)
+            {
+                var foundPlugins = PluginManager.ReferencedPlugins.ToList();
+                foundPlugins.Sort();
+                _plugins = foundPlugins.ToList();
+
+                _arePluginsLoaded = true;
+            }
+        }
+
+        /// <summary>
+        /// Check whether the plugin is available in a certain store
+        /// </summary>
+        /// <param name="pluginDescriptor">Plugin descriptor to check</param>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <returns>true - available; false - no</returns>
+        protected virtual bool CheckLoadMode(PluginDescriptor pluginDescriptor, LoadPluginsMode loadMode)
+        {
+            if (pluginDescriptor == null)
+                throw new ArgumentNullException("pluginDescriptor");
+
+            switch (loadMode)
+            {
+                case LoadPluginsMode.All:
+                    //no filering
+                    return true;
+                case LoadPluginsMode.InstalledOnly:
+                    return pluginDescriptor.Installed;
+                case LoadPluginsMode.NotInstalledOnly:
+                    return !pluginDescriptor.Installed;
+                default:
+                    throw new Exception("Not supported LoadPluginsMode");
+            }
+        }
+
+        /// <summary>
+        /// Check whether the plugin is in a certain group
+        /// </summary>
+        /// <param name="pluginDescriptor">Plugin descriptor to check</param>
+        /// <param name="group">Group</param>
+        /// <returns>true - available; false - no</returns>
+        protected virtual bool CheckGroup(PluginDescriptor pluginDescriptor, string group)
+        {
+            if (pluginDescriptor == null)
+                throw new ArgumentNullException("pluginDescriptor");
+
+            if (String.IsNullOrEmpty(group))
+                return true;
+
+            return group.Equals(pluginDescriptor.Group, StringComparison.InvariantCultureIgnoreCase);
+        }
+
+        #endregion
+
+        #region Methods
+
+        /// <summary>
+        /// Gets plugin groups
+        /// </summary>
+        /// <returns>Plugins groups</returns>
+        public virtual IEnumerable<string> GetPluginGroups()
+        {
+            return GetPluginDescriptors(LoadPluginsMode.All).Select(x => x.Group).Distinct().OrderBy(x => x);
+        }
+
+        /// <summary>
+        /// Gets plugins
+        /// </summary>
+        /// <typeparam name="T">The type of plugins to get.</typeparam>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
+        /// <param name="group">Filter by plugin group; pass null to load all records</param>
+        /// <returns>Plugins</returns>
+        public virtual IEnumerable<T> GetPlugins<T>(LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly,
+            int storeId = 0, string group = null) where T : class, IPlugin
+        {
+            return GetPluginDescriptors<T>(loadMode, storeId, group).Select(p => p.Instance<T>());
+        }
+
+        /// <summary>
+        /// Get plugin descriptors
+        /// </summary>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
+        /// <param name="group">Filter by plugin group; pass null to load all records</param>
+        /// <returns>Plugin descriptors</returns>
+        public virtual IEnumerable<PluginDescriptor> GetPluginDescriptors(LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly,
+            int storeId = 0, string group = null)
+        {
+            //ensure plugins are loaded
+            EnsurePluginsAreLoaded();
+
+            return _plugins.Where(p => CheckLoadMode(p, loadMode) && CheckGroup(p, group));
+        }
+
+        /// <summary>
+        /// Get plugin descriptors
+        /// </summary>
+        /// <typeparam name="T">The type of plugin to get.</typeparam>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
+        /// <param name="group">Filter by plugin group; pass null to load all records</param>
+        /// <returns>Plugin descriptors</returns>
+        public virtual IEnumerable<PluginDescriptor> GetPluginDescriptors<T>(LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly,
+            int storeId = 0, string group = null)
+            where T : class, IPlugin
+        {
+            return GetPluginDescriptors(loadMode, storeId, group)
+                .Where(p => typeof(T).IsAssignableFrom(p.PluginType));
+        }
+
+        /// <summary>
+        /// Get a plugin descriptor by its system name
+        /// </summary>
+        /// <param name="systemName">Plugin system name</param>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <returns>>Plugin descriptor</returns>
+        public virtual PluginDescriptor GetPluginDescriptorBySystemName(string systemName, LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly)
+        {
+            return GetPluginDescriptors(loadMode)
+                .SingleOrDefault(p => p.SystemName.Equals(systemName, StringComparison.InvariantCultureIgnoreCase));
+        }
+
+        /// <summary>
+        /// Get a plugin descriptor by its system name
+        /// </summary>
+        /// <typeparam name="T">The type of plugin to get.</typeparam>
+        /// <param name="systemName">Plugin system name</param>
+        /// <param name="loadMode">Load plugins mode</param>
+        /// <returns>>Plugin descriptor</returns>
+        public virtual PluginDescriptor GetPluginDescriptorBySystemName<T>(string systemName, LoadPluginsMode loadMode = LoadPluginsMode.InstalledOnly)
+            where T : class, IPlugin
+        {
+            return GetPluginDescriptors<T>(loadMode)
+                .SingleOrDefault(p => p.SystemName.Equals(systemName, StringComparison.InvariantCultureIgnoreCase));
+        }
+
+        /// <summary>
+        /// Reload plugins
+        /// </summary>
+        public virtual void ReloadPlugins()
+        {
+            _arePluginsLoaded = false;
+            EnsurePluginsAreLoaded();
+        }
+
+        #endregion
+    }
+}

+ 476 - 0
GreenTree.Nachtragsmanagement.Core/Plugins/PluginManager.cs

@@ -0,0 +1,476 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using GreenTree.Nachtragsmanagement.Core.Plugins;
+using System.Threading;
+using System.Web.Hosting;
+using GreenTree.Nachtragsmanagement.Core.ComponentModel;
+using System.Configuration;
+using System.Reflection;
+using System.Web.Compilation;
+
+[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]
+namespace GreenTree.Nachtragsmanagement.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
+
+        /// <summary>
+        /// Returns a collection of all referenced plugin assemblies that have been shadow copied
+        /// </summary>
+        public static IEnumerable<PluginDescriptor> ReferencedPlugins { get; set; }
+
+        /// <summary>
+        /// Returns a collection of all plugin which are not compatible with the current version
+        /// </summary>
+        public static IEnumerable<string> IncompatiblePlugins { get; set; }
+
+        /// <summary>
+        /// Initialize
+        /// </summary>
+        public static void Initialize()
+        {
+            using (new WriteLockDisposable(Locker))
+            {
+                // TODO: Add verbose exception handling / raising here since this is happening on app startup and could
+                // prevent app from starting altogether
+                var pluginFolder = new DirectoryInfo(HostingEnvironment.MapPath(PluginsPath));
+                _shadowCopyFolder = new DirectoryInfo(HostingEnvironment.MapPath(ShadowCopyPath));
+
+                var referencedPlugins = new List<PluginDescriptor>();
+                var incompatiblePlugins = new List<string>();
+
+                _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) &&
+                   Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]);
+
+                try
+                {
+                    var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
+
+                    Debug.WriteLine("Creating shadow copy folder and querying for dlls");
+                    //ensure folders are created
+                    Directory.CreateDirectory(pluginFolder.FullName);
+                    Directory.CreateDirectory(_shadowCopyFolder.FullName);
+
+                    //get list of all files in bin
+                    var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories);
+                    if (_clearShadowDirectoryOnStartup)
+                    {
+                        //clear out shadow copied plugins
+                        foreach (var f in binFiles)
+                        {
+                            Debug.WriteLine("Deleting " + f.Name);
+                            try
+                            {
+                                File.Delete(f.FullName);
+                            }
+                            catch (Exception exc)
+                            {
+                                Debug.WriteLine("Error deleting file " + f.Name + ". Exception: " + exc);
+                            }
+                        }
+                    }
+
+                    //load description files
+                    foreach (var dfd in GetDescriptionFilesAndDescriptors(pluginFolder))
+                    {
+                        var descriptionFile = dfd.Key;
+                        var pluginDescriptor = dfd.Value;
+
+                        //ensure that version of plugin is valid
+                        if (!pluginDescriptor.SupportedVersions.Contains(AppendixVersion.CurrentVersion, StringComparer.InvariantCultureIgnoreCase))
+                        {
+                            incompatiblePlugins.Add(pluginDescriptor.SystemName);
+                            continue;
+                        }
+
+                        //some validation
+                        if (String.IsNullOrWhiteSpace(pluginDescriptor.SystemName))
+                            throw new Exception(string.Format("A plugin '{0}' has no system name. Try assigning the plugin a unique name and recompiling.", descriptionFile.FullName));
+                        if (referencedPlugins.Contains(pluginDescriptor))
+                            throw new Exception(string.Format("A plugin with '{0}' system name is already defined", pluginDescriptor.SystemName));
+
+                        //set 'Installed' property
+                        pluginDescriptor.Installed = installedPluginSystemNames
+                            .FirstOrDefault(x => x.Equals(pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase)) != null;
+
+                        try
+                        {
+                            if (descriptionFile.Directory == null)
+                                throw new Exception(string.Format("Directory cannot be resolved for '{0}' description file", descriptionFile.Name));
+                            //get list of all DLLs in plugins (not in bin!)
+                            var pluginFiles = descriptionFile.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();
+
+                            //other plugin description info
+                            var mainPluginFile = pluginFiles
+                                .FirstOrDefault(x => x.Name.Equals(pluginDescriptor.PluginFileName, StringComparison.InvariantCultureIgnoreCase));
+                            pluginDescriptor.OriginalAssemblyFile = mainPluginFile;
+
+                            //shadow copy main plugin file
+                            pluginDescriptor.ReferencedAssembly = PerformFileDeploy(mainPluginFile);
+
+                            //load all other referenced assemblies now
+                            foreach (var plugin in pluginFiles
+                                .Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase))
+                                .Where(x => !IsAlreadyLoaded(x)))
+                                PerformFileDeploy(plugin);
+
+                            //init plugin type (only one plugin per assembly is allowed)
+                            foreach (var t in pluginDescriptor.ReferencedAssembly.GetTypes())
+                                if (typeof(IPlugin).IsAssignableFrom(t))
+                                    if (!t.IsInterface)
+                                        if (t.IsClass && !t.IsAbstract)
+                                        {
+                                            pluginDescriptor.PluginType = t;
+                                            break;
+                                        }
+
+                            referencedPlugins.Add(pluginDescriptor);
+                        }
+                        catch (ReflectionTypeLoadException ex)
+                        {
+                            var msg = string.Empty;
+                            foreach (var e in ex.LoaderExceptions)
+                                msg += e.Message + Environment.NewLine;
+
+                            var fail = new Exception(msg, ex);
+                            Debug.WriteLine(fail.Message, fail);
+
+                            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);
+                    Debug.WriteLine(fail.Message, fail);
+
+                    throw fail;
+                }
+
+
+                ReferencedPlugins = referencedPlugins;
+                IncompatiblePlugins = incompatiblePlugins;
+
+            }
+        }
+
+        /// <summary>
+        /// Mark plugin as installed
+        /// </summary>
+        /// <param name="systemName">Plugin system name</param>
+        public static void MarkPluginAsInstalled(string systemName)
+        {
+            if (String.IsNullOrEmpty(systemName))
+                throw new ArgumentNullException("systemName");
+
+            var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath);
+            if (!File.Exists(filePath))
+                using (File.Create(filePath))
+                {
+                    //we use 'using' to close the file after it's created
+                }
+
+
+            var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
+            bool alreadyMarkedAsInstalled = installedPluginSystemNames
+                                .FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null;
+            if (!alreadyMarkedAsInstalled)
+                installedPluginSystemNames.Add(systemName);
+            PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames, filePath);
+        }
+
+        /// <summary>
+        /// Mark plugin as uninstalled
+        /// </summary>
+        /// <param name="systemName">Plugin system name</param>
+        public static void MarkPluginAsUninstalled(string systemName)
+        {
+            if (String.IsNullOrEmpty(systemName))
+                throw new ArgumentNullException("systemName");
+
+            var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath);
+            if (!File.Exists(filePath))
+                using (File.Create(filePath))
+                {
+                    //we use 'using' to close the file after it's created
+                }
+
+
+            var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
+            bool alreadyMarkedAsInstalled = installedPluginSystemNames
+                                .FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null;
+            if (alreadyMarkedAsInstalled)
+                installedPluginSystemNames.Remove(systemName);
+            PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames, filePath);
+        }
+
+        /// <summary>
+        /// Mark plugin as uninstalled
+        /// </summary>
+        public static void MarkAllPluginsAsUninstalled()
+        {
+            var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath);
+            if (File.Exists(filePath))
+                File.Delete(filePath);
+        }
+
+        #endregion
+
+        #region Utilities
+
+        /// <summary>
+        /// Get description files
+        /// </summary>
+        /// <param name="pluginFolder">Plugin direcotry info</param>
+        /// <returns>Original and parsed description files</returns>
+        private static IEnumerable<KeyValuePair<FileInfo, PluginDescriptor>> GetDescriptionFilesAndDescriptors(DirectoryInfo pluginFolder)
+        {
+            if (pluginFolder == null)
+                throw new ArgumentNullException("pluginFolder");
+
+            //create list (<file info, parsed plugin descritor>)
+            var result = new List<KeyValuePair<FileInfo, PluginDescriptor>>();
+            //add display order and path to list
+            foreach (var descriptionFile in pluginFolder.GetFiles("Description.txt", SearchOption.AllDirectories))
+            {
+                if (!IsPackagePluginFolder(descriptionFile.Directory))
+                    continue;
+
+                //parse file
+                var pluginDescriptor = PluginFileParser.ParsePluginDescriptionFile(descriptionFile.FullName);
+
+                //populate list
+                result.Add(new KeyValuePair<FileInfo, PluginDescriptor>(descriptionFile, pluginDescriptor));
+            }
+            result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder));
+            return result;
+        }
+
+        /// <summary>
+        /// Indicates whether assembly file is already loaded
+        /// </summary>
+        /// <param name="fileInfo">File info</param>
+        /// <returns>Result</returns>
+        private static bool IsAlreadyLoaded(FileInfo fileInfo)
+        {
+            //compare full assembly name
+            //var fileAssemblyName = AssemblyName.GetAssemblyName(fileInfo.FullName);
+            //foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
+            //{
+            //    if (a.FullName.Equals(fileAssemblyName.FullName, StringComparison.InvariantCultureIgnoreCase))
+            //        return true;
+            //}
+            //return false;
+
+            //do not compare the full assembly name, just filename
+            try
+            {
+                string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName);
+                if (fileNameWithoutExt == null)
+                    throw new Exception(string.Format("Cannot get file extnension for {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("Cannot validate whether an assembly is already loaded. " + exc);
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// Perform file deply
+        /// </summary>
+        /// <param name="plug">Plugin file info</param>
+        /// <returns>Assembly</returns>
+        private static Assembly PerformFileDeploy(FileInfo plug)
+        {
+            if (plug.Directory.Parent == null)
+                throw new InvalidOperationException("The plugin directory for the " + plug.Name +
+                                                    " file exists in a folder outside of the allowed nopCommerce folder heirarchy");
+
+            FileInfo shadowCopiedPlug;
+
+            if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
+            {
+                //all plugins will need to be copied to ~/Plugins/bin/
+                //this is aboslutely required because all of this relies on probingPaths being set statically in the web.config
+
+                //were running in med trust, so copy to custom bin folder
+                var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName);
+                shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder);
+            }
+            else
+            {
+                var directory = AppDomain.CurrentDomain.DynamicDirectory;
+                Debug.WriteLine(plug.FullName + " to " + directory);
+                //were running in full trust so copy to standard dynamic folder
+                shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory));
+            }
+
+            //we can now register the plugin definition
+            var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));
+
+            //add the reference to the build manager
+            Debug.WriteLine("Adding to BuildManager: '{0}'", shadowCopiedAssembly.FullName);
+            BuildManager.AddReferencedAssembly(shadowCopiedAssembly);
+
+            return shadowCopiedAssembly;
+        }
+
+        /// <summary>
+        /// Used to initialize plugins when running in Full Trust
+        /// </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 + " is locked, attempting to rename");
+                //this occurs when the files are locked,
+                //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
+                //which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy
+                try
+                {
+                    var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
+                    File.Move(shadowCopiedPlug.FullName, oldFile);
+                }
+                catch (IOException exc)
+                {
+                    throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc);
+                }
+                //ok, we've made it this far, now retry the shadow copy
+                File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
+            }
+            return shadowCopiedPlug;
+        }
+
+        /// <summary>
+        /// Used to initialize plugins when running in Medium Trust
+        /// </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));
+
+            //check if a shadow copied file already exists and if it does, check if it's updated, if not don't copy
+            if (shadowCopiedPlug.Exists)
+            {
+                //it's better to use LastWriteTimeUTC, but not all file systems have this property
+                //maybe it is better to compare file hash?
+                var areFilesIdentical = shadowCopiedPlug.CreationTimeUtc.Ticks >= plug.CreationTimeUtc.Ticks;
+                if (areFilesIdentical)
+                {
+                    Debug.WriteLine("Not copying; files appear identical: '{0}'", shadowCopiedPlug.Name);
+                    shouldCopy = false;
+                }
+                else
+                {
+                    //delete an existing file
+
+                    //More info: http://www.nopcommerce.com/boards/t/11511/access-error-nopplugindiscountrulesbillingcountrydll.aspx?p=4#60838
+                    Debug.WriteLine("New plugin found; Deleting the old file: '{0}'", shadowCopiedPlug.Name);
+                    File.Delete(shadowCopiedPlug.FullName);
+                }
+            }
+
+            if (shouldCopy)
+            {
+                try
+                {
+                    File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
+                }
+                catch (IOException)
+                {
+                    Debug.WriteLine(shadowCopiedPlug.FullName + " is locked, attempting to rename");
+                    //this occurs when the files are locked,
+                    //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
+                    //which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy
+                    try
+                    {
+                        var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
+                        File.Move(shadowCopiedPlug.FullName, oldFile);
+                    }
+                    catch (IOException exc)
+                    {
+                        throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc);
+                    }
+                    //ok, we've made it this far, now retry the shadow copy
+                    File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
+                }
+            }
+
+            return shadowCopiedPlug;
+        }
+
+        /// <summary>
+        /// Determines if the folder is a bin plugin folder for a package
+        /// </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>
+        /// Gets the full path of InstalledPlugins.txt file
+        /// </summary>
+        /// <returns></returns>
+        private static string GetInstalledPluginsFilePath()
+        {
+            var filePath = HostingEnvironment.MapPath(InstalledPluginsFilePath);
+            return filePath;
+        }
+
+        #endregion
+    }
+}

+ 1 - 1
GreenTree.Nachtragsmanagement.Web.Framework/Singleton.cs → GreenTree.Nachtragsmanagement.Core/Singleton.cs

@@ -4,7 +4,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 
-namespace GreenTree.Nachtragsmanagement.Web.Framework
+namespace GreenTree.Nachtragsmanagement.Core
 {
     /// <summary>
     /// A statically compiled "singleton" used to store objects throughout the 

+ 80 - 0
GreenTree.Nachtragsmanagement.Core/WebAppTypeFinder.cs

@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using System.Web.Hosting;
+
+namespace GreenTree.Nachtragsmanagement.Core
+{
+    /// <summary>
+    /// Provides information about types in the current web application. 
+    /// Optionally this class can look at all assemblies in the bin folder.
+    /// </summary>
+    public class WebAppTypeFinder : AppDomainTypeFinder
+    {
+        #region Fields
+
+        private bool _ensureBinFolderAssembliesLoaded = true;
+        private bool _binFolderAssembliesLoaded;
+
+        #endregion
+
+        #region Ctor
+
+        public WebAppTypeFinder()
+        {
+            _ensureBinFolderAssembliesLoaded = true;
+        }
+
+        #endregion
+
+        #region Properties
+
+        /// <summary>
+        /// Gets or sets wether assemblies in the bin folder of the web application should be specificly checked for beeing loaded on application load. This is need in situations where plugins need to be loaded in the AppDomain after the application been reloaded.
+        /// </summary>
+        public bool EnsureBinFolderAssembliesLoaded
+        {
+            get { return _ensureBinFolderAssembliesLoaded; }
+            set { _ensureBinFolderAssembliesLoaded = value; }
+        }
+
+        #endregion
+
+        #region Methods
+
+        /// <summary>
+        /// Gets a physical disk path of \Bin directory
+        /// </summary>
+        /// <returns>The physical path. E.g. "c:\inetpub\wwwroot\bin"</returns>
+        public virtual string GetBinDirectory()
+        {
+            if (HostingEnvironment.IsHosted)
+            {
+                //hosted
+                return HttpRuntime.BinDirectory;
+            }
+
+            //not hosted. For example, run either in unit tests
+            return AppDomain.CurrentDomain.BaseDirectory;
+        }
+
+        public override IList<Assembly> GetAssemblies()
+        {
+            if (this.EnsureBinFolderAssembliesLoaded && !_binFolderAssembliesLoaded)
+            {
+                _binFolderAssembliesLoaded = true;
+                string binPath = GetBinDirectory();
+                //binPath = _webHelper.MapPath("~/bin");
+                LoadMatchingAssemblies(binPath);
+            }
+
+            return base.GetAssemblies();
+        }
+
+        #endregion
+    }
+}

+ 1 - 1
GreenTree.Nachtragsmanagement.Core/WebHelper.cs

@@ -10,7 +10,7 @@ using System.Web.Hosting;
 
 namespace GreenTree.Nachtragsmanagement.Core
 {
-    public class WebHelper
+    public class WebHelper : IWebHelper
     {
         #region Fields 
 

+ 9 - 0
GreenTree.Nachtragsmanagement.Core/packages.config

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Autofac" version="4.0.1" targetFramework="net452" />
+  <package id="Autofac.Mvc5" version="4.0.2" targetFramework="net452" />
+  <package id="Microsoft.AspNet.Mvc" version="5.1.0" targetFramework="net452" />
+  <package id="Microsoft.AspNet.Razor" version="3.1.0" targetFramework="net452" />
+  <package id="Microsoft.AspNet.WebPages" version="3.1.0" targetFramework="net452" />
+  <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net452" />
+</packages>

+ 27 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/Controllers/TestController.cs

@@ -0,0 +1,27 @@
+using GreenTree.Nachtragsmanagement.Services.User;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web.Mvc;
+
+namespace GreenTree.Nachtragsmanagement.Plugin.Test.Controllers
+{
+    public class TestController : Controller
+    {
+        private readonly IUserService _userService;
+
+        public TestController(
+            IUserService userService)
+        {
+            _userService = userService;
+        }
+
+        // GET: Home
+        public ActionResult Index()
+        {
+            return View("~/Views/Home/Index.cshtml");
+        }
+    }
+}

+ 8 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/Description.txt

@@ -0,0 +1,8 @@
+Group: Misc
+FriendlyName: Nachtragsmanagement Test
+SystemName: GreenTree.Nachtragsmanagement.Test
+Version: 1.0
+SupportedVersions: 1.0.0
+Author: GreenTree Studios
+DisplayOrder: 1
+FileName: GreenTree.Nachtragsmanagement.Plugin.Test

+ 118 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/GreenTree.Nachtragsmanagement.Plugin.Test.csproj

@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{702A7197-9C1F-4D47-8EF0-DB27E97CD66F}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>GreenTree.Nachtragsmanagement.Plugin.Test</RootNamespace>
+    <AssemblyName>GreenTree.Nachtragsmanagement.Plugin.Test</AssemblyName>
+    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
+      <HintPath>..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll</HintPath>
+    </Reference>
+    <Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
+      <HintPath>..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.ComponentModel.DataAnnotations" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Web" />
+    <Reference Include="System.Web.Helpers, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.Services" />
+    <Reference Include="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages.Deployment, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Controllers\TestController.cs" />
+    <Compile Include="RouteProvider.cs" />
+    <Compile Include="TestPlugin.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\GreenTree.Nachtragsmanagement.Core\GreenTree.Nachtragsmanagement.Core.csproj">
+      <Project>{0b80c4a0-cb8f-423c-8af4-92b489c238dd}</Project>
+      <Name>GreenTree.Nachtragsmanagement.Core</Name>
+      <Private>False</Private>
+    </ProjectReference>
+    <ProjectReference Include="..\GreenTree.Nachtragsmanagement.Data\GreenTree.Nachtragsmanagement.Data.csproj">
+      <Project>{0c45ecbc-6ad6-4eb1-89bb-f05a3f0fda13}</Project>
+      <Name>GreenTree.Nachtragsmanagement.Data</Name>
+      <Private>False</Private>
+    </ProjectReference>
+    <ProjectReference Include="..\GreenTree.Nachtragsmanagement.Services\GreenTree.Nachtragsmanagement.Services.csproj">
+      <Project>{7cc45abb-5398-49a2-85f5-966718afa7d8}</Project>
+      <Name>GreenTree.Nachtragsmanagement.Services</Name>
+      <Private>False</Private>
+    </ProjectReference>
+    <ProjectReference Include="..\GreenTree.Nachtragsmanagement.Web.Framework\GreenTree.Nachtragsmanagement.Web.Framework.csproj">
+      <Project>{faff64ea-de01-40bf-b805-de48d4cbef1c}</Project>
+      <Name>GreenTree.Nachtragsmanagement.Web.Framework</Name>
+      <Private>False</Private>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Content Include="Description.txt">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="logo.jpg">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+  <ItemGroup>
+    <Folder Include="Content\" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="app.config" />
+    <None Include="packages.config" />
+    <Content Include="Views\Home\Index.cshtml" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project>

+ 36 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/Properties/AssemblyInfo.cs

@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// Allgemeine Informationen über eine Assembly werden über die folgenden
+// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern,
+// die einer Assembly zugeordnet sind.
+[assembly: AssemblyTitle("GreenTree.Nachtragsmanagement.Plugin.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("GreenTree.Nachtragsmanagement.Plugin.Test")]
+[assembly: AssemblyCopyright("Copyright ©  2017")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly
+// für COM-Komponenten unsichtbar.  Wenn Sie auf einen Typ in dieser Assembly von
+// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen.
+[assembly: ComVisible(false)]
+
+// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird
+[assembly: Guid("702a7197-9c1f-4d47-8ef0-db27e97cd66f")]
+
+// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten:
+//
+//      Hauptversion
+//      Nebenversion
+//      Buildnummer
+//      Revision
+//
+// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden,
+// indem Sie "*" wie unten gezeigt eingeben:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 36 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/RouteProvider.cs

@@ -0,0 +1,36 @@
+using GreenTree.Nachtragsmanagement.Web.Framework.Mvc.Routes;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web.Mvc;
+using System.Web.Routing;
+
+namespace GreenTree.Nachtragsmanagement.Plugin.Test
+{
+    public class RouteProvider : IRouteProvider
+    {
+        public int Priority
+        {
+            get { return 0; }
+        }
+
+        public void RegisterRoutes(RouteCollection routes)
+        {
+            routes.MapRoute(
+                "GreenTree.Nachtragsmanagement.Plugin.Test.Index",
+                "plugins/test/",
+                new
+                {
+                    controller = "Test",
+                    action = "Index"
+                },
+                new[]
+                {
+                    "GreenTree.Nachtragsmanagement.Plugin.Test.Controllers"
+                }
+            );
+        }
+    }
+}

+ 32 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/TestPlugin.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using GreenTree.Nachtragsmanagement.Core.Plugins;
+using System.Web.Mvc;
+using System.Web.Routing;
+
+namespace GreenTree.Nachtragsmanagement.Plugin.Test
+{
+    public class TestPlugin : BasePlugin, IPlugin
+    {
+        public override void Install()
+        {
+            RouteTable.Routes.MapRoute(
+                name: "Default", // Route name
+                url: "{controller}/{action}", // URL with parameters
+                defaults: new { controller = "Test", action = "Index" } // Parameter defaults
+            );
+
+            base.Install();
+        }
+
+        public override void Uninstall()
+        {
+
+
+            base.Uninstall();
+        }
+    }
+}

+ 12 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/Views/Home/Index.cshtml

@@ -0,0 +1,12 @@
+@{
+    ViewBag.Title = "Index";
+    Layout = "~/Views/Shared/_Layout.cshtml";
+}
+
+<h2>Plugin (GreenTree.Nachtragsmanagement.Plugin.Test) - Home</h2>
+
+@Html.Partial("~/Views/Shared/_HeaderNavBar.cshtml")
+
+<h5>
+	Läuft
+</h5>

+ 21 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/app.config

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <configSections>
+    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
+    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
+  </configSections>
+  <runtime>
+    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+      <dependentAssembly>
+        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
+      </dependentAssembly>
+    </assemblyBinding>
+  </runtime>
+  <entityFramework>
+    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
+    <providers>
+      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
+    </providers>
+  </entityFramework>
+</configuration>

BIN
GreenTree.Nachtragsmanagement.Plugin.Test/logo.jpg


+ 8 - 0
GreenTree.Nachtragsmanagement.Plugin.Test/packages.config

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="EntityFramework" version="6.1.3" targetFramework="net452" />
+  <package id="Microsoft.AspNet.Mvc" version="5.2.3" targetFramework="net452" />
+  <package id="Microsoft.AspNet.Razor" version="3.2.3" targetFramework="net452" />
+  <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net452" />
+  <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net452" />
+</packages>

+ 64 - 2
GreenTree.Nachtragsmanagement.Services/Appendix/AppendixService.cs

@@ -5,6 +5,7 @@ using System.Text;
 using System.Threading.Tasks;
 using GreenTree.Nachtragsmanagement.Core.Data;
 using GreenTree.Nachtragsmanagement.Core.Domain.Appendix;
+using GreenTree.Nachtragsmanagement.Core.Domain.Invoice;
 
 namespace GreenTree.Nachtragsmanagement.Services.Appendix
 {
@@ -14,6 +15,7 @@ namespace GreenTree.Nachtragsmanagement.Services.Appendix
 
         private readonly IRepository<Core.Domain.Appendix.Appendix> _appendixRepository;
         private readonly IRepository<Category> _categoryRepository;
+        private readonly IRepository<Invoice> _invoiceRepository;
 
         #endregion
 
@@ -24,10 +26,12 @@ namespace GreenTree.Nachtragsmanagement.Services.Appendix
         /// </summary>
         public AppendixService(
             IRepository<Core.Domain.Appendix.Appendix> appendixRepository,
-            IRepository<Category> categoryRepository)
+            IRepository<Category> categoryRepository,
+            IRepository<Invoice> invoiceRepository)
         {
             _appendixRepository = appendixRepository;
             _categoryRepository = categoryRepository;
+            _invoiceRepository = invoiceRepository;
         }
 
         #endregion
@@ -157,5 +161,63 @@ namespace GreenTree.Nachtragsmanagement.Services.Appendix
         }
 
         #endregion
+
+        #region Invoice
+
+        /// <summary>
+        /// Gets all invoices
+        /// </summary>
+        public IList<Invoice> GetAllInvoices()
+        {
+            return _invoiceRepository.Table.ToList();
+        }
+
+        /// <summary>
+        /// Gets a invoice by specified Id
+        /// </summary>
+        /// <param name="id">Invoice identifier.</param>
+        public Invoice GetInvoiceById(int id)
+        {
+            return _invoiceRepository.GetById(id);
+        }
+
+        /// <summary>
+        /// Gets all invoices to the specified ids
+        /// </summary>
+        public IList<Invoice> GetInvoicesByIds(int[] ids)
+        {
+            return _invoiceRepository.Table
+                .Where(r => ids.Contains(r.Id))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Insert a appendix
+        /// </summary>
+        /// <param name="invoice">Invoice.</param>
+        public void InsertInvoice(Invoice invoice)
+        {
+            _invoiceRepository.Insert(invoice);
+        }
+
+        /// <summary>
+        /// Update a invoice
+        /// </summary>
+        /// <param name="invoice">Invoice.</param>
+        public void UpdateInvoice(Invoice invoice)
+        {
+            _invoiceRepository.Update(invoice);
+        }
+
+        /// <summary>
+        /// Delete a invoice
+        /// </summary>
+        /// <param name="invoice">Invoice.</param>
+        public void DeleteInvoice(Invoice invoice)
+        {
+            _invoiceRepository.Delete(invoice);
+        }
+
+        #endregion
     }
-}
+}

+ 39 - 0
GreenTree.Nachtragsmanagement.Services/Appendix/IAppendixService.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using GreenTree.Nachtragsmanagement.Core.Domain.Appendix;
+using GreenTree.Nachtragsmanagement.Core.Domain.Invoice;
 
 namespace GreenTree.Nachtragsmanagement.Services.Appendix
 {
@@ -90,5 +91,43 @@ namespace GreenTree.Nachtragsmanagement.Services.Appendix
         void DeleteCategory(Category category);
 
         #endregion
+
+        #region Invoice
+
+        /// <summary>
+        /// Gets all invoices
+        /// </summary>
+        IList<Invoice> GetAllInvoices();
+
+        /// <summary>
+        /// Gets a invoice by specified Id
+        /// </summary>
+        /// <param name="id">Invoice identifier.</param>
+        Invoice GetInvoiceById(int id);
+
+        /// <summary>
+        /// Gets all invoices to the specified ids
+        /// </summary>
+        IList<Invoice> GetInvoicesByIds(int[] ids);
+
+        /// <summary>
+        /// Insert a invoice
+        /// </summary>
+        /// <param name="invoice">Invoice.</param>
+        void InsertInvoice(Invoice invoice);
+
+        /// <summary>
+        /// Update a invoice
+        /// </summary>
+        /// <param name="invoice">Invoice.</param>
+        void UpdateInvoice(Invoice invoice);
+
+        /// <summary>
+        /// Delete a invoice
+        /// </summary>
+        /// <param name="invoice">Invoice.</param>
+        void DeleteInvoice(Invoice invoice);
+
+        #endregion
     }
 }

+ 283 - 0
GreenTree.Nachtragsmanagement.Services/Deviation/DeviationService.cs

@@ -0,0 +1,283 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using GreenTree.Nachtragsmanagement.Core.Data;
+using GreenTree.Nachtragsmanagement.Core.Domain.Deviation;
+
+namespace GreenTree.Nachtragsmanagement.Services.Deviation
+{
+    public class DeviationService : IDeviationService
+    {
+        #region Fields
+
+        private readonly IRepository<Core.Domain.Deviation.Deviation> _deviationRepository;
+        private readonly IRepository<Disturbance> _disturbanceRepository;
+        private readonly IRepository<Kind> _kindRepository;
+        private readonly IRepository<Status> _statusRepository;
+
+        #endregion
+
+        #region Ctor
+
+        /// <summary>
+        /// Initializes a new instance of the DeviationService class
+        /// </summary>
+        public DeviationService(
+            IRepository<Core.Domain.Deviation.Deviation> deviationRepository,
+            IRepository<Disturbance> disturbanceRepository,
+            IRepository<Kind> kindRepository,
+            IRepository<Status> statusRepository)
+        {
+            _deviationRepository = deviationRepository;
+            _disturbanceRepository = disturbanceRepository;
+            _kindRepository = kindRepository;
+            _statusRepository = statusRepository;
+        }
+
+        #endregion
+
+        #region Deviation
+
+        /// <summary>
+        /// Gets all deviations
+        /// </summary>
+        public IList<Core.Domain.Deviation.Deviation> GetAllDeviations()
+        {
+            return _deviationRepository.Table.ToList();
+        }
+
+        /// <summary>
+        /// Gets a deviation by specified Id
+        /// </summary>
+        /// <param name="id">Deviation identifier.</param>
+        public Core.Domain.Deviation.Deviation GetDeviationById(int id)
+        {
+            return _deviationRepository.GetById(id);
+        }
+
+        /// <summary>
+        /// Gets all deviations to the specified ids
+        /// </summary>
+        public IList<Core.Domain.Deviation.Deviation> GetDeviationsByIds(int[] ids)
+        {
+            return _deviationRepository.Table
+                .Where(u => ids.Contains(u.Id))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Gets a deviation by specified custom number
+        /// </summary>
+        /// <param name="id">Customer number.</param>
+        public Core.Domain.Deviation.Deviation GetDeviationByCustomNumber(int customNumber)
+        {
+            return _deviationRepository
+                .Table.FirstOrDefault(u => u.CustomNumber == customNumber);
+        }
+
+        /// <summary>
+        /// Insert a deviation
+        /// </summary>
+        /// <param name="deviation">Deviation.</param>
+        public void InsertDeviation(Core.Domain.Deviation.Deviation deviation)
+        {
+            _deviationRepository.Insert(deviation);
+        }
+
+        /// <summary>
+        /// Update a deviation
+        /// </summary>
+        /// <param name="deviation">Deviation.</param>
+        public void UpdateDeviation(Core.Domain.Deviation.Deviation deviation)
+        {
+            _deviationRepository.Update(deviation);
+        }
+
+        /// <summary>
+        /// Delete a deviation
+        /// </summary>
+        /// <param name="deviation">Deviation.</param>
+        public void DeleteDeviation(Core.Domain.Deviation.Deviation deviation)
+        {
+            _deviationRepository.Delete(deviation);
+        }
+
+        #endregion
+
+        #region Disturbance
+
+        /// <summary>
+        /// Gets all disturbances
+        /// </summary>
+        public IList<Disturbance> GetAllDisturbances()
+        {
+            return _disturbanceRepository.Table.ToList();
+        }
+
+        /// <summary>
+        /// Gets a disturbance by specified Id
+        /// </summary>
+        /// <param name="id">Disturbance identifier.</param>
+        public Disturbance GetDisturbanceById(int id)
+        {
+            return _disturbanceRepository.GetById(id);
+        }
+
+        /// <summary>
+        /// Gets all disturbances to the specified ids
+        /// </summary>
+        public IList<Disturbance> GetDisturbancesByIds(int[] ids)
+        {
+            return _disturbanceRepository.Table
+                .Where(r => ids.Contains(r.Id))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Insert a deviation
+        /// </summary>
+        /// <param name="disturbance">Disturbance.</param>
+        public void InsertDisturbance(Disturbance disturbance)
+        {
+            _disturbanceRepository.Insert(disturbance);
+        }
+
+        /// <summary>
+        /// Update a disturbance
+        /// </summary>
+        /// <param name="disturbance">Disturbance.</param>
+        public void UpdateDisturbance(Disturbance disturbance)
+        {
+            _disturbanceRepository.Update(disturbance);
+        }
+
+        /// <summary>
+        /// Delete a disturbance
+        /// </summary>
+        /// <param name="disturbance">Disturbance.</param>
+        public void DeleteDisturbance(Disturbance disturbance)
+        {
+            _disturbanceRepository.Delete(disturbance);
+        }
+
+        #endregion
+
+        #region Kind
+
+        /// <summary>
+        /// Gets all kinds
+        /// </summary>
+        public IList<Kind> GetAllKinds()
+        {
+            return _kindRepository.Table.ToList();
+        }
+
+        /// <summary>
+        /// Gets a kind by specified Id
+        /// </summary>
+        /// <param name="id">Kind identifier.</param>
+        public Kind GetKindById(int id)
+        {
+            return _kindRepository.GetById(id);
+        }
+
+        /// <summary>
+        /// Gets all kinds to the specified ids
+        /// </summary>
+        public IList<Kind> GetKindsByIds(int[] ids)
+        {
+            return _kindRepository.Table
+                .Where(r => ids.Contains(r.Id))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Insert a kind
+        /// </summary>
+        /// <param name="kind">Kind.</param>
+        public void InsertKind(Kind kind)
+        {
+            _kindRepository.Insert(kind);
+        }
+
+        /// <summary>
+        /// Update a kind
+        /// </summary>
+        /// <param name="kind">Kind.</param>
+        public void UpdateKind(Kind kind)
+        {
+            _kindRepository.Update(kind);
+        }
+
+        /// <summary>
+        /// Delete a kind
+        /// </summary>
+        /// <param name="kind">Kind.</param>
+        public void DeleteKind(Kind kind)
+        {
+            _kindRepository.Delete(kind);
+        }
+
+        #endregion
+
+        #region Status
+
+        /// <summary>
+        /// Gets all statuses
+        /// </summary>
+        public IList<Status> GetAllStatuses()
+        {
+            return _statusRepository.Table.ToList();
+        }
+
+        /// <summary>
+        /// Gets a status by specified Id
+        /// </summary>
+        /// <param name="id">Status identifier.</param>
+        public Status GetStatusById(int id)
+        {
+            return _statusRepository.GetById(id);
+        }
+
+        /// <summary>
+        /// Gets all statuses to the specified ids
+        /// </summary>
+        public IList<Status> GetStatusesByIds(int[] ids)
+        {
+            return _statusRepository.Table
+                .Where(r => ids.Contains(r.Id))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Insert a status
+        /// </summary>
+        /// <param name="status">Status.</param>
+        public void InsertStatus(Status status)
+        {
+            _statusRepository.Insert(status);
+        }
+
+        /// <summary>
+        /// Update a status
+        /// </summary>
+        /// <param name="status">Status.</param>
+        public void UpdateStatus(Status status)
+        {
+            _statusRepository.Update(status);
+        }
+
+        /// <summary>
+        /// Delete a status
+        /// </summary>
+        /// <param name="status">Status.</param>
+        public void DeleteStatus(Status status)
+        {
+            _statusRepository.Delete(status);
+        }
+
+        #endregion
+    }
+}

+ 170 - 0
GreenTree.Nachtragsmanagement.Services/Deviation/IDeviationService.cs

@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using GreenTree.Nachtragsmanagement.Core.Domain.Deviation;
+
+namespace GreenTree.Nachtragsmanagement.Services.Deviation
+{
+    public interface IDeviationService
+    {
+        #region Deviation
+
+        /// <summary>
+        /// Gets all deviations
+        /// </summary>
+        IList<Core.Domain.Deviation.Deviation> GetAllDeviations();
+
+        /// <summary>
+        /// Gets a deviation by specified Id
+        /// </summary>
+        /// <param name="id">Deviation identifier.</param>
+        Core.Domain.Deviation.Deviation GetDeviationById(int id);
+
+        /// <summary>
+        /// Gets all deviations to the specified ids
+        /// </summary>
+        IList<Core.Domain.Deviation.Deviation> GetDeviationsByIds(int[] ids);
+
+        /// <summary>
+        /// Gets a deviation by specified customer number
+        /// </summary>
+        /// <param name="id">Customer number.</param>
+        Core.Domain.Deviation.Deviation GetDeviationByCustomNumber(int customNumber);
+
+        /// <summary>
+        /// Insert a deviation
+        /// </summary>
+        /// <param name="deviation">Deviation.</param>
+        void InsertDeviation(Core.Domain.Deviation.Deviation deviation);
+
+        /// <summary>
+        /// Update a deviation
+        /// </summary>
+        /// <param name="deviation">Deviation.</param>
+        void UpdateDeviation(Core.Domain.Deviation.Deviation deviation);
+
+        /// <summary>
+        /// Delete a deviation
+        /// </summary>
+        /// <param name="deviation">Deviation.</param>
+        void DeleteDeviation(Core.Domain.Deviation.Deviation deviation);
+
+        #endregion
+
+        #region Disturbance
+
+        /// <summary>
+        /// Gets all disturbances
+        /// </summary>
+        IList<Disturbance> GetAllDisturbances();
+
+        /// <summary>
+        /// Gets a disturbance by specified Id
+        /// </summary>
+        /// <param name="id">Disturbance identifier.</param>
+        Disturbance GetDisturbanceById(int id);
+
+        /// <summary>
+        /// Gets all disturbances to the specified ids
+        /// </summary>
+        IList<Disturbance> GetDisturbancesByIds(int[] ids);
+
+        /// <summary>
+        /// Insert a disturbance
+        /// </summary>
+        /// <param name="disturbance">Disturbance.</param>
+        void InsertDisturbance(Disturbance disturbance);
+
+        /// <summary>
+        /// Update a disturbance
+        /// </summary>
+        /// <param name="disturbance">Disturbance.</param>
+        void UpdateDisturbance(Disturbance disturbance);
+
+        /// <summary>
+        /// Delete a disturbance
+        /// </summary>
+        /// <param name="disturbance">Disturbance.</param>
+        void DeleteDisturbance(Disturbance disturbance);
+
+        #endregion
+
+        #region Kind
+
+        /// <summary>
+        /// Gets all kinds
+        /// </summary>
+        IList<Kind> GetAllKinds();
+
+        /// <summary>
+        /// Gets a kind by specified Id
+        /// </summary>
+        /// <param name="id">Kind identifier.</param>
+        Kind GetKindById(int id);
+
+        /// <summary>
+        /// Gets all kinds to the specified ids
+        /// </summary>
+        IList<Kind> GetKindsByIds(int[] ids);
+
+        /// <summary>
+        /// Insert a kind
+        /// </summary>
+        /// <param name="kind">Kind.</param>
+        void InsertKind(Kind kind);
+
+        /// <summary>
+        /// Update a kind
+        /// </summary>
+        /// <param name="kind">Kind.</param>
+        void UpdateKind(Kind kind);
+
+        /// <summary>
+        /// Delete a kind
+        /// </summary>
+        /// <param name="kind">Kind.</param>
+        void DeleteKind(Kind kind);
+
+        #endregion
+
+        #region Status
+
+        /// <summary>
+        /// Gets all statuses
+        /// </summary>
+        IList<Status> GetAllStatuses();
+
+        /// <summary>
+        /// Gets a status by specified Id
+        /// </summary>
+        /// <param name="id">Status identifier.</param>
+        Status GetStatusById(int id);
+
+        /// <summary>
+        /// Gets all statuses to the specified ids
+        /// </summary>
+        IList<Status> GetStatusesByIds(int[] ids);
+
+        /// <summary>
+        /// Insert a status
+        /// </summary>
+        /// <param name="status">Status.</param>
+        void InsertStatus(Status status);
+
+        /// <summary>
+        /// Update a status
+        /// </summary>
+        /// <param name="status">Status.</param>
+        void UpdateStatus(Status status);
+
+        /// <summary>
+        /// Delete a status
+        /// </summary>
+        /// <param name="status">Status.</param>
+        void DeleteStatus(Status status);
+
+        #endregion
+    }
+}

+ 5 - 2
GreenTree.Nachtragsmanagement.Services/GreenTree.Nachtragsmanagement.Services.csproj

@@ -55,8 +55,10 @@
     <Compile Include="Configuration\IConfigurationService.cs" />
     <Compile Include="DbContext\DbContextService.cs" />
     <Compile Include="DbContext\IDbContextService.cs" />
-    <Compile Include="Notification\INotificationLogic.cs" />
-    <Compile Include="Notification\INotificationService.cs" />
+    <Compile Include="Deviation\DeviationService.cs" />
+    <Compile Include="Deviation\IDeviationService.cs" />
+    <Compile Include="Misc\MiscService.cs" />
+    <Compile Include="Misc\IMiscService.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Site\ISiteService.cs" />
     <Compile Include="Site\SiteService.cs" />
@@ -77,6 +79,7 @@
     </ProjectReference>
   </ItemGroup>
   <ItemGroup>
+    <None Include="app.config" />
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup />

+ 126 - 0
GreenTree.Nachtragsmanagement.Services/Misc/IMiscService.cs

@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using GreenTree.Nachtragsmanagement.Core.Domain.Misc;
+
+namespace GreenTree.Nachtragsmanagement.Services.Misc
+{
+    public interface IMiscService
+    {
+        #region MailNotification
+
+        /// <summary>
+        /// Gets all mailNotifications
+        /// </summary>
+        IList<MailNotification> GetAllMailNotifications();
+
+        /// <summary>
+        /// Gets a mailNotification by specified Id
+        /// </summary>
+        /// <param name="id">MailNotification identifier.</param>
+        MailNotification GetMailNotificationById(int id);
+
+        /// <summary>
+        /// Gets all mailNotifications to the specified ids
+        /// </summary>
+        IList<MailNotification> GetMailNotificationsByIds(int[] ids);
+
+        /// <summary>
+        /// Insert a mailNotification
+        /// </summary>
+        /// <param name="mailNotification">MailNotification.</param>
+        void InsertMailNotification(MailNotification mailNotification);
+
+        /// <summary>
+        /// Update a mailNotification
+        /// </summary>
+        /// <param name="mailNotification">MailNotification.</param>
+        void UpdateMailNotification(MailNotification mailNotification);
+
+        /// <summary>
+        /// Delete a mailNotification
+        /// </summary>
+        /// <param name="mailNotification">MailNotification.</param>
+        void DeleteMailNotification(MailNotification mailNotification);
+
+        #endregion
+
+        #region NotificationEvent
+
+        /// <summary>
+        /// Gets all notificationEvents
+        /// </summary>
+        IList<NotificationEvent> GetAllNotificationEvents();
+
+        /// <summary>
+        /// Gets a notificationEvent by specified Id
+        /// </summary>
+        /// <param name="id">NotificationEvent identifier.</param>
+        NotificationEvent GetNotificationEventById(int id);
+
+        /// <summary>
+        /// Gets all notificationEvents to the specified ids
+        /// </summary>
+        IList<NotificationEvent> GetNotificationEventsByIds(int[] ids);
+
+        /// <summary>
+        /// Insert a notificationEvent
+        /// </summary>
+        /// <param name="notificationEvent">NotificationEvent.</param>
+        void InsertNotificationEvent(NotificationEvent notificationEvent);
+
+        /// <summary>
+        /// Update a notificationEvent
+        /// </summary>
+        /// <param name="notificationEvent">NotificationEvent.</param>
+        void UpdateNotificationEvent(NotificationEvent notificationEvent);
+
+        /// <summary>
+        /// Delete a notificationEvent
+        /// </summary>
+        /// <param name="notificationEvent">NotificationEvent.</param>
+        void DeleteNotificationEvent(NotificationEvent notificationEvent);
+
+        #endregion
+
+        #region NotificationEventType
+
+        /// <summary>
+        /// Gets all notificationEventTypes
+        /// </summary>
+        IList<NotificationEventType> GetAllNotificationEventTypes();
+
+        /// <summary>
+        /// Gets a notificationEventType by specified Id
+        /// </summary>
+        /// <param name="id">NotificationEventType identifier.</param>
+        NotificationEventType GetNotificationEventTypeById(int id);
+
+        /// <summary>
+        /// Gets all notificationEventTypes to the specified ids
+        /// </summary>
+        IList<NotificationEventType> GetNotificationEventTypesByIds(int[] ids);
+
+        /// <summary>
+        /// Insert a notificationEventType
+        /// </summary>
+        /// <param name="notificationEventType">NotificationEventType.</param>
+        void InsertNotificationEventType(NotificationEventType notificationEventType);
+
+        /// <summary>
+        /// Update a notificationEventType
+        /// </summary>
+        /// <param name="notificationEventType">NotificationEventType.</param>
+        void UpdateNotificationEventType(NotificationEventType notificationEventType);
+
+        /// <summary>
+        /// Delete a notificationEventType
+        /// </summary>
+        /// <param name="notificationEventType">NotificationEventType.</param>
+        void DeleteNotificationEventType(NotificationEventType notificationEventType);
+
+        #endregion
+    }
+}

+ 212 - 0
GreenTree.Nachtragsmanagement.Services/Misc/MiscService.cs

@@ -0,0 +1,212 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using GreenTree.Nachtragsmanagement.Core.Data;
+using GreenTree.Nachtragsmanagement.Core.Domain.Misc;
+
+namespace GreenTree.Nachtragsmanagement.Services.Misc
+{
+    public class MiscService : IMiscService
+    {
+        #region Fields
+
+        private readonly IRepository<MailNotification> _mailNotificationRepository;
+        private readonly IRepository<NotificationEvent> _notificationEventRepository;
+        private readonly IRepository<NotificationEventType> _notificationEventTypeRepository;
+
+        #endregion
+
+        #region Ctor
+
+        /// <summary>
+        /// Initializes a new instance of the MiscService class
+        /// </summary>
+        public MiscService(
+            IRepository<MailNotification> mailNotificationRepository,
+            IRepository<NotificationEvent> notificationEventRepository,
+            IRepository<NotificationEventType> notificationEventTypeRepository)
+        {
+            _mailNotificationRepository = mailNotificationRepository;
+            _notificationEventRepository = notificationEventRepository;
+            _notificationEventTypeRepository = notificationEventTypeRepository;
+        }
+
+        #endregion
+
+        #region MailNotification
+
+        /// <summary>
+        /// Gets all mailNotifications
+        /// </summary>
+        public IList<MailNotification> GetAllMailNotifications()
+        {
+            return _mailNotificationRepository.Table.ToList();
+        }
+
+        /// <summary>
+        /// Gets a mailNotification by specified Id
+        /// </summary>
+        /// <param name="id">MailNotification identifier.</param>
+        public MailNotification GetMailNotificationById(int id)
+        {
+            return _mailNotificationRepository.GetById(id);
+        }
+
+        /// <summary>
+        /// Gets all mailNotifications to the specified ids
+        /// </summary>
+        public IList<MailNotification> GetMailNotificationsByIds(int[] ids)
+        {
+            return _mailNotificationRepository.Table
+                .Where(u => ids.Contains(u.Id))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Insert a mailNotification
+        /// </summary>
+        /// <param name="mailNotification">MailNotification.</param>
+        public void InsertMailNotification(MailNotification mailNotification)
+        {
+            _mailNotificationRepository.Insert(mailNotification);
+        }
+
+        /// <summary>
+        /// Update a mailNotification
+        /// </summary>
+        /// <param name="mailNotification">MailNotification.</param>
+        public void UpdateMailNotification(MailNotification mailNotification)
+        {
+            _mailNotificationRepository.Update(mailNotification);
+        }
+
+        /// <summary>
+        /// Delete a mailNotification
+        /// </summary>
+        /// <param name="mailNotification">MailNotification.</param>
+        public void DeleteMailNotification(MailNotification mailNotification)
+        {
+            _mailNotificationRepository.Delete(mailNotification);
+        }
+
+        #endregion
+
+        #region NotificationEvent
+
+        /// <summary>
+        /// Gets all notificationEvents
+        /// </summary>
+        public IList<NotificationEvent> GetAllNotificationEvents()
+        {
+            return _notificationEventRepository.Table.ToList();
+        }
+
+        /// <summary>
+        /// Gets a notificationEvent by specified Id
+        /// </summary>
+        /// <param name="id">NotificationEvent identifier.</param>
+        public NotificationEvent GetNotificationEventById(int id)
+        {
+            return _notificationEventRepository.GetById(id);
+        }
+
+        /// <summary>
+        /// Gets all notificationEvents to the specified ids
+        /// </summary>
+        public IList<NotificationEvent> GetNotificationEventsByIds(int[] ids)
+        {
+            return _notificationEventRepository.Table
+                .Where(r => ids.Contains(r.Id))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Insert a mailNotification
+        /// </summary>
+        /// <param name="notificationEvent">NotificationEvent.</param>
+        public void InsertNotificationEvent(NotificationEvent notificationEvent)
+        {
+            _notificationEventRepository.Insert(notificationEvent);
+        }
+
+        /// <summary>
+        /// Update a notificationEvent
+        /// </summary>
+        /// <param name="notificationEvent">NotificationEvent.</param>
+        public void UpdateNotificationEvent(NotificationEvent notificationEvent)
+        {
+            _notificationEventRepository.Update(notificationEvent);
+        }
+
+        /// <summary>
+        /// Delete a notificationEvent
+        /// </summary>
+        /// <param name="notificationEvent">NotificationEvent.</param>
+        public void DeleteNotificationEvent(NotificationEvent notificationEvent)
+        {
+            _notificationEventRepository.Delete(notificationEvent);
+        }
+
+        #endregion
+
+        #region NotificationEventType
+
+        /// <summary>
+        /// Gets all notificationEventTypes
+        /// </summary>
+        public IList<NotificationEventType> GetAllNotificationEventTypes()
+        {
+            return _notificationEventTypeRepository.Table.ToList();
+        }
+
+        /// <summary>
+        /// Gets a notificationEventType by specified Id
+        /// </summary>
+        /// <param name="id">NotificationEventType identifier.</param>
+        public NotificationEventType GetNotificationEventTypeById(int id)
+        {
+            return _notificationEventTypeRepository.GetById(id);
+        }
+
+        /// <summary>
+        /// Gets all notificationEventTypes to the specified ids
+        /// </summary>
+        public IList<NotificationEventType> GetNotificationEventTypesByIds(int[] ids)
+        {
+            return _notificationEventTypeRepository.Table
+                .Where(r => ids.Contains(r.Id))
+                .ToList();
+        }
+
+        /// <summary>
+        /// Insert a appendix
+        /// </summary>
+        /// <param name="notificationEventType">NotificationEventType.</param>
+        public void InsertNotificationEventType(NotificationEventType notificationEventType)
+        {
+            _notificationEventTypeRepository.Insert(notificationEventType);
+        }
+
+        /// <summary>
+        /// Update a notificationEventType
+        /// </summary>
+        /// <param name="notificationEventType">NotificationEventType.</param>
+        public void UpdateNotificationEventType(NotificationEventType notificationEventType)
+        {
+            _notificationEventTypeRepository.Update(notificationEventType);
+        }
+
+        /// <summary>
+        /// Delete a notificationEventType
+        /// </summary>
+        /// <param name="notificationEventType">NotificationEventType.</param>
+        public void DeleteNotificationEventType(NotificationEventType notificationEventType)
+        {
+            _notificationEventTypeRepository.Delete(notificationEventType);
+        }
+
+        #endregion
+    }
+}

+ 0 - 21
GreenTree.Nachtragsmanagement.Services/Notification/INotificationLogic.cs

@@ -1,21 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace GreenTree.Nachtragsmanagement.Services.Notification
-{
-    public interface INotificationLogic
-    {
-        /// <summary>
-        /// Id of the notification plugin
-        /// </summary>
-        Guid Id { get; set; }
-
-        /// <summary>
-        /// Name of the notification plugin
-        /// </summary>
-        string Name { get; set; }
-    }
-}

+ 0 - 15
GreenTree.Nachtragsmanagement.Services/Notification/INotificationService.cs

@@ -1,15 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using GreenTree.Nachtragsmanagement.Core.Domain.Misc;
-
-namespace GreenTree.Nachtragsmanagement.Services.Notification
-{
-    public interface INotificationService
-    {
-
-        string GenerateMailBody(MailNotification mailNotification);
-    }
-}

+ 11 - 0
GreenTree.Nachtragsmanagement.Services/app.config

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <runtime>
+    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+      <dependentAssembly>
+        <assemblyIdentity name="Autofac" publicKeyToken="17863af14b0044da" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-4.6.1.0" newVersion="4.6.1.0" />
+      </dependentAssembly>
+    </assemblyBinding>
+  </runtime>
+</configuration>

+ 34 - 0
GreenTree.Nachtragsmanagement.Web.Framework/ApplicationContext.cs

@@ -18,6 +18,12 @@ using GreenTree.Nachtragsmanagement.Services.DbContext;
 using GreenTree.Nachtragsmanagement.Services.Site;
 using GreenTree.Nachtragsmanagement.Services.Test;
 using GreenTree.Nachtragsmanagement.Services.User;
+using GreenTree.Nachtragsmanagement.Services.Deviation;
+using GreenTree.Nachtragsmanagement.Services.Misc;
+using GreenTree.Nachtragsmanagement.Core;
+using GreenTree.Nachtragsmanagement.Core.Plugins;
+using GreenTree.Nachtragsmanagement.Web.Framework.Mvc.Routes;
+using System.Web.Routing;
 
 namespace GreenTree.Nachtragsmanagement.Web.Framework
 {
@@ -44,6 +50,10 @@ namespace GreenTree.Nachtragsmanagement.Web.Framework
 
                 return _current;
             }
+            private set
+            {
+                _current = value;
+            }
         }
 
         #endregion
@@ -64,7 +74,10 @@ namespace GreenTree.Nachtragsmanagement.Web.Framework
         /// </summary>
         public ApplicationContext()
         {
+            var appContext = Singleton<ApplicationContext>.Instance;
 
+            if (appContext == null)
+                Singleton<ApplicationContext>.Instance = this;
         }
 
         #endregion
@@ -91,6 +104,12 @@ namespace GreenTree.Nachtragsmanagement.Web.Framework
             builder.RegisterType<DbRelationService>().As<IDbRelationService>();
             builder.RegisterType<UserService>().As<IUserService>();
             builder.RegisterType<SiteService>().As<ISiteService>();
+            builder.RegisterType<DeviationService>().As<IDeviationService>();
+            builder.RegisterType<MiscService>().As<IMiscService>();
+            builder.RegisterType<PluginFinder>().As<IPluginFinder>();
+            builder.RegisterType<WebHelper>().As<IWebHelper>();
+            builder.RegisterType<WebAppTypeFinder>().As<ITypeFinder>();
+            builder.RegisterType<RoutePublisher>().As<IRoutePublisher>();
 
             // Register controllers
             builder.RegisterControllers(Assembly.GetCallingAssembly());
@@ -98,9 +117,24 @@ namespace GreenTree.Nachtragsmanagement.Web.Framework
             // Register modules
             builder.RegisterModule(new AutofacWebTypesModule());
 
+            Current = new ApplicationContext();
+
             _appContainer = builder.Build();
 
             DependencyResolver.SetResolver(new AutofacDependencyResolver(_appContainer));
+
+            Singleton<IContainer>.Instance = _appContainer;
+        }
+
+        /// <summary>
+        /// Registers all routes provided by every installed Plugin
+        /// </summary>
+        /// <param name="routes">Registered routes where plugin routes should be append to.</param>
+        public static void InitPluginRoutes(RouteCollection routes)
+        {
+            var routePublisher = _appContainer.Resolve<IRoutePublisher>();
+
+            routePublisher.RegisterRoutes(routes);
         }
 
         #endregion

+ 4 - 1
GreenTree.Nachtragsmanagement.Web.Framework/GreenTree.Nachtragsmanagement.Web.Framework.csproj

@@ -71,8 +71,11 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="ApplicationContext.cs" />
+    <Compile Include="Mvc\Routes\GuidConstraint.cs" />
+    <Compile Include="Mvc\Routes\IRouteProvider.cs" />
+    <Compile Include="Mvc\Routes\IRoutePublisher.cs" />
+    <Compile Include="Mvc\Routes\RoutePublisher.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="Singleton.cs" />
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Configuration\" />

+ 37 - 0
GreenTree.Nachtragsmanagement.Web.Framework/Mvc/Routes/GuidConstraint.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web;
+using System.Web.Routing;
+
+namespace GreenTree.Nachtragsmanagement.Web.Framework.Mvc.Routes
+{
+    public class GuidConstraint : IRouteConstraint
+    {
+        private readonly bool _allowEmpty;
+
+        public GuidConstraint(bool allowEmpty)
+        {
+            this._allowEmpty = allowEmpty;
+        }
+        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
+        {
+            if (values.ContainsKey(parameterName))
+            {
+                string stringValue = values[parameterName] != null ? values[parameterName].ToString() : null;
+
+                if (!string.IsNullOrEmpty(stringValue))
+                {
+                    Guid guidValue;
+
+                    return Guid.TryParse(stringValue, out guidValue) &&
+                        (_allowEmpty || guidValue != Guid.Empty);
+                }
+            }
+
+            return false;
+        }
+    }
+}

+ 16 - 0
GreenTree.Nachtragsmanagement.Web.Framework/Mvc/Routes/IRouteProvider.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web.Routing;
+
+namespace GreenTree.Nachtragsmanagement.Web.Framework.Mvc.Routes
+{
+    public interface IRouteProvider
+    {
+        void RegisterRoutes(RouteCollection routes);
+
+        int Priority { get; }
+    }
+}

+ 21 - 0
GreenTree.Nachtragsmanagement.Web.Framework/Mvc/Routes/IRoutePublisher.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web.Routing;
+
+namespace GreenTree.Nachtragsmanagement.Web.Framework.Mvc.Routes
+{
+    /// <summary>
+    /// Route publisher
+    /// </summary>
+    public interface IRoutePublisher
+    {
+        /// <summary>
+        /// Register routes
+        /// </summary>
+        /// <param name="routes">Routes</param>
+        void RegisterRoutes(RouteCollection routes);
+    }
+}

+ 69 - 0
GreenTree.Nachtragsmanagement.Web.Framework/Mvc/Routes/RoutePublisher.cs

@@ -0,0 +1,69 @@
+using GreenTree.Nachtragsmanagement.Core;
+using GreenTree.Nachtragsmanagement.Core.Plugins;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Web.Routing;
+
+namespace GreenTree.Nachtragsmanagement.Web.Framework.Mvc.Routes
+{
+    public class RoutePublisher : IRoutePublisher
+    {
+        protected readonly ITypeFinder typeFinder;
+
+        /// <summary>
+        /// Ctor
+        /// </summary>
+        /// <param name="typeFinder"></param>
+        public RoutePublisher(ITypeFinder typeFinder)
+        {
+            this.typeFinder = typeFinder;
+        }
+
+        /// <summary>
+        /// Find a plugin descriptor by some type which is located into its assembly
+        /// </summary>
+        /// <param name="providerType">Provider type</param>
+        /// <returns>Plugin descriptor</returns>
+        protected virtual PluginDescriptor FindPlugin(Type providerType)
+        {
+            if (providerType == null)
+                throw new ArgumentNullException("providerType");
+
+            foreach (var plugin in PluginManager.ReferencedPlugins)
+            {
+                if (plugin.ReferencedAssembly == null)
+                    continue;
+
+                if (plugin.ReferencedAssembly.FullName == providerType.Assembly.FullName)
+                    return plugin;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Register routes
+        /// </summary>
+        /// <param name="routes">Routes</param>
+        public virtual void RegisterRoutes(RouteCollection routes)
+        {
+            var routeProviderTypes = typeFinder.FindClassesOfType<IRouteProvider>();
+            var routeProviders = new List<IRouteProvider>();
+            foreach (var providerType in routeProviderTypes)
+            {
+                //Ignore not installed plugins
+                var plugin = FindPlugin(providerType);
+                if (plugin != null && !plugin.Installed)
+                    continue;
+
+                var provider = Activator.CreateInstance(providerType) as IRouteProvider;
+                routeProviders.Add(provider);
+            }
+            routeProviders = routeProviders.OrderByDescending(rp => rp.Priority).ToList();
+            routeProviders.ForEach(rp => rp.RegisterRoutes(routes));
+        }
+    }
+}

+ 0 - 0
GreenTree.Nachtragsmanagement.Web/App_Data/InstalledPlugins.txt


+ 74 - 2
GreenTree.Nachtragsmanagement.Web/Controllers/HomeController.cs

@@ -10,6 +10,8 @@ using GreenTree.Nachtragsmanagement.Web.Framework;
 using GreenTree.Nachtragsmanagement.Web.Models.Test;
 using GreenTree.Nachtragsmanagement.Web.Models.User;
 using Newtonsoft.Json;
+using GreenTree.Nachtragsmanagement.Core.Plugins;
+using GreenTree.Nachtragsmanagement.Core;
 
 namespace GreenTree.Nachtragsmanagement.Web.Controllers
 {
@@ -18,19 +20,30 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
         private readonly IDbRelationService _dbRelationService;
         private readonly IConfigurationService _configurationService;
         private readonly IUserService _userService;
+        private readonly IPluginFinder _pluginFinder;
+        private readonly IWebHelper _webHelper;
 
         public HomeController(
             IDbRelationService dbRelationService,
             IConfigurationService configurationService,
-            IUserService userService)
+            IUserService userService,
+            IPluginFinder pluginFinder,
+            IWebHelper webHelper)
         {
             _dbRelationService = dbRelationService;
             _configurationService = configurationService;
             _userService = userService;
+            _pluginFinder = pluginFinder;
+            _webHelper = webHelper;
         }
 
         // GET: Home
         public ActionResult Index()
+        {
+            return View("~/Views/Home/Index.cshtml");
+        }
+
+        public ActionResult Relations()
         {
             var users = _userService.GetAllUsers();
 
@@ -46,7 +59,66 @@ namespace GreenTree.Nachtragsmanagement.Web.Controllers
 
             var configSection = _configurationService.GetCurrentConfiguration();
 
-            return View("~/Views/Home/Index.cshtml", model);
+            return View("~/Views/Home/Relations.cshtml", model);
+        }
+
+        public ActionResult Plugins()
+        {
+            var model = new PluginModel
+            {
+                PluginNames = new List<string[]>()
+            };
+
+            var uninstalledPlugins = _pluginFinder.GetPlugins<IPlugin>(LoadPluginsMode.NotInstalledOnly);
+            var installedPlugins = _pluginFinder.GetPlugins<IPlugin>(LoadPluginsMode.InstalledOnly);
+
+            if (installedPlugins.Any())
+                model.PluginNames.AddRange(new List<string[]>()
+                {
+                    new [] { installedPlugins.First().PluginDescriptor.SystemName, "installed" }
+                });
+
+            if (uninstalledPlugins.Any())
+                model.PluginNames.AddRange(new List<string[]>()
+                {
+                    new [] { uninstalledPlugins.First().PluginDescriptor.SystemName, "uninstalled" }
+                });
+
+            return View("~/Views/Home/Plugins.cshtml", model);
+        }
+
+        [HttpPost]
+        public ActionResult InstallPlugin(string pluginName)
+        {
+            var pluginDescriptor = _pluginFinder.GetPluginDescriptorBySystemName(pluginName, LoadPluginsMode.All);
+            if (pluginDescriptor == null)
+                return RedirectToAction("Plugins");
+
+            if (pluginDescriptor.Installed)
+                return RedirectToAction("Plugins");
+
+            pluginDescriptor.Instance().Install();
+
+            _webHelper.RestartAppDomain();
+
+            return RedirectToAction("Plugins");
+        }
+
+        [HttpPost]
+        public ActionResult UninstallPlugin(string pluginName)
+        {
+            var pluginDescriptor = _pluginFinder.GetPluginDescriptorBySystemName(pluginName, LoadPluginsMode.All);
+            if (pluginDescriptor == null)
+                return RedirectToAction("Plugins");
+
+            if (!pluginDescriptor.Installed)
+                return RedirectToAction("Plugins");
+
+            pluginDescriptor.Instance().Uninstall();
+
+            _webHelper.RestartAppDomain();
+
+            return RedirectToAction("Plugins");
         }
     }
 }

+ 4 - 0
GreenTree.Nachtragsmanagement.Web/Global.asax.cs

@@ -8,6 +8,9 @@ using System.Web.Routing;
 using GreenTree.Nachtragsmanagement.Web.Framework;
 using GreenTree.Nachtragsmanagement.Core.Domain;
 using System.Reflection;
+using GreenTree.Nachtragsmanagement.Core;
+using Autofac;
+using GreenTree.Nachtragsmanagement.Web.Framework.Mvc.Routes;
 
 namespace GreenTree.Nachtragsmanagement.Web
 {
@@ -27,6 +30,7 @@ namespace GreenTree.Nachtragsmanagement.Web
             ModelBinders.Binders.DefaultBinder = new DevExpress.Web.Mvc.DevExpressEditorsBinder();
 
             ApplicationContext.InitApplication();
+            ApplicationContext.InitPluginRoutes(RouteTable.Routes);
 
             DevExpress.Web.ASPxWebControl.CallbackError += Application_Error;
         }

+ 4 - 0
GreenTree.Nachtragsmanagement.Web/GreenTree.Nachtragsmanagement.Web.csproj

@@ -178,6 +178,9 @@
     </Content>
     <Content Include="Views\Shared\_Layout.cshtml" />
     <Content Include="Views\_ViewStart.cshtml" />
+    <Content Include="Views\Home\Relations.cshtml" />
+    <Content Include="Views\Home\Plugins.cshtml" />
+    <Content Include="Views\Shared\_HeaderNavBar.cshtml" />
     <None Include="Web.Debug.config">
       <DependentUpon>Web.config</DependentUpon>
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -196,6 +199,7 @@
       <DependentUpon>Global.asax</DependentUpon>
     </Compile>
     <Compile Include="Models\Test\DbRelationModel.cs" />
+    <Compile Include="Models\Test\PluginModel.cs" />
     <Compile Include="Models\User\UserModel.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>

+ 12 - 0
GreenTree.Nachtragsmanagement.Web/Models/Test/PluginModel.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+
+namespace GreenTree.Nachtragsmanagement.Web.Models.Test
+{
+    public class PluginModel
+    {
+        public List<string[]> PluginNames { get; set; }
+    }
+}

+ 8 - 0
GreenTree.Nachtragsmanagement.Web/Plugins/Misc.Test/Description.txt

@@ -0,0 +1,8 @@
+Group: Misc
+FriendlyName: Nachtragsmanagement Test
+SystemName: GreenTree.Nachtragsmanagement.Test
+Version: 1.0
+SupportedVersions: 1.0.0
+Author: GreenTree Studios
+DisplayOrder: 1
+FileName: GreenTree.Nachtragsmanagement.Plugin.Test.dll

BIN
GreenTree.Nachtragsmanagement.Web/Plugins/Misc.Test/GreenTree.Nachtragsmanagement.Plugin.Test.dll


+ 21 - 0
GreenTree.Nachtragsmanagement.Web/Plugins/Misc.Test/GreenTree.Nachtragsmanagement.Plugin.Test.dll.config

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+  <configSections>
+    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
+    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
+  </configSections>
+  <runtime>
+    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+      <dependentAssembly>
+        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
+        <bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
+      </dependentAssembly>
+    </assemblyBinding>
+  </runtime>
+  <entityFramework>
+    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
+    <providers>
+      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
+    </providers>
+  </entityFramework>
+</configuration>

BIN
GreenTree.Nachtragsmanagement.Web/Plugins/Misc.Test/logo.jpg


+ 2 - 21
GreenTree.Nachtragsmanagement.Web/Views/Home/Index.cshtml

@@ -3,25 +3,6 @@
     Layout = "~/Views/Shared/_Layout.cshtml";
 }
 
-@model GreenTree.Nachtragsmanagement.Web.Models.Test.DbRelationModel
+<h2>Home - Funktionen</h2>
 
-<h2>Datenbank - Relationen</h2>
-
-<div style="width: auto; padding-top: 8px; border-top: 1px solid black">
-	<div style="float: left">
-		<h6>Users:</h6>
-		@Html.TextAreaFor(m => m.UserJson, new { rows = 50, cols = 60 })
-	</div>
-	<div style="float: left">
-		<h6>Deviations:</h6>
-		@Html.TextAreaFor(m => m.DeviationJson, new { rows = 50, cols = 60 })
-	</div>
-	<div style="float: left">
-		<h6>Sites:</h6>
-		@Html.TextAreaFor(m => m.SiteJson, new { rows = 50, cols = 60 })
-	</div>
-	<div style="float: left">
-		<h6>Appendices:</h6>
-		@Html.TextAreaFor(m => m.AppendixJson, new { rows = 50, cols = 60 })
-	</div>
-</div>
+@Html.Partial("~/Views/Shared/_HeaderNavBar.cshtml")

+ 55 - 0
GreenTree.Nachtragsmanagement.Web/Views/Home/Plugins.cshtml

@@ -0,0 +1,55 @@
+@{
+    ViewBag.Title = "Plugins";
+    Layout = "~/Views/Shared/_Layout.cshtml";
+}
+
+@model GreenTree.Nachtragsmanagement.Web.Models.Test.PluginModel
+
+@Html.Partial("~/Views/Shared/_HeaderNavBar.cshtml")
+
+<h2>Plugins</h2>
+
+<div style="width: auto; padding-top: 8px; border-top: 1px solid black">
+	<table>
+		<tbody>
+			@if (Model.PluginNames.Any())
+			{
+				foreach (var plugin in Model.PluginNames)
+				{
+					if (plugin[1] == "uninstalled")
+					{
+						<tr>
+							<td>@plugin[0]</td>
+							<td>
+								@using (Html.BeginForm("InstallPlugin", "Home", FormMethod.Post))
+								{
+									@Html.Hidden("pluginName", plugin[0])
+									<input type="submit" value="Installieren" />
+								}
+							</td>
+						</tr>
+					}
+					else
+					{
+						<tr>
+							<td>@plugin[0]</td>
+							<td>
+								@using (Html.BeginForm("UninstallPlugin", "Home", FormMethod.Post))
+								{
+									@Html.Hidden("pluginName", plugin[0])
+									<input type="submit" value="Deinstallieren" />
+								}
+							</td>
+						</tr>
+					}
+				}
+			}
+			else
+			{
+				<tr>
+					<td>Keine Plugins gefunden!</td>
+				</tr>
+			}
+		</tbody>
+	</table>
+</div>

+ 29 - 0
GreenTree.Nachtragsmanagement.Web/Views/Home/Relations.cshtml

@@ -0,0 +1,29 @@
+@{
+    ViewBag.Title = "Relationen";
+    Layout = "~/Views/Shared/_Layout.cshtml";
+}
+
+@model GreenTree.Nachtragsmanagement.Web.Models.Test.DbRelationModel
+
+@Html.Partial("~/Views/Shared/_HeaderNavBar.cshtml")
+
+<h2>Datenbank - Relationen</h2>
+
+<div style="width: auto; padding-top: 8px; border-top: 1px solid black">
+	<div style="float: left">
+		<h6>Users:</h6>
+		@Html.TextAreaFor(m => m.UserJson, new { rows = 50, cols = 60 })
+	</div>
+	<div style="float: left">
+		<h6>Deviations:</h6>
+		@Html.TextAreaFor(m => m.DeviationJson, new { rows = 50, cols = 60 })
+	</div>
+	<div style="float: left">
+		<h6>Sites:</h6>
+		@Html.TextAreaFor(m => m.SiteJson, new { rows = 50, cols = 60 })
+	</div>
+	<div style="float: left">
+		<h6>Appendices:</h6>
+		@Html.TextAreaFor(m => m.AppendixJson, new { rows = 50, cols = 60 })
+	</div>
+</div>

+ 6 - 0
GreenTree.Nachtragsmanagement.Web/Views/Shared/_HeaderNavBar.cshtml

@@ -0,0 +1,6 @@
+<div style="width: auto; border-bottom: 1px solid black; padding: 0; overflow: auto">
+	<div style="padding: 4px 6px; float: left; overflow: auto">
+		<a href="~/home/relations">Relationen</a>
+		<a href="~/home/plugins">Plugins</a>
+	</div>
+</div>

+ 3 - 0
GreenTree.Nachtragsmanagement.Web/packages.config

@@ -3,12 +3,15 @@
   <package id="Autofac" version="4.0.1" targetFramework="net452" />
   <package id="EntityFramework" version="6.1.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.Mvc" version="5.2.3" targetFramework="net452" />
+  <package id="Microsoft.AspNet.Mvc.de" version="5.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.Razor" version="3.2.3" targetFramework="net452" />
+  <package id="Microsoft.AspNet.Razor.de" version="3.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.WebApi" version="5.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net452" />
   <package id="Microsoft.AspNet.WebPages.Data" version="3.2.3" targetFramework="net452" />
+  <package id="Microsoft.AspNet.WebPages.de" version="3.2.3" targetFramework="net452" />
   <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net452" />
 </packages>

+ 7 - 0
GreenTree.Nachtragsmanagement.sln

@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenTree.Nachtragsmanageme
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenTree.Nachtragsmanagement.Web", "GreenTree.Nachtragsmanagement.Web\GreenTree.Nachtragsmanagement.Web.csproj", "{1C75D2AF-A273-4C14-BB2A-6F4659E57816}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenTree.Nachtragsmanagement.Plugin.Test", "GreenTree.Nachtragsmanagement.Plugin.Test\GreenTree.Nachtragsmanagement.Plugin.Test.csproj", "{702A7197-9C1F-4D47-8EF0-DB27E97CD66F}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -45,6 +47,10 @@ Global
 		{1C75D2AF-A273-4C14-BB2A-6F4659E57816}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{1C75D2AF-A273-4C14-BB2A-6F4659E57816}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{1C75D2AF-A273-4C14-BB2A-6F4659E57816}.Release|Any CPU.Build.0 = Release|Any CPU
+		{702A7197-9C1F-4D47-8EF0-DB27E97CD66F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{702A7197-9C1F-4D47-8EF0-DB27E97CD66F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{702A7197-9C1F-4D47-8EF0-DB27E97CD66F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{702A7197-9C1F-4D47-8EF0-DB27E97CD66F}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -55,5 +61,6 @@ Global
 		{0C45ECBC-6AD6-4EB1-89BB-F05A3F0FDA13} = {FE55B9AA-2B14-4D78-880E-19BB7AB5394E}
 		{FAFF64EA-DE01-40BF-B805-DE48D4CBEF1C} = {1C471DB6-6627-4D80-A291-9B757CEE2B06}
 		{1C75D2AF-A273-4C14-BB2A-6F4659E57816} = {1C471DB6-6627-4D80-A291-9B757CEE2B06}
+		{702A7197-9C1F-4D47-8EF0-DB27E97CD66F} = {3474D228-B960-467A-8088-082732C575A3}
 	EndGlobalSection
 EndGlobal