Add project files.

This commit is contained in:
2022-10-06 16:53:07 +03:00
parent 45fac5e086
commit 6f3acce7d4
38 changed files with 1051 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
**/.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
@@ -0,0 +1,107 @@
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using PracticeCalendar.API.Model;
using PracticeCalendar.Domain.Common.Interfaces;
using PracticeCalendar.Domain.Entities;
using PracticeCalendar.Domain.Entities.Specifications;
namespace PrcaticeCalendar.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class EventsController : ControllerBase
{
private readonly IRepository<PracticeEvent> eventsRepo;
private readonly IMapper mapper;
private readonly ILogger<EventsController> _logger;
public EventsController(IRepository<PracticeEvent> eventsRepo,
IMapper mapper,
ILogger<EventsController> logger)
{
this.eventsRepo = eventsRepo;
this.mapper = mapper;
_logger = logger;
}
[HttpGet(Name = "GetAll")]
public async Task<ActionResult<List<EventModel>>> Get()
{
var spec = new PracticeEventsWithAttendees();
var repoList = await eventsRepo.ListAsync(spec);
var evList = repoList.Select(x=> {
var model = mapper.Map<EventModel>(x);
model.Attendees = x.Attendees.Select(m=>mapper.Map<AttendeeModel>(m)).ToArray();
return model;
})
.ToList();
return evList;
}
[HttpPost(Name = "Create practice event")]
public async Task<IActionResult> CreateEvent(EventModel eventModel)
{
var practiceEvent = new PracticeEvent(eventModel.Title, eventModel.Description);
foreach (var att in eventModel.Attendees)
{
practiceEvent.AddAttendee(new Attendee(att.Name, att.EmailAddress));
}
var result = await eventsRepo.AddAsync(practiceEvent);
await eventsRepo.SaveChangesAsync();
return Ok(mapper.Map<EventModel>(result));
}
[HttpPut(Name = "Update practice event")]
public async Task<IActionResult> UpdateEvent(EventModel eventModel)
{
var practiceEvent = await eventsRepo.GetByIdAsync(eventModel.Id);
if(practiceEvent == null)
{
return NotFound();
}
practiceEvent.UpdateTitleAndDescription(eventModel.Title, eventModel.Description);
await eventsRepo.UpdateAsync(practiceEvent);
await eventsRepo.SaveChangesAsync();
return Ok(mapper.Map<EventModel>(practiceEvent));
}
[HttpDelete(Name = "Delete practice event")]
public async Task<IActionResult> DeleteEvent(int practiceEventId)
{
var org = await eventsRepo.GetByIdAsync(practiceEventId);
await eventsRepo.DeleteAsync(org);
await eventsRepo.SaveChangesAsync();
return Ok();
}
[HttpPost]
[Route("accept/{eventId}/{attendeeId}")]
public async Task<IActionResult> AttendeeAcceptEvent(int eventId, int attendeeId)
{
var spec = new PracticeEventByIdWithAttendees(eventId);
var practiceEvent = await eventsRepo.GetBySpecAsync(spec);
if (practiceEvent == null)
{
return NotFound();
}
practiceEvent.AttendeeAcceptEvent(attendeeId);
await eventsRepo.SaveChangesAsync();
return Ok();
}
[HttpPost]
[Route("decline/{eventId}/{attendeeId}")]
public async Task<IActionResult> AttendeeDeclineEvent(int eventId, int attendeeId)
{
var spec = new PracticeEventByIdWithAttendees(eventId);
var practiceEvent = await eventsRepo.GetBySpecAsync(spec);
if (practiceEvent == null)
{
return NotFound();
}
practiceEvent.AttendeeDeclineEvent(attendeeId);
await eventsRepo.SaveChangesAsync();
return Ok();
}
}
}
+22
View File
@@ -0,0 +1,22 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["PrcaticeCalendar/PrcaticeCalendar.csproj", "PrcaticeCalendar/"]
RUN dotnet restore "PrcaticeCalendar/PrcaticeCalendar.csproj"
COPY . .
WORKDIR "/src/PrcaticeCalendar"
RUN dotnet build "PrcaticeCalendar.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "PrcaticeCalendar.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "PrcaticeCalendar.dll"]
+34
View File
@@ -0,0 +1,34 @@
using AutoMapper;
using PracticeCalendar.Domain.Entities;
namespace PracticeCalendar.API.Model
{
public class EventModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public AttendeeModel[] Attendees { get; set; } = new AttendeeModel[0];
}
public class AttendeeModel
{
public string Name { get; set; }
public string EmailAddress { get; set; }
public bool IsAttending { get; set; }
}
public class EventModelProfile : Profile
{
public EventModelProfile()
{
CreateMap<PracticeEvent, EventModel>();
CreateMap<EventModel, PracticeEvent>();
CreateMap<Attendee, AttendeeModel>();
}
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>78351d12-8544-4ae0-877a-3522681401ca</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Hellang.Middleware.ProblemDetails" Version="6.5.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PracticeCalendar.Infrastructure\PracticeCalendar.Infrastructure.csproj" />
</ItemGroup>
</Project>
+69
View File
@@ -0,0 +1,69 @@
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Hosting;
using PracticeCalendar.Infrastructure;
using PracticeCalendar.Infrastructure.Persistence;
using System;
namespace PracticeCalendar
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddProblemDetails();
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//inject infrastructure
builder.Services.AddInfrastructure(builder.Configuration);
var app = builder.Build();
app.UseProblemDetails();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
// Seed Database
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<ApplicationDbContext>();
//context.Database.Migrate();
context.Database.EnsureCreated();
//SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB. {exceptionMessage}", ex.Message);
}
}
app.Run();
}
}
}
@@ -0,0 +1,38 @@
{
"profiles": {
"PracticeCalendar": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7267;http://localhost:5267"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"publishAllPorts": true,
"useSSL": true
}
},
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:58588",
"sslPort": 44388
}
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"UseInMemoryDatabase": true,
"ConnectionStrings": {
"SqliteConnection": "Data Source=practicecalendar.sqlite"
}
}
Binary file not shown.
@@ -0,0 +1,7 @@
namespace PracticeCalendar.Domain.Common
{
public class DomainEventBase
{
public DateTime EventDate { get; protected set; } = DateTime.UtcNow;
}
}
@@ -0,0 +1,6 @@
namespace PracticeCalendar.Domain.Common
{
public abstract class DomainException : Exception
{
}
}
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace PracticeCalendar.Domain.Common
{
public abstract class EntityBase
{
public int Id { get; set; }
private List<DomainEventBase> _domainEvents = new();
[NotMapped]
public IEnumerable<DomainEventBase> DomainEvents => _domainEvents.AsReadOnly();
protected void RegisterDomainEvent(DomainEventBase domainEvent) => _domainEvents.Add(domainEvent);
internal void ClearDomainEvents() => _domainEvents.Clear();
}
}
@@ -0,0 +1,8 @@
namespace PracticeCalendar.Domain.Common
{
// source: https://github.com/jhewlett/ValueObject
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class IgnoreMemberAttribute : Attribute
{
}
}
@@ -0,0 +1,6 @@
namespace PracticeCalendar.Domain.Common.Interfaces
{
// Apply this marker interface only to aggregate root entities
// Repositories will only work with aggregate roots, not their children
public interface IAggregateRoot { }
}
@@ -0,0 +1,8 @@
using Ardalis.Specification;
namespace PracticeCalendar.Domain.Common.Interfaces
{
public interface IRepository<T> : IRepositoryBase<T> where T : class, IAggregateRoot
{
}
}
@@ -0,0 +1,110 @@
using System.Reflection;
namespace PracticeCalendar.Domain.Common
{
// source: https://github.com/jhewlett/ValueObject
public abstract class ValueObject : IEquatable<ValueObject>
{
private List<PropertyInfo>? properties;
private List<FieldInfo>? fields;
public static bool operator ==(ValueObject? obj1, ValueObject? obj2)
{
if (object.Equals(obj1, null))
{
if (object.Equals(obj2, null))
{
return true;
}
return false;
}
return obj1.Equals(obj2);
}
public static bool operator !=(ValueObject? obj1, ValueObject? obj2)
{
return !(obj1 == obj2);
}
public bool Equals(ValueObject? obj)
{
return Equals(obj as object);
}
public override bool Equals(object? obj)
{
if (obj == null || GetType() != obj.GetType()) return false;
return GetProperties().All(p => PropertiesAreEqual(obj, p))
&& GetFields().All(f => FieldsAreEqual(obj, f));
}
private bool PropertiesAreEqual(object obj, PropertyInfo p)
{
return object.Equals(p.GetValue(this, null), p.GetValue(obj, null));
}
private bool FieldsAreEqual(object obj, FieldInfo f)
{
return object.Equals(f.GetValue(this), f.GetValue(obj));
}
private IEnumerable<PropertyInfo> GetProperties()
{
if (this.properties == null)
{
this.properties = GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.GetCustomAttribute(typeof(IgnoreMemberAttribute)) == null)
.ToList();
// Not available in Core
// !Attribute.IsDefined(p, typeof(IgnoreMemberAttribute))).ToList();
}
return this.properties;
}
private IEnumerable<FieldInfo> GetFields()
{
if (this.fields == null)
{
this.fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.GetCustomAttribute(typeof(IgnoreMemberAttribute)) == null)
.ToList();
}
return this.fields;
}
public override int GetHashCode()
{
unchecked //allow overflow
{
int hash = 17;
foreach (var prop in GetProperties())
{
var value = prop.GetValue(this, null);
hash = HashValue(hash, value);
}
foreach (var field in GetFields())
{
var value = field.GetValue(this);
hash = HashValue(hash, value);
}
return hash;
}
}
private int HashValue(int seed, object? value)
{
var currentHash = value != null
? value.GetHashCode()
: 0;
return seed * 23 + currentHash;
}
}
}
@@ -0,0 +1,44 @@
using Ardalis.GuardClauses;
using PracticeCalendar.Domain.Common;
using System.Diagnostics.Contracts;
namespace PracticeCalendar.Domain.Entities
{
/// <summary>
/// Attendee to an event
/// </summary>
public class Attendee: EntityBase
{
public Attendee(string name, string emailAddress)
{
Guard.Against.NullOrEmpty(name);
Guard.Against.NullOrEmpty(emailAddress);
Name = name;
EmailAddress = emailAddress;
}
public int PracticeEventId { get; private set; }
public string Name { get; set; } = string.Empty;
public string EmailAddress { get; set; } = string.Empty;
public bool IsAttending { get; private set; }
/// <summary>
/// Set if the Attendee is attending
/// </summary>
/// <param name="isAttending"></param>
public void SetIsAttending (bool isAttending)
{
this.IsAttending = isAttending;
//TODO - raise event
}
/// <summary>
/// Assign Attendee to the practice event
/// </summary>
/// <param name="practiceEventId"></param>
public void AssignToEvent(int practiceEventId)
{
this.PracticeEventId = practiceEventId;
}
}
}
@@ -0,0 +1,63 @@
using Ardalis.GuardClauses;
using PracticeCalendar.Domain.Common;
using PracticeCalendar.Domain.Common.Interfaces;
using PracticeCalendar.Domain.Events;
using PracticeCalendar.Domain.Exceptions;
namespace PracticeCalendar.Domain.Entities
{
/// <summary>
/// Practice event aggregate
/// </summary>
public class PracticeEvent : EntityBase, IAggregateRoot
{
public PracticeEvent(string title, string description)
{
Guard.Against.NullOrEmpty(title, nameof(title));
Guard.Against.NullOrEmpty(description, nameof(description));
this.Title = title;
this.Description = description;
}
public string Title { get; private set; } = string.Empty;
public string Description{ get; private set; } = string.Empty;
private List<Attendee> attendees = new List<Attendee>();
public IEnumerable<Attendee> Attendees => attendees.AsReadOnly();
public DateTime StartTime { get; private set; }
public DateTime EndTime { get; private set; }
public void AddAttendee(Attendee attendee)
{
Guard.Against.Null(attendee, nameof(attendee));
attendee.AssignToEvent(this.Id);
attendees.Add(attendee);
var attendeeAddedEvent = new AttendeeAddedEvent(this, attendee);
base.RegisterDomainEvent(attendeeAddedEvent);
}
public void UpdateTitleAndDescription(string title, string description)
{
this.Title = title;
this.Description = description;
}
public void AttendeeAcceptEvent(int attendeeId)
{
var attendee = this.Attendees.FirstOrDefault(x => x.Id == attendeeId);
if (attendee == null)
throw new InvalidAttendeeException(attendeeId);
attendee.SetIsAttending(true);
}
public void AttendeeDeclineEvent(int attendeeId)
{
var attendee = this.Attendees.FirstOrDefault(x => x.Id == attendeeId);
if (attendee == null)
throw new InvalidAttendeeException(attendeeId);
attendee.SetIsAttending(false);
}
}
}
@@ -0,0 +1,13 @@
using Ardalis.Specification;
namespace PracticeCalendar.Domain.Entities.Specifications
{
public class PracticeEventByIdWithAttendees : Specification<PracticeEvent>
{
public PracticeEventByIdWithAttendees(int eventId)
{
Query.Where(x=>x.Id == eventId)
.Include(x => x.Attendees);
}
}
}
@@ -0,0 +1,12 @@
using Ardalis.Specification;
namespace PracticeCalendar.Domain.Entities.Specifications
{
public class PracticeEventsWithAttendees : Specification<PracticeEvent>
{
public PracticeEventsWithAttendees()
{
Query.Include(x => x.Attendees);
}
}
}
@@ -0,0 +1,17 @@
using PracticeCalendar.Domain.Common;
using PracticeCalendar.Domain.Entities;
namespace PracticeCalendar.Domain.Events
{
public sealed class AttendeeAddedEvent : DomainEventBase
{
public AttendeeAddedEvent(PracticeEvent eventAggregate, Attendee addedAtendee)
{
EventAggregate = eventAggregate;
AddedAtendee = addedAtendee;
}
public PracticeEvent EventAggregate { get; }
public Attendee AddedAtendee { get; set; }
}
}
@@ -0,0 +1,16 @@
using PracticeCalendar.Domain.Common;
namespace PracticeCalendar.Domain.Exceptions
{
public class InvalidAttendeeException : DomainException
{
private int attendeeId;
public InvalidAttendeeException(int attendeeId)
{
this.attendeeId = attendeeId;
}
public override string Message => $"Invalid Attendee with id: {attendeeId}";
}
}
@@ -0,0 +1,7 @@
namespace PracticeCalendar.Domain.Interfaces
{
public interface IEmailSender
{
Task SendEmailAsync(string to, string from, string subject, string body);
}
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="ValueObjects\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="4.0.1" />
<PackageReference Include="Ardalis.Specification" Version="6.1.0" />
<PackageReference Include="MediatR" Version="11.0.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using PracticeCalendar.Domain.Common.Interfaces;
using PracticeCalendar.Domain.Interfaces;
using PracticeCalendar.Infrastructure.Notification;
using PracticeCalendar.Infrastructure.Persistence;
using System;
namespace PracticeCalendar.Infrastructure
{
public static class InfrastructureDI
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfigurationRoot configuration)
{
string connectionString = configuration.GetConnectionString("SqliteConnection");
services.AddDbContext(connectionString);
services.AddTransient<IEmailSender, FileEmailSender>();
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
return services;
}
public static void AddDbContext(this IServiceCollection services, string connectionString) =>
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(connectionString));
}
}
@@ -0,0 +1,21 @@
using Microsoft.Extensions.Logging;
using PracticeCalendar.Domain.Interfaces;
namespace PracticeCalendar.Infrastructure.Notification
{
public class FileEmailSender : IEmailSender
{
private readonly ILogger<FileEmailSender> logger;
public FileEmailSender(ILogger<FileEmailSender> logger)
{
this.logger = logger;
}
public Task SendEmailAsync(string to, string from, string subject, string body)
{
logger.LogDebug($"Sending email from {from}, to {to}, subject {subject}, body {body}");
//TODO save email content to a file
return Task.CompletedTask;
}
}
}
@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using PracticeCalendar.Domain.Entities;
using System.Reflection;
namespace PracticeCalendar.Infrastructure.Persistence
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
public DbSet<Attendee> Atendees => Set<Attendee>();
public DbSet<PracticeEvent> PracticeEvents => Set<PracticeEvent>();
}
}
@@ -0,0 +1,65 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PracticeCalendar.Domain.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PracticeCalendar.Infrastructure.Persistence
{
public class ApplicationDbContextInitialiser
{
private readonly ILogger<ApplicationDbContextInitialiser> logger;
private readonly ApplicationDbContext context;
public ApplicationDbContextInitialiser(ILogger<ApplicationDbContextInitialiser> logger,
ApplicationDbContext context)
{
this.logger = logger;
this.context = context;
}
public async Task InitialiseAsync()
{
try
{
if (context.Database.IsSqlite())
{
await context.Database.MigrateAsync();
}
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while initialising the database.");
throw;
}
}
public async Task SeedAsync()
{
try
{
await TrySeedAsync();
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while seeding the database.");
throw;
}
}
public async Task TrySeedAsync()
{
// Default data
// Seed, if necessary
if (!context.PracticeEvents.Any())
{
context.PracticeEvents.Add(new PracticeEvent("Event 1", "Event 1 desc"));
await context.SaveChangesAsync();
}
}
}
}
@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using PracticeCalendar.Domain.Entities;
namespace PracticeCalendar.Infrastructure.Persistence.Configuration
{
public class AttendeeConfiguration : IEntityTypeConfiguration<Attendee>
{
public void Configure(EntityTypeBuilder<Attendee> builder)
{
builder.Property(x => x.Name)
.HasMaxLength(100)
.IsRequired();
builder.Property(x => x.EmailAddress)
.HasMaxLength(100)
.IsRequired();
}
}
}
@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using PracticeCalendar.Domain.Entities;
namespace PracticeCalendar.Infrastructure.Persistence.Configuration
{
public class PracticeEventConfiguration : IEntityTypeConfiguration<PracticeEvent>
{
public void Configure(EntityTypeBuilder<PracticeEvent> builder)
{
builder.Property(x => x.Title)
.HasMaxLength(120)
.IsRequired();
builder.Property(x => x.Description)
.HasMaxLength(1000)
.IsRequired();
}
}
}
@@ -0,0 +1,12 @@
using Ardalis.Specification.EntityFrameworkCore;
using PracticeCalendar.Domain.Common.Interfaces;
namespace PracticeCalendar.Infrastructure.Persistence
{
public class EfRepository<T> : RepositoryBase<T>, IRepository<T> where T : class, IAggregateRoot
{
public EfRepository(ApplicationDbContext dbContext) : base(dbContext)
{
}
}
}
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="6.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PracticeCalendar.Domain\PracticeCalendar.Domain.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PracticeCalendar.Domain\PracticeCalendar.Domain.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,56 @@
using FluentAssertions;
using PracticeCalendar.Domain.Entities;
namespace PracticeCalendar.UnitTests
{
public class PracticeEventTest
{
string _eventTitle = "Event1";
string _eventDescription = "Description";
string _attendeeName = "Claudiu Farcas";
string _atendeeEmail = "claudiu.farcas@testingbee.com";
[Fact]
public void InitializeProperties()
{
var practiceEvent = new PracticeEvent(_eventTitle, _eventDescription);
practiceEvent.Title.Should().Be(_eventTitle);
practiceEvent.Description.Should().Be(_eventDescription);
practiceEvent.Attendees.Should().HaveCount(0);
}
[Fact]
public void InitializeWithNullShouldThrowException()
{
Action act = () => {
var practiceEvent = new PracticeEvent(null, _eventDescription);
};
act.Should().Throw<ArgumentNullException>();
act = () => {
var practiceEvent = new PracticeEvent(_eventTitle, null);
};
act.Should().Throw<ArgumentNullException>();
act = () => {
var practiceEvent = new PracticeEvent(string.Empty, _eventDescription);
};
act.Should().Throw<ArgumentException>();
act = () => {
var practiceEvent = new PracticeEvent(_eventTitle, string.Empty);
};
act.Should().Throw<ArgumentException>();
}
[Fact]
public void AddAttendeeToEvent()
{
var practiceEvent = new PracticeEvent(_eventTitle, _eventDescription);
var testAttendee = new Attendee(_attendeeName, _atendeeEmail);
practiceEvent.AddAttendee(testAttendee);
practiceEvent.Attendees.Should().HaveCount(1);
}
}
}
+1
View File
@@ -0,0 +1 @@
global using Xunit;
+48
View File
@@ -0,0 +1,48 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32922.545
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PracticeCalendar.API", "PracticeCalendar.Api\PracticeCalendar.API.csproj", "{4B183BF7-16D7-4DA8-B80A-E08DB2CBC827}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PracticeCalendar.Domain", "PracticeCalendar.Domain\PracticeCalendar.Domain.csproj", "{002B8118-8B5A-4CF3-A29D-12A06803221B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PracticeCalendar.Infrastructure", "PracticeCalendar.Infrastructure\PracticeCalendar.Infrastructure.csproj", "{211BEF2A-5FB1-4F55-84FB-88FEF90A8316}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PracticeCalendar.UnitTests", "PracticeCalendar.UnitTests\PracticeCalendar.UnitTests.csproj", "{74849455-5E08-43FE-A718-0872DE7BC350}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5F2B7855-F03D-48C9-8733-FF1E077F18F5}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4B183BF7-16D7-4DA8-B80A-E08DB2CBC827}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4B183BF7-16D7-4DA8-B80A-E08DB2CBC827}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4B183BF7-16D7-4DA8-B80A-E08DB2CBC827}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4B183BF7-16D7-4DA8-B80A-E08DB2CBC827}.Release|Any CPU.Build.0 = Release|Any CPU
{002B8118-8B5A-4CF3-A29D-12A06803221B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{002B8118-8B5A-4CF3-A29D-12A06803221B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{002B8118-8B5A-4CF3-A29D-12A06803221B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{002B8118-8B5A-4CF3-A29D-12A06803221B}.Release|Any CPU.Build.0 = Release|Any CPU
{211BEF2A-5FB1-4F55-84FB-88FEF90A8316}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{211BEF2A-5FB1-4F55-84FB-88FEF90A8316}.Debug|Any CPU.Build.0 = Debug|Any CPU
{211BEF2A-5FB1-4F55-84FB-88FEF90A8316}.Release|Any CPU.ActiveCfg = Release|Any CPU
{211BEF2A-5FB1-4F55-84FB-88FEF90A8316}.Release|Any CPU.Build.0 = Release|Any CPU
{74849455-5E08-43FE-A718-0872DE7BC350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{74849455-5E08-43FE-A718-0872DE7BC350}.Debug|Any CPU.Build.0 = Debug|Any CPU
{74849455-5E08-43FE-A718-0872DE7BC350}.Release|Any CPU.ActiveCfg = Release|Any CPU
{74849455-5E08-43FE-A718-0872DE7BC350}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D24E61FE-EF01-414B-B09C-1B5F9575BD3C}
EndGlobalSection
EndGlobal
+19
View File
@@ -0,0 +1,19 @@
# Practice Calendar Api project
## Projects
- API - (REST) webservice endpoint with OpenAPI capabilities (swagger)
- Domain - containd core elements of the domain (entities, domain events, interfaces)
- Infrastructure - plugged dependencies like persitence, email, files operations
- UnitTests - for aggregate and application domain testing
## Running unit tests
```sh
dotnet test
```
## Running the api service
```sh
dotnet run --project .\PracticeCalendar.Api\PracticeCalendar.API.csproj
```