thảo luận [Thảo luận] Repository Pattern là cái bullsh*t nhất khi đã có ORM framework?

Có nên sử dụng repository pattern khi đã có ORM framework rồi ko?

  • Có - có lợi nhiều lắm để tôi cmt bên dưới

    Votes: 29 19.2%
  • Có - thấy người ta hay xài thì mình xài thôi (chắc là cũng có lợi gì đó nhưng dek biết)

    Votes: 25 16.6%
  • Không - tốn công vô ích, đẽ ra cho thêm việc phức tạp chứ ko lợi lộc gì đáng kể

    Votes: 19 12.6%
  • Không - code nhiều mệt vãi, xài trực tiếp ORM cho nhanh (cũng ko biết là có lợi hay ko)

    Votes: 21 13.9%
  • Tuỳ - tuỳ dự án, tuỳ yêu cầu

    Votes: 57 37.7%

  • Total voters
    151
Tôi nghĩ là các anh nên thống nhất lại cái định nghĩa về Repository đi. Cái GenericRepository là sai về mặt định nghĩa rồi, Output của nó là IQueryable cũng là sai định nghĩa rồi. Bàn luận trên 1 cái sai thì bàn đến bao giờ .

DDD Quickly
68747470733a2f2f692e6962622e636f2f736a77425764342f556e7469746c65642e706e67

Therefore, use a Repository, the purpose of which is to encapsulate all the logic needed to obtain object references. The domain objects won’t have to deal with the infrastructure to get the needed references to other objects of the domain. They will just get them from the Repository and the model is regaining its clarity and focus. The Repository may store references to some of the objects. When an object is created, it may be saved in the Repository, and retrieved from there to be used later. If the client requested an object from the Repository, and the Repository does not have it, it may get it from the storage. Either way, the Repository acts as a storage place for globally accessible objects.
 
nếu dùng ORM thay thế cho repository, tức là chúng ta sẽ inject Dbcontext vào các service chúng ta cần và gọi _context.Account.Find, hay là _context.Account.Where .. Include ... hoặc cũng có thể là
_context.Account.Where...Select(x=>x.GetFromAnotherSource()).toList ...
-----
nếu dùng repository chúng ta sẽ chỉ có:
getbyId
getAccountWith ...
----
và như thế chúng ta đã tránh việc lặp code ^^ cũng như một loại lợi ích khác
----
Một số entity chúng ta có thể dùng repository pattern, nhưng một số chỉ cần generic repository. vì nó hầu hết chỉ là CRUD
 
Tôi nghĩ là các anh nên thống nhất lại cái định nghĩa về Repository đi. Cái GenericRepository là sai về mặt định nghĩa rồi, Output của nó là IQueryable cũng là sai định nghĩa rồi. Bàn luận trên 1 cái sai thì bàn đến bao giờ .

DDD Quickly
68747470733a2f2f692e6962622e636f2f736a77425764342f556e7469746c65642e706e67

Đồng ý!

Cái tôi chỉ trích chính là cái Generic Repository trong link của Microsoft. Còn chuyện Repository ở DDD thì hoàn toàn ko có vấn đề gì.

Cái gây confuse mọi người là những bài viết về Generic Repository (như bài trong link của Microsoft) đều sử dụng tên là Repository Pattern nhưng bản chất nó giải quyết 1 vấn đề hoàn toàn khác so với Repository DDD. Cho nên nhiều anh đọc Generic Repository xong lại nghĩ đó là implementation của Repository DDD nên lại đem nó apply vào dự án là sai.

Như vậy Generic Repository là gì? Nó chính xác là Object Relational Mapping (ORM) và cái design này đi giải quyết 1 vấn đề hoàn toàn khác là làm abstraction query cho từng Entity type riêng lẽ, và quan trọng nhất là phục vụ Arregate root.

Điểm mấu chốt trong Generic Repository này là mổi Repository phải specific cho 1 Entity type, đồng thời phải abstract được IQueryable để khi thay đổi data storeage ví dụ từ MSSQL -> Postgresql sẽ ko phải thay đổi query logic ở business rules mà chỉ cần re-implement phần T-SQL -> PL/SQL (CRUD, join, where... cho riêng 1 entity/table).

