From 5bde7b112f11fdf370b2aba3c1865ca7ad2cff81 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:29:44 +0000 Subject: [PATCH] Add SampleWebApp.Core project with custom services, MediatR, and FluentValidation Phase 1 implementation to replace GenericServices with modern .NET Core-compatible architecture: - Create SampleWebApp.Core .NET Standard 2.0 project - Add NuGet packages: MediatR 9.0.0, AutoMapper 10.1.1, FluentValidation 10.4.0 - Implement base classes (BaseDto, BaseQuery, BaseCommand) - Implement result types (CreateResult, UpdateResult, DeleteResult, PagedResult, SuccessOrErrors) - Create DTOs (PostDto, SimplePostDto, TagDto, BlogDto) - Create generic service interfaces (IListService, IDetailService, ICreateService, IUpdateService, IDeleteService) - Create domain-specific service interfaces (IPostService, ITagService, IBlogService) - Implement MediatR queries and commands for Posts, Tags, and Blogs - Implement MediatR handlers for all queries and commands - Implement service classes (PostService, TagService, BlogService) using IDbContextAdapter pattern - Implement FluentValidation validators (PostValidator, TagValidator, BlogValidator) - Create AutoMapper profiles for entity-to-DTO mapping - Create anti-corruption layer (GenericServicesAdapter) - Configure dependency injection (ServiceCollectionExtensions) Co-Authored-By: Abhay Aggarwal --- .../Adapters/GenericServicesAdapter.cs | 130 +++++++++++++ SampleWebApp.Core/Common/BaseDto.cs | 14 ++ .../Common/Commands/BaseCommand.cs | 8 + SampleWebApp.Core/Common/Queries/BaseQuery.cs | 8 + .../Common/Results/CreateResult.cs | 40 ++++ .../Common/Results/DeleteResult.cs | 38 ++++ .../Common/Results/PagedResult.cs | 28 +++ .../Common/Results/SuccessOrErrors.cs | 138 ++++++++++++++ .../Common/Results/UpdateResult.cs | 38 ++++ SampleWebApp.Core/DTOs/BlogDto.cs | 23 +++ SampleWebApp.Core/DTOs/PostDto.cs | 126 +++++++++++++ SampleWebApp.Core/DTOs/SimplePostDto.cs | 37 ++++ SampleWebApp.Core/DTOs/TagDto.cs | 22 +++ .../ServiceCollectionExtensions.cs | 42 +++++ .../Blogs/Commands/CreateBlogCommand.cs | 11 ++ .../Blogs/Commands/CreateBlogHandler.cs | 23 +++ .../Blogs/Commands/DeleteBlogCommand.cs | 10 + .../Blogs/Commands/DeleteBlogHandler.cs | 23 +++ .../Blogs/Commands/UpdateBlogCommand.cs | 11 ++ .../Blogs/Commands/UpdateBlogHandler.cs | 23 +++ .../Blogs/Queries/GetBlogByIdHandler.cs | 23 +++ .../Blogs/Queries/GetBlogByIdQuery.cs | 10 + .../Handlers/Blogs/Queries/GetBlogsHandler.cs | 24 +++ .../Handlers/Blogs/Queries/GetBlogsQuery.cs | 12 ++ .../Posts/Commands/CreatePostCommand.cs | 11 ++ .../Posts/Commands/CreatePostHandler.cs | 23 +++ .../Posts/Commands/DeletePostCommand.cs | 10 + .../Posts/Commands/DeletePostHandler.cs | 23 +++ .../Posts/Commands/UpdatePostCommand.cs | 11 ++ .../Posts/Commands/UpdatePostHandler.cs | 23 +++ .../Posts/Queries/GetPostByIdHandler.cs | 23 +++ .../Posts/Queries/GetPostByIdQuery.cs | 10 + .../Handlers/Posts/Queries/GetPostsHandler.cs | 29 +++ .../Handlers/Posts/Queries/GetPostsQuery.cs | 13 ++ .../Tags/Commands/CreateTagCommand.cs | 11 ++ .../Tags/Commands/CreateTagHandler.cs | 23 +++ .../Tags/Commands/DeleteTagCommand.cs | 10 + .../Tags/Commands/DeleteTagHandler.cs | 23 +++ .../Tags/Commands/UpdateTagCommand.cs | 11 ++ .../Tags/Commands/UpdateTagHandler.cs | 23 +++ .../Tags/Queries/GetTagByIdHandler.cs | 23 +++ .../Handlers/Tags/Queries/GetTagByIdQuery.cs | 10 + .../Tags/Queries/GetTagBySlugHandler.cs | 23 +++ .../Tags/Queries/GetTagBySlugQuery.cs | 10 + .../Handlers/Tags/Queries/GetTagsHandler.cs | 24 +++ .../Handlers/Tags/Queries/GetTagsQuery.cs | 12 ++ SampleWebApp.Core/Mapping/MappingProfile.cs | 63 +++++++ SampleWebApp.Core/SampleWebApp.Core.csproj | 16 ++ SampleWebApp.Core/Services/BlogService.cs | 108 +++++++++++ SampleWebApp.Core/Services/IBlogService.cs | 13 ++ SampleWebApp.Core/Services/ICreateService.cs | 12 ++ .../Services/IDbContextAdapter.cs | 58 ++++++ SampleWebApp.Core/Services/IDeleteService.cs | 11 ++ SampleWebApp.Core/Services/IDetailService.cs | 11 ++ SampleWebApp.Core/Services/IListService.cs | 16 ++ SampleWebApp.Core/Services/IPostService.cs | 18 ++ SampleWebApp.Core/Services/ITagService.cs | 15 ++ SampleWebApp.Core/Services/IUpdateService.cs | 13 ++ SampleWebApp.Core/Services/PostService.cs | 176 ++++++++++++++++++ SampleWebApp.Core/Services/TagService.cs | 114 ++++++++++++ SampleWebApp.Core/Validators/BlogValidator.cs | 27 +++ SampleWebApp.Core/Validators/PostValidator.cs | 45 +++++ SampleWebApp.Core/Validators/TagValidator.cs | 25 +++ SampleWebApp.sln | 98 ++++++++++ 64 files changed, 2080 insertions(+) create mode 100644 SampleWebApp.Core/Adapters/GenericServicesAdapter.cs create mode 100644 SampleWebApp.Core/Common/BaseDto.cs create mode 100644 SampleWebApp.Core/Common/Commands/BaseCommand.cs create mode 100644 SampleWebApp.Core/Common/Queries/BaseQuery.cs create mode 100644 SampleWebApp.Core/Common/Results/CreateResult.cs create mode 100644 SampleWebApp.Core/Common/Results/DeleteResult.cs create mode 100644 SampleWebApp.Core/Common/Results/PagedResult.cs create mode 100644 SampleWebApp.Core/Common/Results/SuccessOrErrors.cs create mode 100644 SampleWebApp.Core/Common/Results/UpdateResult.cs create mode 100644 SampleWebApp.Core/DTOs/BlogDto.cs create mode 100644 SampleWebApp.Core/DTOs/PostDto.cs create mode 100644 SampleWebApp.Core/DTOs/SimplePostDto.cs create mode 100644 SampleWebApp.Core/DTOs/TagDto.cs create mode 100644 SampleWebApp.Core/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Commands/CreateBlogCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Commands/CreateBlogHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Commands/DeleteBlogCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Commands/DeleteBlogHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Commands/UpdateBlogCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Commands/UpdateBlogHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogByIdHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogByIdQuery.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogsHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogsQuery.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Commands/CreatePostCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Commands/CreatePostHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Commands/DeletePostCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Commands/DeletePostHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Commands/UpdatePostCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Commands/UpdatePostHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Queries/GetPostByIdHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Queries/GetPostByIdQuery.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Queries/GetPostsHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Posts/Queries/GetPostsQuery.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Commands/CreateTagCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Commands/CreateTagHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Commands/DeleteTagCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Commands/DeleteTagHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Commands/UpdateTagCommand.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Commands/UpdateTagHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Queries/GetTagByIdHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Queries/GetTagByIdQuery.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Queries/GetTagBySlugHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Queries/GetTagBySlugQuery.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Queries/GetTagsHandler.cs create mode 100644 SampleWebApp.Core/Handlers/Tags/Queries/GetTagsQuery.cs create mode 100644 SampleWebApp.Core/Mapping/MappingProfile.cs create mode 100644 SampleWebApp.Core/SampleWebApp.Core.csproj create mode 100644 SampleWebApp.Core/Services/BlogService.cs create mode 100644 SampleWebApp.Core/Services/IBlogService.cs create mode 100644 SampleWebApp.Core/Services/ICreateService.cs create mode 100644 SampleWebApp.Core/Services/IDbContextAdapter.cs create mode 100644 SampleWebApp.Core/Services/IDeleteService.cs create mode 100644 SampleWebApp.Core/Services/IDetailService.cs create mode 100644 SampleWebApp.Core/Services/IListService.cs create mode 100644 SampleWebApp.Core/Services/IPostService.cs create mode 100644 SampleWebApp.Core/Services/ITagService.cs create mode 100644 SampleWebApp.Core/Services/IUpdateService.cs create mode 100644 SampleWebApp.Core/Services/PostService.cs create mode 100644 SampleWebApp.Core/Services/TagService.cs create mode 100644 SampleWebApp.Core/Validators/BlogValidator.cs create mode 100644 SampleWebApp.Core/Validators/PostValidator.cs create mode 100644 SampleWebApp.Core/Validators/TagValidator.cs diff --git a/SampleWebApp.Core/Adapters/GenericServicesAdapter.cs b/SampleWebApp.Core/Adapters/GenericServicesAdapter.cs new file mode 100644 index 0000000..5718a3a --- /dev/null +++ b/SampleWebApp.Core/Adapters/GenericServicesAdapter.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Handlers.Posts.Commands; +using SampleWebApp.Core.Handlers.Posts.Queries; + +namespace SampleWebApp.Core.Adapters +{ + public class GenericServicesAdapter + { + private readonly IMediator _mediator; + private readonly IMapper _mapper; + + public GenericServicesAdapter(IMediator mediator, IMapper mapper) + { + _mediator = mediator; + _mapper = mapper; + } + + public async Task> GetAllPosts() + { + var query = new GetPostsQuery(); + var result = await _mediator.Send(query); + return result.AsQueryable(); + } + + public async Task> GetPostsByBlogId(int blogId) + { + var query = new GetPostsQuery { BlogId = blogId }; + var result = await _mediator.Send(query); + return result.AsQueryable(); + } + + public async Task> GetPostDetail(int id) + { + try + { + var query = new GetPostByIdQuery { Id = id }; + var result = await _mediator.Send(query); + + if (result == null) + { + return SuccessOrErrors.FailSingleError("Post not found"); + } + + return SuccessOrErrors.Success(result); + } + catch (Exception ex) + { + return SuccessOrErrors.FailSingleError(ex.Message); + } + } + + public async Task> CreatePost(PostDto dto) + { + try + { + var command = new CreatePostCommand { Post = dto }; + var result = await _mediator.Send(command); + + if (!result.IsValid) + { + return SuccessOrErrors.FailSingleError(string.Join(", ", result.Errors)); + } + + dto.PostId = result.CreatedId; + return SuccessOrErrors.Success(dto, result.SuccessMessage); + } + catch (Exception ex) + { + return SuccessOrErrors.FailSingleError(ex.Message); + } + } + + public async Task UpdatePost(PostDto dto) + { + try + { + var command = new UpdatePostCommand { Post = dto }; + var result = await _mediator.Send(command); + + if (!result.IsValid) + { + return SuccessOrErrors.FailSingleError(string.Join(", ", result.Errors)); + } + + return SuccessOrErrors.Success(result.SuccessMessage); + } + catch (Exception ex) + { + return SuccessOrErrors.FailSingleError(ex.Message); + } + } + + public async Task DeletePost(int id) + { + try + { + var command = new DeletePostCommand { Id = id }; + var result = await _mediator.Send(command); + + if (!result.IsValid) + { + return SuccessOrErrors.FailSingleError(string.Join(", ", result.Errors)); + } + + return SuccessOrErrors.Success(result.SuccessMessage); + } + catch (Exception ex) + { + return SuccessOrErrors.FailSingleError(ex.Message); + } + } + + public async Task GetCreateDto() + { + return await Task.FromResult(new PostDto()); + } + + public async Task ResetDto(PostDto dto) + { + return await Task.FromResult(dto); + } + } +} diff --git a/SampleWebApp.Core/Common/BaseDto.cs b/SampleWebApp.Core/Common/BaseDto.cs new file mode 100644 index 0000000..2bffaeb --- /dev/null +++ b/SampleWebApp.Core/Common/BaseDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace SampleWebApp.Core.Common +{ + public abstract class BaseDto + { + [Key] + public int Id { get; set; } + } + + public abstract class BaseDto : BaseDto where TEntity : class + { + } +} diff --git a/SampleWebApp.Core/Common/Commands/BaseCommand.cs b/SampleWebApp.Core/Common/Commands/BaseCommand.cs new file mode 100644 index 0000000..e569fbf --- /dev/null +++ b/SampleWebApp.Core/Common/Commands/BaseCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace SampleWebApp.Core.Common.Commands +{ + public abstract class BaseCommand : IRequest + { + } +} diff --git a/SampleWebApp.Core/Common/Queries/BaseQuery.cs b/SampleWebApp.Core/Common/Queries/BaseQuery.cs new file mode 100644 index 0000000..d15f9a7 --- /dev/null +++ b/SampleWebApp.Core/Common/Queries/BaseQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace SampleWebApp.Core.Common.Queries +{ + public abstract class BaseQuery : IRequest where TResponse : class + { + } +} diff --git a/SampleWebApp.Core/Common/Results/CreateResult.cs b/SampleWebApp.Core/Common/Results/CreateResult.cs new file mode 100644 index 0000000..470ecc6 --- /dev/null +++ b/SampleWebApp.Core/Common/Results/CreateResult.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace SampleWebApp.Core.Common.Results +{ + public class CreateResult + { + public bool IsValid => Errors.Count == 0; + public string SuccessMessage { get; private set; } + public List Errors { get; private set; } + public int CreatedId { get; private set; } + + private CreateResult() + { + Errors = new List(); + } + + public static CreateResult Success(string message, int createdId = 0) + { + return new CreateResult + { + SuccessMessage = message, + CreatedId = createdId + }; + } + + public static CreateResult Fail(string error) + { + var result = new CreateResult(); + result.Errors.Add(error); + return result; + } + + public static CreateResult Fail(IEnumerable errors) + { + var result = new CreateResult(); + result.Errors.AddRange(errors); + return result; + } + } +} diff --git a/SampleWebApp.Core/Common/Results/DeleteResult.cs b/SampleWebApp.Core/Common/Results/DeleteResult.cs new file mode 100644 index 0000000..b7248b6 --- /dev/null +++ b/SampleWebApp.Core/Common/Results/DeleteResult.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace SampleWebApp.Core.Common.Results +{ + public class DeleteResult + { + public bool IsValid => Errors.Count == 0; + public string SuccessMessage { get; private set; } + public List Errors { get; private set; } + + private DeleteResult() + { + Errors = new List(); + } + + public static DeleteResult Success(string message) + { + return new DeleteResult + { + SuccessMessage = message + }; + } + + public static DeleteResult Fail(string error) + { + var result = new DeleteResult(); + result.Errors.Add(error); + return result; + } + + public static DeleteResult Fail(IEnumerable errors) + { + var result = new DeleteResult(); + result.Errors.AddRange(errors); + return result; + } + } +} diff --git a/SampleWebApp.Core/Common/Results/PagedResult.cs b/SampleWebApp.Core/Common/Results/PagedResult.cs new file mode 100644 index 0000000..6d536b8 --- /dev/null +++ b/SampleWebApp.Core/Common/Results/PagedResult.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace SampleWebApp.Core.Common.Results +{ + public class PagedResult where T : class + { + public IEnumerable Items { get; set; } + public int TotalCount { get; set; } + public int Page { get; set; } + public int PageSize { get; set; } + public int TotalPages => (TotalCount + PageSize - 1) / PageSize; + public bool HasPreviousPage => Page > 1; + public bool HasNextPage => Page < TotalPages; + + public PagedResult() + { + Items = new List(); + } + + public PagedResult(IEnumerable items, int totalCount, int page, int pageSize) + { + Items = items; + TotalCount = totalCount; + Page = page; + PageSize = pageSize; + } + } +} diff --git a/SampleWebApp.Core/Common/Results/SuccessOrErrors.cs b/SampleWebApp.Core/Common/Results/SuccessOrErrors.cs new file mode 100644 index 0000000..5cee5da --- /dev/null +++ b/SampleWebApp.Core/Common/Results/SuccessOrErrors.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SampleWebApp.Core.Common.Results +{ + public interface ISuccessOrErrors + { + bool IsValid { get; } + string SuccessMessage { get; } + IReadOnlyList Errors { get; } + string ErrorsAsHtml(); + } + + public interface ISuccessOrErrors : ISuccessOrErrors + { + T Result { get; } + } + + public class ValidationError + { + public string PropertyName { get; set; } + public string ErrorMessage { get; set; } + + public ValidationError(string propertyName, string errorMessage) + { + PropertyName = propertyName; + ErrorMessage = errorMessage; + } + + public ValidationError(string errorMessage) + { + PropertyName = string.Empty; + ErrorMessage = errorMessage; + } + } + + public class SuccessOrErrors : ISuccessOrErrors + { + private readonly List _errors; + + public bool IsValid => _errors.Count == 0; + public string SuccessMessage { get; private set; } + public IReadOnlyList Errors => _errors.AsReadOnly(); + + protected SuccessOrErrors() + { + _errors = new List(); + } + + public static SuccessOrErrors Success(string message) + { + return new SuccessOrErrors { SuccessMessage = message }; + } + + public static SuccessOrErrors FailSingleError(string errorMessage) + { + var result = new SuccessOrErrors(); + result._errors.Add(new ValidationError(errorMessage)); + return result; + } + + public static SuccessOrErrors FailSingleError(string propertyName, string errorMessage) + { + var result = new SuccessOrErrors(); + result._errors.Add(new ValidationError(propertyName, errorMessage)); + return result; + } + + public void AddNamedParameterError(string propertyName, string errorMessage) + { + _errors.Add(new ValidationError(propertyName, errorMessage)); + } + + public void AddError(string errorMessage) + { + _errors.Add(new ValidationError(errorMessage)); + } + + public string ErrorsAsHtml() + { + if (!_errors.Any()) + return string.Empty; + + var sb = new StringBuilder(); + sb.Append("
    "); + foreach (var error in _errors) + { + sb.Append("
  • "); + if (!string.IsNullOrEmpty(error.PropertyName)) + sb.Append($"{error.PropertyName}: "); + sb.Append(error.ErrorMessage); + sb.Append("
  • "); + } + sb.Append("
"); + return sb.ToString(); + } + } + + public class SuccessOrErrors : SuccessOrErrors, ISuccessOrErrors + { + public T Result { get; private set; } + + private SuccessOrErrors() { } + + public static SuccessOrErrors Success(T result, string message = null) + { + return new SuccessOrErrors + { + Result = result + }; + } + + public new static SuccessOrErrors FailSingleError(string errorMessage) + { + var result = new SuccessOrErrors(); + result.AddError(errorMessage); + return result; + } + + public new static SuccessOrErrors FailSingleError(string propertyName, string errorMessage) + { + var result = new SuccessOrErrors(); + result.AddNamedParameterError(propertyName, errorMessage); + return result; + } + + public static SuccessOrErrors ConvertNonResultStatus(ISuccessOrErrors status) + { + var result = new SuccessOrErrors(); + foreach (var error in status.Errors) + { + result.AddNamedParameterError(error.PropertyName, error.ErrorMessage); + } + return result; + } + } +} diff --git a/SampleWebApp.Core/Common/Results/UpdateResult.cs b/SampleWebApp.Core/Common/Results/UpdateResult.cs new file mode 100644 index 0000000..71b0438 --- /dev/null +++ b/SampleWebApp.Core/Common/Results/UpdateResult.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; + +namespace SampleWebApp.Core.Common.Results +{ + public class UpdateResult + { + public bool IsValid => Errors.Count == 0; + public string SuccessMessage { get; private set; } + public List Errors { get; private set; } + + private UpdateResult() + { + Errors = new List(); + } + + public static UpdateResult Success(string message) + { + return new UpdateResult + { + SuccessMessage = message + }; + } + + public static UpdateResult Fail(string error) + { + var result = new UpdateResult(); + result.Errors.Add(error); + return result; + } + + public static UpdateResult Fail(IEnumerable errors) + { + var result = new UpdateResult(); + result.Errors.AddRange(errors); + return result; + } + } +} diff --git a/SampleWebApp.Core/DTOs/BlogDto.cs b/SampleWebApp.Core/DTOs/BlogDto.cs new file mode 100644 index 0000000..48b5840 --- /dev/null +++ b/SampleWebApp.Core/DTOs/BlogDto.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using SampleWebApp.Core.Common; + +namespace SampleWebApp.Core.DTOs +{ + public class BlogDto : BaseDto + { + [UIHint("HiddenInput")] + public int BlogId { get; set; } + + [Required] + [MinLength(2)] + [MaxLength(64)] + public string Name { get; set; } + + [Required] + [MaxLength(256)] + [EmailAddress] + public string EmailAddress { get; set; } + + public int PostsCount { get; set; } + } +} diff --git a/SampleWebApp.Core/DTOs/PostDto.cs b/SampleWebApp.Core/DTOs/PostDto.cs new file mode 100644 index 0000000..6a46038 --- /dev/null +++ b/SampleWebApp.Core/DTOs/PostDto.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using SampleWebApp.Core.Common; + +namespace SampleWebApp.Core.DTOs +{ + public class PostDto : BaseDto + { + [UIHint("HiddenInput")] + [Key] + public int PostId { get; set; } + + [MinLength(2), MaxLength(128)] + public string Title { get; set; } + + [DataType(DataType.MultilineText)] + [Required] + public string Content { get; set; } + + public string BloggerName { get; set; } + + [ScaffoldColumn(false)] + public DateTime LastUpdated { get; set; } + + [UIHint("HiddenInput")] + public int BlogId { get; set; } + + [ScaffoldColumn(false)] + public ICollection Tags { get; set; } + + public DropDownListType Bloggers { get; set; } + + public MultiSelectListType UserChosenTags { get; set; } + + public DateTime LastUpdatedUtc => DateTime.SpecifyKind(LastUpdated, DateTimeKind.Utc); + + public string TagNames => Tags != null ? string.Join(", ", Tags.Select(x => x.Name)) : string.Empty; + + public PostDto() + { + Tags = new List(); + Bloggers = new DropDownListType(); + UserChosenTags = new MultiSelectListType(); + } + } + + public class DropDownListType + { + public List> KeyValueList { get; private set; } + + [Required] + public string SelectedValue { get; set; } + + public int? SelectedValueAsInt + { + get + { + if (int.TryParse(SelectedValue, out int result)) + return result; + return null; + } + } + + public DropDownListType() + { + KeyValueList = new List>(); + } + + public void SetupDropDownListContent(IEnumerable> keyValueList, string promptString) + { + KeyValueList = keyValueList.ToList(); + if (promptString != null) + KeyValueList.Insert(0, new KeyValuePair(promptString, null)); + else if (KeyValueList.Count > 0) + SelectedValue = KeyValueList[0].Value; + } + + public void SetSelectedValue(string valueAsString) + { + var foundEntry = KeyValueList.FirstOrDefault(x => x.Value == valueAsString); + SelectedValue = KeyValueList.Any(x => x.Value == valueAsString) + ? KeyValueList.First(x => x.Value == valueAsString).Value + : "--- select from list ---"; + } + } + + public class MultiSelectListType + { + public List> AllPossibleOptions { get; private set; } + + public List> InitialSelection { get; private set; } + + public string[] FinalSelection { get; set; } + + public MultiSelectListType() + { + AllPossibleOptions = new List>(); + InitialSelection = new List>(); + } + + public void SetupMultiSelectList(IEnumerable> allPossibleOptions, + IEnumerable> initialSelectionValues) + { + AllPossibleOptions = allPossibleOptions.ToList(); + InitialSelection = initialSelectionValues.ToList(); + FinalSelection = InitialSelection.Select(x => x.Value.ToString("D")).ToArray(); + } + + public int[] GetFinalSelectionAsInts() + { + var result = new List(); + if (FinalSelection == null) + return result.ToArray(); + + foreach (var intAsString in FinalSelection) + { + if (!int.TryParse(intAsString, out int id)) + throw new ArgumentException("One of the FinalSelection answers was not an integer"); + result.Add(id); + } + return result.ToArray(); + } + } +} diff --git a/SampleWebApp.Core/DTOs/SimplePostDto.cs b/SampleWebApp.Core/DTOs/SimplePostDto.cs new file mode 100644 index 0000000..0d6d62c --- /dev/null +++ b/SampleWebApp.Core/DTOs/SimplePostDto.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using SampleWebApp.Core.Common; + +namespace SampleWebApp.Core.DTOs +{ + public class SimplePostDto : BaseDto + { + [UIHint("HiddenInput")] + public int PostId { get; set; } + + [UIHint("HiddenInput")] + public int BlogId { get; set; } + + public string BloggerName { get; set; } + + [MinLength(2), MaxLength(128)] + public string Title { get; set; } + + [ScaffoldColumn(false)] + public ICollection Tags { get; set; } + + [ScaffoldColumn(false)] + public DateTime LastUpdated { get; set; } + + public DateTime LastUpdatedUtc => DateTime.SpecifyKind(LastUpdated, DateTimeKind.Utc); + + public string TagNames => Tags != null ? string.Join(", ", Tags.Select(x => x.Name)) : string.Empty; + + public SimplePostDto() + { + Tags = new List(); + } + } +} diff --git a/SampleWebApp.Core/DTOs/TagDto.cs b/SampleWebApp.Core/DTOs/TagDto.cs new file mode 100644 index 0000000..21cea14 --- /dev/null +++ b/SampleWebApp.Core/DTOs/TagDto.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using SampleWebApp.Core.Common; + +namespace SampleWebApp.Core.DTOs +{ + public class TagDto : BaseDto + { + [UIHint("HiddenInput")] + public int TagId { get; set; } + + [Required] + [MaxLength(64)] + [RegularExpression(@"\w*", ErrorMessage = "The slug must not contain spaces or non-alphanumeric characters.")] + public string Slug { get; set; } + + [Required] + [MaxLength(128)] + public string Name { get; set; } + + public int PostsCount { get; set; } + } +} diff --git a/SampleWebApp.Core/DependencyInjection/ServiceCollectionExtensions.cs b/SampleWebApp.Core/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..9a770c1 --- /dev/null +++ b/SampleWebApp.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +using FluentValidation; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using SampleWebApp.Core.Adapters; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Mapping; +using SampleWebApp.Core.Services; +using SampleWebApp.Core.Validators; + +namespace SampleWebApp.Core.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddCoreServices(this IServiceCollection services) + { + services.AddMediatR(typeof(ServiceCollectionExtensions).Assembly); + + services.AddAutoMapper(typeof(MappingProfile).Assembly); + + services.AddValidatorsFromAssembly(typeof(ServiceCollectionExtensions).Assembly); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + + services.AddScoped, PostValidator>(); + services.AddScoped, TagValidator>(); + services.AddScoped, BlogValidator>(); + + return services; + } + + public static IServiceCollection AddDbContextAdapter(this IServiceCollection services) + where TAdapter : class, IDbContextAdapter + { + services.AddScoped(); + return services; + } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Commands/CreateBlogCommand.cs b/SampleWebApp.Core/Handlers/Blogs/Commands/CreateBlogCommand.cs new file mode 100644 index 0000000..c283229 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Commands/CreateBlogCommand.cs @@ -0,0 +1,11 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Blogs.Commands +{ + public class CreateBlogCommand : BaseCommand + { + public BlogDto Blog { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Commands/CreateBlogHandler.cs b/SampleWebApp.Core/Handlers/Blogs/Commands/CreateBlogHandler.cs new file mode 100644 index 0000000..e1831a3 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Commands/CreateBlogHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Blogs.Commands +{ + public class CreateBlogHandler : IRequestHandler + { + private readonly IBlogService _blogService; + + public CreateBlogHandler(IBlogService blogService) + { + _blogService = blogService; + } + + public async Task Handle(CreateBlogCommand request, CancellationToken cancellationToken) + { + return await _blogService.CreateAsync(request.Blog); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Commands/DeleteBlogCommand.cs b/SampleWebApp.Core/Handlers/Blogs/Commands/DeleteBlogCommand.cs new file mode 100644 index 0000000..8a96028 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Commands/DeleteBlogCommand.cs @@ -0,0 +1,10 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; + +namespace SampleWebApp.Core.Handlers.Blogs.Commands +{ + public class DeleteBlogCommand : BaseCommand + { + public int Id { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Commands/DeleteBlogHandler.cs b/SampleWebApp.Core/Handlers/Blogs/Commands/DeleteBlogHandler.cs new file mode 100644 index 0000000..a1ec15a --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Commands/DeleteBlogHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Blogs.Commands +{ + public class DeleteBlogHandler : IRequestHandler + { + private readonly IBlogService _blogService; + + public DeleteBlogHandler(IBlogService blogService) + { + _blogService = blogService; + } + + public async Task Handle(DeleteBlogCommand request, CancellationToken cancellationToken) + { + return await _blogService.DeleteAsync(request.Id); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Commands/UpdateBlogCommand.cs b/SampleWebApp.Core/Handlers/Blogs/Commands/UpdateBlogCommand.cs new file mode 100644 index 0000000..244b1ae --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Commands/UpdateBlogCommand.cs @@ -0,0 +1,11 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Blogs.Commands +{ + public class UpdateBlogCommand : BaseCommand + { + public BlogDto Blog { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Commands/UpdateBlogHandler.cs b/SampleWebApp.Core/Handlers/Blogs/Commands/UpdateBlogHandler.cs new file mode 100644 index 0000000..ad36665 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Commands/UpdateBlogHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Blogs.Commands +{ + public class UpdateBlogHandler : IRequestHandler + { + private readonly IBlogService _blogService; + + public UpdateBlogHandler(IBlogService blogService) + { + _blogService = blogService; + } + + public async Task Handle(UpdateBlogCommand request, CancellationToken cancellationToken) + { + return await _blogService.UpdateAsync(request.Blog); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogByIdHandler.cs b/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogByIdHandler.cs new file mode 100644 index 0000000..5cd1b4a --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogByIdHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Blogs.Queries +{ + public class GetBlogByIdHandler : IRequestHandler + { + private readonly IBlogService _blogService; + + public GetBlogByIdHandler(IBlogService blogService) + { + _blogService = blogService; + } + + public async Task Handle(GetBlogByIdQuery request, CancellationToken cancellationToken) + { + return await _blogService.GetByIdAsync(request.Id); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogByIdQuery.cs b/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogByIdQuery.cs new file mode 100644 index 0000000..7bd3ca2 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogByIdQuery.cs @@ -0,0 +1,10 @@ +using SampleWebApp.Core.Common.Queries; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Blogs.Queries +{ + public class GetBlogByIdQuery : BaseQuery + { + public int Id { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogsHandler.cs b/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogsHandler.cs new file mode 100644 index 0000000..bbb543d --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogsHandler.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Blogs.Queries +{ + public class GetBlogsHandler : IRequestHandler> + { + private readonly IBlogService _blogService; + + public GetBlogsHandler(IBlogService blogService) + { + _blogService = blogService; + } + + public async Task> Handle(GetBlogsQuery request, CancellationToken cancellationToken) + { + return await _blogService.GetAllAsync(); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogsQuery.cs b/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogsQuery.cs new file mode 100644 index 0000000..a812a0d --- /dev/null +++ b/SampleWebApp.Core/Handlers/Blogs/Queries/GetBlogsQuery.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using SampleWebApp.Core.Common.Queries; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Blogs.Queries +{ + public class GetBlogsQuery : BaseQuery> + { + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Commands/CreatePostCommand.cs b/SampleWebApp.Core/Handlers/Posts/Commands/CreatePostCommand.cs new file mode 100644 index 0000000..386cc23 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Commands/CreatePostCommand.cs @@ -0,0 +1,11 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Posts.Commands +{ + public class CreatePostCommand : BaseCommand + { + public PostDto Post { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Commands/CreatePostHandler.cs b/SampleWebApp.Core/Handlers/Posts/Commands/CreatePostHandler.cs new file mode 100644 index 0000000..9bd15c7 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Commands/CreatePostHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Posts.Commands +{ + public class CreatePostHandler : IRequestHandler + { + private readonly IPostService _postService; + + public CreatePostHandler(IPostService postService) + { + _postService = postService; + } + + public async Task Handle(CreatePostCommand request, CancellationToken cancellationToken) + { + return await _postService.CreateAsync(request.Post); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Commands/DeletePostCommand.cs b/SampleWebApp.Core/Handlers/Posts/Commands/DeletePostCommand.cs new file mode 100644 index 0000000..86b6c38 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Commands/DeletePostCommand.cs @@ -0,0 +1,10 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; + +namespace SampleWebApp.Core.Handlers.Posts.Commands +{ + public class DeletePostCommand : BaseCommand + { + public int Id { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Commands/DeletePostHandler.cs b/SampleWebApp.Core/Handlers/Posts/Commands/DeletePostHandler.cs new file mode 100644 index 0000000..d6ffa51 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Commands/DeletePostHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Posts.Commands +{ + public class DeletePostHandler : IRequestHandler + { + private readonly IPostService _postService; + + public DeletePostHandler(IPostService postService) + { + _postService = postService; + } + + public async Task Handle(DeletePostCommand request, CancellationToken cancellationToken) + { + return await _postService.DeleteAsync(request.Id); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Commands/UpdatePostCommand.cs b/SampleWebApp.Core/Handlers/Posts/Commands/UpdatePostCommand.cs new file mode 100644 index 0000000..14a5555 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Commands/UpdatePostCommand.cs @@ -0,0 +1,11 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Posts.Commands +{ + public class UpdatePostCommand : BaseCommand + { + public PostDto Post { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Commands/UpdatePostHandler.cs b/SampleWebApp.Core/Handlers/Posts/Commands/UpdatePostHandler.cs new file mode 100644 index 0000000..543bd38 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Commands/UpdatePostHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Posts.Commands +{ + public class UpdatePostHandler : IRequestHandler + { + private readonly IPostService _postService; + + public UpdatePostHandler(IPostService postService) + { + _postService = postService; + } + + public async Task Handle(UpdatePostCommand request, CancellationToken cancellationToken) + { + return await _postService.UpdateAsync(request.Post); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Queries/GetPostByIdHandler.cs b/SampleWebApp.Core/Handlers/Posts/Queries/GetPostByIdHandler.cs new file mode 100644 index 0000000..fee3c62 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Queries/GetPostByIdHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Posts.Queries +{ + public class GetPostByIdHandler : IRequestHandler + { + private readonly IPostService _postService; + + public GetPostByIdHandler(IPostService postService) + { + _postService = postService; + } + + public async Task Handle(GetPostByIdQuery request, CancellationToken cancellationToken) + { + return await _postService.GetByIdAsync(request.Id); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Queries/GetPostByIdQuery.cs b/SampleWebApp.Core/Handlers/Posts/Queries/GetPostByIdQuery.cs new file mode 100644 index 0000000..99743d6 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Queries/GetPostByIdQuery.cs @@ -0,0 +1,10 @@ +using SampleWebApp.Core.Common.Queries; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Posts.Queries +{ + public class GetPostByIdQuery : BaseQuery + { + public int Id { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Queries/GetPostsHandler.cs b/SampleWebApp.Core/Handlers/Posts/Queries/GetPostsHandler.cs new file mode 100644 index 0000000..f6e3c0c --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Queries/GetPostsHandler.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Posts.Queries +{ + public class GetPostsHandler : IRequestHandler> + { + private readonly IPostService _postService; + + public GetPostsHandler(IPostService postService) + { + _postService = postService; + } + + public async Task> Handle(GetPostsQuery request, CancellationToken cancellationToken) + { + if (request.BlogId.HasValue) + { + return await _postService.GetSimpleByBlogIdAsync(request.BlogId.Value); + } + + return await _postService.GetAllSimpleAsync(); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Posts/Queries/GetPostsQuery.cs b/SampleWebApp.Core/Handlers/Posts/Queries/GetPostsQuery.cs new file mode 100644 index 0000000..2fb76dc --- /dev/null +++ b/SampleWebApp.Core/Handlers/Posts/Queries/GetPostsQuery.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using SampleWebApp.Core.Common.Queries; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Posts.Queries +{ + public class GetPostsQuery : BaseQuery> + { + public int? BlogId { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Commands/CreateTagCommand.cs b/SampleWebApp.Core/Handlers/Tags/Commands/CreateTagCommand.cs new file mode 100644 index 0000000..25ab572 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Commands/CreateTagCommand.cs @@ -0,0 +1,11 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Tags.Commands +{ + public class CreateTagCommand : BaseCommand + { + public TagDto Tag { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Commands/CreateTagHandler.cs b/SampleWebApp.Core/Handlers/Tags/Commands/CreateTagHandler.cs new file mode 100644 index 0000000..6c5b674 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Commands/CreateTagHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Tags.Commands +{ + public class CreateTagHandler : IRequestHandler + { + private readonly ITagService _tagService; + + public CreateTagHandler(ITagService tagService) + { + _tagService = tagService; + } + + public async Task Handle(CreateTagCommand request, CancellationToken cancellationToken) + { + return await _tagService.CreateAsync(request.Tag); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Commands/DeleteTagCommand.cs b/SampleWebApp.Core/Handlers/Tags/Commands/DeleteTagCommand.cs new file mode 100644 index 0000000..4039016 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Commands/DeleteTagCommand.cs @@ -0,0 +1,10 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; + +namespace SampleWebApp.Core.Handlers.Tags.Commands +{ + public class DeleteTagCommand : BaseCommand + { + public int Id { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Commands/DeleteTagHandler.cs b/SampleWebApp.Core/Handlers/Tags/Commands/DeleteTagHandler.cs new file mode 100644 index 0000000..6cef5a4 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Commands/DeleteTagHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Tags.Commands +{ + public class DeleteTagHandler : IRequestHandler + { + private readonly ITagService _tagService; + + public DeleteTagHandler(ITagService tagService) + { + _tagService = tagService; + } + + public async Task Handle(DeleteTagCommand request, CancellationToken cancellationToken) + { + return await _tagService.DeleteAsync(request.Id); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Commands/UpdateTagCommand.cs b/SampleWebApp.Core/Handlers/Tags/Commands/UpdateTagCommand.cs new file mode 100644 index 0000000..014df9f --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Commands/UpdateTagCommand.cs @@ -0,0 +1,11 @@ +using SampleWebApp.Core.Common.Commands; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Tags.Commands +{ + public class UpdateTagCommand : BaseCommand + { + public TagDto Tag { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Commands/UpdateTagHandler.cs b/SampleWebApp.Core/Handlers/Tags/Commands/UpdateTagHandler.cs new file mode 100644 index 0000000..86e1ed8 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Commands/UpdateTagHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Tags.Commands +{ + public class UpdateTagHandler : IRequestHandler + { + private readonly ITagService _tagService; + + public UpdateTagHandler(ITagService tagService) + { + _tagService = tagService; + } + + public async Task Handle(UpdateTagCommand request, CancellationToken cancellationToken) + { + return await _tagService.UpdateAsync(request.Tag); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Queries/GetTagByIdHandler.cs b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagByIdHandler.cs new file mode 100644 index 0000000..ac7719d --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagByIdHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Tags.Queries +{ + public class GetTagByIdHandler : IRequestHandler + { + private readonly ITagService _tagService; + + public GetTagByIdHandler(ITagService tagService) + { + _tagService = tagService; + } + + public async Task Handle(GetTagByIdQuery request, CancellationToken cancellationToken) + { + return await _tagService.GetByIdAsync(request.Id); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Queries/GetTagByIdQuery.cs b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagByIdQuery.cs new file mode 100644 index 0000000..25ac987 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagByIdQuery.cs @@ -0,0 +1,10 @@ +using SampleWebApp.Core.Common.Queries; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Tags.Queries +{ + public class GetTagByIdQuery : BaseQuery + { + public int Id { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Queries/GetTagBySlugHandler.cs b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagBySlugHandler.cs new file mode 100644 index 0000000..277f378 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagBySlugHandler.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Tags.Queries +{ + public class GetTagBySlugHandler : IRequestHandler + { + private readonly ITagService _tagService; + + public GetTagBySlugHandler(ITagService tagService) + { + _tagService = tagService; + } + + public async Task Handle(GetTagBySlugQuery request, CancellationToken cancellationToken) + { + return await _tagService.GetBySlugAsync(request.Slug); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Queries/GetTagBySlugQuery.cs b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagBySlugQuery.cs new file mode 100644 index 0000000..7c235f8 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagBySlugQuery.cs @@ -0,0 +1,10 @@ +using SampleWebApp.Core.Common.Queries; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Tags.Queries +{ + public class GetTagBySlugQuery : BaseQuery + { + public string Slug { get; set; } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Queries/GetTagsHandler.cs b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagsHandler.cs new file mode 100644 index 0000000..777e9c7 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagsHandler.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Handlers.Tags.Queries +{ + public class GetTagsHandler : IRequestHandler> + { + private readonly ITagService _tagService; + + public GetTagsHandler(ITagService tagService) + { + _tagService = tagService; + } + + public async Task> Handle(GetTagsQuery request, CancellationToken cancellationToken) + { + return await _tagService.GetAllAsync(); + } + } +} diff --git a/SampleWebApp.Core/Handlers/Tags/Queries/GetTagsQuery.cs b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagsQuery.cs new file mode 100644 index 0000000..f7b1b53 --- /dev/null +++ b/SampleWebApp.Core/Handlers/Tags/Queries/GetTagsQuery.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using SampleWebApp.Core.Common.Queries; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Handlers.Tags.Queries +{ + public class GetTagsQuery : BaseQuery> + { + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 10; + } +} diff --git a/SampleWebApp.Core/Mapping/MappingProfile.cs b/SampleWebApp.Core/Mapping/MappingProfile.cs new file mode 100644 index 0000000..876335f --- /dev/null +++ b/SampleWebApp.Core/Mapping/MappingProfile.cs @@ -0,0 +1,63 @@ +using AutoMapper; +using SampleWebApp.Core.DTOs; +using SampleWebApp.Core.Services; + +namespace SampleWebApp.Core.Mapping +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.PostId)) + .ForMember(dest => dest.PostId, opt => opt.MapFrom(src => src.PostId)) + .ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title)) + .ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content)) + .ForMember(dest => dest.BlogId, opt => opt.MapFrom(src => src.BlogId)) + .ForMember(dest => dest.BloggerName, opt => opt.MapFrom(src => src.BloggerName)) + .ForMember(dest => dest.LastUpdated, opt => opt.MapFrom(src => src.LastUpdated)) + .ForMember(dest => dest.Tags, opt => opt.MapFrom(src => src.Tags)) + .ForMember(dest => dest.Bloggers, opt => opt.Ignore()) + .ForMember(dest => dest.UserChosenTags, opt => opt.Ignore()); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.PostId)) + .ForMember(dest => dest.PostId, opt => opt.MapFrom(src => src.PostId)) + .ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title)) + .ForMember(dest => dest.BlogId, opt => opt.MapFrom(src => src.BlogId)) + .ForMember(dest => dest.BloggerName, opt => opt.MapFrom(src => src.BloggerName)) + .ForMember(dest => dest.LastUpdated, opt => opt.MapFrom(src => src.LastUpdated)) + .ForMember(dest => dest.Tags, opt => opt.MapFrom(src => src.Tags)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.TagId)) + .ForMember(dest => dest.TagId, opt => opt.MapFrom(src => src.TagId)) + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.Slug, opt => opt.MapFrom(src => src.Slug)) + .ForMember(dest => dest.PostsCount, opt => opt.MapFrom(src => src.PostsCount)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.BlogId)) + .ForMember(dest => dest.BlogId, opt => opt.MapFrom(src => src.BlogId)) + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.EmailAddress)) + .ForMember(dest => dest.PostsCount, opt => opt.MapFrom(src => src.PostsCount)); + + CreateMap() + .ForMember(dest => dest.PostId, opt => opt.MapFrom(src => src.PostId)) + .ForMember(dest => dest.Title, opt => opt.MapFrom(src => src.Title)) + .ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content)) + .ForMember(dest => dest.BlogId, opt => opt.MapFrom(src => src.BlogId)); + + CreateMap() + .ForMember(dest => dest.TagId, opt => opt.MapFrom(src => src.TagId)) + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.Slug, opt => opt.MapFrom(src => src.Slug)); + + CreateMap() + .ForMember(dest => dest.BlogId, opt => opt.MapFrom(src => src.BlogId)) + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.EmailAddress)); + } + } +} diff --git a/SampleWebApp.Core/SampleWebApp.Core.csproj b/SampleWebApp.Core/SampleWebApp.Core.csproj new file mode 100644 index 0000000..f4563bf --- /dev/null +++ b/SampleWebApp.Core/SampleWebApp.Core.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + + + + + + + + + + + + diff --git a/SampleWebApp.Core/Services/BlogService.cs b/SampleWebApp.Core/Services/BlogService.cs new file mode 100644 index 0000000..588c743 --- /dev/null +++ b/SampleWebApp.Core/Services/BlogService.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using AutoMapper; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Services +{ + public class BlogService : IBlogService + { + private readonly IMapper _mapper; + private readonly IDbContextAdapter _dbContext; + + public BlogService(IDbContextAdapter dbContext, IMapper mapper) + { + _dbContext = dbContext; + _mapper = mapper; + } + + public async Task> GetAllAsync() + { + var blogs = await _dbContext.GetAllBlogsAsync(); + return _mapper.Map>(blogs); + } + + public async Task> GetFilteredAsync(Expression> filter) + { + var allBlogs = await GetAllAsync(); + return allBlogs.AsQueryable().Where(filter); + } + + public async Task> GetPagedAsync(int page, int pageSize) + { + var allBlogs = await GetAllAsync(); + var blogsList = allBlogs.ToList(); + var pagedBlogs = blogsList.Skip((page - 1) * pageSize).Take(pageSize); + return new PagedResult(pagedBlogs, blogsList.Count, page, pageSize); + } + + public async Task GetByIdAsync(int id) + { + var blog = await _dbContext.GetBlogByIdAsync(id); + return _mapper.Map(blog); + } + + public async Task GetBySlugAsync(string slug) + { + return await Task.FromResult(null); + } + + public async Task CreateAsync(BlogDto dto) + { + try + { + var blogId = await _dbContext.CreateBlogAsync(dto.Name, dto.EmailAddress); + return CreateResult.Success($"Blog '{dto.Name}' created successfully", blogId); + } + catch (Exception ex) + { + return CreateResult.Fail(ex.Message); + } + } + + public async Task GetCreateDtoAsync() + { + return await Task.FromResult(new BlogDto()); + } + + public async Task UpdateAsync(BlogDto dto) + { + try + { + await _dbContext.UpdateBlogAsync(dto.BlogId, dto.Name, dto.EmailAddress); + return UpdateResult.Success($"Blog '{dto.Name}' updated successfully"); + } + catch (Exception ex) + { + return UpdateResult.Fail(ex.Message); + } + } + + public async Task GetUpdateDtoAsync(int id) + { + return await GetByIdAsync(id); + } + + public async Task ResetDtoAsync(BlogDto dto) + { + return await Task.FromResult(dto); + } + + public async Task DeleteAsync(int id) + { + try + { + await _dbContext.DeleteBlogAsync(id); + return DeleteResult.Success("Blog deleted successfully"); + } + catch (Exception ex) + { + return DeleteResult.Fail(ex.Message); + } + } + } +} diff --git a/SampleWebApp.Core/Services/IBlogService.cs b/SampleWebApp.Core/Services/IBlogService.cs new file mode 100644 index 0000000..115891f --- /dev/null +++ b/SampleWebApp.Core/Services/IBlogService.cs @@ -0,0 +1,13 @@ +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Services +{ + public interface IBlogService : + IListService, + IDetailService, + ICreateService, + IUpdateService, + IDeleteService + { + } +} diff --git a/SampleWebApp.Core/Services/ICreateService.cs b/SampleWebApp.Core/Services/ICreateService.cs new file mode 100644 index 0000000..a833056 --- /dev/null +++ b/SampleWebApp.Core/Services/ICreateService.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using SampleWebApp.Core.Common; +using SampleWebApp.Core.Common.Results; + +namespace SampleWebApp.Core.Services +{ + public interface ICreateService where TDto : BaseDto + { + Task CreateAsync(TDto dto); + Task GetCreateDtoAsync(); + } +} diff --git a/SampleWebApp.Core/Services/IDbContextAdapter.cs b/SampleWebApp.Core/Services/IDbContextAdapter.cs new file mode 100644 index 0000000..7c68573 --- /dev/null +++ b/SampleWebApp.Core/Services/IDbContextAdapter.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SampleWebApp.Core.Services +{ + public interface IDbContextAdapter + { + // Post operations + Task> GetPostsWithIncludesAsync(); + Task> GetPostsByBlogIdAsync(int blogId); + Task GetPostByIdAsync(int id); + Task CreatePostAsync(string title, string content, int blogId, int[] tagIds); + Task UpdatePostAsync(int postId, string title, string content, int blogId, int[] tagIds); + Task DeletePostAsync(int id); + + // Tag operations + Task> GetAllTagsAsync(); + Task GetTagByIdAsync(int id); + Task GetTagBySlugAsync(string slug); + Task CreateTagAsync(string name, string slug); + Task UpdateTagAsync(int tagId, string name, string slug); + Task DeleteTagAsync(int id); + + // Blog operations + Task> GetAllBlogsAsync(); + Task GetBlogByIdAsync(int id); + Task CreateBlogAsync(string name, string emailAddress); + Task UpdateBlogAsync(int blogId, string name, string emailAddress); + Task DeleteBlogAsync(int id); + } + + public class PostEntity + { + public int PostId { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public int BlogId { get; set; } + public string BloggerName { get; set; } + public System.DateTime LastUpdated { get; set; } + public ICollection Tags { get; set; } + } + + public class TagEntity + { + public int TagId { get; set; } + public string Name { get; set; } + public string Slug { get; set; } + public int PostsCount { get; set; } + } + + public class BlogEntity + { + public int BlogId { get; set; } + public string Name { get; set; } + public string EmailAddress { get; set; } + public int PostsCount { get; set; } + } +} diff --git a/SampleWebApp.Core/Services/IDeleteService.cs b/SampleWebApp.Core/Services/IDeleteService.cs new file mode 100644 index 0000000..e822c6c --- /dev/null +++ b/SampleWebApp.Core/Services/IDeleteService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using SampleWebApp.Core.Common; +using SampleWebApp.Core.Common.Results; + +namespace SampleWebApp.Core.Services +{ + public interface IDeleteService where TDto : BaseDto + { + Task DeleteAsync(int id); + } +} diff --git a/SampleWebApp.Core/Services/IDetailService.cs b/SampleWebApp.Core/Services/IDetailService.cs new file mode 100644 index 0000000..2123460 --- /dev/null +++ b/SampleWebApp.Core/Services/IDetailService.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using SampleWebApp.Core.Common; + +namespace SampleWebApp.Core.Services +{ + public interface IDetailService where TDto : BaseDto + { + Task GetByIdAsync(int id); + Task GetBySlugAsync(string slug); + } +} diff --git a/SampleWebApp.Core/Services/IListService.cs b/SampleWebApp.Core/Services/IListService.cs new file mode 100644 index 0000000..8241e0f --- /dev/null +++ b/SampleWebApp.Core/Services/IListService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using SampleWebApp.Core.Common; +using SampleWebApp.Core.Common.Results; + +namespace SampleWebApp.Core.Services +{ + public interface IListService where TDto : BaseDto + { + Task> GetAllAsync(); + Task> GetFilteredAsync(Expression> filter); + Task> GetPagedAsync(int page, int pageSize); + } +} diff --git a/SampleWebApp.Core/Services/IPostService.cs b/SampleWebApp.Core/Services/IPostService.cs new file mode 100644 index 0000000..6c28b08 --- /dev/null +++ b/SampleWebApp.Core/Services/IPostService.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Services +{ + public interface IPostService : + IListService, + IDetailService, + ICreateService, + IUpdateService, + IDeleteService + { + Task> GetByBlogIdAsync(int blogId); + Task> GetAllSimpleAsync(); + Task> GetSimpleByBlogIdAsync(int blogId); + } +} diff --git a/SampleWebApp.Core/Services/ITagService.cs b/SampleWebApp.Core/Services/ITagService.cs new file mode 100644 index 0000000..f83365b --- /dev/null +++ b/SampleWebApp.Core/Services/ITagService.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Services +{ + public interface ITagService : + IListService, + IDetailService, + ICreateService, + IUpdateService, + IDeleteService + { + new Task GetBySlugAsync(string slug); + } +} diff --git a/SampleWebApp.Core/Services/IUpdateService.cs b/SampleWebApp.Core/Services/IUpdateService.cs new file mode 100644 index 0000000..92ab331 --- /dev/null +++ b/SampleWebApp.Core/Services/IUpdateService.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using SampleWebApp.Core.Common; +using SampleWebApp.Core.Common.Results; + +namespace SampleWebApp.Core.Services +{ + public interface IUpdateService where TDto : BaseDto + { + Task UpdateAsync(TDto dto); + Task GetUpdateDtoAsync(int id); + Task ResetDtoAsync(TDto dto); + } +} diff --git a/SampleWebApp.Core/Services/PostService.cs b/SampleWebApp.Core/Services/PostService.cs new file mode 100644 index 0000000..b519a1c --- /dev/null +++ b/SampleWebApp.Core/Services/PostService.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using AutoMapper; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Services +{ + public class PostService : IPostService + { + private readonly IMapper _mapper; + private readonly IDbContextAdapter _dbContext; + + public PostService(IDbContextAdapter dbContext, IMapper mapper) + { + _dbContext = dbContext; + _mapper = mapper; + } + + public async Task> GetAllAsync() + { + var posts = await _dbContext.GetPostsWithIncludesAsync(); + return _mapper.Map>(posts); + } + + public async Task> GetFilteredAsync(Expression> filter) + { + var allPosts = await GetAllAsync(); + return allPosts.AsQueryable().Where(filter); + } + + public async Task> GetPagedAsync(int page, int pageSize) + { + var allPosts = await GetAllAsync(); + var postsList = allPosts.ToList(); + var pagedPosts = postsList.Skip((page - 1) * pageSize).Take(pageSize); + return new PagedResult(pagedPosts, postsList.Count, page, pageSize); + } + + public async Task GetByIdAsync(int id) + { + var post = await _dbContext.GetPostByIdAsync(id); + if (post == null) + return null; + + var dto = _mapper.Map(post); + await SetupSecondaryDataAsync(dto); + return dto; + } + + public async Task GetBySlugAsync(string slug) + { + return await Task.FromResult(null); + } + + public async Task CreateAsync(PostDto dto) + { + try + { + var tagIds = dto.UserChosenTags?.GetFinalSelectionAsInts() ?? new int[0]; + if (!tagIds.Any()) + return CreateResult.Fail("You must select at least one tag for the post."); + + var blogId = dto.Bloggers?.SelectedValueAsInt; + if (!blogId.HasValue) + return CreateResult.Fail("The blogger was not selected. You must do that before the post can be saved."); + + dto.BlogId = blogId.Value; + + var postId = await _dbContext.CreatePostAsync(dto.Title, dto.Content, dto.BlogId, tagIds); + return CreateResult.Success($"Post '{dto.Title}' created successfully", postId); + } + catch (Exception ex) + { + return CreateResult.Fail(ex.Message); + } + } + + public async Task GetCreateDtoAsync() + { + var dto = new PostDto(); + await SetupSecondaryDataAsync(dto); + return dto; + } + + public async Task UpdateAsync(PostDto dto) + { + try + { + var tagIds = dto.UserChosenTags?.GetFinalSelectionAsInts() ?? new int[0]; + if (!tagIds.Any()) + return UpdateResult.Fail("You must select at least one tag for the post."); + + var blogId = dto.Bloggers?.SelectedValueAsInt; + if (!blogId.HasValue) + return UpdateResult.Fail("The blogger was not selected. You must do that before the post can be saved."); + + dto.BlogId = blogId.Value; + + await _dbContext.UpdatePostAsync(dto.PostId, dto.Title, dto.Content, dto.BlogId, tagIds); + return UpdateResult.Success($"Post '{dto.Title}' updated successfully"); + } + catch (Exception ex) + { + return UpdateResult.Fail(ex.Message); + } + } + + public async Task GetUpdateDtoAsync(int id) + { + return await GetByIdAsync(id); + } + + public async Task ResetDtoAsync(PostDto dto) + { + await SetupSecondaryDataAsync(dto); + return dto; + } + + public async Task DeleteAsync(int id) + { + try + { + await _dbContext.DeletePostAsync(id); + return DeleteResult.Success("Post deleted successfully"); + } + catch (Exception ex) + { + return DeleteResult.Fail(ex.Message); + } + } + + public async Task> GetByBlogIdAsync(int blogId) + { + var posts = await _dbContext.GetPostsByBlogIdAsync(blogId); + return _mapper.Map>(posts); + } + + public async Task> GetAllSimpleAsync() + { + var posts = await _dbContext.GetPostsWithIncludesAsync(); + return _mapper.Map>(posts); + } + + public async Task> GetSimpleByBlogIdAsync(int blogId) + { + var posts = await _dbContext.GetPostsByBlogIdAsync(blogId); + return _mapper.Map>(posts); + } + + private async Task SetupSecondaryDataAsync(PostDto dto) + { + var blogs = await _dbContext.GetAllBlogsAsync(); + var tags = await _dbContext.GetAllTagsAsync(); + + dto.Bloggers.SetupDropDownListContent( + blogs.Select(b => new KeyValuePair(b.Name, b.BlogId.ToString("D"))), + "--- choose blogger ---"); + + if (dto.PostId != 0) + dto.Bloggers.SetSelectedValue(dto.BlogId.ToString("D")); + + var preselectedTags = dto.PostId == 0 + ? new List>() + : dto.Tags?.Select(t => new KeyValuePair(t.Name, t.TagId)).ToList() + ?? new List>(); + + dto.UserChosenTags.SetupMultiSelectList( + tags.Select(t => new KeyValuePair(t.Name, t.TagId)), + preselectedTags); + } + } +} diff --git a/SampleWebApp.Core/Services/TagService.cs b/SampleWebApp.Core/Services/TagService.cs new file mode 100644 index 0000000..b9c7145 --- /dev/null +++ b/SampleWebApp.Core/Services/TagService.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using AutoMapper; +using SampleWebApp.Core.Common.Results; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Services +{ + public class TagService : ITagService + { + private readonly IMapper _mapper; + private readonly IDbContextAdapter _dbContext; + + public TagService(IDbContextAdapter dbContext, IMapper mapper) + { + _dbContext = dbContext; + _mapper = mapper; + } + + public async Task> GetAllAsync() + { + var tags = await _dbContext.GetAllTagsAsync(); + return _mapper.Map>(tags); + } + + public async Task> GetFilteredAsync(Expression> filter) + { + var allTags = await GetAllAsync(); + return allTags.AsQueryable().Where(filter); + } + + public async Task> GetPagedAsync(int page, int pageSize) + { + var allTags = await GetAllAsync(); + var tagsList = allTags.ToList(); + var pagedTags = tagsList.Skip((page - 1) * pageSize).Take(pageSize); + return new PagedResult(pagedTags, tagsList.Count, page, pageSize); + } + + public async Task GetByIdAsync(int id) + { + var tag = await _dbContext.GetTagByIdAsync(id); + return _mapper.Map(tag); + } + + Task IDetailService.GetBySlugAsync(string slug) + { + return GetBySlugAsync(slug); + } + + public async Task GetBySlugAsync(string slug) + { + var tag = await _dbContext.GetTagBySlugAsync(slug); + return _mapper.Map(tag); + } + + public async Task CreateAsync(TagDto dto) + { + try + { + var tagId = await _dbContext.CreateTagAsync(dto.Name, dto.Slug); + return CreateResult.Success($"Tag '{dto.Name}' created successfully", tagId); + } + catch (Exception ex) + { + return CreateResult.Fail(ex.Message); + } + } + + public async Task GetCreateDtoAsync() + { + return await Task.FromResult(new TagDto()); + } + + public async Task UpdateAsync(TagDto dto) + { + try + { + await _dbContext.UpdateTagAsync(dto.TagId, dto.Name, dto.Slug); + return UpdateResult.Success($"Tag '{dto.Name}' updated successfully"); + } + catch (Exception ex) + { + return UpdateResult.Fail(ex.Message); + } + } + + public async Task GetUpdateDtoAsync(int id) + { + return await GetByIdAsync(id); + } + + public async Task ResetDtoAsync(TagDto dto) + { + return await Task.FromResult(dto); + } + + public async Task DeleteAsync(int id) + { + try + { + await _dbContext.DeleteTagAsync(id); + return DeleteResult.Success("Tag deleted successfully"); + } + catch (Exception ex) + { + return DeleteResult.Fail(ex.Message); + } + } + } +} diff --git a/SampleWebApp.Core/Validators/BlogValidator.cs b/SampleWebApp.Core/Validators/BlogValidator.cs new file mode 100644 index 0000000..f89ee87 --- /dev/null +++ b/SampleWebApp.Core/Validators/BlogValidator.cs @@ -0,0 +1,27 @@ +using FluentValidation; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Validators +{ + public class BlogValidator : AbstractValidator + { + public BlogValidator() + { + RuleFor(b => b.Name) + .NotEmpty() + .WithMessage("Name is required") + .MinimumLength(2) + .WithMessage("Name must be at least 2 characters") + .MaximumLength(64) + .WithMessage("Name cannot exceed 64 characters"); + + RuleFor(b => b.EmailAddress) + .NotEmpty() + .WithMessage("Email address is required") + .MaximumLength(256) + .WithMessage("Email address cannot exceed 256 characters") + .EmailAddress() + .WithMessage("Please enter a valid email address"); + } + } +} diff --git a/SampleWebApp.Core/Validators/PostValidator.cs b/SampleWebApp.Core/Validators/PostValidator.cs new file mode 100644 index 0000000..00a1567 --- /dev/null +++ b/SampleWebApp.Core/Validators/PostValidator.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Validators +{ + public class PostValidator : AbstractValidator + { + public PostValidator() + { + RuleFor(p => p.Title) + .NotEmpty() + .WithMessage("Title is required") + .MinimumLength(2) + .WithMessage("Title must be at least 2 characters") + .MaximumLength(128) + .WithMessage("Title cannot exceed 128 characters") + .Must(title => title == null || !title.Contains("!")) + .WithMessage("Sorry, but you can't get too excited and include a ! in the title.") + .Must(title => title == null || !title.EndsWith("?")) + .WithMessage("Sorry, but you can't ask a question, i.e. the title can't end with '?'."); + + RuleFor(p => p.Content) + .NotEmpty() + .WithMessage("Content is required") + .Must(content => content == null || !content.Contains(" sheep.")) + .WithMessage("Sorry. Not allowed to end a sentence with 'sheep'.") + .Must(content => content == null || !content.Contains(" lamb.")) + .WithMessage("Sorry. Not allowed to end a sentence with 'lamb'.") + .Must(content => content == null || !content.Contains(" cow.")) + .WithMessage("Sorry. Not allowed to end a sentence with 'cow'.") + .Must(content => content == null || !content.Contains(" calf.")) + .WithMessage("Sorry. Not allowed to end a sentence with 'calf'."); + + RuleFor(p => p.BlogId) + .GreaterThan(0) + .When(p => p.Bloggers?.SelectedValueAsInt == null) + .WithMessage("Please select a valid blog"); + + RuleFor(p => p.Tags) + .Must(tags => tags != null && tags.Count > 0) + .When(p => p.UserChosenTags?.FinalSelection == null || p.UserChosenTags.FinalSelection.Length == 0) + .WithMessage("At least one tag must be selected"); + } + } +} diff --git a/SampleWebApp.Core/Validators/TagValidator.cs b/SampleWebApp.Core/Validators/TagValidator.cs new file mode 100644 index 0000000..aeaf505 --- /dev/null +++ b/SampleWebApp.Core/Validators/TagValidator.cs @@ -0,0 +1,25 @@ +using FluentValidation; +using SampleWebApp.Core.DTOs; + +namespace SampleWebApp.Core.Validators +{ + public class TagValidator : AbstractValidator + { + public TagValidator() + { + RuleFor(t => t.Name) + .NotEmpty() + .WithMessage("Name is required") + .MaximumLength(128) + .WithMessage("Name cannot exceed 128 characters"); + + RuleFor(t => t.Slug) + .NotEmpty() + .WithMessage("Slug is required") + .MaximumLength(64) + .WithMessage("Slug cannot exceed 64 characters") + .Matches(@"^\w*$") + .WithMessage("The slug must not contain spaces or non-alphanumeric characters."); + } + } +} diff --git a/SampleWebApp.sln b/SampleWebApp.sln index b474189..f1cc563 100644 --- a/SampleWebApp.sln +++ b/SampleWebApp.sln @@ -17,45 +17,143 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWebApp.Core", "SampleWebApp.Core\SampleWebApp.Core.csproj", "{2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution AzureRelease|Any CPU = AzureRelease|Any CPU + AzureRelease|x64 = AzureRelease|x64 + AzureRelease|x86 = AzureRelease|x86 Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 WebWizRelease|Any CPU = WebWizRelease|Any CPU + WebWizRelease|x64 = WebWizRelease|x64 + WebWizRelease|x86 = WebWizRelease|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.AzureRelease|Any CPU.ActiveCfg = AzureRelease|Any CPU {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.AzureRelease|Any CPU.Build.0 = AzureRelease|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.AzureRelease|x64.ActiveCfg = AzureRelease|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.AzureRelease|x64.Build.0 = AzureRelease|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.AzureRelease|x86.ActiveCfg = AzureRelease|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.AzureRelease|x86.Build.0 = AzureRelease|Any CPU {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Debug|x64.Build.0 = Debug|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Debug|x86.Build.0 = Debug|Any CPU {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Release|Any CPU.ActiveCfg = Release|Any CPU {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Release|Any CPU.Build.0 = Release|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Release|x64.ActiveCfg = Release|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Release|x64.Build.0 = Release|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Release|x86.ActiveCfg = Release|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.Release|x86.Build.0 = Release|Any CPU {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.WebWizRelease|Any CPU.ActiveCfg = WebWizRelease|Any CPU {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.WebWizRelease|Any CPU.Build.0 = WebWizRelease|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.WebWizRelease|x64.ActiveCfg = WebWizRelease|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.WebWizRelease|x64.Build.0 = WebWizRelease|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.WebWizRelease|x86.ActiveCfg = WebWizRelease|Any CPU + {CFFEE5E0-3B99-46E0-9A82-2E74621C17C5}.WebWizRelease|x86.Build.0 = WebWizRelease|Any CPU {264E1878-12DE-4099-B8D7-CC53A73FEA49}.AzureRelease|Any CPU.ActiveCfg = AzureRelease|Any CPU {264E1878-12DE-4099-B8D7-CC53A73FEA49}.AzureRelease|Any CPU.Build.0 = AzureRelease|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.AzureRelease|x64.ActiveCfg = AzureRelease|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.AzureRelease|x64.Build.0 = AzureRelease|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.AzureRelease|x86.ActiveCfg = AzureRelease|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.AzureRelease|x86.Build.0 = AzureRelease|Any CPU {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Debug|x64.ActiveCfg = Debug|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Debug|x64.Build.0 = Debug|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Debug|x86.ActiveCfg = Debug|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Debug|x86.Build.0 = Debug|Any CPU {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Release|Any CPU.ActiveCfg = Release|Any CPU {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Release|Any CPU.Build.0 = Release|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Release|x64.ActiveCfg = Release|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Release|x64.Build.0 = Release|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Release|x86.ActiveCfg = Release|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.Release|x86.Build.0 = Release|Any CPU {264E1878-12DE-4099-B8D7-CC53A73FEA49}.WebWizRelease|Any CPU.ActiveCfg = WebWizRelease|Any CPU {264E1878-12DE-4099-B8D7-CC53A73FEA49}.WebWizRelease|Any CPU.Build.0 = WebWizRelease|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.WebWizRelease|x64.ActiveCfg = WebWizRelease|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.WebWizRelease|x64.Build.0 = WebWizRelease|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.WebWizRelease|x86.ActiveCfg = WebWizRelease|Any CPU + {264E1878-12DE-4099-B8D7-CC53A73FEA49}.WebWizRelease|x86.Build.0 = WebWizRelease|Any CPU {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.AzureRelease|Any CPU.ActiveCfg = AzureRelease|Any CPU {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.AzureRelease|Any CPU.Build.0 = AzureRelease|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.AzureRelease|x64.ActiveCfg = AzureRelease|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.AzureRelease|x64.Build.0 = AzureRelease|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.AzureRelease|x86.ActiveCfg = AzureRelease|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.AzureRelease|x86.Build.0 = AzureRelease|Any CPU {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Debug|x64.ActiveCfg = Debug|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Debug|x64.Build.0 = Debug|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Debug|x86.Build.0 = Debug|Any CPU {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Release|Any CPU.Build.0 = Release|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Release|x64.ActiveCfg = Release|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Release|x64.Build.0 = Release|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Release|x86.ActiveCfg = Release|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.Release|x86.Build.0 = Release|Any CPU {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.WebWizRelease|Any CPU.ActiveCfg = WebWizRelease|Any CPU {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.WebWizRelease|Any CPU.Build.0 = WebWizRelease|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.WebWizRelease|x64.ActiveCfg = WebWizRelease|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.WebWizRelease|x64.Build.0 = WebWizRelease|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.WebWizRelease|x86.ActiveCfg = WebWizRelease|Any CPU + {D2813927-0F38-43C3-B47C-AE8F00D50CAE}.WebWizRelease|x86.Build.0 = WebWizRelease|Any CPU {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.AzureRelease|Any CPU.ActiveCfg = AzureRelease|Any CPU {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.AzureRelease|Any CPU.Build.0 = AzureRelease|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.AzureRelease|x64.ActiveCfg = AzureRelease|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.AzureRelease|x64.Build.0 = AzureRelease|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.AzureRelease|x86.ActiveCfg = AzureRelease|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.AzureRelease|x86.Build.0 = AzureRelease|Any CPU {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Debug|x64.Build.0 = Debug|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Debug|x86.Build.0 = Debug|Any CPU {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Release|Any CPU.Build.0 = Release|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Release|x64.ActiveCfg = Release|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Release|x64.Build.0 = Release|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Release|x86.ActiveCfg = Release|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.Release|x86.Build.0 = Release|Any CPU {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.WebWizRelease|Any CPU.ActiveCfg = WebWizRelease|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.WebWizRelease|x64.ActiveCfg = WebWizRelease|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.WebWizRelease|x64.Build.0 = WebWizRelease|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.WebWizRelease|x86.ActiveCfg = WebWizRelease|Any CPU + {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}.WebWizRelease|x86.Build.0 = WebWizRelease|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.AzureRelease|Any CPU.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.AzureRelease|Any CPU.Build.0 = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.AzureRelease|x64.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.AzureRelease|x64.Build.0 = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.AzureRelease|x86.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.AzureRelease|x86.Build.0 = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Debug|x64.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Debug|x64.Build.0 = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Debug|x86.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Debug|x86.Build.0 = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Release|Any CPU.Build.0 = Release|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Release|x64.ActiveCfg = Release|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Release|x64.Build.0 = Release|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Release|x86.ActiveCfg = Release|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.Release|x86.Build.0 = Release|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.WebWizRelease|Any CPU.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.WebWizRelease|Any CPU.Build.0 = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.WebWizRelease|x64.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.WebWizRelease|x64.Build.0 = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.WebWizRelease|x86.ActiveCfg = Debug|Any CPU + {2594F708-AFBF-4EDF-B79D-C2A9F9FD05DA}.WebWizRelease|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE