Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7732045
add domain models and enums
AnnaVerh04 Sep 22, 2025
d6d103d
Adding test data about a counterparty
AnnaVerh04 Sep 23, 2025
ee1641c
Adding test data about real estate and requests
AnnaVerh04 Sep 23, 2025
cdceb19
Writing tests
AnnaVerh04 Sep 24, 2025
b4726fa
adding comments
AnnaVerh04 Sep 24, 2025
9c21073
Adding results and descriptions
AnnaVerh04 Sep 24, 2025
b47b7a0
Making changes
AnnaVerh04 Sep 30, 2025
d7880b5
Correction of comments
AnnaVerh04 Oct 1, 2025
d16c29a
fix
AnnaVerh04 Oct 1, 2025
35beeff
Setting up an action
AnnaVerh04 Oct 1, 2025
878d4c6
new yml
AnnaVerh04 Oct 1, 2025
a906e7f
error search
AnnaVerh04 Oct 1, 2025
e183893
fix
AnnaVerh04 Oct 1, 2025
7482a48
new yml
AnnaVerh04 Oct 1, 2025
3282a22
start for next lab
AnnaVerh04 Dec 7, 2025
49f799c
DTO development
AnnaVerh04 Dec 7, 2025
b5659bc
Implementing repositories
AnnaVerh04 Dec 7, 2025
4d67fd5
Add AutoMapper configuration
AnnaVerh04 Dec 8, 2025
9116445
Creating controllers and a separate service for analytical queries
AnnaVerh04 Dec 8, 2025
dfc0930
developing tests and verifying the correctness of the results
AnnaVerh04 Dec 8, 2025
3aebced
Configuring projects and enabling MongoDB
AnnaVerh04 Dec 9, 2025
86510cf
Implementing MongoDB repositories
AnnaVerh04 Dec 10, 2025
0675297
Creating a service to fill the database with initial data
AnnaVerh04 Dec 10, 2025
5ec4c07
Support for two operating modes
AnnaVerh04 Dec 10, 2025
961ca89
Integration tests for MongoDB and verification of results
AnnaVerh04 Dec 10, 2025
cde8a6c
Error Correction
AnnaVerh04 Dec 18, 2025
c923bb8
Update Readme
AnnaVerh04 Dec 18, 2025
9b57a91
Correction of comments
AnnaVerh04 Dec 22, 2025
a4a2927
Making edits to the RealEstateDbContext constructor
AnnaVerh04 Dec 23, 2025
eaab0d9
lab4 done
AnnaVerh04 Dec 25, 2025
4726d10
fix
AnnaVerh04 Dec 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/dotnet-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: .NET Tests Simple

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Run tests directly
run: |
cd RealEstateAgency.tests
dotnet restore
dotnet build
dotnet test --verbosity normal
213 changes: 80 additions & 133 deletions README.md

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions RealEstateAgency.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
var builder = DistributedApplication.CreateBuilder(args);

// MongoDB
var mongodb = builder.AddMongoDB("mongodb")
.WithDataVolume("mongodb-data");

var mongoDatabase = mongodb.AddDatabase("realestatedb");

// NATS
var nats = builder.AddNats("nats")
.WithJetStream()
.WithDataVolume("nats-data");

// WebApi
builder.AddProject<Projects.RealEstateAgency_WebApi>("webapi")
.WithReference(mongoDatabase)
.WithReference(nats)
.WaitFor(mongoDatabase)
.WaitFor(nats)
.WithExternalHttpEndpoints();

// Generator
builder.AddProject<Projects.RealEstateAgency_Generator>("generator")
.WithReference(nats)
.WaitFor(nats)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В целом если вам нужны какие то адреса - можно просто через переменные окружения их пробрасывать:

Suggested change
.WaitFor(nats)
.WaitFor(nats)
.WithEnvironment("Nats__Url",nats.GetEndpoint("tcp"))

Тут же вам было бы достаточно обратиться к ConnectionString с именем "nats"

.WaitFor(mongoDatabase);

builder.Build().Run();
29 changes: 29 additions & 0 deletions RealEstateAgency.AppHost/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17197;http://localhost:15247",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21093",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22056"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15247",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19133",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20054"
}
}
}
}
25 changes: 25 additions & 0 deletions RealEstateAgency.AppHost/RealEstateAgency.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.2" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>e80bf9ea-04bf-4190-adb9-7d9a244a2049</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.2" />
<PackageReference Include="Aspire.Hosting.MongoDB" Version="9.5.2" />
<PackageReference Include="Aspire.Hosting.Nats" Version="9.5.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RealEstateAgency.Generator\RealEstateAgency.Generator.csproj" />
<ProjectReference Include="..\RealEstateAgency.ServiceDefaults\RealEstateAgency.ServiceDefaults.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\RealEstateAgency.WebApi\RealEstateAgency.WebApi.csproj" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions RealEstateAgency.AppHost/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions RealEstateAgency.AppHost/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
30 changes: 30 additions & 0 deletions RealEstateAgency.Application/Mapping/MappingProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using AutoMapper;
using RealEstateAgency.Contracts.Dto;
using RealEstateAgency.Domain.Models;

