Implementing authorization in Blazor ASP.NET Core applications using Azure AD security groups

This article shows how to implement authorization in an ASP.NET Core Blazor application using Azure AD security groups as the data source for the authorization definitions. Policies and claims are used in the application which decouples the descriptions from the Azure AD security groups and the application specific authorization requirements. With this setup, it is easy to support any complex authorization requirement and IT admins can manager the accounts independently in Azure. This solution will work for Azure AD B2C or can easily be adapted to use data from your database instead of Azure AD security groups if required.

Code: https://github.com/damienbod/AzureADAuthRazorUiServiceApiCertificate/tree/main/BlazorBff

Setup the AAD security groups

Before we start using the Azure AD security groups, the groups need to be created. I use Powershell to create the security groups. This is really simple using the Powershell AZ module with AD. For this demo, just two groups are created, one for users and one for admins. The script can be run from your Powershell console. You are required to authenticate before running the script and the groups are added if you have the rights. In DevOps, you could use a managed identity and the client credentials flow.

# https://theitbros.com/install-azure-powershell/
#
# https://docs.microsoft.com/en-us/powershell/module/az.accounts/connect-azaccount?view=azps-7.1.0
#
# Connect-AzAccount -Tenant “–tenantId–”
# AZ LOGIN –tenant “–tenantId–”

$tenantId = “–tenantId–”
$gpAdmins = “demo-admins”
$gpUsers = “demo-users”

function testParams {

if (!$tenantId)
{
Write-Host “tenantId is null”
exit 1
}
}

testParams

function CreateGroup([string]$name) {
Write-Host ” – Create new group”
$group = az ad group create –display-name $name –mail-nickname $name

$gpObjectId = ($group | ConvertFrom-Json).objectId
Write-Host ” $gpObjectId $name”
}

Write-Host “Creating groups”

##################################
### Create groups
##################################

CreateGroup $gpAdmins
CreateGroup $gpUsers

#az ad group list –display-name $groupName

return

Once created, the new security groups should be visible in the Azure portal. You need to add group members or user members to the groups.

That’s all the configuration required to setup the security groups. Now the groups can be used in the applications.

Define the authorization policies

We do not use the security groups directly in the applications because this can change a lot or maybe the application is deployed to different host environments. The security groups are really just descriptions about the identity. How you use this, is application specific and depends on the solution business requirements which tend to change a lot. In the applications, shared authorization policies are defined and only used in the Blazor WASM and the Blazor server part. The definitions have nothing to do with the security groups, the groups get mapped to application claims. A Policies class definition was created for all the policies in the shared Blazor project because this is defined once, but used in the server project and the client project. The code was built based on the excellent blog from Chris Sainty. The claims definition for the authorization check have nothing to do with the Azure security groups, this logic is application specific and sometimes the applications need to apply different authorization logic how the security groups are used in different applications inside the same solution.

using Microsoft.AspNetCore.Authorization;

namespace BlazorAzureADWithApis.Shared.Authorization
{
public static class Policies
{
public const string DemoAdminsIdentifier = “demo-admins”;
public const string DemoAdminsValue = “1”;

public const string DemoUsersIdentifier = “demo-users”;
public const string DemoUsersValue = “1”;

public static AuthorizationPolicy DemoAdminsPolicy()
{
return new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim(DemoAdminsIdentifier, DemoAdminsValue)
.Build();
}

public static AuthorizationPolicy DemoUsersPolicy()
{
return new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim(DemoUsersIdentifier, DemoUsersValue)
.Build();
}
}
}

Add the authorization to the WASM and the server project

The policy definitions can now be added to the Blazor Server project and the Blazor WASM project. The AddAuthorization extension method is used to add the authorization to the Blazor server. The policy names can be anything you want.

services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
options.AddPolicy(“DemoAdmins”, Policies.DemoAdminsPolicy());
options.AddPolicy(“DemoUsers”, Policies.DemoUsersPolicy());
});

The AddAuthorizationCore method is used to add the authorization policies to the Blazor WASM client project.

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore(options =>
{
options.AddPolicy(“DemoAdmins”, Policies.DemoAdminsPolicy());
options.AddPolicy(“DemoUsers”, Policies.DemoUsersPolicy());
});

Now the application policies, claims are defined. Next job is to connect the Azure security definitions to the application authorization claims used for the authorization policies.

Link the security groups from Azure to the app authorization

