This project exemplify the implementation and Dockerization of a simple Razor Web MVC Core consuming a full GraphQL 3 Web API, build in a .NET 5 multi-layer project, considering development best practices, like SOLID and DRY, applying Domain-Driven concepts in a Hexagonal Architecture.
WebAPI |
---|
WebMVC |
Docker images: WebAPI | WebMVC
To configure database resource, init
secrets in ./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 ./src/Dotnet5.GraphQL3.Store.WebMVC
and define Store client host:
dotnet user-secrets init
dotnet user-secrets set "HttpClient:Store" "http://localhost:5000"
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"
}
}
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:
WebAPI
{
"ConnectionStrings": {
"DefaultConnection": "Server=mssql;Database=Store;User=sa;Password=!MyComplexPassword"
}
}
WebMCV
{
"HttpClient": {
"Store": "http://webapi:5000"
}
}
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, implementing 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 the GraphQL the notification context delivery a ExecutionErrors
that is propagated to result
from execution by a personalised Executer
:
public override async Task<ExecutionResult> ExecuteAsync(string operationName, string query, Inputs variables, IDictionary<string, object> context, IServiceProvider requestServices, CancellationToken cancellationToken = new CancellationToken())
{
var result = await base.ExecuteAsync(operationName, query, variables, context, requestServices, cancellationToken);
var notification = requestServices.GetRequiredService<INotificationContext>();
if (notification.HasNotifications is false) return result;
result.Errors = notification.ExecutionErrors;
result.Data = default;
return result;
}
It's no more necessary after version 4.2.0 from GraphQL Server. By default, the Service Provider is already being propagated.
Is necessary, in the same personalised Executer
define the service provider that will be used from resolvers
on fields
:
var options = base.GetOptions(operationName, query, variables, context, cancellationToken);
options.RequestServices = _serviceProvider;
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;
}
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;
}
}
The ./docker-compose.yml
provide the WebAPI
, WebMVC
and MS SQL Server
applications:
docker-compose up -d
It's possible to run without a clone of the project using the respective compose:
version: "3.7"
services:
mssql:
container_name: mssql
image: mcr.microsoft.com/mssql/server
ports:
- 1433:1433
environment:
SA_PASSWORD: "!MyComplexPassword"
ACCEPT_EULA: "Y"
healthcheck:
test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$$SA_PASSWORD" -Q "SELECT 1" || exit 1
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
networks:
- graphqlstore
webapi:
container_name: webapi
image: antoniofalcaojr/dotnet5-graphql3-webapi
environment:
- ASPNETCORE_URLS=http://*:5000
ports:
- 5000:5000
depends_on:
mssql:
condition: service_healthy
networks:
- graphqlstore
webmvc:
container_name: webmvc
image: antoniofalcaojr/dotnet5-graphql3-webmvc
environment:
- ASPNETCORE_URLS=http://*:7000
ports:
- 7000:7000
depends_on:
- webapi
networks:
- graphqlstore
networks:
graphqlstore:
driver: bridge
Based on cloud-native concepts, Readiness and Liveness integrity verification strategies were implemented.
Web API
http://localhost:5000/health/ready
http://localhost:5000/health/live
Web MVC
http://localhost:7000/health/ready
http://localhost:7000/health/live
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"
});
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
query all {
products {
items {
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 {
items {
id
name
}
}
}
query byid($productId: ID!) {
product(id: $productId) {
id
name
}
}"
}
PLAYGROUND
QUERY
query all($showPrice: Boolean = false) {
products {
items {
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 {
items {
id
name
price @include(if: $showPrice)
rating @skip(if: $showPrice)
}
}
}"
}
QUERY
{
products(pageParams: { index: 2, size: 1 }) {
items {
id
}
pageInfo {
current
hasNext
hasPrevious
size
}
}
}
RESULT
{
"data": {
"products": {
"items": [
{
"id": "3b2f6ce4-1b1d-4376-80a6-0b8d51932757"
}
],
"pageInfo": {
"current": 2,
"hasNext": true,
"hasPrevious": true,
"size": 1
}
}
}
}
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"
}
}
}
SUBSCRIPTION
The Mutation stay listening if a new review is added.
subscription {
reviewAdded {
title
}
}
RESULT
{
"data": {
"reviewAdded": {
"title": "Some title"
}
}
}
- .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 - 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.
- 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.
Available soon!
We use SemVer for versioning. For the versions available, see the tags on this repository.
- Antônio Falcão - GitHub
See also the list of contributors who participated in this project.
This project is licensed under the MIT License - see the LICENSE file for details
- Nothing more, for now.