Skip to content
Open
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
5 changes: 3 additions & 2 deletions src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ExtensionsVersion>10.0.5</ExtensionsVersion>
<EntityFrameworkVersion>10.0.10</EntityFrameworkVersion>

<IdentityServerVersion>2.0.0-*</IdentityServerVersion>
<IdentityServerVersion>2.1.0-*</IdentityServerVersion>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -61,7 +61,8 @@
<PackageReference Update="Microsoft.EntityFrameworkCore.Design" Version="$(EntityFrameworkVersion)" PrivateAssets="All"/>

<!--other entity framework-->
<PackageReference Update="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageReference Update="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
<PackageReference Update="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

#nullable enable

namespace Open.IdentityServer.EntityFramework.Mappers;

/// <summary>
/// Mapping extension methods for IdentityServerServerSideSessions objects
/// </summary>
public static class IdentityServerServerSideSessionsExtensions
{
/// <summary>
/// Mapping extension methods for <see cref="Entities.IdentityServerServerSideSessions"/>
/// </summary>
/// <param name="sessionEntity">The entity.</param>
extension(Entities.IdentityServerServerSideSessions sessionEntity)
{
/// <summary>
/// Mapper for <see cref="Entities.IdentityServerServerSideSessions"/> to convert into an instance of <see cref="Models.IdentityServerServerSideSessions"/>
/// </summary>
/// <returns>mapped instance of <see cref="Models.IdentityServerServerSideSessions"/></returns>
public Models.IdentityServerServerSideSessions ToModel()
{
return new Models.IdentityServerServerSideSessions
{
Key = sessionEntity.Key,
Scheme = sessionEntity.Scheme,
SubjectId = sessionEntity.SubjectId,
SessionId = sessionEntity.SessionId,
DisplayName = sessionEntity.DisplayName,
Created = sessionEntity.Created,
Renewed = sessionEntity.Renewed,
Expires = sessionEntity.Expires,
Data = sessionEntity.Data
};
}
}

/// <summary>
/// Mapping extension methods for <see cref="Models.IdentityServerServerSideSessions"/>
/// </summary>
/// <param name="sessionModel">The model.</param>
extension(Models.IdentityServerServerSideSessions sessionModel)
{
/// <summary>
/// Mapper for <see cref="Models.IdentityServerServerSideSessions"/> to convert into an instance of <see cref="Entities.IdentityServerServerSideSessions"/>
/// </summary>
/// <returns>mapped instance of <see cref="Entities.IdentityServerServerSideSessions"/></returns>
public Entities.IdentityServerServerSideSessions ToEntity()
{
return new Entities.IdentityServerServerSideSessions
{
Key = sessionModel.Key,
Scheme = sessionModel.Scheme,
SubjectId = sessionModel.SubjectId,
SessionId = sessionModel.SessionId,
DisplayName = sessionModel.DisplayName,
Created = sessionModel.Created,
Renewed = sessionModel.Renewed,
Expires = sessionModel.Expires,
Data = sessionModel.Data
};
}

/// <summary>
/// Updates <see cref="Entities.IdentityServerServerSideSessions"/> with instance of <see cref="Models.IdentityServerServerSideSessions"/>
/// </summary>
/// <param name="existingEntity">The entity.</param>
public void UpdateEntity(Entities.IdentityServerServerSideSessions existingEntity)
{
existingEntity.Key = sessionModel.Key;
existingEntity.Scheme = sessionModel.Scheme;
existingEntity.SubjectId = sessionModel.SubjectId;
existingEntity.SessionId = sessionModel.SessionId;
existingEntity.DisplayName = sessionModel.DisplayName;
existingEntity.Created = sessionModel.Created;
existingEntity.Renewed = sessionModel.Renewed;
existingEntity.Expires = sessionModel.Expires;
existingEntity.Data = sessionModel.Data;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

#nullable enable

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Open.IdentityServer.EntityFramework.Interfaces;
using Open.IdentityServer.EntityFramework.Mappers;
using Open.IdentityServer.Services;
using Open.IdentityServer.Stores;
using IdentityServerServerSideSessions = Open.IdentityServer.Models.IdentityServerServerSideSessions;

namespace Open.IdentityServer.EntityFramework.Stores;

/// <summary>
/// Storage and retrieval of server-side sessions using entity framework core
/// </summary>
public class IdentityServerServerSideSessionStore(
IPersistedGrantDbContext dbContext,
ITelemetryService telemetry,
ILogger<IdentityServerServerSideSessionStore> logger): IIdentityServerServerSideSessionStore
{
/// <inheritdoc />
public async Task<IdentityServerServerSideSessions?> GetSession(string key)
{
using var trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this);

ArgumentException.ThrowIfNullOrWhiteSpace(key);

Entities.IdentityServerServerSideSessions? session = await dbContext.ServerSideSessions
.SingleOrDefaultAsync(x => x.Key == key);

return session?.ToModel();
}

/// <inheritdoc />
public async Task CreateSession(IdentityServerServerSideSessions session)
{
using var trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this);

ArgumentException.ThrowIfNullOrWhiteSpace(session.Key);

Entities.IdentityServerServerSideSessions? existing = await dbContext.ServerSideSessions
.SingleOrDefaultAsync(x => x.Key == session.Key);

if (existing != null)
{
logger.LogError("failed storing '{SessionKey}' session in database, session with key already exists", session.Key);
return;
}

Entities.IdentityServerServerSideSessions sessionEntity = session.ToEntity();

await dbContext.ServerSideSessions.AddAsync(sessionEntity);

try
{
await dbContext.SaveChangesAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "exception storing '{SessionKey}' session in database", session.Key);
}
}

/// <inheritdoc />
public async Task UpdateSession(IdentityServerServerSideSessions session)
{
using var trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this);

ArgumentException.ThrowIfNullOrWhiteSpace(session.Key);

Entities.IdentityServerServerSideSessions? existing = await dbContext.ServerSideSessions
.SingleOrDefaultAsync(x => x.Key == session.Key);

if (existing == null)
{
logger.LogError("failed updating '{SessionKey}' session in database, session not found", session.Key);
return;
}

session.UpdateEntity(existing);

try
{
await dbContext.SaveChangesAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "exception updating '{SessionKey}' session in database", session.Key);
}
}