This can be done using the IClaimsTransformation interface which gets called after a successful authentication. An application Microsoft Graph client is used to request the Azure AD security groups. The IDs of the Azure security groups are mapped to the application claims. Any logic can be added here which is application specific. If a hierarchical authorization system is required, this could be mapped here.

public class GraphApiClaimsTransformation : IClaimsTransformation
{
private readonly MsGraphApplicationService _msGraphApplicationService;

public GraphApiClaimsTransformation(MsGraphApplicationService msGraphApplicationService)
{
_msGraphApplicationService = msGraphApplicationService;
}

public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
ClaimsIdentity claimsIdentity = new();
var groupClaimType = “group”;
if (!principal.HasClaim(claim => claim.Type == groupClaimType))
{
var objectidentifierClaimType = “http://schemas.microsoft.com/identity/claims/objectidentifier”;
var objectIdentifier = principal
.Claims.FirstOrDefault(t => t.Type == objectidentifierClaimType);

var groupIds = await _msGraphApplicationService
.GetGraphUserMemberGroups(objectIdentifier.Value);

foreach (var groupId in groupIds.ToList())
{
var claim = GetGroupClaim(groupId);
if (claim != null) claimsIdentity.AddClaim(claim);
}
}

principal.AddIdentity(claimsIdentity);
return principal;
}

private Claim GetGroupClaim(string groupId)
{
Dictionary<string, Claim> mappings = new Dictionary<string, Claim>() {
{ “1d9fba7e-b98a-45ec-b576-7ee77366cf10”,
new Claim(Policies.DemoUsersIdentifier, Policies.DemoUsersValue)},

{ “be30f1dd-39c9-457b-ab22-55f5b67fb566”,
new Claim(Policies.DemoAdminsIdentifier, Policies.DemoAdminsValue)},
};

if (mappings.ContainsKey(groupId))
{
return mappings[groupId];
}

return null;
}
}

The MsGraphApplicationService class is used to implement the Microsoft Graph requests. This uses application permissions with a ClientSecretCredential. I use secrets which are read from an Azure Key vault. You need to implement rotation for this or make it last forever and update the secrets in the DevOps builds every time you deploy. My secrets are only defined in Azure and used from the Azure Key Vault. You could use certificates but this adds no extra security unless you need to use the secret/certificate outside of Azure or in app settings somewhere. The GetMemberGroups method is used to get the groups for the authenticated user using the object identifier.

public class MsGraphApplicationService
{
private readonly IConfiguration _configuration;

public MsGraphApplicationService(IConfiguration configuration)
{
_configuration = configuration;
}

public async Task<IUserAppRoleAssignmentsCollectionPage>
GetGraphUserAppRoles(string objectIdentifier)
{
var graphServiceClient = GetGraphClient();

return await graphServiceClient.Users[objectIdentifier]
.AppRoleAssignments
.Request()
.GetAsync();
}

public async Task<IDirectoryObjectGetMemberGroupsCollectionPage>
GetGraphUserMemberGroups(string objectIdentifier)
{
var securityEnabledOnly = true;

var graphServiceClient = GetGraphClient();

return await graphServiceClient.Users[objectIdentifier]
.GetMemberGroups(securityEnabledOnly)
.Request().PostAsync();
}

private GraphServiceClient GetGraphClient()
{
string[] scopes = new[] { “https://graph.microsoft.com/.default” };
var tenantId = _configuration[“AzureAd:TenantId”];

// Values from app registration
var clientId = _configuration.GetValue<string>(“AzureAd:ClientId”);
var clientSecret = _configuration.GetValue<string>(“AzureAd:ClientSecret”);

var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};

// https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);

return new GraphServiceClient(clientSecretCredential, scopes);
}
}

The security groups are mapped to the application claims and policies. The policies can be applied in the application.

Use the Policies in the Server

The Blazor server applications implements secure APIs for the Blazor WASM. The Authorize attribute is used with the policy definition. Now the user must be authorized using our definition to get data from this API. We also use cookies because the Blazor application is secured using the BFF architecture which has improved security compared to using tokens in the untrusted SPA.

[ValidateAntiForgeryToken]
[Authorize(Policy= “DemoAdmins”,
AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[ApiController]
[Route(“api/[controller]”)]
public class DemoAdminController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new List<string>
{
“admin data”,
“secret admin record”,
“loads of admin data”
};
}
}

Use the policies in the WASM

