Skip to content

Commit

Permalink
Introduce operation-id. Add hydro-link, make hydro-boost obsolete.
Browse files Browse the repository at this point in the history
  • Loading branch information
kjeske committed Nov 20, 2023
1 parent 79429f3 commit 926c301
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 136 deletions.
6 changes: 3 additions & 3 deletions docs/content/features/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ There are 3 kinds of managed navigation in applications using Hydro:

## Navigation via links

With `hydro-boost` attribute relative links in your application can be loaded in the background and applied to the current document instead of doing the full page reload.
With `hydro-link` attribute relative links in your application can be loaded in the background and applied to the current document instead of doing the full page reload.

Examples:

Attribute applied directly on a link:
```html
<a href="/page" hydro-boost>My page</a>
<a href="/page" hydro-link>My page</a>
```

Attribute applied directly on a parent of the links:
```html
<ul hydro-boost>
<ul hydro-link>
<li><a href="/page1">My page 1</a></li>
<li><a href="/page2">My page 2</a></li>
</ul>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/features/ui-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ outline: deep
CSS class added to elements that triggered a Hydro operation and is currently being processed

### `.hydro-loading`
CSS class added to `body` element when a page is loading using boost functionality
CSS class added to `body` element when a page is loading using hydro-link functionality

### `disabled`
Attribute added to elements that triggered a Hydro operation and is currently being processed
Expand Down
164 changes: 99 additions & 65 deletions src/HydroComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Hydro;
/// </summary>
public abstract class HydroComponent : ViewComponent
{
private string _id;
private string _componentId;

private readonly ConcurrentDictionary<CacheKey, object> _requestCache = new();
private static readonly ConcurrentDictionary<CacheKey, object> PersistentCache = new();
Expand All @@ -31,7 +31,7 @@ public abstract class HydroComponent : ViewComponent
private static readonly MethodInfo InvokeActionMethod = typeof(HydroComponent).GetMethod(nameof(InvokeAction), BindingFlags.Static | BindingFlags.NonPublic);
private static readonly MethodInfo InvokeActionAsyncMethod = typeof(HydroComponent).GetMethod(nameof(InvokeActionAsync), BindingFlags.Static | BindingFlags.NonPublic);

private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
private static readonly JsonSerializerSettings JsonSerializerSettings = new()
{
Converters = new JsonConverter[] { new Int32Converter() }.ToList()
};
Expand Down Expand Up @@ -123,24 +123,33 @@ public void Subscribe<TEvent>(Func<TEvent, Task> action) =>
/// </summary>
/// <param name="data">Data to pass</param>
/// <param name="scope">Scope of the event</param>
/// <param name="asynchronous">Do not chain the execution of handlers and run them separately</param>
/// <typeparam name="TEvent">Event type</typeparam>
public void Dispatch<TEvent>(TEvent data, Scope scope = Scope.Parent) =>
Dispatch(GetFullTypeName(typeof(TEvent)), data, scope);
public void Dispatch<TEvent>(TEvent data, Scope scope = Scope.Parent, bool asynchronous = false) =>
Dispatch(GetFullTypeName(typeof(TEvent)), data, scope, asynchronous);

/// <summary>
/// Triggers a Hydro event
/// </summary>
/// <param name="name">Name of the event</param>
/// <param name="data">Data to pass</param>
/// <param name="scope">Scope of the event</param>
/// <param name="asynchronous">Do not chain the execution of handlers and run them separately</param>
/// <typeparam name="TEvent">Event type</typeparam>
public void Dispatch<TEvent>(string name, TEvent data, Scope scope = Scope.Parent) =>
public void Dispatch<TEvent>(string name, TEvent data, Scope scope = Scope.Parent, bool asynchronous = false)
{
var operationId = !asynchronous && HttpContext.Request.Headers.TryGetValue(HydroConsts.RequestHeaders.OperationId, out var incomingOperationId)
? incomingOperationId.First()
: Guid.NewGuid().ToString("N");

_dispatchEvents.Add(new HydroComponentEvent
{
Name = name,
Data = data,
Scope = scope.ToString().ToLower()
Scope = scope.ToString().ToLower(),
OperationId = operationId
});
}

/// <summary>
/// Triggered once the component is mounted
Expand Down Expand Up @@ -200,7 +209,7 @@ protected Cache<T> Cache<T>(Func<T> func, CacheLifetime lifetime = CacheLifetime
{
var cache = lifetime == CacheLifetime.Request ? _requestCache : PersistentCache;

var cacheKey = new CacheKey(_id, func);
var cacheKey = new CacheKey(_componentId, func);
if (cache.TryGetValue(cacheKey, out var dic))
{
var value = (Cache<T>)dic;
Expand Down Expand Up @@ -239,7 +248,7 @@ private bool DetermineRootComponent()
private async Task<string> RenderOnlineRootComponent(IPersistentState persistentState)
{
var componentId = GetRootComponentId();
_id = componentId;
_componentId = componentId;

PopulateBaseModel(persistentState);
PopulateRequestModel();
Expand All @@ -259,7 +268,7 @@ private async Task<string> RenderOnlineRootComponent(IPersistentState persistent
private async Task<string> RenderOnlineNestedComponent(IPersistentState persistentState)
{
var componentId = GenerateComponentId(Key);
_id = componentId;
_componentId = componentId;

if (IsComponentIdRendered(componentId))
{
Expand All @@ -282,7 +291,7 @@ private static string GetComponentPlaceholderTemplate(string componentId) =>
private async Task<string> RenderStaticComponent(IPersistentState persistentState)
{
var componentId = GenerateComponentId(Key);
_id = componentId;
_componentId = componentId;

if (!await AuthorizeAsync())
{
Expand Down Expand Up @@ -406,7 +415,7 @@ private void PopulateDispatchers()
}

var data = _dispatchEvents
.Select(e => new { name = e.Name, data = e.Data, scope = e.Scope })
.Select(e => new { name = e.Name, data = e.Data, scope = e.Scope, operationId = e.OperationId })
.ToList();

HttpContext.Response.Headers.TryAdd(HydroConsts.ResponseHeaders.Trigger, JsonConvert.SerializeObject(data));
Expand All @@ -420,41 +429,53 @@ private bool IsComponentIdRendered(string componentId)

private async Task TriggerMethod()
{
if (HttpContext.Items.TryGetValue(HydroConsts.ContextItems.MethodName, out var method) && method is string methodValue && !string.IsNullOrWhiteSpace(methodValue))
if (!HttpContext.Items.TryGetValue(HydroConsts.ContextItems.MethodName, out var method)
|| method is not string methodValue || string.IsNullOrWhiteSpace(methodValue))
{
var methodInfo = GetType()
.GetMethod(methodValue, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
return;
}

if (methodInfo != null)
{
var requestParameters = GetParameters();
var methodParameters = methodInfo.GetParameters();
var methodInfo = GetType()
.GetMethod(methodValue, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);

if (requestParameters.Count != methodParameters.Length || requestParameters.Any(rp => !methodParameters.Any(mp => rp.Key == mp.Name)))
{
throw new InvalidOperationException("Wrong action parameters");
}
if (methodInfo == null)
{
return;
}

var orderedParameters = methodParameters
.Select(p =>
{
var sourceType = requestParameters[p.Name].GetType();
var requestParameters = GetParameters();
var methodParameters = methodInfo.GetParameters();

return sourceType == p.ParameterType
? requestParameters[p.Name]
: TypeDescriptor.GetConverter(p.ParameterType).ConvertFrom(requestParameters[p.Name]);
})
.ToArray();
if (requestParameters.Count != methodParameters.Length
|| requestParameters.Any(rp => !methodParameters.Any(mp => rp.Key == mp.Name)))
{
throw new InvalidOperationException("Wrong action parameters");
}

var operationId = HttpContext.Request.Headers.TryGetValue(HydroConsts.RequestHeaders.OperationId, out var incomingOperationId)
? incomingOperationId.First()
: Guid.NewGuid().ToString("N");

if (typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
await (Task)methodInfo.Invoke(this, orderedParameters)!;
}
else
{
methodInfo.Invoke(this, orderedParameters);
}
}
HttpContext.Response.Headers.TryAdd(HydroConsts.ResponseHeaders.OperationId, operationId);

var orderedParameters = methodParameters
.Select(p =>
{
var sourceType = requestParameters[p.Name!].GetType();
return sourceType == p.ParameterType
? requestParameters[p.Name]
: TypeDescriptor.GetConverter(p.ParameterType).ConvertFrom(requestParameters[p.Name]);
})
.ToArray();

if (typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
await (Task)methodInfo.Invoke(this, orderedParameters)!;
}
else
{
methodInfo.Invoke(this, orderedParameters);
}
}

Expand All @@ -475,30 +496,43 @@ public T GetPayload<T>() =>

private async Task TriggerEvent()
{
if (HttpContext.Items.TryGetValue(HydroConsts.ContextItems.EventName, out var eventName) && eventName is string eventNameValue && !string.IsNullOrWhiteSpace(eventNameValue))
if (!HttpContext.Items.TryGetValue(HydroConsts.ContextItems.EventName, out var eventName)
|| eventName is not string eventNameValue || string.IsNullOrWhiteSpace(eventNameValue))
{
var subscription = _subscriptions.FirstOrDefault(s => s.EventName == eventNameValue);
return;
}

if (subscription != null)
{
var methodInfo = subscription.Action.Method;
var parameters = methodInfo.GetParameters();
var parameterType = parameters.First().ParameterType;
var model = HttpContext.Items.TryGetValue(HydroConsts.ContextItems.EventData, out var eventModel) ? JsonConvert.DeserializeObject((string)eventModel, parameterType) : null;
var subscription = _subscriptions.FirstOrDefault(s => s.EventName == eventNameValue);

var isAsync = typeof(Task).IsAssignableFrom(methodInfo.ReturnType);
if (subscription == null)
{
return;
}

if (isAsync)
{
var method = InvokeActionAsyncMethod.MakeGenericMethod(parameterType);
await (Task)method.Invoke(null, new[] { subscription.Action, model })!;
}
else
{
var method = InvokeActionMethod.MakeGenericMethod(parameterType);
method.Invoke(null, new[] { subscription.Action, model });
}
}
var methodInfo = subscription.Action.Method;
var parameters = methodInfo.GetParameters();
var parameterType = parameters.First().ParameterType;
var model = HttpContext.Items.TryGetValue(HydroConsts.ContextItems.EventData, out var eventModel)
? JsonConvert.DeserializeObject((string)eventModel!, parameterType)
: null;

var operationId = HttpContext.Request.Headers.TryGetValue(HydroConsts.RequestHeaders.OperationId, out var incomingOperationId)
? incomingOperationId.First()
: Guid.NewGuid().ToString("N");

HttpContext.Response.Headers.TryAdd(HydroConsts.ResponseHeaders.OperationId, operationId);

var isAsync = typeof(Task).IsAssignableFrom(methodInfo.ReturnType);

if (isAsync)
{
var method = InvokeActionAsyncMethod.MakeGenericMethod(parameterType);
await (Task)method.Invoke(null, new[] { subscription.Action, model })!;
}
else
{
var method = InvokeActionMethod.MakeGenericMethod(parameterType);
method.Invoke(null, new[] { subscription.Action, model });
}
}

Expand Down Expand Up @@ -538,7 +572,7 @@ private string GetViewPath()
{
var type = GetType();
var assemblyName = type.Assembly.GetName().Name;
return $"{type.FullName.Replace(assemblyName, "~").Replace(".", "/")}.cshtml";
return $"{type.FullName!.Replace(assemblyName!, "~").Replace(".", "/")}.cshtml";
}

private async Task<string> GetComponentHtml()
Expand Down Expand Up @@ -659,7 +693,7 @@ private void ValidateModel()
{
if (IsModelTouched || TouchedProperties.Contains(memberName))
{
ModelState.AddModelError(memberName, validationResult.ErrorMessage);
ModelState.AddModelError(memberName, validationResult.ErrorMessage!);
}
}
}
Expand All @@ -686,15 +720,15 @@ private static IEnumerable<ValidationResult> ExtractValidationResults(IEnumerabl
private async Task<bool> AuthorizeAsync()
{
var type = GetType();

if (!ComponentAuthorizationAttributes.ContainsKey(type))
{
ComponentAuthorizationAttributes.TryAdd(type, type.GetCustomAttributes(true)
.Where(attr => attr is IHydroAuthorizationFilter)
.Cast<IHydroAuthorizationFilter>()
.ToArray());
}

foreach (var authorizationFilter in ComponentAuthorizationAttributes[type])
{
if (!await authorizationFilter.AuthorizeAsync(HttpContext, this))
Expand All @@ -703,7 +737,7 @@ private async Task<bool> AuthorizeAsync()
{
HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
}

return false;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/HydroComponentEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ internal class HydroComponentEvent
public string Name { get; init; }
public object Data { get; init; }
public string Scope { get; set; }
public string OperationId { get; set; }
}
2 changes: 2 additions & 0 deletions src/HydroConsts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ public static class RequestHeaders
public const string Boosted = "Hydro-Boosted";
public const string Hydro = "Hydro-Request";
public const string Parameters = "Hydro-Parameters";
public const string OperationId = "Hydro-Operation-Id";
public const string Payload = "Hydro-Payload";
public const string RenderedComponentIds = "hydro-all-ids";
}

public static class ResponseHeaders
{
public const string Trigger = "Hydro-Trigger";
public const string OperationId = "Hydro-Operation-Id";
}

public static class ContextItems
Expand Down
Loading

0 comments on commit 926c301

Please sign in to comment.