Мы разрабатываем внутренний пакет NuGet, который содержит классы ServiceRes
и ServiceRes<T>
, которые можно использовать для переноса ответа уровня сервиса в общую структуру. Это код, который у нас есть на данный момент:
public class ServiceRes
{
protected internal ServiceRes() { }
public bool IsSuccess { get; set; }
public string? Message { get; set; }
public static ServiceRes Success()
{
return Success(message: null);
}
public static ServiceRes Success(string? message)
{
return new ServiceRes
{
IsSuccess = true,
Message = message
};
}
public static ServiceRes MapFrom<TPrev>(ServiceRes<TPrev> othr)
where TPrev : class
{
return new ServiceRes
{
IsSuccess = othr.IsSuccess,
Message = othr.Message
};
}
}
public class ServiceRes<T> : ServiceRes where T : class
{
protected internal ServiceRes() { }
public T? ResponseObj { get; set; }
public static ServiceRes<T> Success(T responseObj)
{
return new ServiceRes<T>
{
IsSuccess = true,
Message = null,
ResponseObj = responseObj
};
}
public static new ServiceRes<T> MapFrom<TPrev>(ServiceRes<TPrev> othr)
where TPrev : class
{
return MapFrom(othr, null);
}
public static ServiceRes<T> MapFrom<TPrev>(ServiceRes<TPrev> othr, Func<TPrev?, T?>? responseObjMapper)
where TPrev : class
{
return new ServiceRes<T>
{
IsSuccess = othr.IsSuccess,
Message = othr.Message,
ResponseObj = responseObjMapper?.Invoke(othr.ResponseObj)
};
}
}
У нас есть проект веб-приложения .NET Core, который ссылается на наш частный пакет NuGet. Мы можем использовать его следующим образом:
public class ServiceResTest
{
private class Obj1
{
public string? Name { get; set; }
}
private class Obj2
{
public string? Value { get; set; }
}
public void Test()
{
// base usage
var x1 = ServiceRes.Success();
var x2 = ServiceRes.Success(message: "User created successfully");
var x3 = ServiceRes<Obj1>.Success(new Obj1());
// mapping
var x4 = ServiceRes.MapFrom(x3); // ServiceResult<Obj1> -> ServiceResult
var x5 = ServiceRes<Obj2>.MapFrom(x3); // ServiceResult<Obj1> -> ServiceResult<Obj2> (ResponseObj = null)
var x6 = ServiceRes<Obj2>.MapFrom(x3, x => new Obj2() // ServiceResult<Obj1> -> ServiceResult<Obj2>
{
Value = $"{x?.Name}_value"
});
}
}
Однако в нашем веб-приложении есть некоторые расширенные варианты использования, которые требуют расширения класса BaseService<T>
. Мы хотели бы иметь возможность сопоставлять ServiceResult<T>
с ServiceResultExt<T>
, желательно аналогично существующим методам MapFrom
. Комментарии ниже указывают, где процесс завершается неудачей.
public class ServiceResExt<T> : ServiceRes<T> where T : class
{
public int FailedLoginsCount { get; set; }
public static ServiceResExt<T> SuccessExt(T responseObj, int failedLoginsCount)
{
var baseRes = ServiceRes<T>.Success(responseObj);
// map ServiceRes<T> to ServiceResExt<T>
var x1 = ServiceResExt<T>.MapFrom(baseRes); // x1 is of type ServiceRes<T>
// we would like to assing parameter failedLoginsCount to property FailedLoginsCount
x1.FailedLoginsCount = failedLoginsCount;
return x1;
}
}
Как лучше всего обрабатывать случаи, когда у нас есть класс, расширяющий ServiceRes<T>
(например, ServiceResExt<T>
), и мы хотели бы использовать существующие методы (например, Success
), но сопоставить ответ с ServiceResExt<T>
? Может ли логика отображения производных классов быть размещена внутри класса ServiceRes<T>
, чтобы она была общей для всех классов, которые наследуют от него (мы не хотим, чтобы кто-либо вручную создавал экземпляр класса ServiceRes
, следовательно, конструктор protected internal
)? Может ли ServiceRes<T>
вернуть экземпляр производного класса?
Вы можете использовать защищенный виртуальный метод для создания правильного типа объекта при сопоставлении.
Я бы написал это примерно так:
public class ServiceRes
{
protected ServiceRes(bool isSuccess, string message)
=> ( IsSuccess, Message ) = ( isSuccess, message );
public static ServiceRes Success => new (true, "");
public static ServiceRes Fail(string message) => new(false, message);
public bool IsSuccess { get; }
public string Message { get; }
}
public class ServiceRes<T> : ServiceRes
where T : class
{
private readonly T responseObj;
protected ServiceRes(T response, bool isSuccess, string message) : base(isSuccess, message)
=> responseObj = response;
protected virtual ServiceRes<T2> Create<T2>(T2 v) where T2 : class => new(v, IsSuccess, Message);
public T Response => IsSuccess ? responseObj : throw new InvalidOperationException();
public new static ServiceRes<T> Success(T response) => new(response, true, "");
public new static ServiceRes<T> Fail(string message) => new(null, false,message);
public ServiceRes<T2> MapFrom<T2>(Func<T, T2> select) where T2 : class
=> IsSuccess ? Create(select(Response)) : Create<T2>(null);
}
public class ServiceResExt<T> : ServiceRes<T>
where T : class
{
protected ServiceResExt(T value, bool isSuccess, string message, int failedLoginsCount) : base(value, isSuccess, message)
=> FailedLoginsCount = failedLoginsCount;
protected virtual ServiceResExt<T2> CreateExt<T2>(T2 v) where T2 : class
=> new(v, IsSuccess, Message, FailedLoginsCount);
protected override ServiceRes<T2> Create<T2>(T2 v) => CreateExt(v);
public int FailedLoginsCount { get; }
public new static ServiceResExt<T> Success(T response, int failedLoginsCount) => new(response, true, "", failedLoginsCount);
public new static ServiceResExt<T> Fail(string message, int failedLoginsCount) => new(null, false, message, failedLoginsCount);
public new ServiceResExt<T2> MapFrom<T2>(Func<T, T2> select) where T2 : class
=> IsSuccess ? CreateExt(select(Response)) : CreateExt<T2>(null);
}
Нет смысла отображать базовый класс ServiceRes
, поэтому в нем отсутствуют такие методы.
И я не уверен, что это достигнет вашей цели — наличие логики отображения в базе ServiceRes<T>
. Но в этом не должно быть такой уж логики, и вы можете заменить ServiceResExt.MapFrom
на (ServiceResExt<T2>)base.MapFrom(select)
, даже если это будет несколько менее безопасно, если будут добавлены дополнительные расширения.
Но он должен сохранять один и тот же внешний тип до и после отображения, независимо от типа ссылки. А также вернуть тот же внешний тип ссылки из-за сокрытия метода MapFrom
.
Возможно, вам также придется добавить немного ?
, я написал это без ссылочных типов, допускающих значение NULL. Я бы порекомендовал сделать эти классы неизменяемыми, поскольку публичные сеттеры кажутся странными и опасными.