You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优雅处理方法返回不同错误与成功数据的场景?

处理方法返回多类型结果的更优方案

Great question! Dealing with methods that need to return different success/error data types is a common pain point, and your two existing approaches do have the drawbacks you mentioned—forced casting feels clunky, and stuffing all possible fields into a single base class violates the single responsibility principle. Let's look at some cleaner alternatives that balance type safety, maintainability, and readability:

1. Use a Discriminated Union (via Libraries or Custom Implementation)

C# doesn't have native discriminated unions, but libraries like OneOf fill this gap perfectly. This approach lets you explicitly define all possible return types, no casting required, and each result type only holds the data it needs.

Example with OneOf:

First, install the OneOf NuGet package. Then structure your code like this:

// Define distinct result types (no shared base class needed)
public record ValidUserResult(IAuthenticableUser User, string Token);
public record UserExistsResult;
public record InvalidPasswordResult;
public record InvalidEmailResult;

// Update your authentication method
public async Task<OneOf<ValidUserResult, UserExistsResult, InvalidPasswordResult, InvalidEmailResult>> AuthenticateAsync(string email, string password)
{
    // Validate email format first
    if (!IsValidEmail(email))
        return new InvalidEmailResult();

    var existingUser = await _dbContext.Users.SingleOrDefaultAsync(x => x.Email == email);
    if (existingUser == null)
    {
        // Create new user logic
        var newUser = CreateUser(email, password);
        await _dbContext.Users.AddAsync(newUser);
        await _dbContext.SaveChangesAsync();
        return new ValidUserResult(newUser, GenerateAuthToken(newUser));
    }

    // Verify password
    if (!PasswordHasher.Verify(password, existingUser.PasswordHash))
        return new InvalidPasswordResult();

    return new ValidUserResult(existingUser, GenerateAuthToken(existingUser));
}

// Handle the result with type-safe matching
var authResult = await _authService.AuthenticateAsync(userEmail, userPassword);
authResult.Match(
    valid => {
        // Use valid.User and valid.Token directly
        Console.WriteLine($"Authenticated as {valid.User.Email}");
    },
    exists => {
        Console.WriteLine("This email is already registered.");
    },
    invalidPass => {
        Console.WriteLine("Incorrect password.");
    },
    invalidEmail => {
        Console.WriteLine("Please enter a valid email address.");
    }
);

Why this works:

  • Type safety: The compiler ensures you handle every possible result type (no missing cases!).
  • Clean separation: Each result class only contains the data relevant to its scenario.
  • Readable: The Match method makes it crystal clear how each outcome is handled.

If you don't want to use a third-party library, you can build a simple custom discriminated union (though it's less scalable for adding new result types later).

2. Generic Result Pattern with Explicit Error Types

If you prefer a no-dependency approach, a generic Result class that separates success data from error details is a solid choice.

Example Implementation:

// Generic result container
public class Result<T>
{
    public bool IsSuccess { get; }
    public T SuccessData { get; }
    public ValidationStatus ErrorStatus { get; }
    public string ErrorMessage { get; }

    // Private constructors enforce using factory methods
    private Result(T data)
    {
        IsSuccess = true;
        SuccessData = data;
        ErrorStatus = default;
        ErrorMessage = null;
    }

    private Result(ValidationStatus errorStatus, string message)
    {
        IsSuccess = false;
        SuccessData = default;
        ErrorStatus = errorStatus;
        ErrorMessage = message;
    }

    public static Result<T> Success(T data) => new Result<T>(data);
    public static Result<T> Failure(ValidationStatus errorStatus, string message) => new Result<T>(errorStatus, message);
}

// DTO for success data
public record AuthSuccessData(IAuthenticableUser User, string Token);

