Skip to content

A modern .NET library for processing and modeling calendar data according to the JSCalendar specification (RFC 8984).

License

Notifications You must be signed in to change notification settings

JMAP-Net/JSCalendar.Net

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JSCalendar.Net

.NET License

A modern .NET implementation of RFC 8984 (JSCalendar) - a JSON representation of calendar data.

Features

  • Full RFC 8984 Compliance - Complete implementation of the JSCalendar specification
  • Native .NET 10 - Built with the latest .NET features
  • Type-Safe - Strongly-typed models with comprehensive validation
  • JSON Serialization - Seamless integration with System.Text.Json
  • Recurrence Support - Full support for recurring events with overrides
  • Localization - Multi-language support via localizations
  • Time Zones - Complete time zone handling

Installation

dotnet add package JSCalendar.Net

Or via NuGet Package Manager:

Install-Package JSCalendar.Net

Quick Start

Creating a Simple Event

using JSCalendar.Net;
using JSCalendar.Net.Enums;
using System.Text.Json;

var meeting = new Event
{
    Uid = "meeting-001",
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0)),
    Duration = new Duration { Hours = 1, Minutes = 30 },
    Title = "Team Meeting",
    Description = "Weekly team sync",
    Status = EventStatus.Confirmed
};

// Serialize to JSON
var json = JsonSerializer.Serialize(meeting, new JsonSerializerOptions 
{ 
    WriteIndented = true 
});

Console.WriteLine(json);

Output:

{
  "@type": "Event",
  "uid": "meeting-001",
  "updated": "2024-12-02T10:00:00Z",
  "start": "2024-12-15T14:00:00",
  "duration": "PT1H30M",
  "title": "Team Meeting",
  "description": "Weekly team sync",
  "status": "confirmed"
}

Deserializing Events

var json = """
{
    "@type": "Event",
    "uid": "event-123",
    "updated": "2024-12-02T10:00:00Z",
    "start": "2024-12-15T14:00:00",
    "title": "Conference Call"
}
""";

var evt = JsonSerializer.Deserialize<Event>(json);
Console.WriteLine($"Event: {evt.Title} at {evt.Start}");

Core Concepts

Events

Events represent scheduled calendar items with a specific start time.

var conference = new Event
{
    Uid = "conf-2024",
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(new DateTime(2024, 12, 15, 9, 0, 0)),
    Duration = new Duration { Hours = 8 },
    Title = "Tech Conference 2024",
    Locations = new Dictionary<string, Location>
    {
        ["venue"] = new Location
        {
            Name = "Convention Center",
            Coordinates = "geo:52.520008,13.404954"
        }
    },
    VirtualLocations = new Dictionary<string, VirtualLocation>
    {
        ["stream"] = new VirtualLocation
        {
            Name = "Live Stream",
            Uri = "https://stream.example.com/conf2024",
            Features = new Dictionary<VirtualLocationFeature, bool>
            {
                [VirtualLocationFeature.Video] = true,
                [VirtualLocationFeature.Chat] = true
            }
        }
    }
};

Tasks

Tasks represent action items with optional due dates.

var task = new Task
{
    Uid = "task-001",
    Updated = DateTimeOffset.UtcNow,
    Title = "Prepare presentation",
    Due = new LocalDateTime(new DateTime(2024, 12, 14, 17, 0, 0)),
    Progress = ProgressStatus.InProcess,
    PercentComplete = 60,
    Priority = 1
};

Groups

Groups allow you to organize related events and tasks together.

var projectGroup = new Group
{
    Uid = "project-2024",
    Updated = DateTimeOffset.UtcNow,
    Title = "Product Launch 2024",
    Description = "All events and tasks related to the product launch",
    Categories = new Dictionary<string, bool>
    {
        ["project"] = true,
        ["marketing"] = true
    },
    Entries = new List<IJSCalendarObject>
    {
        new Event
        {
            Uid = "kickoff-meeting",
            Updated = DateTimeOffset.UtcNow,
            Start = new LocalDateTime(new DateTime(2024, 12, 1, 9, 0, 0)),
            Duration = new Duration { Hours = 2 },
            Title = "Project Kickoff Meeting",
            Description = "Initial project planning session"
        },
        new Task
        {
            Uid = "prepare-marketing",
            Updated = DateTimeOffset.UtcNow,
            Title = "Prepare marketing materials",
            Due = new LocalDateTime(new DateTime(2024, 12, 10, 17, 0, 0)),
            Priority = 1,
            Progress = ProgressStatus.Needs_Action
        },
        new Event
        {
            Uid = "launch-event",
            Updated = DateTimeOffset.UtcNow,
            Start = new LocalDateTime(new DateTime(2024, 12, 15, 10, 0, 0)),
            Duration = new Duration { Hours = 4 },
            Title = "Product Launch Event",
            Status = EventStatus.Confirmed,
            Locations = new Dictionary<string, Location>
            {
                ["venue"] = new Location
                {
                    Name = "Main Auditorium"
                }
            }
        }
    }
};

