Add project
All checks were successful
SanStudent Multi-Project Deployment / deploy-api (push) Successful in 41s
SanStudent Multi-Project Deployment / deploy-frontadmin (push) Successful in 41s
SanStudent Multi-Project Deployment / deploy-frontstudent (push) Successful in 40s

This commit is contained in:
aherman-san
2026-03-07 11:14:26 +01:00
parent bca807d4c1
commit b8f03bf6d3
191 changed files with 122377 additions and 0 deletions

30
Api/.dockerignore Normal file
View File

@@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**

33
Api/Api.csproj Normal file
View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>0ba4ac51-a551-4875-80a9-1d0b702565c2</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Version>1.0.0.2</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.12.54" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@
using Api.Services.Interfaces;
using Common.Dtos.Season;
using Common.Dtos.Student;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
namespace Api.Controllers;
[ApiController]
public class SeasonController(ISeasonService service) : ControllerBase
{
private readonly ISeasonService _service = service;
[HttpPost]
[Route("admin/season/create")]
public async Task<ActionResult> CreateAsync([FromBody] CreateSeasonRequest request)
{
var result = await _service.CreateAsync(request);
return result.Ok ? Ok(result.CreateSeasonResult) : Conflict(result.Error);
}
[HttpGet]
[Route("admin/season")]
public async Task<ActionResult> GetAllAsync()
{
var result = await _service.GetAllAsync();
return result.Length != 0 ? Ok(result) : NoContent();
}
[HttpGet]
[Route("admin/season/paged")]
public async Task<ActionResult> GetAllPagedAsync([FromQuery] GetSeasonsRequest request)
{
var result = await _service.GetAllPagedAsync(request);
return Ok(result);
}
}

View File

@@ -0,0 +1,36 @@
using Api.Services.Interfaces;
using Common.Dtos.Season;
using Common.Dtos.Specialization;
using Microsoft.AspNetCore.Mvc;
namespace Api.Controllers;
[ApiController]
public class SpecializationController(ISpecializationService service) : ControllerBase
{
private readonly ISpecializationService _service = service;
[HttpPost]
[Route("admin/specialization/create")]
public async Task<ActionResult> CreateAsync([FromBody] CreateSpecializationRequest request)
{
var result = await _service.CreateAsync(request);
return result.Ok ? Ok(result.CreateSpecializationResult) : Conflict(result.Error);
}
[HttpGet]
[Route("admin/specialization")]
public async Task<ActionResult> GetAllAsync()
{
var result = await _service.GetAllAsync();
return result.Length != 0 ? Ok(result) : NoContent();
}
[HttpGet]
[Route("admin/specialization/paged")]
public async Task<ActionResult> GetAllPagedAsync([FromQuery] GetSpecializationsRequest request)
{
var result = await _service.GetAllPagedAsync(request);
return Ok(result);
}
}

View File

@@ -0,0 +1,36 @@
using Api.Services.Interfaces;
using Common.Dtos.Subject;
using Microsoft.AspNetCore.Mvc;
namespace Api.Controllers
{
[ApiController]
public class SubjectController(ISubjectService service) : ControllerBase
{
private readonly ISubjectService _service = service;
[HttpPost]
[Route("admin/subject/create")]
public async Task<ActionResult> CreateAsync([FromBody] CreateSubjectRequest request)
{
var result = await _service.CreateAsync(request);
return result.Ok ? Ok(result.CreateSubjectResult) : Conflict(result.Error);
}
[HttpGet]
[Route("admin/subject")]
public async Task<ActionResult> GetAllAsync()
{
var result = await _service.GetAllAsync();
return result.Length != 0 ? Ok(result) : NoContent();
}
[HttpGet]
[Route("admin/subject/paged")]
public async Task<ActionResult> GetAllPagedAsync([FromQuery] GetSubjectsRequest request)
{
var result = await _service.GetAllPagedAsync(request);
return Ok(result);
}
}
}

View File

