前言

故事是这样开始的:

然后突然又来了句...

扪心自问自从不知道怎么当了 FreeSql 开发群 (QQ群号:4336577) 的管理以来, 几乎没有给 FreeSql 做过什么贡献...惭愧惭愧.

借此机会, 似乎可以做点什么.

整起来

根据官方文档描述, 如要实现自定义存储, 需要实现这3个接口, 它们分别是 IClientStore, IPersistedGrantStore, IResourceStore

新建一个项目 IdentityServer4.FreeSql.

然后新建一个目录 Stores, 用来放置几个接口的实现类.

新建实现类, 它们分别是 ClientStore.cs, PersistedGrantStore.cs, ResourceStore.cs

各自的实现代码如下:

// ClientStore.cs
using FreeSql;
using IdentityServer4.FreeSql.Interfaces;
using IdentityServer4.FreeSql.Mappers;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks; namespace IdentityServer4.FreeSql.Stores
{
public class ClientStore : IClientStore
{
/// <summary>
/// 数据库上下文
/// </summary>
protected readonly IConfigurationDbContext Context; /// <summary>
/// 日志
/// </summary>
protected readonly ILogger<ClientStore> Logger; /// <summary>
/// 初始化一个 <参阅 cref="ClientStore"/> 类的新实例.
/// </summary>
/// <param name="context">数据库上下文</param>
/// <param name="logger">日志</param>
/// <exception cref="ArgumentNullException">context</exception>
public ClientStore(IConfigurationDbContext context, ILogger<ClientStore> logger)
{
Context = context ?? throw new ArgumentNullException(paramName: nameof(context));
Logger = logger;
} /// <summary>
/// 通过客户端标识查找客户端
/// </summary>
/// <param name="clientId">客户端标识</param>
/// <returns>客户端</returns>
public virtual async Task<Client> FindClientByIdAsync(string clientId)
{
ISelect<Entities.Client> baseQuery = Context.Clients
.Where(x => x.ClientId == clientId)
.Take(1); var client = await baseQuery.ToOneAsync();
if (client == null) return null; await baseQuery.Include(x => x.AllowedCorsOrigins).IncludeMany(c => c.AllowedCorsOrigins).ToListAsync();
await baseQuery.Include(x => x.AllowedGrantTypes).IncludeMany(c => c.AllowedGrantTypes).ToListAsync();
await baseQuery.Include(x => x.AllowedScopes).IncludeMany(c => c.AllowedScopes).ToListAsync();
await baseQuery.Include(x => x.Claims).IncludeMany(c => c.Claims).ToListAsync();
await baseQuery.Include(x => x.ClientSecrets).IncludeMany(c => c.ClientSecrets).ToListAsync();
await baseQuery.Include(x => x.IdentityProviderRestrictions).IncludeMany(c => c.IdentityProviderRestrictions).ToListAsync();
await baseQuery.Include(x => x.PostLogoutRedirectUris).IncludeMany(c => c.PostLogoutRedirectUris).ToListAsync();
await baseQuery.Include(x => x.Properties).IncludeMany(c => c.Properties).ToListAsync();
await baseQuery.Include(x => x.RedirectUris).IncludeMany(c => c.RedirectUris).ToListAsync(); var model = client.ToModel(); Logger.LogDebug("{clientId} found in database: {clientIdFound}", clientId, model != null); return model;
}
}
}
// PersistedGrantStore.cs
using System.Collections.Generic;
using System;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.FreeSql.Interfaces;
using IdentityServer4.FreeSql.Mappers;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.Extensions.Logging;
using FreeSql; namespace IdentityServer4.FreeSql.Stores
{
/// <summary>
/// Implementation of IPersistedGrantStore thats uses FreeSql.
/// </summary>
/// <seealso cref="IdentityServer4.Stores.IPersistedGrantStore" />
public class PersistedGrantStore : IPersistedGrantStore
{
/// <summary>
/// The DbContext.
/// </summary>
protected readonly IPersistedGrantDbContext Context; /// <summary>
/// The logger.
/// </summary>
protected readonly ILogger Logger; /// <summary>
/// Initializes a new instance of the <see cref="PersistedGrantStore"/> class.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="logger">The logger.</param>
public PersistedGrantStore(IPersistedGrantDbContext context, ILogger<PersistedGrantStore> logger)
{
Context = context;
Logger = logger;
} /// <summary>
/// Stores the asynchronous.
/// </summary>
/// <param name="token">The token.</param>
/// <returns></returns>
public virtual async Task StoreAsync(PersistedGrant token)
{
var existing = await Context.PersistedGrants.Where(x => x.Key == token.Key).ToOneAsync();
if (existing == null)
{
Logger.LogDebug("{persistedGrantKey} not found in database", token.Key); var persistedGrant = token.ToEntity();
Context.PersistedGrants.Add(persistedGrant);
}
else
{
Logger.LogDebug("{persistedGrantKey} found in database", token.Key); token.UpdateEntity(existing);
} try
{
await Context.SaveChangesAsync();
}
catch (Exception ex)
{
Logger.LogWarning("exception updating {persistedGrantKey} persisted grant in database: {error}", token.Key, ex.Message);
}
} /// <summary>
/// Gets the grant.
/// </summary>
/// <param name="key">The key.</param>
/// <returns></returns>
public virtual async Task<PersistedGrant> GetAsync(string key)
{
var persistedGrant = await Context.PersistedGrants.Where(x => x.Key == key).ToOneAsync();
var model = persistedGrant?.ToModel(); Logger.LogDebug("{persistedGrantKey} found in database: {persistedGrantKeyFound}", key, model != null); return model;
} /// <summary>
/// Gets all grants for a given subject id.
/// </summary>
/// <param name="subjectId">The subject identifier.</param>
/// <returns></returns>
public virtual async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
{
var persistedGrants = await Context.PersistedGrants.Where(x => x.SubjectId == subjectId).ToListAsync();
var model = persistedGrants.Select(x => x.ToModel()); Logger.LogDebug("{persistedGrantCount} persisted grants found for {subjectId}", persistedGrants.Count, subjectId); return model;
} /// <summary>
/// Removes the grant by key.
/// </summary>
/// <param name="key">The key.</param>
/// <returns></returns>
public virtual async Task RemoveAsync(string key)
{
var persistedGrant = await Context.PersistedGrants.Where(x => x.Key == key).ToOneAsync();
if (persistedGrant != null)
{
Logger.LogDebug("removing {persistedGrantKey} persisted grant from database", key); Context.PersistedGrants.Remove(persistedGrant); try
{
await Context.SaveChangesAsync();
}
catch (Exception ex)
{
Logger.LogInformation("exception removing {persistedGrantKey} persisted grant from database: {error}", key, ex.Message);
}
}
else
{
Logger.LogDebug("no {persistedGrantKey} persisted grant found in database", key);
}
} /// <summary>
/// Removes all grants for a given subject id and client id combination.
/// </summary>
/// <param name="subjectId">The subject identifier.</param>
/// <param name="clientId">The client identifier.</param>
/// <returns></returns>
public virtual async Task RemoveAllAsync(string subjectId, string clientId)
{
var persistedGrants = await Context.PersistedGrants.Where(x => x.SubjectId == subjectId && x.ClientId == clientId).ToListAsync(); Logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}", persistedGrants.Count, subjectId, clientId); Context.PersistedGrants.RemoveRange(persistedGrants); try
{
await Context.SaveChangesAsync();
}
catch (Exception ex)
{
Logger.LogInformation("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}: {error}", persistedGrants.Count, subjectId, clientId, ex.Message);
}
} /// <summary>
/// Removes all grants of a give type for a given subject id and client id combination.
/// </summary>
/// <param name="subjectId">The subject identifier.</param>
/// <param name="clientId">The client identifier.</param>
/// <param name="type">The type.</param>
/// <returns></returns>
public virtual async Task RemoveAllAsync(string subjectId, string clientId, string type)
{
var persistedGrants = await Context.PersistedGrants.Where(x =>
x.SubjectId == subjectId &&
x.ClientId == clientId &&
x.Type == type).ToListAsync(); Logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {persistedGrantType}", persistedGrants.Count, subjectId, clientId, type); Context.PersistedGrants.RemoveRange(persistedGrants); try
{
await Context.SaveChangesAsync();
}
catch (Exception ex)
{
Logger.LogInformation("exception removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {persistedGrantType}: {error}", persistedGrants.Count, subjectId, clientId, type, ex.Message);
}
}
}
}
// ResourceStore.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.FreeSql.Interfaces;
using IdentityServer4.FreeSql.Mappers;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using FreeSql;
using Microsoft.Extensions.Logging; namespace IdentityServer4.FreeSql.Stores
{
/// <summary>
/// Implementation of IResourceStore thats uses FreeSql.
/// </summary>
/// <seealso cref="IdentityServer4.Stores.IResourceStore" />
public class ResourceStore : IResourceStore
{
/// <summary>
/// The DbContext.
/// </summary>
protected readonly IConfigurationDbContext Context; /// <summary>
/// The logger.
/// </summary>
protected readonly ILogger<ResourceStore> Logger; /// <summary>
/// Initializes a new instance of the <see cref="ResourceStore"/> class.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="logger">The logger.</param>
/// <exception cref="ArgumentNullException">context</exception>
public ResourceStore(IConfigurationDbContext context, ILogger<ResourceStore> logger)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
Logger = logger;
} /// <summary>
/// Finds the API resource by name.
/// </summary>
/// <param name="name">The name.</param>
/// <returns></returns>
public virtual async Task<ApiResource> FindApiResourceAsync(string name)
{
var query =
from apiResource in Context.ApiResources
where apiResource.Name == name
select apiResource; var apis = query
.IncludeMany(x => x.Secrets)
.IncludeMany(x => x.Scopes, then => then.IncludeMany(s => s.UserClaims))
.IncludeMany(x => x.UserClaims)
.IncludeMany(x => x.Properties); var api = await apis.ToOneAsync(); if (api != null)
{
Logger.LogDebug("Found {api} API resource in database", name);
}
else
{
Logger.LogDebug("Did not find {api} API resource in database", name);
} return api.ToModel();
} /// <summary>
/// Gets API resources by scope name.
/// </summary>
/// <param name="scopeNames"></param>
/// <returns></returns>
public virtual async Task<IEnumerable<ApiResource>> FindApiResourcesByScopeAsync(IEnumerable<string> scopeNames)
{
var names = scopeNames.ToArray(); var query =
from api in Context.ApiResources
where api.Scopes.Where(x => names.Contains(x.Name)).Any()
select api; var apis = query
.IncludeMany(x => x.Secrets)
.IncludeMany(x => x.Scopes, then => then.IncludeMany(s => s.UserClaims))
.IncludeMany(x => x.UserClaims)
.IncludeMany(x => x.Properties); var results = await apis.ToListAsync();
var models = results.Select(x => x.ToModel()).ToArray(); Logger.LogDebug("Found {scopes} API scopes in database", models.SelectMany(x => x.Scopes).Select(x => x.Name)); return models;
} /// <summary>
/// Gets identity resources by scope name.
/// </summary>
/// <param name="scopeNames"></param>
/// <returns></returns>
public virtual async Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeAsync(IEnumerable<string> scopeNames)
{
var scopes = scopeNames.ToArray(); var query =
from identityResource in Context.IdentityResources
where scopes.Contains(identityResource.Name)
select identityResource; /*
var resources = query
.Include(x => x.UserClaims)
.Include(x => x.Properties)
.AsNoTracking();
*/
var resources = query
.IncludeMany(x => x.UserClaims)
.IncludeMany(x => x.Properties); var results = await resources.ToListAsync(); Logger.LogDebug("Found {scopes} identity scopes in database", results.Select(x => x.Name)); return results.Select(x => x.ToModel()).ToArray();
} /// <summary>
/// Gets all resources.
/// </summary>
/// <returns></returns>
public virtual async Task<Resources> GetAllResourcesAsync()
{
/*
var identity = Context.IdentityResources
.Include(x => x.UserClaims)
.Include(x => x.Properties);
*/
var identity = Context.IdentityResources.Select
.IncludeMany(x => x.UserClaims)
.IncludeMany(x => x.Properties); /*
var apis = Context.ApiResources
.Include(x => x.Secrets)
.Include(x => x.Scopes)
.ThenInclude(s => s.UserClaims)
.Include(x => x.UserClaims)
.Include(x => x.Properties)
.AsNoTracking();
*/
var apis = Context.ApiResources.Select
.IncludeMany(x => x.Secrets)
.IncludeMany(x => x.Scopes, then => then.IncludeMany(s => s.UserClaims))
.IncludeMany(x => x.UserClaims)
.IncludeMany(x => x.Properties); var result = new Resources(
(await identity.ToListAsync()).Select(x => x.ToModel()),
(await apis.ToListAsync()).Select(x => x.ToModel())
); Logger.LogDebug("Found {scopes} as all scopes in database", result.IdentityResources.Select(x => x.Name).Union(result.ApiResources.SelectMany(x => x.Scopes).Select(x => x.Name))); return result;
}
}
}

这里没有直接用 FreeSql 的 DbContext 对象, 而是抽象了一层 IConfigurationDbContextIPersistedGrantDbContext 以便用接口约束需要的方法集.

// IConfigurationDbContext.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using FreeSql;
using IdentityServer4.FreeSql.Entities; namespace IdentityServer4.FreeSql.Interfaces
{
/// <summary>
/// 配置上下文的抽象
/// </summary>
/// <可参阅 cref="System.IDisposable">
public interface IConfigurationDbContext : IDisposable
{
/// <summary>
/// Gets or sets the clients.
/// </summary>
/// <value>
/// The clients.
/// </value>
DbSet<Client> Clients { get; set; } /// <summary>
/// Gets or sets the identity resources.
/// </summary>
/// <value>
/// The identity resources.
/// </value>
DbSet<IdentityResource> IdentityResources { get; set; } /// <summary>
/// Gets or sets the API resources.
/// </summary>
/// <value>
/// The API resources.
/// </value>
DbSet<ApiResource> ApiResources { get; set; } /// <summary>
/// Saves the changes.
/// </summary>
/// <returns></returns>
int SaveChanges(); /// <summary>
/// Saves the changes.
/// </summary>
/// <returns></returns>
Task<int> SaveChangesAsync();
}
}
// IPersistedGrantDbContext.cs
using System;
using System.Threading.Tasks;
using IdentityServer4.FreeSql.Entities;
using FreeSql; namespace IdentityServer4.FreeSql.Interfaces
{
/// <summary>
/// Abstraction for the operational data context.
/// </summary>
/// <seealso cref="System.IDisposable" />
public interface IPersistedGrantDbContext : IDisposable
{
/// <summary>
/// Gets or sets the persisted grants.
/// </summary>
/// <value>
/// The persisted grants.
/// </value>
DbSet<PersistedGrant> PersistedGrants { get; set; } /// <summary>
/// Gets or sets the device flow codes.
/// </summary>
/// <value>
/// The device flow codes.
/// </value>
DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; } /// <summary>
/// Saves the changes.
/// </summary>
/// <returns></returns>
int SaveChanges(); /// <summary>
/// Saves the changes.
/// </summary>
/// <returns></returns>
Task<int> SaveChangesAsync();
}
}

