Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #pragma warning disable 1591
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Linq.Expressions;
- using System.Threading.Tasks;
- using DomainDbHelpers.Domain;
- using DomainDbHelpers.DomainHelpers;
- using DomainDbHelpers.Persistence;
- using DomainDbHelpers.PersistenceHelpers;
- using LinqKit;
- using Microsoft.EntityFrameworkCore;
- namespace DomainDbHelpers
- {
- class Program
- {
- static void Main(string[] args)
- {
- QueryableExtensions.UnsafeConfigureNotFoundException(
- (type, spec) => new ResourceNotFoundException(
- $"Entity '{type.Name}' was not found by spec {spec}."
- ));
- DateTimeOffset time = DateTimeOffset.UtcNow;
- using (var db = new BloggingContext())
- {
- if (db.Database.EnsureCreated())
- {
- for (int i = 0; i < 10; i++)
- {
- var created = time.AddDays(-i);
- Blog blog = new Blog($"url{i}", createdAt: created);
- Post post = blog.CreatePost("title", "content", created);
- db.Blogs!.Add(blog);
- db.Posts!.Add(post);
- }
- db.SaveChanges();
- }
- }
- using (var db = new BloggingContext())
- {
- var q = new Domain.BlogPostSearchQuery
- {
- CreatedBefore = time.AddDays(-5)
- };
- Page<Post> posts = db.Posts!
- .Where(Post.MatchesSearchCriteria(q))
- .GetPage(10, 2)
- .GetAwaiter()
- .GetResult();
- Console.WriteLine(
- $"Found {posts.TotalCount} posts, "
- + $"page number {posts.PageNumber}, page size {posts.PageSize}.");
- q = new Domain.BlogPostSearchQuery
- {
- Title = "not_found"
- };
- try
- {
- Post post =
- db.Posts!.GetOne(Post.MatchesSearchCriteria(q)).GetAwaiter().GetResult();
- }
- catch (ResourceNotFoundException ex)
- {
- Console.WriteLine(ex.Message);
- }
- }
- }
- }
- public class ResourceNotFoundException : Exception
- {
- public ResourceNotFoundException(string message) : base(message)
- {
- }
- }
- }
- namespace DomainDbHelpers.Domain
- {
- public class BlogPostSearchQuery
- {
- public string? BlogUrl { get; set; }
- public string? Title { get; set; }
- public DateTimeOffset? CreatedBefore { get; set; }
- }
- public class Blog
- {
- public Blog(string url, DateTimeOffset createdAt)
- {
- Url = url;
- BlogId = Guid.NewGuid();
- Posts = new List<Post>();
- CreatedAt = createdAt;
- }
- protected Blog()
- {
- Url = "";
- }
- public Guid BlogId { get; protected set; }
- public string Url { get; protected set; }
- public DateTimeOffset CreatedAt { get; protected set; }
- public List<Post>? Posts { get; protected set; }
- internal Post CreatePost(string title, string content, DateTimeOffset createdAt)
- {
- return new Post(this, title, content, createdAt);
- }
- }
- public class Post
- {
- public Post(Blog blog, string title, string content, DateTimeOffset createdAt)
- {
- PostId = Guid.NewGuid();
- Blog = blog;
- BlogId = blog.BlogId;
- Title = title;
- Content = content;
- CreatedAt = createdAt;
- }
- protected Post()
- {
- // Fill with non-null values to persuade null checker that these props are loaded from DB.
- Title = "";
- Content = "";
- }
- public static Specification<Post> MatchesSearchCriteria(BlogPostSearchQuery q) {
- var pred = Linq.Expr((Post post) => true);
- if (q.BlogUrl != null)
- {
- pred = pred.And(x => x.Blog!.Url.Contains(q.BlogUrl));
- }
- if (q.CreatedBefore != null)
- {
- pred = pred.And(x => x.CreatedAt < q.CreatedBefore);
- }
- if (q.Title != null)
- {
- pred = pred.And(x => x.Title.Contains(q.Title));
- }
- return new Specification<Post>(nameof(MatchesSearchCriteria), pred.Expand());
- }
- public Guid PostId { get; protected set; }
- public string Title { get; protected set; }
- public string Content { get; protected set; }
- public DateTimeOffset CreatedAt { get; protected set; }
- public Guid BlogId { get; protected set; }
- public Blog? Blog { get; protected set; }
- }
- }
- namespace DomainDbHelpers.Persistence
- {
- public class BloggingContext : DbContext
- {
- // To run Postgres locally, execute
- // docker run -it --rm -p 5432:5432 postgres
- private const string _connectionString =
- "Host=localhost;Database=tmp_db;Username=postgres;Password=postgres";
- public DbSet<Blog>? Blogs { get; set; }
- public DbSet<Post>? Posts { get; set; }
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- optionsBuilder.UseNpgsql(_connectionString);
- }
- }
- }
- namespace DomainDbHelpers.DomainHelpers
- {
- /// <summary>
- /// Represents a filter over entities.
- /// </summary>
- /// <typeparam name="TEntity">
- /// The entity type.
- /// </typeparam>
- public class Specification<TEntity>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="Specification{TEntity}"/> class.
- /// </summary>
- /// <param name="name">The name of this specification (for better diagnostic).</param>
- /// <param name="predicate">The predicate.</param>
- /// <param name="parameters">The parameters.</param>
- public Specification(
- string name,
- Expression<Func<TEntity, bool>> predicate,
- params object[] parameters)
- {
- Name = name;
- Parameters = parameters;
- Predicate = predicate;
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="Specification{TEntity}"/> class.
- /// </summary>
- protected Specification()
- {
- Name = GetType().Name;
- Parameters = new object[0];
- Predicate = e => false;
- }
- /// <summary>
- /// Gets the name of this specification.
- /// </summary>
- public string Name { get; protected set; }
- /// <summary>
- /// Gets the predicate which returns true
- /// for entities satisfying this specification.
- /// </summary>
- public Expression<Func<TEntity, bool>> Predicate { get; protected set; }
- /// <summary>
- /// Gets the parameters of this specification.
- /// </summary>
- protected object[] Parameters { get; private set; }
- /// <summary>
- /// Returns a string that represents the current object.
- /// </summary>
- /// <returns>
- /// A string that represents the current object.
- /// </returns>
- public override string ToString()
- {
- string parameters = string.Join(", ", Parameters.Select(x => x ?? "(null)"));
- return $"{Name}({parameters})";
- }
- }
- }
- namespace DomainDbHelpers.PersistenceHelpers
- {
- /// <summary>
- /// A contigous subsequence of entities taken at a certain offset from results of a query.
- /// </summary>
- /// <typeparam name="TEntity">The entity type.</typeparam>
- public class Page<TEntity>
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="Page{TEntity}"/> class.
- /// </summary>
- /// <param name="totalCount">
- /// The total number of entities satisfying the query specifications.
- /// </param>
- /// <param name="pageNumber">
- /// The number of the actually returned page.
- /// </param>
- /// <param name="pageSize">
- /// The size of the actually returned page (if too big one was requested).
- /// </param>
- /// <param name="entities">
- /// The collection of entities belonging to the requested page.
- /// </param>
- public Page(
- int totalCount,
- int pageNumber,
- int pageSize,
- TEntity[] entities)
- {
- if (totalCount < 0)
- {
- throw new ArgumentOutOfRangeException(
- nameof(totalCount),
- $"The count was out of range. TotalCount={totalCount}.");
- }
- if (entities == null)
- {
- throw new ArgumentNullException(nameof(entities));
- }
- TotalCount = totalCount;
- PageSize = pageSize;
- PageNumber = pageNumber;
- Entities = entities;
- }
- /// <summary>
- /// Gets the total number of entities in the database that satisfy the query conditions.
- /// </summary>
- public int TotalCount { get; private set; }
- /// <summary>
- /// Gets the number of the actually returned page.
- /// </summary>
- public int PageNumber { get; private set; }
- /// <summary>
- /// Gets the size of the actually returned page (if too big one was requested).
- /// </summary>
- public int PageSize { get; private set; }
- /// <summary>
- /// Gets the collection of entities belonging to the requested page.
- /// </summary>
- public TEntity[] Entities { get; private set; }
- }
- public static class QueryableExtensions
- {
- /// <summary>
- /// Creates a "not found" exception based on the specified entity type and specification.
- /// </summary>
- private static Func<Type, string, Exception> _createException = DefaultExceptionFactory;
- public static void UnsafeConfigureNotFoundException(
- Func<Type, string, Exception> exceptionFactory)
- {
- _createException = exceptionFactory;
- }
- private static Exception DefaultExceptionFactory(Type entityType, string specification)
- {
- return new InvalidOperationException(
- $"The entity of type '{entityType.Name}' was not found. "
- + $"Specification: {specification}.");
- }
- /// <summary>
- /// Gets the single entity based on the specification.
- /// Throws if the entity is not found.
- /// Throws if more than one entity satisfies the specification.
- /// </summary>
- /// <typeparam name="T">The entity type.</typeparam>
- /// <param name="repository">The entity repository.</param>
- /// <param name="spec">The specification.</param>
- /// <returns>The entity satisfying the specification.</returns>
- public static async Task<T> GetOne<T>(
- this IQueryable<T> repository,
- Specification<T> spec)
- where T: class
- {
- T entity = await repository.GetOneOrDefault(spec);
- if (entity == null)
- {
- throw _createException(typeof(T), spec.ToString());
- }
- return entity;
- }
- /// <summary>
- /// Gets the single entity based on the specification.
- /// Returns <c>null</c> if the entity is not found.
- /// Throws if more than one entity satisfies the specification.
- /// </summary>
- /// <typeparam name="T">The entity type.</typeparam>
- /// <param name="repository">The entity repository.</param>
- /// <param name="spec">The specification.</param>
- /// <returns>
- /// The entity satisfying the specification or <c>null</c> if no such entity exists.
- /// </returns>
- public static async Task<T> GetOneOrDefault<T>(
- this IQueryable<T> repository,
- Specification<T> spec)
- where T: class
- {
- return await repository.SingleOrDefaultAsync(spec.Predicate);
- }
- /// <summary>
- /// Filters the entities in the repository based on the specification.
- /// </summary>
- /// <typeparam name="T">The entity type.</typeparam>
- /// <param name="repository">The entity repository.</param>
- /// <param name="spec">The specification.</param>
- /// <returns>The filtered repository.</returns>
- public static IQueryable<T> Where<T>(
- this IQueryable<T> repository,
- Specification<T> spec)
- where T: class
- {
- return repository.Where(spec.Predicate);
- }
- /// <summary>
- /// Gets a list of entities based on the specification.
- /// </summary>
- /// <typeparam name="T">The entity type.</typeparam>
- /// <param name="repository">The entity repository.</param>
- /// <param name="spec">The specification.</param>
- /// <returns>The entities satisfying the specification.</returns>
- public static async Task<List<T>> Get<T>(
- this IQueryable<T> repository,
- Specification<T> spec)
- where T: class
- {
- return await repository.Where(spec.Predicate).ToListAsync();
- }
- /// <summary>
- /// Gets a page of entities based on the specification. The returned page number and size
- /// may differ from requested.
- /// </summary>
- /// <typeparam name="T">The entity type.</typeparam>
- /// <param name="repository">The entity repository.</param>
- /// <param name="pageNumber">The requested page number.</param>
- /// <param name="pageSize">The requested page size.</param>
- /// <param name="maxPageSize">The maximum allowed page size.</param>
- /// <returns>The page of entities.</returns>
- public static async Task<Page<T>> GetPage<T>(
- this IQueryable<T> repository,
- int pageNumber,
- int pageSize,
- int maxPageSize = 100)
- where T: class
- {
- int count = await repository.CountAsync();
- (int realPageNumber, int realPageSize) =
- GetRealPageNumberAndSize(pageNumber, pageSize, count, maxPageSize);
- T[] entities = await repository
- .Skip((realPageNumber - 1) * realPageSize)
- .Take(realPageSize)
- .ToArrayAsync();
- return new Page<T>(count, realPageNumber, realPageSize, entities);
- }
- /// <summary>
- /// Clips the desired page number and size to the allowed range, returning
- /// the last available page number, if the requested one is too big, and the corrected
- /// page size.
- /// </summary>
- /// <param name="requestedPageNumber">The page number requested by the caller.</param>
- /// <param name="count">The total count of records satisfying the query in the DB.</param>
- /// <param name="maxPageSize">The maximum allowed page size.</param>
- /// <param name="requestedPageSize">The page size.</param>
- /// <returns>
- /// The pair (real page number, real page size).
- /// </returns>
- private static (int, int) GetRealPageNumberAndSize(
- int requestedPageNumber,
- int requestedPageSize,
- int count,
- int maxPageSize)
- {
- int pageSize = requestedPageSize > maxPageSize
- ? maxPageSize
- : requestedPageSize < 1 ? 1 : requestedPageSize;
- int totalPages = count / pageSize;
- if (totalPages * pageSize < count || count == 0)
- {
- totalPages += 1;
- }
- int pageNumber = requestedPageNumber < 1
- ? 1
- : requestedPageNumber > totalPages ? totalPages : requestedPageNumber;
- return (pageNumber, pageSize);
- }
- }
- }
Add Comment
Please, Sign In to add comment