订单接口和命令映射验证管道
This commit is contained in:
		
							parent
							
								
									780c489e02
								
							
						
					
					
						commit
						3cad5598b7
					
				@ -0,0 +1,18 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using AutoMapper;
 | 
			
		||||
using HelloShop.OrderingService.Commands.Orders;
 | 
			
		||||
using HelloShop.OrderingService.Models.Orders;
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.AutoMapper
 | 
			
		||||
{
 | 
			
		||||
    public class OrdersMapConfiguration : Profile
 | 
			
		||||
    {
 | 
			
		||||
        public OrdersMapConfiguration()
 | 
			
		||||
        {
 | 
			
		||||
            CreateMap<BasketItem, CreateOrderCommand.CreateOrderCommandItem>().ForMember(dest => dest.Units, opts => opts.MapFrom(src => src.Quantity));
 | 
			
		||||
            CreateMap<CreateOrderRequest, CreateOrderCommand>().ForMember(dest => dest.OrderItems, opt => opt.MapFrom(src => src.Items));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/HelloShop.OrderingService/Behaviors/LoggingBehavior.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/HelloShop.OrderingService/Behaviors/LoggingBehavior.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using HelloShop.OrderingService.Extensions;
 | 
			
		||||
using MediatR;
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Behaviors
 | 
			
		||||
{
 | 
			
		||||
    public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
 | 
			
		||||
    {
 | 
			
		||||
        public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogInformation("Handling command {CommandName} {Request}", request.GetGenericTypeName(), request);
 | 
			
		||||
 | 
			
		||||
            var response = await next();
 | 
			
		||||
 | 
			
		||||
            logger.LogInformation("Command {CommandName} handled {Response}", request.GetGenericTypeName(), response);
 | 
			
		||||
 | 
			
		||||
            return response;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								src/HelloShop.OrderingService/Behaviors/ValidatorBehavior.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/HelloShop.OrderingService/Behaviors/ValidatorBehavior.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using FluentValidation;
 | 
			
		||||
using HelloShop.OrderingService.Extensions;
 | 
			
		||||
using MediatR;
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Behaviors
 | 
			
		||||
{
 | 
			
		||||
    public class ValidatorBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators, ILogger<ValidatorBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
 | 
			
		||||
    {
 | 
			
		||||
        public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
 | 
			
		||||
        {
 | 
			
		||||
            string typeName = request.GetGenericTypeName();
 | 
			
		||||
 | 
			
		||||
            logger.LogInformation("Validating command {CommandType}", typeName);
 | 
			
		||||
 | 
			
		||||
            var failures = validators.Select(v => v.Validate(request)).SelectMany(result => result.Errors).Where(error => error != null).ToList();
 | 
			
		||||
 | 
			
		||||
            if (failures.Count != 0)
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogWarning("Validation errors {CommandType} Command {Command} Errors {ValidationErrors}", typeName, request, failures);
 | 
			
		||||
                throw new ApplicationException($"Command Validation Errors for type {typeof(TRequest).Name}", new ValidationException("Command Validation exception", failures));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return await next();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,65 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using AutoMapper;
 | 
			
		||||
using HelloShop.OrderingService.Commands;
 | 
			
		||||
using HelloShop.OrderingService.Commands.Orders;
 | 
			
		||||
using HelloShop.OrderingService.Models.Orders;
 | 
			
		||||
using MediatR;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Controllers
 | 
			
		||||
{
 | 
			
		||||
    [Route("api/[controller]")]
 | 
			
		||||
    [ApiController]
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    public class OrdersController(ILogger<OrdersController> logger, IMediator mediator, IMapper mapper) : ControllerBase
 | 
			
		||||
    {
 | 
			
		||||
        [HttpPost]
 | 
			
		||||
        public async Task<IActionResult> CreateOrder([FromHeader(Name = "x-request-id")] Guid requestId, CreateOrderRequest request)
 | 
			
		||||
        {
 | 
			
		||||
            using (logger.BeginScope(new List<KeyValuePair<string, object>> { new("IdentifiedCommandId", requestId) }))
 | 
			
		||||
            {
 | 
			
		||||
                string? nameIdentifier = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
 | 
			
		||||
                string? userName = User.FindFirst(ClaimTypes.Name)?.Value;
 | 
			
		||||
 | 
			
		||||
                if (!int.TryParse(nameIdentifier, out int userId) || string.IsNullOrWhiteSpace(userName))
 | 
			
		||||
                {
 | 
			
		||||
                    throw new InvalidOperationException("User id or name not found in claims.");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                CreateOrderCommand createOrderCommand = mapper.Map<CreateOrderCommand>(request, opts => opts.AfterMap((src, dest) =>
 | 
			
		||||
                {
 | 
			
		||||
                    dest.UserId = userId;
 | 
			
		||||
                    dest.UserName = userName;
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
                var createOrderIdentifiedCommand = new IdentifiedCommand<CreateOrderCommand, bool>(createOrderCommand, requestId);
 | 
			
		||||
 | 
			
		||||
                logger.LogInformation("Sending create order command.");
 | 
			
		||||
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var result = await mediator.Send(createOrderIdentifiedCommand);
 | 
			
		||||
                    return result ? Ok() : Problem(detail: "Create order failed to process.", statusCode: 500);
 | 
			
		||||
 | 
			
		||||
                }
 | 
			
		||||
                catch (ApplicationException ex) when (ex.InnerException is FluentValidation.ValidationException validationException)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.LogWarning("Validation error in create order command.");
 | 
			
		||||
 | 
			
		||||
                    ModelStateDictionary modelState = validationException.Errors.Aggregate(ModelState, (acc, error) =>
 | 
			
		||||
                    {
 | 
			
		||||
                        acc.AddModelError(error.PropertyName, error.ErrorMessage);
 | 
			
		||||
                        return acc;
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    return ValidationProblem(modelState);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Controllers;
 | 
			
		||||
 | 
			
		||||
[ApiController]
 | 
			
		||||
[Route("[controller]")]
 | 
			
		||||
public class WeatherForecastController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static readonly string[] Summaries = new[]
 | 
			
		||||
    {
 | 
			
		||||
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    private readonly ILogger<WeatherForecastController> _logger;
 | 
			
		||||
 | 
			
		||||
    public WeatherForecastController(ILogger<WeatherForecastController> logger)
 | 
			
		||||
    {
 | 
			
		||||
        _logger = logger;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [HttpGet(Name = "GetWeatherForecast")]
 | 
			
		||||
    public IEnumerable<WeatherForecast> Get()
 | 
			
		||||
    {
 | 
			
		||||
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
 | 
			
		||||
        {
 | 
			
		||||
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
 | 
			
		||||
            TemperatureC = Random.Shared.Next(-20, 55),
 | 
			
		||||
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
 | 
			
		||||
        })
 | 
			
		||||
        .ToArray();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using HelloShop.OrderingService.Behaviors;
 | 
			
		||||
using HelloShop.OrderingService.Constants;
 | 
			
		||||
using HelloShop.OrderingService.Infrastructure;
 | 
			
		||||
using HelloShop.OrderingService.Services;
 | 
			
		||||
@ -26,6 +27,8 @@ namespace HelloShop.OrderingService.Extensions
 | 
			
		||||
            builder.Services.AddMediatR(options =>
 | 
			
		||||
            {
 | 
			
		||||
                options.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
 | 
			
		||||
                options.AddBehavior(typeof(LoggingBehavior<,>));
 | 
			
		||||
                options.AddBehavior(typeof(ValidatorBehavior<,>));
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,30 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Extensions
 | 
			
		||||
{
 | 
			
		||||
    public static class GenericTypeExtensions
 | 
			
		||||
    {
 | 
			
		||||
        public static string GetGenericTypeName(this Type type)
 | 
			
		||||
        {
 | 
			
		||||
            string typeName;
 | 
			
		||||
 | 
			
		||||
            if (type.IsGenericType)
 | 
			
		||||
            {
 | 
			
		||||
                var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray());
 | 
			
		||||
                typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>";
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                typeName = type.Name;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return typeName;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static string GetGenericTypeName(this object @object)
 | 
			
		||||
        {
 | 
			
		||||
            return @object.GetType().GetGenericTypeName();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/HelloShop.OrderingService/Models/Orders/BasketItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/HelloShop.OrderingService/Models/Orders/BasketItem.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Models.Orders
 | 
			
		||||
{
 | 
			
		||||
    public class BasketItem
 | 
			
		||||
    {
 | 
			
		||||
        public int ProductId { get; set; }
 | 
			
		||||
 | 
			
		||||
        public int Quantity { get; set; }
 | 
			
		||||
 | 
			
		||||
        public required string ProductName { get; set; }
 | 
			
		||||
 | 
			
		||||
        public required string PictureUrl { get; set; }
 | 
			
		||||
 | 
			
		||||
        public decimal UnitPrice { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,30 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Models.Orders
 | 
			
		||||
{
 | 
			
		||||
    public class CreateOrderRequest
 | 
			
		||||
    {
 | 
			
		||||
        public required List<BasketItem> Items { get; init; }
 | 
			
		||||
 | 
			
		||||
        public required string Country { get; init; }
 | 
			
		||||
 | 
			
		||||
        public required string State { get; init; }
 | 
			
		||||
 | 
			
		||||
        public required string City { get; init; }
 | 
			
		||||
 | 
			
		||||
        public required string Street { get; init; }
 | 
			
		||||
 | 
			
		||||
        public required string ZipCode { get; init; }
 | 
			
		||||
 | 
			
		||||
        public required string CardAlias { get; init; }
 | 
			
		||||
 | 
			
		||||
        public required string CardNumber { get; init; }
 | 
			
		||||
 | 
			
		||||
        public required string CardHolderName { get; init; }
 | 
			
		||||
 | 
			
		||||
        public string? CardSecurityNumber { get; init; }
 | 
			
		||||
 | 
			
		||||
        public DateTimeOffset? CardExpiration { get; init; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using FluentValidation;
 | 
			
		||||
using HelloShop.OrderingService.Commands.Orders;
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Validations.Commands
 | 
			
		||||
{
 | 
			
		||||
    public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
 | 
			
		||||
    {
 | 
			
		||||
        public CreateOrderCommandValidator()
 | 
			
		||||
        {
 | 
			
		||||
            RuleFor(x => x.UserId).GreaterThan(0);
 | 
			
		||||
            RuleFor(x => x.UserName).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.OrderItems).NotNull().NotEmpty();
 | 
			
		||||
            RuleForEach(x => x.OrderItems).SetValidator(new BasketListItemValidator());
 | 
			
		||||
            RuleFor(x => x.Country).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.State).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.City).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.Street).NotNull().NotEmpty().MaximumLength(32);
 | 
			
		||||
            RuleFor(x => x.ZipCode).NotNull().NotEmpty().Length(6).Must(x => x.All(char.IsNumber));
 | 
			
		||||
            RuleFor(x => x.CardAlias).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.CardNumber).NotNull().NotEmpty().Length(16).Must(x => x.All(char.IsNumber));
 | 
			
		||||
            RuleFor(x => x.CardHolderName).NotNull().NotEmpty().Length(1, 8);
 | 
			
		||||
            RuleFor(x => x.CardSecurityNumber).NotNull().NotEmpty().Length(6).Must(x => x.All(char.IsNumber));
 | 
			
		||||
            RuleFor(x => x.CardExpiration).Must(x => x.HasValue && x.Value > DateTimeOffset.Now);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class BasketListItemValidator : AbstractValidator<CreateOrderCommand.CreateOrderCommandItem>
 | 
			
		||||
        {
 | 
			
		||||
            public BasketListItemValidator()
 | 
			
		||||
            {
 | 
			
		||||
                RuleFor(x => x.ProductId).GreaterThan(0);
 | 
			
		||||
                RuleFor(x => x.ProductName).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
                RuleFor(x => x.PictureUrl).NotNull().NotEmpty().Length(1, 256);
 | 
			
		||||
                RuleFor(x => x.ProductId).GreaterThan(0);
 | 
			
		||||
                RuleFor(x => x.UnitPrice).GreaterThan(0);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using FluentValidation;
 | 
			
		||||
using HelloShop.OrderingService.Commands;
 | 
			
		||||
using HelloShop.OrderingService.Commands.Orders;
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Validations.Commands
 | 
			
		||||
{
 | 
			
		||||
    public class CreateOrderIdentifiedCommandValidator : AbstractValidator<IdentifiedCommand<CreateOrderCommand, bool>>
 | 
			
		||||
    {
 | 
			
		||||
        public CreateOrderIdentifiedCommandValidator() => RuleFor(command => command.Id).Must(id => id != Guid.Empty).WithMessage("Invalid x-request-id in the request header.");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,39 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
using FluentValidation;
 | 
			
		||||
using HelloShop.OrderingService.Models.Orders;
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService.Validations.Models
 | 
			
		||||
{
 | 
			
		||||
    public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
 | 
			
		||||
    {
 | 
			
		||||
        public CreateOrderRequestValidator()
 | 
			
		||||
        {
 | 
			
		||||
            RuleFor(x => x.Items).NotNull().NotEmpty();
 | 
			
		||||
            RuleForEach(x => x.Items).SetValidator(new BasketListItemValidator());
 | 
			
		||||
            RuleFor(x => x.Country).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.State).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.City).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.Street).NotNull().NotEmpty().MaximumLength(32);
 | 
			
		||||
            RuleFor(x => x.ZipCode).NotNull().NotEmpty().Length(6).Must(x => x.All(char.IsNumber));
 | 
			
		||||
            RuleFor(x => x.CardAlias).NotNull().NotEmpty().Length(1, 16);
 | 
			
		||||
            RuleFor(x => x.CardNumber).NotNull().NotEmpty().Length(16).Must(x => x.All(char.IsNumber));
 | 
			
		||||
            RuleFor(x => x.CardHolderName).NotNull().NotEmpty().Length(1, 8);
 | 
			
		||||
            RuleFor(x => x.CardSecurityNumber).NotNull().NotEmpty().Length(6).Must(x => x.All(char.IsNumber));
 | 
			
		||||
            RuleFor(x => x.CardExpiration).Must(x => x.HasValue && x.Value > DateTimeOffset.Now);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public class BasketListItemValidator : AbstractValidator<BasketItem>
 | 
			
		||||
        {
 | 
			
		||||
            public BasketListItemValidator()
 | 
			
		||||
            {
 | 
			
		||||
                RuleFor(x => x.ProductId).GreaterThan(0);
 | 
			
		||||
                RuleFor(x => x.Quantity).GreaterThan(0);
 | 
			
		||||
                RuleFor(x => x.ProductName).Length(2, 16);
 | 
			
		||||
                RuleFor(x => x.PictureUrl).MaximumLength(256);
 | 
			
		||||
                RuleFor(x => x.UnitPrice).GreaterThan(0);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
// Copyright (c) HelloShop Corporation. All rights reserved.
 | 
			
		||||
// See the license file in the project root for more information.
 | 
			
		||||
 | 
			
		||||
namespace HelloShop.OrderingService;
 | 
			
		||||
 | 
			
		||||
public class WeatherForecast
 | 
			
		||||
{
 | 
			
		||||
    public DateOnly Date { get; set; }
 | 
			
		||||
 | 
			
		||||
    public int TemperatureC { get; set; }
 | 
			
		||||
 | 
			
		||||
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
 | 
			
		||||
 | 
			
		||||
    public string? Summary { get; set; }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user