DamianEdwards / RazorSlices

Lightweight Razor-based templates for ASP.NET Core without MVC, Razor Pages, or Blazor.
MIT License
312 stars 13 forks source link

Razor slice with name '/Slices/Home.cshtml' was found but it's model type is DateTime. #23

Closed jchannon closed 1 year ago

jchannon commented 1 year ago

Just found this project and was considering adding it to a Carter extension package so you can content negotiate (which Carter already supports) HTML from the Carter/Minimal API routes.

Have followed the instructions but get the titled exception.

I also tried it with my own POCO as the model and get the same error

Any ideas?

System.InvalidOperationException: Razor slice with name '/Slices/Home.cshtml' was found but it's model type is DateTime.
   at RazorSlices.RazorSlice.ResolveSliceFactoryImpl[TModel](String sliceName) in /_/src/RazorSlices/RazorSlice.ResolveAndCreate.cs:line 445
   at RazorSlices.RazorSlice.ResolveSliceFactory[TModel](String sliceName) in /_/src/RazorSlices/RazorSlice.ResolveAndCreate.cs:line 292
   at RazorSlices.RazorSlice.Create[TModel](String sliceName, TModel model) in /_/src/RazorSlices/RazorSlice.ResolveAndCreate.cs:line 353
   at RazorSlices.RazorSlice.CreateHttpResult[TModel](String sliceName, TModel model, Int32 statusCode) in /_/src/RazorSlices/RazorSlice.CreateHttpResult.cs:line 58
   at Microsoft.AspNetCore.Http.HttpResultsExtensions.RazorSlice[TModel](IResultExtensions resultExtensions, String sliceName, TModel model, Int32 statusCode) in /_/src/RazorSlices/HttpResultsExtensions.cs:line 53
   at Carter.HtmlNegotiator.Razor.RazorResponseNegotiator.Handle(HttpRequest req, HttpResponse res, Object model, CancellationToken cancellationToken) in /Users/jonathan/Projects/Carter.HtmlNegotiator/src/Razor/RazorResponseNegotiator.cs:line 18
jchannon commented 1 year ago

Aha, think I found the issue and it's because in my extension I have:

Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken) so you can pass any model instance to it so I specified object but in ResolveSliceFactoryImpl when it tries to compare the sliceDefinition.Factory to SliceFactory<TModel> it fails because DateTime is not object.

DamianEdwards commented 1 year ago

Can you make your method generic? Also share a minimal repro?

jchannon commented 1 year ago

Yeah looking into adding a IResponseNegotiator<T> interface into Carter

jchannon commented 1 year ago

Has proven to be a PITA due to libs not being able to resolve open generics, for example here https://github.com/CarterCommunity/Carter/blob/main/src/Carter/Response/ResponseExtensions.cs#L26 I want to resolve IResponseNegotiator<>

DamianEdwards commented 1 year ago

Why not have a generic version of this method?

jchannon commented 1 year ago

That’s what I was going to do but I’d still have to resolve the correct generic interface

On Tue, 18 Apr 2023 at 20:42, Damian Edwards @.***> wrote:

Why not have a generic version of this method?

— Reply to this email directly, view it on GitHub https://github.com/DamianEdwards/RazorSlices/issues/23#issuecomment-1513704835, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAZVJSMBKVKG7UP5RE5OELXB3VDTANCNFSM6AAAAAAXC236MM . You are receiving this because you authored the thread.Message ID: @.***>

DamianEdwards commented 1 year ago

Right, ultimately the "last mile" is a generic invocation so at some point you have to bridge the gap from late-bound matching to actual generic invocation. Pasting this in case it helps:

using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(typeof(IResponseNegotiator<>), typeof(MyNegotiator<>));

var app = builder.Build();

app.MapGet("/", (HttpResponse response) => response.Negotiate(DateTime.Now));
app.MapGet("/anontype", (HttpResponse response) => response.Negotiate(new { message = "I'm an anonymous type" }));

app.Run();

class MyNegotiator<TModel> : IResponseNegotiator<TModel>
{
    public bool CanHandle(MediaTypeHeaderValue mediaType) => true;

    public Task Handle(HttpRequest request, HttpResponse response, TModel viewModel, CancellationToken cancellationToken)
    {
        response.StatusCode = 200;
        response.ContentType = "text/plain";
        return response.WriteAsync(GenericThings.GetMessage(viewModel));
    }

    public Task Handle(HttpRequest request, HttpResponse response, object viewModel, CancellationToken cancellationToken)
    {
        response.StatusCode = 200;
        response.ContentType = "text/plain";
        return response.WriteAsync(GenericThings.GetMessage(viewModel));
    }
}

static class GenericThings
{
    public static string GetMessage<TModel>(TModel model)
    {
        return $"Model is a {typeof(TModel).Name} and its value is '{model?.ToString() ?? "[null]"}'.";
    }
}

public interface IResponseNegotiator
{
    bool CanHandle(MediaTypeHeaderValue mediaType) => false;
    Task Handle(HttpRequest request, HttpResponse response, object viewModel, CancellationToken cancellationToken);
}

public interface IResponseNegotiator<TModel> : IResponseNegotiator
{
    Task Handle(HttpRequest request, HttpResponse response, TModel viewModel, CancellationToken cancellationToken);
}

static class HttpResponseExtensions
{
    public static Task Negotiate<TModel>(this HttpResponse response, TModel viewModel, CancellationToken cancellationToken = default)
    {
        var responseNegotiatorType = typeof(IResponseNegotiator<>).GetGenericTypeDefinition().MakeGenericType(typeof(TModel));
        var negotiators = response.HttpContext.RequestServices.GetServices(responseNegotiatorType).OfType<IResponseNegotiator>().ToList();
        IResponseNegotiator<TModel>? negotiator = null;

        MediaTypeHeaderValue.TryParseList(response.HttpContext.Request.Headers["Accept"], out var accept);
        if (accept != null)
        {
            var ordered = accept.OrderByDescending(x => x.Quality ?? 1);

            foreach (var acceptHeader in ordered)
            {
                var candidate = negotiators.FirstOrDefault(x => x.CanHandle(acceptHeader));
                if (candidate != null)
                {
                    negotiator = (IResponseNegotiator<TModel>)candidate;
                    break;
                }
            }
        }

        if (negotiator == null)
        {
            negotiator = (IResponseNegotiator<TModel>)negotiators.First(x => x.CanHandle(new MediaTypeHeaderValue("application/json")));
        }

        return negotiator.Handle(response.HttpContext.Request, response, viewModel, cancellationToken);
    }
}
jchannon commented 1 year ago

Thanks, it actually turned out to be a bit easier in that I could change the existing IResponseNegotiator to be generic =)

Thanks for the help