namespace RealEstateAgency.Application.Mapping;

/// <summary>
/// Профиль AutoMapper для маппинга сущностей и DTO
/// </summary>
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Counterparty, CounterpartyDto>();
CreateMap<CreateCounterpartyDto, Counterparty>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
CreateMap<UpdateCounterpartyDto, Counterparty>()
.ForMember(dest => dest.Id, opt => opt.Ignore());

CreateMap<RealEstateProperty, RealEstatePropertyDto>();
CreateMap<CreateRealEstatePropertyDto, RealEstateProperty>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
CreateMap<UpdateRealEstatePropertyDto, RealEstateProperty>()
.ForMember(dest => dest.Id, opt => opt.Ignore());

CreateMap<Request, RequestDto>()
.ForMember(dest => dest.CounterpartyId, opt => opt.MapFrom(src => src.Counterparty.Id))
.ForMember(dest => dest.PropertyId, opt => opt.MapFrom(src => src.Property.Id));
}
}
18 changes: 18 additions & 0 deletions RealEstateAgency.Application/RealEstateAgency.Application.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RealEstateAgency.Contracts\RealEstateAgency.Contracts.csproj" />
</ItemGroup>

</Project>
148 changes: 148 additions & 0 deletions RealEstateAgency.Application/Services/AnalyticsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using RealEstateAgency.Contracts.Dto;
using RealEstateAgency.Contracts.Interfaces;
using RealEstateAgency.Domain.Enums;
using RealEstateAgency.Domain.Interfaces;

namespace RealEstateAgency.Application.Services;

