graphql-dotnet / authorization

A toolset for authorizing access to graph types for GraphQL .NET.
MIT License
157 stars 38 forks source link

Confusion with extension methods - looking for a more complete example #25

Closed ddecours closed 3 years ago

ddecours commented 5 years ago

I am trying to add authorization logic to an ASP.NET core application utilizing graphql-dotnet with your authorization strategy (field level, type level, etc.).

You reference some examples by pasting in a few functions (which I assume are extensions), such as UseGraphQLWithAuth and AddGraphQLWithAuth. I found in the examples where AddGraphQLWithAuth is defined in authorization/src/Harness/GraphQLAuthExtension, but could not locate UseGraphQLWithAuth. Is that something we would add or is it now baked into the graphql-dotnet/server package?

Also, I've been reading issue 502 (https://github.com/graphql-dotnet/graphql-dotnet/issues/502) and wondering if this information is outdated? Is the intention that we define our own middleware code or is that logic now part of graphql-dotnet server? I ask because I've defined my own middleware that works for handling the graphql request, but (for reasons unknown) does not honor the authorization rules.

I really appreciate the graphql-dotnet stuff - I just wish I could find a a current/complete example showing this stuff in action. Any direction appreciated!

Key files below:

ghc.GQLAPI.csproj

netcoreapp2.1 All All *** Startup.cs *** using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using AutoMapper; using GraphQL.Server; using GraphQL.Http; using GraphQL; using GraphQL.Types; using ghc.GQLAPI.Helpers; using ghc.Data; using ghc.Data.Interfaces; using ghc.Data.Repositories; using ghc.GQLAPI.Models; using ghc.GQLAPI.Middleware; using ghc.Model.Entities; using GraphQL.Validation; namespace ghc.GQLAPI { public class Startup { //TODO: user user secrets to avoid storing salt key in clear text private IHostingEnvironment _env; public IConfiguration _configuration { get; } public Startup(IConfiguration configuration, IHostingEnvironment env) { _configuration = configuration; _env = env; var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); builder.AddEnvironmentVariables(); _configuration = builder.Build(); } // ===DEVELOPMENT // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureDevelopmentServices(IServiceCollection services) { // Allow for other classes to access the configuration via dependency injection services.AddSingleton(_configuration); // *** Configure security/identity **** // TODO: This is a very weak password strength policy - strengthen for prod IdentityBuilder builder = services.AddIdentityCore(opt => { opt.Password.RequireDigit = false; opt.Password.RequiredLength = 4; opt.Password.RequireNonAlphanumeric = false; opt.Password.RequireUppercase = false; }); builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services); builder.AddEntityFrameworkStores(); // tell identity system to store security info in the entity framework data store builder.AddRoleValidator>(); builder.AddRoleManager>(); builder.AddSignInManager>(); // Add authentication strategy var key = Encoding.ASCII.GetBytes(_configuration.GetSection("AppSettings:TokenKey").Value); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false }; }); // Allow us to reference the data seeding process in the configure method below services.AddTransient(); // Specify the DB Context (SqlServer) services.AddDbContext(options => options.UseSqlServer(_configuration["ConnectionStrings:GHCOpsDB"], b => b.MigrationsAssembly("ghc.Migrations.Development")) .ConfigureWarnings( warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning)) ); // Add support for cross origin resource support services.AddCors(); // Add support for mapping between domain objects and data transfer objects services.AddAutoMapper(); // BEGIN: GRAPHQL $$$$$$$$$$$$$$$$$$$ // Extension method to allow for GraphQL Authorization logic services.AddGraphQLAuth(options => { options.AddPolicy("AdminPolicy", p => p.RequireClaim("role", "Administrator")); }); // Add GraphQL services and configure options services.AddGraphQL(options => { options.EnableMetrics = true; options.ExposeExceptions = _env.IsDevelopment(); }) .AddWebSockets() // Add required services for web socket support .AddDataLoader() // Add required services for DataLoader support .AddUserContextBuilder(httpContext => new GraphQLUserContext { User = httpContext.User }); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); // had to fully qualify since this conflicts with existing asp.net framework type services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // Do note move these two lines above the graphql services var sp = services.BuildServiceProvider(); services.AddSingleton(new GHCSchema(new FuncDependencyResolver(type => sp.GetService(type)))); // END: GRAPHQL $$$$$$$$$$$$$$$$$$$$$ // Add Support for Model View Controller Framework services.AddMvc(options => { var mustBeAuthenticatedPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); options.Filters.Add(new AuthorizeFilter(mustBeAuthenticatedPolicy)); }) .SetCompatibilityVersion(CompatibilityVersion.Version_2_1) .AddJsonOptions(opt => opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, Seed seeder) { Console.WriteLine("========================================="); Console.WriteLine("Environment: " + (env.EnvironmentName).ToUpper()); Console.WriteLine("========================================="); // Ensures migrations have been run or creates the DB from scratch if not found UpdateDatabase(app); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); } else { // Establish a global exception handler in case we are not in Development mode app.UseExceptionHandler(builder => { builder.Run(async context => { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; var error = context.Features.Get(); if (error != null) { context.Response.AddApplicationError(error.Error.Message); await context.Response.WriteAsync(error.Error.Message); } }); }); } // Cross Origin Resource - Order matters - this must come before UseMvc() // TODO: Tighten up security here app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials()); app.UseWebSockets(); // this is required for websockets support at the ASP.NET CORE level app.UseGraphQL("/graphqlmw"); app.UseGraphiQl("/graphiql"); // must come before app.UseMvc()!! app.UseAuthentication(); app.UseMvcWithDefaultRoute(); } private static void UpdateDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices .GetRequiredService() .CreateScope()) { using (var context = serviceScope.ServiceProvider.GetService()) { context.Database.Migrate(); } } } } } ``` *** GraphQLAuthExtensions *** using System; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using GraphQL.Authorization; using GraphQL.Validation; using Microsoft.AspNetCore.Builder; using System.Threading.Tasks; using ghc.GQLAPI.Middleware; namespace ghc.GQLAPI.Helpers { public static class GraphQLAuthExtensions { public static void AddGraphQLAuth(this IServiceCollection services, Action configure) { services.TryAddSingleton(); services.TryAddSingleton(); services.AddTransient(); services.TryAddSingleton(s => { var authSettings = new AuthorizationSettings(); configure(authSettings); return authSettings; }); } } } *** ValueType.cs with Authorization specs for a simple type*** using ghc.Model.Entities; using GraphQL.Authorization; using GraphQL.Types; namespace ghc.GQLAPI.Models { public class ValueType : ObjectGraphType { public ValueType() { // this.AuthorizeWith("AdminPolicy"); // this can be used to protect the entire type Name = "Value"; Field(x => x.Id).Description("The Value Id"); Field(x => x.Created).Description("Date & Time item was created"); Field("Name", "This is the name").AuthorizeWith("AdminPolicy"); } } } *** GraphQLMiddleware.cs - currently not using since I could not get the code to honor authorization rules *** using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using GraphQL; using GraphQL.Http; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using GraphQL.Types; using Microsoft.AspNetCore.Authorization; using Newtonsoft.Json.Linq; using ghc.GQLAPI.Helpers; using System.Net; namespace ghc.GQLAPI.Middleware { // DDD: This middleware is redundant with the graphqlcontroller, not sure if I should use this code or the controller. public class GraphQLMiddleware { private readonly RequestDelegate _next; private readonly GraphQLSettings _settings; private readonly IDocumentExecuter _executor; private readonly IDocumentWriter _writer; public GraphQLMiddleware(RequestDelegate next, IDocumentWriter writer, IDocumentExecuter executor, GraphQLSettings settings = null) { System.Diagnostics.Debugger.Break(); Console.WriteLine("===================================== INITIALIZING GQL middleware ====================================="); _next = next; _writer = writer; _executor = executor; _settings = settings; } public async Task InvokeAsync(HttpContext httpContext, ISchema schema) { Console.WriteLine("======== Calling GQL middleware"); if (!IsGraphQLRequest(httpContext)) { await _next(httpContext); return; } await ExecuteAsync(httpContext, schema); } private async Task ExecuteAsync(HttpContext httpContext, ISchema schema) { var request = Deserialize(httpContext.Request.Body); if (request.Query == null) { throw new ArgumentNullException(nameof(request.Query)); } var response = await _executor.ExecuteAsync(options => { options.Schema = schema; options.Query = request.Query; options.OperationName = request.OperationName; options.Inputs = request.Variables.ToInputs(); options.UserContext = _settings.BuildUserContext?.Invoke(httpContext); }).ConfigureAwait(false); if (response.Errors?.Count > 0) { var errors = WriteErrors(response); ExecutionResult executionResult = new ExecutionResult { Errors = errors }; await WriteResponseAsync(httpContext, executionResult, (int)HttpStatusCode.BadRequest); } else { await WriteResponseAsync(httpContext, response, (int)HttpStatusCode.OK); } } private bool IsGraphQLRequest(HttpContext context) { return context.Request.Path.StartsWithSegments(_settings.Path) && string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase); } private async Task WriteResponseAsync(HttpContext httpContext, ExecutionResult result, int httpStatusCode) { var json = new DocumentWriter(indent: true).Write(result); httpContext.Response.StatusCode = httpStatusCode; httpContext.Response.ContentType = _settings.ResponseContentType; await httpContext.Response.WriteAsync(json); } private ExecutionErrors WriteErrors(ExecutionResult result) { var errors = new ExecutionErrors(); foreach (var error in result.Errors) { var ex = new ExecutionError(error.Message); if (error.InnerException != null) { ex = new ExecutionError(error.Message, error.InnerException); } errors.Add(ex); } return errors; } public static T Deserialize(Stream s) { using (var reader = new StreamReader(s)) using (var jsonReader = new JsonTextReader(reader)) { var ser = new JsonSerializer(); return ser.Deserialize(jsonReader); } } private async Task WriteResponseAsync(HttpContext context, ExecutionResult result) { var json = _writer.Write(result); context.Response.ContentType = "application/json"; context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK; await context.Response.WriteAsync(json); } } public class GraphQLRequest { public string Query { get; set; } public string OperationName { get; set; } public JObject Variables { get; set; } } }
RehanSaeed commented 5 years ago

You can see a fully working example with auth using the ASP.NET Core GraphQL.NET Boxed project template.

You should be aware I submitted a PR to create a new auth NuGet package which builds on top of the ASP.NET Core auth packages. This PR was merged in the server repo recently but the NuGet package has not yet been released. Once it is released, I plan to update the project template above.

Hope that helps!

ddecours commented 5 years ago

Joe - kudos to you for this outstanding package! I'm trying to develop a graphql api using ASP.NET and SQL Server, with your package as one of the primary components. Related to my original question above, is the use of middleware (as shown above) no longer the intended approach to utilize your GraphQL-Dotnet package? Should we instead implement the package without creating our own custom middleware?

Thanks in advance for any guidance!


Rehan - your boxed templates are amazing! I'm relatively new to asp.net and graphql and appreciate having such a full-featured starting point - thank you!

I did create a new project using your GQL template and it works just fine. However, your example utilizes an in-memory db instead of a SQL backed datasource (I am using MS SQL Server) with a scoped database context. I'm utilizing the repository pattern, so when I try to inject my database context into one of the repositories, I run into the 'cannot consume scoped service inside a singleton' run-time error. I've tried changing my repositories to have a scoped service lifetime, but the error persisted.

Now I've gone so far as to create my own scope in the base repository (from which all repositories are derived), using this approach:

            // Create a new scope
            using(var scope = provider.CreateScope())
            {
                // Resolve the Scoped service
                _context = scope.ServiceProvider.GetService<GHCOpsDbContext>();
            }

but this code led to run-time errors when the context was disposed and no longer available for subsequent method calls.

So I then modified this code to remove the 'using':

            // TODO: Warning - this code COULD lead to memory leak if the database context keeps getting created without disposal (not sure)
            var scope = provider.CreateScope();
            _context = scope.ServiceProvider.GetService<GHCOpsDbContext>();

This code did work, but I fear this code could lead to a memory leak if the database context keeps getting created without disposal (really not sure!).

I'd appreciate any insights on the right architectural approach to creating an ASP.NET / GraphQL based API that utilizes a SQL back-end. Would you have a GraphQL template that uses SQL with a scoped database context? Or could you point me to a similar resource?

My current approach feels like a hack and a little bit of redirection could get me back on the right track.

Thank you in advance!

I've included key files in case that helps:

========================================================= STARTUP.CS

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

using AutoMapper;
using GraphQL.Server;
using GraphQL.Http;
using GraphQL;
using GraphQL.Types;
using CorrelationId;

using ghc.GQLAPI.Helpers;
using ghc.Data;
using ghc.Data.Interfaces;
using ghc.Data.Repositories;
using ghc.GQLAPI.Models;
// using ghc.GQLAPI.Middleware;
using ghc.Model.Entities;
using GraphQL.Validation;
using GraphQL.Server.Ui.Playground;
using GraphQL.Server.Ui.Voyager;

namespace ghc.GQLAPI
{
    public class Startup
    {
        //TODO: user user secrets to avoid storing salt key in clear text
        private IHostingEnvironment hostingEnvironment;
        public IConfiguration configuration { get; }

        public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
        {
            this.configuration = configuration;
            this.hostingEnvironment = hostingEnvironment;

            var builder = new ConfigurationBuilder()
                .SetBasePath(hostingEnvironment.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{this.hostingEnvironment.EnvironmentName}.json", optional: true);

            builder.AddEnvironmentVariables();
            this.configuration = builder.Build();
        }

        // ==== DEVELOPMENT ====
       // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureDevelopmentServices(IServiceCollection services)
        {
            // Allow for other classes to access the configuration via dependency injection
            services
                .AddSingleton<IConfiguration>(this.configuration)
                .AddCorrelationIdFluent()
                .AddCustomCaching()
                .AddCustomOptions(this.configuration)
                .AddCustomRouting()
                .AddCustomResponseCompression()
                .AddCustomStrictTransportSecurity()
                .AddMvcCore()
                    .SetCompatibilityVersion(CompatibilityVersion.Latest)
                    .AddAuthorization()
                    .AddJsonFormatters()
                    .AddCustomJsonOptions(this.hostingEnvironment)
                    .AddCustomCors()
                    .AddCustomMvcOptions(this.hostingEnvironment)
                .Services
                .AddCustomGraphQL(this.hostingEnvironment)
                .AddCustomGraphQLAuthorization()
                .AddProjectRepositories()
                .AddProjectSchemas()
                .BuildServiceProvider();

            // *** Configure security/identity ****
            // TODO: This is a very weak password strength policy - strengthen for prod
            IdentityBuilder builder = services.AddIdentityCore<User>(opt => 
            {
                opt.Password.RequireDigit = false;
                opt.Password.RequiredLength = 4;
                opt.Password.RequireNonAlphanumeric = false;
                opt.Password.RequireUppercase = false;
            });

            builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
            builder.AddEntityFrameworkStores<GHCOpsDbContext>();  // tell identity system to store security info in the entity framework data store
            builder.AddRoleValidator<RoleValidator<Role>>();
            builder.AddRoleManager<RoleManager<Role>>();
            builder.AddSignInManager<SignInManager<User>>();

            // Add authentication strategy
            var key = Encoding.ASCII.GetBytes(this.configuration.GetSection("AppSettings:TokenKey").Value);
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options => {
                    options.TokenValidationParameters = new TokenValidationParameters {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(key),
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                });

            // Allow us to reference the data seeding process in the configure method below
            services.AddTransient<Seed>();

            // Specify the DB Context (SqlServer)
            services.AddDbContext<GHCOpsDbContext>(options => 
                options.UseSqlServer(this.configuration["ConnectionStrings:GHCOpsDB"],
                    b => b.MigrationsAssembly("ghc.Migrations.Development"))
                 .ConfigureWarnings( warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
                 );

            // Add support for mapping between domain objects and data transfer objects
            services.AddAutoMapper();

            // Add Support for Model View Controller Framework
            services.AddMvc(options => 
                {
                var mustBeAuthenticatedPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                options.Filters.Add(new AuthorizeFilter(mustBeAuthenticatedPolicy));
                })
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
                .AddJsonOptions(opt => opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
        }

        /////////////////////////////////////////////////////////////////////////////////////////////////////
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, Seed seeder) 
        {
            Console.WriteLine("=========================================");
            Console.WriteLine("Environment: " + (env.EnvironmentName).ToUpper());
            Console.WriteLine("=========================================");

            // Ensures migrations have been run or creates the DB from scratch if not found
            UpdateDatabase(app);

            if (env.IsDevelopment()) {
                app.UseDeveloperErrorPages();
            }
            else {
                // Establish a global exception handler in case we are not in Development mode
                app.UseExceptionHandler(builder => {
                    builder.Run(async context => {
                        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                        var error = context.Features.Get<IExceptionHandlerFeature>();
                        if (error != null) {
                            context.Response.AddApplicationError(error.Error.Message);
                            await context.Response.WriteAsync(error.Error.Message);
                        }
                    });
                });
                app.UseHsts(); // Adds strict transport security header
            }

            app.UseCorrelationId();
            app.UseHostFiltering();
            app.UseResponseCompression();

            // Cross Origin Resource - Order matters - this must come before UseMvc()
            app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials());  // TODO: Tighten up security here, this is wide open

            app.UseStaticFilesWithCacheControl();

            app.UseWebSockets();
            app.UseGraphQLWebSockets<MainSchema>();
            app.UseGraphQL<MainSchema>();

            // Add the GraphQL Playground UI to try out the GraphQL API at /.
            app.UseGraphQLPlayground(new GraphQLPlaygroundOptions() { Path = "/" });

            // Add the GraphQL Voyager UI to let you navigate your GraphQL API as a spider graph at /voyager.
            app.UseGraphQLVoyager(new GraphQLVoyagerOptions() { Path = "/voyager" });

            // Seed database if needed
            seeder.SeedRoles();
            seeder.SeedUsers();
            seeder.SeedNotes();
            seeder.SeedClients();
            seeder.SeedCaregivers();
            seeder.SeedClientNotes();
            seeder.SeedCaregiverNotes();
            seeder.SeedPhotos();
            seeder.SeedUserPhotos();

            app.UseAuthentication();
            app.UseMvcWithDefaultRoute();
        }

        private static void UpdateDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices
                .GetRequiredService<IServiceScopeFactory>()
                .CreateScope())
            {
                using (var context = serviceScope.ServiceProvider.GetService<GHCOpsDbContext>())
                {
                    context.Database.Migrate();
                }
            }
        }

    }
}

==========================================================
PROJECTSERVICECOLLECTIONEXTENSIONS.CS

namespace ghc.GQLAPI { using ghc.Data.Interfaces; using ghc.Data.Repositories; using ghc.GQLAPI.Models; using ghc.GQLAPI.Repositories; using ghc.GQLAPI.Schemas; using Microsoft.Extensions.DependencyInjection;

/// <summary>
/// <see cref="IServiceCollection"/> extension methods add project services.
/// </summary>
/// <remarks>
/// AddSingleton - Only one instance is ever created and returned.
/// AddScoped - A new instance is created and returned for each request/response cycle.
/// AddTransient - A new instance is created and returned each time.
/// </remarks>
public static class ProjectServiceCollectionExtensions
{
    /// <summary>
    /// Add project data repositories.
    /// </summary>
    public static IServiceCollection AddProjectRepositories(this IServiceCollection services) =>
        services
            .AddScoped<IDroidRepository, DroidRepository>()
            .AddScoped<IHumanRepository, HumanRepository>()
            .AddScoped<IValuesRepository, ValuesRepository>()
            .AddScoped<IUsersRepository, UsersRepository>()
            .AddScoped<IClientsRepository, ClientsRepository>()
            .AddScoped<ICaregiversRepository, CaregiversRepository>()
            .AddScoped<IKeywordsRepository, KeywordsRepository>()
            .AddScoped<INotesRepository, NotesRepository>()
            .AddScoped<IPhotosRepository, PhotosRepository>();

    /// <summary>
    /// Add project GraphQL schema and web socket types.
    /// </summary>
    public static IServiceCollection AddProjectSchemas(this IServiceCollection services) =>
        services
            .AddScoped<MainSchema>();
            // .AddSingleton<MainSchema>();

}

}


==========================================================
ENTITYBASEREPOSITORY.CS

using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using ghc.Data.Interfaces; using ghc.Model; using ghc.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.DependencyInjection;

namespace ghc.Data.Repositories { public class EntityBaseRepository : IEntityBaseRepository where T : class, IEntityBase, new() {

    private readonly GHCOpsDbContext _context;
    public EntityBaseRepository(IServiceProvider provider)
    {

        // TODO: Warning - this code COULD lead to memory leak if the database context keeps getting created without disposal (not sure)
        var scope = provider.CreateScope();
        _context = scope.ServiceProvider.GetService<GHCOpsDbContext>();

    }

    public async Task<T> GetSingle(int id)
    {
        var entity = await _context.Set<T>().FirstOrDefaultAsync(x => x.Id == id);
        return entity;
    }

    // public Task<IEnumerable<T>> GetAll(CancellationToken cancellationToken)
    // {
    //     throw new NotImplementedException();
    // }

    public async Task<IEnumerable<T>> GetAll(CancellationToken cancellationToken)
    {
        var entities = await _context.Set<T>().ToListAsync();
        return entities;
    }

    public async Task<T> GetRandom()
    {
        return await _context.Set<T>().OrderBy(o => Guid.NewGuid()).FirstOrDefaultAsync();
    }

    public async Task<T> AddAsync(T entity)
    {
        var addedEntity = await _context.Set<T>().AddAsync(entity);
        await _context.SaveChangesAsync();
        return addedEntity.Entity;
    }

    public void Add(T entity)
    {
        _context.Add(entity);
    }

    public void Delete(T entity)
    {
        _context.Remove(entity);
    }

    public async Task<bool> SaveAll()
    {
        return await _context.SaveChangesAsync() > 0;
    }

}

}

================================================================= CUSTOMSERVICECOLLECTIONEXTENSIONS.CS

using System;
using System.IO.Compression;
using System.Linq;
using Boxed.AspNetCore;
using CorrelationId;
using ghc.GQLAPI.Constants;
using ghc.GQLAPI.Options;
using GraphQL;
using GraphQL.Authorization;
using GraphQL.Server;
using GraphQL.Server.Internal;
using GraphQL.Validation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HostFiltering;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace ghc.GQLAPI.Helpers
{

    /// <summary>
    /// <see cref="IServiceCollection"/> extension methods which extend ASP.NET Core services.
    /// </summary>
    public static class CustomServiceCollectionExtensions
    {

        public static IServiceCollection AddCorrelationIdFluent(this IServiceCollection services)
        {
            services.AddCorrelationId();
            return services;
        }

        /// <summary>
        /// Configures caching for the application. Registers the <see cref="IDistributedCache"/> and
        /// <see cref="IMemoryCache"/> types with the services collection or IoC container. The
        /// <see cref="IDistributedCache"/> is intended to be used in cloud hosted scenarios where there is a shared
        /// cache, which is shared between multiple instances of the application. Use the <see cref="IMemoryCache"/>
        /// otherwise.
        /// </summary>
        public static IServiceCollection AddCustomCaching(this IServiceCollection services) =>
            services
                // Adds IMemoryCache which is a simple in-memory cache.
                .AddMemoryCache()
                // Adds IDistributedCache which is a distributed cache shared between multiple servers. This adds a
                // default implementation of IDistributedCache which is not distributed. See below:
                .AddDistributedMemoryCache();
                // Uncomment the following line to use the Redis implementation of IDistributedCache. This will
                // override any previously registered IDistributedCache service.
                // Redis is a very fast cache provider and the recommended distributed cache provider.
                // .AddDistributedRedisCache(options => { ... });
                // Uncomment the following line to use the Microsoft SQL Server implementation of IDistributedCache.
                // Note that this would require setting up the session state database.
                // Redis is the preferred cache implementation but you can use SQL Server if you don't have an alternative.
                // .AddSqlServerCache(
                //     x =>
                //     {
                //         x.ConnectionString = "Server=.;Database=ASPNET5SessionState;Trusted_Connection=True;";
                //         x.SchemaName = "dbo";
                //         x.TableName = "Sessions";
                //     });

        /// <summary>
        /// Configures the settings by binding the contents of the appsettings.json file to the specified Plain Old CLR
        /// Objects (POCO) and adding <see cref="IOptions{T}"/> objects to the services collection.
        /// </summary>
        public static IServiceCollection AddCustomOptions(
            this IServiceCollection services,
            IConfiguration configuration) =>
            services
                // ConfigureSingleton registers IOptions<T> and also T as a singleton to the services collection.
                .ConfigureSingleton<ApplicationOptions>(configuration)
                .ConfigureSingleton<CacheProfileOptions>(configuration.GetSection(nameof(ApplicationOptions.CacheProfiles)))
                .ConfigureSingleton<CompressionOptions>(configuration.GetSection(nameof(ApplicationOptions.Compression)))
                .ConfigureSingleton<HostFilteringOptions>(configuration.GetSection(nameof(ApplicationOptions.HostFiltering)))
                .ConfigureSingleton<GraphQLOptions>(configuration.GetSection(nameof(ApplicationOptions.GraphQL)));

        /// <summary>
        /// Adds dynamic response compression to enable GZIP compression of responses. This is turned off for HTTPS
        /// requests by default to avoid the BREACH security vulnerability.
        /// </summary>
        public static IServiceCollection AddCustomResponseCompression(this IServiceCollection services) =>
            services
                .AddResponseCompression(
                    options =>
                    {
                        // Add additional MIME types (other than the built in defaults) to enable GZIP compression for.
                        var customMimeTypes = services
                            .BuildServiceProvider()
                            .GetRequiredService<CompressionOptions>()
                            .MimeTypes ?? Enumerable.Empty<string>();
                        options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(customMimeTypes);
                    })
                .Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Optimal);

        /// <summary>
        /// Add custom routing settings which determines how URL's are generated.
        /// </summary>
        public static IServiceCollection AddCustomRouting(this IServiceCollection services) =>
            services.AddRouting(
                options =>
                {
                    // All generated URL's should be lower-case.
                    options.LowercaseUrls = true;
                });

        /// <summary>
        /// Adds the Strict-Transport-Security HTTP header to responses. This HTTP header is only relevant if you are
        /// using TLS. It ensures that content is loaded over HTTPS and refuses to connect in case of certificate
        /// errors and warnings.
        /// See https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security and
        /// http://www.troyhunt.com/2015/06/understanding-http-strict-transport.html
        /// Note: Including subdomains and a minimum maxage of 18 weeks is required for preloading.
        /// Note: You can refer to the following article to clear the HSTS cache in your browser:
        /// http://classically.me/blogs/how-clear-hsts-settings-major-browsers
        /// </summary>
        public static IServiceCollection AddCustomStrictTransportSecurity(this IServiceCollection services) =>
            services
                .AddHsts(
                    options =>
                    {
                        // Preload the HSTS HTTP header for better security. See https://hstspreload.org/
                        options.IncludeSubDomains = true;
                        options.MaxAge = TimeSpan.FromSeconds(31536000); // 1 Year
                        options.Preload = true;
                    });

        public static IServiceCollection AddCustomGraphQL(this IServiceCollection services, IHostingEnvironment hostingEnvironment) =>
            services
                // Add a way for GraphQL.NET to resolve types.
                .AddSingleton<IDependencyResolver, GraphQLDependencyResolver>()
                .AddGraphQL(
                    options =>
                    {
                        // Set some limits for security, read from configuration.
                        options.ComplexityConfiguration = services
                            .BuildServiceProvider()
                            .GetRequiredService<IOptions<GraphQLOptions>>()
                            .Value
                            .ComplexityConfiguration;
                        // Show stack traces in exceptions. Don't turn this on in production.
                        options.ExposeExceptions = hostingEnvironment.IsDevelopment();
                        options.EnableMetrics = true;
                    })
                .AddGraphTypes()    // Adds all graph types in the current assembly with a singleton lifetime.
                // Adds ConnectionType<T>, EdgeType<T> and PageInfoType.
                .AddRelayGraphTypes()
                // Add a user context from the HttpContext and make it available in field resolvers.
                .AddUserContextBuilder<GraphQLUserContextBuilder>()
                // Add GraphQL data loader to reduce the number of calls to our repository.
                .AddDataLoader()
                // Add WebSockets support for subscriptions.
                .AddWebSockets()
                .Services
                .AddTransient(typeof(IGraphQLExecuter<>), typeof(InstrumentingGraphQLExecutor<>));

        /// <summary>
        /// Add GraphQL authorization (See https://github.com/graphql-dotnet/authorization).
        /// </summary>
        public static IServiceCollection AddCustomGraphQLAuthorization(this IServiceCollection services) =>
            services
                .AddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>()
                .AddTransient<IValidationRule, AuthorizationValidationRule>()
                .AddSingleton(
                    x =>
                    {
                        var authorizationSettings = new AuthorizationSettings();
                        authorizationSettings.AddPolicy(
                            AuthorizationPolicyName.Admin,
                            y => y.RequireClaim("role", "Administrator"));
                        return authorizationSettings;
                    });

    }
}
RehanSaeed commented 5 years ago

