feat: implement Phase 1 — solution skeleton and domain model
Creates the 9-project .NET 8 solution (5 src + 4 test) with Marathon.Domain fully implemented: value objects (SportCode, EventId, OddsRate, OddsValue, BetScope hierarchy), enums (Side, BetType, OddsSource, AnomalyKind), and entities (Sport, Country, League, Event, Bet, OddsSnapshot, EventResult, Anomaly) with all invariants enforced in constructors. 96 domain tests pass (FluentAssertions + xUnit). Directory.Build.props and Directory.Packages.props centralise build settings and NuGet versions. Both Marathon.sln and Marathon.slnx are committed; dotnet build Marathon.sln succeeds with 0 warnings/errors.
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marathon.Domain\Marathon.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,5 @@
|
||||
// Phase 2/3/4 will populate this project.
|
||||
// This file exists only to prevent the compiler from treating the project as empty.
|
||||
namespace Marathon.Application;
|
||||
|
||||
internal static class Placeholder { }
|
||||
@@ -0,0 +1,37 @@
|
||||
using Marathon.Domain.Enums;
|
||||
using Marathon.Domain.ValueObjects;
|
||||
|
||||
namespace Marathon.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A detected anomaly in odds behaviour for an event.
|
||||
/// <c>Score</c> is a normalised confidence score in [0, 1] — higher means stronger signal.
|
||||
/// <c>EvidenceJson</c> is a JSON string containing the raw evidence timeline (snapshots, diffs).
|
||||
/// </summary>
|
||||
public sealed record Anomaly(
|
||||
Guid Id,
|
||||
EventId EventId,
|
||||
DateTimeOffset DetectedAt,
|
||||
AnomalyKind Kind,
|
||||
decimal Score,
|
||||
string EvidenceJson)
|
||||
{
|
||||
public Guid Id { get; } = Id == Guid.Empty
|
||||
? throw new ArgumentException("Anomaly Id must not be an empty GUID.", nameof(Id))
|
||||
: Id;
|
||||
|
||||
public EventId EventId { get; } = EventId ?? throw new ArgumentNullException(nameof(EventId));
|
||||
|
||||
public DateTimeOffset DetectedAt { get; } = DetectedAt;
|
||||
|
||||
public AnomalyKind Kind { get; } = Kind;
|
||||
|
||||
public decimal Score { get; } = Score is >= 0m and <= 1m
|
||||
? Score
|
||||
: throw new ArgumentOutOfRangeException(nameof(Score), Score,
|
||||
"Anomaly Score must be in the range [0, 1].");
|
||||
|
||||
public string EvidenceJson { get; } = string.IsNullOrWhiteSpace(EvidenceJson)
|
||||
? throw new ArgumentException("EvidenceJson must not be empty.", nameof(EvidenceJson))
|
||||
: EvidenceJson;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using Marathon.Domain.Enums;
|
||||
using Marathon.Domain.ValueObjects;
|
||||
|
||||
namespace Marathon.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A single betting option within an odds snapshot.
|
||||
/// Invariants enforced in constructor:
|
||||
/// <list type="bullet">
|
||||
/// <item>Win: Side ∈ {Side1, Side2}, Value == null</item>
|
||||
/// <item>Draw: Side == Draw, Value == null</item>
|
||||
/// <item>WinFora: Side ∈ {Side1, Side2}, Value != null (handicap threshold)</item>
|
||||
/// <item>Total: Side ∈ {Less, More}, Value != null (total threshold)</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed record Bet
|
||||
{
|
||||
public BetScope Scope { get; }
|
||||
public BetType Type { get; }
|
||||
public Side Side { get; }
|
||||
public OddsValue? Value { get; }
|
||||
public OddsRate Rate { get; }
|
||||
|
||||
public Bet(BetScope scope, BetType type, Side side, OddsValue? value, OddsRate rate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scope);
|
||||
ArgumentNullException.ThrowIfNull(rate);
|
||||
|
||||
ValidateInvariants(type, side, value);
|
||||
|
||||
Scope = scope;
|
||||
Type = type;
|
||||
Side = side;
|
||||
Value = value;
|
||||
Rate = rate;
|
||||
}
|
||||
|
||||
private static void ValidateInvariants(BetType type, Side side, OddsValue? value)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case BetType.Win:
|
||||
if (side is not (Side.Side1 or Side.Side2))
|
||||
throw new ArgumentException(
|
||||
$"Win bet requires Side1 or Side2. Got: {side}.", nameof(side));
|
||||
if (value is not null)
|
||||
throw new ArgumentException(
|
||||
"Win bet must have Value == null.", nameof(value));
|
||||
break;
|
||||
|
||||
case BetType.Draw:
|
||||
if (side != Side.Draw)
|
||||
throw new ArgumentException(
|
||||
$"Draw bet requires Side == Draw. Got: {side}.", nameof(side));
|
||||
if (value is not null)
|
||||
throw new ArgumentException(
|
||||
"Draw bet must have Value == null.", nameof(value));
|
||||
break;
|
||||
|
||||
case BetType.WinFora:
|
||||
if (side is not (Side.Side1 or Side.Side2))
|
||||
throw new ArgumentException(
|
||||
$"WinFora bet requires Side1 or Side2. Got: {side}.", nameof(side));
|
||||
if (value is null)
|
||||
throw new ArgumentException(
|
||||
"WinFora bet requires a non-null handicap Value.", nameof(value));
|
||||
break;
|
||||
|
||||
case BetType.Total:
|
||||
if (side is not (Side.Less or Side.More))
|
||||
throw new ArgumentException(
|
||||
$"Total bet requires Side == Less or More. Got: {side}.", nameof(side));
|
||||
if (value is null)
|
||||
throw new ArgumentException(
|
||||
"Total bet requires a non-null threshold Value.", nameof(value));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown BetType.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Marathon.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A country or geographic group associated with a league.
|
||||
/// <c>Code</c> is the bookmaker's string identifier (e.g., breadcrumb text).
|
||||
/// </summary>
|
||||
public sealed record Country(string Code, string NameRu, string NameEn)
|
||||
{
|
||||
public string Code { get; } = string.IsNullOrWhiteSpace(Code)
|
||||
? throw new ArgumentException("Country Code must not be empty.", nameof(Code))
|
||||
: Code;
|
||||
|
||||
public string NameRu { get; } = string.IsNullOrWhiteSpace(NameRu)
|
||||
? throw new ArgumentException("NameRu must not be empty.", nameof(NameRu))
|
||||
: NameRu;
|
||||
|
||||
public string NameEn { get; } = string.IsNullOrWhiteSpace(NameEn)
|
||||
? throw new ArgumentException("NameEn must not be empty.", nameof(NameEn))
|
||||
: NameEn;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Marathon.Domain.ValueObjects;
|
||||
|
||||
namespace Marathon.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A sporting event that can be bet on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><c>ScheduledAt</c> is stored in Europe/Moscow time (UTC+3, no DST).
|
||||
/// The offset <c>+03:00</c> is baked in — it is NOT converted to UTC.
|
||||
/// This matches <c>initData.serverTime</c> from the scraped page, which is in Moscow time.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed record Event(
|
||||
EventId Id,
|
||||
SportCode Sport,
|
||||
string CountryCode,
|
||||
string LeagueId,
|
||||
string Category,
|
||||
DateTimeOffset ScheduledAt,
|
||||
string Side1Name,
|
||||
string Side2Name)
|
||||
{
|
||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
||||
|
||||
public EventId Id { get; } = Id ?? throw new ArgumentNullException(nameof(Id));
|
||||
|
||||
public SportCode Sport { get; } = Sport ?? throw new ArgumentNullException(nameof(Sport));
|
||||
|
||||
public string CountryCode { get; } = string.IsNullOrWhiteSpace(CountryCode)
|
||||
? throw new ArgumentException("CountryCode must not be empty.", nameof(CountryCode))
|
||||
: CountryCode;
|
||||
|
||||
public string LeagueId { get; } = string.IsNullOrWhiteSpace(LeagueId)
|
||||
? throw new ArgumentException("LeagueId must not be empty.", nameof(LeagueId))
|
||||
: LeagueId;
|
||||
|
||||
public string Category { get; } = Category ?? string.Empty;
|
||||
|
||||
public DateTimeOffset ScheduledAt { get; } = ScheduledAt.Offset == MoscowOffset
|
||||
? ScheduledAt
|
||||
: throw new ArgumentException(
|
||||
$"ScheduledAt must be in Europe/Moscow time (UTC+03:00). " +
|
||||
$"Received offset: {ScheduledAt.Offset:hh\\:mm}. " +
|
||||
"Convert to Moscow time before constructing the Event.",
|
||||
nameof(ScheduledAt));
|
||||
|
||||
public string Side1Name { get; } = string.IsNullOrWhiteSpace(Side1Name)
|
||||
? throw new ArgumentException("Side1Name must not be empty.", nameof(Side1Name))
|
||||
: Side1Name;
|
||||
|
||||
public string Side2Name { get; } = string.IsNullOrWhiteSpace(Side2Name)
|
||||
? throw new ArgumentException("Side2Name must not be empty.", nameof(Side2Name))
|
||||
: Side2Name;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Marathon.Domain.Enums;
|
||||
using Marathon.Domain.ValueObjects;
|
||||
|
||||
namespace Marathon.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// The final result of a sporting event after it has completed.
|
||||
/// </summary>
|
||||
public sealed record EventResult(
|
||||
EventId EventId,
|
||||
int Side1Score,
|
||||
int Side2Score,
|
||||
Side WinnerSide,
|
||||
DateTimeOffset CompletedAt)
|
||||
{
|
||||
public EventId EventId { get; } = EventId ?? throw new ArgumentNullException(nameof(EventId));
|
||||
|
||||
public int Side1Score { get; } = Side1Score >= 0
|
||||
? Side1Score
|
||||
: throw new ArgumentOutOfRangeException(nameof(Side1Score), "Score must be non-negative.");
|
||||
|
||||
public int Side2Score { get; } = Side2Score >= 0
|
||||
? Side2Score
|
||||
: throw new ArgumentOutOfRangeException(nameof(Side2Score), "Score must be non-negative.");
|
||||
|
||||
public Side WinnerSide { get; } = WinnerSide is Side.Side1 or Side.Side2 or Side.Draw
|
||||
? WinnerSide
|
||||
: throw new ArgumentException(
|
||||
$"WinnerSide must be Side1, Side2, or Draw. Got: {WinnerSide}.", nameof(WinnerSide));
|
||||
|
||||
public DateTimeOffset CompletedAt { get; } = CompletedAt;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Marathon.Domain.ValueObjects;
|
||||
|
||||
namespace Marathon.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A sports league or tournament within a country and sport.
|
||||
/// </summary>
|
||||
public sealed record League(
|
||||
string Id,
|
||||
SportCode Sport,
|
||||
string Country,
|
||||
string NameRu,
|
||||
string NameEn,
|
||||
string Category)
|
||||
{
|
||||
public string Id { get; } = string.IsNullOrWhiteSpace(Id)
|
||||
? throw new ArgumentException("League Id must not be empty.", nameof(Id))
|
||||
: Id;
|
||||
|
||||
public SportCode Sport { get; } = Sport ?? throw new ArgumentNullException(nameof(Sport));
|
||||
|
||||
public string Country { get; } = string.IsNullOrWhiteSpace(Country)
|
||||
? throw new ArgumentException("Country must not be empty.", nameof(Country))
|
||||
: Country;
|
||||
|
||||
public string NameRu { get; } = string.IsNullOrWhiteSpace(NameRu)
|
||||
? throw new ArgumentException("NameRu must not be empty.", nameof(NameRu))
|
||||
: NameRu;
|
||||
|
||||
public string NameEn { get; } = string.IsNullOrWhiteSpace(NameEn)
|
||||
? throw new ArgumentException("NameEn must not be empty.", nameof(NameEn))
|
||||
: NameEn;
|
||||
|
||||
public string Category { get; } = Category ?? string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Marathon.Domain.Enums;
|
||||
using Marathon.Domain.ValueObjects;
|
||||
|
||||
namespace Marathon.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A point-in-time capture of all odds for a specific event.
|
||||
/// </summary>
|
||||
public sealed record OddsSnapshot
|
||||
{
|
||||
public EventId EventId { get; }
|
||||
public DateTimeOffset CapturedAt { get; }
|
||||
public OddsSource Source { get; }
|
||||
public IReadOnlyList<Bet> Bets { get; }
|
||||
|
||||
public OddsSnapshot(
|
||||
EventId eventId,
|
||||
DateTimeOffset capturedAt,
|
||||
OddsSource source,
|
||||
IReadOnlyList<Bet> bets)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(eventId);
|
||||
ArgumentNullException.ThrowIfNull(bets);
|
||||
|
||||
if (bets.Count == 0)
|
||||
throw new ArgumentException(
|
||||
"OddsSnapshot must contain at least one Bet.", nameof(bets));
|
||||
|
||||
EventId = eventId;
|
||||
CapturedAt = capturedAt;
|
||||
Source = source;
|
||||
Bets = bets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Marathon.Domain.ValueObjects;
|
||||
|
||||
namespace Marathon.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A sport supported by the bookmaker.
|
||||
/// </summary>
|
||||
public sealed record Sport(SportCode Code, string NameRu, string NameEn)
|
||||
{
|
||||
public SportCode Code { get; } = Code ?? throw new ArgumentNullException(nameof(Code));
|
||||
|
||||
public string NameRu { get; } = string.IsNullOrWhiteSpace(NameRu)
|
||||
? throw new ArgumentException("NameRu must not be empty.", nameof(NameRu))
|
||||
: NameRu;
|
||||
|
||||
public string NameEn { get; } = string.IsNullOrWhiteSpace(NameEn)
|
||||
? throw new ArgumentException("NameEn must not be empty.", nameof(NameEn))
|
||||
: NameEn;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Marathon.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// The category of a detected anomaly.
|
||||
/// Extensible — new kinds will be added in future phases.
|
||||
/// </summary>
|
||||
public enum AnomalyKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Bookmaker suspended the market, then flipped the underdog/favourite coefficients.
|
||||
/// </summary>
|
||||
SuspensionFlip,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Marathon.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// The type of a bet.
|
||||
/// Win — match or period winner (Side1 or Side2; no draw).
|
||||
/// Draw — match or period draw (Side = Draw).
|
||||
/// WinFora — handicap / fora bet (Side1 or Side2; Value = handicap threshold).
|
||||
/// Total — total goals/points/games bet (Side = Less or More; Value = threshold).
|
||||
/// </summary>
|
||||
public enum BetType
|
||||
{
|
||||
Win,
|
||||
Draw,
|
||||
WinFora,
|
||||
Total,
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Marathon.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether an odds snapshot was captured from the pre-match or live section.
|
||||
/// </summary>
|
||||
public enum OddsSource
|
||||
{
|
||||
PreMatch,
|
||||
Live,
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Marathon.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Vocabulary-agnostic representation of a bet side.
|
||||
/// Side1/Side2 map to home/away for win-type bets.
|
||||
/// Draw applies only to win-type markets where a draw is possible.
|
||||
/// Less/More apply to total-type bets.
|
||||
/// </summary>
|
||||
public enum Side
|
||||
{
|
||||
Side1,
|
||||
Side2,
|
||||
Draw,
|
||||
Less,
|
||||
More,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Marathon.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Discriminated union representing the scope of a bet: the full match or a specific period.
|
||||
/// Use pattern matching to distinguish:
|
||||
/// switch (scope) { case MatchScope: ... case PeriodScope(var n): ... }
|
||||
/// </summary>
|
||||
public abstract record BetScope
|
||||
{
|
||||
// Private constructor prevents external derivation outside this assembly.
|
||||
private protected BetScope() { }
|
||||
}
|
||||
|
||||
/// <summary>Bet applies to the entire match.</summary>
|
||||
public sealed record MatchScope : BetScope
|
||||
{
|
||||
/// <summary>Singleton instance — MatchScope carries no additional data.</summary>
|
||||
public static readonly MatchScope Instance = new();
|
||||
}
|
||||
|
||||
/// <summary>Bet applies to a specific period (1-based). No max enforced — sport-dependent.</summary>
|
||||
public sealed record PeriodScope(int Number) : BetScope
|
||||
{
|
||||
public int Number { get; } = Number > 0
|
||||
? Number
|
||||
: throw new ArgumentOutOfRangeException(nameof(Number), Number,
|
||||
"Period number must be greater than zero.");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Marathon.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The bookmaker's stable event identifier — corresponds to <c>data-event-eventId</c>.
|
||||
/// Modelled as a string for forward compatibility with non-numeric IDs from other bookmakers.
|
||||
/// For marathonbet.by this is a numeric string in the ~26-million range (e.g., "26456117").
|
||||
/// </summary>
|
||||
public sealed record EventId
|
||||
{
|
||||
public string Value { get; }
|
||||
|
||||
public EventId(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new ArgumentException("EventId must not be empty or whitespace.", nameof(value));
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Marathon.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Decimal odds for a bet (e.g., 1.65, 2.10).
|
||||
/// Must be strictly greater than 1.0 — odds of 1.0 or less are mathematically invalid.
|
||||
/// </summary>
|
||||
public sealed record OddsRate
|
||||
{
|
||||
public decimal Value { get; }
|
||||
|
||||
public OddsRate(decimal value)
|
||||
{
|
||||
if (value <= 1.0m)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value,
|
||||
"OddsRate must be greater than 1.0. Odds of 1.0 or less are invalid.");
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString("G");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Marathon.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The threshold value for a handicap (fora) or total bet.
|
||||
/// Handicaps can be negative (e.g., -1.5 for the favourite).
|
||||
/// Totals are positive and non-zero (e.g., 3.5, 213.5).
|
||||
/// Zero is excluded as it has no meaningful betting interpretation.
|
||||
/// </summary>
|
||||
public sealed record OddsValue
|
||||
{
|
||||
public decimal Value { get; }
|
||||
|
||||
public OddsValue(decimal value)
|
||||
{
|
||||
if (value == 0m)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value,
|
||||
"OddsValue must not be zero. Use a non-zero handicap or total threshold.");
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString("G");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Marathon.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical sport identifier — corresponds to <c>data-sport-treeId</c> in marathonbet.by breadcrumbs.
|
||||
/// Known values: Basketball=6, Football=11, Tennis=22723, Hockey=43658.
|
||||
/// </summary>
|
||||
public sealed record SportCode
|
||||
{
|
||||
public int Value { get; }
|
||||
|
||||
public SportCode(int value)
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value,
|
||||
"SportCode must be greater than zero.");
|
||||
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Application x:Class="Marathon.Hosts.WpfBlazor.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Marathon.Hosts.WpfBlazor"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace Marathon.Hosts.WpfBlazor;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly:ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
@@ -0,0 +1,12 @@
|
||||
<Window x:Class="Marathon.Hosts.WpfBlazor.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Marathon.Hosts.WpfBlazor"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="450" Width="800">
|
||||
<Grid>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Marathon.Hosts.WpfBlazor;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marathon.UI\Marathon.UI.csproj" />
|
||||
<ProjectReference Include="..\Marathon.Infrastructure\Marathon.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Marathon.Application\Marathon.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marathon.Domain\Marathon.Domain.csproj" />
|
||||
<ProjectReference Include="..\Marathon.Application\Marathon.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,5 @@
|
||||
// Phase 2/3 will populate this project.
|
||||
// This file exists only to prevent the compiler from treating the project as empty.
|
||||
namespace Marathon.Infrastructure;
|
||||
|
||||
internal static class Placeholder { }
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="my-component">
|
||||
This component is defined in the <strong>Marathon.UI</strong> library.
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
.my-component {
|
||||
border: 2px dashed red;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
background-image: url('background.png');
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Marathon.UI;
|
||||
|
||||
// This class provides an example of how JavaScript functionality can be wrapped
|
||||
// in a .NET class for easy consumption. The associated JavaScript module is
|
||||
// loaded on demand when first needed.
|
||||
//
|
||||
// This class can be registered as scoped DI service and then injected into Blazor
|
||||
// components for use.
|
||||
|
||||
public class ExampleJsInterop : IAsyncDisposable
|
||||
{
|
||||
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
|
||||
|
||||
public ExampleJsInterop(IJSRuntime jsRuntime)
|
||||
{
|
||||
moduleTask = new (() => jsRuntime.InvokeAsync<IJSObjectReference>(
|
||||
"import", "./_content/Marathon.UI/exampleJsInterop.js").AsTask());
|
||||
}
|
||||
|
||||
public async ValueTask<string> Prompt(string message)
|
||||
{
|
||||
var module = await moduleTask.Value;
|
||||
return await module.InvokeAsync<string>("showPrompt", message);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (moduleTask.IsValueCreated)
|
||||
{
|
||||
var module = await moduleTask.Value;
|
||||
await module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marathon.Domain\Marathon.Domain.csproj" />
|
||||
<ProjectReference Include="..\Marathon.Application\Marathon.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 378 B |
@@ -0,0 +1,6 @@
|
||||
// This is a JavaScript module that is loaded on demand. It can export any number of
|
||||
// functions, and may import other JavaScript modules if required.
|
||||
|
||||
export function showPrompt(message) {
|
||||
return prompt(message, 'Type anything here');
|
||||
}
|
||||
Reference in New Issue
Block a user