Skip to content

Commit

Permalink
Fix file change detection (#401)
Browse files Browse the repository at this point in the history
Fixes #400
  • Loading branch information
sebastienros authored Nov 22, 2021
1 parent 1c94467 commit ff64afa
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 176 deletions.
101 changes: 0 additions & 101 deletions Fluid.MinimalApisSample/README.md

This file was deleted.

28 changes: 7 additions & 21 deletions Fluid.MinimalApisSample/Views/_layout.liquid
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">

<title>Hello, world!</title>
</head>
<body>

<div class="px-4 py-5 my-5 text-center">
<h1 class="display-5 fw-bold">Title from the layout</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">
{% renderbody %}

{% partial 'Component', x: 1, y:2 %}
<h1>Title from the layout</h1>
{% renderbody %}

</p>
</div>
</div>
{% partial 'Component', x: 1, y:2 %}

<pre>
{% rendersection footer %}
</pre>
<footer>
{% rendersection footer %}
</footer>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"></script>
</body>
</html>
</html>
1 change: 0 additions & 1 deletion Fluid.MinimalApisSample/Views/_viewstart.liquid
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
{% layout '_Layout' %}
From /Views/_ViewStart.liquid
2 changes: 1 addition & 1 deletion Fluid.MinimalApisSample/Views/component.liquid
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<div>Using a component {{ x }} + {{ y }} = {{ x | plus: y }}</div>
<div>Using a component {{ x }} + {{ y }} = {{ x | plus: y }}</div>
4 changes: 3 additions & 1 deletion Fluid.MinimalApisSample/Views/index.liquid
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<hr />
Hello World from the body

<hr />
Name: {{ Name }} <br />
IsComplete: {{ IsComplete}}

<hr />

{% section footer %}
Hello from the footer section
{% endsection %}
{% endsection %}
2 changes: 1 addition & 1 deletion Fluid.MvcViewEngine/Fluid.MvcViewEngine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<LangVersion>9</LangVersion>
<LangVersion>latest</LangVersion>
<PackageIcon>logo_64x64.png</PackageIcon>
<IsPackable>true</IsPackable>
</PropertyGroup>
Expand Down
20 changes: 16 additions & 4 deletions Fluid.MvcViewEngine/FluidViewEngine.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand All @@ -17,6 +18,7 @@ public class FluidViewEngine : IFluidViewEngine
private const string ControllerKey = "controller";
private const string AreaKey = "area";
private FluidMvcViewOptions _options;
private ConcurrentDictionary<LocationCacheKey, FluidView> _locationCache = new();

public FluidViewEngine(FluidRendering fluidRendering,
IOptions<FluidMvcViewOptions> optionsAccessor,
Expand All @@ -37,19 +39,28 @@ private ViewEngineResult LocatePageFromViewLocations(ActionContext actionContext
var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
var areaName = GetNormalizedRouteValue(actionContext, AreaKey);

var key = new LocationCacheKey(controllerName, areaName, viewName);

if (_locationCache.TryGetValue(key, out var fluidView))
{
return ViewEngineResult.Found(viewName, fluidView);
}

var fileProvider = _options.ViewsFileProvider ?? _hostingEnvironment.ContentRootFileProvider;

var checkedLocations = new List<string>();
List<string> checkedLocations = null;

foreach (var location in _options.ViewsLocationFormats)
{
var view = String.Format(location, viewName, controllerName, areaName);

if (fileProvider.GetFileInfo(view).Exists)
{
return ViewEngineResult.Found(viewName, new FluidView(view, _fluidRendering));
_locationCache[key] = fluidView = new FluidView(view, _fluidRendering);
return ViewEngineResult.Found(viewName, fluidView);
}

checkedLocations ??= new();
checkedLocations.Add(view);
}

Expand Down Expand Up @@ -139,8 +150,7 @@ public static string GetNormalizedRouteValue(ActionContext context, string key)
var actionDescriptor = context.ActionDescriptor;
string normalizedValue = null;

if (actionDescriptor.RouteValues.TryGetValue(key, out string value) &&
!string.IsNullOrEmpty(value))
if (actionDescriptor.RouteValues.TryGetValue(key, out string value) && !string.IsNullOrEmpty(value))
{
normalizedValue = value;
}
Expand All @@ -153,5 +163,7 @@ public static string GetNormalizedRouteValue(ActionContext context, string key)

return stringRouteValue;
}

public readonly record struct LocationCacheKey(string ControllerName, string AreaName, string ViewName);
}
}
10 changes: 10 additions & 0 deletions Fluid.MvcViewEngine/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#if NETCOREAPP3_1
using System.ComponentModel;

