Search API Sample
A sample is provided that implements the user and identity role search integration API protocol. It is implemented using ASP.NET Core, and uses a middleware that encapsulates most of the protocol details. The only custom code required is implementing two interfaces (one for users, and another for identity roles) that performs the actual search in the external store.
Hosting
The sample is based on an empty ASP.NET Core hosting application. It references the PolicyServer.Management.Api.Abstractions NuGet packages, which contains the middleware.
The Startup
code then looks something like:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddPolicyServerSearchApi(new PolicyServerSearchApiOptions {
Authority = "https://your_policy_server/",
})
.AddUserSearchService<YourUserSearchClassName>()
.AddRoleSearchService<YourIdentityRoleSearchClassName>();
}
public void Configure(IApplicationBuilder app)
{
app.UsePolicyServerSearchApi();
}
}
AddPolicyServerSearchApi
will add the services to implement the search protocol.
The PolicyServerSearchApiOptions
parameter allows configuring the Authority
which should be the base URL of the PolicyServer authority.
This is used to validate the token passed for authentication.
AddUserSearchService
and AddRoleSearchService
allow configuring your implementations of the user and identity role search services.
UsePolicyServerSearchApi
configures the middleware in the pipeline to handle requests for the paths /users
and /roles
.
Search Interfaces
To implement the user and identity role search, you would need to implement the IUserSearchService
and IRoleSearchService
interfaces, respectively.
The definitions are:
public interface IUserSearchService
{
Task<QueryResult<UserSearchResult>> SearchUsers(UserSearch query);
}
public interface IRoleSearchService
{
Task<QueryResult<RoleSearchResult>> SearchRoles(RoleSearch query);
}
The UserSearch
and RoleSearch
hold the request parameters:
public class Query
{
public Query(string filter, int page = 1, int count = 20);
public string Filter { get; set; }
public int Count { get; set; }
public int Page { get; set; }
public int Start { get; }
}
public class UserSearch : Query
{
public string Tenant { get; }
}
public class RoleSearch : Query
{
public string Tenant { get; }
}
The Query
base class constructor handles the logic to determine the starting row number based on the page and count requested.
And the QueryResult<UserSearchResult>
and QueryResult<RoleSearchResult>
model the results:
public class QueryResult<T>
{
public QueryResult(Query query, int totalCount, IEnumerable<T> results);
public int TotalCount { get; set; }
public int TotalPages { get; }
public IEnumerable<T> Items { get; set; }
}
The QueryResult<T>
constructor takes care of the logic of determining the total pages based on the query.
With the UserSearchResult
and RoleSearchResult
definitions:
public class UserSearchResult
{
public string SubjectId { get; set; }
public string DisplayName { get; set; }
}
public class RoleSearchResult
{
public string RoleName { get; set; }
public string Description { get; set; }
}
A sample implementation that uses ASP.NET Identity might look something like this:
public class AspNetIdentitySearch : IUserSearchService, IRoleSearchService
{
private readonly IdentityDbContext _identityDb;
public AspNetIdentitySearch(IdentityDbContext identityDb)
{
_identityDb = identityDb;
}
public async Task<QueryResult<UserSearchResult>> SearchUsers(UserSearch query)
{
var filter = query.Filter;
var users = _identityDb.Users;
var q =
from user in users
where user.UserName.Contains(filter) ||
user.Email.Contains(filter) ||
filter == null
select new UserSearchResult
{
SubjectId = user.Id,
DisplayName = $"{user.UserName} ({user.Email})",
};
var totalCount = await q.CountAsync();
var items = await q.Skip(query.Start).Take(query.Count).ToArrayAsync();
var result = new QueryResult<UserSearchResult>(query, totalCount, items);
return result;
}
public async Task<QueryResult<RoleSearchResult>> SearchRoles(RoleSearch query)
{
var filter = query.Filter;
var roles = _identityDb.Roles;
var q =
from role in roles
where role.Name.Contains(filter) || filter == null
select new RoleSearchResult
{
RoleName = role.Name
};
var totalCount = await q.CountAsync();
var items = await q.Skip(query.Start).Take(query.Count).ToArrayAsync();
var result = new QueryResult<RoleSearchResult>(query, totalCount, items);
return result;
}
}
Notice in the above sample, it is up to the logic in the implementation to decide how to use the filter.
The use of the Start
and Count
are then used to obtain the correct page.
Finally, the QueryResult
constructor simply requires the total count and the items for the current page.