@@ -0,0 +1,32 @@
using Api.Extensions;
using Api.Services.Interfaces;
using Common.Dtos.Student;
using Common.Extensions;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
namespace Api.Controllers;
[ApiController]
public class VersionController() : ControllerBase
{
[HttpGet]
[Route("version")]
public async Task<ActionResult> GetVersion()
{
var version = GetVersionNumber();
return Ok(version);
}
public static string GetVersionNumber()
{
var assembly = Assembly.GetExecutingAssembly();
var version = new VersionResponse { Version = assembly.GetName().Version!.ToString() };
return version.ToJson();
}
}
public class VersionResponse
{
public string Version { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Api.Database.Entities;
public abstract class BaseEntity
{
[Key]
[Column("Id")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Api.Database.Entities;
public abstract class BaseEntityGuid
{
[Key]
[Column("Id")]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; } = Guid.CreateVersion7();
[Column("CreatedAt")]
public DateTime CreatedAt { get; set; } = DateTime.Now;
[Column("LastModifiedAt")]
public DateTime LastModifiedAt { get; set; } = DateTime.Now;
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
namespace Api.Database.Entities;
[Table("Seasons")]
public class Season : BaseEntity
{
[Column("Name")]
[NotNull]
[MaxLength(20)]
public string? Name { get; set; }
[Column("StartDate")]
[NotNull]
public DateOnly StartDate { get; set; }
[Column("EndDate")]
[NotNull]
public DateOnly EndDate { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
namespace Api.Database.Entities;
[Table("Specializations")]
public class Specialization : BaseEntity
{
[Column("Name")]
[NotNull]
[MaxLength(200)]
public string? Name { get; set; }
[Column("ShortName")]
[NotNull]
[MaxLength(10)]
public string? ShortName { get; set; }
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
namespace Api.Database.Entities;
[Table("Students")]
public class Student : BaseEntityGuid
{
[Column("FirstName")]
[NotNull]
[MaxLength(50)]
public string? FirstName { get; set; }
[Column("LastName")]
[NotNull]
[MaxLength(50)]
public string? LastName { get; set; }
[Column("AlbumNumber")]
[NotNull]
[MaxLength(6)]
public string? AlbumNumber { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
namespace Api.Database.Entities;
[Table("Subjects")]
public class Subject : BaseEntity
{
[Column("Name")]
[NotNull]
[MaxLength(200)]
public string? Name { get; set; }
[Column("ShortName")]
[NotNull]
[MaxLength(10)]
public string? ShortName { get; set; }
}

View File

@@ -0,0 +1,70 @@
// <auto-generated />
using System;
using Api.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Database.Migrations
{
[DbContext(typeof(SanStudentContext))]
[Migration("20260301222003_CreateDb")]
partial class CreateDb
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Database.Entities.Student", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("AlbumNumber")
.IsRequired()
.HasMaxLength(6)
.HasColumnType("nvarchar(6)")
.HasColumnName("AlbumNumber");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("FirstName");
b.Property<DateTime>("LastModifiedAt")
.HasColumnType("datetime2")
.HasColumnName("LastModifiedAt");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("LastName");
b.HasKey("Id");
b.HasIndex("AlbumNumber")
.IsUnique();
b.ToTable("Students");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Api.Database.Migrations
{
/// <inheritdoc />
public partial class CreateDb : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Students",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
FirstName = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
LastName = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
AlbumNumber = table.Column<string>(type: "nvarchar(6)", maxLength: 6, nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
LastModifiedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Students", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Students_AlbumNumber",
table: "Students",
column: "AlbumNumber",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Students");
}
}
}

View File

@@ -0,0 +1,150 @@
// <auto-generated />
using System;
using Api.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Database.Migrations
{
[DbContext(typeof(SanStudentContext))]
[Migration("20260305195643_SpecsSubjectsAndSeasons")]
partial class SpecsSubjectsAndSeasons
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Database.Entities.Season", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("Id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateOnly>("EndDate")
.HasColumnType("date")
.HasColumnName("EndDate");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)")
.HasColumnName("Name");
b.Property<DateOnly>("StartDate")
.HasColumnType("date")
.HasColumnName("StartDate");
b.HasKey("Id");
b.ToTable("Seasons");
});
modelBuilder.Entity("Api.Database.Entities.Specialization", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("Id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("Name");
b.Property<string>("ShortName")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)")
.HasColumnName("ShortName");
b.HasKey("Id");
b.ToTable("Specializations");
});
modelBuilder.Entity("Api.Database.Entities.Student", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("AlbumNumber")
.IsRequired()
.HasMaxLength(6)
.HasColumnType("nvarchar(6)")
.HasColumnName("AlbumNumber");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("FirstName");
b.Property<DateTime>("LastModifiedAt")
.HasColumnType("datetime2")
.HasColumnName("LastModifiedAt");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("LastName");
b.HasKey("Id");
b.HasIndex("AlbumNumber")
.IsUnique();
b.ToTable("Students");
});
modelBuilder.Entity("Api.Database.Entities.Subject", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("Id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("Name");
b.Property<string>("ShortName")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)")
.HasColumnName("ShortName");
b.HasKey("Id");
b.ToTable("Subjects");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Api.Database.Migrations
{
/// <inheritdoc />
public partial class SpecsSubjectsAndSeasons : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Seasons",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
StartDate = table.Column<DateOnly>(type: "date", nullable: false),
EndDate = table.Column<DateOnly>(type: "date", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Seasons", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Specializations",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
ShortName = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Specializations", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Subjects",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
ShortName = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Subjects", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Seasons");
migrationBuilder.DropTable(
name: "Specializations");
migrationBuilder.DropTable(
name: "Subjects");
}
}
}

View File

@@ -0,0 +1,147 @@
// <auto-generated />
using System;
using Api.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Database.Migrations
{
[DbContext(typeof(SanStudentContext))]
partial class SanStudentContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Database.Entities.Season", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("Id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateOnly>("EndDate")
.HasColumnType("date")
.HasColumnName("EndDate");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)")
.HasColumnName("Name");
b.Property<DateOnly>("StartDate")
.HasColumnType("date")
.HasColumnName("StartDate");
b.HasKey("Id");
b.ToTable("Seasons");
});
modelBuilder.Entity("Api.Database.Entities.Specialization", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("Id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("Name");
b.Property<string>("ShortName")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)")
.HasColumnName("ShortName");
b.HasKey("Id");
b.ToTable("Specializations");
});
modelBuilder.Entity("Api.Database.Entities.Student", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier")
.HasColumnName("Id");
b.Property<string>("AlbumNumber")
.IsRequired()
.HasMaxLength(6)
.HasColumnType("nvarchar(6)")
.HasColumnName("AlbumNumber");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2")
.HasColumnName("CreatedAt");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("FirstName");
b.Property<DateTime>("LastModifiedAt")
.HasColumnType("datetime2")
.HasColumnName("LastModifiedAt");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)")
.HasColumnName("LastName");
b.HasKey("Id");
b.HasIndex("AlbumNumber")
.IsUnique();
b.ToTable("Students");
});
modelBuilder.Entity("Api.Database.Entities.Subject", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("Id");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)")
.HasColumnName("Name");
b.Property<string>("ShortName")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)")
.HasColumnName("ShortName");
b.HasKey("Id");
b.ToTable("Subjects");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,22 @@
using Api.Database.Entities;
using Microsoft.EntityFrameworkCore;
namespace Api.Database;
public class SanStudentContext : DbContext
{
public SanStudentContext(DbContextOptions<SanStudentContext> options) : base(options) { }
public required virtual DbSet<Specialization> Specializations { get; set; }
public required virtual DbSet<Subject> Subjects { get; set; }
public virtual required DbSet<Season> Seasons { get; set; }
public required virtual DbSet<Student> Students { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>(entity =>
{
entity.HasIndex(e => e.AlbumNumber).IsUnique();
});
}
}