当然了不要忘了我们的日志 ILogger<ConfigurationDbContext>ILogger<PersistedGrantStore>, 方便后续我们跟踪调试.

数据库上下文

新建一个目录 DbContexts, 用来放置我们需要实现的数据库上下文类 ConfigurationDbContextPersistedGrantDbContext.

非常幸运的是, FreeSql 有对应 EntityFramework DbContext 类似的实现, 叫 FreeSql.DbContext, 又省了不少事.

实现如下:

// ConfigurationDbContext.cs
using FreeSql;
using IdentityServer4.FreeSql.Entities;
using IdentityServer4.FreeSql.Interfaces;
using IdentityServer4.FreeSql.Options;
using System;
using System.Threading.Tasks; namespace IdentityServer4.FreeSql.DbContexts
{
/// <summary>
/// DbContext for the IdentityServer configuration data.
/// </summary>
/// <seealso cref="FreeSql.DbContext" />
/// <seealso cref="IdentityServer4.FreeSql.Interfaces.IConfigurationDbContext" />
public class ConfigurationDbContext : ConfigurationDbContext<ConfigurationDbContext>
{
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationDbContext"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="storeOptions">The store options.</param>
/// <exception cref="ArgumentNullException">storeOptions</exception>
public ConfigurationDbContext(IFreeSql<ConfigurationDbContext> freeSql, ConfigurationStoreOptions storeOptions)
: base(freeSql, storeOptions)
{
}
} /// <summary>
/// DbContext for the IdentityServer configuration data.
/// </summary>
/// <seealso cref="Free.DbContext" />
/// <seealso cref="IdentityServer4.Free.Interfaces.IConfigurationDbContext" />
public class ConfigurationDbContext<TContext> : DbContext, IConfigurationDbContext
where TContext : DbContext, IConfigurationDbContext
{
private readonly IFreeSql<ConfigurationDbContext> freeSql;
//private readonly DbContextOptions options;
private readonly ConfigurationStoreOptions storeOptions; /// <summary>
/// Initializes a new instance of the <see cref="ConfigurationDbContext"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="storeOptions">The store options.</param>
/// <exception cref="ArgumentNullException">storeOptions</exception>
public ConfigurationDbContext(IFreeSql<ConfigurationDbContext> freeSql, ConfigurationStoreOptions storeOptions)
: base(freeSql, null)
{
this.freeSql = freeSql;
this.storeOptions = storeOptions ?? throw new ArgumentNullException(nameof(storeOptions));
} /// <summary>
/// Gets or sets the clients.
/// </summary>
/// <value>
/// The clients.
/// </value>
public DbSet<Client> Clients { get; set; } /// <summary>
/// Gets or sets the identity resources.
/// </summary>
/// <value>
/// The identity resources.
/// </value>
public DbSet<IdentityResource> IdentityResources { get; set; } /// <summary>
/// Gets or sets the API resources.
/// </summary>
/// <value>
/// The API resources.
/// </value>
public DbSet<ApiResource> ApiResources { get; set; } /// <summary>
/// Saves the changes.
/// </summary>
/// <returns></returns>
public override async Task<int> SaveChangesAsync()
{
return await base.SaveChangesAsync();
} /// <summary>
/// Override this method to further configure the model that was discovered by convention from the entity types
/// exposed in <see cref="T:FreeSql.DbSet`1" /> properties on your derived context. The resulting model may be cached
/// and re-used for subsequent instances of your derived context.
/// </summary>
/// <param name="modelBuilder">The builder being used to construct the model for this context. Databases (and other extensions) typically
/// define extension methods on this object that allow you to configure aspects of the model that are specific
/// to a given database.</param>
/// <remarks>
/// If a model is explicitly set on the options for this context (via <see cref="M:FreeSql.DbContextOptionsBuilder.UseModel(FreeSql.Metadata.IModel)" />)
/// then this method will not be run.
/// </remarks>
//protected override void OnModelCreating(ModelBuilder modelBuilder)
//{
// modelBuilder.ConfigureClientContext(storeOptions);
// modelBuilder.ConfigureResourcesContext(storeOptions); // base.OnModelCreating(modelBuilder);
//} protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseFreeSql(orm: freeSql);
//builder.UseOptions(options: options);
base.OnConfiguring(builder);
}
}
}
// PersistedGrantDbContext.cs
using System;
using System.Threading.Tasks;
using IdentityServer4.FreeSql.Entities;
using IdentityServer4.FreeSql.Interfaces;
using IdentityServer4.FreeSql.Options;
using FreeSql; namespace IdentityServer4.FreeSql.DbContexts
{
/// <summary>
/// DbContext for the IdentityServer operational data.
/// </summary>
/// <seealso cref="FreeSql.DbContext" />
/// <seealso cref="IdentityServer4.FreeSql.Interfaces.IPersistedGrantDbContext" />
public class PersistedGrantDbContext : PersistedGrantDbContext<PersistedGrantDbContext>
{
/// <summary>
/// Initializes a new instance of the <see cref="PersistedGrantDbContext"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="storeOptions">The store options.</param>
/// <exception cref="ArgumentNullException">storeOptions</exception>
public PersistedGrantDbContext(IFreeSql<PersistedGrantDbContext> freeSql, OperationalStoreOptions storeOptions)
: base(freeSql, storeOptions)
{
}
} /// <summary>
/// DbContext for the IdentityServer operational data.
/// </summary>
/// <seealso cref="FreeSql.DbContext" />
/// <seealso cref="IdentityServer4.FreeSql.Interfaces.IPersistedGrantDbContext" />
public class PersistedGrantDbContext<TContext> : DbContext, IPersistedGrantDbContext
where TContext : DbContext, IPersistedGrantDbContext
{
private readonly IFreeSql<PersistedGrantDbContext> freeSql;
private readonly OperationalStoreOptions storeOptions; /// <summary>
/// Initializes a new instance of the <see cref="PersistedGrantDbContext"/> class.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="storeOptions">The store options.</param>
/// <exception cref="ArgumentNullException">storeOptions</exception>
public PersistedGrantDbContext(IFreeSql<PersistedGrantDbContext> freeSql, OperationalStoreOptions storeOptions)
:base(freeSql, null)
{
this.freeSql = freeSql;
if (storeOptions == null) throw new ArgumentNullException(nameof(storeOptions));
this.storeOptions = storeOptions;
} /// <summary>
/// Gets or sets the persisted grants.
/// </summary>
/// <value>
/// The persisted grants.
/// </value>
public DbSet<PersistedGrant> PersistedGrants { get; set; } /// <summary>
/// Gets or sets the device codes.
/// </summary>
/// <value>
/// The device codes.
/// </value>
public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; } /// <summary>
/// Saves the changes.
/// </summary>
/// <returns></returns>
public override async Task<int> SaveChangesAsync()
{
return await base.SaveChangesAsync();
} /// <summary>
/// Override this method to further configure the model that was discovered by convention from the entity types
/// exposed in <see cref="T:Microsoft.EntityFrameworkCore.DbSet`1" /> properties on your derived context. The resulting model may be cached
/// and re-used for subsequent instances of your derived context.
/// </summary>
/// <param name="modelBuilder">The builder being used to construct the model for this context. Databases (and other extensions) typically
/// define extension methods on this object that allow you to configure aspects of the model that are specific
/// to a given database.</param>
/// <remarks>
/// If a model is explicitly set on the options for this context (via <see cref="M:Microsoft.EntityFrameworkCore.DbContextOptionsBuilder.UseModel(Microsoft.EntityFrameworkCore.Metadata.IModel)" />)
/// then this method will not be run.
/// </remarks>
//protected override void OnModelCreating(ModelBuilder modelBuilder)
//{
// modelBuilder.ConfigurePersistedGrantContext(storeOptions); // base.OnModelCreating(modelBuilder);
//}
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseFreeSql(orm: freeSql);
//builder.UseOptions(options);
base.OnConfiguring(builder);
}
}
}

