XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.ServerInteractive

[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig

公开
关注 0 Fork 0 Star 0
README.md

XFEExtension.NetCore.ServerInteractive

NuGet NuGet Downloads License: MIT .NET

📖 English | 简体中文

Description

ServerInteractive is a C# DLL library built on the CyberComm network architecture provided by XFEExtension.NetCore. It enables developers to rapidly build server-side and client-side network frameworks. It supports route-based request dispatch, incremental code generation (Source Generator), user login system, data table management, IP banning, and other out-of-the-box standard features.


Table of Contents


Quick Start

Before using, add the XFEExtension.NetCore.ServerInteractive NuGet package to your project.

Simplest server example (an echo service bound to local port 3300):

Service definition (EchoService.cs)

using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

// The class must be partial so the Source Generator can automatically generate the route dictionary
public partial class EchoService : ServerCoreStandardServiceBase
{
    [EntryPoint("echo")]         // Route path: /echo
    public async Task Echo()
    {
        var message = Json?["message"]?.GetValue<string>() ?? string.Empty;
        await Close(message);    // Reply to the client and end the request
    }
}

Program entry point (Program.cs)

using XFEExtension.NetCore.ServerInteractive.Utilities.Extensions;
using XFEExtension.NetCore.ServerInteractive.Utilities.Server;

var server = XFEServerBuilder.CreateBuilder()
    .UseXFEServer()                               // Register logger, exception handler, and core processor
    .AddServerCore(
        XFEServerCoreBuilder.CreateBuilder()
            .AddService<EchoService>()            // Register EchoService (routes are automatically obtained from [EntryPoint])
            .Build(options =>
            {
                options.BindIP("http://localhost:3300/");
            }))
    .Build();

using var shutdown = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; shutdown.Cancel(); };
await server.RunAsync(shutdown.Token);

Cross-platform console EXE

XFEExtension.NetCore.ServerInteractive.TServer is the production console-host template. It does not register a Windows Service or depend on ASP.NET Core. HTTP/1.1, HTTPS, WebSocket, and WSS are provided exclusively by CyberComm, with graceful shutdown on Windows Ctrl+C and Linux SIGINT/SIGTERM.

dotnet publish XFEExtension.NetCore.ServerInteractive.TServer/XFEExtension.NetCore.ServerInteractive.TServer.csproj -p:PublishProfile=win-x64-single-file
dotnet publish XFEExtension.NetCore.ServerInteractive.TServer/XFEExtension.NetCore.ServerInteractive.TServer.csproj -p:PublishProfile=linux-x64-single-file

Both profiles are self-contained single-file publications with trimming disabled. All runtime configuration and structured server data are persisted by AutoConfig under data/: ServerProfile.xpf, DataProfile.xpf, and UserProfile.xpf. The first start materializes ServerProfile.xpf with safe defaults. The run directory also contains certificates/, logs/, and temp/; inject a PFX password with XFE_TLS_PFX_PASSWORD, --tls-password, or --tls-password-stdin, never in a profile.


Server Side

Setting Up the Server

Use XFEServerBuilder to assemble the server.

var server = XFEServerBuilder.CreateBuilder()
    .UseXFEServer()                   // Use XFE standard server (includes log initialization, exception handling, core processor)
    .AddServerCore(serverCore)        // Add an XFEServerCore instance
    .Build();                         // Build the XFEServer instance

await server.RunAsync();              // Start and run; production hosts should pass a shutdown CancellationToken

Multiple server cores can bind to the same port as long as their MainEntryPoint values differ. The framework starts one underlying listener for the shared port and dispatches HTTP and WebSocket events by the combination of port and main entry point:

var apiCore = XFEServerCoreBuilder.CreateBuilder().Build(options =>
{
    options.ServerCoreName = "ApiServer";
    options.MainEntryPoint = "api";
    options.BindIP("http://localhost:3300/");
});

var backendCore = XFEServerCoreBuilder.CreateBuilder().Build(options =>
{
    options.ServerCoreName = "BackendServer";
    options.MainEntryPoint = "backend";
    options.BindIP("http://localhost:3300/"); // Shares the port with apiCore
});

var server = XFEServerBuilder.CreateBuilder()
    .UseXFEServer()
    .AddServerCore(apiCore)       // /api/{sub-route}
    .AddServerCore(backendCore)   // /backend/{sub-route}
    .Build();

MainEntryPoint must be unique on each port; otherwise, server startup throws XFEServerBuilderException. Leading, trailing, and duplicate / separators are normalized automatically. When nested entry points both match, the longer, more specific entry point wins; an empty entry point acts as the fallback core for that port.

UseXFEServer() is equivalent to:

.AddInitializer<ServerLogInitializer>()      // Log initializer
.AddService<ServerExceptionProcessService>() // Global exception handler
.AddCoreProcessor<XFEServerCoreProcessService>() // Core request processor

You can also register only custom services without using the standard suite:

var server = XFEServerBuilder.CreateBuilder()
    .AddInitializer<MyInitializerService>()  // Custom initializer service (runs once before the server starts)
    .AddService<MyService>()                 // Custom synchronous service (runs when the server starts)
    .AddAsyncService<MyAsyncService>()       // Custom asynchronous service (runs asynchronously when the server starts)
    .AddCoreProcessor<MyCoreProcessor>()     // A core processor must be added
    .Build();

Defining Standard Core Services (Route Handling)

Inherit ServerCoreStandardServiceBase and mark methods with [EntryPoint("route path")].
The class must be declared as partial for the Source Generator to automatically generate the route dictionary.

using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

public partial class MathService : ServerCoreStandardServiceBase
{
    [EntryPoint("math/add")]
    public async Task Add()
    {
        var a = Json?["a"]?.GetValue<double>() ?? 0;
        var b = Json?["b"]?.GetValue<double>() ?? 0;
        await Close(a + b);     // Return result and close the request
    }

    [EntryPoint("math/subtract")]
    public void Subtract()
    {
        var a = Json?["a"]?.GetValue<double>() ?? 0;
        var b = Json?["b"]?.GetValue<double>() ?? 0;
        OK();                   // Mark as handled
        // Synchronous methods are also supported; the return type is void
    }
}

Register in XFEServerCoreBuilder:

XFEServerCoreBuilder.CreateBuilder()
    .AddService<MathService>()   // Automatically reads route paths from [EntryPoint] and registers
    .Build(options => { options.BindIP("http://localhost:3300/"); });

Add [NoLog] to a high-frequency entry point when its routine request logs are not needed:

[EntryPoint("health")]
[NoLog]
public async Task Health() => await Close("OK");

This suppresses the framework's request-received, validation, route, and elapsed-time messages. Exception logs are still emitted, and business logs explicitly written inside the method through Console.Write* or another logger are unaffected.

Commonly accessible properties in service methods:

Property Type Description
Route string The route path of the current request
ClientIP string The client's IP address
Json QueryableJsonNode? The parsed JSON node of the request body
Args CyberCommRequestEventArgs The raw request event args (includes Headers, Request, etc.)
XFEServerCore XFEServerCore The server core instance this service belongs to
Handled bool Whether the request has been handled
IsStandardError bool Whether this is a standard error response

Wildcard Routes

An independent * in a route segment can match any single path segment, and multi-level wildcards are supported.
The framework prioritizes patterns with more literal segments (most-specific-first principle), regardless of registration order.

public partial class DynamicService : ServerCoreStandardServiceBase
{
    // Matches v1/anything/info, e.g., v1/users/info or v1/orders/info
    [EntryPoint("v1/*/info")]
    public async Task DynamicInfo()
    {
        // The Route property contains the actual request path, e.g., v1/users/info
        await Close($"Your route is: {Route}, your IP is: {ClientIP}");
    }