21
Api/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src
COPY ["Api/Api.csproj", "Api/"]
COPY ["Common/Common.csproj", "Common/"]
RUN dotnet restore "Api/Api.csproj"
COPY . .
WORKDIR /src/Api
RUN dotnet publish "Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "Api.dll"]

View File

@@ -0,0 +1,16 @@
using Common.Extensions;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace Api.Extensions;
public static class CacheExtensions
{
public static async Task SetWithExpirationTimeAsync<T>(this IDistributedCache cache, string key, T value, int minutes = 10)
{
var options = new DistributedCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(minutes));
await cache.SetStringAsync(key, value.ToJson(), options);
}
}

View File

@@ -0,0 +1,19 @@
using Common.Dtos.Common;
using Microsoft.EntityFrameworkCore;
namespace Api.Extensions;
public static class PagingExtensions
{
public static async Task<PagedList<T>> ToPagedListAsync<T>(this IQueryable<T> queryable, int pageNumber, int pageSize)
{
var query = queryable;
int totalItemsCount = await queryable.CountAsync();
int totalPagesCount = (totalItemsCount / pageSize) + 1;
var number = Math.Min(pageNumber, totalPagesCount);
int skip = (number - 1) * pageSize;
var items = await query.Skip(skip).Take(pageSize).ToArrayAsync();
return new PagedList<T>(items, pageNumber, pageSize, totalItemsCount, totalPagesCount);
}
}

