Membership API: Managers and access
The managers the generator writes for a resource's member class and for its role class, BoardMemberManager and BoardRoleManager, and what they take; what the application answers about who may be a member and which roles go to one; the access manager of one kind of resource, the requirements a request states, and the contracts that publish a resource's access to row access rules. A generated manager is named after your class and closed over your types: each section shows the package's class it derives from, with the methods it inherits. The access manager is asked for by the resource's class, MembershipAccessManager<Board>, and the requirements by the resource's id, so those of documents never answer for folders. Their registration is with Entity Framework, under Registration.
Member manager
Namespace DTS.Membership.Managers.
MemberManager<TResource, TResourceId, TMember, TId, TMemberId, TRoleId>
public class MemberManager<TResource, TResourceId, TMember, TId, TMemberId, TRoleId>
where TResource : AggregateRoot<TResourceId>, IMembershipResource, IMembershipResource<TMember, TMemberId, TRoleId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
where TMember : MemberEntity<TId, TMemberId, TRoleId>
where TId : struct, IEntityId, IEquatable<TId>
where TMemberId : struct, IEntityId, IEquatable<TMemberId>
where TRoleId : struct, IEntityId, IEquatable<TRoleId>
The members of one kind of resource: adding them, giving and taking their roles, removing them, leaving, naming another owner, opening a new resource, and listing who is on one. Your code takes it as BoardMemberManager, a class the package's generator writes for your member class, closed over your resource and its ids, and the container makes per scope.
public sealed class AddMemberHandler(BoardMemberManager members) : ICommandHandler<AddMember, Board>
{
public async ValueTask<Board> Handle(AddMember command, CancellationToken cancellationToken)
=> await members.AddAsync(command.Board, command.Seat, command.Role, until: null, cancellationToken);
}
Each change takes the resource, or its id. Both do the same: the manager loads the resource itself, tracked, and answers it as it changed and saved it. A member and a role are ids, since a member is somebody of another module and a role is a row of your role class or a name the rules declare. After the cancellation token comes the version the caller last read, for a command that names one: a resource at another version is a lost race.
What a change checks, in order. The caller's right first, as the access manager refuses: the caller's own refusal when it is nobody, not-found for a resource it may not see, not-permitted naming the permission. A change of the members needs the permission the rules mark ManagesMembers, and naming an owner the one marked NamesOwner; a resource that marks none lets only system work make the change. Where the request in hand was checked for that permission on that resource, the manager takes the check's answer and asks nothing again. Then whether the member and the role may go on the list: an active member where the application registered an IMemberDirectory<TResourceId, TMemberId>, a role in use that goes to a member. Then the resource's guard, OnChangingMembers(), and the member list's rules, then the save.
The caller's own place. A caller that takes a role from itself, takes itself off, leaves, or hands on the resource it owns can lose the very right the save needs, where a database checks every row. Such a save runs as the resource's own work in its scope, for the caller (IOwnPlaceWork<TResourceId, TMemberId>), when the request in hand is the one whose check read the resource and the unit of work holds nothing but the resource. Otherwise every save runs as the caller.
What stays yours. The resource, its fields and its constructor, which opens its member list. The hooks the generator declares on it: the guard, OnChangingMembers(), and the events, OnMemberAdded, OnMemberRemoved, OnRoleGiven, OnRoleTaken and OnOwnerNamed. A rule of your own, such as that nobody gives a role for longer than they hold the permission, asked with the access manager before the call.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.TMember: The application's member class.TId: The member class's own id.TMemberId: What a member is known by.TRoleId: What a role is known by.
| Member | Summary |
|---|---|
MostResources = 200 | The most resources one list of several is asked about. |
MemberManager(MembershipAccessManager<TResource>, IMemberStore<TResource, TResourceId, TMemberId>, ICurrentUser, TimeProvider, IMemberRoles<TResourceId, TRoleId>?, IMemberDirectory<TResourceId, TMemberId>?, IMemberRolePolicy<TResourceId, TRoleId>?, IOwnPlaceWork<TResourceId, TMemberId>?) | The member manager of one kind of resource. |
MembershipRules Rules { get; } | The rules of the resource, with the codes it refuses under. |
OpenAsync(TResource, CancellationToken) | Opens a new resource: the owner its constructor put on its member list is admitted, and the resource is added and saved. |
AddAsync(TResourceId, TMemberId, TRoleId?, DateTimeOffset?, CancellationToken, long?) | Adds a member, from now until until or for good, with role for the same period where one is given. |
AddAsync(TResource, TMemberId, TRoleId?, DateTimeOffset?, CancellationToken, long?) | Adds a member, as AddAsync does with the resource's id. |
GiveRoleAsync(TResourceId, TMemberId, TRoleId, DateTimeOffset?, CancellationToken, long?) | Gives a member a role, from now until until or for good, next to whatever roles it holds. |
GiveRoleAsync(TResource, TMemberId, TRoleId, DateTimeOffset?, CancellationToken, long?) | Gives a member a role, as GiveRoleAsync does with the resource's id. |
TakeRoleAsync(TResourceId, TMemberId, TRoleId, CancellationToken, long?) | Takes a role from a member, who stays a member with whatever else it holds. |
TakeRoleAsync(TResource, TMemberId, TRoleId, CancellationToken, long?) | Takes a role from a member, as TakeRoleAsync does with the resource's id. |
RemoveAsync(TResourceId, TMemberId, CancellationToken, long?) | Takes a member off the resource, with every role it holds. |
RemoveAsync(TResource, TMemberId, CancellationToken, long?) | Takes a member off the resource, as RemoveAsync does with the resource's id. |
LeaveAsync(TResourceId, CancellationToken, long?) | Takes the caller off the resource, with every role it holds there. |
LeaveAsync(TResource, CancellationToken, long?) | Takes the caller off the resource, as LeaveAsync does with the resource's id. |
NameOwnerAsync(TResourceId, TMemberId, FormerOwner<TRoleId>, CancellationToken, long?) | Names another owner: a member from now on and with no end, added when not a member yet, holding no role. |
NameOwnerAsync(TResource, TMemberId, FormerOwner<TRoleId>, CancellationToken, long?) | Names another owner, as NameOwnerAsync does with the resource's id. |
ListAsync(TResourceId, CancellationToken) | The members of a resource the caller sees, read for showing: the owner first, then by when each membership starts, then by member, and each member's roles by when each starts, then by role. |
ListAsync(TResource, CancellationToken) | The members of a resource, as ListAsync answers them with the resource's id. |
ListAsync(IReadOnlyCollection<TResourceId>, CancellationToken) | The members of each of resources the caller sees, in one statement whatever their number, each list in the order ListAsync answers in. |
MemberManager(MembershipAccessManager<TResource>, IMemberStore<TResource, TResourceId, TMemberId>, ICurrentUser, TimeProvider, IMemberRoles<TResourceId, TRoleId>?, IMemberDirectory<TResourceId, TMemberId>?, IMemberRolePolicy<TResourceId, TRoleId>?, IOwnPlaceWork<TResourceId, TMemberId>?)
public MemberManager(
MembershipAccessManager<TResource> access,
IMemberStore<TResource, TResourceId, TMemberId> store,
ICurrentUser user,
TimeProvider clock,
IMemberRoles<TResourceId, TRoleId>? roles = null,
IMemberDirectory<TResourceId, TMemberId>? directory = null,
IMemberRolePolicy<TResourceId, TRoleId>? policy = null,
IOwnPlaceWork<TResourceId, TMemberId>? ownPlace = null)
The member manager of one kind of resource. The container makes it; the generated class has the same constructor.
Parameters
access: The resource's access manager, which asks what the caller holds.store: Where the resource is loaded and saved.user: Who is calling.clock: What "now" is.roles: Which roles there are; the rules answer it themselves where the roles are fixed.directory: Who may be made a member, ornullwhen everybody may.policy: Which roles may go to a member, ornullwhen every one may.ownPlace: How the save of a change to the caller's own place begins, ornullfor system work in the resource's scope.
OpenAsync(TResource, CancellationToken)
public Task<TResource> OpenAsync(TResource resource, CancellationToken cancellationToken)
Opens a new resource: the owner its constructor put on its member list is admitted, and the resource is added and saved. Who may open one is the request's to say: there is no resource yet to hold a permission on.
Parameters
resource: The resource, just made, with its member list opened in its constructor.cancellationToken: Cancels the work.
Returns: The resource, saved.
Exceptions
RefusalException:member-not-activefor an owner the application's directory does not admit.
Example
var board = await members.OpenAsync(new Board(BoardId.Create(), command.Name, owner, clock.GetUtcNow()), cancellationToken);
AddAsync(TResourceId, TMemberId, TRoleId?, DateTimeOffset?, CancellationToken, long?)
public Task<TResource> AddAsync(
TResourceId resource,
TMemberId member,
TRoleId? role,
DateTimeOffset? until,
CancellationToken cancellationToken,
long? expectedVersion = null)
Adds a member, from now until until or for good, with role for the same period where one is given. Who adds it is the caller, as a member of the resource.
Parameters
resource: The resource.member: The member.role: The role it holds from the start, ornullfor none.until: When the membership ends, ornullfor no end.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, with the member on it.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that manages the members,member-not-active,role-not-for-members, what the resource's guard refuses,invalid-periodandalready-member, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
Example
var board = await members.AddAsync(command.Board, command.Seat, command.Role, until: null, cancellationToken);
AddAsync(TResource, TMemberId, TRoleId?, DateTimeOffset?, CancellationToken, long?)
public Task<TResource> AddAsync(
TResource resource,
TMemberId member,
TRoleId? role,
DateTimeOffset? until,
CancellationToken cancellationToken,
long? expectedVersion = null)
Adds a member, as AddAsync does with the resource's id.
Parameters
resource: The resource.member: The member.role: The role it holds from the start, ornullfor none.until: When the membership ends, ornullfor no end.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, with the member on it.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that manages the members,member-not-active,role-not-for-members, what the resource's guard refuses,invalid-periodandalready-member, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
Example
var board = await members.AddAsync(command.Board, command.Seat, command.Role, until: null, cancellationToken);
GiveRoleAsync(TResourceId, TMemberId, TRoleId, DateTimeOffset?, CancellationToken, long?)
public Task<TResource> GiveRoleAsync(
TResourceId resource,
TMemberId member,
TRoleId role,
DateTimeOffset? until,
CancellationToken cancellationToken,
long? expectedVersion = null)
Gives a member a role, from now until until or for good, next to whatever roles it holds. Who gives it is the caller, as a member of the resource.
Parameters
resource: The resource.member: The member.role: The role.until: When the role ends, ornullfor no end.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, with the role given.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that manages the members,role-not-for-members, what the resource's guard refuses,invalid-period,member-not-found,role-held, andowner-protectedfor the owner, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
GiveRoleAsync(TResource, TMemberId, TRoleId, DateTimeOffset?, CancellationToken, long?)
public Task<TResource> GiveRoleAsync(
TResource resource,
TMemberId member,
TRoleId role,
DateTimeOffset? until,
CancellationToken cancellationToken,
long? expectedVersion = null)
Gives a member a role, as GiveRoleAsync does with the resource's id.
Parameters
resource: The resource.member: The member.role: The role.until: When the role ends, ornullfor no end.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, with the role given.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that manages the members,role-not-for-members, what the resource's guard refuses,invalid-period,member-not-found,role-held, andowner-protectedfor the owner, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
TakeRoleAsync(TResourceId, TMemberId, TRoleId, CancellationToken, long?)
public Task<TResource> TakeRoleAsync(
TResourceId resource,
TMemberId member,
TRoleId role,
CancellationToken cancellationToken,
long? expectedVersion = null)
Takes a role from a member, who stays a member with whatever else it holds. Any role can be taken, one that is no longer in use too. A role the caller takes from itself is saved as the resource's own work (see the class).
Parameters
resource: The resource.member: The member.role: The role.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, with the role taken.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that manages the members, what the resource's guard refuses,member-not-foundandrole-not-held, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
TakeRoleAsync(TResource, TMemberId, TRoleId, CancellationToken, long?)
public Task<TResource> TakeRoleAsync(
TResource resource,
TMemberId member,
TRoleId role,
CancellationToken cancellationToken,
long? expectedVersion = null)
Takes a role from a member, as TakeRoleAsync does with the resource's id.
Parameters
resource: The resource.member: The member.role: The role.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, with the role taken.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that manages the members, what the resource's guard refuses,member-not-foundandrole-not-held, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
RemoveAsync(TResourceId, TMemberId, CancellationToken, long?)
public Task<TResource> RemoveAsync(
TResourceId resource,
TMemberId member,
CancellationToken cancellationToken,
long? expectedVersion = null)
Takes a member off the resource, with every role it holds. The owner stays until somebody else is named owner. A caller that takes itself off is saved as the resource's own work (see the class).
Parameters
resource: The resource.member: The member.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, without the member.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that manages the members, what the resource's guard refuses,member-not-foundandowner-protected, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
RemoveAsync(TResource, TMemberId, CancellationToken, long?)
public Task<TResource> RemoveAsync(
TResource resource,
TMemberId member,
CancellationToken cancellationToken,
long? expectedVersion = null)
Takes a member off the resource, as RemoveAsync does with the resource's id.
Parameters
resource: The resource.member: The member.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, without the member.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that manages the members, what the resource's guard refuses,member-not-foundandowner-protected, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
LeaveAsync(TResourceId, CancellationToken, long?)
public Task<TResource> LeaveAsync(
TResourceId resource,
CancellationToken cancellationToken,
long? expectedVersion = null)
Takes the caller off the resource, with every role it holds there. It needs no permission: the caller sees the resource, is a member of it and is not its owner. Saved as the resource's own work (see the class).
Parameters
resource: The resource.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, without the caller.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundfor a resource it does not see; what the resource's guard refuses;member-not-foundfor a caller that is no member;owner-protectedfor the owner. Under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
LeaveAsync(TResource, CancellationToken, long?)
public Task<TResource> LeaveAsync(
TResource resource,
CancellationToken cancellationToken,
long? expectedVersion = null)
Takes the caller off the resource, as LeaveAsync does with the resource's id.
Parameters
resource: The resource.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, without the caller.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundfor a resource it does not see; what the resource's guard refuses;member-not-foundfor a caller that is no member;owner-protectedfor the owner. Under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
NameOwnerAsync(TResourceId, TMemberId, FormerOwner<TRoleId>, CancellationToken, long?)
public Task<TResource> NameOwnerAsync(
TResourceId resource,
TMemberId owner,
FormerOwner<TRoleId> formerOwner,
CancellationToken cancellationToken,
long? expectedVersion = null)
Names another owner: a member from now on and with no end, added when not a member yet, holding no role. The owner before becomes what formerOwner says: a member in a role, or nobody on the list. The resource's OnOwnerNamed hook hears everything that changed. An owner that hands on its own resource is saved as the resource's own work (see the class).
Parameters
resource: The resource.owner: The member that is to own the resource.formerOwner:FormerOwner.StaysAs(role), orFormerOwner.Leaves.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, with its new owner.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that names the owner,member-not-activefor the new owner,role-not-for-membersfor the role the owner before stays in, what the resource's guard refuses, andalready-owner, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
NameOwnerAsync(TResource, TMemberId, FormerOwner<TRoleId>, CancellationToken, long?)
public Task<TResource> NameOwnerAsync(
TResource resource,
TMemberId owner,
FormerOwner<TRoleId> formerOwner,
CancellationToken cancellationToken,
long? expectedVersion = null)
Names another owner, as NameOwnerAsync does with the resource's id.
Parameters
resource: The resource.owner: The member that is to own the resource.formerOwner:FormerOwner.StaysAs(role), orFormerOwner.Leaves.cancellationToken: Cancels the work.expectedVersion: The version of the resource the caller last read, ornull.
Returns: The resource, with its new owner.
Exceptions
RefusalException:not-found,not-permittedwithout the permission that names the owner,member-not-activefor the new owner,role-not-for-membersfor the role the owner before stays in, what the resource's guard refuses, andalready-owner, under the resource's codes.ConcurrencyConflictException: The resource is not atexpectedVersion, or changed while this was saved.
ListAsync(TResourceId, CancellationToken)
public Task<IReadOnlyList<TMember>> ListAsync(
TResourceId resource,
CancellationToken cancellationToken)
The members of a resource the caller sees, read for showing: the owner first, then by when each membership starts, then by member, and each member's roles by when each starts, then by role. Members whose membership has ended, or is still to start, are in it too; member.AppliesAt(now) and member.HoldsAt(role, now) say what counts at a moment.
Parameters
resource: The resource.cancellationToken: Cancels the query.
Returns: Its members, your member class, as they were read.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundfor a resource it does not see.
ListAsync(TResource, CancellationToken)
public Task<IReadOnlyList<TMember>> ListAsync(
TResource resource,
CancellationToken cancellationToken)
The members of a resource, as ListAsync answers them with the resource's id.
Parameters
resource: The resource.cancellationToken: Cancels the query.
Returns: Its members, your member class, as they were read.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundfor a resource it does not see.
ListAsync(IReadOnlyCollection<TResourceId>, CancellationToken)
public Task<IReadOnlyDictionary<TResourceId, IReadOnlyList<TMember>>> ListAsync(
IReadOnlyCollection<TResourceId> resources,
CancellationToken cancellationToken)
The members of each of resources the caller sees, in one statement whatever their number, each list in the order ListAsync answers in. A resource the caller does not see is not in the answer.
Parameters
resources: The resources, at mostMostResourcesdifferent ones.cancellationToken: Cancels the query.
Returns: The members of each resource the caller sees, by its id.
Exceptions
ArgumentException: More thanMostResourcesdifferent resources are asked about.RefusalException: The caller's own refusal when it is nobody.
IMemberStore<TResource, TResourceId, TMemberId>
public interface IMemberStore<TResource, TResourceId, TMemberId>
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
where TMemberId : struct, IEntityId, IEquatable<TMemberId>
Where the member manager of one kind of resource loads, adds and saves the resource: the unit of work of the scope. A storage package implements it and registers it with the resource; an application never takes it.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.TMemberId: What a member is known by.
| Member | Summary |
|---|---|
TMemberId? CurrentMember { get; } | Who the current caller is as a member of the resource, as its rules say, or null for a caller that is nobody's member, system work among them: who a change is recorded as made by. |
FindAsync(TResourceId, long?, CancellationToken) | The resource with its members, tracked by the unit of work for a change and its save; or null when it is not there for the caller. |
ReadAsync(IReadOnlyCollection<TResourceId>, MemberReach<TResource>, CancellationToken) | The resources of resources within reach, each with its members, read for showing: the unit of work does not track them. |
Add(TResource) | Adds a new resource to the unit of work. |
ChangedBesides(IReadOnlySet<object>) | What the unit of work would write besides parts: the name of each kind of thing it holds a change to that is none of them, or none. |
SaveAsync(CancellationToken) | Saves everything the unit of work holds, with the domain events raised, in one transaction. |
FindAsync(TResourceId, long?, CancellationToken)
Task<TResource?> FindAsync(
TResourceId resource,
long? expectedVersion,
CancellationToken cancellationToken)
The resource with its members, tracked by the unit of work for a change and its save; or null when it is not there for the caller.
Parameters
resource: The resource.expectedVersion: The version the caller last read, ornullto change the resource as it is now: a resource at another version is a lost race.cancellationToken: Cancels the query.
Exceptions
ConcurrencyConflictException: The resource is not atexpectedVersion.
ReadAsync(IReadOnlyCollection<TResourceId>, MemberReach<TResource>, CancellationToken)
Task<IReadOnlyList<TResource>> ReadAsync(
IReadOnlyCollection<TResourceId> resources,
MemberReach<TResource> reach,
CancellationToken cancellationToken)
The resources of resources within reach, each with its members, read for showing: the unit of work does not track them.
Parameters
resources: The resources asked about.reach: The resources the caller sees, as the resource's access manager made it.cancellationToken: Cancels the query.
Add(TResource)
void Add(TResource resource)
Adds a new resource to the unit of work.
Parameters
resource: The resource, just made.
ChangedBesides(IReadOnlySet<object>)
IReadOnlyList<string> ChangedBesides(IReadOnlySet<object> parts)
What the unit of work would write besides parts: the name of each kind of thing it holds a change to that is none of them, or none.
Parameters
parts: The resource, its members and the roles they hold, each by reference.
SaveAsync(CancellationToken)
Task SaveAsync(CancellationToken cancellationToken)
Saves everything the unit of work holds, with the domain events raised, in one transaction.
Parameters
cancellationToken: Cancels the save.
Exceptions
ConcurrencyConflictException: Somebody else saved the same resource first.
IOwnPlaceWork<TResourceId, TMemberId>
public interface IOwnPlaceWork<TResourceId, TMemberId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
where TMemberId : struct, IEntityId, IEquatable<TMemberId>
Begins the resource's own work for the save of a change to the caller's own place on it: a role it takes from itself, itself taken off, leaving, or handing on the resource it owns. Such a change can take away the very right the save needs where a database checks every row, so the member manager saves it as the resource's own work in its scope, for the request in hand whose check read the resource, when the unit of work holds nothing else.
Optional. Without one the manager begins system work in that scope for the current user, access.BeginSystem(scope) on the core's AccessManager. For rules that name no scope it begins access.BeginSystemBypassingRowAccess(). The join with an organization's package registers one that begins it in the caller's organization, for the caller's seat.
Type parameters
TResourceId: The id of the kind of resource the members are of.TMemberId: What a member is known by.
| Member | Summary |
|---|---|
Begin(TMemberId, string?) | Begins the resource's own work in scope, for member, the caller. |
Begin(TMemberId, string?)
IDisposable Begin(TMemberId member, string? scope)
Begins the resource's own work in scope, for member, the caller.
Parameters
member: The caller, as a member of the resource.scope: The scope of the resource's own work, one its rules name;nullfor rules that name none, whose own work is the application's, past every row access rule.
Returns: What ends the work when disposed.
Role manager
Namespace DTS.Membership.Managers.
CustomRoleManager<TRole, TRoleId, TResource>
public class CustomRoleManager<TRole, TRoleId, TResource>
where TRole : CustomRoleAggregate<TRoleId>
where TRoleId : struct, IEntityId, IEquatable<TRoleId>
where TResource : class
The roles a customer makes for one kind of resource: making them, the starter roles among them, renaming them, setting their permissions, archiving them, following their starter roles, and reading them. Your code takes it as BoardRoleManager, a class the package's generator writes for your role class, closed over it and the resource, and the container makes per scope.
public sealed class RenameBoardRoleHandler(BoardRoleManager roles) : ICommandHandler<RenameBoardRole, BoardRole>
{
public async ValueTask<BoardRole> Handle(RenameBoardRole command, CancellationToken cancellationToken)
=> await roles.RenameAsync(command.Role, command.Name, command.Description, cancellationToken);
}
Each change takes the role, or its id; both load the role and answer it as it was changed and saved. The manager hands the role the resource's rules, so no handler does. Each change needs the permission the rules name for it (Roles.ManagedWith), held for the whole of what the roles are kept for: the manager asks it of what the application registered for that (ICustomRoleAccess<TResource>), which the join with an organization's package answers for the whole organization. Where the rules name none, or nothing answers, only system work changes the roles.
What stays yours. The role class, its constructor and the fields it adds, such as the customer a role is of. Its events: the generator declares the partial methods OnRenamed(), OnPermissionsSet(set) and OnArchived() on it, and the role calls each after a change that changed something. A role class kept per a scope of your own, a garden say, passes that scope to the reads and to the starter roles: scope: role => role.GardenId == garden.
The reads answer the roles there are for the caller: what the context's own rules let through, the roles of the caller's organization for a class marked with the organization package's marker. They ask no permission.
Type parameters
TRole: The application's role class.TRoleId: The role class's id.TResource: The resource the roles are of.
| Member | Summary |
|---|---|
MostIds = 200 | How many ids one question by id takes. |
CustomRoleManager(ICustomRoleStore<TRole, TRoleId>, MembershipAccessManager<TResource>, ICurrentUser, ICustomRoleAccess<TResource>?) | The role manager of one role class. |
MembershipRules Rules { get; } | The rules of the resource the roles are of: what a role's constructor takes. |
CreateAsync(TRole, CancellationToken) | Adds a role made with your class's constructor, and saves it. |
CreateStarterRolesAsync(Func<CustomRoleDraft, TRole>, CancellationToken, Expression<Func<TRole, bool>>?) | Makes the starter roles the rules declare that the roles there are for the caller do not have yet, each through make, and saves them. |
FollowStarterRolesAsync(Func<CustomRoleDraft, TRole>, CancellationToken, Expression<Func<TRole, bool>>?) | Makes the starter roles the rules declare that the roles there are for the caller do not have yet, each through make, and makes every other role made from a starter role follow it (CustomRoleAggregate.FollowStarterRole), in one save. |
RenameAsync(TRoleId, string, string?, CancellationToken) | Renames a role and says again what it is for. |
RenameAsync(TRole, string, string?, CancellationToken) | Renames a role, as RenameAsync does with its id. |
SetPermissionsAsync(TRoleId, IReadOnlyCollection<string>, CancellationToken) | Sets the permissions a role gives, for every member that holds it. |
SetPermissionsAsync(TRole, IReadOnlyCollection<string>, CancellationToken) | Sets the permissions a role gives, as SetPermissionsAsync does with its id. |
ArchiveAsync(TRoleId, CancellationToken) | Archives a role: it stays on every member that holds it, gives nothing from now on, and is not given again. |
ArchiveAsync(TRole, CancellationToken) | Archives a role, as ArchiveAsync does with its id. |
ListAsync(CancellationToken, Expression<Func<TRole, bool>>?) | Every role there is for the caller, archived ones included, by name: your class, whole, read for showing. |
ByIdAsync(IReadOnlyCollection<TRoleId>, CancellationToken) | The roles of roles there are for the caller, read for showing: for a list that names the roles its rows hold by id. |
CustomRoleManager(ICustomRoleStore<TRole, TRoleId>, MembershipAccessManager<TResource>, ICurrentUser, ICustomRoleAccess<TResource>?)
public CustomRoleManager(
ICustomRoleStore<TRole, TRoleId> store,
MembershipAccessManager<TResource> access,
ICurrentUser user,
ICustomRoleAccess<TResource>? roleAccess = null)
The role manager of one role class. The container makes it; the generated class has the same constructor.
Parameters
store: Where the roles are loaded and saved.access: The access manager of the resource the roles are of, for its rules.user: Who is calling.roleAccess: Whether the caller may change the roles, ornullwhere only system work may.
CreateAsync(TRole, CancellationToken)
public Task<TRole> CreateAsync(TRole role, CancellationToken cancellationToken)
Adds a role made with your class's constructor, and saves it.
Parameters
role: The role, just made.cancellationToken: Cancels the work.
Returns: The role, saved.
Exceptions
RefusalException:not-permittedwithout the permission that changes the roles; whatever your context refuses a second role of one name with.
Example
var role = await roles.CreateAsync(new BoardRole(BoardRoleId.Create(), organization, new CustomRoleDraft(command.Name, command.Description, command.Permissions)), cancellationToken);
CreateStarterRolesAsync(Func<CustomRoleDraft, TRole>, CancellationToken, Expression<Func<TRole, bool>>?)
public Task<IReadOnlyList<TRole>> CreateStarterRolesAsync(
Func<CustomRoleDraft, TRole> make,
CancellationToken cancellationToken,
Expression<Func<TRole, bool>>? scope = null)
Makes the starter roles the rules declare that the roles there are for the caller do not have yet, each through make, and saves them. A starter role is told by what it was made from, so one a customer renamed or archived is not made again, and asking twice makes nothing the second time.
Parameters
make: Makes a role of your class from a starter role's draft: its name, its permissions and the starter role it is made from.cancellationToken: Cancels the work.scope: The roles of one scope of your own, for a role class kept per that scope; left out, every role there is for the caller.
Returns: The roles it made, in the order the rules declare them; none when every one is there.
Exceptions
RefusalException:not-permittedwithout the permission that changes the roles.
Example
var made = await roles.CreateStarterRolesAsync(draft => new BoardRole(BoardRoleId.Create(), organization, draft), cancellationToken);
FollowStarterRolesAsync(Func<CustomRoleDraft, TRole>, CancellationToken, Expression<Func<TRole, bool>>?)
public Task<StarterRolesFollowed<TRole, TRoleId>> FollowStarterRolesAsync(
Func<CustomRoleDraft, TRole> make,
CancellationToken cancellationToken,
Expression<Func<TRole, bool>>? scope = null)
Makes the starter roles the rules declare that the roles there are for the caller do not have yet, each through make, and makes every other role made from a starter role follow it (CustomRoleAggregate.FollowStarterRole), in one save. A permission the starter role gained is added, a permission it lost is taken out, and what the customer changed stays. An archived role follows nothing and is not made again. A renamed role follows.
System work alone runs it, in any scope, because what a starter role holds is the application's decision. In an application joined to an organization's package, that package runs it when it makes a customer and at every start of the host, through the class the generator writes for the role class. It saves only when something changed, so a second run in a row reads and writes nothing. A starter role whose name a customer already gave another role fails the save of a role class with a unique index on the name, and with it the rest of that customer's roles, until that role is renamed.
An application with no such package calls CreateStarterRolesAsync, or this, itself. The toolkit cannot know when one of its scopes begins.
Parameters
make: Makes a role of your class from a starter role's draft: its name, its permissions and the starter role it is made from.cancellationToken: Cancels the work.scope: The roles of one scope of your own, for a role class kept per that scope; left out, every role there is for the caller.
Returns: The roles it made, the roles whose permissions changed, and the roles whose starter role the rules no longer declare.
Exceptions
RefusalException:access.system-onlyfor anyone but begun system work.
Example
using (access.BeginSystem("gardens"))
{
var followed = await roles.FollowStarterRolesAsync(draft => new PlotRole(PlotRoleId.Create(), garden, draft), cancellationToken, scope: role => role.GardenId == garden);
}
RenameAsync(TRoleId, string, string?, CancellationToken)
public Task<TRole> RenameAsync(
TRoleId role,
string name,
string? description,
CancellationToken cancellationToken)
Renames a role and says again what it is for. The same name and description again change nothing.
Parameters
role: The role.name: Its name, as it is to be.description: What it is for, as it is to be;nullclears it.cancellationToken: Cancels the work.
Returns: The role, renamed.
Exceptions
RefusalException:not-permittedwithout the permission that changes the roles;role-not-found,role-archived,role-name-invalid, under the resource's codes; whatever your context refuses a second role of one name with.
RenameAsync(TRole, string, string?, CancellationToken)
public Task<TRole> RenameAsync(
TRole role,
string name,
string? description,
CancellationToken cancellationToken)
Renames a role, as RenameAsync does with its id.
Parameters
role: The role.name: Its name, as it is to be.description: What it is for, as it is to be;nullclears it.cancellationToken: Cancels the work.
Returns: The role, renamed.
Exceptions
RefusalException:not-permittedwithout the permission that changes the roles;role-not-found,role-archived,role-name-invalid, under the resource's codes; whatever your context refuses a second role of one name with.
SetPermissionsAsync(TRoleId, IReadOnlyCollection<string>, CancellationToken)
public Task<TRole> SetPermissionsAsync(
TRoleId role,
IReadOnlyCollection<string> permissions,
CancellationToken cancellationToken)
Sets the permissions a role gives, for every member that holds it. The same permissions again change nothing.
Parameters
role: The role.permissions: Every permission it gives from now on.cancellationToken: Cancels the work.
Returns: The role, with its new permissions.
Exceptions
RefusalException:not-permittedwithout the permission that changes the roles;role-not-found,role-archived,permission-not-for-members, under the resource's codes.
SetPermissionsAsync(TRole, IReadOnlyCollection<string>, CancellationToken)
public Task<TRole> SetPermissionsAsync(
TRole role,
IReadOnlyCollection<string> permissions,
CancellationToken cancellationToken)
Sets the permissions a role gives, as SetPermissionsAsync does with its id.
Parameters
role: The role.permissions: Every permission it gives from now on.cancellationToken: Cancels the work.
Returns: The role, with its new permissions.
Exceptions
RefusalException:not-permittedwithout the permission that changes the roles;role-not-found,role-archived,permission-not-for-members, under the resource's codes.
ArchiveAsync(TRoleId, CancellationToken)
public Task<TRole> ArchiveAsync(TRoleId role, CancellationToken cancellationToken)
Archives a role: it stays on every member that holds it, gives nothing from now on, and is not given again.
Parameters
role: The role.cancellationToken: Cancels the work.
Returns: The role, archived.
Exceptions
RefusalException:not-permittedwithout the permission that changes the roles;role-not-foundandrole-archived, under the resource's codes.
ArchiveAsync(TRole, CancellationToken)
public Task<TRole> ArchiveAsync(TRole role, CancellationToken cancellationToken)
Archives a role, as ArchiveAsync does with its id.
Parameters
role: The role.cancellationToken: Cancels the work.
Returns: The role, archived.
Exceptions
RefusalException:not-permittedwithout the permission that changes the roles;role-not-foundandrole-archived, under the resource's codes.
ListAsync(CancellationToken, Expression<Func<TRole, bool>>?)
public Task<IReadOnlyList<TRole>> ListAsync(
CancellationToken cancellationToken,
Expression<Func<TRole, bool>>? scope = null)
Every role there is for the caller, archived ones included, by name: your class, whole, read for showing.
Parameters
cancellationToken: Cancels the query.scope: The roles of one scope of your own; left out, every role there is for the caller.
ByIdAsync(IReadOnlyCollection<TRoleId>, CancellationToken)
public Task<IReadOnlyList<TRole>> ByIdAsync(
IReadOnlyCollection<TRoleId> roles,
CancellationToken cancellationToken)
The roles of roles there are for the caller, read for showing: for a list that names the roles its rows hold by id. A role that is not there for the caller is not in the answer.
Parameters
roles: The roles, at mostMostIdsdifferent ones.cancellationToken: Cancels the query.
Exceptions
ArgumentException: More thanMostIdsdifferent roles are asked about.
ICustomRoleStore<TRole, TRoleId>
public interface ICustomRoleStore<TRole, TRoleId>
where TRole : CustomRoleAggregate<TRoleId>
where TRoleId : struct, IEntityId, IEquatable<TRoleId>
Where the role manager of one role class loads, adds and saves roles: the unit of work of the scope, under the rules the application's context keeps its rows to, such as the customer a role is of. A storage package implements it and registers it with the resource the roles are of; an application never takes it.
Type parameters
TRole: The application's role class.TRoleId: The role class's id.
| Member | Summary |
|---|---|
FindAsync(TRoleId, CancellationToken) | The role, tracked for a change and its save; or null when it is not there for the caller. |
ListAsync(Expression<Func<TRole, bool>>?, bool, CancellationToken) | The roles there are for the caller, archived ones included, within scope where one is given: tracked, for the starter roles made beside them, or read for showing. |
ByIdAsync(IReadOnlyCollection<TRoleId>, CancellationToken) | The roles of roles there are for the caller, read for showing. |
Add(TRole) | Adds a new role to the unit of work. |
SaveAsync(CancellationToken) | Saves everything the unit of work holds, with the domain events raised, in one transaction. |
FindAsync(TRoleId, CancellationToken)
Task<TRole?> FindAsync(TRoleId role, CancellationToken cancellationToken)
The role, tracked for a change and its save; or null when it is not there for the caller.
Parameters
role: The role.cancellationToken: Cancels the query.
ListAsync(Expression<Func<TRole, bool>>?, bool, CancellationToken)
Task<IReadOnlyList<TRole>> ListAsync(
Expression<Func<TRole, bool>>? scope,
bool tracked,
CancellationToken cancellationToken)
The roles there are for the caller, archived ones included, within scope where one is given: tracked, for the starter roles made beside them, or read for showing.
Parameters
scope: The roles of one scope of the application's own, ornullfor every role there is for the caller.tracked: Whether the unit of work tracks them.cancellationToken: Cancels the query.
ByIdAsync(IReadOnlyCollection<TRoleId>, CancellationToken)
Task<IReadOnlyList<TRole>> ByIdAsync(
IReadOnlyCollection<TRoleId> roles,
CancellationToken cancellationToken)
The roles of roles there are for the caller, read for showing.
Parameters
roles: The roles asked about.cancellationToken: Cancels the query.
Add(TRole)
void Add(TRole role)
Adds a new role to the unit of work.
Parameters
role: The role, just made.
SaveAsync(CancellationToken)
Task SaveAsync(CancellationToken cancellationToken)
Saves everything the unit of work holds, with the domain events raised, in one transaction.
Parameters
cancellationToken: Cancels the save.
ICustomRoleAccess<TResource>
public interface ICustomRoleAccess<TResource>
where TResource : class
Whether the caller may change the roles a customer makes for one kind of resource: holds the permission the rules name for it (Roles.ManagedWith) for the whole of what the roles are kept for. The roles are a customer's as a whole, not one resource's, so Membership cannot answer it from a resource's members: the role manager asks this.
Optional. Without one only system work changes the roles. The join with an organization's package registers one that asks the organization whether the caller holds the permission for the whole of it.
Type parameters
TResource: The resource the roles are of.
| Member | Summary |
|---|---|
RequireAsync(string, CancellationToken) | Requires the caller to hold permission for the whole of what the roles are kept for. |
RequireAsync(string, CancellationToken)
ValueTask RequireAsync(string permission, CancellationToken cancellationToken)
Requires the caller to hold permission for the whole of what the roles are kept for.
Parameters
permission: The permission the rules name for changing the roles.cancellationToken: Cancels the question.
Exceptions
RefusalException: The caller does not hold it.
StarterRolesFollowed<TRole, TRoleId>
public sealed record StarterRolesFollowed<TRole, TRoleId>
where TRole : CustomRoleAggregate<TRoleId>
where TRoleId : struct, IEntityId, IEquatable<TRoleId>
What following the starter roles did (CustomRoleManager.FollowStarterRolesAsync). A role that follows its starter role already, one a customer made and one that is archived are in none of the lists.
Type parameters
TRole: The application's role class.TRoleId: The role class's id.
| Member | Summary |
|---|---|
StarterRolesFollowed(IReadOnlyList<TRole>, IReadOnlyList<TRoleId>, IReadOnlyList<TRoleId>) | The primary constructor: the type's summary says what it makes. |
IReadOnlyList<TRole> Made { get; init; } | The roles made, in the order the rules declare their starter roles. |
IReadOnlyList<TRoleId> Changed { get; init; } | The roles whose permissions changed, each with the class's OnPermissionsSet(set) called. |
IReadOnlyList<TRoleId> WithoutTheirStarterRole { get; init; } | The roles made from a starter role the rules no longer declare. They keep their permissions, and follow the starter role again if the rules declare it again under the same name. |
StarterRolesFollowed(IReadOnlyList<TRole>, IReadOnlyList<TRoleId>, IReadOnlyList<TRoleId>)
public StarterRolesFollowed(
IReadOnlyList<TRole> Made,
IReadOnlyList<TRoleId> Changed,
IReadOnlyList<TRoleId> WithoutTheirStarterRole)
The primary constructor: the type's summary says what it makes.
Parameters
Made: The roles made, in the order the rules declare their starter roles.Changed: The roles whose permissions changed, each with the class'sOnPermissionsSet(set)called.WithoutTheirStarterRole: The roles made from a starter role the rules no longer declare. They keep their permissions, and follow the starter role again if the rules declare it again under the same name.
Who may be a member
Namespace DTS.Membership.Managers.
IMemberDirectory<TResourceId, TMemberId>
public interface IMemberDirectory<TResourceId, TMemberId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
where TMemberId : struct, IEntityId, IEquatable<TMemberId>
Says whether somebody can be made a member of one kind of resource: whether the id is one the application knows, and whoever it stands for is active. An application implements it where it keeps its users, and the member manager of that kind of resource asks it before a member is added, an owner named or a resource opened.
Optional. Without one every id is admitted: a resource whose members are users of an identity provider often has nowhere to ask. The join with an organization's package answers it for members that are seats.
It is asked for by the resource, so two kinds of resource whose members are known by the same id each have their own, or share one registered for both.
Type parameters
TResourceId: The id of the kind of resource the members are of.TMemberId: What a member is known by.
| Member | Summary |
|---|---|
IsActiveAsync(TMemberId, CancellationToken) | Whether member is known and active, so that it may be made a member. |
IsActiveAsync(TMemberId, CancellationToken)
ValueTask<bool> IsActiveAsync(TMemberId member, CancellationToken cancellationToken)
Whether member is known and active, so that it may be made a member.
Parameters
member: The one to be made a member.cancellationToken: Cancels the lookup.
IMemberRolePolicy<TResourceId, TRoleId>
public interface IMemberRolePolicy<TResourceId, TRoleId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
where TRoleId : struct, IEntityId, IEquatable<TRoleId>
Says whether a role that exists may go to a member of one kind of resource: a rule of the application's own on top of the roles there are, such as roles an organization keeps for itself and never gives on a resource. The member manager of that kind of resource asks it when a role is given; what was given once can always be taken back, so it is not asked when a role is taken.
Optional. Without one every role there is may go to a member. It is asked about the role alone: whether this caller may give it is the manager's to check by the permission that manages the members, and a rule of the application's own beyond that is asked in its handler, before the manager is called.
Type parameters
TResourceId: The id of the kind of resource the members are of.TRoleId: What a role is known by.
| Member | Summary |
|---|---|
MayHoldAsync(TRoleId, CancellationToken) | Whether role may be given to a member of the resource. |
MayHoldAsync(TRoleId, CancellationToken)
ValueTask<bool> MayHoldAsync(TRoleId role, CancellationToken cancellationToken)
Whether role may be given to a member of the resource.
Parameters
role: A role that exists.cancellationToken: Cancels the lookup.
Access manager
Namespace DTS.Membership.Access.
MembershipAccessManager
public static class MembershipAccessManager
Makes the access manager of one kind of resource over what a storage answers. A storage package calls it where it registers the resource; an application takes the manager it made, MembershipAccessManager<TResource>.
| Member | Summary |
|---|---|
MostResources = 200 | The most resources one question about several is asked about. |
Over(IMembershipStorage<TResource, TResourceId>) | The access manager of TResource, over what storage answers. |
Over(IMembershipStorage<TResource, TResourceId>)
public static MembershipAccessManager<TResource> Over<TResource, TResourceId>(IMembershipStorage<TResource, TResourceId> storage)
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
The access manager of TResource, over what storage answers.
Parameters
storage: What the storage answers about the resource, for the current caller.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.
Exceptions
ArgumentNullException:storageis null.
MembershipAccessManager<TResource>
public sealed class MembershipAccessManager<TResource>
where TResource : class
What the current caller may do on the resources of one kind, such as MembershipAccessManager<Board>: whether it holds a permission on a board, how and until when, and which permissions it holds on several. Your code takes it from the container, as any service:
public sealed class ArchiveBoardHandler(IBoardRepository boards, MembershipAccessManager<Board> boardAccess) : ICommandHandler<ArchiveBoard>
{
public async ValueTask<Unit> Handle(ArchiveBoard command, CancellationToken cancellationToken)
{
var board = await boards.GetAsync(command.Board, cancellationToken);
var hold = await boardAccess.HoldAsync(board, BoardPermissions.Edit, cancellationToken);
// ...
}
}
Each question takes the resource, and has an overload that takes its id, for a caller that has only the id, as a request does: boardAccess.HoldAsync(command.Board, ...) (MembershipAccessManagerExtensions). An answer about a resource names it as it was asked about: the resource for the resource, the id for an id.
Which resources the caller holds a permission on is asked of the resource's own set, inside a statement of your own: db.Boards.WhereIHold(BoardPermissions.View), which the storage package writes. Reach is the lower level under it, for a statement that reads more than which resources are held.
The order of what a caller is refused with is the same for every question: the caller's own refusal when it is nobody; then not-found for a resource it may not see, exactly as for one that does not exist, so nobody learns a resource is there; then not-permitted, naming the permission, for a resource it sees without holding the permission. All under the codes of the resource (MembershipRules.Codes).
It answers what a caller holds and nothing more. Who may add a member, give a role or hand a resource on is the application's to decide, by requiring a permission of its choice on each of its commands, and by whatever further rule it writes over the answer. The storage package registers it for each kind of resource, per scope; every question is asked for the caller that is current when it is asked, so it follows every Begin made after it was handed out.
Type parameters
TResource: The resource's aggregate: the application's class that holds the members.
| Member | Summary |
|---|---|
MembershipRules Rules { get; } | The rules of the resource, with the codes it refuses under. |
RequireCurrentUser() | Refuses a caller nothing can be answered for at all, with that caller's own refusal. |
Reach(string) | The resources the caller reaches for permission, as a set a statement of your own takes: the lower level under db.Boards.WhereIHold(permission), for a statement that reads how each resource is held, or one on another context than the request's. |
ReachSeen() | The resources the caller sees, as a set a statement of your own takes: those it is a member of now, whatever roles it holds there; those it owns, from the moment the resource names it; and, where the rules let a resource be reached from above and name the permission every member holds (MembershipRules.EveryMemberPermission), those that sit where the caller holds that permission, or below. |
HoldAsync(TResource, string, CancellationToken) | What the caller holds on resource, in one statement: null when the caller does not see it, which is also the answer for a resource that no longer exists; otherwise the resource, the version it is at, and how the caller holds permission on it and until when, if it does. |
RequireAsync(TResource, string, CancellationToken) | Requires the caller to hold permission on resource: what a command about one resource passes before it changes it. |
ViaAsync(TResource, string, CancellationToken) | How the caller holds permission on a resource it may see, or null when it does not hold it: for a query that answers with what its caller may do, where not holding the permission is an answer and not a refusal. |
PermissionsOnAsync(IReadOnlyCollection<TResource>, IReadOnlyCollection<string>, CancellationToken) | The permissions of permissions the caller holds on each of resources it sees, in one statement whatever their number, keyed by the resources that were given. |
RequireCurrentUser()
public void RequireCurrentUser()
Refuses a caller nothing can be answered for at all, with that caller's own refusal. Where the caller's member id is its own id or a claim there is no such caller: one that is nobody's member simply reaches nothing, so this returns for everybody. Where the application resolves who a caller is as a member (MemberSource.Resolved), a caller the application refuses outright, somebody who is nobody in the organization the resources belong to, is refused here, before anything is read.
Exceptions
RefusalException: The caller is nobody the questions can be asked about.
Reach(string)
public MemberReach<TResource> Reach(string permission)
The resources the caller reaches for permission, as a set a statement of your own takes: the lower level under db.Boards.WhereIHold(permission), for a statement that reads how each resource is held, or one on another context than the request's. Those it is a member of now with a role that gives the permission, for a permission that being a member gives those it is a member of at all, for a permission of the resource those it owns, and, where the rules let a resource be reached from above, those that sit where the caller holds the permission, or below. Every resource for the application's own work; none for a caller that is nobody.
A reach says where one permission is held, and nothing of the others. Through its members a resource is held only where it is seen. From above it is seen by the permission every member holds (MembershipRules.EveryMemberPermission), so a caller that holds another permission there and not that one is within this reach without seeing the resource. HoldAsync and PermissionsOnAsync answer only for what the caller sees; a statement of your own that should do the same puts the reach for the permission that sees in front of this one.
Parameters
permission: A permission.
ReachSeen()
public MemberReach<TResource> ReachSeen()
The resources the caller sees, as a set a statement of your own takes: those it is a member of now, whatever roles it holds there; those it owns, from the moment the resource names it; and, where the rules let a resource be reached from above and name the permission every member holds (MembershipRules.EveryMemberPermission), those that sit where the caller holds that permission, or below. What HoldAsync and PermissionsOnAsync answer about, and what a statement of your own puts in front of a reach for another permission so it answers about the same resources. Every resource for the application's own work.
HoldAsync(TResource, string, CancellationToken)
public Task<MemberHold<TResource>?> HoldAsync(
TResource resource,
string permission,
CancellationToken cancellationToken)
What the caller holds on resource, in one statement: null when the caller does not see it, which is also the answer for a resource that no longer exists; otherwise the resource, the version it is at, and how the caller holds permission on it and until when, if it does. It refuses nobody: a module builds its own answers on it.
Parameters
resource: The resource.permission: A permission.cancellationToken: Cancels the query.
Exceptions
ArgumentNullException:resourceis null.
RequireAsync(TResource, string, CancellationToken)
public Task<MemberHold<TResource>> RequireAsync(
TResource resource,
string permission,
CancellationToken cancellationToken)
Requires the caller to hold permission on resource: what a command about one resource passes before it changes it. One statement.
Parameters
resource: The resource.permission: The permission needed on it.cancellationToken: Cancels the query.
Returns: The resource, with the version it was checked at and how the permission is held.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundwhen it may not see the resource;not-permitted, with thePermission, when it sees the resource but does not hold the permission on it.
ViaAsync(TResource, string, CancellationToken)
public Task<MemberVia?> ViaAsync(
TResource resource,
string permission,
CancellationToken cancellationToken)
How the caller holds permission on a resource it may see, or null when it does not hold it: for a query that answers with what its caller may do, where not holding the permission is an answer and not a refusal. One statement.
Parameters
resource: The resource.permission: A permission.cancellationToken: Cancels the query.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundwhen the caller may not see the resource.
PermissionsOnAsync(IReadOnlyCollection<TResource>, IReadOnlyCollection<string>, CancellationToken)
public Task<IReadOnlyDictionary<TResource, IReadOnlySet<string>>> PermissionsOnAsync(
IReadOnlyCollection<TResource> resources,
IReadOnlyCollection<string> permissions,
CancellationToken cancellationToken)
The permissions of permissions the caller holds on each of resources it sees, in one statement whatever their number, keyed by the resources that were given. A resource the caller does not see, or holds none of the permissions on, is not in the answer. What is answered is for showing: whether a command may run is asked again when it runs.
Parameters
resources: The resources asked about, at mostMembershipAccessManager.MostResourcesdifferent ones.permissions: Permissions.cancellationToken: Cancels the query.
Exceptions
ArgumentException: More thanMembershipAccessManager.MostResourcesdifferent resources are asked about.
MembershipAccessManagerExtensions
public static class MembershipAccessManagerExtensions
The questions of MembershipAccessManager<TResource> by the resource's id, for a caller that has only the id, as a request does. Each answers what the question by the resource answers, and names the resource by the id it was given:
var hold = await boardAccess.HoldAsync(command.Board, BoardPermissions.Edit, cancellationToken); // a MemberHold<BoardId>
The resource's id is inferred from the argument, and has to be the id the resource is known by.
| Member | Summary |
|---|---|
HoldAsync(MembershipAccessManager<TResource>, TResourceId, string, CancellationToken) | What the caller holds on the resource resource names, in one statement: null when the caller does not see it, which is also the answer for a resource that does not exist; otherwise its id, the version it is at, and how the caller holds permission on it and until when, if it does. |
RequireAsync(MembershipAccessManager<TResource>, TResourceId, string, CancellationToken) | Requires the caller to hold permission on the resource resource names: what a command or a query about one resource passes before its handler. |
HoldEachAsync(MembershipAccessManager<TResource>, IReadOnlyCollection<TResourceId>, string, CancellationToken) | What the caller holds on each of the resources resources names, in one statement, keyed by id. |
RequireEachAsync(MembershipAccessManager<TResource>, IEnumerable<TResourceId>, string, CancellationToken) | Requires the caller to hold permission on each of the resources resources names: what a request about several resources passes before its handler. |
ViaAsync(MembershipAccessManager<TResource>, TResourceId, string, CancellationToken) | How the caller holds permission on the resource resource names, or null when it does not hold it. |
PermissionsOnAsync(MembershipAccessManager<TResource>, IReadOnlyCollection<TResourceId>, IReadOnlyCollection<string>, CancellationToken) | The permissions of permissions the caller holds on each of the resources resources names that it sees, in one statement whatever their number, keyed by id. |
HoldAsync(MembershipAccessManager<TResource>, TResourceId, string, CancellationToken)
public static Task<MemberHold<TResourceId>?> HoldAsync<TResource, TResourceId>(
this MembershipAccessManager<TResource> access,
TResourceId resource,
string permission,
CancellationToken cancellationToken)
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
What the caller holds on the resource resource names, in one statement: null when the caller does not see it, which is also the answer for a resource that does not exist; otherwise its id, the version it is at, and how the caller holds permission on it and until when, if it does. It refuses nobody.
Parameters
access: The access manager of the resource's kind.resource: The resource's id.permission: A permission.cancellationToken: Cancels the query.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.
Exceptions
ArgumentNullException:accessis null.
RequireAsync(MembershipAccessManager<TResource>, TResourceId, string, CancellationToken)
public static Task<MemberHold<TResourceId>> RequireAsync<TResource, TResourceId>(
this MembershipAccessManager<TResource> access,
TResourceId resource,
string permission,
CancellationToken cancellationToken)
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
Requires the caller to hold permission on the resource resource names: what a command or a query about one resource passes before its handler. One statement.
Parameters
access: The access manager of the resource's kind.resource: The resource's id.permission: The permission needed on it.cancellationToken: Cancels the query.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.
Returns: The resource's id, with the version it was checked at and how the permission is held.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundwhen it may not see the resource, for a missing one too;not-permitted, with thePermission, when it sees the resource but does not hold the permission.
HoldEachAsync(MembershipAccessManager<TResource>, IReadOnlyCollection<TResourceId>, string, CancellationToken)
public static Task<IReadOnlyDictionary<TResourceId, MemberHold<TResourceId>>> HoldEachAsync<TResource, TResourceId>(
this MembershipAccessManager<TResource> access,
IReadOnlyCollection<TResourceId> resources,
string permission,
CancellationToken cancellationToken)
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
What the caller holds on each of the resources resources names, in one statement, keyed by id. A resource the caller sees has a hold, with MemberHold.Via null where the caller does not hold permission. A resource the caller does not see, or that does not exist, is not in the answer. It refuses nobody.
Parameters
access: The access manager of the resource's kind.resources: The resources' ids, at mostMembershipAccessManager.MostResourcesdifferent ones.permission: A permission.cancellationToken: Cancels the query.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.
Exceptions
ArgumentException: More thanMembershipAccessManager.MostResourcesdifferent resources are asked about.
RequireEachAsync(MembershipAccessManager<TResource>, IEnumerable<TResourceId>, string, CancellationToken)
public static Task<IReadOnlyList<MemberHold<TResourceId>>> RequireEachAsync<TResource, TResourceId>(
this MembershipAccessManager<TResource> access,
IEnumerable<TResourceId> resources,
string permission,
CancellationToken cancellationToken)
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
Requires the caller to hold permission on each of the resources resources names: what a request about several resources passes before its handler. One statement for every MembershipAccessManager.MostResources resources. A resource named twice is asked about once. Every not-found comes before any not-permitted, and neither names the resource. No resources require a caller the questions can be asked about, and nothing more.
Parameters
access: The access manager of the resource's kind.resources: The resources' ids.permission: The permission needed on each.cancellationToken: Cancels the queries.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.
Returns: A hold for each resource, in the order the resources are first named.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundwhen it may not see one of the resources, for a missing one too;not-permitted, with thePermission, when it sees all of them but does not hold the permission on one.
ViaAsync(MembershipAccessManager<TResource>, TResourceId, string, CancellationToken)
public static Task<MemberVia?> ViaAsync<TResource, TResourceId>(
this MembershipAccessManager<TResource> access,
TResourceId resource,
string permission,
CancellationToken cancellationToken)
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
How the caller holds permission on the resource resource names, or null when it does not hold it. One statement.
Parameters
access: The access manager of the resource's kind.resource: The resource's id.permission: A permission.cancellationToken: Cancels the query.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.
Exceptions
RefusalException: The caller's own refusal when it is nobody;not-foundwhen the caller may not see the resource.
PermissionsOnAsync(MembershipAccessManager<TResource>, IReadOnlyCollection<TResourceId>, IReadOnlyCollection<string>, CancellationToken)
public static Task<IReadOnlyDictionary<TResourceId, IReadOnlySet<string>>> PermissionsOnAsync<TResource, TResourceId>(
this MembershipAccessManager<TResource> access,
IReadOnlyCollection<TResourceId> resources,
IReadOnlyCollection<string> permissions,
CancellationToken cancellationToken)
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
The permissions of permissions the caller holds on each of the resources resources names that it sees, in one statement whatever their number, keyed by id. A resource the caller does not see, or holds none of the permissions on, is not in the answer, so asking about an id tells nothing about it.
Parameters
access: The access manager of the resource's kind.resources: The resources' ids, at mostMembershipAccessManager.MostResourcesdifferent ones.permissions: Permissions.cancellationToken: Cancels the query.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.
Exceptions
ArgumentException: More thanMembershipAccessManager.MostResourcesdifferent resources are asked about. A use case that takes the ids from a request refuses its caller, under a code of its own, before it asks.
MemberHold<TResource>
public sealed record MemberHold<TResource>
where TResource : notnull
What the access manager read of one resource the caller may see: how the caller holds the permission asked about and until when, and the version the resource was at. It names the resource as it was asked about: the resource itself where the question was given the resource (a MemberHold<Board>), its id where it was given an id (a MemberHold<BoardId>).
Type parameters
TResource: The resource as it was asked about: the application's aggregate, or its id.
Remarks
MemberAccessCheck<TResource, TResourceId> keeps the one it read for a request, by the resource's id, in Checked<T>, for the expert hold: a context that asks for it holds every save of the resource to Version. A handler on the default path takes nothing from there. A rule of its own that reads Via or Until asks the access manager for a hold (MembershipAccessManager.HoldAsync).
| Member | Summary |
|---|---|
MemberHold(TResource, MemberVia?, long, DateTimeOffset?) | The primary constructor: the type's summary says what it makes. |
TResource Resource { get; init; } | The resource that was read, as it was asked about: the resource, or its id. |
MemberVia? Via { get; init; } | How the caller holds the permission on it, or null when the caller sees the resource without holding the permission. So never null on a hold the permission's requirement let through (MemberAccess.On), and null on one the caller's own way through let through (MemberAccess.Sees) where the caller does not hold the permission as well. |
long Version { get; init; } | The resource's version when it was read. Under the expert hold a save of the resource at another version is a lost race, so what is changed is what was checked. |
Until | The first moment the caller no longer holds the permission on the resource, as its membership and its roles stand now, or null when the hold has no end: so for the application's own work, and for a permission the resource's owner holds by owning it, the permissions marked Owner (MembershipRules.OwnerPermissions). |
MemberHold(TResource, MemberVia?, long, DateTimeOffset?)
public MemberHold(TResource Resource, MemberVia? Via, long Version, DateTimeOffset? Until)
The primary constructor: the type's summary says what it makes.
Parameters
-
Resource: The resource that was read, as it was asked about: the resource, or its id. -
Via: How the caller holds the permission on it, ornullwhen the caller sees the resource without holding the permission. So nevernullon a hold the permission's requirement let through (MemberAccess.On), andnullon one the caller's own way through let through (MemberAccess.Sees) where the caller does not hold the permission as well. -
Version: The resource's version when it was read. Under the expert hold a save of the resource at another version is a lost race, so what is changed is what was checked. -
Until: The first moment the caller no longer holds the permission on the resource, as its membership and its roles stand now, ornullwhen the hold has no end: so for the application's own work, and for a permission the resource's owner holds by owning it, the permissions markedOwner(MembershipRules.OwnerPermissions). The owner holds no other permission by owning, and holds no role. A member holds a permission that being a member gives until its membership ends, and a permission a role gives until the last of its roles that give the permission now ends, or its membership if that ends sooner; a role that only starts later does not lengthen it.nullas well when the permission is not held (Viaisnull).A permission held from above has no end the package knows: it is held where the application says, and the application knows until when. It is held on a resource for as long as the caller sees that resource. So it is
nullwhere the caller sees the resource from above as well, by holding the permission that sees there, whatever its membership and its roles say; and where the caller sees the resource as a member alone, it is the end of that membership, since the resource is not there for it after.The package refuses nothing by it. It is there for a rule of the application's own, such as that nobody gives a role for longer than they hold the permission they give it with.
Remarks
MemberAccessCheck<TResource, TResourceId> keeps the one it read for a request, by the resource's id, in Checked<T>, for the expert hold: a context that asks for it holds every save of the resource to Version. A handler on the default path takes nothing from there. A rule of its own that reads Via or Until asks the access manager for a hold (MembershipAccessManager.HoldAsync).
Until
public DateTimeOffset? Until { get; init; }
The first moment the caller no longer holds the permission on the resource, as its membership and its roles stand now, or null when the hold has no end: so for the application's own work, and for a permission the resource's owner holds by owning it, the permissions marked Owner (MembershipRules.OwnerPermissions). The owner holds no other permission by owning, and holds no role. A member holds a permission that being a member gives until its membership ends, and a permission a role gives until the last of its roles that give the permission now ends, or its membership if that ends sooner; a role that only starts later does not lengthen it. null as well when the permission is not held (Via is null).
A permission held from above has no end the package knows: it is held where the application says, and the application knows until when. It is held on a resource for as long as the caller sees that resource. So it is null where the caller sees the resource from above as well, by holding the permission that sees there, whatever its membership and its roles say; and where the caller sees the resource as a member alone, it is the end of that membership, since the resource is not there for it after.
The package refuses nothing by it. It is there for a rule of the application's own, such as that nobody gives a role for longer than they hold the permission they give it with.
MemberVia
public enum MemberVia
How a caller holds a permission on a resource. An application that shows it has its own word for each, "staff" for its members say, and maps these at its edge.
| Member | Summary |
|---|---|
Members = 0 | Through the resource's members: the caller is a member now and holds a role there, now, that gives the permission; or the permission is one that being a member gives by itself; or the caller is the owner, who holds the permissions on the resource that its owner holds. |
Above = 1 | From above the resource: the caller holds the permission where the resource sits, or above it, and does not hold it as one of the resource's members, which comes first. Only a resource whose rules say it is reached from above (MembershipRules.ReachFromAbove) is reached this way. |
System = 2 | The application's own work, which holds every permission. |
MemberReach<TResource>
public abstract class MemberReach<TResource>
where TResource : class
Which resources of one kind the caller reaches for one permission, at one moment: the lower level under db.Boards.WhereIHold(permission), for a statement that reads more than which resources are held, such as how each is held, or one made on another context than the one that asks.
It is made by the access manager of the resource (MembershipAccessManager.Reach), for the caller the request runs as, and the storage that manager reads through is what puts it into a query (Within and Reached of the Entity Framework package). What a query is filtered by is therefore always what the rules decided, and never a set somebody put together elsewhere: a storage takes only the reaches its own manager made. What is public here is what a use case reads of it.
Type parameters
TResource: The resource's aggregate.
| Member | Summary |
|---|---|
MemberReach(string, DateTimeOffset, bool) | For the access manager of a storage. |
MemberReach(DateTimeOffset, bool) | For the access manager of a storage: the reach of what the caller sees, under rules that name no permission for seeing (MembershipRules.EveryMemberPermission). |
string? Permission { get; } | The permission the reach is for. null only for the reach of what the caller sees under rules that name no permission for seeing. |
DateTimeOffset Now { get; } | The moment a membership, and a role held in it, must apply at to count. One moment for the whole statement, and the one an answer made from it compares periods with. |
bool Everything { get; } | Whether the caller is the application's own work, which reaches every resource: a statement then leaves the filter out. |
Via(bool, bool) | How a resource read with this reach is reached, from what the statement found. |
MemberReach(string, DateTimeOffset, bool)
protected MemberReach(string permission, DateTimeOffset now, bool everything)
For the access manager of a storage.
Parameters
permission: The permission the reach is for.now: The moment a membership, and a role held in it, must apply at.everything: Whether the caller reaches every resource: the application's own work.
MemberReach(DateTimeOffset, bool)
protected MemberReach(DateTimeOffset now, bool everything)
For the access manager of a storage: the reach of what the caller sees, under rules that name no permission for seeing (MembershipRules.EveryMemberPermission). A resource is seen by its members and its owner all the same, so there is such a reach, and no permission it is for.
Parameters
now: The moment a membership must apply at.everything: Whether the caller reaches every resource: the application's own work.
Via(bool, bool)
public MemberVia? Via(bool asMember, bool fromAbove = false)
How a resource read with this reach is reached, from what the statement found. The members come first: a member that also holds the permission from above reaches the resource as a member.
Parameters
asMember: Whether the statement found the caller among the members, now, with what this reach asks of a membership.fromAbove: Whether the statement found the permission held where the resource sits, or above it.
Returns: How, or null when the resource is not within this reach.
IMembershipStorage<TResource, TResourceId>
public interface IMembershipStorage<TResource, TResourceId>
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
What a storage package answers about one kind of resource, for the current caller: the part of the access manager that reads where the resources are kept. An application never takes it: it takes the access manager of the resource, MembershipAccessManager<TResource>, which a storage package registers over this (MembershipAccessManager.Over<TResource, TResourceId>).
A caller holds a permission on a resource through its members: it is a member now, and holds a role there, now, that gives the permission. A role counts only inside its membership, and only for the permissions a member's role can give (MembershipRules.RolePermissions). Being a member gives one permission by itself, when the rules name one (MembershipRules.EveryMemberPermission). The owner of a resource holds the permissions marked Owner (MembershipRules.OwnerPermissions), by owning it, and nothing through a role, and the application's own work holds every permission: the application itself, and its work in a scope the rules name (MembershipRules.IsOwnWork). Where the rules say so (MembershipRules.ReachFromAbove), a permission held where the resource sits, or above it, is held on the resource as well, by a caller that is no member of it. A resource the caller reaches in no way does not exist for it.
The storage answers, because whether a membership and a role apply now is compared where the rows are, inside one statement. What is refused, and in which order, is not the storage's: the access manager decides that over HoldAsync, the same for every storage.
Type parameters
TResource: The resource's aggregate.TResourceId: The resource's id.
| Member | Summary |
|---|---|
MembershipRules Rules { get; } | The rules of the resource, with the codes it refuses under. |
RequireCurrentUser() | Refuses a caller nothing can be answered for at all, with that caller's own refusal. |
Reach(string) | The resources the caller reaches for permission, as a set the storage puts into a statement: what MembershipAccessManager.Reach answers. |
ReachSeen() | The resources the caller sees, as a set the storage puts into a statement: what MembershipAccessManager.ReachSeen answers. |
HoldAsync(TResourceId, string, CancellationToken) | What the caller holds on one resource, in one statement: null when the caller does not see the resource, which is also the answer for a resource that does not exist; otherwise the resource, its version, and how the caller holds permission on it and until when, if it does. |
HoldEachAsync(IReadOnlyCollection<TResourceId>, string, CancellationToken) | What the caller holds on each of resources, in one statement. |
PermissionsOnAsync(IReadOnlyCollection<TResourceId>, IReadOnlyCollection<string>, CancellationToken) | The permissions of permissions the caller holds on each of resources it sees, in one statement whatever their number. |
RequireCurrentUser()
void RequireCurrentUser()
Refuses a caller nothing can be answered for at all, with that caller's own refusal. Where the caller's member id is its own id or a claim there is no such caller: one that is nobody's member simply reaches nothing, so this returns for everybody. Where the application resolves who a caller is as a member (MemberSource.Resolved), a caller the application refuses outright is refused here, before anything is read (ICurrentMember.Require).
Exceptions
RefusalException: The caller is nobody the questions can be asked about.
Reach(string)
MemberReach<TResource> Reach(string permission)
The resources the caller reaches for permission, as a set the storage puts into a statement: what MembershipAccessManager.Reach answers.
Parameters
permission: A permission.
ReachSeen()
MemberReach<TResource> ReachSeen()
The resources the caller sees, as a set the storage puts into a statement: what MembershipAccessManager.ReachSeen answers.
HoldAsync(TResourceId, string, CancellationToken)
Task<MemberHold<TResourceId>?> HoldAsync(
TResourceId resource,
string permission,
CancellationToken cancellationToken)
What the caller holds on one resource, in one statement: null when the caller does not see the resource, which is also the answer for a resource that does not exist; otherwise the resource, its version, and how the caller holds permission on it and until when, if it does. It refuses nobody.
Parameters
resource: The resource.permission: A permission.cancellationToken: Cancels the query.
HoldEachAsync(IReadOnlyCollection<TResourceId>, string, CancellationToken)
Task<IReadOnlyDictionary<TResourceId, MemberHold<TResourceId>>> HoldEachAsync(
IReadOnlyCollection<TResourceId> resources,
string permission,
CancellationToken cancellationToken)
What the caller holds on each of resources, in one statement. The answer has a hold for each resource the caller sees, keyed by its id. The hold has the version, and how the caller holds permission there and until when. Its MemberHold.Via is null where the caller does not hold the permission. A resource the caller does not see, or that does not exist, is not in the answer. It refuses nobody.
Parameters
resources: The resources asked about, at mostMembershipAccessManager.MostResourcesdifferent ones.permission: A permission.cancellationToken: Cancels the query.
Exceptions
ArgumentException: More thanMembershipAccessManager.MostResourcesdifferent resources are asked about.
PermissionsOnAsync(IReadOnlyCollection<TResourceId>, IReadOnlyCollection<string>, CancellationToken)
Task<IReadOnlyDictionary<TResourceId, IReadOnlySet<string>>> PermissionsOnAsync(
IReadOnlyCollection<TResourceId> resources,
IReadOnlyCollection<string> permissions,
CancellationToken cancellationToken)
The permissions of permissions the caller holds on each of resources it sees, in one statement whatever their number. A resource the caller does not see, or holds none of the permissions on, is not in the answer.
Parameters
resources: The resources asked about, at mostMembershipAccessManager.MostResourcesdifferent ones.permissions: Permissions.cancellationToken: Cancels the query.
Exceptions
ArgumentException: More thanMembershipAccessManager.MostResourcesdifferent resources are asked about.
Requirements
Namespace DTS.Membership.Access.
MemberAccess
public static class MemberAccess
What a request about a resource with members requires of its caller, said where the request is declared:
public sealed record ShareDocument(DocumentId Document, UserId With, NamedRole Role, long? ExpectedVersion = null) : IDocumentsRequest
{
AccessRequirement IRequireAccess.RequiredAccess => MemberAccess.On(DocumentPermissions.Share, Document, ExpectedVersion);
}
The cases are MemberAccess<TResourceId>'s, closed over the id of the resource the request is about, which is what tells the requirements of one kind of resource from those of another: MemberAccessCheck<TResource, TResourceId> decides the cases of its own resource and no other.
Where the requirement reads no value of the request but the permission, the resource and the version it carries, the request says it with attributes instead, and the toolkit's generator writes the same call:
public sealed record ShareDocument(
[OnResource(DocumentPermissions.Share)] DocumentId Document, UserId With, NamedRole Role, [ExpectedVersion] long? ExpectedVersion = null) : IDocumentsRequest;
[SeenWith<DocumentId>(DocumentPermissions.View)]
public sealed record ListDocuments : IDocumentsRequest;
(OnResourceAttribute, ExpectedVersionAttribute, SeenWithAttribute<TResourceId>). These methods stay for a requirement computed from the request's values.
Which permission a request requires is the application's to say, for the commands that change the members as for any other: the package has no permission of its own for adding a member, giving a role or handing a resource on. A command an application wants only some callers to run requires a permission those callers hold.
| Member | Summary |
|---|---|
On(string, TResourceId, long?) | The caller holds permission on resource, through its members or however else the resource's rules let a permission be held. |
OnEach(string, IEnumerable<TResourceId>) | The caller holds permission on each of resources. |
SeenWith(string) | The caller is shown only the resources it holds permission on. |
Sees(string, TResourceId, long?) | The caller sees resource, whether or not it holds permission there: what a request about the caller's own on the resource requires of it, beside the caller's own, where the permission is the other way through. |
On(string, TResourceId, long?)
public static On On<TResourceId>(
string permission,
TResourceId resource,
long? expectedVersion = null)
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
The caller holds permission on resource, through its members or however else the resource's rules let a permission be held.
Parameters
permission: The permission the request needs on the resource.resource: The resource, from the request.expectedVersion: The version of the resource the caller last read, for a command that says so.
Type parameters
TResourceId: The resource's id.
Exceptions
ArgumentException:permissionis blank.
OnEach(string, IEnumerable<TResourceId>)
public static OnEach OnEach<TResourceId>(string permission, IEnumerable<TResourceId> resources)
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
The caller holds permission on each of resources. The check reads them in one statement for every 200 resources, and refuses the request when one resource is refused. [OnResource] on a member that is a collection of ids says it:
public sealed record ArchiveBoards([OnResource(BoardPermissions.Edit)] IReadOnlyCollection<BoardId> Boards) : IBoardsRequest;
is MemberAccess.OnEach(BoardPermissions.Edit, request.Boards).
Parameters
permission: The permission the request needs on each resource.resources: The resources, from the request. A resource named twice is asked about once.
Type parameters
TResourceId: The resource's id.
Exceptions
ArgumentException:permissionis blank.ArgumentNullException:resourcesis null.
SeenWith(string)
public static SeenWith SeenWith<TResourceId>(string permission)
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
The caller is shown only the resources it holds permission on. For queries only.
Parameters
permission: The permission that decides what the query shows.
Type parameters
TResourceId: The id of the kind of resource the query is about.
Exceptions
ArgumentException:permissionis blank.
Sees(string, TResourceId, long?)
public static Sees Sees<TResourceId>(
string permission,
TResourceId resource,
long? expectedVersion = null)
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
The caller sees resource, whether or not it holds permission there: what a request about the caller's own on the resource requires of it, beside the caller's own, where the permission is the other way through. [OrOwn] beside [OnResource] says it:
public sealed record LeaveDocument( // take yourself off, or anybody with the permission
[OnResource(DocumentPermissions.Share)] DocumentId Document, [OrOwn] UserId Member) : IDocumentsRequest;
is AccessRequirement.Any(AccessRequirement.All(AccessRequirement.Own(request.Member), MemberAccess.Sees(DocumentPermissions.Share, request.Document)), MemberAccess.On(DocumentPermissions.Share, request.Document)).
Parameters
permission: The permission the other way through requires on the resource: asked about, and not required.resource: The resource, from the request.expectedVersion: The version of the resource the caller last read, for a command that says so.
Type parameters
TResourceId: The resource's id.
Exceptions
ArgumentException:permissionis blank.
MemberAccess<TResourceId>
public abstract record MemberAccess<TResourceId> : AccessRequirement
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
The requirements about one kind of resource with members. A closed set: the constructor is private, so the cases below are all there are, and each is a record, so two requirements that say the same are equal and a test can hold every request to the one it is meant to declare.
What a requirement cannot say stays in the handler, in plain sight: whether the one to be made a member is active, which roles go to a member, a second permission a command's own rule asks for, a rule of the application's own about what its caller may give. A rule that needs how or until when the caller holds the permission asks for it (MembershipAccessManager.HoldAsync).
Type parameters
TResourceId: The resource's id.
| Member | Summary |
|---|---|
string Permission { get; } | The permission the requirement is about. |
MemberAccess.On
public sealed record On : MemberAccess<TResourceId>
The permission is held by the caller on Resource. A resource the caller may not see is not-found, exactly as one that does not exist; one it may see without holding the permission is not-permitted, naming the permission. Both under the resource's own codes.
| Member | Summary |
|---|---|
TResourceId Resource { get; } | The resource, from the request. |
long? ExpectedVersion { get; } | The version of the resource the caller last read, for a command that says so, or null. It is compared only once the caller is known to see the resource and to hold the permission, and any other version is a lost race (ConcurrencyConflictException). In that order, so a caller without access learns nothing from a version: whatever it sends, it is answered as if it had sent none. |
MemberAccess.SeenWith
public sealed record SeenWith : MemberAccess<TResourceId>
The caller is shown only what it holds the permission on. Nothing is refused for want of the permission: the query's own statement leaves out what the caller does not reach (db.Boards.WhereIHold(permission)), so a signed-in caller who reaches nothing gets an empty answer. A caller who did not sign in is refused, not-permitted naming the permission: it reaches nothing under any rules, and no statement is sent for it. For queries only; a command that declared it would change things unchecked.
MemberAccess.Sees
public sealed record Sees : MemberAccess<TResourceId>
The caller sees Resource, whether or not it holds MemberAccess.Permission there: what a request about the caller's own on a resource requires of the resource, beside AccessRequirement.Own<TValue>, where the permission on it is the alternative. A member who takes itself off a document sees the document, as every member does; it need not hold the permission that takes anybody off. A resource the caller may not see is not-found, exactly as one that does not exist, under the resource's own codes, and nothing else is refused.
Remarks
It reads what MemberAccess.On reads, in one statement, and keeps it the same way, a MemberHold<TResource> with the request in hand, so the caller's own way through holds a save of the resource to the version read as the permission's way does, under the expert hold too, and compares the version the request names. The permission is asked about so that what is kept says whether the caller holds it as well: MemberHold.Via is null where it does not. [OnResource(permission)] on the resource and [OrOwn] on the member become Any(All(Own(member), Sees(permission, resource)), On(permission, resource)).
| Member | Summary |
|---|---|
TResourceId Resource { get; } | The resource, from the request. |
long? ExpectedVersion { get; } | The version of the resource the caller last read, for a command that says so, or null. It is compared once the caller is known to see the resource, and any other version is a lost race (ConcurrencyConflictException), as for MemberAccess.On.ExpectedVersion. |
OnResourceAttribute
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, Inherited = false)]
public sealed class OnResourceAttribute : Attribute
Marks the resource on which the caller of a request holds a permission. The caller holds it through the resource's members, or in another way that the resource's rules allow. The generator turns the marker into a call of MemberAccess.On<TResourceId> on the marked member.
public sealed record ShareDocument(
[OnResource(DocumentPermissions.Share)] DocumentId Document, UserId With, [ExpectedVersion] long? ExpectedVersion = null) : IDocumentsRequest;
is MemberAccess.On(DocumentPermissions.Share, request.Document, request.ExpectedVersion).
Remarks
It goes on a positional parameter of a record, or on a property. The generator also reads [property: OnResource(permission)]. The member is the id of a resource with members: an aggregate root that a class declared with [Member<TId, TMemberId, TRoleId, TResource>] names as its resource. On another id it is DDD00081, in a project that sees such a class. The permission is declared on that resource with [On<TResource>]. A permission declared only on another resource is DDD00093. ExpectedVersionAttribute beside it marks the version the caller read, for a command that names one.
A request can have several of these markers, each on its own resource. All of them are required. Attaching an asset to a board needs a permission on each:
public sealed record AttachAsset(
[OnResource(BoardPermissions.Edit)] BoardId Board,
[OnResource(AssetPermissions.Use)] AssetId Asset,
[ExpectedVersion<Board>] long? Version = null) : ICommand<Board>, IBoardsRequest;
becomes AccessRequirement.All(MemberAccess.On(BoardPermissions.Edit, request.Board, request.Version), MemberAccess.On(AssetPermissions.Use, request.Asset)). Each MemberAccess.On call checks its own resource, keeps a hold on that resource, and refuses with that resource's refusal codes.
The member can be a collection of ids of one resource, any type that implements IEnumerable<TId>, such as an array, a list or a set. The caller then holds the permission on each resource:
public sealed record ArchiveBoards([OnResource(BoardPermissions.Edit)] IReadOnlyCollection<BoardId> Boards) : ICommand, IBoardsRequest;
becomes MemberAccess.OnEach(BoardPermissions.Edit, request.Boards). The check reads the resources in one statement for every 200 ids. One resource that is refused refuses the request, with not-found before not-permitted, and the refusal names no resource. An empty collection lets a caller through that the questions can be asked about. ExpectedVersionAttribute and OrOwnAttribute do not pair with a marker on a collection (DDD00109).
The resource is what the request acts on, so OrOwnAttribute can pair with the marker. The caller is let through when the request is about the caller's own on that resource, or when the caller holds the permission on the resource.
public sealed record LeaveCrew( // take yourself off, or manage the crew
[OnResource(ProjectPermissionNames.ManageCrew)] ProjectId Project, [OrOwn] SeatId Seat) : IProjectsRequest;
becomes AccessRequirement.Any(AccessRequirement.All(AccessRequirement.Own(request.Seat), MemberAccess.Sees(ProjectPermissionNames.ManageCrew, request.Project)), MemberAccess.On(ProjectPermissionNames.ManageCrew, request.Project)). On both ways through, the project's check reads the project. A caller on the own way through only needs to see the project. The check compares the version that ExpectedVersionAttribute marks, and keeps the hold that the expert hold checks the save against. The handler still looks up the seat on that resource, and refuses a seat that is not there. So the caller's own way through covers the caller's own place on that resource only.
Where a permission on the resource manages the members (MembershipRules.ManagesMembersPermission), a database that checks every row lets a member row change only for a caller that holds that permission, so a caller who takes itself off without it is refused there. The handler then saves the caller's own place as the application's own work, begun for that caller once its request is the one in hand (RequestInHand): the check decided it.
| Member | Summary |
|---|---|
OnResourceAttribute(string) | The primary constructor: the type's summary says what it makes. |
string Permission { get; } | The permission the caller holds on the resource. |
OnResourceAttribute(string)
public OnResourceAttribute(string permission)
The primary constructor: the type's summary says what it makes.
Parameters
permission: The permission the caller holds on the resource: a constant declared[On<TResource>].
Remarks
It goes on a positional parameter of a record, or on a property. The generator also reads [property: OnResource(permission)]. The member is the id of a resource with members: an aggregate root that a class declared with [Member<TId, TMemberId, TRoleId, TResource>] names as its resource. On another id it is DDD00081, in a project that sees such a class. The permission is declared on that resource with [On<TResource>]. A permission declared only on another resource is DDD00093. ExpectedVersionAttribute beside it marks the version the caller read, for a command that names one.
A request can have several of these markers, each on its own resource. All of them are required. Attaching an asset to a board needs a permission on each:
public sealed record AttachAsset(
[OnResource(BoardPermissions.Edit)] BoardId Board,
[OnResource(AssetPermissions.Use)] AssetId Asset,
[ExpectedVersion<Board>] long? Version = null) : ICommand<Board>, IBoardsRequest;
becomes AccessRequirement.All(MemberAccess.On(BoardPermissions.Edit, request.Board, request.Version), MemberAccess.On(AssetPermissions.Use, request.Asset)). Each MemberAccess.On call checks its own resource, keeps a hold on that resource, and refuses with that resource's refusal codes.
The member can be a collection of ids of one resource, any type that implements IEnumerable<TId>, such as an array, a list or a set. The caller then holds the permission on each resource:
public sealed record ArchiveBoards([OnResource(BoardPermissions.Edit)] IReadOnlyCollection<BoardId> Boards) : ICommand, IBoardsRequest;
becomes MemberAccess.OnEach(BoardPermissions.Edit, request.Boards). The check reads the resources in one statement for every 200 ids. One resource that is refused refuses the request, with not-found before not-permitted, and the refusal names no resource. An empty collection lets a caller through that the questions can be asked about. ExpectedVersionAttribute and OrOwnAttribute do not pair with a marker on a collection (DDD00109).
The resource is what the request acts on, so OrOwnAttribute can pair with the marker. The caller is let through when the request is about the caller's own on that resource, or when the caller holds the permission on the resource.
public sealed record LeaveCrew( // take yourself off, or manage the crew
[OnResource(ProjectPermissionNames.ManageCrew)] ProjectId Project, [OrOwn] SeatId Seat) : IProjectsRequest;
becomes AccessRequirement.Any(AccessRequirement.All(AccessRequirement.Own(request.Seat), MemberAccess.Sees(ProjectPermissionNames.ManageCrew, request.Project)), MemberAccess.On(ProjectPermissionNames.ManageCrew, request.Project)). On both ways through, the project's check reads the project. A caller on the own way through only needs to see the project. The check compares the version that ExpectedVersionAttribute marks, and keeps the hold that the expert hold checks the save against. The handler still looks up the seat on that resource, and refuses a seat that is not there. So the caller's own way through covers the caller's own place on that resource only.
Where a permission on the resource manages the members (MembershipRules.ManagesMembersPermission), a database that checks every row lets a member row change only for a caller that holds that permission, so a caller who takes itself off without it is refused there. The handler then saves the caller's own place as the application's own work, begun for that caller once its request is the one in hand (RequestInHand): the check decided it.
ExpectedVersionAttribute
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, Inherited = false)]
public sealed class ExpectedVersionAttribute : Attribute
Marks the version of the resource that the caller last read, beside the request's only OnResourceAttribute. The member is the third argument of MemberAccess.On<TResourceId>.
public sealed record ShareDocument(
[OnResource(DocumentPermissions.Share)] DocumentId Document, UserId With, [ExpectedVersion] long? ExpectedVersion = null) : IDocumentsRequest;
is MemberAccess.On(DocumentPermissions.Share, request.Document, request.ExpectedVersion), so the check compares it only once the caller may act, and a caller that may not is refused rather than told the resource moved on.
Remarks
It goes with one OnResourceAttribute. On a request without a resource marker it is DDD00080. A request with markers on two or more kinds of resource uses ExpectedVersionAttribute<TResource>, which names the resource. A request with two markers on one kind of resource implements RequiredAccess. Otherwise each of these is DDD00080. The member is a long?, or a long, because the call's parameter takes either. Another type is DDD00081.
ExpectedVersionAttribute<TResource>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, Inherited = false)]
public sealed class ExpectedVersionAttribute<TResource> : Attribute
where TResource : class
The form of ExpectedVersionAttribute for a request with markers on several resources. Its type argument names the resource whose version the caller last read.
public sealed record AttachAsset(
[OnResource(BoardPermissions.Edit)] BoardId Board,
[OnResource(AssetPermissions.Use)] AssetId Asset,
[ExpectedVersion<Board>] long? Version = null) : ICommand<Board>, IBoardsRequest;
becomes AccessRequirement.All(MemberAccess.On(BoardPermissions.Edit, request.Board, request.Version), MemberAccess.On(AssetPermissions.Use, request.Asset)). The board's check compares the version. The asset's check only reads the asset.
Type parameters
TResource: The resource whose version the member holds.
Remarks
TResource is the resource class, the aggregate root that a member class names, such as Board. The version belongs to the OnResourceAttribute on that resource's id. The generic form is required beside markers on two or more kinds of resource, and allowed beside one. These are DDD00080: a resource that no OnResourceAttribute of the request is on, and two markers on that resource.
SeenWithAttribute<TResourceId>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
public sealed class SeenWithAttribute<TResourceId> : Attribute
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
Says on a query that it shows the caller only the resources it holds permission on: what MemberAccess.SeenWith<TResourceId> says, written where the query is declared.
[SeenWith<DocumentId>(DocumentPermissions.View)]
public sealed record ListDocuments : IQuery<IReadOnlyList<DocumentListing>>, IDocumentsRequest;
is MemberAccess.SeenWith<DocumentId>(DocumentPermissions.View).
Type parameters
TResourceId: The id of the kind of resource the query is about.
Remarks
For queries only. It refuses nobody for want of the permission: it lets a signed-in caller through, one that holds nothing too, and says what the query's own statement filters by, which is the check. On a message of the toolkit's mediator that is no query, a command or a plain request, it would let the handler run unchecked, so there it is DDD00082.
| Member | Summary |
|---|---|
SeenWithAttribute(string) | The primary constructor: the type's summary says what it makes. |
string Permission { get; } | The permission that decides what the query shows. |
SeenWithAttribute(string)
public SeenWithAttribute(string permission)
The primary constructor: the type's summary says what it makes.
Parameters
permission: The permission that decides what the query shows: a constant, one of the resource's rules.
Remarks
For queries only. It refuses nobody for want of the permission: it lets a signed-in caller through, one that holds nothing too, and says what the query's own statement filters by, which is the check. On a message of the toolkit's mediator that is no query, a command or a plain request, it would let the handler run unchecked, so there it is DDD00082.
MemberAccessCheck<TResource, TResourceId>
public sealed class MemberAccessCheck<TResource, TResourceId> : IAccessCheck
where TResource : class, IEntity<TResourceId>
where TResourceId : struct, IEntityId, IEquatable<TResourceId>
Decides the requirements about one kind of resource with members (MemberAccess<TResourceId>) before a handler runs. A module registers it for its request interface, once for each kind of resource its requests are about:
services.AddAccessCheck<IDocumentsRequest, MemberAccessCheck<Document, DocumentId>>();
The order of what a caller is refused with is the same for every request: the caller's own refusal when it is nobody; then not-found for a resource it may not see; then not-permitted for a permission it does not hold; then, for a command that names the version its caller read, a lost race when the resource is at another one. Only then does the handler run, and whatever else it refuses comes after these.
A query that shows what a permission is held on (MemberAccess.SeenWith) refuses nobody for want of the permission: its own statement leaves out what the caller does not reach. Only a caller who did not sign in is refused there, not-permitted naming the permission, after the caller's own refusal: under no rules is such a caller anybody's member or holds a permission from above, so it reaches nothing, and its query is not run at all. A database that checks every row may refuse such a caller outright, rather than answer nothing, so its statement is never sent.
The handler needs nothing of it. It loads the resource its request names, which is the one that was checked, with the version the caller named, context.ExpectVersion(document, command.ExpectedVersion), so a resource changed since the caller read it is a lost race at the load as well as here; the save compares the version it loaded, the aggregate keeps its own rules, and a database that checks every row checks the write again. A rule that needs how or until when the caller holds the permission asks the access manager for it (MembershipAccessManager.HoldAsync).
What it read it keeps all the same, as a MemberHold<TResource> in Checked<T>, with the request in hand: that costs nothing, and it is what the expert hold holds a save to, where a context asks for it (UseMemberHolds of the Entity Framework package). Then a save that changes the resource is refused when it is at another version than the check read, and one that changes a resource no check of the request in hand read is refused outright. A request whose requirement names several resources, two documents of AccessRequirement.All say, has a hold kept for each, and each save is held to its own.
A request about several resources of one kind (MemberAccess.OnEach, which [OnResource] on a collection of ids writes) is decided in one statement for every 200 resources. One resource that is refused refuses the request. Every not-found comes before any not-permitted. A hold is kept for each resource.
The caller's own way through a request about a resource (MemberAccess.Sees, which [OrOwn] beside [OnResource] writes) is decided the same way, without the permission: not-found for a resource the caller may not see, the version compared, and the hold kept.
It decides the cases of its own resource only. A module with two kinds of resource registers two, and a requirement about a kind nobody registered a check for stops the request rather than letting it through.
Type parameters
TResource: The resource's aggregate, which a lost race names.TResourceId: The resource's id.
| Member | Summary |
|---|---|
MemberAccessCheck(MembershipAccessManager<TResource>, Checked<MemberHold<TResourceId>>, ICurrentUser) | The primary constructor: the type's summary says what it makes. |
Decides(AccessRequirement) | Whether requirement is one of this check's cases. |
RequireAsync(AccessRequirement, IRequireAccess, CancellationToken) | Holds the caller to requirement: returns when the caller meets it, and throws when it does not. |
MemberAccessCheck(MembershipAccessManager<TResource>, Checked<MemberHold<TResourceId>>, ICurrentUser)
public MemberAccessCheck(
MembershipAccessManager<TResource> access,
Checked<MemberHold<TResourceId>> kept,
ICurrentUser user)
The primary constructor: the type's summary says what it makes.
Parameters
access: The access manager of this kind of resource.kept: Where what was read of a request's resource is kept, for the expert hold.user: Who is calling: what tells a caller who did not sign in.
Decides(AccessRequirement)
public bool Decides(AccessRequirement requirement)
Whether requirement is one of this check's cases. It reads nothing and refuses nobody: it is asked for every requirement of the requests the check is registered for, and by a test that holds every requirement a module declares to having a check.
Parameters
requirement: What a request declared.
RequireAsync(AccessRequirement, IRequireAccess, CancellationToken)
public ValueTask RequireAsync(
AccessRequirement requirement,
IRequireAccess request,
CancellationToken cancellationToken)
Holds the caller to requirement: returns when the caller meets it, and throws when it does not. Called only for a requirement IAccessCheck.Decides said yes to.
Parameters
requirement: What the request declared, read from it once.request: The very request being handled, which is what anything kept for its handler is kept under.cancellationToken: Stops the reading.
Exceptions
RefusalException: The caller is not who the request requires.ConcurrencyConflictException: The resource is not at the version the request expects.InvalidOperationException: The requirement is not one of this check's cases.
MemberAccess.OnEach
public sealed record OnEach : MemberAccess<TResourceId>
The permission is held by the caller on each of Resources. The check reads all of them in one statement for every 200 resources. A resource the caller may not see is not-found, exactly as one that does not exist. A resource it may see without holding the permission is not-permitted, naming the permission. The request is refused when one resource is refused, and the refusal names no resource. Every not-found comes before any not-permitted. An empty set of resources requires a caller the questions can be asked about, and nothing more.
| Member | Summary |
|---|---|
IReadOnlyList<TResourceId> Resources { get; } | The resources, from the request. Each resource is in the list once, at the place the request first names it. |
Asking about a resource by its id
From DTS.DDD.Abstractions: the questions a row access rule of any module asks about a resource with members, by the resource's id, and the sets its access answers.
ResourceQuestions
public static class ResourceQuestions
Namespace DTS.DDD.Abstractions.Access.
The questions a row access rule asks about a resource, by the resource's id. A module publishes the id in its contracts, and that is all a rule of any module needs:
public static bool Allows(Checklist checklist, IRowUser user) => user.Sees<BoardId>().Contains(checklist.BoardId);
public static bool Allows(Board board, IRowUser user) => user.Holds<BoardId>(BoardPermissions.Edit).Contains(board.Id);
public static bool Allows(Seat seat, IRowUser user) => user.FellowMembers<BoardId, SeatId>().Contains(seat.Id);
Remarks
Each question names no function. The generator writes it as the resource's id and the set, {fn:@Boards.Contracts.BoardId/seen}, and the export resolves that to the function a contribution says answers the set for that resource, under whatever name the package that keeps the resource's access gives it. A rule that asks a set no contribution answers is refused when its access file is written, naming the rule.
Each question answers only for the caller of the connection, so anyone who sees the id may ask it. Only the database answers. Called in C#, each throws DatabaseOnlyException. They are extension methods on IRowUser, so they cannot be called where there is no rule.
| Member | Summary |
|---|---|
Sees(IRowUser) | The ids of the resources the caller sees. |
Holds(IRowUser, string) | The ids of the resources on which the caller holds permission. |
FellowMembers(IRowUser) | The ids of the members of every resource the caller is a member of now, the caller among them: the people on the projects a seat works on, which a read rule on the application's seat class asks to let a seat read who it works with. |
Sees(IRowUser)
public static AccessSet<TKey> Sees<TKey>(this IRowUser user)
where TKey : struct, IEntityId
The ids of the resources the caller sees. A resource with members is seen by its members, by its owner, and, where its rules let a permission held above reach it, by whoever holds the permission that sees it there.
Parameters
user: The rule's user.
Type parameters
TKey: The id of the resource's aggregate.
Returns: Nothing in C#. A rule asks the answer's Contains, in SQL.
Exceptions
DatabaseOnlyException: Always, since the question is the database's.
Holds(IRowUser, string)
public static AccessSet<TKey> Holds<TKey>(this IRowUser user, string permission)
where TKey : struct, IEntityId
The ids of the resources on which the caller holds permission. On a resource with members a permission is held through a role, by being a member for the permission that being one gives, by owning the resource, or from above.
Parameters
user: The rule's user.permission: A permission.
Type parameters
TKey: The id of the resource's aggregate.
Returns: Nothing in C#. A rule asks the answer's Contains, in SQL.
Exceptions
DatabaseOnlyException: Always, since the question is the database's.
FellowMembers(IRowUser)
public static AccessSet<TMemberId> FellowMembers<TKey, TMemberId>(this IRowUser user)
where TKey : struct, IEntityId
where TMemberId : struct, IEntityId
The ids of the members of every resource the caller is a member of now, the caller among them: the people on the projects a seat works on, which a read rule on the application's seat class asks to let a seat read who it works with.
Parameters
user: The rule's user.
Type parameters
TKey: The id of the resource's aggregate.TMemberId: The id the members are known by, such asSeatId, which the resource's id does not say.
Returns: Nothing in C#. A rule asks the answer's Contains, in SQL.
Exceptions
DatabaseOnlyException: Always, since the question is the database's.
ResourceAccessSet
public enum ResourceAccessSet
Namespace DTS.DDD.Abstractions.Attributes.
One of the questions the access to a resource answers for the caller of the connection, with the ids of the resources it answers yes for, or, for FellowMembers, with the ids of the members it finds. A row access rule asks each through ResourceQuestions, by the resource's id.
Remarks
The package that keeps the resource's access writes the function that answers each, under whatever name it gives it, and says which set the function answers. The Membership package answers all three for a resource with members.
| Member | Summary |
|---|---|
Seen = 0 | The resources the caller sees, asked without arguments: user.Sees<TKey>(). A resource with members is seen by its members, by its owner, and, where its rules let a permission held above reach it, by whoever holds the permission that sees it there. |
HeldOn = 1 | The resources the caller holds a permission on, asked with the permission: user.Holds<TKey>(permission). On a resource with members a permission is held through a role, by being a member for the permission that being one gives, by owning the resource, or from above. |
FellowMembers = 2 | The members of every resource the caller is a member of now, the caller among them, asked without arguments: user.FellowMembers<TKey, TMemberId>(). Its ids are the members', not the resources': the people on the projects a seat works on, say, which a read rule on the application's seat class asks to let a seat read who it works with. |