diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj
index 9baec673..4d6ad43b 100644
--- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj
+++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj
@@ -9,6 +9,7 @@
false
false
true
+ true
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor
index 2b32fcb4..af8a4839 100644
--- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor
+++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor
@@ -42,8 +42,8 @@ else
public override List Resources => new List()
{
- new Stylesheet(ModulePath() + "[Owner].Module.[Module]/Module.css"),
- new Script(ModulePath() + "[Owner].Module.[Module]/Module.js")
+ new Stylesheet(ModulePath() + "Module.css"),
+ new Script(ModulePath() + "Module.js")
};
List<[Module]> _[Module]s;
diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/template.json b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/internal.module.template.json
similarity index 100%
rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/template.json
rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/internal.module.template.json
diff --git a/Oqtane.Application/Server/wwwroot/Themes/Templates/Internal/template.json b/Oqtane.Application/Server/wwwroot/Themes/Templates/Internal/internal.theme.template.json
similarity index 100%
rename from Oqtane.Application/Server/wwwroot/Themes/Templates/Internal/template.json
rename to Oqtane.Application/Server/wwwroot/Themes/Templates/Internal/internal.theme.template.json
diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor
index 13e89847..4eebbbcb 100644
--- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor
+++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor
@@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase
@using System.Text.RegularExpressions
+@using System.Reflection
@inject NavigationManager NavigationManager
@inject IModuleDefinitionService ModuleDefinitionService
@inject IModuleService ModuleService
@@ -84,7 +85,7 @@
private List _templates;
private string _template = "-";
private string _minversion = "2.0.0";
- private string _type = "External";
+ private string _type = "";
private string[] _versions;
private string _reference = "local";
private string _location = string.Empty;
@@ -97,6 +98,16 @@
{
AddModuleMessage(Localizer["Info.Module.Development"], MessageType.Info);
}
+ else
+ {
+ var entryAssemblyName = Assembly.GetEntryAssembly().GetName().Name;
+ if (entryAssemblyName.EndsWith(".Oqtane"))
+ {
+ // Oqtane Application assemblies end with .Server.Oqtane or .Client.Oqtane
+ string[] segments = entryAssemblyName.Split('.');
+ _owner = string.Join(".", segments, 0, segments.Length - 2);
+ }
+ }
}
protected override async Task OnParametersSetAsync()
@@ -127,11 +138,18 @@
if (string.IsNullOrEmpty(_description)) _description = _module;
if (IsValidXML(_description))
{
+ if (_type == "Internal")
+ {
+ AddModuleMessage(Localizer["Success.Module.Create.Internal"], MessageType.Success);
+ }
var template = _templates.FirstOrDefault(item => item.Name == _template);
var moduleDefinition = new ModuleDefinition { Owner = _owner, Name = _module, Description = _description, Template = _template, Version = _reference, ModuleDefinitionName = template.Namespace };
moduleDefinition = await ModuleDefinitionService.CreateModuleDefinitionAsync(moduleDefinition);
- GetLocation();
- AddModuleMessage(string.Format(Localizer["Success.Module.Create"], NavigateUrl("admin/system")), MessageType.Success);
+ if (_type == "External")
+ {
+ GetLocation();
+ AddModuleMessage(string.Format(Localizer["Success.Module.Create.External"], NavigateUrl("admin/system")), MessageType.Success);
+ }
}
else
{
@@ -178,7 +196,7 @@
else
{
_minversion = "2.0.0";
- _type = "External";
+ _type = "";
}
GetLocation();
}
diff --git a/Oqtane.Client/Modules/Admin/Themes/Create.razor b/Oqtane.Client/Modules/Admin/Themes/Create.razor
index 7c1025c7..9006dfd0 100644
--- a/Oqtane.Client/Modules/Admin/Themes/Create.razor
+++ b/Oqtane.Client/Modules/Admin/Themes/Create.razor
@@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.Themes
@inherits ModuleBase
@using System.Text.RegularExpressions
+@using System.Reflection
@inject NavigationManager NavigationManager
@inject IThemeService ThemeService
@inject IModuleService ModuleService
@@ -75,7 +76,7 @@
private List _templates;
private string _template = "-";
private string _minversion = "2.0.0";
- private string _type = "External";
+ private string _type = "";
private string[] _versions;
private string _reference = "local";
private string _location = string.Empty;
@@ -88,9 +89,19 @@
{
AddModuleMessage(Localizer["Info.Theme.CreatorIntent"], MessageType.Info);
}
+ else
+ {
+ var entryAssemblyName = Assembly.GetEntryAssembly().GetName().Name;
+ if (entryAssemblyName.EndsWith(".Oqtane"))
+ {
+ // Oqtane Application assemblies end with .Server.Oqtane or .Client.Oqtane
+ string[] segments = entryAssemblyName.Split('.');
+ _owner = string.Join(".", segments, 0, segments.Length - 2);
+ }
+ }
}
-
- protected override async Task OnParametersSetAsync()
+
+ protected override async Task OnParametersSetAsync()
{
try
{
@@ -109,11 +120,18 @@
{
if (IsValid(_owner) && IsValid(_theme) && _owner != _theme && _template != "-")
{
+ if (_type == "Internal")
+ {
+ AddModuleMessage(Localizer["Success.Theme.Create.Internal"], MessageType.Success);
+ }
var template = _templates.FirstOrDefault(item => item.Name == _template);
var theme = new Theme { Owner = _owner, Name = _theme, Template = _template, Version = _reference, ThemeName = template.Namespace };
theme = await ThemeService.CreateThemeAsync(theme);
- GetLocation();
- AddModuleMessage(string.Format(Localizer["Success.Theme.Create"], NavigateUrl("admin/system")), MessageType.Success);
+ if (_type == "External")
+ {
+ GetLocation();
+ AddModuleMessage(string.Format(Localizer["Success.Theme.Create.External"], NavigateUrl("admin/system")), MessageType.Success);
+ }
}
else
{
@@ -144,7 +162,7 @@
else
{
_minversion = "2.0.0";
- _type = "External";
+ _type = "";
}
GetLocation();
}
diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx
index 94942a50..2f7ed37d 100644
--- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Create.resx
@@ -136,7 +136,7 @@
You Must Provide A Valid Description (ie. No Punctuation)
- Enter the name of the organization who is developing this module. It should not contain spaces or punctuation or contain the word "oqtane".
+ Enter the name of the organization who is developing this module. It should not contain spaces or punctuation or contain the word "oqtane". If you are using an Internal template then make sure the owner matches the name of the project.
Enter a name for this module. It should not contain spaces or punctuation or contain the word "oqtane".
@@ -168,7 +168,10 @@
Location:
-
+
+ The Source Code For Your Module Has Been Created In Your Solution And Must Be Compiled In Order To Make It Functional
+
+
The Source Code For Your Module Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href={0}>Restart</a> Your Application To Activate The Module.
diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Create.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Create.resx
index 96bfd1c4..fe956c14 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Themes/Create.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Create.resx
@@ -141,14 +141,17 @@
Please Note That The Theme Creator Is Only Intended To Be Used In A Development Environment
-
- The Source Code For Your Theme Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href={0}>Restart</a> Your Application To Activate The Module.
+
+ The Source Code For Your Theme Has Been Created In Your Solution And Must Be Compiled In Order To Make It Functional
+
+
+ The Source Code For Your Theme Has Been Created At The Location Specified Below And Must Be Compiled In Order To Make It Functional. Once It Has Been Compiled You Must <a href={0}>Restart</a> Your Application To Activate The Theme.
You Must Provide A Valid Owner Name And Theme Name ( ie. No Punctuation Or Spaces And The Values Cannot Be The Same ) And Choose A Template
- Enter the name of the organization who is developing this theme. It should not contain spaces or punctuation.
+ Enter the name of the organization who is developing this theme. It should not contain spaces or punctuation or contain the word "oqtane". If you are using an Internal template then make sure the owner matches the name of the project.
Enter a name for this theme. It should not contain spaces or punctuation.
diff --git a/Oqtane.Client/UI/PageState.cs b/Oqtane.Client/UI/PageState.cs
index 91cf158c..8aac9627 100644
--- a/Oqtane.Client/UI/PageState.cs
+++ b/Oqtane.Client/UI/PageState.cs
@@ -29,6 +29,8 @@ namespace Oqtane.UI
public bool Refresh { get; set; }
public bool AllowCookies { get; set; }
+ public int? StatusCode { get; set; }
+
public List Pages
{
get { return Site?.Pages; }
@@ -63,7 +65,8 @@ namespace Oqtane.UI
IsInternalNavigation = IsInternalNavigation,
RenderId = RenderId,
Refresh = Refresh,
- AllowCookies = AllowCookies
+ AllowCookies = AllowCookies,
+ StatusCode = StatusCode
};
}
}
diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor
index a345a7f0..e7b11256 100644
--- a/Oqtane.Client/UI/SiteRouter.razor
+++ b/Oqtane.Client/UI/SiteRouter.razor
@@ -158,7 +158,9 @@
// verify user is authenticated for current site
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
- if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey))
+ if (authState.User.Identity.IsAuthenticated
+ && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey)
+ && PageState.StatusCode != (int)HttpStatusCode.NotFound)
{
// get user
var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
@@ -337,6 +339,7 @@
IsInternalNavigation = _isInternalNavigation,
RenderId = renderid,
Refresh = false,
+ StatusCode = PageState?.StatusCode,
AllowCookies = _allowCookies.GetValueOrDefault(true)
};
OnStateChange?.Invoke(_pagestate);
diff --git a/Oqtane.Package/FixProps.exe b/Oqtane.Package/FixProps.exe
index e4a9ef00..e2c449ec 100644
Binary files a/Oqtane.Package/FixProps.exe and b/Oqtane.Package/FixProps.exe differ
diff --git a/Oqtane.Package/release.cmd b/Oqtane.Package/release.cmd
index f957ed98..cc12b123 100644
--- a/Oqtane.Package/release.cmd
+++ b/Oqtane.Package/release.cmd
@@ -1,4 +1,5 @@
dotnet build -c Release ..\Oqtane.slnx
+FixProps.exe
nuget.exe pack Oqtane.Client.nuspec
nuget.exe pack Oqtane.Server.nuspec
nuget.exe pack Oqtane.Shared.nuspec
diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor
index 7382976b..333ed42f 100644
--- a/Oqtane.Server/Components/App.razor
+++ b/Oqtane.Server/Components/App.razor
@@ -170,6 +170,7 @@
if (page == null || page.IsDeleted)
{
HandlePageNotFound(site, page, route);
+ return;
}
else
{
@@ -248,6 +249,7 @@
IsInternalNavigation = false,
RenderId = Guid.NewGuid(),
Refresh = true,
+ StatusCode = Context.Response.StatusCode,
AllowCookies = _allowCookies
};
}
@@ -300,8 +302,16 @@
{
if (route.PagePath != "404")
{
- // redirect to 404 page
- NavigationManager.NavigateTo(route.SiteUrl + "/404", true);
+ // handle not found request in static mode
+ if(_renderMode == RenderModes.Static)
+ {
+ NavigationManager.NotFound();
+ }
+ else
+ {
+ // redirect to 404 page
+ NavigationManager.NavigateTo(route.SiteUrl + "/404", true);
+ }
}
}
}
diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs
index df876829..139da8e2 100644
--- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs
+++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs
@@ -1,20 +1,20 @@
+using System;
using System.Collections.Generic;
-using Microsoft.AspNetCore.Mvc;
-using Oqtane.Models;
-using Oqtane.Shared;
-using Microsoft.AspNetCore.Authorization;
using System.IO;
-using System.Reflection;
using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Text.Json;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
using Oqtane.Enums;
using Oqtane.Infrastructure;
+using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Security;
-using System;
-using Microsoft.Extensions.DependencyInjection;
-using System.Text.Json;
-using System.Net;
+using Oqtane.Shared;
namespace Oqtane.Controllers
{
@@ -132,9 +132,8 @@ namespace Oqtane.Controllers
if (moduleDefinition.Template.ToLower().Contains("internal"))
{
rootPath = Utilities.PathCombine(rootFolder.FullName, Path.DirectorySeparatorChar.ToString());
- var assemblyName = Assembly.GetExecutingAssembly().GetName().Name;
- moduleDefinition.ServerManagerType = moduleDefinition.ModuleDefinitionName + ".Manager." + moduleDefinition.Name + "Manager, " + assemblyName;
- moduleDefinition.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName + ", " + assemblyName.Replace(".Server", ".Client");
+ moduleDefinition.ServerManagerType = moduleDefinition.ModuleDefinitionName + ".Manager." + moduleDefinition.Name + "Manager, " + moduleDefinition.Owner + ".Server.Oqtane";
+ moduleDefinition.ModuleDefinitionName = moduleDefinition.ModuleDefinitionName + ", " + moduleDefinition.Owner + ".Client.Oqtane";
}
else
{
@@ -271,9 +270,10 @@ namespace Oqtane.Controllers
foreach (string directory in Directory.GetDirectories(templatePath))
{
string name = directory.Replace(templatePath, "");
- if (System.IO.File.Exists(Path.Combine(directory, "template.json")))
+ var manifest = Directory.GetFiles(directory, "*.json");
+ if (manifest.Any())
{
- var template = JsonSerializer.Deserialize(System.IO.File.ReadAllText(Path.Combine(directory, "template.json")));
+ var template = JsonSerializer.Deserialize(System.IO.File.ReadAllText(manifest[0]));
template.Name = name;
template.Location = "";
if (template.Type.ToLower() != "internal")
diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs
index 0463ae34..3f025b40 100644
--- a/Oqtane.Server/Controllers/ThemeController.cs
+++ b/Oqtane.Server/Controllers/ThemeController.cs
@@ -183,9 +183,10 @@ namespace Oqtane.Controllers
foreach (string directory in Directory.GetDirectories(templatePath))
{
string name = directory.Replace(templatePath, "");
- if (System.IO.File.Exists(Path.Combine(directory, "template.json")))
+ var manifest = Directory.GetFiles(directory, "*.json");
+ if (manifest.Any())
{
- var template = JsonSerializer.Deserialize(System.IO.File.ReadAllText(Path.Combine(directory, "template.json")));
+ var template = JsonSerializer.Deserialize(System.IO.File.ReadAllText(manifest[0]));
template.Name = name;
template.Location = "";
if (template.Type.ToLower() != "internal")
@@ -226,8 +227,7 @@ namespace Oqtane.Controllers
if (theme.Template.ToLower().Contains("internal"))
{
rootPath = Utilities.PathCombine(rootFolder.FullName, Path.DirectorySeparatorChar.ToString());
- var assemblyName = Assembly.GetExecutingAssembly().GetName().Name;
- theme.ThemeName = theme.ThemeName + ", " + assemblyName.Replace(".Server", ".Client");
+ theme.ThemeName = theme.ThemeName + ", " + theme.Owner + ".Client.Oqtane";
}
else
{
diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs
index fa3835ea..a18f4d02 100644
--- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs
+++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs
@@ -1,8 +1,11 @@
using System;
+using System.IO;
using System.Linq;
+using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors.Infrastructure;
+using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
@@ -65,6 +68,7 @@ namespace Oqtane.Extensions
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
+ app.UseNotFoundResponse();
// execute any IServerStartup logic
app.ConfigureOqtaneAssemblies(environment);
@@ -146,5 +150,66 @@ namespace Oqtane.Extensions
public static IApplicationBuilder UseExceptionMiddleWare(this IApplicationBuilder builder)
=> builder.UseMiddleware();
+
+ public static IApplicationBuilder UseNotFoundResponse(this IApplicationBuilder app)
+ {
+ const string notFoundRoute = "/404";
+ app.UseStatusCodePagesWithReExecute(notFoundRoute, createScopeForStatusCodePages: true);
+
+ app.Use(async (context, next) =>
+ {
+ var path = context.Request.Path.Value ?? string.Empty;
+ if (string.IsNullOrEmpty(path) || ShouldSkipStatusCodeReExecution(path))
+ {
+ var feature = context.Features.Get();
+ feature?.Enabled = false;
+ }
+
+ await next();
+ });
+
+ app.Use(async (context, next) =>
+ {
+ var feature = context.Features.Get();
+ var handled = false;
+ if (feature != null
+ && context.Response.StatusCode == (int)HttpStatusCode.NotFound
+ && notFoundRoute.Equals(context.Request.Path.Value, StringComparison.OrdinalIgnoreCase))
+ {
+ var alias = context.GetAlias();
+ if (!string.IsNullOrEmpty(alias?.Path))
+ {
+ var originalPath = context.Request.Path;
+ context.Request.Path = new PathString($"/{alias.Path}{notFoundRoute}");
+ try
+ {
+ handled = true;
+ await next();
+ }
+ finally
+ {
+ context.Request.Path = originalPath;
+ }
+ }
+ }
+
+ if (!handled)
+ {
+ await next();
+ }
+ });
+
+ return app;
+ }
+
+ static bool ShouldSkipStatusCodeReExecution(string path)
+ {
+ return Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/")) || HasStaticFileExtension(path);
+ }
+
+ static bool HasStaticFileExtension(string path)
+ {
+ return !string.IsNullOrEmpty(Path.GetExtension(path));
+ }
}
}
diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs
index f77edd50..d57e2eb7 100644
--- a/Oqtane.Server/Infrastructure/UpgradeManager.cs
+++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs
@@ -99,24 +99,6 @@ namespace Oqtane.Infrastructure
private void Upgrade_2_0_2(Tenant tenant, IServiceScope scope)
{
- if (tenant.Name == TenantNames.Master)
- {
- // remove Internal module template files as they are no longer supported
- var internalTemplatePath = Utilities.PathCombine(_environment.WebRootPath, "Modules", "Templates", "Internal", Path.DirectorySeparatorChar.ToString());
- if (Directory.Exists(internalTemplatePath))
- {
- try
- {
- Directory.Delete(internalTemplatePath, true);
- }
- catch (Exception ex)
- {
- // error deleting directory
- _filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: Error In 2.0.2 Upgrade Logic - {ex}"));
- }
- }
- }
-
// initialize SiteGuid
try
{
diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/template.json b/Oqtane.Server/wwwroot/Modules/Templates/External/exteneral.module.template.json
similarity index 100%
rename from Oqtane.Server/wwwroot/Modules/Templates/External/template.json
rename to Oqtane.Server/wwwroot/Modules/Templates/External/exteneral.module.template.json
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/template.json b/Oqtane.Server/wwwroot/Themes/Templates/External/exteneral.theme.template.json
similarity index 100%
rename from Oqtane.Server/wwwroot/Themes/Templates/External/template.json
rename to Oqtane.Server/wwwroot/Themes/Templates/External/exteneral.theme.template.json
diff --git a/README.md b/README.md
index 30e0422f..f15b4995 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline
# Latest Release
-[6.2.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1) was released on September 29, 2025 and is a maintenance release including 65 pull requests by 6 different contributors, pushing the total number of project commits all-time over 7100. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
+[10.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0) was released on November 14, 2025 and is a major release including 77 pull requests by 6 different contributors, pushing the total number of project commits all-time over 7300. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
# Try It Now!
@@ -22,11 +22,15 @@ Microsoft's Public Cloud (requires an Azure account)
A free ASP.NET hosting account. No hidden fees. No credit card required.
[](https://www.monsterasp.net/)
-# Getting Started (Version 6.2+)
+# Getting Started (Version 10.0.0+)
**Installing using the Oqtane Application Template:**
-(Note that "MyCompany.MyProject" can be replaced with your own unique company and project name)
+If you have an older version of the Oqtane Application Template installed and want to use the latest, use the following .NET CLI command to uninstall the old version:
+```
+dotnet new uninstall Oqtane.Application.Template
+```
+To install the Oqtane Application Template and create a new project, use the following .NET CLI commands (note that "MyCompany.MyProject" can be replaced with your own unique company and project name):
```
dotnet new install Oqtane.Application.Template
@@ -38,19 +42,20 @@ dotnet run
```
- Browse to the Url specified to run the application (an Installation Wizard screen will be displayed the first time you run the application)
-- To develop/debug the application in an IDE, open the *.sln file in the root folder and hit F5
+- To develop/debug the application in an IDE, open the *.slnx file in the root folder and hit F5
**Installing using source code from the Dev/Master branch:**
-- Install Latest **[.NET 9.0 SDK](https://dotnet.microsoft.com/en-us/download)**.
+- Install Latest **[.NET 10.0 SDK](https://dotnet.microsoft.com/en-us/download)**.
-- Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
+- Install the latest edition of [Visual Studio 2026](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
-- Clone (or download) the Oqtane Master or Dev branch source code to your local system.
+- Clone (or download) the Oqtane source code to your local system:
-- Open the **Oqtane.sln** solution file.
-
-- Make sure you specify Oqtane.Server as the Startup Project.
+ - Dev Branch: git clone https://github.com/oqtane/oqtane.framework
+ - Master Branch: git clone --single-branch --branch master https://github.com/oqtane/oqtane.framework
+
+- Open the **Oqtane.slnx** solution file (make sure you specify Oqtane.Server as the Startup Project)
- Run the application... an Installation Wizard screen will be displayed which will allow you to configure your preferred database and create a host user account.
@@ -83,7 +88,7 @@ dotnet run
- If you have already installed a previous version of Oqtane and you wish to do a clean database install, simply reset the DefaultConnection value in the Oqtane.Server\appsettings.json file to "". This will trigger a re-install when you run the application which will execute the database installation.
-- If you want to submit pull requests make sure you install the [Github Extension For Visual Studio](https://visualstudio.github.com/). It is recommended you ignore any local changes you have made to the appsettings.json file before you submit a pull request. To automate this activity, open a command prompt and navigate to the /Oqtane.Server/ folder and enter the command "git update-index --skip-worktree appsettings.json"
+- If you want to submit pull requests it is recommended you ignore any local changes you have made to the appsettings.json file before you submit a pull request. To automate this activity, open a command prompt and navigate to the /Oqtane.Server/ folder and enter the command "git update-index --skip-worktree appsettings.json"
**Video Series**
@@ -106,6 +111,10 @@ Connect with other developers, get support, and share ideas by joining the Oqtan
# Roadmap
This project is open source, and therefore is a work in progress...
+[10.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.0) (Nov 14, 2025)
+- [x] Migration to .NET 10
+- [x] Passkey Authentication
+
[6.2.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.2.1) (Sep 29, 2025)
- [x] Stabilization improvements
@@ -188,7 +197,7 @@ This project is open source, and therefore is a work in progress...
➡️ Full list and older versions can be found in the [docs roadmap](https://docs.oqtane.org/guides/roadmap/index.html)
# Background
-Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
+Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and was inspired by his earlier efforts creating the DotNetNuke web application framework for the .NET Framework. Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
# Reference Implementations
diff --git a/azuredeploy.json b/azuredeploy.json
index 49a1e1f2..4b3df91b 100644
--- a/azuredeploy.json
+++ b/azuredeploy.json
@@ -205,7 +205,7 @@
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
"siteConfig": {
"webSocketsEnabled": true,
- "netFrameworkVersion": "v9.0"
+ "netFrameworkVersion": "v10.0"
}
},
"dependsOn": [
@@ -220,7 +220,7 @@
"apiVersion": "2024-04-01",
"name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]",
"properties": {
- "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.2.1/Oqtane.Framework.6.2.1.Install.zip"
+ "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.0/Oqtane.Framework.10.0.0.Install.zip"
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]"