52
Api/Mapping/Mapper.cs Normal file
View File

@@ -0,0 +1,52 @@
using Api.Database.Entities;
using Common.Dtos.Season;
using Common.Dtos.Specialization;
using Common.Dtos.Student;
using Common.Dtos.Subject;
namespace Api.Mapping;
public static class Mapper
{
public static SpecializationDto ToSpecializationDto(this Specialization specialization)
{
return new SpecializationDto
{
Id = specialization.Id,
Name = specialization.Name,
ShortName = specialization.ShortName
};
}
public static SubjectDto ToSubjectDto(this Subject subject)
{
return new SubjectDto
{
Id = subject.Id,
Name = subject.Name,
ShortName = subject.ShortName
};
}
public static SeasonDto ToSeasonDto(this Season season)
{
return new SeasonDto
{
Id = season.Id,
Name = season.Name,
StartDate = season.StartDate,
EndDate = season.EndDate
};
}
public static StudentBasicDto ToStudentBasicDto(this Student student)
{
return new StudentBasicDto
{
Id = student.Id,
FirstName = student.FirstName,
LastName = student.LastName,
AlbumNumber = student.AlbumNumber
};
}
}

61
Api/Program.cs Normal file
View File

@@ -0,0 +1,61 @@
using Api.Database;
using Api.Services.Implementation;
using Api.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowBlazorFrontends",
policy =>
{
policy.WithOrigins("https://sanstudent.aherman.eu",
"https://sanstudent.eu",
"https://www.sanstudent.eu",
"https://localhost:7001",
"https://localhost:7000"
)
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddDbContext<SanStudentContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("RedisCache");
options.InstanceName = "SanStudentApi_";
});
builder.Services.AddScoped<ISpecializationService, SpecializationService>();
builder.Services.AddScoped<ISubjectService, SubjectService>();
builder.Services.AddScoped<ISeasonService, SeasonService>();
builder.Services.AddControllers();
builder.Services.AddOpenApi();
var app = builder.Build();
app.UseCors("AllowBlazorFrontends");
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
options.Title = "SanStudent API";
options.Theme = ScalarTheme.BluePlanet;
options.DefaultHttpClient = new(ScalarTarget.CSharp, ScalarClient.HttpClient);
options.CustomCss = "";
options.ShowSidebar = true;
});
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,33 @@
{
"profiles": {
"http": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5001"
},
"https": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "scalar/v1",
"applicationUrl": "https://localhost:5000;http://localhost:5001"
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": {
"ASPNETCORE_HTTPS_PORTS": "8081",
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true,
"useSSL": true
}
},
"$schema": "https://json.schemastore.org/launchsettings.json"
}