    // Matches paths under v1/test/* (* only matches a single path segment), e.g., v1/test/a, v1/test/hello
    [EntryPoint("v1/test/*")]
    public async Task TestAny()
    {
        await Close($"Server: {XFEServerCore.ServerCoreName}, Route: {Route}");
    }
}

Note: The wildcard * must be a complete path segment and cannot be mixed with other characters (e.g., a*b is invalid).


Using the XFE Standard Server Core

The UseXFEStandardServerCore<T>() extension method provides a complete standardized server core with built-in:

  • User login (server route user/login, client can call using alias login), auto relogin (route user/relogin, alias relogin)
  • Data table CRUD management (table/get/{tableName}, table/add/{tableName}, table/change/{tableName}, table/remove/{tableName})
  • IP banning (ip/banned/get, ip/banned/add, ip/banned/remove), daily counting
  • Connection check (check_connect), server logging (route log/get, alias get_log; route log/clear, alias clear_log)
var serverCore = XFEServerCoreBuilder.CreateBuilder()
    .UseXFEStandardServerCore<IUserFaceInfo>(options =>
    {
        // Provide user data source
        options.GetUserFunction = () => UserProfile.UserTable;
        options.AddUserFunction = user => UserProfile.UserTable.Add((User)user);
        options.UpdateUserFunction = _ => UserProfile.SaveProfile();
        options.GetEncryptedUserLoginModelFunction = () => UserProfile.EncryptedUserLoginModelTable;
        options.AddEncryptedUserLoginModelFunction = UserProfile.EncryptedUserLoginModelTable.Add;
        options.RemoveEncryptedUserLoginModelFunction = model => UserProfile.EncryptedUserLoginModelTable.Remove(model);
        options.GetLoginKeepDays = () => 7;  // Number of days to keep the login session

        // Convert the User object to the interface type returned to the client after a successful login
        options.LoginResultConvertFunction = user => UserPublicDto.From((IUserInfo)user);

        // Configure the data table manager
        options.DataTableManagerBuilder = XFEDataTableManagerBuilder.CreateBuilder()
            // Parameters: display name, add permission, remove permission, change permission, get permission
            .AddTable<Order, DataProfile>("Orders", addPermissionLevel: 1, removePermissionLevel: 2, changePermissionLevel: 1, getPermissionLevel: 1)
            .AddTable<User, UserProfile>("Users", addPermissionLevel: 2, removePermissionLevel: 2, changePermissionLevel: 2, getPermissionLevel: 1);
    })
    .AddService<MyCustomService>()   // Append custom business services
    .Build(options =>
    {
        options.MainEntryPoint = "api";     // Route prefix; the full path for a login request would be /api/user/login
        options.AcceptPost = true;
        options.AcceptGet = true;
        options.BindIP("http://localhost:3300/")
               .BindIP("https://localhost:3301/");
    });

Complete server example (with TServer reference implementation):

var server = XFEServerBuilder.CreateBuilder()
    .UseXFEServer()
    .AddServerCore(
        XFEServerCoreBuilder.CreateBuilder()
            .UseXFEStandardServerCore<IUserFaceInfo>(options =>
            {
                options.GetUserFunction = () => UserProfile.UserTable;
                options.AddUserFunction = user => UserProfile.UserTable.Add((User)user);
                options.GetEncryptedUserLoginModelFunction = () => UserProfile.EncryptedUserLoginModelTable;
                options.AddEncryptedUserLoginModelFunction = UserProfile.EncryptedUserLoginModelTable.Add;
                options.RemoveEncryptedUserLoginModelFunction = model => UserProfile.EncryptedUserLoginModelTable.Remove(model);
                options.GetLoginKeepDays = () => 7;
                options.UpdateUserFunction = _ => UserProfile.SaveProfile();
                options.LoginResultConvertFunction = user => UserPublicDto.From((IUserInfo)user);
                options.DataTableManagerBuilder = XFEDataTableManagerBuilder.CreateBuilder()
                    .AddTable<Order, DataProfile>("Orders", 1, 2, 1, 1)
                    .AddTable<Person, DataProfile>("Persons", 1, 2, 1, 1)
                    .AddTable<User, UserProfile>("Users", 2, 2, 2, 1);
            })
            .AddService<TestCoreService>()
            .AddService<EchoCoreService>()
            .Build(options =>
            {
                options.MainEntryPoint = "api";
                options.AcceptPost = true;
                options.AcceptGet = true;
                options.BindIP("http://localhost:3305/")
                       .BindIP("https://localhost:3306/");
            }))
    .Build();

await server.RunAsync();

Manual Service Composition

If you don't need the standard suite, you can add individual services as needed:

XFEServerCoreBuilder.CreateBuilder()
    // Register a standard service (routes are automatically read from [EntryPoint])
    .AddService<MyRouteService>()
    // Explicitly specify a route (useful for dynamic routes or when not using a Source Generator)
    .AddServiceWithRoute<MyDynamicService>("dynamic/*/process")
    // Original service (listens for server start events and all raw requests)
    .AddOriginalService<MyRawRequestHandler>()
    // Verify service (validates each request before route dispatch; returning false intercepts the request)
    .AddVerifyService<MyAuthVerifyService>()
    // Add user base parameters (used by login-related services)
    .AddUserParameterBase(getUserFn, addUserFn, getEncryptedModelFn, addEncryptedModelFn, removeEncryptedModelFn, getKeepDaysFn, convertFn)
    // Add a data table manager
    .AddDataTableManager(tableManagerBuilder, getUserFn, getEncryptedModelFn)
    // Add individual built-in services
    .AddEntryPointVerify()          // Entry point validation service
    .AddDailyCounterService()       // Daily request statistics
    .AddXFEErrorProcessService()    // Exception handling service
    .AddConnectService()            // Connection check service (check_connect route)
    .AddStandardLoginService<IUserFaceInfo>()  // Standard login service (login, relogin routes)
    .AddServerLogService()          // Server log query (get_log route)
    .AddIPBannerService()           // IP banning service (get_bannedIPList, add_bannedIP, remove_bannedIP routes)
    .Build(options =>
    {
        options.BindIP("http://localhost:3300/");
    });

Original Services and Verify Services

Original Service (IServerCoreOriginalService): Inherit ServerCoreOriginalServiceBase to listen for server start events and every raw request (before route dispatch).

using XFEExtension.NetCore.CyberComm;
using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

public class MyRawService : ServerCoreOriginalServiceBase
{
    public override void ServerStarted(object? sender, EventArgs e)
    {
        Console.WriteLine("Server has started!");
    }

    public override void RequestReceived(object? sender, CyberCommRequestEventArgs e)
    {
        Console.WriteLine($"Raw request received: {e.Request.Url}");
    }
}

Verify Service (IServerCoreVerifyService): Inherit ServerCoreVerifyServiceBase. Every request is validated before route dispatch. Returning false from VerifyRequest or returning a Task<bool> that resolves to false from VerifyRequestAsync interrupts further processing.

using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

public class MyAuthService : ServerCoreVerifyServiceBase
{
    public override bool VerifyRequest()
    {
        // Synchronous validation; returning false intercepts the request
        var token = Args.Request.Headers["X-Token"];
        return !string.IsNullOrEmpty(token);
    }

    public override async Task<bool> VerifyRequestAsync()
    {
        // Asynchronous validation; returns true by default (does not intercept)
        return true;
    }
}

Server Core Configuration Options

XFEServerCoreBuilder.Build(options => { ... }) accepts XFEServerCoreOptions:

Property Type Default Description
BindingIPAddress List<string> [] List of URLs to bind the server to
ServerCoreName string Auto-generated Server core name (used in logs)
MainEntryPoint string "" Main entry prefix (empty means matching starts from sub-routes directly)
AcceptGet bool false Whether to accept GET requests
AcceptPost bool true Whether to accept POST requests
AcceptNonStandardJson bool true Whether to accept non-standard JSON request bodies
GetIPFunction Func<CyberCommRequestEventArgs, string> Reads from ClientIP Custom IP retrieval function (e.g., for proxy scenarios)
.Build(options =>
{
    options.ServerCoreName = "MainServer";
    options.MainEntryPoint = "api";    // Request path format: /api/{sub-route}
    options.AcceptGet = true;
    options.AcceptPost = true;
    options.GetIPFunction = args => args.Request.Headers["X-Forwarded-For"] ?? args.ClientIP;
    options.BindIP("http://localhost:3300/")
           .BindIP("https://localhost:3301/");
});

Responding to Clients in Service Methods

In subclasses of ServerCoreStandardServiceBase, the following methods are available for responding:

// Send data (without closing the connection)
await Send("message text");
await Send(new { code = 0, data = "ok" }); // Automatically serialized to JSON

// Send data and close the request (most common)
await Close("response content");
await Close(new { result = 42 });

// Mark the request as handled (for synchronous methods, sends no response body)
OK();

// Return an error message (throw to send the error and stop further processing)
throw Error("Missing parameters", HttpStatusCode.BadRequest);
await CloseWithError("Unauthorized", HttpStatusCode.Unauthorized);

Client Side

Setting Up a Requester

Use XFEClientRequesterBuilder to build an XFEClientRequester:

using XFEExtension.NetCore.ServerInteractive.Utilities.Requester;

var requester = XFEClientRequesterBuilder.CreateBuilder()
    .AddRequest("echo", (session, deviceInfo, parameters) => new
    {
        execute = "echo",
        message = parameters.Length > 0 ? parameters[0] : string.Empty
    }, response => response)
    .Build(options =>
    {
        options.RequestAddress = "http://localhost:3300";  // Server address
        options.Session = string.Empty;                    // User session (updated after login)
        options.DeviceInfo = DeviceHelper.GetUniqueHardwareId(); // Unique device identifier
    });

// Make a request
var result = await requester.Request<string>("echo", "Hello, World!");
if (result.StatusCode == HttpStatusCode.OK)
    Console.WriteLine(result.Result);  // Output: Hello, World!
else
    Console.WriteLine($"Failed: {result.StatusCode} {result.Message}");

XFEClientRequesterOptions properties:

Property Type Default Description
RequestAddress string "http://localhost:3300/" Server request address
Session string "" Current user session
DeviceInfo string Unique hardware ID Device information

Inline Request Registration

AddRequest(route, constructBody, processResponse) is suitable for simple one-off requests:

var requester = XFEClientRequesterBuilder.CreateBuilder()
    // Request with no parameters
    .AddRequest("status", (_, _, _) => new { execute = "status" }, response => response)
    // Request with parameters
    .AddRequest("math/add", (_, _, parameters) => new
    {
        execute = "math/add",
        a = parameters.Length > 0 ? parameters[0] : 0,
        b = parameters.Length > 1 ? parameters[1] : 0
    }, response => response)
    // Login request example with session and deviceInfo
    .AddRequest("login", (session, deviceInfo, parameters) => new
    {
        execute = "login",
        account = parameters[0],
        password = parameters[1],
        deviceInfo
    }, response => JsonSerializer.Deserialize<UserLoginResult<UserFaceInfo>>(response)!)
    .Build(options =>
    {
        options.RequestAddress = "http://localhost:3300";
    });
  • constructBody: (string session, string deviceInfo, object[] parameters) => object — returns the request body object (automatically JSON-serialized).
  • processResponse: (string response) => object — parses the response string; can be null (no processing).

Defining Standard Request Services (Source Generator)

Inherit StandardRequestServiceBase and use the [Request]/[Response] attributes.
The class must be declared as partial.

using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Implements.Requester;

public partial class MathRequestService : StandardRequestServiceBase
{
    // [Request] marks the method that constructs the request body; Path is the route path, Name is an optional call alias
    [Request("math/add", Name = "add")]
    public object BuildAddRequest() => new
    {
        execute = "math/add",
        a = Parameters.Length > 0 ? Parameters[0] : 0,
        b = Parameters.Length > 1 ? Parameters[1] : 0
    };

    // [Response] marks the method that parses the response; Path and Name correspond to those in [Request]
    [Response("math/add", Name = "add")]
    public object ParseAddResponse() => double.Parse(UnescapedResponse);

    [Request("math/multiply")]
    public object BuildMultiplyRequest() => new
    {
        execute = "math/multiply",
        a = Parameters.Length > 0 ? Parameters[0] : 0,
        b = Parameters.Length > 1 ? Parameters[1] : 0
    };

    [Response("math/multiply")]
    public object ParseMultiplyResponse() => double.Parse(UnescapedResponse);
}

Properties accessible in [Request]/[Response] methods:

Property Description
Parameters The parameter array passed when calling Request(name, params object[] parameters)
Session The current user session
DeviceInfo Device information
Response The raw response string
UnescapedResponse The unescaped response string
Route The actual request route path

Register with the requester:

var requester = XFEClientRequesterBuilder.CreateBuilder()
    .AddRequest<MathRequestService>()   // Automatically reads routes from [Request]/[Response] and registers
    .Build(options =>
    {
        options.RequestAddress = "http://localhost:3300";
    });

// Call using the route path
var result1 = await requester.Request<double>("math/add", 3.0, 5.0);
// Call using the Name alias (equivalent)
var result2 = await requester.Request<double>("add", 3.0, 5.0);

Using XFE Standard Requests

The UseXFEStandardRequest<T>() extension method registers all standard service requests in one call (login, re-login, IP banning, logging, connection check):

using XFEExtension.NetCore.ServerInteractive.Utilities.Extensions;
using XFEExtension.NetCore.ServerInteractive.Utilities.Requester;

var requester = XFEClientRequesterBuilder.CreateBuilder()
    .UseXFEStandardRequest<UserFaceInfo>()   // T is the implementing class of the user info interface returned on login
    .Build(options =>
    {
        options.RequestAddress = "http://localhost:3300";
    });

// Login
var loginResult = await requester.Request<UserLoginResult<UserFaceInfo>>("login", "your-account", "your-password");
if (loginResult.StatusCode == HttpStatusCode.OK)
{
    Console.WriteLine($"Session: {loginResult.Result.Session}");
    Console.WriteLine($"Expiry: {loginResult.Result.ExpireDate}");
    Console.WriteLine($"Nickname: {loginResult.Result.UserInfo.NickName}");
    requester.Session = loginResult.Result.Session;  // Save session for subsequent requests
}

// Re-login (automatically verified using Session + device info)
var reloginResult = await requester.Request<UserFaceInfo>("relogin");

// Check connection
var checkResult = await requester.Request<DateTime>("check_connect");

// Get logs
var logResult = await requester.Request<string>("get_log", DateTime.MinValue, DateTime.MaxValue);

// IP ban management
// Note: AddBannedIPRequest() registers call names get_bannedIPList / add_bannedIP / remove_bannedIP
// which correspond to server routes ip/banned/get / ip/banned/add / ip/banned/remove
var bannedIPs = await requester.Request<List<IPAddressInfo>>("get_bannedIPList");
await requester.Request<string>("add_bannedIP", "192.168.1.100", "Malicious request");
await requester.Request<bool>("remove_bannedIP", "192.168.1.100");

UseXFEStandardRequest<T>() is equivalent to:

.AddLoginRequest<T>()        // Register login (→ user/login) and relogin (→ user/relogin) requests
.AddBannedIPRequest()        // Register get_bannedIPList, add_bannedIP, remove_bannedIP requests
                             //   corresponding to server routes: ip/banned/get, ip/banned/add, ip/banned/remove
