using System.Collections.Immutable;
using System.Globalization;
using Dpz.Core.Public.Entity.Auth;
using OpenIddict.Abstractions;

namespace Dpz.Core.Service.OpenIddictStores;

public sealed class DpzScopeStore(IRepository<DpzScope> repository, ILogger<DpzScopeStore> logger)
    : IOpenIddictScopeStore<DpzScope>
{
    public async ValueTask<long> CountAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Counting scopes");
        return await repository.MongodbQueryable.LongCountAsync(
            cancellationToken: cancellationToken
        );
    }

    public async ValueTask<long> CountAsync<TResult>(
        Func<IQueryable<DpzScope>, IQueryable<TResult>> query,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Counting scopes with custom query");
        return await query(repository.MongodbQueryable)
            .LongCountAsync(cancellationToken: cancellationToken);
    }

    public async ValueTask CreateAsync(DpzScope scope, CancellationToken cancellationToken)
    {
        logger.LogInformation("Creating scope {Name} ({Id})", scope.Name, scope.Id);
        await repository.InsertAsync(scope, cancellationToken);
    }

    public async ValueTask DeleteAsync(DpzScope scope, CancellationToken cancellationToken)
    {
        logger.LogWarning("Deleting scope {Name} ({Id})", scope.Name, scope.Id);
        await repository.DeleteAsync(scope.Id, cancellationToken);
    }

    public async ValueTask<DpzScope?> FindByIdAsync(
        string identifier,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Find scope by id {Identifier}", identifier);
        if (ObjectId.TryParse(identifier, out var id))
        {
            return await repository.FindAsync(id, cancellationToken);
        }
        return null;
    }

    public async ValueTask<DpzScope?> FindByNameAsync(
        string name,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Find scope by name {Name}", name);
        return await repository
            .SearchFor(x => x.Name == name)
            .FirstOrDefaultAsync(cancellationToken);
    }

    public IAsyncEnumerable<DpzScope> FindByNamesAsync(
        ImmutableArray<string> names,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Find scopes by names (count={Count})", names.Length);
        if (names.IsDefaultOrEmpty)
        {
            return AsyncEnumerable.Empty<DpzScope>();
        }

        var filter = Builders<DpzScope>.Filter.In(x => x.Name, names);
        return repository.SearchForAsync(filter, cancellationToken);
    }

    public IAsyncEnumerable<DpzScope> FindByResourceAsync(
        string resource,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Find scopes by resource {Resource}", resource);
        var filter = Builders<DpzScope>.Filter.AnyEq(x => x.Resources, resource);
        return repository.SearchForAsync(filter, cancellationToken);
    }

    public async ValueTask<TResult?> GetAsync<TState, TResult>(
        Func<IQueryable<DpzScope>, TState, IQueryable<TResult>> query,
        TState state,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Get with custom query state={State}", state);
        return await Task.FromResult(query(repository.MongodbQueryable, state).FirstOrDefault());
    }

    public async ValueTask<string?> GetDescriptionAsync(
        DpzScope scope,
        CancellationToken cancellationToken
    )
    {
        logger.LogTrace("Get description of scope {Id}", scope.Id);
        return await ValueTask.FromResult(scope.Description);
    }

    public async ValueTask<ImmutableDictionary<CultureInfo, string>> GetDescriptionsAsync(
        DpzScope scope,
        CancellationToken cancellationToken
    )
    {
        logger.LogTrace("Get descriptions of scope {Id}", scope.Id);
        if (scope.Descriptions is null || scope.Descriptions.Count == 0)
        {
            return await ValueTask.FromResult(ImmutableDictionary<CultureInfo, string>.Empty);
        }

        var builder = ImmutableDictionary.CreateBuilder<CultureInfo, string>();
        foreach (var kv in scope.Descriptions)
        {
            try
            {
                builder[CultureInfo.GetCultureInfo(kv.Key)] = kv.Value;
            }
            catch
            {
                // 跳过无法解析的文化键
            }
        }
        return await ValueTask.FromResult(builder.ToImmutable());
    }

    public async ValueTask<string?> GetDisplayNameAsync(
        DpzScope scope,
        CancellationToken cancellationToken
    )
    {
        logger.LogTrace("Get display name of scope {Id}", scope.Id);
        return await ValueTask.FromResult(scope.DisplayName);
    }

    public async ValueTask<ImmutableDictionary<CultureInfo, string>> GetDisplayNamesAsync(
        DpzScope scope,
        CancellationToken cancellationToken
    )
    {
        logger.LogTrace("Get display names of scope {Id}", scope.Id);
        if (scope.DisplayNames is null || scope.DisplayNames.Count == 0)
        {
            return await ValueTask.FromResult(ImmutableDictionary<CultureInfo, string>.Empty);
        }

        var builder = ImmutableDictionary.CreateBuilder<CultureInfo, string>();
        foreach (var kv in scope.DisplayNames)
        {
            try
            {
                builder[CultureInfo.GetCultureInfo(kv.Key)] = kv.Value;
            }
            catch
            {
                // 跳过无法解析的文化键
            }
        }
        return await ValueTask.FromResult(builder.ToImmutable());
    }

    public async ValueTask<string?> GetIdAsync(DpzScope scope, CancellationToken cancellationToken)
    {
        logger.LogTrace("Get id of scope {Id}", scope.Id);
        return await ValueTask.FromResult(scope.Id.ToString());
    }

    public async ValueTask<string?> GetNameAsync(
        DpzScope scope,
        CancellationToken cancellationToken
    )
    {
        logger.LogTrace("Get name of scope {Id}", scope.Id);
        return await ValueTask.FromResult(scope.Name);
    }

    public ValueTask<ImmutableDictionary<string, JsonElement>> GetPropertiesAsync(
        DpzScope scope,
        CancellationToken cancellationToken
    )
    {
        logger.LogTrace("Get properties of scope {Id}", scope.Id);
        return ValueTask.FromResult(
            OpenIddictBsonPropertySerializer.ToProperties(scope.Properties)
        );
    }

    public async ValueTask<ImmutableArray<string>> GetResourcesAsync(
        DpzScope scope,
        CancellationToken cancellationToken
    )
    {
        logger.LogTrace("Get resources of scope {Id}", scope.Id);
        var list = scope.Resources ?? [];
        return await ValueTask.FromResult(ImmutableArray.CreateRange(list));
    }

    public async ValueTask<DpzScope> InstantiateAsync(CancellationToken cancellationToken)
    {
        logger.LogInformation("Instantiate new scope");
        return await ValueTask.FromResult(new DpzScope { Id = ObjectId.GenerateNewId() });
    }

    public IAsyncEnumerable<DpzScope> ListAsync(
        int? count,
        int? offset,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation(
            "List scopes with pagination: offset={Offset}, count={Count}",
            offset,
            count
        );
        var options = new FindOptions<DpzScope>();
        if (offset.HasValue)
        {
            options.Skip = offset.Value;
        }
        if (count.HasValue)
        {
            options.Limit = count.Value;
        }

        return repository.SearchForAsync(
            Builders<DpzScope>.Filter.Empty,
            options,
            cancellationToken
        );
    }

    public async IAsyncEnumerable<TResult> ListAsync<TState, TResult>(
        Func<IQueryable<DpzScope>, TState, IQueryable<TResult>> query,
        TState state,
        [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken
    )
    {
        logger.LogInformation("List scopes with custom query state={State}", state);
        var projected = query(repository.MongodbQueryable, state);
        await foreach (
            var element in projected.ToAsyncEnumerable().WithCancellation(cancellationToken)
        )
        {
            yield return element;
        }
    }

    public async ValueTask SetDescriptionAsync(
        DpzScope scope,
        string? description,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Set description of scope {Id}", scope.Id);
        scope.Description = description;
        await ValueTask.CompletedTask;
    }

    public async ValueTask SetDescriptionsAsync(
        DpzScope scope,
        ImmutableDictionary<CultureInfo, string> descriptions,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Set descriptions of scope {Id}", scope.Id);
        scope.Descriptions = descriptions.ToDictionary(k => k.Key.Name, v => v.Value);
        await ValueTask.CompletedTask;
    }

    public async ValueTask SetDisplayNameAsync(
        DpzScope scope,
        string? name,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Set display name of scope {Id}", scope.Id);
        scope.DisplayName = name;
        await ValueTask.CompletedTask;
    }

    public async ValueTask SetDisplayNamesAsync(
        DpzScope scope,
        ImmutableDictionary<CultureInfo, string> names,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Set display names of scope {Id}", scope.Id);
        scope.DisplayNames = names.ToDictionary(k => k.Key.Name, v => v.Value);
        await ValueTask.CompletedTask;
    }

    public async ValueTask SetNameAsync(
        DpzScope scope,
        string? name,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Set name of scope {Id}", scope.Id);
        scope.Name = name;
        await ValueTask.CompletedTask;
    }

    public ValueTask SetPropertiesAsync(
        DpzScope scope,
        ImmutableDictionary<string, JsonElement> properties,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Set properties of scope {Id}", scope.Id);
        scope.Properties = OpenIddictBsonPropertySerializer.ToDocument(properties);
        return ValueTask.CompletedTask;
    }

    public async ValueTask SetResourcesAsync(
        DpzScope scope,
        ImmutableArray<string> resources,
        CancellationToken cancellationToken
    )
    {
        logger.LogInformation("Set resources of scope {Id}", scope.Id);
        scope.Resources = resources.IsDefault ? [] : resources.ToArray();
        await ValueTask.CompletedTask;
    }

    public async ValueTask UpdateAsync(DpzScope scope, CancellationToken cancellationToken)
    {
        logger.LogInformation("Updating scope {Name} ({Id})", scope.Name, scope.Id);
        await repository.UpdateAsync(scope, cancellationToken);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释:OpenIddict Scope Store 实现

这是一个实现了 OpenIddict 框架中 IOpenIddictScopeStore<DpzScope> 接口的自定义作用域存储类,用于在 MongoDB 数据库中管理 OAuth 2.0/OpenID Connect 的作用域(Scope)数据。

核心功能

1. 类定义与依赖注入

public sealed class DpzScopeStore(IRepository<DpzScope> repository, ILogger<DpzScopeStore> logger)
  • 使用主构造函数(C# 12特性)注入 MongoDB 仓储和日志记录器
  • sealed 表示该类不可被继承

2. CRUD 操作

创建 (Create)

  • CreateAsync: 插入新的作用域记录
  • InstantiateAsync: 创建新的作用域实例(生成新的 ObjectId)

读取 (Read)

  • FindByIdAsync: 根据 ID 查找作用域
  • FindByNameAsync: 根据名称查找作用域
  • FindByNamesAsync: 批量根据名称查找(返回异步枚举)
  • FindByResourceAsync: 根据资源标识符查找关联的作用域
  • ListAsync: 支持分页的列表查询

更新 (Update)

  • UpdateAsync: 更新作用域信息
  • 多个 Set*Async 方法:设置作用域的各个属性

删除 (Delete)

  • DeleteAsync: 删除指定作用域

3. 统计功能

CountAsync(): 统计所有作用域数量
CountAsync<TResult>(...): 使用自定义查询统计

4. 属性访问方法

基本属性

  • GetIdAsync: 获取 ID(转换为字符串)
  • GetNameAsync: 获取名称
  • GetDisplayNameAsync: 获取显示名称
  • GetDescriptionAsync: 获取描述

多语言支持

GetDescriptionsAsync / GetDisplayNamesAsync
  • 返回 ImmutableDictionary<CultureInfo, string>,支持多文化本地化
  • 处理文化键解析异常(跳过无效的文化信息)

关联资源

GetResourcesAsync(): 返回作用域关联的资源列表

扩展属性

GetPropertiesAsync() / SetPropertiesAsync()
  • 使用 OpenIddictBsonPropertySerializer 在 BSON 和 JSON 之间转换自定义属性

5. 高级查询

GetAsync<TState, TResult>(): 执行自定义 LINQ 查询
ListAsync<TState, TResult>(): 执行自定义查询并返回异步枚举
  • 支持传入状态参数和自定义查询逻辑
  • 使用 yield return 实现流式数据返回

6. 关键技术细节

MongoDB 过滤器构建

Builders<DpzScope>.Filter.In(x => x.Name, names)  // IN 查询
Builders<DpzScope>.Filter.AnyEq(x => x.Resources, resource)  // 数组元素匹配

异步枚举处理

[EnumeratorCancellation] // 支持取消令牌传播
await foreach (var element in projected.ToAsyncEnumerable()...)

不可变集合使用

  • 方法返回值大量使用 ImmutableArrayImmutableDictionary
  • 确保数据安全性和线程安全

日志记录

  • 所有操作都有对应的日志记录
  • 使用不同的日志级别(Trace/Information/Warning)

应用场景

此类用于 OpenIddict 身份认证服务器中,管理 OAuth 2.0 作用域,例如:

  • openidprofileemail 等标准作用域
  • 自定义 API 作用域
  • 支持多语言的作用域描述和显示名称
  • 将作用域与资源服务器关联

总结

这是一个完整的、生产级的 MongoDB 存储适配器实现,展示了如何将 OpenIddict 与 MongoDB 集成,包含了完善的错误处理、日志记录和异步操作支持。

评论加载中...