Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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.

License

NotificationsYou must be signed in to change notification settings

AntonioFalcaoJr/Dotnet6.GraphQL4.WebApplication

This project exemplifies the implementation anddockerization of a simple Razor Web MVC Core consuming a fullGraphQL 4 Web API, build in a.NET 6 multi-layer project, considering development best practices, likeSOLID,KISS andDRY, applyingDomain-Driven concepts in aOnion Architecture.

Give a Star! ⭐

WebAPI
Build/TestAPI ImagePush APICodeQLCodacy Badge
WebMVC
Build/TestMVC ImagePush MVCCodeQLCodacy 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 theDefaultConnection:

dotnet user-secrets initdotnet user-secretsset"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 defineStore client host:

dotnet user-secrets initdotnet user-secretsset"HttpClient:Store""http://localhost:5000"
AppSettings

If you prefer, is possible to define it on WebAPIappsettings.Development.json and WebMVCappsettings.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 respectivecompose both web applications and sql server are in the same network, and then we can use named hosts. Already defined on WebAPIappsettings.json and WebMVCappsettings.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 theWebAPI,WebMVC andMS 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:mssqlimage:mcr.microsoft.com/mssql/serverports:      -1433:1433environment:SA_PASSWORD:"!MyComplexPassword"ACCEPT_EULA:"Y"healthcheck:test:/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$$SA_PASSWORD" -Q "SELECT 1" || exit 1interval:10stimeout:3sretries:10start_period:10snetworks:      -graphqlstorewebapi:container_name:webapiimage:antoniofalcaojr/dotnet6-graphql4-webapienvironment:      -ASPNETCORE_URLS=http://*:5000ports:      -5000:5000depends_on:mssql:condition:service_healthynetworks:      -graphqlstorewebmvc:container_name:webmvcimage:antoniofalcaojr/dotnet6-graphql4-webmvcenvironment:      -ASPNETCORE_URLS=http://*:7000ports:      -7000:7000depends_on:      -webapinetworks:      -graphqlstorehealthchecks:container_name:healthchecks-uiimage:xabarilcoding/healthchecksuidepends_on:mssql:condition:service_healthyenvironment:      -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/healthzports:      -8000:80networks:      -graphqlstorenetworks:graphqlstore:driver:bridge

GraphQL Playground

By defaultPlayground respond athttp://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 andLiveness 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.

publicvoidConfigure(IApplicationBuilderapp){app.UseEndpoints(endpoints=>{endpoints.MapDumpConfig(pattern:"/dump-config",configurationRoot:_configurationasIConfigurationRoot,isProduction:_env.IsProduction());});}

Highlights

UnitOfWork + Execution Strategy + Transaction Scope

The implementation of theUnitOfWork gives support to theExecutionStrategy fromEF Core withTransactionScope.

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

publicTask<Review>AddReviewAsync(ReviewModelreviewModel,CancellationTokencancellationToken){returnUnitOfWork.ExecuteInTransactionScopeAsync(operationAsync:async ct=>{varproduct=awaitRepository.GetByIdAsync(id:reviewModel.ProductId,include: products=>products.Include(x=>x.Reviews),asTracking:true,cancellationToken:ct);varreview=Mapper.Map<Review>(reviewModel);product?.AddReview(review);awaitOnEditAsync(product,ct);returnreview;},condition: _=>NotificationContext.AllValidAsync,cancellationToken:cancellationToken);}

Notifications (pattern/context)

To avoid handle exceptions, was implemented aNotificationContext that's allow all layers add business notifications through the request, with support to receiveDomain notifications, that by other side, implementing validators fromFluent Validation and return aValidationResult.

protectedboolOnValidate<TEntity>(TEntityentity,AbstractValidator<TEntity>validator){ValidationResult=validator.Validate(entity);returnIsValid;}protectedvoidAddError(stringerrorMessage,ValidationResultvalidationResult=default){ValidationResult.Errors.Add(newValidationFailure(default,errorMessage));validationResult?.Errors.ToList().ForEach(failure=>ValidationResult.Errors.Add(failure));}

To theGraphQL the notification context delivery aExecutionErrors that is propagated toresult from execution by a personalisedExecuter:

publicoverrideasyncTask<ExecutionResult>ExecuteAsync(stringoperationName,stringquery,Inputsvariables,IDictionary<string,object>context,IServiceProviderrequestServices,CancellationTokencancellationToken=newCancellationToken()){varresult=awaitbase.ExecuteAsync(operationName,query,variables,context,requestServices,cancellationToken);varnotification=requestServices.GetRequiredService<INotificationContext>();if(notification.HasNotificationsisfalse)returnresult;result.Errors=notification.ExecutionErrors;result.Data=default;returnresult;}

ResolvingScoped dependencies withSingleton Schema.

It's no more necessary after version 4.2.0 fromGraphQL Server. By default, the Service Provider is already being propagated.

Is necessary, in the same personalisedExecuter define theservice provider that will be used fromresolvers onfields:

varoptions=base.GetOptions(operationName,query,variables,context,cancellationToken);options.RequestServices=_serviceProvider;

Abstractions

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

...Domain.Abstractions

publicabstractclassEntity<TId>whereTId:struct
publicabstractclassBuilder<TBuilder,TEntity,TId>:IBuilder<TEntity,TId>whereTBuilder:Builder<TBuilder,TEntity,TId>whereTEntity:Entity<TId>whereTId:struct

...Repositories.Abstractions

publicabstractclassRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:struct{privatereadonlyDbSet<TEntity>_dbSet;protectedRepository(DbContextdbDbContext){_dbSet=dbDbContext.Set<TEntity>();}

...Services.Abstractions

publicabstractclassService<TEntity,TModel,TId>:IService<TEntity,TModel,TId>whereTEntity:Entity<TId>whereTModel:Model<TId>whereTId:struct{protectedreadonlyIMapperMapper;protectedreadonlyINotificationContextNotificationContext;protectedreadonlyIRepository<TEntity,TId>Repository;protectedreadonlyIUnitOfWorkUnitOfWork;protectedService(IUnitOfWorkunitOfWork,IRepository<TEntity,TId>repository,IMappermapper,INotificationContextnotificationContext){UnitOfWork=unitOfWork;Repository=repository;Mapper=mapper;NotificationContext=notificationContext;}
publicabstractclassMessageService<TMessage,TModel,TId>:IMessageService<TMessage,TModel,TId>whereTMessage:classwhereTModel:Model<TId>whereTId:struct{privatereadonlyIMapper_mapper;privatereadonlyISubject<TMessage>_subject;protectedMessageService(IMappermapper,ISubject<TMessage>subject){_mapper=mapper;_subject=subject;}

FromEF TPH toGraphQL Interface

ENTITY

publicclassProductConfig:IEntityTypeConfiguration<Product>{publicvoidConfigure(EntityTypeBuilder<Product>builder){builder.HasDiscriminator().HasValue<Boot>(nameof(Boot)).HasValue<Kayak>(nameof(Kayak)).HasValue<Backpack>(nameof(Backpack));}}

INHERITOR

publicclassKayakConfig:IEntityTypeConfiguration<Kayak>{publicvoidConfigure(EntityTypeBuilder<Kayak>builder){builder.HasBaseType<Product>();}}

INTERFACE

publicsealedclassProductInterfaceGraphType:InterfaceGraphType<Product>{publicProductInterfaceGraphType(BootGraphTypebootGraphType,BackpackGraphTypebackpackGraphType,KayakGraphTypekayakGraphType){Name="product";ResolveType= @object=>{return@objectswitch{Boot _=>bootGraphType,Backpack _=>backpackGraphType,Kayak _=>kayakGraphType,                _=>default};};}}

OBJECT

publicsealedclassKayakGraphType:ObjectGraphType<Kayak>{publicKayakGraphType(){Name="kayak";Interface<ProductInterfaceGraphType>();IsTypeOf= o=>oisProduct;}}

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

Versioning

We useSemVer for versioning. For the versions available, see thetags on this repository.

Authors

See the list ofcontributors who participated in this project.

License

This project is licensed under the MIT License - see theLICENSE 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.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors4

  •  
  •  
  •  
  •  

[8]ページ先頭

©2009-2025 Movatter.jp