View File

@@ -0,0 +1,156 @@
using Api.Database;
using Api.Database.Entities;
using Api.Extensions;
using Api.Mapping;
using Api.Services.Interfaces;
using Common.Dtos.Common;
using Common.Dtos.Season;
using Common.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
namespace Api.Services.Implementation;
public class SeasonService(SanStudentContext context, IDistributedCache cache) : ISeasonService, IServiceHelper<CreateSeasonRequest>
{
private readonly SanStudentContext _context = context;
private readonly IDistributedCache _cache = cache;
private List<string> _cacheKeys = [];
public async Task<CreateSeasonResponse> CreateAsync(CreateSeasonRequest request)
{
try
{
var problems = await Validate(request);
if (problems.Errors.Count > 0)
{
return new CreateSeasonResponse
{
Ok = false,
CreateSeasonResult = string.Join(", ", problems.Errors)
};
}
var season = new Season()
{
Name = request.Name!.Trim(),
StartDate = request.StartDate,
EndDate = request.EndDate
};
_context.Seasons.Add(season);
await _context.SaveChangesAsync();
await RemoveCache();
return new CreateSeasonResponse
{
Ok = true,
CreateSeasonResult = $"Zapisano nowy semestr \"{season.Name}\"",
};
}
catch (Exception error)
{
return new CreateSeasonResponse
{
Ok = false,
CreateSeasonResult = $"Błąd podczas próby zapisu semestru: {error.Message}",
};
}
}
public async Task<SeasonDto[]> GetAllAsync()
{
var key = "seasons";
AddKeyToCache(key);
var cachedSeasons = await _cache.GetStringAsync(key);
if (!string.IsNullOrEmpty(cachedSeasons))
return cachedSeasons.FromJson<SeasonDto[]>();
var query = _context.Seasons as IQueryable<Season>;
var result = await query
.Where(s => s.EndDate > DateOnly.FromDateTime(DateTime.Now))
.OrderBy(s => s.StartDate)
.ThenBy(s => s.Name)
.Select(s => s.ToSeasonDto()).ToArrayAsync();
await _cache.SetWithExpirationTimeAsync(key, result, 131487);
return result;
}
public async Task<PagedList<SeasonDto>> GetAllPagedAsync(GetSeasonsRequest request)
{
var key = $"seasons_{request.PageNumber}_{request.PageSize}";
if (!string.IsNullOrWhiteSpace(request.Name))
key += $"_{request.Name.ToLower()}";
if (request.HideObsolete.HasValue && request.HideObsolete.Value)
key += $"_{true}";
AddKeyToCache(key);
var cachedSeasons = await _cache.GetStringAsync(key);
if (!string.IsNullOrEmpty(cachedSeasons))
return cachedSeasons.FromJson<PagedList<SeasonDto>>();
var query = _context.Seasons as IQueryable<Season>;
if (!string.IsNullOrEmpty(request.Name))
query = query.Where(s => s.Name.ToLower().StartsWith(request.Name.ToLower()));
if (request.HideObsolete.HasValue && request.HideObsolete.Value)
query = query.Where(s => s.EndDate > DateOnly.FromDateTime(DateTime.Now));
var data = query
.OrderBy(s => s.StartDate)
.ThenBy(s => s.Name)
.Select(s => s.ToSeasonDto());
var result = await data.ToPagedListAsync(request.PageNumber, request.PageSize);
await _cache.SetWithExpirationTimeAsync(key, result, 131487);
return result;
}
public async Task<ValidationProblems> Validate(CreateSeasonRequest request)
{
var problems = new ValidationProblems();
if (string.IsNullOrWhiteSpace(request.Name))
problems.Errors.Add("Brak nazwy semestru");
if (problems.Errors.Count > 0)
return problems;
var season = await _context.Seasons
.FirstOrDefaultAsync(s => s.Name.ToLower() == request.Name!.ToLower());
if (season is not null)
problems.Errors.Add("Semestr o podanej nazwie już istnieje");
season = await _context.Seasons
.FirstOrDefaultAsync(s => s.StartDate == request.StartDate);
if (season is not null)
problems.Errors.Add("Semestr o podanej dacie rozpoczęcia już istnieje");
season = await _context.Seasons
.FirstOrDefaultAsync(s => s.EndDate == request.EndDate);
if (season is not null)
problems.Errors.Add("Semestr o podanej dacie zakończenia już istnieje");
return problems;
}
public async Task RemoveCache()
{
foreach (var cacheKey in _cacheKeys)
await _cache.RemoveAsync(cacheKey);
}
public void AddKeyToCache(string key)
{
if (!_cacheKeys.Contains(key))
_cacheKeys.Add(key);
}
}

