score:2
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.
score:0
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;
}
}
score:0
- 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)
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 }; } }
score:1
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
}
}
}
score:1
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();
score:1
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.
score:2
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.
score:3
i had the same problem about one year ago and unified the code by few steps:
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).
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); } }
i added an extension method to get current user credentials
getcurrentuserid
. if method parameters contains a class that implementsiauthorizedinput
that contains 1 property withuserid
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.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 classoutputdatamodel<tdata>
).i'm also used automapper to map my entities to ouput-classes instead of tons of
createfromentity
methods.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.
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.
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);
score:4
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;
}
}
Source: stackoverflow.com
Related Query
- C# WebApi refactoring Select Linq
- Linq code to select one item
- LINQ Source Code Available
- creating Linq to sqlite dbml from DbLinq source code
- Avoiding duplicate code in Linq Select method
- linq null refactoring code
- How to Select top (5) contributors group by Business type code in c# linq query
- Linq code refactoring
- source code for LINQ 101 samples
- How to write aggregate query in LINQ reusing part of the select code
- how to select data by linq in many-to-many relationship in First code Entity framework 5
- Refactoring C# code - doing more within Linq
- How to write C# LINQ code to select based on condition
- LINQ Select mutiply columns as data source for combobox C#
- c# Linq or code to extract groups from a single list of source data
- How to convert a string to C# code in the SELECT of C# LINQ
- How to use LINQ to select object with minimum or maximum property value
- Convert string[] to int[] in one line of code using LINQ
- Async await in linq select
- Select distinct using linq
- LINQ query to select top five
- Linq select objects in list where exists IN (A,B,C)
- Code equivalent to the 'let' keyword in chained LINQ extension method calls
- Select a Dictionary<T1, T2> with LINQ
- LINQ Select Distinct with Anonymous Types
- Select multiple records based on list of Id's with linq
- Select Multiple Fields from List in Linq
- How to select only the records with the highest date in LINQ
- LINQ Using Max() to select a single row
- Create a Tuple in a Linq Select
More Query from same tag
- Group By Department and get number of employees present in that department
- C# Create Dictionary with All Non-Object Property Names Mapped to the Object's Structure
- Group by JsonDocument's property with unknown type using Entity Framework Core
- C# LINQ question about foreach
- LINQ not accepting Contains()
- How can I use LINQ, to Sort and Filter items in a List<ReturnItem> collection?
- Entity Framework, DbSet<T>, Dispose() performance questions
- initializing properties with private sets in .Net
- Linq query to return a data structure holding max DateTime property of each passed object
- LINQ many-to-many relationship, how to write a correct WHERE clause?
- Fastest way to select distinct values from list based on two properties
- C# Linq to XML not working with
- What's better for creating distinct data structures: HashSet or Linq's Distinct()?
- LINQ .Where .Max and Writing to File
- LINQ nested collection
- Why predicate isn't filtering when building it via reflection
- Is it possible to pass an OrderBy expression as an argument?
- What are the differences between these 2 LINQ statements?
- Instantiate 2D Array with a LINQ Query
- Is it possible to use Intersect on complex arrays in C#?
- Entity Framework Data Transfer Objects Best Practice
- Memory optimized OrderBy and Take?
- How to perform addition to number rather than appending to end in Linq
- Fill Datatable from linq query
- How to remove formatting of XDocument in VB.net
- Binding an Enum to LINQ and SelectListItem
- Linq to entities: update creates new row
- Why is DataItem always null in the Page_Load handler?
- Sequence contains no matching element
- Best way to compare two large string lists, using C# and LINQ?