The Blazor WASM application can also use the authorization policies. This is not really authorization but only usability because you cannot implement authorization in an untrusted application which you have no control of once it’s running. We would like to hide the components and menus which cannot be used, if you are not authorized. I use an AuthorizeView with a policy definition for this.

<div class=”@NavMenuCssClass” @onclick=”ToggleNavMenu”>
<ul class=”nav flex-column”>
<AuthorizeView Policy=”DemoAdmins”>
<Authorized>
<li class=”nav-item px-3″>
<NavLink class=”nav-link” href=”demoadmin”>
<span class=”oi oi-list-rich” aria-hidden=”true”></span> DemoAdmin
</NavLink>
</li>
</Authorized>
</AuthorizeView>

<AuthorizeView Policy=”DemoUsers”>
<Authorized>
<li class=”nav-item px-3″>
<NavLink class=”nav-link” href=”demouser”>
<span class=”oi oi-list-rich” aria-hidden=”true”></span> DemoUser
</NavLink>
</li>
</Authorized>
</AuthorizeView>

<AuthorizeView>
<Authorized>
<li class=”nav-item px-3″>
<NavLink class=”nav-link” href=”graphprofile”>
<span class=”oi oi-list-rich” aria-hidden=”true”></span> Graph Profile
</NavLink>
</li>
<li class=”nav-item px-3″>
<NavLink class=”nav-link” href=”” Match=”NavLinkMatch.All”>
<span class=”oi oi-home” aria-hidden=”true”></span> Home
</NavLink>
</li>
</Authorized>
<NotAuthorized>
<li class=”nav-item px-3″>
<p style=”color:white”>Please sign in</p>
</li>
</NotAuthorized>
</AuthorizeView>

</ul>
</div>

The Blazor UI pages should also use an Authorize attribute. This prevents an unhandled exception. You could add logic which forces you to login then with the permissions required or just display an error page. This depends on the UI strategy.

@page “/demoadmin”
@using Microsoft.AspNetCore.Authorization
@inject IHttpClientFactory HttpClientFactory
@inject IJSRuntime JSRuntime
@attribute [Authorize(Policy =”DemoAdmins”)]

<h1>Demo Admin</h1>

When the application is started, you will only see what you allowed to see and more important, only be able to get data for what you are authorized.

If you open a page where you have no access rights:

Notes:

This solution is very flexible and can work with any source of identity definitions, not just Azure security groups. I could very easily switch to a database. One problem with this, is that with a lot of authorization definitions, the size of the cookie might get to big and you would need to switch from using claims in the policies definitions to using a cache database or something. This would also be easy to adapt because the claims are only mapped in the policies and the IClaimsTransformation implementation. Only the policies are used in the application logic.

Links

https://chrissainty.com/securing-your-blazor-apps-configuring-policy-based-authorization-with-blazor/

https://docs.microsoft.com/en-us/aspnet/core/blazor/security

Flatlogic Admin Templates banner

All about Web API Versioning in ASP.NET Core

Introduction

Before releasing an API, we should consider how to release future improvements and possible breaking changes. For example, it is most likely that something would have to change in our APIs, such as the business requirements, resources and their relationships, third-party APIs, etc.

In general, versioning is the process of assigning a unique version (based on a specific format) per software release. The most widely used versioning format is the Semantic versioning (also known as SemVer) which is defined as follows: X.Y.Z (Major.Minor.Patch).

Major: A version for incompatible API changes.
Minor: A version for adding backward compatible functionalities.
Patch: A version for making backward compatible bug fixes.

Providing versioning in Web APIs (i.e., services) is more complex in compassion to API libraries (e.g., NuGet libraries). That is because multiple implementations (and versions) of the Web API (including their dependencies, e.g., third-party systems, databases, etc.) should be maintained and be available to the consumers. The consumers can request a specific API version by defining the version in the URI, query-string, HTTP header, or media type.

As we can understand, serving and maintaining multiple implementations of the Minor and Patch versions are not practical for Web APIs because they are backward compatible (assuming we are using the Semantic versioning). Supporting and maintaining multiple implementations of the Major versions of our APIs is a way to control how old consumers coexist with new ones.

