SteveDunn / Vogen

A semi-opinionated library which is a source generator and a code analyser. It Source generates Value Objects
Apache License 2.0
781 stars 45 forks source link

It is not fully compatible with ASP.NET 8 #559

Closed arteny closed 4 months ago

arteny commented 6 months ago

Describe the feature

Only case you described in doc is adding full route for each controller's method. But it is not very useful case. Most of cases route generated automatically for common rules for controller and method has only [HttpGet] attribute. But it is not works in this case. Also it is not working for minimal API mechanism of Asp.Net For instance, the following code

app.MapGet("test/{eventId}", (EventId eventId) => Results.Ok(eventId));

generates the error: error ASP0020: Parameter 'eventId' of type EventId should define a bool TryParse(string, IFormatProvider, out EventId) method, or implement IParsable<EventId> (https://aka.ms/aspnet/analyzers) which is fixing by adding ValueObject's method:

    public static bool TryParse(string value, IFormatProvider provider, out EventId eventId)
    {
        eventId= From(value);
        return true;
    }

Could you please to add more compatiblity with Asp.Net?

SteveDunn commented 4 months ago

Thanks for the bug report! This is fixed in #570 - please let me know how you get on, which will be available in v4.0.0, hopefully later today.

fbestfriends commented 4 months ago

@SteveDunn it is working now for this case, thanks. Only what is not working - it is parameters for Azure Functions. If is it possible to fix this, will be great. Code:

[ValueObject<string>(conversions: Conversions.EfCoreValueConverter | Conversions.Default)]
public partial struct MarketId{}

        [Function("Function1")]
        public async Task<MarketId> Run1([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, 
            MarketId marketId)
        {
            return marketId;
        }

Test call: http://localhost:7129/api/Function1?marketId=s

Result:

[2024-04-23T20:10:57.045Z] Executing 'Functions.Function1' (Reason='This function was programmatically called via the host APIs.', Id=84494f7d-8bef-4fcf-ab56-ba059fa68f05)
[2024-04-23T20:10:57.064Z] Function 'Function1', Invocation id '84494f7d-8bef-4fcf-ab56-ba059fa68f05': An exception was thrown by the invocation.
[2024-04-23T20:10:57.064Z] Result: Function 'Function1', Invocation id '84494f7d-8bef-4fcf-ab56-ba059fa68f05': An exception was thrown by the invocation.
Exception: Microsoft.Azure.Functions.Worker.FunctionInputConverterException: Error converting 1 input parameters for Function 'Function1': Cannot convert input parameter 'marketId' to type 'Domain.Enums.MarketId' from type 'System.String'. Error:System.Text.Json.JsonException: 's' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
[2024-04-23T20:10:57.065Z]  ---> System.Text.Json.JsonReaderException: 's' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.
[2024-04-23T20:10:57.065Z] Executed 'Functions.Function1' (Failed, Id=84494f7d-8bef-4fcf-ab56-ba059fa68f05, Duration=21ms)
[2024-04-23T20:10:57.065Z]    at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
[2024-04-23T20:10:57.066Z] System.Private.CoreLib: Exception while executing function: Functions.Function1. System.Private.CoreLib: Result: Failure
Exception: Microsoft.Azure.Functions.Worker.FunctionInputConverterException: Error converting 1 input parameters for Function 'Function1': Cannot convert input parameter 'marketId' to type 'Domain.Enums.MarketId' from type 'System.String'. Error:System.Text.Json.JsonException: 's' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
[2024-04-23T20:10:57.066Z]    at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
[2024-04-23T20:10:57.067Z]  ---> System.Text.Json.JsonReaderException: 's' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.
[2024-04-23T20:10:57.067Z]    at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first)
[2024-04-23T20:10:57.067Z]    at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
[2024-04-23T20:10:57.068Z]    at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
[2024-04-23T20:10:57.068Z]    at System.Text.Json.Utf8JsonReader.Read()
[2024-04-23T20:10:57.069Z]    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
[2024-04-23T20:10:57.069Z]    --- End of inner exception stack trace ---
[2024-04-23T20:10:57.069Z]    at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, JsonReaderException ex)
[2024-04-23T20:10:57.070Z]    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
[2024-04-23T20:10:57.070Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.ContinueDeserialize(ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack)
[2024-04-23T20:10:57.068Z]    at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
[2024-04-23T20:10:57.071Z]    at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first)
[2024-04-23T20:10:57.071Z]    at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
[2024-04-23T20:10:57.071Z]    at System.Text.Json.Utf8JsonReader.Read()
[2024-04-23T20:10:57.070Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
[2024-04-23T20:10:57.072Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken)
[2024-04-23T20:10:57.071Z]    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
[2024-04-23T20:10:57.072Z]    at Microsoft.Azure.Functions.Worker.Converters.JsonPocoConverter.GetConversionResultFromDeserialization(Byte[] bytes, Type type) in D:\a\_work\1\s\src\DotNetWorker.Core\Converters\JsonPocoConverter.cs:line 66
[2024-04-23T20:10:57.073Z]    --- End of inner exception stack trace ---
[2024-04-23T20:10:57.073Z]    at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, JsonReaderException ex)
[2024-04-23T20:10:57.073Z]    at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
[2024-04-23T20:10:57.073Z]    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 94
[2024-04-23T20:10:57.074Z]    at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 46
[2024-04-23T20:10:57.074Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2024-04-23T20:10:57.074Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.ContinueDeserialize(ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack)
[2024-04-23T20:10:57.075Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
[2024-04-23T20:10:57.075Z]    at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken)
[2024-04-23T20:10:57.076Z]    at Microsoft.Azure.Functions.Worker.Converters.JsonPocoConverter.GetConversionResultFromDeserialization(Byte[] bytes, Type type) in D:\a\_work\1\s\src\DotNetWorker.Core\Converters\JsonPocoConverter.cs:line 66
[2024-04-23T20:10:57.076Z]    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 94
[2024-04-23T20:10:57.076Z]    at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 46
[2024-04-23T20:10:57.077Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2024-04-23T20:10:57.075Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77
Stack:    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 94
[2024-04-23T20:10:57.077Z]    at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 46
[2024-04-23T20:10:57.077Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77
[2024-04-23T20:10:57.078Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2024-04-23T20:10:57.078Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77.
[2024-04-23T20:10:57.078Z]    at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(InvocationRequest request) in D:\a\_work\1\s\src\DotNetWorker.Grpc\Handlers\InvocationHandler.cs:line 88
Stack:    at Microsoft.Azure.Functions.Worker.Context.Features.DefaultFunctionInputBindingFeature.BindFunctionInputAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Context\Features\DefaultFunctionInputBindingFeature.cs:line 94
[2024-04-23T20:10:57.079Z]    at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 46
[2024-04-23T20:10:57.079Z]    at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13
[2024-04-23T20:10:57.080Z]    at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77
[2024-04-23T20:10:57.080Z]    at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(InvocationRequest request) in D:\a\_work\1\s\src\DotNetWorker.Grpc\Handlers\InvocationHandler.cs:line 88.
SteveDunn commented 4 months ago

From what I can see, for Azure Function parameters, the value object needs to be decorated with an attribute:

[ValueObject<string>(conversions: Conversions.Default)]
[InputConverter(typeof(NameInputConverter))]
public partial struct Name
{
}

The converter looks like this:

public class NameInputConverter : IInputConverter
{
    public ValueTask<ConversionResult> ConvertAsync(ConverterContext context)
    {
        if (context.TargetType == typeof(Name))
        {
            return ValueTask.FromResult(ConversionResult.Success(Name.From(context.Source as string)));
        }

        return ValueTask.FromResult(ConversionResult.Unhandled());
    }
}

This makes this function work correctly:

    public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req, Name name)

Vogen could generate this attribute, much like it does for type converters. However, the difficulty with this approach arises when value objects wrap primitives such as DateTime, DateTimeOffset etc. It could use the exact same assumptions that the type converters use for dates and times ('round trip' kind, with invariant culture).

I'll keep this idea in mind though. On one hand, it will be useful for the people who use Vogen's Value Objects as Azure function parameters, but on the other hand, it's a fairly big addition for what I guess is a very small amount of users.

fbestfriends commented 4 months ago

I see. Anyway thank you for workaround, I'll try to use it.

arteny commented 3 months ago

It is still not well working with last version. Following code for asp.net conroller method:

        [HttpGet, Route("{marketId}")]
        public IActionResult CurrentOrders([FromRoute] MarketId marketId)

where

[ValueObject]
public partial class MarketId { } 

works in debug mode, but generate following error when publish: Failed to update your API in Azure (Status code: BadRequest).

So to fix this i need to return for using base layer int type and later don't forget to convert it to ValueObject when using in EF query:

        [HttpGet, Route("{marketId}")]
        public IActionResult CurrentOrders([FromRoute] int marketId)
SteveDunn commented 3 months ago

@arteny - this might be Azure specific. I added a controller to the WebApplicationConsumer project and it worked as expected:

image