.AddLogRequest()             // Register get_log (→ log/get) and clear_log (→ log/clear) requests
.AddCheckConnectRequest()    // Register check_connect request

TableRequester (Data Table Requester)

TableRequester is specifically designed for CRUD interactions with the server-side data table manager:

var tableRequester = new TableRequester
{
    RequestAddress = "http://localhost:3300",
    Session = "your-session-here",
    DeviceInfo = DeviceHelper.GetUniqueHardwareId()
};

// Get all orders (no pagination)
var result = await tableRequester.Get<Order>();
foreach (var order in result.DataList)
    Console.WriteLine($"ID:{order.Id}\tName:{order.Name}");

// Paginated get (10 per page, page 1)
var paged = await tableRequester.Get<Order>(pageSize: 10, page: 1);

// Add data
bool success = await tableRequester.Add(new Order { Name = "New Order", Description = "Description" });

// Change data (matched by Id)
var order = result.DataList[0];
order.Name = "Updated Name";
await tableRequester.Change(order);

// Remove data
await tableRequester.Remove<Order>(order.Id);

The table name is inferred from the type name by default: the type name with its first letter lowercased is used as the request table name (TableNameInRequest). For example, Orderorder.
This is the name actually used by TableRequester when communicating with the server, which differs from the tableShowName (display name, e.g., "Orders") specified in AddTable.

// Default: Order type → request table name is automatically inferred as "order" (type name with first letter lowercased)
await tableRequester.Get<Order>();

// Manually specify the request table name (must match the server-side TableNameInRequest, i.e., the type name with first letter lowercased)
// Note: This is the table name used in the request path, not the tableShowName specified in AddTable
await tableRequester.Get<Order>("order", pageSize: 10, page: 1);

Data models must implement the IIdModel interface (which includes a string Id property):

public class Order : IIdModel
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
}

Source Generator

This library provides two incremental source generators that automatically generate route dictionaries for service classes and request classes at compile time, eliminating the need for manual maintenance.

Server Side: EntryPoint Generator

Automatically generates SyncEntryPoints and AsyncEntryPoints dictionaries for partial classes that inherit ServerCoreStandardServiceBase.

Requirements:

  • The class must be declared as partial
  • Method return types must be void (synchronous) or Task (asynchronous)
  • Methods must not have parameters
  • Route paths must not contain quotes (") or backslashes (\)
  • The wildcard * must be a complete path segment (cannot be mixed with other characters)
  • A single class cannot have more than one handler for the same route path

Example:

public partial class ApiService : ServerCoreStandardServiceBase
{
    [EntryPoint("user/profile")]
    [NoLog]
    public async Task GetProfile()
    {
        // ...
    }

    [EntryPoint("user/update")]
    public void UpdateUser()
    {
        // ...
    }

    // Wildcard route: matches resource/anything/details
    [EntryPoint("resource/*/details")]
    public async Task ResourceDetails()
    {
        // Route contains the actual matched path
        await Close($"Resource detail for {Route}");
    }
}

The generator automatically produces (no manual writing required):

// ApiService.EntryPoints.g.cs (auto-generated, shown for illustration only)
public override Dictionary<string, Action> SyncEntryPoints => new()
{
    { "user/update", UpdateUser },
};

public override Dictionary<string, Func<Task>> AsyncEntryPoints => new()
{
    { "user/profile", GetProfile },
    { "resource/*/details", ResourceDetails },
};

public override HashSet<string> NoLogEntryPoints => new()
{
    "user/profile",
};

Client Side: ClientRequest Generator

Automatically generates RequestPoints, ResponsePoints, and RequestRouteMap dictionaries for partial classes that inherit StandardRequestServiceBase.

Requirements:

  • The class must be declared as partial
  • Method return types must be object
  • Methods must not have parameters
  • The same path or name (within [Request] or [Response]) cannot be registered more than once within a single class

Example:

public partial class UserRequestService<T> : StandardRequestServiceBase where T : IUserFaceInfo
{
    // Path is the route path; Name is an optional request alias (you can call using either Path or Name)
    [Request("login", Name = "login")]
    public object LoginRequest() => new
    {
        execute = "login",
        account = Parameters[0],
        password = Parameters[1],
        deviceInfo = DeviceInfo
    };

    [Response("login", Name = "login")]
    public object LoginResponse() => JsonSerializer.Deserialize<UserLoginResult<T>>(UnescapedResponse)!;

    [Request("relogin", Name = "relogin")]
    public object ReloginRequest() => new
    {
        execute = "relogin",
        session = Session,
        deviceInfo = DeviceInfo
    };

    [Response("relogin", Name = "relogin")]
    public object ReloginResponse() => JsonSerializer.Deserialize<T>(UnescapedResponse)!;
}

Diagnostic Rules

The incremental generators check code at compile time and report diagnostics:

Code Description Scope
XFE0003 Classes containing [EntryPoint] methods must be partial Server side
XFE0004 [EntryPoint] methods must not have parameters Server side
XFE0005 [EntryPoint] method return type must be void or Task Server side
XFE0006 [EntryPoint] path contains invalid characters (quotes or backslashes) Server side
XFE0007 Classes containing [Request]/[Response] methods must be partial Client side
XFE0008 [Request]/[Response] methods must not have parameters Client side
XFE0009 [Request]/[Response] method return type must be object Client side
XFE0010 [Request]/[Response] path contains invalid characters Client side
XFE0011 [Request]/[Response] path or name is registered more than once Client side
XFE0012 [EntryPoint] path is registered more than once in the same class Server side
XFE0013 Invalid wildcard usage in [EntryPoint] (* must be a complete path segment) Server side

All diagnostics have corresponding online documentation at: https://docs.xfegzs.com/View/Errors/ServerInteractive/XFE{code} (e.g., https://docs.xfegzs.com/View/Errors/ServerInteractive/XFE0003)


Common Errors and Solutions

Q: The service is not receiving requests and there is no console output

  • Confirm that the correct address is bound in XFEServerCoreBuilder.Build(options => { options.BindIP("..."); })
  • If MainEntryPoint is set, request paths must include that prefix — for example, if api is set, request /api/echo

Q: InvalidOperationException: EntryPointList for type MyService is empty

  • Confirm that MyService inherits ServerCoreStandardServiceBase
  • Confirm that the class is declared with the partial keyword
  • Confirm that the method has a [EntryPoint("path")] attribute with a non-empty path

Q: XFEServerBuilderException: No core processor has been added

  • Before calling XFEServerBuilder.Build(), you must call AddCoreProcessor<T>(), or use the UseXFEServer() extension method

Q: A route is matching an unexpected service

  • The framework uses the "most-specific-first" principle: route patterns with more literal segments have higher priority, regardless of registration order
  • Exact routes (without wildcards) always take priority over wildcard routes

Q: The return type of a [Request]/[Response] method is causing an error

  • The method must declare object as its return type (it cannot be string, int, or any other specific type)

Q: Client requests always return 500

  • Ensure options.RequestAddress does not have a trailing slash — the requester builds URLs as RequestAddress + "/" + route, so a trailing slash would create a double slash (e.g., use "http://localhost:3300" not "http://localhost:3300/")
  • Confirm that the server-side route exactly matches the request name (case-sensitive)
LICENSE.txt MIT

MIT License

Copyright (c) [2026] [XFEstudio]

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.zh-CN.md 33.69 KB 查看文件 原始文件

XFEExtension.NetCore.ServerInteractive

NuGet NuGet Downloads License: MIT .NET

📖 English | 简体中文

描述

ServerInteractive 是一个 C# 的 DLL 库,基于 XFEExtension.NetCore 提供的 CyberComm 网络架构,让使用者可以快速构建服务器端和客户端网络框架。支持基于路由的请求分发、增量代码生成(Source Generator)、用户登录体系、数据表管理、IP 封禁等开箱即用的标准功能。


目录


快速开始

使用前请在项目中引用 XFEExtension.NetCore.ServerInteractive NuGet 包。

最简单的服务器示例(一个 echo 服务,绑定本地 3300 端口):

服务定义(EchoService.cs)

using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

// 类必须为 partial,以便 Source Generator 自动生成路由字典
public partial class EchoService : ServerCoreStandardServiceBase
{
    [EntryPoint("echo")]         // 路由路径:/echo
    public async Task Echo()
    {
        var message = Json?["message"]?.GetValue<string>() ?? string.Empty;
        await Close(message);    // 回复客户端并结束请求
    }
}

程序入口(Program.cs)

using XFEExtension.NetCore.ServerInteractive.Utilities.Extensions;
using XFEExtension.NetCore.ServerInteractive.Utilities.Server;

var server = XFEServerBuilder.CreateBuilder()
    .UseXFEServer()                               // 注册日志、异常处理、核心处理器
    .AddServerCore(
        XFEServerCoreBuilder.CreateBuilder()
            .AddService<EchoService>()            // 注册 EchoService(路由从 [EntryPoint] 自动获取)
            .Build(options =>
            {
                options.BindIP("http://localhost:3300/");
            }))
    .Build();

using var shutdown = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; shutdown.Cancel(); };
await server.RunAsync(shutdown.Token);