The advice of Roy Thomas Fielding (creator or the REST architectural style on how to approach versioning in Web APIs is “Don’t” (Fielding R.T., 2013). In a very interesting interview (Fielding R.T., 2014), he provides additional information about versioning and HATEOAS in Web APIs.

From my point of view, the main question to recognize the need for versioning (Major) in a Web API is if the old consumers can coexist with the new ones. In such a case, we can provide major versioning not to break or replace already deployed components. However, we should decide carefully how many versions we will serve and maintain and how we will deprecate them. So, we will need a versioning plan 🙂.

Note: The main question to recognize the need for Major Versioning in an API is if the old consumers can coexist with the new ones.

In in-house software, where we have control over the consumers and servers (on the code and release process), we might decide not to use major versioning in our API. That is not something terrible. For example, when our consumers are internal Web APIs that we can release together or a small downtime is acceptable.

However, even in in-house software, we should consider the Single Page Applications (SPAs) cases in which different released versions can coexist on the consumer browsers. Based on our project, we should decide if it’s possible to force a refresh on the user’s web page or let the users decide when to trigger it.

Tip: In the case of SPAs, different released versions can coexist on the consumers.

In this article, we will learn the most commonly used versioning mechanisms and how we can apply them in our Web APIs by using .NET Core. Furthermore, we will see two strategies to organize our controller actions and code files. Finally, we will use Postman to perform API requests for the examined versioning mechanisms.

Versioning Mechanisms

The following table shows several versioning mechanisms that we can use to provide versioning in our Web APIs. It is important to note that the decision of the versioning mechanism influences the use of client caching and HATEOAS.

Version Mechanisms
Description
Example
Client Caching Friendly

New Route Versioning
An easy to implement but difficult to maintain solution in both consumers (clients) and server code.

From: https://api.mydomain.tld/products/
To: https://api.mydomain.tld/productsv2/
Yes

URI Versioning
The most commonly used versioning, in which we can add a version (e.g., number) to the API base URL.
https://api.mydomain.tld/v2/products/
Yes

Query String Versioning
The version is provided by using a query string parameter, such as “api-version”.
https://api.mydomain.tld/products/?api-version=2
Yes (in most cases)

Custom Header Versioning
Α custom header to indicate the version (e.g., API-Version, Accept-Version, etc.).

GET https://api.mydomain.tld/products
Accept-Version: 2
No

Media Type Versioning
The Accept header is used to indicate the version.

GET https://api.mydomain.tld/products
Accept: application/json;api-version=2.0
No

Apply Versioning at Existing ASP.NET Core Web API Project

ASP.NET Core provides all the tools that we need to apply all the mentioned versioning mechanisms. Let’s assume that we are would like to apply versioning to an existing project. So, the current API calls should work whether we specify a version or not.

Step 1: Install the Versioning Package Reference

We should install the Microsoft.AspNetCore.Mvc.Versioning package reference either by using the NuGet Package Manager in Visual Studio or using the .NET CLI (check more options here). To install it using the NuGet Package Manager (Figure 1), follow the steps:

Right-click on your Web API project and then click on “Manage NuGet Packages…”.
Search with the term “Microsoft.AspNetCore.Mvc.Versioning”.
Click on the Install button.
Click OK to the Preview Changes window and Accept the License.

Figure 1. – How to install the “Microsoft.AspNetCore.Mvc.Versioning” package.

Step 2: Configure the Versioning Services

To configure the API versioning properties of our project, such as return headers, version format, etc. I would recommend creating an extension method, such as the following. In this way, we would keep our Startup.cs (or Program.cs in .NET 6.0) file cleaner and readable.

public static class ConfigureApiVersioning
{
/// <summary>

/// Configure the API versioning properties of the project, such as return headers, version format, etc.

/// </summary>

/// <param name=”services”></param>

public static void AddApiVersioningConfigured(this IServiceCollection services)
{
services.AddApiVersioning(options =>
{
// ReportApiVersions will return the “api-supported-versions” and “api-deprecated-versions” headers.

options.ReportApiVersions = true;

// Set a default version when it’s not provided,

// e.g., for backward compatibility when applying versioning on existing APIs

options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);

// Combine (or not) API Versioning Mechanisms:

options.ApiVersionReader = ApiVersionReader.Combine(
// The Default versioning mechanism which reads the API version from the “api-version” Query String paramater.

new QueryStringApiVersionReader(“api-version”),
// Use the following, if you would like to specify the version as a custom HTTP Header.

new HeaderApiVersionReader(“Accept-Version”),
// Use the following, if you would like to specify the version as a Media Type Header.

new MediaTypeApiVersionReader(“api-version”)
);
});

// Here, we will add another service, e.g., to support versioning on our documentation.

}
}

Tip: Creating custom extension methods to add services to the container will keep our Startup.cs (or Program.cs in .NET 6.0) file cleaner and readable