View File

@@ -0,0 +1,146 @@
using Api.Database;
using Api.Database.Entities;
using Api.Extensions;
using Api.Mapping;
using Api.Services.Interfaces;
using Common.Dtos.Common;
using Common.Dtos.Season;
using Common.Dtos.Specialization;
using Common.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
namespace Api.Services.Implementation;
public class SpecializationService(SanStudentContext context, IDistributedCache cache)
: ISpecializationService, IServiceHelper<CreateSpecializationRequest>
{
private readonly SanStudentContext _context = context;
private readonly IDistributedCache _cache = cache;
private List<string> _cacheKeys = [];
public async Task<CreateSpecializationResponse> CreateAsync(CreateSpecializationRequest request)
{
try
{
var problems = await Validate(request);
if (problems.Errors.Count > 0)
{
return new CreateSpecializationResponse
{
Ok = false,
CreateSpecializationResult = string.Join(", ", problems.Errors)
};
}
var specialization = new Specialization()
{
Name = request.Name!.Trim(),
ShortName = request.ShortName!.Trim()
};
_context.Specializations.Add(specialization);
await _context.SaveChangesAsync();
await RemoveCache();
return new CreateSpecializationResponse
{
Ok = true,
CreateSpecializationResult = $"Zapisano nową specializację \"{specialization.Name}\"",
};
}
catch (Exception error)
{
return new CreateSpecializationResponse
{
Ok = false,
CreateSpecializationResult = $"Błąd podczas próby zapisu specializacji: {error.Message}",
};
}
}
public async Task<SpecializationDto[]> GetAllAsync()
{
var key = "specializations";
AddKeyToCache(key);
var cachedSpecs = await _cache.GetStringAsync(key);
if (!string.IsNullOrEmpty(cachedSpecs))
return cachedSpecs.FromJson<SpecializationDto[]>();
var query = _context.Specializations as IQueryable<Specialization>;
var result = await query
.OrderBy(s => s.Name)
.ThenBy(s => s.ShortName)
.Select(s => s.ToSpecializationDto()).ToArrayAsync();
await _cache.SetWithExpirationTimeAsync(key, result, 262487);
return result;
}
public async Task<PagedList<SpecializationDto>> GetAllPagedAsync(GetSpecializationsRequest request)
{
var key = $"specializations_{request.PageNumber}_{request.PageSize}";
if (!string.IsNullOrWhiteSpace(request.Name))
key += $"_{request.Name.ToLower()}";
AddKeyToCache(key);
var cachedSpecs = await _cache.GetStringAsync(key);
if (!string.IsNullOrEmpty(cachedSpecs))
return cachedSpecs.FromJson<PagedList<SpecializationDto>>();
var query = _context.Specializations as IQueryable<Specialization>;
if (!string.IsNullOrEmpty(request.Name))
query = query.Where(s => s.Name.ToLower().StartsWith(request.Name.ToLower()));
var data = query
.OrderBy(s => s.Name)
.ThenBy(s => s.ShortName)
.Select(s => s.ToSpecializationDto());
var result = await data.ToPagedListAsync(request.PageNumber, request.PageSize);
await _cache.SetWithExpirationTimeAsync(key, result, 262487);
return result;
}
public async Task<ValidationProblems> Validate(CreateSpecializationRequest request)
{
var problems = new ValidationProblems();
if (string.IsNullOrWhiteSpace(request.Name))
problems.Errors.Add("Brak nazwy specializacji");
if (string.IsNullOrWhiteSpace(request.ShortName))
problems.Errors.Add("Brak nazwy skróconej specializacji");
if (problems.Errors.Count > 0)
return problems;
var specialization = await _context.Specializations
.FirstOrDefaultAsync(s => s.Name.ToLower() == request.Name!.ToLower());
if (specialization is not null)
problems.Errors.Add("Specializacja o podanej nazwie już istnieje");
specialization = await _context.Specializations
.FirstOrDefaultAsync(s => s.ShortName.ToLower() == request.ShortName!.ToLower());
if (specialization is not null)
problems.Errors.Add("Specializacja o podanej nazwie skróconej już istnieje");
return problems;
}
public async Task RemoveCache()
{
foreach (var cacheKey in _cacheKeys)
await _cache.RemoveAsync(cacheKey);
}
public void AddKeyToCache(string key)
{
if (!_cacheKeys.Contains(key))
_cacheKeys.Add(key);
}
}

