I have a class that I use every time I need to return something in my APIs.
public class OperResult<T>
{
public bool Ok { get; private set; }
public T Data { get; private set; }
// Failed result (Ok: false)
public string ErrorCode { get; private set; }
public string ErrorMessage { get; private set; }
public static OperResult<T> Success(T data)
{
return new OperResult<T>(data);
}
public static OperResult<T> Error(string message, string code = null)
{
return new OperResult<T>(message, code);
}
private OperResult(T data = default(T))
{
Data = data;
Ok = true;
}
private OperResult(string message, string code = null)
{
ErrorCode = code;
ErrorMessage = message;
Ok = false;
}
}
And one that doesn't have generics
public class OperResult
{
public string ErrorCode { get; private set; }
public string ErrorMessage { get; private set; }
public object Data { get; private set; }
public bool Ok { get; private set; }
public static OperResult Success(object data = null)
{
return new OperResult(data);
}
public static OperResult Error(string message, string code = null)
{
return new OperResult(message, code);
}
private OperResult(object data)
{
Data = data;
Ok = true;
}
private OperResult(string message, string code = null)
{
ErrorCode = code;
ErrorMessage = message;
Ok = false;
}
}
In every API response, I return these objects so I know the data and the status of the operation.
public async Task<OperResult<IEnumerable<string>>> GetLocationNames()
{
var locations = await locationService.GetAll(AuthState);
if (location != null)
return OperResult<IEnumerable<string>>.Success(locations.Select(u => u.name));
else
return OperResult<IEnumerable<string>>.Error("No location data found");
}
I would like to be able not to write the Generics part every time I use it. Maybe something like:
public async Task<OperResult<IEnumerable<string>>> GetLocationNames()
{
var locations = await locationService.GetAll(AuthState);
if (location != null)
return OperResult.Success(locations.Select(u => u.name)); // infer <IEnumerable<string>>
else
return OperResult.Error("No location data found"); // here too
}
Is this possible?
CodePudding user response:
First, I noticed that the non-generic OperResult
can inherit from OperResult<object>
. I recommend doing that to avoid code duplication.
The below is inspired by Simulating Return Type Inference in C#.
Create a DelayedResult<T>
as a wrapper for some value. This will be useful later on:
public readonly struct DelayedResult<T>
{
public T Value { get; }
public DelayedResult(T value)
{
Value = value;
}
}
In the non-generic OperResult
, create an Error
and a generic Success
method. These are what clients will use to create OperResult<T>
and OperResult
s. Note that these will return the DelayedResult
declared earlier:
public static DelayedResult<T> Success<T>(T ok) =>
new DelayedResult<T>(ok);
public static DelayedResult<Error> Error(string error, string code = null) =>
new DelayedResult<Error>(new Error(error, code));
where Error
is just a simple type containing the error and code. It could be as simple as a record:
record Error(string Message, string Code);
In OperResult<T>
, create implicit conversion operators that converts from DelayedResult
to actual OperResult<T>
s:
public static implicit operator OperResult<T>(DelayedResult<T> ok) =>
new OperResult<T>(ok.Value);
public static implicit operator OperResult<T>(DelayedResult<Error> error) =>
new OperResult<T>(error.Value.Message, error.Value.Code);
Note that you should never have OperResult<Error>
(doesn’t make sense anyway), otherwise the above two overloads will be ambiguous.
In the non-generic OperResult
, you can also have:
public static implicit operator OperResult(DelayedResult<object> ok) =>
new OperResult(ok.Value);
public static implicit operator OperResult(DelayedResult<Error> error) =>
new OperResult(error.Value.Msg, error.Value.Code);
Now you can do
public async Task<OperResult<IEnumerable<string>>> GetLocationNames()
{
var locations = await locationService.GetAll(AuthState);
if (location != null)
return OperResult.Success(locations.Select(u => u.name));
else
return OperResult.Error("No location data found");
}
Full code:
public record Error(string Message, string Code);
public readonly struct DelayedResult<T>
{
public T Value { get; }
public DelayedResult(T value)
{
Value = value;
}
}
public class OperResult: OperResult<object>
{
private OperResult(object data) : base(data) {}
private OperResult(string message, string code = null): base(message, code) {}
public static implicit operator OperResult(DelayedResult<object> ok) =>
new OperResult(ok.Value);
public static implicit operator OperResult(DelayedResult<Error> error) =>
new OperResult(error.Value.Message, error.Value.Code);
public static DelayedResult<T> Success<T>(T ok) =>
new DelayedResult<T>(ok);
public static DelayedResult<Error> Error(string message, string code = null) =>
new DelayedResult<Error>(new Error(message, code));
}
public class OperResult<T>
{
public bool Ok { get; private set; }
public T Data { get; private set; }
public string ErrorCode { get; private set; }
public string ErrorMessage { get; private set; }
public static implicit operator OperResult<T>(DelayedResult<T> ok) =>
new OperResult<T>(ok.Value);
public static implicit operator OperResult<T>(DelayedResult<Error> error) =>
new OperResult<T>(error.Value.Message, error.Value.Code);
protected OperResult(T data = default(T))
{
Data = data;
Ok = true;
}
protected OperResult(string message, string code = null)
{
ErrorCode = code;
ErrorMessage = message;
Ok = false;
}
}
CodePudding user response:
Calling static from OperResult<T>
will always require you to specify the generic part. However, if its a method that returns OperResult<T>
from different class, it may infer the generic via method parameters (assuming OperResult<T>
constructor is public):
public static class OperHelper
{
public static OperResult<T> Success<T>(T data)
{
return new OperResult<T>(data);
}
}
The method above can be called by OperHelper.Create(myPayload)
. However, it relies on the fact that myPayload
can be used to infer what type is T
. This will allow you to handle the "success" part of the OperResult
.
The proposed solution below involves breaking down the OperResult<T>
into several class. It assumes the following:
- "Error" always have the
bool Ok
property asfalse
and vice versa - "Error" doesnt need to bring payload/data
First, you have class OperResult
, it only holds the contract for bool Ok
:
public abstract class OperResult
{
public abstract bool Ok { get; }
}
Then, we extend the OperResult
into two - OperSuccess
and OperError
:
public class OperSuccess<T> : OperResult
{
public override bool Ok { get { return true;} }
public T Data { get; private set; }
public OperSuccess(T data)
{
this.Data = data;
}
}
public class OperError : OperResult
{
public override bool Ok { get { return false;} }
public string ErrorCode { get; private set; }
public string ErrorMessage { get; private set; }
public OperError(string message, string code = null)
{
this.ErrorCode = code;
this.ErrorMessage = message;
}
}
This way, we can expand our OperHelper
into accomodating the "error" part.
public static class OperHelper
{
public static OperSuccess<T> Success<T>(T data)
{
return new OperSuccess<T>(data);
}
public static OperError Error(string message, string code = null)
{
return new OperError(message, code);
}
}
This way, it allow us to call either OperHelper.Success(myObject)
or OperHelper.Error("my error message")
.
Note:
- Yes, none stops you from creating either
OperSuccess
andOperError
manually - Yes, it works on .Net 4.7