Endjin.FreeAgent.Domain.Extensions 1.0.5

dotnet add package Endjin.FreeAgent.Domain.Extensions --version 1.0.5
                    
NuGet\Install-Package Endjin.FreeAgent.Domain.Extensions -Version 1.0.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Endjin.FreeAgent.Domain.Extensions" Version="1.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Endjin.FreeAgent.Domain.Extensions" Version="1.0.5" />
                    
Directory.Packages.props
<PackageReference Include="Endjin.FreeAgent.Domain.Extensions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Endjin.FreeAgent.Domain.Extensions --version 1.0.5
                    
#r "nuget: Endjin.FreeAgent.Domain.Extensions, 1.0.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Endjin.FreeAgent.Domain.Extensions@1.0.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Endjin.FreeAgent.Domain.Extensions&version=1.0.5
                    
Install as a Cake Addin
#tool nuget:?package=Endjin.FreeAgent.Domain.Extensions&version=1.0.5
                    
Install as a Cake Tool

Endjin.FreeAgent.Domain.Extensions

Extension types and helper methods for the Endjin.FreeAgent.Domain library, providing additional domain models and utilities for enhanced FreeAgent API interactions.

Features

  • Extended Domain Models - Timesheet aggregations, activity summaries, and employment data
  • C# 14 Extension Members - Clean, property-like extension methods for domain types
  • Date/Time Extensions - Helper methods using NodaTime for dates and fiscal periods
  • Currency Extensions - Decimal formatting and currency handling utilities
  • Timesheet Models - Weekly, monthly, and yearly timesheet aggregations
  • User Settings - Employee payroll configuration loaded from YAML
  • Activity Tracking - Comprehensive timesheet activity summaries by project

Installation

Install via NuGet Package Manager:

dotnet add package Endjin.FreeAgent.Domain.Extensions

Or via Package Manager Console:

Install-Package Endjin.FreeAgent.Domain.Extensions

Domain Models

Timesheet Models

Timesheet aggregation models for various time periods:

using Endjin.FreeAgent.Domain.Extensions;
using NodaTime;

// Weekly timesheet with timeslips
WeeklyTimesheet weeklyTimesheet = new()
{
    Week = new Week
    {
        StartOfWeek = new LocalDate(2024, 10, 14),
        EndOfWeek = new LocalDate(2024, 10, 20)
    },
    User = user,
    WeekOfYear = 42,
    Timeslips = timeslips
};

// Use extension members to calculate hours
decimal hours = weeklyTimesheet.CalculateHours();
bool isValid = weeklyTimesheet.IsValid();  // Validates against 40-hour week

Console.WriteLine($"Week 42: {hours}h (Valid: {isValid})");

// Monthly timesheet
MonthlyTimesheet monthlyTimesheet = new()
{
    Month = new Month
    {
        StartOfMonth = new LocalDate(2024, 10, 1),
        EndOfMonth = new LocalDate(2024, 10, 31)
    },
    User = user,
    Timeslips = timeslips
};

// Monthly timesheets have a TargetHours() method
decimal monthlyHours = monthlyTimesheet.CalculateHours();
decimal target = monthlyTimesheet.TargetHours();
bool monthValid = monthlyTimesheet.IsValid();

Console.WriteLine($"October: {monthlyHours}h / {target}h (Valid: {monthValid})");

// Yearly timesheet aggregation
YearlyTimesheet yearlyTimesheet = new()
{
    Year = 2024,
    User = user,
    UserSettings = userSettings,
    WeeklyTimesheets = weeklyTimesheets
};

// Yearly timesheets have additional aggregation methods
decimal totalHours = yearlyTimesheet.CalculateTotalHours();
decimal weeklyAverage = yearlyTimesheet.CalculateWeeklyAverage();
decimal missingHours = yearlyTimesheet.CalculateTotalMissingHours();

Console.WriteLine($"Year 2024: {totalHours}h total, {weeklyAverage}h/week average");

Timesheet Activity Models

Track activity by project with detailed summaries:

using Endjin.FreeAgent.Domain.Extensions;

// Individual activity entry
TimesheetActivityEntry entry = new()
{
    ProjectId = "123",
    ProjectName = "Client Project A",
    Hours = 8.5m,
    Date = DateTime.Now
};

// Activity grouped by project
TimesheetActivity activity = new()
{
    ProjectId = "123",
    ProjectName = "Client Project A",
    Entries = new List<TimesheetActivityEntry> { entry }
};

