Skip to content

Commit

Permalink
Fix property injection & wrapping RazorSlice in IResult (#46)
Browse files Browse the repository at this point in the history
* Fix injectable properties when not doing native AOT

* Ensure HttpContext is set by RazorSliceHttpResultWrapper

* Fix copy pasta
  • Loading branch information
DamianEdwards authored Jun 7, 2024
1 parent 6b45f40 commit 65fde58
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 24 deletions.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>

<PropertyGroup>
<VersionPrefix>0.8.0</VersionPrefix>
<VersionPrefix>0.8.1</VersionPrefix>
<!-- VersionSuffix used for local builds -->
<VersionSuffix>dev</VersionSuffix>
<!-- VersionSuffix to be used for CI builds -->
Expand Down
8 changes: 6 additions & 2 deletions src/RazorSlices/RazorSlice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ public IServiceProvider? ServiceProvider
/// <summary>
/// Gets or sets a delegate used to initialize the template class before <see cref="ExecuteAsync"/> is called.
/// </summary>
internal Action<RazorSlice, IServiceProvider?, HttpContext?>? Initialize { get; set; }
/// <remarks>
/// This property should not be interacted with directly. It's used by the Razor Slices infrastructure.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public Action<RazorSlice, IServiceProvider?, HttpContext?>? Initialize { get; set; }

/// <summary>
/// Implemented by the generated template class.
Expand Down Expand Up @@ -176,7 +180,7 @@ private ValueTask RenderToTextWriterAsync(TextWriter textWriter, HtmlEncoder? ht

if (renderLayout && this is IUsesLayout useLayout)
{
return RenderViaLayout(RenderToTextWriterAsync, useLayout, textWriter, htmlEncoder, cancellationToken);
return RenderViaLayout(RenderToTextWriterAsync, useLayout, _textWriter, htmlEncoder, cancellationToken);
}

var executeTask = ExecuteAsyncImpl();
Expand Down
57 changes: 43 additions & 14 deletions src/RazorSlices/RazorSliceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,17 @@ public static class RazorSliceFactory
{
private static readonly HashSet<string> ExcludedServiceNames =
new(StringComparer.OrdinalIgnoreCase) { "IModelExpressionProvider", "IUrlHelper", "IViewComponentHelper", "IJsonHelper", "IHtmlHelper`1" };
private static readonly PropertyInfo _requestServicesProperty = typeof(HttpContext).GetProperty(nameof(HttpContext.RequestServices))
?? throw new InvalidOperationException("Could not find HttpContext.RequestServices. Likely a bug in Razor Slices itself.");
private static readonly MethodInfo _getServiceMethod = typeof(IServiceProvider).GetMethod(nameof(IServiceProvider.GetService))
?? throw new InvalidOperationException("Could not find IServiceProvider.GetService. Likely a bug in Razor Slices itself.");
private static readonly MethodInfo _getRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), [typeof(IServiceProvider), typeof(Type)])
?? throw new InvalidOperationException("Could not find ServiceProviderServiceExtensions.GetRequirdService. Likely a bug in Razor Slices itself.");

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)]
private static readonly Type _razorSliceType = typeof(RazorSlice);
private static readonly PropertyInfo _razorSliceInitializeProperty = _razorSliceType.GetProperty(nameof(RazorSlice.Initialize))!;
private static readonly PropertyInfo _razorSliceInitializeProperty = _razorSliceType.GetProperty(nameof(RazorSlice.Initialize))
?? throw new InvalidOperationException("Could not find RazorSlice.Initialize property. Likely a bug in Razor Slices itself.");
private static readonly ConstructorInfo _ioeCtor = typeof(InvalidOperationException).GetConstructor([typeof(string)])!;
private static readonly NullabilityInfoContext _nullabilityContext = new();
private static readonly Action<RazorSlice, IServiceProvider?, HttpContext?> _emptyInit = (_, __, ___) => { };
Expand Down Expand Up @@ -94,53 +102,74 @@ internal static (bool Any, PropertyInfo[] Nullable, PropertyInfo[] NonNullable)
}

