Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/reference/ef.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,79 @@ To use the configuration store support, use the ``AddConfigurationStore`` extens

To configure the configuration store, use the ``ConfigurationStoreOptions`` options object passed to the configuration callback.

Customize the Models Created for Clients and Resources
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If you need to extend the Client and/or Resource models that are returned from the configuration stores, you can do so by
creating your own models that inherit from the default models and then override the ToModel methods in a custom store. Below
is an example of how to do this for the Client model. The same approach can be used for the Resources.

Start by extending the Client model:

.. code-block:: csharp

public class MyClient : Client
{
private const string CibaNotificationEndpointKey = nameof(CibaNotificationEndpoint);

public DateTime? LastAccessed { get; set; }

public string? CibaNotificationEndpoint
{
get
{
if (!Properties.ContainsKey(Client.CibaNotificationEndpointKey))
return null;
return Properties[Client.CibaNotificationEndpointKey];
}
set
{
if (string.IsNullOrEmpty(value))
Properties.Remove(CibaNotificationEndpointKey);
else
Properties[CibaNotificationEndpointKey] = value;
}
}
}

Then create a custom store that overrides the ToModel method:

.. code-block:: csharp

public class MyClientStore : ClientStore
{
public MyClientStore(ConfigurationDbContext context, ITelemetryService telemetry, IOptions<ConfigurationStoreOptions> options)
: base(context, telemetry, options)
{
}

protected override Models.Client ToModel(Entities.Client client)
{
var model = client.ToModel<MyClient>();

//Map additional properties here (not needed for properties that uses the Properties for storage and retrieval)
model.LastAccessed = client.LastAccessed;

return model;
}
}

Finally register your custom store in the DI container:

.. code-block:: csharp

services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddClientStore<MyClientStore>();

By doing this you can now use your own models in customizations or extensions of the built-in support without needing to
create your own store from scratch (including all mapping of existing properties).

ConfigurationStoreOptions
^^^^^^^^^^^^^^^^^^^^^^^^^
This options class contains properties to control the configuration store and ``ConfigurationDbContext``.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ public static class ApiResourceMappingExtensions
/// <returns>mapped instance of <see cref="Models.ApiResource"/></returns>
public Models.ApiResource ToModel()
{
return new Models.ApiResource
return apiResourceEntity.ToModel<Models.ApiResource>();
}