实体模型

IdentityServer4 的实体及属性有辣么多...

这里为了方便, 直接整个 Entities 目录从 IdentityServer4.EntityFramework 里拷贝过来.

扩展方法和配置选项

我们需要提供选项功能, 以便你在注入和使用有可调整的控制能力.

有以下几项必须实现

  • FreeSql 实例的构造注入, 这点跟 EntityFramework 只有一个 DbContext 不同, FreeSql 和 FreeSql.DbContext 是 2 个分开的对象
  • IdentityServer4.EntityFramework 中已经提供的最佳实践配置项, 毕竟过来人.
  • 提供注入服时必需的扩展方法, 不然你还得手动 new ...
  • ...

集成测试

这里用的是传说中的用户测试大法(我自己)...先用 SQLite 试试水

这里用 FreeSql 的 CodeFirst 模式, 自动生成数据结构.

  • 新建一个 ASP.NET Core 应用程序;
  • 添加依赖 IdentityServer4.FreeSql
  • 添加依赖 FreeSql 和驱动提供器 FreeSql.Provider.Sqlite
  • Startup.cs 里实例化 FreeSql, 注入服务
  • ...

看下集成测试的项目文件:

// IdentityServer4.FreeSql.IntegrationTest.csproj
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> <ItemGroup>
<PackageReference Include="FreeSql" Version="1.2.0" />
<PackageReference Include="FreeSql.Provider.Sqlite" Version="1.2.0" />
</ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\IdentityServer4.FreeSql\IdentityServer4.FreeSql.csproj" />
</ItemGroup> </Project>

再来看看 Startup.cs 中的 FreeSql 实例化以及 IdentityServer4.FreeSql 的服务配置注入.

// Startup.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using FreeSql;
using IdentityServer4.FreeSql.DbContexts;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
//using IdentityServer4.FreeSql; namespace IdentityServer4.FreeSql.IntegrationTest
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
var freeSqlC = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, @"Data Source=|DataDirectory|\idsr_freesql_config.db;Pooling=true;Max Pool Size=10")
.UseAutoSyncStructure(true)
.UseNoneCommandParameter(true)
.UseMonitorCommand(cmd => Trace.WriteLine(cmd.CommandText))
.Build<ConfigurationDbContext>(); var freeSqlO = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, @"Data Source=|DataDirectory|\idsr_freesql_op.db;Pooling=true;Max Pool Size=10")
.UseAutoSyncStructure(true)
.UseNoneCommandParameter(true)
.UseMonitorCommand(cmd => Trace.WriteLine(cmd.CommandText))
.Build<PersistedGrantDbContext>(); services.AddSingleton<IFreeSql<ConfigurationDbContext>>(freeSqlC);
services.AddSingleton<IFreeSql<PersistedGrantDbContext>>(freeSqlO);
services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseFreeSql(orm: freeSqlC);
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseFreeSql(orm: freeSqlO); // this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 3600; // interval in seconds (default is 3600)
});
} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseRouting();
app.UseIdentityServer(); app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
}
}