View File

@@ -0,0 +1,144 @@
using Api.Database;
using Api.Database.Entities;
using Api.Extensions;
using Api.Mapping;
using Api.Services.Interfaces;
using Common.Dtos.Common;
using Common.Dtos.Subject;
using Common.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
namespace Api.Services.Implementation;
public class SubjectService(SanStudentContext context, IDistributedCache cache)
: ISubjectService, IServiceHelper<CreateSubjectRequest>
{
private readonly SanStudentContext _context = context;
private readonly IDistributedCache _cache = cache;
private List<string> _cacheKeys = [];
public async Task<CreateSubjectResponse> CreateAsync(CreateSubjectRequest request)
{
try
{
var problems = await Validate(request);
if (problems.Errors.Count > 0)
{
return new CreateSubjectResponse
{
Ok = false,
CreateSubjectResult = string.Join(", ", problems.Errors)
};
}
var subject = new Subject()
{
Name = request.Name!.Trim(),
ShortName = request.ShortName!.Trim()
};
_context.Subjects.Add(subject);
await _context.SaveChangesAsync();
await RemoveCache();
return new CreateSubjectResponse
{
Ok = true,
CreateSubjectResult = $"Zapisano nowy przedmiot \"{subject.Name}\"",
};
}
catch (Exception error)
{
return new CreateSubjectResponse
{
Ok = false,
CreateSubjectResult = $"Błąd podczas próby zapisu przedmiotu: {error.Message}",
};
}
}
public async Task<SubjectDto[]> GetAllAsync()
{
var key = "subjects";
AddKeyToCache(key);
var cachedSubjects = await _cache.GetStringAsync(key);
if (!string.IsNullOrEmpty(cachedSubjects))
return cachedSubjects.FromJson<SubjectDto[]>();
var query = _context.Subjects as IQueryable<Subject>;
var result = await query
.OrderBy(s => s.Name)
.ThenBy(s => s.ShortName)
.Select(s => s.ToSubjectDto()).ToArrayAsync();
await _cache.SetWithExpirationTimeAsync(key, result, 262487);
return result;
}
public async Task<PagedList<SubjectDto>> GetAllPagedAsync(GetSubjectsRequest request)
{
var key = $"subjects_{request.PageNumber}_{request.PageSize}";
if (!string.IsNullOrWhiteSpace(request.Name))
key += $"_{request.Name.ToLower()}";
AddKeyToCache(key);
var cachedSubjects = await _cache.GetStringAsync(key);
if (!string.IsNullOrEmpty(cachedSubjects))
return cachedSubjects.FromJson<PagedList<SubjectDto>>();
var query = _context.Subjects as IQueryable<Subject>;
if (!string.IsNullOrEmpty(request.Name))
query = query.Where(s => s.Name.ToLower().StartsWith(request.Name.ToLower()));
var data = query
.OrderBy(s => s.Name)
.ThenBy(s => s.ShortName)
.Select(s => s.ToSubjectDto());
var result = await data.ToPagedListAsync(request.PageNumber, request.PageSize);
await _cache.SetWithExpirationTimeAsync(key, result, 262487);
return result;
}
public async Task<ValidationProblems> Validate(CreateSubjectRequest request)
{
var problems = new ValidationProblems();
if (string.IsNullOrWhiteSpace(request.Name))
problems.Errors.Add("Brak nazwy przedmiotu");
if (string.IsNullOrWhiteSpace(request.ShortName))
problems.Errors.Add("Brak nazwy skróconej przedmiotu");
if (problems.Errors.Count > 0)
return problems;
var subject = await _context.Subjects
.FirstOrDefaultAsync(s => s.Name.ToLower() == request.Name!.ToLower());
if (subject is not null)
problems.Errors.Add("Przedmiot o podanej nazwie już istnieje");
subject = await _context.Subjects
.FirstOrDefaultAsync(s => s.ShortName.ToLower() == request.ShortName!.ToLower());
if (subject is not null)
problems.Errors.Add("Przedmiot o podanej nazwie skróconej już istnieje");
return problems;
}
public void AddKeyToCache(string key)
{
if (!_cacheKeys.Contains(key))
_cacheKeys.Add(key);
}
public async Task RemoveCache()
{
foreach (var cacheKey in _cacheKeys)
await _cache.RemoveAsync(cacheKey);
}
}