[RequiresDynamicCode("Uses System.Linq.Expressions to dynamically generate delegates for initializing slices")]
private static Expression<Action<RazorSlice, IServiceProvider?>> GetExpressionInitAction(SliceDefinition sliceDefinition)
private static Expression<Action<RazorSlice, IServiceProvider?, HttpContext?>> GetExpressionInitAction(SliceDefinition sliceDefinition)
{
if (!sliceDefinition.InjectableProperties.Any) throw new InvalidOperationException("Shouldn't call GetExpressionInitAction if there's no injectable properties.");

// Make a delegate like:
//
// (RazorSlice slice, IServiceProvider? sp) =>
// (RazorSlice slice, IServiceProvider? sp, HttpContext? httpContext) =>
// {
// if (sp is null) throw new InvalidOperationException("Cannot initialize @inject properties of slice because the ServiceProvider property is null.");
// var services = sp;
// if (services == null && httpContext != null)
// {
// services = httpContext.RequestServices;
// }
// if (services == null) throw new InvalidOperationException("Cannot initialize @inject properties of slice because the ServiceProvider property is null.");
// var s = (MySlice)slice;
// s.SomeProp = (SomeService)sp.GetService(typeof(SomeService));
// s.NextProp = (SomeOtherService)sp.GetRequiredService(typeof(SomeOtherService));
// s.SomeProp = (SomeService)services.GetService(typeof(SomeService));
// s.NextProp = (SomeOtherService)services.GetRequiredService(typeof(SomeOtherService));
// }

var sliceParam = Expression.Parameter(_razorSliceType, "slice");
var spParam = Expression.Parameter(typeof(IServiceProvider), "sp");
var httpContextParam = Expression.Parameter(typeof(HttpContext), "httpContext");
var servicesVar = Expression.Variable(typeof(IServiceProvider), "services");
var castSliceVar = Expression.Variable(sliceDefinition.SliceType, "s");

var body = new List<Expression>
{
// var services = sp;
Expression.Assign(servicesVar, spParam),
// if
Expression.IfThen(
Expression.And(
// services == null
Expression.Equal(servicesVar, Expression.Constant(null)),
// httpContext != null
Expression.NotEqual(httpContextParam, Expression.Constant(null))),
// services = httpContext.RequestServices
Expression.Assign(servicesVar, Expression.MakeMemberAccess(httpContextParam, _requestServicesProperty))),
Expression.IfThen(
Expression.Equal(spParam, Expression.Constant(null)),
// services == null
Expression.Equal(servicesVar, Expression.Constant(null)),
// throw new InvalidOperationException
Expression.Throw(Expression.New(_ioeCtor, Expression.Constant("Cannot initialize @inject properties of slice because the ServiceProvider property is null.")))),
// var s = (MySlice)slice;
Expression.Assign(castSliceVar, Expression.Convert(sliceParam, sliceDefinition.SliceType))
};

var getServiceMethod = typeof(IServiceProvider).GetMethod("GetService")!;
foreach (var ip in sliceDefinition.InjectableProperties.Nullable)
{
// s.SomeProp = (SomeService)services.GetService(typeof(SomeService));
var propertyAccess = Expression.MakeMemberAccess(castSliceVar, ip);
var getServiceCall = Expression.Call(spParam, getServiceMethod, Expression.Constant(ip.PropertyType));
var getServiceCall = Expression.Call(servicesVar, _getServiceMethod, Expression.Constant(ip.PropertyType));
body.Add(Expression.Assign(propertyAccess, Expression.Convert(getServiceCall, ip.PropertyType)));
}

var getRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod("GetRequiredService", [typeof(IServiceProvider), typeof(Type)])!;
foreach (var ip in sliceDefinition.InjectableProperties.NonNullable)
{
// s.SomeProp = (SomeService)services.GetRequiredService(typeof(SomeService));
var propertyAccess = Expression.MakeMemberAccess(castSliceVar, ip);
var getServiceCall = Expression.Call(null, getRequiredServiceMethod, spParam, Expression.Constant(ip.PropertyType));
var getServiceCall = Expression.Call(null, _getRequiredServiceMethod, servicesVar, Expression.Constant(ip.PropertyType));
body.Add(Expression.Assign(propertyAccess, Expression.Convert(getServiceCall, ip.PropertyType)));
}

return Expression.Lambda<Action<RazorSlice, IServiceProvider?>>(
return Expression.Lambda<Action<RazorSlice, IServiceProvider?, HttpContext?>>(
body: Expression.Block(
variables: [castSliceVar],
variables: [servicesVar, castSliceVar],
body),
parameters: [sliceParam, spParam]);
parameters: [sliceParam, spParam, httpContextParam]);
}

[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.",
Expand Down
6 changes: 0 additions & 6 deletions src/RazorSlices/RazorSliceHttpResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,3 @@ Task IResult.ExecuteAsync(HttpContext httpContext)
return RazorSliceHttpResultHelpers.ExecuteAsync(this, httpContext, HtmlEncoder, StatusCode, ContentType);
}
}

//public abstract class RazorSliceHttpResult<TLayout> : RazorSliceHttpResult, IUseLayout
// where TLayout : IUseLayout
//{
// RazorSlice IUseLayout.CreateLayout() => TLayout.Create();
//}
6 changes: 5 additions & 1 deletion src/RazorSlices/RazorSliceHttpResultWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ internal sealed class RazorSliceHttpResultWrapper(RazorSlice razorSlice) : IRazo
public HtmlEncoder? HtmlEncoder { get; set; }

/// <inheritdoc />
Task IResult.ExecuteAsync(HttpContext httpContext) => RazorSliceHttpResultHelpers.ExecuteAsync(razorSlice, httpContext, HtmlEncoder, StatusCode ?? StatusCodes.Status200OK, ContentType);
Task IResult.ExecuteAsync(HttpContext httpContext)
{
razorSlice.HttpContext = httpContext;
return RazorSliceHttpResultHelpers.ExecuteAsync(razorSlice, httpContext, HtmlEncoder, StatusCode ?? StatusCodes.Status200OK, ContentType);
}

public void Dispose()
{
Expand Down

0 comments on commit 65fde58

Please sign in to comment.