In this example extension method, we perform all the configurations regarding versioning based on our needs. So, in this example, we:

Enable the ReportApiVersions to let .NET return lists of our API’s supported and deprecated versions as HTTP headers (api-supported-versions and api-deprecated-versions, respectively). In this way, our API consumers can be informed by the API for any change in versions.

Set a default API version when the consumers do not provide it (e.g., v1.0). In our case, we are assuming that we are applying versioning to an existing project. Thus, our API should be backward compatible for the consumers that do not define the version.

Read the version from multiple sources, such as query string, custom HTTP header, and Media Type header. However, we could select only one or none of them. Moreover, reading the version from our API base URL (URI Versioning) is not configured here. Thus, we could remove this part of the code if we would like to support only the URI Versioning.

Step 3: Register the Versioning Services

In this step, we should register the API versioning services in the Startup.cs or Program.cs file depending on the used style (current or .NET 6.0). That’s an easy task because of our extension method. Just add the services.AddApiVersioningConfigured(); line in the ConfigureServices method, as shown below.

// Startup.cs file in the current style

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();

services.AddApiVersioningConfigured();
}

// Program.cs file in .NET 6.0 new style

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddApiVersioningConfigured();

Step 4: Apply Versioning

Applying versioning either to an existing or new project is relatively the same. In both cases, we will have to define the API version(s) of the controller by using the ApiVersion attribute (e.g., [ApiVersion(“1.0”)]). Depending on our chosen versioning mechanism, the main difference is the route or routes that we would specify.

In the following code example, we are specifying two routes on the controller.

[Route(“api/v{version:apiVersion}/[controller]”)]: This route is specified to support URI Versioning on new or existing controllers, such as:

api/v1/weatherforecast
api/v1.0/weatherforecast
api/v2.0/weatherforecast

[Route(“api/[controller]”)]: This route could be used in an existing implementation, in which we would like to support versioning (e.g., Query string and HTTP header versioning) or just used for backward compatibility.

// List of available and deprecated Versions for this ApiController. [ApiVersion(“1.0”)]

// Specify the base (or current) API route to:

// – Keep the existing route serving a default version (backward compatible).

// – Support query string and HTTP header versioning.

[Route(“api/[controller]“)]

// Specify the route to support URI Versioning. URI example: api/v1/weatherforecast

[Route(“api/v{version:apiVersion}/[controller]“)]
public class WeatherForecastController : ControllerBase
{
// Our controller’s code…

}

Strategies to Organize Controllers and Actions

So, we have decided to support and maintain multiple implementations of the Major versions of our API. First, however, we could organize our controller actions and code files, either by the version or in the controller. You can find the following examples in GitHub.

Organize Controllers by Version

In this strategy, we group the files related to each version, such as Controllers, Data Transfer Objects (DTOs), Extensions, etc. For example, in Figure 2, we see how we could organize the files per version. We can observe that each controller uses the DTOs of its version, which can be entirely different (Figure 3).

As we can understand, the drawback of this strategy is that if only a small part of the project has Major changes (i.e., not backward compatible), it will result in a lot of duplicate code. So, we can use this strategy when we are not releasing Major changes very often.

Figure 2. – Example of organizing controllers by version.

Figure 3. – Example of different DTOs per version.

Organize Versioning in the Controller

When we organize the versioning in the controller, all different versions of the same functionality (action) are in the same controller. In this case, we use the ApiVersion attribute multiple times in the controller to list the available and deprecated versions. Furthermore, the MapToApiVersion attribute is used per action to map the API version with the specific implementation. In Figure 4, we can see how we can organize the versioning in the controller.

As we can see, if an action doesn’t have breaking changes, we can map it to multiple versions. For the sake of the example, let’s assume that the GetById should return the WeatherFoecast class. Thus, this strategy has the flexibility to perform Major changes only for some parts of the project. However, it may lead to complex code and, thus, difficult to maintain, e.g., when multiple versions exist, removing deprecated code, sharing the same DTOs, etc.

Figure 4. – Example of organizing the versioning in the controller.

API Requests with Versioning: Postman Examples

This section demonstrates how we can perform API requests for each versioning mechanism by using Postman. The presented code examples, which use the mentioned versioning mechanisms, can be found in GitHub. In addition, you can download the Postman collection for the given examples from here.

Supported and Deprecated Versions Headers

Figure 5. – Postman Example of supported and deprecated versions headers.

URI Versioning

Figure 6. – Postman Example of URI Versioning.

