Guest User

Untitled

a guest
Dec 26th, 2018
173
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.03 KB | None | 0 0
  1. #pragma warning disable 1591
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Linq.Expressions;
  6. using System.Threading.Tasks;
  7. using DomainDbHelpers.Domain;
  8. using DomainDbHelpers.DomainHelpers;
  9. using DomainDbHelpers.Persistence;
  10. using DomainDbHelpers.PersistenceHelpers;
  11. using LinqKit;
  12. using Microsoft.EntityFrameworkCore;
  13.  
  14. namespace DomainDbHelpers
  15. {
  16. class Program
  17. {
  18. static void Main(string[] args)
  19. {
  20. QueryableExtensions.UnsafeConfigureNotFoundException(
  21. (type, spec) => new ResourceNotFoundException(
  22. $"Entity '{type.Name}' was not found by spec {spec}."
  23. ));
  24. DateTimeOffset time = DateTimeOffset.UtcNow;
  25. using (var db = new BloggingContext())
  26. {
  27. if (db.Database.EnsureCreated())
  28. {
  29. for (int i = 0; i < 10; i++)
  30. {
  31. var created = time.AddDays(-i);
  32. Blog blog = new Blog($"url{i}", createdAt: created);
  33. Post post = blog.CreatePost("title", "content", created);
  34. db.Blogs!.Add(blog);
  35. db.Posts!.Add(post);
  36. }
  37.  
  38. db.SaveChanges();
  39. }
  40. }
  41. using (var db = new BloggingContext())
  42. {
  43. var q = new Domain.BlogPostSearchQuery
  44. {
  45. CreatedBefore = time.AddDays(-5)
  46. };
  47. Page<Post> posts = db.Posts!
  48. .Where(Post.MatchesSearchCriteria(q))
  49. .GetPage(10, 2)
  50. .GetAwaiter()
  51. .GetResult();
  52. Console.WriteLine(
  53. $"Found {posts.TotalCount} posts, "
  54. + $"page number {posts.PageNumber}, page size {posts.PageSize}.");
  55. q = new Domain.BlogPostSearchQuery
  56. {
  57. Title = "not_found"
  58. };
  59. try
  60. {
  61. Post post =
  62. db.Posts!.GetOne(Post.MatchesSearchCriteria(q)).GetAwaiter().GetResult();
  63. }
  64. catch (ResourceNotFoundException ex)
  65. {
  66. Console.WriteLine(ex.Message);
  67. }
  68. }
  69. }
  70. }
  71.  
  72. public class ResourceNotFoundException : Exception
  73. {
  74. public ResourceNotFoundException(string message) : base(message)
  75. {
  76. }
  77. }
  78. }
  79.  
  80. namespace DomainDbHelpers.Domain
  81. {
  82. public class BlogPostSearchQuery
  83. {
  84. public string? BlogUrl { get; set; }
  85. public string? Title { get; set; }
  86. public DateTimeOffset? CreatedBefore { get; set; }
  87. }
  88.  
  89. public class Blog
  90. {
  91. public Blog(string url, DateTimeOffset createdAt)
  92. {
  93. Url = url;
  94. BlogId = Guid.NewGuid();
  95. Posts = new List<Post>();
  96. CreatedAt = createdAt;
  97. }
  98. protected Blog()
  99. {
  100. Url = "";
  101. }
  102.  
  103. public Guid BlogId { get; protected set; }
  104. public string Url { get; protected set; }
  105. public DateTimeOffset CreatedAt { get; protected set; }
  106.  
  107. public List<Post>? Posts { get; protected set; }
  108.  
  109. internal Post CreatePost(string title, string content, DateTimeOffset createdAt)
  110. {
  111. return new Post(this, title, content, createdAt);
  112. }
  113. }
  114.  
  115. public class Post
  116. {
  117. public Post(Blog blog, string title, string content, DateTimeOffset createdAt)
  118. {
  119. PostId = Guid.NewGuid();
  120. Blog = blog;
  121. BlogId = blog.BlogId;
  122. Title = title;
  123. Content = content;
  124. CreatedAt = createdAt;
  125. }
  126.  
  127. protected Post()
  128. {
  129. // Fill with non-null values to persuade null checker that these props are loaded from DB.
  130. Title = "";
  131. Content = "";
  132. }
  133.  
  134. public static Specification<Post> MatchesSearchCriteria(BlogPostSearchQuery q) {
  135. var pred = Linq.Expr((Post post) => true);
  136. if (q.BlogUrl != null)
  137. {
  138. pred = pred.And(x => x.Blog!.Url.Contains(q.BlogUrl));
  139. }
  140. if (q.CreatedBefore != null)
  141. {
  142. pred = pred.And(x => x.CreatedAt < q.CreatedBefore);
  143. }
  144. if (q.Title != null)
  145. {
  146. pred = pred.And(x => x.Title.Contains(q.Title));
  147. }
  148. return new Specification<Post>(nameof(MatchesSearchCriteria), pred.Expand());
  149. }
  150. public Guid PostId { get; protected set; }
  151. public string Title { get; protected set; }
  152. public string Content { get; protected set; }
  153. public DateTimeOffset CreatedAt { get; protected set; }
  154. public Guid BlogId { get; protected set; }
  155. public Blog? Blog { get; protected set; }
  156. }
  157. }
  158.  
  159. namespace DomainDbHelpers.Persistence
  160. {
  161. public class BloggingContext : DbContext
  162. {
  163. // To run Postgres locally, execute
  164. // docker run -it --rm -p 5432:5432 postgres
  165. private const string _connectionString =
  166. "Host=localhost;Database=tmp_db;Username=postgres;Password=postgres";
  167.  
  168. public DbSet<Blog>? Blogs { get; set; }
  169.  
  170. public DbSet<Post>? Posts { get; set; }
  171.  
  172. protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  173. {
  174. optionsBuilder.UseNpgsql(_connectionString);
  175. }
  176. }
  177. }
  178.  
  179. namespace DomainDbHelpers.DomainHelpers
  180. {
  181. /// <summary>
  182. /// Represents a filter over entities.
  183. /// </summary>
  184. /// <typeparam name="TEntity">
  185. /// The entity type.
  186. /// </typeparam>
  187. public class Specification<TEntity>
  188. {
  189. /// <summary>
  190. /// Initializes a new instance of the <see cref="Specification{TEntity}"/> class.
  191. /// </summary>
  192. /// <param name="name">The name of this specification (for better diagnostic).</param>
  193. /// <param name="predicate">The predicate.</param>
  194. /// <param name="parameters">The parameters.</param>
  195. public Specification(
  196. string name,
  197. Expression<Func<TEntity, bool>> predicate,
  198. params object[] parameters)
  199. {
  200. Name = name;
  201. Parameters = parameters;
  202. Predicate = predicate;
  203. }
  204.  
  205. /// <summary>
  206. /// Initializes a new instance of the <see cref="Specification{TEntity}"/> class.
  207. /// </summary>
  208. protected Specification()
  209. {
  210. Name = GetType().Name;
  211. Parameters = new object[0];
  212. Predicate = e => false;
  213. }
  214.  
  215. /// <summary>
  216. /// Gets the name of this specification.
  217. /// </summary>
  218. public string Name { get; protected set; }
  219.  
  220. /// <summary>
  221. /// Gets the predicate which returns true
  222. /// for entities satisfying this specification.
  223. /// </summary>
  224. public Expression<Func<TEntity, bool>> Predicate { get; protected set; }
  225.  
  226. /// <summary>
  227. /// Gets the parameters of this specification.
  228. /// </summary>
  229. protected object[] Parameters { get; private set; }
  230.  
  231. /// <summary>
  232. /// Returns a string that represents the current object.
  233. /// </summary>
  234. /// <returns>
  235. /// A string that represents the current object.
  236. /// </returns>
  237. public override string ToString()
  238. {
  239. string parameters = string.Join(", ", Parameters.Select(x => x ?? "(null)"));
  240. return $"{Name}({parameters})";
  241. }
  242. }
  243. }
  244.  
  245. namespace DomainDbHelpers.PersistenceHelpers
  246. {
  247. /// <summary>
  248. /// A contigous subsequence of entities taken at a certain offset from results of a query.
  249. /// </summary>
  250. /// <typeparam name="TEntity">The entity type.</typeparam>
  251. public class Page<TEntity>
  252. {
  253. /// <summary>
  254. /// Initializes a new instance of the <see cref="Page{TEntity}"/> class.
  255. /// </summary>
  256. /// <param name="totalCount">
  257. /// The total number of entities satisfying the query specifications.
  258. /// </param>
  259. /// <param name="pageNumber">
  260. /// The number of the actually returned page.
  261. /// </param>
  262. /// <param name="pageSize">
  263. /// The size of the actually returned page (if too big one was requested).
  264. /// </param>
  265. /// <param name="entities">
  266. /// The collection of entities belonging to the requested page.
  267. /// </param>
  268. public Page(
  269. int totalCount,
  270. int pageNumber,
  271. int pageSize,
  272. TEntity[] entities)
  273. {
  274. if (totalCount < 0)
  275. {
  276. throw new ArgumentOutOfRangeException(
  277. nameof(totalCount),
  278. $"The count was out of range. TotalCount={totalCount}.");
  279. }
  280. if (entities == null)
  281. {
  282. throw new ArgumentNullException(nameof(entities));
  283. }
  284.  
  285. TotalCount = totalCount;
  286. PageSize = pageSize;
  287. PageNumber = pageNumber;
  288. Entities = entities;
  289. }
  290.  
  291. /// <summary>
  292. /// Gets the total number of entities in the database that satisfy the query conditions.
  293. /// </summary>
  294. public int TotalCount { get; private set; }
  295.  
  296. /// <summary>
  297. /// Gets the number of the actually returned page.
  298. /// </summary>
  299. public int PageNumber { get; private set; }
  300.  
  301. /// <summary>
  302. /// Gets the size of the actually returned page (if too big one was requested).
  303. /// </summary>
  304. public int PageSize { get; private set; }
  305.  
  306. /// <summary>
  307. /// Gets the collection of entities belonging to the requested page.
  308. /// </summary>
  309. public TEntity[] Entities { get; private set; }
  310.  
  311. }
  312.  
  313. public static class QueryableExtensions
  314. {
  315. /// <summary>
  316. /// Creates a "not found" exception based on the specified entity type and specification.
  317. /// </summary>
  318. private static Func<Type, string, Exception> _createException = DefaultExceptionFactory;
  319.  
  320. public static void UnsafeConfigureNotFoundException(
  321. Func<Type, string, Exception> exceptionFactory)
  322. {
  323. _createException = exceptionFactory;
  324. }
  325.  
  326. private static Exception DefaultExceptionFactory(Type entityType, string specification)
  327. {
  328. return new InvalidOperationException(
  329. $"The entity of type '{entityType.Name}' was not found. "
  330. + $"Specification: {specification}.");
  331. }
  332.  
  333. /// <summary>
  334. /// Gets the single entity based on the specification.
  335. /// Throws if the entity is not found.
  336. /// Throws if more than one entity satisfies the specification.
  337. /// </summary>
  338. /// <typeparam name="T">The entity type.</typeparam>
  339. /// <param name="repository">The entity repository.</param>
  340. /// <param name="spec">The specification.</param>
  341. /// <returns>The entity satisfying the specification.</returns>
  342. public static async Task<T> GetOne<T>(
  343. this IQueryable<T> repository,
  344. Specification<T> spec)
  345. where T: class
  346. {
  347. T entity = await repository.GetOneOrDefault(spec);
  348. if (entity == null)
  349. {
  350. throw _createException(typeof(T), spec.ToString());
  351. }
  352.  
  353. return entity;
  354. }
  355.  
  356. /// <summary>
  357. /// Gets the single entity based on the specification.
  358. /// Returns <c>null</c> if the entity is not found.
  359. /// Throws if more than one entity satisfies the specification.
  360. /// </summary>
  361. /// <typeparam name="T">The entity type.</typeparam>
  362. /// <param name="repository">The entity repository.</param>
  363. /// <param name="spec">The specification.</param>
  364. /// <returns>
  365. /// The entity satisfying the specification or <c>null</c> if no such entity exists.
  366. /// </returns>
  367. public static async Task<T> GetOneOrDefault<T>(
  368. this IQueryable<T> repository,
  369. Specification<T> spec)
  370. where T: class
  371. {
  372. return await repository.SingleOrDefaultAsync(spec.Predicate);
  373. }
  374.  
  375. /// <summary>
  376. /// Filters the entities in the repository based on the specification.
  377. /// </summary>
  378. /// <typeparam name="T">The entity type.</typeparam>
  379. /// <param name="repository">The entity repository.</param>
  380. /// <param name="spec">The specification.</param>
  381. /// <returns>The filtered repository.</returns>
  382. public static IQueryable<T> Where<T>(
  383. this IQueryable<T> repository,
  384. Specification<T> spec)
  385. where T: class
  386. {
  387. return repository.Where(spec.Predicate);
  388. }
  389.  
  390. /// <summary>
  391. /// Gets a list of entities based on the specification.
  392. /// </summary>
  393. /// <typeparam name="T">The entity type.</typeparam>
  394. /// <param name="repository">The entity repository.</param>
  395. /// <param name="spec">The specification.</param>
  396. /// <returns>The entities satisfying the specification.</returns>
  397. public static async Task<List<T>> Get<T>(
  398. this IQueryable<T> repository,
  399. Specification<T> spec)
  400. where T: class
  401. {
  402. return await repository.Where(spec.Predicate).ToListAsync();
  403. }
  404.  
  405. /// <summary>
  406. /// Gets a page of entities based on the specification. The returned page number and size
  407. /// may differ from requested.
  408. /// </summary>
  409. /// <typeparam name="T">The entity type.</typeparam>
  410. /// <param name="repository">The entity repository.</param>
  411. /// <param name="pageNumber">The requested page number.</param>
  412. /// <param name="pageSize">The requested page size.</param>
  413. /// <param name="maxPageSize">The maximum allowed page size.</param>
  414. /// <returns>The page of entities.</returns>
  415. public static async Task<Page<T>> GetPage<T>(
  416. this IQueryable<T> repository,
  417. int pageNumber,
  418. int pageSize,
  419. int maxPageSize = 100)
  420. where T: class
  421. {
  422. int count = await repository.CountAsync();
  423. (int realPageNumber, int realPageSize) =
  424. GetRealPageNumberAndSize(pageNumber, pageSize, count, maxPageSize);
  425. T[] entities = await repository
  426. .Skip((realPageNumber - 1) * realPageSize)
  427. .Take(realPageSize)
  428. .ToArrayAsync();
  429. return new Page<T>(count, realPageNumber, realPageSize, entities);
  430. }
  431.  
  432. /// <summary>
  433. /// Clips the desired page number and size to the allowed range, returning
  434. /// the last available page number, if the requested one is too big, and the corrected
  435. /// page size.
  436. /// </summary>
  437. /// <param name="requestedPageNumber">The page number requested by the caller.</param>
  438. /// <param name="count">The total count of records satisfying the query in the DB.</param>
  439. /// <param name="maxPageSize">The maximum allowed page size.</param>
  440. /// <param name="requestedPageSize">The page size.</param>
  441. /// <returns>
  442. /// The pair (real page number, real page size).
  443. /// </returns>
  444. private static (int, int) GetRealPageNumberAndSize(
  445. int requestedPageNumber,
  446. int requestedPageSize,
  447. int count,
  448. int maxPageSize)
  449. {
  450. int pageSize = requestedPageSize > maxPageSize
  451. ? maxPageSize
  452. : requestedPageSize < 1 ? 1 : requestedPageSize;
  453. int totalPages = count / pageSize;
  454. if (totalPages * pageSize < count || count == 0)
  455. {
  456. totalPages += 1;
  457. }
  458.  
  459. int pageNumber = requestedPageNumber < 1
  460. ? 1
  461. : requestedPageNumber > totalPages ? totalPages : requestedPageNumber;
  462. return (pageNumber, pageSize);
  463. }
  464. }
  465. }
Add Comment
Please, Sign In to add comment