Lý do thằng này ra đời là vì thời xa xưa khi chưa có ORM mổi khi đổi data store sẽ phải đổi chổ query logic ở business rules. Hiện tại có ORM đã làm điều này rồi. Và query logic đã được abstract bằng linq..., functions...

Cái confuse ở đây là:
1. Generic Repository abstract cả query logic, nó ở tầng thấp hơn cả Repository DDD
2. Nhiều người lầm tưởng implement của Generic Repository chính là Repository DDD nên lấy về áp dụng sai chổ.
3. Hiện tại vì có ORM nên Generic Repository là obsolete và đó là lý do tôi bảo nên bỏ nó đi.
 
nếu dùng ORM thay thế cho repository, tức là chúng ta sẽ inject Dbcontext vào các service chúng ta cần và gọi _context.Account.Find, hay là _context.Account.Where .. Include ... hoặc cũng có thể là
_context.Account.Where...Select(x=>x.GetFromAnotherSource()).toList ...
-----
nếu dùng repository chúng ta sẽ chỉ có:
getbyId
getAccountWith ...
----
và như thế chúng ta đã tránh việc lặp code ^^ cũng như một loại lợi ích khác

Đúng vậy và repository mà có chứa getbyId, getAccountWith chính là Repository DDD. Nếu dự án xài 3 layers thì có thằng này. Còn 2 layers thì inject dbContext vào service và có khả năng logic bị lặp.

----
Một số entity chúng ta có thể dùng repository pattern, nhưng một số chỉ cần generic repository. vì nó hầu hết chỉ là CRUD

Không cần generic repository. Ví bản thân DbSet<T> của EF chính là generic repository rồi. Đây là cái tôi muốn nói.
 
Tôi nghĩ là các anh nên thống nhất lại cái định nghĩa về Repository đi. Cái GenericRepository là sai về mặt định nghĩa rồi, Output của nó là IQueryable cũng là sai định nghĩa rồi. Bàn luận trên 1 cái sai thì bàn đến bao giờ .

DDD Quickly
68747470733a2f2f692e6962622e636f2f736a77425764342f556e7469746c65642e706e67

Anw, tôi block con ếch xanh kia rồi. Vào đây để đọc comment có tính đóng góp của mấy ông thôi.

Tôi chưa hiểu định nghĩa Generic repo ông nói ở đây là gì lắm, nhưng tôi hiểu nó build on top của thằng orm bên dưới. Cùng một số extension liên quan đến resolve current db context trên request đó.

Còn 1 cái nữa liên quan đến DI, và việc giải resolve nó (scoped, transient, singleton) cũng chưa thấy ông nào nhắc đến mối quan hệ của nó trong repo + uow

Cộng với database connection pool. Bài toán này chắc các bác bên java sẽ đụng nhiều hơn c# bọn tôi.

Chắc nhờ thêm cao nhân vào tranh luận những cái này.
 
Anh ếch xanh chê là microsoft viết cái repository pattern sai rồi
https://docs.microsoft.com/en-us/as...f-work-patterns-in-an-asp-net-mvc-application

Nhưng theo tôi thì nó viết không sai, mà nó viết chung chung quá. Repository Pattern của Microsoft viết thì dữ liệu nó đã được chuyển qua IEnumerable (In-memory) mà không phải IQueryable (build sql). Tức là về tính chất thì nó không sai nhưng nó viết 1 case quá cơ bản nên nhiều người nhiều sai.
Trên Google có thể tìm được rất nhiều bài viết về GenericRepository mà mỗi function sẽ trả ra IQueryable (cái này thì cơ bản là sai về định nghĩa của Repository Pattern) và cái này là thừa (tôi đồng ý với quan điểm thừa anh ếch xanh)

Còn 1 cái nữa liên quan đến DI, và việc giải resolve nó (scoped, transient, singleton) cũng chưa thấy ông nào nhắc đến mối quan hệ của nó trong repo + uow
Cái này khá đơn giản.
Scoped = mỗi thread thì 1 class sẽ chỉ sinh ra 1 object tương ứng, do đó việc inject giữa nhiều service khác nhau thực chất vẫn là 1 object, do đó có thể thực hiện transaction của UOW được.
Transient = mỗi class sẽ tự tạo 1 object tương ứng với lần inject đó => không thực hiện được UOW transaction vì DataContext nó khác nhau.
Singleton = tạo 1 object xuyên suốt life time của ứng dụng => xung đột dữ liệu của DataContext khi có nhiều thread thực hiện vì EF yêu cầu thread safe.
=> Do đó cách dùng đúng của UOW là phải dùng Scoped thôi.

Về Database connection pool thì mặc định từ bản 3.1 trở xuống thì với connection string không chỉ định connection pool thì set là 100 connection. Còn với bản EF 5 trở lên thì đã bỏ giới hạn này đi. Do đó với các ứng dụng nhiều luồng truy cập thì phải nâng maximum connection pool lên 1000 hoặc hơn, tùy vào stress testing thôi. Còn cái chuyện connection pool tức là nó dùng connection xong thì nó trả về pool cho thread khác sử dụng thôi.
 
Anh ếch xanh chê là microsoft viết cái repository pattern sai rồi
https://docs.microsoft.com/en-us/as...f-work-patterns-in-an-asp-net-mvc-application

Nhưng theo tôi thì nó viết không sai, mà nó viết chung chung quá. Repository Pattern của Microsoft viết thì dữ liệu nó đã được chuyển qua IEnumerable (In-memory) mà không phải IQueryable (build sql). Tức là về tính chất thì nó không sai nhưng nó viết 1 case quá cơ bản nên nhiều người nhiều sai.

OK, cái ví dụ của Microsoft nó trả ra IEnumerable thì nó vẫn là Repository DDD. Nếu vậy thằng này cũng chỉ là gom query logic phức tạp lại đúng như bản chất của Repository DDD. Trường hợp query phức tạp, join nhiều bảng thì ko thể query giữa 2 repository với nhau mà phải pickup 1 thằng Repository rồi implement cái logic phức tạp trong đó, bao gồm join nhiều tables nhờ vào dbContext.

Ví dụ ko có chuyện join 2 thằng này mà khi đụng tới query logic có liên quan ở cả 2 thằng thì chọn 1 thằng để viết cái logic đó. Am I right?

C#:
  private GenericRepository<Department> departmentRepository;
  private GenericRepository<Course> courseRepository;
.........

courseRepository.ListAvaiableCourseDepartments(); // bên trong sẽ join Course và Department

Trên Google có thể tìm được rất nhiều bài viết về GenericRepository mà mỗi function sẽ trả ra IQueryable (cái này thì cơ bản là sai về định nghĩa của Repository Pattern) và cái này là thừa (tôi đồng ý với quan điểm thừa anh ếch xanh)

GenericRepository mà mổi function trả ra IQueryable là vì mục đích của nó là abstract query chứ ko phải như một cái Repository DDD. Nó sai về định nghĩa Repository DDD vì mục đích của nó khác.

Mục đích của nó là để khi query người ta có thể thực hiện như này, và đây là cái tào lao vì EF nó làm rồi:
C#:
from departmentRepository d
join courseRepository c on c.DepartmentId = d.Id....

Cái này khá đơn giản.
Scoped = mỗi thread thì 1 class sẽ chỉ sinh ra 1 object tương ứng, do đó việc inject giữa nhiều service khác nhau thực chất vẫn là 1 object, do đó có thể thực hiện transaction của UOW được.
Transient = mỗi class sẽ tự tạo 1 object tương ứng với lần inject đó => không thực hiện được UOW transaction vì DataContext nó khác nhau.
Singleton = tạo 1 object xuyên suốt life time của ứng dụng => xung đột dữ liệu của DataContext khi có nhiều thread thực hiện vì EF yêu cầu thread safe.
=> Do đó cách dùng đúng của UOW là phải dùng Scoped thôi.

Về Database connection pool thì mặc định từ bản 3.1 trở xuống thì với connection string không chỉ định connection pool thì set là 100 connection. Còn với bản EF 5 trở lên thì đã bỏ giới hạn này đi. Do đó với các ứng dụng nhiều luồng truy cập thì phải nâng maximum connection pool lên 1000 hoặc hơn, tùy vào stress testing thôi. Còn cái chuyện connection pool tức là nó dùng connection xong thì nó trả về pool cho thread khác sử dụng thôi.

Sẵn nói luôn vụ này, thằng Asp.Net nó đã support luôn cả chuyện tạo dbContext với default Scoped life time khi gọi services.AddDbContext<T> rồi.

