Skip to content

Useful classes

Jay Malhotra edited this page Nov 14, 2023 · 1 revision

This page details the various custom pieces of infrastructure that are employed to make development easier.

DragaliaControllerBase

All controllers should inherit from this. It provides two things:

  1. The ability to look up the account ID (used as the primary key in all DB tables) after the request has passed through SessionAuthenticationHandler which looks up the request's SID header in Redis.
  2. An overridden Ok() method that returns responses structured as the game expects.

DragaliaResponse

DragaliaResponse is a custom type that wraps the actual content of the response in the characteristic manner that the client expects. The following example is from /tool/get_service_status.

{
    "data_headers": {
        "result_code": 1
    },
    "data": {
        "service_status": 1
    }
}

Every endpoint has a data_headers key containing a result_code, which is 1 for success. The actual interesting content of the endpoint is usually in data. However, to return the above response, only the following code is required:

[HttpPost("get_service_status")]
public ActionResult<DragaliaResult> GetServiceStatus()
{
    return this.Ok(new ToolGetServiceStatusData() { service_status = 1 });
}

This is because the Ok() method is overridden in DragaliaControllerBase:

public override OkObjectResult Ok(object? value)
{
    return base.Ok(
        new DragaliaResponse<object>(
            value ?? throw new ArgumentNullException(nameof(value)),
            ResultCode.SUCCESS
        )
    );
}

and DragaliaResponse is a class that has the required structure:

[MessagePackObject(keyAsPropertyName: true)]
public class DragaliaResponse<TData> where TData : class
{
    public DataHeaders data_headers { get; init; }

    public TData data { get; init; }

    public DragaliaResponse(TData data, ResultCode result_code = ResultCode.SUCCESS)
    {
        this.data = data;
        this.data_headers = new(result_code);
    }

    [JsonConstructor]
    [SerializationConstructor]
    public DragaliaResponse(DataHeaders data_headers, TData data)
    {
        this.data_headers = data_headers;
        this.data = data;
    }
}

DragaliaException

If an exception is thrown during code execution, the usual behaviour is to return a 500 status code. However, this will cause the client to show "Failed to connect to server. Try again?". This is not desirable behaviour as the exact same message is shown for 404 errors, which are quite common while the project is in development. We want to be able to show an actual error message when an exception occurs so that users can report these.

Errors are shown to the client via passing a result_code in data_headers that is not 1. So we have a middleware called ExceptionHandlerMiddleware which will return a result_code that will make the client show "Server error. Returning to title screen" for most exceptions. However, you can pass a custom result_code by throwing a DragaliaException with a value of your choice -- there is an enum for all possible values accepted by the client. This will allow a more specific error message to be shown and enables easier investigation of bug reports.

UpdateDataService

The client keeps track of the user's savefile by loading it from /load/index, and then updates it if an endpoint returns changed data in an update_data_list. UpdateDataService is a tool that automates the creation of these objects by examining the EntityFramework change tracker -- before you call .SaveChanges(), every change you have made to the database is stored in the tracker. This maps nicely onto the purpose of the update_data_list, and allows us to send them without having to reconstruct every action taken to the database.

PlayerDetailsService

This is a service that can be injected anywhere to provide access to the account ID and viewer ID via use of IHttpContextAccessor, which is the same mechanism as in DragaliaControllerBase. This works because scoped services (like this one) have a lifetime equal to that of the request, and it is initialized using the claims generated after authenticating the request. Use this in new repositories and services to avoid repeatedly passing these variables down and simplify your method signatures.

Clone this wiki locally