// Serialize to JSON
var json = JsonSerializer.Serialize(projectGroup, new JsonSerializerOptions 
{ 
    WriteIndented = true 
});

Output:

{
  "@type": "Group",
  "uid": "project-2024",
  "updated": "2024-12-03T10:00:00Z",
  "title": "Product Launch 2024",
  "description": "All events and tasks related to the product launch",
  "categories": {
    "project": true,
    "marketing": true
  },
  "entries": [
    {
      "@type": "Event",
      "uid": "kickoff-meeting",
      "updated": "2024-12-03T10:00:00Z",
      "start": "2024-12-01T09:00:00",
      "duration": "PT2H",
      "title": "Project Kickoff Meeting",
      "description": "Initial project planning session"
    },
    {
      "@type": "Task",
      "uid": "prepare-marketing",
      "updated": "2024-12-03T10:00:00Z",
      "title": "Prepare marketing materials",
      "due": "2024-12-10T17:00:00",
      "priority": 1,
      "progress": "needs-action"
    },
    {
      "@type": "Event",
      "uid": "launch-event",
      "updated": "2024-12-03T10:00:00Z",
      "start": "2024-12-15T10:00:00",
      "duration": "PT4H",
      "title": "Product Launch Event",
      "status": "confirmed",
      "locations": {
        "venue": {
          "name": "Main Auditorium"
        }
      }
    }
  ]
}

Recurring Events

Create events that repeat with flexible recurrence rules.

var weeklyMeeting = new Event
{
    Uid = "weekly-standup",
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(new DateTime(2024, 12, 2, 10, 0, 0)),
    Duration = new Duration { Minutes = 30 },
    Title = "Daily Standup",
    RecurrenceRules = new List<RecurrenceRule>
    {
        new RecurrenceRule
        {
            Frequency = RecurrenceFrequency.Weekly,
            ByDay = new List<NDay>
            {
                new NDay { Day = DayOfWeek.Monday },
                new NDay { Day = DayOfWeek.Tuesday },
                new NDay { Day = DayOfWeek.Wednesday },
                new NDay { Day = DayOfWeek.Thursday },
                new NDay { Day = DayOfWeek.Friday }
            },
            Until = new LocalDateTime(new DateTime(2024, 12, 31, 23, 59, 59))
        }
    }
};

Recurrence Overrides

Override specific instances of recurring events.

var recurringEvent = new Event
{
    Uid = "meeting-recurring",
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(new DateTime(2024, 12, 1, 10, 0, 0)),
    Title = "Weekly Meeting",
    RecurrenceRules = new List<RecurrenceRule>
    {
        new RecurrenceRule { Frequency = RecurrenceFrequency.Weekly }
    },
    RecurrenceOverrides = new Dictionary<LocalDateTime, PatchObject>
    {
        // Special meeting on Dec 15
        [new LocalDateTime(new DateTime(2024, 12, 15, 10, 0, 0))] = new PatchObject
        {
            ["title"] = "Special Year-End Meeting",
            ["duration"] = "PT2H",  // 2 hours instead of default
            ["locations/main/name"] = "Large Conference Room"
        },
        // Cancel meeting on Dec 25 (Christmas)
        [new LocalDateTime(new DateTime(2024, 12, 25, 10, 0, 0))] = new PatchObject
        {
            ["excluded"] = true
        }
    }
};

Participants

Add participants with roles and participation status.

var meeting = new Event
{
    Uid = "team-meeting",
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(DateTime.Now),
    Participants = new Dictionary<string, Participant>
    {
        ["organizer"] = new Participant
        {
            Name = "Alice Smith",
            Email = "alice@example.com",
            Roles = new Dictionary<ParticipantRole, bool>
            {
                [ParticipantRole.Owner] = true,
                [ParticipantRole.Chair] = true
            },
            ParticipationStatus = ParticipationStatus.Accepted
        },
        ["attendee1"] = new Participant
        {
            Name = "Bob Johnson",
            Email = "bob@example.com",
            Roles = new Dictionary<ParticipantRole, bool>
            {
                [ParticipantRole.Attendee] = true
            },
            ParticipationStatus = ParticipationStatus.Tentative,
            ExpectReply = true
        }
    }
};

Alerts

Set up reminders and notifications.

var eventWithAlerts = new Event
{
    Uid = "important-meeting",
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0)),
    Alerts = new Dictionary<string, Alert>
    {
        ["reminder15"] = new Alert
        {
            Trigger = new OffsetTrigger
            {
                Offset = "-PT15M",  // 15 minutes before
                RelativeTo = TriggerRelation.Start
            },
            Action = AlertAction.Display
        },
        ["reminder1day"] = new Alert
        {
            Trigger = new OffsetTrigger
            {
                Offset = "-P1D",  // 1 day before
                RelativeTo = TriggerRelation.Start
            },
            Action = AlertAction.Email
        }
    }
};