Còn chuyện muốn để thằng service ko phụ thuộc dbContext thì nên dùng AOP để move toàn bộ cái đoạn BeginTransaction, SaveChanges, Rollback… ra khỏi service luôn. Lúc này cũng chẳng cần implement thêm cái UnitofWork.
 
Last edited:
Anh ếch xanh chê là microsoft viết cái repository pattern sai rồi
https://docs.microsoft.com/en-us/as...f-work-patterns-in-an-asp-net-mvc-application

Nhưng theo tôi thì nó viết không sai, mà nó viết chung chung quá. Repository Pattern của Microsoft viết thì dữ liệu nó đã được chuyển qua IEnumerable (In-memory) mà không phải IQueryable (build sql). Tức là về tính chất thì nó không sai nhưng nó viết 1 case quá cơ bản nên nhiều người nhiều sai.
Trên Google có thể tìm được rất nhiều bài viết về GenericRepository mà mỗi function sẽ trả ra IQueryable (cái này thì cơ bản là sai về định nghĩa của Repository Pattern) và cái này là thừa (tôi đồng ý với quan điểm thừa anh ếch xanh)


Cái này khá đơn giản.
Scoped = mỗi thread thì 1 class sẽ chỉ sinh ra 1 object tương ứng, do đó việc inject giữa nhiều service khác nhau thực chất vẫn là 1 object, do đó có thể thực hiện transaction của UOW được.
Transient = mỗi class sẽ tự tạo 1 object tương ứng với lần inject đó => không thực hiện được UOW transaction vì DataContext nó khác nhau.
Singleton = tạo 1 object xuyên suốt life time của ứng dụng => xung đột dữ liệu của DataContext khi có nhiều thread thực hiện vì EF yêu cầu thread safe.
=> Do đó cách dùng đúng của UOW là phải dùng Scoped thôi.

Về Database connection pool thì mặc định từ bản 3.1 trở xuống thì với connection string không chỉ định connection pool thì set là 100 connection. Còn với bản EF 5 trở lên thì đã bỏ giới hạn này đi. Do đó với các ứng dụng nhiều luồng truy cập thì phải nâng maximum connection pool lên 1000 hoặc hơn, tùy vào stress testing thôi. Còn cái chuyện connection pool tức là nó dùng connection xong thì nó trả về pool cho thread khác sử dụng thôi.

ConnectionPool ở application mà mở 1000 nghe sai sai.
 
Nói như này thôi mai mốt anh đừng dùng private class member nữa, public hết member luôn. Đợi vào code review xem thằng dev nào access đến member nào không đúng thì bắt lỗi nó.

Anh lội page từ từ, đặc biệt là những page cuối rồi hãy lao vào vật tôi.

Lúc đó tôi với a kia vật nhau là vì view của tôi đang nói chuyện abstract query/command. Trong khi view của a ta đang nói chuyện abstract business rule.

Hơn nữa nếu nó muốn làm sai thì nó access truc tiep ef dek thông qua interface/repo gì hết thì muốn cản nó cũng ko dc. Dev bựa thì level nào cũng có. Nhiều khi còn dek biết DI là gì, new trực tiếp object kìa.

via theNEXTvoz for iPhone
 
Anh lội page từ từ, đặc biệt là những page cuối rồi hãy lao vào vật tôi.

Lúc đó tôi với a kia vật nhau là vì view của tôi đang nói chuyện abstract query/command. Trong khi view của a ta đang nói chuyện abstract business rule.
Tôi lội rồi anh à. Tôi không bàn đến vấn đề repo và dbcontext nữa. Nó đã được bàn đủ rồi. Tôi chỉ nói đến cách lập luận này của anh thôi.

Hơn nữa nếu nó muốn làm sai thì nó access truc tiep ef dek thông qua interface/repo gì hết thì muốn cản nó cũng ko dc
Với logic này của anh, thì anh hãy public hết member như tôi nói đi. Private làm gì, dev bựa nó cũng biết tự tạo 1 public member để có thể access đến cái private member đó.
 
Tôi lội rồi anh à. Tôi không bàn đến vấn đề repo và dbcontext nữa. Nó đã được bàn đủ rồi. Tôi chỉ nói đến cách lập luận này của anh thôi.


Với logic này của anh, thì anh hãy public hết member như tôi nói đi. Private làm gì, dev bựa nó cũng biết tự tạo 1 public member để có thể access đến cái private member đó.