// Updated authentication method
public async Task<Result<AuthSuccessData>> AuthenticateAsync(string email, string password)
{
    if (!IsValidEmail(email))
        return Result<AuthSuccessData>.Failure(ValidationStatus.InvalidEmail, "Invalid email format.");

    var existingUser = await _dbContext.Users.SingleOrDefaultAsync(x => x.Email == email);
    if (existingUser == null)
    {
        var newUser = CreateUser(email, password);
        await _dbContext.Users.AddAsync(newUser);
        await _dbContext.SaveChangesAsync();
        return Result<AuthSuccessData>.Success(new AuthSuccessData(newUser, GenerateAuthToken(newUser)));
    }

    if (!PasswordHasher.Verify(password, existingUser.PasswordHash))
        return Result<AuthSuccessData>.Failure(ValidationStatus.InvalidPassword, "Wrong password.");

    return Result<AuthSuccessData>.Success(new AuthSuccessData(existingUser, GenerateAuthToken(existingUser)));
}

// Handling the result
var authResult = await _authService.AuthenticateAsync(userEmail, userPassword);
if (authResult.IsSuccess)
{
    var user = authResult.SuccessData.User;
    var token = authResult.SuccessData.Token;
    // Proceed with authenticated logic
}
else
{
    switch (authResult.ErrorStatus)
    {
        case ValidationStatus.InvalidEmail:
            // Handle invalid email
            break;
        case ValidationStatus.InvalidPassword:
            // Handle wrong password
            break;
        case ValidationStatus.UserExists:
            // Handle existing user
            break;
    }
}

Why this works:

  • No dependencies: No external libraries required—just a simple generic class.
  • Clear separation: Success data and error details are kept distinct, so you don't have unused fields cluttering up your objects.
  • Easy to extend: Add new error statuses to the ValidationStatus enum without modifying the Result class.

3. Visitor Pattern (For Inheritance-Based Systems)

If you want to stick with a base class inheritance model but avoid forced casting, the Visitor Pattern is a classic object-oriented solution.

Example Implementation:

// Visitor interface defining handlers for each result type
public interface IUserResultVisitor<TOutput>
{
    TOutput Visit(ValidUserResult result);
    TOutput Visit(UserExistsResult result);
    TOutput Visit(InvalidPasswordResult result);
}

// Base result class with Accept method
public abstract class UserServiceResult
{
    public abstract TOutput Accept<TOutput>(IUserResultVisitor<TOutput> visitor);
}

// Concrete result classes
public class ValidUserResult : UserServiceResult
{
    public IAuthenticableUser User { get; }
    public string Token { get; }

    public ValidUserResult(IAuthenticableUser user, string token)
    {
        User = user;
        Token = token;
    }

    public override TOutput Accept<TOutput>(IUserResultVisitor<TOutput> visitor)
    {
        return visitor.Visit(this);
    }
}

public class UserExistsResult : UserServiceResult
{
    public override TOutput Accept<TOutput>(IUserResultVisitor<TOutput> visitor)
    {
        return visitor.Visit(this);
    }
}

// Example visitor to handle authentication results
public class AuthResultHandler : IUserResultVisitor<string>
{
    public string Visit(ValidUserResult result)
    {
        return $"Authenticated: {result.User.Email} (Token: {result.Token})";
    }

    public string Visit(UserExistsResult result)
    {
        return "Error: User already exists.";
    }

    public string Visit(InvalidPasswordResult result)
    {
        return "Error: Incorrect password.";
    }
}

// Your original authentication method (returns base class)
public async Task<UserServiceResult> AuthenticateAsync(string email, string password)
{
    // Same logic as before, returning the appropriate result instance
}

// Handling the result with the visitor
var authResult = await _authService.AuthenticateAsync(userEmail, userPassword);
var handler = new AuthResultHandler();
var resultMessage = authResult.Accept(handler);
Console.WriteLine(resultMessage);

Why this works:

  • No casting: The visitor pattern lets you handle each result type explicitly without downcasting.
  • Open/Closed Principle: Add new result types by creating a new class and updating the visitor interface—no changes to existing result classes needed.

The downside is that it adds more boilerplate code compared to the other options, so it's best suited for larger systems where you need to enforce OOP principles strictly.

Final Recommendations

  • Go with OneOf if you want the most type-safe, clean solution—It's my go-to for these scenarios.
  • Use the generic Result pattern if you want a lightweight, dependency-free approach.
  • Try the Visitor Pattern only if you're committed to an inheritance-based design and need to avoid casting.

内容的提问来源于stack exchange,提问作者Notbad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 10:12:54