简体   繁体   中英

C# WebApi refactoring Select Linq

I'm currently writing a C# Web Api in Visual Studio 2015. I'm actually copy pasting quite a lot of code.

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;
    }
}

How should I refactor that? At first I thought about moving the var to a class member, but that doesn't seem to be allowed.

I would make a DTO factory method that worked on IQueryable<T> and then the two functions would only be responsible for creating the proper query.

This will position you better in the future when you make these functions async.

    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;
        }
    }

I had the same problem about one year ago and unified the code by few steps:

  1. At First, I separated my business logic from the controller in another classes. It's not one to one separation, I created class for each Entity. The other way is to using CQRS for each query/command. The general case that my business logic always returns one of this models:

     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 } 

    Operation result is an Enumeration that contains something like StatusCode in a platform independent style:

     public enum OperationResult { AccessDenied, BadRequest, Conflict, NotFound, NotModified, AccessDenied, Created, Success } 

    It allowed me to handle all web api calls on the same manner and uses my business logic not only in web api but in other clients (for example, I created small WPF app which uses my business logic classes to display operational information).

  2. I created base API controller that handle OutputDataModel to compose response:

     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 } 

    Now my api controllers almost did not contain the code! Look at the example with really heavy controller:

     [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. I added an extension method to get current user credentials GetCurrentUserId . If method parameters contains a class that implements IAuthorizedInput that contains 1 property with USerId then I added this info in a global filter. In other cases I need to add this manually. GetCurrentUserId is depend on your authorization method.

  4. It's just a code style, but I called all input models for my business logic with Input suffix (see examples above: DeleteShoppingListEntryInput , ClearShoppingListEntriesInput , SaveShoppingListEntryInput ) and result models with output syntax (it's interesting that you no need to declare this types in controller because it's a part of generic class OutputDataModel<TData> ).

  5. I'm also used AutoMapper to map my entities to Ouput-classes instead of tons of CreateFromEntity methods.

  6. I'm using an abstraction for data source. In my scenario it was Repository but this solution has no English documentation then the better way is to use one of more common solutions .

  7. I also had a base class for my business logic that helps me to create 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 } 

    Finally my "services" with business logic looks like similar to each other. Lets look an example:

     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); } } 

    Now all routine of creating DTOs, responses and other nonBusinessLogic actions are in the base classes and we can add features in a easiest and clear way. For new Entity creates new "service" (repository will be created automatically in a generic manner) and inherit it from service base. For new action add a method to existing "service" and actions in API. That is all.

  8. It's just a recommendation that not related with question but it is very useful for me to check routings with auto-generated help page . I also used simple client to execute web api queries from the help page.

My results:

  • Platform-independent & testable business logic layer;
  • Map business logic result to HttpResponseMessage in base class in a generic manner;
  • Half-automated security with ActionFilterAttribute ;
  • "Empty" controllers;
  • Readable code (code conventions and model hierarchy);

Reusing the Select part (projection) in such scenarios is quite easy.

Let take a look at Queryable.Select method signature

public static IQueryable<TResult> Select<TSource, TResult>(
    this IQueryable<TSource> source,
    Expression<Func<TSource, TResult>> selector
)

What you call "selection code" is actually the selector parameter. Assuming your entity class is called Drone , then according to the above definition we can extract that part as Expression<Func<Drone, DroneDto>> and reuse it in both places like this

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;
    }
}

Of course these two methods can further be refactored (to become "one liners"), but the above is the minimal refactoring that allows reusing the Select part w/o changing any semantics, executing context or the way you write your queries.

I'd suggest to go with the Repository Pattern . Here you have - IMO - an excellent article about it. This should be one of the easiest refactoring you can do.

Following the guidelines from the indicated article you could refactor the code like the following:

  • Create the base repository interface

     public interface IRepository<TEntity, in TKey> where TEntity : class { TEntity Get(TKey id); void Save(TEntity entity); void Delete(TEntity entity); } 
  • Create the specialized repository interface:

     public interface IDroneDTORepository : IRepository<DroneDTO, int> { IEnumerable<DroneDTO> FindAll(); IEnumerable<DroneDTO> Find(int id); } 
  • Implement the specialized repository interface:

     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(); } } 
  • Use the repository in the code:

     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; } 

This should be close to the resulting code (obviously something might need changes). Let me know if anything is unclear.

Put your mapping to DTO code into a single method that you reuse then you can just do something like:

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
         }
    }
}

First, try avoid use db directly in the webapi, move to a service.

And second, if I've understand your question, you want avoid write the conversion. You can use AutoMapper , install via nuget with extensions AutoMapper.QueryableExtensions, and configure the mapping between Drone and DroneDto. Configure the mapper:

Mapper.CreateMap<Drone, Dtos.DroneDTO>();

And use as simple as:

db.Drones
  .Where(d => ... condition ...)
  .Project()
  .To<DroneDto>()
  .ToList();

Like ben did, you can put your conversion code into a static method on the DroneDto class as such:

public class DroneDto
{
    public int iddrones {get;set;}

    public static DroneDto CreateFromEntity(DroneEntity dbEntity)
    {
        return new DroneDto
        {
            iddrones = dbEntity.iddrones,
            ...
        };
    }
}

However, the problem with Bens approach was that the .Select method was called on the DbSet, and LINQ to Entities do not handle these methods. So, you need to do your queries on the DbSet first, then collect the result. For example by calling .ToList(). Then you can do the conversion.

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;
    }
}

Alternatively, if you want to avoid multiple enumerations of the result, have a look at AutoMapper . Specifically the Queryable-Extensions .

Use a separate data access layer. I assumed the GetDrone(int Id) will retrieve one or no drone and used SingleOrDefault(). You can adjust that as needed.

//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;
    }
}

Then from the client:

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. The DB Call should be in a separate layer to the web api (reason: separation of concerns: you may want to change the DB technology in the future, and your web API may want to get data from other sources)
  2. Use a factory to build your DroneDTO. If you are using dependency injection, you can inject it into the web api controller. If this factory is simple (is not depended on by other factories) you can get away with making it static, but be careful with this: you don't want to have lots of static factories that depend on each other because as soon as one needs to not be static any more you will have to change all of them.

     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 }; } } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM