Difference between revisions of "Services"
| Line 144: | Line 144: | ||
.. | .. | ||
/// <summary></summary> | |||
public void DoThingWithService<T, ID>(AutoService<T, ID> service) | |||
where T : Content<ID>, new() | where T : Content<ID>, new() | ||
where ID : struct, IConvertible, IEquatable<ID>, IComparable<ID> | where ID : struct, IConvertible, IEquatable<ID>, IComparable<ID> | ||
Latest revision as of 12:10, 14 February 2023
Services are the way how parts of a SocialStack API communicate with each other. They are essentially the code to code interfaces between different content types or bits of functionality inside your API. They are instanced once at startup and can can use dependency injection to access other services:
public ThingService(UserService userService) {
Events.Thing.BeforeCreate.AddEventListener(async (Context context, Thing thing) => {
if(thing == null){
return null;
}
// Get the creator user (this is likely the same as context.User).
// Throws if context user is not permitted to access this data.
var aUser = await userService.Get(context, thing.UserId);
// Do something with aUser here!
return thing;
});
}
Generating a new service
Services are just .cs files with no other fiddling required. You can of course copy/paste existing service files but alternatively to make creating them very simple, socialstack's generate command will output them too:
socialstack generate Api/Things
The above will create a directory at Api/Thing and inside that will be a new service, Api/Things/ThingService.cs
Alternatively if you don't want an entity (Thing.cs in the case above), you can make a basic service instead. The only difference is you give it a singular name:
socialstack generate Api/FrontendEventConfig
This will first confirm that it is a basic service that you want (they're relatively uncommon) and then create Api/FrontendEventConfig/Api/FrontendEventConfigService.cs for you.
Getting a service
There's two ways to get a service - either by C# dependency injection:
public ThingService(UserService userService) {
Or when dependency injection isn't available due to either a cyclical dependency or simply being outside the scope of another service, you can use Services.Get. Note that this call will return null if the services have not been instanced yet; you can guarantee success if you wait until at least the Events.Service.AfterStart event triggers.
var userService = Services.Get<UserService>(); // In Startup namespace
Common Features
Services contain a variety of common features for doing common tasks such as getting an object, creating one etc. Here's the main usage examples of each:
- Get - Get an entity from the service by its ID which is usually a uint.
uint entityId = 14;
var entity = await aService.Get(context, entityId);
entityId could be e.g. a user ID, aService is the UserService and entity is therefore a User object.
- Delete - Deletes an entity by its ID which is usually a uint.
uint entityId = 14;
await aService.Delete(context, entityId);
// Or:
await aService.Delete(context, entity);
entityId could be e.g. a user ID, aService is the UserService and entity is therefore a User object.
- Where - Used to list multiple entities from the service with a filter string. Note that it is possible to entirely avoid all allocations with this part of the API - ListAll is just one of the available methods.
string token = "";
var listOfResults = await aService.Where("Token=?").Bind(tokenString).ListAll(context);
- Create - Creates an entity in the target service.
var thingEntity = await aService.Create(context, new Thing(){
Token = "123123"
});
- Update - Updates an entity in the target service. Update uses a callback mechanism as this helps avoid allocations and allows internal field comparisons to happen to be able to easily identify which fields have changed.
var thingEntity = await aService.Update(context, thingEntity, (Context context, Thing toUpdate, Thing original) => {
toUpdate.Token = "something-else";
});
Service Permissions
The permissions system is also modular in that it adds an event handler on the various core events (like create, delete and so on) for a particular content type. This means that when you access a service from some other part of the code, your current context must be permitted to access it. It also means that the event handler can be removed should you wish to do so, although naturally, removing the permission system is likely not a good idea on any project!
Ignoring the permissions
You can, however, ask a particular request to ignore the permission system all together. This effectively just makes the above event handler skip itself. This happens in situations where the incoming user is not permitted to generally access a certain piece of data, but the API internally needs to check something. Password resets are one example - the user (and their context) is anonymous, and anonymous users certainly shouldn't be able to publicly view any password resets. They can however potentially access one of them if a token value is correct, so that token lookup can be done by ignoring the permission system. That is done like so:
// Won't throw with a permissions failure
var aUser = await userService.Get(context, thing.UserId, DataOptions.IgnorePermissions);
DataOptions is generally available on many service methods - Create, Get, Where and so on - so you can skip over the permission check whenever necessary.
Generic Services
Most services use C# generics, specifically AutoService<T, ID>, which indicates the content type and the type the ID uses. If you end up with an object reference to a service and would like to do something that requires the generic AutoService functionality, here is a common way to do that. This is the basis of how the permission system automatically mounts every content type:
// AutoService service
var doThingMethod = GetType().GetMethod(nameof(DoThingWithService));
var doThingGenericMethod = doThingMethod.MakeGenericMethod(new Type[] {
service.ServicedType,
service.IdType
});
doThingGenericMethod.Invoke(this, new object[] {
service
});
..
/// <summary></summary>
public void DoThingWithService<T, ID>(AutoService<T, ID> service)
where T : Content<ID>, new()
where ID : struct, IConvertible, IEquatable<ID>, IComparable<ID>
{
// service is now a more specialised instance of AutoService, without specifically knowing
// what content type it is or what type its ID is. The ID will often be a uint, but can also be a ulong.
}