View File

@@ -0,0 +1,11 @@
using Common.Dtos.Common;
using Common.Dtos.Season;
namespace Api.Services.Interfaces;
public interface ISeasonService
{
Task<CreateSeasonResponse> CreateAsync(CreateSeasonRequest request);
Task<PagedList<SeasonDto>> GetAllPagedAsync(GetSeasonsRequest request);
Task<SeasonDto[]> GetAllAsync();
}

View File

@@ -0,0 +1,11 @@
using Common.Dtos.Common;
namespace Api.Services.Interfaces
{
public interface IServiceHelper<T>
{
Task<ValidationProblems> Validate(T request);
Task RemoveCache();
void AddKeyToCache(string key);
}
}

View File

@@ -0,0 +1,12 @@
using Common.Dtos.Common;
using Common.Dtos.Season;
using Common.Dtos.Specialization;
namespace Api.Services.Interfaces;
public interface ISpecializationService
{
Task<CreateSpecializationResponse> CreateAsync(CreateSpecializationRequest request);
Task<PagedList<SpecializationDto>> GetAllPagedAsync(GetSpecializationsRequest request);
Task<SpecializationDto[]> GetAllAsync();
}

View File

@@ -0,0 +1,12 @@
using Common.Dtos.Common;
using Common.Dtos.Specialization;
using Common.Dtos.Subject;
namespace Api.Services.Interfaces;
public interface ISubjectService
{
Task<CreateSubjectResponse> CreateAsync(CreateSubjectRequest request);
Task<PagedList<SubjectDto>> GetAllPagedAsync(GetSubjectsRequest request);
Task<SubjectDto[]> GetAllAsync();
}

View File

@@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=89.167.52.197;Database=SanStudent;User ID=sanstudentuser;Password=@sanStudent2026@;TrustServerCertificate=True;",
"RedisCache": "localhost:6379,password=#PanTadeusz1973#,abortConnect=false"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
Api/appsettings.json Normal file
View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}