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改变任何语义,执行上下文或您编写查询的方式。

大约一年前我遇到了同样的问题,并通过几步统一了代码:

  1. 首先,我在另一个 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 来显示操作信息)。

  2. 我创建了基础 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);
        }
    }
    
  3. 我添加了一个扩展方法来获取当前用户凭据GetCurrentUserId。如果方法参数包含实现 IAuthorizedInput 的 class,其中包含 1 属性 和 USerId,那么我将此信息添加到全局过滤器中。在其他情况下,我需要手动添加。 GetCurrentUserId 取决于您的授权方式。

  4. 这只是一种代码风格,但是我为我的业务逻辑调用了所有输入模型,并带有Input后缀(见上面的示例:DeleteShoppingListEntryInputClearShoppingListEntriesInputSaveShoppingListEntryInput) 和具有输出语法的结果模型(有趣的是,您无需在控制器中声明此类型,因为它是通用 class OutputDataModel<TData> 的一部分)。

  5. 我还使用 AutoMapper 将我的实体映射到 Ouput-classes 而不是大量的 CreateFromEntity 方法。

  6. 我正在使用数据源的抽象。在我的场景中是 Repository 但是这个解决方案当时没有英文文档 更好的方法是使用 more common solutions.

  7. 之一
  8. 我的业务逻辑也有一个基础 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 中的操作。就这些。

  9. 这只是一个与问题无关的建议,但它对我检查路由 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;
    }
}
  1. DB Call 应该在一个单独的层到 web api(原因:关注点分离:你以后可能想改变 DB 技术,你的 web API 可能想从其他来源获取数据)
  2. 使用工厂构建您的 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
            };
        }
    }