/// <summary>
/// Реализация сервиса аналитики
/// </summary>
public class AnalyticsService(
IRequestRepository requestRepository,
ICounterpartyRepository counterpartyRepository,
IRealEstatePropertyRepository propertyRepository) : IAnalyticsService
{

/// <summary>
/// Получить продавцов за указанный период
/// </summary>
public async Task<IEnumerable<string>> GetSellersInPeriodAsync(DateTime startDate, DateTime endDate)
{
var requests = (await requestRepository.GetAllAsync()).ToList();
var counterparties = (await counterpartyRepository.GetAllAsync()).ToDictionary(c => c.Id);

return
[
.. requests
.Where(r => r.Type == RequestType.Sale &&
r.Date >= startDate &&
r.Date <= endDate)
.Select(r => counterparties.TryGetValue(r.CounterpartyId, out var c) ? c.FullName : r.Counterparty?.FullName)
.Where(name => !string.IsNullOrEmpty(name))
.Distinct()
.Order()
];
}

/// <summary>
/// Получить топ-5 клиентов по количеству заявок
/// </summary>
public async Task<Top5ClientsResultDto> GetTop5ClientsByRequestCountAsync()
{
var requests = (await requestRepository.GetAllAsync()).ToList();
var counterparties = (await counterpartyRepository.GetAllAsync()).ToDictionary(c => c.Id);

var topPurchaseClients = requests
.Where(r => r.Type == RequestType.Purchase)
.GroupBy(r => r.CounterpartyId)
.Select(g => new TopClientDto
{
FullName = counterparties.TryGetValue(g.Key, out var c) ? c.FullName : g.First().Counterparty?.FullName ?? "",
RequestCount = g.Count()
})
.Where(x => !string.IsNullOrEmpty(x.FullName))
.OrderByDescending(x => x.RequestCount)
.ThenBy(x => x.FullName)
.Take(5);

var topSaleClients = requests
.Where(r => r.Type == RequestType.Sale)
.GroupBy(r => r.CounterpartyId)
.Select(g => new TopClientDto
{
FullName = counterparties.TryGetValue(g.Key, out var c) ? c.FullName : g.First().Counterparty?.FullName ?? "",
RequestCount = g.Count()
})
.Where(x => !string.IsNullOrEmpty(x.FullName))
.OrderByDescending(x => x.RequestCount)
.ThenBy(x => x.FullName)
.Take(5);

return new Top5ClientsResultDto
{
TopPurchaseClients = [.. topPurchaseClients],
TopSaleClients = [.. topSaleClients]
};
}

/// <summary>
/// Получить статистику заявок по типам недвижимости
/// </summary>
public async Task<IEnumerable<PropertyTypeStatisticsDto>> GetRequestCountByPropertyTypeAsync()
{
var requests = (await requestRepository.GetAllAsync()).ToList();
var properties = (await propertyRepository.GetAllAsync()).ToDictionary(p => p.Id);

return
[
.. requests
.Select(r => new
{
PropertyType = properties.TryGetValue(r.PropertyId, out var p) ? p.Type : r.Property?.Type ?? default
})
.GroupBy(x => x.PropertyType)
.Select(g => new PropertyTypeStatisticsDto
{
PropertyType = g.Key,
RequestCount = g.Count()
})
.OrderBy(x => x.PropertyType)
];
}

/// <summary>
/// Получить клиентов с минимальной суммой заявки
/// </summary>
public async Task<ClientWithMinAmountDto> GetClientsWithMinAmountAsync()
{
var requests = (await requestRepository.GetAllAsync()).ToList();
var counterparties = (await counterpartyRepository.GetAllAsync()).ToDictionary(c => c.Id);

var minAmount = requests.Min(r => r.Amount);

var clients = requests
.Where(r => r.Amount == minAmount)
.Select(r => counterparties.TryGetValue(r.CounterpartyId, out var c) ? c.FullName : r.Counterparty?.FullName)
.Where(name => !string.IsNullOrEmpty(name))
.Distinct()
.Order();

return new ClientWithMinAmountDto
{
FullName = string.Join(", ", clients),
MinAmount = minAmount
};
}

/// <summary>
/// Получить клиентов, ищущих определённый тип недвижимости
/// </summary>
public async Task<IEnumerable<string>> GetClientsSeekingPropertyTypeAsync(PropertyType propertyType)
{
var requests = (await requestRepository.GetAllAsync()).ToList();
var counterparties = (await counterpartyRepository.GetAllAsync()).ToDictionary(c => c.Id);
var properties = (await propertyRepository.GetAllAsync()).ToDictionary(p => p.Id);

return
[
.. requests
.Where(r => r.Type == RequestType.Purchase &&
(properties.TryGetValue(r.PropertyId, out var p) ? p.Type : r.Property?.Type ?? default) == propertyType)
.Select(r => counterparties.TryGetValue(r.CounterpartyId, out var c) ? c.FullName : r.Counterparty?.FullName)
.Where(name => !string.IsNullOrEmpty(name))
.Distinct()
.Order()
];
}
}
57 changes: 57 additions & 0 deletions RealEstateAgency.Application/Services/CounterpartyService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using AutoMapper;
using RealEstateAgency.Contracts.Dto;
using RealEstateAgency.Contracts.Interfaces;
using RealEstateAgency.Domain.Interfaces;
using RealEstateAgency.Domain.Models;

namespace RealEstateAgency.Application.Services;

/// <summary>
/// Сервис контрагентов
/// </summary>
public class CounterpartyService(ICounterpartyRepository repository, IMapper mapper) : ICounterpartyService
{
/// <inheritdoc />
public async Task<IEnumerable<CounterpartyDto>> GetAllAsync()
{
var counterparties = await repository.GetAllAsync();
return mapper.Map<IEnumerable<CounterpartyDto>>(counterparties);
}

/// <inheritdoc />
public async Task<CounterpartyDto?> GetByIdAsync(Guid id)
{
var counterparty = await repository.GetByIdAsync(id);
return counterparty == null ? null : mapper.Map<CounterpartyDto>(counterparty);
}

/// <inheritdoc />
public async Task<CounterpartyDto> CreateAsync(CreateCounterpartyDto dto)
{
var counterparty = mapper.Map<Counterparty>(dto);
var created = await repository.AddAsync(counterparty);
return mapper.Map<CounterpartyDto>(created);
}

/// <inheritdoc />
public async Task<CounterpartyDto?> UpdateAsync(Guid id, CreateCounterpartyDto dto)
{
var counterparty = mapper.Map<Counterparty>(dto);
var updated = await repository.UpdateAsync(id, counterparty);
return updated == null ? null : mapper.Map<CounterpartyDto>(updated);
}

/// <inheritdoc />
public async Task<CounterpartyDto?> UpdateAsync(Guid id, UpdateCounterpartyDto dto)
{
var counterparty = mapper.Map<Counterparty>(dto);
var updated = await repository.UpdateAsync(id, counterparty);
return updated == null ? null : mapper.Map<CounterpartyDto>(updated);
}

/// <inheritdoc />
public async Task<bool> DeleteAsync(Guid id)
{
return await repository.DeleteAsync(id);
}
}
Loading