Localizations

Support multiple languages.

var internationalEvent = new Event
{
    Uid = "global-event",
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(DateTime.Now),
    Title = "International Conference",
    Description = "Annual technology conference",
    Locale = "en",
    Localizations = new Dictionary<string, PatchObject>
    {
        ["de"] = new PatchObject
        {
            ["title"] = "Internationale Konferenz",
            ["description"] = "Jährliche Technologiekonferenz"
        },
        ["fr"] = new PatchObject
        {
            ["title"] = "Conférence Internationale",
            ["description"] = "Conférence technologique annuelle"
        },
        ["es"] = new PatchObject
        {
            ["title"] = "Conferencia Internacional",
            ["description"] = "Conferencia tecnológica anual"
        }
    }
};

Advanced Features

Duration Format

JSCalendar uses ISO 8601 duration format:

// Various duration examples
var durations = new[]
{
    new Duration { Hours = 1 },                    // PT1H
    new Duration { Minutes = 30 },                 // PT30M
    new Duration { Hours = 2, Minutes = 15 },      // PT2H15M
    new Duration { Days = 1 },                     // P1D
    new Duration { Weeks = 2 },                    // P2W
    new Duration { Days = 1, Hours = 3 }           // P1DT3H
};

// From TimeSpan
var duration = Duration.FromTimeSpan(TimeSpan.FromHours(1.5));
Console.WriteLine(duration.ToString());  // PT1H30M

PatchObject for Dynamic Updates

PatchObjects allow you to modify properties without redefining entire objects.

var patch = new PatchObject
{
    // Update top-level property
    ["title"] = "Updated Title",
    
    // Update nested property using JSON Pointer notation
    ["locations/venue/name"] = "New Venue Name",
    
    // Remove a property
    ["alerts"] = null,
    
    // Add complex nested data
    ["participants/newPerson/email"] = "new@example.com"
};

// Validation
if (patch.Validate())
{
    Console.WriteLine("Patch is valid!");
}

Time Zones

Handle time zones properly:

var zonedEvent = new Event
{
    Uid = "zoned-event",
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0)),
    TimeZone = "Europe/Berlin",
    TimeZones = new Dictionary<string, TimeZone>
    {
        ["Europe/Berlin"] = new TimeZone
        {
            TzId = "Europe/Berlin",
            Updated = DateTimeOffset.UtcNow
        }
    }
};

Validation

JSCalendar.Net includes built-in validation:

// PatchObject validation
var patch = new PatchObject
{
    ["title"] = "Valid",
    ["items/0/name"] = "Invalid"  // Array indices not allowed
};

if (!patch.Validate())
{
    Console.WriteLine("Invalid patch!");
}

// Check forbidden properties for recurrence overrides
if (PatchObject.IsForbiddenForRecurrence("uid"))
{
    Console.WriteLine("Cannot patch 'uid' in recurrence overrides");
}

Serialization Options

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

var json = JsonSerializer.Serialize(myEvent, options);

Best Practices

1. Always Set Required Properties

// Good
var evt = new Event
{
    Uid = Guid.NewGuid().ToString(),
    Updated = DateTimeOffset.UtcNow,
    Start = new LocalDateTime(DateTime.Now)
};

// Bad - Missing required properties will cause errors
var evt = new Event();

2. Use Meaningful UIDs

// Good - Descriptive and unique
Uid = $"meeting-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid():N}"

// Bad - Not unique
Uid = "meeting"

3. Validate Before Serialization

if (myPatchObject.Validate())
{
    var json = JsonSerializer.Serialize(myPatchObject);
}

4. Handle Time Zones Properly

// For floating time (no time zone)
Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0))

// For zoned time
Start = new LocalDateTime(new DateTime(2024, 12, 15, 14, 0, 0)),
TimeZone = "Europe/Berlin"

Testing

Run the test suite:

cd tests/JSCalendar.Net.Tests
dotnet test

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development Setup

git clone https://github.com/JMAP-Net/JSCalendar.Net
cd JSCalendar.Net
dotnet restore
dotnet build
dotnet test

RFC 8984 Compliance

This library implements the complete JSCalendar specification as defined in RFC 8984.

Supported Sections:

  • Section 2: Object Model (Event, Task, Group)
  • Section 4: Common Properties
  • Section 5: Type-Specific Properties
  • Recurrence Rules (Section 4.3)
  • PatchObject (Section 1.4.9)
  • Time Zones (Section 4.7)
  • Participants (Section 4.4)
  • Alerts (Section 4.5)
  • Localization (Section 4.6)

Resources

License

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

Acknowledgments

  • Uses System.Text.Json for high-performance JSON serialization
  • Built with ❤️ for the .NET community
  • Made with ☕ and .NET

About

A modern .NET library for processing and modeling calendar data according to the JSCalendar specification (RFC 8984).

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages