Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/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.
https://github.com/antoniofalcaojr/dotnet6.graphql4.webapplication
asp-core-mvc asp-net-core asp-net-core-5 asp-net-mvc5 ddd dotnet dotnet6 dry efcore efcore6 graphql graphql-api graphql-dotnet graphql-server onion-architecture solid web-mvc webapi
Last synced: about 1 month ago
JSON representation
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.
- Host: GitHub
- URL: https://github.com/antoniofalcaojr/dotnet6.graphql4.webapplication
- Owner: AntonioFalcaoJr
- License: mit
- Created: 2020-07-25T22:04:58.000Z (over 4 years ago)
- Default Branch: release
- Last Pushed: 2023-02-21T08:57:48.000Z (over 1 year ago)
- Last Synced: 2024-10-11T13:20:14.073Z (about 1 month ago)
- Topics: asp-core-mvc, asp-net-core, asp-net-core-5, asp-net-mvc5, ddd, dotnet, dotnet6, dry, efcore, efcore6, graphql, graphql-api, graphql-dotnet, graphql-server, onion-architecture, solid, web-mvc, webapi
- Language: C#
- Homepage: https://dev.to/antoniofalcao/about-a-net-5-rc1-web-application-with-graphql-3-5d3c
- Size: 4.52 MB
- Stars: 75
- Watchers: 2
- Forks: 24
- Open Issues: 11
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
# 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! :star:
|WebAPI|
|:----:|
|[![Build/Test](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/workflows/Build/Test/badge.svg)](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/actions/workflows/build-and-test.yml) [![API Image](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/workflows/API%20Image/badge.svg)](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/actions/workflows/build-and-test.yml) [![Push API](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/workflows/Push%20API/badge.svg?branch=master)](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/actions/workflows/webapi-docker-publish.yml) [![CodeQL](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/workflows/CodeQL/badge.svg)](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/actions/workflows/codeql-analysis.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/35ba87f59d524982baeb803a3a9c42f7)](https://www.codacy.com/gh/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/dashboard?utm_source=github.com&utm_medium=referral&utm_content=AntonioFalcao/Dotnet6.GraphQL4.WebApplication&utm_campaign=Badge_Grade) [![](https://img.shields.io/docker/pulls/antoniofalcaojr/dotnet6-graphql4-webapi?style=flat)](https://hub.docker.com/repository/docker/antoniofalcaojr/dotnet6-graphql4-webapi)|
|**WebMVC**|
|[![Build/Test](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/workflows/Build/Test/badge.svg)](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/actions/workflows/build-and-test.yml) [![MVC Image](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/workflows/MVC%20Image/badge.svg)](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/actions/workflows/webmvc-docker-image.yml) [![Push MVC](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/workflows/Push%20MVC/badge.svg?branch=master)](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/actions/workflows/webmvc-docker-publish.yml) [![CodeQL](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/workflows/CodeQL/badge.svg)](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/actions/workflows/codeql-analysis.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/35ba87f59d524982baeb803a3a9c42f7)](https://www.codacy.com/gh/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/dashboard?utm_source=github.com&utm_medium=referral&utm_content=AntonioFalcao/Dotnet6.GraphQL4.WebApplication&utm_campaign=Badge_Grade) [![](https://img.shields.io/docker/pulls/antoniofalcaojr/dotnet6-graphql4-webmvc?style=flat)](https://hub.docker.com/repository/docker/antoniofalcaojr/dotnet6-graphql4-webmvc)|**Oldest version**: Dotnet**5**.GraphQL**3**.WebApplication
|WebAPI|[![](https://img.shields.io/docker/pulls/antoniofalcaojr/dotnet5-graphql3-webapi?style=flat)](https://hub.docker.com/repository/docker/antoniofalcaojr/dotnet5-graphql3-webapi)|
|:----:|--- |
|**WebMVC**|[![](https://img.shields.io/docker/pulls/antoniofalcaojr/dotnet5-graphql3-webmvc?style=flat)](https://hub.docker.com/repository/docker/antoniofalcaojr/dotnet5-graphql3-webmvc)|![home](./.assets/img/home.PNG)
---
![diagram](./.assets/img/diagram.png)
---
## Running
### Development (secrets)
To configure database resource, `init` secrets in [`./src/Dotnet6.GraphQL4.Store.WebAPI`](./src/Dotnet6.GraphQL4.Store.WebAPI), and then define the `DefaultConnection`:```bash
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`](./src/Dotnet6.GraphQL4.Store.WebMVC) and define **Store** client host:
```bash
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`](./src/Dotnet6.GraphQL4.Store.WebAPI/appsettings.Development.json) and WebMVC [`appsettings.Development.json`](./src/Dotnet6.GraphQL4.Store.WebMVC/appsettings.Development.json) files:
WebAPI
```json5
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost,1433;Database=Store;User=sa;Password=!MyComplexPassword"
}
}
```WebMCV
```json5
{
"HttpClient": {
"Store": "http://localhost:5000"
}
}
```### Production
Considering use Docker for CD (Continuous Deployment). On respective [compose](./docker-compose.yml) both web applications and sql server are in the same network, and then we can use named hosts. Already defined on WebAPI [`appsettings.json`](./src/Dotnet6.GraphQL4.Store.WebAPI/appsettings.json) and WebMVC [`appsettings.json`](./src/Dotnet6.GraphQL4.Store.WebMVC/appsettings.json) files:
#### AppSettings
WebAPI
```json5
{
"ConnectionStrings": {
"DefaultConnection": "Server=mssql;Database=Store;User=sa;Password=!MyComplexPassword"
}
}
```WebMCV
```json5
{
"HttpClient": {
"Store": "http://webapi:5000"
}
}
```### Docker
The [`./docker-compose.yml`](./docker-compose.yml) provide the `WebAPI`, `WebMVC` and `MS SQL Server` applications:
```bash
docker-compose up -d
```It's possible to run without a clone of the project using the respective compose:
```yaml
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:
- graphqlstorewebapi:
container_name: webapi
image: antoniofalcaojr/dotnet6-graphql4-webapi
environment:
- ASPNETCORE_URLS=http://*:5000
ports:
- 5000:5000
depends_on:
mssql:
condition: service_healthy
networks:
- graphqlstorewebmvc:
container_name: webmvc
image: antoniofalcaojr/dotnet6-graphql4-webmvc
environment:
- ASPNETCORE_URLS=http://*:7000
ports:
- 7000:7000
depends_on:
- webapi
networks:
- graphqlstorehealthchecks:
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:
- graphqlstorenetworks:
graphqlstore:
driver: bridge
```
### GraphQL PlaygroundBy default **Playground** respond at `http://localhost:5000/ui/playground` but is possible configure the host and many others details in [`../DependencyInjection/Extensions/ApplicationBuilderExtensions.cs`](./src/Dotnet6.GraphQL4.Store.WebAPI/DependencyInjection/Extensions/ApplicationBuilderExtensions.cs)
```c#
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`
```json
{
"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](./.assets/img/ReadinessMVC.png)
### Dump configuration
It is possible to dump the state of the environment configuration in through the middleware resource `/dump-config` in both applications.
```c#
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.```c#
public Task 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(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`](./src/Dotnet6.GraphQL4.CrossCutting/Notifications/NotificationContext.cs) 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`.
```c#
protected bool OnValidate(TEntity entity, AbstractValidator 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`](./src/Dotnet6.GraphQL4.Store.WebAPI/Graphs/Executers/StoreGraphQLExecuter.cs):
```c#
public override async Task ExecuteAsync(string operationName, string query, Inputs variables, IDictionary context, IServiceProvider requestServices, CancellationToken cancellationToken = new CancellationToken())
{
var result = await base.ExecuteAsync(operationName, query, variables, context, requestServices, cancellationToken);
var notification = requestServices.GetRequiredService();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`](./src/Dotnet6.GraphQL4.Store.WebAPI/Graphs/Executers/StoreGraphQLExecuter.cs) define the _service provider_ that will be used from `resolvers` on `fields`:~~
```c#
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`](./src/Dotnet6.GraphQL4.Domain.Abstractions)
```c#
public abstract class Entity
where TId : struct
``````c#
public abstract class Builder : IBuilder
where TBuilder : Builder
where TEntity : Entity
where TId : struct
```
[`...Repositories.Abstractions`](./src/Dotnet6.GraphQL4.Repositories.Abstractions/Repository.cs)```c#
public abstract class Repository : IRepository
where TEntity : Entity
where TId : struct
{
private readonly DbSet _dbSet;protected Repository(DbContext dbDbContext)
{
_dbSet = dbDbContext.Set();
}
```[`...Services.Abstractions`](./src/Dotnet6.GraphQL4.Services.Abstractions/Service.cs)
```c#
public abstract class Service : IService
where TEntity : Entity
where TModel : Model
where TId : struct
{
protected readonly IMapper Mapper;
protected readonly INotificationContext NotificationContext;
protected readonly IRepository Repository;
protected readonly IUnitOfWork UnitOfWork;protected Service(
IUnitOfWork unitOfWork,
IRepository repository,
IMapper mapper,
INotificationContext notificationContext)
{
UnitOfWork = unitOfWork;
Repository = repository;
Mapper = mapper;
NotificationContext = notificationContext;
}
``````c#
public abstract class MessageService : IMessageService
where TMessage : class
where TModel : Model
where TId : struct
{
private readonly IMapper _mapper;
private readonly ISubject _subject;protected MessageService(IMapper mapper, ISubject subject)
{
_mapper = mapper;
_subject = subject;
}
```### From `EF TPH` to `GraphQL Interface`
ENTITY
```c#
public class ProductConfig : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder
.HasDiscriminator()
.HasValue(nameof(Boot))
.HasValue(nameof(Kayak))
.HasValue(nameof(Backpack));
}
}
```INHERITOR
```c#
public class KayakConfig : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder
.HasBaseType();
}
}
```INTERFACE
```c#
public sealed class ProductInterfaceGraphType : InterfaceGraphType
{
public ProductInterfaceGraphType(BootGraphType bootGraphType, BackpackGraphType backpackGraphType, KayakGraphType kayakGraphType)
{
Name = "product";ResolveType = @object =>
{
return @object switch
{
Boot _ => bootGraphType,
Backpack _ => backpackGraphType,
Kayak _ => kayakGraphType,
_ => default
};
};
}
}
```OBJECT
```c#
public sealed class KayakGraphType : ObjectGraphType
{
public KayakGraphType()
{
Name = "kayak";
Interface();
IsTypeOf = o => o is Product;
}
}
```___
## Queries
#### Fragment for comparison and Arguments
QUERY
```markdown
{
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```json5
{
"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
```markdown
query all {
products {
items {
id
name
}
}
}query byid($productId: Guid!) {
product(id: $productId) {
id
name
}
}
```VARIABLES
```json5
{
"productId": "2c05b59b-8fb3-4cba-8698-01d55a0284e5"
}
```HTTP BODY
```markdown
{
"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](./.assets/img/queries.PNG)
___
#### Variables with include, skip and default valueQUERY
```markdown
query all($showPrice: Boolean = false) {
products {
items {
id
name
price @include(if: $showPrice)
rating @skip(if: $showPrice)
}
}
}
```VARIABLES
```json5
{
"showPrice": true
}
```HTTP BODY
```markdown
{
"operationName": "all",
"variables": {
"showPrice": false
},
"query": "query all($showPrice: Boolean = false) {
products {
items {
id
name
price @include(if: $showPrice)
rating @skip(if: $showPrice)
}
}
}"
}
```
___
### PaginationQUERY
```markdown
{
products(pageParams: { index: 2, size: 1 }) {
items {
id
}
pageInfo {
current
hasNext
hasPrevious
size
}
}
}
```RESULT
```json5
{
"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.
```markdown
mutation($review: reviewInput!) {
createReview(review: $review) {
id
}
}
```
VARIABLES```json5
{
"review": {
"title": "some title",
"comment": "some comment",
"productId": "0fb8ec7e-7af1-4fe3-a2e2-000996ffd20f"
}
}
```RESULT
```json5
{
"data": {
"createReview": {
"title": "some title"
}
}
}
```
___## Subscriptions
SUBSCRIPTION
> The Mutation stay listening if a new review is added.
```markdown
subscription {
reviewAdded {
title
}
}```
RESULT```json5
{
"data": {
"reviewAdded": {
"title": "Some title"
}
}
}
```
___## Built With
### Microsoft Stack - v6 - preview 2
* [.NET 6](https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-2/) - Base framework;
* [ASP.NET 6](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-preview-2/) - Web framework;
* [Entity Framework Core 6](https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-6-0-preview-2/) - ORM;
* [Microsoft SQL Server on Linux for Docker](https://hub.docker.com/_/microsoft-mssql-server) - Database.### GraphQL Stack - v4
* [GraphQL](https://graphql.org/) - GraphQL is a query language for APIs and a runtime for fulfilling those queries with data;
* [GraphQL for .NET](https://github.com/graphql-dotnet/graphql-dotnet/) - This is an implementation of GraphQL in .NET;
* [GraphQL Client](https://github.com/graphql-dotnet/graphql-client) - A GraphQL Client for .NET over HTTP;
* [GraphQL Playground](https://github.com/prisma-labs/graphql-playground/) - GraphQL IDE for better development workflows.### Community Stack
* [AutoMapper](https://automapper.org/) - A convention-based object-object mapper;
* [FluentValidation](https://fluentvalidation.net/) - A popular .NET library for building strongly-typed validation rules;
* [Bogus](https://github.com/bchavez/Bogus) - A simple and sane fake data generator for C#, F#, and VB.NET;
* [Bootstrap](https://getbootstrap.com/) - The most popular HTML, CSS, and JS library in the world.
* [Serilog](https://serilog.net/) - Serilog provides diagnostic logging to files, the console, and elsewhere.## Contributing
All contributions are welcome. Please take a look at [contributing](./CONTRIBUTING.md) guide.
## Versioning
We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/tags).
## Authors
> See the list of [contributors](https://github.com/AntonioFalcao/Dotnet6.GraphQL4.WebApplication/graphs/contributors) who participated in this project.
## License
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details