You need to register your graph types as scoped types if you want to make use of scoped dependencies within them. See this code in CustomServiceCollectionExtensions:

// Adds all graph types in the current assembly with a singleton lifetime.
.AddGraphTypes()

There is an overload where you can select a different lifetime.

ddecours commented 5 years ago

Thank you for the input Rehan - I did adjust the service lifetime to scoped, but this then lead to another error:

InvalidOperationException: Cannot resolve scoped service 'ghc.GQLAPI.Schemas.QueryObject' from root provider.

I believe this is due to the fact that my root QueryObject attempts to inject one of my repositories in the constructor. Since this repository is now scoped, the .net core throws this error since you're not supposed to inject scoped dependencies in the constructor.

So I'd assume I need to resolve the dependency some other way (i.e. Invoke method?). But I think that approach involves adding another layer of middleware (which then goes back to the original intent of this question - Do I add this middleware or is it now baked into the Graphql-DotNet packages?

I sense I'm going down a rabbit hole here :-)

Would you happen to have a Boxed template for GraphQL that shows the proper architecture using a true database backend (i.e. with a scoped database context)?

Any input/examples appreciated!

RehanSaeed commented 5 years ago

My Dotnet Boxed GraphQL template uses the repository pattern, it's up to you to use whatever storage you want.

sungam3r commented 3 years ago

Closed as outdated.