- Notifications
You must be signed in to change notification settings - Fork25
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
AntonioFalcaoJr/Dotnet6.GraphQL4.WebApplication
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
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.
| WebAPI |
|---|
| WebMVC |
Oldest version: Dotnet5.GraphQL3.WebApplication
| WebAPI | |
|---|---|
| WebMVC |
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"
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"}}
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:
WebAPI
{"ConnectionStrings":{"DefaultConnection":"Server=mssql;Database=Store;User=sa;Password=!MyComplexPassword"}}
WebMCV
{"HttpClient":{"Store":"http://webapi:5000"}}
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
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");
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
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());});}
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);}
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;}
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;
With abstract designs, it is possible to reduce coupling in addition to applyingDRY concepts, providing resources for the main behaviors:
publicabstractclassEntity<TId>whereTId:struct
publicabstractclassBuilder<TBuilder,TEntity,TId>:IBuilder<TEntity,TId>whereTBuilder:Builder<TBuilder,TEntity,TId>whereTEntity:Entity<TId>whereTId:struct
publicabstractclassRepository<TEntity,TId>:IRepository<TEntity,TId>whereTEntity:Entity<TId>whereTId:struct{privatereadonlyDbSet<TEntity>_dbSet;protectedRepository(DbContextdbDbContext){_dbSet=dbDbContext.Set<TEntity>();}
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;}
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;}}
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
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
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) } } }"}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}}}}
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"}}}
SUBSCRIPTION
The Mutation stay listening if a new review is added.
subscription { reviewAdded { title }}RESULT
{"data":{"reviewAdded":{"title":"Some title"}}}
- .NET 6 - Base framework;
- ASP.NET 6 - Web framework;
- Entity Framework Core 6 - ORM;
- Microsoft SQL Server on Linux for Docker - Database.
- 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.
- 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.
All contributions are welcome. Please take a look atcontributing guide.
We useSemVer for versioning. For the versions available, see thetags on this repository.
See the list ofcontributors who participated in this project.
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
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors4
Uh oh!
There was an error while loading.Please reload this page.

