mardi 9 juillet 2019

Stackoverflow calling aspnet core controller action

I got all my code copied in to a new solution and spent hours tirelessly "fixing" all the breaking change differences to be faced with this ...

info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3] Route matched with {action = "Get", controller = "App"}.     
Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult Get(Microsoft.AspNet.OData.Query.ODataQueryOptions`1[Core.Objects.Entities.CMS.App]) on controller Api.Controllers.AppController (Api).

Process is terminating due to StackOverflowException.

.. The code didn't previously exhibit this behaviour of course and putting a breakpoint in the action does nothing as it isn't getting that far which makes me think it's getting stuck in a loop in the DI container somehow.

Any ideas how to debug this?

Further information ...

Startup.cs

public class Startup
{
    static readonly ILog log = LogManager.GetLogger(typeof(Startup));

    static Config Config;

    public Startup(IConfiguration configuration)
    {
        Config = new Config();
        configuration.Bind(Config);
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpContextAccessor();
        services.AddAuthInfo();
        services.AddSingleton(Config);
        services.AddResponseCompression();
        services.AddMvcCore(options =>
        {
            options.EnableEndpointRouting = false; // TODO: Remove when OData does not causes exceptions anymore
        })
        .AddAuthorization()
        .AddDataAnnotations()
        .AddJsonFormatters(options => options.ReferenceLoopHandling = ReferenceLoopHandling.Ignore)
        .AddCors()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        services.AddOData();

        services.AddDbContext<MembershipDataContext>(options => options.UseSqlServer(Config.Connections["Membership"]));
        services.AddDbContext<CoreDataContext>(options => options.UseSqlServer(Config.Connections["Core"]));
        services.AddDbContext<B2BDataContext>(options => options.UseSqlServer(Config.Connections["B2B"]));

        services.AddScoped<IMembershipDataContext, MembershipDataContext>();
        services.AddScoped<ICoreDataContext, CoreDataContext>();
        services.AddScoped<IB2BDataContext, B2BDataContext>();

        services.AddServices();

        services.AddSingleton(typeof(ICrypto<Signature>), new AesCrypto<Signature>());

        var installers = TypeHelper.GetWebStackAssemblies()
            .SelectMany(a => a.GetTypes()
            .Where(t => !t.IsAbstract && t.GetInterfaces().Any(i => i == typeof(IPackageInstaller))))
            .ToArray();

        var importers = TypeHelper.GetWebStackAssemblies()
            .SelectMany(a => a.GetTypes()
            .Where(t => !t.IsAbstract && t.GetInterfaces().Any(i => i == typeof(IPackageItemImporter))))
            .ToArray();

        installers.ForEach(i => services.AddScoped(typeof(IPackageInstaller), i));
        importers.ForEach(i => services.AddScoped(typeof(IPackageItemImporter), i));

        services.AddSignalR();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        log.Info("Initialising ...");
        app.UseDeveloperExceptionPage();
        app.UseHttpsRedirection();

        app.MapWhen(
            context => Regex.IsMatch(context.Request.Path.ToString(), "^App({[0-9]:[0-9]})/DMS/.*"),
            appBranch => appBranch.UseMiddleware<DMSMiddleware>()
        );

        log.Info("Databases");
        using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
        {
            new DbContext[] {
                serviceScope.ServiceProvider.GetRequiredService<MembershipDataContext>(),
                serviceScope.ServiceProvider.GetRequiredService<CoreDataContext>(),
                serviceScope.ServiceProvider.GetRequiredService<B2BDataContext>()
            }.ForEach(ctx => ctx.Database.Migrate());
        }

        log.Info("WebApi & Odata");
        app.UseMvc(routeBuilder =>
        {
            routeBuilder.EnableDependencyInjection();
            routeBuilder.MapRoute("default", "{controller=Home}/{action=Get}");
        });

        foreach (var builder in new ODataModelBuilder[] { new MembersModelBuilder(), new CoreModelBuilder(), new B2BModelBuilder(), new SearchModelBuilder() })
        {
            var model = builder.Build();
            app.UseOData(model.Context + "Api", builder.GetType().Name.Replace("ModelBuilder", ""), model.EDMModel);
        }

        log.Info("Starting SignalR");
        app.UseSignalR(routeBuilder => {
            routeBuilder.MapHub<NotificationHub>("/Hub/Core");
            routeBuilder.MapHub<NotificationHub>("/Hub/Notification");
            routeBuilder.MapHub<NotificationHub>("/Hub/Workflow");
        });

        log.Info("Initialisation Complete");
    }
}

.. the app hosts a few misc controllers (standard webapi) 4 OData contexts and a set of SignalR hubs.

There's no front end for this it's purely API.

I also added this extension which is used for IServiceCollection ...

static Type[] serviceTypes = TypeHelper.GetWebStackAssemblies()
    .SelectMany(a => a.GetTypes())
    .Where(t => t.ImplementsGenericInterface(typeof(IService<,,>)))
    .Where(t => !t.IsAbstract)
    .ToArray();

public static void AddServices(this IServiceCollection services)
{
    foreach (var tCtx in TypeHelper.GetContextTypes())
    {
        var entityTypes = TypeHelper.GetEntityTypesFor(tCtx);
        var tUser = tCtx.GetInterfaces().First(i => i.Name == "IDataContext`1").GenericTypeArguments[0];
        var contextInterfaceBase = TypeHelper.GetWebStackAssemblies()
            .SelectMany(a => a.GetTypes())
            .First(t => t.IsInterface && t.Name.StartsWith($"I{tCtx.Name.Replace("DataContext", "")}Service"));

        entityTypes.ForEach(entity =>
        {
            var types = new[] { entity, entity.GetIdProperty().PropertyType };
            var result = serviceTypes
                .Where(t => t.Name == entity.Name + "Service")
                .FirstOrDefault(t => t.GetInterfaces().FirstOrDefault(ii => ii.Name.StartsWith($"I{tCtx.Name.Replace("DataContext", "")}Service")) != null);

            if (result == null)
                result = serviceTypes.First(t => t.Name.StartsWith($"{tCtx.Name.Replace("DataContext", "")}Service")).MakeGenericType(types);

            // 2. For most specific implemntation extract interfaces in the stacked naming form
            var interfaces = result.GetInterfaces()
                .Where(ii => ii.Name.StartsWith($"I{tCtx.Name.Replace("DataContext", "")}Service") || ii.Name == $"I{entity.Name}Service")
                .ToArray();

            // 3. For each interface add binding
            interfaces.ForEach(i => services.AddScoped(i, result));
        });
    }
}

The idea here is to pick the "most concrete implementation" of an interface stack that looks something like ...

IService<T, TKey, TUser>
IContextService<T, TKey> : IService<T, TKey, ContextUser>
IFooService : IContextService<Foo, int> 

... it then is supposed to register the most concrete implementation against all of the found interfaces that are relevant generically .. this is essentially a re-implementation of Ninject's convention based bindings that look something like this ...

var kernel = new StandardKernel();

kernel.Bind(x => x.FromAssembliesMatching("Core.*")
    .SelectAllClasses()
    .BindAllInterfaces()
);

... Is it possible that this code would result in some weird circular referencing issue?

I can load the application and do a simple get request on the RootControllers default Get method but as soon as I hit the OData controllers (any of them) I get a stack overflow and the app crashes giving the exception details in the VS output window.

I have this issue whatever OData controller I hit but as an example my AppController found at "~/Core/App" is contructed like this ...

public AppController(IAppService service, IFolderService dms, IFileService files, IAuthInfo auth) : base(service, auth) { Dms = dms; Files = files; }

... If I follow the dependency graph down by looking through the code I don't see anything like say an IFooService that requires an IBarService which in turn Requires an IFooService nor are there any dependencies that do that in larger circles.

I had considered an issue in the DbContext but that should at least allow me to put a breakpoint in the controller and work my down to the issue with the debugger surely?





Aucun commentaire:

Enregistrer un commentaire