Localizer
+
+@if (_[Module]s == null)
+{
+ Loading...
+}
+else
+{
+
+
+
+ @if (@_[Module]s.Count != 0)
+ {
+
+
+ |
+ |
+ @Localizer["Name"] |
+
+
+ |
+ |
+ @context.Name |
+
+
+ }
+ else
+ {
+ @Localizer["Message.DisplayNone"]
+ }
+}
+
+@code {
+ public override string RenderMode => RenderModes.Static;
+
+ public override List Resources => new List()
+ {
+ new Stylesheet(ModulePath() + "[Owner].Module.[Module]/Module.css"),
+ new Script(ModulePath() + "[Owner].Module.[Module]/Module.js")
+ };
+
+ List<[Module]> _[Module]s;
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
+ }
+ catch (Exception ex)
+ {
+ await logger.LogError(ex, "Error Loading [Module] {Error}", ex.Message);
+ AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error);
+ }
+ }
+
+ private async Task Delete([Module] [Module])
+ {
+ try
+ {
+ await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId);
+ await logger.LogInformation("[Module] Deleted {[Module]}", [Module]);
+ _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId);
+ StateHasChanged();
+ }
+ catch (Exception ex)
+ {
+ await logger.LogError(ex, "Error Deleting [Module] {[Module]} {Error}", [Module], ex.Message);
+ AddModuleMessage(Localizer["Message.DeleteError"], MessageType.Error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Interop.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Interop.cs
new file mode 100644
index 00000000..bcb7d729
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Interop.cs
@@ -0,0 +1,15 @@
+using Microsoft.JSInterop;
+using System.Threading.Tasks;
+
+namespace [Owner].Module.[Module]
+{
+ public class Interop
+ {
+ private readonly IJSRuntime _jsRuntime;
+
+ public Interop(IJSRuntime jsRuntime)
+ {
+ _jsRuntime = jsRuntime;
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/ModuleInfo.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/ModuleInfo.cs
new file mode 100644
index 00000000..15c96b86
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/ModuleInfo.cs
@@ -0,0 +1,16 @@
+using Oqtane.Models;
+using Oqtane.Modules;
+
+namespace [Owner].Module.[Module]
+{
+ public class ModuleInfo : IModule
+ {
+ public ModuleDefinition ModuleDefinition => new ModuleDefinition
+ {
+ Name = "[Module]",
+ Description = "[Description]",
+ Version = "1.0.0",
+ ServerManagerType = "[ServerManagerType]"
+ };
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Settings.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Settings.razor
new file mode 100644
index 00000000..624dee2a
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Settings.razor
@@ -0,0 +1,47 @@
+@namespace [Owner].Module.[Module]
+@inherits ModuleBase
+@inject ISettingService SettingService
+@inject IStringLocalizer Localizer
+
+
+
+@code {
+ private string resourceType = "[Owner].Module.[Module].Settings, [Owner].Module.[Module].Client.Oqtane"; // for localization
+ public override string Title => "[Module] Settings";
+
+ string _value;
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ Dictionary settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
+ _value = SettingService.GetSetting(settings, "SettingName", "");
+ }
+ catch (Exception ex)
+ {
+ AddModuleMessage(ex.Message, MessageType.Error);
+ }
+ }
+
+ public async Task UpdateSettings()
+ {
+ try
+ {
+ var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
+ settings = SettingService.SetSetting(settings, "SettingName", _value);
+ await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
+ }
+ catch (Exception ex)
+ {
+ AddModuleMessage(ex.Message, MessageType.Error);
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Edit.resx b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Edit.resx
new file mode 100644
index 00000000..eebd66cd
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Edit.resx
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Name:
+
+
+ Enter the name
+
+
+ Save
+
+
+ Cancel
+
+
+ Error Loading [Module]
+
+
+ Please Provide All Required Information
+
+
+ Error Saving [Module]
+
+
\ No newline at end of file
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Index.resx b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Index.resx
new file mode 100644
index 00000000..721a853a
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Index.resx
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Name
+
+
+ Add [Module]
+
+
+ Edit
+
+
+ Delete
+
+
+ Delete [Module]
+
+
+ Are You Sure You Wish To Delete This [Module]?
+
+
+ No [Module]s To Display
+
+
+ Error Loading [Module]
+
+
+ Error Deleting [Module]
+
+
\ No newline at end of file
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Settings.resx b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Settings.resx
new file mode 100644
index 00000000..ba0390d8
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Resources/[Owner].Module.[Module]/Settings.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Name:
+
+
+ Enter a value
+
+
\ No newline at end of file
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs
new file mode 100644
index 00000000..d433d698
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Oqtane.Services;
+using Oqtane.Shared;
+
+namespace [Owner].Module.[Module].Services
+{
+ public interface I[Module]Service
+ {
+ Task> Get[Module]sAsync(int ModuleId);
+
+ Task Get[Module]Async(int [Module]Id, int ModuleId);
+
+ Task Add[Module]Async(Models.[Module] [Module]);
+
+ Task Update[Module]Async(Models.[Module] [Module]);
+
+ Task Delete[Module]Async(int [Module]Id, int ModuleId);
+ }
+
+ public class [Module]Service : ServiceBase, I[Module]Service
+ {
+ public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { }
+
+ private string Apiurl => CreateApiUrl("[Module]");
+
+ public async Task> Get[Module]sAsync(int ModuleId)
+ {
+ List [Module]s = await GetJsonAsync>(CreateAuthorizationPolicyUrl($"{Apiurl}?moduleid={ModuleId}", EntityNames.Module, ModuleId), Enumerable.Empty().ToList());
+ return [Module]s.OrderBy(item => item.Name).ToList();
+ }
+
+ public async Task Get[Module]Async(int [Module]Id, int ModuleId)
+ {
+ return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}/{ModuleId}", EntityNames.Module, ModuleId));
+ }
+
+ public async Task Add[Module]Async(Models.[Module] [Module])
+ {
+ return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}", EntityNames.Module, [Module].ModuleId), [Module]);
+ }
+
+ public async Task Update[Module]Async(Models.[Module] [Module])
+ {
+ return await PutJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module].[Module]Id}", EntityNames.Module, [Module].ModuleId), [Module]);
+ }
+
+ public async Task Delete[Module]Async(int [Module]Id, int ModuleId)
+ {
+ await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}/{ModuleId}", EntityNames.Module, ModuleId));
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs
new file mode 100644
index 00000000..95ed096c
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using System.Linq;
+using Oqtane.Services;
+using [Owner].Module.[Module].Services;
+
+namespace [Owner].Module.[Module].Startup
+{
+ public class ClientStartup : IClientStartup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ if (!services.Any(s => s.ServiceType == typeof(I[Module]Service)))
+ {
+ services.AddScoped();
+ }
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Controllers/[Module]Controller.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Controllers/[Module]Controller.cs
new file mode 100644
index 00000000..5dd25b2b
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Controllers/[Module]Controller.cs
@@ -0,0 +1,114 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+using Oqtane.Shared;
+using Oqtane.Enums;
+using Oqtane.Infrastructure;
+using [Owner].Module.[Module].Services;
+using Oqtane.Controllers;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace [Owner].Module.[Module].Controllers
+{
+ [Route(ControllerRoutes.ApiRoute)]
+ public class [Module]Controller : ModuleControllerBase
+ {
+ private readonly I[Module]Service _[Module]Service;
+
+ public [Module]Controller(I[Module]Service [Module]Service, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor)
+ {
+ _[Module]Service = [Module]Service;
+ }
+
+ // GET: api/?moduleid=x
+ [HttpGet]
+ [Authorize(Policy = PolicyNames.ViewModule)]
+ public async Task> Get(string moduleid)
+ {
+ int ModuleId;
+ if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
+ {
+ return await _[Module]Service.Get[Module]sAsync(ModuleId);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {ModuleId}", moduleid);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ return null;
+ }
+ }
+
+ // GET api//5
+ [HttpGet("{id}/{moduleid}")]
+ [Authorize(Policy = PolicyNames.ViewModule)]
+ public async Task Get(int id, int moduleid)
+ {
+ Models.[Module] [Module] = await _[Module]Service.Get[Module]Async(id, moduleid);
+ if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
+ {
+ return [Module];
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {[Module]Id} {ModuleId}", id, moduleid);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ return null;
+ }
+ }
+
+ // POST api/
+ [HttpPost]
+ [Authorize(Policy = PolicyNames.EditModule)]
+ public async Task Post([FromBody] Models.[Module] [Module])
+ {
+ if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
+ {
+ [Module] = await _[Module]Service.Add[Module]Async([Module]);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Post Attempt {[Module]}", [Module]);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ [Module] = null;
+ }
+ return [Module];
+ }
+
+ // PUT api//5
+ [HttpPut("{id}")]
+ [Authorize(Policy = PolicyNames.EditModule)]
+ public async Task Put(int id, [FromBody] Models.[Module] [Module])
+ {
+ if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
+ {
+ [Module] = await _[Module]Service.Update[Module]Async([Module]);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Put Attempt {[Module]}", [Module]);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ [Module] = null;
+ }
+ return [Module];
+ }
+
+ // DELETE api//5
+ [HttpDelete("{id}/{moduleid}")]
+ [Authorize(Policy = PolicyNames.EditModule)]
+ public async Task Delete(int id, int moduleid)
+ {
+ Models.[Module] [Module] = await _[Module]Service.Get[Module]Async(id, moduleid);
+ if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
+ {
+ await _[Module]Service.Delete[Module]Async(id, [Module].ModuleId);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id} {ModuleId}", id, moduleid);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Manager/[Module]Manager.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Manager/[Module]Manager.cs
new file mode 100644
index 00000000..0779a608
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Manager/[Module]Manager.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Oqtane.Modules;
+using Oqtane.Models;
+using Oqtane.Infrastructure;
+using Oqtane.Interfaces;
+using Oqtane.Enums;
+using Oqtane.Repository;
+using [Owner].Module.[Module].Repository;
+using System.Threading.Tasks;
+
+namespace [Owner].Module.[Module].Manager
+{
+ public class [Module]Manager : MigratableModuleBase, IInstallable, IPortable, ISearchable
+ {
+ private readonly I[Module]Repository _[Module]Repository;
+ private readonly IDBContextDependencies _DBContextDependencies;
+
+ public [Module]Manager(I[Module]Repository [Module]Repository, IDBContextDependencies DBContextDependencies)
+ {
+ _[Module]Repository = [Module]Repository;
+ _DBContextDependencies = DBContextDependencies;
+ }
+
+ public bool Install(Tenant tenant, string version)
+ {
+ return Migrate(new [Module]Context(_DBContextDependencies), tenant, MigrationType.Up);
+ }
+
+ public bool Uninstall(Tenant tenant)
+ {
+ return Migrate(new [Module]Context(_DBContextDependencies), tenant, MigrationType.Down);
+ }
+
+ public string ExportModule(Oqtane.Models.Module module)
+ {
+ string content = "";
+ List [Module]s = _[Module]Repository.Get[Module]s(module.ModuleId).ToList();
+ if ([Module]s != null)
+ {
+ content = JsonSerializer.Serialize([Module]s);
+ }
+ return content;
+ }
+
+ public void ImportModule(Oqtane.Models.Module module, string content, string version)
+ {
+ List [Module]s = null;
+ if (!string.IsNullOrEmpty(content))
+ {
+ [Module]s = JsonSerializer.Deserialize>(content);
+ }
+ if ([Module]s != null)
+ {
+ foreach(var [Module] in [Module]s)
+ {
+ _[Module]Repository.Add[Module](new Models.[Module] { ModuleId = module.ModuleId, Name = [Module].Name });
+ }
+ }
+ }
+
+ public Task> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn)
+ {
+ var searchContentList = new List();
+
+ foreach (var [Module] in _[Module]Repository.Get[Module]s(pageModule.ModuleId))
+ {
+ if ([Module].ModifiedOn >= lastIndexedOn)
+ {
+ searchContentList.Add(new SearchContent
+ {
+ EntityName = "[Owner][Module]",
+ EntityId = [Module].[Module]Id.ToString(),
+ Title = [Module].Name,
+ Body = [Module].Name,
+ ContentModifiedBy = [Module].ModifiedBy,
+ ContentModifiedOn = [Module].ModifiedOn
+ });
+ }
+ }
+
+ return Task.FromResult(searchContentList);
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Migrations/01000000_[Module]Initialize.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Migrations/01000000_[Module]Initialize.cs
new file mode 100644
index 00000000..be35bee0
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Migrations/01000000_[Module]Initialize.cs
@@ -0,0 +1,30 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Oqtane.Databases.Interfaces;
+using Oqtane.Migrations;
+using [Owner].Module.[Module].Migrations.EntityBuilders;
+using [Owner].Module.[Module].Repository;
+
+namespace [Owner].Module.[Module].Migrations
+{
+ [DbContext(typeof([Module]Context))]
+ [Migration("[Owner].Module.[Module].01.00.00.00")]
+ public class [Module]Initialize : MultiDatabaseMigration
+ {
+ public [Module]Initialize(IDatabase database) : base(database)
+ {
+ }
+
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ var entityBuilder = new [Module]EntityBuilder(migrationBuilder, ActiveDatabase);
+ entityBuilder.Create();
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ var entityBuilder = new [Module]EntityBuilder(migrationBuilder, ActiveDatabase);
+ entityBuilder.Drop();
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Migrations/EntityBuilders/[Module]EntityBuilder.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Migrations/EntityBuilders/[Module]EntityBuilder.cs
new file mode 100644
index 00000000..32ec9adb
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Migrations/EntityBuilders/[Module]EntityBuilder.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations.Operations;
+using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
+using Oqtane.Databases.Interfaces;
+using Oqtane.Migrations;
+using Oqtane.Migrations.EntityBuilders;
+
+namespace [Owner].Module.[Module].Migrations.EntityBuilders
+{
+ public class [Module]EntityBuilder : AuditableBaseEntityBuilder<[Module]EntityBuilder>
+ {
+ private const string _entityTableName = "[Owner][Module]";
+ private readonly PrimaryKey<[Module]EntityBuilder> _primaryKey = new("PK_[Owner][Module]", x => x.[Module]Id);
+ private readonly ForeignKey<[Module]EntityBuilder> _moduleForeignKey = new("FK_[Owner][Module]_Module", x => x.ModuleId, "Module", "ModuleId", ReferentialAction.Cascade);
+
+ public [Module]EntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
+ {
+ EntityTableName = _entityTableName;
+ PrimaryKey = _primaryKey;
+ ForeignKeys.Add(_moduleForeignKey);
+ }
+
+ protected override [Module]EntityBuilder BuildTable(ColumnsBuilder table)
+ {
+ [Module]Id = AddAutoIncrementColumn(table,"[Module]Id");
+ ModuleId = AddIntegerColumn(table,"ModuleId");
+ Name = AddMaxStringColumn(table,"Name");
+ AddAuditableColumns(table);
+ return this;
+ }
+
+ public OperationBuilder [Module]Id { get; set; }
+ public OperationBuilder ModuleId { get; set; }
+ public OperationBuilder Name { get; set; }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Repository/[Module]Context.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Repository/[Module]Context.cs
new file mode 100644
index 00000000..2cce3945
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Repository/[Module]Context.cs
@@ -0,0 +1,26 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.AspNetCore.Http;
+using Oqtane.Modules;
+using Oqtane.Repository;
+using Oqtane.Infrastructure;
+using Oqtane.Repository.Databases.Interfaces;
+
+namespace [Owner].Module.[Module].Repository
+{
+ public class [Module]Context : DBContextBase, ITransientService, IMultiDatabase
+ {
+ public virtual DbSet [Module] { get; set; }
+
+ public [Module]Context(IDBContextDependencies DBContextDependencies) : base(DBContextDependencies)
+ {
+ // ContextBase handles multi-tenant database connections
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ builder.Entity().ToTable(ActiveDatabase.RewriteName("[Owner][Module]"));
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Repository/[Module]Repository.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Repository/[Module]Repository.cs
new file mode 100644
index 00000000..75e26527
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Repository/[Module]Repository.cs
@@ -0,0 +1,75 @@
+using Microsoft.EntityFrameworkCore;
+using System.Linq;
+using System.Collections.Generic;
+using Oqtane.Modules;
+
+namespace [Owner].Module.[Module].Repository
+{
+ public interface I[Module]Repository
+ {
+ IEnumerable Get[Module]s(int ModuleId);
+ Models.[Module] Get[Module](int [Module]Id);
+ Models.[Module] Get[Module](int [Module]Id, bool tracking);
+ Models.[Module] Add[Module](Models.[Module] [Module]);
+ Models.[Module] Update[Module](Models.[Module] [Module]);
+ void Delete[Module](int [Module]Id);
+ }
+
+ public class [Module]Repository : I[Module]Repository, ITransientService
+ {
+ private readonly IDbContextFactory<[Module]Context> _factory;
+
+ public [Module]Repository(IDbContextFactory<[Module]Context> factory)
+ {
+ _factory = factory;
+ }
+
+ public IEnumerable Get[Module]s(int ModuleId)
+ {
+ using var db = _factory.CreateDbContext();
+ return db.[Module].Where(item => item.ModuleId == ModuleId).ToList();
+ }
+
+ public Models.[Module] Get[Module](int [Module]Id)
+ {
+ return Get[Module]([Module]Id, true);
+ }
+
+ public Models.[Module] Get[Module](int [Module]Id, bool tracking)
+ {
+ using var db = _factory.CreateDbContext();
+ if (tracking)
+ {
+ return db.[Module].Find([Module]Id);
+ }
+ else
+ {
+ return db.[Module].AsNoTracking().FirstOrDefault(item => item.[Module]Id == [Module]Id);
+ }
+ }
+
+ public Models.[Module] Add[Module](Models.[Module] [Module])
+ {
+ using var db = _factory.CreateDbContext();
+ db.[Module].Add([Module]);
+ db.SaveChanges();
+ return [Module];
+ }
+
+ public Models.[Module] Update[Module](Models.[Module] [Module])
+ {
+ using var db = _factory.CreateDbContext();
+ db.Entry([Module]).State = EntityState.Modified;
+ db.SaveChanges();
+ return [Module];
+ }
+
+ public void Delete[Module](int [Module]Id)
+ {
+ using var db = _factory.CreateDbContext();
+ Models.[Module] [Module] = db.[Module].Find([Module]Id);
+ db.[Module].Remove([Module]);
+ db.SaveChanges();
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs
new file mode 100644
index 00000000..63d11ccc
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs
@@ -0,0 +1,101 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Oqtane.Enums;
+using Oqtane.Infrastructure;
+using Oqtane.Models;
+using Oqtane.Security;
+using Oqtane.Shared;
+using [Owner].Module.[Module].Repository;
+
+namespace [Owner].Module.[Module].Services
+{
+ public class Server[Module]Service : I[Module]Service
+ {
+ private readonly I[Module]Repository _[Module]Repository;
+ private readonly IUserPermissions _userPermissions;
+ private readonly ILogManager _logger;
+ private readonly IHttpContextAccessor _accessor;
+ private readonly Alias _alias;
+
+ public Server[Module]Service(I[Module]Repository [Module]Repository, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
+ {
+ _[Module]Repository = [Module]Repository;
+ _userPermissions = userPermissions;
+ _logger = logger;
+ _accessor = accessor;
+ _alias = tenantManager.GetAlias();
+ }
+
+ public Task> Get[Module]sAsync(int ModuleId)
+ {
+ if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
+ {
+ return Task.FromResult(_[Module]Repository.Get[Module]s(ModuleId).ToList());
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {ModuleId}", ModuleId);
+ return null;
+ }
+ }
+
+ public Task Get[Module]Async(int [Module]Id, int ModuleId)
+ {
+ if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View))
+ {
+ return Task.FromResult(_[Module]Repository.Get[Module]([Module]Id));
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {[Module]Id} {ModuleId}", [Module]Id, ModuleId);
+ return null;
+ }
+ }
+
+ public Task Add[Module]Async(Models.[Module] [Module])
+ {
+ if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, [Module].ModuleId, PermissionNames.Edit))
+ {
+ [Module] = _[Module]Repository.Add[Module]([Module]);
+ _logger.Log(LogLevel.Information, this, LogFunction.Create, "[Module] Added {[Module]}", [Module]);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Add Attempt {[Module]}", [Module]);
+ [Module] = null;
+ }
+ return Task.FromResult([Module]);
+ }
+
+ public Task Update[Module]Async(Models.[Module] [Module])
+ {
+ if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, [Module].ModuleId, PermissionNames.Edit))
+ {
+ [Module] = _[Module]Repository.Update[Module]([Module]);
+ _logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Update Attempt {[Module]}", [Module]);
+ [Module] = null;
+ }
+ return Task.FromResult([Module]);
+ }
+
+ public Task Delete[Module]Async(int [Module]Id, int ModuleId)
+ {
+ if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
+ {
+ _[Module]Repository.Delete[Module]([Module]Id);
+ _logger.Log(LogLevel.Information, this, LogFunction.Delete, "[Module] Deleted {[Module]Id}", [Module]Id);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id} {ModuleId}", [Module]Id, ModuleId);
+ }
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Startup/[Module]ServerStartup.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Startup/[Module]ServerStartup.cs
new file mode 100644
index 00000000..c4aaf22b
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Startup/[Module]ServerStartup.cs
@@ -0,0 +1,28 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Oqtane.Infrastructure;
+using [Owner].Module.[Module].Repository;
+using [Owner].Module.[Module].Services;
+
+namespace [Owner].Module.[Module].Startup
+{
+ public class [Module]ServerStartup : IServerStartup
+ {
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ // not implemented
+ }
+
+ public void ConfigureMvc(IMvcBuilder mvcBuilder)
+ {
+ // not implemented
+ }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddTransient();
+ services.AddDbContextFactory<[Module]Context>(opt => { }, ServiceLifetime.Transient);
+ }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/wwwroot/Modules/MyCompany.MyProject.[Module]/Module.css b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/wwwroot/Modules/MyCompany.MyProject.[Module]/Module.css
new file mode 100644
index 00000000..0856a263
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/wwwroot/Modules/MyCompany.MyProject.[Module]/Module.css
@@ -0,0 +1 @@
+/* Module Custom Styles */
\ No newline at end of file
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/wwwroot/Modules/MyCompany.MyProject.[Module]/Module.js b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/wwwroot/Modules/MyCompany.MyProject.[Module]/Module.js
new file mode 100644
index 00000000..8f072470
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/wwwroot/Modules/MyCompany.MyProject.[Module]/Module.js
@@ -0,0 +1,5 @@
+/* Module Script */
+var [Owner] = [Owner] || {};
+
+[Owner].[Module] = {
+};
\ No newline at end of file
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Models/[Module].cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Models/[Module].cs
new file mode 100644
index 00000000..7236fb25
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Models/[Module].cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Oqtane.Models;
+
+namespace [Owner].Module.[Module].Models
+{
+ [Table("[Owner][Module]")]
+ public class [Module] : ModelBase
+ {
+ [Key]
+ public int [Module]Id { get; set; }
+ public int ModuleId { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/template.json b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/template.json
new file mode 100644
index 00000000..a2710cdc
--- /dev/null
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/template.json
@@ -0,0 +1,6 @@
+{
+ "Title": "Default Module Template",
+ "Type": "Internal",
+ "Version": "10.0.0",
+ "Namespace": "[Owner].Module.[Module]"
+}