GraphQL
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools. It is a single API face for different clients like mobile, desktop apps, tablets. ASP.NET Zero includes an implementation of GraphQL API. In the solution the *.GraphQL project exposes some built-in queries like Organization Units, Roles and Users . There is also a unit test project *.GraphQL.Tests in the test
folder. You can add your own GraphQL unit tests to this project.
For more information about GraphQL visit https://graphql.org/
GraphQL Structure
The *.GraphQL project consists of a MainSchema
. In the MainSchema
there is QueryContainer
that hosts all queries. Out of the box, there are queries for Roles
, Organization Units
and Users
.
The .GraphQL project depends on AbpZeroTemplateCoreModule
. It is used in the *.Web.Host and *.Web.Mvc projects. So both ASP.NET Core Angular
and ASP.NET Core MVC
templates contain GraphQL project.
The GraphQL is being added in the Configure
method of *.Web.Host.Startup for Angular
project and *.Web.Mvc.Startup for MVC
project.
There is also GraphQL Playground included in the project. GraphQL Playground is an IDE like Swagger, helps you to write your queries directly from your website. The default endpoint of the playground is http://your-domain.com/ui/playground
app.UseGraphQL<MainSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
The GraphQL middleware is being added in ConfigureService
method of *.Web.Host.Startup for Angular
project and *.Web.Mvc.Startup for MVC
project.
services.AddAndConfigureGraphQL();
AddAndConfigureGraphQL()
is an extension method to add the GraphQL middleware and makes necessary configurations. In debug mode, all GraphQL exceptions are being sent to the client but in release mode exceptions are not being sent to the client.
Enabling GraphQL
By default, GraphQL is disabled. To enable the GraphQL, open WebConsts.cs
in the *.Web.Core project and set GraphQL.Enabled = true
.
Web.Core/Common/WebConsts.cs
public static class WebConsts
{
//ignored...
public static class GraphQL
{
//ignored...
public static bool Enabled = true;
}
}
GraphQL Endpoint
The default GraphQL endpoints are;
Changing The Default GraphQL Endpoint
You can change the default endpoint of the GraphQL. To do this, open the Startup.cs
in the *.Web.Host project for Angular
or in the *.Web.Mvc project for MVC
. In the Configure
method, find the line app.UseGraphQL<MainSchema>()
. And set the path
parameter like below;
app.UseGraphQL<MainSchema>(path: "/mygraphql");
This will change your endpoint to yourdomain.com/mygraphql
.
GraphQL Playground
GraphQL Playground is an extension tool that helps you write your GraphQL queries easily. It supports autocompletion & error highlighting. It is included AspNet Zero but disabled by default. The default GraphQL Playground endpoints are;
There is also a desktop version of the GraphQL Playground. The desktop app is the same as the web version but includes these additional features:
- Partial support for graphql-config enabling features like multi-environment setups (no support for sending HTTP headers).
- Double click
*.graphql
files to open.
To download GraphQL Desktop version click on the following link;
https://github.com/prisma/graphql-playground/releases
Enabling GraphQL Playground
By default, GraphQL Playground is disabled. To enable it, open WebConsts.cs
in the *.Web.Core project and set GraphQL.PlaygroundEnabled=true
in the WebConsts.GraphQL
class.
Web.Core/Common/WebConsts.cs
public static class WebConsts
{
//ignored...
public static class GraphQL
{
//ignored...
public static bool PlaygroundEnabled = true;
}
}
Changing GraphQL Playground Default Endpoint
You can change the default endpoint of the GraphQL Playground. To do this, open Startup.cs
of the *.Web.Host for Angular
or the *.Web.Mvc for MVC
. In the Configure
method, find the lineapp.UseGraphQLPlayground()
and set the Path
property of GraphQLPlaygroundOptions
;
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions
{
Path = new PathString("/myplayground")
});
This will change your playground endpoint to yourdomain.com/myplayground
.
GraphQL Playground Settings
To be able to run the queries that requires authentication, you need to open Playground on your browser, open settings tab and change the request.credentials
from omit
to include
. This is needed for cookie authentication.
On the other hand, Playground uses polling to update the schema and refreshes the schema every 2 seconds. You can also safely disable this option to not see flooding XHR requests on your browser's network tab.
The necessary changes are;
{
"request.credentials": "include",
"schema.polling.enable": false
}
After these settings, you need to save it by pressing SAVE SETTINGS
button.
Running Queries on GraphQL Playground
Playground gives you the advantage of writing the queries with intellisense and see the results on your browser. There are two types of queries; Queries require authentication and Queries don't require authentication. For the queries that don't require authentication, you can directly write the query on the Playground and see the results. But for the authentication required queries you have to authenticate first. To authenticate, read the next section of the project that is relevant to you..
Authenticating Playground Requests in MVC Project
Authenticating With Cookie
MVC project uses cookie authentication. Before running any queries that need authentication, you need to login to your website to retrieve the authentication cookie. Start your *.Web.Mvc project, open your browser and navigate to http://localhost:62114/Account/Login. After successful login, you will have a valid authentication cookie in your browser's storage. Now you can go to http://localhost:62114/ui/playground and run your query as authenticated.
Authenticating Playground Requests in Angular Project
Angular project uses both token (Bearer) authentication and cookie authentication. The simplest way is authenticating with cookie.
Authenticating With Cookie
Start your *.Web.Host project and go to http://localhost:22742/ui/login. After the successful login, you will see the GraphQL Playground box in the redirected page. Click the link that goes to http://localhost:22742/ui/playground and run your query as authenticated.
Authenticating With Token
Alternatively you can use token authentication in Angular
project.
Make a post request to http://localhost:22742/api/TokenAuth/Authenticate to get the token.
See the below request to get the token:
POST /api/TokenAuth/Authenticate HTTP/1.1
Host: localhost:22742
Content-Type: application/json
cache-control: no-cache
{
TenancyName: "",
UserNameOrEmailAddress: "admin",
Password: "123qwe"
}
You will get a similar response like below. You will use accessToken
value.
{
"result": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJBc3BOZXQuSWRlbnRpdHkuU2VjdXJpdHlTdGFtcCI6IktGWDUzQldLWlNZNUE1TjNQM0xYRFM2S1NUNUIyRDdPIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbIkFkbWluIiwiMjU2ZTc4NmVmZmFmNDJmZDk5N2MyNTk5MTNjNjEyNDciXSwic3ViIjoiMSIsImp0aSI6ImZkMmY3NWFhLWNiMWItNDlmMi1iYjg2LWZjNTNiMmZhYTI3NyIsImlhdCI6MTU1MjYzOTM5NiwidG9rZW5fdmFsaWRpdHlfa2V5IjoiNjFhOGVkNjYtMTMwYS00ODhhLWEwMTAtYThkMDI2M2RiNTUxIiwidXNlcl9pZGVudGlmaWVyIjoiMSIsIm5iZiI6MTU1MjYzOTM5NiwiZXhwIjoxNTUyNzI1Nzk2LCJpc3MiOiJBYnBaZXJvVGVtcGxhdGUiLCJhdWQiOiJBYnBaZXJvVGVtcGxhdGUifQ.nGcwto1Ih8z15FIZ_4lAXc_Q7Lf1ccJwKDAnGh8AzY8",
"encryptedAccessToken": "wNYmO41/48SHNstaLVXxHCCre29BZQl1NhC6NM3R3rzpXtPQxVzH6jEzA/QhXFN5tu6Fk7pO53uppm1mVXMZgxbyRVz26dnepi/FyB6axBY+6gq1GL+uRQgoiFUCjRN2p8w6LevViwKlHyWZZJZO1DGVSjAi1m2U+og9pkHw9/QR4Nl/DPnoP9JYDMpZ1zxx09u6s0GZ9/Q5Sjk+L0UfcSCbl38X8he5w9UIn/Hvxh7ysM1CiPLsoOwtbiieSRVmrmt0JjnipAn4/K283F8GrGwzwgehWsqefmUnM0ckMwP9ZAdwQxWDhxv0IqNw4tDhwUYs/1SYdYozdNzgByhgNOBPzQDObNLlWc4vV5VMOiajyrvabgoLnMvpYCP0YrrR2Trkgw9U1crOz6qWkGpdxJs8t6le2nIPkkMDOuzJL+9gOpxxPd6pOXWTmPN73/ioJF5xcwxX3KnKQX7IbIlv7MRIHQf0EjGdpCk+TMkVcAAYSOGMv/0AMZSS0LZQYDrDCEy1wQMDhH8FT4VaJDnugINdL8TWD7ZX3lYu3HLvTpyDX59wqSqUuT/M0XaTW+J/7lGWOyz8zzPUXPsID4wDAXGe6vfLrzn0Tz9zyeRbXApmmP0aFJN7xfh96O3H4H7L9QWNefIZjq0UMPGmDkCW7FubMRuLNdJNLs6ItfUDCJcDVppn0hNRJB7mpocPeII4fd06Q7YGdbB3sruMYXx/CQxvZCiohK4q0hQxFXqG7Xb2Ig1xYJOA5qT9JikvS2vVcvDGh/+Shn6FQISBwDGwOBjlJ4EupqQ1XalfAqa4IPUjskHsN1spypNb4WMZvu5gooxES4mFdTiJoPPR7WgpauAdu2CHKr7CLla6fBeHfVbNXgVYPEM/mJFnd++RZj5SYul+6FhwZwNfTixawjknoGX9+vJfeYID5V1F4mjN86FAP4caVp5nAOuycdYjgnc6vdvEXjuZ+KU5nVFFO8U5i+Xd9Hc0qMNllF6aM3TilIHVWQKdESuHdBLfCT7MYyeWu1YNh7LsXuadb7eTctUUzxJ3roUh6tg/hyRh9TE7hGvmvucz+2ZJwfnYfLNeXXnTJjsDKIWwHC9v999e5B2LeA==",
"expireInSeconds": 86400,
"shouldResetPassword": false,
"passwordResetCode": null,
"userId": 1,
"requiresTwoFactorVerification": false,
"twoFactorAuthProviders": null,
"twoFactorRememberClientToken": null,
"returnUrl": null,
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJBc3BOZXQuSWRlbnRpdHkuU2VjdXJpdHlTdGFtcCI6IktGWDUzQldLWlNZNUE1TjNQM0xYRFM2S1NUNUIyRDdPIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjpbIkFkbWluIiwiMjU2ZTc4NmVmZmFmNDJmZDk5N2MyNTk5MTNjNjEyNDciXSwic3ViIjoiMSIsImp0aSI6ImU1NTY3MTkxLWY0YTktNDZjOC04Mjc3LTgyMjdlYjRiZjNmMCIsImlhdCI6MTU1MjYzOTM5NiwidG9rZW5fdmFsaWRpdHlfa2V5IjoiNjJmYWMzNDMtNDhiOS00MTgzLTkyMWItNGM4Njk5YWQ1YzdhIiwidXNlcl9pZGVudGlmaWVyIjoiMSIsIm5iZiI6MTU1MjYzOTM5NiwiZXhwIjoxNTg0MTc1Mzk2LCJpc3MiOiJBYnBaZXJvVGVtcGxhdGUiLCJhdWQiOiJBYnBaZXJvVGVtcGxhdGUifQ.u6cnxzB6Hnd4VpPYt-hOi23f6YlEkNZdw3cnyzi-0go"
},
"targetUrl": null,
"success": true,
"error": null,
"unAuthorizedRequest": false,
"__abp": true
}
Write the accessToken
value to the HTTP headers as seen below.
Sample GraphQL Query
Here's a sample query that retrieves built-in entities like role
, organizationUnit
and user
. You can use this query to test if your GraphQL API is working properly.
query MyQuery {
users(id: 1) {
totalCount
items {
name
surname
roles {
id
name
displayName
}
organizationUnits {
id
code
displayName
}
}
}
roles {
displayName
id
name
tenantId
}
organizationUnits {
id
code
displayName
tenantId
}
}
Be aware that if your authentication is not valid or if you didn't set "request.credentials": "include"
in your Playground settings, you will see the below error response for the authentication required queries.
errors": [
{
"message": "GraphQL.ExecutionError: Error trying to resolve users. ---> Abp.Authorization.AbpAuthorizationException: Current user did not login to the application!\r\n at Abp.Authorization.AuthorizationHelper.AuthorizeAsync(IEnumerable`1 authorizeAttributes) in D:\\Github\\aspnetboilerplate\\src\\Abp\\Authorization\\AuthorizationHelper.cs:line 42\r\n at Abp.Authorization.AuthorizationHelper.CheckPermissions(MethodInfo methodInfo, Type type) in D:\\Github\\aspnetboilerplate\\src\\Abp\\Authorization\\AuthorizationHelper.cs:line 107\r\n at Abp.Authorization.AuthorizationHelper.AuthorizeAsync(MethodInfo methodInfo, Type type) in D:\\Github\\aspnetboilerplate\\src\\Abp\\Authorization\\AuthorizationHelper.cs:line 56\r\n at Nito.AsyncEx.Synchronous.TaskExtensions.WaitAndUnwrapException(Task task)\r\n at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at Nito.AsyncEx.Synchronous.TaskExtensions.WaitAndUnwrapException(Task task)\r\n at Nito.AsyncEx.AsyncContext.Run(Func`1 action)\r\n at Abp.Authorization.AuthorizationInterceptor.Intercept(IInvocation invocation) in D:\\Github\\aspnetboilerplate\\src\\Abp\\Authorization\\AuthorizationInterceptor.cs:line 20\r\n at Castle.DynamicProxy.AbstractInvocation.Proceed()\r\n at Castle.DynamicProxy.AbstractInvocation.Proceed()\r\n at Castle.Proxies.UserQueryProxy.Resolve(ResolveFieldContext`1 context)\r\n at MyCompanyName.AbpZeroTemplate.Core.Base.AbpZeroTemplateQueryBase`2.InternalResolve(ResolveFieldContext`1 context) in D:\\Github\\aspnet-zero-core\\aspnet-core\\src\\MyCompanyName.AbpZeroTemplate.GraphQL\\Core\\Base\\AbpZeroTemplateQueryBase.cs:line 47\r\n at Abp.Threading.InternalAsyncHelper.AwaitTaskWithPostActionAndFinallyAndGetResult[T](/aspnet-core-angular/v8.2.0/Task%601%20actualReturnValue,%20Func%601%20postAction,%20Action%601%20finalAction?LanguageCode=en)\r\n at GraphQL.Instrumentation.MiddlewareResolver.Resolve(ResolveFieldContext context)\r\n at GraphQL.Execution.ExecutionStrategy.ExecuteNodeAsync(ExecutionContext context, ExecutionNode node)\r\n --- End of inner exception stack trace ---",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"users"
],
"extensions": {
"code": "ABP_AUTHORIZATION"
}
}
]
GraphQL Query Execution Flow
When a new query is initiated, the GraphQL middleware parses the query then routes it to the corresponding query handler which is registered in QueryContainer
class of the *.GraphQL project. Out of the box there are 3 query handlers registered in QueryContainer
: RoleQuery,
UserQuery
and OrganizationUnitQuery
. The next step is the Resolve
method of the query with the query context. The query context has all the information about a query like UserContext
, Arguments
, Selections
and Variables
. If there is an [AbpAuthorize]
attribute on the Resolve
method, AspNet Boilerplate framework checks the related permission of the logged on user and throws ABP_AUTHORIZATION
exception when the related permission is not granted. If there is no [AbpAuthorize]
attribute, then it can be run by anonymous users. In the Resolve
method, the necessary query runs and returns a DTO
object. Then the DTO
object is being converted to the relevant IGraphType
. This conversation is made by the GraphQL middleware and the middleware knows which IGraphType
to be converted, as it is registered it in the QueryContainer
.
Writing a New GraphQL Query
Let's create a new GraphQL query. For this example we will use AbpAuditLogs
that stores all logs in the database. Below is a simplified version of the AuditLog
entity.
AuditLog Entity:
public class AuditLog : Entity<long>, IMayHaveTenant
{
//...ignored other properties
public virtual int? TenantId { get; set; }
public virtual long? UserId { get; set; }
public virtual string ServiceName { get; set; }
public virtual DateTime ExecutionTime { get; set; }
}
Create a new
DTO
class. Name itAuditLogDto.cs
, locate it in theDTO
folder of the *.GraphQL project. Add the[AutoMapFrom(typeof(AuditLog))]
attribute to register theAuditLogDto
for mapping. This will be used to map from theAuditLog
entity toAuditLogDto
.AuditLogDto.cs
[AutoMapFrom(typeof(AuditLog))] public class AuditLogDto : EntityDto<long> { public int? TenantId { get; set; } public long? UserId { get; set; } public string ServiceName { get; set; } public DateTime ExecutionTime { get; set; } }
Create a new class in the
Types
folder of the *.GraphQL project and name itAuditLogType.cs
. Derive this class fromObjectGraphType<AuditLogDto>
.AuditLogType
will be used to map from theAuditLogDto
class.Be careful with the
nullable
types. You must explicitly define thenullable
field.AuditLogType.cs
public class AuditLogType : ObjectGraphType<AuditLogDto> { public AuditLogType() { Field(x => x.Id); Field(x => x.UserId, nullable: true); Field(x => x.TenantId, nullable: true); Field(x => x.ExecutionTime); Field(x => x.ServiceName); } }
Create a new class and name it
AuditLogQuery.cs
in theQueries
folder of the *.GraphQL project. This is the query resolver forAuditLogs
. It should be derived from the baseAbpZeroTemplateQueryBase<TField,TResult>
. This abstract base class has two generic types.TField
which is the type to return to the client. In this example, it is a list ofAuditLogType
. Other field isTResult
, this is the return type of theResolve()
method. BasicallyTResult
will be mapped toTField
by the GraphQL middleware.To get the list of audit logs,
IRepository<AuditLog, long>
is injected into the query. Note that the base classAbpZeroTemplateQueryBase
is registered to the dependency injection container withITransient
lifecycle. So all the GraphQL queries derived fromAbpZeroTemplateQueryBase
are meant to beITransientDependency
.The base constructor of the query requires a field name. In our sample, the field name is
auditLogs
. This name will be used in the query text. The second parameter is the list ofArguments
. It is aDictionary<string, Type>
to allow clients to filter the query with these arguments. In this example,userId
andserviceName
are arguments and can be sent by clients to filter the records.AuditLogQuery.cs
public class AuditLogQuery : AbpZeroTemplateQueryBase<ListGraphType<AuditLogType>, List<AuditLogDto>> { private readonly IRepository<AuditLog, long> _auditLogRepository; public static class Args { public const string UserId = "userId"; public const string ServiceName = "serviceName"; } public AuditLogQuery(IRepository<AuditLog, long> auditLogRepository) :base("auditLogs",new Dictionary<string, Type> { {Args.UserId, typeof(IdGraphType)}, {Args.ServiceName, typeof(StringGraphType)} }) { _auditLogRepository = auditLogRepository; } protected override Task<List<AuditLogDto>> Resolve(ResolveFieldContext<object> context) { throw new NotImplementedException(); } }
Let's write the content of
Resolve()
method. Basically we need to return a list ofAuditLogDto
filtered with the given arguments. To keep it simple, we will return only the 10 records. In the below code,ContainsArgument
is an extension method that helps us to add a condition to the query if there is an argument. If the client sendsUserId
orServiceName
arguments, they will be filtered in the query. On the last row,ProjectToListAsync
is a base method and maps the queryable list to the list ofAuditLogDto
.protected override async Task<List<AuditLogDto>> Resolve(ResolveFieldContext<object> context) { var query = _auditLogRepository.GetAll().Take(10).AsNoTracking(); context .ContainsArgument<int>(Args.UserId, userId => query = query.Where(a => a.UserId == userId)) .ContainsArgument<string>(Args.ServiceName, serviceName => query = query.Where(a => a.ServiceName == serviceName)); return await ProjectToListAsync<AuditLogDto>(query); }
Add the
AuditLogQuery
to theQueryContainer
. This is needed for exposing schema.public sealed class QueryContainer : ObjectGraphType, ITransientDependency { public QueryContainer( //other queries... , AuditLogQuery auditLogQuery) { //other queries... AddField(auditLogQuery.GetFieldType()); } }
It is completed. We have added a new query to get audit logs. You can now run the query on the GraphQL Playground and see the results. Below is the sample query to get the audit logs from GraphQL API.
query MyAuditLogQuery { auditLogs { id serviceName tenantId userId executionTime } }
The result is a JSON data and similar to the below:
Note that, to see some data in the response, there should be data in your
AbpAuditLogs
table."data": { "auditLogs": [ { "id": 1, "serviceName": "MyProject.Web.Controllers.AccountController", "tenantId": null, "userId": 1, "executionTime": "2019-03-14" }, { "id": 2, "serviceName": "MyProject.Web.Controllers.AccountController", "tenantId": null, "userId": 1, "executionTime": "2019-03-14" } ] }
The new query is ready but accessible by anonymous users. If you would like to add a permission check for your query, it is the same as we use in Application Services. Add the
[AbpAuthorize]
attribute with the required permission to theResolve()
method.[AbpAuthorize(AppPermissions.Pages_Administration_AuditLogs)]
The final look of the
Resolve()
method:[AbpAuthorize(AppPermissions.Pages_Administration_AuditLogs)] protected override async Task<List<AuditLogDto>> Resolve(ResolveFieldContext<object> context) { var query = _auditLogRepository.GetAll().Take(10).AsNoTracking(); context .ContainsArgument<int>(Args.UserId, userId => query = query.Where(a => a.UserId == userId)) .ContainsArgument<string>(Args.ServiceName, serviceName => query = query.Where(a => a.ServiceName == serviceName)); return await ProjectToListAsync<AuditLogDto>(query); }
To run the final query with
[AbpAuthorize]
attribute, you need to get an authentication cookie. Go to your login page:- http://localhost:62114/Account/Login
MVC
. - http://localhost:22742/ui/login
Angular
After successful login, you will have your authentication cookie and you can run the query.
- http://localhost:62114/Account/Login
Writing a New Unit Test For Your Query
When you write a new GraphQL query, it is suggested to write a new unit test for the query. ASP.NET Zero provides a unit test project for your GraphQL queries. There is a test
folder in your ASP.NET Zero solution. In the test
folder, open the *.GraphQL.Tests project. Create a new folder for your new query.
In our previous sample, we have written a query for AuditLog
entity. Let's write a unit test for the AuditLogQuery
.
Before writing a new unit test for a new entity. You need to add sample data to the in-memory test database. For the AuditLog
tests, let's add some test data;
Open *.Test.Base project and create a new class, name it
TestAuditLogsBuilder.cs
. In the below class two test audit log entries are being added.public class TestAuditLogsBuilder { private readonly AbpZeroTemplateDbContext _context; public TestAuditLogsBuilder(AbpZeroTemplateDbContext context) { _context = context; } public void Create() { _context.AuditLogs.Add(new AuditLog { ServiceName = "MyProject.Web.Controllers.AccountController", ExecutionTime = DateTime.Parse("2019-12-30"), UserId = 1 }); _context.AuditLogs.Add(new AuditLog { ServiceName = "MyProject.MultiTenancy.TenantAppService", ExecutionTime = DateTime.Parse("2019-12-28"), UserId = 2 }); } }
Open *.Test.Base\TestData\TestDataBuilder.cs class and add the
TestAuditLogsBuilder
into theCreate()
method.public class TestDataBuilder { //ignored... public void Create() { //ignored... new TestAuditLogsBuilder(_context).Create(); _context.SaveChanges(); } }
Create a new folder in the root directory of *.GraphQL.Tests project, name it
AuditLogs
.Create a new class in the
AuditLogs
folder and name itAuditLogQuery_Tests.cs.
Derive the
AuditLogQuery_Tests
class fromGraphQLTestBase<MainSchema>
. This will help us to use some handy base functionality.Note that
MainSchema
is the schema for all our queries and it was registered in theStartup
of the *.Web.Mvc or *.Web.Host project.Below is the full content of the
AuditLogQuery_Tests
class.AssertQuerySuccessAsync()
method comes from the base class. It gets a GraphQL query and an expected value. It performs a string comparison with the actual value and the expected value when it's not equal, it fails the test.public class AuditLogQuery_Tests : GraphQLTestBase<MainSchema> { [Fact] public async Task Should_Get_AuditLogs() { LoginAsHostAdmin(); const string query = @" query MyAuditLogQuery { auditLogs { id serviceName tenantId userId executionTime } }"; const string expectedResult = @" { ""auditLogs"": [ { ""id"": 1, ""serviceName"": ""MyProject.Web.Controllers.AccountController"", ""tenantId"": null, ""userId"": 1, ""executionTime"": ""2019-12-30"" }, { ""id"": 2, ""serviceName"": ""MyProject.MultiTenancy.TenantAppService"", ""tenantId"": null, ""userId"": 2, ""executionTime"": ""2019-12-28"" } ] }"; await AssertQuerySuccessAsync(query, expectedResult); } }
Conclusion
GraphQL helps us providing our clients a very flexible Web API. In this document, we have seen the structure of the ASP.NET Zero GraphQL implementation. We understood how we can enable GraphQL and GraphQL Playground. We added a new sample GraphQL query to understand it better. We also added a new unit test for the query.