Dev bựa thì nó tạo luôn cái hàm Update/Delete trong cái Repo luôn đấy. Lúc đó thì các anh có quy định Repo đó chỉ readonly ko thì cũng đâu cản dc nó. Bởi vậy có thím kia mới nói là ràng buộc cứng ở tầng db luôn ấy.

Mà thôi tôi ko cãi cái chuyện này. A bắt bẽ tôi vì câu nói đó thì tôi chịu thua. :surrender:

via theNEXTvoz for iPhone
 
Các anh còn trẻ nên khi các anh bắt đầu code, các anh tiếp cận với EF và vì thằng EF nó làm việc theo cách của nó như các anh đang thấy. Và các anh nghĩ nó là cả thế giới.

Rồi các anh quay lại chửi Repository Pattern như các hàm GetById, GetByName là ko cần thiết. Các anh đang bắn đại bác vào lịch sử phát triển phần mềm với tri kiến của các anh đấy.

Tui đã đưa ví dụ với NHibernate Session rồi. Các anh muốn biết thì thử học nó đi. Khi các anh làm việc với NHibernate Session thì các anh mới hiểu thế nào là Unit Of Work.

Và vì là Unit Of Work nên cần các hàm GetById để gói gọn và chấm dứt
Session/Transaction trong một Unit Of Work để nó không ảnh hưởng đến các tác vụ khác.

Tui ko phải chuyên gia Ef, nhưng hồi còn làm NHibernate thì trong 1 UnitOfWork tôi có thể gởi đồng thời nhiều query lên server cùng một thời điểm để tránh round trip rất hay. Mà giờ già rồi ko nhớ nó gọi là gì.

Và đó là cách làm việc của các ORM thời đó. (Và bây giờ?)
. Cho tới khi thằng EF ra đời. Nó hoạt động theo một cách hoàn toàn khác để tracking changes của Entity và loại bỏ khái niệm Session (hoặc nó có internal Session tui ko rành) nên Unit Of Work nó ko có ý nghĩa trong EF.

Một cái khác biệt lớn nhất khi làm việc với Ef là Repository không khởi tạo Singleton được. Trong khi NHibernate thì tạo Singleton Repository bình thường.

Các anh em làm Python, Php,... có thể chia sẻ Orm bên các ngôn ngữ đó hoạt động như thế nào? Nhưng tui đoán các anh em vẫn sẽ cần Repository Pattern với GetById hay GetByName.

Edit: Định trả lời anh Ếch ở dưới mà nghĩ. Đọc tới dòng addscoped inject vào singleton instance, tôi lắc đầu. Chắc hi vọng nó sẽ tự tạo instance mới của DbContext bằng phép màu. Rồi multi-theading kiểu gì, muli-user, parallel editing kiểu gì. Thôi tốn thời gian vô ích nên tui out topic. Bye bye anh em.
 
Last edited:
Các anh còn trẻ nên khi các anh bắt đầu code, các anh tiếp cận với EF và vì thằng EF nó làm việc theo cách của nó như các anh đang thấy. Và các anh nghĩ nó là cả thế giới.
=> Tôi cũng thế hệ 8x nên ko trẻ đâu a. Tôi làm từ Ado.Net, Nhibernate, EF đời đầu tới giờ
Rồi các anh quay lại chửi Repository Pattern như các hàm GetById, GetByName là ko cần thiết. Các anh đang bắn đại bác vào lịch sử phát triển phần mềm với tri kiến của các anh đấy.
=> Đọc lại nha tôi ko có nói nó là ko cần thiết. Anh xài 3 layers thì anh có Repository DDD chứa đám này. Tôi xài 2 layers thì tôi query trực tiếp và có khả năng code bị lặp như tôi trả lời a trên kia. Đây ko phải là vấn đề tôi chỉ trích.

Tui đã đưa ví dụ với NHibernate Session rồi. Các anh muốn biết thì thử học nó đi. Khi các anh làm việc với NHibernate Session thì các anh mới hiểu thế nào là Unit Of Work.

Và vì là Unit Of Work nên cần các hàm GetById để gói gọn và chấm dứt
Session/Transaction trong một Unit Of Work để nó không ảnh hưởng đến các tác vụ khác.
Tui ko phải chuyên gia Ef, nhưng hồi còn làm NHibernate thì trong 1 UnitOfWork tôi có thể gởi đồng thời nhiều query lên server cùng một thời điểm để tránh round trip rất hay. Mà giờ già rồi ko nhớ nó gọi là gì.

Và đó là cách làm việc của các ORM thời đó. (Và bây giờ?)
. Cho tới khi thằng EF ra đời. Nó hoạt động theo một cách hoàn toàn khác để tracking changes của Entity và loại bỏ khái niệm Session (hoặc nó có internal Session tui ko rành) nên Unit Of Work nó ko có ý nghĩa trong EF.
=> Đọc lại luôn là tôi ko nói Repository/Unit of Work là sai tôi chỉ nói xài ORM nó có support. Nhiberante có cái session đó, anh muốn Unit of Work thì anh dùng thằng ISession đó trên service để BeginTransaction/Commit/Rollback. Anh muốn ko depenency trên ISession này thì anh dùng AOP để move cái đoạn wrap transaction này ra ngoài. Toàn bộ logic về commit/rollback được gọi tự động chứ ko cần phải đẽ ra cái class IUnitofWork. Còn trong service anh vẫn dùng Repository.GetById.. bình thường. Tôi hiểu cái Repository của a đang xài là dạng Repository DDD nên nó ko có gì sai cả.

Và nói thêm cho rõ thì việc quản lý transaction là bắt buộc phải nằm ở tầng service để khi service thao tác trên nhiều repo thì đảm bảo unit of work nhé. A nói gì đó về hàm "GetById để gói gọn và chấm dứt Session/Transaction trong một Unit Of Work để nó không ảnh hưởng đến các tác vụ khác." là có gì đó sai sai.:doubt:

Một cái khác biệt lớn nhất khi làm việc với Ef là Repository không khởi tạo Singleton được. Trong khi NHibernate thì tạo Singleton Repository bình thường.
=> Chẳng có cái khác biệt nào ở đây cả. Tôi hiểu Repository DDD là chưa logic query/ business rule thì anh tạo singleton bình thường. Nhưng ai lại để singleton cho Repository…

Còn dbContext và ISession được inject vào thì nó phải là Scoped để phục vụ quản lý transaction.

Clear?
 
Last edited:
Dev bựa thì nó tạo luôn cái hàm Update/Delete trong cái Repo luôn đấy. Lúc đó thì các anh có quy định Repo đó chỉ readonly ko thì cũng đâu cản dc nó. Bởi vậy có thím kia mới nói là ràng buộc cứng ở tầng db luôn ấy.

Mà thôi tôi ko cãi cái chuyện này. A bắt bẽ tôi vì câu nói đó thì tôi chịu thua. :surrender:

via theNEXTvoz for iPhone
Anh dùng luận điểm đó để support cho việc không cần thiết dùng repo. Tôi bác bỏ luận điểm của anh thì anh lại bảo tôi bắt bẽ và không cãi. Khi tranh luận, không phục thì anh cứ nói anh không phục, cớ chi phải bảo tôi bắt bẻ, nào rồi nói kháy anh chịu thua.

Tôi không tranh luận với anh nữa. Vì tranh luận kiểu này anh nói gà thành vịt, vịt thành gà cũng chẳng ai nói anh sai được.
 
Anh dùng luận điểm đó để support cho việc không cần thiết dùng repo. Tôi bác bỏ luận điểm của anh thì anh lại bảo tôi bắt bẽ và không cãi. Khi tranh luận, không phục thì anh cứ nói anh không phục, cớ chi phải bảo tôi bắt bẻ, nào rồi nói kháy anh chịu thua.

Tôi không tranh luận với anh nữa. Vì tranh luận kiểu này anh nói gà thành vịt, vịt thành gà cũng chẳng ai nói anh sai được.

Hic. Tôi đã bảo tôi thua rồi mà, tôi phục mà :sad:
OK cách anh kia có thể prevent phần nào chuyện dev code sai y như chuyện private/public member trong class. Nói thế dc chưa? :ah:
 
Những a tranh luận ko đến cùng vấn đề thì lượn hoặc ignore tôi đi nhé. Như cái a trên kia bảo xài singleton cho Repository thì tôi cũng ko muốn tranh luận làm gì. :sweat:

via theNEXTvoz for iPhone
 
Back
Top