跨平台控制台 EXE

XFEExtension.NetCore.ServerInteractive.TServer 是正式的控制台宿主模板,不注册 Windows Service,也不依赖 ASP.NET Core。它只通过 CyberComm 提供 HTTP/1.1、HTTPS、WebSocket 和 WSS,并响应 Windows Ctrl+C、Linux SIGINT/SIGTERM 后优雅停止。

dotnet publish XFEExtension.NetCore.ServerInteractive.TServer/XFEExtension.NetCore.ServerInteractive.TServer.csproj -p:PublishProfile=win-x64-single-file
dotnet publish XFEExtension.NetCore.ServerInteractive.TServer/XFEExtension.NetCore.ServerInteractive.TServer.csproj -p:PublishProfile=linux-x64-single-file

两个配置均为自包含单文件且默认不裁剪。所有运行配置和结构化服务数据统一由 AutoConfig 持久化到 data/ServerProfile.xpfDataProfile.xpfUserProfile.xpf;首次启动会以安全默认值生成 ServerProfile.xpf。运行目录还包含 certificates/logs/temp/;PFX 密码只通过 XFE_TLS_PFX_PASSWORD--tls-password--tls-password-stdin 注入,不写入 Profile。


服务器端

搭建服务器

使用 XFEServerBuilder 来组装服务器。

var server = XFEServerBuilder.CreateBuilder()
    .UseXFEServer()                   // 使用XFE标准服务器(包含日志初始化、异常处理、核心处理器)
    .AddServerCore(serverCore)        // 添加一个 XFEServerCore 核心服务器实例
    .Build();                         // 构建 XFEServer 实例

await server.RunAsync();              // 启动并持续运行;正式宿主应传入退出 CancellationToken

多个核心服务器可以绑定同一个端口,只要它们的 MainEntryPoint 不同。框架会为共享端口只启动一个底层监听器,并按“端口 + 主入口点”把 HTTP 和 WebSocket 事件分发给对应核心:

var apiCore = XFEServerCoreBuilder.CreateBuilder().Build(options =>
{
    options.ServerCoreName = "ApiServer";
    options.MainEntryPoint = "api";
    options.BindIP("http://localhost:3300/");
});

var backendCore = XFEServerCoreBuilder.CreateBuilder().Build(options =>
{
    options.ServerCoreName = "BackendServer";
    options.MainEntryPoint = "backend";
    options.BindIP("http://localhost:3300/"); // 与 apiCore 共享端口
});

var server = XFEServerBuilder.CreateBuilder()
    .UseXFEServer()
    .AddServerCore(apiCore)       // /api/{次级路由}
    .AddServerCore(backendCore)   // /backend/{次级路由}
    .Build();

同一端口上的 MainEntryPoint 必须唯一,否则服务器启动时会抛出 XFEServerBuilderException。主入口点会自动移除首尾及重复的 /;嵌套入口点同时匹配时优先选择更长、更具体的入口点,空入口点则作为该端口的后备核心。

UseXFEServer() 等价于:

.AddInitializer<ServerLogInitializer>()      // 日志初始化器
.AddService<ServerExceptionProcessService>() // 全局异常处理
.AddCoreProcessor<XFEServerCoreProcessService>() // 核心请求处理器

你也可以只注册自定义服务而不使用标准套件:

var server = XFEServerBuilder.CreateBuilder()
    .AddInitializer<MyInitializerService>()  // 自定义初始化服务(服务器启动前执行一次)
    .AddService<MyService>()                 // 自定义同步服务(服务器启动时执行)
    .AddAsyncService<MyAsyncService>()       // 自定义异步服务(服务器启动时异步执行)
    .AddCoreProcessor<MyCoreProcessor>()     // 必须添加一个核心处理器
    .Build();

定义标准核心服务(路由处理)

继承 ServerCoreStandardServiceBase 并使用 [EntryPoint("路由路径")] 标记方法。
类必须声明为 partial,Source Generator 才能自动生成路由字典。

using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

public partial class MathService : ServerCoreStandardServiceBase
{
    [EntryPoint("math/add")]
    public async Task Add()
    {
        var a = Json?["a"]?.GetValue<double>() ?? 0;
        var b = Json?["b"]?.GetValue<double>() ?? 0;
        await Close(a + b);     // 返回结果并关闭请求
    }

    [EntryPoint("math/subtract")]
    public void Subtract()
    {
        var a = Json?["a"]?.GetValue<double>() ?? 0;
        var b = Json?["b"]?.GetValue<double>() ?? 0;
        OK();                   // 标记已处理
        // 同步方法同样支持,返回类型为 void
    }
}

XFEServerCoreBuilder 中注册:

XFEServerCoreBuilder.CreateBuilder()
    .AddService<MathService>()   // 从 [EntryPoint] 自动获取路由路径并注册
    .Build(options => { options.BindIP("http://localhost:3300/"); });

如果某个高频入口点不需要输出常规请求日志,可在方法上添加 [NoLog]

[EntryPoint("health")]
[NoLog]
public async Task Health() => await Close("OK");

该特性会关闭框架产生的“接收到请求、校验结果、请求路由、耗时”日志。异常日志仍会输出;方法内部主动调用 Console.Write* 或其他日志组件产生的业务日志不受影响。

可在服务方法中访问的常用属性:

属性 类型 说明
Route string 当前请求的路由路径
ClientIP string 客户端 IP 地址
Json QueryableJsonNode? 解析后的请求体 JSON 节点
Args CyberCommRequestEventArgs 原始请求事件参数(含 Headers、Request 等)
XFEServerCore XFEServerCore 所属的服务器核心实例
Handled bool 是否已处理请求
IsStandardError bool 是否为标准错误响应

通配符路由

路由段中使用独立的 * 可以匹配任意单段路径,支持多级通配符。
框架会优先匹配字面量段更多的模式(最具体优先原则),与注册顺序无关。

public partial class DynamicService : ServerCoreStandardServiceBase
{
    // 匹配 v1/任意值/info,例如 v1/users/info、v1/orders/info
    [EntryPoint("v1/*/info")]
    public async Task DynamicInfo()
    {
        // Route 属性包含实际请求路径,如 v1/users/info
        await Close($"您请求的路由是:{Route},您的IP是:{ClientIP}");
    }

    // 匹配 v1/test/* 下的路径(* 仅匹配单个路径段),例如 v1/test/a, v1/test/hello
    [EntryPoint("v1/test/*")]
    public async Task TestAny()
    {
        await Close($"Server: {XFEServerCore.ServerCoreName}, Route: {Route}");
    }
}

注意: 通配符 * 必须是完整的路径段,不能与其他字符混合(如 a*b 是非法的)。


使用XFE标准服务器核心

UseXFEStandardServerCore<T>() 扩展方法提供了一套完整的标准化服务器核心,内置:

  • 用户登录(服务端路由 user/login,客户端可通过别名 login 调用)、自动重登(路由 user/relogin,别名 relogin
  • 数据表 CRUD 管理(table/get/{表名}table/add/{表名}table/change/{表名}table/remove/{表名}
  • IP 封禁(ip/banned/getip/banned/addip/banned/remove)、每日计数
  • 连接检查(check_connect)、服务器日志(路由 log/get,别名 get_log;路由 log/clear,别名 clear_log
var serverCore = XFEServerCoreBuilder.CreateBuilder()
    .UseXFEStandardServerCore<IUserFaceInfo>(options =>
    {
        // 提供用户数据源
        options.GetUserFunction = () => UserProfile.UserTable;
        options.AddUserFunction = user => UserProfile.UserTable.Add((User)user);
        options.UpdateUserFunction = _ => UserProfile.SaveProfile();
        options.GetEncryptedUserLoginModelFunction = () => UserProfile.EncryptedUserLoginModelTable;
        options.AddEncryptedUserLoginModelFunction = UserProfile.EncryptedUserLoginModelTable.Add;
        options.RemoveEncryptedUserLoginModelFunction = model => UserProfile.EncryptedUserLoginModelTable.Remove(model);
        options.GetLoginKeepDays = () => 7;  // 登录 Session 保持天数

        // 登录成功后将 User 对象转换为返回给客户端的接口类型
        options.LoginResultConvertFunction = user => UserPublicDto.From((IUserInfo)user);

        // 配置数据表管理器
        options.DataTableManagerBuilder = XFEDataTableManagerBuilder.CreateBuilder()
            // 参数:显示名称, 添加权限, 删除权限, 修改权限, 查询权限
            .AddTable<Order, DataProfile>("订单", addPermissionLevel: 1, removePermissionLevel: 2, changePermissionLevel: 1, getPermissionLevel: 1)
            .AddTable<User, UserProfile>("用户", addPermissionLevel: 2, removePermissionLevel: 2, changePermissionLevel: 2, getPermissionLevel: 1);
    })
    .AddService<MyCustomService>()   // 追加自定义业务服务
    .Build(options =>
    {
        options.MainEntryPoint = "api";     // 所有路由前缀,例如登录请求的完整路径为 /api/user/login
        options.AcceptPost = true;
        options.AcceptGet = true;
        options.BindIP("http://localhost:3300/")
               .BindIP("https://localhost:3301/");
    });

完整服务器示例(配合 TServer 参考实现):

var server = XFEServerBuilder.CreateBuilder()
    .UseXFEServer()
    .AddServerCore(
        XFEServerCoreBuilder.CreateBuilder()
            .UseXFEStandardServerCore<IUserFaceInfo>(options =>
            {
                options.GetUserFunction = () => UserProfile.UserTable;
                options.AddUserFunction = user => UserProfile.UserTable.Add((User)user);
                options.GetEncryptedUserLoginModelFunction = () => UserProfile.EncryptedUserLoginModelTable;
                options.AddEncryptedUserLoginModelFunction = UserProfile.EncryptedUserLoginModelTable.Add;
                options.RemoveEncryptedUserLoginModelFunction = model => UserProfile.EncryptedUserLoginModelTable.Remove(model);
                options.GetLoginKeepDays = () => 7;
                options.UpdateUserFunction = _ => UserProfile.SaveProfile();
                options.LoginResultConvertFunction = user => UserPublicDto.From((IUserInfo)user);
                options.DataTableManagerBuilder = XFEDataTableManagerBuilder.CreateBuilder()
                    .AddTable<Order, DataProfile>("订单", 1, 2, 1, 1)
                    .AddTable<Person, DataProfile>("人物", 1, 2, 1, 1)
                    .AddTable<User, UserProfile>("用户", 2, 2, 2, 1);
            })
            .AddService<TestCoreService>()
            .AddService<EchoCoreService>()
            .Build(options =>
            {
                options.MainEntryPoint = "api";
                options.AcceptPost = true;
                options.AcceptGet = true;
                options.BindIP("http://localhost:3305/")
                       .BindIP("https://localhost:3306/");
            }))
    .Build();

await server.RunAsync();

手动组合服务

如果不需要标准套件,可以单独按需添加各个服务:

XFEServerCoreBuilder.CreateBuilder()
    // 注册标准服务(路由从 [EntryPoint] 自动获取)
    .AddService<MyRouteService>()
    // 显式指定路由注册(适用于动态路由或不使用 Source Generator 的场景)
    .AddServiceWithRoute<MyDynamicService>("dynamic/*/process")
    // 原始服务(监听服务器启动事件和所有原始请求)
    .AddOriginalService<MyRawRequestHandler>()
    // 校验服务(在路由分发前对每个请求进行验证,返回 false 可拦截请求)
    .AddVerifyService<MyAuthVerifyService>()
    // 添加用户基础参数(供登录相关服务使用)
    .AddUserParameterBase(getUserFn, addUserFn, getEncryptedModelFn, addEncryptedModelFn, removeEncryptedModelFn, getKeepDaysFn, convertFn)
    // 添加数据表管理器
    .AddDataTableManager(tableManagerBuilder, getUserFn, getEncryptedModelFn)
    // 添加各内置服务
    .AddEntryPointVerify()          // 入口点校验服务
    .AddDailyCounterService()       // 每日请求统计
    .AddXFEErrorProcessService()    // 异常处理服务
    .AddConnectService()            // 连接检查服务(check_connect 路由)
    .AddStandardLoginService<IUserFaceInfo>()  // 标准登录服务(login、relogin 路由)
    .AddServerLogService()          // 服务器日志查询(get_log 路由)
    .AddIPBannerService()           // IP 封禁服务(get_bannedIPList、add_bannedIP、remove_bannedIP 路由)
    .Build(options =>
    {
        options.BindIP("http://localhost:3300/");
    });

原始服务与校验服务

原始服务(IServerCoreOriginalService:继承 ServerCoreOriginalServiceBase,可以监听服务器启动事件和每一个原始请求(在路由分发之前)。

using XFEExtension.NetCore.CyberComm;
using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

public class MyRawService : ServerCoreOriginalServiceBase
{
    public override void ServerStarted(object? sender, EventArgs e)
    {
        Console.WriteLine("服务器已启动!");
    }

    public override void RequestReceived(object? sender, CyberCommRequestEventArgs e)
    {
        Console.WriteLine($"收到原始请求:{e.Request.Url}");
    }
}

校验服务(IServerCoreVerifyService:继承 ServerCoreVerifyServiceBase,每个请求在路由分发前都会执行校验。返回 falseTask<false> 会中断后续处理。

using XFEExtension.NetCore.ServerInteractive.Implements.CoreService;

public class MyAuthService : ServerCoreVerifyServiceBase
{
    public override bool VerifyRequest()
    {
        // 同步校验,返回 false 则拦截请求
        var token = Args.Request.Headers["X-Token"];
        return !string.IsNullOrEmpty(token);
    }

    public override async Task<bool> VerifyRequestAsync()
    {
        // 异步校验,默认返回 true(不拦截)
        return true;
    }
}

服务器核心配置选项

XFEServerCoreBuilder.Build(options => { ... }) 接受 XFEServerCoreOptions

属性 类型 默认值 说明
BindingIPAddress List<string> [] 服务器绑定的 URL 列表
ServerCoreName string 自动生成 服务器核心名称(日志显示用)
MainEntryPoint string "" 主入口前缀(空则直接从次级路由匹配)
AcceptGet bool false 是否接受 GET 请求
AcceptPost bool true 是否接受 POST 请求
AcceptNonStandardJson bool true 是否接受非标准 JSON 请求体
GetIPFunction Func<CyberCommRequestEventArgs, string> ClientIP 获取 自定义 IP 获取函数(如代理场景)
.Build(options =>
{
    options.ServerCoreName = "主服务器";
    options.MainEntryPoint = "api";    // 请求路径格式:/api/{子路由}
    options.AcceptGet = true;
    options.AcceptPost = true;
    options.GetIPFunction = args => args.Request.Headers["X-Forwarded-For"] ?? args.ClientIP;
    options.BindIP("http://localhost:3300/")
           .BindIP("https://localhost:3301/");
});

服务方法中响应客户端

ServerCoreStandardServiceBase 子类中,可以使用以下方法响应:

// 发送数据(不关闭连接)
await Send("消息文本");
await Send(new { code = 0, data = "ok" }); // 自动序列化为 JSON

// 发送数据并关闭请求(最常用)
await Close("响应内容");
await Close(new { result = 42 });

// 标记请求已处理(适用于同步方法,不发送响应体)
OK();

// 返回错误信息
Error("参数缺失", HttpStatusCode.BadRequest);
await CloseWithError("未授权", HttpStatusCode.Unauthorized);

客户端

搭建请求器

使用 XFEClientRequesterBuilder 构建 XFEClientRequester

using XFEExtension.NetCore.ServerInteractive.Utilities.Requester;

var requester = XFEClientRequesterBuilder.CreateBuilder()
    .AddRequest("echo", (session, deviceInfo, parameters) => new
    {
        execute = "echo",
        message = parameters.Length > 0 ? parameters[0] : string.Empty
    }, response => response)
    .Build(options =>
    {
        options.RequestAddress = "http://localhost:3300";  // 服务器地址
        options.Session = string.Empty;                    // 用户 Session(登录后更新)
        options.DeviceInfo = DeviceHelper.GetUniqueHardwareId(); // 设备唯一标识
    });

// 发起请求
var result = await requester.Request<string>("echo", "Hello, World!");
if (result.StatusCode == HttpStatusCode.OK)
    Console.WriteLine(result.Result);  // 输出:Hello, World!
else
    Console.WriteLine($"失败:{result.StatusCode} {result.Message}");

XFEClientRequesterOptions 属性:

属性 类型 默认值 说明
RequestAddress string "http://localhost:3300/" 服务器请求地址
Session string "" 当前用户 Session
DeviceInfo string 硬件唯一 ID 设备信息

内联请求注册

AddRequest(route, constructBody, processResponse) 适合简单的一次性请求:

var requester = XFEClientRequesterBuilder.CreateBuilder()
    // 无参请求
    .AddRequest("status", (_, _, _) => new { execute = "status" }, response => response)
    // 带参数请求
    .AddRequest("math/add", (_, _, parameters) => new
    {
        execute = "math/add",
        a = parameters.Length > 0 ? parameters[0] : 0,
        b = parameters.Length > 1 ? parameters[1] : 0
    }, response => response)
    // 带 session 和 deviceInfo 的登录请求示例
    .AddRequest("login", (session, deviceInfo, parameters) => new
    {
        execute = "login",
        account = parameters[0],
        password = parameters[1],
        deviceInfo
    }, response => JsonSerializer.Deserialize<UserLoginResult<UserFaceInfo>>(response)!)
    .Build(options =>
    {
        options.RequestAddress = "http://localhost:3300";
    });
  • constructBody(string session, string deviceInfo, object[] parameters) => object,返回请求体对象(自动 JSON 序列化)。
  • processResponse(string response) => object,解析响应字符串,可为 null(不处理)。

定义标准请求服务(Source Generator)

继承 StandardRequestServiceBase 并使用 [Request]/[Response] 特性。
类必须声明为 partial

using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Implements.Requester;

public partial class MathRequestService : StandardRequestServiceBase
{
    // [Request] 标记构造请求体的方法,Path 为路由路径,Name 为可选的调用别名
    [Request("math/add", Name = "add")]
    public object BuildAddRequest() => new
    {
        execute = "math/add",
        a = Parameters.Length > 0 ? Parameters[0] : 0,
        b = Parameters.Length > 1 ? Parameters[1] : 0
    };

    // [Response] 标记解析响应的方法,Path 和 Name 与 [Request] 对应
    [Response("math/add", Name = "add")]
    public object ParseAddResponse() => double.Parse(UnescapedResponse);

    [Request("math/multiply")]
    public object BuildMultiplyRequest() => new
    {
        execute = "math/multiply",
        a = Parameters.Length > 0 ? Parameters[0] : 0,
        b = Parameters.Length > 1 ? Parameters[1] : 0
    };

    [Response("math/multiply")]
    public object ParseMultiplyResponse() => double.Parse(UnescapedResponse);
}

[Request]/[Response] 方法中可访问的属性:

属性 说明
Parameters 调用 Request(name, params object[] parameters) 时传入的参数数组
Session 当前用户 Session
DeviceInfo 设备信息
Response 原始响应字符串
UnescapedResponse 反转义后的响应字符串
Route 实际请求路由路径

注册到请求器:

var requester = XFEClientRequesterBuilder.CreateBuilder()
    .AddRequest<MathRequestService>()   // 从 [Request]/[Response] 自动获取路由并注册
    .Build(options =>
    {
        options.RequestAddress = "http://localhost:3300";
    });

// 通过路由路径调用
var result1 = await requester.Request<double>("math/add", 3.0, 5.0);
// 通过 Name 别名调用(等价)
var result2 = await requester.Request<double>("add", 3.0, 5.0);

使用XFE标准请求

UseXFEStandardRequest<T>() 扩展方法一键注册所有标准服务请求(登录、重登、IP 封禁、日志、连接检查):

using XFEExtension.NetCore.ServerInteractive.Utilities.Extensions;
using XFEExtension.NetCore.ServerInteractive.Utilities.Requester;

var requester = XFEClientRequesterBuilder.CreateBuilder()
    .UseXFEStandardRequest<UserFaceInfo>()   // T 为登录返回的用户信息接口实现类
    .Build(options =>
    {
        options.RequestAddress = "http://localhost:3300";
    });

// 登录
var loginResult = await requester.Request<UserLoginResult<UserFaceInfo>>("login", "your-account", "your-password");
if (loginResult.StatusCode == HttpStatusCode.OK)
{
    Console.WriteLine($"Session: {loginResult.Result.Session}");
    Console.WriteLine($"过期时间: {loginResult.Result.ExpireDate}");
    Console.WriteLine($"昵称: {loginResult.Result.UserInfo.NickName}");
    requester.Session = loginResult.Result.Session;  // 保存 Session 供后续请求使用
}

// 重新登录(使用 Session + 设备信息自动验证)
var reloginResult = await requester.Request<UserFaceInfo>("relogin");

// 检查连接
var checkResult = await requester.Request<DateTime>("check_connect");

// 获取日志
var logResult = await requester.Request<string>("get_log", DateTime.MinValue, DateTime.MaxValue);

// IP 封禁管理
// 说明:AddBannedIPRequest() 注册的调用名称为 get_bannedIPList / add_bannedIP / remove_bannedIP
// 对应服务端实际路由为 ip/banned/get / ip/banned/add / ip/banned/remove
var bannedIPs = await requester.Request<List<IPAddressInfo>>("get_bannedIPList");
await requester.Request<string>("add_bannedIP", "192.168.1.100", "恶意请求");
await requester.Request<bool>("remove_bannedIP", "192.168.1.100");

UseXFEStandardRequest<T>() 等价于:

.AddLoginRequest<T>()        // 注册 login(→ user/login)、relogin(→ user/relogin)请求
.AddBannedIPRequest()        // 注册 get_bannedIPList、add_bannedIP、remove_bannedIP 请求
                             //   对应服务端路由:ip/banned/get、ip/banned/add、ip/banned/remove
.AddLogRequest()             // 注册 get_log(→ log/get)、clear_log(→ log/clear)请求
.AddCheckConnectRequest()    // 注册 check_connect 请求

TableRequester(数据表请求器)

TableRequester 专门用于与服务端数据表管理器进行 CRUD 交互:

var tableRequester = new TableRequester
{
    RequestAddress = "http://localhost:3300",
    Session = "your-session-here",
    DeviceInfo = DeviceHelper.GetUniqueHardwareId()
};

// 获取所有订单(无分页)
var result = await tableRequester.Get<Order>();
foreach (var order in result.DataList)
    Console.WriteLine($"ID:{order.Id}\tName:{order.Name}");

// 分页获取(每页 10 条,第 1 页)
var paged = await tableRequester.Get<Order>(pageSize: 10, page: 1);

// 添加数据
bool success = await tableRequester.Add(new Order { Name = "新订单", Description = "描述" });

// 修改数据(通过 Id 匹配)
var order = result.DataList[0];
order.Name = "修改后的名称";
await tableRequester.Change(order);

// 删除数据
await tableRequester.Remove<Order>(order.Id);

表名默认从类型名自动推断:类型名首字母小写即为请求表名(TableNameInRequest),例如 Orderorder
这是 TableRequester 与服务端通信时实际使用的名称,与 AddTable 时指定的 tableShowName(显示名称,如 "订单")不同。

// 默认:Order 类型 → 请求表名自动推断为 "order"(类型名首字母小写)
await tableRequester.Get<Order>();

// 手动指定请求表名(需与服务端 TableNameInRequest 一致,即类型名首字母小写)
// 注意:这里填写的是请求路径中使用的表名,不是 AddTable 时的 tableShowName
await tableRequester.Get<Order>("order", pageSize: 10, page: 1);

数据模型需实现 IIdModel 接口(包含 string Id 与用于乐观并发的 long Version):

public class Order : IIdModel
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public long Version { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
}

增量生成器(Source Generator)

本库提供了两个增量生成器,在编译期自动为服务类和请求类生成路由字典,无需手动维护。

服务端:EntryPoint 生成器

自动为继承 ServerCoreStandardServiceBasepartial 类生成 SyncEntryPointsAsyncEntryPoints 字典。

使用要求:

  • 类必须声明为 partial
  • 方法返回类型必须为 void(同步)或 Task/Task<T>(异步)
  • 方法不能有参数
  • 路由路径不能包含引号(")或反斜杠(\
  • 通配符 * 必须是独立的路径段(不能与其他字符混合)
  • 同一个类中同一个路径只能有一个处理方法

示例:

public partial class ApiService : ServerCoreStandardServiceBase
{
    [EntryPoint("user/profile")]
    [NoLog]
    public async Task GetProfile()
    {
        // ...
    }

    [EntryPoint("user/update")]
    public void UpdateUser()
    {
        // ...
    }

    // 通配符路由:匹配 resource/任意/details
    [EntryPoint("resource/*/details")]
    public async Task ResourceDetails()
    {
        // Route 包含实际匹配的路径
        await Close($"Resource detail for {Route}");
    }
}

生成器自动生成(无需手写):

// ApiService.EntryPoints.g.cs(自动生成,仅展示)
public override Dictionary<string, Action> SyncEntryPoints => new()
{
    { "user/update", UpdateUser },
};

public override Dictionary<string, Func<Task>> AsyncEntryPoints => new()
{
    { "user/profile", GetProfile },
    { "resource/*/details", ResourceDetails },
};

public override HashSet<string> NoLogEntryPoints => new()
{
    "user/profile",
};

客户端:ClientRequest 生成器

自动为继承 StandardRequestServiceBasepartial 类生成 RequestPointsResponsePointsRequestRouteMap 字典。

使用要求:

  • 类必须声明为 partial
  • 方法返回类型必须为 object
  • 方法不能有参数
  • 同一个类中同一路径或名称(在 Request 或 Response 中)不能重复

示例:

public partial class UserRequestService<T> : StandardRequestServiceBase where T : IUserFaceInfo
{
    // Path 为路由路径,Name 为可选的请求别名(调用时可用 Path 或 Name)
    [Request("login", Name = "login")]
    public object LoginRequest() => new
    {
        execute = "login",
        account = Parameters[0],
        password = Parameters[1],
        deviceInfo = DeviceInfo
    };

    [Response("login", Name = "login")]
    public object LoginResponse() => JsonSerializer.Deserialize<UserLoginResult<T>>(UnescapedResponse)!;

    [Request("relogin", Name = "relogin")]
    public object ReloginRequest() => new
    {
        execute = "relogin",
        session = Session,
        deviceInfo = DeviceInfo
    };

    [Response("relogin", Name = "relogin")]
    public object ReloginResponse() => JsonSerializer.Deserialize<T>(UnescapedResponse)!;
}

诊断规则

增量生成器会在编译期检查代码并报告诊断信息:

代码 说明 适用范围
XFE0003 包含 [EntryPoint] 方法的类必须为 partial 服务端
XFE0004 [EntryPoint] 方法不能有参数 服务端
XFE0005 [EntryPoint] 方法返回类型必须为 voidTask 服务端
XFE0006 [EntryPoint] 路径包含无效字符(引号或反斜杠) 服务端
XFE0007 包含 [Request]/[Response] 方法的类必须为 partial 客户端
XFE0008 [Request]/[Response] 方法不能有参数 客户端
XFE0009 [Request]/[Response] 方法返回类型必须为 object 客户端
XFE0010 [Request]/[Response] 路径包含无效字符 客户端
XFE0011 [Request]/[Response] 路径或名称重复注册 客户端
XFE0012 [EntryPoint] 路径在同一类中重复注册 服务端
XFE0013 [EntryPoint] 通配符使用无效(* 必须是完整路径段) 服务端

所有诊断均有对应的在线文档:https://docs.xfegzs.com/View/Errors/ServerInteractive/XFE{代码}


常见错误与解决方案

Q:服务没有收到请求,控制台没有任何输出

  • 确认 XFEServerCoreBuilder.Build(options => { options.BindIP("..."); }) 中绑定了正确的地址
  • 如果设置了 MainEntryPoint,请求路径需要包含该前缀,例如设置 api 后请求 /api/echo

Q:InvalidOperationException: 类型 MyService 的 EntryPointList 为空

  • 确认 MyService 继承了 ServerCoreStandardServiceBase
  • 确认类声明了 partial 关键字
  • 确认方法上有 [EntryPoint("路径")] 特性且路径不为空

Q:XFEServerBuilderException: 未添加核心处理器

  • XFEServerBuilder.Build() 前必须调用 AddCoreProcessor<T>(),或使用 UseXFEServer() 扩展方法

Q:路由匹配到了不期望的服务

  • 框架按"最具体优先"原则:字面量段越多的路由模式优先级越高,与注册顺序无关
  • 精确路由(无通配符)总是优先于通配符路由

Q:[Request]/[Response] 方法的返回类型报错

  • 方法必须声明返回类型为 object(不能是 stringint 等具体类型)

Q:客户端请求总是返回 500

  • 检查 options.RequestAddress 末尾是否有多余的斜杠(框架会自动拼接路由:RequestAddress + "/" + route
  • 确认服务端路由与请求名称完全一致(区分大小写)