Yesterday (09/14) was released the first version (RC1) from .NET 5, ASP.NET, EF Core and C# 9.
Still talking about releases, since last week is available the version 3 from GraphQL for .NET.
On occasion, this project exemplify the implementation and Dockerization of a simple Razor Web MVC Core consuming an GraphQL 3 Web API, build in a .NET 5 multilayer project, considering development best practices, like SOLID and DRY, applying Domain-Driven concepts in a Hexagonal Architecture.
AntonioFalcaoJr / Dotnet6.GraphQL4.WebApplication
This project exemplifies the implementation and dockerization of a simple Razor Web MVC Core consuming a full GraphQL 4 Web API, build in a .NET 6 multi-layer project, considering development best practices, like SOLID and DRY, applying Domain-Driven concepts in a Onion Architecture.
Highlights
Notifications (pattern/context)
To avoid handle exceptions, was implemented a NotificationContext
that's allow all layers add business notifications through the request, with support to receive Domain notifications, that by other side, implement validators from Fluent Validation and return a ValidationResult
.
protected bool OnValidate<TEntity>(TEntity entity, AbstractValidator<TEntity> validator)
{
ValidationResult = validator.Validate(entity);
return IsValid;
}
protected void AddError(string errorMessage, ValidationResult validationResult = default)
{
ValidationResult.Errors.Add(new ValidationFailure(default, errorMessage));
validationResult?.Errors.ToList().ForEach(failure => ValidationResult.Errors.Add(failure));
}
To GraphQL the notification context delivers a ExecutionErrors
that is propagated to result
from execution by a personalized Executer
:
var result = await base.ExecuteAsync(operationName, query, variables, context, cancellationToken);
var notificationContext = _serviceProvider.GetRequiredService<INotificationContext>();
if (notificationContext.HasNotifications)
{
result.Errors = notificationContext.ExecutionErrors;
result.Data = default;
}
Resolving Scoped
dependencies with Singleton
Schema.
Is necessary, in the same personalized Executer
to define the service provider that will be used for resolvers
on fields
:
var options = base.GetOptions(operationName, query, variables, context, cancellationToken);
options.RequestServices = _serviceProvider;
Abstractions
With abstract designs, it is possible to reduce coupling in addition to applying DRY concepts, providing resources for the main behaviors:
public abstract class Entity<TId>
where TId : struct
public abstract class Builder<TBuilder, TEntity, TId> : IBuilder<TEntity, TId>
where TBuilder : Builder<TBuilder, TEntity, TId>
where TEntity : Entity<TId>
where TId : struct
public abstract class Repository<TEntity, TId> : IRepository<TEntity, TId>
where TEntity : Entity<TId>
where TId : struct
{
private readonly DbSet<TEntity> _dbSet;
protected Repository(DbContext dbDbContext)
{
_dbSet = dbDbContext.Set<TEntity>();
}
public abstract class Service<TEntity, TModel, TId> : IService<TEntity, TModel, TId>
where TEntity : Entity<TId>
where TModel : Model<TId>
where TId : struct
{
protected readonly IMapper Mapper;
protected readonly INotificationContext NotificationContext;
protected readonly IRepository<TEntity, TId> Repository;
protected readonly IUnitOfWork UnitOfWork;
protected Service(
IUnitOfWork unitOfWork,
IRepository<TEntity, TId> repository,
IMapper mapper,
INotificationContext notificationContext)
{
UnitOfWork = unitOfWork;
Repository = repository;
Mapper = mapper;
NotificationContext = notificationContext;
}
public abstract class MessageService<TMessage, TModel, TId> : IMessageService<TMessage, TModel, TId>
where TMessage : class
where TModel : Model<TId>
where TId : struct
{
private readonly IMapper _mapper;
private readonly ISubject<TMessage> _subject;
protected MessageService(IMapper mapper, ISubject<TMessage> subject)
{
_mapper = mapper;
_subject = subject;
}
From EF TPH
to GraphQL Interface
GraphQL interfaces provide a very interesting way to represent types derived from entities. In turn, in EF Core we can structure with TPH.
ENTITY
public class ProductConfig : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder
.HasDiscriminator()
.HasValue<Boot>(nameof(Boot))
.HasValue<Kayak>(nameof(Kayak))
.HasValue<Backpack>(nameof(Backpack));
}
}
INHERITOR
public class KayakConfig : IEntityTypeConfiguration<Kayak>
{
public void Configure(EntityTypeBuilder<Kayak> builder)
{
builder
.HasBaseType<Product>();
}
}
INTERFACE
public sealed class ProductInterfaceGraphType : InterfaceGraphType<Product>
{
public ProductInterfaceGraphType(BootGraphType bootGraphType, BackpackGraphType backpackGraphType, KayakGraphType kayakGraphType)
{
Name = "product";
ResolveType = @object =>
{
return @object switch
{
Boot _ => bootGraphType,
Backpack _ => backpackGraphType,
Kayak _ => kayakGraphType,
_ => default
};
};
}
}
OBJECT
public sealed class KayakGraphType : ObjectGraphType<Kayak>
{
public KayakGraphType()
{
Name = "kayak";
Interface<ProductInterfaceGraphType>();
IsTypeOf = o => o is Product;
}
}
Environment configuration
Development
Secrets
To configure database resource, init
secrets in https://github.com/AntonioFalcao/Dotnet5.GraphQL3.WebApplication/tree/master/src/Dotnet5.GraphQL3.Store.WebAPI
, and then define the DefaultConnection
:
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost,1433;Database=Store;User=sa;Password=!MyComplexPassword"
After this, to configure the HTTP client, init
secrets in https://github.com/AntonioFalcao/Dotnet5.GraphQL3.WebApplication/tree/master/src/Dotnet5.GraphQL3.Store.WebMVC
and define Store client host:
dotnet user-secrets init
dotnet user-secrets set "HttpClient:Store" "http://localhost:5000/graphql"
AppSettings
If you prefer, is possible to define it on WebAPI appsettings.Development.json
and WebMVC appsettings.Development.json
files:
WebAPI
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Database=Store;User=sa;Password=!MyComplexPassword"
}
}
WebMCV
{
"HttpClient": {
"Store": "http://localhost:5000/graphql"
}
}
Production
Considering use Docker for CD (Continuous Deployment). On respective compose both web applications and sql server are in the same network, and then we can use named hosts. Already defined on WebAPI appsettings.json
and WebMVC appsettings.json
files:
AppSettings
WebAPI
{
"ConnectionStrings": {
"DefaultConnection": "Server=mssql;Database=Store;User=sa;Password=!MyComplexPassword"
}
}
WebMCV
{
"HttpClient": {
"Store": "http://webapi:5000/graphql"
}
}
Running
The ./docker-compose.yml
provide the WebAPI
, WebMVC
and MS SQL Server
applications:
docker-compose up -d
GraphQL Playground
By default Playground respond at http://localhost:5000/ui/playground
but is possible configure the host and many others details in ../...WebAPI/GraphQL/DependencyInjection/Configure.cs
app.UseGraphQLPlayground(
new GraphQLPlaygroundOptions
{
Path = "/ui/playground",
BetaUpdates = true,
RequestCredentials = RequestCredentials.Omit,
HideTracingResponse = false,
EditorCursorShape = EditorCursorShape.Line,
EditorTheme = EditorTheme.Dark,
EditorFontSize = 14,
EditorReuseHeaders = true,
EditorFontFamily = "JetBrains Mono"
});
Queries
Fragment for comparison and Arguments
QUERY
{
First: product(id: "2c05b59b-8fb3-4cba-8698-01d55a0284e5") {
...comparisonFields
}
Second: product(id: "65af82e8-27f6-44f3-af4a-029b73f14530") {
...comparisonFields
}
}
fragment comparisonFields on Product {
id
name
rating
description
}
RESULT
{
"data": {
"First": {
"id": "2c05b59b-8fb3-4cba-8698-01d55a0284e5",
"name": "libero",
"rating": 5,
"description": "Deleniti voluptas quidem accusamus est debitis quisquam enim."
},
"Second": {
"id": "65af82e8-27f6-44f3-af4a-029b73f14530",
"name": "debitis",
"rating": 10,
"description": "Est veniam unde."
}
}
}
Query named's and Variables
QUERY
query all {
products {
id
name
}
}
query byid($productId: ID!) {
product(id: $productId) {
id
name
}
}
VARIABLES
{
"productId": "2c05b59b-8fb3-4cba-8698-01d55a0284e5"
}
HTTP BODY
{
"operationName": "byid",
"variables": {
"productId": "2c05b59b-8fb3-4cba-8698-01d55a0284e5"
},
"query": "query all {
products {
id
name
}
}
query byid($productId: ID!) {
product(id: $productId) {
id
name
}
}"
}
PLAYGROUND
Variables with include, skip and default value
QUERY
query all($showPrice: Boolean = false) {
products {
id
name
price @include(if: $showPrice)
rating @skip(if: $showPrice)
}
}
VARIABLES
{
"showPrice": true
}
HTTP BODY
{
"operationName": "all",
"variables": {
"showPrice": false
},
"query": "query all($showPrice: Boolean = false) {
products {
id
name
price @include(if: $showPrice)
rating @skip(if: $showPrice)
}
}"
}
Mutations
MUTATION
Creating / adding a new Review to the respective product.
mutation($review: reviewInput!) {
createReview(review: $review) {
id
}
}
VARIABLES
{
"review": {
"title": "some title",
"comment": "some comment",
"productId": "0fb8ec7e-7af1-4fe3-a2e2-000996ffd20f"
}
}
RESULT
{
"data": {
"createReview": {
"title": "some title"
}
}
}
Subscriptions
SUBSCRIPTION
The Mutation stay listening if a new review is added.
subscription {
reviewAdded {
title
}
}
RESULT
{
"data": {
"reviewAdded": {
"title": "Some title"
}
}
}
Built With
Microsoft Stack - v5.0 (RC 1)
- .NET 5.0 - Base framework;
- ASP.NET 5.0 - Web framework;
- Entity Framework Core 5.0 - ORM;
- Microsoft SQL Server on Linux for Docker - Database.
GraphQL Stack - v3.0 (preview/alpha)
- GraphQL - GraphQL is a query language for APIs and a runtime for fulfilling those queries with data;
- GraphQL for .NET - This is an implementation of GraphQL in .NET;
- GraphQL.Client - A GraphQL Client for .NET over HTTP;
- GraphQL Playground - GraphQL IDE for better development workflows.
Community Stack
- AutoMapper - A convention-based object-object mapper;
- FluentValidation - A popular .NET library for building strongly-typed validation rules;
- Bogus - A simple and sane fake data generator for C#, F#, and VB.NET;
- Bootstrap - The most popular HTML, CSS, and JS library in the world.
Top comments (5)
I just try to add migrations, found error : Unable to create an object of type 'StoreDbContext'. For the different patterns supported at design time, see go.microsoft.com/fwlink/?linkid=85...
Hello AZMIMANSUR!
This project has the database context in a different layer than the entry point.
To perform migrations, in this case, you must indicate both projects in the command:
Thanks, Antonio
Working now.
Hi Antonio,
Question again.
I created a service below to update, I'm still confused about how to mapper changed between
clientModel
withclient
?Hi AZMIMANSUR!
In this case, the abstractions implemented in the application already resolve a "save" of a new entity (root aggregate) for you. Just need to add a new field in the respective mutation class.
For that, and based on "GraphQL for .NET", you will need an
InputObjectGraphType<T>
for the input, and anObjectGraphType<T>
for output.Obs: Is necessary to create a new migration including the new table "Clients".