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.

Home Page:https://dev.to/antoniofalcao/about-a-net-5-rc1-web-application-with-graphql-3-5d3c

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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, KISS and DRY, applying Domain-Driven concepts in a Onion Architecture.

Give a Star! ⭐

WebAPI
Build/Test API Image Push API CodeQL Codacy Badge
WebMVC
Build/Test MVC Image Push MVC CodeQL Codacy Badge

Oldest version: Dotnet5.GraphQL3.WebApplication

WebAPI
WebMVC

home


diagram


Running

Development (secrets)

To configure database resource, init secrets in ./src/Dotnet6.GraphQL4.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/Dotnet6.GraphQL4.Store.WebMVC and define Store client host:

dotnet user-secrets init
dotnet user-secrets set "HttpClient:Store" "http://localhost:5000"
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"
  }
}

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"
  }
}

Docker

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/dotnet6-graphql4-webapi
    environment:
      - ASPNETCORE_URLS=http://*:5000
    ports:
      - 5000:5000
    depends_on:
      mssql:
        condition: service_healthy
    networks:
      - graphqlstore

  webmvc:
    container_name: webmvc
    image: antoniofalcaojr/dotnet6-graphql4-webmvc
    environment:
      - ASPNETCORE_URLS=http://*:7000
    ports:
      - 7000:7000
    depends_on:
      - webapi
    networks:
      - graphqlstore

  healthchecks:
    container_name: healthchecks-ui
    image: xabarilcoding/healthchecksui
    depends_on:
      mssql:
        condition: service_healthy
    environment:
      - storage_provider=SqlServer
      - storage_connection=Server=mssql;Database=Store;User=sa;Password=!MyComplexPassword
      - Logging:LogLevel:Default=Debug
      - Logging:Loglevel:Microsoft=Warning
      - Logging:LogLevel:HealthChecks=Debug
      - HealthChecksUI:HealthChecks:0:Name=webapi
      - HealthChecksUI:HealthChecks:0:Uri=http://webapi:5000/healthz
      - HealthChecksUI:HealthChecks:1:Name=webmvc
      - HealthChecksUI:HealthChecks:1:Uri=http://webmvc:7000/healthz
    ports:
      - 8000:80
    networks:
      - graphqlstore

networks:
  graphqlstore:
    driver: bridge

GraphQL Playground

By default Playground respond at http://localhost:5000/ui/playground but is possible configure the host and many others details in ../DependencyInjection/Extensions/ApplicationBuilderExtensions.cs

app.UseGraphQLPlayground(
       options: new() 
       {
           BetaUpdates = true,
           RequestCredentials = RequestCredentials.Omit,
           HideTracingResponse = false,
           EditorCursorShape = EditorCursorShape.Line,
           EditorTheme = EditorTheme.Dark,
           EditorFontSize = 14,
           EditorReuseHeaders = true,
           EditorFontFamily = "JetBrains Mono"
       },
       path: "/ui/playground");

Health checks

Based on cloud-native concepts, Readiness and Liveness integrity verification strategies were implemented.

/health
Just check if the instance is running.

/health/live
Check if the instance is running and all the dependencies too.

/health/ready
Check if the instance and all the dependencies are ready to attend to all functionalities.

Web API

http://localhost:5000/health/ready

{
  "status": "Healthy",
  "totalDuration": "00:00:00.2344435",
  "entries": {
    "Sql Server (Ready)": {
      "data": {},
      "duration": "00:00:00.2251420",
      "status": "Healthy",
      "tags": [
        "ready"
      ]
    }
  }
}

Web MVC

http://localhost:7000/health/ready

Readiness

Dump configuration

It is possible to dump the state of the environment configuration in through the middleware resource /dump-config in both applications.

public void Configure(IApplicationBuilder app)
{
    app.UseEndpoints(endpoints =>
        {
            endpoints.MapDumpConfig(
                pattern: "/dump-config",
                configurationRoot: _configuration as IConfigurationRoot,
                isProduction: _env.IsProduction());
        });
}

Highlights

UnitOfWork + Execution Strategy + Transaction Scope

The implementation of the UnitOfWork gives support to the ExecutionStrategy from EF Core with TransactionScope.

operationAsync: Encapsulates all desired transactions;
condition: External control for commitment;
cancellationToken: The cancellation token to be used within operation.

public Task<Review> AddReviewAsync(ReviewModel reviewModel, CancellationToken cancellationToken)
{
    return UnitOfWork.ExecuteInTransactionScopeAsync(
        operationAsync: async ct =>
        {
            var product = await Repository.GetByIdAsync(
                id: reviewModel.ProductId,
                include: products => products.Include(x => x.Reviews),
                asTracking: true,
                cancellationToken: ct);

            var review = Mapper.Map<Review>(reviewModel);
            product?.AddReview(review);
            await OnEditAsync(product, ct);
            return review;
        },
        condition: _ => NotificationContext.AllValidAsync,
        cancellationToken: cancellationToken);      
}

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, 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;
}

Resolving Scoped dependencies with Singleton Schema.

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;

Abstractions

With abstract designs, it is possible to reduce coupling in addition to applying DRY concepts, providing resources for the main behaviors:

...Domain.Abstractions

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

...Repositories.Abstractions

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>();
    }

...Services.Abstractions

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

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;
    }
}

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 {
    items {
      id
      name
    }
  }
}

query byid($productId: Guid!) {
  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: Guid!) {
        product(id: $productId) {
          id
          name
        }
    }"
}

PLAYGROUND

queries


Variables with include, skip and default value

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)
            }
        }
    }"
}

Pagination

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
      }
    }
  }
}

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 - v6 - preview 2

GraphQL Stack - v4

  • 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.
  • Serilog - Serilog provides diagnostic logging to files, the console, and elsewhere.

Contributing

All contributions are welcome. Please take a look at contributing guide.

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository.

Authors

See the list of contributors who participated in this project.

License

This project is licensed under the MIT License - see the LICENSE file for details

About

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.

https://dev.to/antoniofalcao/about-a-net-5-rc1-web-application-with-graphql-3-5d3c

License:MIT License


Languages

Language:C# 93.0%Language:HTML 3.7%Language:CSS 1.9%Language:Dockerfile 1.3%Language:JavaScript 0.1%