// Complete activity summary for a user
TimesheetActivitySummary summary = new()
{
    UserId = "user123",
    Activities = new List<TimesheetActivity> { activity },
    TotalHours = 40.0m,
    ProjectCount = 5
};

Console.WriteLine($"User {summary.UserId}: {summary.TotalHours}h across {summary.ProjectCount} projects");

User Settings (Employment Data)

User settings represent employee payroll configuration:

using Endjin.FreeAgent.Domain.Extensions;

// UserSettings stores employment and payroll data
UserSettings settings = new()
{
    Id = "usr_12345",
    FirstName = "Emma",
    LastName = "Thompson",
    Salutation = "Dr",
    MiddleInitials = "J",
    Address = "123 Main Street, Cambridge",
    PayRef = "EMP001",
    IsDirector = true,
    StartDate = new DateTime(2020, 1, 15),
    EndDate = null,  // null means currently employed
    Roles = new List<EmploymentRole>
    {
        new()
        {
            Title = "Software Engineer",
            Cost = 65000,
            StartDate = new DateTime(2020, 1, 15),
            EndDate = new DateTime(2022, 6, 30)
        },
        new()
        {
            Title = "Senior Engineer",
            Cost = 85000,
            StartDate = new DateTime(2022, 7, 1),
            EndDate = null  // Current role
        }
    }
};

// Use extension members for convenient properties
string fullName = settings.FullName;  // Extension member property
bool isActive = settings.IsActive;    // Extension member property

Console.WriteLine($"{fullName} - Active: {isActive}");

// Find current role
EmploymentRole? currentRole = settings.Roles
    .OrderBy(r => r.StartDate)
    .LastOrDefault(r => r.EndDate == null);

Console.WriteLine($"Current Role: {currentRole?.Title} - {currentRole?.Cost:C}");

User Summary Models

// Basic user summary
UserSummary summary = new()
{
    Id = "usr_123",
    FirstName = "John",
    LastName = "Smith",
    Email = "john.smith@example.com"
};

// FullName is a computed property
string fullName = summary.FullName;  // "John Smith"

// User activity summary with counts
UserActivitySummary activitySummary = new()
{
    UserCount = 10,
    Users = userList
};

Project Models

// Basic project summary
ProjectSummary projectSummary = new()
{
    Id = "proj_123",
    Name = "Website Development",
    ContactName = "Acme Corp",
    ContactId = "contact_456"
};

// Detailed project entry with time tracking
ProjectDetail detail = new()
{
    Customer = "Acme Corp",
    Project = "Website Development",
    User = "John Smith",
    Date = DateTimeOffset.Now,
    Year = 2024,
    WeekNumber = 42,
    Effort = 8.5m,
    DayRate = 800.0m,
    HourlyRate = 100.0m,
    Level = "Senior",
    Cost = 850.0m,
    Comment = "Frontend development work",
    BillingPeriod = "2024-10",
    Week = new Week
    {
        StartOfWeek = new LocalDate(2024, 10, 14),
        EndOfWeek = new LocalDate(2024, 10, 20)
    }
};

// Project collection summary
ProjectDetails projectDetails = new()
{
    Project = project,
    Timeslips = timeslips,
    TotalHours = 120.5m,
    TotalCost = 12050.0m
};

// Projects summary with counts
ProjectsSummary projectsSummary = new()
{
    ProjectCount = 25,
    Projects = projectList
};

