JasperFx / wolverine

Supercharged .NET server side development!
https://wolverinefx.net
MIT License
1.24k stars 135 forks source link

Compilation error in generated Wolverine code for HTTP Endpoint with IFormFile #748

Closed woksin closed 8 months ago

woksin commented 8 months ago

Wolverine 1.18.1 When using a HTTP endpoint for uploading a file using the IFormFile the generated wolverine code fails to compile when the return type is either a side effect or is a tuple containing a side effect. This seems to be consistent with having the return type include a side effect as I have tried many permutations of the method signature including IMessageContext, injected services, a projected aggregate, etc

Screenshot 2024-02-23 at 00 54 53
woksin commented 8 months ago

Generated code when signature was public static (SomeSideEffect, OutgoingMessages) Upload(IFormFile file, ...) here:

// <auto-generated/>
#pragma warning disable
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using Wolverine.Http;
using Wolverine.Marten.Publishing;
using Wolverine.Runtime;

namespace Internal.Generated.WolverineHandlers
{
    // START: POST_api_signingrequests_id_rawdocument
    public class POST_api_signingrequests_id_rawdocument : Wolverine.Http.HttpHandler
    {
        private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
        private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime;
        private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory;

        public POST_api_signingrequests_id_rawdocument(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory) : base(wolverineHttpOptions)
        {
            _wolverineHttpOptions = wolverineHttpOptions;
            _wolverineRuntime = wolverineRuntime;
            _outboxedSessionFactory = outboxedSessionFactory;
        }

        public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
        {
            var object = new object();
            var envelope = new Wolverine.Envelope(object);
            if (!System.Guid.TryParse((string)httpContext.GetRouteValue("id"), out var id))
            {
                httpContext.Response.StatusCode = 404;
                return;
            }

            // Tenant Id detection
            // 1. Detecting tenant using Woksin.Extensions.Tenancy.Context.ITenantContextAccessor using different strategies to resolve the tenant
            var tenantId = await TryDetectTenantId(httpContext);
            if (string.IsNullOrEmpty(tenantId))
            {
                await WriteTenantIdNotFound(httpContext);
                return;
            }

            var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime);
            messageContext.TenantId = tenantId;
            Wolverine.Http.Runtime.RequestIdMiddleware.Apply(httpContext, messageContext);
            // Retrieve header value from the request
            var file = ReadSingleFormFileValue(httpContext);
            await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext);
            var eventStore = documentSession.Events;

            // Loading Marten aggregate
            var eventStream = await eventStore.FetchForWriting<DigitalSigning.Signing.SigningRequests.GettingDetails.SigningRequestDetails>(id, httpContext.RequestAborted).ConfigureAwait(false);

            var result1 = Wolverine.Http.Marten.AggregateAttribute.ValidateAggregateExists<DigitalSigning.Signing.SigningRequests.GettingDetails.SigningRequestDetails>(eventStream);
            // Evaluate whether or not the execution should be stopped based on the IResult value
            if (!(result1 is Wolverine.Http.WolverineContinue))
            {
                await result1.ExecuteAsync(httpContext).ConfigureAwait(false);
                return;
            }

            // The actual HTTP request handler execution
            (var storeRawDocument, var outgoingMessages) = DigitalSigning.Signing.SigningRequests.Documents.DocumentsHandler.Upload(file, eventStream.Aggregate, messageContext, documentSession, messageContext);

            // Placed by Wolverine's ISideEffect policy
            await storeRawDocument.ExecuteAsync(documentSession, envelope, httpContext.RequestAborted).ConfigureAwait(false);

            // Outgoing, cascaded message
            await messageContext.EnqueueCascadingAsync(outgoingMessages).ConfigureAwait(false);

            await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false);

            // Commit any outstanding Marten changes
            await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false);

            // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536
            await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false);

            // Wolverine automatically sets the status code to 204 for empty responses
            if (!httpContext.Response.HasStarted) httpContext.Response.StatusCode = 204;
        }

    }

    // END: POST_api_signingrequests_id_rawdocument

}
woksin commented 8 months ago

Generated code when just removing the side effect from the signature:

// <auto-generated/>
#pragma warning disable
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using Wolverine.Http;
using Wolverine.Marten.Publishing;
using Wolverine.Runtime;

namespace Internal.Generated.WolverineHandlers
{
    // START: POST_api_signingrequests_id_rawdocument
    public class POST_api_signingrequests_id_rawdocument : Wolverine.Http.HttpHandler
    {
        private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
        private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory;
        private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime;

        public POST_api_signingrequests_id_rawdocument(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory, Wolverine.Runtime.IWolverineRuntime wolverineRuntime) : base(wolverineHttpOptions)
        {
            _wolverineHttpOptions = wolverineHttpOptions;
            _outboxedSessionFactory = outboxedSessionFactory;
            _wolverineRuntime = wolverineRuntime;
        }

        public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
        {

            // Tenant Id detection
            // 1. Detecting tenant using Woksin.Extensions.Tenancy.Context.ITenantContextAccessor using different strategies to resolve the tenant
            var tenantId = await TryDetectTenantId(httpContext);
            if (string.IsNullOrEmpty(tenantId))
            {
                await WriteTenantIdNotFound(httpContext);
                return;
            }

            var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime);
            messageContext.TenantId = tenantId;
            if (!System.Guid.TryParse((string)httpContext.GetRouteValue("id"), out var id))
            {
                httpContext.Response.StatusCode = 404;
                return;
            }

            Wolverine.Http.Runtime.RequestIdMiddleware.Apply(httpContext, messageContext);
            // Retrieve header value from the request
            var file = ReadSingleFormFileValue(httpContext);
            await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext);
            var eventStore = documentSession.Events;

            // Loading Marten aggregate
            var eventStream = await eventStore.FetchForWriting<DigitalSigning.Signing.SigningRequests.GettingDetails.SigningRequestDetails>(id, httpContext.RequestAborted).ConfigureAwait(false);

            var result1 = Wolverine.Http.Marten.AggregateAttribute.ValidateAggregateExists<DigitalSigning.Signing.SigningRequests.GettingDetails.SigningRequestDetails>(eventStream);
            // Evaluate whether or not the execution should be stopped based on the IResult value
            if (!(result1 is Wolverine.Http.WolverineContinue))
            {
                await result1.ExecuteAsync(httpContext).ConfigureAwait(false);
                return;
            }

            // The actual HTTP request handler execution
            var outgoingMessages = DigitalSigning.Signing.SigningRequests.Documents.DocumentsHandler.Upload(file, eventStream.Aggregate, messageContext, documentSession, messageContext);

            // Outgoing, cascaded message
            await messageContext.EnqueueCascadingAsync(outgoingMessages).ConfigureAwait(false);

            await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false);

            // Commit any outstanding Marten changes
            await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false);

            // Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536
            await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false);

            // Wolverine automatically sets the status code to 204 for empty responses
            if (!httpContext.Response.HasStarted) httpContext.Response.StatusCode = 204;
        }

    }

    // END: POST_api_signingrequests_id_rawdocument

}
woksin commented 8 months ago

I also noticed another, slightly related problem. As a work around just now I don't return the side effect but I just create the side effect object and execute it manually. However in the sideeffect I do use the document session to store som object to the document store. I also in that method called SaveChangesAsync on the document session. The HTTP endpoint also returns Events. What I noticed is that the events were not published / stored. I debugged a bit and found out that after the events has been appended to the stream and SaveChangesAsync is called then it returns prematurely because it hits the

if (!session._workTracker.HasOutstandingWork())
        return;

and returns from the method without persisting the events.

This was fixed by simply removing the manual call to SaveChangesAsync in the side effect Execute method. I don't know if this is expected or not, but I can certainly not find documentation on that you shouldn't do this.

@jeremydmiller

jeremydmiller commented 8 months ago

@woksin Yeah, sorry, the answer to that one is "don't manually call SaveChangesAsync() if using the transactional middleware"

jeremydmiller commented 8 months ago

I think this was fixed by recent changes to Lamar and should be gone in Wolverine 2.0. I can't reproduce this today.