/// <inheritdoc />
public async Task DeleteSession(string key)
{
using var trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this);

ArgumentException.ThrowIfNullOrWhiteSpace(key);

Entities.IdentityServerServerSideSessions? existing = await dbContext.ServerSideSessions
.SingleOrDefaultAsync(x => x.Key == key);

if (existing == null)
{
logger.LogError("failed deleting '{SessionKey}' session in database, session not found", key);
return;
}

dbContext.ServerSideSessions.Remove(existing);

try
{
await dbContext.SaveChangesAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "exception deleting '{SessionKey}' session in database", key);
}
}
}
52 changes: 52 additions & 0 deletions src/EntityFramework.Storage/test/IntegrationTests/MockLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

#nullable enable

using System;
using Microsoft.Extensions.Logging;
using Moq;

namespace Open.IdentityServer.EntityFramework.IntegrationTests;

public class MockLogger<T> : ILogger<T>
{
private readonly ILogger<T> _mock = Mock.Of<ILogger<T>>();

public static MockLogger<T> Create() => new();

public IDisposable? BeginScope<TState>(TState state) where TState : notnull
=> _mock.BeginScope(state);

public bool IsEnabled(LogLevel logLevel)
=> _mock.IsEnabled(logLevel);

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
=> _mock.Log(logLevel, eventId, state, exception, formatter);

public void VerifyLog(LogLevel level, string message, Times? times = null)
{
Mock.Get(_mock)
.Verify(
x => x.Log(
level,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, _) => v.ToString()!.Contains(message)),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
times ?? Times.Once());
}

public void VerifyLog(LogLevel level, Times? times = null)
{
Mock.Get(_mock)
.Verify(
x => x.Log(
level,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
times ?? Times.Once());
}
}
Loading