Query String Versioning

Figure 7. – Postman Example of Query String Versioning.

Custom Header Versioning

Figure 8. – Postman Example of Custom Header Versioning.

Media Type Versioning

Figure 9. – Postman Example of Media Type Versioning.

Summary

Versioning is the acceptance that improvements and breaking changes will occur in our project. For that reason, we are assigning a unique version per software release (usually based on the Semantic versioning format: Major.Minor.Patch).

When we release Major (breaking) changes in Web APIs, the current consumers (clients) could coexist with the new ones. In that case, we should decide carefully how many versions we will serve and maintain and how we will deprecate them. So, we will need a versioning plan 🙂.

The most commonly used versioning mechanisms use the URI, Query String, Custom Header, and Media Type. It is important to notice that the decision of the versioning mechanism influence the use of client caching and HATEOAS. Finally, we saw how the examined versioning mechanisms can be applied using .NET Core and how we can perform API requests using Postman.

Furthermore, we saw two strategies to organize our controller actions and code files, either by the version or in the controller. If we are not sure which strategy is appropriate for our project, we could start by organizing our controller actions and code in the controller (e.g., for V1). Then, we can decide when the “V2” requirements are available.

References

Fielding R.T. (2013). EVOLVE Conference 2013 Presentation. https://www.slideshare.net/evolve_conference/201308-fielding-evolve/31

Fielding R.T. (2014, December 17). Roy Fielding on Versioning, Hypermedia, and REST. https://www.infoq.com/articles/roy-fielding-on-versioning/

GraphQL in ASP.NET Core with EF Core

This post is about GraphQL in ASP.NET Core with EF Core. In the earlier post I discussed about integrating GraphQL in ASP.NET Core with HotChocolate. In this post I will discuss about how to use GraphQL on top EF Core.

First I will be adding nuget packages required to work with EF Core – Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Design – this optional, since I am running migrations this package is required. Next I am modifying the code – adding DbContext and wiring the the DbContext to the application. Here is the DbContext code and updated Query class.

public class Link
{
public int Id { get; set; }
public string Url { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string ImageUrl { get; set; }
public DateTime CreatedOn { get; set; }
public ICollection<Tag> Tags { get; set; } = new List<Tag>();
}

public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
public int LinkId { get; set; }
public Link Link { get; set; }
}

public class BookmarkDbContext : DbContext
{
public BookmarkDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<Link> Links { get; set; }
public DbSet<Tag> Tags { get; set; }
}

I wrote the OnModelCreating method to seed the database. And I modified the code of the Program.cs and added the DbCotext class.

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<BookmarkDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString(“BookmarkDbConnection”)));
builder.Services.AddGraphQLServer().AddQueryType<Query>();
var app = builder.Build();

app.MapGet(“/”, () => “Hello World!”);
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.Run();

And Query class modified like this.

public class Query
{
public IQueryable<Link> Links([Service] BookmarkDbContext bookmarkDbContext)
=> bookmarkDbContext.Links;
}

In this code the Service attribute will help to inject the DbContext to the method. Next lets run the application and execute query.

query {
links{
id
url
title
imageUrl
description
createdOn
}
}

We will be able to see result like this.

Next let us remove some parameters in the query and run it again.

query {
links{
title
imageUrl
}
}

We can see the result like this.

And when we look into the EF Core log, we will be able to see the EF Core SQL Log like this.

In the Log, even though we are querying only two fields it is querying all the fields. We can fix this issue by adding a new nuget package HotChocolate.Data.EntityFramework. And modify the code like this.

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<BookmarkDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString(“BookmarkDbConnection”)));
builder.Services.AddGraphQLServer().AddQueryType<Query>().AddProjections().AddFiltering().AddSorting();
var app = builder.Build();

app.MapGet(“/”, () => “Hello World!”);
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.Run();

And modify the query class as well, decorate with the HotChocolate attributes for Projections, Filtering and Sorting.

public class Query
{
[UseProjection]
[UseFiltering]
[UseSorting]
public IQueryable<Link> Links([Service] BookmarkDbContext bookmarkDbContext)
=> bookmarkDbContext.Links;
}

Now lets run the query again and check the logs.

We can see only the required fields are queried. Not every fields in the table.

This way you can configure GraphQL in ASP.NET Core with EF Core. This code will fail, if you try to execute the GraphQL query with alias. We can use the DbContextFactory class to fix this issue. We will look into it in the next blog post.

Happy Programming 🙂