Extension Members (C# 14)

This library uses C# 14 extension members for clean, intuitive APIs:

Timesheet Extensions

using Endjin.FreeAgent.Domain.Extensions;

WeeklyTimesheet weekly = /* ... */;

// Extension member methods
decimal hours = weekly.CalculateHours();
bool isValid = weekly.IsValid();  // Validates against WorkConstants.HoursPerWeek (40)

MonthlyTimesheet monthly = /* ... */;

// MonthlyTimesheet has additional extension methods
decimal monthlyHours = monthly.CalculateHours();
decimal targetHours = monthly.TargetHours();  // Calculates based on working days in month
bool monthlyValid = monthly.IsValid();

YearlyTimesheet yearly = /* ... */;

// YearlyTimesheet has aggregation methods
decimal totalHours = yearly.CalculateTotalHours();
decimal weeklyAverage = yearly.CalculateWeeklyAverage();
decimal missingHours = yearly.CalculateTotalMissingHours();

User Extensions

using Endjin.FreeAgent.Domain.Extensions;

UserSettings user = /* ... */;

// Extension member properties
string fullName = user.FullName;  // "{FirstName} {LastName}"
bool isActive = user.IsActive;     // true if EndDate is null

Console.WriteLine($"{fullName} - Active: {isActive}");

JournalSet Extensions

using Endjin.FreeAgent.Domain.Extensions;
using Endjin.FreeAgent.Client;

JournalSet journalSet = /* ... */;

// Calculate balance
decimal balance = journalSet.Balance();

Extension Methods

DateTime Extensions

Helper methods for working with dates and fiscal periods using NodaTime:

using Endjin.FreeAgent.Domain.Extensions;
using NodaTime;

// Get week number
int weekNumber = DateTime.Now.WeekNumber();

// Get last week number of the year
int lastWeek = DateTime.Now.LastWeekNumber();

// Get first day of month
DateTime firstDay = DateTime.Now.FirstDayOfMonth();

// Get last day of month
DateTime lastDay = DateTime.Now.LastDayOfMonth();

// Get Week object for a specific week
Week week = DateTimeExtensions.GetWeek(2024, 42);
Console.WriteLine($"Week starts: {week.StartOfWeek}");
Console.WriteLine($"Week ends: {week.EndOfWeek}");

// Get Month object for a specific month
Month month = DateTimeExtensions.GetMonth(2024, 10);
Console.WriteLine($"Month starts: {month.StartOfMonth}");
Console.WriteLine($"Month ends: {month.EndOfMonth}");

// Extract DateTime from month-year string
DateTime? date = DateTimeExtensions.ExtractDateTimeFromMonthYearString("2024-10");

// Calculate working hours between dates
LocalDate startDate = new LocalDate(2024, 10, 1);
LocalDate endDate = new LocalDate(2024, 10, 31);
decimal workingHours = DateTimeExtensions.CalculateWorkingHours(startDate, endDate);

Console.WriteLine($"Working hours in October: {workingHours}");

Currency Extensions

Decimal formatting and currency utilities:

using Endjin.FreeAgent.Domain.Extensions;

// Format as currency (default UK format)
decimal amount = 1234.56m;
string formatted = amount.ToCurrencyString();  // "£1,234.56"

// Specify culture
string usFormatted = amount.ToCurrencyString("en-US");  // "$1,234.56"

// Round to currency precision (defined in WorkConstants.CurrencyDecimalPlaces)
decimal rounded = amount.Round();  // 1234.56

// Calculate cost from timeslip
Timeslip slip = new()
{
    Hours = 8.5m,
    /* ... */
};
decimal cost = slip.ToCost();

UserSettingsRepository

YAML-based repository for loading employee payroll configuration:

using Endjin.FreeAgent.Domain.Extensions;
using Spectre.IO;

// Initialize file system abstraction
FileSystem fileSystem = new();

// Initialize repository with file system and YAML file
UserSettingsRepository repository = new(fileSystem, fileSystem.GetFile("Artefacts/example-employee-payroll.yml"));

// Get all user settings asynchronously
List<UserSettings> allUsers = await repository.GetAllUserSettingsAsync();

// Get only active users (EndDate is null)
List<UserSettings> activeUsers = await repository.GetActiveUserSettingsAsync();

// Clear the cache to force reload
repository.ClearCache();

// Work with user settings
foreach (UserSettings user in activeUsers)
{
    string fullName = user.FullName;
    bool isActive = user.IsActive;

    Console.WriteLine($"{fullName} - Active: {isActive}");

    // Find current role
    EmploymentRole? currentRole = user.Roles
        .OrderBy(r => r.StartDate)
        .LastOrDefault(r => r.EndDate == null);

    if (currentRole != null)
    {
        Console.WriteLine($"  {currentRole.Title}: {currentRole.Cost:C}");
    }
}

YAML File Format

The repository loads data from a YAML file:

# Artefacts/example-employee-payroll.yml
- id: "usr_12345"
  firstName: "Emma"
  lastName: "Thompson"
  salutation: "Dr"
  middleInitials: "J"
  address: "123 Main Street, Cambridge"
  payRef: "EMP001"
  isDirector: true
  startDate: 2020-01-15
  roles:
    - title: "Software Engineer"
      cost: 65000
      startDate: 2020-01-15
      endDate: 2022-06-30
    - title: "Senior Engineer"
      cost: 85000
      startDate: 2022-07-01

Time Period Models

Models representing time periods using NodaTime:

using NodaTime;

// Week model with NodaTime LocalDate
Week week = new()
{
    StartOfWeek = new LocalDate(2024, 10, 14),
    EndOfWeek = new LocalDate(2024, 10, 20)
};

// Month model with NodaTime LocalDate
Month month = new()
{
    StartOfMonth = new LocalDate(2024, 10, 1),
    EndOfMonth = new LocalDate(2024, 10, 31)
};

// Timesheet is a container for yearly timesheets
Timesheet timesheet = new()
{
    YearlyTimesheets = new List<YearlyTimesheet>
    {
        new() { Year = 2024, /* ... */ }
    }
};

Work Constants

Standard constants for time and currency calculations:

using Endjin.FreeAgent.Domain.Extensions;

// Hours per day constant
decimal hoursPerDay = WorkConstants.HoursPerDay;  // 8.0

// Hours per week constant
decimal hoursPerWeek = WorkConstants.HoursPerWeek;  // 40.0

// Currency decimal places
int currencyPlaces = WorkConstants.CurrencyDecimalPlaces;  // 2

Employment Roles

Domain model for employment roles with compensation history:

EmploymentRole role = new()
{
    Title = "Senior Developer",
    Cost = 85000,  // Annual cost to company
    StartDate = new DateTime(2022, 7, 1),
    EndDate = null  // null means current role
};

// Check if role is current
bool isCurrent = role.EndDate == null;

Complete Example

Here's a complete example showing how to work with timesheets and user settings:

using Endjin.FreeAgent.Domain.Extensions;
using NodaTime;
using Spectre.IO;

// Initialize file system abstraction
var fileSystem = new FileSystem();

// Load user settings from YAML
UserSettingsRepository repository = new(fileSystem, fileSystem.GetFile("Artefacts/employee-payroll.yml"));
List<UserSettings> activeUsers = await repository.GetActiveUserSettingsAsync();

// Create a weekly timesheet
WeeklyTimesheet weekly = new()
{
    Week = DateTimeExtensions.GetWeek(2024, 42),
    User = user,
    WeekOfYear = 42,
    Timeslips = timeslips
};

// Use extension members to analyze
decimal hours = weekly.CalculateHours();
bool isValid = weekly.IsValid();

Console.WriteLine($"Week 42: {hours}h (Valid: {isValid})");

// Find user settings for the timesheet user
UserSettings? userSettings = activeUsers.FirstOrDefault(u => u.Id == weekly.User?.Id);

if (userSettings != null)
{
    // Use extension member properties
    string fullName = userSettings.FullName;
    bool isActive = userSettings.IsActive;

    Console.WriteLine($"User: {fullName} (Active: {isActive})");

    // Get current role
    EmploymentRole? currentRole = userSettings.Roles
        .OrderBy(r => r.StartDate)
        .LastOrDefault(r => r.EndDate == null);

    if (currentRole != null)
    {
        Console.WriteLine($"Role: {currentRole.Title} - {currentRole.Cost:C}");
    }
}

// Create monthly aggregation
MonthlyTimesheet monthly = new()
{
    Month = DateTimeExtensions.GetMonth(2024, 10),
    User = user,
    Timeslips = timeslips
};

decimal monthlyHours = monthly.CalculateHours();
decimal targetHours = monthly.TargetHours();
Console.WriteLine($"October: {monthlyHours}h / {targetHours}h");

Requirements

  • .NET 10.0 or later
  • Endjin.FreeAgent.Domain
  • NodaTime (for LocalDate types)
  • YamlDotNet (for UserSettingsRepository)

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Support

For support, please contact Endjin Limited or raise an issue on our GitHub repository.


Copyright (c) Endjin Limited. All rights reserved.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Endjin.FreeAgent.Domain.Extensions:

Package Downloads
Endjin.FreeAgent.Client.Extensions

Extension methods and additional functionality for the Endjin.FreeAgent.Client library, providing timesheets management, project filtering, user settings, and activity summaries.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.5 304 11/21/2025
1.0.4 229 11/16/2025
1.0.3 217 11/16/2025
1.0.2 215 11/16/2025
1.0.1 155 11/15/2025
1.0.0 276 11/13/2025
1.0.0-preview.2 142 9/23/2025