diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 000000000..8e3aae52b
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "meshweaver.azure.publish": {
+ "version": "1.0.0",
+ "commands": [
+ "MeshWeaver.Azure.Publish"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f4423de37..6a70d02f2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,6 +16,8 @@
+
+
@@ -32,11 +34,16 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+
+
+
+
diff --git a/MeshWeaver.sln b/MeshWeaver.sln
index 43cbe1b2e..d2738355d 100644
--- a/MeshWeaver.sln
+++ b/MeshWeaver.sln
@@ -207,8 +207,8 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "monolith", "monolith", "{4E1A12BB-5348-4DA4-B05F-F831DC16F7DE}"
ProjectSection(SolutionItems) = preProject
monolith\Directory.Build.props = monolith\Directory.Build.props
- Readme.md = Readme.md
MIT License.md = MIT License.md
+ Readme.md = Readme.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshWeaver.Portal", "monolith\MeshWeaver.Portal\MeshWeaver.Portal.csproj", "{1471FB82-912E-411A-9141-096E0AECAAAE}"
@@ -221,6 +221,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshWeaver.Search", "src\Me
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshWeaver.Search.Test", "test\MeshWeaver.Search.Test\MeshWeaver.Search.Test.csproj", "{BDEC6D93-F212-456F-BEEE-DE2BADAE2A56}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeshWeaver.Azure.Publish", "src\MeshWeaver.Azure.Publish\MeshWeaver.Azure.Publish.csproj", "{844B4566-81A4-4CB2-9761-9B174FB7BCCB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -587,6 +589,10 @@ Global
{BDEC6D93-F212-456F-BEEE-DE2BADAE2A56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDEC6D93-F212-456F-BEEE-DE2BADAE2A56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDEC6D93-F212-456F-BEEE-DE2BADAE2A56}.Release|Any CPU.Build.0 = Release|Any CPU
+ {844B4566-81A4-4CB2-9761-9B174FB7BCCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {844B4566-81A4-4CB2-9761-9B174FB7BCCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {844B4566-81A4-4CB2-9761-9B174FB7BCCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {844B4566-81A4-4CB2-9761-9B174FB7BCCB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/modules/Northwind/MeshWeaver.Northwind.ViewModel/AnnualReportExtensions.cs b/modules/Northwind/MeshWeaver.Northwind.ViewModel/AnnualReportExtensions.cs
index ae649137d..4e420c5a1 100644
--- a/modules/Northwind/MeshWeaver.Northwind.ViewModel/AnnualReportExtensions.cs
+++ b/modules/Northwind/MeshWeaver.Northwind.ViewModel/AnnualReportExtensions.cs
@@ -1,5 +1,6 @@
using MeshWeaver.Layout;
using MeshWeaver.Layout.Composition;
+using System;
namespace MeshWeaver.Northwind.ViewModel
{
diff --git a/modules/Northwind/MeshWeaver.Northwind.ViewModel/MeshWeaver.Northwind.ViewModel.csproj b/modules/Northwind/MeshWeaver.Northwind.ViewModel/MeshWeaver.Northwind.ViewModel.csproj
index 1c2e39df5..928174865 100644
--- a/modules/Northwind/MeshWeaver.Northwind.ViewModel/MeshWeaver.Northwind.ViewModel.csproj
+++ b/modules/Northwind/MeshWeaver.Northwind.ViewModel/MeshWeaver.Northwind.ViewModel.csproj
@@ -1,32 +1,48 @@
+ net8.0
true
true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
true
bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
true
+
$(AssemblyName).xml
@@ -34,5 +50,4 @@
-
-
+
\ No newline at end of file
diff --git a/modules/Northwind/MeshWeaver.Northwind.ViewModel/OrdersSummaryArea.cs b/modules/Northwind/MeshWeaver.Northwind.ViewModel/OrdersSummaryArea.cs
index 7fe72bfb1..deb73910a 100644
--- a/modules/Northwind/MeshWeaver.Northwind.ViewModel/OrdersSummaryArea.cs
+++ b/modules/Northwind/MeshWeaver.Northwind.ViewModel/OrdersSummaryArea.cs
@@ -1,7 +1,6 @@
using System.Reactive.Linq;
using MeshWeaver.Application.Styles;
using MeshWeaver.Data;
-using MeshWeaver.Domain.Layout;
using MeshWeaver.Domain.Layout.Documentation;
using MeshWeaver.Layout;
using MeshWeaver.Layout.Composition;
@@ -48,7 +47,8 @@ RenderingContext ctx
)
{
var years = layoutArea
- .Workspace.GetObservable()
+ .Workspace
+ .GetObservable()
.DistinctUntilChanged()
.Select(x =>
x.Select(y => y.OrderDate.Year)
diff --git a/modules/Northwind/MeshWeaver.Northwind.ViewModel/ProductOverviewItem.cs b/modules/Northwind/MeshWeaver.Northwind.ViewModel/ProductOverviewItem.cs
index 3dbee581f..8a43aac7e 100644
--- a/modules/Northwind/MeshWeaver.Northwind.ViewModel/ProductOverviewItem.cs
+++ b/modules/Northwind/MeshWeaver.Northwind.ViewModel/ProductOverviewItem.cs
@@ -44,5 +44,5 @@ public record ProductOverviewItem
/// Gets the total amount.
///
[DisplayFormat(DataFormatString = "N2")]
- public double TotalAmount { get; init; }
+ public double TotalAmount { get; init; }
}
diff --git a/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.csproj b/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.csproj
new file mode 100644
index 000000000..b1ae36a0f
--- /dev/null
+++ b/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.csproj
@@ -0,0 +1,22 @@
+
+
+ Exe
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.runtimeconfig.json b/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.runtimeconfig.json
new file mode 100644
index 000000000..170449ccb
--- /dev/null
+++ b/src/MeshWeaver.Azure.Publish/MeshWeaver.Azure.Publish.runtimeconfig.json
@@ -0,0 +1,9 @@
+{
+ "runtimeOptions": {
+ "tfm": "net8.0",
+ "framework": {
+ "name": "Microsoft.NETCore.App",
+ "version": "8.0.0"
+ }
+ }
+}
diff --git a/src/MeshWeaver.Azure.Publish/Program.cs b/src/MeshWeaver.Azure.Publish/Program.cs
new file mode 100644
index 000000000..620551468
--- /dev/null
+++ b/src/MeshWeaver.Azure.Publish/Program.cs
@@ -0,0 +1,159 @@
+using System.CommandLine;
+using System.CommandLine.NamingConventionBinder;
+using System.Text.Json;
+using Azure.Storage.Blobs;
+using MeshWeaver.Search;
+using Microsoft.Extensions.Logging;
+
+namespace MeshWeaver.Azure.Publish
+{
+ public static class Program
+ {
+ public static async Task Main(string[] args)
+ {
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddConsole();
+ });
+ var logger = loggerFactory.CreateLogger(typeof(Program));
+
+ var rootCommand = new RootCommand
+ {
+ new Option(
+ "--path",
+ "The input files to process"),
+ new Option(
+ "--connection-string",
+ "The connection string for the blob storage"),
+ new Option(
+ "--container",
+ "The name of the blob container"),
+ new Option(
+ "--html-container",
+ "The name of the blob container containing pre-rendered html"),
+ new Option(
+ "--address",
+ "The address to use")
+ };
+
+ rootCommand.Description = "MeshWeaver Build Tasks";
+
+ rootCommand.Handler = CommandHandler.Create(async (path, connectionString, container, htmlContainer, address) =>
+ {
+ if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(connectionString) || string.IsNullOrEmpty(container) || string.IsNullOrEmpty(address))
+ {
+ logger.LogError("Error: All options (--path, --connection-string, --container, --address) must be provided and cannot be null or empty.");
+ return;
+ }
+
+ // Call your custom task logic here
+ logger.LogInformation("Processing files from\npath: {path}\nstorage: {connectionString}\ncontainer: {container}\nhtmlContainer: {htmlContainer}\naddress: {address}", path, connectionString, container, htmlContainer, address);
+ await ExecuteAsync(connectionString, container, htmlContainer, path, address, logger);
+ });
+
+ await rootCommand.InvokeAsync(args);
+ }
+
+ public static async Task ExecuteAsync(string connectionString, string container, string htmlContainer, string path, string address, ILogger logger)
+ {
+ try
+ {
+ var blobServiceClient = new BlobServiceClient(connectionString);
+ var blobContainer = blobServiceClient.GetBlobContainerClient(container);
+ var htmlBlobContainer = htmlContainer == null ? null : blobServiceClient.GetBlobContainerClient(container);
+
+ // Create the container if it doesn't exist
+ await blobContainer.CreateIfNotExistsAsync();
+ if(htmlContainer != null)
+ await htmlBlobContainer.CreateIfNotExistsAsync();
+
+
+ var localFiles = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Select(f => Path.Combine(address, Path.GetRelativePath(path, f))).ToHashSet();
+ var blobs = blobContainer.GetBlobs(prefix:address).ToDictionary(b => b.Name);
+
+ // Delete blobs that no longer exist in the local directory
+ foreach (var blob in blobs)
+ {
+ if (!localFiles.Contains(blob.Key))
+ {
+ logger.LogInformation($"Deleting blob {blob.Key} as it no longer exists in the local directory.");
+ await blobContainer.DeleteBlobAsync(blob.Key);
+ }
+ }
+
+ foreach (var inputFile in Directory.GetFiles(path))
+ {
+ try
+ {
+ var filePath = Path.Combine(address, Path.GetRelativePath(path, inputFile)).Replace('\\', '/');
+ var fileContent = await File.ReadAllTextAsync(inputFile);
+ var fileLastModified = File.GetLastWriteTimeUtc(inputFile);
+
+ // Check if the blob exists and if the local file is newer
+ if (blobs.TryGetValue(filePath, out var blobItem) && blobItem.Properties.LastModified >= fileLastModified)
+ {
+ logger.LogInformation($"Skipping upload for {filePath} as the blob is up-to-date.");
+ continue;
+ }
+
+ var extension = Path.GetExtension(filePath);
+ logger.LogInformation($"Uploading file {inputFile} to {filePath}. Parsing as {extension}");
+
+ var metadata = extension switch
+ {
+ ".md" => await ParseMarkdown(address, logger, filePath, fileContent, htmlBlobContainer),
+ _ => DefaultMetadata(filePath, extension.Trim('.'))
+ };
+
+ logger.LogInformation("Uploading {filePath} with {metadata}", filePath, JsonSerializer.Serialize(metadata));
+ var blob = blobContainer.GetBlobClient(filePath);
+ await blob.UploadAsync(new BinaryData(fileContent), overwrite: true);
+ await blob.SetMetadataAsync(metadata);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An error occurred during file upload.");
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "An error occurred during execution.");
+ return false;
+ }
+ }
+
+ private static Dictionary DefaultMetadata(string filePath, string type)
+ {
+ return new Dictionary
+ {
+ { "type", type },
+ { "path", filePath }
+ };
+}
+
+ private static async Task> ParseMarkdown(string address, ILogger logger, string filePath, string fileContent, BlobContainerClient htmlContainer)
+ {
+ // Parse metadata and HTML
+ var (article, html) = MarkdownIndexer.ParseArticle(filePath, fileContent, address);
+
+ // Set metadata on the HTML blob
+ var metadata = article?.ToMetadata(filePath, "md") ?? DefaultMetadata(filePath, "md");
+
+ if (htmlContainer != null)
+ {
+ // Store metadata and HTML in blob storage
+ var htmlFilePath = Path.ChangeExtension(filePath, ".html");
+ logger.LogInformation("Uploading pre-rendered html {path}", htmlFilePath);
+ var htmlBlobClient = htmlContainer.GetBlobClient(htmlFilePath);
+ await htmlBlobClient.UploadAsync(new BinaryData(html), overwrite: true);
+ await htmlBlobClient.SetMetadataAsync(metadata);
+
+ }
+
+ return metadata;
+ }
+ }
+}
diff --git a/src/MeshWeaver.Blazor/BlazorClientRegistry.cs b/src/MeshWeaver.Blazor/BlazorClientRegistry.cs
index cea78bf6c..666722f7d 100644
--- a/src/MeshWeaver.Blazor/BlazorClientRegistry.cs
+++ b/src/MeshWeaver.Blazor/BlazorClientRegistry.cs
@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Text.Json;
+using MeshWeaver.Application;
using MeshWeaver.Data;
using Microsoft.DotNet.Interactive.Formatting;
using MeshWeaver.Data.Serialization;
@@ -23,7 +24,7 @@ internal static MessageHubConfiguration AddBlazor(
) => config
.AddData()
.AddLayoutClient(c => (configuration ?? (x => x)).Invoke(c.WithView(DefaultFormatting)))
-
+ .WithTypes(typeof(ApplicationAddress))
;
#region Standard Formatting
private static ViewDescriptor DefaultFormatting(
diff --git a/src/MeshWeaver.Data.Contract/EntityStore.cs b/src/MeshWeaver.Data.Contract/EntityStore.cs
index b0ee5e3c0..f39b855d0 100644
--- a/src/MeshWeaver.Data.Contract/EntityStore.cs
+++ b/src/MeshWeaver.Data.Contract/EntityStore.cs
@@ -147,8 +147,15 @@ public override int GetHashCode()
=> Collections.Values
.Select(x => x.GetHashCode())
.Aggregate((x, y) => x ^ y);
+
+}
+
+public record EntityStoreAndUpdates(IEnumerable Changes, EntityStore Store)
+{
+ public EntityStoreAndUpdates(EntityStore Store) : this([], Store)
+ {
+ }
}
-public record EntityStoreAndUpdates(IEnumerable Changes, EntityStore Store);
public record EntityStoreUpdate(string Collection, object Id, object Value)
{
diff --git a/src/MeshWeaver.Data/Serialization/SynchronizationStream.cs b/src/MeshWeaver.Data/Serialization/SynchronizationStream.cs
index d1c0e73f3..f18cbad6f 100644
--- a/src/MeshWeaver.Data/Serialization/SynchronizationStream.cs
+++ b/src/MeshWeaver.Data/Serialization/SynchronizationStream.cs
@@ -146,6 +146,7 @@ public virtual void Initialize(ChangeItem initial)
current = initial ?? throw new ArgumentNullException(nameof(initial));
Store.OnNext(initial);
initialized.SetResult(current.Value);
+ deferral.Dispose();
}
public void OnCompleted()
@@ -181,8 +182,13 @@ IMessageDelivery delivery
public void Update(Func> update) =>
InvokeAsync(() => SetCurrent(update.Invoke(Current is null ? default : Current.Value)));
- public void OnNext(ChangeItem value) =>
- InvokeAsync(() => SetCurrent(value));
+ public void OnNext(ChangeItem value)
+ {
+ if(!IsInitialized)
+ Initialize(value);
+ else
+ InvokeAsync(() => SetCurrent(value));
+ }
public virtual DataChangeResponse RequestChange(Func> update)
{
@@ -207,6 +213,7 @@ public SynchronizationStream(object Owner,
this.Reference = Reference;
this.InitializationMode = InitializationMode;
synchronizationStreamHub = Hub.GetHostedHub(new SynchronizationStreamAddress(Hub.Address));
+ deferral = synchronizationStreamHub.Defer(_ => true);
}
@@ -226,6 +233,7 @@ private record SynchronizationStreamAddress(object Host) : IHostedAddress
}
private readonly IMessageHub synchronizationStreamHub;
+ private readonly IDisposable deferral;
//private void InvokeAsync(Func task)
// => synchronizationStreamHub.InvokeAsync(task);
diff --git a/src/MeshWeaver.Data/Workspace.cs b/src/MeshWeaver.Data/Workspace.cs
index 9ccd9a1d8..76cf832ae 100644
--- a/src/MeshWeaver.Data/Workspace.cs
+++ b/src/MeshWeaver.Data/Workspace.cs
@@ -3,6 +3,7 @@
using System.Reactive.Linq;
using System.Reflection;
using System.Text.Json;
+using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using MeshWeaver.Activities;
using MeshWeaver.Data.Serialization;
@@ -210,7 +211,8 @@ TReference reference
where TReference : WorkspaceReference
{
// link to deserialized world. Will also potentially link to workspace.
-
+ if(owner is JsonObject obj)
+ owner = obj.Deserialize