Skip to content

This project exemplify the implementation and Dockerization of a simple Razor Web MVC Core consuming an 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.

License

Notifications You must be signed in to change notification settings

DBollella/Dotnet5.GraphQL3.WebApplication

 
 

Repository files navigation

Dotnet5.GraphQL3.WebApplication

This project exemplify the implementation and Dockerization of a simple Razor Web MVC Core consuming an 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.

CI WebAPI Docker Image WebMVC Docker Image


home


Environment configuration

Development

Secrets

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

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, 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:

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

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

queries


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)

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.

Contributing

Available soon!

Versioning

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

Authors

See also the list of contributors who participated in this project.

License

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

Acknowledgments

  • Nothing more, for now.

About

This project exemplify the implementation and Dockerization of a simple Razor Web MVC Core consuming an 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.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 92.3%
  • HTML 4.1%
  • CSS 2.1%
  • Dockerfile 1.4%
  • JavaScript 0.1%