Closed ddecours closed 3 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!
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
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;
});
}
}
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.
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!
My Dotnet Boxed GraphQL template uses the repository pattern, it's up to you to use whatever storage you want.
Closed as outdated.
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