// Fix for: error CS0518: Predefined type 'System.Runtime.CompilerServices.IsExternalInit' is not defined or imported
namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal class IsExternalInit { }
}
#endif
3 changes: 2 additions & 1 deletion Fluid.Tests/Mocks/MockFileProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace Fluid.Tests.Mocks
{
Expand Down Expand Up @@ -45,7 +46,7 @@ public MockFileProvider Add(string path, string content)

public IChangeToken Watch(string filter)
{
throw new NotImplementedException();
return NullChangeToken.Singleton;
}

private string NormalizePath(string path)
Expand Down
2 changes: 1 addition & 1 deletion Fluid.ViewEngine/Fluid.ViewEngine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<LangVersion>9</LangVersion>
<LangVersion>latest</LangVersion>
<IsPackable>true</IsPackable>
<PackageIcon>logo_64x64.png</PackageIcon>
</PropertyGroup>
Expand Down
5 changes: 5 additions & 0 deletions Fluid.ViewEngine/FluidViewEngineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,10 @@ public class FluidViewEngineOptions
/// /Views/Includes/{0}.liquid
/// </example>
public List<string> PartialsLocationFormats { get; } = new();

/// <summary>
/// Gets or sets whether files should be reloaded automatically when changed. Default is <code>true</code>;
/// </summary>
public bool TrackFileChanges { get; set; } = true;
}
}
60 changes: 42 additions & 18 deletions Fluid.ViewEngine/FluidViewRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ namespace Fluid.ViewEngine
/// </summary>
public class FluidViewRenderer : IFluidViewRenderer
{
private readonly ConcurrentDictionary<string, IFluidTemplate> _cache = new ConcurrentDictionary<string, IFluidTemplate>();
private class CacheEntry
{
public IDisposable Callback;
public ConcurrentDictionary<string, IFluidTemplate> TemplateCache = new();
}

private readonly ConcurrentDictionary<IFileProvider, CacheEntry> _cache = new();

public FluidViewRenderer(FluidViewEngineOptions fluidViewEngineOptions)
{
Expand All @@ -34,18 +40,18 @@ public virtual async Task RenderViewAsync(TextWriter writer, string relativePath

var template = await GetFluidTemplateAsync(relativePath, _fluidViewEngineOptions.ViewsFileProvider, true);

// The body is rendered and buffer before the Layout since it can contain fragments
// The body is rendered and buffered before the Layout since it can contain fragments
// that need to be rendered as part of the Layout.
// Also the body or its _ViewStarts might contain a Layout tag.

var body = await template.RenderAsync(context, _fluidViewEngineOptions.TextEncoder);

// If a layout is specified while rendering a view, execute it
if (context.AmbientValues.TryGetValue(Constants.LayoutIndex, out var layoutPath) && !String.IsNullOrEmpty(Convert.ToString(layoutPath)))
if (context.AmbientValues.TryGetValue(Constants.LayoutIndex, out var layoutPath) && layoutPath is string layoutPathString && !String.IsNullOrEmpty(layoutPathString))
{
context.AmbientValues[Constants.ViewPathIndex] = layoutPath;
context.AmbientValues[Constants.BodyIndex] = body;
var layoutTemplate = await GetFluidTemplateAsync((string)layoutPath, _fluidViewEngineOptions.ViewsFileProvider, false);
var layoutTemplate = await GetFluidTemplateAsync(layoutPathString, _fluidViewEngineOptions.ViewsFileProvider, false);

await layoutTemplate.RenderAsync(writer, _fluidViewEngineOptions.TextEncoder, context);
}
Expand All @@ -58,7 +64,6 @@ public virtual async Task RenderViewAsync(TextWriter writer, string relativePath
public virtual async Task RenderPartialAsync(TextWriter writer, string relativePath, TemplateContext context)
{
// Substitute View Path
context.AmbientValues.TryGetValue(Constants.ViewPathIndex, out var viewPath);
context.AmbientValues[Constants.ViewPathIndex] = relativePath;

var template = await GetFluidTemplateAsync(relativePath, _fluidViewEngineOptions.PartialsFileProvider, false);
Expand Down Expand Up @@ -97,30 +102,49 @@ protected virtual List<string> FindViewStarts(string viewPath, IFileProvider fil
return viewStarts;
}

protected async ValueTask<IFluidTemplate> GetFluidTemplateAsync(string path, IFileProvider fileProvider, bool includeViewStarts)
protected virtual async ValueTask<IFluidTemplate> GetFluidTemplateAsync(string path, IFileProvider fileProvider, bool includeViewStarts)
{
if (TryGetCachedTemplate(path, out var template))
var cache = _cache.GetOrAdd(fileProvider, f =>
{
var cacheEntry = new CacheEntry();

if (_fluidViewEngineOptions.TrackFileChanges)
{
Action<object> callback = null;

callback = c =>
{
// The order here is important. We need to take the token and then apply our changes BEFORE
// registering. This prevents us from possible having two change updates to process concurrently.
//
// If the file changes after we take the token, then we'll process the update immediately upon
// registering the callback.

var entry = (CacheEntry)c;
var previousCallBack = entry.Callback;
previousCallBack?.Dispose();
var token = fileProvider.Watch("**/*" + Constants.ViewExtension);
entry.TemplateCache.Clear();
entry.Callback = token.RegisterChangeCallback(callback, c);
};

cacheEntry.Callback = fileProvider.Watch("**/*" + Constants.ViewExtension).RegisterChangeCallback(callback, cacheEntry);
}
return cacheEntry;
});

if (cache.TemplateCache.TryGetValue(path, out var template))
{
return template;
}

template = await ParseLiquidFileAsync(path, fileProvider, includeViewStarts);

SetCachedTemplate(path, template);
cache.TemplateCache[path] = template;

return template;
}

protected virtual bool TryGetCachedTemplate(string path, out IFluidTemplate template)
{
return _cache.TryGetValue(path, out template);
}

protected virtual void SetCachedTemplate(string path, IFluidTemplate template)
{
_cache[path] = template;
}

protected virtual async ValueTask<IFluidTemplate> ParseLiquidFileAsync(string path, IFileProvider fileProvider, bool includeViewStarts)
{
var fileInfo = fileProvider.GetFileInfo(path);
Expand Down
Loading

0 comments on commit ff64afa

Please sign in to comment.