DEV Community

Cover image for About a .NET 5 (RC1) Web application with GraphQL 3
Antonio Falcão Jr.
Antonio Falcão Jr.

Posted on • Edited on

About a .NET 5 (RC1) Web application with GraphQL 3

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.

GitHub logo 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


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));
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
public abstract class Builder<TBuilder, TEntity, TId> : IBuilder<TEntity, TId>
    where TBuilder : Builder<TBuilder, TEntity, TId>
    where TEntity : Entity<TId>
    where TId : struct
Enter fullscreen mode Exit fullscreen mode

...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>();
    }
Enter fullscreen mode Exit fullscreen mode

...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;
    }
Enter fullscreen mode Exit fullscreen mode
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;
    }
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

INHERITOR

public class KayakConfig : IEntityTypeConfiguration<Kayak>
{
    public void Configure(EntityTypeBuilder<Kayak> builder)
    {
        builder
            .HasBaseType<Product>();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            };
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

OBJECT

public sealed class KayakGraphType : ObjectGraphType<Kayak>
{
    public KayakGraphType()
    {
        Name = "kayak";
        Interface<ProductInterfaceGraphType>();
        IsTypeOf = o => o is Product;
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
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"
  }
}
Enter fullscreen mode Exit fullscreen mode

WebMCV

{
  "HttpClient": {
    "Store": "http://localhost:5000/graphql"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

WebMCV

{
  "HttpClient": {
    "Store": "http://webapi:5000/graphql"
  }
}
Enter fullscreen mode Exit fullscreen mode

Running

The ./docker-compose.yml provide the WebAPI, WebMVC and MS SQL Server applications:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

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"
    });
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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."
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Query named's and Variables

QUERY

query all {
  products {
    id
    name
  }
}

query byid($productId: ID!) {
  product(id: $productId) {
    id
    name
  }
}
Enter fullscreen mode Exit fullscreen mode

VARIABLES

{
  "productId": "2c05b59b-8fb3-4cba-8698-01d55a0284e5"
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }"
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

VARIABLES

{
  "showPrice": true
}
Enter fullscreen mode Exit fullscreen mode

HTTP BODY

{
    "operationName": "all",
    "variables": {
        "showPrice": false
    },
    "query": "query all($showPrice: Boolean = false) {
          products {
            id
            name
            price @include(if: $showPrice)
            rating @skip(if: $showPrice)
          }
    }"
}
Enter fullscreen mode Exit fullscreen mode

Mutations

MUTATION

Creating / adding a new Review to the respective product.

mutation($review: reviewInput!) {
  createReview(review: $review) {
    id
  }
}
Enter fullscreen mode Exit fullscreen mode

VARIABLES

{
  "review": {
    "title": "some title",
    "comment": "some comment",
    "productId": "0fb8ec7e-7af1-4fe3-a2e2-000996ffd20f"
  }
}
Enter fullscreen mode Exit fullscreen mode

RESULT

{
  "data": {
    "createReview": {
      "title": "some title"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Subscriptions

SUBSCRIPTION

The Mutation stay listening if a new review is added.

subscription {
  reviewAdded {
    title
  }
}

Enter fullscreen mode Exit fullscreen mode

RESULT

{
  "data": {
    "reviewAdded": {
      "title": "Some title"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

GitHub logo 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.

Top comments (5)

Collapse
 
azmimansur profile image
AZMIMANSUR

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

Collapse
 
antoniofalcaojr profile image
Antonio Falcão Jr. • Edited

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:

dotnet ef migrations add "new_migration" -p .\src\Dotnet5.GraphQL3.Store.Repositories\ -s .\src\Dotnet5.GraphQL3.Store.WebAPI\
Enter fullscreen mode Exit fullscreen mode
Collapse
 
azmimansur profile image
AZMIMANSUR • Edited

Thanks, Antonio

Working now.

Thread Thread
 
azmimansur profile image
AZMIMANSUR

Hi Antonio,

Question again.

I created a service below to update, I'm still confused about how to mapper changed between clientModel with client ?

public async Task<Client> EditClientAsync(Guid id, ClientModel clientModel, CancellationToken cancellationToken)
{
    if (clientModel is null)
    {
        NotificationContext.AddNotificationWithType(ServicesResource.Object_Null, typeof(ClientModel));
        return default;
    }

    var client = await Repository.GetByIdAsync(
        id: id,
        asTracking: true,
        cancellationToken: cancellationToken);

    //Mapper ?

    var updateClient = await OnEditAsync(client, cancellationToken);

    return updateClient;
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
antoniofalcaojr profile image
Antonio Falcão Jr.

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 an ObjectGraphType<T> for output.

public class StoreMutation : ObjectGraphType
{
    public StoreMutation()
    {
            FieldAsync<ClientModelGraphType>(
                name: "createClient",
                arguments: new QueryArguments(new QueryArgument<NonNullGraphType<ClientInputGraphType>> {Name = "client"}),
                resolve: async context 
                    => await context.RequestServices
                        .GetRequiredService<IClientService>()
                        .SaveAsync(
                            model: context.GetArgument<ClientModel>("client"), 
                            cancellationToken: context.CancellationToken));
    }
}
Enter fullscreen mode Exit fullscreen mode

Obs: Is necessary to create a new migration including the new table "Clients".