C# WebApi 重构 Select Linq
C# WebApi refactoring Select Linq
我目前正在 Visual Studio 2015 年编写 C# Web Api。我实际上复制粘贴了很多代码。
public class APIController : ApiController
{
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = db.drones.Select(d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
});
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = db.drones.Select(d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
}).Where(drones => drones.iddrones == id);
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
我应该如何重构它?起初我想将 var 移动到 class 成员,但这似乎是不允许的。
将您到 DTO 代码的映射放入一个您可以重复使用的方法中,然后您可以执行类似以下操作:
var drone = db.drones.Select(d => DroneDto.FromDb(d))
.Where(drones => drones.iddrones == id);
public class DroneDto
{
public int iddrones {get;set;}
// ...other props
public static DroneDto FromDb(DroneEntity dbEntity)
{
return new DroneDto
{
iddrones = dbEntity.iddrones,
//... other props
}
}
}
首先,尽量避免在 webapi 中直接使用 db,移动到服务。
其次,如果我理解您的问题,您希望避免编写转换。您可以使用 AutoMapper,通过带扩展 AutoMapper.QueryableExtensions 的 nuget 安装,并配置 Drone 和 DroneDto 之间的映射。配置映射器:
Mapper.CreateMap<Drone, Dtos.DroneDTO>();
使用起来很简单:
db.Drones
.Where(d => ... condition ...)
.Project()
.To<DroneDto>()
.ToList();
像 ben 一样,您可以将转换代码放入 DroneDto class 的静态方法中,如下所示:
public class DroneDto
{
public int iddrones {get;set;}
public static DroneDto CreateFromEntity(DroneEntity dbEntity)
{
return new DroneDto
{
iddrones = dbEntity.iddrones,
...
};
}
}
但是,Bens 方法的问题是在 DbSet 上调用了 .Select 方法,而 LINQ to Entities 不处理这些方法。因此,您需要先对 DbSet 进行查询,然后收集结果。例如通过调用 .ToList()。然后就可以进行转换了。
public class APIController : ApiController
{
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = db.drones.ToList().Select(d => DroneDto.CreateFromEntity(d));
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = db.drones.Where(d => d.iddrone == id)
.ToList().Select(d => DroneDto.CreateFromEntity(d));
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
或者,如果您想避免多次枚举结果,请查看 AutoMapper. Specifically the Queryable-Extensions。
我会制作一个适用于 IQueryable<T>
的 DTO 工厂方法,然后这两个函数将只负责创建正确的查询。
这将使您在将来使这些函数异步时处于更好的位置。
public class DroneDTO
{
public int Id { get; set; }
public static IEnumerable<DroneDTO> CreateFromQuery(IQueryable<Drone> query)
{
return query.Select(r=> new DroneDTO
{
Id = r.Id
});
}
}
public class APIController : ApiController
{
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = DroneDTO.CreateFromQuery(db.drones);
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = DroneDTO.CreateFromQuery(db.drones.Where(d => d.iddrone == id));
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
在这种情况下重用 Select
部分(投影)非常容易。
让我们看一下Queryable.Select方法签名
public static IQueryable<TResult> Select<TSource, TResult>(
this IQueryable<TSource> source,
Expression<Func<TSource, TResult>> selector
)
你所说的"selection code"其实就是selector
这个参数。假设你的实体 class 被称为 Drone
,那么根据上面的定义,我们可以将那部分提取为 Expression<Func<Drone, DroneDto>>
并在这两个地方重复使用它
public class APIController : ApiController
{
static Expression<Func<Drone, DroneDto>> ToDto()
{
// The code that was inside Select(...)
return d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
};
}
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = db.drones.Select(ToDto());
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = db.drones.Where(d => d.iddrones == id).Select(ToDto());
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
当然这两个方法可以进一步重构(成为"one liners"),但上面是最小的重构,允许重用Select
部分w/o改变任何语义,执行上下文或您编写查询的方式。
大约一年前我遇到了同样的问题,并通过几步统一了代码:
首先,我在另一个 classes 中将我的业务逻辑与控制器分开。这不是一对一的分离,我为每个实体创建了 class。另一种方法是对每个 query/command 使用 CQRS。一般情况下,我的业务逻辑总是 returns 以下模型之一:
public class OutputModel
{
[JsonIgnore]
public OperationResult Result { get; private set; }
public OutputDataModel(OperationResult result)
{
Result = result;
}
#region Initializatiors
public static OutputModel CreateResult(OperationResult result)
{
return new OutputModel(result);
}
public static OutputModel CreateSuccessResult()
{
return new OutputModel(OperationResult.Success);
}
#endregion Initializatiors
}
public class OutputDataModel<TData> : OutputModel
{
public TData Data { get; private set; }
public OutputDataModel(OperationResult result)
: base(result)
{
}
public OutputDataModel(OperationResult result, TData data)
: this(result)
{
Data = data;
}
#region Initializatiors
public static OutputDataModel<TData> CreateSuccessResult(TData data)
{
return new OutputDataModel<TData>(OperationResult.Success, data);
}
public static OutputDataModel<TData> CreateResult(OperationResult result, TData data)
{
return new OutputDataModel<TData>(result, data);
}
public new static OutputDataModel<TData> CreateResult(OperationResult result)
{
return new OutputDataModel<TData>(result);
}
#endregion Initializatiors
}
操作结果是一个枚举,其中包含平台独立样式的 StatusCode 之类的内容:
public enum OperationResult
{
AccessDenied,
BadRequest,
Conflict,
NotFound,
NotModified,
AccessDenied,
Created,
Success
}
它允许我以相同的方式处理所有网络 api 调用,并且不仅在网络 api 中而且在其他客户端中使用我的业务逻辑(例如,我创建了小型 WPF 应用程序,它使用我的业务逻辑 classes 来显示操作信息)。
我创建了基础 API 控制器来处理 OutputDataModel
来撰写响应:
public class RikropApiControllerBase : ApiController
{
#region Result handling
protected HttpResponseMessage Response(IOutputModel result, HttpStatusCode successStatusCode = HttpStatusCode.OK)
{
switch (result.Result)
{
case OperationResult.AccessDenied:
return Request.CreateResponse(HttpStatusCode.Forbidden);
case OperationResult.BadRequest:
return Request.CreateResponse(HttpStatusCode.BadRequest);
case OperationResult.Conflict:
return Request.CreateResponse(HttpStatusCode.Conflict);
case OperationResult.NotFound:
return Request.CreateResponse(HttpStatusCode.NotFound);
case OperationResult.NotModified:
return Request.CreateResponse(HttpStatusCode.NotModified);
case OperationResult.Created:
return Request.CreateResponse(HttpStatusCode.Created);
case OperationResult.Success:
return Request.CreateResponse(successStatusCode);
default:
return Request.CreateResponse(HttpStatusCode.NotImplemented);
}
}
protected HttpResponseMessage Response<TData>(IOutputDataModel<TData> result, HttpStatusCode successStatusCode = HttpStatusCode.OK)
{
switch (result.Result)
{
case OperationResult.AccessDenied:
return Request.CreateResponse(HttpStatusCode.Forbidden);
case OperationResult.BadRequest:
return Request.CreateResponse(HttpStatusCode.BadRequest);
case OperationResult.Conflict:
return Request.CreateResponse(HttpStatusCode.Conflict);
case OperationResult.NotFound:
return Request.CreateResponse(HttpStatusCode.NotFound);
case OperationResult.NotModified:
return Request.CreateResponse(HttpStatusCode.NotModified, result.Data);
case OperationResult.Created:
return Request.CreateResponse(HttpStatusCode.Created, result.Data);
case OperationResult.Success:
return Request.CreateResponse(successStatusCode, result.Data);
default:
return Request.CreateResponse(HttpStatusCode.NotImplemented);
}
}
#endregion Result handling
}
现在我的api控制器几乎没有代码!看一下控制器非常重的例子:
[RoutePrefix("api/ShoppingList/{shoppingListId:int}/ShoppingListEntry")]
public class ShoppingListEntryController : RikropApiControllerBase
{
private readonly IShoppingListService _shoppingListService;
public ShoppingListEntryController(IShoppingListService shoppingListService)
{
_shoppingListService = shoppingListService;
}
[Route("")]
[HttpPost]
public HttpResponseMessage AddNewEntry(int shoppingListId, SaveShoppingListEntryInput model)
{
model.ShoppingListId = shoppingListId;
var result = _shoppingListService.SaveShoppingListEntry(model);
return Response(result);
}
[Route("")]
[HttpDelete]
public HttpResponseMessage ClearShoppingList(int shoppingListId)
{
var model = new ClearShoppingListEntriesInput {ShoppingListId = shoppingListId, InitiatorId = this.GetCurrentUserId()};
var result = _shoppingListService.ClearShoppingListEntries(model);
return Response(result);
}
[Route("{shoppingListEntryId:int}")]
public HttpResponseMessage Put(int shoppingListId, int shoppingListEntryId, SaveShoppingListEntryInput model)
{
model.ShoppingListId = shoppingListId;
model.ShoppingListEntryId = shoppingListEntryId;
var result = _shoppingListService.SaveShoppingListEntry(model);
return Response(result);
}
[Route("{shoppingListEntry:int}")]
public HttpResponseMessage Delete(int shoppingListId, int shoppingListEntry)
{
var model = new DeleteShoppingListEntryInput
{
ShoppingListId = shoppingListId,
ShoppingListEntryId = shoppingListEntry,
InitiatorId = this.GetCurrentUserId()
};
var result = _shoppingListService.DeleteShoppingListEntry(model);
return Response(result);
}
}
我添加了一个扩展方法来获取当前用户凭据GetCurrentUserId
。如果方法参数包含实现 IAuthorizedInput
的 class,其中包含 1 属性 和 USerId
,那么我将此信息添加到全局过滤器中。在其他情况下,我需要手动添加。 GetCurrentUserId
取决于您的授权方式。
这只是一种代码风格,但是我为我的业务逻辑调用了所有输入模型,并带有Input后缀(见上面的示例:DeleteShoppingListEntryInput
、ClearShoppingListEntriesInput
、SaveShoppingListEntryInput
) 和具有输出语法的结果模型(有趣的是,您无需在控制器中声明此类型,因为它是通用 class OutputDataModel<TData>
的一部分)。
我还使用 AutoMapper 将我的实体映射到 Ouput-classes 而不是大量的 CreateFromEntity
方法。
我正在使用数据源的抽象。在我的场景中是
Repository 但是这个解决方案当时没有英文文档
更好的方法是使用 more common solutions.
之一
我的业务逻辑也有一个基础 class,可以帮助我创建 output-models:
public class ServiceBase
{
#region Output parameters
public IOutputDataModel<TData> SuccessOutput<TData>(TData data)
{
return OutputDataModel<TData>.CreateSuccessResult(data);
}
public IOutputDataModel<TData> Output<TData>(OperationResult result, TData data)
{
return OutputDataModel<TData>.CreateResult(result, data);
}
public IOutputDataModel<TData> Output<TData>(OperationResult result)
{
return OutputDataModel<TData>.CreateResult(result);
}
public IOutputModel SuccessOutput()
{
return OutputModel.CreateSuccessResult();
}
public IOutputModel Output(OperationResult result)
{
return OutputModel.CreateResult(result);
}
#endregion Output parameters
}
最后,我的 "services" 与业务逻辑看起来很相似。让我们看一个例子:
public class ShoppingListService : ServiceBase, IShoppingListService
{
private readonly IRepository<ShoppingList, int> _shoppingListRepository;
private readonly IRepository<ShoppingListEntry, int> _shoppingListEntryRepository;
public ShoppingListService(IRepository<ShoppingList, int> shoppingListRepository,
IRepository<ShoppingListEntry, int> shoppingListEntryRepository)
{
_shoppingListRepository = shoppingListRepository;
_shoppingListEntryRepository = shoppingListEntryRepository;
}
public IOutputDataModel<ListModel<ShoppingListDto>> GetUserShoppingLists(GetUserShoppingListsInput model)
{
var shoppingLists =
_shoppingListRepository.Get(q => q.Filter(sl => sl.OwnerId == model.InitiatorId).Include(sl => sl.Entries));
return SuccessOutput(new ListModel<ShoppingListDto>(Mapper.Map<IEnumerable<ShoppingList>, ShoppingListDto[]>(shoppingLists)));
}
public IOutputDataModel<GetShoppingListOutputData> GetShoppingList(GetShoppingListInput model)
{
var shoppingList =
_shoppingListRepository
.Get(q => q.Filter(sl => sl.Id == model.ShoppingListId).Include(sl => sl.Entries).Take(1))
.SingleOrDefault();
if (shoppingList == null)
return Output<GetShoppingListOutputData>(OperationResult.NotFound);
if (shoppingList.OwnerId != model.InitiatorId)
return Output<GetShoppingListOutputData>(OperationResult.AccessDenied);
return
SuccessOutput(new GetShoppingListOutputData(Mapper.Map<ShoppingListDto>(shoppingList),
Mapper.Map<IEnumerable<ShoppingListEntry>, List<ShoppingListEntryDto>>(shoppingList.Entries)));
}
public IOutputModel DeleteShoppingList(DeleteShoppingListInput model)
{
var shoppingList = _shoppingListRepository.Get(model.ShoppingListId);
if (shoppingList == null)
return Output(OperationResult.NotFound);
if (shoppingList.OwnerId != model.InitiatorId)
return Output(OperationResult.AccessDenied);
_shoppingListRepository.Delete(shoppingList);
return SuccessOutput();
}
public IOutputModel DeleteShoppingListEntry(DeleteShoppingListEntryInput model)
{
var entry =
_shoppingListEntryRepository.Get(
q => q.Filter(e => e.Id == model.ShoppingListEntryId).Include(e => e.ShoppingList).Take(1))
.SingleOrDefault();
if (entry == null)
return Output(OperationResult.NotFound);
if (entry.ShoppingList.OwnerId != model.InitiatorId)
return Output(OperationResult.AccessDenied);
if (entry.ShoppingListId != model.ShoppingListId)
return Output(OperationResult.BadRequest);
_shoppingListEntryRepository.Delete(entry);
return SuccessOutput();
}
public IOutputModel ClearShoppingListEntries(ClearShoppingListEntriesInput model)
{
var shoppingList =
_shoppingListRepository.Get(
q => q.Filter(sl => sl.Id == model.ShoppingListId).Include(sl => sl.Entries).Take(1))
.SingleOrDefault();
if (shoppingList == null)
return Output(OperationResult.NotFound);
if (shoppingList.OwnerId != model.InitiatorId)
return Output(OperationResult.AccessDenied);
if (shoppingList.Entries != null)
_shoppingListEntryRepository.Delete(shoppingList.Entries.ToList());
return SuccessOutput();
}
private IOutputDataModel<int> CreateShoppingList(SaveShoppingListInput model)
{
var shoppingList = new ShoppingList
{
OwnerId = model.InitiatorId,
Title = model.ShoppingListTitle,
Entries = model.Entries.Select(Mapper.Map<ShoppingListEntry>).ForEach(sle => sle.Id = 0).ToList()
};
shoppingList = _shoppingListRepository.Save(shoppingList);
return Output(OperationResult.Created, shoppingList.Id);
}
}
现在所有创建 DTO、响应和其他非业务逻辑操作的例程都在基础 classes 中,我们可以以最简单明了的方式添加功能。对于新的实体创建新的 "service" (存储库将以通用方式自动创建)并从服务基础继承它。对于新操作,将方法添加到现有 "service" 和 API 中的操作。就这些。
这只是一个与问题无关的建议,但它对我检查路由 auto-generated help page. I also used simple client 以执行网络 api 查询非常有用帮助页面。
我的结果:
- Platform-independent & 可测试业务逻辑层;
- 以通用方式将业务逻辑结果映射到基础 class 中的
HttpResponseMessage
;
- Half-automated 安全
ActionFilterAttribute
;
- "Empty" 控制器;
- 可读代码(代码约定和模型层次结构);
我建议您使用 Repository Pattern. Here 您拥有的 - IMO - 一篇关于它的优秀文章。这应该是您可以进行的最简单的重构之一。
按照指定文章中的指南,您可以重构代码,如下所示:
创建基本存储库界面
public interface IRepository<TEntity, in TKey> where TEntity : class
{
TEntity Get(TKey id);
void Save(TEntity entity);
void Delete(TEntity entity);
}
创建专用存储库接口:
public interface IDroneDTORepository : IRepository<DroneDTO, int>
{
IEnumerable<DroneDTO> FindAll();
IEnumerable<DroneDTO> Find(int id);
}
实现专门的存储库接口:
public class DroneDTORepository : IDroneDTORepository
{
private readonly DbContext _dbContext;
public DroneDTORepository(DbContext dbContext)
{
_dbContext = dbContext;
}
public DroneDTO Get(int id)
{
return _dbContext.DroneDTOs.FirstOrDefault(x => x.Id == id);
}
public void Save(DroneDTO entity)
{
_dbContext.DroneDTOs.Attach(entity);
}
public void Delete(DroneDTO entity)
{
_dbContext.DroneDTOs.Remove(entity);
}
public IEnumerable<DroneDTO> FindAll()
{
return _dbContext.DroneDTOs
.Select(d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
})
.ToList();
}
public IEnumerable<DroneDTO> Find(int id)
{
return FindAll().Where(x => x.iddrones == id).ToList();
}
}
在代码中使用存储库:
private IDroneDTORepository _repository = new DroneDTORepository(dbContext);
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = _repository.FindAll();
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = _repository.Find(id);
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
这应该接近结果代码(显然有些地方可能需要更改)。如果有任何不清楚的地方,请告诉我。
使用单独的数据访问层。我假设 GetDrone(int Id) 将检索一架无人机或没有无人机并使用 SingleOrDefault()。您可以根据需要进行调整。
//move all the db access stuff here
public class Db
{
//assuming single drone is returned
public Drone GetDrone(int id)
{
//do SingleOrDefault or Where depending on the needs
Drone drone = GetDrones().SingleOrDefault(drones => drones.iddrones == id);
return drone;
}
public IQueryable<Drone> GetDrones()
{
var drone = db.drones.Select(d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
});
return drone;
}
}
然后来自客户端:
public class APIController : ApiController
{
//this can be injected, service located, etc. simple instance in this eg.
private Db dataAccess = new Db();
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = dataAccess.GetDrones();
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = dataAccess.GetDrone(int id);
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
- DB Call 应该在一个单独的层到 web api(原因:关注点分离:你以后可能想改变 DB 技术,你的 web API 可能想从其他来源获取数据)
使用工厂构建您的 DroneDTO。如果你正在使用依赖注入,你可以将它注入到 web api 控制器中。如果这个工厂很简单(不被其他工厂依赖),你可以让它静态化,但要小心:你不希望有很多静态工厂相互依赖,因为一旦一个需要不再是静态的,您将不得不更改所有这些。
public class APIController : ApiController
{
private readonly IDroneService _droneService;
public APIController(IDroneService droneService)
{
_droneService = droneService;
}
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage GetDrones()
{
var drones = _droneService
.GetDrones()
.Select(DroneDTOFactory.Build);
return Request.CreateResponse(HttpStatusCode.OK, drones);
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage GetDrones(int id)
{
// I am assuming you meant to get a single drone here
var drone = DroneDTOFactory.Build(_droneService.GetDrone(id));
return Request.CreateResponse(HttpStatusCode.OK, drone);
}
}
public static class DroneDTOFactory
{
public static DroneDTO Build(Drone d)
{
if (d == null)
return null;
return new DroneDTO
{
iddrones = d.iddrones,
//more stuff
};
}
}
我目前正在 Visual Studio 2015 年编写 C# Web Api。我实际上复制粘贴了很多代码。
public class APIController : ApiController
{
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = db.drones.Select(d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
});
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = db.drones.Select(d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
}).Where(drones => drones.iddrones == id);
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
我应该如何重构它?起初我想将 var 移动到 class 成员,但这似乎是不允许的。
将您到 DTO 代码的映射放入一个您可以重复使用的方法中,然后您可以执行类似以下操作:
var drone = db.drones.Select(d => DroneDto.FromDb(d))
.Where(drones => drones.iddrones == id);
public class DroneDto
{
public int iddrones {get;set;}
// ...other props
public static DroneDto FromDb(DroneEntity dbEntity)
{
return new DroneDto
{
iddrones = dbEntity.iddrones,
//... other props
}
}
}
首先,尽量避免在 webapi 中直接使用 db,移动到服务。
其次,如果我理解您的问题,您希望避免编写转换。您可以使用 AutoMapper,通过带扩展 AutoMapper.QueryableExtensions 的 nuget 安装,并配置 Drone 和 DroneDto 之间的映射。配置映射器:
Mapper.CreateMap<Drone, Dtos.DroneDTO>();
使用起来很简单:
db.Drones
.Where(d => ... condition ...)
.Project()
.To<DroneDto>()
.ToList();
像 ben 一样,您可以将转换代码放入 DroneDto class 的静态方法中,如下所示:
public class DroneDto
{
public int iddrones {get;set;}
public static DroneDto CreateFromEntity(DroneEntity dbEntity)
{
return new DroneDto
{
iddrones = dbEntity.iddrones,
...
};
}
}
但是,Bens 方法的问题是在 DbSet 上调用了 .Select 方法,而 LINQ to Entities 不处理这些方法。因此,您需要先对 DbSet 进行查询,然后收集结果。例如通过调用 .ToList()。然后就可以进行转换了。
public class APIController : ApiController
{
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = db.drones.ToList().Select(d => DroneDto.CreateFromEntity(d));
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = db.drones.Where(d => d.iddrone == id)
.ToList().Select(d => DroneDto.CreateFromEntity(d));
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
或者,如果您想避免多次枚举结果,请查看 AutoMapper. Specifically the Queryable-Extensions。
我会制作一个适用于 IQueryable<T>
的 DTO 工厂方法,然后这两个函数将只负责创建正确的查询。
这将使您在将来使这些函数异步时处于更好的位置。
public class DroneDTO
{
public int Id { get; set; }
public static IEnumerable<DroneDTO> CreateFromQuery(IQueryable<Drone> query)
{
return query.Select(r=> new DroneDTO
{
Id = r.Id
});
}
}
public class APIController : ApiController
{
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = DroneDTO.CreateFromQuery(db.drones);
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = DroneDTO.CreateFromQuery(db.drones.Where(d => d.iddrone == id));
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
在这种情况下重用 Select
部分(投影)非常容易。
让我们看一下Queryable.Select方法签名
public static IQueryable<TResult> Select<TSource, TResult>(
this IQueryable<TSource> source,
Expression<Func<TSource, TResult>> selector
)
你所说的"selection code"其实就是selector
这个参数。假设你的实体 class 被称为 Drone
,那么根据上面的定义,我们可以将那部分提取为 Expression<Func<Drone, DroneDto>>
并在这两个地方重复使用它
public class APIController : ApiController
{
static Expression<Func<Drone, DroneDto>> ToDto()
{
// The code that was inside Select(...)
return d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
};
}
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = db.drones.Select(ToDto());
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = db.drones.Where(d => d.iddrones == id).Select(ToDto());
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
当然这两个方法可以进一步重构(成为"one liners"),但上面是最小的重构,允许重用Select
部分w/o改变任何语义,执行上下文或您编写查询的方式。
大约一年前我遇到了同样的问题,并通过几步统一了代码:
首先,我在另一个 classes 中将我的业务逻辑与控制器分开。这不是一对一的分离,我为每个实体创建了 class。另一种方法是对每个 query/command 使用 CQRS。一般情况下,我的业务逻辑总是 returns 以下模型之一:
public class OutputModel { [JsonIgnore] public OperationResult Result { get; private set; } public OutputDataModel(OperationResult result) { Result = result; } #region Initializatiors public static OutputModel CreateResult(OperationResult result) { return new OutputModel(result); } public static OutputModel CreateSuccessResult() { return new OutputModel(OperationResult.Success); } #endregion Initializatiors } public class OutputDataModel<TData> : OutputModel { public TData Data { get; private set; } public OutputDataModel(OperationResult result) : base(result) { } public OutputDataModel(OperationResult result, TData data) : this(result) { Data = data; } #region Initializatiors public static OutputDataModel<TData> CreateSuccessResult(TData data) { return new OutputDataModel<TData>(OperationResult.Success, data); } public static OutputDataModel<TData> CreateResult(OperationResult result, TData data) { return new OutputDataModel<TData>(result, data); } public new static OutputDataModel<TData> CreateResult(OperationResult result) { return new OutputDataModel<TData>(result); } #endregion Initializatiors }
操作结果是一个枚举,其中包含平台独立样式的 StatusCode 之类的内容:
public enum OperationResult { AccessDenied, BadRequest, Conflict, NotFound, NotModified, AccessDenied, Created, Success }
它允许我以相同的方式处理所有网络 api 调用,并且不仅在网络 api 中而且在其他客户端中使用我的业务逻辑(例如,我创建了小型 WPF 应用程序,它使用我的业务逻辑 classes 来显示操作信息)。
我创建了基础 API 控制器来处理
OutputDataModel
来撰写响应:public class RikropApiControllerBase : ApiController { #region Result handling protected HttpResponseMessage Response(IOutputModel result, HttpStatusCode successStatusCode = HttpStatusCode.OK) { switch (result.Result) { case OperationResult.AccessDenied: return Request.CreateResponse(HttpStatusCode.Forbidden); case OperationResult.BadRequest: return Request.CreateResponse(HttpStatusCode.BadRequest); case OperationResult.Conflict: return Request.CreateResponse(HttpStatusCode.Conflict); case OperationResult.NotFound: return Request.CreateResponse(HttpStatusCode.NotFound); case OperationResult.NotModified: return Request.CreateResponse(HttpStatusCode.NotModified); case OperationResult.Created: return Request.CreateResponse(HttpStatusCode.Created); case OperationResult.Success: return Request.CreateResponse(successStatusCode); default: return Request.CreateResponse(HttpStatusCode.NotImplemented); } } protected HttpResponseMessage Response<TData>(IOutputDataModel<TData> result, HttpStatusCode successStatusCode = HttpStatusCode.OK) { switch (result.Result) { case OperationResult.AccessDenied: return Request.CreateResponse(HttpStatusCode.Forbidden); case OperationResult.BadRequest: return Request.CreateResponse(HttpStatusCode.BadRequest); case OperationResult.Conflict: return Request.CreateResponse(HttpStatusCode.Conflict); case OperationResult.NotFound: return Request.CreateResponse(HttpStatusCode.NotFound); case OperationResult.NotModified: return Request.CreateResponse(HttpStatusCode.NotModified, result.Data); case OperationResult.Created: return Request.CreateResponse(HttpStatusCode.Created, result.Data); case OperationResult.Success: return Request.CreateResponse(successStatusCode, result.Data); default: return Request.CreateResponse(HttpStatusCode.NotImplemented); } } #endregion Result handling }
现在我的api控制器几乎没有代码!看一下控制器非常重的例子:
[RoutePrefix("api/ShoppingList/{shoppingListId:int}/ShoppingListEntry")] public class ShoppingListEntryController : RikropApiControllerBase { private readonly IShoppingListService _shoppingListService; public ShoppingListEntryController(IShoppingListService shoppingListService) { _shoppingListService = shoppingListService; } [Route("")] [HttpPost] public HttpResponseMessage AddNewEntry(int shoppingListId, SaveShoppingListEntryInput model) { model.ShoppingListId = shoppingListId; var result = _shoppingListService.SaveShoppingListEntry(model); return Response(result); } [Route("")] [HttpDelete] public HttpResponseMessage ClearShoppingList(int shoppingListId) { var model = new ClearShoppingListEntriesInput {ShoppingListId = shoppingListId, InitiatorId = this.GetCurrentUserId()}; var result = _shoppingListService.ClearShoppingListEntries(model); return Response(result); } [Route("{shoppingListEntryId:int}")] public HttpResponseMessage Put(int shoppingListId, int shoppingListEntryId, SaveShoppingListEntryInput model) { model.ShoppingListId = shoppingListId; model.ShoppingListEntryId = shoppingListEntryId; var result = _shoppingListService.SaveShoppingListEntry(model); return Response(result); } [Route("{shoppingListEntry:int}")] public HttpResponseMessage Delete(int shoppingListId, int shoppingListEntry) { var model = new DeleteShoppingListEntryInput { ShoppingListId = shoppingListId, ShoppingListEntryId = shoppingListEntry, InitiatorId = this.GetCurrentUserId() }; var result = _shoppingListService.DeleteShoppingListEntry(model); return Response(result); } }
我添加了一个扩展方法来获取当前用户凭据
GetCurrentUserId
。如果方法参数包含实现IAuthorizedInput
的 class,其中包含 1 属性 和USerId
,那么我将此信息添加到全局过滤器中。在其他情况下,我需要手动添加。GetCurrentUserId
取决于您的授权方式。这只是一种代码风格,但是我为我的业务逻辑调用了所有输入模型,并带有Input后缀(见上面的示例:
DeleteShoppingListEntryInput
、ClearShoppingListEntriesInput
、SaveShoppingListEntryInput
) 和具有输出语法的结果模型(有趣的是,您无需在控制器中声明此类型,因为它是通用 classOutputDataModel<TData>
的一部分)。我还使用 AutoMapper 将我的实体映射到 Ouput-classes 而不是大量的
CreateFromEntity
方法。我正在使用数据源的抽象。在我的场景中是 Repository 但是这个解决方案当时没有英文文档 更好的方法是使用 more common solutions.
之一
我的业务逻辑也有一个基础 class,可以帮助我创建 output-models:
public class ServiceBase { #region Output parameters public IOutputDataModel<TData> SuccessOutput<TData>(TData data) { return OutputDataModel<TData>.CreateSuccessResult(data); } public IOutputDataModel<TData> Output<TData>(OperationResult result, TData data) { return OutputDataModel<TData>.CreateResult(result, data); } public IOutputDataModel<TData> Output<TData>(OperationResult result) { return OutputDataModel<TData>.CreateResult(result); } public IOutputModel SuccessOutput() { return OutputModel.CreateSuccessResult(); } public IOutputModel Output(OperationResult result) { return OutputModel.CreateResult(result); } #endregion Output parameters }
最后,我的 "services" 与业务逻辑看起来很相似。让我们看一个例子:
public class ShoppingListService : ServiceBase, IShoppingListService { private readonly IRepository<ShoppingList, int> _shoppingListRepository; private readonly IRepository<ShoppingListEntry, int> _shoppingListEntryRepository; public ShoppingListService(IRepository<ShoppingList, int> shoppingListRepository, IRepository<ShoppingListEntry, int> shoppingListEntryRepository) { _shoppingListRepository = shoppingListRepository; _shoppingListEntryRepository = shoppingListEntryRepository; } public IOutputDataModel<ListModel<ShoppingListDto>> GetUserShoppingLists(GetUserShoppingListsInput model) { var shoppingLists = _shoppingListRepository.Get(q => q.Filter(sl => sl.OwnerId == model.InitiatorId).Include(sl => sl.Entries)); return SuccessOutput(new ListModel<ShoppingListDto>(Mapper.Map<IEnumerable<ShoppingList>, ShoppingListDto[]>(shoppingLists))); } public IOutputDataModel<GetShoppingListOutputData> GetShoppingList(GetShoppingListInput model) { var shoppingList = _shoppingListRepository .Get(q => q.Filter(sl => sl.Id == model.ShoppingListId).Include(sl => sl.Entries).Take(1)) .SingleOrDefault(); if (shoppingList == null) return Output<GetShoppingListOutputData>(OperationResult.NotFound); if (shoppingList.OwnerId != model.InitiatorId) return Output<GetShoppingListOutputData>(OperationResult.AccessDenied); return SuccessOutput(new GetShoppingListOutputData(Mapper.Map<ShoppingListDto>(shoppingList), Mapper.Map<IEnumerable<ShoppingListEntry>, List<ShoppingListEntryDto>>(shoppingList.Entries))); } public IOutputModel DeleteShoppingList(DeleteShoppingListInput model) { var shoppingList = _shoppingListRepository.Get(model.ShoppingListId); if (shoppingList == null) return Output(OperationResult.NotFound); if (shoppingList.OwnerId != model.InitiatorId) return Output(OperationResult.AccessDenied); _shoppingListRepository.Delete(shoppingList); return SuccessOutput(); } public IOutputModel DeleteShoppingListEntry(DeleteShoppingListEntryInput model) { var entry = _shoppingListEntryRepository.Get( q => q.Filter(e => e.Id == model.ShoppingListEntryId).Include(e => e.ShoppingList).Take(1)) .SingleOrDefault(); if (entry == null) return Output(OperationResult.NotFound); if (entry.ShoppingList.OwnerId != model.InitiatorId) return Output(OperationResult.AccessDenied); if (entry.ShoppingListId != model.ShoppingListId) return Output(OperationResult.BadRequest); _shoppingListEntryRepository.Delete(entry); return SuccessOutput(); } public IOutputModel ClearShoppingListEntries(ClearShoppingListEntriesInput model) { var shoppingList = _shoppingListRepository.Get( q => q.Filter(sl => sl.Id == model.ShoppingListId).Include(sl => sl.Entries).Take(1)) .SingleOrDefault(); if (shoppingList == null) return Output(OperationResult.NotFound); if (shoppingList.OwnerId != model.InitiatorId) return Output(OperationResult.AccessDenied); if (shoppingList.Entries != null) _shoppingListEntryRepository.Delete(shoppingList.Entries.ToList()); return SuccessOutput(); } private IOutputDataModel<int> CreateShoppingList(SaveShoppingListInput model) { var shoppingList = new ShoppingList { OwnerId = model.InitiatorId, Title = model.ShoppingListTitle, Entries = model.Entries.Select(Mapper.Map<ShoppingListEntry>).ForEach(sle => sle.Id = 0).ToList() }; shoppingList = _shoppingListRepository.Save(shoppingList); return Output(OperationResult.Created, shoppingList.Id); } }
现在所有创建 DTO、响应和其他非业务逻辑操作的例程都在基础 classes 中,我们可以以最简单明了的方式添加功能。对于新的实体创建新的 "service" (存储库将以通用方式自动创建)并从服务基础继承它。对于新操作,将方法添加到现有 "service" 和 API 中的操作。就这些。
这只是一个与问题无关的建议,但它对我检查路由 auto-generated help page. I also used simple client 以执行网络 api 查询非常有用帮助页面。
我的结果:
- Platform-independent & 可测试业务逻辑层;
- 以通用方式将业务逻辑结果映射到基础 class 中的
HttpResponseMessage
; - Half-automated 安全
ActionFilterAttribute
; - "Empty" 控制器;
- 可读代码(代码约定和模型层次结构);
我建议您使用 Repository Pattern. Here 您拥有的 - IMO - 一篇关于它的优秀文章。这应该是您可以进行的最简单的重构之一。
按照指定文章中的指南,您可以重构代码,如下所示:
创建基本存储库界面
public interface IRepository<TEntity, in TKey> where TEntity : class { TEntity Get(TKey id); void Save(TEntity entity); void Delete(TEntity entity); }
创建专用存储库接口:
public interface IDroneDTORepository : IRepository<DroneDTO, int> { IEnumerable<DroneDTO> FindAll(); IEnumerable<DroneDTO> Find(int id); }
实现专门的存储库接口:
public class DroneDTORepository : IDroneDTORepository { private readonly DbContext _dbContext; public DroneDTORepository(DbContext dbContext) { _dbContext = dbContext; } public DroneDTO Get(int id) { return _dbContext.DroneDTOs.FirstOrDefault(x => x.Id == id); } public void Save(DroneDTO entity) { _dbContext.DroneDTOs.Attach(entity); } public void Delete(DroneDTO entity) { _dbContext.DroneDTOs.Remove(entity); } public IEnumerable<DroneDTO> FindAll() { return _dbContext.DroneDTOs .Select(d => new DroneDTO { iddrones = d.iddrones, //more stuff }) .ToList(); } public IEnumerable<DroneDTO> Find(int id) { return FindAll().Where(x => x.iddrones == id).ToList(); } }
在代码中使用存储库:
private IDroneDTORepository _repository = new DroneDTORepository(dbContext); [HttpGet] [Route("api/drones")] public HttpResponseMessage getDrones() { var drones = _repository.FindAll(); HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones); return res; } [HttpGet] [Route("api/drones/{id}")] public HttpResponseMessage getDrones(int id) { var drone = _repository.Find(id); HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone); return res; }
这应该接近结果代码(显然有些地方可能需要更改)。如果有任何不清楚的地方,请告诉我。
使用单独的数据访问层。我假设 GetDrone(int Id) 将检索一架无人机或没有无人机并使用 SingleOrDefault()。您可以根据需要进行调整。
//move all the db access stuff here
public class Db
{
//assuming single drone is returned
public Drone GetDrone(int id)
{
//do SingleOrDefault or Where depending on the needs
Drone drone = GetDrones().SingleOrDefault(drones => drones.iddrones == id);
return drone;
}
public IQueryable<Drone> GetDrones()
{
var drone = db.drones.Select(d => new DroneDTO
{
iddrones = d.iddrones,
//more stuff
});
return drone;
}
}
然后来自客户端:
public class APIController : ApiController
{
//this can be injected, service located, etc. simple instance in this eg.
private Db dataAccess = new Db();
[HttpGet]
[Route("api/drones")]
public HttpResponseMessage getDrones()
{
var drones = dataAccess.GetDrones();
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drones);
return res;
}
[HttpGet]
[Route("api/drones/{id}")]
public HttpResponseMessage getDrones(int id)
{
var drone = dataAccess.GetDrone(int id);
HttpResponseMessage res = Request.CreateResponse(HttpStatusCode.OK, drone);
return res;
}
}
- DB Call 应该在一个单独的层到 web api(原因:关注点分离:你以后可能想改变 DB 技术,你的 web API 可能想从其他来源获取数据)
使用工厂构建您的 DroneDTO。如果你正在使用依赖注入,你可以将它注入到 web api 控制器中。如果这个工厂很简单(不被其他工厂依赖),你可以让它静态化,但要小心:你不希望有很多静态工厂相互依赖,因为一旦一个需要不再是静态的,您将不得不更改所有这些。
public class APIController : ApiController { private readonly IDroneService _droneService; public APIController(IDroneService droneService) { _droneService = droneService; } [HttpGet] [Route("api/drones")] public HttpResponseMessage GetDrones() { var drones = _droneService .GetDrones() .Select(DroneDTOFactory.Build); return Request.CreateResponse(HttpStatusCode.OK, drones); } [HttpGet] [Route("api/drones/{id}")] public HttpResponseMessage GetDrones(int id) { // I am assuming you meant to get a single drone here var drone = DroneDTOFactory.Build(_droneService.GetDrone(id)); return Request.CreateResponse(HttpStatusCode.OK, drone); } } public static class DroneDTOFactory { public static DroneDTO Build(Drone d) { if (d == null) return null; return new DroneDTO { iddrones = d.iddrones, //more stuff }; } }