Skip to content
Open
Show file tree
Hide file tree
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
28 changes: 17 additions & 11 deletions src/Configuration/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using Hydro.Utils;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
Expand All @@ -16,36 +17,41 @@ public static class ApplicationBuilderExtensions
/// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
/// <param name="environment">Current environment</param>
public static IApplicationBuilder UseHydro(this IApplicationBuilder builder, IWebHostEnvironment environment = null) =>
builder.UseHydro(environment, Assembly.GetCallingAssembly());
builder.UseHydro(environment, new[] { Assembly.GetCallingAssembly() });

/// <summary>
/// Adds configuration for hydro
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
/// <param name="environment">Current environment</param>
/// <param name="assembly">Assembly to scan for the Hydro components</param>
/// <param name="assemblies">List of assemblies to scan for the Hydro components</param>
/// <returns></returns>
public static IApplicationBuilder UseHydro(this IApplicationBuilder builder, IWebHostEnvironment environment, Assembly assembly)
public static IApplicationBuilder UseHydro(this IApplicationBuilder builder, IWebHostEnvironment environment, Assembly[] assemblies)
{
builder.UseEndpoints(endpoints =>
{
var types = assembly.GetTypes().Where(t => t.IsAssignableTo(typeof(HydroComponent))).ToList();
int assemblyCounter = 0;
foreach(var assembly in assemblies)
{
var types = assembly.GetTypes().Where(t => t.IsAssignableTo(typeof(HydroComponent))).ToList();

foreach (var type in types)
{
endpoints.MapHydroComponent(type);
foreach (var type in types)
{
endpoints.MapHydroComponent(type, SHA256Util.ComputeHashForAssemblyName(assembly.FullName));
}

assemblyCounter++;
}
});

environment ??= (IWebHostEnvironment)builder.ApplicationServices.GetService(typeof(IWebHostEnvironment))!;
var existingProvider = environment.WebRootFileProvider;

var existingProvider = environment.WebRootFileProvider;

var scriptsFileProvider = new ScriptsFileProvider(typeof(ApplicationBuilderExtensions).Assembly);
var compositeProvider = new CompositeFileProvider(existingProvider, scriptsFileProvider);
environment.WebRootFileProvider = compositeProvider;

return builder;
}

}
3 changes: 2 additions & 1 deletion src/HydroComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ private async Task<string> GenerateComponentHtml(string componentId, IPersistent

rootElement.SetAttributeValue("id", componentId);
rootElement.SetAttributeValue("hydro-name", GetType().Name);
rootElement.SetAttributeValue("hydro-prefix", SHA256Util.ComputeHashForAssemblyName(GetType().Assembly.FullName));
rootElement.SetAttributeValue("x-data", "hydro");
rootElement.SetAttributeValue("key", componentId);

Expand Down Expand Up @@ -802,7 +803,7 @@ private HtmlNode GetEventSubscriptionScript(HtmlDocument document, HydroEventSub
{
name = subscription.EventName,
subject = subscription.SubjectRetriever?.Invoke(),
path = $"/hydro/{GetType().Name}/event".ToLower()
path = $"/hydro/{SHA256Util.ComputeHashForAssemblyName(GetType().Assembly.GetName().FullName)}/{GetType().Name}/event".ToLower()
};

var scriptNode = document.CreateElement("script");
Expand Down
4 changes: 2 additions & 2 deletions src/HydroComponentsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ namespace Hydro;

internal static class HydroComponentsExtensions
{
public static void MapHydroComponent(this IEndpointRouteBuilder app, Type componentType)
public static void MapHydroComponent(this IEndpointRouteBuilder app, Type componentType, string prefix = "")
{

var componentName = componentType.Name;
var options = app.ServiceProvider.GetRequiredService<HydroOptions>();

app.MapPost($"{options.BasePath}/{componentName}/{{method?}}", async (
app.MapPost($"{options.BasePath}/{prefix}/{componentName}/{{method?}}", async (
[FromServices] IServiceProvider serviceProvider,
[FromServices] IViewComponentHelper viewComponentHelper,
[FromServices] HydroOptions hydroOptions,
Expand Down
8 changes: 5 additions & 3 deletions src/Scripts/hydro.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
return {
id: component.getAttribute("id"),
name: component.getAttribute("hydro-name"),
prefix: component.getAttribute("hydro-prefix"),
element: component
};
}
Expand Down Expand Up @@ -217,7 +218,7 @@
throw new Error('Cannot find Hydro component');
}

const url = `${config.BasePath}/${component.name}`;
const url = `${config.BasePath}/${component.prefix}/${component.name}`;

if (!binding[component.id]) {
binding[component.id] = {
Expand Down Expand Up @@ -278,7 +279,7 @@
}

async function hydroAction(el, component, action) {
const url = `${config.BasePath}/${component.name}/${action.name}`;
const url = `${config.BasePath}/${component.prefix}/${component.name}/${action.name}`;

if (Array.from(el.attributes).some(attr => attr.name.startsWith('x-hydro-bind')) && isElementDirty(el)) {
await hydroBind(el);
Expand Down Expand Up @@ -312,6 +313,7 @@
const component = el.closest("[hydro]");
const componentId = component.getAttribute("id");
const componentName = component.getAttribute("hydro-name");
const componentPrefix = component.getAttribute("hydro-prefix");

if (!component) {
throw new Error('Cannot determine the closest Hydro component');
Expand Down Expand Up @@ -488,7 +490,7 @@
setTimeout(() => {
enablePlainScripts(component);
document.dispatchEvent(new CustomEvent('HydroComponentUpdate', {
detail: { component: { id: componentId, name: componentName, element: component }, url, type }
detail: { component: { id: componentId, name: componentName, prefix: componentPrefix, element: component }, url, type }
}));
});
}
Expand Down
37 changes: 37 additions & 0 deletions src/Utils/SHA256Util.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections.Concurrent;
using System.Text;

namespace Hydro.Utils
{
internal class SHA256Util
{
private static ConcurrentDictionary<string, string> _assemblyCached { get; } = new();

/// <summary>
/// This functions hash assembly names to ensure that the same assembly will always have the same hash, and that different assemblies will have different hashes.
/// </summary>
/// <remarks>Do not use this method for security purposes. Only use to hash assembly names for identification.</remarks>
/// <param name="input">The assembly name to hash.</param>
/// <returns>A unique hash for the assembly name.</returns>
public static string ComputeHashForAssemblyName(string input)
{
// we cache the hashes to avoid computing the hash multiple times for the same assembly.
if (!_assemblyCached.TryGetValue(input, out string hashed))
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hashBytes = sha256.ComputeHash(bytes);

// we only need a short hash for the assembly name, so we take the first 12 characters of the hash. This should be enough to ensure uniqueness while keeping the hash short.
hashed = BitConverter.ToString(hashBytes)
.Replace("-", "")
.Replace(".", "")
.ToLowerInvariant()[..12];

_assemblyCached[input] = hashed;
}

return hashed;
}
}
}