先来几个效果图:

CodeFirst 生成的 2 个 SQLite 数据库:

可以有效访问的发现端点以及对应的查询 SQL 日志:

更多内容

留待下回分解...~其实是其他的还没测试~

源码后续能见人了...也会放出来给大家溜溜

源码在这 IdentityServer4.FreeSql 不要问我为什么用 gitee, 仅仅是因为对于大多数人而言国内访问更快

参考

IdentityServer4.FreeSql 持久化实现的更多相关文章

  1. 认证授权:IdentityServer4 - 数据持久化

    前言: 前面的文章中IdentityServer4 配置内容都存储到内存中,本篇文章开始把配置信息存储到数据库中:本篇文章继续基于github的代码来实现配置数据持久化到MySQL中 一.基于EFCo ...

  2. IdentityServer4之持久化很顺手的事

    前言 原计划打算在春节期间多分享几篇技术文章的,但到最后一篇也没出,偷懒了吗?算是吧,过程是这样的:每次拿出电脑,在孩姥姥家的院子总有阳光沐浴,看不清屏幕,回屋又有点冷(在强行找理由),于是又带着娃遛 ...

  3. 使用MySql对IdentityServer4进行持久化

    哈喽大家好,看见网上很少有使用MySql进行持久化的,毕竟又很多坑,说句实话,就连 MySql.Data.EntityFrameworkCore 都有问题,不知道是.net core更新太快还是其它的 ...

  4. IdentityServer4之Authorization Code(授权码)相对更安全

    前言 接着授权模式聊,这次说说Authorization Code(授权码)模式,熟悉的微博接入.微信接入.QQ接入都是这种方式(这里说的是oauth2.0的授权码模式),从用户体验上来看,交互方式和 ...

  5. 从零搭建一个IdentityServer——项目搭建

    本篇文章是基于ASP.NET CORE 5.0以及IdentityServer4的IdentityServer搭建,为什么要从零搭建呢?IdentityServer4本身就有很多模板可以直接生成一个可 ...

  6. 【.NET Core项目实战-统一认证平台】第九章 授权篇-使用Dapper持久化IdentityServer4

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了IdentityServer4的源码分析的内容,让我们知道了IdentityServer4的一些运行原理,这篇将介绍如何使用d ...

  7. IdentityServer4认证服务器集成Identity&配置持久化数据库

    文章简介 asp.net core的空Web项目集成相关dll和页面文件配置IdnetityServer4认证服务器 Ids4集成Identity Ids4配置持久化到数据库 写在最前面,此文章不详细 ...

  8. IdentityServer4系列 | 支持数据持久化

    一.前言 在前面的篇章介绍中,一些基础配置如API资源.客户端资源等数据以及使用过程中发放的令牌等操作数据,我们都是通过将操作数据和配置数据存储在内存中进行实现的,而在实际开发生产中,我们需要考虑如何 ...

  9. (十一)React Ant Design Pro + .Net5 WebApi:后端环境搭建-IdentityServer4(三)持久化

    一.前言 IdentityServer配合EFCore持久化,框架已经为我们准备了两个上下文: ConfigurationDbContext:配置数据(资源.客户端.身份等) PersistedGra ...

随机推荐

  1. 脚手架搭建vue项目

    1.安装安装node.js: 2.cnpm install vue-cli -g (全局安装,需要注意的是我这里是用淘宝镜像安装的,没有安装cnpm的需要先去安装一下) 3.vue --version ...

  2. 编译原理_P1001

    1 绝大部分软件使用高级程序设计语言来编写.用这些语言编写的软件必须经过编译器的编译,才能转换为可以在计算机上运行的机器代码.编译器所生成代码的正确性和质量会直接影响成千上万的软件.虽然大部分人不会参 ...

  3. VMware-workstation虚拟机安装及配置

    目录 安装准备 开始安装 设置虚拟机文件默认位置 安装准备 系统环境:Windows10 专业版 软件:VMware-workstation-full-14.0.0.24051.exe 秘钥:FF31 ...

  4. dubbo同步/异步调用的方式

    我们知道,Dubbo 缺省协议采用单一长连接,底层实现是 Netty 的 NIO 异步通讯机制:基于这种机制,Dubbo 实现了以下几种调用方式: 同步调用(默认) 异步调用 参数回调 事件通知 同步 ...

  5. spring boot GlobalExceptionHandler @RestControllerAdvice @ExceptionHandler

    package me.zhengjie.common.exception.handler; import lombok.extern.slf4j.Slf4j; import me.zhengjie.c ...

  6. day42-进程池

    #进程池Pool:apply apply_async-close-join-get map callback #1.进程池Pool:执行下面代码发现任务012先执行,345后执行,因为进程池只有3个进 ...

  7. scala编程(七)——内建控制结构

    几乎所有的 Scala 的控制结构都会产生某个值.这是函数式语言所采用的方式,程序被看成是计算值的活动,因此程序的控件也应当这么做.另外,指令式语言经常具有三元操作符(如 C,C++和 Java 的? ...

  8. OA-APP增加空间

    第一步:虚拟机增加一块200G的硬盘,使用fdisk -l 命令可以看到增加的硬盘(centos6可能需要重启系统) 第二步:然后对 /dev/sdc进行分区 第三步:创建一个分区 第四步:重新查看磁 ...

  9. prisoners-of-war|

    The Nazi kept those ________ in their concentration camps.  A. prisoner-of-wars  B. prisoner-of-war ...

  10. Users组权限Win7虚拟机继承Administrator的个性化设置

    在administrator账号下进行的模板设置,配置文件保存在“C:\Documents and Settings\Administrator”文件夹下的profile里面,但是创建的用户虚拟机获取 ...