Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #3068 - support microsites in .NET MAUI #3086

Merged
merged 1 commit into from
Aug 2, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Fix #3068 - support microsites in .NET MAUI
  • Loading branch information
sbwalker committed Aug 2, 2023
commit 7a42646bed2abc865379fea854e3d4be59302826
2 changes: 1 addition & 1 deletion Oqtane.Client/Modules/Admin/Sites/Index.razor
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@

@if (_sites == null)
{
<p><em>Loading...</em></p>
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
15 changes: 13 additions & 2 deletions Oqtane.Client/Program.cs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
using System.Runtime.Loader;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;
@@ -65,6 +66,9 @@ public static async Task Main(string[] args)

private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider serviceProvider)
{
var navigationManager = serviceProvider.GetRequiredService<NavigationManager>();
var urlpath = GetUrlPath(navigationManager.Uri);

var dlls = new Dictionary<string, byte[]>();
var pdbs = new Dictionary<string, byte[]>();
var list = new List<string>();
@@ -76,7 +80,7 @@ private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider
if (files.Count() != 0)
{
// get list of assemblies from server
var json = await http.GetStringAsync("/api/Installation/list");
var json = await http.GetStringAsync($"{urlpath}api/Installation/list");
var assemblies = JsonSerializer.Deserialize<List<string>>(json);

// determine which assemblies need to be downloaded
@@ -138,7 +142,7 @@ private static async Task LoadClientAssemblies(HttpClient http, IServiceProvider
if (list.Count != 0)
{
// get assemblies from server and load into client app domain
var zip = await http.GetByteArrayAsync($"/api/Installation/load?list=" + string.Join(",", list));
var zip = await http.GetByteArrayAsync($"{urlpath}api/Installation/load?list=" + string.Join(",", list));

// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
@@ -254,5 +258,12 @@ private static void SetCulture(string culture)
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
}

private static string GetUrlPath(string url)
{
var path = new Uri(url).AbsolutePath.Substring(1);
path = (!string.IsNullOrEmpty(path) && !path.EndsWith("/")) ? path + "/" : path;
return path;
}
}
}
13 changes: 12 additions & 1 deletion Oqtane.Client/Services/InstallationService.cs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Components;
using System;
using System.Net;
using System.Linq;

namespace Oqtane.Services
{
@@ -14,11 +15,13 @@ public class InstallationService : ServiceBase, IInstallationService
{
private readonly NavigationManager _navigationManager;
private readonly SiteState _siteState;
private readonly HttpClient _http;

public InstallationService(HttpClient http, SiteState siteState, NavigationManager navigationManager) : base(http, siteState)
{
_navigationManager = navigationManager;
_siteState = siteState;
_http = http;
}

private string ApiUrl => (_siteState.Alias == null)
@@ -27,7 +30,15 @@ public InstallationService(HttpClient http, SiteState siteState, NavigationManag

public async Task<Installation> IsInstalled()
{
var path = new Uri(_navigationManager.Uri).LocalPath.Substring(1);
var path = "";
if (_http.DefaultRequestHeaders.UserAgent.ToString().Contains(Constants.MauiUserAgent))
{
path = _http.DefaultRequestHeaders.GetValues(Constants.MauiAliasPath).First();
}
else
{
path = new Uri(_navigationManager.Uri).LocalPath.Substring(1);
}
return await GetJsonAsync<Installation>($"{ApiUrl}/installed/?path={WebUtility.UrlEncode(path)}");
}

28 changes: 19 additions & 9 deletions Oqtane.Client/Services/ServiceBase.cs
Original file line number Diff line number Diff line change
@@ -202,17 +202,27 @@ protected async Task DeleteAsync(string uri)

private async Task<bool> CheckResponse(HttpResponseMessage response, string uri)
{
//if (response.IsSuccessStatusCode && uri.Contains("/api/") && !response.RequestMessage.RequestUri.AbsolutePath.Contains("/api/"))
//{
// await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Not Mapped To An API Controller Method", uri);
// return false;
//}
if (response.IsSuccessStatusCode) return true;
if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.NotFound)
if (response.IsSuccessStatusCode)
{
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Failed With Status {StatusCode} - {ReasonPhrase}", uri, response.StatusCode, response.ReasonPhrase);
// if response from api call is not from an api url then the route was not mapped correctly
if (uri.Contains("/api/") && !response.RequestMessage.RequestUri.AbsolutePath.Contains("/api/"))
{
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Not Mapped To An API Controller Method", uri);
return false;
}
else
{
return true;
}
}
else
{
if (response.StatusCode != HttpStatusCode.NoContent && response.StatusCode != HttpStatusCode.NotFound)
{
await Log(uri, response.RequestMessage.Method.ToString(), response.StatusCode.ToString(), "Request {Uri} Failed With Status {StatusCode} - {ReasonPhrase}", uri, response.StatusCode, response.ReasonPhrase);
}
return false;
}
return false;
}

private static bool ValidateJsonContent(HttpContent content)
4 changes: 2 additions & 2 deletions Oqtane.Client/UI/SiteRouter.razor
Original file line number Diff line number Diff line change
@@ -112,8 +112,8 @@
returnurl = WebUtility.UrlDecode(querystring["returnurl"]);
}

// reload the client application from the server if there is a forced reload or the user navigated to a site with a different alias
if (querystring.ContainsKey("reload") || (!NavigationManager.ToBaseRelativePath(_absoluteUri).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path)))
// reload the client application from the server if there is a forced reload
if (querystring.ContainsKey("reload"))
{
if (querystring.ContainsKey("reload") && querystring["reload"] == "post")
{
30 changes: 22 additions & 8 deletions Oqtane.Maui/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -6,21 +6,21 @@
using Oqtane.Services;
using System.Globalization;
using System.Text.Json;
using Windows.Storage.Provider;

namespace Oqtane.Maui;

public static class MauiProgram
{
// the API service url
//static string apiurl = "https://www.dnfprojects.com"; // for testing
static string apiurl = "http://localhost:44357"; // for local development (Oqtane.Server must be already running for MAUI client to connect)
//static string apiurl = "http://localhost:44357/test/"; // for local development (Oqtane.Server must be already running for MAUI client to connect)
//static string apiurl = "https://www.dnfprojects.com/"; // for testing remote site
static string apiurl = "http://localhost:44357/"; // for local development (Oqtane.Server must be already running for MAUI client to connect)
//static string apiurl = "http://localhost:44357/sitename/"; // local microsite example

public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
builder.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
@@ -31,8 +31,9 @@ public static MauiApp CreateMauiApp()
builder.Services.AddBlazorWebViewDeveloperTools();
#endif

var httpClient = new HttpClient { BaseAddress = new Uri(apiurl) };
var httpClient = new HttpClient { BaseAddress = new Uri(GetBaseUrl(apiurl)) };
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Shared.Constants.MauiUserAgent);
httpClient.DefaultRequestHeaders.Add(Shared.Constants.MauiAliasPath, GetUrlPath(apiurl).Replace("/", ""));
builder.Services.AddSingleton(httpClient);
builder.Services.AddHttpClient(); // IHttpClientFactory for calling remote services via RemoteServiceBase

@@ -85,7 +86,7 @@ private static void LoadClientAssemblies(HttpClient http)
if (files.Count() != 0)
{
// get list of assemblies from server
var json = Task.Run(() => http.GetStringAsync("/api/Installation/list")).GetAwaiter().GetResult();
var json = Task.Run(() => http.GetStringAsync($"{GetUrlPath(apiurl)}api/Installation/list")).GetAwaiter().GetResult();
var assemblies = JsonSerializer.Deserialize<List<string>>(json);

// determine which assemblies need to be downloaded
@@ -149,7 +150,7 @@ private static void LoadClientAssemblies(HttpClient http)
if (list.Count != 0)
{
// get assemblies from server
var zip = Task.Run(() => http.GetByteArrayAsync("/api/Installation/load?list=" + string.Join(",", list))).GetAwaiter().GetResult();
var zip = Task.Run(() => http.GetByteArrayAsync($"{GetUrlPath(apiurl)}api/Installation/load?list=" + string.Join(",", list))).GetAwaiter().GetResult();

// asemblies and debug symbols are packaged in a zip file
using (ZipArchive archive = new ZipArchive(new MemoryStream(zip)))
@@ -246,4 +247,17 @@ private static void RegisterClientStartups(Assembly assembly, IServiceCollection
// could not interrogate assembly - likely missing dependencies
}
}

private static string GetBaseUrl(string url)
{
var uri = new Uri(url);
return uri.Scheme + "://"+ uri.Authority + "/";
}

private static string GetUrlPath(string url)
{
var path = new Uri(url).AbsolutePath.Substring(1);
path = (!string.IsNullOrEmpty(path) && !path.EndsWith("/")) ? path + "/" : path;
return path;
}
}
14 changes: 8 additions & 6 deletions Oqtane.Server/Controllers/InstallationController.cs
Original file line number Diff line number Diff line change
@@ -34,9 +34,9 @@ public class InstallationController : Controller
private readonly IAliasRepository _aliases;
private readonly ILogger<InstallationController> _filelogger;
private readonly ITenantManager _tenantManager;
private readonly ServerStateManager _serverState;
private readonly IServerStateManager _serverState;

public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger, ITenantManager tenantManager, ServerStateManager serverState)
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger, ITenantManager tenantManager, IServerStateManager serverState)
{
_configManager = configManager;
_installationManager = installationManager;
@@ -119,9 +119,9 @@ public IActionResult Load(string list = "*")

private List<ClientAssembly> GetAssemblyList()
{
int siteId = _tenantManager.GetAlias().SiteId;
var siteKey = _tenantManager.GetAlias().SiteKey;

return _cache.GetOrCreate($"assemblieslist:{siteId}", entry =>
return _cache.GetOrCreate($"assemblieslist:{siteKey}", entry =>
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblyList = new List<ClientAssembly>();
@@ -134,7 +134,7 @@ private List<ClientAssembly> GetAssemblyList()
}

// get site assemblies which should be downloaded to client
var assemblies = _serverState.GetServerState(siteId).Assemblies;
var assemblies = _serverState.GetServerState(siteKey).Assemblies;

// populate assembly list
foreach (var assembly in assemblies)
@@ -179,9 +179,11 @@ private List<ClientAssembly> GetAssemblyList()

private byte[] GetAssemblies(string list)
{
var siteKey = _tenantManager.GetAlias().SiteKey;

if (list == "*")
{
return _cache.GetOrCreate("assemblies", entry =>
return _cache.GetOrCreate($"assemblies:{siteKey}", entry =>
{
return GetZIP(list);
});
Original file line number Diff line number Diff line change
@@ -62,7 +62,7 @@ internal static IServiceCollection AddOqtaneSingletonServices(this IServiceColle
services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
services.AddSingleton<AutoValidateAntiforgeryTokenFilter>();
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
services.AddSingleton<ServerStateManager>();
services.AddSingleton<IServerStateManager, ServerStateManager>();
return services;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Oqtane.Infrastructure
{
public interface IServerStateManager
{
ServerState GetServerState(string siteKey);
}
}
9 changes: 9 additions & 0 deletions Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ public async Task Invoke(HttpContext context)
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
string path = context.Request.Path.ToString();


if (config.IsInstalled() && !path.StartsWith("/_blazor"))
{
// get alias (note that this also sets SiteState.Alias)
@@ -43,6 +44,14 @@ public async Task Invoke(HttpContext context)
});
context.Items.Add(Constants.HttpContextSiteSettingsKey, sitesettings);

// handle first request to site
var serverState = context.RequestServices.GetService(typeof(IServerStateManager)) as IServerStateManager;
if (!serverState.GetServerState(alias.SiteKey).IsInitialized)
{
var sites = context.RequestServices.GetService(typeof(ISiteRepository)) as ISiteRepository;
sites.InitializeSite(alias);
}

// rewrite path by removing alias path prefix from reserved route (api,pages,files) requests for consistent routes
if (!string.IsNullOrEmpty(alias.Path))
{
4 changes: 2 additions & 2 deletions Oqtane.Server/Infrastructure/ServerState.cs
Original file line number Diff line number Diff line change
@@ -5,9 +5,9 @@ namespace Oqtane.Infrastructure
{
public class ServerState
{
public int SiteId { get; set; }
public string SiteKey { get; set; }
public List<string> Assemblies { get; set; } = new List<string>();
public List<Resource>Scripts { get; set; } = new List<Resource>();
public bool IsMigrated { get; set; } = false;
public bool IsInitialized { get; set; } = false;
}
}
29 changes: 6 additions & 23 deletions Oqtane.Server/Infrastructure/ServerStateManager.cs
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
namespace Oqtane.Infrastructure
{
// singleton
public class ServerStateManager
public class ServerStateManager : IServerStateManager
{
private List<ServerState> _serverStates { get; set; }

@@ -14,36 +14,19 @@ public ServerStateManager()
_serverStates = new List<ServerState>();
}

public ServerState GetServerState(int siteId)
public ServerState GetServerState(string siteKey)
{
var serverState = _serverStates.FirstOrDefault(item => item.SiteId == siteId);
var serverState = _serverStates.FirstOrDefault(item => item.SiteKey == siteKey);
if (serverState == null)
{
serverState = new ServerState();
serverState.SiteId = siteId;
serverState.SiteKey = siteKey;
serverState.Assemblies = new List<string>();
serverState.Scripts = new List<Resource>();
return serverState;
}
else
{
return serverState;
}
}

public void SetServerState(int siteId, ServerState serverState)
{
var serverstate = _serverStates.FirstOrDefault(item => item.SiteId == siteId);
if (serverstate == null)
{
serverState.SiteId = siteId;
serverState.IsInitialized = false;
_serverStates.Add(serverState);
}
else
{
serverstate.Assemblies = serverState.Assemblies;
serverstate.Scripts = serverState.Scripts;
}
return serverState;
}
}
}
3 changes: 2 additions & 1 deletion Oqtane.Server/Infrastructure/TenantManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Oqtane.Models;
@@ -57,7 +58,7 @@ public Alias GetAlias()
alias.BaseUrl = "";
if (httpcontext.Request.Headers.ContainsKey("User-Agent") && httpcontext.Request.Headers["User-Agent"] == Shared.Constants.MauiUserAgent)
{
alias.BaseUrl = alias.Protocol + alias.Name;
alias.BaseUrl = alias.Protocol + alias.Name.Replace("/" + alias.Path, "");
}
_siteState.Alias = alias;
}
Loading