/// <summary>
/// Mapper for <see cref="Entities.ApiScope"/> to convert into an instance of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of <see cref="Models.ApiResource"/> model to map to.</typeparam>
/// <returns>mapped instance of <typeparamref name="T"/>.</returns>
public T ToModel<T>()
where T : Models.ApiResource, new()
{
return new T
{
Enabled = apiResourceEntity.Enabled,
Name = apiResourceEntity.Name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,18 @@ public static class ClientMappingExtensions
/// <returns>mapped instance of <see cref="Models.Client"/></returns>
public Models.Client ToModel()
{
return new Models.Client
return clientEntity.ToModel<Models.Client>();
}

/// <summary>
/// Mapper for <see cref="Entities.Client"/> to convert into an instance of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of <see cref="Models.Client"/> model to map to.</typeparam>
/// <returns>mapped instance of <typeparamref name="T"/>.</returns>
public T ToModel<T>()
where T : Models.Client, new()
{
return new T
{
Enabled = clientEntity.Enabled,
ClientId = clientEntity.ClientId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ public static class IdentityResourceMappingExtensions
/// <returns>mapped instance of <see cref="Models.IdentityResource"/></returns>
public Models.IdentityResource ToModel()
{
return new Models.IdentityResource
return identityResourceEntity.ToModel<Models.IdentityResource>();
}

/// <summary>
/// Mapper for <see cref="Entities.IdentityResource"/> to convert into an instance of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of <see cref="Models.IdentityResource"/> model to map to.</typeparam>
/// <returns>mapped instance of <typeparamref name="T"/>.</returns>
public T ToModel<T>()
where T : Models.IdentityResource, new()
{
return new T
{
Enabled = identityResourceEntity.Enabled,
Name = identityResourceEntity.Name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ public static class ScopeMappingExtensions
/// <returns>mapped instance of <see cref="Models.ApiScope"/></returns>
public Models.ApiScope ToModel()
{
return new Models.ApiScope
return apiScopeEntity.ToModel<Models.ApiScope>();
}

/// <summary>
/// Mapper for <see cref="Entities.ApiScope"/> to convert into an instance of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of <see cref="Models.ApiScope"/> model to map to.</typeparam>
/// <returns>mapped instance of <typeparamref name="T"/>.</returns>
public T ToModel<T>()
where T : Models.ApiScope, new()
{
return new T
{
Enabled = apiScopeEntity.Enabled,
Name = apiScopeEntity.Name,
Expand Down
16 changes: 15 additions & 1 deletion src/EntityFramework.Storage/src/Stores/ClientStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,24 @@ public virtual async Task<Client> FindClientByIdAsync(string clientId)
await baseQuery.Include(x => x.Properties).SelectMany(c => c.Properties).LoadAsync();
await baseQuery.Include(x => x.RedirectUris).SelectMany(c => c.RedirectUris).LoadAsync();

var model = client.ToModel();
var model = ToModel(client);

Logger.LogDebug("{clientId} found in database: {clientIdFound}", clientId, model != null);

return model;
}

/// <summary>
/// Maps the <see cref="Entities.Client"/> to the <see cref="Client"/>.
/// </summary>
/// <param name="client">The <see cref="Entities.Client"/>.</param>
/// <returns>The <see cref="Client"/> or an object extending Client.</returns>
/// <remarks>
/// Makes it possible to return an extended model.
/// </remarks>
protected virtual Client ToModel(Entities.Client client)
{
return client.ToModel();
}

}
58 changes: 48 additions & 10 deletions src/EntityFramework.Storage/src/Stores/ResourceStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ namespace Open.IdentityServer.EntityFramework.Stores;
/// <summary>
/// Implementation of IResourceStore that uses EF.
/// </summary>
/// <seealso cref="Open.IdentityServer.Stores.IResourceStore" />

/// <seealso cref="IResourceStore" />
public class ResourceStore : IResourceStore
{
/// <summary>
Expand Down Expand Up @@ -79,7 +78,7 @@ where apiResourceNames.Contains(apiResource.Name)

var result = (await apis.ToArrayAsync())
.Where(x => apiResourceNames.Contains(x.Name))
.Select(x => x.ToModel()).ToArray();
.Select(ToApiResourceModel).ToArray();

if (result.Any())
{
Expand Down Expand Up @@ -107,7 +106,7 @@ public virtual async Task<IEnumerable<ApiResource>> FindApiResourcesByScopeNameA

var query =
from api in Context.ApiResources
where api.Scopes.Where(x => names.Contains(x.Scope)).Any()
where api.Scopes.Any(x => names.Contains(x.Scope))
select api;

var apis = query
Expand All @@ -119,13 +118,26 @@ where api.Scopes.Where(x => names.Contains(x.Scope)).Any()

var results = (await apis.ToArrayAsync())
.Where(api => api.Scopes.Any(x => names.Contains(x.Scope)));
var models = results.Select(x => x.ToModel()).ToArray();
var models = results.Select(ToApiResourceModel).ToArray();

Logger.LogDebug("Found {apis} API resources in database", models.Select(x => x.Name));

return models;
}

/// <summary>
/// Maps the <see cref="Entities.ApiResource"/> to the <see cref="ApiResource"/>.
/// </summary>
/// <param name="resource">The <see cref="Entities.ApiResource"/>.</param>
/// <returns>The <see cref="ApiResource"/> or an object extending ApiScope.</returns>
/// <remarks>
/// Makes it possible to return an extended model.
/// </remarks>
protected virtual ApiResource ToApiResourceModel(Entities.ApiResource resource)
{
return resource.ToModel();
}

/// <summary>
/// Gets identity resources by scope name.
/// </summary>
Expand Down Expand Up @@ -153,7 +165,20 @@ where scopes.Contains(identityResource.Name)

Logger.LogDebug("Found {scopes} identity scopes in database", results.Select(x => x.Name));

return results.Select(x => x.ToModel()).ToArray();
return results.Select(ToIdentityResourceModel).ToArray();
}

/// <summary>
/// Maps the <see cref="Entities.IdentityResource"/> to the <see cref="IdentityResource"/>.
/// </summary>
/// <param name="resource">The <see cref="Entities.IdentityResource"/>.</param>
/// <returns>The <see cref="IdentityResource"/> or an object extending IdentityResource.</returns>
/// <remarks>
/// Makes it possible to return an extended model.
/// </remarks>
protected virtual IdentityResource ToIdentityResourceModel(Entities.IdentityResource resource)
{
return resource.ToModel();
}

/// <summary>
Expand Down Expand Up @@ -183,7 +208,20 @@ where scopes.Contains(scope.Name)

Logger.LogDebug("Found {scopes} scopes in database", results.Select(x => x.Name));

return results.Select(x => x.ToModel()).ToArray();
return results.Select(ToApiScopeModel).ToArray();
}

/// <summary>
/// Maps the <see cref="Entities.ApiScope"/> to the <see cref="ApiScope"/>.
/// </summary>
/// <param name="scope">The <see cref="Entities.ApiScope"/>.</param>
/// <returns>The <see cref="ApiScope"/> or an object extending ApiScope.</returns>
/// <remarks>
/// Makes it possible to return an extended model.
/// </remarks>
protected virtual ApiScope ToApiScopeModel(Entities.ApiScope scope)
{
return scope.ToModel();
}

/// <summary>
Expand Down Expand Up @@ -211,9 +249,9 @@ public virtual async Task<Resources> GetAllResourcesAsync()
.AsNoTracking();

var result = new Resources(
(await identity.ToArrayAsync()).Select(x => x.ToModel()),
(await apis.ToArrayAsync()).Select(x => x.ToModel()),
(await scopes.ToArrayAsync()).Select(x => x.ToModel())
(await identity.ToArrayAsync()).Select(ToIdentityResourceModel),
(await apis.ToArrayAsync()).Select(ToApiResourceModel),
(await scopes.ToArrayAsync()).Select(ToApiScopeModel)
);

Logger.LogDebug("Found {scopes} as all scopes, and {apis} as API resources",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,34 @@ public async Task FindClientByIdAsync_WhenClientExists_ExpectClientRetured(
client.Should().NotBeNull();
}

[Theory, MemberData(nameof(TestDatabaseProviders))]
public async Task FindClientByIdAsync_WhenClientExists_ExpectExtendedClientReturned(
DbContextOptions<ConfigurationDbContext> options)
{
var testClient = new Client
{
ClientId = "test_extended_client",
ClientName = "Test Extended Client",
Properties = { { "x", "xx" } }
};

await using (var context = new ConfigurationDbContext(options, StoreOptions))
{
context.Clients.Add(testClient.ToEntity());
await context.SaveChangesAsync(TestContext.Current.CancellationToken);
}

ExtendedClient client;
await using (var context = new ConfigurationDbContext(options, StoreOptions))
{
var store = new ExtendedClientStore(context, _telemetry, FakeLogger<ExtendedClientStore>.Create());
client = await store.FindClientByIdAsync(testClient.ClientId) as ExtendedClient;
}

client.Should().NotBeNull();
client.X.Should().Be("xx");
}

[Theory, MemberData(nameof(TestDatabaseProviders))]
public async Task FindClientByIdAsync_WhenClientExistsWithCollections_ExpectClientReturnedCollections(
DbContextOptions<ConfigurationDbContext> options)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using Open.IdentityServer.Models;
using System;

namespace Open.IdentityServer.EntityFramework.IntegrationTests.Stores;

internal class ExtendedApiResource : ApiResource
{
public DateTime Created { get; set; }
public DateTime? Updated { get; set; }
public DateTime? LastAccessed { get; set; }

public string? X
{
get => Properties.ContainsKey("x") ? Properties["x"] : null;
set => Properties["x"] = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using Open.IdentityServer.Models;
using System;

namespace Open.IdentityServer.EntityFramework.IntegrationTests.Stores;

internal class ExtendedApiScope : ApiScope
{
public DateTime Created { get; set; }
public DateTime? Updated { get; set; }
public DateTime? LastAccessed { get; set; }

public string? X
{
get => Properties.ContainsKey("x") ? Properties["x"] : null;
set => Properties["x"] = value;
}
}
Loading
Loading