Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UnboundOperation throws an exception #764

Open
Mishu opened this issue Jul 26, 2024 · 10 comments
Open

UnboundOperation throws an exception #764

Mishu opened this issue Jul 26, 2024 · 10 comments

Comments

@Mishu
Copy link

Mishu commented Jul 26, 2024

If you create an [UnboundOperation] (by the way Operation is not working as it's shown in the documentation.) and you have one or 2 strings as parameters you will get this exception:

System.InvalidCastException: Unable to cast object of type 'Microsoft.OData.Edm.EdmPrimitiveTypeReference' to type 'Microsoft.OData.Edm.IEdmStringTypeReference'.
  at Microsoft.OpenApi.OData.Generator.OpenApiEdmTypeSchemaGenerator.CreateSchema(ODataContext context, IEdmPrimitiveTypeReference primitiveType)
  at Microsoft.OpenApi.OData.Generator.OpenApiEdmTypeSchemaGenerator.CreateEdmTypeSchema(ODataContext context, IEdmTypeReference edmTypeReference)
  at Microsoft.OpenApi.OData.Generator.OpenApiParameterGenerator.CreateParameters(ODataContext context, IEdmFunction function, IDictionary`2 parameterNameMapping)
  at Microsoft.OpenApi.OData.Generator.OpenApiParameterGenerator.CreateParameters(ODataContext context, IEdmFunctionImport functionImport)
  at Microsoft.OpenApi.OData.Operation.EdmFunctionImportOperationHandler.SetParameters(OpenApiOperation operation)
  at Microsoft.OpenApi.OData.Operation.OperationHandler.CreateOperation(ODataContext context, ODataPath path)
  at Microsoft.OpenApi.OData.PathItem.PathItemHandler.AddOperation(OpenApiPathItem item, OperationType operationType)
  at Microsoft.OpenApi.OData.PathItem.OperationImportPathItemHandler.SetOperations(OpenApiPathItem item)
  at Microsoft.OpenApi.OData.PathItem.PathItemHandler.CreatePathItem(ODataContext context, ODataPath path)
  at Microsoft.OpenApi.OData.Generator.OpenApiPathItemGenerator.CreatePathItems(ODataContext context)
  at Microsoft.OpenApi.OData.Generator.OpenApiPathsGenerator.CreatePaths(ODataContext context)
  at Microsoft.OpenApi.OData.Generator.OpenApiDocumentGenerator.CreateDocument(ODataContext context)
  at Microsoft.OpenApi.OData.EdmModelOpenApiExtensions.ConvertToOpenApi(IEdmModel model, OpenApiConvertSettings settings)
  at Microsoft.Restier.AspNetCore.Swagger.RestierSwaggerProvider.GetSwagger(String documentName, String host, String basePath)\r\n   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
  at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
  at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
  at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.<Invoke>g__Awaited|10_0(ExceptionHandlerMiddlewareImpl middleware, HttpContext context, Task task)

This only happens for the Swagger generation. The actual queries works fine.

Assemblies affected

Microsoft.Restier.AspNetCore Version="1.1.1"
Microsoft.Restier.AspNetCore.Swagger Version="1.1.1"
Microsoft.Restier.Core Version="1.1.1"
Microsoft.Restier.EntityFrameworkCore Version="1.1.1"
Swashbuckle.AspNetCore Version="6.6.2"
Swashbuckle.AspNetCore.SwaggerGen Version="6.6.2"
Swashbuckle.AspNetCore.SwaggerUI Version="6.6.2"

Reproduce steps

As said in this comment: Issue #720

Expected result

Correctly generate the OpenApi json and Swagger UI.

Actual result

An exception is thrown when accessing the Swagger endpoint.

Additional details

If I make them BoundOperation the error goes away but they are completely ignored in Swagger.

Any thoughts?
Thank you!

@cilerler
Copy link
Contributor

Important

Please provide a minimal repository that reproduces the issue.

I am using .NET Core 8 with Restier 1.1.1, and Swagger is working correctly.

Some signatures I have...

[Microsoft.Restier.AspNetCore.Model.UnboundOperation]
public IQueryable<EmailAddress> GetEmailsToValidate()
{
	// ...
	return query.AsQueryable();
}

[Microsoft.Restier.AspNetCore.Model.UnboundOperation]
public async Task<bool> ValidateBulkAsync()
{
	// ...
	return isFinalRecord;
}

@Mishu
Copy link
Author

Mishu commented Jul 26, 2024

You need to add string parameters to the operations.

@cilerler
Copy link
Contributor

You can not. You have to define it as ComplexType.
Here is a workaround.

[Microsoft.Restier.AspNetCore.Model.UnboundOperation(OperationType = Microsoft.Restier.AspNetCore.Model.OperationType.Action)]
public IQueryable<EmailAddress> GetEmailsToValidate(StringRequest input)
{
	// ...
	return query.AsQueryable();
}

public class CustomModelExtender : IModelBuilder
{
    public IModelBuilder InnerHandler { get; set; }

    public IEdmModel GetModel(ModelContext context)
    {
	    IEdmModel model = InnerHandler.GetModel(context);
    
	    var modelBuilder = new ODataConventionModelBuilder();
	    modelBuilder.ComplexType<StringResponse>();
    
	    return modelBuilder.GetEdmModel();
    }
}

public class StringRequest
{
	public string Value { get; set; }
}

public static IHostApplicationBuilder AddRestierInternal(this IHostApplicationBuilder builder)
{
          builder.Services.AddRestier(b =>
		          {
			          // This delegate is executed after OData is added to the container.
			          b.AddRestierApi<ApiController>(routeServices =>
			          {
			              // ...
                                      routeServices.AddChainedService<IModelBuilder, CustomModelExtender>();
                                      // ...
			          }
		          });
}

@Mishu
Copy link
Author

Mishu commented Jul 29, 2024

Hello,
Thank you for your answer!
It's working now.
One quick question, do you happen to have an example of how to call the function? The url for it?

This is how I'm calling it:

https://localhost:8762/odata/GetEmailsToValidate(input =@ input)?@ input ={"Value":"test"}

And when I do, I get this exception:

{
    "error": {
        "code": "",
        "message": "Value cannot be null. (Parameter 's')",
        "details": [],
        "innererror": {
            "message": "Value cannot be null. (Parameter 's')",
            "type": "System.ArgumentNullException",
            "stacktrace": "   at void ArgumentNullException.Throw(string paramName)\r\n   at byte[] System.Text.Encoding.GetBytes(string s)\r\n   at object Microsoft.AspNet.OData.Formatter.ODataModelBinderConverter.ConvertResourceOrResourceSet(object oDataValue, IEdmTypeReference edmTypeReference, ODataDeserializerContext readContext)\r\n   at object Microsoft.AspNet.OData.Formatter.ODataModelBinderConverter.Convert(object graph, IEdmTypeReference edmTypeReference, Type clrType, string parameterName, ODataDeserializerContext readContext, IServiceProvider requestContainer)\r\n   at object Microsoft.Restier.AspNetCore.Formatter.DeserializationHelpers.ConvertValue(object odataValue, string parameterName, Type expectedReturnType, IEdmTypeReference propertyType, IEdmModel model, HttpRequest request, IServiceProvider serviceProvider)\r\n   at async Task<IQueryable> Microsoft.Restier.AspNetCore.Operation.RestierOperationExecutor.ExecuteOperationAsync(OperationContext context, CancellationToken cancellationToken)\r\n   at async Task<IActionResult> Microsoft.Restier.AspNetCore.RestierController.Get(CancellationToken cancellationToken)\r\n   at async ValueTask<IActionResult> Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+TaskOfIActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments)\r\n   at async Task Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()+Awaited(?)\r\n   at async Task Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()+Awaited(?)\r\n   at void Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\r\n   at Task Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)\r\n   at Task Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()\r\n   at async Task Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextExceptionFilterAsync()+Awaited(?)"
        }
    }
}

Thanks!

@cilerler
Copy link
Contributor

cilerler commented Jul 29, 2024

To test the endpoint, use a POST request.

For VSCode RestClient or Visual Studio Rest Client:
You can create a .http file then copy and paste the following text:

###
# @name GetEmailsToValidate
POST {{baseUrl}}/odata/GetEmailsToValidate HTTP/1.1
Content-Type: application/json
Cache-Control: no-cache

{
  "input": {
    "@odata.type": "#MyCompany.MyModels.Dto.Request.StringRequest",  // <== correct this checking it from the `/odata/$metadata`
    "Value": "[email protected]"
  }
}

You may need to adjust the formatting accordingly for Postman or other tools

@Mishu
Copy link
Author

Mishu commented Jul 29, 2024

Thanks, thats for an Action, for a function with GET?
I've tried adding type annotation in the request, same error.
If I put them as string type, everything works ok, except Swagger.

@cilerler
Copy link
Contributor

A simple GET request is unlikely to work because it requires @odata.type and an explicit declaration of the object name. On the client side, if you are using ODataClient, it handles these details automatically and uses a POST request based on the cases I've observed.

@Mishu
Copy link
Author

Mishu commented Jul 29, 2024

Oh, ok, thanks!
I'll try with ODataClient from the Angular App. I've tried POST, I got Method not allowed. I'm adding the @odata.type, not really sure on the "explicit declaration of the object" though.

I'll try that and come back with results.
Thank you so much for the assistance so far!

@Mishu
Copy link
Author

Mishu commented Jul 30, 2024

No matter how I try to call this function with the String Parameter is not working, even with the ODataClient.

@cilerler
Copy link
Contributor

cilerler commented Jul 30, 2024

  1. Update the part below.

Important

{
  "input": { <== update this
    "@odata.type": "#MyCompany.MyModels.Dto.Request.StringRequest",  // <== correct this checking it from the `/odata/$metadata`
    "Value": "[email protected]"
  }
}
  1. Make sure your HTTP request is working first and then try it from the application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants